Convert @babel/template from Flow to TS (#12317)

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
This commit is contained in:
Bogdan Savluk 2020-11-06 03:19:19 +01:00 committed by Nicolò Ribaudo
parent f80478c06d
commit 089c200c8b
11 changed files with 218 additions and 203 deletions

View File

@ -0,0 +1,36 @@
declare module "@babel/template" {
declare type PublicOpts = {
placeholderWhitelist?: ?Set<string>,
placeholderPattern?: ?(RegExp | false),
preserveComments?: ?boolean,
syntacticPlaceholders?: ?boolean,
};
declare type PublicReplacements = { [string]: mixed } | Array<mixed>;
declare type TemplateBuilder<T> = {
ast: {
(tpl: string, opts: ?PublicOpts): T,
(tpl: Array<string>, ...args: Array<mixed>): T,
},
(opts: PublicOpts): TemplateBuilder<T>,
(tpl: string, opts: ?PublicOpts): (?PublicReplacements) => T,
(tpl: Array<string>, ...args: Array<mixed>): (?PublicReplacements) => T,
};
declare type Smart = TemplateBuilder<
Array<BabelNodeStatement> | BabelNodeStatement
>;
declare type Statement = TemplateBuilder<BabelNodeStatement>;
declare type Statements = TemplateBuilder<Array<BabelNodeStatement>>;
declare type Expression = TemplateBuilder<BabelNodeExpression>;
declare type Program = TemplateBuilder<BabelNodeProgram>;
declare export default Smart & {
smart: Smart,
statement: Statement,
statements: Statements,
expression: Expression,
program: Program,
};
}

View File

@ -1,12 +1,5 @@
// @flow import { merge, validate } from "./options";
import type { TemplateOpts, PublicOpts, PublicReplacements } from "./options";
import {
merge,
validate,
type TemplateOpts,
type PublicOpts,
type PublicReplacements,
} from "./options";
import type { Formatter } from "./formatters"; import type { Formatter } from "./formatters";
import stringTemplate from "./string"; import stringTemplate from "./string";
@ -14,20 +7,22 @@ import literalTemplate from "./literal";
export type TemplateBuilder<T> = { export type TemplateBuilder<T> = {
// Build a new builder, merging the given options with the previous ones. // Build a new builder, merging the given options with the previous ones.
(opts: PublicOpts): TemplateBuilder<T>, (opts: PublicOpts): TemplateBuilder<T>;
// Building from a string produces an AST builder function by default. // Building from a string produces an AST builder function by default.
(tpl: string, opts: ?PublicOpts): (?PublicReplacements) => T, (tpl: string, opts?: PublicOpts): (replacements?: PublicReplacements) => T;
// Building from a template literal produces an AST builder function by default. // Building from a template literal produces an AST builder function by default.
(tpl: Array<string>, ...args: Array<mixed>): (?PublicReplacements) => T, (tpl: TemplateStringsArray, ...args: Array<any>): (
replacements?: PublicReplacements,
) => T;
// Allow users to explicitly create templates that produce ASTs, skipping // Allow users to explicitly create templates that produce ASTs, skipping
// the need for an intermediate function. // the need for an intermediate function.
ast: { ast: {
(tpl: string, opts: ?PublicOpts): T, (tpl: string, opts?: PublicOpts): T;
(tpl: Array<string>, ...args: Array<mixed>): T, (tpl: TemplateStringsArray, ...args: Array<any>): T;
}, };
}; };
// Prebuild the options that will be used when parsing a `.ast` template. // Prebuild the options that will be used when parsing a `.ast` template.
@ -69,7 +64,7 @@ export default function createTemplateBuilder<T>(
); );
} }
throw new Error(`Unexpected template param ${typeof tpl}`); throw new Error(`Unexpected template param ${typeof tpl}`);
}: Function), }) as TemplateBuilder<T>,
{ {
ast: (tpl, ...args) => { ast: (tpl, ...args) => {
if (typeof tpl === "string") { if (typeof tpl === "string") {
@ -98,7 +93,9 @@ export default function createTemplateBuilder<T>(
); );
} }
function extendedTrace<Arg, Result>(fn: Arg => Result): Arg => Result { function extendedTrace<Arg, Result>(
fn: (_: Arg) => Result,
): (_: Arg) => Result {
// Since we lazy parse the template, we get the current stack so we have the // Since we lazy parse the template, we get the current stack so we have the
// original stack to append if it errors when parsing // original stack to append if it errors when parsing
let rootStack = ""; let rootStack = "";

View File

@ -1,75 +0,0 @@
// @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.
// Alongside 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: ({ program }) => {
if (program.body.length > 1) {
throw new Error("Found multiple statements but wanted one");
}
// $FlowFixMe
const expression = program.body[0].expression;
if (expression.start === 0) {
throw new Error("Parse result included parens.");
}
},
// $FlowFixMe
unwrap: ast => ast.program.body[0].expression,
};
export const program: Formatter<BabelNodeProgram> = {
code: str => str,
validate: () => {},
unwrap: ast => ast.program,
};

View File

@ -0,0 +1,70 @@
import * as t from "@babel/types";
export type Formatter<T> = {
code: (source: string) => string;
validate: (ast: t.File) => void;
unwrap: (ast: t.File) => T;
};
function makeStatementFormatter<T>(
fn: (statements: Array<t.Statement>) => T,
): Formatter<T> {
return {
// We need to prepend a ";" to force statement parsing so that
// ExpressionStatement strings won't be parsed as directives.
// Alongside 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: t.File): T => {
return fn(ast.program.body.slice(1));
},
};
}
export const smart = makeStatementFormatter(body => {
if (body.length > 1) {
return body;
} else {
return body[0];
}
});
export const statements = makeStatementFormatter(body => body);
export const statement = 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<t.Expression> = {
code: str => `(\n${str}\n)`,
validate: ast => {
if (ast.program.body.length > 1) {
throw new Error("Found multiple statements but wanted one");
}
if (expression.unwrap(ast).start === 0) {
throw new Error("Parse result included parens.");
}
},
unwrap: ({ program }) => {
const [stmt] = program.body;
t.assertExpressionStatement(stmt);
return stmt.expression;
},
};
export const program: Formatter<t.Program> = {
code: str => str,
validate: () => {},
unwrap: ast => ast.program,
};

View File

@ -1,31 +0,0 @@
// @flow
import * as formatters from "./formatters";
import createTemplateBuilder from "./builder";
export const smart = createTemplateBuilder<*>(formatters.smart);
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,
},
);

