@@ -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
13641368import { 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' ;
13681370import coffeescript from ' coffeescript' ;
13691371
13701372const extensionsRegex = / \. (coffee| litcoffee| coffee\. md)$ / ;
13711373
13721374export 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
13971397async 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' ;
14361420import coffeescript from ' coffeescript' ;
14371421
14381422const extensionsRegex = / \. (coffee| litcoffee| coffee\. md)$ / ;
14391423
14401424function 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
14571439function 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
14731452registerHooks ({ load });
@@ -1489,6 +1468,21 @@ console.log "Brought to you by Node.js version #{version}"
14891468export 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+
14921486With 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`
14941488or ` node --import ./coffeescript-sync-hooks.mjs ./main.coffee`
0 commit comments