转译器加载器
可以使用 load
钩子将 Node.js 无法理解的格式的源代码转换为 JavaScript。
但是,在调用该钩子之前,resolve
钩子需要告诉 Node.js 不要在未知文件类型上抛出错误。
这比在运行 Node.js 之前转译源文件的性能要低;转译加载器应该只用于开发和测试目的。
// 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 文件以 .coffee、.litcoffee、或 .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 通常在未知文件扩展名上出错,
// 因此返回以 CoffeeScript 文件扩展名结尾的说明符的 URL。
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href
};
}
// 让 Node.js 处理所有其他说明符。
return nextResolve(specifier);
}
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// 现在我们修补了解析让 CoffeeScript URL 通过,
// 我们需要告诉 Node.js 这些 URL 应该被解释为什么格式。因为
// 因为 CoffeeScript 会转译成 JavaScript,
// 所以它应该是两种 JavaScript 格式之一:'commonjs' 或 'module'。
// CoffeeScript 文件可以是 CommonJS 或 ES 模块,
// 因此我们希望 Node.js 将任何 CoffeeScript 文件视为相同位置的 .js 文件。
// 要确定 Node.js 如何解释任意 .js 文件,
// 则在文件系统中搜索最近的父 package.json 文件
// 并读取其 "type" 字段。
const format = await getPackageType(url);
// 当钩子返回 'commonjs' 格式时,则 `source` 将被忽略。
// 为了处理 CommonJS 文件,需要使用 `require.extensions` 注册句柄,
// 以便使用 CommonJS 加载器处理文件。
// 避免需要单独的 CommonJS 处理程序
// 是 ES 模块加载器计划的未来增强功能。
if (format === 'commonjs') {
return {
format,
shortCircuit: true,
};
}
const { source: rawSource } = await nextLoad(url, { ...context, format });
// 此钩子将所有导入的 CoffeeScript 文件的 CoffeeScript 源代码
// 转换为的 JavaScript 源代码。
const transformedSource = coffeeCompile(rawSource.toString(), url);
return {
format,
shortCircuit: true,
source: transformedSource,
};
}
// 让 Node.js 处理所有其他 URL。
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);
// 如果是文件路径,则获取它所在的目录
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// 生成同一个目录下的 package.json 的文件路径,
// 文件可能存在也可能不存在
const packagePath = resolvePath(dir, 'package.json');
// 尝试读取可能不存在的 package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// 如果 package.json 存在并包含带有值的 `type` 字段
if (type) return type;
// 否则,(如果不在根目录下)继续检查下一个目录
// 如果在根目录,则停止并返回 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
会导致 main.coffee
在其源代码从磁盘加载之后但在 Node.js 执行之前转换为 JavaScript;对于通过任何加载文件的 import
语句引用的任何 .coffee
、.litcoffee
或 .coffee.md
文件,依此类推。
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.
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()
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.