宏
宏是在构建时运行的 JavaScript 函数。宏返回的值将内联到捆绑包中,以替换原始函数调用。这允许您生成常量、代码,甚至额外的资产,而无需任何自定义插件。
宏使用 导入属性 导入,以指示它们应该在构建时运行,而不是捆绑到输出中。您可以将任何 JavaScript 或 TypeScript 模块作为宏导入,包括内置的 Node 模块和来自 npm 的包。
注意:出于安全原因,宏不能从 node_modules
内部调用。
此示例使用 regexgen 库在构建时从一组字符串生成优化的正则表达式。
import regexgen from 'regexgen' with {type: 'macro'};
const regex = regexgen(['foobar', 'foobaz', 'foozap', 'fooza']);
console.log(regex);
这将编译成以下捆绑包
console.log(/foo(?:zap?|ba[rz])/);
如您所见,regexgen
库已被完全编译掉,我们只剩下一个静态正则表达式!
参数
#宏参数是静态评估的,这意味着它们的值必须在构建时已知。您可以传递任何 JavaScript 字面量值,包括字符串、数字、布尔值、对象等。简单的表达式,例如字符串连接、算术和比较运算符也受支持。
import {myMacro} from './macro.ts' with {type: 'macro'};
const result = myMacro({
name: 'Devon'
});
但是,引用非常量变量、调用除宏以外的函数等的值不受支持。
import {myMacro} from './macro.ts' with {type: 'macro'};
const result = myMacro({
name: getName() // Error: Cannot statically evaluate macro argument
});
常量
#Parcel 还评估通过 const
关键字声明的常量。这些可以在宏参数中引用。
import {myMacro} from './macro.ts' with {type: 'macro'};
const name = 'Devon';
const result = myMacro({name});
一个宏的结果也可以传递给另一个宏。
import {myMacro} from './macro.ts' with {type: 'macro'};
import {getName} from './name.ts' with {type: 'macro'};
const name = getName();
const result = myMacro({name});
但是,如果您尝试修改常量的值,这将导致错误。
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});
返回值
#宏可以返回任何 JavaScript 值,包括对象、字符串、布尔值、数字,甚至函数。这些被转换为 AST 并替换您代码中的原始函数调用。
import {getRandomNumber} from './macro.ts' with {type: 'macro'};
console.log(getRandomNumber());
export function getRandomNumber() {
return Math.random();
}
此示例的捆绑输出如下所示
console.log(0.006024956627355804);
异步宏
#宏也可以返回解析为任何支持值的 Promise。例如,您可以在构建时发出 HTTP 请求以获取 URL 内容,并将结果作为字符串内联到捆绑包中。
import {fetchText} from './macro.ts' with {type: 'macro'};
console.log(fetchText('http://example.com'));
export async function fetchText(url: string) {
let res = await fetch(url);
return res.text();
}
生成函数
#宏可以返回函数,这允许您在构建时生成代码。使用 new Function
构造函数从字符串动态生成函数。
此示例使用 micromatch 库在构建时编译 glob 匹配函数。
import {compileGlob} from './glob.ts' with {type: 'macro'};
const isMatch = compileGlob('foo/**/bar.js');
import micromatch from 'micromatch';
export function compileGlob(glob) {
let regex = micromatch.makeRe(glob);
return new Function('string', `return ${regex}.test(string)`);
}
此示例的捆绑输出如下所示
const isMatch = function(string) {
return /^(?:foo(?:\/(?!\.)(?:(?:(?!(?:^|\/)\.).)*?)\/|\/|$)bar\.js)$/.test(string);
};
生成资产
#宏可以生成额外的资产,这些资产成为调用它的 JavaScript 模块的依赖项。例如,宏可以生成 CSS,这些 CSS 将被静态提取到 CSS 捆绑包中,就像它从 JS 文件导入一样。
在宏函数中,this
是一个包含 Parcel 提供的方法的对象。要创建资产,请调用 this.addAsset
并提供类型和内容。
此示例接受一个 CSS 字符串并返回一个生成的类名。CSS 作为资产添加并捆绑到 CSS 文件中,而 JavaScript 捆绑包只包含生成的类名作为静态字符串。
import {css} from './css.ts' with {type: 'macro'};
<div className={css('color: red; &:hover { color: green }')}>
Hello!
</div>
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;
}
上面示例的捆绑输出如下所示
<div className="ax63jk4">
Hello!
</div>
.ax63jk4 {
color: red;
&:hover {
color: green;
}
}
缓存
#默认情况下,Parcel 会缓存宏的结果,直到调用它的文件发生更改。但是,有时,宏可能还有其他输入,这些输入应该使缓存失效。例如,它可能读取文件、访问环境变量等。宏函数中的 this
上下文包含用于控制缓存行为的方法。
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
宏将再次被调用。
import {readFile} from './macro.ts' with {type: 'macro'};
console.log(readFile('message.txt'))
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');
}
hello world!
与其他工具一起使用
#宏只是普通的 JavaScript 函数,因此它们可以轻松地与其他工具集成。
TypeScript
#TypeScript 从版本 5.3 开始支持导入属性,宏的自动完成和类型与普通函数一样。
Babel
#@babel/plugin-syntax-import-attributes
插件使 Babel 能够解析导入属性。如果您使用的是 @babel/preset-env
,启用 shippedProposals
选项也会使导入属性能够被解析。
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true
}
]
]
}
ESLint
#ESLint 在使用支持导入属性的解析器(如 Babel 或 TypeScript)时支持导入属性。
module.exports = {
parser: '@typescript-eslint/parser'
};
单元测试
#单元测试宏就像测试任何其他 JavaScript 函数一样。一个注意事项是,如果您的宏使用上面部分中描述的 this
上下文。如果您正在测试宏本身,您可以模拟 this
参数以验证它是否按预期调用。
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
)将不会运行,但函数应该像往常一样返回一个值。
与 Bun 的区别
#通过导入属性的宏最初是在 Bun 中实现的。Parcel 的实现与 Bun 的宏 API 大致兼容,但有一些区别
- Parcel 支持从宏返回函数。
- Parcel 支持宏中的
this
上下文,以启用生成资产和控制缓存行为。 - Parcel 目前不支持 Bun 的类型化数组、fetch
Response
对象或Blob
对象的特殊情况返回值。您需要在从宏返回之前将它们转换为字符串。 - Parcel 目前不支持
"macro"
package.jsonexports
条件。