UNPKG

@babel/template

Version:

Generate an AST from a string template.

546 lines (536 loc) 16.9 kB
import * as _t from '@babel/types'; import { parse } from '@babel/parser'; import { codeFrameColumns } from '@babel/code-frame'; const { assertExpressionStatement } = _t; function makeStatementFormatter(fn) { return { code: str => `/* @babel/template */;\n${str}`, validate: () => {}, unwrap: ast => { return fn(ast.program.body.slice(1)); } }; } const smart$1 = makeStatementFormatter(body => { if (body.length > 1) { return body; } else { return body[0]; } }); const statements$1 = makeStatementFormatter(body => body); const statement$1 = makeStatementFormatter(body => { 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]; }); const expression$1 = { code: str => `(\n${str}\n)`, validate: ast => { if (ast.program.body.length > 1) { throw new Error("Found multiple statements but wanted one"); } if (expression$1.unwrap(ast).start === 0) { throw new Error("Parse result included parens."); } }, unwrap: ({ program }) => { const [stmt] = program.body; assertExpressionStatement(stmt); return stmt.expression; } }; const program$1 = { code: str => str, validate: () => {}, unwrap: ast => ast.program }; function merge(a, b) { const { placeholderAllowlist = a.placeholderAllowlist, placeholderPattern = a.placeholderPattern, preserveComments = a.preserveComments, syntacticPlaceholders = a.syntacticPlaceholders } = b; return { parser: { ...a.parser, ...b.parser }, placeholderAllowlist, placeholderPattern, preserveComments, syntacticPlaceholders }; } function validate$1(opts) { if (opts != null && typeof opts !== "object") { throw new Error("Unknown template options."); } if (opts != null && Object.hasOwn(opts, "placeholderWhitelist")) { if (!Object.hasOwn(opts, "placeholderAllowlist")) { throw new Error("The 'placeholderWhitelist' option has been renamed to " + "'placeholderAllowlist'. Please update your configuration."); } } const { placeholderAllowlist, placeholderPattern, preserveComments, syntacticPlaceholders, ...parser } = opts || {}; if (placeholderAllowlist != null && !(placeholderAllowlist instanceof Set)) { throw new Error("'.placeholderAllowlist' 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"); } if (syntacticPlaceholders != null && typeof syntacticPlaceholders !== "boolean") { throw new Error("'.syntacticPlaceholders' must be a boolean, null, or undefined"); } if (syntacticPlaceholders === true && (placeholderAllowlist != null || placeholderPattern != null)) { throw new Error("'.placeholderAllowlist' and '.placeholderPattern' aren't compatible" + " with '.syntacticPlaceholders: true'"); } return { parser, placeholderAllowlist: placeholderAllowlist || undefined, placeholderPattern: placeholderPattern == null ? undefined : placeholderPattern, preserveComments: preserveComments == null ? undefined : preserveComments, syntacticPlaceholders: syntacticPlaceholders == null ? undefined : syntacticPlaceholders }; } function normalizeReplacements(replacements) { 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"); } const { isCallExpression, isExpressionStatement, isFunction, isIdentifier, isJSXIdentifier, isNewExpression, isPlaceholder, isStatement: isStatement$1, isStringLiteral: isStringLiteral$1, removePropertiesDeep, traverse } = _t; const PATTERN = /^[_$A-Z0-9]+$/; function parseAndBuildMetadata(formatter, code, opts) { const { placeholderAllowlist, placeholderPattern, preserveComments = false, syntacticPlaceholders } = opts; const ast = parseWithCodeFrame(code, opts.parser, syntacticPlaceholders); removePropertiesDeep(ast, { preserveComments }); formatter.validate(ast); const state = { syntactic: { placeholders: [], placeholderNames: new Set() }, legacy: { placeholders: [], placeholderNames: new Set() }, placeholderAllowlist, placeholderPattern, syntacticPlaceholders }; traverse(ast, placeholderVisitorHandler, state); return { ast, ...(state.syntactic.placeholders.length ? state.syntactic : state.legacy) }; } function placeholderVisitorHandler(node, ancestors, state) { let name; let hasSyntacticPlaceholders = state.syntactic.placeholders.length > 0; if (isPlaceholder(node)) { if (state.syntacticPlaceholders === false) { throw new Error("%%foo%%-style placeholders can't be used when " + "'.syntacticPlaceholders' is false."); } name = node.name.name; hasSyntacticPlaceholders = true; } else if (hasSyntacticPlaceholders || state.syntacticPlaceholders) { return; } else if (isIdentifier(node) || isJSXIdentifier(node)) { name = node.name; } else if (isStringLiteral$1(node)) { name = node.value; } else { return; } if (hasSyntacticPlaceholders && (state.placeholderPattern != null || state.placeholderAllowlist != null)) { throw new Error("'.placeholderAllowlist' and '.placeholderPattern' aren't compatible" + " with '.syntacticPlaceholders: true'"); } if (!hasSyntacticPlaceholders && (state.placeholderPattern === false || !(state.placeholderPattern || PATTERN).test(name)) && !state.placeholderAllowlist?.has(name)) { return; } ancestors = ancestors.slice(); const { node: parent, key } = ancestors[ancestors.length - 1]; let type; if (isStringLiteral$1(node) || isPlaceholder(node, { expectedNode: "StringLiteral" })) { type = "string"; } else if (isNewExpression(parent) && key === "arguments" || isCallExpression(parent) && key === "arguments" || isFunction(parent) && key === "params") { type = "param"; } else if (isExpressionStatement(parent) && !isPlaceholder(node)) { type = "statement"; ancestors = ancestors.slice(0, -1); } else if (isStatement$1(node) && isPlaceholder(node)) { type = "statement"; } else { type = "other"; } const { placeholders, placeholderNames } = !hasSyntacticPlaceholders ? state.legacy : state.syntactic; placeholders.push({ name, type, resolve: ast => resolveAncestors(ast, ancestors), isDuplicate: placeholderNames.has(name) }); placeholderNames.add(name); } function resolveAncestors(ast, ancestors) { let parent = ast; for (let i = 0; i < ancestors.length - 1; i++) { const { key, index } = ancestors[i]; if (index === undefined) { parent = parent[key]; } else { parent = parent[key][index]; } } const { key, index } = ancestors[ancestors.length - 1]; return { parent, key, index }; } function parseWithCodeFrame(code, parserOpts, syntacticPlaceholders) { const plugins = (parserOpts.plugins || []).slice(); if (syntacticPlaceholders !== false) { plugins.push("placeholders"); } parserOpts = { allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true, allowNewTargetOutsideFunction: true, allowSuperOutsideMethod: true, allowYieldOutsideFunction: true, sourceType: "module", ...parserOpts, plugins }; try { return parse(code, parserOpts); } catch (err) { const loc = err.loc; if (loc) { err.message += "\n" + codeFrameColumns(code, { start: loc }); err.code = "BABEL_TEMPLATE_PARSE_ERROR"; } throw err; } } const { blockStatement, cloneNode, emptyStatement, expressionStatement, identifier, isStatement, isStringLiteral, stringLiteral, validate } = _t; function populatePlaceholders(metadata, replacements) { const ast = cloneNode(metadata.ast); if (replacements) { metadata.placeholders.forEach(placeholder => { if (!Object.hasOwn(replacements, placeholder.name)) { const placeholderName = placeholder.name; throw new Error(`Error: No substitution given for "${placeholderName}". 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(['${placeholderName}'])} - { placeholderPattern: /^${placeholderName}$/ }`); } }); Object.keys(replacements).forEach(key => { if (!metadata.placeholderNames.has(key)) { throw new Error(`Unknown substitution "${key}" given`); } }); } 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, ast, replacement) { if (placeholder.isDuplicate) { if (Array.isArray(replacement)) { replacement = replacement.map(node => cloneNode(node)); } else if (typeof replacement === "object") { replacement = cloneNode(replacement); } } const { parent, key, index } = placeholder.resolve(ast); if (placeholder.type === "string") { if (typeof replacement === "string") { replacement = stringLiteral(replacement); } if (!replacement || !isStringLiteral(replacement)) { throw new Error("Expected string substitution"); } } else if (placeholder.type === "statement") { if (index === undefined) { if (!replacement) { replacement = emptyStatement(); } else if (Array.isArray(replacement)) { replacement = blockStatement(replacement); } else if (typeof replacement === "string") { replacement = expressionStatement(identifier(replacement)); } else if (!isStatement(replacement)) { replacement = expressionStatement(replacement); } } else { if (replacement && !Array.isArray(replacement)) { if (typeof replacement === "string") { replacement = identifier(replacement); } if (!isStatement(replacement)) { replacement = expressionStatement(replacement); } } } } else if (placeholder.type === "param") { if (typeof replacement === "string") { replacement = identifier(replacement); } if (index === undefined) throw new Error("Assertion failure."); } else { if (typeof replacement === "string") { replacement = identifier(replacement); } if (Array.isArray(replacement)) { throw new Error("Cannot replace single expression with an array."); } } function set(parent, key, value) { const node = parent[key]; parent[key] = value; if (node.type === "Identifier" || node.type === "Placeholder") { if (node.typeAnnotation) { value.typeAnnotation = node.typeAnnotation; } if (node.optional) { value.optional = node.optional; } if (node.decorators) { value.decorators = node.decorators; } } } if (index === undefined) { validate(parent, key, replacement); set(parent, key, replacement); } else { const items = parent[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 { set(items, index, replacement); } } else { set(items, index, replacement); } validate(parent, key, items); parent[key] = items; } } function stringTemplate(formatter, code, opts) { code = formatter.code(code); let metadata; return arg => { const replacements = normalizeReplacements(arg); if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts); return formatter.unwrap(populatePlaceholders(metadata, replacements)); }; } function literalTemplate(formatter, tpl, opts) { const { metadata, names } = buildLiteralData(formatter, tpl, opts); return arg => { const defaultReplacements = {}; arg.forEach((replacement, i) => { defaultReplacements[names[i]] = replacement; }); return arg => { const replacements = normalizeReplacements(arg); if (replacements) { Object.keys(replacements).forEach(key => { if (Object.hasOwn(defaultReplacements, key)) { throw new Error("Unexpected replacement overlap."); } }); } return formatter.unwrap(populatePlaceholders(metadata, replacements ? Object.assign(replacements, defaultReplacements) : defaultReplacements)); }; }; } function buildLiteralData(formatter, tpl, opts) { let prefix = "BABEL_TPL$"; const raw = tpl.join(""); do { prefix = "$$" + prefix; } while (raw.includes(prefix)); const { names, code } = buildTemplateCode(tpl, prefix); const metadata = parseAndBuildMetadata(formatter, formatter.code(code), { parser: opts.parser, placeholderAllowlist: new Set(names.concat(opts.placeholderAllowlist ? Array.from(opts.placeholderAllowlist) : [])), placeholderPattern: opts.placeholderPattern, preserveComments: opts.preserveComments, syntacticPlaceholders: opts.syntacticPlaceholders }); return { metadata, names }; } function buildTemplateCode(tpl, prefix) { 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 }; } const NO_PLACEHOLDER = validate$1({ placeholderPattern: false }); function createTemplateBuilder(formatter, defaultOpts) { const templateFnCache = new WeakMap(); const templateAstCache = new WeakMap(); const cachedOpts = defaultOpts || validate$1(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$1(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$1(tpl))); } throw new Error(`Unexpected template param ${typeof tpl}`); }, { 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$1(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(fn) { const rootErr = new Error(); return arg => { try { return fn(arg); } catch (err) { err.stack += `\n =============\n${rootErr.stack.split("\n").slice(3).join("\n")}`; throw err; } }; } const smart = createTemplateBuilder(smart$1); const statement = createTemplateBuilder(statement$1); const statements = createTemplateBuilder(statements$1); const expression = createTemplateBuilder(expression$1); const program = createTemplateBuilder(program$1); const index = Object.assign(smart.bind(undefined), { smart, statement, statements, expression, program, ast: smart.ast }); export { index as default, expression, program, smart, statement, statements }; //# sourceMappingURL=index.js.map