宏是在构建时运行的 JavaScript 函数。宏返回的值将代替原始函数调用内联到包中。这允许你生成常量、代码,甚至其他资源,而无需任何自定义插件。

¥Macros are JavaScript functions that run at build time. The value returned by a macro is inlined into the bundle in place of the original function call. This allows you to generate constants, code, and even additional assets without any custom plugins.

使用 导入属性 导入宏以指示它们应该在构建时运行而不是打包到输出中。你可以将任何 JavaScript 或 TypeScript 模块作为宏导入,包括来自 npm 的内置 Node 模块和包。

¥Macros are imported using an import attribute to indicate that they should run at build time rather than being bundled into the output. You can import any JavaScript or TypeScript module as a macro, including built-in Node modules and packages from npm.

注意:出于安全原因,不能从 node_modules 内部调用宏。

¥Note: for security reasons, macros cannot be called from inside node_modules.

此示例使用 regexgen 库在构建时从一组字符串生成优化的正则表达式。

¥This example uses the regexgen library to generate an optimized regular expression from a set of strings at build time.

import regexgen from 'regexgen' with {type: 'macro'};

const regex = regexgen(['foobar', 'foobaz', 'foozap', 'fooza']);
console.log(regex);

这将编译为以下包:

¥This compiles to the following bundle:

console.log(/foo(?:zap?|ba[rz])/);

正如你所看到的,regexgen 库已经被完全编译掉了,我们只剩下一个静态正则表达式!

¥As you can see, the regexgen library has been completely compiled away, and we are left with a static regular expression!

参数

#

¥Arguments

宏参数是静态计算的,这意味着它们的值必须在构建时已知。你可以传递任何 JavaScript 字面量值,包括字符串、数字、布尔值、对象等。还支持字符串连接、算术和比较运算符等简单表达式。

¥Macro arguments are evaluated statically, which means their value must be known at build time. You can pass any JavaScript literal value, including strings, numbers, booleans, objects, etc. Simple expressions such as string concatenation, arithmetic, and comparison operators are supported as well.

import {myMacro} from './macro.ts' with {type: 'macro'};

const result = myMacro({
name: 'Devon'
});

但是,不支持引用非常量变量的值、调用宏以外的函数等。

¥However, values referencing non-constant variables, calling functions other than macros, etc. are not supported.

import {myMacro} from './macro.ts' with {type: 'macro'};

const result = myMacro({
name: getName() // Error: Cannot statically evaluate macro argument
});

常量

#

¥Constants

Parcel 还评估通过 const 关键字声明的常量。这些可以在宏参数中引用。

¥Parcel also evaluates constants declared via the const keyword. These may be referenced in a macro argument.

import {myMacro} from './macro.ts' with {type: 'macro'};

const name = 'Devon';
const result = myMacro({name});

一个宏的结果也可以传递给另一个宏。

¥The result of one macro may also be passed to another macro.

import {myMacro} from './macro.ts' with {type: 'macro'};
import {getName} from './name.ts' with {type: 'macro'};

const name = getName();
const result = myMacro({name});

但是,如果你尝试改变常量的值,则会导致错误。

¥However, if you attempt to mutate the value of a constant, this will result in an error.

import {myMacro} from './macro.ts' with {type: 'macro'};

const arg = {name: 'Devon'};
arg.name = 'Peter'; // Error: Cannot statically evaluate macro argument

const result = myMacro({name});

返回值

#

¥Return values

宏可以返回任何 JavaScript 值,包括对象、字符串、布尔值、数字,甚至函数。它们被转换为 AST 并替换代码中的原始函数调用。

¥Macros can return any JavaScript value, including objects, strings, booleans, numbers, and even functions. These are converted into an AST and replace the original function call in your code.

index.ts:
import {getRandomNumber} from './macro.ts' with {type: 'macro'};

console.log(getRandomNumber());
macro.ts:
export function getRandomNumber() {
return Math.random();
}

此示例的打包输出如下所示:

¥The bundled output of this example looks like this:

console.log(0.006024956627355804);

异步宏

#

¥Async macros

宏还可以返回解析为任何受支持值的 promise。例如,你可以发送 HTTP 请求以在构建时获取 URL 的内容,并将结果作为字符串内联到打包包中。

¥Macros can also return promises that resolve to any supported value. For example, you could make an HTTP request to fetch the contents of a URL at build time, and inline the result into the bundle as a string.

