From ef185feb352340289387db062171c26dfce71d58 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Sat, 7 Oct 2017 22:06:41 -0400 Subject: [PATCH] Split template module into multiple files. --- packages/babel-template/src/index.js | 216 +------------------------ packages/babel-template/src/literal.js | 97 +++++++++++ packages/babel-template/src/string.js | 122 ++++++++++++++ 3 files changed, 221 insertions(+), 214 deletions(-) create mode 100644 packages/babel-template/src/literal.js create mode 100644 packages/babel-template/src/string.js diff --git a/packages/babel-template/src/index.js b/packages/babel-template/src/index.js index 7e92305b5c..67d1472359 100644 --- a/packages/babel-template/src/index.js +++ b/packages/babel-template/src/index.js @@ -1,11 +1,5 @@ -import cloneDeep from "lodash/cloneDeep"; -import has from "lodash/has"; -import traverse from "@babel/traverse"; -import * as babylon from "babylon"; -import { codeFrameColumns } from "@babel/code-frame"; -import * as t from "@babel/types"; - -const FROM_TEMPLATE = new Set(); +import factory from "./string"; +import template from "./literal"; export default function(firstArg, ...rest) { if (typeof firstArg === "string") { @@ -14,209 +8,3 @@ export default function(firstArg, ...rest) { return template(firstArg, ...rest); } } - -function template(partials: Object | string[], ...args: Array) { - if (!Array.isArray(partials)) { - // support template({ options })`string` - return templateApply.bind(undefined, partials); - } - return templateApply(null, partials, ...args); -} - -function templateApply( - opts: Object | null, - partials: string[], - ...args: Array -) { - if (partials.some(str => str.includes("$BABEL_TEMPLATE$"))) { - 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) => { - 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 factory(code: string, opts?: Object): Function { - // 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( - { - allowReturnOutsideFunction: true, - allowSuperOutsideMethod: true, - preserveComments: false, - sourceType: "module", - }, - opts, - ); - - let getAst = function() { - let ast; - - try { - 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) { - 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); - } - }, -}; diff --git a/packages/babel-template/src/literal.js b/packages/babel-template/src/literal.js new file mode 100644 index 0000000000..4e7a12c36b --- /dev/null +++ b/packages/babel-template/src/literal.js @@ -0,0 +1,97 @@ +import * as t from "@babel/types"; + +import factory from "./string"; + +export default function template( + partials: Object | string[], + ...args: Array +) { + if (!Array.isArray(partials)) { + // support template({ options })`string` + return templateApply.bind(undefined, partials); + } + return templateApply(null, partials, ...args); +} + +function templateApply( + opts: Object | null, + partials: string[], + ...args: Array +) { + if (partials.some(str => str.includes("$BABEL_TEMPLATE$"))) { + 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) => { + 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); + }; +} diff --git a/packages/babel-template/src/string.js b/packages/babel-template/src/string.js new file mode 100644 index 0000000000..533a85657f --- /dev/null +++ b/packages/babel-template/src/string.js @@ -0,0 +1,122 @@ +import cloneDeep from "lodash/cloneDeep"; +import has from "lodash/has"; +import traverse from "@babel/traverse"; +import * as babylon from "babylon"; +import { codeFrameColumns } from "@babel/code-frame"; + +const FROM_TEMPLATE = new Set(); + +export default function factory(code: string, opts?: Object): Function { + // 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( + { + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + preserveComments: false, + sourceType: "module", + }, + opts, + ); + + let getAst = function() { + let ast; + + try { + 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) { + 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); + } + }, +};