From 2d9f590ad9701d692625c07ed62f0a0f91227991 Mon Sep 17 00:00:00 2001 From: Christoph Tavan Date: Wed, 29 Apr 2020 13:40:21 +0200 Subject: [PATCH] feat: native Node.js ES Modules (wrapper approach) (#423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 https://github.com/rollup/rollup/issues/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 --- .github/workflows/ci.yml | 2 ++ .local/wrapper.mjs | 1 + README.md | 14 ++++---- README_js.md | 14 ++++---- examples/node-esmodules/README.md | 6 ++++ examples/node-esmodules/example.mjs | 43 +++++++++++++++++++++++ examples/node-esmodules/package-lock.json | 11 ++++++ examples/node-esmodules/package.json | 11 ++++++ package.json | 20 +++++++---- wrapper.mjs | 5 +++ 10 files changed, 105 insertions(+), 22 deletions(-) create mode 120000 .local/wrapper.mjs create mode 100644 examples/node-esmodules/README.md create mode 100644 examples/node-esmodules/example.mjs create mode 100644 examples/node-esmodules/package-lock.json create mode 100644 examples/node-esmodules/package.json create mode 100644 wrapper.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47a71bd2..342840f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/.local/wrapper.mjs b/.local/wrapper.mjs new file mode 120000 index 00000000..4d9f862a --- /dev/null +++ b/.local/wrapper.mjs @@ -0,0 +1 @@ +../wrapper.mjs \ No newline at end of file diff --git a/README.md b/README.md index 33f7d5c6..7f6958b4 100644 --- a/README.md +++ b/README.md @@ -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: ``` diff --git a/README_js.md b/README_js.md index aa3cae28..9ad030a7 100644 --- a/README_js.md +++ b/README_js.md @@ -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: ``` diff --git a/examples/node-esmodules/README.md b/examples/node-esmodules/README.md new file mode 100644 index 00000000..f1a1e233 --- /dev/null +++ b/examples/node-esmodules/README.md @@ -0,0 +1,6 @@ +# uuid example Node.js ESModules + +``` +npm install +npm test +``` diff --git a/examples/node-esmodules/example.mjs b/examples/node-esmodules/example.mjs new file mode 100644 index 00000000..d63b6974 --- /dev/null +++ b/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)); diff --git a/examples/node-esmodules/package-lock.json b/examples/node-esmodules/package-lock.json new file mode 100644 index 00000000..407244eb --- /dev/null +++ b/examples/node-esmodules/package-lock.json @@ -0,0 +1,11 @@ +{ + "name": "uuid-example-node-esmodules", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "uuid": { + "version": "file:../../.local" + } + } +} diff --git a/examples/node-esmodules/package.json b/examples/node-esmodules/package.json new file mode 100644 index 00000000..8afddfed --- /dev/null +++ b/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" + } +} diff --git a/package.json b/package.json index 2cd78a15..192a77bd 100644 --- a/package.json +++ b/package.json @@ -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", @@ -35,7 +39,8 @@ "v1.js", "v3.js", "v4.js", - "v5.js" + "v5.js", + "wrapper.mjs" ], "devDependencies": { "@babel/cli": "7.8.4", @@ -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", diff --git a/wrapper.mjs b/wrapper.mjs new file mode 100644 index 00000000..87e7f2e7 --- /dev/null +++ b/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;