Skip to content

Commit 9897e8e

Browse files
joyeecheungaduh95
authored andcommitted
doc: fix transpiler loader hooks documentation
The loader hooks examples have been broken for a while: 1. The nextLoad() hook cannot be used on a .coffee file that ends up going to the default load step without an explict format, which would cause a ERR_UNKNOWN_FILE_EXTENSION. Mention adding a package.json with a type field to work around it in the example. 2. Pass the context parameter to the nextLoad() invocation and document that context.format is mandatory when module type is not explicitly inferrable from the module. 3. Correct the getPackageType() implementation which returns false instead of undefined in the absence of an explict format, which is not a valid type for format. PR-URL: #57037 Refs: #57030 Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 38797a8 commit 9897e8e

File tree

1 file changed

+48
-54
lines changed

1 file changed

+48
-54
lines changed

doc/api/module.md

+48-54
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,11 @@ changes:
13081308
Node.js default `load` hook after the last user-supplied `load` hook
13091309
* `url` {string}
13101310
* `context` {Object|undefined} When omitted, defaults are provided. When provided, defaults are
1311-
merged in with preference to the provided properties.
1311+
merged in with preference to the provided properties. In the default `nextLoad`, if
1312+
the module pointed to by `url` does not have explicit module type information,
1313+
`context.format` is mandatory.
1314+
<!-- TODO(joyeecheung): make it at least optionally non-mandatory by allowing
1315+
JS-style/TS-style module detection when the format is simply unknown -->
13121316
* Returns: {Object|Promise} The asynchronous version takes either an object containing the
13131317
following properties, or a `Promise` that will resolve to such an object. The
13141318
synchronous version only accepts an object returned synchronously.
@@ -1506,36 +1510,32 @@ transpiler hooks should only be used for development and testing purposes.
15061510
```mjs
15071511
// coffeescript-hooks.mjs
15081512
import { readFile } from 'node:fs/promises';
1509-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1510-
import { cwd } from 'node:process';
1511-
import { fileURLToPath, pathToFileURL } from 'node:url';
1513+
import { findPackageJSON } from 'node:module';
15121514
import coffeescript from 'coffeescript';
15131515

15141516
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
15151517

15161518
export async function load(url, context, nextLoad) {
15171519
if (extensionsRegex.test(url)) {
1518-
// CoffeeScript files can be either CommonJS or ES modules, so we want any
1519-
// CoffeeScript file to be treated by Node.js the same as a .js file at the
1520-
// same location. To determine how Node.js would interpret an arbitrary .js
1521-
// file, search up the file system for the nearest parent package.json file
1522-
// and read its "type" field.
1523-
const format = await getPackageType(url);
1524-
1525-
const { source: rawSource } = await nextLoad(url, { ...context, format });
1520+
// CoffeeScript files can be either CommonJS or ES modules. Use a custom format
1521+
// to tell Node.js not to detect its module type.
1522+
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
15261523
// This hook converts CoffeeScript source code into JavaScript source code
15271524
// for all imported CoffeeScript files.
15281525
const transformedSource = coffeescript.compile(rawSource.toString(), url);
15291526

1527+
// To determine how Node.js would interpret the transpilation result,
1528+
// search up the file system for the nearest parent package.json file
1529+
// and read its "type" field.
15301530
return {
1531-
format,
1531+
format: await getPackageType(url),
15321532
shortCircuit: true,
15331533
source: transformedSource,
15341534
};
15351535
}
15361536

15371537
// Let Node.js handle all other URLs.
1538-
return nextLoad(url);
1538+
return nextLoad(url, context);
15391539
}
15401540

15411541
async function getPackageType(url) {
@@ -1546,72 +1546,51 @@ async function getPackageType(url) {
15461546
// this simple truthy check for whether `url` contains a file extension will
15471547
// work for most projects but does not cover some edge-cases (such as
15481548
// extensionless files or a url ending in a trailing space)
1549-
const isFilePath = !!extname(url);
1550-
// If it is a file path, get the directory it's in
1551-
const dir = isFilePath ?
1552-
dirname(fileURLToPath(url)) :
1553-
url;
1554-
// Compose a file path to a package.json in the same directory,
1555-
// which may or may not exist
1556-
const packagePath = resolvePath(dir, 'package.json');
1557-
// Try to read the possibly nonexistent package.json
1558-
const type = await readFile(packagePath, { encoding: 'utf8' })
1559-
.then((filestring) => JSON.parse(filestring).type)
1560-
.catch((err) => {
1561-
if (err?.code !== 'ENOENT') console.error(err);
1562-
});
1563-
// If package.json existed and contained a `type` field with a value, voilà
1564-
if (type) return type;
1565-
// Otherwise, (if not at the root) continue checking the next directory up
1566-
// If at the root, stop and return false
1567-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
1549+
const pJson = findPackageJSON(url);
1550+
1551+
return readFile(pJson, 'utf8')
1552+
.then(JSON.parse)
1553+
.then((json) => json?.type)
1554+
.catch(() => undefined);
15681555
}
15691556
```
15701557
15711558
##### Synchronous version
15721559
15731560
```mjs
15741561
// coffeescript-sync-hooks.mjs
1575-
import { readFileSync } from 'node:fs/promises';
1576-
import { registerHooks } from 'node:module';
1577-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1578-
import { cwd } from 'node:process';
1579-
import { fileURLToPath, pathToFileURL } from 'node:url';
1562+
import { readFileSync } from 'node:fs';
1563+
import { registerHooks, findPackageJSON } from 'node:module';
15801564
import coffeescript from 'coffeescript';
15811565

15821566
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
15831567

15841568
function load(url, context, nextLoad) {
15851569
if (extensionsRegex.test(url)) {
1586-
const format = getPackageType(url);
1587-
1588-
const { source: rawSource } = nextLoad(url, { ...context, format });
1570+
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
15891571
const transformedSource = coffeescript.compile(rawSource.toString(), url);
15901572

15911573
return {
1592-
format,
1574+
format: getPackageType(url),
15931575
shortCircuit: true,
15941576
source: transformedSource,
15951577
};
15961578
}
15971579

1598-
return nextLoad(url);
1580+
return nextLoad(url, context);
15991581
}
16001582

16011583
function getPackageType(url) {
1602-
const isFilePath = !!extname(url);
1603-
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
1604-
const packagePath = resolvePath(dir, 'package.json');
1605-
1606-
let type;
1584+
const pJson = findPackageJSON(url);
1585+
if (!pJson) {
1586+
return undefined;
1587+
}
16071588
try {
1608-
const filestring = readFileSync(packagePath, { encoding: 'utf8' });
1609-
type = JSON.parse(filestring).type;
1610-
} catch (err) {
1611-
if (err?.code !== 'ENOENT') console.error(err);
1589+
const file = readFileSync(pJson, 'utf-8');
1590+
return JSON.parse(file)?.type;
1591+
} catch {
1592+
return undefined;
16121593
}
1613-
if (type) return type;
1614-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
16151594
}
16161595

16171596
registerHooks({ load });
@@ -1633,6 +1612,21 @@ console.log "Brought to you by Node.js version #{version}"
16331612
export scream = (str) -> str.toUpperCase()
16341613
```
16351614
1615+
For the sake of running the example, add a `package.json` file containing the
1616+
module type of the CoffeeScript files.
1617+
1618+
```json
1619+
{
1620+
"type": "module"
1621+
}
1622+
```
1623+
1624+
This is only for running the example. In real world loaders, `getPackageType()` must be
1625+
able to return an `format` known to Node.js even in the absence of an explicit type in a
1626+
`package.json`, or otherwise the `nextLoad` call would throw `ERR_UNKNOWN_FILE_EXTENSION`
1627+
(if undefined) or `ERR_UNKNOWN_MODULE_FORMAT` (if it's not a known format listed in
1628+
the [load hook][] documentation).
1629+
16361630
With the preceding hooks modules, running
16371631
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
16381632
or `node --import ./coffeescript-sync-hooks.mjs ./main.coffee`

0 commit comments

Comments
 (0)