1import * as fs from "node:fs/promises";
2import * as path from "node:path";
3import * as ts from "typescript-api/unstable/ast";
4import { createSyntaxProject } from "./typescript-source.ts";
5
6/** Insert only the missing type argument/import, preserving comments and formatting. */
7export function migrateBindingPolicies(
8 sourceFile: ts.SourceFile,
9 providersPath: string,
10): string {
11 const filename = sourceFile.fileName;
12 let source = sourceFile.text;
13 const insertions: { offset: number; text: string }[] = [];
14 const visit = (node: ts.Node) => {
15 if (
16 ts.isCallExpression(node) &&
17 ts.isPropertyAccessExpression(node.expression)
18 ) {
19 const callee = node.expression;
20 if (
21 callee.name.text === "Policy" &&
22 ts.isIdentifier(callee.expression) &&
23 callee.expression.text === "Binding" &&
24 node.typeArguments?.length === 2
25 ) {
26 // Keep an existing trailing comma/comment intact.
27 insertions.push({
28 offset: node.typeArguments[1]!.end,
29 text: ", Providers",
30 });
31 }
32 }
33 node.forEachChild(visit);
34 };
35 sourceFile.forEachChild(visit);
36 if (!insertions.length) return source;
37
38 const imports = sourceFile.statements.filter(ts.isImportDeclaration);
39 const alreadyImported = imports.some((node) => {
40 const bindings = node.importClause?.namedBindings;
41 return (
42 bindings &&
43 ts.isNamedImports(bindings) &&
44 bindings.elements.some((specifier) => specifier.name.text === "Providers")
45 );
46 });
47 if (!alreadyImported) {
48 let relative = path
49 .relative(path.dirname(filename), providersPath)
50 .split(path.sep)
51 .join("/");
52 if (!relative.startsWith(".")) relative = `./${relative}`;
53 const newline = source.includes("\r\n") ? "\r\n" : "\n";
54 const lastImport = imports.at(-1);
55 const offset =
56 lastImport?.end ?? (source.startsWith("#!") ? source.indexOf("\n") : 0);
57 insertions.push({
58 offset,
59 text: `${offset ? newline : ""}import type { Providers } from ${JSON.stringify(relative)};${offset ? "" : newline}`,
60 });
61 }
62 for (const insertion of insertions.sort((a, b) => b.offset - a.offset)) {
63 source =
64 source.slice(0, insertion.offset) +
65 insertion.text +
66 source.slice(insertion.offset);
67 }
68 return source;
69}
70
71async function main() {
72 const provider = process.argv[2] ?? "AWS";
73 const srcRoot = path.join(
74 import.meta.dir,
75 "../packages/alchemy/src",
76 provider,
77 );
78 const providersPath = path.join(srcRoot, "Providers.ts");
79 const changed: string[] = [];
80 const files = (await fs.readdir(srcRoot, { recursive: true }))
81 .sort()
82 .filter((relative) => relative.endsWith(".ts"))
83 .map((relative) => path.join(srcRoot, relative))
84 .filter((filename) => filename !== providersPath);
85 await using syntax = await createSyntaxProject(files);
86 for (const filename of files) {
87 const sourceFile = await syntax.project.program.getSourceFile(filename);
88 if (!sourceFile) throw new Error(`Missing source file ${filename}`);
89 const source = sourceFile.text;
90 const migrated = migrateBindingPolicies(sourceFile, providersPath);
91 if (migrated === source) continue;
92 await fs.writeFile(filename, migrated);
93 changed.push(path.relative(process.cwd(), filename));
94 }
95 console.log(`Modified ${changed.length} file(s):`);
96 for (const file of changed.sort()) console.log(` ${file}`);
97}
98
99if (import.meta.main) await main();