From 5f82c041781a340ea1f858e88658248fb8ec44d5 Mon Sep 17 00:00:00 2001 From: uhyo Date: Tue, 13 Aug 2024 22:20:10 +0900 Subject: [PATCH 1/4] refactor: split generate.ts --- build/logic/ast/getStatementDeclName.ts | 23 ++++ build/logic/generate.ts | 147 +----------------------- build/logic/scanBetterFile.ts | 140 ++++++++++++++++++++++ 3 files changed, 165 insertions(+), 145 deletions(-) create mode 100644 build/logic/ast/getStatementDeclName.ts create mode 100644 build/logic/scanBetterFile.ts diff --git a/build/logic/ast/getStatementDeclName.ts b/build/logic/ast/getStatementDeclName.ts new file mode 100644 index 0000000..9923e4d --- /dev/null +++ b/build/logic/ast/getStatementDeclName.ts @@ -0,0 +1,23 @@ +import ts from "typescript"; + +export function getStatementDeclName( + statement: ts.Statement, +): string | undefined { + if (ts.isVariableStatement(statement)) { + for (const dec of statement.declarationList.declarations) { + if (ts.isIdentifier(dec.name)) { + return dec.name.text; + } + } + } else if ( + ts.isFunctionDeclaration(statement) || + ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement) || + ts.isModuleDeclaration(statement) + ) { + return statement.name?.text; + } else if (ts.isInterfaceDeclaration(statement)) { + return statement.name.text; + } + return undefined; +} diff --git a/build/logic/generate.ts b/build/logic/generate.ts index 7a2227f..3690aeb 100644 --- a/build/logic/generate.ts +++ b/build/logic/generate.ts @@ -1,10 +1,8 @@ import path from "path"; import ts from "typescript"; -import { alias } from "../util/alias"; import { upsert } from "../util/upsert"; -import { projectDir } from "./projectDir"; - -const betterLibDir = path.join(projectDir, "lib"); +import { getStatementDeclName } from "./ast/getStatementDeclName"; +import { ReplacementTarget, scanBetterFile } from "./scanBetterFile"; type GenerateOptions = { emitOriginalAsComment?: boolean; @@ -208,101 +206,6 @@ function generateInterface( return result; } -type ReplacementTarget = ( - | { - type: "interface"; - originalStatement: ts.InterfaceDeclaration; - members: Map< - string, - { - member: ts.TypeElement; - text: string; - }[] - >; - } - | { - type: "non-interface"; - statement: ts.Statement; - } -) & { - sourceFile: ts.SourceFile; -}; - -/** - * Scan better lib file to determine which statements need to be replaced. - */ -function scanBetterFile( - printer: ts.Printer, - targetFile: string, -): Map { - const replacementTargets = new Map(); - { - const betterLibFile = path.join(betterLibDir, targetFile); - const betterProgram = ts.createProgram([betterLibFile], {}); - const betterFile = betterProgram.getSourceFile(betterLibFile); - if (betterFile) { - // Scan better file to determine which statements need to be replaced. - for (const statement of betterFile.statements) { - const name = getStatementDeclName(statement) ?? ""; - const aliasesMap = - alias.get(name) ?? new Map([[name, new Map()]]); - for (const [targetName, typeMap] of aliasesMap) { - const transformedStatement = replaceAliases(statement, typeMap); - if (ts.isInterfaceDeclaration(transformedStatement)) { - const members = new Map< - string, - { - member: ts.TypeElement; - text: string; - }[] - >(); - for (const member of transformedStatement.members) { - const memberName = member.name?.getText(betterFile) ?? ""; - upsert(members, memberName, (members = []) => { - const leadingSpacesMatch = /^\s*/.exec( - member.getFullText(betterFile), - ); - const leadingSpaces = - leadingSpacesMatch !== null ? leadingSpacesMatch[0] : ""; - members.push({ - member, - text: - leadingSpaces + - printer.printNode( - ts.EmitHint.Unspecified, - member, - betterFile, - ), - }); - return members; - }); - } - upsert(replacementTargets, targetName, (targets = []) => { - targets.push({ - type: "interface", - members, - originalStatement: transformedStatement, - sourceFile: betterFile, - }); - return targets; - }); - } else { - upsert(replacementTargets, targetName, (statements = []) => { - statements.push({ - type: "non-interface", - statement: transformedStatement, - sourceFile: betterFile, - }); - return statements; - }); - } - } - } - } - } - return replacementTargets; -} - /** * Determines whether interface can be partially replaced. */ @@ -410,54 +313,8 @@ function printInterface( return result; } -function getStatementDeclName(statement: ts.Statement): string | undefined { - if (ts.isVariableStatement(statement)) { - for (const dec of statement.declarationList.declarations) { - if (ts.isIdentifier(dec.name)) { - return dec.name.text; - } - } - } else if ( - ts.isFunctionDeclaration(statement) || - ts.isInterfaceDeclaration(statement) || - ts.isTypeAliasDeclaration(statement) || - ts.isModuleDeclaration(statement) - ) { - return statement.name?.text; - } else if (ts.isInterfaceDeclaration(statement)) { - return statement.name.text; - } - return undefined; -} - function commentOut(code: string): string { const lines = code.split("\n").filter((line) => line.trim().length > 0); const result = lines.map((line) => `// ${line}`); return result.join("\n") + "\n"; } - -function replaceAliases( - statement: ts.Statement, - typeMap: Map, -): ts.Statement { - if (typeMap.size === 0) return statement; - return ts.transform(statement, [ - (context) => (sourceStatement) => { - const visitor = (node: ts.Node): ts.Node => { - if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) { - const replacementType = typeMap.get(node.typeName.text); - if (replacementType === undefined) { - return node; - } - return ts.factory.updateTypeReferenceNode( - node, - ts.factory.createIdentifier(replacementType), - node.typeArguments, - ); - } - return ts.visitEachChild(node, visitor, context); - }; - return ts.visitNode(sourceStatement, visitor, ts.isStatement); - }, - ]).transformed[0]; -} diff --git a/build/logic/scanBetterFile.ts b/build/logic/scanBetterFile.ts new file mode 100644 index 0000000..22994b7 --- /dev/null +++ b/build/logic/scanBetterFile.ts @@ -0,0 +1,140 @@ +import path from "path"; +import ts from "typescript"; +import { alias } from "../util/alias"; +import { upsert } from "../util/upsert"; +import { getStatementDeclName } from "./ast/getStatementDeclName"; +import { projectDir } from "./projectDir"; + +const betterLibDir = path.join(projectDir, "lib"); + +export type ReplacementTarget = ( + | { + type: "interface"; + originalStatement: ts.InterfaceDeclaration; + members: Map< + string, + { + member: ts.TypeElement; + text: string; + }[] + >; + } + | { + type: "declare-global"; + originalStatement: ts.ModuleDeclaration; + statements: ts.Statement[]; + } + | { + type: "non-interface"; + statement: ts.Statement; + } +) & { + sourceFile: ts.SourceFile; +}; + +/** + * Scan better lib file to determine which statements need to be replaced. + */ +export function scanBetterFile( + printer: ts.Printer, + targetFile: string, +): Map { + const replacementTargets = new Map(); + { + const betterLibFile = path.join(betterLibDir, targetFile); + const betterProgram = ts.createProgram([betterLibFile], {}); + const betterFile = betterProgram.getSourceFile(betterLibFile); + if (betterFile) { + // Scan better file to determine which statements need to be replaced. + for (const statement of betterFile.statements) { + const name = getStatementDeclName(statement) ?? ""; + const aliasesMap = + alias.get(name) ?? new Map([[name, new Map()]]); + for (const [targetName, typeMap] of aliasesMap) { + const transformedStatement = replaceAliases(statement, typeMap); + if (ts.isInterfaceDeclaration(transformedStatement)) { + const members = new Map< + string, + { + member: ts.TypeElement; + text: string; + }[] + >(); + for (const member of transformedStatement.members) { + const memberName = member.name?.getText(betterFile) ?? ""; + upsert(members, memberName, (members = []) => { + const leadingSpacesMatch = /^\s*/.exec( + member.getFullText(betterFile), + ); + const leadingSpaces = + leadingSpacesMatch !== null ? leadingSpacesMatch[0] : ""; + members.push({ + member, + text: + leadingSpaces + + printer.printNode( + ts.EmitHint.Unspecified, + member, + betterFile, + ), + }); + return members; + }); + } + upsert(replacementTargets, targetName, (targets = []) => { + targets.push({ + type: "interface", + members, + originalStatement: transformedStatement, + sourceFile: betterFile, + }); + return targets; + }); + } else if ( + ts.isModuleDeclaration(transformedStatement) && + ts.isIdentifier(transformedStatement.name) && + transformedStatement.name.text === "global" + ) { + // declare global + } else { + upsert(replacementTargets, targetName, (statements = []) => { + statements.push({ + type: "non-interface", + statement: transformedStatement, + sourceFile: betterFile, + }); + return statements; + }); + } + } + } + } + } + return replacementTargets; +} + +function replaceAliases( + statement: ts.Statement, + typeMap: Map, +): ts.Statement { + if (typeMap.size === 0) return statement; + return ts.transform(statement, [ + (context) => (sourceStatement) => { + const visitor = (node: ts.Node): ts.Node => { + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) { + const replacementType = typeMap.get(node.typeName.text); + if (replacementType === undefined) { + return node; + } + return ts.factory.updateTypeReferenceNode( + node, + ts.factory.createIdentifier(replacementType), + node.typeArguments, + ); + } + return ts.visitEachChild(node, visitor, context); + }; + return ts.visitNode(sourceStatement, visitor, ts.isStatement); + }, + ]).transformed[0]; +} From fbdc3df53cba4388261567127561931ee27ff82f Mon Sep 17 00:00:00 2001 From: uhyo Date: Tue, 13 Aug 2024 22:25:07 +0900 Subject: [PATCH 2/4] feat: scan declare global block --- build/logic/scanBetterFile.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/build/logic/scanBetterFile.ts b/build/logic/scanBetterFile.ts index 22994b7..9206a74 100644 --- a/build/logic/scanBetterFile.ts +++ b/build/logic/scanBetterFile.ts @@ -22,7 +22,7 @@ export type ReplacementTarget = ( | { type: "declare-global"; originalStatement: ts.ModuleDeclaration; - statements: ts.Statement[]; + statements: readonly ts.Statement[]; } | { type: "non-interface"; @@ -32,14 +32,17 @@ export type ReplacementTarget = ( sourceFile: ts.SourceFile; }; +export const declareGlobalSymbol = Symbol("declare global"); +export type ReplacementName = string | typeof declareGlobalSymbol; + /** * Scan better lib file to determine which statements need to be replaced. */ export function scanBetterFile( printer: ts.Printer, targetFile: string, -): Map { - const replacementTargets = new Map(); +): Map { + const replacementTargets = new Map(); { const betterLibFile = path.join(betterLibDir, targetFile); const betterProgram = ts.createProgram([betterLibFile], {}); @@ -96,6 +99,19 @@ export function scanBetterFile( transformedStatement.name.text === "global" ) { // declare global + upsert(replacementTargets, declareGlobalSymbol, (targets = []) => { + targets.push({ + type: "declare-global", + originalStatement: transformedStatement, + statements: + transformedStatement.body && + ts.isModuleBlock(transformedStatement.body) + ? transformedStatement.body.statements + : [], + sourceFile: betterFile, + }); + return targets; + }); } else { upsert(replacementTargets, targetName, (statements = []) => { statements.push({ From 9acc9ff875eeab77a333cbb7eb7a6cddf19f18a1 Mon Sep 17 00:00:00 2001 From: uhyo Date: Mon, 23 Sep 2024 20:25:18 +0900 Subject: [PATCH 3/4] refactor: change ReplacementTarget interface to support declare global blocks --- build/logic/scanBetterFile.ts | 169 ++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 80 deletions(-) diff --git a/build/logic/scanBetterFile.ts b/build/logic/scanBetterFile.ts index 9206a74..45908ae 100644 --- a/build/logic/scanBetterFile.ts +++ b/build/logic/scanBetterFile.ts @@ -22,7 +22,7 @@ export type ReplacementTarget = ( | { type: "declare-global"; originalStatement: ts.ModuleDeclaration; - statements: readonly ts.Statement[]; + statements: ReplacementMap; } | { type: "non-interface"; @@ -32,6 +32,8 @@ export type ReplacementTarget = ( sourceFile: ts.SourceFile; }; +export type ReplacementMap = Map; + export const declareGlobalSymbol = Symbol("declare global"); export type ReplacementName = string | typeof declareGlobalSymbol; @@ -41,88 +43,95 @@ export type ReplacementName = string | typeof declareGlobalSymbol; export function scanBetterFile( printer: ts.Printer, targetFile: string, -): Map { +): ReplacementMap { + const betterLibFile = path.join(betterLibDir, targetFile); + const betterProgram = ts.createProgram([betterLibFile], {}); + const betterFile = betterProgram.getSourceFile(betterLibFile); + if (!betterFile) { + // This happens when the better file of that name does not exist. + return new Map(); + } + return scanStatements(printer, betterFile.statements, betterFile); +} + +function scanStatements( + printer: ts.Printer, + statements: ts.NodeArray, + sourceFile: ts.SourceFile, +): ReplacementMap { const replacementTargets = new Map(); - { - const betterLibFile = path.join(betterLibDir, targetFile); - const betterProgram = ts.createProgram([betterLibFile], {}); - const betterFile = betterProgram.getSourceFile(betterLibFile); - if (betterFile) { - // Scan better file to determine which statements need to be replaced. - for (const statement of betterFile.statements) { - const name = getStatementDeclName(statement) ?? ""; - const aliasesMap = - alias.get(name) ?? new Map([[name, new Map()]]); - for (const [targetName, typeMap] of aliasesMap) { - const transformedStatement = replaceAliases(statement, typeMap); - if (ts.isInterfaceDeclaration(transformedStatement)) { - const members = new Map< - string, - { - member: ts.TypeElement; - text: string; - }[] - >(); - for (const member of transformedStatement.members) { - const memberName = member.name?.getText(betterFile) ?? ""; - upsert(members, memberName, (members = []) => { - const leadingSpacesMatch = /^\s*/.exec( - member.getFullText(betterFile), - ); - const leadingSpaces = - leadingSpacesMatch !== null ? leadingSpacesMatch[0] : ""; - members.push({ - member, - text: - leadingSpaces + - printer.printNode( - ts.EmitHint.Unspecified, - member, - betterFile, - ), - }); - return members; - }); - } - upsert(replacementTargets, targetName, (targets = []) => { - targets.push({ - type: "interface", - members, - originalStatement: transformedStatement, - sourceFile: betterFile, - }); - return targets; - }); - } else if ( - ts.isModuleDeclaration(transformedStatement) && - ts.isIdentifier(transformedStatement.name) && - transformedStatement.name.text === "global" - ) { - // declare global - upsert(replacementTargets, declareGlobalSymbol, (targets = []) => { - targets.push({ - type: "declare-global", - originalStatement: transformedStatement, - statements: - transformedStatement.body && - ts.isModuleBlock(transformedStatement.body) - ? transformedStatement.body.statements - : [], - sourceFile: betterFile, - }); - return targets; + for (const statement of statements) { + const name = getStatementDeclName(statement) ?? ""; + const aliasesMap = + alias.get(name) ?? new Map([[name, new Map()]]); + for (const [targetName, typeMap] of aliasesMap) { + const transformedStatement = replaceAliases(statement, typeMap); + if (ts.isInterfaceDeclaration(transformedStatement)) { + const members = new Map< + string, + { + member: ts.TypeElement; + text: string; + }[] + >(); + for (const member of transformedStatement.members) { + const memberName = member.name?.getText(sourceFile) ?? ""; + upsert(members, memberName, (members = []) => { + const leadingSpacesMatch = /^\s*/.exec( + member.getFullText(sourceFile), + ); + const leadingSpaces = + leadingSpacesMatch !== null ? leadingSpacesMatch[0] : ""; + members.push({ + member, + text: + leadingSpaces + + printer.printNode(ts.EmitHint.Unspecified, member, sourceFile), }); - } else { - upsert(replacementTargets, targetName, (statements = []) => { - statements.push({ - type: "non-interface", - statement: transformedStatement, - sourceFile: betterFile, - }); - return statements; - }); - } + return members; + }); } + upsert(replacementTargets, targetName, (targets = []) => { + targets.push({ + type: "interface", + members, + originalStatement: transformedStatement, + sourceFile: sourceFile, + }); + return targets; + }); + } else if ( + ts.isModuleDeclaration(transformedStatement) && + ts.isIdentifier(transformedStatement.name) && + transformedStatement.name.text === "global" + ) { + // declare global + upsert(replacementTargets, declareGlobalSymbol, (targets = []) => { + targets.push({ + type: "declare-global", + originalStatement: transformedStatement, + statements: + transformedStatement.body && + ts.isModuleBlock(transformedStatement.body) + ? scanStatements( + printer, + transformedStatement.body.statements, + sourceFile, + ) + : new Map(), + sourceFile: sourceFile, + }); + return targets; + }); + } else { + upsert(replacementTargets, targetName, (statements = []) => { + statements.push({ + type: "non-interface", + statement: transformedStatement, + sourceFile: sourceFile, + }); + return statements; + }); } } } From 041d2f801c22971187c1fb3f20ff71b74c519b2b Mon Sep 17 00:00:00 2001 From: uhyo Date: Mon, 23 Sep 2024 20:56:12 +0900 Subject: [PATCH 4/4] feat: generate declare global replacement --- build/logic/generate.ts | 101 +++++++++++++++++++++++++++++++++--- build/util/mergeArrayMap.ts | 15 ++++++ 2 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 build/util/mergeArrayMap.ts diff --git a/build/logic/generate.ts b/build/logic/generate.ts index 3690aeb..b98d71c 100644 --- a/build/logic/generate.ts +++ b/build/logic/generate.ts @@ -1,8 +1,15 @@ import path from "path"; import ts from "typescript"; +import { mergeArrayMap } from "../util/mergeArrayMap"; import { upsert } from "../util/upsert"; import { getStatementDeclName } from "./ast/getStatementDeclName"; -import { ReplacementTarget, scanBetterFile } from "./scanBetterFile"; +import { + declareGlobalSymbol, + ReplacementMap, + ReplacementName, + ReplacementTarget, + scanBetterFile, +} from "./scanBetterFile"; type GenerateOptions = { emitOriginalAsComment?: boolean; @@ -39,9 +46,53 @@ export function generate( return result + originalFile.text; } - const consumedReplacements = new Set(); + return ( + result + + generateStatements( + printer, + originalFile, + originalFile.statements, + replacementTargets, + emitOriginalAsComment, + ) + ); +} + +function generateStatements( + printer: ts.Printer, + originalFile: ts.SourceFile, + statements: readonly ts.Statement[], + replacementTargets: ReplacementMap, + emitOriginalAsComment: boolean, +): string { + let result = ""; + const consumedReplacements = new Set(); + for (const statement of statements) { + if ( + ts.isModuleDeclaration(statement) && + ts.isIdentifier(statement.name) && + statement.name.text === "global" + ) { + // declare global { ... } + consumedReplacements.add(declareGlobalSymbol); + + const declareGlobalReplacement = + replacementTargets.get(declareGlobalSymbol); + if (declareGlobalReplacement === undefined) { + result += statement.getFullText(originalFile); + continue; + } + + result += generateDeclareGlobalReplacement( + printer, + originalFile, + statement, + declareGlobalReplacement, + emitOriginalAsComment, + ); + continue; + } - for (const statement of originalFile.statements) { const name = getStatementDeclName(statement); if (name === undefined) { result += statement.getFullText(originalFile); @@ -55,8 +106,9 @@ export function generate( consumedReplacements.add(name); - if (!ts.isInterfaceDeclaration(statement)) { - result += generateFullReplacement( + if (ts.isInterfaceDeclaration(statement)) { + result += generateInterface( + printer, originalFile, statement, replacementTarget, @@ -65,8 +117,7 @@ export function generate( continue; } - result += generateInterface( - printer, + result += generateFullReplacement( originalFile, statement, replacementTarget, @@ -120,6 +171,42 @@ function generateFullReplacement( return result; } +function generateDeclareGlobalReplacement( + printer: ts.Printer, + originalFile: ts.SourceFile, + statement: ts.ModuleDeclaration, + replacementTarget: readonly ReplacementTarget[], + emitOriginalAsComment: boolean, +) { + if (!replacementTarget.every((target) => target.type === "declare-global")) { + throw new Error("Invalid replacement target"); + } + if (!statement.body || !ts.isModuleBlock(statement.body)) { + return statement.getFullText(originalFile); + } + + const nestedStatements = statement.body.statements; + + let result = ""; + + result += "declare global {\n"; + + const nestedReplacementTarget = mergeArrayMap( + replacementTarget.map((t) => t.statements), + ); + + result += generateStatements( + printer, + originalFile, + nestedStatements, + nestedReplacementTarget, + emitOriginalAsComment, + ); + + result += "}\n"; + return result; +} + function generateInterface( printer: ts.Printer, originalFile: ts.SourceFile, diff --git a/build/util/mergeArrayMap.ts b/build/util/mergeArrayMap.ts new file mode 100644 index 0000000..94effd7 --- /dev/null +++ b/build/util/mergeArrayMap.ts @@ -0,0 +1,15 @@ +import { upsert } from "./upsert"; + +export function mergeArrayMap( + arrayMaps: readonly Map[], +): Map { + const result = new Map(); + for (const arrayMap of arrayMaps) { + for (const [key, value] of arrayMap) { + upsert(result, key, (array = []) => { + return array.concat(value); + }); + } + } + return result; +}