Skip to content

Commit

Permalink
feat: Native ESM Support (#68)
Browse files Browse the repository at this point in the history
* feat: import native mjs files;

- all test files must be mjs
- allows -r esm hook still, if wanted
- does not work in < Node 12

Node 8-10 throw with `import()` immediately
With `esm`, "native `require` cannot sideload `mjs` files"

* chore: add "esm.native" example

* v0.4.0-next.0

* fix(run): add `file:///` protocol for windows;

- see nodejs/node#31710
- alt?: https://nodejs.org/api/url.html#url_url_pathtofileurl_path
- thank you @TehShrike

* v0.4.0-next.1

* fix(diff): workaround for `diff` importer;

- blocked by kpdecker/jsdiff#292
- will (finally) craft own diff library if unwilling to resolve

* v0.4.0-next.2

* v0.4.0-next.3

* v0.4.0-next.4

* fix(bin): add `import()` builder;

- avoids Node 8.x and 10.x syntax error

* fix(bin): remove unused import

* chore: bump `diff` version

* fix(parse): export type interfaces

* break(parse): default -> named export

* break(run): default -> named export

* v0.5.0-next.0

* fix(uvu): don't immediately exit process;

When invoking `node` directly on a test file, allow native `stderr` output error(s) to write to console, allowing Node to explain why _it_ aborted. Important for ESM adopters/transitions.

* v0.5.0-next.1

* chore: add "esm" examples

* chore: add `esm` docs

* chore: add `readme` to "esm" examples
  • Loading branch information
lukeed authored Nov 24, 2020
1 parent 113ca82 commit 5ae6740
Show file tree
Hide file tree
Showing 27 changed files with 430 additions and 28 deletions.
24 changes: 13 additions & 11 deletions bin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#!/usr/bin/env node
const sade = require('sade');
const parse = require('./parse');
const pkg = require('./package');
const { parse } = require('./parse');

const dimport = x => new Function(`return import(${ JSON.stringify(x) })`).call(0);

const hasImport = (() => {
try { new Function('import').call(0) }
catch (err) { return !/unexpected/i.test(err.message) }
})();

sade('uvu [dir] [pattern]')
.version(pkg.version)
Expand All @@ -14,17 +21,12 @@ sade('uvu [dir] [pattern]')
try {
if (opts.color) process.env.FORCE_COLOR = '1';
let { suites } = await parse(dir, pattern, opts);
let { exec, QUEUE } = require('.');

// TODO: mjs vs js file
globalThis.UVU_DEFER = 1;
suites.forEach((x, idx) => {
globalThis.UVU_INDEX = idx;
QUEUE.push([x.name]);
require(x.file); // auto-add to queue
});

await exec(opts.bail);
if (hasImport) {
await dimport('uvu/run').then(m => m.run(suites, opts));
} else {
await require('uvu/run').run(suites, opts);
}
} catch (err) {
console.error(err.stack || err.message);
process.exit(1);
Expand Down
92 changes: 92 additions & 0 deletions docs/esm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# ES Modules

EcmaScript Modules have landed in Node.js (> 12.x)! Check out the official language documentation<sup>[[1](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules)][[2](https://nodejs.org/api/packages.html)]</sup> to learn more.

...but, here's the **TL;DR:**

* by default, only files with `.mjs` extension are treated as ESM
* by default, `.js` – and now `.cjs` – files are treated as CommonJS
* by defining `"type": "module"` in your `package.json` file, all `.js` files are treated as ESM
* when using ESM, any `import`s must reference the _full_ filepath, including its extension
* the `.cjs` extension is _always_ CommonJS, even if `"type": "module"` is defined

## Examples

Knowing the above, there are a few ways we can use/integrate ESM into our `uvu` test suites!

> **Important:** Only uvu v0.5.0+ has native ESM support
### Native ESM – via `.mjs` files

> Visit the working [`/examples/esm.mjs`](/examples/esm.mjs) demonstration~!
This example only works in Node.js v12.0 and later. In other words, it requires that _both_ your test files _and_ your source files possess the `.mjs` extension. This is – by default – the only way Node.js will load ES Modules and allow them to import/reference one another.

***PRO***

* Modern
* Native / less tooling

***CON***

* Requires Node.js 12.0 and later
* Exposes you to CommonJS <-> ESM interop issues
* Cannot test older Node.js versions – unless maintain a duplicate set of source _and_ test files


### Polyfill – via `esm` package

> Visit the working [`/examples/esm.loader`](/examples/esm.loader) demonstration~!
Thanks to [`esm`](http://npmjs.com/package/esm), this example works in **all** Node.js versions. However, for best/consistent results, you **should avoid** using `.mjs` files when using this approach. This is because `esm` has some [limitations](https://www.npmjs.com/package/esm#extensions) and chooses not to interact/tamper with files that, by definition, should only be running with the native loader anyway.

In other words, it requires that _both_ your test files _and_ your source files possess the `.mjs` extension. This is – by default – the only way Node.js will load ES Modules and allow them to import/reference one another.

***PRO***

* Makes ESM accessible to older Node.js versions
* Solves (most) CommonJS <-> ESM interop issues
* Only requires a simple `--require/-r` hook
* Quick to attach and quick to execute

***CON***

* Not native
* Not compatible with `.mjs` files


### Native ESM – via `"type": "module"`

> Visit the working [`/examples/esm.dual`](/examples/esm.dual) demonstration~!
This example combines the best of both worlds! It makes use of native ESM in Node.js versions that support it, while still making it possible to run your tests in older/legacy Node.js versions.

With `"type": "module"`, we are able to use ESM within `.js` files.
Node 12.x and later to process those files as ESM, through native behavior.
And then older Node.js versions can run/process the _same_ files by simply including the [`esm`](http://npmjs.com/package/esm) loader.

At worst – all we have is a "duplicate" test script... which is much, much better than duplicating sets of files. We end up with something like this:

```js
{
"type": "module",
// ...
"scripts": {
"test:legacy": "uvu -r esm tests",
"test:native": "uvu tests"
}
}
```

Your CI environment would execute the appropriate script according to its Node version :tada:

***PRO***

* Native when possible
* No additional maintenance
* Run tests in wider Node.js matrix
* Easy to drop legacy support at anytime

***CON***

* Defining `"type": "module"` may change how your package is consumed
12 changes: 12 additions & 0 deletions examples/esm.dual/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"type": "module",
"scripts": {
"test:legacy": "uvu -r esm tests",
"test:native": "uvu tests"
},
"devDependencies": {
"esm": "3.2.25",
"uvu": "^0.5.0"
}
}
24 changes: 24 additions & 0 deletions examples/esm.dual/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Example: esm.dual

Please read [/docs/esm](/docs/esm.md) for full details & comparisons.

## Why

Unlike [/examples/esm.loader](/examples/esm.loader), this example uses the native ESM loader whenever it's available.

Unlike [/examples/esm.mjs](/examples/esm.mjs), this example will run in all versions of Node.js – including older versions where ESM is not natively supported.


## Highlights

* Define `"type": "module"` within `package.json` <br>Allows Node.js to treat `.js` files as ESM.

* Define `import` statements with full file paths <br>Required by Node.js whenever ESM in use.

* Define two `test` scripts:
* `"test:native"` – for use within Node 12+
* `"test:legacy"` – for use with Node < 12

## License

MIT © [Luke Edwards](https://lukeed.com)
File renamed without changes.
File renamed without changes.
49 changes: 49 additions & 0 deletions examples/esm.dual/tests/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import * as math from '../src/math.js';

const sum = suite('sum');

sum('should be a function', () => {
assert.type(math.sum, 'function');
});

sum('should compute values', () => {
assert.is(math.sum(1, 2), 3);
assert.is(math.sum(-1, -2), -3);
assert.is(math.sum(-1, 1), 0);
});

sum.run();

// ---

const div = suite('div');

div('should be a function', () => {
assert.type(math.div, 'function');
});

div('should compute values', () => {
assert.is(math.div(1, 2), 0.5);
assert.is(math.div(-1, -2), 0.5);
assert.is(math.div(-1, 1), -1);
});

div.run();

// ---

const mod = suite('mod');

mod('should be a function', () => {
assert.type(math.mod, 'function');
});

mod('should compute values', () => {
assert.is(math.mod(1, 2), 1);
assert.is(math.mod(-3, -2), -1);
assert.is(math.mod(7, 4), 3);
});

mod.run();
38 changes: 38 additions & 0 deletions examples/esm.dual/tests/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import * as utils from '../src/utils.js';

const capitalize = suite('capitalize');

capitalize('should be a function', () => {
assert.type(utils.capitalize, 'function');
});

capitalize('should capitalize a word', () => {
assert.is(utils.capitalize('hello'), 'Hello');
});

capitalize('should only capitalize the 1st word', () => {
assert.is(utils.capitalize('foo bar'), 'Foo bar');
});

capitalize.run();

// ---

const dashify = suite('dashify');

dashify('should be a function', () => {
assert.type(utils.dashify, 'function');
});

dashify('should replace camelCase with dash-case', () => {
assert.is(utils.dashify('fooBar'), 'foo-bar');
assert.is(utils.dashify('FooBar'), 'foo-bar');
});

dashify('should enforce lowercase', () => {
assert.is(utils.dashify('foobar'), 'foobar');
});

dashify.run();
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
},
"devDependencies": {
"esm": "3.2.25",
"uvu": "^0.0.18"
"uvu": "^0.5.0"
}
}
21 changes: 21 additions & 0 deletions examples/esm.loader/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Example: esm.loader

Please read [/docs/esm](/docs/esm.md) for full details & comparisons.

## Why

This example makes use of the [`esm`](https://npmjs.com/package/esm) module, which rewrites all ESM syntax to CommonJS on the fly.


## Highlights

* Use ESM within regular `.js` files

* Works in all versions of Node.js <br>Because the `esm` loader is invoked – never the native behavior.

* Solves CommonJS <-> ESM interop issues <br>A significant portion of the npm ecosystem is still CommonJS-only.


## License

MIT © [Luke Edwards](https://lukeed.com)
3 changes: 3 additions & 0 deletions examples/esm.loader/src/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const sum = (a, b) => a + b;
export const div = (a, b) => a / b;
export const mod = (a, b) => a % b;
7 changes: 7 additions & 0 deletions examples/esm.loader/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function capitalize(str) {
return str[0].toUpperCase() + str.substring(1);
}

export function dashify(str) {
return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase();
}
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions examples/esm.mjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"test": "uvu tests"
},
"devDependencies": {
"uvu": "^0.5.0"
}
}
19 changes: 19 additions & 0 deletions examples/esm.mjs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Example: esm.mjs

Please read [/docs/esm](/docs/esm.md) for full details & comparisons.

## Why

This example makes use of the native ESM loader – available in version Node 12 and later!


## Highlights

* Uses native ESM via the `.mjs` file extension <br>This is the Node's default behavior.

* Define `import` statements with full file paths <br>Required by Node.js whenever ESM in use.


## License

MIT © [Luke Edwards](https://lukeed.com)
3 changes: 3 additions & 0 deletions examples/esm.mjs/src/math.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const sum = (a, b) => a + b;
export const div = (a, b) => a / b;
export const mod = (a, b) => a % b;
7 changes: 7 additions & 0 deletions examples/esm.mjs/src/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function capitalize(str) {
return str[0].toUpperCase() + str.substring(1);
}

export function dashify(str) {
return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase();
}
49 changes: 49 additions & 0 deletions examples/esm.mjs/tests/math.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import * as math from '../src/math.mjs';

const sum = suite('sum');

sum('should be a function', () => {
assert.type(math.sum, 'function');
});

sum('should compute values', () => {
assert.is(math.sum(1, 2), 3);
assert.is(math.sum(-1, -2), -3);
assert.is(math.sum(-1, 1), 0);
});

sum.run();

// ---

const div = suite('div');

div('should be a function', () => {
assert.type(math.div, 'function');
});

div('should compute values', () => {
assert.is(math.div(1, 2), 0.5);
assert.is(math.div(-1, -2), 0.5);
assert.is(math.div(-1, 1), -1);
});

div.run();

// ---

const mod = suite('mod');

mod('should be a function', () => {
assert.type(math.mod, 'function');
});

mod('should compute values', () => {
assert.is(math.mod(1, 2), 1);
assert.is(math.mod(-3, -2), -1);
assert.is(math.mod(7, 4), 3);
});

mod.run();
Loading

0 comments on commit 5ae6740

Please sign in to comment.