From 18b3a80e5b82a7a4f1365781a9aba00e3fe433e0 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sat, 9 Jul 2022 22:44:02 -0700 Subject: [PATCH 1/2] Starting point for helpers design doc --- README.md | 6 +++--- doc/design/helpers.md | 49 ++++++++++++++++++++++++++++++++++++++++++ doc/design/overview.md | 2 ++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 doc/design/helpers.md diff --git a/README.md b/README.md index c376188..370b5a6 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ This team is spun off from the [Modules team](https://github.com/nodejs/modules) - [ ] Move loaders off thread -- [ ] Add helper/utility functions [`module`](https://nodejs.org/api/module.html) module +- [ ] Add [helper/utility functions](./doc/design/helpers.md) to the [`module`](https://nodejs.org/api/module.html) module - [ ] Start with the functions that make up the ESM resolution algorithm as defined in the [spec](https://nodejs.org/api/esm.html#resolver-algorithm-specification). Create helper functions for each of the functions defined in that psuedocode: `esmResolve`, `packageImportsResolve`, `packageResolve`, `esmFileFormat`, `packageSelfResolve`, `readPackageJson`, `packageExportsResolve`, `lookupPackageScope`, `packageTargetResolve`, `packageImportsExportsResolve`, `patternKeyCompare`. (Not necessarily all with these exact names, but corresponding to these functions from the spec.) - + - [ ] Follow up with similar helper functions that make up what happens within Node’s internal `load`. (Definitions to come.) - [ ] Support loading source when the return value of `load` has `format: 'commonjs'`. See https://github.com/nodejs/node/issues/34753#issuecomment-735921348 and https://github.com/nodejs/loaders-test/blob/835506a638c6002c1b2d42ab7137db3e7eda53fa/coffeescript-loader/loader.js#L45-L50. @@ -47,7 +47,7 @@ We hope that moving loaders off thread will allow us to preserve an async `resol - [ ] Convert `resolve` from async to sync https://github.com/nodejs/node/pull/43363 - [ ] Add an async `resolve` to [`module`](https://nodejs.org/api/module.html) module - + - [ ] Consider an API for async operations before resolution begins, such as `preImport` https://github.com/nodejs/loaders/pull/89 After this, we should get user feedback regarding the developer experience; for example, is too much boilerplate required? Should we have a separate `transform` hook? And so on. We should also investigate and potentially implement the [technical improvements](doc/use-cases.md#improvements) on our to-do list. diff --git a/doc/design/helpers.md b/doc/design/helpers.md new file mode 100644 index 0000000..38f7637 --- /dev/null +++ b/doc/design/helpers.md @@ -0,0 +1,49 @@ +# Helpers + +A user loader that defines a hook might need to reimplement all of Node’s original version of that hook, if the user hook can’t call `next` to get the result of the internal version. A case where this might occur is input that Node would error on, for example a `resolve` hook trying to resolve a protocol that Node doesn’t support. For such a hook, it could involve a lot of boilerplate or dependencies to reimplement all of the logic contained within Node’s internal version of that hook. We plan to create helper functions to reduce that need. + +These will be added in stages, starting with helpers for the `resolve` hook that cover the various steps that Node’s internal `resolve` performs. + +## `resolve` helpers + +### `packageResolve` + +Public reference to https://github.com/nodejs/node/blob/3350d9610864af3219de7dd20e3ac18b3c214c52/lib/internal/modules/esm/resolve.js#L847-L910. + +Existing signature: + +```js +/** + * @param {string} specifier + * @param {string | URL | undefined} base + * @param {Set} conditions + * @returns {resolved: URL, format? : string} + */ +function packageResolve(specifier, base, conditions) { +``` + +New signature, where many supporting functions that this function calls are passed in (and if left undefined, the default versions are used): + +```js +function packageResolve(specifier, base, conditions, { + parsePackageName, + getPackageScopeConfig, + packageExportsResolve, + findPackageJson, // Part of current packageResolve extracted into its own function + getPackageConfig, + legacyMainResolve, +}) { +``` + +The middle of the function, where it walks up the disk looking for `package.json`, would be moved into a separate function `findPackageJson` that would follow a pattern similar to this, where the various file system-related functions could all be overridden (so that a virtual filesystem could be simulated, for example). + +### `findPackageJson` + +Extracted from `packageResolve` https://github.com/nodejs/node/blob/3350d9610864af3219de7dd20e3ac18b3c214c52/lib/internal/modules/esm/resolve.js#L873-L887 plus the `while` condition: + +```js +function findPackageJson(packageName, base, isScoped, { + fileURLToPath, + tryStatSync, +}) +``` diff --git a/doc/design/overview.md b/doc/design/overview.md index a2a6a70..0334c55 100644 --- a/doc/design/overview.md +++ b/doc/design/overview.md @@ -13,6 +13,8 @@ There are currently [three loader hooks](https://github.com/nodejs/node/tree/mas * `globalPreload`: Defines a string of JavaScript to be injected into the application global scope. +In order to reduce boilerplate within hooks, new [helper functions](./helpers.md) will be made available. + ## Chaining Custom loaders are intended to chain to support various concerns beyond the scope of core, such as build tooling, mocking, transpilation, etc. From 0d263e3b7c9ff83812aa02a96e99cbbf47d7e444 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Mon, 11 Jul 2022 20:48:19 -0700 Subject: [PATCH 2/2] Add example --- doc/design/helpers.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/doc/design/helpers.md b/doc/design/helpers.md index 38f7637..6b1605c 100644 --- a/doc/design/helpers.md +++ b/doc/design/helpers.md @@ -4,6 +4,48 @@ A user loader that defines a hook might need to reimplement all of Node’s orig These will be added in stages, starting with helpers for the `resolve` hook that cover the various steps that Node’s internal `resolve` performs. +## Usage + +The intended usage for these helpers is to eliminate boilerplate within user-defined hooks. For example: + +```js +import { isBareSpecifier, packageResolve } from 'node:module'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + + +const needsTranspilation = new Set(); + +export function resolve(specifier, context, next) { + if (!isBareSpecifier(specifier)) { + return next(specifier, context); + } + + const pathToPackage = fileURLToPath(packageResolve(specifier, context.parentURL, context.conditions)); + const pathToPackageJson = join(pathToPackage, 'package.json'); + const packageMetadata = JSON.parse(readFileSync(pathToPackageJson, 'utf-8')); + + if (!packageMetadata.exports && packageMetadata.module) { + // If this package has a "module" field but no "exports" field, + // return the value of "module" and transpile later + // within a `load` hook + return { + url: pathToFileURL(join(pathToPackage, packageMetadata.module)) + } + } +} + +export async function load(url, context, next) { + if (!needsTranspilation.has(url)) { + return next(url, context); + } + + // TODO: Transpile the faux-ESM in the "module" URL + // and return the transpiled, runnable ESM source +} +``` + ## `resolve` helpers ### `packageResolve`