View File

@ -0,0 +1,25 @@
import * as formatters from "./formatters";
import createTemplateBuilder from "./builder";
export const smart = createTemplateBuilder(formatters.smart);
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;
};
export default Object.assign(smart.bind(undefined) as DefaultTemplateBuilder, {
smart,
statement,
statements,
expression,
program,
ast: smart.ast,
});

View File

@ -1,7 +1,6 @@
// @flow
import type { Formatter } from "./formatters"; import type { Formatter } from "./formatters";
import { normalizeReplacements, type TemplateOpts } from "./options"; import type { TemplateReplacements, TemplateOpts } from "./options";
import { normalizeReplacements } from "./options";
import parseAndBuildMetadata from "./parse"; import parseAndBuildMetadata from "./parse";
import populatePlaceholders from "./populate"; import populatePlaceholders from "./populate";
@ -9,16 +8,16 @@ export default function literalTemplate<T>(
formatter: Formatter<T>, formatter: Formatter<T>,
tpl: Array<string>, tpl: Array<string>,
opts: TemplateOpts, opts: TemplateOpts,
): (Array<mixed>) => mixed => T { ): (_: Array<unknown>) => (_: unknown) => T {
const { metadata, names } = buildLiteralData(formatter, tpl, opts); const { metadata, names } = buildLiteralData(formatter, tpl, opts);
return (arg: Array<mixed>) => { return arg => {
const defaultReplacements = arg.reduce((acc, replacement, i) => { const defaultReplacements: TemplateReplacements = {};
acc[names[i]] = replacement; arg.forEach((replacement, i) => {
return acc; defaultReplacements[names[i]] = replacement;
}, {}); });
return (arg: mixed) => { return (arg: unknown) => {
const replacements = normalizeReplacements(arg); const replacements = normalizeReplacements(arg);
if (replacements) { if (replacements) {
@ -88,7 +87,7 @@ function buildLiteralData<T>(
function buildTemplateCode( function buildTemplateCode(
tpl: Array<string>, tpl: Array<string>,
prefix: string, prefix: string,
): { names: Array<string>, code: string } { ): { names: Array<string>; code: string } {
const names = []; const names = [];
let code = tpl[0]; let code = tpl[0];

View File

@ -1,6 +1,4 @@
// @flow import type { ParserOptions as ParserOpts } from "@babel/parser";
import type { Options as ParserOpts } from "@babel/parser/src/options";
export type { ParserOpts }; export type { ParserOpts };
@ -15,8 +13,7 @@ export type PublicOpts = {
* *
* This option can be used when using %%foo%% style placeholders. * This option can be used when using %%foo%% style placeholders.
*/ */
placeholderWhitelist?: ?Set<string>, placeholderWhitelist?: Set<string>;
/** /**
* A pattern to search for when looking for Identifier and StringLiteral * A pattern to search for when looking for Identifier and StringLiteral
* nodes that can be replaced. * nodes that can be replaced.
@ -28,30 +25,28 @@ export type PublicOpts = {
* *
* This option can be used when using %%foo%% style placeholders. * This option can be used when using %%foo%% style placeholders.
*/ */
placeholderPattern?: ?(RegExp | false), placeholderPattern?: RegExp | false;
/** /**
* 'true' to pass through comments from the template into the resulting AST, * 'true' to pass through comments from the template into the resulting AST,
* or 'false' to automatically discard comments. Defaults to 'false'. * or 'false' to automatically discard comments. Defaults to 'false'.
*/ */
preserveComments?: ?boolean, preserveComments?: boolean;
/** /**
* 'true' to use %%foo%% style placeholders, 'false' to use legacy placeholders * 'true' to use %%foo%% style placeholders, 'false' to use legacy placeholders
* described by placeholderPattern or placeholderWhitelist. * described by placeholderPattern or placeholderWhitelist.
* When it is not set, it behaves as 'true' if there are syntactic placeholders, * When it is not set, it behaves as 'true' if there are syntactic placeholders,
* otherwise as 'false'. * otherwise as 'false'.
*/ */
syntacticPlaceholders?: ?boolean, syntacticPlaceholders?: boolean | null;
}; };
export type TemplateOpts = {| export type TemplateOpts = {
parser: ParserOpts, parser: ParserOpts;
placeholderWhitelist: Set<string> | void, placeholderWhitelist?: Set<string>;
placeholderPattern: RegExp | false | void, placeholderPattern?: RegExp | false;
preserveComments: boolean | void, preserveComments?: boolean;
syntacticPlaceholders: boolean | void, syntacticPlaceholders?: boolean;
|}; };
export function merge(a: TemplateOpts, b: TemplateOpts): TemplateOpts { export function merge(a: TemplateOpts, b: TemplateOpts): TemplateOpts {
const { const {
@ -73,7 +68,7 @@ export function merge(a: TemplateOpts, b: TemplateOpts): TemplateOpts {
}; };
} }
export function validate(opts: mixed): TemplateOpts { export function validate(opts: unknown): TemplateOpts {
if (opts != null && typeof opts !== "object") { if (opts != null && typeof opts !== "object") {
throw new Error("Unknown template options."); throw new Error("Unknown template options.");
} }
@ -84,7 +79,7 @@ export function validate(opts: mixed): TemplateOpts {
preserveComments, preserveComments,
syntacticPlaceholders, syntacticPlaceholders,
...parser ...parser
} = opts || {}; } = opts || ({} as any);
if (placeholderWhitelist != null && !(placeholderWhitelist instanceof Set)) { if (placeholderWhitelist != null && !(placeholderWhitelist instanceof Set)) {
throw new Error( throw new Error(
@ -137,11 +132,11 @@ export function validate(opts: mixed): TemplateOpts {
}; };
} }
export type PublicReplacements = { [string]: mixed } | Array<mixed>; export type PublicReplacements = { [x: string]: unknown } | Array<unknown>;
export type TemplateReplacements = { [string]: mixed } | void; export type TemplateReplacements = { [x: string]: unknown } | void;
export function normalizeReplacements( export function normalizeReplacements(
replacements: mixed, replacements: unknown,
): TemplateReplacements { ): TemplateReplacements {
if (Array.isArray(replacements)) { if (Array.isArray(replacements)) {
return replacements.reduce((acc, replacement, i) => { return replacements.reduce((acc, replacement, i) => {
@ -149,7 +144,7 @@ export function normalizeReplacements(
return acc; return acc;
}, {}); }, {});
} else if (typeof replacements === "object" || replacements == null) { } else if (typeof replacements === "object" || replacements == null) {
return (replacements: any) || undefined; return (replacements as any) || undefined;
} }
throw new Error( throw new Error(

View File

@ -1,4 +1,3 @@
// @flow
import * as t from "@babel/types"; import * as t from "@babel/types";
import type { TraversalAncestors, TraversalHandler } from "@babel/types"; import type { TraversalAncestors, TraversalHandler } from "@babel/types";
import { parse } from "@babel/parser"; import { parse } from "@babel/parser";
@ -7,18 +6,18 @@ import type { TemplateOpts, ParserOpts } from "./options";
import type { Formatter } from "./formatters"; import type { Formatter } from "./formatters";
export type Metadata = { export type Metadata = {
ast: BabelNodeFile, ast: t.File;
placeholders: Array<Placeholder>, placeholders: Array<Placeholder>;
placeholderNames: Set<string>, placeholderNames: Set<string>;
}; };
type PlaceholderType = "string" | "param" | "statement" | "other"; type PlaceholderType = "string" | "param" | "statement" | "other";
export type Placeholder = {| export type Placeholder = {
name: string, name: string;
resolve: BabelNodeFile => { parent: BabelNode, key: string, index?: number }, resolve: (a: t.File) => { parent: t.Node; key: string; index?: number };
type: PlaceholderType, type: PlaceholderType;
isDuplicate: boolean, isDuplicate: boolean;
|}; };
const PATTERN = /^[_$A-Z0-9]+$/; const PATTERN = /^[_$A-Z0-9]+$/;
@ -44,15 +43,15 @@ export default function parseAndBuildMetadata<T>(
const syntactic = { const syntactic = {
placeholders: [], placeholders: [],
placeholderNames: new Set(), placeholderNames: new Set<string>(),
}; };
const legacy = { const legacy = {
placeholders: [], placeholders: [],
placeholderNames: new Set(), placeholderNames: new Set<string>(),
}; };
const isLegacyRef = { value: undefined }; const isLegacyRef = { value: undefined };
t.traverse(ast, (placeholderVisitorHandler: TraversalHandler<*>), { t.traverse(ast, placeholderVisitorHandler as TraversalHandler<any>, {
syntactic, syntactic,
legacy, legacy,
isLegacyRef, isLegacyRef,
@ -68,11 +67,11 @@ export default function parseAndBuildMetadata<T>(
} }
function placeholderVisitorHandler( function placeholderVisitorHandler(
node: BabelNode, node: t.Node,
ancestors: TraversalAncestors, ancestors: TraversalAncestors,
state: MetadataState, state: MetadataState,
) { ) {
let name; let name: string;
if (t.isPlaceholder(node)) { if (t.isPlaceholder(node)) {
if (state.syntacticPlaceholders === false) { if (state.syntacticPlaceholders === false) {
@ -81,16 +80,16 @@ function placeholderVisitorHandler(
"'.syntacticPlaceholders' is false.", "'.syntacticPlaceholders' is false.",
); );
} else { } else {
name = ((node: any).name: BabelNodeIdentifier).name; name = node.name.name;
state.isLegacyRef.value = false; state.isLegacyRef.value = false;
} }
} else if (state.isLegacyRef.value === false || state.syntacticPlaceholders) { } else if (state.isLegacyRef.value === false || state.syntacticPlaceholders) {
return; return;
} else if (t.isIdentifier(node) || t.isJSXIdentifier(node)) { } else if (t.isIdentifier(node) || t.isJSXIdentifier(node)) {
name = ((node: any): BabelNodeIdentifier).name; name = node.name;
state.isLegacyRef.value = true; state.isLegacyRef.value = true;
} else if (t.isStringLiteral(node)) { } else if (t.isStringLiteral(node)) {
name = ((node: any): BabelNodeStringLiteral).value; name = node.value;
state.isLegacyRef.value = true; state.isLegacyRef.value = true;
} else { } else {
return; return;
@ -156,15 +155,15 @@ function placeholderVisitorHandler(
placeholderNames.add(name); placeholderNames.add(name);
} }
function resolveAncestors(ast: BabelNodeFile, ancestors: TraversalAncestors) { function resolveAncestors(ast: t.File, ancestors: TraversalAncestors) {
let parent: BabelNode = ast; let parent: t.Node = ast;
for (let i = 0; i < ancestors.length - 1; i++) { for (let i = 0; i < ancestors.length - 1; i++) {
const { key, index } = ancestors[i]; const { key, index } = ancestors[i];
if (index === undefined) { if (index === undefined) {
parent = (parent: any)[key]; parent = (parent as any)[key];
} else { } else {
parent = (parent: any)[key][index]; parent = (parent as any)[key][index];
} }
} }
@ -175,24 +174,26 @@ function resolveAncestors(ast: BabelNodeFile, ancestors: TraversalAncestors) {
type MetadataState = { type MetadataState = {
syntactic: { syntactic: {
placeholders: Array<Placeholder>, placeholders: Array<Placeholder>;
placeholderNames: Set<string>, placeholderNames: Set<string>;
}, };
legacy: { legacy: {
placeholders: Array<Placeholder>, placeholders: Array<Placeholder>;
placeholderNames: Set<string>, placeholderNames: Set<string>;
}, };
isLegacyRef: { value: boolean | void }, isLegacyRef: {
placeholderWhitelist: Set<string> | void, value?: boolean;
placeholderPattern: RegExp | false | void, };
syntacticPlaceholders: boolean | void, placeholderWhitelist?: Set<string>;
placeholderPattern?: RegExp | false;
syntacticPlaceholders?: boolean;
}; };
function parseWithCodeFrame( function parseWithCodeFrame(
code: string, code: string,
parserOpts: ParserOpts, parserOpts: ParserOpts,
syntacticPlaceholders?: boolean, syntacticPlaceholders?: boolean,
): BabelNodeFile { ): t.File {
const plugins = (parserOpts.plugins || []).slice(); const plugins = (parserOpts.plugins || []).slice();
if (syntacticPlaceholders !== false) { if (syntacticPlaceholders !== false) {
plugins.push("placeholders"); plugins.push("placeholders");
@ -207,7 +208,6 @@ function parseWithCodeFrame(
}; };
try { try {
// $FlowFixMe - The parser AST is not the same type as the babel-types type.
return parse(code, parserOpts); return parse(code, parserOpts);
} catch (err) { } catch (err) {
const loc = err.loc; const loc = err.loc;

View File

@ -1,4 +1,3 @@
// @flow
import * as t from "@babel/types"; import * as t from "@babel/types";
import type { TemplateReplacements } from "./options"; import type { TemplateReplacements } from "./options";
@ -7,7 +6,7 @@ import type { Metadata, Placeholder } from "./parse";
export default function populatePlaceholders( export default function populatePlaceholders(
metadata: Metadata, metadata: Metadata,
replacements: TemplateReplacements, replacements: TemplateReplacements,
): BabelNodeFile { ): t.File {
const ast = t.cloneNode(metadata.ast); const ast = t.cloneNode(metadata.ast);
if (replacements) { if (replacements) {
@ -55,7 +54,7 @@ export default function populatePlaceholders(
function applyReplacement( function applyReplacement(
placeholder: Placeholder, placeholder: Placeholder,
ast: BabelNodeFile, ast: t.File,
replacement: any, replacement: any,
) { ) {
// Track inserted nodes and clone them if they are inserted more than // Track inserted nodes and clone them if they are inserted more than
@ -86,7 +85,7 @@ function applyReplacement(
} else if (typeof replacement === "string") { } else if (typeof replacement === "string") {
replacement = t.expressionStatement(t.identifier(replacement)); replacement = t.expressionStatement(t.identifier(replacement));
} else if (!t.isStatement(replacement)) { } else if (!t.isStatement(replacement)) {
replacement = t.expressionStatement((replacement: any)); replacement = t.expressionStatement(replacement as any);
} }
} else { } else {
if (replacement && !Array.isArray(replacement)) { if (replacement && !Array.isArray(replacement)) {
@ -94,7 +93,7 @@ function applyReplacement(
replacement = t.identifier(replacement); replacement = t.identifier(replacement);
} }
if (!t.isStatement(replacement)) { if (!t.isStatement(replacement)) {
replacement = t.expressionStatement((replacement: any)); replacement = t.expressionStatement(replacement as any);
} }
} }
} }
@ -116,9 +115,9 @@ function applyReplacement(
if (index === undefined) { if (index === undefined) {
t.validate(parent, key, replacement); t.validate(parent, key, replacement);
(parent: any)[key] = replacement; (parent as any)[key] = replacement;
} else { } else {
const items: Array<BabelNode> = (parent: any)[key].slice(); const items: Array<t.Node> = (parent as any)[key].slice();
if (placeholder.type === "statement" || placeholder.type === "param") { if (placeholder.type === "statement" || placeholder.type === "param") {
if (replacement == null) { if (replacement == null) {
@ -133,6 +132,6 @@ function applyReplacement(
} }
t.validate(parent, key, items); t.validate(parent, key, items);
(parent: any)[key] = items; (parent as any)[key] = items;
} }
} }

View File

@ -1,6 +1,6 @@
// @flow
import type { Formatter } from "./formatters"; import type { Formatter } from "./formatters";
import { normalizeReplacements, type TemplateOpts } from "./options"; import type { TemplateOpts } from "./options";
import { normalizeReplacements } from "./options";
import parseAndBuildMetadata from "./parse"; import parseAndBuildMetadata from "./parse";
import populatePlaceholders from "./populate"; import populatePlaceholders from "./populate";
@ -8,12 +8,12 @@ export default function stringTemplate<T>(
formatter: Formatter<T>, formatter: Formatter<T>,
code: string, code: string,
opts: TemplateOpts, opts: TemplateOpts,
): mixed => T { ): (arg?: unknown) => T {
code = formatter.code(code); code = formatter.code(code);
let metadata; let metadata;
return (arg?: mixed) => { return (arg?: unknown) => {
const replacements = normalizeReplacements(arg); const replacements = normalizeReplacements(arg);
if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts); if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts);