Skip to content

Commit c02494f

Browse files
joyeecheungtargos
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 <jacob@frende.me> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 286bb84 commit c02494f

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
@@ -1156,7 +1156,11 @@ changes:
11561156
Node.js default `load` hook after the last user-supplied `load` hook
11571157
* `url` {string}
11581158
* `context` {Object|undefined} When omitted, defaults are provided. When provided, defaults are
1159-
merged in with preference to the provided properties.
1159+
merged in with preference to the provided properties. In the default `nextLoad`, if
1160+
the module pointed to by `url` does not have explicit module type information,
1161+
`context.format` is mandatory.
1162+
<!-- TODO(joyeecheung): make it at least optionally non-mandatory by allowing
1163+
JS-style/TS-style module detection when the format is simply unknown -->
11601164
* Returns: {Object|Promise} The asynchronous version takes either an object containing the
11611165
following properties, or a `Promise` that will resolve to such an object. The
11621166
synchronous version only accepts an object returned synchronously.
@@ -1354,36 +1358,32 @@ transpiler hooks should only be used for development and testing purposes.
13541358
```mjs
13551359
// coffeescript-hooks.mjs
13561360
import { readFile } from 'node:fs/promises';
1357-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1358-
import { cwd } from 'node:process';
1359-
import { fileURLToPath, pathToFileURL } from 'node:url';
1361+
import { findPackageJSON } from 'node:module';
13601362
import coffeescript from 'coffeescript';
13611363

13621364
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
13631365

13641366
export async function load(url, context, nextLoad) {
13651367
if (extensionsRegex.test(url)) {
1366-
// CoffeeScript files can be either CommonJS or ES modules, so we want any
1367-
// CoffeeScript file to be treated by Node.js the same as a .js file at the
1368-
// same location. To determine how Node.js would interpret an arbitrary .js
1369-
// file, search up the file system for the nearest parent package.json file
1370-
// and read its "type" field.
1371-
const format = await getPackageType(url);
1372-
1373-
const { source: rawSource } = await nextLoad(url, { ...context, format });
1368+
// CoffeeScript files can be either CommonJS or ES modules. Use a custom format
1369+
// to tell Node.js not to detect its module type.
1370+
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
13741371
// This hook converts CoffeeScript source code into JavaScript source code
13751372
// for all imported CoffeeScript files.
13761373
const transformedSource = coffeescript.compile(rawSource.toString(), url);
13771374

1375+
// To determine how Node.js would interpret the transpilation result,
1376+
// search up the file system for the nearest parent package.json file
1377+
// and read its "type" field.
13781378
return {
1379-
format,
1379+
format: await getPackageType(url),
13801380
shortCircuit: true,
13811381
source: transformedSource,
13821382
};
13831383
}
13841384

13851385
// Let Node.js handle all other URLs.
1386-
return nextLoad(url);
1386+
return nextLoad(url, context);
13871387
}
13881388

13891389
async function getPackageType(url) {
@@ -1394,72 +1394,51 @@ async function getPackageType(url) {
13941394
// this simple truthy check for whether `url` contains a file extension will
13951395
// work for most projects but does not cover some edge-cases (such as
13961396
// extensionless files or a url ending in a trailing space)
1397-
const isFilePath = !!extname(url);
1398-
// If it is a file path, get the directory it's in
1399-
const dir = isFilePath ?
1400-
dirname(fileURLToPath(url)) :
1401-
url;
1402-
// Compose a file path to a package.json in the same directory,
1403-
// which may or may not exist
1404-
const packagePath = resolvePath(dir, 'package.json');
1405-
// Try to read the possibly nonexistent package.json
1406-
const type = await readFile(packagePath, { encoding: 'utf8' })
1407-
.then((filestring) => JSON.parse(filestring).type)
1408-
.catch((err) => {
1409-
if (err?.code !== 'ENOENT') console.error(err);
1410-
});
1411-
// If package.json existed and contained a `type` field with a value, voilà
1412-
if (type) return type;
1413-
// Otherwise, (if not at the root) continue checking the next directory up
1414-
// If at the root, stop and return false
1415-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
1397+
const pJson = findPackageJSON(url);
1398+
1399+
return readFile(pJson, 'utf8')
1400+
.then(JSON.parse)
1401+
.then((json) => json?.type)
1402+
.catch(() => undefined);
14161403
}
14171404
```
14181405
14191406
##### Synchronous version
14201407
14211408
```mjs
14221409
// coffeescript-sync-hooks.mjs
1423-
import { readFileSync } from 'node:fs/promises';
1424-
import { registerHooks } from 'node:module';
1425-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1426-
import { cwd } from 'node:process';
1427-
import { fileURLToPath, pathToFileURL } from 'node:url';
1410+
import { readFileSync } from 'node:fs';
1411+
import { registerHooks, findPackageJSON } from 'node:module';
14281412
import coffeescript from 'coffeescript';
14291413

14301414
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
14311415

14321416
function load(url, context, nextLoad) {
14331417
if (extensionsRegex.test(url)) {
1434-
const format = getPackageType(url);
1435-
1436-
const { source: rawSource } = nextLoad(url, { ...context, format });
1418+
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
14371419
const transformedSource = coffeescript.compile(rawSource.toString(), url);
14381420

14391421
return {
1440-
format,
1422+
format: getPackageType(url),
14411423
shortCircuit: true,
14421424
source: transformedSource,
14431425
};
14441426
}
14451427

1446-
return nextLoad(url);
1428+
return nextLoad(url, context);
14471429
}
14481430

14491431
function getPackageType(url) {
1450-
const isFilePath = !!extname(url);
1451-
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
1452-
const packagePath = resolvePath(dir, 'package.json');
1453-
1454-
let type;
1432+
const pJson = findPackageJSON(url);
1433+
if (!pJson) {
1434+
return undefined;
1435+
}
14551436
try {
1456-
const filestring = readFileSync(packagePath, { encoding: 'utf8' });
1457-
type = JSON.parse(filestring).type;
1458-
} catch (err) {
1459-
if (err?.code !== 'ENOENT') console.error(err);
1437+
const file = readFileSync(pJson, 'utf-8');
1438+
return JSON.parse(file)?.type;
1439+
} catch {
1440+
return undefined;
14601441
}
1461-
if (type) return type;
1462-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
14631442
}
14641443

14651444
registerHooks({ load });
@@ -1481,6 +1460,21 @@ console.log "Brought to you by Node.js version #{version}"
14811460
export scream = (str) -> str.toUpperCase()
14821461
```
14831462
1463+
For the sake of running the example, add a `package.json` file containing the
1464+
module type of the CoffeeScript files.
1465+
1466+
```json
1467+
{
1468+
"type": "module"
1469+
}
1470+
```
1471+
1472+
This is only for running the example. In real world loaders, `getPackageType()` must be
1473+
able to return an `format` known to Node.js even in the absence of an explicit type in a
1474+
`package.json`, or otherwise the `nextLoad` call would throw `ERR_UNKNOWN_FILE_EXTENSION`
1475+
(if undefined) or `ERR_UNKNOWN_MODULE_FORMAT` (if it's not a known format listed in
1476+
the [load hook][] documentation).
1477+
14841478
With the preceding hooks modules, running
14851479
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
14861480
or `node --import ./coffeescript-sync-hooks.mjs ./main.coffee`

0 commit comments

Comments
 (0)