巨集

巨集是會在建置時執行的 JavaScript 函式。巨集傳回的值會內嵌到套件中,取代原本的函式呼叫。這讓你可以產生常數、程式碼,甚至額外的資源,而不需要任何自訂的外掛。

巨集會使用 import 屬性 來匯入,用以表示它們應該在建置時執行,而不是被套件到輸出的檔案中。你可以匯入任何 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,並取代程式碼中原本的函式呼叫。

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

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

這個範例的捆綁輸出看起來像這樣

console.log(0.006024956627355804);

非同步巨集

#

巨集也可以傳回解決為任何支援值的承諾。例如,你可以在建置時對 URL 內容發出 HTTP 要求,並將結果內嵌到捆綁中,作為字串。

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();
}

產生函式

#

巨集可以傳回函式,這允許你在建置時產生程式碼。使用 new Function 建構函式,從字串動態產生函式。

這個範例使用 micromatch 函式庫,在建置時編譯 glob 匹配函式。

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)`);
}

這個範例的捆綁輸出看起來像這樣

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

產生資源

#

巨集可以產生額外的資源,這些資源會成為呼叫它的 JavaScript 模組的相依性。例如,巨集可以產生 CSS,這些 CSS 會靜態萃取到 CSS 捆綁中,就像從 JS 檔案匯入一樣。

在巨集函式中,this 是包含 Parcel 提供方法的物件。若要建立資源,請呼叫 this.addAsset 並提供類型和內容。

這個範例接受 CSS 字串並傳回產生的類別名稱。CSS 會新增為資源並捆綁到 CSS 檔案中,而 JavaScript 捆綁只包含產生的類別名稱,作為靜態字串。

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;
}

上述範例的捆綁輸出看起來像這樣

index.js
<div className="ax63jk4">
Hello!
</div>
index.css
.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 巨集。

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!

與其他工具一起使用

#

巨集只是正常的 JavaScript 函式,因此它們可以輕鬆地與其他工具整合。

TypeScript

#

從 5.3 版開始,TypeScript 支援匯入屬性,且巨集的自動完成和類型就像一般函式一樣運作。

Babel

#

@babel/plugin-syntax-import-attributes 外掛程式讓 Babel 可以剖析匯入屬性。如果您使用 @babel/preset-env,啟用 shippedProposals 選項也會啟用剖析匯入屬性。

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

ESLint

#

ESLint 在使用支援匯入屬性的剖析器(例如 Babel 或 TypeScript)時支援匯入屬性。

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

單元測試

#

單元測試巨集就像測試任何其他 JavaScript 函式一樣。一個注意事項是,如果您的巨集使用上述章節中所述的 this 語境。如果您要測試巨集本身,您可以模擬 this 參數,以驗證它是否如預期般被呼叫。

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)不會執行,但函式應該如常傳回一個值。

與 Bun 的差異

#

透過匯入屬性進行巨集處理最初是在 Bun 中實作的。Parcel 的實作大部分與 Bun 的巨集 API 相容,但有一些差異