Skip to content

Commit 1c07b23

Browse files
authored
feat: supprort recursively replacing declare global modules (#50)
* refactor: split generate.ts * feat: scan declare global block * refactor: change ReplacementTarget interface to support declare global blocks * feat: generate declare global replacement
1 parent e0c10d4 commit 1c07b23

File tree

4 files changed

+298
-151
lines changed

4 files changed

+298
-151
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ts from "typescript";
2+
3+
export function getStatementDeclName(
4+
statement: ts.Statement,
5+
): string | undefined {
6+
if (ts.isVariableStatement(statement)) {
7+
for (const dec of statement.declarationList.declarations) {
8+
if (ts.isIdentifier(dec.name)) {
9+
return dec.name.text;
10+
}
11+
}
12+
} else if (
13+
ts.isFunctionDeclaration(statement) ||
14+
ts.isInterfaceDeclaration(statement) ||
15+
ts.isTypeAliasDeclaration(statement) ||
16+
ts.isModuleDeclaration(statement)
17+
) {
18+
return statement.name?.text;
19+
} else if (ts.isInterfaceDeclaration(statement)) {
20+
return statement.name.text;
21+
}
22+
return undefined;
23+
}

build/logic/generate.ts

Lines changed: 95 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import path from "path";
22
import ts from "typescript";
3-
import { alias } from "../util/alias";
3+
import { mergeArrayMap } from "../util/mergeArrayMap";
44
import { upsert } from "../util/upsert";
5-
import { projectDir } from "./projectDir";
6-
7-
const betterLibDir = path.join(projectDir, "lib");
5+
import { getStatementDeclName } from "./ast/getStatementDeclName";
6+
import {
7+
declareGlobalSymbol,
8+
ReplacementMap,
9+
ReplacementName,
10+
ReplacementTarget,
11+
scanBetterFile,
12+
} from "./scanBetterFile";
813

914
type GenerateOptions = {
1015
emitOriginalAsComment?: boolean;
@@ -41,9 +46,53 @@ export function generate(
4146
return result + originalFile.text;
4247
}
4348

44-
const consumedReplacements = new Set<string>();
49+
return (
50+
result +
51+
generateStatements(
52+
printer,
53+
originalFile,
54+
originalFile.statements,
55+
replacementTargets,
56+
emitOriginalAsComment,
57+
)
58+
);
59+
}
60+
61+
function generateStatements(
62+
printer: ts.Printer,
63+
originalFile: ts.SourceFile,
64+
statements: readonly ts.Statement[],
65+
replacementTargets: ReplacementMap,
66+
emitOriginalAsComment: boolean,
67+
): string {
68+
let result = "";
69+
const consumedReplacements = new Set<ReplacementName>();
70+
for (const statement of statements) {
71+
if (
72+
ts.isModuleDeclaration(statement) &&
73+
ts.isIdentifier(statement.name) &&
74+
statement.name.text === "global"
75+
) {
76+
// declare global { ... }
77+
consumedReplacements.add(declareGlobalSymbol);
78+
79+
const declareGlobalReplacement =
80+
replacementTargets.get(declareGlobalSymbol);
81+
if (declareGlobalReplacement === undefined) {
82+
result += statement.getFullText(originalFile);
83+
continue;
84+
}
85+
86+
result += generateDeclareGlobalReplacement(
87+
printer,
88+
originalFile,
89+
statement,
90+
declareGlobalReplacement,
91+
emitOriginalAsComment,
92+
);
93+
continue;
94+
}
4595

46-
for (const statement of originalFile.statements) {
4796
const name = getStatementDeclName(statement);
4897
if (name === undefined) {
4998
result += statement.getFullText(originalFile);
@@ -57,8 +106,9 @@ export function generate(
57106

58107
consumedReplacements.add(name);
59108

60-
if (!ts.isInterfaceDeclaration(statement)) {
61-
result += generateFullReplacement(
109+
if (ts.isInterfaceDeclaration(statement)) {
110+
result += generateInterface(
111+
printer,
62112
originalFile,
63113
statement,
64114
replacementTarget,
@@ -67,8 +117,7 @@ export function generate(
67117
continue;
68118
}
69119

70-
result += generateInterface(
71-
printer,
120+
result += generateFullReplacement(
72121
originalFile,
73122
statement,
74123
replacementTarget,
@@ -122,6 +171,42 @@ function generateFullReplacement(
122171
return result;
123172
}
124173

174+
function generateDeclareGlobalReplacement(
175+
printer: ts.Printer,
176+
originalFile: ts.SourceFile,
177+
statement: ts.ModuleDeclaration,
178+
replacementTarget: readonly ReplacementTarget[],
179+
emitOriginalAsComment: boolean,
180+
) {
181+
if (!replacementTarget.every((target) => target.type === "declare-global")) {
182+
throw new Error("Invalid replacement target");
183+
}
184+
if (!statement.body || !ts.isModuleBlock(statement.body)) {
185+
return statement.getFullText(originalFile);
186+
}
187+
188+
const nestedStatements = statement.body.statements;
189+
190+
let result = "";
191+
192+
result += "declare global {\n";
193+
194+
const nestedReplacementTarget = mergeArrayMap(
195+
replacementTarget.map((t) => t.statements),
196+
);
197+
198+
result += generateStatements(
199+
printer,
200+
originalFile,
201+
nestedStatements,
202+
nestedReplacementTarget,
203+
emitOriginalAsComment,
204+
);
205+
206+
result += "}\n";
207+
return result;
208+
}
209+
125210
function generateInterface(
126211
printer: ts.Printer,
127212
originalFile: ts.SourceFile,
@@ -208,101 +293,6 @@ function generateInterface(
208293
return result;
209294
}
210295

211-
type ReplacementTarget = (
212-
| {
213-
type: "interface";
214-
originalStatement: ts.InterfaceDeclaration;
215-
members: Map<
216-
string,
217-
{
218-
member: ts.TypeElement;
219-
text: string;
220-
}[]
221-
>;
222-
}
223-
| {
224-
type: "non-interface";
225-
statement: ts.Statement;
226-
}
227-
) & {
228-
sourceFile: ts.SourceFile;
229-
};
230-
231-
/**
232-
* Scan better lib file to determine which statements need to be replaced.
233-
*/
234-
function scanBetterFile(
235-
printer: ts.Printer,
236-
targetFile: string,
237-
): Map<string, ReplacementTarget[]> {
238-
const replacementTargets = new Map<string, ReplacementTarget[]>();
239-
{
240-
const betterLibFile = path.join(betterLibDir, targetFile);
241-
const betterProgram = ts.createProgram([betterLibFile], {});
242-
const betterFile = betterProgram.getSourceFile(betterLibFile);
243-
if (betterFile) {
244-
// Scan better file to determine which statements need to be replaced.
245-
for (const statement of betterFile.statements) {
246-
const name = getStatementDeclName(statement) ?? "";
247-
const aliasesMap =
248-
alias.get(name) ?? new Map([[name, new Map<string, string>()]]);
249-
for (const [targetName, typeMap] of aliasesMap) {
250-
const transformedStatement = replaceAliases(statement, typeMap);
251-
if (ts.isInterfaceDeclaration(transformedStatement)) {
252-
const members = new Map<
253-
string,
254-
{
255-
member: ts.TypeElement;
256-
text: string;
257-
}[]
258-
>();
259-
for (const member of transformedStatement.members) {
260-
const memberName = member.name?.getText(betterFile) ?? "";
261-
upsert(members, memberName, (members = []) => {
262-
const leadingSpacesMatch = /^\s*/.exec(
263-
member.getFullText(betterFile),
264-
);
265-
const leadingSpaces =
266-
leadingSpacesMatch !== null ? leadingSpacesMatch[0] : "";
267-
members.push({
268-
member,
269-
text:
270-
leadingSpaces +
271-
printer.printNode(
272-
ts.EmitHint.Unspecified,
273-
member,
274-
betterFile,
275-
),
276-
});
277-
return members;
278-
});
279-
}
280-
upsert(replacementTargets, targetName, (targets = []) => {
281-
targets.push({
282-
type: "interface",
283-
members,
284-
originalStatement: transformedStatement,
285-
sourceFile: betterFile,
286-
});
287-
return targets;
288-
});
289-
} else {
290-
upsert(replacementTargets, targetName, (statements = []) => {
291-
statements.push({
292-
type: "non-interface",
293-
statement: transformedStatement,
294-
sourceFile: betterFile,
295-
});
296-
return statements;
297-
});
298-
}
299-
}
300-
}
301-
}
302-
}
303-
return replacementTargets;
304-
}
305-
306296
/**
307297
* Determines whether interface can be partially replaced.
308298
*/
@@ -410,54 +400,8 @@ function printInterface(
410400
return result;
411401
}
412402

413-
function getStatementDeclName(statement: ts.Statement): string | undefined {
414-
if (ts.isVariableStatement(statement)) {
415-
for (const dec of statement.declarationList.declarations) {
416-
if (ts.isIdentifier(dec.name)) {
417-
return dec.name.text;
418-
}
419-
}
420-
} else if (
421-
ts.isFunctionDeclaration(statement) ||
422-
ts.isInterfaceDeclaration(statement) ||
423-
ts.isTypeAliasDeclaration(statement) ||
424-
ts.isModuleDeclaration(statement)
425-
) {
426-
return statement.name?.text;
427-
} else if (ts.isInterfaceDeclaration(statement)) {
428-
return statement.name.text;
429-
}
430-
return undefined;
431-
}
432-
433403
function commentOut(code: string): string {
434404
const lines = code.split("\n").filter((line) => line.trim().length > 0);
435405
const result = lines.map((line) => `// ${line}`);
436406
return result.join("\n") + "\n";
437407
}
438-
439-
function replaceAliases(
440-
statement: ts.Statement,
441-
typeMap: Map<string, string>,
442-
): ts.Statement {
443-
if (typeMap.size === 0) return statement;
444-
return ts.transform(statement, [
445-
(context) => (sourceStatement) => {
446-
const visitor = (node: ts.Node): ts.Node => {
447-
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
448-
const replacementType = typeMap.get(node.typeName.text);
449-
if (replacementType === undefined) {
450-
return node;
451-
}
452-
return ts.factory.updateTypeReferenceNode(
453-
node,
454-
ts.factory.createIdentifier(replacementType),
455-
node.typeArguments,
456-
);
457-
}
458-
return ts.visitEachChild(node, visitor, context);
459-
};
460-
return ts.visitNode(sourceStatement, visitor, ts.isStatement);
461-
},
462-
]).transformed[0];
463-
}

0 commit comments

Comments
 (0)