From 089c200c8b56c02e1c03418816eb5fc2cc12ce58 Mon Sep 17 00:00:00 2001 From: Bogdan Savluk Date: Fri, 6 Nov 2020 03:19:19 +0100 Subject: [PATCH] Convert `@babel/template` from Flow to TS (#12317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolò Ribaudo --- lib/babel-packages.js.flow | 36 +++++++++ .../src/{builder.js => builder.ts} | 31 ++++---- packages/babel-template/src/formatters.js | 75 ------------------- packages/babel-template/src/formatters.ts | 70 +++++++++++++++++ packages/babel-template/src/index.js | 31 -------- packages/babel-template/src/index.ts | 25 +++++++ .../src/{literal.js => literal.ts} | 21 +++--- .../src/{options.js => options.ts} | 41 +++++----- .../babel-template/src/{parse.js => parse.ts} | 68 ++++++++--------- .../src/{populate.js => populate.ts} | 15 ++-- .../src/{string.js => string.ts} | 8 +- 11 files changed, 218 insertions(+), 203 deletions(-) rename packages/babel-template/src/{builder.js => builder.ts} (86%) delete mode 100644 packages/babel-template/src/formatters.js create mode 100644 packages/babel-template/src/formatters.ts delete mode 100644 packages/babel-template/src/index.js create mode 100644 packages/babel-template/src/index.ts rename packages/babel-template/src/{literal.js => literal.ts} (85%) rename packages/babel-template/src/{options.js => options.ts} (83%) rename packages/babel-template/src/{parse.js => parse.ts} (78%) rename packages/babel-template/src/{populate.js => populate.ts} (92%) rename packages/babel-template/src/{string.js => string.ts} (76%) diff --git a/lib/babel-packages.js.flow b/lib/babel-packages.js.flow index e69de29bb2..2f2c9a61bc 100644 --- a/lib/babel-packages.js.flow +++ b/lib/babel-packages.js.flow @@ -0,0 +1,36 @@ +declare module "@babel/template" { + declare type PublicOpts = { + placeholderWhitelist?: ?Set, + placeholderPattern?: ?(RegExp | false), + preserveComments?: ?boolean, + syntacticPlaceholders?: ?boolean, + }; + + declare type PublicReplacements = { [string]: mixed } | Array; + + declare type TemplateBuilder = { + ast: { + (tpl: string, opts: ?PublicOpts): T, + (tpl: Array, ...args: Array): T, + }, + (opts: PublicOpts): TemplateBuilder, + (tpl: string, opts: ?PublicOpts): (?PublicReplacements) => T, + (tpl: Array, ...args: Array): (?PublicReplacements) => T, + }; + + declare type Smart = TemplateBuilder< + Array | BabelNodeStatement + >; + declare type Statement = TemplateBuilder; + declare type Statements = TemplateBuilder>; + declare type Expression = TemplateBuilder; + declare type Program = TemplateBuilder; + + declare export default Smart & { + smart: Smart, + statement: Statement, + statements: Statements, + expression: Expression, + program: Program, + }; +} diff --git a/packages/babel-template/src/builder.js b/packages/babel-template/src/builder.ts similarity index 86% rename from packages/babel-template/src/builder.js rename to packages/babel-template/src/builder.ts index b299e2e7b6..7040f81595 100644 --- a/packages/babel-template/src/builder.js +++ b/packages/babel-template/src/builder.ts @@ -1,12 +1,5 @@ -// @flow - -import { - merge, - validate, - type TemplateOpts, - type PublicOpts, - type PublicReplacements, -} from "./options"; +import { merge, validate } from "./options"; +import type { TemplateOpts, PublicOpts, PublicReplacements } from "./options"; import type { Formatter } from "./formatters"; import stringTemplate from "./string"; @@ -14,20 +7,22 @@ import literalTemplate from "./literal"; export type TemplateBuilder = { // Build a new builder, merging the given options with the previous ones. - (opts: PublicOpts): TemplateBuilder, + (opts: PublicOpts): TemplateBuilder; // 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. - (tpl: Array, ...args: Array): (?PublicReplacements) => T, + (tpl: TemplateStringsArray, ...args: Array): ( + replacements?: 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, ...args: Array): T, - }, + (tpl: string, opts?: PublicOpts): T; + (tpl: TemplateStringsArray, ...args: Array): T; + }; }; // Prebuild the options that will be used when parsing a `.ast` template. @@ -69,7 +64,7 @@ export default function createTemplateBuilder( ); } throw new Error(`Unexpected template param ${typeof tpl}`); - }: Function), + }) as TemplateBuilder, { ast: (tpl, ...args) => { if (typeof tpl === "string") { @@ -98,7 +93,9 @@ export default function createTemplateBuilder( ); } -function extendedTrace(fn: Arg => Result): Arg => Result { +function extendedTrace( + 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 = ""; diff --git a/packages/babel-template/src/formatters.js b/packages/babel-template/src/formatters.js deleted file mode 100644 index 4feb1d5484..0000000000 --- a/packages/babel-template/src/formatters.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow - -export type Formatter = { - code: string => string, - validate: BabelNodeFile => void, - unwrap: BabelNodeFile => T, -}; - -function makeStatementFormatter( - fn: (Array) => T, -): Formatter { - 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, -> = makeStatementFormatter(body => { - if (body.length > 1) { - return body; - } else { - return body[0]; - } -}); - -export const statements: Formatter< - Array, -> = makeStatementFormatter(body => body); - -export const statement: Formatter = 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 = { - 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 = { - code: str => str, - validate: () => {}, - unwrap: ast => ast.program, -}; diff --git a/packages/babel-template/src/formatters.ts b/packages/babel-template/src/formatters.ts new file mode 100644 index 0000000000..3a34fa68a2 --- /dev/null +++ b/packages/babel-template/src/formatters.ts @@ -0,0 +1,70 @@ +import * as t from "@babel/types"; + +export type Formatter = { + code: (source: string) => string; + validate: (ast: t.File) => void; + unwrap: (ast: t.File) => T; +}; + +function makeStatementFormatter( + fn: (statements: Array) => T, +): Formatter { + 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 = { + 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 = { + code: str => str, + validate: () => {}, + unwrap: ast => ast.program, +}; diff --git a/packages/babel-template/src/index.js b/packages/babel-template/src/index.js deleted file mode 100644 index 7322d9ab99..0000000000 --- a/packages/babel-template/src/index.js +++ /dev/null @@ -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, - }, -); diff --git a/packages/babel-template/src/index.ts b/packages/babel-template/src/index.ts new file mode 100644 index 0000000000..84d2c0e3a4 --- /dev/null +++ b/packages/babel-template/src/index.ts @@ -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, +}); diff --git a/packages/babel-template/src/literal.js b/packages/babel-template/src/literal.ts similarity index 85% rename from packages/babel-template/src/literal.js rename to packages/babel-template/src/literal.ts index 0cc7415e72..c436edcbd8 100644 --- a/packages/babel-template/src/literal.js +++ b/packages/babel-template/src/literal.ts @@ -1,7 +1,6 @@ -// @flow - 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 populatePlaceholders from "./populate"; @@ -9,16 +8,16 @@ export default function literalTemplate( formatter: Formatter, tpl: Array, opts: TemplateOpts, -): (Array) => mixed => T { +): (_: Array) => (_: unknown) => T { const { metadata, names } = buildLiteralData(formatter, tpl, opts); - return (arg: Array) => { - const defaultReplacements = arg.reduce((acc, replacement, i) => { - acc[names[i]] = replacement; - return acc; - }, {}); + return arg => { + const defaultReplacements: TemplateReplacements = {}; + arg.forEach((replacement, i) => { + defaultReplacements[names[i]] = replacement; + }); - return (arg: mixed) => { + return (arg: unknown) => { const replacements = normalizeReplacements(arg); if (replacements) { @@ -88,7 +87,7 @@ function buildLiteralData( function buildTemplateCode( tpl: Array, prefix: string, -): { names: Array, code: string } { +): { names: Array; code: string } { const names = []; let code = tpl[0]; diff --git a/packages/babel-template/src/options.js b/packages/babel-template/src/options.ts similarity index 83% rename from packages/babel-template/src/options.js rename to packages/babel-template/src/options.ts index b1c6f38a6e..8be69dcc69 100644 --- a/packages/babel-template/src/options.js +++ b/packages/babel-template/src/options.ts @@ -1,6 +1,4 @@ -// @flow - -import type { Options as ParserOpts } from "@babel/parser/src/options"; +import type { ParserOptions as ParserOpts } from "@babel/parser"; export type { ParserOpts }; @@ -15,8 +13,7 @@ export type PublicOpts = { * * This option can be used when using %%foo%% style placeholders. */ - placeholderWhitelist?: ?Set, - + placeholderWhitelist?: Set; /** * A pattern to search for when looking for Identifier and StringLiteral * nodes that can be replaced. @@ -28,30 +25,28 @@ export type PublicOpts = { * * 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, * or 'false' to automatically discard comments. Defaults to 'false'. */ - preserveComments?: ?boolean, - + preserveComments?: boolean; /** * 'true' to use %%foo%% style placeholders, 'false' to use legacy placeholders * described by placeholderPattern or placeholderWhitelist. * When it is not set, it behaves as 'true' if there are syntactic placeholders, * otherwise as 'false'. */ - syntacticPlaceholders?: ?boolean, + syntacticPlaceholders?: boolean | null; }; -export type TemplateOpts = {| - parser: ParserOpts, - placeholderWhitelist: Set | void, - placeholderPattern: RegExp | false | void, - preserveComments: boolean | void, - syntacticPlaceholders: boolean | void, -|}; +export type TemplateOpts = { + parser: ParserOpts; + placeholderWhitelist?: Set; + placeholderPattern?: RegExp | false; + preserveComments?: boolean; + syntacticPlaceholders?: boolean; +}; export function merge(a: TemplateOpts, b: TemplateOpts): TemplateOpts { 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") { throw new Error("Unknown template options."); } @@ -84,7 +79,7 @@ export function validate(opts: mixed): TemplateOpts { preserveComments, syntacticPlaceholders, ...parser - } = opts || {}; + } = opts || ({} as any); if (placeholderWhitelist != null && !(placeholderWhitelist instanceof Set)) { throw new Error( @@ -137,11 +132,11 @@ export function validate(opts: mixed): TemplateOpts { }; } -export type PublicReplacements = { [string]: mixed } | Array; -export type TemplateReplacements = { [string]: mixed } | void; +export type PublicReplacements = { [x: string]: unknown } | Array; +export type TemplateReplacements = { [x: string]: unknown } | void; export function normalizeReplacements( - replacements: mixed, + replacements: unknown, ): TemplateReplacements { if (Array.isArray(replacements)) { return replacements.reduce((acc, replacement, i) => { @@ -149,7 +144,7 @@ export function normalizeReplacements( return acc; }, {}); } else if (typeof replacements === "object" || replacements == null) { - return (replacements: any) || undefined; + return (replacements as any) || undefined; } throw new Error( diff --git a/packages/babel-template/src/parse.js b/packages/babel-template/src/parse.ts similarity index 78% rename from packages/babel-template/src/parse.js rename to packages/babel-template/src/parse.ts index 3a0b6596d4..55af745271 100644 --- a/packages/babel-template/src/parse.js +++ b/packages/babel-template/src/parse.ts @@ -1,4 +1,3 @@ -// @flow import * as t from "@babel/types"; import type { TraversalAncestors, TraversalHandler } from "@babel/types"; import { parse } from "@babel/parser"; @@ -7,18 +6,18 @@ import type { TemplateOpts, ParserOpts } from "./options"; import type { Formatter } from "./formatters"; export type Metadata = { - ast: BabelNodeFile, - placeholders: Array, - placeholderNames: Set, + ast: t.File; + placeholders: Array; + placeholderNames: Set; }; type PlaceholderType = "string" | "param" | "statement" | "other"; -export type Placeholder = {| - name: string, - resolve: BabelNodeFile => { parent: BabelNode, key: string, index?: number }, - type: PlaceholderType, - isDuplicate: boolean, -|}; +export type Placeholder = { + name: string; + resolve: (a: t.File) => { parent: t.Node; key: string; index?: number }; + type: PlaceholderType; + isDuplicate: boolean; +}; const PATTERN = /^[_$A-Z0-9]+$/; @@ -44,15 +43,15 @@ export default function parseAndBuildMetadata( const syntactic = { placeholders: [], - placeholderNames: new Set(), + placeholderNames: new Set(), }; const legacy = { placeholders: [], - placeholderNames: new Set(), + placeholderNames: new Set(), }; const isLegacyRef = { value: undefined }; - t.traverse(ast, (placeholderVisitorHandler: TraversalHandler<*>), { + t.traverse(ast, placeholderVisitorHandler as TraversalHandler, { syntactic, legacy, isLegacyRef, @@ -68,11 +67,11 @@ export default function parseAndBuildMetadata( } function placeholderVisitorHandler( - node: BabelNode, + node: t.Node, ancestors: TraversalAncestors, state: MetadataState, ) { - let name; + let name: string; if (t.isPlaceholder(node)) { if (state.syntacticPlaceholders === false) { @@ -81,16 +80,16 @@ function placeholderVisitorHandler( "'.syntacticPlaceholders' is false.", ); } else { - name = ((node: any).name: BabelNodeIdentifier).name; + name = node.name.name; state.isLegacyRef.value = false; } } else if (state.isLegacyRef.value === false || state.syntacticPlaceholders) { return; } else if (t.isIdentifier(node) || t.isJSXIdentifier(node)) { - name = ((node: any): BabelNodeIdentifier).name; + name = node.name; state.isLegacyRef.value = true; } else if (t.isStringLiteral(node)) { - name = ((node: any): BabelNodeStringLiteral).value; + name = node.value; state.isLegacyRef.value = true; } else { return; @@ -156,15 +155,15 @@ function placeholderVisitorHandler( placeholderNames.add(name); } -function resolveAncestors(ast: BabelNodeFile, ancestors: TraversalAncestors) { - let parent: BabelNode = ast; +function resolveAncestors(ast: t.File, ancestors: TraversalAncestors) { + let parent: t.Node = ast; for (let i = 0; i < ancestors.length - 1; i++) { const { key, index } = ancestors[i]; if (index === undefined) { - parent = (parent: any)[key]; + parent = (parent as any)[key]; } else { - parent = (parent: any)[key][index]; + parent = (parent as any)[key][index]; } } @@ -175,24 +174,26 @@ function resolveAncestors(ast: BabelNodeFile, ancestors: TraversalAncestors) { type MetadataState = { syntactic: { - placeholders: Array, - placeholderNames: Set, - }, + placeholders: Array; + placeholderNames: Set; + }; legacy: { - placeholders: Array, - placeholderNames: Set, - }, - isLegacyRef: { value: boolean | void }, - placeholderWhitelist: Set | void, - placeholderPattern: RegExp | false | void, - syntacticPlaceholders: boolean | void, + placeholders: Array; + placeholderNames: Set; + }; + isLegacyRef: { + value?: boolean; + }; + placeholderWhitelist?: Set; + placeholderPattern?: RegExp | false; + syntacticPlaceholders?: boolean; }; function parseWithCodeFrame( code: string, parserOpts: ParserOpts, syntacticPlaceholders?: boolean, -): BabelNodeFile { +): t.File { const plugins = (parserOpts.plugins || []).slice(); if (syntacticPlaceholders !== false) { plugins.push("placeholders"); @@ -207,7 +208,6 @@ function parseWithCodeFrame( }; try { - // $FlowFixMe - The parser AST is not the same type as the babel-types type. return parse(code, parserOpts); } catch (err) { const loc = err.loc; diff --git a/packages/babel-template/src/populate.js b/packages/babel-template/src/populate.ts similarity index 92% rename from packages/babel-template/src/populate.js rename to packages/babel-template/src/populate.ts index 90b13853ba..bca35c3e01 100644 --- a/packages/babel-template/src/populate.js +++ b/packages/babel-template/src/populate.ts @@ -1,4 +1,3 @@ -// @flow import * as t from "@babel/types"; import type { TemplateReplacements } from "./options"; @@ -7,7 +6,7 @@ import type { Metadata, Placeholder } from "./parse"; export default function populatePlaceholders( metadata: Metadata, replacements: TemplateReplacements, -): BabelNodeFile { +): t.File { const ast = t.cloneNode(metadata.ast); if (replacements) { @@ -55,7 +54,7 @@ export default function populatePlaceholders( function applyReplacement( placeholder: Placeholder, - ast: BabelNodeFile, + ast: t.File, replacement: any, ) { // Track inserted nodes and clone them if they are inserted more than @@ -86,7 +85,7 @@ function applyReplacement( } else if (typeof replacement === "string") { replacement = t.expressionStatement(t.identifier(replacement)); } else if (!t.isStatement(replacement)) { - replacement = t.expressionStatement((replacement: any)); + replacement = t.expressionStatement(replacement as any); } } else { if (replacement && !Array.isArray(replacement)) { @@ -94,7 +93,7 @@ function applyReplacement( replacement = t.identifier(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) { t.validate(parent, key, replacement); - (parent: any)[key] = replacement; + (parent as any)[key] = replacement; } else { - const items: Array = (parent: any)[key].slice(); + const items: Array = (parent as any)[key].slice(); if (placeholder.type === "statement" || placeholder.type === "param") { if (replacement == null) { @@ -133,6 +132,6 @@ function applyReplacement( } t.validate(parent, key, items); - (parent: any)[key] = items; + (parent as any)[key] = items; } } diff --git a/packages/babel-template/src/string.js b/packages/babel-template/src/string.ts similarity index 76% rename from packages/babel-template/src/string.js rename to packages/babel-template/src/string.ts index 3fbf7df419..1f8ad4c9f7 100644 --- a/packages/babel-template/src/string.js +++ b/packages/babel-template/src/string.ts @@ -1,6 +1,6 @@ -// @flow 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 populatePlaceholders from "./populate"; @@ -8,12 +8,12 @@ export default function stringTemplate( formatter: Formatter, code: string, opts: TemplateOpts, -): mixed => T { +): (arg?: unknown) => T { code = formatter.code(code); let metadata; - return (arg?: mixed) => { + return (arg?: unknown) => { const replacements = normalizeReplacements(arg); if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts);