Reimplement 'babel-template' with better caching and tagged literal utilities.

This commit is contained in:
Logan Smyth
2017-10-15 15:50:14 -04:00
parent 191624d800
commit cc802c1e00
15 changed files with 1050 additions and 422 deletions

View File

@@ -1,34 +1,202 @@
import generator from "../../babel-generator";
import template from "../lib";
import chai from "chai";
import { expect } from "chai";
import * as t from "babel-types";
const comments = "// Sum two numbers\nconst add = (a, b) => a + b;";
describe("templating", function() {
describe("babel-template", function() {
it("import statements are allowed by default", function() {
chai
.expect(function() {
template("import foo from 'foo'")({});
})
.not.to.throw();
expect(function() {
template("import foo from 'foo'")({});
}).not.to.throw();
});
it("with statements are allowed with sourceType: script", function() {
chai
.expect(function() {
template("with({}){}", { sourceType: "script" })({});
})
.not.to.throw();
expect(function() {
template("with({}){}", { sourceType: "script" })({});
}).not.to.throw();
});
it("should strip comments by default", function() {
const code = "const add = (a, b) => a + b;";
const output = template(comments)();
chai.expect(generator(output).code).to.be.equal(code);
expect(generator(output).code).to.be.equal(code);
});
it("should preserve comments with a flag", function() {
const output = template(comments, { preserveComments: true })();
chai.expect(generator(output).code).to.be.equal(comments);
expect(generator(output).code).to.be.equal(comments);
});
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).to.equal("IfStatement");
expect(result.test.type).to.equal("BinaryExpression");
expect(result.test.left).to.equal(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).to.equal("IfStatement");
expect(result.test.type).to.equal("BinaryExpression");
expect(result.test.left).to.equal(value);
});
it("should handle replacing values with null to remove them", () => {
const result = template(`
callee(ARG);
`)({ ARG: null });
expect(result.type).to.equal("ExpressionStatement");
expect(result.expression.type).to.equal("CallExpression");
expect(result.expression.arguments).to.eql([]);
});
it("should handle replacing values that are string content", () => {
const result = template(`
("ARG");
`)({ ARG: "some new content" });
expect(result.type).to.equal("ExpressionStatement");
expect(result.expression.type).to.equal("StringLiteral");
expect(result.expression.value).to.equal("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).to.equal("ExpressionStatement");
expect(result[0].expression).to.equal(id);
expect(result[1].type).to.equal("ExpressionStatement");
expect(result[1].expression).not.to.equal(id);
expect(result[1].expression).to.eql(id);
});
it("should allow passing in a whitelist of replacement names", () => {
const id = t.identifier("someIdent");
const result = template(
`
some_id;
`,
{ placeholderWhitelist: new Set(["some_id"]) },
)({ some_id: id });
expect(result.type).to.equal("ExpressionStatement");
expect(result.expression).to.equal(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).to.equal("ExpressionStatement");
expect(result[0].expression).to.equal(id);
expect(result[1].type).to.equal("ExpressionStatement");
expect(result[1].expression.type).to.equal("Identifier");
expect(result[1].expression.name).to.equal("ANOTHER_ID");
});
it("should throw if unknown replacements are provided", () => {
expect(() => {
template(`
ID;
`)({ ID: t.identifier("someIdent"), ANOTHER_ID: null });
}).to.throw(Error, 'Unknown substitution "ANOTHER_ID" given');
});
it("should throw if placeholders are not given explicit values", () => {
expect(() => {
template(`
ID;
ANOTHER_ID;
`)({ ID: t.identifier("someIdent") });
}).to.throw(Error, 'No substitution given for "ANOTHER_ID"');
});
it("should return the AST directly when using .ast", () => {
const result = template.ast(`
if ("some string value" === "") {}
`);
expect(result.type).to.equal("IfStatement");
expect(result.test.type).to.equal("BinaryExpression");
expect(result.test.left.type).to.equal("StringLiteral");
expect(result.test.left.value).to.equal("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).to.equal("IfStatement");
expect(result.test.type).to.equal("BinaryExpression");
expect(result.test.left).to.equal(value);
});
it("should handle replacing values with null to remove them", () => {
const result = template`
callee(${null});
`();
expect(result.type).to.equal("ExpressionStatement");
expect(result.expression.type).to.equal("CallExpression");
expect(result.expression.arguments).to.eql([]);
});
it("should handle replacing values that are string content", () => {
const result = template`
("${"some new content"}");
`();
expect(result.type).to.equal("ExpressionStatement");
expect(result.expression.type).to.equal("StringLiteral");
expect(result.expression.value).to.equal("some new content");
});
it("should allow setting options by passing an object", () => {
const result = template({ sourceType: "script" })`
with({}){}
`();
expect(result.type).to.equal("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).to.equal("IfStatement");
expect(result.test.type).to.equal("BinaryExpression");
expect(result.test.left).to.equal(value);
});
});
});

View File

