Reimplement 'babel-template' with better caching and tagged literal utilities.
This commit is contained in:
parent
191624d800
commit
cc802c1e00
@ -5,9 +5,9 @@ import template from "@babel/template";
|
|||||||
const helpers = {};
|
const helpers = {};
|
||||||
export default helpers;
|
export default helpers;
|
||||||
|
|
||||||
function defineHelper(str) {
|
// Helpers never include placeholders, so we disable placeholder pattern
|
||||||
return template(str);
|
// matching to allow us to use pattern-like variable names.
|
||||||
}
|
const defineHelper = template({ placeholderPattern: false });
|
||||||
|
|
||||||
helpers.typeof = defineHelper(`
|
helpers.typeof = defineHelper(`
|
||||||
export default function _typeof(obj) {
|
export default function _typeof(obj) {
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import hoistVariables from "@babel/helper-hoist-variables";
|
|||||||
import template from "@babel/template";
|
import template from "@babel/template";
|
||||||
|
|
||||||
const buildTemplate = template(`
|
const buildTemplate = template(`
|
||||||
SYSTEM_REGISTER(MODULE_NAME, [SOURCES], function (EXPORT_IDENTIFIER, CONTEXT_IDENTIFIER) {
|
SYSTEM_REGISTER(MODULE_NAME, SOURCES, function (EXPORT_IDENTIFIER, CONTEXT_IDENTIFIER) {
|
||||||
"use strict";
|
"use strict";
|
||||||
BEFORE_BODY;
|
BEFORE_BODY;
|
||||||
return {
|
return {
|
||||||
setters: [SETTERS],
|
setters: SETTERS,
|
||||||
execute: function () {
|
execute: function () {
|
||||||
BODY;
|
BODY;
|
||||||
}
|
}
|
||||||
@ -365,8 +365,8 @@ export default function({ types: t }, options) {
|
|||||||
),
|
),
|
||||||
BEFORE_BODY: beforeBody,
|
BEFORE_BODY: beforeBody,
|
||||||
MODULE_NAME: moduleName,
|
MODULE_NAME: moduleName,
|
||||||
SETTERS: setters,
|
SETTERS: t.arrayExpression(setters),
|
||||||
SOURCES: sources,
|
SOURCES: t.arrayExpression(sources),
|
||||||
BODY: path.node.body,
|
BODY: path.node.body,
|
||||||
EXPORT_IDENTIFIER: exportIdent,
|
EXPORT_IDENTIFIER: exportIdent,
|
||||||
CONTEXT_IDENTIFIER: contextIdent,
|
CONTEXT_IDENTIFIER: contextIdent,
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export default function convertFunctionParams(path, loose) {
|
|||||||
});
|
});
|
||||||
body.push(defNode);
|
body.push(defNode);
|
||||||
} else if (firstOptionalIndex !== null) {
|
} else if (firstOptionalIndex !== null) {
|
||||||
const defNode = buildArgumentsAccess(param.node, t.numericLiteral(i));
|
const defNode = buildArgumentsAccess([param.node, t.numericLiteral(i)]);
|
||||||
body.push(defNode);
|
body.push(defNode);
|
||||||
} else if (param.isObjectPattern() || param.isArrayPattern()) {
|
} else if (param.isObjectPattern() || param.isArrayPattern()) {
|
||||||
const uid = path.scope.generateUidIdentifier("ref");
|
const uid = path.scope.generateUidIdentifier("ref");
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# @babel/template
|
# @babel/template
|
||||||
|
|
||||||
> Generate an AST from a string template.
|
> Generate an AST from a string template or template literal.
|
||||||
|
|
||||||
In computer science, this is known as an implementation of quasiquotes.
|
In computer science, this is known as an implementation of quasiquotes.
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ In computer science, this is known as an implementation of quasiquotes.
|
|||||||
npm install --save-dev @babel/template
|
npm install --save-dev @babel/template
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## String Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import template from "@babel/template";
|
import template from "@babel/template";
|
||||||
@ -33,6 +33,93 @@ console.log(generate(ast).code);
|
|||||||
const myModule = require("my-module");
|
const myModule = require("my-module");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `.ast`
|
||||||
|
|
||||||
|
If no placeholders are in use and you just want a simple way to parse a
|
||||||
|
string into an AST, you can use the `.ast` version of the template.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ast = template.ast(`
|
||||||
|
var myModule = require("my-module");
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
which will parse and return the AST directly.
|
||||||
|
|
||||||
|
|
||||||
|
## Template Literal Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
import template from "babel-template";
|
||||||
|
import generate from "babel-generator";
|
||||||
|
import * as t from "babel-types";
|
||||||
|
|
||||||
|
const fn = template`
|
||||||
|
var IMPORT_NAME = require('${"my-module"}');
|
||||||
|
`);
|
||||||
|
|
||||||
|
const ast = fn({
|
||||||
|
IMPORT_NAME: t.identifier("myModule");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(generate(ast).code);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that placeholders can be passed directly as part of the template literal
|
||||||
|
in order to make things as readable as possible, or they can be passed into
|
||||||
|
the template function.
|
||||||
|
|
||||||
|
### `.ast`
|
||||||
|
|
||||||
|
If no placeholders are in use and you just want a simple way to parse a
|
||||||
|
string into an AST, you can use the `.ast` version of the template.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const name = "my-module";
|
||||||
|
const mod = "myModule";
|
||||||
|
|
||||||
|
const ast = template.ast`
|
||||||
|
var ${mod} = require("${name}");
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
which will parse and return the AST directly. Note that unlike the string-based
|
||||||
|
version mentioned earlier, since this is a template literal, it is still
|
||||||
|
valid to perform replacements using template literal replacements.
|
||||||
|
|
||||||
|
|
||||||
|
## AST results
|
||||||
|
|
||||||
|
The `babel-template` API exposes a few flexible APIs to make it as easy as
|
||||||
|
possible to create ASTs with an expected structure. Each of these also has
|
||||||
|
the `.ast` property mentioned above.
|
||||||
|
|
||||||
|
### `template`
|
||||||
|
|
||||||
|
`template` returns either a single statement, or an array of
|
||||||
|
statements, depending on the parsed result.
|
||||||
|
|
||||||
|
### `template.smart`
|
||||||
|
|
||||||
|
This is the same as the default `template` API, returning either a single
|
||||||
|
node, or an array of nodes, depending on the parsed result.
|
||||||
|
|
||||||
|
### `template.statement`
|
||||||
|
|
||||||
|
`template.statement("foo;")()` returns a single statement node, and throw
|
||||||
|
an exception if the result is anything but a single statement.
|
||||||
|
|
||||||
|
### `template.statements`
|
||||||
|
|
||||||
|
`template.statements("foo;foo;")()` returns an array of statement nodes.
|
||||||
|
|
||||||
|
### `template.expression`
|
||||||
|
|
||||||
|
`template.expression("foo")()` returns the expression node.
|
||||||
|
|
||||||
|
### `template.program`
|
||||||
|
|
||||||
|
`template.program("foo;")()` returns the `Program` node for the template.
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `template(code, [opts])`
|
### `template(code, [opts])`
|
||||||
@ -48,6 +135,25 @@ some defaults of its own:
|
|||||||
|
|
||||||
* `allowReturnOutsideFunction` is set to `true` by default.
|
* `allowReturnOutsideFunction` is set to `true` by default.
|
||||||
* `allowSuperOutsideMethod` is set to `true` by default.
|
* `allowSuperOutsideMethod` is set to `true` by default.
|
||||||
|
* `sourceType` is set to `module` by default.
|
||||||
|
|
||||||
|
##### placeholderWhitelist
|
||||||
|
|
||||||
|
Type: `Set<string>`
|
||||||
|
Default: `undefined`
|
||||||
|
|
||||||
|
A set of placeholder names to automatically accept. Items in this list do
|
||||||
|
not need to match the given placeholder pattern.
|
||||||
|
|
||||||
|
##### placeholderPattern
|
||||||
|
|
||||||
|
Type: `RegExp | false`
|
||||||
|
Default: `/^[_$A-Z0-9]+$/`
|
||||||
|
|
||||||
|
A pattern to search for when looking for Identifier and StringLiteral
|
||||||
|
nodes that should be considered placeholders.
|
||||||
|
'false' will disable placeholder searching entirely, leaving only the
|
||||||
|
'placeholderWhitelist' value to find placeholders.
|
||||||
|
|
||||||
##### preserveComments
|
##### preserveComments
|
||||||
|
|
||||||
@ -58,7 +164,9 @@ Set this to `true` to preserve any comments from the `code` parameter.
|
|||||||
|
|
||||||
#### Return value
|
#### Return value
|
||||||
|
|
||||||
`@babel/template` returns a `function` which is invoked with an optional object
|
By default `@babel/template` returns a `function` which is invoked with an
|
||||||
of replacements. See the usage section for an example.
|
optional object of replacements. See the usage section for an example.
|
||||||
|
|
||||||
|
When using `.ast`, the AST will be returned directly.
|
||||||
|
|
||||||
[babylon]: https://github.com/babel/babylon#options
|
[babylon]: https://github.com/babel/babylon#options
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "7.0.0-beta.3",
|
"@babel/code-frame": "7.0.0-beta.3",
|
||||||
"@babel/traverse": "7.0.0-beta.3",
|
|
||||||
"@babel/types": "7.0.0-beta.3",
|
"@babel/types": "7.0.0-beta.3",
|
||||||
"babylon": "7.0.0-beta.29",
|
"babylon": "7.0.0-beta.29",
|
||||||
"lodash": "^4.2.0"
|
"lodash": "^4.2.0"
|
||||||
|
|||||||
131
packages/babel-template/src/builder.js
Normal file
131
packages/babel-template/src/builder.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
merge,
|
||||||
|
validate,
|
||||||
|
type TemplateOpts,
|
||||||
|
type PublicOpts,
|
||||||
|
type PublicReplacements,
|
||||||
|
} from "./options";
|
||||||
|
import type { Formatter } from "./formatters";
|
||||||
|
|
||||||
|
import stringTemplate from "./string";
|
||||||
|
import literalTemplate from "./literal";
|
||||||
|
|
||||||
|
export type TemplateBuilder<T> = {
|
||||||
|
// Build a new builder, merging the given options with the previous ones.
|
||||||
|
(opts: PublicOpts): TemplateBuilder<T>,
|
||||||
|
|
||||||
|
// Building from a string produces an AST builder function by default.
|
||||||
|
(tpl: string, opts: ?PublicOpts): (?PublicReplacements) => T,
|
||||||
|
|
||||||
|
// Building from a template literal produces an AST builder function by default.
|
||||||
|
(tpl: Array<string>, ...args: Array<mixed>): (?PublicReplacements) => T,
|
||||||
|
|
||||||
|
// Allow users to explicitly create templates that produce ASTs, skipping
|
||||||
|
// the need for an intermediate function.
|
||||||
|
ast: {
|
||||||
|
(tpl: string, opts: ?PublicOpts): T,
|
||||||
|
(tpl: Array<string>, ...args: Array<mixed>): T,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prebuild the options that will be used when parsing a `.ast` template.
|
||||||
|
// These do not use a pattern because there is no way for users to pass in
|
||||||
|
// replacement patterns to begin with, and disabling pattern matching means
|
||||||
|
// users have more flexibility in what type of content they have in their
|
||||||
|
// template JS.
|
||||||
|
const NO_PLACEHOLDER: TemplateOpts = validate({
|
||||||
|
placeholderPattern: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function createTemplateBuilder<T>(
|
||||||
|
formatter: Formatter<T>,
|
||||||
|
defaultOpts?: TemplateOpts,
|
||||||
|
): TemplateBuilder<T> {
|
||||||
|
const templateFnCache = new WeakMap();
|
||||||
|
const templateAstCache = new WeakMap();
|
||||||
|
const cachedOpts = defaultOpts || validate(null);
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
((tpl, ...args) => {
|
||||||
|
if (typeof tpl === "string") {
|
||||||
|
if (args.length > 1) throw new Error("Unexpected extra params.");
|
||||||
|
return extendedTrace(
|
||||||
|
stringTemplate(formatter, tpl, merge(cachedOpts, validate(args[0]))),
|
||||||
|
);
|
||||||
|
} else if (Array.isArray(tpl)) {
|
||||||
|
let builder = templateFnCache.get(tpl);
|
||||||
|
if (!builder) {
|
||||||
|
builder = literalTemplate(formatter, tpl, cachedOpts);
|
||||||
|
templateFnCache.set(tpl, builder);
|
||||||
|
}
|
||||||
|
return extendedTrace(builder(args));
|
||||||
|
} else if (typeof tpl === "object" && tpl) {
|
||||||
|
if (args.length > 0) throw new Error("Unexpected extra params.");
|
||||||
|
return createTemplateBuilder(
|
||||||
|
formatter,
|
||||||
|
merge(cachedOpts, validate(tpl)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected template param ${typeof tpl}`);
|
||||||
|
}: Function),
|
||||||
|
{
|
||||||
|
ast: (tpl, ...args) => {
|
||||||
|
if (typeof tpl === "string") {
|
||||||
|
if (args.length > 1) throw new Error("Unexpected extra params.");
|
||||||
|
return stringTemplate(
|
||||||
|
formatter,
|
||||||
|
tpl,
|
||||||
|
merge(merge(cachedOpts, validate(args[0])), NO_PLACEHOLDER),
|
||||||
|
)();
|
||||||
|
} else if (Array.isArray(tpl)) {
|
||||||
|
let builder = templateAstCache.get(tpl);
|
||||||
|
if (!builder) {
|
||||||
|
builder = literalTemplate(
|
||||||
|
formatter,
|
||||||
|
tpl,
|
||||||
|
merge(cachedOpts, NO_PLACEHOLDER),
|
||||||
|
);
|
||||||
|
templateAstCache.set(tpl, builder);
|
||||||
|
}
|
||||||
|
return builder(args)();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected template param ${typeof tpl}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendedTrace<Arg, Result>(fn: Arg => Result): Arg => Result {
|
||||||
|
// Since we lazy parse the template, we get the current stack so we have the
|
||||||
|
// original stack to append if it errors when parsing
|
||||||
|
let rootStack = "";
|
||||||
|
try {
|
||||||
|
// error stack gets populated in IE only on throw
|
||||||
|
// (https://msdn.microsoft.com/en-us/library/hh699850(v=vs.94).aspx)
|
||||||
|
throw new Error();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.stack) {
|
||||||
|
// error.stack does not exists in IE <= 9
|
||||||
|
// We slice off the top 3 items in the stack to remove the call to
|
||||||
|
// 'extendedTrace', and the anonymous builder function, with the final
|
||||||
|
// stripped line being the error message itself since we threw it
|
||||||
|
// in the first place and it doesn't matter.
|
||||||
|
rootStack = error.stack
|
||||||
|
.split("\n")
|
||||||
|
.slice(3)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (arg: Arg) => {
|
||||||
|
try {
|
||||||
|
return fn(arg);
|
||||||
|
} catch (err) {
|
||||||
|
err.stack += `\n =============\n${rootStack}`;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
74
packages/babel-template/src/formatters.js
Normal file
74
packages/babel-template/src/formatters.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
export type Formatter<T> = {
|
||||||
|
code: string => string,
|
||||||
|
validate: BabelNodeFile => void,
|
||||||
|
unwrap: BabelNodeFile => T,
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeStatementFormatter<T>(
|
||||||
|
fn: (Array<BabelNodeStatement>) => T,
|
||||||
|
): Formatter<T> {
|
||||||
|
return {
|
||||||
|
// We need to prepend a ";" to force statement parsing so that
|
||||||
|
// ExpressionStatement strings won't be parsed as directives.
|
||||||
|
// Alonside that, we also prepend a comment so that when a syntax error
|
||||||
|
// is encountered, the user will be less likely to get confused about
|
||||||
|
// where the random semicolon came from.
|
||||||
|
code: str => `/* @babel/template */;\n${str}`,
|
||||||
|
validate: () => {},
|
||||||
|
unwrap: (ast: BabelNodeFile): T => {
|
||||||
|
return fn(ast.program.body.slice(1));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const smart: Formatter<
|
||||||
|
Array<BabelNodeStatement> | BabelNodeStatement,
|
||||||
|
> = makeStatementFormatter(body => {
|
||||||
|
if (body.length > 1) {
|
||||||
|
return body;
|
||||||
|
} else {
|
||||||
|
return body[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statements: Formatter<
|
||||||
|
Array<BabelNodeStatement>,
|
||||||
|
> = makeStatementFormatter(body => body);
|
||||||
|
|
||||||
|
export const statement: Formatter<
|
||||||
|
BabelNodeStatement,
|
||||||
|
> = makeStatementFormatter(body => {
|
||||||
|
// We do this validation when unwrapping since the replacement process
|
||||||
|
// could have added or removed statements.
|
||||||
|
if (body.length === 0) {
|
||||||
|
throw new Error("Found nothing to return.");
|
||||||
|
}
|
||||||
|
if (body.length > 1) {
|
||||||
|
throw new Error("Found multiple statements but wanted one");
|
||||||
|
}
|
||||||
|
|
||||||
|
return body[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expression: Formatter<BabelNodeExpression> = {
|
||||||
|
code: str => `(\n${str}\n)`,
|
||||||
|
validate: (ast: BabelNodeFile) => {
|
||||||
|
const { program } = ast;
|
||||||
|
if (program.body.length > 1) {
|
||||||
|
throw new Error("Found multiple statements but wanted one");
|
||||||
|
}
|
||||||
|
const expression = program.body[0].expression;
|
||||||
|
if (expression.start === 0) {
|
||||||
|
throw new Error("Parse result included parens.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unwrap: ast => ast.program.body[0].expression,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const program: Formatter<BabelNodeProgram> = {
|
||||||
|
code: str => str,
|
||||||
|
validate: () => {},
|
||||||
|
unwrap: ast => ast.program,
|
||||||
|
};
|
||||||
@ -1,10 +1,31 @@
|
|||||||
import factory from "./string";
|
// @flow
|
||||||
import template from "./literal";
|
|
||||||
|
|
||||||
export default function(firstArg, ...rest) {
|
import * as formatters from "./formatters";
|
||||||
if (typeof firstArg === "string") {
|
import createTemplateBuilder from "./builder";
|
||||||
return factory(firstArg, ...rest);
|
|
||||||
} else {
|
export const smart = createTemplateBuilder(formatters.smart);
|
||||||
return template(firstArg, ...rest);
|
export const statement = createTemplateBuilder(formatters.statement);
|
||||||
}
|
export const statements = createTemplateBuilder(formatters.statements);
|
||||||
}
|
export const expression = createTemplateBuilder(formatters.expression);
|
||||||
|
export const program = createTemplateBuilder(formatters.program);
|
||||||
|
|
||||||
|
type DefaultTemplateBuilder = typeof smart & {
|
||||||
|
smart: typeof smart,
|
||||||
|
statement: typeof statement,
|
||||||
|
statements: typeof statements,
|
||||||
|
expression: typeof expression,
|
||||||
|
program: typeof program,
|
||||||
|
ast: typeof smart.ast,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Object.assign(
|
||||||
|
((smart.bind(undefined): any): DefaultTemplateBuilder),
|
||||||
|
{
|
||||||
|
smart,
|
||||||
|
statement,
|
||||||
|
statements,
|
||||||
|
expression,
|
||||||
|
program,
|
||||||
|
ast: smart.ast,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -1,97 +1,103 @@
|
|||||||
import * as t from "@babel/types";
|
// @flow
|
||||||
|
|
||||||
import factory from "./string";
|
import type { Formatter } from "./formatters";
|
||||||
|
import { normalizeReplacements, type TemplateOpts } from "./options";
|
||||||
|
import parseAndBuildMetadata from "./parse";
|
||||||
|
import populatePlaceholders from "./populate";
|
||||||
|
|
||||||
export default function template(
|
export default function literalTemplate<T>(
|
||||||
partials: Object | string[],
|
formatter: Formatter<T>,
|
||||||
...args: Array<Object>
|
tpl: Array<string>,
|
||||||
) {
|
opts: TemplateOpts,
|
||||||
if (!Array.isArray(partials)) {
|
): (Array<mixed>) => mixed => T {
|
||||||
// support template({ options })`string`
|
const { metadata, names } = buildLiteralData(formatter, tpl, opts);
|
||||||
return templateApply.bind(undefined, partials);
|
|
||||||
|
return (arg: Array<mixed>) => {
|
||||||
|
const defaultReplacements = arg.reduce((acc, replacement, i) => {
|
||||||
|
acc[names[i]] = replacement;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (arg: mixed) => {
|
||||||
|
const replacements = normalizeReplacements(arg);
|
||||||
|
|
||||||
|
if (replacements) {
|
||||||
|
Object.keys(replacements).forEach(key => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaultReplacements, key)) {
|
||||||
|
throw new Error("Unexpected replacement overlap.");
|
||||||
}
|
}
|
||||||
return templateApply(null, partials, ...args);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function templateApply(
|
return formatter.unwrap(
|
||||||
opts: Object | null,
|
populatePlaceholders(
|
||||||
partials: string[],
|
metadata,
|
||||||
...args: Array<Object>
|
replacements
|
||||||
) {
|
? Object.assign(replacements, defaultReplacements)
|
||||||
if (partials.some(str => str.includes("$BABEL_TEMPLATE$"))) {
|
: defaultReplacements,
|
||||||
throw new Error("Template contains illegal substring $BABEL_TEMPLATE$");
|
),
|
||||||
}
|
|
||||||
|
|
||||||
if (partials.length == 1) {
|
|
||||||
return factory(partials[0], opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const replacementSet = new Set();
|
|
||||||
const replacementMap = new Map();
|
|
||||||
const replacementValueMap = new Map();
|
|
||||||
let hasNonNumericReplacement = false;
|
|
||||||
for (const arg of args) {
|
|
||||||
if (replacementMap.has(arg)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof arg === "number") {
|
|
||||||
replacementMap.set(arg, `$${arg}`);
|
|
||||||
} else if (typeof arg === "string") {
|
|
||||||
// avoid duplicates should t.toIdentifier produce the same result for different arguments
|
|
||||||
const replacementBase = `$BABEL_TEMPLATE$$${t.toIdentifier(arg)}`;
|
|
||||||
let replacement = replacementBase;
|
|
||||||
for (let i = 2; replacementSet.has(replacement); i++) {
|
|
||||||
replacement = `${replacementBase}${i}`;
|
|
||||||
}
|
|
||||||
replacementSet.add(replacement);
|
|
||||||
replacementMap.set(arg, replacement);
|
|
||||||
hasNonNumericReplacement = true;
|
|
||||||
} else {
|
|
||||||
// there can't be duplicates as the size always grows
|
|
||||||
const name = `$BABEL_TEMPLATE$VALUE$${replacementValueMap.size}`;
|
|
||||||
|
|
||||||
// TODO: check if the arg is a Node
|
|
||||||
replacementMap.set(arg, name);
|
|
||||||
replacementValueMap.set(name, arg);
|
|
||||||
hasNonNumericReplacement = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNonNumericReplacement && replacementMap.has(0)) {
|
|
||||||
throw new Error(
|
|
||||||
"Template cannot have a '0' replacement and a named replacement at the same time",
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const code = partials.reduce((acc, partial, i) => {
|
|
||||||
if (acc == null) {
|
|
||||||
return partial;
|
|
||||||
}
|
|
||||||
|
|
||||||
const replacement = replacementMap.get(args[i - 1]);
|
|
||||||
return `${acc}${replacement}${partial}`;
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
const func = factory(code, opts);
|
|
||||||
|
|
||||||
return (...args: Array<Object>) => {
|
|
||||||
if (hasNonNumericReplacement) {
|
|
||||||
const argObj = args[0] || {};
|
|
||||||
const converted = {};
|
|
||||||
|
|
||||||
for (const [key, replacement] of replacementMap) {
|
|
||||||
if (typeof key === "number") continue;
|
|
||||||
if (replacementValueMap.has(replacement)) {
|
|
||||||
converted[replacement] = replacementValueMap.get(replacement);
|
|
||||||
} else {
|
|
||||||
converted[replacement] = argObj[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args[0] = converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(...args);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLiteralData<T>(
|
||||||
|
formatter: Formatter<T>,
|
||||||
|
tpl: Array<string>,
|
||||||
|
opts: TemplateOpts,
|
||||||
|
) {
|
||||||
|
let names;
|
||||||
|
let nameSet;
|
||||||
|
let metadata;
|
||||||
|
let prefix = "";
|
||||||
|
|
||||||
|
do {
|
||||||
|
// If there are cases where the template already contains $0 or any other
|
||||||
|
// matching pattern, we keep adding "$" characters until a unique prefix
|
||||||
|
// is found.
|
||||||
|
prefix += "$";
|
||||||
|
const result = buildTemplateCode(tpl, prefix);
|
||||||
|
|
||||||
|
names = result.names;
|
||||||
|
nameSet = new Set(names);
|
||||||
|
metadata = parseAndBuildMetadata(formatter, formatter.code(result.code), {
|
||||||
|
parser: opts.parser,
|
||||||
|
|
||||||
|
// Explicitly include our generated names in the whitelist so users never
|
||||||
|
// have to think about whether their placeholder pattern will match.
|
||||||
|
placeholderWhitelist: new Set(
|
||||||
|
result.names.concat(
|
||||||
|
opts.placeholderWhitelist
|
||||||
|
? Array.from(opts.placeholderWhitelist)
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
placeholderPattern: opts.placeholderPattern,
|
||||||
|
preserveComments: opts.preserveComments,
|
||||||
|
});
|
||||||
|
} while (
|
||||||
|
metadata.placeholders.some(
|
||||||
|
placeholder => placeholder.isDuplicate && nameSet.has(placeholder.name),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { metadata, names };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTemplateCode(
|
||||||
|
tpl: Array<string>,
|
||||||
|
prefix: string,
|
||||||
|
): { names: Array<string>, code: string } {
|
||||||
|
const names = [];
|
||||||
|
|
||||||
|
let code = tpl[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < tpl.length; i++) {
|
||||||
|
const value = `${prefix}${i - 1}`;
|
||||||
|
names.push(value);
|
||||||
|
|
||||||
|
code += value + tpl[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { names, code };
|
||||||
|
}
|
||||||
|
|||||||
116
packages/babel-template/src/options.js
Normal file
116
packages/babel-template/src/options.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the options that 'babel-template' actually accepts and typechecks
|
||||||
|
* when called. All other options are passed through to the parser.
|
||||||
|
*/
|
||||||
|
export type PublicOpts = {
|
||||||
|
/**
|
||||||
|
* A set of placeholder names to automatically accept, ignoring the given
|
||||||
|
* pattern entirely.
|
||||||
|
*/
|
||||||
|
placeholderWhitelist?: ?Set<string>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pattern to search for when looking for Identifier and StringLiteral
|
||||||
|
* nodes that can be replaced.
|
||||||
|
*
|
||||||
|
* 'false' will disable placeholder searching entirely, leaving only the
|
||||||
|
* 'placeholderWhitelist' value to find replacements.
|
||||||
|
*
|
||||||
|
* Defaults to /^[_$A-Z0-9]+$/.
|
||||||
|
*/
|
||||||
|
placeholderPattern?: ?(RegExp | false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'true' to pass through comments from the template into the resulting AST,
|
||||||
|
* or 'false' to automatically discard comments. Defaults to 'false'.
|
||||||
|
*/
|
||||||
|
preserveComments?: ?boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TemplateOpts = {|
|
||||||
|
parser: {},
|
||||||
|
placeholderWhitelist: Set<string> | void,
|
||||||
|
placeholderPattern: RegExp | false | void,
|
||||||
|
preserveComments: boolean | void,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export function merge(a: TemplateOpts, b: TemplateOpts): TemplateOpts {
|
||||||
|
const {
|
||||||
|
placeholderWhitelist = a.placeholderWhitelist,
|
||||||
|
placeholderPattern = a.placeholderPattern,
|
||||||
|
preserveComments = a.preserveComments,
|
||||||
|
} = b;
|
||||||
|
|
||||||
|
return {
|
||||||
|
parser: Object.assign({}, a.parser, b.parser),
|
||||||
|
placeholderWhitelist,
|
||||||
|
placeholderPattern,
|
||||||
|
preserveComments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate(opts: mixed): TemplateOpts {
|
||||||
|
if (opts != null && typeof opts !== "object") {
|
||||||
|
throw new Error("Unknown template options.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
placeholderWhitelist,
|
||||||
|
placeholderPattern,
|
||||||
|
preserveComments,
|
||||||
|
...parser
|
||||||
|
} =
|
||||||
|
opts || {};
|
||||||
|
|
||||||
|
if (placeholderWhitelist != null && !(placeholderWhitelist instanceof Set)) {
|
||||||
|
throw new Error(
|
||||||
|
"'.placeholderWhitelist' must be a Set, null, or undefined",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
placeholderPattern != null &&
|
||||||
|
!(placeholderPattern instanceof RegExp) &&
|
||||||
|
placeholderPattern !== false
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"'.placeholderPattern' must be a RegExp, false, null, or undefined",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preserveComments != null && typeof preserveComments !== "boolean") {
|
||||||
|
throw new Error(
|
||||||
|
"'.preserveComments' must be a boolean, null, or undefined",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parser,
|
||||||
|
placeholderWhitelist: placeholderWhitelist || undefined,
|
||||||
|
placeholderPattern:
|
||||||
|
placeholderPattern == null ? undefined : placeholderPattern,
|
||||||
|
preserveComments: preserveComments == null ? false : preserveComments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PublicReplacements = { [string]: mixed } | Array<mixed>;
|
||||||
|
export type TemplateReplacements = { [string]: mixed } | void;
|
||||||
|
|
||||||
|
export function normalizeReplacements(
|
||||||
|
replacements: mixed,
|
||||||
|
): TemplateReplacements {
|
||||||
|
if (Array.isArray(replacements)) {
|
||||||
|
return replacements.reduce((acc, replacement, i) => {
|
||||||
|
acc["$" + i] = replacement;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
} else if (typeof replacements === "object" || replacements == null) {
|
||||||
|
return replacements || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
"Template replacements must be an array, object, null, or undefined",
|
||||||
|
);
|
||||||
|
}
|
||||||
156
packages/babel-template/src/parse.js
Normal file
156
packages/babel-template/src/parse.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// @flow
|
||||||
|
import * as t from "@babel/types";
|
||||||
|
import type { TraversalAncestors, TraversalHandler } from "@babel/types";
|
||||||
|
import { parse } from "babylon";
|
||||||
|
import { codeFrameColumns } from "@babel/code-frame";
|
||||||
|
import type { TemplateOpts } from "./options";
|
||||||
|
import type { Formatter } from "./formatters";
|
||||||
|
|
||||||
|
export type Metadata = {
|
||||||
|
ast: BabelNodeFile,
|
||||||
|
placeholders: Array<Placeholder>,
|
||||||
|
placeholderNames: Set<string>,
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaceholderType = "string" | "param" | "statement" | "other";
|
||||||
|
export type Placeholder = {|
|
||||||
|
name: string,
|
||||||
|
resolve: BabelNodeFile => { parent: BabelNode, key: string, index?: number },
|
||||||
|
type: PlaceholderType,
|
||||||
|
isDuplicate: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
|
const PATTERN = /^[_$A-Z0-9]+$/;
|
||||||
|
|
||||||
|
export default function parseAndBuildMetadata<T>(
|
||||||
|
formatter: Formatter<T>,
|
||||||
|
code: string,
|
||||||
|
opts: TemplateOpts,
|
||||||
|
): Metadata {
|
||||||
|
const ast = parseWithCodeFrame(code, opts.parser);
|
||||||
|
|
||||||
|
const {
|
||||||
|
placeholderWhitelist,
|
||||||
|
placeholderPattern = PATTERN,
|
||||||
|
preserveComments,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
t.removePropertiesDeep(ast, {
|
||||||
|
preserveComments,
|
||||||
|
});
|
||||||
|
|
||||||
|
formatter.validate(ast);
|
||||||
|
|
||||||
|
const placeholders = [];
|
||||||
|
const placeholderNames = new Set();
|
||||||
|
|
||||||
|
t.traverse(ast, (placeholderVisitorHandler: TraversalHandler<*>), {
|
||||||
|
placeholders,
|
||||||
|
placeholderNames,
|
||||||
|
placeholderWhitelist,
|
||||||
|
placeholderPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ast,
|
||||||
|
placeholders,
|
||||||
|
placeholderNames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeholderVisitorHandler(
|
||||||
|
node: BabelNode,
|
||||||
|
ancestors: TraversalAncestors,
|
||||||
|
state: MetadataState,
|
||||||
|
) {
|
||||||
|
let name;
|
||||||
|
if (t.isIdentifier(node)) {
|
||||||
|
name = ((node: any): BabelNodeIdentifier).name;
|
||||||
|
} else if (t.isStringLiteral(node)) {
|
||||||
|
name = ((node: any): BabelNodeStringLiteral).value;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!state.placeholderPattern || !state.placeholderPattern.test(name)) &&
|
||||||
|
(!state.placeholderWhitelist || !state.placeholderWhitelist.has(name))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep our own copy of the ancestors so we can use it in .resolve().
|
||||||
|
ancestors = ancestors.slice();
|
||||||
|
|
||||||
|
const { node: parent, key } = ancestors[ancestors.length - 1];
|
||||||
|
|
||||||
|
let type: PlaceholderType;
|
||||||
|
if (t.isStringLiteral(node)) {
|
||||||
|
type = "string";
|
||||||
|
} else if (
|
||||||
|
(t.isNewExpression(parent) && key === "arguments") ||
|
||||||
|
(t.isCallExpression(parent) && key === "arguments") ||
|
||||||
|
(t.isFunction(parent) && key === "params")
|
||||||
|
) {
|
||||||
|
type = "param";
|
||||||
|
} else if (t.isExpressionStatement(parent)) {
|
||||||
|
type = "statement";
|
||||||
|
ancestors = ancestors.slice(0, -1);
|
||||||
|
} else {
|
||||||
|
type = "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
state.placeholders.push({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
resolve: ast => resolveAncestors(ast, ancestors),
|
||||||
|
isDuplicate: state.placeholderNames.has(name),
|
||||||
|
});
|
||||||
|
state.placeholderNames.add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAncestors(ast: BabelNodeFile, ancestors: TraversalAncestors) {
|
||||||
|
let parent: BabelNode = ast;
|
||||||
|
for (let i = 0; i < ancestors.length - 1; i++) {
|
||||||
|
const { key, index } = ancestors[i];
|
||||||
|
|
||||||
|
if (index === undefined) {
|
||||||
|
parent = (parent: any)[key];
|
||||||
|
} else {
|
||||||
|
parent = (parent: any)[key][index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key, index } = ancestors[ancestors.length - 1];
|
||||||
|
|
||||||
|
return { parent, key, index };
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetadataState = {
|
||||||
|
placeholders: Array<Placeholder>,
|
||||||
|
placeholderNames: Set<string>,
|
||||||
|
placeholderWhitelist: Set<string> | void,
|
||||||
|
placeholderPattern: RegExp | false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseWithCodeFrame(code: string, parserOpts: {}): BabelNodeFile {
|
||||||
|
parserOpts = Object.assign(
|
||||||
|
{
|
||||||
|
allowReturnOutsideFunction: true,
|
||||||
|
allowSuperOutsideMethod: true,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
parserOpts,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parse(code, parserOpts);
|
||||||
|
} catch (err) {
|
||||||
|
const loc = err.loc;
|
||||||
|
if (loc) {
|
||||||
|
err.loc = null;
|
||||||
|
err.message += "\n" + codeFrameColumns(code, { start: loc });
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
131
packages/babel-template/src/populate.js
Normal file
131
packages/babel-template/src/populate.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// @flow
|
||||||
|
import * as t from "@babel/types";
|
||||||
|
|
||||||
|
import type { TemplateReplacements } from "./options";
|
||||||
|
import type { Metadata, Placeholder } from "./parse";
|
||||||
|
|
||||||
|
export default function populatePlaceholders(
|
||||||
|
metadata: Metadata,
|
||||||
|
replacements: TemplateReplacements,
|
||||||
|
): BabelNodeFile {
|
||||||
|
const ast = t.cloneDeep(metadata.ast);
|
||||||
|
|
||||||
|
if (replacements) {
|
||||||
|
metadata.placeholders.forEach(placeholder => {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(replacements, placeholder.name)
|
||||||
|
) {
|
||||||
|
throw new Error(`No substitution given for "${placeholder.name}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.keys(replacements).forEach(key => {
|
||||||
|
if (!metadata.placeholderNames.has(key)) {
|
||||||
|
throw new Error(`Unknown substitution "${key}" given`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process in reverse order to AST mutation doesn't change indices that
|
||||||
|
// will be needed for later calls to `placeholder.resolve()`.
|
||||||
|
metadata.placeholders
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.forEach(placeholder => {
|
||||||
|
try {
|
||||||
|
applyReplacement(
|
||||||
|
placeholder,
|
||||||
|
ast,
|
||||||
|
(replacements && replacements[placeholder.name]) || null,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
e.message = `babel-template placeholder "${placeholder.name}": ${e.message}`;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyReplacement(
|
||||||
|
placeholder: Placeholder,
|
||||||
|
ast: BabelNodeFile,
|
||||||
|
replacement: any,
|
||||||
|
) {
|
||||||
|
// Track inserted nodes and clone them if they are inserted more than
|
||||||
|
// once to avoid injecting the same node multiple times.
|
||||||
|
if (placeholder.isDuplicate) {
|
||||||
|
if (Array.isArray(replacement)) {
|
||||||
|
replacement = replacement.map(node => t.cloneDeep(node));
|
||||||
|
} else if (typeof replacement === "object") {
|
||||||
|
replacement = t.cloneDeep(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parent, key, index } = placeholder.resolve(ast);
|
||||||
|
|
||||||
|
if (placeholder.type === "string") {
|
||||||
|
if (typeof replacement === "string") {
|
||||||
|
replacement = t.stringLiteral(replacement);
|
||||||
|
}
|
||||||
|
if (!replacement || !t.isStringLiteral(replacement)) {
|
||||||
|
throw new Error("Expected string substitution");
|
||||||
|
}
|
||||||
|
} else if (placeholder.type === "statement") {
|
||||||
|
if (index === undefined) {
|
||||||
|
if (!replacement) {
|
||||||
|
replacement = t.emptyStatement();
|
||||||
|
} else if (Array.isArray(replacement)) {
|
||||||
|
replacement = t.blockStatement(replacement);
|
||||||
|
} else if (typeof replacement === "string") {
|
||||||
|
replacement = t.expressionStatement(t.identifier(replacement));
|
||||||
|
} else if (!t.isStatement(replacement)) {
|
||||||
|
replacement = t.expressionStatement((replacement: any));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (replacement && !Array.isArray(replacement)) {
|
||||||
|
if (typeof replacement === "string") {
|
||||||
|
replacement = t.identifier(replacement);
|
||||||
|
}
|
||||||
|
if (!t.isStatement(replacement)) {
|
||||||
|
replacement = t.expressionStatement((replacement: any));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (placeholder.type === "param") {
|
||||||
|
if (typeof replacement === "string") {
|
||||||
|
replacement = t.identifier(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === undefined) throw new Error("Assertion failure.");
|
||||||
|
} else {
|
||||||
|
if (typeof replacement === "string") {
|
||||||
|
replacement = t.identifier(replacement);
|
||||||
|
}
|
||||||
|
if (Array.isArray(replacement)) {
|
||||||
|
throw new Error("Cannot replace single expression with an array.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === undefined) {
|
||||||
|
t.validate(parent, key, replacement);
|
||||||
|
|
||||||
|
(parent: any)[key] = replacement;
|
||||||
|
} else {
|
||||||
|
const items: Array<BabelNode> = (parent: any)[key].slice();
|
||||||
|
|
||||||
|
if (placeholder.type === "statement" || placeholder.type === "param") {
|
||||||
|
if (replacement == null) {
|
||||||
|
items.splice(index, 1);
|
||||||
|
} else if (Array.isArray(replacement)) {
|
||||||
|
items.splice(index, 1, ...replacement);
|
||||||
|
} else {
|
||||||
|
items[index] = replacement;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items[index] = replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.validate(parent, key, items);
|
||||||
|
(parent: any)[key] = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,122 +1,23 @@
|
|||||||
import cloneDeep from "lodash/cloneDeep";
|
// @flow
|
||||||
import has from "lodash/has";
|
import type { Formatter } from "./formatters";
|
||||||
import traverse from "@babel/traverse";
|
import { normalizeReplacements, type TemplateOpts } from "./options";
|
||||||
import * as babylon from "babylon";
|
import parseAndBuildMetadata from "./parse";
|
||||||
import { codeFrameColumns } from "@babel/code-frame";
|
import populatePlaceholders from "./populate";
|
||||||
|
|
||||||
const FROM_TEMPLATE = new Set();
|
export default function stringTemplate<T>(
|
||||||
|
formatter: Formatter<T>,
|
||||||
|
code: string,
|
||||||
|
opts: TemplateOpts,
|
||||||
|
): mixed => T {
|
||||||
|
code = formatter.code(code);
|
||||||
|
|
||||||
export default function factory(code: string, opts?: Object): Function {
|
let metadata;
|
||||||
// since we lazy parse the template, we get the current stack so we have the
|
|
||||||
// original stack to append if it errors when parsing
|
|
||||||
let stack;
|
|
||||||
try {
|
|
||||||
// error stack gets populated in IE only on throw
|
|
||||||
// (https://msdn.microsoft.com/en-us/library/hh699850(v=vs.94).aspx)
|
|
||||||
throw new Error();
|
|
||||||
} catch (error) {
|
|
||||||
if (error.stack) {
|
|
||||||
// error.stack does not exists in IE <= 9
|
|
||||||
stack = error.stack
|
|
||||||
.split("\n")
|
|
||||||
.slice(2)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = Object.assign(
|
return (arg?: mixed) => {
|
||||||
{
|
const replacements = normalizeReplacements(arg);
|
||||||
allowReturnOutsideFunction: true,
|
|
||||||
allowSuperOutsideMethod: true,
|
|
||||||
preserveComments: false,
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
opts,
|
|
||||||
);
|
|
||||||
|
|
||||||
let getAst = function() {
|
if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts);
|
||||||
let ast;
|
|
||||||
|
|
||||||
try {
|
return formatter.unwrap(populatePlaceholders(metadata, replacements));
|
||||||
ast = babylon.parse(code, opts);
|
|
||||||
|
|
||||||
ast = traverse.removeProperties(ast, {
|
|
||||||
preserveComments: opts.preserveComments,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
const loc = err.loc;
|
|
||||||
if (loc) {
|
|
||||||
err.loc = null;
|
|
||||||
err.message += "\n" + codeFrameColumns(code, { start: loc });
|
|
||||||
}
|
|
||||||
err.stack = `${err.stack}\n ==========================\n${stack}`;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAst = function() {
|
|
||||||
return ast;
|
|
||||||
};
|
|
||||||
|
|
||||||
return ast;
|
|
||||||
};
|
|
||||||
|
|
||||||
return function(...args) {
|
|
||||||
return useTemplate(getAst(), args);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTemplate(ast, nodes?: Array<Object>) {
|
|
||||||
ast = cloneDeep(ast);
|
|
||||||
const { program } = ast;
|
|
||||||
|
|
||||||
if (nodes.length) {
|
|
||||||
traverse.cheap(ast, function(node) {
|
|
||||||
FROM_TEMPLATE.add(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
traverse(ast, templateVisitor, null, nodes);
|
|
||||||
|
|
||||||
FROM_TEMPLATE.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (program.body.length > 1) {
|
|
||||||
return program.body;
|
|
||||||
} else {
|
|
||||||
return program.body[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateVisitor = {
|
|
||||||
// 360
|
|
||||||
noScope: true,
|
|
||||||
|
|
||||||
Identifier(path, args) {
|
|
||||||
const { node, parentPath } = path;
|
|
||||||
if (!FROM_TEMPLATE.has(node)) return path.skip();
|
|
||||||
|
|
||||||
let replacement;
|
|
||||||
if (has(args[0], node.name)) {
|
|
||||||
replacement = args[0][node.name];
|
|
||||||
} else if (node.name[0] === "$") {
|
|
||||||
const i = +node.name.slice(1);
|
|
||||||
if (args[i]) replacement = args[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentPath.isExpressionStatement()) {
|
|
||||||
path = parentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replacement === null) {
|
|
||||||
path.remove();
|
|
||||||
} else if (replacement) {
|
|
||||||
path.replaceInline(replacement);
|
|
||||||
path.skip();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
exit({ node }) {
|
|
||||||
if (!node.loc) {
|
|
||||||
traverse.clearNode(node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,34 +1,202 @@
|
|||||||
import generator from "../../babel-generator";
|
import generator from "../../babel-generator";
|
||||||
import template from "../lib";
|
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;";
|
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() {
|
it("import statements are allowed by default", function() {
|
||||||
chai
|
expect(function() {
|
||||||
.expect(function() {
|
|
||||||
template("import foo from 'foo'")({});
|
template("import foo from 'foo'")({});
|
||||||
})
|
}).not.to.throw();
|
||||||
.not.to.throw();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("with statements are allowed with sourceType: script", function() {
|
it("with statements are allowed with sourceType: script", function() {
|
||||||
chai
|
expect(function() {
|
||||||
.expect(function() {
|
|
||||||
template("with({}){}", { sourceType: "script" })({});
|
template("with({}){}", { sourceType: "script" })({});
|
||||||
})
|
}).not.to.throw();
|
||||||
.not.to.throw();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should strip comments by default", function() {
|
it("should strip comments by default", function() {
|
||||||
const code = "const add = (a, b) => a + b;";
|
const code = "const add = (a, b) => a + b;";
|
||||||
const output = template(comments)();
|
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() {
|
it("should preserve comments with a flag", function() {
|
||||||
const output = template(comments, { preserveComments: true })();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user