From 8f3268723a4aa48fb2d4eb919701b59bbfbd2938 Mon Sep 17 00:00:00 2001 From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sun, 10 Oct 2021 22:16:53 +0200 Subject: [PATCH 1/7] update to support chaining & incorporate short-circuit as a return flag --- doc/design/proposal-chaining-recursive.md | 301 ++++++++++++++++------ 1 file changed, 224 insertions(+), 77 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index b4b2d7f..b285f08 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, `unpkg`, `http-to-https`, `cache-buster`: +Say you had a chain of three loaders: 1. The `unpkg` loader resolves a specifier `foo` to an URL `http://unpkg.com/foo`. @@ -10,121 +10,268 @@ Say you had a chain of three loaders, `unpkg`, `http-to-https`, `cache-buster`: 3. The `cache-buster` that takes the URL and adds a timestamp to the end, so like `https://unpkg.com/foo?ts=1234567890`. -In the new loaders design, these three loaders could be implemented as follows: +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. -### `unpkg` loader +Following the pattern of `--require`: -```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); -} +```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 +} { ``` -### `http-to-https` loader +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` ```js -export async function resolve(specifier, context, next) { // next is the unpkg loader’s resolve +export async function resolve( + specifier, + context, + next, // https-resolver +) { const result = await next(specifier, context); - if (result.url.startsWith('http://')) { - result.url = `https${result.url.slice('http'.length)}`; + + 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 -### `cache-buster` loader +
+`https-resolver.mjs` ```js -export async function resolve(specifier, context, next) { // next is the http-to-https loader’s resolve +export async function resolve( + specifier, + context, + next, // unpkg-resolver +) { 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()}`; + + 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; } ``` +
-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`. +### `unpkg` resolver -## Chaining `load` hooks +
+`unpkg-resolver.mjs` -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`). +```js +export async function resolve( + specifier, + context, + next, // Node's defaultResolve +) { + if (isBareSpecifier(specifier)) { + return `http://unpkg.com/${specifier}`; + } -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: + return next(specifier, context); +} +``` +
-### `coffeescript` loader +## Chaining `load` hooks -```js -import CoffeeScript from 'coffeescript'; +Say you had a chain of three loaders: -// CoffeeScript files end in .coffee, .litcoffee or .coffee.md -const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/; +* `babel-loader` +* `coffeescript-loader` +* `https-loader` -export async function load(url, context, next) { - const result = await next(url, context); +```console +node \ +--loader babel-loader \ +--loader coffeescript-loader \ +--loader https-loader \ +``` - // 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; -} +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, +} { ``` -And the other example loader in the docs, to allow `import` of `https://` URLs, would similarly only need a `load` hook: +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 +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 + +
+`babel-loader.mjs` ```js -import { get } from 'https'; +export async function resolve(/* … */) {/* … */ } -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}; - } +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 next(url, context); + return { + format, + source: transformedSource, + }; } + +function getBabelConfig(url) {/* … */ } +const babelOutputToFormat = new Map([ + ['cjs', 'commonjs'], + ['esm', 'module'], + // … +]); ``` +
-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`: +### `coffeescript` loader -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. +
+`coffeescript-loader.mjs` -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. +```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, + }; +} -## Chaining `globalPreload` hooks +function getPackageType(url) {/* … */} +const coffeescriptExtensionsRgs = /* … */ +``` +
+ +### `https` loader -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`. +
+`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)); + }); +} +``` +
From 57a4e9828137b0b7c42ae2b807cd47ae3731f7b6 Mon Sep 17 00:00:00 2001 From: Jacob <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 16 Oct 2021 18:48:20 +0200 Subject: [PATCH 2/7] fix copy-pasta, incorporate review feedback, fix mixed whitespace --- doc/design/proposal-chaining-recursive.md | 172 ++++++++++++---------- 1 file changed, 92 insertions(+), 80 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index b285f08..934a1af 100644 --- a/doc/design/proposal-chaining-recursive.md +++ b/doc/design/proposal-chaining-recursive.md @@ -21,7 +21,7 @@ node \ --loader cache-buster-resolver ``` -These would be called in the following sequence (babel-loader is called first): +These would be called in the following sequence (cache-buster is called first): `cache-buster-resolver` ← `https-resolver` ← `unpkg-resolver` @@ -29,22 +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 +So in JavaScript terms, `cacheBuster(httpToHttps(unpkg(input)))`. + Resolve hooks would have the following signature: ```ts export async function resolve( - specifier: string, // The result from the previous hook + specifier: string, // The original specifier 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 of the relevant package.json + parentUrl = null, // foo.mjs imports bar.mjs + // when module is bar, parentUrl is foo.mjs + // when module is bar.mjs, parentUrl is foo.mjs }, - defaultResolve, // node's default resolve hook + next: function, // the subsequent resolve hook in the chain (or, + // node's defaultResolve if the hook is the final + // supplied by the user) ): { - 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 + 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 } { ``` @@ -57,21 +62,23 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm ```js export async function resolve( - specifier, - context, - next, // https-resolver + specifier, + context, + next, // https' resolve ) { - const result = await next(specifier, context); + const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + 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; - } + if (supportsQueryString(url.protocol)) { // exclude data: & friends + url.searchParams.set('ts', Date.now()); + result.url = url.href; + } - return result; + return result; } + +function supportsQueryString(/* … */) {/* … */} ``` @@ -82,20 +89,20 @@ export async function resolve( ```js export async function resolve( - specifier, - context, - next, // unpkg-resolver + specifier, + context, + next, // unpkg's resolve ) { - const result = await next(specifier, context); + const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + const url = new URL(result.url); // this can throw, so handle appropriately - if (url.protocol = 'http:') { - url.protocol = 'https:'; - result.url = url.href; - } + if (url.protocol = 'http:') { + url.protocol = 'https:'; + result.url = url.href; + } - return result; + return result; } ``` @@ -107,15 +114,15 @@ export async function resolve( ```js export async function resolve( - specifier, - context, - next, // Node's defaultResolve + specifier, + context, + next, // Node's defaultResolve ) { - if (isBareSpecifier(specifier)) { - return `http://unpkg.com/${specifier}`; - } + if (isBareSpecifier(specifier)) { + return `http://unpkg.com/${specifier}`; + } - return next(specifier, context); + return next(specifier, context); } ``` @@ -143,21 +150,26 @@ 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 +So in JavaScript terms, `babel(coffeescript(https(input)))`. + 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` + 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 + next: function, // the subsequent load hook in the chain (or, + // node's defaultLoad if the hook is the final + // supplied by the user) ): { format: string, // the final hook must return one node understands - shortCircuit?: true, // immediately terminate the `load` chain + shortCircuit?: true, // signal that this hook intends to terminate the + // `load` chain source: string | ArrayBuffer | TypedArray, } { ``` @@ -177,7 +189,7 @@ export async function resolve(/* … */) {/* … */ } export async function load( url, context, - next, // coffeescript ← https-loader ← defaultLoader + next, // coffeescript's load ← https' load ← node's defaultLoad ) { const babelConfig = await getBabelConfig(url); @@ -212,25 +224,25 @@ const babelOutputToFormat = new Map([ export async function resolve(/* … */) {/* … */} export async function load( - url, - context, - next, // https-loader ← defaultLoader + url, + context, + next, // https' load ← node's defaultLoad ) { - if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad); + if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad); - const format = await getPackageType(url); - if (format === 'commonjs') return { format }; + 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, - }); + const { source: rawSource } = await next(url, { ...context, format }); + const transformedSource = CoffeeScript.compile(rawSource.toString(), { + bare: true, + filename: url, + }); - return { - format, - source: transformedSource, - }; + return { + format, + source: transformedSource, + }; } function getPackageType(url) {/* … */} @@ -247,31 +259,31 @@ const coffeescriptExtensionsRgs = /* … */ import { get } from 'https'; const mimeTypeToFormat = new Map([ - ['application/node', 'commonjs'], - ['application/javascript', 'module'], - ['application/json', 'json'], - // … + ['application/node', 'commonjs'], + ['application/javascript', 'module'], + ['application/json', 'json'], + // … ]); export async function load( - url, - context, - next, // defaultLoader + url, + context, + next, // node's defaultLoad ) { - 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)); - }); + 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)); + }); } ``` From 4c0dfecb0f7542ca16cc299e1587e50a302e9be8 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Mon, 25 Oct 2021 21:28:36 -0700 Subject: [PATCH 3/7] Cleanup pass --- doc/design/proposal-chaining-recursive.md | 265 +++++++++++----------- 1 file changed, 128 insertions(+), 137 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index 934a1af..6fb9cb6 100644 --- a/doc/design/proposal-chaining-recursive.md +++ b/doc/design/proposal-chaining-recursive.md @@ -16,40 +16,36 @@ Following the pattern of `--require`: ```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 (cache-buster is called first): +These would be called in the following sequence: `cache-buster` calls `https`, which calls `unpkg`. Or in JavaScript terms, `cacheBuster(httpToHttps(unpkg(input)))`: -`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 - -So in JavaScript terms, `cacheBuster(httpToHttps(unpkg(input)))`. +1. `cache-buster` needs the output of `https` to append the query param +2. `https` needs output of unpkg to convert it to https +3. `unpkg` returns the remote url Resolve hooks would have the following signature: ```ts export async function resolve( - specifier: string, // The original specifier - 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 - // when module is bar.mjs, parentUrl is foo.mjs - }, - next: function, // the subsequent resolve hook in the chain (or, - // node's defaultResolve if the hook is the final - // supplied by the user) + specifier: string, // The original specifier + 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 + // when module is bar.mjs, parentUrl is foo.mjs + }, + next: function, // the subsequent resolve hook in the chain (or, + // node's defaultResolve if the hook is the final + // supplied by the user) ): { - 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) + shortCircuit?: true, // signal that this hook intends to terminate the + // `resolve` chain + url: string, // the final hook must return a valid URL string } { ``` @@ -58,24 +54,24 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm ### `cache-buster` resolver
-`cachebuster-resolver.mjs` +`cachebuster.mjs` ```js export async function resolve( - specifier, - context, - next, // https' resolve + specifier, + context, + next, // https' resolve ) { - const result = await next(specifier, context); + const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + 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; - } + if (supportsQueryString(url.protocol)) { // exclude `data:` & friends + url.searchParams.set('ts', Date.now()); + result.url = url.href; + } - return result; + return result; } function supportsQueryString(/* … */) {/* … */} @@ -85,24 +81,24 @@ function supportsQueryString(/* … */) {/* … */} ### `https` resolver
-`https-resolver.mjs` +`https.mjs` ```js export async function resolve( - specifier, - context, - next, // unpkg's resolve + specifier, + context, + next, // unpkg's resolve ) { - const result = await next(specifier, context); + const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + const url = new URL(result.url); // this can throw, so handle appropriately - if (url.protocol = 'http:') { - url.protocol = 'https:'; - result.url = url.href; - } + if (url.protocol = 'http:') { + url.protocol = 'https:'; + result.url = url.href; + } - return result; + return result; } ```
@@ -110,19 +106,19 @@ export async function resolve( ### `unpkg` resolver
-`unpkg-resolver.mjs` +`unpkg.mjs` ```js export async function resolve( - specifier, - context, - next, // Node's defaultResolve + specifier, + context, + next, // Node's defaultResolve ) { - if (isBareSpecifier(specifier)) { - return `http://unpkg.com/${specifier}`; - } + if (isBareSpecifier(specifier)) { + return `http://unpkg.com/${specifier}`; + } - return next(specifier, context); + return next(specifier, context); } ```
@@ -131,46 +127,42 @@ export async function resolve( Say you had a chain of three loaders: -* `babel-loader` -* `coffeescript-loader` -* `https-loader` +* `babel` +* `coffeescript` +* `https` ```console node \ ---loader babel-loader \ ---loader coffeescript-loader \ ---loader https-loader \ +--loader babel \ +--loader coffeescript \ +--loader https \ ``` -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 +These would be called in the following sequence: `babel` calls `coffeescript`, which calls _either_ `https` or `defaultLoad`. Or in JavaScript terms, `babel(coffeescript(https(input)))` or `babel(coffeescript(defaultLoad(input)))`: -So in JavaScript terms, `babel(coffeescript(https(input)))`. +1. `babel` needs the output of `coffeescript` to transform bleeding-edge JavaScript features to a desired target +2. `coffeescript` needs the raw source (the output of either `defaultLoad` or `https`) to transform CoffeeScript files into JavaScript +3. `defaultLoad` / `https` gets 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 subsequent load hook in the chain (or, - // node's defaultLoad if the hook is the final - // supplied by the user) + 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 subsequent load hook in the chain (or, + // node's defaultLoad if the hook is the final + // supplied by the user) ): { - 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, + 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, } { ``` @@ -181,36 +173,36 @@ The below examples are not exhaustive and provide only the gist of what each loa ### `babel` loader
-`babel-loader.mjs` +`babel.mjs` ```js export async function resolve(/* … */) {/* … */ } export async function load( - url, - context, - next, // coffeescript's load ← https' load ← node's defaultLoad + url, + context, + next, // coffeescript's load ← https' load ← node's defaultLoad ) { - const babelConfig = await getBabelConfig(url); + const babelConfig = await getBabelConfig(url); - const format = babelOutputToFormat.get(babelConfig.output.format); + const format = babelOutputToFormat.get(babelConfig.output.format); - if (format === 'commonjs') return { format }; + if (format === 'commonjs') return { format }; - const { source: transpiledSource } = await next(url, { ...context, format }); - const { code: transformedSource } = Babel.transformSync(transpiledSource.toString(), babelConfig); + const { source: transpiledSource } = await next(url, { ...context, format }); + const { code: transformedSource } = Babel.transformSync(transpiledSource.toString(), babelConfig); - return { - format, - source: transformedSource, - }; + return { + format, + source: transformedSource, + }; } function getBabelConfig(url) {/* … */ } const babelOutputToFormat = new Map([ - ['cjs', 'commonjs'], - ['esm', 'module'], - // … + ['cjs', 'commonjs'], + ['esm', 'module'], + // … ]); ```
@@ -218,31 +210,31 @@ const babelOutputToFormat = new Map([ ### `coffeescript` loader
-`coffeescript-loader.mjs` +`coffeescript.mjs` ```js export async function resolve(/* … */) {/* … */} export async function load( - url, - context, - next, // https' load ← node's defaultLoad + url, + context, + next, // https' load ← node's defaultLoad ) { - if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad); + if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad); - const format = await getPackageType(url); - if (format === 'commonjs') return { format }; + 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, - }); + const { source: rawSource } = await next(url, { ...context, format }); + const transformedSource = CoffeeScript.compile(rawSource.toString(), { + bare: true, + filename: url, + }); - return { - format, - source: transformedSource, - }; + return { + format, + source: transformedSource, + }; } function getPackageType(url) {/* … */} @@ -253,37 +245,36 @@ const coffeescriptExtensionsRgs = /* … */ ### `https` loader
-`https-loader.mjs` +`https.mjs` ```js import { get } from 'https'; const mimeTypeToFormat = new Map([ - ['application/node', 'commonjs'], - ['application/javascript', 'module'], - ['application/json', 'json'], - // … + ['application/node', 'commonjs'], + ['application/javascript', 'module'], + ['application/json', 'json'], + // … ]); export async function load( - url, - context, - next, // node's defaultLoad + url, + context, + next, // node's defaultLoad ) { - 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)); - }); + if (!url.startsWith('https://')) return next(url, context); + + return new Promise(function loadHttpsSource(resolve, reject) { + get(url, function getHttpsSource(res) { + // Determine the format from the MIME type of the response + const format = mimeTypeToFormat.get(res.headers['content-type']); + let source = ''; + + res.on('data', (chunk) => source += chunk); + res.on('end', () => resolve({ format, source })); + res.on('error', reject); + }).on('error', (err) => reject(err)); + }); } ```
From b3972f7a3194a6d9b403fba70d39914bfead93b2 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:33:07 +0200 Subject: [PATCH 4/7] improve consistency between design docs (nomenclature, etc) --- doc/design/proposal-chaining-recursive.md | 39 ++++++++++++----------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index 6fb9cb6..c568d26 100644 --- a/doc/design/proposal-chaining-recursive.md +++ b/doc/design/proposal-chaining-recursive.md @@ -4,11 +4,9 @@ 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`. +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`. 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. @@ -54,7 +52,7 @@ A hook including `shortCircuit: true` will cause the chain to short-circuit, imm ### `cache-buster` resolver
-`cachebuster.mjs` +`cache-buster-resolver.mjs` ```js export async function resolve( @@ -81,7 +79,7 @@ function supportsQueryString(/* … */) {/* … */} ### `https` resolver
-`https.mjs` +`https-loader.mjs` ```js export async function resolve( @@ -100,13 +98,15 @@ export async function resolve( return result; } + +export async function load(/* … */) {/* … */ } ```
### `unpkg` resolver
-`unpkg.mjs` +`unpkg-resolver.mjs` ```js export async function resolve( @@ -173,7 +173,7 @@ The below examples are not exhaustive and provide only the gist of what each loa ### `babel` loader
-`babel.mjs` +`babel-loader.mjs` ```js export async function resolve(/* … */) {/* … */ } @@ -210,7 +210,7 @@ const babelOutputToFormat = new Map([ ### `coffeescript` loader
-`coffeescript.mjs` +`coffeescript-loader.mjs` ```js export async function resolve(/* … */) {/* … */} @@ -245,18 +245,11 @@ const coffeescriptExtensionsRgs = /* … */ ### `https` loader
-`https.mjs` +`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, @@ -266,7 +259,6 @@ export async function load( return new Promise(function loadHttpsSource(resolve, reject) { get(url, function getHttpsSource(res) { - // Determine the format from the MIME type of the response const format = mimeTypeToFormat.get(res.headers['content-type']); let source = ''; @@ -276,5 +268,14 @@ export async function load( }).on('error', (err) => reject(err)); }); } + +const mimeTypeToFormat = new Map([ + ['application/node', 'commonjs'], + ['application/javascript', 'module'], + ['text/javascript', 'module'], + ['application/json', 'json'], + ['text/coffeescript', 'coffeescript'], + // … +]); ```
From 068f3430200f7a23cff2faedd617a18a3004a2a1 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:34:42 +0200 Subject: [PATCH 5/7] improve note for shortCircuit and its purpose --- doc/design/proposal-chaining-recursive.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index c568d26..58c2b33 100644 --- a/doc/design/proposal-chaining-recursive.md +++ b/doc/design/proposal-chaining-recursive.md @@ -47,7 +47,7 @@ export async function resolve( } { ``` -A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called). +A hook including `shortCircuit: true` will allow the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called). The chain would naturally short-circuit if `next()` is not called, but that can lead to unexpected results that are often difficult to troubleshoot, so an error is thrown if both `next()` is not called and `shortCircuit` is not set. ### `cache-buster` resolver @@ -166,7 +166,7 @@ export async function load( } { ``` -A hook including `shortCircuit: true` will cause the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called). +A hook including `shortCircuit: true` will allow the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called). The chain would naturally short-circuit if `next()` is not called, but that can lead to unexpected results that are often difficult to troubleshoot, so an error is thrown if both `next()` is not called and `shortCircuit` is not set. 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 617ed2f7f0ced68c3f6555c17556b786e2e269d0 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 26 Oct 2021 20:05:33 -0700 Subject: [PATCH 6/7] Final cleanup pass --- doc/design/proposal-chaining-recursive.md | 214 ++++++++++------------ 1 file changed, 99 insertions(+), 115 deletions(-) diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-recursive.md index 58c2b33..41a481c 100644 --- a/doc/design/proposal-chaining-recursive.md +++ b/doc/design/proposal-chaining-recursive.md @@ -1,29 +1,27 @@ -# Chaining Hooks “Recursive” Design +# Chaining Hooks “Middleware” Design ## Chaining `resolve` hooks 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`. -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. +The hook functions nest: each one must always return a plain object, and the chaining happens as a result of each function calling `next()`, which is a reference to the subsequent loader’s hook. + +A hook that fails to return triggers an exception. A hook that returns without calling `next()`, and without returning `shortCircuit: true`, also triggers an exception. These errors are to help prevent unintentional breaks in the chain. Following the pattern of `--require`: ```console node \ --loader unpkg \ - --loader https \ + --loader http-to-https \ --loader cache-buster ``` -These would be called in the following sequence: `cache-buster` calls `https`, which calls `unpkg`. Or in JavaScript terms, `cacheBuster(httpToHttps(unpkg(input)))`: - -1. `cache-buster` needs the output of `https` to append the query param -2. `https` needs output of unpkg to convert it to https -3. `unpkg` returns the remote url +These would be called in the following sequence: `cache-buster` calls `http-to-https`, which calls `unpkg`. Or in JavaScript terms, `cacheBuster(httpToHttps(unpkg(input)))`. Resolve hooks would have the following signature: @@ -31,91 +29,82 @@ Resolve hooks would have the following signature: export async function resolve( specifier: string, // The original specifier 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 - // when module is bar.mjs, parentUrl is foo.mjs + 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 }, - next: function, // the subsequent resolve hook in the chain (or, - // node's defaultResolve if the hook is the final - // supplied by the user) + next: function, // The subsequent `resolve` hook in the chain, + // or Node’s default `resolve` hook after the + // last user-supplied `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 might be ignored) + shortCircuit?: true, // A signal that this hook intends to terminate + // the chain of `resolve` hooks + url: string, // The absolute URL that this input resolves to } { ``` -A hook including `shortCircuit: true` will allow the chain to short-circuit, immediately terminating the hook's chain (no subsequent `resolve` hooks are called). The chain would naturally short-circuit if `next()` is not called, but that can lead to unexpected results that are often difficult to troubleshoot, so an error is thrown if both `next()` is not called and `shortCircuit` is not set. - -### `cache-buster` resolver +### `cache-buster` loader
-`cache-buster-resolver.mjs` +`cache-buster.mjs` ```js export async function resolve( specifier, context, - next, // https' resolve + next, // In this example, `next` is https’ resolve ) { const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + const url = new URL(result.url); - if (supportsQueryString(url.protocol)) { // exclude `data:` & friends + if (url.protocol !== 'data:')) { // `data:` URLs don’t support query strings url.searchParams.set('ts', Date.now()); - result.url = url.href; } - return result; + return { url: url.href }; } - -function supportsQueryString(/* … */) {/* … */} ```
-### `https` resolver +### `http-to-https` loader
-`https-loader.mjs` +`http-to-https.mjs` ```js export async function resolve( specifier, context, - next, // unpkg's resolve + next, // In this example, `next` is unpkg’s resolve ) { const result = await next(specifier, context); - const url = new URL(result.url); // this can throw, so handle appropriately + const url = new URL(result.url); - if (url.protocol = 'http:') { + if (url.protocol === 'http:') { url.protocol = 'https:'; - result.url = url.href; } - return result; + return { url: url.href }; } - -export async function load(/* … */) {/* … */ } ```
-### `unpkg` resolver +### `unpkg` loader
-`unpkg-resolver.mjs` +`unpkg.mjs` ```js export async function resolve( specifier, context, - next, // Node's defaultResolve + next, // In this example, `next` is Node’s default `resolve` ) { - if (isBareSpecifier(specifier)) { - return `http://unpkg.com/${specifier}`; + if (isBareSpecifier(specifier)) { // Implemented elsewhere + return { url: `http://unpkg.com/${specifier}` }; } return next(specifier, context); @@ -127,103 +116,102 @@ export async function resolve( Say you had a chain of three loaders: -* `babel` -* `coffeescript` -* `https` +* `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 babel \ ---loader coffeescript \ ---loader https \ + --loader babel \ + --loader coffeescript \ + --loader https ``` -These would be called in the following sequence: `babel` calls `coffeescript`, which calls _either_ `https` or `defaultLoad`. Or in JavaScript terms, `babel(coffeescript(https(input)))` or `babel(coffeescript(defaultLoad(input)))`: - -1. `babel` needs the output of `coffeescript` to transform bleeding-edge JavaScript features to a desired target -2. `coffeescript` needs the raw source (the output of either `defaultLoad` or `https`) to transform CoffeeScript files into JavaScript -3. `defaultLoad` / `https` gets the actual, raw source +These would be called in the following sequence: `babel` calls `coffeescript`, which calls `https`. Or in JavaScript terms, `babel(coffeescript(https(input)))`: Load hooks would have the following signature: ```ts export async function load( - resolvedUrl: string, // the url to which the resolve hook chain settled + resolvedUrl: string, // The URL returned by the last hook of the + // `resolve` chain 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` + 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 + resolvedFormat?: string, // The format returned by the last hook of the + // `resolve` chain }, - next: function, // the subsequent load hook in the chain (or, - // node's defaultLoad if the hook is the final - // supplied by the user) + next: function, // The subsequent `load` hook in the chain, + // or Node’s default `load` hook after the + // last user-supplied `load` hook ): { - 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, + format: 'builtin' | 'commonjs' | 'module' | 'json' | 'wasm', // A format + // that Node understands + shortCircuit?: true, // A signal that this hook intends to terminate + // the chain of `load` hooks + source: string | ArrayBuffer | TypedArray, // The source for Node to evaluate } { ``` -A hook including `shortCircuit: true` will allow the chain to short-circuit, immediately terminating the hook's chain (no subsequent `load` hooks are called). The chain would naturally short-circuit if `next()` is not called, but that can lead to unexpected results that are often difficult to troubleshoot, so an error is thrown if both `next()` is not called and `shortCircuit` is not set. - -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
-`babel-loader.mjs` +`babel.mjs` ```js -export async function resolve(/* … */) {/* … */ } +const babelOutputToFormat = new Map([ + ['cjs', 'commonjs'], + ['esm', 'module'], + // … +]); export async function load( url, context, - next, // coffeescript's load ← https' load ← node's defaultLoad + next, // In this example, `next` is coffeescript’s hook ) { - const babelConfig = await getBabelConfig(url); + const babelConfig = await getBabelConfig(url); // Implemented elsewhere const format = babelOutputToFormat.get(babelConfig.output.format); - if (format === 'commonjs') return { format }; + if (format === 'commonjs') { + return { format, source: '' }; // Source is ignored for CommonJS + } const { source: transpiledSource } = await next(url, { ...context, format }); const { code: transformedSource } = Babel.transformSync(transpiledSource.toString(), babelConfig); - return { - format, - source: transformedSource, - }; + return { format, source: transformedSource }; } - -function getBabelConfig(url) {/* … */ } -const babelOutputToFormat = new Map([ - ['cjs', 'commonjs'], - ['esm', 'module'], - // … -]); ```
### `coffeescript` loader
-`coffeescript-loader.mjs` +`coffeescript.mjs` ```js -export async function resolve(/* … */) {/* … */} +// CoffeeScript files end in .coffee, .litcoffee or .coffee.md. +const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/; export async function load( url, context, - next, // https' load ← node's defaultLoad + next, // In this example, `next` is https’ hook ) { - if (!coffeescriptExtensionsRgx.test(url)) return next(url, context, defaultLoad); + if (!extensionsRegex.test(url)) { // Skip this hook for non-CoffeeScript imports + return next(url, context); + } + + const format = await getPackageType(url); // Implemented elsewhere - const format = await getPackageType(url); - if (format === 'commonjs') return { format }; + if (format === 'commonjs') { + return { format, source: '' }; // Source is ignored for CommonJS + } const { source: rawSource } = await next(url, { ...context, format }); const transformedSource = CoffeeScript.compile(rawSource.toString(), { @@ -231,51 +219,47 @@ export async function load( filename: url, }); - return { - format, - source: transformedSource, - }; + return { format, source: transformedSource }; } - -function getPackageType(url) {/* … */} -const coffeescriptExtensionsRgs = /* … */ ```
### `https` loader
-`https-loader.mjs` +`https.mjs` ```js -import { get } from 'https'; +import { get } from 'node:https'; + +const mimeTypeToFormat = new Map([ + ['application/node', 'commonjs'], + ['application/javascript', 'module'], + ['text/javascript', 'module'], + ['application/json', 'json'], + ['application/wasm', 'wasm'], + ['text/coffeescript', 'coffeescript'], + // … +]); export async function load( url, context, - next, // node's defaultLoad + next, // In this example, `next` is Node’s default `load` ) { - if (!url.startsWith('https://')) return next(url, context); + if (!url.startsWith('https://')) { // Skip this hook for non-https imports + return next(url, context); + } return new Promise(function loadHttpsSource(resolve, reject) { get(url, function getHttpsSource(res) { const format = mimeTypeToFormat.get(res.headers['content-type']); let source = ''; - res.on('data', (chunk) => source += chunk); res.on('end', () => resolve({ format, source })); res.on('error', reject); }).on('error', (err) => reject(err)); }); } - -const mimeTypeToFormat = new Map([ - ['application/node', 'commonjs'], - ['application/javascript', 'module'], - ['text/javascript', 'module'], - ['application/json', 'json'], - ['text/coffeescript', 'coffeescript'], - // … -]); ```
From deaa499f210ad962822b5b2cab3df52a420cb463 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 26 Oct 2021 20:07:00 -0700 Subject: [PATCH 7/7] Rename design --- doc/design/overview.md | 2 +- ...al-chaining-recursive.md => proposal-chaining-middleware.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/design/{proposal-chaining-recursive.md => proposal-chaining-middleware.md} (100%) diff --git a/doc/design/overview.md b/doc/design/overview.md index 38c5d0b..4b5ad82 100644 --- a/doc/design/overview.md +++ b/doc/design/overview.md @@ -19,7 +19,7 @@ Custom loaders are intended to chain to support various concerns beyond the scop ### Proposals -* [Recursive chaining](./proposal-chaining-recursive.md) +* [Chaining Hooks “Middleware” Design](./proposal-chaining-middleware.md) ## History diff --git a/doc/design/proposal-chaining-recursive.md b/doc/design/proposal-chaining-middleware.md similarity index 100% rename from doc/design/proposal-chaining-recursive.md rename to doc/design/proposal-chaining-middleware.md