index.ts:
import {fetchText} from './macro.ts' with {type: 'macro'};

console.log(fetchText('http://example.com'));
macro.ts:
export async function fetchText(url: string) {
let res = await fetch(url);
return res.text();
}

生成函数

#

¥Generating functions

宏可以返回函数,这允许你在构建时生成代码。使用 new Function 构造函数从字符串动态生成函数。

¥Macros can return functions, which allows you to generate code at build time. Use the new Function constructor to generate a function dynamically from a string.

此示例使用 micromatch 库在构建时编译 glob 匹配函数。

¥This example uses the micromatch library to compile a glob matching function at build time.

index.ts:
import {compileGlob} from './glob.ts' with {type: 'macro'};

const isMatch = compileGlob('foo/**/bar.js');
glob.ts:
import micromatch from 'micromatch';

export function compileGlob(glob) {
let regex = micromatch.makeRe(glob);
return new Function('string', `return ${regex}.test(string)`);
}

此示例的打包输出如下所示:

¥The bundled output of this example looks like this:

const isMatch = function(string) {
return /^(?:foo(?:\/(?!\.)(?:(?:(?!(?:^|\/)\.).)*?)\/|\/|$)bar\.js)$/.test(string);
};

生成资源

#

¥Generating assets

宏可以生成额外的资源,这些资源成为调用它的 JavaScript 模块的依赖。例如,宏可以生成 CSS,该 CSS 将被静态提取到 CSS 包中,就像从 JS 文件导入一样。

¥A macro can generate additional assets which become dependencies of the JavaScript module that called it. For example, macros can generate CSS which will be statically extracted into a CSS bundle as if it was imported from the JS file.

在宏函数中,this 是一个具有 Parcel 提供的方法的对象。要创建资源,请调用 this.addAsset 并提供类型和内容。

¥Within a macro function, this is an object with Parcel-provided methods. To create an asset, call this.addAsset and provide the type and contents.

此示例接受 CSS 字符串并返回生成的类名。CSS 作为资源添加并打包到 CSS 文件中,而 JavaScript 打包包仅包含生成的类名称作为静态字符串。

¥This example accepts a string of CSS and returns a generated class name. The CSS is added as an asset and bundled into a CSS file, and the JavaScript bundle only includes the generated class name as a static string.

index.ts:
import {css} from './css.ts' with {type: 'macro'};

<div className={css('color: red; &:hover { color: green }')}>
Hello!
</div>
css.ts:
import type {MacroContext} from '@parcel/macros';

export async function css(this: MacroContext | void, code: string) {
let className = hash(code);
code = `.${className} { ${code} }`;

this?.addAsset({
type: 'css',
content: code
});

return className;
}

上面示例的打包输出如下所示:

¥The bundled output of the above example would look like this:

index.js:
<div className="ax63jk4">
Hello!
</div>
index.css:
.ax63jk4 {
color: red;
&:hover {
color: green;
}
}

缓存

#

¥Caching

默认情况下,Parcel 会缓存宏的结果,直到调用它的文件发生更改。然而,有时,宏可能有其他输入,这些输入会使缓存失效。例如,它可能读取文件、访问环境变量等。宏函数中的 this 上下文包括控制缓存行为的方法。

¥By default, Parcel caches the result of a macro until the file that calls it changes. However, sometimes, a macro may have other inputs which should invalidate the cache. For example, it might read a file, access an environment variable, etc. The this context within a macro function includes methods to control the caching behavior.

interface MacroContext {
/** Invalidate the macro call whenever the given file changes. */
invalidateOnFileChange(filePath: string): void,
/** Invalidate the macro call when a file matching the given pattern is created. */
invalidateOnFileCreate(options: FileCreateInvalidation): void,
/** Invalidate the macro whenever the given environment variable changes. */
invalidateOnEnvChange(env: string): void,
/** Invalidate the macro whenever Parcel restarts. */
invalidateOnStartup(): void,
/** Invalidate the macro on every build. */
invalidateOnBuild(): void,
}

type FileCreateInvalidation = FileInvalidation | GlobInvalidation | FileAboveInvalidation;

/** Invalidate when a file matching a glob is created. */
interface GlobInvalidation {
glob: string
}

