diff --git a/doc/api/module.md b/doc/api/module.md index 7f9137a25f068e..5535da223f583d 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -1156,7 +1156,11 @@ changes: Node.js default `load` hook after the last user-supplied `load` hook * `url` {string} * `context` {Object|undefined} When omitted, defaults are provided. When provided, defaults are - merged in with preference to the provided properties. + merged in with preference to the provided properties. In the default `nextLoad`, if + the module pointed to by `url` does not have explicit module type information, + `context.format` is mandatory. + * Returns: {Object|Promise} The asynchronous version takes either an object containing the following properties, or a `Promise` that will resolve to such an object. The synchronous version only accepts an object returned synchronously. @@ -1354,36 +1358,32 @@ transpiler hooks should only be used for development and testing purposes. ```mjs // coffeescript-hooks.mjs import { readFile } from 'node:fs/promises'; -import { dirname, extname, resolve as resolvePath } from 'node:path'; -import { cwd } from 'node:process'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { findPackageJSON } from 'node:module'; import coffeescript from 'coffeescript'; const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/; export async function load(url, context, nextLoad) { if (extensionsRegex.test(url)) { - // CoffeeScript files can be either CommonJS or ES modules, so we want any - // CoffeeScript file to be treated by Node.js the same as a .js file at the - // same location. To determine how Node.js would interpret an arbitrary .js - // file, search up the file system for the nearest parent package.json file - // and read its "type" field. - const format = await getPackageType(url); - - const { source: rawSource } = await nextLoad(url, { ...context, format }); + // CoffeeScript files can be either CommonJS or ES modules. Use a custom format + // to tell Node.js not to detect its module type. + const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' }); // This hook converts CoffeeScript source code into JavaScript source code // for all imported CoffeeScript files. const transformedSource = coffeescript.compile(rawSource.toString(), url); + // To determine how Node.js would interpret the transpilation result, + // search up the file system for the nearest parent package.json file + // and read its "type" field. return { - format, + format: await getPackageType(url), shortCircuit: true, source: transformedSource, }; } // Let Node.js handle all other URLs. - return nextLoad(url); + return nextLoad(url, context); } async function getPackageType(url) { @@ -1394,25 +1394,12 @@ async function getPackageType(url) { // this simple truthy check for whether `url` contains a file extension will // work for most projects but does not cover some edge-cases (such as // extensionless files or a url ending in a trailing space) - const isFilePath = !!extname(url); - // If it is a file path, get the directory it's in - const dir = isFilePath ? - dirname(fileURLToPath(url)) : - url; - // Compose a file path to a package.json in the same directory, - // which may or may not exist - const packagePath = resolvePath(dir, 'package.json'); - // Try to read the possibly nonexistent package.json - const type = await readFile(packagePath, { encoding: 'utf8' }) - .then((filestring) => JSON.parse(filestring).type) - .catch((err) => { - if (err?.code !== 'ENOENT') console.error(err); - }); - // If package.json existed and contained a `type` field with a value, voilĂ  - if (type) return type; - // Otherwise, (if not at the root) continue checking the next directory up - // If at the root, stop and return false - return dir.length > 1 && getPackageType(resolvePath(dir, '..')); + const pJson = findPackageJSON(url); + + return readFile(pJson, 'utf8') + .then(JSON.parse) + .then((json) => json?.type) + .catch(() => undefined); } ``` @@ -1420,46 +1407,38 @@ async function getPackageType(url) { ```mjs // coffeescript-sync-hooks.mjs -import { readFileSync } from 'node:fs/promises'; -import { registerHooks } from 'node:module'; -import { dirname, extname, resolve as resolvePath } from 'node:path'; -import { cwd } from 'node:process'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { readFileSync } from 'node:fs'; +import { registerHooks, findPackageJSON } from 'node:module'; import coffeescript from 'coffeescript'; const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/; function load(url, context, nextLoad) { if (extensionsRegex.test(url)) { - const format = getPackageType(url); - - const { source: rawSource } = nextLoad(url, { ...context, format }); + const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' }); const transformedSource = coffeescript.compile(rawSource.toString(), url); return { - format, + format: getPackageType(url), shortCircuit: true, source: transformedSource, }; } - return nextLoad(url); + return nextLoad(url, context); } function getPackageType(url) { - const isFilePath = !!extname(url); - const dir = isFilePath ? dirname(fileURLToPath(url)) : url; - const packagePath = resolvePath(dir, 'package.json'); - - let type; + const pJson = findPackageJSON(url); + if (!pJson) { + return undefined; + } try { - const filestring = readFileSync(packagePath, { encoding: 'utf8' }); - type = JSON.parse(filestring).type; - } catch (err) { - if (err?.code !== 'ENOENT') console.error(err); + const file = readFileSync(pJson, 'utf-8'); + return JSON.parse(file)?.type; + } catch { + return undefined; } - if (type) return type; - return dir.length > 1 && getPackageType(resolvePath(dir, '..')); } registerHooks({ load }); @@ -1481,6 +1460,21 @@ console.log "Brought to you by Node.js version #{version}" export scream = (str) -> str.toUpperCase() ``` +For the sake of running the example, add a `package.json` file containing the +module type of the CoffeeScript files. + +```json +{ + "type": "module" +} +``` + +This is only for running the example. In real world loaders, `getPackageType()` must be +able to return an `format` known to Node.js even in the absence of an explicit type in a +`package.json`, or otherwise the `nextLoad` call would throw `ERR_UNKNOWN_FILE_EXTENSION` +(if undefined) or `ERR_UNKNOWN_MODULE_FORMAT` (if it's not a known format listed in +the [load hook][] documentation). + With the preceding hooks modules, running `node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee` or `node --import ./coffeescript-sync-hooks.mjs ./main.coffee`