Skip to content

Commit e892e79

Browse files
authored
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 1d8593e commit e892e79

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
@@ -1164,7 +1164,11 @@ changes:
11641164
Node.js default `load` hook after the last user-supplied `load` hook
11651165
* `url` {string}
11661166
* `context` {Object|undefined} When omitted, defaults are provided. When provided, defaults are
1167-
merged in with preference to the provided properties.
1167+
merged in with preference to the provided properties. In the default `nextLoad`, if
1168+
the module pointed to by `url` does not have explicit module type information,
1169+
`context.format` is mandatory.
1170+
<!-- TODO(joyeecheung): make it at least optionally non-mandatory by allowing
1171+
JS-style/TS-style module detection when the format is simply unknown -->
11681172
* Returns: {Object|Promise} The asynchronous version takes either an object containing the
11691173
following properties, or a `Promise` that will resolve to such an object. The
11701174
synchronous version only accepts an object returned synchronously.
@@ -1362,36 +1366,32 @@ transpiler hooks should only be used for development and testing purposes.
13621366
```mjs
13631367
// coffeescript-hooks.mjs
13641368
import { readFile } from 'node:fs/promises';
1365-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1366-
import { cwd } from 'node:process';
1367-
import { fileURLToPath, pathToFileURL } from 'node:url';
1369+
import { findPackageJSON } from 'node:module';
13681370
import coffeescript from 'coffeescript';
13691371

13701372
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
13711373

13721374
export async function load(url, context, nextLoad) {
13731375
if (extensionsRegex.test(url)) {
1374-
// CoffeeScript files can be either CommonJS or ES modules, so we want any
1375-
// CoffeeScript file to be treated by Node.js the same as a .js file at the
1376-
// same location. To determine how Node.js would interpret an arbitrary .js
1377-
// file, search up the file system for the nearest parent package.json file
1378-
// and read its "type" field.
1379-
const format = await getPackageType(url);
1380-
1381-
const { source: rawSource } = await nextLoad(url, { ...context, format });
1376+
// CoffeeScript files can be either CommonJS or ES modules. Use a custom format
1377+
// to tell Node.js not to detect its module type.
1378+
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
13821379
// This hook converts CoffeeScript source code into JavaScript source code
13831380
// for all imported CoffeeScript files.
13841381
const transformedSource = coffeescript.compile(rawSource.toString(), url);
13851382

1383+
// To determine how Node.js would interpret the transpilation result,
1384+
// search up the file system for the nearest parent package.json file
1385+
// and read its "type" field.
13861386
return {
1387-
format,
1387+
format: await getPackageType(url),
13881388
shortCircuit: true,
13891389
source: transformedSource,
13901390
};
13911391
}
13921392

13931393
// Let Node.js handle all other URLs.
1394-
return nextLoad(url);
1394+
return nextLoad(url, context);
13951395
}
13961396

13971397
async function getPackageType(url) {
@@ -1402,72 +1402,51 @@ async function getPackageType(url) {
14021402
// this simple truthy check for whether `url` contains a file extension will
14031403
// work for most projects but does not cover some edge-cases (such as
14041404
// extensionless files or a url ending in a trailing space)
1405-
const isFilePath = !!extname(url);
1406-
// If it is a file path, get the directory it's in
1407-
const dir = isFilePath ?
1408-
dirname(fileURLToPath(url)) :
1409-
url;
1410-
// Compose a file path to a package.json in the same directory,
1411-
// which may or may not exist
1412-
const packagePath = resolvePath(dir, 'package.json');
1413-
// Try to read the possibly nonexistent package.json
1414-
const type = await readFile(packagePath, { encoding: 'utf8' })
1415-
.then((filestring) => JSON.parse(filestring).type)
1416-
.catch((err) => {
1417-
if (err?.code !== 'ENOENT') console.error(err);
1418-
});
1419-
// If package.json existed and contained a `type` field with a value, voilà
1420-
if (type) return type;
1421-
// Otherwise, (if not at the root) continue checking the next directory up
1422-
// If at the root, stop and return false
1423-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
1405+
const pJson = findPackageJSON(url);
1406+
1407+
return readFile(pJson, 'utf8')
1408+
.then(JSON.parse)
1409+
.then((json) => json?.type)
1410+
.catch(() => undefined);
14241411
}
14251412
```
14261413
14271414
##### Synchronous version
14281415
14291416
```mjs
14301417
// coffeescript-sync-hooks.mjs
1431-
import { readFileSync } from 'node:fs/promises';
1432-
import { registerHooks } from 'node:module';
1433-
import { dirname, extname, resolve as resolvePath } from 'node:path';
1434-
import { cwd } from 'node:process';
1435-
import { fileURLToPath, pathToFileURL } from 'node:url';
1418+
import { readFileSync } from 'node:fs';
1419+
import { registerHooks, findPackageJSON } from 'node:module';
14361420
import coffeescript from 'coffeescript';
14371421

14381422
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
14391423

14401424
function load(url, context, nextLoad) {
14411425
if (extensionsRegex.test(url)) {
1442-
const format = getPackageType(url);
1443-
1444-
const { source: rawSource } = nextLoad(url, { ...context, format });
1426+
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
14451427
const transformedSource = coffeescript.compile(rawSource.toString(), url);
14461428

14471429
return {
1448-
format,
1430+
format: getPackageType(url),
14491431
shortCircuit: true,
14501432
source: transformedSource,
14511433
};
14521434
}
14531435

1454-
return nextLoad(url);
1436+
return nextLoad(url, context);
14551437
}
14561438

14571439
function getPackageType(url) {
1458-
const isFilePath = !!extname(url);
1459-
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
1460-
const packagePath = resolvePath(dir, 'package.json');
1461-
1462-
let type;
1440+
const pJson = findPackageJSON(url);
1441+
if (!pJson) {
1442+
return undefined;
1443+
}
14631444
try {
1464-
const filestring = readFileSync(packagePath, { encoding: 'utf8' });
1465-
type = JSON.parse(filestring).type;
1466-
} catch (err) {
1467-
if (err?.code !== 'ENOENT') console.error(err);
1445+
const file = readFileSync(pJson, 'utf-8');
1446+
return JSON.parse(file)?.type;
1447+
} catch {
1448+
return undefined;
14681449
}
1469-
if (type) return type;
1470-
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
14711450
}
14721451

14731452
registerHooks({ load });
@@ -1489,6 +1468,21 @@ console.log "Brought to you by Node.js version #{version}"
14891468
export scream = (str) -> str.toUpperCase()
14901469
```
14911470
1471+
For the sake of running the example, add a `package.json` file containing the
1472+
module type of the CoffeeScript files.
1473+
1474+
```json
1475+
{
1476+
"type": "module"
1477+
}
1478+
```
1479+
1480+
This is only for running the example. In real world loaders, `getPackageType()` must be
1481+
able to return an `format` known to Node.js even in the absence of an explicit type in a
1482+
`package.json`, or otherwise the `nextLoad` call would throw `ERR_UNKNOWN_FILE_EXTENSION`
1483+
(if undefined) or `ERR_UNKNOWN_MODULE_FORMAT` (if it's not a known format listed in
1484+
the [load hook][] documentation).
1485+
14921486
With the preceding hooks modules, running
14931487
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
14941488
or `node --import ./coffeescript-sync-hooks.mjs ./main.coffee`

0 commit comments

Comments
 (0)