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

1 file changed

Lines changed: 48 additions & 54 deletions

File tree

‎doc/api/module.md‎

Lines changed: 48 additions & 54 deletions
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, resolveasresolvePath } from'node:path';
1358-
import { cwd } from'node:process';
1359-
import { fileURLToPath, pathToFileURL } from'node:url';
1361+
import { findPackageJSON } from'node:module';
13601362
importcoffeescriptfrom'coffeescript';
13611363

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

13641366
exportasyncfunctionload(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-
constformat=awaitgetPackageType(url);
1372-
1373-
const { source:rawSource } =awaitnextLoad(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 } =awaitnextLoad(url, { ...context, format:'coffee' });
13741371
// This hook converts CoffeeScript source code into JavaScript source code
13751372
// for all imported CoffeeScript files.
13761373
consttransformedSource=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:awaitgetPackageType(url),
13801380
shortCircuit:true,
13811381
source: transformedSource,
13821382
};
13831383
}
13841384

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

13891389
asyncfunctiongetPackageType(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-
constisFilePath=!!extname(url);
1398-
// If it is a file path, get the directory it's in
1399-
constdir= 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-
constpackagePath=resolvePath(dir, 'package.json');
1405-
// Try to read the possibly nonexistent package.json
1406-
consttype=awaitreadFile(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-
returndir.length>1&&getPackageType(resolvePath(dir, '..'));
1397+
constpJson=findPackageJSON(url);
1398+
1399+
returnreadFile(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, resolveasresolvePath } 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
importcoffeescriptfrom'coffeescript';
14291413

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

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

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

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

14491431
functiongetPackageType(url) {
1450-
constisFilePath=!!extname(url);
1451-
constdir= isFilePath ?dirname(fileURLToPath(url)) : url;
1452-
constpackagePath=resolvePath(dir, 'package.json');
1453-
1454-
let type;
1432+
constpJson=findPackageJSON(url);
1433+
if (!pJson) {
1434+
returnundefined;
1435+
}
14551436
try {
1456-
constfilestring=readFileSync(packagePath, { encoding:'utf8' });
1457-
type =JSON.parse(filestring).type;
1458-
} catch(err) {
1459-
if (err?.code!=='ENOENT') console.error(err);
1437+
constfile=readFileSync(pJson, 'utf-8');
1438+
returnJSON.parse(file)?.type;
1439+
} catch {
1440+
returnundefined;
14601441
}
1461-
if (type) return type;
1462-
returndir.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
exportscream= (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)