269 lines
10 KiB
TypeScript
269 lines
10 KiB
TypeScript
import { escapeHTML } from "bun" assert { type: "macro" };
|
|
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDir } from "harness";
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import defaultMacro, {
|
|
addStrings,
|
|
addStringsUTF16,
|
|
default as defaultMacroAlias,
|
|
escape,
|
|
identity,
|
|
identity as identity1,
|
|
identity as identity2,
|
|
ireturnapromise,
|
|
} from "./macro.ts" assert { type: "macro" };
|
|
|
|
import * as macros from "./macro.ts" assert { type: "macro" };
|
|
|
|
test("bun builtins can be used in macros", async () => {
|
|
expect(escapeHTML("abc!")).toBe("abc!");
|
|
});
|
|
|
|
test("latin1 string", () => {
|
|
expect(identity("©")).toBe("©");
|
|
});
|
|
|
|
test("ascii string", () => {
|
|
expect(identity("abc")).toBe("abc");
|
|
});
|
|
|
|
test("type coercion", () => {
|
|
expect(identity({ a: 1 })).toEqual({ a: 1 });
|
|
expect(identity([1, 2, 3])).toEqual([1, 2, 3]);
|
|
expect(identity(undefined)).toBe(undefined);
|
|
expect(identity(null)).toBe(null);
|
|
expect(identity(1.5)).toBe(1.5);
|
|
expect(identity(1)).toBe(1);
|
|
expect(identity(true)).toBe(true);
|
|
});
|
|
|
|
test("escaping", () => {
|
|
expect(identity("\\")).toBe("\\");
|
|
expect(identity("\f")).toBe("\f");
|
|
expect(identity("\n")).toBe("\n");
|
|
expect(identity("\r")).toBe("\r");
|
|
expect(identity("\t")).toBe("\t");
|
|
expect(identity("\v")).toBe("\v");
|
|
expect(identity("\0")).toBe("\0");
|
|
expect(identity("'")).toBe("'");
|
|
expect(identity('"')).toBe('"');
|
|
expect(identity("`")).toBe("`");
|
|
// prettier-ignore
|
|
expect(identity("\'")).toBe("\'");
|
|
// prettier-ignore
|
|
expect(identity('\"')).toBe('\"');
|
|
// prettier-ignore
|
|
expect(identity("\`")).toBe("\`");
|
|
expect(identity("$")).toBe("$");
|
|
expect(identity("\x00")).toBe("\x00");
|
|
expect(identity("\x0B")).toBe("\x0B");
|
|
expect(identity("\x0C")).toBe("\x0C");
|
|
|
|
expect(identity("\\")).toBe("\\");
|
|
|
|
expect(escape()).toBe("\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C");
|
|
|
|
expect(addStrings("abc")).toBe("abc\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\n")).toBe("\n\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\r")).toBe("\r\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\t")).toBe("\t\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("©")).toBe("©\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\x00")).toBe("\x00\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\x0B")).toBe("\x0B\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\x0C")).toBe("\x0C\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\\")).toBe("\\\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\f")).toBe("\f\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\v")).toBe("\v\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("\0")).toBe("\0\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("'")).toBe("'\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings('"')).toBe('"\\\f\n\r\t\v\0\'"`$\x00\x0B\x0C©');
|
|
expect(addStrings("`")).toBe("`\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
expect(addStrings("😊")).toBe("😊\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C©");
|
|
|
|
expect(addStringsUTF16("abc")).toBe("abc\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\n")).toBe("\n\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\r")).toBe("\r\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\t")).toBe("\t\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("©")).toBe("©\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\x00")).toBe("\x00\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\x0B")).toBe("\x0B\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\x0C")).toBe("\x0C\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\\")).toBe("\\\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\f")).toBe("\f\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\v")).toBe("\v\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("\0")).toBe("\0\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("'")).toBe("'\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16('"')).toBe('"\\\f\n\r\t\v\0\'"`$\x00\x0B\x0C😊');
|
|
expect(addStringsUTF16("`")).toBe("`\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
expect(addStringsUTF16("😊")).toBe("😊\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C😊");
|
|
});
|
|
|
|
test("utf16 string", () => {
|
|
expect(identity("😊 Smiling Face with Smiling Eyes Emoji")).toBe("😊 Smiling Face with Smiling Eyes Emoji");
|
|
});
|
|
|
|
test("import aliases", () => {
|
|
expect(identity1({ a: 1 })).toEqual({ a: 1 });
|
|
expect(identity1([1, 2, 3])).toEqual([1, 2, 3]);
|
|
expect(identity2({ a: 1 })).toEqual({ a: 1 });
|
|
expect(identity2([1, 2, 3])).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("default import", () => {
|
|
expect(defaultMacro()).toBe("defaultdefaultdefault");
|
|
expect(defaultMacroAlias()).toBe("defaultdefaultdefault");
|
|
});
|
|
|
|
test("namespace import", () => {
|
|
expect(macros.identity({ a: 1 })).toEqual({ a: 1 });
|
|
expect(macros.identity([1, 2, 3])).toEqual([1, 2, 3]);
|
|
expect(macros.escape()).toBe("\\\f\n\r\t\v\0'\"`$\x00\x0B\x0C");
|
|
});
|
|
|
|
// test("template string ascii", () => {
|
|
// expect(identity(`A${""}`)).toBe("A");
|
|
// });
|
|
|
|
// test("template string latin1", () => {
|
|
// expect(identity(`©${""}`)).toBe("©");
|
|
// });
|
|
|
|
test("ireturnapromise", async () => {
|
|
expect(await ireturnapromise()).toEqual("aaa");
|
|
});
|
|
|
|
// A numeric key >= 100000 (JSC's MIN_SPARSE_ARRAY_INDEX) makes the property put inside
|
|
// JSC__JSValue__putToPropertyKey take a path that can throw, so the binding must check for
|
|
// an exception. BUN_JSC_validateExceptionChecks=1 aborts the child if the check is missing.
|
|
test("object argument with a sparse numeric key", async () => {
|
|
using dir = tempDir("macro-sparse-key", {
|
|
"take.ts": `export function take(o: any) {\n return Object.keys(o).join(",");\n}\n`,
|
|
"index.ts": `import { take } from "./take.ts" with { type: "macro" };\nconsole.log(take({ 200000: 1 }));\n`,
|
|
});
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "index.ts"],
|
|
env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" },
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
// One combined assertion so stderr (where JSC prints the exception check failure) shows up in
|
|
// the diff if the child aborts. Debug builds print "[macro] call take" to stdout before the
|
|
// script's own output, so only the tail of stdout is matched.
|
|
expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toMatchObject({
|
|
stdout: expect.stringMatching(/200000\n$/),
|
|
exitCode: 0,
|
|
signalCode: null,
|
|
});
|
|
});
|
|
|
|
test("object destructuring of a macro result keeps every bound property regardless of key order or repeated keys", async () => {
|
|
using dir = tempDir("macro-destructure-object", {
|
|
"m.ts": `export function m() {\n return { a: 1, c: 2 };\n}\n`,
|
|
"index.ts": [
|
|
`import { m } from "./m.ts" with { type: "macro" };`,
|
|
`const { c, a } = m();`,
|
|
`const { a: x, a: y, c: z } = m();`,
|
|
`console.log(JSON.stringify([c, a, x, y, z]));`,
|
|
``,
|
|
].join("\n"),
|
|
});
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "index.ts"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
expect({ lastLine: stdout.trim().split("\n").pop(), stderr }).toEqual({ lastLine: "[2,1,1,1,2]", stderr: "" });
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
describe("--no-macros", () => {
|
|
const files = {
|
|
"macro.ts": `
|
|
import { writeFileSync } from "node:fs";
|
|
export function f() {
|
|
writeFileSync("MACRO_RAN", "macro executed");
|
|
return "INLINED_RESULT";
|
|
}
|
|
`,
|
|
"entry.ts": `
|
|
import { f } from "./macro.ts" with { type: "macro" };
|
|
console.log(f());
|
|
`,
|
|
};
|
|
|
|
test("bun build --no-macros refuses to run macros", async () => {
|
|
using dir = tempDir("bundler-no-macros-cli", files);
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "--no-macros", "./entry.ts", "--outdir", "dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
expect({ stdout, stderr, exitCode }).toMatchObject({
|
|
stderr: expect.stringContaining("Macros are disabled"),
|
|
exitCode: 1,
|
|
});
|
|
expect(existsSync(path.join(String(dir), "MACRO_RAN"))).toBe(false);
|
|
expect(existsSync(path.join(String(dir), "dist", "entry.js"))).toBe(false);
|
|
});
|
|
|
|
test("Bun.build({ macros: false }) refuses to run macros", async () => {
|
|
using dir = tempDir("bundler-no-macros-api", {
|
|
...files,
|
|
"build.ts": `
|
|
const result = await Bun.build({
|
|
entrypoints: ["./entry.ts"],
|
|
outdir: "./dist",
|
|
macros: false,
|
|
throw: false,
|
|
});
|
|
console.log(JSON.stringify({
|
|
success: result.success,
|
|
logs: result.logs.map(l => l.message),
|
|
}));
|
|
`,
|
|
});
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "build.ts"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
const parsed = JSON.parse(stdout.trim().split("\n").pop()!);
|
|
expect({ parsed, stderr, exitCode }).toMatchObject({
|
|
parsed: {
|
|
success: false,
|
|
logs: expect.arrayContaining([expect.stringContaining("Macros are disabled")]),
|
|
},
|
|
exitCode: 0,
|
|
});
|
|
expect(existsSync(path.join(String(dir), "MACRO_RAN"))).toBe(false);
|
|
});
|
|
|
|
test("bun build without --no-macros still runs macros", async () => {
|
|
using dir = tempDir("bundler-macros-enabled", files);
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "./entry.ts", "--outdir", "dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 });
|
|
const out = await Bun.file(path.join(String(dir), "dist", "entry.js")).text();
|
|
expect(out).toContain("INLINED_RESULT");
|
|
expect(existsSync(path.join(String(dir), "MACRO_RAN"))).toBe(true);
|
|
});
|
|
});
|