Skip to content

Commit d856f09

Browse files
authored
refactor: refactor aliasing system (#68)
* refactor: renew aliasing logic * chore: revert docs diff change
1 parent 1d4bc98 commit d856f09

8 files changed

+698
-573
lines changed

Diff for: build/logic/ReplacementMap.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type ts from "typescript";
2+
3+
export type ReplacementTarget = (
4+
| {
5+
type: "interface";
6+
originalStatement: ts.InterfaceDeclaration;
7+
members: Map<
8+
string,
9+
{
10+
member: ts.TypeElement;
11+
text: string;
12+
}[]
13+
>;
14+
}
15+
| {
16+
type: "declare-global";
17+
originalStatement: ts.ModuleDeclaration;
18+
statements: ReplacementMap;
19+
}
20+
| {
21+
type: "non-interface";
22+
statement: ts.Statement;
23+
}
24+
) & {
25+
optional: boolean;
26+
sourceFile: ts.SourceFile;
27+
};
28+
29+
export type ReplacementMap = Map<ReplacementName, ReplacementTarget[]>;
30+
31+
export const declareGlobalSymbol = Symbol("declare global");
32+
export type ReplacementName = string | typeof declareGlobalSymbol;
33+
34+
export function mergeReplacementMapInto(
35+
target: ReplacementMap,
36+
source: ReplacementMap,
37+
): void {
38+
for (const [key, value] of source) {
39+
target.set(key, [...(target.get(key) ?? []), ...value]);
40+
}
41+
}

Diff for: build/logic/generate.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { upsert } from "../util/upsert";
55
import { getStatementDeclName } from "./ast/getStatementDeclName";
66
import {
77
declareGlobalSymbol,
8-
ReplacementMap,
9-
ReplacementName,
10-
ReplacementTarget,
11-
scanBetterFile,
12-
} from "./scanBetterFile";
8+
mergeReplacementMapInto,
9+
type ReplacementMap,
10+
type ReplacementName,
11+
type ReplacementTarget,
12+
} from "./ReplacementMap";
13+
import { loadAliasFile, scanBetterFile } from "./scanBetterFile";
1314

