ESM
Boilerplate to show how ESM are working today, both on browsers or from NodeJS. Use Rollup to bundle the modules in a compatible bundle for non ESM browser.
Install / Use
/learn @Mimetis/ESMREADME
EcmaScript Modules session
Demos on ESM Session during DEVOXX 2018
- Demo 01: Using TypeScript to understand the ESM syntax, and show the fallback to commonjs or amd.
- Demo 02: Creating an Express application and show how to deal with browsers that does not support yet ESM. This demo focuses on modules on the client side.
- Demo 03: Creating a full application with ESM on the browser.
- Demo 04: Make the compatible version for non ESM browser. Using Rollup to bundle / transpile.
- Demo 05: Show the complete solution, and migrate to .mjs nodejs.
- Demo 06: Demo on the future dynamic
import()statement.
Demo 01: Undestanding ESM
This demo will focus on:
- Writing modules in ESM style.
- Transpiling for CommonJS / AMD with TypeScript.
Open /Demo01/start folder. The project is starter initiliazed with NodeJS / TypeScript, and the node-fetch module:
If you want to create from scratch, here are the command lines:
npm init -f
tsc --init
npm install node-fetch
npm install @types/node-fetch
Create 3 files:
- app.ts
- people.ts
- speakerService.ts
Copy/Paste the people.ts file, and explain the export pattern
export class speaker {
constructor(public firstName: string, public lastName: string, public company: string) {
}
getFullName() {
return `Full name : ${this.firstName} ${this.lastName} from ${this.company} `;
}
}
export class attendee {
constructor(public firstName: string, public lastName: string) { }
}
export class user {
constructor(public firstName: string, public lastName: string) { }
}
export class dog {
constructor(public name: string) { }
}
export class cat {
constructor(public name: string) { }
}
Copy/Paste this speakerService.ts file, and complete with the solution below :
let url = "http://cfp.devoxx.fr/api/conferences/DevoxxFR2018/speakers";
async function getAllSpeakers(): Promise<Array<speaker>> {
var response:any;
var speakersJson: Array<any>;
var speakers = speakersJson.map(s => new speaker(s.firstName, s.lastName, s.company));
return speakers;
}
Solution:
import { speaker } from "./people";
import fetch from 'node-fetch';
let url = "http://cfp.devoxx.fr/api/conferences/DevoxxFR2018/speakers";
async function getAllSpeakers(): Promise<Array<speaker>> {
var response = await fetch(url);
var speakersList: Array<any> = await response.json();
var speakers = speakersList.map(s => new speaker(s.firstName, s.lastName, s.company));
return speakers;
}
export default getAllSpeakers;
and explain default pattern
import { speaker } from "./people";
import fetch from "node-fetch";
let url = "http://cfp.devoxx.fr/api/conferences/DevoxxFR2018/speakers";
export default async () => {
var response = await fetch(url);
var speakersJson: Array<any> = await response.json();
var speakers = speakersJson.map(s => new speaker(s.firstName, s.lastName, s.company));
return speakers;
};
// export default getAllSpeakers;
Copy/Paste this app.ts file, and complete with the solution below :
// (async () => {
// var speakers = await getAllSpeakers();
// speakers.forEach(speaker => {
// console.log(speaker.getFullName());
// });
// })();
Solution :
import { speaker } from "./people";
import getAllSpeakers from "./speakerServices";
(async () => {
var speakers = await getAllSpeakers();
speakers.forEach(speaker => {
console.log(speaker.getFullName());
});
})();
Move everything about speaker people.ts and speakerService.ts in a subfolder called /people.
Create a file /people/index.ts to export everything from /people
export * from './people';
export * from './speakerServices'
Explain why we can't export default in a export *
Workaround to export a default export in a export all pattern
export * from './people';
import getAllSpeakers from './speakerServices'
export { getAllSpeakers };
Go back to first version and replace in speakerServices:
export { getAllSpeakers };
Then change the import statement in app.ts:
import * as ppl from "./people/index";
(async () => {
var speakers = await ppl.getAllSpeakers()
speakers.forEach(speaker => {
console.log(speaker.getFullName());
});
})();
Demos 02: Checking the browser capabilities for ESM
This demo will focus on:
- Showing how to see if a browser is Modules compliant.
- Showing how to load a script version for non compatible browsers with
nomoduleattribute - Showing why
deferis important when working with Modules
Open Demo2/start folder or create an express application, using hbs view engine.
express -v hbs
npm install
Adding javascript files in layout.hbs, just before </body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Adding a new file javascripts/module.js (already done in starter folder)
$(() => {
$('#supportId').html('Ce navigateur supporte les modules.');
})
- Tips: Use // @ts-check. To check your code, and see how VS Code is reacting :)
// @ts-check
$(() => {
$('#supportId').html('Ce navigateur supporte les modules.');
})
Add the types definition for Jquery :
npm install @types/jquery -D
Adding javascripts/nomodule.js
// @ts-check
$(() => {
$('#supportId').html('Ce navigateur ne supporte pas les modules.');
})
Adding lines in index.hbs
<script src="javascripts/module.js" type="module"></script>
<script src="javascripts/nomodule.js" nomodule ></script>
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>
<div id="supportId"></div>
- Result in Chrome, Brave, Edge : OK
- Result in Firefox : Not OK
See the console log from Firefox:
ReferenceError: $ is not defined
- Because the script was loaded before the
jqueryscript file. - Because Modules are defered by default.
- Add the keyword
deferfor thenomodulescript
<script src="javascripts/nomodule.js" nomodule defer></script>
- Tips : If you wants Firefox to works with ESM, open a tab on
about:configand setdom.moduleScripts.enabledtotrue
Demos 03: Creating a full application with ESM for browsers
This demo will focus on:
- Showing how to works whith ES modules in a boilerplate browser application
- Showing why we should add extensions to export file
- Showing on which browsers it actually works
Get the /Sample 03/start folder. This folder contains a starter kit, composed with severals pre-coded things:
- An Express server, with
Handlebarsrendered and one route, to/speakerpage. - A full layout with bootstrap css/js.
- A directory
public/javacripts/speaker, containing some code to handle speakers. It lakes import / export !
Completing the export / import stuff
In public/javacripts/speaker/speakerPage.js, explain we have to import. But javascript requires to make some *.js references. So:
import { speakerServices } from "./speakerServices.js";
import applySpeakerTemplate from "./speakerTemplate.js";
Once explained, uncomment the export in public/javacripts/speaker/index.js:
export * from "./speakerPage.js";
export * from "./speakerServices.js";
Open view: views/speakers.hbs and add the module script:
<script type="module">
import { speakerPage } from './javascripts/speaker/index.js'
new speakerPage().loadAsync();
</script>
Demos 04: Make the compatible version for non ESM browser. Using Rollup to bundle / transpile
This demo will focus on:
- Adding a bundler / transpiler that will handle the browsers with no ESM
- Showing multiples pages export
Get the /Sample 04/start folder (actually it's the end of the Sample 03 solution.
*** Adding Rollup
Adding Rollup package
npm install rollup -D
Create a folder src/client.
Move public/javascripts/speaker to /src/client/speaker.
Add a file in src/client called index.js:
Adding a config file /rollup.config.js:
export default [
{
input: 'src/client/speaker/index.js',
output: {
file: 'public/javascripts/index.es.js',
format: 'es',
sourcemap: true
},
},
// SystemJS version, for older browsers
{
input: 'src/client/speaker/index.js',
output: {
file: 'public/javascripts/index.legacy.js',
format: 'system',
sourcemap: true
},
}
]
Using Rollup to make it work:
rollup -c -w
-
-c: Using Rollup with a config file
-
-w: Using Rollup with watch mode
-
Tips: Show the output result from Rollup bundling
Replace in views/speaker.hbs, with this code, and explains that we can't use defer without the src attribute:
<script nomodule src='https://unpkg.com/systemjs@0.21.0/dist/system-production.js'></script>
<script nomodule>
// because we can't use "defer" attribute without "src"
document.addEventListener("DOMContentLoaded", async (ev) => {
let legacy = await System.import('/javascripts/index.legacy.js');
let sp = new legacy.speakerPage();
await sp.loadAsync();
});
</script>
<script type="module">
import { speakerPage } from './javascripts/index.es.js'
new speakerPage().loadAsync();
</script>
Demos 05 : Show the complete solution, and migrate to .mjs nodejs
This demo will focus on:
- Making a node js application working with modules
- Show that we don't use a script anymore in page, but instead, a script in
layout.hbs.
