my monorepo for atproto based applications
1import nodePath from "node:path";
2import process from "node:process";
3import fs from "node:fs/promises";
4import child_process from "node:child_process";
5import { promisify } from "node:util";
6
7import { Project } from "ts-morph";
8
9const lexiconDir = nodePath.join(import.meta.dirname!, "..", "..", "lexicon");
10
11export async function generate(): Promise<void> {
12 await Promise.all([
13 generateCLI("api").then(() => fixTS(`${lexiconDir}/api`)),
14 generateCLI("server").then(() => fixTS(`${lexiconDir}/server`)),
15 ]);
16 await promisify(child_process.exec)(`npx prettier -w ${lexiconDir}`);
17}
18
19async function generateCLI(genType: "api" | "server"): Promise<void> {
20 let shell = process.env.SHELL ?? "sh";
21
22 await promisify(child_process.exec)(
23 `${shell} -c "echo y | npx @atproto/lex-cli@0.7 -- gen-${genType} '${lexiconDir}/${genType}/' ${lexiconDir}/definitions/**/*.json"`,
24 );
25}
26
27// async function generateAPI(): Promise<void> {
28// const cmd = new Deno.Command(Deno.execPath(), {
29// args: ["task", "lex-gen-api"],
30// cwd: import.meta.dirname,
31// });
32
33// const { success, code: exitCode } = await cmd.output();
34// if (!success) {
35// console.error(
36// "Error: task lex-gen-api returned with error exit code",
37// exitCode,
38// );
39// process.exit(exitCode);
40// }
41// const errors = await denofyTsDir(apiOutDir);
42// if (errors) {
43// console.error(
44// `Errors occurred during file changes:\n${errors
45// .map((e) => e.message)
46// .join("\n")}`,
47// );
48// process.exit(1);
49// }
50// console.log("done generating api");
51// }
52
53// async function generateServer(): Promise<void> {
54// const cmd = new Deno.Command(Deno.execPath(), {
55// args: ["task", "lex-gen-server"],
56// cwd: import.meta.dirname,
57// });
58
59// const { success, code: exitCode } = await cmd.output();
60// if (!success) {
61// console.error(
62// "Error: task lex-gen-api returned with error exit code",
63// exitCode,
64// );
65// process.exit(exitCode);
66// }
67// const errors = await denofyTsDir(serverOutDir);
68// if (errors) {
69// console.error(
70// `Errors occurred during file changes:\n${errors
71// .map((e) => e.message)
72// .join("\n")}`,
73// );
74// process.exit(1);
75// }
76// console.log("done generating server");
77// }
78
79async function fixTS(root: string): Promise<undefined | Error[]> {
80 const ps: Promise<void>[] = [];
81 for await (const fname of tsFiles(root)) {
82 ps.push(
83 (async () => {
84 try {
85 await fixTSFile(fname);
86 } catch (e) {
87 const msg = e instanceof Error ? e.message : `${e}`;
88 throw new Error(`${fname}: ${msg}`);
89 }
90 })(),
91 );
92 }
93 const errors: Error[] = [];
94 for (const r of await Promise.allSettled(ps)) {
95 if (r.status === "rejected") {
96 errors.push(r.reason as Error);
97 }
98 }
99 console.log("root", root, "errors=", errors.length);
100 return errors.length == 0 ? undefined : errors;
101}
102// async function denofyTsDir(root: string): Promise<undefined | Error[]> {
103// const ps: Promise<void>[] = [];
104// for await (const fname of tsFiles(root)) {
105// ps.push(
106// (async () => {
107// try {
108// await denofyTsFile(fname);
109// } catch (e) {
110// const msg = e instanceof Error ? e.message : `${e}`;
111// throw new Error(`${fname}: ${msg}`);
112// }
113// })(),
114// );
115// }
116// const errors: Error[] = [];
117// for (const r of await Promise.allSettled(ps)) {
118// if (r.status === "rejected") {
119// errors.push(r.reason as Error);
120// }
121// }
122// return errors.length == 0 ? undefined : errors;
123// }
124
125async function fixTSFile(filePath: string): Promise<void> {
126 const src = new Project({
127 skipFileDependencyResolution: true,
128 skipLoadingLibFiles: true,
129 skipAddingFilesFromTsConfig: true,
130 }).addSourceFileAtPath(filePath);
131
132 src.getImportDeclarations().forEach((imp) => {
133 if (!imp.isModuleSpecifierRelative()) {
134 // const current = imp.getModuleSpecifierValue();
135 // if (
136 // !current.startsWith("@atproto") &&
137 // !current.startsWith("multiformats")
138 // ) {
139 // imp.setModuleSpecifier("npm:" + current);
140 // }
141 return;
142 }
143 const current = imp.getModuleSpecifierValue();
144 let modified = current;
145 if (current.endsWith(".js")) {
146 modified = current.slice(0, current.length - ".js".length);
147 }
148 modified += ".ts";
149 imp.setModuleSpecifier(modified);
150 console.log(
151 "modified",
152 modified,
153 "in",
154 src.getFilePath().split("/").at(-1),
155 );
156 });
157
158 src.getExportDeclarations().forEach((exp) => {
159 if (!exp.isModuleSpecifierRelative()) {
160 return;
161 }
162 const current = exp.getModuleSpecifierValue();
163 if (!current) {
164 throw new Error(`unexpected export: ${exp.getText()}`);
165 }
166 let modified = current;
167 if (current.endsWith(".js")) {
168 modified = current.slice(0, current.length - ".js".length);
169 }
170 modified += ".ts";
171 exp.setModuleSpecifier(modified);
172 });
173
174 await src.save();
175 console.log("saved", src.getFilePath());
176}
177
178// async function denofyTsFile(filePath: string): Promise<void> {
179// const src = new Project({
180// skipFileDependencyResolution: true,
181// skipLoadingLibFiles: true,
182// skipAddingFilesFromTsConfig: true,
183// }).addSourceFileAtPath(filePath);
184
185// src.getImportDeclarations().forEach((imp) => {
186// if (!imp.isModuleSpecifierRelative()) {
187// const current = imp.getModuleSpecifierValue();
188// if (
189// !current.startsWith("@atproto") &&
190// !current.startsWith("multiformats")
191// ) {
192// imp.setModuleSpecifier("npm:" + current);
193// }
194// return;
195// }
196// const current = imp.getModuleSpecifierValue();
197// let modified = current;
198// if (current.endsWith(".js")) {
199// modified = current.slice(0, current.length - ".js".length);
200// }
201// modified += ".ts";
202// imp.setModuleSpecifier(modified);
203// });
204
205// src.getExportDeclarations().forEach((exp) => {
206// if (!exp.isModuleSpecifierRelative()) {
207// return;
208// }
209// const current = exp.getModuleSpecifierValue();
210// if (!current) {
211// throw new Error(`unexpected export: ${exp.getText()}`);
212// }
213// let modified = current;
214// if (current.endsWith(".js")) {
215// modified = current.slice(0, current.length - ".js".length);
216// }
217// modified += ".ts";
218// exp.setModuleSpecifier(modified);
219// });
220// await src.save();
221// }
222
223async function* tsFiles(rootDir: string): AsyncIterable<string> {
224 for (const f of await fs.readdir(rootDir, { withFileTypes: true })) {
225 const currentPath = nodePath.join(rootDir, f.name);
226 if (f.isDirectory()) {
227 yield* tsFiles(currentPath);
228 }
229 if (f.isFile() && f.name.endsWith(".ts")) {
230 yield currentPath;
231 }
232 }
233}