Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: fix transpiler loader hooks documentation #57037

Merged
merged 1 commit into from
Feb 16, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 48 additions & 54 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,11 @@ changes:
Node.js default `load` hook after the last user-supplied `load` hook
* `url` {string}
* `context` {Object|undefined} When omitted, defaults are provided. When provided, defaults are
merged in with preference to the provided properties.
merged in with preference to the provided properties. In the default `nextLoad`, if
the module pointed to by `url` does not have explicit module type information,
`context.format` is mandatory.
<!-- TODO(joyeecheung): make it at least optionally non-mandatory by allowing
JS-style/TS-style module detection when the format is simply unknown -->
* Returns: {Object|Promise} The asynchronous version takes either an object containing the
following properties, or a `Promise` that will resolve to such an object. The
synchronous version only accepts an object returned synchronously.
Expand Down Expand Up @@ -1362,36 +1366,32 @@ transpiler hooks should only be used for development and testing purposes.
```mjs
// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

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

export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// CoffeeScript files can be either CommonJS or ES modules, so we want any
// CoffeeScript file to be treated by Node.js the same as a .js file at the
// same location. To determine how Node.js would interpret an arbitrary .js
// file, search up the file system for the nearest parent package.json file
// and read its "type" field.
const format = await getPackageType(url);

const { source: rawSource } = await nextLoad(url, { ...context, format });
// CoffeeScript files can be either CommonJS or ES modules. Use a custom format
// to tell Node.js not to detect its module type.
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeescript.compile(rawSource.toString(), url);

// To determine how Node.js would interpret the transpilation result,
// search up the file system for the nearest parent package.json file
// and read its "type" field.
return {
format,
format: await getPackageType(url),
shortCircuit: true,
source: transformedSource,
};
}

// Let Node.js handle all other URLs.
return nextLoad(url);
return nextLoad(url, context);
}

async function getPackageType(url) {
Expand All @@ -1402,72 +1402,51 @@ async function getPackageType(url) {
// this simple truthy check for whether `url` contains a file extension will
// work for most projects but does not cover some edge-cases (such as
// extensionless files or a url ending in a trailing space)
const isFilePath = !!extname(url);
// If it is a file path, get the directory it's in
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// Compose a file path to a package.json in the same directory,
// which may or may not exist
const packagePath = resolvePath(dir, 'package.json');
// Try to read the possibly nonexistent package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// If package.json existed and contained a `type` field with a value, voilà
if (type) return type;
// Otherwise, (if not at the root) continue checking the next directory up
// If at the root, stop and return false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
const pJson = findPackageJSON(url);

return readFile(pJson, 'utf8')
.then(JSON.parse)
.then((json) => json?.type)
.catch(() => undefined);
}
```

##### Synchronous version

```mjs
// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs/promises';
import { registerHooks } from 'node:module';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readFileSync } from 'node:fs';
import { registerHooks, findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

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

function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
const format = getPackageType(url);

const { source: rawSource } = nextLoad(url, { ...context, format });
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
const transformedSource = coffeescript.compile(rawSource.toString(), url);

return {
format,
format: getPackageType(url),
shortCircuit: true,
source: transformedSource,
};
}

return nextLoad(url);
return nextLoad(url, context);
}

function getPackageType(url) {
const isFilePath = !!extname(url);
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
const packagePath = resolvePath(dir, 'package.json');

let type;
const pJson = findPackageJSON(url);
if (!pJson) {
return undefined;
}
try {
const filestring = readFileSync(packagePath, { encoding: 'utf8' });
type = JSON.parse(filestring).type;
} catch (err) {
if (err?.code !== 'ENOENT') console.error(err);
const file = readFileSync(pJson, 'utf-8');
return JSON.parse(file)?.type;
} catch {
return undefined;
}
if (type) return type;
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}

registerHooks({ load });
Expand All @@ -1489,6 +1468,21 @@ console.log "Brought to you by Node.js version #{version}"
export scream = (str) -> str.toUpperCase()
```

For the sake of running the example, add a `package.json` file containing the
module type of the CoffeeScript files.

```json
{
"type": "module"
}
```
Comment on lines +1471 to +1478
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ovflowd in the incoming new API docs, will we have access to the awesome multi-file (tabbed) code samples like we do in Learn articles?


This is only for running the example. In real world loaders, `getPackageType()` must be
able to return an `format` known to Node.js even in the absence of an explicit type in a
`package.json`, or otherwise the `nextLoad` call would throw `ERR_UNKNOWN_FILE_EXTENSION`
(if undefined) or `ERR_UNKNOWN_MODULE_FORMAT` (if it's not a known format listed in
the [load hook][] documentation).

With the preceding hooks modules, running
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
or `node --import ./coffeescript-sync-hooks.mjs ./main.coffee`
Expand Down
Loading