/** Invalidate when a specific file is created. */
interface FileInvalidation {
filePath: string
}

/** Invalidate when a file of a specific name is created above a certain directory in the hierarchy. */
interface FileAboveInvalidation {
fileName: string,
aboveFilePath: string
}

例如,在宏中读取文件时,将文件路径添加为无效,以便每当该文件发生更改时都会重新编译调用代码。在本例中,每当编辑 message.txt 时,都会重新编译 index.ts,并再次调用 readFile 宏。

¥For example, when reading a file in a macro, add the file path as an invalidation so that the calling code is recompiled whenever that file changes. In this example, whenever message.txt is edited, index.ts will be recompiled and the readFile macro will be called again.

index.ts:
import {readFile} from './macro.ts' with {type: 'macro'};

console.log(readFile('message.txt'))
macro.ts:
import type {MacroContext} from '@parcel/macros';
import fs from 'fs';

export async function readFile(this: MacroContext | void, filePath: string) {
this?.invalidateOnFileChange(filePath);
return fs.readFileSync(filePath, 'utf8');
}
message.txt:
hello world!

与其他工具一起使用

#

¥Usage with other tools

宏只是普通的 JavaScript 函数,因此它们可以轻松地与其他工具集成。

¥Macros are just normal JavaScript functions, so they integrate with other tools easily.

TypeScript

#

从版本 5.3 开始,TypeScript 支持开箱即用的导入属性,并且宏的自动补齐和类型的工作方式就像常规函数一样。

¥TypeScript supports import attributes out of the box as of version 5.3, and autocomplete and types for macros work just like regular functions.

Babel

#

@babel/plugin-syntax-import-attributes 插件使 Babel 能够解析导入属性。如果你使用的是 @babel/preset-env,启用 shippedProposals 选项还可以解析导入属性。

¥The @babel/plugin-syntax-import-attributes plugin enables Babel to parse import attributes. If you're using @babel/preset-env, enabling the shippedProposals option also enables import attributes to be parsed.

babel.config.json:
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true
}
]
]
}

ESLint

#

当使用支持导入属性的解析器(例如 Babel 或 TypeScript)时,ESLint 支持导入属性。

¥ESLint supports import attributes when using a parser such as Babel or TypeScript that supports them.

.eslintrc.js:
module.exports = {
parser: '@typescript-eslint/parser'
};

单元测试

#

¥Unit testing

单元测试宏就像测试任何其他 JavaScript 函数一样。需要注意的是,如果你的宏使用上述部分中描述的 this 上下文。如果你正在测试宏本身,则可以模拟 this 参数以验证它是否按预期调用。

¥Unit testing macros is just like testing any other JavaScript function. One caveat is if your macro uses the this context described in the above sections. If you are testing a macro itself, you can mock the this argument to verify it is called as expected.

css.test.ts:
import {css} from '../src/css.ts';

it('should generate css', () => {
let addAsset = jest.fn();
let className = css.call({
addAsset,
// ...
}, 'color: red');

expect(addAsset).toHaveBeenCalledWith({
type: 'css',
content: '.ax63jk4 { color: red }'
});
expect(className).toBe('ax63jk4');
});

当测试间接使用宏的代码时,宏函数将在运行时作为普通函数调用,而不是在编译时由 Parcel 调用。在这种情况下,通常由 Parcel 提供的宏上下文将不可用。这就是为什么 this 参数在上面的示例中被键入为 MacroContext | void,并且我们进行运行时检查以查看 this 是否存在。当上下文不可用时,使用它的代码(例如 this?.addAsset)将不会运行,但该函数应正常返回一个值。

¥When testing code that indirectly uses a macro, the macro function will be called as a normal function at runtime rather than by Parcel at compile time. In this case, the macro context that would normally be provided by Parcel won't be available. That's why the this argument is typed as MacroContext | void in the above examples and we do a runtime check to see if this exists. When the context isn't available, code that uses it such as this?.addAsset won't run, but the function should return a value as normal.

与 Bun 的区别

#

¥Differences from Bun

通过导入属性的宏最初是在 Bun 中实现的。Parcel 的实现大部分与 Bun 的宏 API 兼容,但也有一些区别:

¥Macros via import attributes were originally implemented in Bun. Parcel's implementation is compatible with Bun's macro API for the most part, but there are a few differences:

Parcel 中文网 - 粤ICP备13048890号