@@ -1,183 +0,0 @@
import generator from "../../babel-generator";
import * as t from "@babel/types";
import template from "../lib";
import chai from "chai";
const expect = chai.expect;
describe("tagged templating", () => {
it("basic support", () => {
const tpl = template`("stringLiteral")`;
const result = tpl();
expect(result).to.be.ok;
expect(t.isStringLiteral(result.expression)).to.be.true;
});
describe("numeric interpolation", () => {
it("single replacement", () => {
const tpl = template`+${0}`;
const node = t.numericLiteral(123);
const result = tpl(node);
expect(result).to.be.ok;
expect(t.isUnaryExpression(result.expression)).to.be.true;
expect(result.expression.argument).to.equal(node);
});
it("duplicate replacement", () => {
const tpl = template`${0} + ${0}`;
const node = t.numericLiteral(123);
const result = tpl(node);
expect(result).to.be.ok;
expect(t.isBinaryExpression(result.expression)).to.be.true;
expect(result.expression.left).to.equal(node);
expect(result.expression.right).to.equal(result.expression.left);
});
it("multiple replacement", () => {
const tpl = template`${0}.${1}(${2})`;
const object = t.identifier("foo");
const property = t.identifier("bar");
const argument = t.numericLiteral(123);
const result = tpl(object, property, argument);
expect(result).to.be.ok;
expect(t.isCallExpression(result.expression)).to.be.true;
const { callee, arguments: args } = result.expression;
expect(t.isMemberExpression(callee)).to.be.true;
expect(callee.object).to.equal(object);
expect(callee.property).to.equal(property);
expect(args).to.deep.equal([argument]);
});
});
describe("string interpolation", () => {
it("has expected internal representation", () => {
const tpl = template`${"foo"}(${"b a r"})`;
expect(generator(tpl()).code).to.equal(
"$BABEL_TEMPLATE$$foo($BABEL_TEMPLATE$$bAR);",
);
});
it("simple replacement", () => {
const tpl = template`${"foo"}(${"b a r"})`;
const arg = {
foo: t.identifier("baz"),
"b a r": t.numericLiteral(123),
};
const result = tpl(arg);
expect(result).to.be.ok;
expect(t.isCallExpression(result.expression)).to.be.true;
const { callee, arguments: args } = result.expression;
expect(callee).to.equal(arg.foo);
expect(args).to.deep.equal([arg["b a r"]]);
});
it("does not conflict with similar identifiers", () => {
const tpl = template`foo + ${"foo"}`;
const arg = {
foo: t.identifier("foo"),
};
const result = tpl(arg);
expect(result).to.be.ok;
expect(t.isBinaryExpression(result.expression)).to.be.true;
const { left, right } = result.expression;
expect(left).to.not.equal(right);
expect(t.isIdentifier(left, { name: "foo" })).to.be.true;
expect(right).to.equal(arg.foo);
});
it("does not conflict when t.toIdentifier conflicts", () => {
const tpl = template`${"fOO"} + ${"f o o"}`;
const arg = {
fOO: t.numericLiteral(123),
"f o o": t.numericLiteral(321),
};
const result = tpl(arg);
expect(result).to.be.ok;
expect(t.isBinaryExpression(result.expression)).to.be.true;
const { left, right } = result.expression;
expect(left).to.not.equal(right);
expect(left).to.equal(arg.fOO);
expect(right).to.equal(arg["f o o"]);
});
});
describe("mixed interpolation", () => {
it("throws when 0 is used", () => {
expect(() => template`${0} - ${"foo"}`).to.throw(
"Template cannot have a '0' replacement and a named replacement at the same time",
);
});
it("works", () => {
const tpl = template`${1}.${"prop"}`;
const arg = {
prop: t.identifier("prop"),
};
const result = tpl(arg, t.thisExpression());
expect(result).to.be.ok;
expect(t.isMemberExpression(result.expression)).to.be.true;
const { object, property } = result.expression;
expect(t.isThisExpression(object)).to.be.true;
expect(property).to.equal(arg.prop);
});
});
describe("Node interpolation", () => {
it("works", () => {
const node = t.identifier("foo");
const tpl = template`${node}`;
const result = tpl();
expect(result).to.be.ok;
expect(result.expression).to.equal(node);
});
});
describe("options", () => {
it("works", () => {
const remove = template({ preserveComments: false })`// comment\nid;`;
const preserve = template({ preserveComments: true })`// comment\nid;`;
const removeResult = remove();
const preserveResult = preserve();
expect(removeResult);
expect(preserveResult).to.be.ok;
// it exists, it just resets to undefined
expect(removeResult.leadingComments).to.be.undefined;
expect(Array.isArray(preserveResult.leadingComments)).to.be.true;
expect(preserveResult.leadingComments[0]).to.have.property(
"type",
"CommentLine",
);
expect(preserveResult.leadingComments[0]).to.have.property(
"value",
" comment",
);
});
});
});