From 1f308142f2acd5e436646980d8b09a250fdb6129 Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 21 Sep 2021 21:35:47 +0200
Subject: [PATCH 01/11] update design docs to support multiple concurrent
proposals
---
doc/{design.md => design/overview.md} | 0
doc/design/proposal-chaining-iterative.md | 0
doc/design/proposal-chaining-recrusive.md | 0
doc/design/proposal-fs-hooks.md | 0
4 files changed, 0 insertions(+), 0 deletions(-)
rename doc/{design.md => design/overview.md} (100%)
create mode 100644 doc/design/proposal-chaining-iterative.md
create mode 100644 doc/design/proposal-chaining-recrusive.md
create mode 100644 doc/design/proposal-fs-hooks.md
diff --git a/doc/design.md b/doc/design/overview.md
similarity index 100%
rename from doc/design.md
rename to doc/design/overview.md
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
new file mode 100644
index 0000000..e69de29
diff --git a/doc/design/proposal-chaining-recrusive.md b/doc/design/proposal-chaining-recrusive.md
new file mode 100644
index 0000000..e69de29
diff --git a/doc/design/proposal-fs-hooks.md b/doc/design/proposal-fs-hooks.md
new file mode 100644
index 0000000..e69de29
From 4246971a6b644e56b31da277b3e60c4d55b66dee Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Sun, 26 Sep 2021 23:15:28 +0200
Subject: [PATCH 02/11] flesh out design overview, add content to chaining
proposals
WIP: mock-loader
---
README.md | 2 +-
doc/design/overview.md | 149 +++---------
doc/design/proposal-chaining-iterative.md | 272 ++++++++++++++++++++++
doc/design/proposal-chaining-recrusive.md | 0
doc/design/proposal-chaining-recursive.md | 244 +++++++++++++++++++
doc/design/proposal-fs-hooks.md | 0
6 files changed, 546 insertions(+), 121 deletions(-)
delete mode 100644 doc/design/proposal-chaining-recrusive.md
create mode 100644 doc/design/proposal-chaining-recursive.md
delete mode 100644 doc/design/proposal-fs-hooks.md
diff --git a/README.md b/README.md
index ca819b0..0703f04 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ This team is spun off from the [Modules team](https://github.com/nodejs/modules)
- [Use cases](./doc/use-cases.md)
-- [Design](./doc/design.md)
+- [Design](./doc/design/overview.md)
- [Project board](https://github.com/nodejs/node/projects/17)
diff --git a/doc/design/overview.md b/doc/design/overview.md
index 57f119c..62eef0b 100644
--- a/doc/design/overview.md
+++ b/doc/design/overview.md
@@ -1,140 +1,49 @@
# Loaders Design
-## Hooks
+There are currently [three loader hooks](https://github.com/nodejs/node/tree/master/doc/api/esm.html#esm_hooks):
-There are currently [three loader hooks](https://nodejs.org/api/esm.html#esm_hooks):
+1. `resolve`: Takes a specifier (the string after `from` in an `import` statement) and converts it into an URL to be loaded.
-- `resolve`: Take a specifier (the string after `from` in an `import` statement) and convert it into an URL to be loaded.
+1. `load`: Takes the resolved URL and returns runnable code (JavaScript, Wasm, etc.) as well as the name of one of Node’s ESM loader’s [“translators”](https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/translators.js):
+ * `commonjs`
+ * `module`
+ * `builtin` (a Node internal module, like `fs`)
+ * `json` (with `--experimental-json-modules`)
+ * `wasm` (with `--experimental-wasm-modules`)
-- `load`: Take the resolved URL and return runnable code (JavaScript, Wasm, etc.) as well as the name of one of Node’s ESM loader’s [“translators”](https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/translators.js): `commonjs`, `module`, `builtin` (a Node internal module like `fs`), `json` (with `--experimental-json-modules`) or `wasm` (with `--experimental-wasm-modules`).
+* `globalPreload`: Defines a string of JavaScript to be injected into the application global scope.
-- `globalPreload`: Define a string of JavaScript to be injected into the application global scope.
+## History
-## Chaining `resolve` hooks
+### Hook consolidation
-Say you had a chain of three loaders, `unpkg`, `http-to-https`, `cache-buster`:
+The initial experimental implementation consisted of 5 hooks:
-1. The `unpkg` loader resolves a specifier `foo` to an URL `http://unpkg.com/foo`.
+* `resolve`
+* `getFormat`
+* `getSource`
+* `transformSource`
+* `getGlobalPreloadCode`
-2. The `http-to-https` loader rewrites that URL to `https://unpkg.com/foo`.
+These were consolidated (in nodejs/node#37468) to avoid counter-intuitive and paradoxical behaviour when used in multiple custom loaders.
-3. The `cache-buster` that takes the URL and adds a timestamp to the end, so like `https://unpkg.com/foo?ts=1234567890`.
+## Chaining
-In the new loaders design, these three loaders could be implemented as follows:
+Custom loaders are intended to chain to support various concerns beyond the scope of core, such as build tooling, mocking, transpilation, etc.
-### `unpkg` loader
-
-```js
-export async function resolve(specifier, context, next) { // next is Node’s resolve
- if (isBareSpecifier(specifier)) {
- return `http://unpkg.com/${specifier}`;
- }
- return next(specifier, context);
-}
-```
+### `globalPreload`
-### `http-to-https` loader
-
-```js
-export async function resolve(specifier, context, next) { // next is the unpkg loader’s resolve
- const result = await next(specifier, context);
- if (result.url.startsWith('http://')) {
- result.url = `https${result.url.slice('http'.length)}`;
- }
- return result;
-}
-```
-
-### `cache-buster` loader
-
-```js
-export async function resolve(specifier, context, next) { // next is the http-to-https loader’s resolve
- const result = await next(specifier, context);
- if (supportsQueryString(result.url)) { // exclude data: & friends
- // TODO: do this properly in case the URL already has a query string
- result.url += `?ts=${Date.now()}`;
- }
- return result;
-}
-```
-
-The hook functions nest: each one always just returns a string, like Node’s `resolve`, and the chaining happens as a result of calling `next`; and if a hook doesn’t call `next`, the chain short-circuits. The API would be `node --loader unpkg --loader http-to-https --loader cache-buster`, following the pattern set by `--require`.
-
-## Chaining `load` hooks
-
-Chaining `load` hooks would be similar to chaining `resolve` hooks, though slightly more complicated in that instead of returning a single string, each `load` hook returns an object `{ format, source }` where `source` is the loaded module’s source code/contents and `format` is the name of one of Node’s ESM loader’s [“translators”](https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/translators.js): `commonjs`, `module`, `builtin` (a Node internal module like `fs`), `json` (with `--experimental-json-modules`) or `wasm` (with `--experimental-wasm-modules`).
-
-Currently, Node’s internal ESM loader throws an error on unknown file types: `import('file.javascript')` throws, even if the contents of `file.javascript` are perfectly acceptable JavaScript. This error happens during Node’s internal `resolve` when it encounters a file extension it doesn’t recognize; hence the current [CoffeeScript loader example](https://nodejs.org/api/esm.html#esm_transpiler_loader) has lots of code to tell Node to allow CoffeeScript file extensions. We should move this validation check to be after the format is determined, which is one of the return values of `load`; so basically, it’s the responsibility of `load` to return a `format` that Node recognizes. Node’s internal `load` doesn’t know to resolve a URL ending in `.coffee` to `module`, so Node would continue to error like it does now; but the CoffeeScript loader under this new design no longer needs to hook into `resolve` at all, since it can determine the format of CoffeeScript files within `load`. In code:
-
-### `coffeescript` loader
-
-```js
-import CoffeeScript from 'coffeescript';
-
-// CoffeeScript files end in .coffee, .litcoffee or .coffee.md
-const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
-
-export async function load(url, context, next) {
- const result = await next(url, context);
+For now, we think that this wouldn’t be chained the way `resolve` and `load` would be. This hook would just be called sequentially for each registered loader, in the same order as the loaders themselves are registered. If this is insufficient, for example for instrumentation use cases, we can discuss and potentially change this to follow the chaining style of `load`.
- // The first check is technically not needed but ensures that
- // we don’t try to compile things that already _are_ compiled.
- if (result.format === undefined && extensionsRegex.test(url)) {
- // For simplicity, all CoffeeScript URLs are ES modules.
- const format = 'module';
- const source = CoffeeScript.compile(result.source, { bare: true });
- return {format, source};
- }
- return result;
-}
-```
+### `resolve`
-And the other example loader in the docs, to allow `import` of `https://` URLs, would similarly only need a `load` hook:
+### `load`
-### `https` loader
+Chaining `load` hooks would be similar to chaining `resolve` hooks, where `source` is the loaded module’s source code/contents and `format` is the name of one of Node’s ESM loader’s [“translators”](https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/translators.js): `builtin` (a Node internal module like `fs`), `commonjs`, `json` (with `--experimental-json-modules`), `module`, or `wasm` (with `--experimental-wasm-modules`).
-```js
-import { get } from 'https';
+Currently, Node’s internal ESM loader throws an error on unknown file types: `import('file.javascript')` throws, even if the contents of `file.javascript` are perfectly acceptable JavaScript. This error happens during Node’s internal `resolve` when it encounters a file extension it doesn’t recognize; hence the current [CoffeeScript loader example](https://nodejs.org/api/esm.html#esm_transpiler_loader) has lots of code to tell Node to allow CoffeeScript file extensions. We should move this validation check to be after the format is determined, which is one of the return values of `load`; so basically, it’s the responsibility of `load` to return a `format` that Node recognizes. Node’s internal `load` doesn’t know to resolve a URL ending in `.coffee` to `module`, so Node would continue to error like it does now; but the CoffeeScript loader under this new design no longer needs to hook into `resolve` at all, since it can determine the format of CoffeeScript files within `load`.
-export async function load(url, context, next) {
- if (url.startsWith('https://')) {
- let format; // default: format is undefined
- const source = await new Promise((resolve, reject) => {
- get(url, (res) => {
- // Determine the format from the MIME type of the response
- switch (res.headers['content-type']) {
- case 'application/javascript':
- case 'text/javascript': // etc.
- format = 'module';
- break;
- case 'application/node':
- case 'application/vnd.node.node':
- format = 'commonjs';
- break;
- case 'application/json':
- format = 'json';
- break;
- // etc.
- }
-
- let data = '';
- res.on('data', (chunk) => data += chunk);
- res.on('end', () => resolve({ source: data }));
- }).on('error', (err) => reject(err));
- });
- return {format, source};
- }
-
- return next(url, context);
-}
-```
-
-If these two loaders are used together, where the `coffeescript` loader’s `next` is the `https` loader’s hook and `https` loader’s `next` is Node’s native hook, then for a URL like `https://example.com/module.coffee`:
-
-1. The `https` loader would load the source over the network, but return `format: undefined`, assuming the server supplied a correct `Content-Type` header like `application/vnd.coffeescript` which our `https` loader doesn’t recognize.
-
-2. The `coffeescript` loader would get that `{ source, format: undefined }` early on from its call to `next`, and set `format: 'module'` based on the `.coffee` at the end of the URL. It would also transpile the source into JavaScript. It then returns `{ format: 'module', source }` where `source` is runnable JavaScript rather than the original CoffeeScript.
-
-## Chaining `globalPreload` hooks
+### Proposals
-For now, we think that this wouldn’t be chained the way `resolve` and `load` would be. This hook would just be called sequentially for each registered loader, in the same order as the loaders themselves are registered. If this is insufficient, for example for instrumentation use cases, we can discuss and potentially change this to follow the chaining style of `load`.
+* [Recursive chaining](./proposal-chaining-recursive.md)
+* [Iterative chaining](./proposal-chaining-iterative.md)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index e69de29..2ecdac7 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -0,0 +1,272 @@
+# Proposal 2: Iterative
+
+## Iterative `resolve`
+
+Say you had a chain of three loaders:
+
+* `unpkg-resolver` converts a bare specifier like `foo` to a url `http://unpkg.com/foo`.
+1. `http-to-https-resolver` rewrites insecure http urls to the https, like `https://unpkg.com/foo`.
+1. `cache-buster-resolver` adds a timestamp to a url end, like `https://unpkg.com/foo?t=1234567890`.
+
+```console
+node \
+--loader unpkg-resolver \
+--loader https-resolver \
+--loader cache-buster-resolver
+```
+
+These would be called in the following sequence:
+
+`unpkg-resolver` → `https-resolver` → `cache-buster-resolver`
+
+Resolve hooks have the following signature:
+
+```js
+export async function resolve(
+ interimResult: { // The result from the previous hook
+ format = '', // if resolve settled with a `format`, this is that value
+ // until a load hook provides a different value
+ url = '', // the most recently provided value from a previous hook
+ },
+ context: {
+ conditions, // export conditions (from the relevant package.json)
+ parentUrl, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ originalSpecifier, // The original value of the import specifier
+ },
+ defaultResolve, // node's default resolve hook
+ shortCircuit, // special function for terminating the chain early
+): { format: string, url: string } {
+```
+
+### `unpkg` resolver
+
+
+`unpkg-resolver.mjs`
+
+```js
+export async function resolve(
+ interimResult,
+ { originalSpecifier },
+) {
+ if (isBareSpecifier(originalSpecifier)) return `http://unpkg.com/${originalSpecifier}`;
+}
+```
+
+
+### `https` resolver
+
+
+`https-resolver.mjs`
+
+```js
+export async function resolve(
+ interimResult,
+ context,
+) {
+ const url = new URL(interimResult.url); // this can throw, so handle appropriately
+
+ if (url.protocol = 'http:') url.protocol = 'https:';
+
+ return { url: url.toString() };
+}
+```
+
+
+### `cache-buster` resolver
+
+
+`cachebuster-resolver.mjs`
+
+```js
+export async function resolve(
+ interimResult,
+) {
+ const url = new URL(interimResult.url); // this can throw, so handle appropriately
+
+ if (supportsQueryString(url.protocol)) { // exclude data: & friends
+ url.searchParams.set('t', Date.now());
+ }
+
+ return { url: url.toString() };
+}
+```
+
+
+
+## Iterative `load`
+
+Say you had a chain of three loaders:
+
+* `babel-loader` backwards time-machine
+* `coffeescript-loader` transforms coffeescript to vanilla javascript
+* `https-loader` loads source from remote
+
+```console
+node \
+--loader https-loader \
+--loader babel-loader \
+--loader coffeescript-loader \
+```
+
+These would be called in the following sequence:
+
+(`https-loader` OR `defaultLoad`) → `coffeescript-loader` → `babel-loader`
+
+1. `defaultLoad` / `https-loader` needs to be first to actually get the source, which is fed to the subsequent loader
+1. `coffeescript-loader` receives the raw source from the previous loader and transpiles coffeescript files to regular javascript
+1. `babel-loader` receives potentially bleeding-edge JavaScript and transforms it to some ancient JavaScript target
+
+The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
+
+Load hooks have the following signature:
+
+```js
+export async function load(
+ interimResult: { // The result from the previous hook
+ format = '', // if resolve settled with a `format`, this is that value
+ // until a load hook provides a different value
+ source = '', //
+ },
+ context: {
+ conditions, // export conditions (from the relevant package.json)
+ parentUrl, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl, // the url to which the resolve hook chain settled
+ },
+ defaultLoad, // node's default load hook
+ shortCircuit, // special function for terminating the chain early
+): { format: string, source: string | ArrayBuffer | TypedArray } {
+```
+
+### `https` loader
+
+
+`https-loader.mjs`
+
+```js
+export async function load(
+ interimResult,
+ { resolvedUrl },
+) {
+ if (interimResult.source) return; // step aside (content already retrieved)
+
+ if (!resolvedUrl.startsWith('https://')) return; // step aside
+
+ return new Promise(function loadHttpsSource(resolve, reject) {
+ get(resolvedUrl, function getHttpsSource(rsp) {
+ const format = mimeTypeToFormat.get(rsp.headers['content-type']);
+ let source = '';
+
+ rsp.on('data', (chunk) => source += chunk);
+ rsp.on('end', () => resolve({ format, source }));
+ rsp.on('error', reject);
+ });
+ });
+}
+
+const mimeTypeToFormat = new Map([
+ ['application/node', 'commonjs'],
+ ['application/javascript', 'module'],
+ ['text/javascript', 'module'],
+ ['application/json', 'json'],
+ ['text/coffeescript', 'coffeescript'],
+ // …
+]);
+```
+
+
+### `coffeescript` loader
+
+
+`coffeescript-loader.mjs`
+
+```js
+export async function resolve(/* … */) {/* … */ }
+
+export async function load(
+ interimResult, // possibly output of https-loader
+ context,
+ defaulLoad,
+) {
+ const { resolvedUrl } = context;
+ if (!coffeescriptExtensionsRgx.test(resolvedUrl)) return; // step aside
+
+ const format = interimResult.format || await getPackageType(resolvedUrl);
+ if (format === 'commonjs') return { format };
+
+ const rawSource = (
+ interimResult.source
+ || await defaulLoad(resolvedUrl, { ...context, format }).source
+ )
+ const transformedSource = CoffeeScript.compile(rawSource.toString(), {
+ bare: true,
+ filename: resolvedUrl,
+ });
+
+ return {
+ format,
+ source: transformedSource,
+ };
+}
+
+function getPackageType(url) {/* … */ }
+const coffeescriptExtensionsRgs = /* … */
+```
+
+
+### `babel` loader
+
+
+`babel-loader.mjs`
+
+```js
+export async function resolve(/* … */) {/* … */ }
+
+export async function load(
+ interimResult, // possibly output of coffeescript-loader
+ context,
+ defaulLoad,
+) {
+ const { resolvedUrl } = context;
+ const babelConfig = await getBabelConfig(resolvedUrl);
+
+ const format = (
+ interimResult.format
+ || babelOutputToFormat.get(babelConfig.output.format)
+ );
+
+ if (format === 'commonjs') return { format };
+
+ const sourceToTranspile = (
+ interimResult.source
+ || await defaulLoad(resolvedUrl, { ...context, format }).source
+ );
+ const transformedSource = Babel.transformSync(
+ sourceToTranspile.toString(),
+ babelConfig,
+ ).code;
+
+ return {
+ format,
+ source: transformedSource,
+ };
+}
+
+function getBabelConfig(url) {/* … */ }
+const babelOutputToFormat = new Map([
+ ['cjs', 'commonjs'],
+ ['esm', 'module'],
+ // …
+]);
+```
+
+
+## Mock Loader
+
+
+`mock-loader.mjs`
+
+```js
+```
+
\ No newline at end of file
diff --git a/doc/design/proposal-chaining-recrusive.md b/doc/design/proposal-chaining-recrusive.md
deleted file mode 100644
index e69de29..0000000
diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md
new file mode 100644
index 0000000..d44768b
--- /dev/null
+++ b/doc/design/proposal-chaining-recursive.md
@@ -0,0 +1,244 @@
+# Proposal: Recursive
+
+## Recursive `resolve`
+
+Say you had a chain of three loaders:
+
+1. The `unpkg` loader resolves a specifier `foo` to an URL `http://unpkg.com/foo`.
+
+2. The `http-to-https` loader rewrites that URL to `https://unpkg.com/foo`.
+
+3. The `cache-buster` that takes the URL and adds a timestamp to the end, so like `https://unpkg.com/foo?ts=1234567890`.
+
+The hook functions nest: each one always just returns `{ [format: string,] url: string }`, and the chaining happens as a result of calling `next()`. The chain short-circuits if a hook doesn’t call `next()`. Following the pattern of `--require`:
+
+```console
+node \
+--loader unpkg-resolver \
+--loader https-resolver \
+--loader cache-buster-resolver
+```
+
+These would be called in the following sequence (babel-loader is called first):
+
+`cache-buster-resolver` ← `https-resolver` ← `unpkg-resolver`
+
+1. `cache-buster-resolver` needs the output of `https-resolver` to append the query param
+1. `https-resolver` needs output of unpkg to convert it to https
+1. `unpkg-resolver` returns the remote url
+
+### `cache-buster` resolver
+
+
+`cachebuster-resolver.mjs`
+
+```js
+export async function resolve(
+ specifier,
+ context,
+ next, // https-resolver
+) {
+ const result = await next(specifier, context);
+
+ const url = new URL(result.url); // this can throw, so handle appropriately
+
+ if (supportsQueryString(url.protocol)) { // exclude data: & friends
+ url.searchParams.set('ts', Date.now());
+ result.url = url.href;
+ }
+
+ return result;
+}
+```
+
+
+### `https` resolver
+
+
+`https-resolver.mjs`
+
+```js
+export async function resolve(
+ specifier,
+ context,
+ next, // unpkg-resolver
+) {
+ const result = await next(specifier, context);
+
+ const url = new URL(result.url); // this can throw, so handle appropriately
+
+ if (url.protocol = 'http:') {
+ url.protocol = 'https:';
+ result.url = url.href;
+ }
+
+ return result;
+}
+```
+
+
+### `unpkg` resolver
+
+
+`unpkg-resolver.mjs`
+
+```js
+export async function resolve(
+ specifier,
+ context,
+ next, // Node's defaultResolve
+) {
+ if (isBareSpecifier(specifier)) {
+ return `http://unpkg.com/${specifier}`;
+ }
+
+ return next(specifier, context);
+}
+```
+
+
+## Recursive `load`
+
+Say you had a chain of three loaders:
+
+* `babel-loader`
+* `coffeescript-loader`
+* `https-loader`
+
+```console
+node \
+--loader babel-loader \
+--loader coffeescript-loader \
+--loader https-loader \
+```
+
+These would be called in the following sequence (babel-loader is called first):
+
+`babel-loader` ← `coffeescript-loader` ← `https-loader` ← `defaultLoader`
+
+1. `babel-loader` needs the output of `coffeescript-loader` to transform bleeding-edge JavaScript features to some ancient target
+1. `coffeescript-loader` needs the raw source (the output of `defaultLoad` / `https-loader`) to transform coffeescript files to regular javascript
+1. `defaultLoad` / `https-loader` returns the actual, raw source
+
+The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
+
+Any loader that neglects to call `next` causes a short-circuit.
+
+### `babel` loader
+
+
+`babel-loader.mjs`
+
+```js
+export async function resolve(/* … */) {/* … */ }
+
+export async function load(
+ url,
+ context,
+ next, // coffeescript ← https-loader ← defaultLoader
+) {
+ const babelConfig = await getBabelConfig(url);
+
+ const format = babelOutputToFormat.get(babelConfig.output.format);
+
+ if (format === 'commonjs') return { format };
+
+ const { source: transpiledSource } = await next(url, { ...context, format });
+ const { code: transformedSource } = Babel.transformSync(transpiledSource.toString(), babelConfig);
+
+ return {
+ format,
+ source: transformedSource,
+ };
+}
+
+function getBabelConfig(url) {/* … */ }
+const babelOutputToFormat = new Map([
+ ['cjs', 'commonjs'],
+ ['esm', 'module'],
+ // …
+]);
+```
+
+
+### `coffeescript` loader
+
+
+`coffeescript-loader.mjs`
+
+```js
+export async function resolve(/* … */) {/* … */}
+
+export async function load(
+ url,
+ context,
+ next, // https-loader ← defaultLoader
+) {
+ if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad);
+
+ const format = await getPackageType(url);
+ if (format === 'commonjs') return { format };
+
+ const { source: rawSource } = await next(url, { ...context, format });
+ const transformedSource = CoffeeScript.compile(rawSource.toString(), {
+ bare: true,
+ filename: url,
+ });
+
+ return {
+ format,
+ source: transformedSource,
+ };
+}
+
+function getPackageType(url) {/* … */}
+const coffeescriptExtensionsRgs = /* … */
+```
+
+
+### `https` loader
+
+
+`https-loader.mjs`
+
+```js
+import { get } from 'https';
+
+const mimeTypeToFormat = new Map([
+ ['application/node', 'commonjs'],
+ ['application/javascript', 'module'],
+ ['application/json', 'json'],
+ // …
+]);
+
+export async function load(
+ url,
+ context,
+ next, // defaultLoader
+) {
+ if (!url.startsWith('https://')) return next(url, context);
+
+ return new Promise(function loadHttpsSource(resolve, reject) {
+ get(url, function getHttpsSource(rsp) {
+ // Determine the format from the MIME type of the response
+ const format = mimeTypeToFormat.get(rsp.headers['content-type']);
+ let source = '';
+
+ rsp.on('data', (chunk) => source += chunk);
+ rsp.on('end', () => resolve({ format, source }));
+ rsp.on('error', reject);
+ })
+ .on('error', (err) => reject(err));
+ });
+}
+```
+
+
+## Mock Loader
+
+
+`mock-loader.mjs`
+
+```js
+```
+
\ No newline at end of file
diff --git a/doc/design/proposal-fs-hooks.md b/doc/design/proposal-fs-hooks.md
deleted file mode 100644
index e69de29..0000000
From 5a168cf1985d418073aa7b28428fe436bde20dbc Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Mon, 27 Sep 2021 23:11:59 +0200
Subject: [PATCH 03/11] remove WIP mock-loader
---
doc/design/proposal-chaining-iterative.md | 9 ---------
doc/design/proposal-chaining-recursive.md | 9 ---------
2 files changed, 18 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index 9eddaed..e28485c 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -260,13 +260,4 @@ const babelOutputToFormat = new Map([
// …
]);
```
-
-
-## Mock Loader
-
-
-`mock-loader.mjs`
-
-```js
-```
\ No newline at end of file
diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md
index f851741..1ad2dfb 100644
--- a/doc/design/proposal-chaining-recursive.md
+++ b/doc/design/proposal-chaining-recursive.md
@@ -232,13 +232,4 @@ export async function load(
});
}
```
-
-
-## Mock Loader
-
-
-`mock-loader.mjs`
-
-```js
-```
\ No newline at end of file
From 8d60598e9789fef9de78b1e8cb25b3a694ae217e Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 28 Sep 2021 22:08:42 +0200
Subject: [PATCH 04/11] update proposals to incorporate short-circuit as a
returned flag
---
doc/design/proposal-chaining-iterative.md | 66 +++++++++++++----------
doc/design/proposal-chaining-recursive.md | 64 ++++++++++++++++++----
2 files changed, 91 insertions(+), 39 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index e28485c..d4fb417 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -19,26 +19,31 @@ These would be called in the following sequence:
`unpkg-resolver` → `https-resolver` → `cache-buster-resolver`
-Resolve hooks have the following signature:
+Resolve hooks would have the following signature:
-```js
+```ts
export async function resolve(
- interimResult: { // The result from the previous hook
- format = '', // if resolve settled with a `format`, this is that value
- // until a load hook provides a different value
- url = '', // the most recently provided value from a previous hook
+ interimResult: { // result from the previous hook
+ format = '', // most recently provided value from a previous hook
+ url = '', // most recently provided value from a previous hook
},
context: {
- conditions, // export conditions (from the relevant package.json)
- parentUrl, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- originalSpecifier, // The original value of the import specifier
+ conditions = string[], // export conditions from the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ originalSpecifier, // The original value of the import specifier
},
- defaultResolve, // node's default resolve hook
- shortCircuit, // special function for terminating the chain early
-): { format: string, url: string } {
+ defaultResolve, // node's default resolve hook
+): {
+ format?: string, // a hint to the load hook (it can be ignored)
+ shortCircuit?: true, // immediately terminate the `resolve` chain
+
+ url: string, // the final hook must return a valid URL string
+} {
```
+A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called).
+
### `unpkg` resolver
@@ -65,7 +70,7 @@ export async function resolve(
context,
) {
const url = new URL(interimResult.url); // this can throw, so handle appropriately
-
+
if (url.protocol = 'http:') url.protocol = 'https:';
return { url: url.toString() };
@@ -119,32 +124,37 @@ These would be called in the following sequence:
The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
-Load hooks have the following signature:
+Load hooks would have the following signature:
```js
export async function load(
- interimResult: { // The result from the previous hook
- format = '', // if resolve settled with a `format`, this is that value
- // until a load hook provides a different value
- source = '', //
+ interimResult: { // result from the previous hook
+ format = '', // the value if resolve settled with a `format`
+ // until a load hook provides a different value
+ source = '', //
},
context: {
- conditions, // export conditions (from the relevant package.json)
- parentUrl, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl, // the url to which the resolve hook chain settled
+ conditions, // export conditions from the relevant package.json
+ parentUrl, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl, // the url to which the resolve hook chain settled
},
- defaultLoad, // node's default load hook
- shortCircuit, // special function for terminating the chain early
-): { format: string, source: string | ArrayBuffer | TypedArray } {
+ defaultLoad, // node's default load hook
+): {
+ format: string, // the final hook must return one node understands
+ shortCircuit?: true, // signal to immediately terminate the `load` chain
+ source: string | ArrayBuffer | TypedArray,
+} {
```
+A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called).
+
### `https` loader
`https-loader.mjs`
-```js
+```ts
export async function load(
interimResult,
{ resolvedUrl },
@@ -260,4 +270,4 @@ const babelOutputToFormat = new Map([
// …
]);
```
-
\ No newline at end of file
+
diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md
index 1ad2dfb..b285f08 100644
--- a/doc/design/proposal-chaining-recursive.md
+++ b/doc/design/proposal-chaining-recursive.md
@@ -10,7 +10,9 @@ Say you had a chain of three loaders:
3. The `cache-buster` that takes the URL and adds a timestamp to the end, so like `https://unpkg.com/foo?ts=1234567890`.
-The hook functions nest: each one always just returns `{ [format: string,] url: string }`, and the chaining happens as a result of calling `next()`. The chain short-circuits if a hook doesn’t call `next()`. Following the pattern of `--require`:
+The hook functions nest: each one always must returns a plain object, and the chaining happens as a result of calling `next()`. A hook that fails to return triggers an exception.
+
+Following the pattern of `--require`:
```console
node \
@@ -27,6 +29,27 @@ These would be called in the following sequence (babel-loader is called first):
1. `https-resolver` needs output of unpkg to convert it to https
1. `unpkg-resolver` returns the remote url
+Resolve hooks would have the following signature:
+
+```ts
+export async function resolve(
+ specifier: string, // The result from the previous hook
+ context: {
+ conditions, // export conditions (from the relevant package.json)
+ parentUrl, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ originalSpecifier, // The original value of the import specifier
+ },
+ defaultResolve, // node's default resolve hook
+): {
+ format?: string, // a hint to the load hook (it can be ignored)
+ shortCircuit?: true, // signal to immediately terminate the `resolve` chain
+ url: string, // the final hook must return a valid URL string
+} {
+```
+
+A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called).
+
### `cache-buster` resolver
@@ -39,14 +62,14 @@ export async function resolve(
next, // https-resolver
) {
const result = await next(specifier, context);
-
+
const url = new URL(result.url); // this can throw, so handle appropriately
-
+
if (supportsQueryString(url.protocol)) { // exclude data: & friends
url.searchParams.set('ts', Date.now());
result.url = url.href;
}
-
+
return result;
}
```
@@ -64,9 +87,9 @@ export async function resolve(
next, // unpkg-resolver
) {
const result = await next(specifier, context);
-
+
const url = new URL(result.url); // this can throw, so handle appropriately
-
+
if (url.protocol = 'http:') {
url.protocol = 'https:';
result.url = url.href;
@@ -120,9 +143,28 @@ These would be called in the following sequence (babel-loader is called first):
1. `coffeescript-loader` needs the raw source (the output of `defaultLoad` / `https-loader`) to transform coffeescript files to regular javascript
1. `defaultLoad` / `https-loader` returns the actual, raw source
-The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
+Load hooks would have the following signature:
+
+```ts
+export async function load(
+ resolvedUrl: string, // the url to which the resolve hook chain settled
+ context: {
+ conditions = string[], // export conditions of the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedFormat?: string, // the value if resolve settled with a `format`
+ },
+ next: function, // the "next" hook in the chain
+): {
+ format: string, // the final hook must return one node understands
+ shortCircuit?: true, // immediately terminate the `load` chain
+ source: string | ArrayBuffer | TypedArray,
+} {
+```
-Any loader that neglects to call `next` causes a short-circuit.
+A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called).
+
+The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
### `babel` loader
@@ -175,7 +217,7 @@ export async function load(
next, // https-loader ← defaultLoader
) {
if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad);
-
+
const format = await getPackageType(url);
if (format === 'commonjs') return { format };
@@ -217,7 +259,7 @@ export async function load(
next, // defaultLoader
) {
if (!url.startsWith('https://')) return next(url, context);
-
+
return new Promise(function loadHttpsSource(resolve, reject) {
get(url, function getHttpsSource(rsp) {
// Determine the format from the MIME type of the response
@@ -232,4 +274,4 @@ export async function load(
});
}
```
-
\ No newline at end of file
+
From 82f5172436c5136600481c14c9f8173cf720626d Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Sun, 10 Oct 2021 22:19:00 +0200
Subject: [PATCH 05/11] revert to main
changes to recursive design forked to #41
---
doc/design/proposal-chaining-recursive.md | 301 ++++++----------------
1 file changed, 77 insertions(+), 224 deletions(-)
diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md
index b285f08..b4b2d7f 100644
--- a/doc/design/proposal-chaining-recursive.md
+++ b/doc/design/proposal-chaining-recursive.md
@@ -2,7 +2,7 @@
## Chaining `resolve` hooks
-Say you had a chain of three loaders:
+Say you had a chain of three loaders, `unpkg`, `http-to-https`, `cache-buster`:
1. The `unpkg` loader resolves a specifier `foo` to an URL `http://unpkg.com/foo`.
@@ -10,268 +10,121 @@ Say you had a chain of three loaders:
3. The `cache-buster` that takes the URL and adds a timestamp to the end, so like `https://unpkg.com/foo?ts=1234567890`.
-The hook functions nest: each one always must returns a plain object, and the chaining happens as a result of calling `next()`. A hook that fails to return triggers an exception.
+In the new loaders design, these three loaders could be implemented as follows:
-Following the pattern of `--require`:
-
-```console
-node \
---loader unpkg-resolver \
---loader https-resolver \
---loader cache-buster-resolver
-```
-
-These would be called in the following sequence (babel-loader is called first):
-
-`cache-buster-resolver` ← `https-resolver` ← `unpkg-resolver`
-
-1. `cache-buster-resolver` needs the output of `https-resolver` to append the query param
-1. `https-resolver` needs output of unpkg to convert it to https
-1. `unpkg-resolver` returns the remote url
-
-Resolve hooks would have the following signature:
-
-```ts
-export async function resolve(
- specifier: string, // The result from the previous hook
- context: {
- conditions, // export conditions (from the relevant package.json)
- parentUrl, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- originalSpecifier, // The original value of the import specifier
- },
- defaultResolve, // node's default resolve hook
-): {
- format?: string, // a hint to the load hook (it can be ignored)
- shortCircuit?: true, // signal to immediately terminate the `resolve` chain
- url: string, // the final hook must return a valid URL string
-} {
-```
-
-A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called).
-
-### `cache-buster` resolver
-
-
-`cachebuster-resolver.mjs`
+### `unpkg` loader
```js
-export async function resolve(
- specifier,
- context,
- next, // https-resolver
-) {
- const result = await next(specifier, context);
-
- const url = new URL(result.url); // this can throw, so handle appropriately
-
- if (supportsQueryString(url.protocol)) { // exclude data: & friends
- url.searchParams.set('ts', Date.now());
- result.url = url.href;
+export async function resolve(specifier, context, next) { // next is Node’s resolve
+ if (isBareSpecifier(specifier)) {
+ return `http://unpkg.com/${specifier}`;
}
-
- return result;
+ return next(specifier, context);
}
```
-
-
-### `https` resolver
-
-`https-resolver.mjs`
+### `http-to-https` loader
```js
-export async function resolve(
- specifier,
- context,
- next, // unpkg-resolver
-) {
+export async function resolve(specifier, context, next) { // next is the unpkg loader’s resolve
const result = await next(specifier, context);
-
- const url = new URL(result.url); // this can throw, so handle appropriately
-
- if (url.protocol = 'http:') {
- url.protocol = 'https:';
- result.url = url.href;
+ if (result.url.startsWith('http://')) {
+ result.url = `https${result.url.slice('http'.length)}`;
}
-
return result;
}
```
-
-
-### `unpkg` resolver
-
-`unpkg-resolver.mjs`
+### `cache-buster` loader
```js
-export async function resolve(
- specifier,
- context,
- next, // Node's defaultResolve
-) {
- if (isBareSpecifier(specifier)) {
- return `http://unpkg.com/${specifier}`;
+export async function resolve(specifier, context, next) { // next is the http-to-https loader’s resolve
+ const result = await next(specifier, context);
+ if (supportsQueryString(result.url)) { // exclude data: & friends
+ // TODO: do this properly in case the URL already has a query string
+ result.url += `?ts=${Date.now()}`;
}
-
- return next(specifier, context);
+ return result;
}
```
-
-## Chaining `load` hooks
+The hook functions nest: each one always just returns a string, like Node’s `resolve`, and the chaining happens as a result of calling `next`; and if a hook doesn’t call `next`, the chain short-circuits. The API would be `node --loader unpkg --loader http-to-https --loader cache-buster`, following the pattern set by `--require`.
-Say you had a chain of three loaders:
-
-* `babel-loader`
-* `coffeescript-loader`
-* `https-loader`
-
-```console
-node \
---loader babel-loader \
---loader coffeescript-loader \
---loader https-loader \
-```
-
-These would be called in the following sequence (babel-loader is called first):
-
-`babel-loader` ← `coffeescript-loader` ← `https-loader` ← `defaultLoader`
-
-1. `babel-loader` needs the output of `coffeescript-loader` to transform bleeding-edge JavaScript features to some ancient target
-1. `coffeescript-loader` needs the raw source (the output of `defaultLoad` / `https-loader`) to transform coffeescript files to regular javascript
-1. `defaultLoad` / `https-loader` returns the actual, raw source
-
-Load hooks would have the following signature:
-
-```ts
-export async function load(
- resolvedUrl: string, // the url to which the resolve hook chain settled
- context: {
- conditions = string[], // export conditions of the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedFormat?: string, // the value if resolve settled with a `format`
- },
- next: function, // the "next" hook in the chain
-): {
- format: string, // the final hook must return one node understands
- shortCircuit?: true, // immediately terminate the `load` chain
- source: string | ArrayBuffer | TypedArray,
-} {
-```
-
-A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called).
+## Chaining `load` hooks
-The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
+Chaining `load` hooks would be similar to chaining `resolve` hooks, though slightly more complicated in that instead of returning a single string, each `load` hook returns an object `{ format, source }` where `source` is the loaded module’s source code/contents and `format` is the name of one of Node’s ESM loader’s [“translators”](https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/translators.js): `commonjs`, `module`, `builtin` (a Node internal module like `fs`), `json` (with `--experimental-json-modules`) or `wasm` (with `--experimental-wasm-modules`).
-### `babel` loader
+Currently, Node’s internal ESM loader throws an error on unknown file types: `import('file.javascript')` throws, even if the contents of `file.javascript` are perfectly acceptable JavaScript. This error happens during Node’s internal `resolve` when it encounters a file extension it doesn’t recognize; hence the current [CoffeeScript loader example](https://nodejs.org/api/esm.html#esm_transpiler_loader) has lots of code to tell Node to allow CoffeeScript file extensions. We should move this validation check to be after the format is determined, which is one of the return values of `load`; so basically, it’s the responsibility of `load` to return a `format` that Node recognizes. Node’s internal `load` doesn’t know to resolve a URL ending in `.coffee` to `module`, so Node would continue to error like it does now; but the CoffeeScript loader under this new design no longer needs to hook into `resolve` at all, since it can determine the format of CoffeeScript files within `load`. In code:
-
-`babel-loader.mjs`
+### `coffeescript` loader
```js
-export async function resolve(/* … */) {/* … */ }
-
-export async function load(
- url,
- context,
- next, // coffeescript ← https-loader ← defaultLoader
-) {
- const babelConfig = await getBabelConfig(url);
-
- const format = babelOutputToFormat.get(babelConfig.output.format);
+import CoffeeScript from 'coffeescript';
- if (format === 'commonjs') return { format };
+// CoffeeScript files end in .coffee, .litcoffee or .coffee.md
+const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
- const { source: transpiledSource } = await next(url, { ...context, format });
- const { code: transformedSource } = Babel.transformSync(transpiledSource.toString(), babelConfig);
+export async function load(url, context, next) {
+ const result = await next(url, context);
- return {
- format,
- source: transformedSource,
- };
+ // The first check is technically not needed but ensures that
+ // we don’t try to compile things that already _are_ compiled.
+ if (result.format === undefined && extensionsRegex.test(url)) {
+ // For simplicity, all CoffeeScript URLs are ES modules.
+ const format = 'module';
+ const source = CoffeeScript.compile(result.source, { bare: true });
+ return {format, source};
+ }
+ return result;
}
-
-function getBabelConfig(url) {/* … */ }
-const babelOutputToFormat = new Map([
- ['cjs', 'commonjs'],
- ['esm', 'module'],
- // …
-]);
```
-
-### `coffeescript` loader
+And the other example loader in the docs, to allow `import` of `https://` URLs, would similarly only need a `load` hook:
-
-`coffeescript-loader.mjs`
+### `https` loader
```js
-export async function resolve(/* … */) {/* … */}
-
-export async function load(
- url,
- context,
- next, // https-loader ← defaultLoader
-) {
- if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad);
-
- const format = await getPackageType(url);
- if (format === 'commonjs') return { format };
-
- const { source: rawSource } = await next(url, { ...context, format });
- const transformedSource = CoffeeScript.compile(rawSource.toString(), {
- bare: true,
- filename: url,
- });
-
- return {
- format,
- source: transformedSource,
- };
-}
+import { get } from 'https';
-function getPackageType(url) {/* … */}
-const coffeescriptExtensionsRgs = /* … */
+export async function load(url, context, next) {
+ if (url.startsWith('https://')) {
+ let format; // default: format is undefined
+ const source = await new Promise((resolve, reject) => {
+ get(url, (res) => {
+ // Determine the format from the MIME type of the response
+ switch (res.headers['content-type']) {
+ case 'application/javascript':
+ case 'text/javascript': // etc.
+ format = 'module';
+ break;
+ case 'application/node':
+ case 'application/vnd.node.node':
+ format = 'commonjs';
+ break;
+ case 'application/json':
+ format = 'json';
+ break;
+ // etc.
+ }
+
+ let data = '';
+ res.on('data', (chunk) => data += chunk);
+ res.on('end', () => resolve({ source: data }));
+ }).on('error', (err) => reject(err));
+ });
+ return {format, source};
+ }
+
+ return next(url, context);
+}
```
-
-### `https` loader
+If these two loaders are used together, where the `coffeescript` loader’s `next` is the `https` loader’s hook and `https` loader’s `next` is Node’s native hook, then for a URL like `https://example.com/module.coffee`:
-
-`https-loader.mjs`
+1. The `https` loader would load the source over the network, but return `format: undefined`, assuming the server supplied a correct `Content-Type` header like `application/vnd.coffeescript` which our `https` loader doesn’t recognize.
-```js
-import { get } from 'https';
+2. The `coffeescript` loader would get that `{ source, format: undefined }` early on from its call to `next`, and set `format: 'module'` based on the `.coffee` at the end of the URL. It would also transpile the source into JavaScript. It then returns `{ format: 'module', source }` where `source` is runnable JavaScript rather than the original CoffeeScript.
-const mimeTypeToFormat = new Map([
- ['application/node', 'commonjs'],
- ['application/javascript', 'module'],
- ['application/json', 'json'],
- // …
-]);
-
-export async function load(
- url,
- context,
- next, // defaultLoader
-) {
- if (!url.startsWith('https://')) return next(url, context);
-
- return new Promise(function loadHttpsSource(resolve, reject) {
- get(url, function getHttpsSource(rsp) {
- // Determine the format from the MIME type of the response
- const format = mimeTypeToFormat.get(rsp.headers['content-type']);
- let source = '';
-
- rsp.on('data', (chunk) => source += chunk);
- rsp.on('end', () => resolve({ format, source }));
- rsp.on('error', reject);
- })
- .on('error', (err) => reject(err));
- });
-}
-```
-
+## Chaining `globalPreload` hooks
+
+For now, we think that this wouldn’t be chained the way `resolve` and `load` would be. This hook would just be called sequentially for each registered loader, in the same order as the loaders themselves are registered. If this is insufficient, for example for instrumentation use cases, we can discuss and potentially change this to follow the chaining style of `load`.
From 00ae3fb18a17b7340aeaddc4594bfb934bf24ef9 Mon Sep 17 00:00:00 2001
From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Sat, 16 Oct 2021 18:57:46 +0200
Subject: [PATCH 06/11] fix mixed whitespace & port review feedback from
recursive PR
---
doc/design/proposal-chaining-iterative.md | 49 ++++++++++++-----------
1 file changed, 25 insertions(+), 24 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index d4fb417..e3d5de9 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -24,20 +24,20 @@ Resolve hooks would have the following signature:
```ts
export async function resolve(
interimResult: { // result from the previous hook
- format = '', // most recently provided value from a previous hook
- url = '', // most recently provided value from a previous hook
+ format = '', // most recently provided value from a previous hook
+ url = '', // most recently provided value from a previous hook
},
context: {
- conditions = string[], // export conditions from the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- originalSpecifier, // The original value of the import specifier
+ conditions = string[], // export conditions from the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar.mjs, parentUrl is foo.mjs
+ originalSpecifier, // The original value of the import specifier
},
defaultResolve, // node's default resolve hook
): {
format?: string, // a hint to the load hook (it can be ignored)
- shortCircuit?: true, // immediately terminate the `resolve` chain
-
+ shortCircuit?: true, // signal that this hook intends to terminate the
+ // `resolve` chain
url: string, // the final hook must return a valid URL string
} {
```
@@ -51,10 +51,10 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm
```js
export async function resolve(
- interimResult,
- { originalSpecifier },
+ interimResult,
+ { originalSpecifier },
) {
- if (isBareSpecifier(originalSpecifier)) return `http://unpkg.com/${originalSpecifier}`;
+ if (isBareSpecifier(originalSpecifier)) return `http://unpkg.com/${originalSpecifier}`;
}
```
@@ -66,14 +66,14 @@ export async function resolve(
```js
export async function resolve(
- interimResult,
- context,
+ interimResult,
+ context,
) {
- const url = new URL(interimResult.url); // this can throw, so handle appropriately
+ const url = new URL(interimResult.url); // this can throw, so handle appropriately
- if (url.protocol = 'http:') url.protocol = 'https:';
+ if (url.protocol = 'http:') url.protocol = 'https:';
- return { url: url.toString() };
+ return { url: url.toString() };
}
```
@@ -129,20 +129,21 @@ Load hooks would have the following signature:
```js
export async function load(
interimResult: { // result from the previous hook
- format = '', // the value if resolve settled with a `format`
- // until a load hook provides a different value
- source = '', //
+ format = '', // the value if resolve settled with a `format`
+ // until a load hook provides a different value
+ source = '', //
},
context: {
- conditions, // export conditions from the relevant package.json
- parentUrl, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl, // the url to which the resolve hook chain settled
+ conditions, // export conditions from the relevant package.json
+ parentUrl, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl, // the url to which the resolve hook chain settled
},
defaultLoad, // node's default load hook
): {
format: string, // the final hook must return one node understands
- shortCircuit?: true, // signal to immediately terminate the `load` chain
+ shortCircuit?: true, // signal that this hook intends to terminate the `load`
+ // chain
source: string | ArrayBuffer | TypedArray,
} {
```
From 9e2642a47bfe233e99af753988eddcd9bd4854a6 Mon Sep 17 00:00:00 2001
From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 26 Oct 2021 20:45:35 +0200
Subject: [PATCH 07/11] add missing types
---
doc/design/proposal-chaining-iterative.md | 36 +++++++++++------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index e3d5de9..28b949c 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -23,15 +23,15 @@ Resolve hooks would have the following signature:
```ts
export async function resolve(
- interimResult: { // result from the previous hook
- format = '', // most recently provided value from a previous hook
- url = '', // most recently provided value from a previous hook
+ interimResult: { // result from the previous hook
+ format = '', // most recently provided value from a previous hook
+ url = '', // most recently provided value from a previous hook
},
context: {
conditions = string[], // export conditions from the relevant package.json
parentUrl = null, // foo.mjs imports bar.mjs
// when module is bar.mjs, parentUrl is foo.mjs
- originalSpecifier, // The original value of the import specifier
+ originalSpecifier: string, // The original value of the import specifier
},
defaultResolve, // node's default resolve hook
): {
@@ -126,24 +126,24 @@ The below examples are not exhaustive and provide only the gist of what each loa
Load hooks would have the following signature:
-```js
+```ts
export async function load(
- interimResult: { // result from the previous hook
- format = '', // the value if resolve settled with a `format`
- // until a load hook provides a different value
- source = '', //
+ interimResult: { // result from the previous hook
+ format = '', // the value if resolve settled with a `format`
+ // until a load hook provides a different value
+ source = '', //
},
context: {
- conditions, // export conditions from the relevant package.json
- parentUrl, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl, // the url to which the resolve hook chain settled
+ conditions = string[], // export conditions from the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl: string, // the url to which the resolve hook chain settled
},
- defaultLoad, // node's default load hook
+ defaultLoad: function, // node's default load hook
): {
- format: string, // the final hook must return one node understands
- shortCircuit?: true, // signal that this hook intends to terminate the `load`
- // chain
+ format: string, // the final hook must return one node understands
+ shortCircuit?: true, // signal that this hook intends to terminate the
+ // `load` chain
source: string | ArrayBuffer | TypedArray,
} {
```
@@ -155,7 +155,7 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm
`https-loader.mjs`
-```ts
+```js
export async function load(
interimResult,
{ resolvedUrl },
From 6f6bd71239a56e2147c06c1d19b161242719660f Mon Sep 17 00:00:00 2001
From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 26 Oct 2021 21:05:33 +0200
Subject: [PATCH 08/11] add return `signals`
---
doc/design/proposal-chaining-iterative.md | 54 +++++++++++++----------
1 file changed, 30 insertions(+), 24 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index 28b949c..3f18874 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -23,22 +23,25 @@ Resolve hooks would have the following signature:
```ts
export async function resolve(
- interimResult: { // result from the previous hook
- format = '', // most recently provided value from a previous hook
- url = '', // most recently provided value from a previous hook
+ interimResult: { // results from the previous hook
+ format = '',
+ url = '',
},
context: {
- conditions = string[], // export conditions from the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar.mjs, parentUrl is foo.mjs
- originalSpecifier: string, // The original value of the import specifier
+ conditions = string[], // export conditions of the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar.mjs, parentUrl is foo.mjs
+ originalSpecifier: string, // the original value of the import specifier
},
- defaultResolve, // node's default resolve hook
+ defaultResolve, // node's default resolve hook
): {
- format?: string, // a hint to the load hook (it can be ignored)
- shortCircuit?: true, // signal that this hook intends to terminate the
- // `resolve` chain
- url: string, // the final hook must return a valid URL string
+ format?: string, // a hint to the load hook (it can be ignored)
+ signals?: { // signals from this hook to the ESMLoader
+ contextOverride?: object, // the new `context` argument for the next hook
+ interimIgnored?: true, // interimResult was intentionally ignored
+ shortCircuit?: true, // `resolve` chain should be terminated
+ },
+ url: string, // the final hook must return a valid URL string
} {
```
@@ -128,22 +131,25 @@ Load hooks would have the following signature:
```ts
export async function load(
- interimResult: { // result from the previous hook
- format = '', // the value if resolve settled with a `format`
- // until a load hook provides a different value
- source = '', //
+ interimResult: { // result from the previous hook
+ format = '', // the value if `resolve` settled with a `format`
+ // until a load hook provides a different value
+ source = '',
},
context: {
- conditions = string[], // export conditions from the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl: string, // the url to which the resolve hook chain settled
+ conditions = string[], // export conditions of the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl: string, // url to which the resolve hook chain settled
},
- defaultLoad: function, // node's default load hook
+ defaultLoad: function, // node's default load hook
): {
- format: string, // the final hook must return one node understands
- shortCircuit?: true, // signal that this hook intends to terminate the
- // `load` chain
+ format: string, // the final hook must return any node supports
+ signals?: { // signals from this hook to the ESMLoader
+ contextOverride?: object, // the new `context` argument for the next hook
+ interimIgnored?: true, // interimResult was intentionally ignored
+ shortCircuit?: true, // `resolve` chain should be terminated
+ },
source: string | ArrayBuffer | TypedArray,
} {
```
From 2c09f152b459cf8385d6c627b5f5b7e748c9be7c Mon Sep 17 00:00:00 2001
From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 26 Oct 2021 21:11:09 +0200
Subject: [PATCH 09/11] fix github eff'ing up tabs
---
doc/design/proposal-chaining-iterative.md | 224 +++++++++++-----------
1 file changed, 112 insertions(+), 112 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index 3f18874..cdbb2c4 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -23,22 +23,22 @@ Resolve hooks would have the following signature:
```ts
export async function resolve(
- interimResult: { // results from the previous hook
- format = '',
- url = '',
- },
- context: {
- conditions = string[], // export conditions of the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar.mjs, parentUrl is foo.mjs
- originalSpecifier: string, // the original value of the import specifier
- },
- defaultResolve, // node's default resolve hook
+ interimResult: { // results from the previous hook
+ format = '',
+ url = '',
+ },
+ context: {
+ conditions = string[], // export conditions of the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar.mjs, parentUrl is foo.mjs
+ originalSpecifier: string, // the original value of the import specifier
+ },
+ defaultResolve, // node's default resolve hook
): {
- format?: string, // a hint to the load hook (it can be ignored)
+ format?: string, // a hint to the load hook (it can be ignored)
signals?: { // signals from this hook to the ESMLoader
contextOverride?: object, // the new `context` argument for the next hook
- interimIgnored?: true, // interimResult was intentionally ignored
+ interimIgnored?: true, // interimResult was intentionally ignored
shortCircuit?: true, // `resolve` chain should be terminated
},
url: string, // the final hook must return a valid URL string
@@ -54,10 +54,10 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm
```js
export async function resolve(
- interimResult,
- { originalSpecifier },
+ interimResult,
+ { originalSpecifier },
) {
- if (isBareSpecifier(originalSpecifier)) return `http://unpkg.com/${originalSpecifier}`;
+ if (isBareSpecifier(originalSpecifier)) return `http://unpkg.com/${originalSpecifier}`;
}
```
@@ -69,14 +69,14 @@ export async function resolve(
```js
export async function resolve(
- interimResult,
- context,
+ interimResult,
+ context,
) {
- const url = new URL(interimResult.url); // this can throw, so handle appropriately
+ const url = new URL(interimResult.url); // this can throw, so handle appropriately
- if (url.protocol = 'http:') url.protocol = 'https:';
+ if (url.protocol = 'http:') url.protocol = 'https:';
- return { url: url.toString() };
+ return { url: url.toString() };
}
```
@@ -88,15 +88,15 @@ export async function resolve(
```js
export async function resolve(
- interimResult,
+ interimResult,
) {
- const url = new URL(interimResult.url); // this can throw, so handle appropriately
+ const url = new URL(interimResult.url); // this can throw, so handle appropriately
- if (supportsQueryString(url.protocol)) { // exclude data: & friends
- url.searchParams.set('t', Date.now());
- }
+ if (supportsQueryString(url.protocol)) { // exclude data: & friends
+ url.searchParams.set('t', Date.now());
+ }
- return { url: url.toString() };
+ return { url: url.toString() };
}
```
@@ -131,26 +131,26 @@ Load hooks would have the following signature:
```ts
export async function load(
- interimResult: { // result from the previous hook
- format = '', // the value if `resolve` settled with a `format`
- // until a load hook provides a different value
- source = '',
- },
- context: {
- conditions = string[], // export conditions of the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl: string, // url to which the resolve hook chain settled
- },
- defaultLoad: function, // node's default load hook
+ interimResult: { // result from the previous hook
+ format = '', // the value if `resolve` settled with a `format`
+ // until a load hook provides a different value
+ source = '',
+ },
+ context: {
+ conditions = string[], // export conditions of the relevant package.json
+ parentUrl = null, // foo.mjs imports bar.mjs
+ // when module is bar, parentUrl is foo.mjs
+ resolvedUrl: string, // url to which the resolve hook chain settled
+ },
+ defaultLoad: function, // node's default load hook
): {
- format: string, // the final hook must return any node supports
+ format: string, // the final hook must return any node supports
signals?: { // signals from this hook to the ESMLoader
contextOverride?: object, // the new `context` argument for the next hook
- interimIgnored?: true, // interimResult was intentionally ignored
+ interimIgnored?: true, // interimResult was intentionally ignored
shortCircuit?: true, // `resolve` chain should be terminated
},
- source: string | ArrayBuffer | TypedArray,
+ source: string | ArrayBuffer | TypedArray,
} {
```
@@ -163,32 +163,32 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm
```js
export async function load(
- interimResult,
- { resolvedUrl },
+ interimResult,
+ { resolvedUrl },
) {
- if (interimResult.source) return; // step aside (content already retrieved)
+ if (interimResult.source) return; // step aside (content already retrieved)
- if (!resolvedUrl.startsWith('https://')) return; // step aside
+ if (!resolvedUrl.startsWith('https://')) return; // step aside
- return new Promise(function loadHttpsSource(resolve, reject) {
- get(resolvedUrl, function getHttpsSource(rsp) {
- const format = mimeTypeToFormat.get(rsp.headers['content-type']);
- let source = '';
+ return new Promise(function loadHttpsSource(resolve, reject) {
+ get(resolvedUrl, function getHttpsSource(rsp) {
+ const format = mimeTypeToFormat.get(rsp.headers['content-type']);
+ let source = '';
- rsp.on('data', (chunk) => source += chunk);
- rsp.on('end', () => resolve({ format, source }));
- rsp.on('error', reject);
- });
- });
+ rsp.on('data', (chunk) => source += chunk);
+ rsp.on('end', () => resolve({ format, source }));
+ rsp.on('error', reject);
+ });
+ });
}
const mimeTypeToFormat = new Map([
- ['application/node', 'commonjs'],
- ['application/javascript', 'module'],
- ['text/javascript', 'module'],
- ['application/json', 'json'],
- ['text/coffeescript', 'coffeescript'],
- // …
+ ['application/node', 'commonjs'],
+ ['application/javascript', 'module'],
+ ['text/javascript', 'module'],
+ ['application/json', 'json'],
+ ['text/coffeescript', 'coffeescript'],
+ // …
]);
```
@@ -202,29 +202,29 @@ const mimeTypeToFormat = new Map([
export async function resolve(/* … */) {/* … */ }
export async function load(
- interimResult, // possibly output of https-loader
- context,
- defaulLoad,
+ interimResult, // possibly output of https-loader
+ context,
+ defaulLoad,
) {
- const { resolvedUrl } = context;
- if (!coffeescriptExtensionsRgx.test(resolvedUrl)) return; // step aside
-
- const format = interimResult.format || await getPackageType(resolvedUrl);
- if (format === 'commonjs') return { format };
-
- const rawSource = (
- interimResult.source
- || await defaulLoad(resolvedUrl, { ...context, format }).source
- )
- const transformedSource = CoffeeScript.compile(rawSource.toString(), {
- bare: true,
- filename: resolvedUrl,
- });
-
- return {
- format,
- source: transformedSource,
- };
+ const { resolvedUrl } = context;
+ if (!coffeescriptExtensionsRgx.test(resolvedUrl)) return; // step aside
+
+ const format = interimResult.format || await getPackageType(resolvedUrl);
+ if (format === 'commonjs') return { format };
+
+ const rawSource = (
+ interimResult.source
+ || await defaulLoad(resolvedUrl, { ...context, format }).source
+ )
+ const transformedSource = CoffeeScript.compile(rawSource.toString(), {
+ bare: true,
+ filename: resolvedUrl,
+ });
+
+ return {
+ format,
+ source: transformedSource,
+ };
}
function getPackageType(url) {/* … */ }
@@ -241,40 +241,40 @@ const coffeescriptExtensionsRgs = /* … */
export async function resolve(/* … */) {/* … */ }
export async function load(
- interimResult, // possibly output of coffeescript-loader
- context,
- defaulLoad,
+ interimResult, // possibly output of coffeescript-loader
+ context,
+ defaulLoad,
) {
- const { resolvedUrl } = context;
- const babelConfig = await getBabelConfig(resolvedUrl);
-
- const format = (
- interimResult.format
- || babelOutputToFormat.get(babelConfig.output.format)
- );
-
- if (format === 'commonjs') return { format };
-
- const sourceToTranspile = (
- interimResult.source
- || await defaulLoad(resolvedUrl, { ...context, format }).source
- );
- const transformedSource = Babel.transformSync(
- sourceToTranspile.toString(),
- babelConfig,
- ).code;
-
- return {
- format,
- source: transformedSource,
- };
+ const { resolvedUrl } = context;
+ const babelConfig = await getBabelConfig(resolvedUrl);
+
+ const format = (
+ interimResult.format
+ || babelOutputToFormat.get(babelConfig.output.format)
+ );
+
+ if (format === 'commonjs') return { format };
+
+ const sourceToTranspile = (
+ interimResult.source
+ || await defaulLoad(resolvedUrl, { ...context, format }).source
+ );
+ const transformedSource = Babel.transformSync(
+ sourceToTranspile.toString(),
+ babelConfig,
+ ).code;
+
+ return {
+ format,
+ source: transformedSource,
+ };
}
function getBabelConfig(url) {/* … */ }
const babelOutputToFormat = new Map([
- ['cjs', 'commonjs'],
- ['esm', 'module'],
- // …
+ ['cjs', 'commonjs'],
+ ['esm', 'module'],
+ // …
]);
```
From ec129a0efbb17b5a13da1b434f9cec213c032957 Mon Sep 17 00:00:00 2001
From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Tue, 26 Oct 2021 21:41:28 +0200
Subject: [PATCH 10/11] improve consistency between design docs (nomenclature,
etc)
---
doc/design/proposal-chaining-iterative.md | 42 +++++++++++++----------
1 file changed, 23 insertions(+), 19 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index cdbb2c4..71a71d3 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -4,20 +4,20 @@
Say you had a chain of three loaders:
-* `unpkg-resolver` converts a bare specifier like `foo` to a url `http://unpkg.com/foo`.
-1. `http-to-https-resolver` rewrites insecure http urls to the https, like `https://unpkg.com/foo`.
-1. `cache-buster-resolver` adds a timestamp to a url end, like `https://unpkg.com/foo?t=1234567890`.
+1. `unpkg` resolves a specifier `foo` to an URL `http://unpkg.com/foo`.
+2. `https` rewrites that URL to `https://unpkg.com/foo`.
+3. `cache-buster` takes the URL and adds a timestamp to the end, like `https://unpkg.com/foo?ts=1234567890`.
```console
node \
---loader unpkg-resolver \
---loader https-resolver \
---loader cache-buster-resolver
+--loader unpkg \
+--loader https \
+--loader cache-buster
```
These would be called in the following sequence:
-`unpkg-resolver` → `https-resolver` → `cache-buster-resolver`
+`unpkg` → `https` → `cache-buster`
Resolve hooks would have the following signature:
@@ -65,7 +65,7 @@ export async function resolve(
### `https` resolver
-`https-resolver.mjs`
+`https-loader.mjs`
```js
export async function resolve(
@@ -78,13 +78,15 @@ export async function resolve(
return { url: url.toString() };
}
+
+export async function load(/* … */) {/* … */ }
```
### `cache-buster` resolver
-`cachebuster-resolver.mjs`
+`cache-buster-resolver.mjs`
```js
export async function resolve(
@@ -98,6 +100,8 @@ export async function resolve(
return { url: url.toString() };
}
+
+function supportsQueryString(/* … */) {/* … */}
```
@@ -106,24 +110,24 @@ export async function resolve(
Say you had a chain of three loaders:
-* `babel-loader` backwards time-machine
-* `coffeescript-loader` transforms coffeescript to vanilla javascript
-* `https-loader` loads source from remote
+* `babel` backwards time-machine
+* `coffeescript` transforms coffeescript to vanilla javascript
+* `https` loads source from remote
```console
node \
---loader https-loader \
---loader babel-loader \
---loader coffeescript-loader \
+--loader https \
+--loader babel \
+--loader coffeescript \
```
These would be called in the following sequence:
-(`https-loader` OR `defaultLoad`) → `coffeescript-loader` → `babel-loader`
+(`https` OR `defaultLoad`) → `coffeescript` → `babel`
-1. `defaultLoad` / `https-loader` needs to be first to actually get the source, which is fed to the subsequent loader
-1. `coffeescript-loader` receives the raw source from the previous loader and transpiles coffeescript files to regular javascript
-1. `babel-loader` receives potentially bleeding-edge JavaScript and transforms it to some ancient JavaScript target
+1. `defaultLoad` / `https` needs to be first to actually get the source, which is fed to the subsequent loader
+1. `coffeescript` receives the raw source from the previous loader and transpiles coffeescript files to regular javascript
+1. `babel` receives potentially bleeding-edge JavaScript and transforms it to some ancient JavaScript target
The below examples are not exhaustive and provide only the gist of what each loader needs to do and how it interacts with the others.
From fcae8d291e067f1de04f68e7ccb789d0479e3318 Mon Sep 17 00:00:00 2001
From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
Date: Wed, 27 Oct 2021 21:11:40 +0200
Subject: [PATCH 11/11] align to updates to "middleware" design doc
---
doc/design/proposal-chaining-iterative.md | 86 +++++++++++------------
1 file changed, 43 insertions(+), 43 deletions(-)
diff --git a/doc/design/proposal-chaining-iterative.md b/doc/design/proposal-chaining-iterative.md
index 71a71d3..b3d75f3 100644
--- a/doc/design/proposal-chaining-iterative.md
+++ b/doc/design/proposal-chaining-iterative.md
@@ -5,19 +5,21 @@
Say you had a chain of three loaders:
1. `unpkg` resolves a specifier `foo` to an URL `http://unpkg.com/foo`.
-2. `https` rewrites that URL to `https://unpkg.com/foo`.
+2. `http-to-https` rewrites that URL to `https://unpkg.com/foo`.
3. `cache-buster` takes the URL and adds a timestamp to the end, like `https://unpkg.com/foo?ts=1234567890`.
+Following the pattern of `--require`:
+
```console
node \
---loader unpkg \
---loader https \
---loader cache-buster
+ --loader unpkg \
+ --loader http-to-https \
+ --loader cache-buster
```
These would be called in the following sequence:
-`unpkg` → `https` → `cache-buster`
+`unpkg` → `http-to-https` → `cache-buster`
Resolve hooks would have the following signature:
@@ -28,29 +30,29 @@ export async function resolve(
url = '',
},
context: {
- conditions = string[], // export conditions of the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar.mjs, parentUrl is foo.mjs
- originalSpecifier: string, // the original value of the import specifier
+ conditions = string[], // Export conditions of the relevant package.json
+ parentUrl = null, // The module importing this one, or null if
+ // this is the Node entry point
+ specifier: string, // The original value of the import specifier
},
- defaultResolve, // node's default resolve hook
+ defaultResolve, // Node's default resolve hook
): {
- format?: string, // a hint to the load hook (it can be ignored)
- signals?: { // signals from this hook to the ESMLoader
- contextOverride?: object, // the new `context` argument for the next hook
+ format?: string, // A hint to the load hook (it might be ignored)
+ signals?: { // Signals from this hook to the ESMLoader
+ contextOverride?: object, // A new `context` argument for the next hook
interimIgnored?: true, // interimResult was intentionally ignored
shortCircuit?: true, // `resolve` chain should be terminated
},
- url: string, // the final hook must return a valid URL string
+ url: string, // The absolute URL that this input resolves to
} {
```
A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called).
-### `unpkg` resolver
+### `unpkg` loader
-`unpkg-resolver.mjs`
+`unpkg.mjs`
```js
export async function resolve(
@@ -62,10 +64,10 @@ export async function resolve(
```
-### `https` resolver
+### `http-to-https` loader
-`https-loader.mjs`
+`http-to-https.mjs`
```js
export async function resolve(
@@ -78,15 +80,13 @@ export async function resolve(
return { url: url.toString() };
}
-
-export async function load(/* … */) {/* … */ }
```
### `cache-buster` resolver
-`cache-buster-resolver.mjs`
+`cache-buster.mjs`
```js
export async function resolve(
@@ -110,15 +110,17 @@ function supportsQueryString(/* … */) {/* … */}
Say you had a chain of three loaders:
-* `babel` backwards time-machine
-* `coffeescript` transforms coffeescript to vanilla javascript
-* `https` loads source from remote
+* `babel` transforms modern JavaScript source into a specified target
+* `coffeescript` transforms CoffeeScript source into JavaScript source
+* `https` fetches `https:` URLs and returns their contents
+
+Following the pattern of `--require`:
```console
node \
---loader https \
---loader babel \
---loader coffeescript \
+ --loader https \
+ --loader babel \
+ --loader coffeescript \
```
These would be called in the following sequence:
@@ -141,20 +143,22 @@ export async function load(
source = '',
},
context: {
- conditions = string[], // export conditions of the relevant package.json
- parentUrl = null, // foo.mjs imports bar.mjs
- // when module is bar, parentUrl is foo.mjs
- resolvedUrl: string, // url to which the resolve hook chain settled
+ conditions = string[], // Export conditions of the relevant package.json
+ parentUrl = null, // The module importing this one, or null if
+ // this is the Node entry point
+ resolvedUrl: string, // The URL returned by the last hook of the
+ // `resolve` chain
},
- defaultLoad: function, // node's default load hook
+ defaultLoad: function, // Node's default load hook
): {
- format: string, // the final hook must return any node supports
- signals?: { // signals from this hook to the ESMLoader
- contextOverride?: object, // the new `context` argument for the next hook
+ format: 'builtin' | 'commonjs' | 'module' | 'json' | 'wasm', // A format
+ // that Node understands
+ signals?: { // Signals from this hook to the ESMLoader
+ contextOverride?: object, // A new `context` argument for the next hook
interimIgnored?: true, // interimResult was intentionally ignored
shortCircuit?: true, // `resolve` chain should be terminated
},
- source: string | ArrayBuffer | TypedArray,
+ source: string | ArrayBuffer | TypedArray, // The source for Node to evaluate
} {
```
@@ -163,7 +167,7 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm
### `https` loader
-`https-loader.mjs`
+`https.mjs`
```js
export async function load(
@@ -200,11 +204,9 @@ const mimeTypeToFormat = new Map([
### `coffeescript` loader
-`coffeescript-loader.mjs`
+`coffeescript.mjs`
```js
-export async function resolve(/* … */) {/* … */ }
-
export async function load(
interimResult, // possibly output of https-loader
context,
@@ -239,11 +241,9 @@ const coffeescriptExtensionsRgs = /* … */
### `babel` loader
-`babel-loader.mjs`
+`babel.mjs`
```js
-export async function resolve(/* … */) {/* … */ }
-
export async function load(
interimResult, // possibly output of coffeescript-loader
context,