Skip to content

Commit

Permalink
Convert to type: module
Browse files Browse the repository at this point in the history
Convert to native ECMAScript Modules, supported since Node.js 12.17.
This gains the benefits of native `import`/`export` for faster
asynchronous loading, cycle detection, and tree shaking in bundlers.
Unfortunately, it will create some pain for dependencies, since ESM can
not be imported using `require()` and must be imported using `import()`,
which operates asynchronously and returns a `Promise`.

See discussion in sindresorhus/meta#15

Signed-off-by: Kevin Locke <kevin@kevinlocke.name>
  • Loading branch information
kevinoid committed May 6, 2021
1 parent 8b8be50 commit b5865e5
Show file tree
Hide file tree
Showing 10 changed files with 31 additions and 40 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"bin/*.js"
],
"rules": {
// Executable scripts are not expected to have exports
"import/no-unused-modules": "off",

// Executable scripts should have a shebang
"node/shebang": "off"
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
- ubuntu-latest
- windows-latest
node:
- '10.17'
- '12.17'
- '*'
exclude:
# Exclude os/version already run in test-primary
Expand Down
4 changes: 1 addition & 3 deletions bin/cmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
* @license MIT
*/

'use strict';

const main = require('../cli.js');
import main from '../cli.js';

// This file was invoked directly.
// Note: Could pass process.exit as callback to force immediate exit.
Expand Down
20 changes: 8 additions & 12 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
* @module modulename/cli.js
*/

'use strict';
import { Command } from 'commander';
// TODO [engine:node@>=14]: Use readFile from 'fs/promises'
import { promises as fsPromises } from 'fs';

const { Command } = require('commander');
// https://github.com/mysticatea/eslint-plugin-node/issues/174
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const { readFile } = require('fs').promises;
const path = require('path');
import modulename from './index.js';
import { modulenameMockSymbol } from './lib/symbols.js';

const modulename = require('.');
const { modulenameMockSymbol } = require('./lib/symbols.js');
const { readFile } = fsPromises;

/** Option parser to count the number of occurrences of the option.
*
Expand Down Expand Up @@ -55,7 +53,7 @@ async function readJson(pathOrUrl, options) {
* @returns {!Promise<number>} Promise for exit code. Only rejected for
* arguments with invalid type (or args.length < 2).
*/
async function modulenameMain(args, options) {
export default async function modulenameMain(args, options) {
if (!Array.isArray(args) || args.length < 2) {
throw new TypeError('args must be an Array with at least 2 items');
}
Expand Down Expand Up @@ -105,7 +103,7 @@ async function modulenameMain(args, options) {

if (argOpts.version) {
const packageJson =
await readJson(path.join(__dirname, 'package.json'));
await readJson(new URL('package.json', import.meta.url));
options.stdout.write(`${packageJson.version}\n`);
return 0;
}
Expand All @@ -123,5 +121,3 @@ async function modulenameMain(args, options) {
return 1;
}
}

module.exports = modulenameMain;
7 changes: 2 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
* @module modulename
*/

'use strict';

module.exports =
async function func(options) {
export default async function func(options) {
if (options !== undefined && typeof options !== 'object') {
throw new TypeError('options must be an object');
}

// Do stuff
};
}
5 changes: 2 additions & 3 deletions lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
* This file is intentionally excluded from package.json#exports.
*/

'use strict';

/** Symbol of mock function used in place of modulename for testing.
*
* @private
*/
exports.modulenameMockSymbol = Symbol('modulenameMock');
// eslint-disable-next-line import/prefer-default-export
export const modulenameMockSymbol = Symbol('modulename');
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
"type": "git",
"url": "https://github.com/kevinoid/node-project-template.git"
},
"type": "commonjs",
"type": "module",
"files": [
"*.js",
"bin/",
"lib/",
"!**/.*"
],
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
Expand Down Expand Up @@ -72,7 +71,7 @@
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=10.17",
"node": ">=12.17",
"npm": ">=1.3.7"
},
"david": {
Expand Down
3 changes: 3 additions & 0 deletions test/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
// Braces around body of it() function is more consistent/readable
"arrow-body-style": "off",

// Tests are not expected to have exports
"import/no-unused-modules": "off",

// Allow null use in tests
"unicorn/no-null": "off"
}
Expand Down
18 changes: 8 additions & 10 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
* @license MIT
*/

'use strict';
import assert from 'assert';
// TODO [engine:node@>=14]: Use readFile from 'fs/promises'
import { promises as fsPromises } from 'fs';
import { PassThrough } from 'stream';

const assert = require('assert');
// https://github.com/mysticatea/eslint-plugin-node/issues/174
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const { readFile } = require('fs').promises;
const path = require('path');
const { PassThrough } = require('stream');
import main from '../cli.js';
import { modulenameMockSymbol } from '../lib/symbols.js';

const main = require('../cli.js');
const { modulenameMockSymbol } = require('../lib/symbols.js');
const { readFile } = fsPromises;

const packageJsonPromise =
readFile(path.join(__dirname, '..', 'package.json'), { encoding: 'utf8' })
readFile(new URL('../package.json', import.meta.url), { encoding: 'utf8' })
.then(JSON.parse);

const sharedArgs = ['node', 'modulename'];
Expand Down
4 changes: 1 addition & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
* @license MIT
*/

'use strict';

const modulename = require('..');
import modulename from '../index.js';

describe('modulename', () => {
it('does something', (done) => {
Expand Down

0 comments on commit b5865e5

Please sign in to comment.