Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: refactor aliasing system #68

Merged
merged 2 commits into from
Mar 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions build/logic/ReplacementMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type ts from "typescript";

export type ReplacementTarget = (
| {
type: "interface";
originalStatement: ts.InterfaceDeclaration;
members: Map<
string,
{
member: ts.TypeElement;
text: string;
}[]
>;
}
| {
type: "declare-global";
originalStatement: ts.ModuleDeclaration;
statements: ReplacementMap;
}
| {
type: "non-interface";
statement: ts.Statement;
}
) & {
optional: boolean;
sourceFile: ts.SourceFile;
};

export type ReplacementMap = Map<ReplacementName, ReplacementTarget[]>;

export const declareGlobalSymbol = Symbol("declare global");
export type ReplacementName = string | typeof declareGlobalSymbol;

export function mergeReplacementMapInto(
target: ReplacementMap,
source: ReplacementMap,
): void {
for (const [key, value] of source) {
target.set(key, [...(target.get(key) ?? []), ...value]);
}
}
32 changes: 24 additions & 8 deletions build/logic/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { upsert } from "../util/upsert";
import { getStatementDeclName } from "./ast/getStatementDeclName";
import {
declareGlobalSymbol,
ReplacementMap,
ReplacementName,
ReplacementTarget,
scanBetterFile,
} from "./scanBetterFile";
mergeReplacementMapInto,
type ReplacementMap,
type ReplacementName,
type ReplacementTarget,
} from "./ReplacementMap";
import { loadAliasFile, scanBetterFile } from "./scanBetterFile";

type GenerateOptions = {
emitOriginalAsComment?: boolean;
Expand Down Expand Up @@ -41,6 +42,11 @@ export function generate(
: "";

const replacementTargets = scanBetterFile(printer, targetFile);
const { replacementMap: aliasReplacementMap } = loadAliasFile(
printer,
targetFile,
);
mergeReplacementMapInto(replacementTargets, aliasReplacementMap);

if (replacementTargets.size === 0) {
return result + originalFile.text;
Expand Down Expand Up @@ -130,11 +136,21 @@ function generateStatements(
for (const name of consumedReplacements) {
replacementTargets.delete(name);
}
if (replacementTargets.size > 0) {
result += "// --------------------\n";
}

let lineInserted = false;
for (const target of replacementTargets.values()) {
for (const statement of target) {
if (statement.optional) {
// Since target from aliases may not be present in the original file,
// aliases that have not been consumed are skipped.
continue;
}

if (!lineInserted) {
result += "// --------------------\n";
lineInserted = true;
}

if (statement.type === "non-interface") {
result += statement.statement.getFullText(statement.sourceFile);
} else {
Expand Down
238 changes: 140 additions & 98 deletions build/logic/scanBetterFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,67 @@ import { alias } from "../util/alias";
import { upsert } from "../util/upsert";
import { getStatementDeclName } from "./ast/getStatementDeclName";
import { projectDir } from "./projectDir";
import {
declareGlobalSymbol,
type ReplacementMap,
type ReplacementName,
type ReplacementTarget,
} from "./ReplacementMap";

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

export type ReplacementTarget = (
| {
type: "interface";
originalStatement: ts.InterfaceDeclaration;
members: Map<
string,
{
member: ts.TypeElement;
text: string;
}[]
>;
}
| {
type: "declare-global";
originalStatement: ts.ModuleDeclaration;
statements: ReplacementMap;
type AliasFile = {
replacementMap: ReplacementMap;
};

// Cache for alias file statements
let aliasFileCache: ts.SourceFile | undefined;

/**
* Load the alias file applied to the target file.
*/
export function loadAliasFile(
printer: ts.Printer,
targetFileName: string,
): AliasFile {
if (!aliasFileCache) {
const aliasProgram = ts.createProgram([aliasFilePath], {});
const aliasFile = aliasProgram.getSourceFile(aliasFilePath);

if (!aliasFile) {
throw new Error("Alias file not found in the program");
}
| {
type: "non-interface";
statement: ts.Statement;
aliasFileCache = aliasFile;
}
const aliasFile = aliasFileCache;
const statements = aliasFile.statements.flatMap((statement) => {
const name = getStatementDeclName(statement) ?? "";
const aliases = alias.get(name);
if (!aliases) {
return [statement];
}
) & {
sourceFile: ts.SourceFile;
};
return aliases.map((aliasDetails) => {
if (aliasDetails.file !== targetFileName) {
return statement;
}
return replaceAliases(statement, aliasDetails.replacement);
});
});

export type ReplacementMap = Map<ReplacementName, ReplacementTarget[]>;
// Scan the target file
const replacementMap = scanStatements(printer, statements, aliasFile);
// mark everything as optional
for (const targets of replacementMap.values()) {
for (const target of targets) {
target.optional = true;
}
}

export const declareGlobalSymbol = Symbol("declare global");
export type ReplacementName = string | typeof declareGlobalSymbol;
return {
replacementMap,
};
}

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

function scanStatements(
printer: ts.Printer,
statements: ts.NodeArray<ts.Statement>,
statements: readonly ts.Statement[],
sourceFile: ts.SourceFile,
): ReplacementMap {
const replacementTargets = new Map<ReplacementName, ReplacementTarget[]>();
for (const statement of 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(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),
});
return members;
});
}
upsert(replacementTargets, targetName, (targets = []) => {
targets.push({
type: "interface",
members,
originalStatement: transformedStatement,
sourceFile: sourceFile,
const transformedStatement = statement;
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),
});
return targets;
return members;
});
} 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;
}
upsert(replacementTargets, name, (targets = []) => {
targets.push({
type: "interface",
members,
originalStatement: transformedStatement,
optional: false,
sourceFile: sourceFile,
});
} else {
upsert(replacementTargets, targetName, (statements = []) => {
statements.push({
type: "non-interface",
statement: transformedStatement,
sourceFile: sourceFile,
});
return statements;
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(),
optional: false,
sourceFile: sourceFile,
});
}
return targets;
});
} else {
upsert(replacementTargets, name, (statements = []) => {
statements.push({
type: "non-interface",
statement: transformedStatement,
optional: false,
sourceFile: sourceFile,
});
return statements;
});
}
}
return replacementTargets;
}

function replaceAliases(
statement: ts.Statement,
typeMap: Map<string, string>,
replacement: 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.isInterfaceDeclaration(node)) {
const toName = replacement.get(node.name.text);
if (toName === undefined) {
return node;
}
const visited = ts.visitEachChild(node, visitor, context);
return ts.factory.updateInterfaceDeclaration(
visited,
visited.modifiers,
ts.factory.createIdentifier(toName),
visited.typeParameters,
visited.heritageClauses,
visited.members,
);
}
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
const replacementType = typeMap.get(node.typeName.text);
if (replacementType === undefined) {
const toName = replacement.get(node.typeName.text);
if (toName === undefined) {
return node;
}
return ts.factory.updateTypeReferenceNode(
node,
ts.factory.createIdentifier(replacementType),
ts.factory.createIdentifier(toName),
node.typeArguments,
);
}
Expand Down
Loading