1415
type GenerateOptions = {
1516
emitOriginalAsComment?: boolean;
@@ -41,6 +42,11 @@ export function generate(
4142
: "";
4243

4344
const replacementTargets = scanBetterFile(printer, targetFile);
45+
const { replacementMap: aliasReplacementMap } = loadAliasFile(
46+
printer,
47+
targetFile,
48+
);
49+
mergeReplacementMapInto(replacementTargets, aliasReplacementMap);
4450

4551
if (replacementTargets.size === 0) {
4652
return result + originalFile.text;
@@ -130,11 +136,21 @@ function generateStatements(
130136
for (const name of consumedReplacements) {
131137
replacementTargets.delete(name);
132138
}
133-
if (replacementTargets.size > 0) {
134-
result += "// --------------------\n";
135-
}
139+
140+
let lineInserted = false;
136141
for (const target of replacementTargets.values()) {
137142
for (const statement of target) {
143+
if (statement.optional) {
144+
// Since target from aliases may not be present in the original file,
145+
// aliases that have not been consumed are skipped.
146+
continue;
147+
}
148+
149+
if (!lineInserted) {
150+
result += "// --------------------\n";
151+
lineInserted = true;
152+
}
153+
138154
if (statement.type === "non-interface") {
139155
result += statement.statement.getFullText(statement.sourceFile);
140156
} else {

Diff for: build/logic/scanBetterFile.ts

+140-98
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,67 @@ import { alias } from "../util/alias";
44
import { upsert } from "../util/upsert";
55
import { getStatementDeclName } from "./ast/getStatementDeclName";
66
import { projectDir } from "./projectDir";
7+
import {
8+
declareGlobalSymbol,
9+
type ReplacementMap,
10+
type ReplacementName,
11+
type ReplacementTarget,
12+
} from "./ReplacementMap";
713

814
const betterLibDir = path.join(projectDir, "lib");
15+
const aliasFilePath = path.join(betterLibDir, "alias.d.ts");
916

10-
export type ReplacementTarget = (
11-
| {
12-
type: "interface";
13-
originalStatement: ts.InterfaceDeclaration;
14-
members: Map<
15-
string,
16-
{
17-
member: ts.TypeElement;
18-
text: string;
19-
}[]
20-
>;
21-
}
22-
| {
23-
type: "declare-global";
24-
originalStatement: ts.ModuleDeclaration;
25-
statements: ReplacementMap;
17+
type AliasFile = {
18+
replacementMap: ReplacementMap;
19+
};
20+
21+
// Cache for alias file statements
22+
let aliasFileCache: ts.SourceFile | undefined;
23+
24+
/**
25+
* Load the alias file applied to the target file.
26+
*/
27+
export function loadAliasFile(
28+
printer: ts.Printer,
29+
targetFileName: string,
30+
): AliasFile {
31+
if (!aliasFileCache) {
32+
const aliasProgram = ts.createProgram([aliasFilePath], {});
33+
const aliasFile = aliasProgram.getSourceFile(aliasFilePath);
34+
35+
if (!aliasFile) {
36+
throw new Error("Alias file not found in the program");
2637
}
27-
| {
28-
type: "non-interface";
29-
statement: ts.Statement;
38+
aliasFileCache = aliasFile;
39+
}
40+
const aliasFile = aliasFileCache;
41+
const statements = aliasFile.statements.flatMap((statement) => {
42+
const name = getStatementDeclName(statement) ?? "";
43+
const aliases = alias.get(name);
44+
if (!aliases) {
45+
return [statement];
3046
}
31-
) & {
32-
sourceFile: ts.SourceFile;
33-
};
47+
return aliases.map((aliasDetails) => {
48+
if (aliasDetails.file !== targetFileName) {
49+
return statement;
50+
}
51+
return replaceAliases(statement, aliasDetails.replacement);
52+
});
53+
});
3454

35-
export type ReplacementMap = Map<ReplacementName, ReplacementTarget[]>;
55+
// Scan the target file
56+
const replacementMap = scanStatements(printer, statements, aliasFile);
57+
// mark everything as optional
58+
for (const targets of replacementMap.values()) {
59+
for (const target of targets) {
60+
target.optional = true;
61+
}
62+
}
3663

37-
export const declareGlobalSymbol = Symbol("declare global");
38-
export type ReplacementName = string | typeof declareGlobalSymbol;
64+
return {
65+
replacementMap,
66+
};
67+
}
3968

4069
/**
4170
* Scan better lib file to determine which statements need to be replaced.
@@ -56,104 +85,117 @@ export function scanBetterFile(
5685

5786
function scanStatements(
5887
printer: ts.Printer,
59-
statements: ts.NodeArray<ts.Statement>,
88+
statements: readonly ts.Statement[],
6089
sourceFile: ts.SourceFile,
6190
): ReplacementMap {
6291
const replacementTargets = new Map<ReplacementName, ReplacementTarget[]>();
6392
for (const statement of statements) {
6493
const name = getStatementDeclName(statement) ?? "";
65-
const aliasesMap =
66-
alias.get(name) ?? new Map([[name, new Map<string, string>()]]);
67-
for (const [targetName, typeMap] of aliasesMap) {
68-
const transformedStatement = replaceAliases(statement, typeMap);
69-
if (ts.isInterfaceDeclaration(transformedStatement)) {
70-
const members = new Map<
71-
string,
72-
{
73-
member: ts.TypeElement;
74-
text: string;
75-
}[]
76-
>();
77-
for (const member of transformedStatement.members) {
78-
const memberName = member.name?.getText(sourceFile) ?? "";
79-
upsert(members, memberName, (members = []) => {
80-
const leadingSpacesMatch = /^\s*/.exec(
81-
member.getFullText(sourceFile),
82-
);
83-
const leadingSpaces =
84-
leadingSpacesMatch !== null ? leadingSpacesMatch[0] : "";
85-
members.push({
86-
member,
87-
text:
88-
leadingSpaces +
89-
printer.printNode(ts.EmitHint.Unspecified, member, sourceFile),
90-
});
91-
return members;
92-
});
93-
}
94-
upsert(replacementTargets, targetName, (targets = []) => {
95-
targets.push({
96-
type: "interface",
97-
members,
98-
originalStatement: transformedStatement,
99-
sourceFile: sourceFile,
94+
const transformedStatement = statement;
95+
if (ts.isInterfaceDeclaration(transformedStatement)) {
96+
const members = new Map<
97+
string,
98+
{
99+
member: ts.TypeElement;
100+
text: string;
101+
}[]
102+
>();
103+
for (const member of transformedStatement.members) {
104+
const memberName = member.name?.getText(sourceFile) ?? "";
105+
upsert(members, memberName, (members = []) => {
106+
const leadingSpacesMatch = /^\s*/.exec(
107+
member.getFullText(sourceFile),
108+
);
109+
const leadingSpaces =
110+
leadingSpacesMatch !== null ? leadingSpacesMatch[0] : "";
111+
members.push({
112+
member,
113+
text:
114+
leadingSpaces +
115+
printer.printNode(ts.EmitHint.Unspecified, member, sourceFile),
100116
});
101-
return targets;
117+
return members;
102118
});
103-
} else if (
104-
ts.isModuleDeclaration(transformedStatement) &&
105-
ts.isIdentifier(transformedStatement.name) &&
106-
transformedStatement.name.text === "global"
107-
) {
108-
// declare global
109-
upsert(replacementTargets, declareGlobalSymbol, (targets = []) => {
110-
targets.push({
111-
type: "declare-global",
112-
originalStatement: transformedStatement,
113-
statements:
114-
transformedStatement.body &&
115-
ts.isModuleBlock(transformedStatement.body)
116-
? scanStatements(
117-
printer,
118-
transformedStatement.body.statements,
119-
sourceFile,
120-
)
121-
: new Map(),
122-
sourceFile: sourceFile,
123-
});
124-
return targets;
119+
}
120+
upsert(replacementTargets, name, (targets = []) => {
121+
targets.push({
122+
type: "interface",
123+
members,
124+
originalStatement: transformedStatement,
125+
optional: false,
126+
sourceFile: sourceFile,
125127
});
126-
} else {
127-
upsert(replacementTargets, targetName, (statements = []) => {
128-
statements.push({
129-
type: "non-interface",
130-
statement: transformedStatement,
131-
sourceFile: sourceFile,
132-
});
133-
return statements;
128+
return targets;
129+
});
130+
} else if (
131+
ts.isModuleDeclaration(transformedStatement) &&
132+
ts.isIdentifier(transformedStatement.name) &&
133+
transformedStatement.name.text === "global"
134+
) {
135+
// declare global
136+
upsert(replacementTargets, declareGlobalSymbol, (targets = []) => {
137+
targets.push({
138+
type: "declare-global",
139+
originalStatement: transformedStatement,
140+
statements:
141+
transformedStatement.body &&
142+
ts.isModuleBlock(transformedStatement.body)
143+
? scanStatements(
144+
printer,
145+
transformedStatement.body.statements,
146+
sourceFile,
147+
)
148+
: new Map(),
149+
optional: false,
150+
sourceFile: sourceFile,
134151
});
135-
}
152+
return targets;
153+
});
154+
} else {
155+
upsert(replacementTargets, name, (statements = []) => {
156+
statements.push({
157+
type: "non-interface",
158+
statement: transformedStatement,
159+
optional: false,
160+
sourceFile: sourceFile,
161+
});
162+
return statements;
163+
});
136164
}
137165
}
138166
return replacementTargets;
139167
}
140168

141169
function replaceAliases(
142170
statement: ts.Statement,
143-
typeMap: Map<string, string>,
171+
replacement: Map<string, string>,
144172
): ts.Statement {
145-
if (typeMap.size === 0) return statement;
146173
return ts.transform(statement, [
147174
(context) => (sourceStatement) => {
148175
const visitor = (node: ts.Node): ts.Node => {
176+
if (ts.isInterfaceDeclaration(node)) {
177+
const toName = replacement.get(node.name.text);
178+
if (toName === undefined) {
179+
return node;
180+
}
181+
const visited = ts.visitEachChild(node, visitor, context);
182+
return ts.factory.updateInterfaceDeclaration(
183+
visited,
184+
visited.modifiers,
185+
ts.factory.createIdentifier(toName),
186+
visited.typeParameters,
187+
visited.heritageClauses,
188+
visited.members,
189+
);
190+
}
149191
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
150-
const replacementType = typeMap.get(node.typeName.text);
151-
if (replacementType === undefined) {
192+
const toName = replacement.get(node.typeName.text);
193+
if (toName === undefined) {
152194
return node;
153195
}
154196
return ts.factory.updateTypeReferenceNode(
155197
node,
156-
ts.factory.createIdentifier(replacementType),
198+
ts.factory.createIdentifier(toName),
157199
node.typeArguments,
158200
);
159201
}

0 commit comments

Comments
 (0)