Skip to content

Commit

Permalink
feat: supprort recursively replacing declare global modules (#50)
Browse files Browse the repository at this point in the history
* refactor: split generate.ts

* feat: scan declare global block

* refactor: change ReplacementTarget interface to support declare global blocks

* feat: generate declare global replacement
  • Loading branch information
uhyo authored Sep 23, 2024
1 parent e0c10d4 commit 1c07b23
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 151 deletions.
23 changes: 23 additions & 0 deletions build/logic/ast/getStatementDeclName.ts
Original file line number Diff line number Diff line change
@@ -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;
}
246 changes: 95 additions & 151 deletions build/logic/generate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import path from "path";
import ts from "typescript";
import { alias } from "../util/alias";
import { mergeArrayMap } from "../util/mergeArrayMap";
import { upsert } from "../util/upsert";
import { projectDir } from "./projectDir";

const betterLibDir = path.join(projectDir, "lib");
import { getStatementDeclName } from "./ast/getStatementDeclName";
import {
declareGlobalSymbol,
ReplacementMap,
ReplacementName,
ReplacementTarget,
scanBetterFile,
} from "./scanBetterFile";

type GenerateOptions = {
emitOriginalAsComment?: boolean;
Expand Down Expand Up @@ -41,9 +46,53 @@ export function generate(
return result + originalFile.text;
}

const consumedReplacements = new Set<string>();
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<ReplacementName>();
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);
Expand All @@ -57,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,
Expand All @@ -67,8 +117,7 @@ export function generate(
continue;
}

result += generateInterface(
printer,
result += generateFullReplacement(
originalFile,
statement,
replacementTarget,
Expand Down Expand Up @@ -122,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,
Expand Down Expand Up @@ -208,101 +293,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<string, ReplacementTarget[]> {
const replacementTargets = new Map<string, ReplacementTarget[]>();
{
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<string, string>()]]);
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.
*/
Expand Down Expand Up @@ -410,54 +400,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<string, string>,
): 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];
}
Loading

0 comments on commit 1c07b23

Please sign in to comment.