Skip to content

Commit

Permalink
feat: native Node.js ES Modules (wrapper approach) (#423)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Native ES Modules is still an experimental API in
Node.js 14.0.0 and has so far not officially been supported by the
`uuid` module.

Since Node.js allows importing CommonJS modules it was possible to
import the `uuid` module like this:

```js
import uuid from 'uuid';
console.log(uuid.v4()); // -> 'cd6c3b08-0adc-4f4b-a6ef-36087a1c9869'
```

This will no longer work with proper ES Module exports in place. You
can now import the `uuid` library as described in the documentation:

```js
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
```

or

```js
import * as uuid from 'uuid';
console.log(uuid.v4()); // -> 'cd6c3b08-0adc-4f4b-a6ef-36087a1c9869'
```

Enabling native ES Modules for Node.js requires some special care for
the v1 algorithm which needs internal state. This makes this library
susceptible to the dual package hazard described in
https://nodejs.org/docs/latest-v14.x/api/esm.html#esm_dual_commonjs_es_module_packages

While the "isolated state" solution seems to make more sense it causes
trouble with rollup which supports CommonJS files only with an
additional plugin, see rollup/rollup#3514.

It is worth noting that webpack could deal with the "isolated state"
solution since webpack supports CommonJS sources out of the box without
further plugins and also doesn't get confused by `.cjs` file extensions
that would have to be used in the state isolation approach for
compatibility with Node.js.

The wrapper approach should however work fine. Here's what code will be
used in each case:

1. Node.js `require('uuid')`
  -> dist/index.js (CommonJS) -> dist/v1.js (CommonJS)
2. Node.js `import { v1 as uuidv1 } from 'uuid'`
  -> wrapper.mjs (ESM) -> dist/v1.js (CommonJS)
3. rollup/webpack (targeting Node.js environments)
  -> dist/esm-node/index.js (ESM) -> dist/esm-node/v1.js (ESM)
4. rollup/webpack (targeting Browser environments)
  -> dist/esm-browser/index.js (ESM) -> dist/esm-browser/v1.js (ESM)

Fixes #245
Fixes #419
Fixes #342
  • Loading branch information
ctavan committed Apr 29, 2020
1 parent aa4b7f2 commit 2d9f590
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -38,3 +38,5 @@ jobs:
if: matrix.node-version == '12.x'
env:
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
- run: npm run test:node
if: matrix.node-version == '12.x' || matrix.node-version == '14.x'
1 change: 1 addition & 0 deletions .local/wrapper.mjs
14 changes: 6 additions & 8 deletions README.md
Expand Up @@ -297,20 +297,18 @@ defined by RFC4122

## ECMAScript Modules

For usage in the browser `uuid` provides support for [ECMAScript
Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) that enable
tree-shaking for bundlers, like [rollup.js](https://rollupjs.org/guide/en/#tree-shaking)
([example](./examples/browser-rollup/)) and [webpack](https://webpack.js.org/guides/tree-shaking/)
([example](./examples/browser-webpack/)).
This library comes with [ECMAScript
Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) support for Node.js
versions that support it ([example](./examples/node-esmodules/)) as well as bundlers like
[rollup.js](https://rollupjs.org/guide/en/#tree-shaking) ([example](./examples/browser-rollup/))
and [webpack](https://webpack.js.org/guides/tree-shaking/)
([example](./examples/browser-webpack/)) (targeting both, Node.js and browser environments).

```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
```

There is experimental native ESM support for [the browser](./examples/browser-esmodules/) but it
should not be considered ready for production use and may change or disappear in future releases.

To run the examples you must first create a dist build of this library in the module root:

```
Expand Down
14 changes: 6 additions & 8 deletions README_js.md
Expand Up @@ -287,20 +287,18 @@ defined by RFC4122

## ECMAScript Modules

For usage in the browser `uuid` provides support for [ECMAScript
Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) that enable
tree-shaking for bundlers, like [rollup.js](https://rollupjs.org/guide/en/#tree-shaking)
([example](./examples/browser-rollup/)) and [webpack](https://webpack.js.org/guides/tree-shaking/)
([example](./examples/browser-webpack/)).
This library comes with [ECMAScript
Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) support for Node.js
versions that support it ([example](./examples/node-esmodules/)) as well as bundlers like
[rollup.js](https://rollupjs.org/guide/en/#tree-shaking) ([example](./examples/browser-rollup/))
and [webpack](https://webpack.js.org/guides/tree-shaking/)
([example](./examples/browser-webpack/)) (targeting both, Node.js and browser environments).

```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
```

There is experimental native ESM support for [the browser](./examples/browser-esmodules/) but it
should not be considered ready for production use and may change or disappear in future releases.

To run the examples you must first create a dist build of this library in the module root:

```
Expand Down
6 changes: 6 additions & 0 deletions examples/node-esmodules/README.md
@@ -0,0 +1,6 @@
# uuid example Node.js ESModules

```
npm install
npm test
```
43 changes: 43 additions & 0 deletions examples/node-esmodules/example.mjs
@@ -0,0 +1,43 @@
import { v1 as uuidv1, v4 as uuidv4, v3 as uuidv3, v5 as uuidv5 } from 'uuid';
import * as uuid from 'uuid';

console.log('uuidv1()', uuidv1());

console.log('uuidv4()', uuidv4());

// ... using predefined DNS namespace (for domain names)
console.log('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS));

// ... using predefined URL namespace (for, well, URLs)
console.log('uuidv3() URL', uuidv3('http://example.com/hello', uuidv3.URL));

// ... using a custom namespace
//
// Note: Custom namespaces should be a UUID string specific to your application!
// E.g. the one here was generated using this modules `uuid` CLI.
const MY_NAMESPACE = '55238d15-c926-4598-b49d-cf4e913ba13c';
console.log('uuidv3() MY_NAMESPACE', uuidv3('Hello, World!', MY_NAMESPACE));

// ... using predefined DNS namespace (for domain names)
console.log('uuidv5() DNS', uuidv5('hello.example.com', uuidv5.DNS));

// ... using predefined URL namespace (for, well, URLs)
console.log('uuidv5() URL', uuidv5('http://example.com/hello', uuidv5.URL));

// ... using a custom namespace
//
// Note: Custom namespaces should be a UUID string specific to your application!
// E.g. the one here was generated using this modules `uuid` CLI.
// const MY_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341';
console.log('uuidv5() MY_NAMESPACE', uuidv5('Hello, World!', MY_NAMESPACE));

console.log('Same with default export');

console.log('uuid.v1()', uuid.v1());
console.log('uuid.v4()', uuid.v4());
console.log('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS));
console.log('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL));
console.log('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE));
console.log('uuid.v5() DNS', uuid.v5('hello.example.com', uuid.v5.DNS));
console.log('uuid.v5() URL', uuid.v5('http://example.com/hello', uuid.v5.URL));
console.log('uuid.v5() MY_NAMESPACE', uuid.v5('Hello, World!', MY_NAMESPACE));
11 changes: 11 additions & 0 deletions examples/node-esmodules/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions examples/node-esmodules/package.json
@@ -0,0 +1,11 @@
{
"name": "uuid-example-node-esmodules",
"version": "0.0.0",
"private": true,
"scripts": {
"test": "node --experimental-modules example.mjs"
},
"dependencies": {
"uuid": "file:../../.local"
}
}
20 changes: 14 additions & 6 deletions package.json
Expand Up @@ -14,10 +14,14 @@
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "./dist/bin/uuid"
},
"sideEffects": false,
"main": "./dist/index.js",
"exports": {
"require": "./dist/index.js",
"import": "./wrapper.mjs"
},
"module": "./dist/esm-node/index.js",
"browser": {
"./dist/md5.js": "./dist/md5-browser.js",
Expand All @@ -35,7 +39,8 @@
"v1.js",
"v3.js",
"v4.js",
"v5.js"
"v5.js",
"wrapper.mjs"
],
"devDependencies": {
"@babel/cli": "7.8.4",
Expand Down Expand Up @@ -67,16 +72,19 @@
"standard-version": "7.1.0"
},
"scripts": {
"examples:browser-webpack:build": "cd examples/browser-webpack && npm install && npm run build",
"examples:browser-rollup:build": "cd examples/browser-rollup && npm install && npm run build",
"examples:browser-esmodules:build": "cd examples/browser-esmodules && npm install && npm run build",
"examples:browser:webpack:build": "cd examples/browser-webpack && npm install && npm run build",
"examples:browser:rollup:build": "cd examples/browser-rollup && npm install && npm run build",
"examples:node:commonjs:test": "cd examples/node-commonjs && npm install && npm test",
"examples:node:esmodules:test": "cd examples/node-esmodules && npm install && npm test",
"lint": "npm run eslint:check && npm run prettier:check",
"eslint:check": "eslint src/ test/ examples/ *.js",
"eslint:fix": "eslint --fix src/ test/ examples/ *.js",
"pretest": "[ -n $CI ] || npm run build",
"test": "BABEL_ENV=commonjs node --throw-deprecation node_modules/.bin/jest test/unit/",
"pretest:browser": "npm run build && npm-run-all --parallel examples:**",
"pretest:browser": "npm run build && npm-run-all --parallel examples:browser:**",
"test:browser": "wdio run ./wdio.conf.js",
"pretest:node": "npm run build",
"test:node": "npm-run-all --parallel examples:node:**",
"prettier:check": "prettier --ignore-path .prettierignore --check '**/*.{js,jsx,json,md}'",
"prettier:fix": "prettier --ignore-path .prettierignore --write '**/*.{js,jsx,json,md}'",
"bundlewatch": "npm run pretest:browser && bundlewatch --config bundlewatch.config.json",
Expand Down
5 changes: 5 additions & 0 deletions wrapper.mjs
@@ -0,0 +1,5 @@
import uuid from './dist/index.js';
export const v1 = uuid.v1;
export const v3 = uuid.v3;
export const v4 = uuid.v4;
export const v5 = uuid.v5;

0 comments on commit 2d9f590

Please sign in to comment.