转译加载器
【Transpiler loader】
Node.js 无法理解的格式来源可以使用 load 钩子 转换为 JavaScript。然而,在调用该钩子之前,resolve 钩子 需要告诉 Node.js 不要对未知的文件类型抛出错误。
【Sources that are in formats Node.js doesn't understand can be converted into
JavaScript using the load hook. Before that hook gets called,
however, a resolve hook needs to tell Node.js not to
throw an error on unknown file types.】
这比在运行 Node.js 之前先转换源文件的性能要低;转换器加载器应该仅用于开发和测试目的。
【This is less performant than transpiling source files before running Node.js; a transpiler loader should only be used for development and testing purposes.】
// coffeescript-loader.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 CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${cwd()}/`).href;
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
export async function resolve(specifier, context, nextResolve) {
if (extensionsRegex.test(specifier)) {
const { parentURL = baseURL } = context;
// Node.js normally errors on unknown file extensions, so return a URL for
// specifiers ending in the CoffeeScript file extensions.
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
}
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. Because
// CoffeeScript transpiles into JavaScript, it should be one of the two
// JavaScript formats: 'commonjs' or 'module'.
// 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);
// When a hook returns a format of 'commonjs', `source` is be ignored.
// To handle CommonJS files, a handler needs to be registered with
// `require.extensions` in order to process the files with the CommonJS
// loader. Avoiding the need for a separate CommonJS handler is a future
// enhancement planned for ES module loaders.
if (format === 'commonjs') {
return {
format,
shortCircuit: true,
};
}
const { source: rawSource } = await nextLoad(url, { ...context, format });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeeCompile(rawSource.toString(), url);
return {
format,
shortCircuit: true,
source: transformedSource,
};
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
async function getPackageType(url) {
// `url` is only a file path during the first iteration when passed the
// resolved url from the load() hook
// an actual file path from load() will contain a file extension as it's
// required by the spec
// 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);
});
// Ff package.json existed and contained a `type` field with a value, voila
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, '..'));
} # main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'
import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}" # scream.coffee
export scream = (str) -> str.toUpperCase() 使用前述的加载器,运行
node --experimental-loader ./coffeescript-loader.mjs main.coffee
会在从磁盘加载源代码后但在 Node.js 执行前将 main.coffee 转换为 JavaScript;对于通过任何已加载文件的 import 语句引用的任何 .coffee、.litcoffee 或 .coffee.md 文件也是如此。
【With the preceding loader, running
node --experimental-loader ./coffeescript-loader.mjs main.coffee
causes main.coffee to be turned into JavaScript after its source code is
loaded from disk but before Node.js executes it; and so on for any .coffee,
.litcoffee or .coffee.md files referenced via import statements of any
loaded file.】