Files

634 lines
19 KiB
JavaScript

import * as t from "@babel/types";
import generator from "../../babel-generator/lib/index.js";
import template from "../lib/index.js";
const comments = "// Sum two numbers\nconst add = (a, b) => a + b;";
describe("@babel/template", function () {
it("import statements are allowed by default", function () {
expect(function () {
template("import foo from 'foo'")({});
}).not.toThrow();
});
it("with statements are allowed with sourceType: script", function () {
expect(function () {
template("with({}){}", { sourceType: "script" })({});
}).not.toThrow();
});
it("should strip comments by default", function () {
const code = "const add = (a, b) => a + b;";
const output = template(comments)();
expect(generator(output).code).toBe(code);
});
it("should preserve comments with a flag", function () {
const output = template(comments, { preserveComments: true })();
expect(generator(output).code).toBe(comments);
});
it("should preserve comments with a flag when using .ast", function () {
const output1 = template.ast(comments, { preserveComments: true });
const output2 = template({ preserveComments: true }).ast(comments);
expect(generator(output1).code).toBe(comments);
expect(generator(output2).code).toBe(comments);
});
it("should allow yield outside generator function by default", function () {
expect(template.ast("yield 1")).toMatchInlineSnapshot(`
{
"expression": {
"argument": {
"extra": {
"raw": "1",
"rawValue": 1,
},
"loc": undefined,
"type": "NumericLiteral",
"value": 1,
},
"delegate": false,
"loc": undefined,
"type": "YieldExpression",
},
"loc": undefined,
"type": "ExpressionStatement",
}
`);
});
describe("string-based", () => {
it("should handle replacing values from an object", () => {
const value = t.stringLiteral("some string value");
const result = template(`
if (SOME_VAR === "") {}
`)({
SOME_VAR: value,
});
expect(result.type).toBe("IfStatement");
expect(result.test.type).toBe("BinaryExpression");
expect(result.test.left).toBe(value);
});
it("should handle replacing values given an array", () => {
const value = t.stringLiteral("some string value");
const result = template(`
if ($0 === "") {}
`)([value]);
expect(result.type).toBe("IfStatement");
expect(result.test.type).toBe("BinaryExpression");
expect(result.test.left).toBe(value);
});
it("should handle replacing values with null to remove them", () => {
const result = template(`
callee(ARG);
`)({ ARG: null });
expect(result.type).toBe("ExpressionStatement");
expect(result.expression.type).toBe("CallExpression");
expect(result.expression.arguments).toEqual([]);
});
it("should handle replacing values that are string content", () => {
const result = template(`
("ARG");
`)({ ARG: "some new content" });
expect(result.type).toBe("ExpressionStatement");
expect(result.expression.type).toBe("StringLiteral");
expect(result.expression.value).toBe("some new content");
});
it("should automatically clone nodes if they are injected twice", () => {
const id = t.identifier("someIdent");
const result = template(`
ID;
ID;
`)({ ID: id });
expect(result[0].type).toBe("ExpressionStatement");
expect(result[0].expression).toBe(id);
expect(result[1].type).toBe("ExpressionStatement");
expect(result[1].expression).not.toBe(id);
expect(result[1].expression).toEqual(id);
});
it("should allow passing in an allowlist of replacement names", () => {
const id = t.identifier("someIdent");
const result = template(
`
some_id;
`,
{ placeholderAllowlist: new Set(["some_id"]) },
)({ some_id: id });
expect(result.type).toBe("ExpressionStatement");
expect(result.expression).toBe(id);
});
it("should throw when using deprecated placeholderWhitelist without placeholderAllowlist", () => {
expect(() => {
template("FOO;", { placeholderWhitelist: new Set(["FOO"]) });
}).toThrow(/placeholderWhitelist.*renamed.*placeholderAllowlist/);
});
it("should not throw when both placeholderWhitelist and placeholderAllowlist are provided", () => {
const id = t.identifier("someIdent");
const result = template(
`
some_id;
`,
{
placeholderWhitelist: new Set(["some_id"]),
placeholderAllowlist: new Set(["some_id"]),
},
)({ some_id: id });
expect(result.type).toBe("ExpressionStatement");
expect(result.expression).toBe(id);
});
it("should allow passing in a RegExp to match replacement patterns", () => {
const id = t.identifier("someIdent");
const result = template(
`
ID;
ANOTHER_ID;
`,
{ placeholderPattern: /^ID$/ },
)({ ID: id });
expect(result[0].type).toBe("ExpressionStatement");
expect(result[0].expression).toBe(id);
expect(result[1].type).toBe("ExpressionStatement");
expect(result[1].expression.type).toBe("Identifier");
expect(result[1].expression.name).toBe("ANOTHER_ID");
});
it("should throw if unknown replacements are provided", () => {
expect(() => {
template(`
ID;
`)({ ID: t.identifier("someIdent"), ANOTHER_ID: null });
}).toThrow('Unknown substitution "ANOTHER_ID" given');
});
it("should throw if VariableDeclaration without init", () => {
expect(() => {
template(`
const %%ID%%;
`)({ ID: t.identifier("someIdent") });
}).toThrow("Missing initializer in destructuring declaration. (3:22)");
});
it("should throw if placeholders are not given explicit values", () => {
expect(() => {
template(`
ID;
ANOTHER_ID;
`)({ ID: t.identifier("someIdent") });
}).toThrow(
`Error: No substitution given for "ANOTHER_ID". If this is not meant to be a
placeholder you may want to consider passing one of the following options to @babel/template:
- { placeholderPattern: false, placeholderAllowlist: new Set(['ANOTHER_ID'])}
- { placeholderPattern: /^ANOTHER_ID$/ }`,
);
});
it("should return the AST directly when using .ast", () => {
const result = template.ast(`
if ("some string value" === "") {}
`);
expect(result.type).toBe("IfStatement");
expect(result.test.type).toBe("BinaryExpression");
expect(result.test.left.type).toBe("StringLiteral");
expect(result.test.left.value).toBe("some string value");
});
});
describe("literal-based", () => {
it("should handle replacing values from an object", () => {
const value = t.stringLiteral("some string value");
const result = template`
if (${value} === "") {}
`();
expect(result.type).toBe("IfStatement");
expect(result.test.type).toBe("BinaryExpression");
expect(result.test.left).toBe(value);
});
it("should handle replacing values with null to remove them", () => {
const result = template`
callee(${null});
`();
expect(result.type).toBe("ExpressionStatement");
expect(result.expression.type).toBe("CallExpression");
expect(result.expression.arguments).toEqual([]);
});
it("should handle replacing values that are string content", () => {
const result = template`
("${"some new content"}");
`();
expect(result.type).toBe("ExpressionStatement");
expect(result.expression.type).toBe("StringLiteral");
expect(result.expression.value).toBe("some new content");
});
it("should allow setting options by passing an object", () => {
const result = template({ sourceType: "script" })`
with({}){}
`();
expect(result.type).toBe("WithStatement");
});
it("should return the AST directly when using .ast", () => {
const value = t.stringLiteral("some string value");
const result = template.ast`
if (${value} === "") {}
`;
expect(result.type).toBe("IfStatement");
expect(result.test.type).toBe("BinaryExpression");
expect(result.test.left).toBe(value);
});
it("should correctly handle empty string as computed property key", () => {
const result = template.ast`obj["${""}"] = 1`;
expect(result.type).toBe("ExpressionStatement");
});
it("should return assertions in ImportDeclaration when using .ast", () => {
const result = template.ast(
`import json from "./foo.json" with { type: "json" };`,
{
plugins: ["importAttributes"],
},
);
expect(result.attributes[0].type).toBe("ImportAttribute");
});
it("should return assertions in ExportNamedDeclaration when using .ast", () => {
const result = template.ast(
`export { default as foo2 } from "foo.json" with { type: "json" };`,
{
plugins: ["importAttributes"],
},
);
expect(result.attributes[0].type).toBe("ImportAttribute");
});
it("should return assertions in ExportDefaultDeclaration when using .ast", () => {
const result = template.ast(
`export foo2 from "foo.json" with { type: "json" };`,
{
plugins: ["importAttributes", "exportDefaultFrom"],
},
);
expect(result.attributes[0].type).toBe("ImportAttribute");
});
it("should return assertions in ExportAllDeclaration when using .ast", () => {
const result = template.ast(
`export * from "foo.json" with { type: "json" };`,
{
plugins: ["importAttributes"],
},
);
expect(result.attributes[0].type).toBe("ImportAttribute");
});
it("should replace JSX placeholder", () => {
const result = template.expression(
`
<TAG>{'content'}</TAG>
`,
{
plugins: ["jsx"],
},
)({
TAG: t.jsxIdentifier("div"),
});
expect(generator(result).code).toEqual("<div>{'content'}</div>");
});
it("should work with `export { x }`", () => {
const result = template.ast`
export { ${t.identifier("x")} }
$$$$BABEL_TPL$0;
`;
expect(result).toMatchInlineSnapshot(`
[
{
"attributes": [],
"declaration": null,
"loc": undefined,
"source": null,
"specifiers": [
{
"exported": {
"name": "x",
"type": "Identifier",
},
"loc": undefined,
"local": {
"name": "x",
"type": "Identifier",
},
"type": "ExportSpecifier",
},
],
"type": "ExportNamedDeclaration",
},
{
"expression": {
"loc": undefined,
"name": "$$$$BABEL_TPL$0",
"type": "Identifier",
},
"loc": undefined,
"type": "ExpressionStatement",
},
]
`);
});
it("should work with `const a = { x }`", () => {
const result = template.ast`
{
const a = { ${t.identifier("x")} };
$$$$BABEL_TPL$0;
}
`;
expect(generator(result).code).toMatchInlineSnapshot(`
"{
const a = {
x
};
$$$$BABEL_TPL$0;
}"
`);
});
});
it("error stack", () => {
let error;
try {
function create() {
return template(``);
}
function call(tpl) {
tpl({
FOO: t.numericLiteral(1),
});
}
call(create());
} catch (e) {
error = e;
}
expect(error.stack).toMatch("=============\n at create");
});
describe(".syntacticPlaceholders", () => {
it("works in function body", () => {
const output = template(`function f() %%A%%`)({
A: t.blockStatement([]),
});
expect(generator(output).code).toMatchInlineSnapshot(`"function f() {}"`);
});
it("works in class body", () => {
const output = template(`class C %%A%%`)({
A: t.classBody([]),
});
expect(generator(output).code).toMatchInlineSnapshot(`"class C {}"`);
});
it("replaces lowercase names", () => {
const output = template(`%%foo%%`)({
foo: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"1;"`);
});
it("pattern", () => {
expect(() => {
template(`%%A%% + %%B%%`, {
placeholderPattern: /B/,
})();
}).toThrow(/aren't compatible with '.syntacticPlaceholders: true'/);
});
it("allowlist", () => {
expect(() => {
template(`%%A%% + %%B%%`, {
placeholderPattern: false,
placeholderAllowlist: new Set(["B"]),
})();
}).toThrow(/aren't compatible with '.syntacticPlaceholders: true'/);
});
describe("option value", () => {
describe("true", () => {
it("allows placeholders", () => {
const output = template(`%%FOO%%`, { syntacticPlaceholders: true })({
FOO: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"1;"`);
});
it("doesn't replace identifiers", () => {
expect(() => {
template(`FOO`, { syntacticPlaceholders: true })({
FOO: t.numericLiteral(1),
});
}).toThrow(/Unknown substitution/);
});
});
describe("false", () => {
it("disallow placeholders", () => {
expect(() => {
template(`%%FOO%%`, { syntacticPlaceholders: false })({
FOO: t.numericLiteral(1),
});
}).toThrow(/Unexpected token.*/);
});
it("disallow manually enabled placeholders", () => {
expect(() => {
template(`%%FOO%%`, {
syntacticPlaceholders: false,
plugins: ["placeholders"],
})({
FOO: t.numericLiteral(1),
});
}).toThrow(/%%.*placeholders can't be used/);
});
it("replaces identifiers", () => {
const output = template(`FOO`, { syntacticPlaceholders: false })({
FOO: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"1;"`);
});
it("allows v8intrinsics", () => {
const output = template(`%DebugPrint(1)`, {
syntacticPlaceholders: false,
plugins: ["v8intrinsic"],
})();
expect(generator(output).code).toMatchInlineSnapshot(
`"%DebugPrint(1);"`,
);
});
});
describe("undefined", () => {
it("allows placeholders", () => {
const output = template(`%%FOO%%`)({
FOO: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"1;"`);
});
it("replaces identifiers", () => {
const output = template(`FOO`)({
FOO: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"1;"`);
});
it("doesn't mix placeholder styles", () => {
const output = template(`FOO + %%FOO%%`)({
FOO: t.numericLiteral(1),
});
expect(generator(output).code).toMatchInlineSnapshot(`"FOO + 1;"`);
});
});
});
it("works in var declaration", () => {
const output = template("var %%LHS%% = %%RHS%%")({
LHS: t.identifier("x"),
RHS: t.numericLiteral(7),
});
expect(generator(output).code).toMatchInlineSnapshot(`"var x = 7;"`);
});
it("works in const declaration", () => {
const output = template("const %%LHS%% = %%RHS%%")({
LHS: t.identifier("x"),
RHS: t.numericLiteral(7),
});
expect(generator(output).code).toMatchInlineSnapshot(`"const x = 7;"`);
});
it("works in const declaration inside for-of without init", () => {
const output = template("for (const %%LHS%% of %%RHS%%){}")({
LHS: t.objectPattern([
t.objectProperty(t.identifier("x"), t.identifier("x")),
]),
RHS: t.identifier("y"),
});
expect(generator(output).code).toMatchInlineSnapshot(`
"for (const {
x: x
} of y) {}"
`);
});
it("works in let declaration", () => {
const output = template("let %%LHS%% = %%RHS%%")({
LHS: t.identifier("x"),
RHS: t.numericLiteral(7),
});
expect(generator(output).code).toMatchInlineSnapshot(`"let x = 7;"`);
});
it("should keep node props", () => {
const outputs = [
template({ plugins: ["typescript"] }).ast`
const ${t.identifier("greeting")}: string = 'Hello';
`,
template({ plugins: ["typescript"] }).ast`
var ${t.objectPattern([])}: string = x;
`,
template({ plugins: ["decorators-legacy"] }).ast`
class X {
f(@dec ${t.identifier("x")}) {}
}
`,
template({ plugins: ["typescript"] }).ast`
function f(${t.identifier("x")}?) {}
`,
template({ plugins: ["typescript"] }).ast`
function f(${Object.assign(t.identifier("x"), { optional: true })}: string) {}
`,
template({ plugins: ["decorators-legacy"] }).ast`
class X {
f(@dec ${Object.assign(t.identifier("x"), { optional: true })}) {}
}
`,
template({ plugins: ["typescript"] }).ast`
function f(${Object.assign(t.identifier("x"), { typeAnnotation: t.tsTypeAnnotation(t.tsStringKeyword()) })}: number) {}
`,
];
expect(outputs.map(ast => generator(ast).code)).toMatchInlineSnapshot(`
[
"const greeting: string = 'Hello';",
"var {}: string = x;",
"class X {
f(@dec
x) {}
}",
"function f(x?) {}",
"function f(x?: string) {}",
"class X {
f(@dec
x?) {}
}",
"function f(x: number) {}",
]
`);
});
it("should keep node props with syntacticPlaceholders", () => {
const outputs = [
template({ plugins: ["typescript"] })(`const %%x%%: string = 'Hello'`)({
x: t.identifier("x"),
}),
template({ plugins: ["typescript"] })(`
var %%x%%: string = x;
`)({
x: t.objectPattern([]),
}),
];
expect(outputs.map(ast => generator(ast).code)).toMatchInlineSnapshot(`
[
"const x: string = 'Hello';",
"var {}: string = x;",
]
`);
});
});
});