···11+{
22+ "**/*.ts": [
33+ "This Source Code Form is subject to the terms of the Mozilla Public",
44+ "License, v. 2.0. If a copy of the MPL was not distributed with this",
55+ "file, You can obtain one at https://mozilla.org/MPL/2.0/."
66+ ],
77+ "ignore": [
88+ "example",
99+ "npm",
1010+ "generated",
1111+ ".lxq"
1212+ ]
1313+}
+373
LICENSE
···11+Mozilla Public License Version 2.0
22+==================================
33+44+1. Definitions
55+--------------
66+77+1.1. "Contributor"
88+ means each individual or legal entity that creates, contributes to
99+ the creation of, or owns Covered Software.
1010+1111+1.2. "Contributor Version"
1212+ means the combination of the Contributions of others (if any) used
1313+ by a Contributor and that particular Contributor's Contribution.
1414+1515+1.3. "Contribution"
1616+ means Covered Software of a particular Contributor.
1717+1818+1.4. "Covered Software"
1919+ means Source Code Form to which the initial Contributor has attached
2020+ the notice in Exhibit A, the Executable Form of such Source Code
2121+ Form, and Modifications of such Source Code Form, in each case
2222+ including portions thereof.
2323+2424+1.5. "Incompatible With Secondary Licenses"
2525+ means
2626+2727+ (a) that the initial Contributor has attached the notice described
2828+ in Exhibit B to the Covered Software; or
2929+3030+ (b) that the Covered Software was made available under the terms of
3131+ version 1.1 or earlier of the License, but not also under the
3232+ terms of a Secondary License.
3333+3434+1.6. "Executable Form"
3535+ means any form of the work other than Source Code Form.
3636+3737+1.7. "Larger Work"
3838+ means a work that combines Covered Software with other material, in
3939+ a separate file or files, that is not Covered Software.
4040+4141+1.8. "License"
4242+ means this document.
4343+4444+1.9. "Licensable"
4545+ means having the right to grant, to the maximum extent possible,
4646+ whether at the time of the initial grant or subsequently, any and
4747+ all of the rights conveyed by this License.
4848+4949+1.10. "Modifications"
5050+ means any of the following:
5151+5252+ (a) any file in Source Code Form that results from an addition to,
5353+ deletion from, or modification of the contents of Covered
5454+ Software; or
5555+5656+ (b) any new file in Source Code Form that contains any Covered
5757+ Software.
5858+5959+1.11. "Patent Claims" of a Contributor
6060+ means any patent claim(s), including without limitation, method,
6161+ process, and apparatus claims, in any patent Licensable by such
6262+ Contributor that would be infringed, but for the grant of the
6363+ License, by the making, using, selling, offering for sale, having
6464+ made, import, or transfer of either its Contributions or its
6565+ Contributor Version.
6666+6767+1.12. "Secondary License"
6868+ means either the GNU General Public License, Version 2.0, the GNU
6969+ Lesser General Public License, Version 2.1, the GNU Affero General
7070+ Public License, Version 3.0, or any later versions of those
7171+ licenses.
7272+7373+1.13. "Source Code Form"
7474+ means the form of the work preferred for making modifications.
7575+7676+1.14. "You" (or "Your")
7777+ means an individual or a legal entity exercising rights under this
7878+ License. For legal entities, "You" includes any entity that
7979+ controls, is controlled by, or is under common control with You. For
8080+ purposes of this definition, "control" means (a) the power, direct
8181+ or indirect, to cause the direction or management of such entity,
8282+ whether by contract or otherwise, or (b) ownership of more than
8383+ fifty percent (50%) of the outstanding shares or beneficial
8484+ ownership of such entity.
8585+8686+2. License Grants and Conditions
8787+--------------------------------
8888+8989+2.1. Grants
9090+9191+Each Contributor hereby grants You a world-wide, royalty-free,
9292+non-exclusive license:
9393+9494+(a) under intellectual property rights (other than patent or trademark)
9595+ Licensable by such Contributor to use, reproduce, make available,
9696+ modify, display, perform, distribute, and otherwise exploit its
9797+ Contributions, either on an unmodified basis, with Modifications, or
9898+ as part of a Larger Work; and
9999+100100+(b) under Patent Claims of such Contributor to make, use, sell, offer
101101+ for sale, have made, import, and otherwise transfer either its
102102+ Contributions or its Contributor Version.
103103+104104+2.2. Effective Date
105105+106106+The licenses granted in Section 2.1 with respect to any Contribution
107107+become effective for each Contribution on the date the Contributor first
108108+distributes such Contribution.
109109+110110+2.3. Limitations on Grant Scope
111111+112112+The licenses granted in this Section 2 are the only rights granted under
113113+this License. No additional rights or licenses will be implied from the
114114+distribution or licensing of Covered Software under this License.
115115+Notwithstanding Section 2.1(b) above, no patent license is granted by a
116116+Contributor:
117117+118118+(a) for any code that a Contributor has removed from Covered Software;
119119+ or
120120+121121+(b) for infringements caused by: (i) Your and any other third party's
122122+ modifications of Covered Software, or (ii) the combination of its
123123+ Contributions with other software (except as part of its Contributor
124124+ Version); or
125125+126126+(c) under Patent Claims infringed by Covered Software in the absence of
127127+ its Contributions.
128128+129129+This License does not grant any rights in the trademarks, service marks,
130130+or logos of any Contributor (except as may be necessary to comply with
131131+the notice requirements in Section 3.4).
132132+133133+2.4. Subsequent Licenses
134134+135135+No Contributor makes additional grants as a result of Your choice to
136136+distribute the Covered Software under a subsequent version of this
137137+License (see Section 10.2) or under the terms of a Secondary License (if
138138+permitted under the terms of Section 3.3).
139139+140140+2.5. Representation
141141+142142+Each Contributor represents that the Contributor believes its
143143+Contributions are its original creation(s) or it has sufficient rights
144144+to grant the rights to its Contributions conveyed by this License.
145145+146146+2.6. Fair Use
147147+148148+This License is not intended to limit any rights You have under
149149+applicable copyright doctrines of fair use, fair dealing, or other
150150+equivalents.
151151+152152+2.7. Conditions
153153+154154+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155155+in Section 2.1.
156156+157157+3. Responsibilities
158158+-------------------
159159+160160+3.1. Distribution of Source Form
161161+162162+All distribution of Covered Software in Source Code Form, including any
163163+Modifications that You create or to which You contribute, must be under
164164+the terms of this License. You must inform recipients that the Source
165165+Code Form of the Covered Software is governed by the terms of this
166166+License, and how they can obtain a copy of this License. You may not
167167+attempt to alter or restrict the recipients' rights in the Source Code
168168+Form.
169169+170170+3.2. Distribution of Executable Form
171171+172172+If You distribute Covered Software in Executable Form then:
173173+174174+(a) such Covered Software must also be made available in Source Code
175175+ Form, as described in Section 3.1, and You must inform recipients of
176176+ the Executable Form how they can obtain a copy of such Source Code
177177+ Form by reasonable means in a timely manner, at a charge no more
178178+ than the cost of distribution to the recipient; and
179179+180180+(b) You may distribute such Executable Form under the terms of this
181181+ License, or sublicense it under different terms, provided that the
182182+ license for the Executable Form does not attempt to limit or alter
183183+ the recipients' rights in the Source Code Form under this License.
184184+185185+3.3. Distribution of a Larger Work
186186+187187+You may create and distribute a Larger Work under terms of Your choice,
188188+provided that You also comply with the requirements of this License for
189189+the Covered Software. If the Larger Work is a combination of Covered
190190+Software with a work governed by one or more Secondary Licenses, and the
191191+Covered Software is not Incompatible With Secondary Licenses, this
192192+License permits You to additionally distribute such Covered Software
193193+under the terms of such Secondary License(s), so that the recipient of
194194+the Larger Work may, at their option, further distribute the Covered
195195+Software under the terms of either this License or such Secondary
196196+License(s).
197197+198198+3.4. Notices
199199+200200+You may not remove or alter the substance of any license notices
201201+(including copyright notices, patent notices, disclaimers of warranty,
202202+or limitations of liability) contained within the Source Code Form of
203203+the Covered Software, except that You may alter any license notices to
204204+the extent required to remedy known factual inaccuracies.
205205+206206+3.5. Application of Additional Terms
207207+208208+You may choose to offer, and to charge a fee for, warranty, support,
209209+indemnity or liability obligations to one or more recipients of Covered
210210+Software. However, You may do so only on Your own behalf, and not on
211211+behalf of any Contributor. You must make it absolutely clear that any
212212+such warranty, support, indemnity, or liability obligation is offered by
213213+You alone, and You hereby agree to indemnify every Contributor for any
214214+liability incurred by such Contributor as a result of warranty, support,
215215+indemnity or liability terms You offer. You may include additional
216216+disclaimers of warranty and limitations of liability specific to any
217217+jurisdiction.
218218+219219+4. Inability to Comply Due to Statute or Regulation
220220+---------------------------------------------------
221221+222222+If it is impossible for You to comply with any of the terms of this
223223+License with respect to some or all of the Covered Software due to
224224+statute, judicial order, or regulation then You must: (a) comply with
225225+the terms of this License to the maximum extent possible; and (b)
226226+describe the limitations and the code they affect. Such description must
227227+be placed in a text file included with all distributions of the Covered
228228+Software under this License. Except to the extent prohibited by statute
229229+or regulation, such description must be sufficiently detailed for a
230230+recipient of ordinary skill to be able to understand it.
231231+232232+5. Termination
233233+--------------
234234+235235+5.1. The rights granted under this License will terminate automatically
236236+if You fail to comply with any of its terms. However, if You become
237237+compliant, then the rights granted under this License from a particular
238238+Contributor are reinstated (a) provisionally, unless and until such
239239+Contributor explicitly and finally terminates Your grants, and (b) on an
240240+ongoing basis, if such Contributor fails to notify You of the
241241+non-compliance by some reasonable means prior to 60 days after You have
242242+come back into compliance. Moreover, Your grants from a particular
243243+Contributor are reinstated on an ongoing basis if such Contributor
244244+notifies You of the non-compliance by some reasonable means, this is the
245245+first time You have received notice of non-compliance with this License
246246+from such Contributor, and You become compliant prior to 30 days after
247247+Your receipt of the notice.
248248+249249+5.2. If You initiate litigation against any entity by asserting a patent
250250+infringement claim (excluding declaratory judgment actions,
251251+counter-claims, and cross-claims) alleging that a Contributor Version
252252+directly or indirectly infringes any patent, then the rights granted to
253253+You by any and all Contributors for the Covered Software under Section
254254+2.1 of this License shall terminate.
255255+256256+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257257+end user license agreements (excluding distributors and resellers) which
258258+have been validly granted by You or Your distributors under this License
259259+prior to termination shall survive termination.
260260+261261+************************************************************************
262262+* *
263263+* 6. Disclaimer of Warranty *
264264+* ------------------------- *
265265+* *
266266+* Covered Software is provided under this License on an "as is" *
267267+* basis, without warranty of any kind, either expressed, implied, or *
268268+* statutory, including, without limitation, warranties that the *
269269+* Covered Software is free of defects, merchantable, fit for a *
270270+* particular purpose or non-infringing. The entire risk as to the *
271271+* quality and performance of the Covered Software is with You. *
272272+* Should any Covered Software prove defective in any respect, You *
273273+* (not any Contributor) assume the cost of any necessary servicing, *
274274+* repair, or correction. This disclaimer of warranty constitutes an *
275275+* essential part of this License. No use of any Covered Software is *
276276+* authorized under this License except under this disclaimer. *
277277+* *
278278+************************************************************************
279279+280280+************************************************************************
281281+* *
282282+* 7. Limitation of Liability *
283283+* -------------------------- *
284284+* *
285285+* Under no circumstances and under no legal theory, whether tort *
286286+* (including negligence), contract, or otherwise, shall any *
287287+* Contributor, or anyone who distributes Covered Software as *
288288+* permitted above, be liable to You for any direct, indirect, *
289289+* special, incidental, or consequential damages of any character *
290290+* including, without limitation, damages for lost profits, loss of *
291291+* goodwill, work stoppage, computer failure or malfunction, or any *
292292+* and all other commercial damages or losses, even if such party *
293293+* shall have been informed of the possibility of such damages. This *
294294+* limitation of liability shall not apply to liability for death or *
295295+* personal injury resulting from such party's negligence to the *
296296+* extent applicable law prohibits such limitation. Some *
297297+* jurisdictions do not allow the exclusion or limitation of *
298298+* incidental or consequential damages, so this exclusion and *
299299+* limitation may not apply to You. *
300300+* *
301301+************************************************************************
302302+303303+8. Litigation
304304+-------------
305305+306306+Any litigation relating to this License may be brought only in the
307307+courts of a jurisdiction where the defendant maintains its principal
308308+place of business and such litigation shall be governed by laws of that
309309+jurisdiction, without reference to its conflict-of-law provisions.
310310+Nothing in this Section shall prevent a party's ability to bring
311311+cross-claims or counter-claims.
312312+313313+9. Miscellaneous
314314+----------------
315315+316316+This License represents the complete agreement concerning the subject
317317+matter hereof. If any provision of this License is held to be
318318+unenforceable, such provision shall be reformed only to the extent
319319+necessary to make it enforceable. Any law or regulation which provides
320320+that the language of a contract shall be construed against the drafter
321321+shall not be used to construe this License against a Contributor.
322322+323323+10. Versions of the License
324324+---------------------------
325325+326326+10.1. New Versions
327327+328328+Mozilla Foundation is the license steward. Except as provided in Section
329329+10.3, no one other than the license steward has the right to modify or
330330+publish new versions of this License. Each version will be given a
331331+distinguishing version number.
332332+333333+10.2. Effect of New Versions
334334+335335+You may distribute the Covered Software under the terms of the version
336336+of the License under which You originally received the Covered Software,
337337+or under the terms of any subsequent version published by the license
338338+steward.
339339+340340+10.3. Modified Versions
341341+342342+If you create software not governed by this License, and you want to
343343+create a new license for such software, you may create and use a
344344+modified version of this License if you rename the license and remove
345345+any references to the name of the license steward (except to note that
346346+such modified license differs from this License).
347347+348348+10.4. Distributing Source Code Form that is Incompatible With Secondary
349349+Licenses
350350+351351+If You choose to distribute Source Code Form that is Incompatible With
352352+Secondary Licenses under the terms of this version of the License, the
353353+notice described in Exhibit B of this License must be attached.
354354+355355+Exhibit A - Source Code Form License Notice
356356+-------------------------------------------
357357+358358+ This Source Code Form is subject to the terms of the Mozilla Public
359359+ License, v. 2.0. If a copy of the MPL was not distributed with this
360360+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
361361+362362+If it is not possible or desirable to put the notice in a particular
363363+file, then You may include the notice in a location (such as a LICENSE
364364+file in a relevant directory) where a recipient would be likely to look
365365+for such a notice.
366366+367367+You may add additional accurate notices of copyright ownership.
368368+369369+Exhibit B - "Incompatible With Secondary Licenses" Notice
370370+---------------------------------------------------------
371371+372372+ This Source Code Form is "Incompatible With Secondary Licenses", as
373373+ defined by the Mozilla Public License, v. 2.0.
+18
README.md
···11+# Lexiconqueror
22+33+_SUBJUGATE YOUR SCHEMAS_
44+55+> To get started, run `deno run -A jsr:@hotsocket/lexiconqueror`
66+77+Lexiconqueror is a utility for using ATProto lexicons. Its primary goal is to generate TypeScript code that feels not
88+unlike the source material. For example, this ref:
99+1010+```txt
1111+com.atproto.admin.defs#repoRef
1212+```
1313+1414+Becomes:
1515+1616+```ts
1717+AT.com.atproto.admin.defs.$repoRef;
1818+```
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+export * from "./src/config.ts";
88+99+// running as program
1010+import { argv, exit } from "node:process";
1111+import { main } from "./src/main.ts";
1212+if (import.meta.main) {
1313+ exit(await main(argv));
1414+}
+132
npm.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import { build, emptyDir } from "@deno/dnt";
88+import { default as denoJson } from "./deno.json" with { type: "json" };
99+import { PackageMappedSpecifier, SpecifierMappings } from "@deno/dnt/transform";
1010+1111+const OUT_DIR = "./npm";
1212+await emptyDir(OUT_DIR);
1313+1414+const imports = denoJson.imports as Record<string, string>;
1515+1616+await build({
1717+ filterDiagnostic(diag) {
1818+ return diag.code != 2345; // fuck off its fine
1919+ },
2020+ postBuild() {
2121+ Deno.copyFileSync("./tsconfig.json", OUT_DIR + "/tsconfig.json");
2222+ Deno.copyFileSync("./LICENSE", OUT_DIR + "/LICENSE");
2323+ Deno.copyFileSync("./readme-npm.md", OUT_DIR + "/readme.md");
2424+ },
2525+ entryPoints: [...Object.values(denoJson.exports)],
2626+ outDir: OUT_DIR,
2727+ package: {
2828+ name: "lxq",
2929+ description: "A tool for conquest of ATProto lexicons.",
3030+ version: denoJson.version,
3131+ license: denoJson.license,
3232+ // bin: {
3333+ // lxq: "esm/src/npx.js",
3434+ // },
3535+ repository: {
3636+ type: "git",
3737+ url: "git+https://github.com/hotsocket-fyi/lexiconqueror",
3838+ },
3939+ bugs: "https://github.com/hotsocket-fyi/lexiconqueror/issues",
4040+ author: {
4141+ name: "HotSocket",
4242+ url: "https://hotsocket.fyi/",
4343+ },
4444+ dependencies: {
4545+ "@types/node": "^24.9.2",
4646+ },
4747+ } as PackageJson,
4848+ compilerOptions: {
4949+ lib: [
5050+ "DOM",
5151+ "ESNext",
5252+ ],
5353+ },
5454+ // don't do commonjs, kids!
5555+ scriptModule: false,
5656+5757+ // https://my.clevelandclinic.org/health/diseases/22131-migraine-aura
5858+ shims: {
5959+ undici: false,
6060+ },
6161+ test: false,
6262+});
6363+6464+// function convertImports(imports: Record<string, string>): Record<string, string> {
6565+// return Object.fromEntries(
6666+// Object.entries(imports).map(
6767+// ([k, v]): [string, string] | void => {
6868+// if (v.startsWith("./")) return;
6969+// const [registry, name_] = v.split(":", 2);
7070+// const lastAt = name_.lastIndexOf("@");
7171+// const name =
7272+// let newKey: string;
7373+// console.log(newKey);
7474+// if (registry == "jsr") {
7575+// }
7676+// },
7777+// ).filter((x) => !!x),
7878+// );
7979+// }
8080+// function findVersion(pkg: string): string {
8181+// return (denoJson.imports as Record<string, string>)[pkg].split("^")[1];
8282+// }
8383+8484+// something with imports fucked up so i copied in the code for my editing pleasure
8585+// https://jsr.io/@deno/dnt/0.42.3/lib/types.ts
8686+interface PackageJsonPerson {
8787+ name: string;
8888+ email?: string;
8989+ url?: string;
9090+}
9191+9292+interface PackageJsonBugs {
9393+ url?: string;
9494+ email?: string;
9595+}
9696+interface PackageJson {
9797+ name: string;
9898+ version: string;
9999+ description?: string;
100100+ keywords?: string[];
101101+ homepage?: string;
102102+ bugs?: PackageJsonBugs | string;
103103+ /**
104104+ * Check https://spdx.org/licenses/ for valid licences
105105+ */
106106+ license?: "MIT" | "ISC" | "UNLICENSED" | string;
107107+ author?: PackageJsonPerson | string;
108108+ contributors?: (PackageJsonPerson | string)[];
109109+ main?: string;
110110+ types?: string;
111111+ scripts?: { [key: string]: string };
112112+ repository?: string | { type: string; url: string; directory?: string };
113113+ dependencies?: { [packageName: string]: string };
114114+ devDependencies?: { [packageName: string]: string };
115115+ peerDependencies?: { [packageName: string]: string };
116116+ bundleDependencies?: { [packageName: string]: string };
117117+ optionalDependencies?: { [packageName: string]: string };
118118+ engines?: { [engineName: string]: string };
119119+ /**
120120+ * A list of os like "darwin", "linux", "win32", OS names can be prefix by a "!"
121121+ */
122122+ os?: string[];
123123+ /**
124124+ * A list of cpu like "x64", "ia32", "arm", "mips", CPU names can be prefix by a "!"
125125+ */
126126+ cpu?: string[];
127127+ private?: boolean;
128128+ /**
129129+ * rest of the fields
130130+ */
131131+ [propertyName: string]: any;
132132+}
+18
readme-npm.md
···11+# Lexiconqueror: NPM Edition
22+33+_SUBJUGATE YOUR SCHEMAS_
44+55+> To get started, run `npx lxq`
66+77+Lexiconqueror is a utility for using ATProto lexicons. Its primary goal is to generate TypeScript code that feels not
88+unlike the original lexicons. For example, this ref:
99+1010+```txt
1111+com.atproto.admin.defs#repoRef
1212+```
1313+1414+Becomes:
1515+1616+```ts
1717+AT.com.atproto.admin.defs.$repoRef;
1818+```
+50
src/config.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import * as z from "@zod/zod";
88+import { did_z, didMethods, didPipe } from "./did_parts.ts";
99+1010+/** zod codec for getting did method and identifier */
1111+export const did_parts = z.codec(
1212+ did_z,
1313+ z.object({
1414+ method: didMethods,
1515+ identifier: z.string(),
1616+ }),
1717+ {
1818+ decode: (str) => {
1919+ const parts = str.split(":");
2020+ return {
2121+ method: parts[1]! as z.infer<typeof didMethods>,
2222+ identifier: parts[2]!,
2323+ };
2424+ },
2525+ encode: (obj) => didPipe.parse(`did:${obj.method}:${obj.identifier}`),
2626+ },
2727+);
2828+2929+/** git input format */
3030+export const gitInput = z.union([
3131+ z.url({ protocol: /^git\+https?$/, normalize: true }),
3232+]);
3333+3434+/** at:// input format */
3535+export const atInput = z.union([
3636+ z.templateLiteral(["at://", did_z]),
3737+ z.templateLiteral(["at://", z.hostname()]),
3838+]);
3939+/** zod union of git and at:// input formats */
4040+export const anyInput = z.union([atInput, gitInput]);
4141+4242+/** lxq.json format */
4343+export const config_z = z.object({
4444+ inputs: z.record(z.string().regex(/^(?!\.).*$/), anyInput),
4545+ dataDir: z.string(),
4646+ outputDir: z.string(),
4747+});
4848+4949+/** actual type for lxq.json format */
5050+export type Config = z.infer<typeof config_z>;
+39
src/did_parts.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import * as z from "@zod/zod";
88+99+export const didMethods = z.enum(["plc", "web"]);
1010+export const did_z = z.templateLiteral([
1111+ "did:",
1212+ didMethods,
1313+ ":",
1414+ z.union([
1515+ z.stringFormat("plc-id", /^([a-z0-9]{24})$/),
1616+ z.hostname().refine((val) => !val.includes(":"), {
1717+ message: "hostname has colon(s)",
1818+ }),
1919+ ]),
2020+]);
2121+export const didPipe = z.string().pipe(did_z);
2222+2323+export const did_parts = z.codec(
2424+ did_z,
2525+ z.object({
2626+ method: didMethods,
2727+ identifier: z.string(),
2828+ }),
2929+ {
3030+ decode: (str) => {
3131+ const parts = str.split(":");
3232+ return {
3333+ method: parts[1]! as z.infer<typeof didMethods>,
3434+ identifier: parts[2]!,
3535+ };
3636+ },
3737+ encode: (obj) => didPipe.parse(`did:${obj.method}:${obj.identifier}`),
3838+ },
3939+);
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import ts, { factory as f } from "typescript";
88+import { PRINTER } from "./shared.ts";
99+import fs from "node:fs/promises";
1010+import type { Dirent } from "node:fs";
1111+1212+// this machine creates _index.ts
1313+export async function generateIndex(dir: string): Promise<string> {
1414+ const dirName = dir.endsWith("/") ? dir : dir + "/";
1515+ const listing = await fs.readdir(dir, { withFileTypes: true });
1616+ const entries: Dirent[] = [];
1717+ // const subdirs: Deno.DirEntry[] = [];
1818+ const pairs: Record<string, boolean> = {};
1919+ for await (const file of listing) {
2020+ if (
2121+ (file.isFile() && !file.name.startsWith("_") &&
2222+ file.name.endsWith(".ts")) || file.isDirectory()
2323+ ) {
2424+ entries.push(file);
2525+ }
2626+ }
2727+ // detect xrpc calls (and therefore the existence of runtime values)
2828+ await Promise.all(entries.map(async (file) => {
2929+ let hasRuntime = false;
3030+ let name = file.name;
3131+ if (file.isFile()) {
3232+ if (file.name == "index.ts") return;
3333+ const content = await fs.readFile(dirName + file.name, "utf-8");
3434+ hasRuntime = content.indexOf("export async function") != -1;
3535+ }
3636+ if (file.isDirectory()) name = name + "/_index.ts";
3737+ pairs[name] = hasRuntime;
3838+ }));
3939+4040+ // bang out the imports
4141+ const body: ts.Statement[] = [];
4242+ Object.keys(pairs).forEach((fileName) => {
4343+ const hasRuntime = pairs[fileName];
4444+ const name = fileName.replace(".ts", "").replace("/_index", "");
4545+ if (!fileName.includes("_")) {
4646+ // import { something as somethingNS } from "./something.ts";
4747+ const defaultExportName = name.replaceAll("-", "_") + "_default";
4848+ body.push(f.createImportDeclaration(
4949+ undefined,
5050+ f.createImportClause(
5151+ undefined,
5252+ f.createIdentifier(defaultExportName),
5353+ undefined, // f.createNamespaceImport(f.createIdentifier(name + "NS")),
5454+ ),
5555+ f.createStringLiteral("./" + fileName),
5656+ ));
5757+ body.push(f.createExportDeclaration(
5858+ undefined,
5959+ !hasRuntime,
6060+ f.createNamedExports([
6161+ f.createExportSpecifier(
6262+ false,
6363+ defaultExportName,
6464+ f.createIdentifier(name),
6565+ ),
6666+ ]),
6767+ ));
6868+ } else {
6969+ body.push(f.createExportDeclaration(
7070+ undefined,
7171+ false,
7272+ f.createNamespaceExport(f.createIdentifier(name.replaceAll("-", "_"))),
7373+ f.createStringLiteral("./" + fileName),
7474+ ));
7575+ }
7676+ });
7777+7878+ // spit out the content
7979+ const sourceFile = f.createSourceFile(
8080+ [
8181+ ...body,
8282+ ],
8383+ f.createToken(ts.SyntaxKind.EndOfFileToken),
8484+ ts.NodeFlags.None,
8585+ );
8686+ return PRINTER.printFile(sourceFile);
8787+}
+30
src/generator/generateRootIndex.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import * as fs from "node:fs/promises";
88+import * as ts from "typescript";
99+import { factory as f } from "typescript";
1010+import { PRINTER } from "./shared.ts";
1111+1212+export async function generateRootIndex(dir: string): Promise<string> {
1313+ const ls = (await fs.readdir(dir, { withFileTypes: true })).filter((x) => x.isDirectory());
1414+ const body: ts.Statement[] = [];
1515+ ls.forEach((dir) => {
1616+ body.push(f.createExportDeclaration(
1717+ undefined,
1818+ false,
1919+ undefined,
2020+ f.createStringLiteral(`./${dir.name}/_index.ts`),
2121+ ));
2222+ });
2323+2424+ const sourceFile = f.createSourceFile(
2525+ body,
2626+ f.createToken(ts.SyntaxKind.EndOfFileToken),
2727+ ts.NodeFlags.None,
2828+ );
2929+ return PRINTER.printFile(sourceFile);
3030+}
+9
src/generator/generator.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+export * from "./generateFile.ts";
88+export * from "./generateIndex.ts";
99+export * from "./generator.ts";
+268
src/generator/shared.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import ts, { factory as f, SyntaxKind } from "typescript";
88+import type { lexicon as lex } from "@hotsocket/atproto-common";
99+1010+/* need to work out the flow
1111+1212+if theres a main def, wrap the rest in a namespace, since the default export needs to be the main def
1313+^> If a main definition exists, it can be referenced without a fragment, just using the NSID.
1414+ > For references in the $type fields in data objects themselves (eg, records or contents of a union),
1515+ > this is a "must" (use of a #main suffix is invalid). For example, com.example.record not com.example.record#main.
1616+1717+then we rename defs not starting with _ to start with $, to create that visual differentiation like lexicon refs
1818+^ com.atproto.label.defs#selfLabels -> AT.com.atproto.label.defs.$selfLabels
1919+2020+also with defs containing an "unknown" type, add a type parameter to the output type, and set the field type T
2121+2222+ */
2323+2424+export const PRINTER = ts.createPrinter({
2525+ newLine: ts.NewLineKind.LineFeed,
2626+ noEmitHelpers: false,
2727+ omitTrailingSemicolon: false,
2828+ removeComments: false,
2929+});
3030+3131+export const CONSTRAINT_SERIALIZABLEOBJECT = f.createTypeParameterDeclaration(
3232+ undefined,
3333+ "T",
3434+ f.createTypeReferenceNode("common.types.SerializableObject"),
3535+ f.createTypeReferenceNode("common.types.SerializableObject"),
3636+);
3737+export const CONSTRAINT_SERIALIZABLEPARAMS = f.createTypeParameterDeclaration(
3838+ undefined,
3939+ "T",
4040+ f.createTypeReferenceNode("common.types.SerializableParams"),
4141+);
4242+4343+export type ImportInfo = {
4444+ defaultImport?: string;
4545+ namedImports?: ts.ImportSpecifier[];
4646+ nsImport?: string;
4747+};
4848+export function finalizeImports(
4949+ imports: Record<string, ImportInfo>,
5050+): ts.Statement[] {
5151+ const output: ts.Statement[] = [];
5252+ for (
5353+ const [module, { defaultImport, namedImports, nsImport }] of Object.entries(imports)
5454+ ) {
5555+ if (nsImport) {
5656+ output.push(
5757+ f.createImportDeclaration(
5858+ undefined,
5959+ f.createImportClause(
6060+ undefined,
6161+ undefined,
6262+ f.createNamespaceImport(f.createIdentifier(nsImport)),
6363+ ),
6464+ f.createStringLiteral(module),
6565+ ),
6666+ );
6767+ } else {
6868+ output.push(f.createImportDeclaration(
6969+ undefined,
7070+ f.createImportClause(
7171+ undefined,
7272+ defaultImport ? f.createIdentifier(defaultImport) : undefined,
7373+ namedImports ? f.createNamedImports(namedImports) : undefined,
7474+ ),
7575+ f.createStringLiteral(module),
7676+ ));
7777+ }
7878+ }
7979+ return output;
8080+}
8181+8282+export function addComments<T extends lex.SchemaObject>(
8383+ def: T,
8484+ declaration: ts.Statement | ts.Declaration,
8585+) {
8686+ const commentParts: string[] = [];
8787+ if (def.description) commentParts.push(def.description);
8888+ if (
8989+ def.type == "query" || def.type == "procedure" || def.type == "subscription"
9090+ ) commentParts.push("@" + def.type);
9191+ if (def.type === "string" && "format" in def && (def as lex.String).format) {
9292+ commentParts.push("@format " + (def as lex.String).format);
9393+ }
9494+ if (commentParts.length > 0) {
9595+ ts.addSyntheticLeadingComment(
9696+ declaration,
9797+ ts.SyntaxKind.MultiLineCommentTrivia,
9898+ "* " + commentParts.join("\n * ") + " ",
9999+ true,
100100+ );
101101+ }
102102+}
103103+export function addNamedImport(
104104+ imports: Record<string, ImportInfo>,
105105+ name: string,
106106+ file: string,
107107+) {
108108+ if (!imports[file]) imports[file] = {};
109109+ if (!imports[file].namedImports) imports[file].namedImports = [];
110110+ if (imports[file].namedImports.find((x) => x.name.text == name)) return;
111111+ imports[file].namedImports.push(
112112+ f.createImportSpecifier(false, undefined, f.createIdentifier(name)),
113113+ );
114114+}
115115+116116+export function lastNSIDPart(nsid: string): string {
117117+ return nsid.substring(nsid.lastIndexOf(".") + 1);
118118+}
119119+120120+export function refToNode(
121121+ ref: string,
122122+ lex: lex.Lexicon,
123123+ imports: Record<string, ImportInfo>,
124124+) {
125125+ const fixedRef = ref.replace("#", ".$");
126126+ // #something means it's local to "here"
127127+ if (ref.startsWith("#")) {
128128+ if (Object.hasOwn(lex.defs, "main")) {
129129+ //${lastNSIDPart(lex.id)}${fixedRef}
130130+ return f.createTypeReferenceNode(`${lastNSIDPart(lex.id)}${fixedRef}`);
131131+ } else {
132132+ return f.createTypeReferenceNode(fixedRef.replace(".", ""));
133133+ }
134134+ } else {
135135+ imports["@/index.ts"] = { nsImport: "AT" };
136136+ return f.createTypeReferenceNode("AT." + fixedRef);
137137+ }
138138+}
139139+140140+export function lookupType(
141141+ object: lex.AnySchemaObject,
142142+ lex: lex.Lexicon,
143143+ name: string,
144144+ imports: Record<string, ImportInfo>,
145145+): ts.TypeNode {
146146+ switch (object.type) {
147147+ case "cid-link":
148148+ case "bytes":
149149+ case "token":
150150+ case "string":
151151+ return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
152152+ case "blob":
153153+ imports["@hotsocket/atproto-common"] = { nsImport: "common" };
154154+ return f.createTypeReferenceNode("common.types.XBlob");
155155+ case "boolean":
156156+ return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
157157+ case "integer":
158158+ return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
159159+ case "unknown":
160160+ return f.createTypeReferenceNode("T");
161161+ case "ref":
162162+ return refToNode(object.ref, lex, imports);
163163+ case "union":
164164+ // TODO: Handle union types properly
165165+ if (object.refs.length == 0) {
166166+ // Record<PropertyKey, never>
167167+ return f.createTypeReferenceNode(
168168+ "Record",
169169+ [
170170+ f.createTypeReferenceNode("PropertyKey"),
171171+ f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
172172+ ],
173173+ );
174174+ } else {
175175+ return f.createUnionTypeNode(
176176+ object.refs.map((ref) => refToNode(ref, lex, imports)),
177177+ );
178178+ }
179179+ case "array":
180180+ // TODO: Handle array types properly
181181+ return f.createArrayTypeNode(
182182+ lookupType(object.items, lex, name + ">array", imports),
183183+ );
184184+ // objects, params are handled differently
185185+ default:
186186+ throw new Error(`unhandled type ${object.type} for ${lex.id}#${name}`);
187187+ }
188188+}
189189+190190+export function propsContainUnknown(
191191+ props: Record<string, lex.AnySchemaObject>,
192192+): boolean {
193193+ for (const name in props) {
194194+ const prop = props[name]!;
195195+ if (prop.type == "unknown") return true;
196196+ if (prop.type == "array" && prop.items.type == "unknown") return true;
197197+ }
198198+ return false;
199199+}
200200+201201+export function convertProperty(
202202+ lex: lex.Lexicon,
203203+ name: string,
204204+ def: lex.FieldTypes,
205205+ imports: Record<string, ImportInfo>,
206206+): ts.Statement {
207207+ return f.createTypeAliasDeclaration(
208208+ [
209209+ f.createModifier(ts.SyntaxKind.ExportKeyword),
210210+ ],
211211+ name,
212212+ undefined,
213213+ lookupType(def, lex, name, imports),
214214+ );
215215+}
216216+217217+export function convertSchemaObject(
218218+ lex: lex.Lexicon,
219219+ name: string,
220220+ def: lex.Object,
221221+ imports: Record<string, ImportInfo>,
222222+): ts.Statement | void {
223223+ const members: ts.TypeElement[] = [];
224224+ imports["@hotsocket/atproto-common"] = { nsImport: "common" };
225225+ // members.push(
226226+ // `\t[key: string]: ${name == "_parameters" ? "string | number | boolean | null | undefined" : "Serializable"};`,
227227+ // );
228228+ function requiredLookup(propName: string): ts.QuestionToken | undefined {
229229+ if (def.required && def.required.indexOf(propName) != -1) {
230230+ return undefined;
231231+ } else {
232232+ return f.createToken(ts.SyntaxKind.QuestionToken);
233233+ }
234234+ }
235235+ // detect whether any property contains `unknown` (including arrays of unknown)
236236+ // so we can add the generic `T` type parameter to the interface when needed.
237237+ const hasUnknown = propsContainUnknown(def.properties);
238238+ for (const propName in def.properties) {
239239+ const prop = def.properties[propName]!;
240240+241241+ const member = f.createPropertySignature(
242242+ undefined,
243243+ propName,
244244+ requiredLookup(propName),
245245+ lookupType(prop, lex, propName, imports),
246246+ );
247247+ addComments(prop, member);
248248+ members.push(member);
249249+ }
250250+ let inheritID;
251251+ if (name == "_parameters") {
252252+ inheritID = f.createIdentifier("common.types.SerializableParams");
253253+ } else {
254254+ inheritID = f.createIdentifier("common.types.SerializableObject");
255255+ }
256256+ return f.createInterfaceDeclaration(
257257+ [f.createModifier(ts.SyntaxKind.ExportKeyword)],
258258+ name,
259259+ hasUnknown ? [CONSTRAINT_SERIALIZABLEOBJECT] : undefined,
260260+ [f.createHeritageClause(SyntaxKind.ExtendsKeyword, [
261261+ f.createExpressionWithTypeArguments(
262262+ inheritID,
263263+ undefined,
264264+ ),
265265+ ])],
266266+ members,
267267+ );
268268+}
+58
src/genmain.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import { Dirent } from "node:fs";
88+import { generateFile } from "./generator/generateFile.ts";
99+import { generateIndex } from "./generator/generateIndex.ts";
1010+import * as fs from "node:fs/promises";
1111+1212+const XSRCPATH = "./_external/atproto/lexicons";
1313+const SRCPATH = "./lexicons";
1414+const OUTPATH = "./support/atproto/generated";
1515+1616+async function convertFiles(rootDir: string, parentDir: string, entry: Dirent) {
1717+ const inputPath = parentDir + "/" + entry.name;
1818+ const outputPath = inputPath.replace(rootDir, OUTPATH);
1919+2020+ if (entry.isFile()) {
2121+ if (entry.name.endsWith(".json")) {
2222+ await fs.writeFile(
2323+ outputPath.substring(0, outputPath.lastIndexOf(".json")) + ".ts",
2424+ generateFile(await fs.readFile(inputPath, { encoding: "utf-8" })),
2525+ );
2626+ }
2727+ } else {
2828+ await fs.mkdir(outputPath, { recursive: true });
2929+ await Promise.all(
3030+ (await fs.readdir(inputPath, { withFileTypes: true })).map((name) => convertFiles(rootDir, inputPath, name)),
3131+ );
3232+ }
3333+}
3434+3535+await Promise.all([
3636+ Promise.all(
3737+ (await fs.readdir(XSRCPATH, { withFileTypes: true })).map(async (root) => {
3838+ await convertFiles(XSRCPATH, XSRCPATH, root);
3939+ }),
4040+ ),
4141+ Promise.all(
4242+ (await fs.readdir(SRCPATH, { withFileTypes: true })).map(async (root) => {
4343+ await convertFiles(SRCPATH, SRCPATH, root);
4444+ }),
4545+ ),
4646+]);
4747+4848+async function createIndexes(parentDir: string, entry: Dirent) {
4949+ const path = parentDir + "/" + entry.name;
5050+5151+ if (entry.isDirectory()) {
5252+ await fs.writeFile(path + "/_index.ts", await generateIndex(path));
5353+ await Promise.all((await fs.readdir(path, { withFileTypes: true })).map((name) => createIndexes(path, name)));
5454+ }
5555+}
5656+5757+await fs.writeFile(OUTPATH + "/_index.ts", await generateIndex(OUTPATH));
5858+await Promise.all((await fs.readdir(OUTPATH, { withFileTypes: true })).map((name) => createIndexes(OUTPATH, name)));
+51
src/internaltypes.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import * as z from "@zod/zod";
88+import * as cfg from "./config.ts";
99+import * as dp from "./did_parts.ts";
1010+1111+export const serviceProperty_z = z.object({
1212+ id: z.string(),
1313+ type: z.string(),
1414+ serviceEndpoint: z.string(),
1515+});
1616+export type ServiceProperty = z.infer<typeof serviceProperty_z>;
1717+// incomplete but like come on i dont need all that
1818+export const didDoc_z = z.object({
1919+ id: z.string(),
2020+ alsoKnownAs: z.optional(z.set(z.string())),
2121+ controller: z.optional(z.union([z.string(), z.set(z.string())])),
2222+ service: z.optional(z.set(serviceProperty_z)),
2323+});
2424+export type DID = z.infer<typeof dp.did_z>;
2525+export type DIDDoc = z.infer<typeof didDoc_z>;
2626+2727+const anyResolvedInput = z.object({
2828+ raw: z.object({
2929+ name: z.string(),
3030+ input: cfg.anyInput,
3131+ }),
3232+});
3333+export const resolvedAtInput = anyResolvedInput.safeExtend({
3434+ kind: z.literal("at"),
3535+ pds: z.httpUrl(),
3636+ did: z.string(),
3737+});
3838+3939+export const resolvedGitInput = anyResolvedInput.safeExtend({
4040+ kind: z.literal("git"),
4141+ url: z.url(),
4242+ dir: z.string(),
4343+ // rev: z.optional(z.string()),
4444+ ref: z.optional(z.string()),
4545+});
4646+4747+export const resolvedInput_z = z.discriminatedUnion("kind", [
4848+ resolvedAtInput,
4949+ resolvedGitInput,
5050+]);
5151+export type ResolvedInput = z.infer<typeof resolvedInput_z>;
+55
src/main.ts
···11+#!/usr/bin/env node
22+/*
33+ * This Source Code Form is subject to the terms of the Mozilla Public
44+ * License, v. 2.0. If a copy of the MPL was not distributed with this
55+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
66+ */
77+88+import * as run from "./run.ts";
99+import { default as denoJson } from "../deno.json" with { type: "json" };
1010+1111+export async function main(argv: (string | undefined)[]): Promise<number> {
1212+ const act = argv[2];
1313+ const cfg = argv[3];
1414+ // GRAAAAHHHHHHHHHHHH
1515+ console.log(" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥");
1616+ console.log(`🔥🔥 ⚔️ LEXICON-QUEROR ⚔️ 🔥🔥`);
1717+ console.log(" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥");
1818+ console.log(`VERSION ${denoJson.version}`);
1919+ console.log(`SUBJUGATE YOUR SCHEMAS`);
2020+ switch (act) {
2121+ case "download":
2222+ await run.download(cfg);
2323+ break;
2424+ case "convert":
2525+ await run.convert(cfg);
2626+ break;
2727+ case "setup":
2828+ await run.setup(cfg);
2929+ break;
3030+3131+ default:
3232+ // console.log(
3333+ // `unknown action ${act ? `'${act}'` : "<none>"}, showing help...`,
3434+ // );
3535+ // /* falls through */
3636+ // case "help":
3737+ console.log(" ___________________________________________________");
3838+ console.log("|_______________________ HELP ______________________|");
3939+ console.log("| |");
4040+ console.log("| CMD: <run lxq> <action> [args] [path/to/lxq.json] |");
4141+ console.log("| |");
4242+ console.log("|__________________ ACTIONS LIST __________________|");
4343+ console.log("| [ action ] [ description ] |");
4444+ console.log("| download 🛜 Downloads configured inputs |");
4545+ console.log("| convert 🔀 Converts lexicons to TypeScript |");
4646+ console.log("| setup 🌅 Gives you an lxq.json to work with |");
4747+ // console.log("| new ✨ Creates a new lexicon definition in |");
4848+ // console.log("| your workspace. Has args: |");
4949+ // console.log("| - group/input name |");
5050+ // console.log("| - NSID (com.example.something) |");
5151+ console.log("|___________________________________________________|");
5252+ break;
5353+ }
5454+ return 0;
5555+}
+321
src/run.ts
···11+/*
22+ * This Source Code Form is subject to the terms of the Mozilla Public
33+ * License, v. 2.0. If a copy of the MPL was not distributed with this
44+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
55+ */
66+77+import * as z from "@zod/zod";
88+import * as dns from "node:dns/promises";
99+import * as fs from "node:fs/promises";
1010+import * as fss from "node:fs";
1111+import * as path from "node:path";
1212+import * as cfg from "./config.ts";
1313+import * as types from "./internaltypes.ts";
1414+import * as dp from "./did_parts.ts";
1515+import { lexicon as lex } from "@hotsocket/atproto-common";
1616+import { spawnSync } from "node:child_process";
1717+import * as gen from "./generator/generator.ts";
1818+1919+// not using generated code to avoid potential circular dependency type trouble
2020+2121+export const DEFAULT_CONFIG_PATH = "./lxq.json";
2222+const DEFAULT_DATA_PATH = ".lxq";
2323+const DEFAULT_OUTPUT_PATH = "generated/lxq";
2424+async function configHelper(configPath: string): Promise<cfg.Config & { root: string }> {
2525+ console.log();
2626+ const configDir = path.dirname(path.resolve(configPath));
2727+ const raw = JSON.parse(await fs.readFile(path.resolve(configPath), { encoding: "utf-8" }));
2828+ console.log("⏳\tValidating config...");
2929+ const config = cfg.config_z.parse(raw);
3030+ const dataDir = path.resolve(configDir, config.dataDir ?? path.resolve(configDir, DEFAULT_DATA_PATH));
3131+ if (!config.dataDir) console.log("👀\tUsing fallback dataDir");
3232+ console.log("🗄️\tAbsolute path of dataDir: " + dataDir);
3333+ const outputDir = path.resolve(configDir, config.outputDir ?? path.resolve(configDir, DEFAULT_OUTPUT_PATH));
3434+ if (!config.outputDir) console.log("👀\tUsing fallback outputDir");
3535+ console.log("🗄️\tAbsolute path of outputDir: " + outputDir);
3636+ return {
3737+ inputs: config.inputs,
3838+ dataDir: dataDir,
3939+ outputDir: outputDir,
4040+ root: configDir,
4141+ };
4242+}
4343+4444+export async function download(configPath: string = "./lxq.json") {
4545+ const config = await configHelper(configPath);
4646+ const dataDir = config.dataDir;
4747+ console.log("⏳\tResolving inputs...");
4848+ const resolvedInputs: types.ResolvedInput[] = [];
4949+ await Promise.all(
5050+ Object.keys(config.inputs).map(async (name) => {
5151+ const input = cfg.anyInput.parse(config.inputs[name]);
5252+ let info = "";
5353+ if (input.startsWith("at://")) { // pulls straight from published in repo
5454+ const inner = cfg.atInput.parse(input).substring(5);
5555+ let did: types.DID;
5656+ if (inner.startsWith("did:")) {
5757+ did = inner as types.DID;
5858+ } else {
5959+ did = await resolveHandle(inner);
6060+ }
6161+ const doc = await getDidDoc(did);
6262+ let svc: types.ServiceProperty;
6363+ try {
6464+ svc = doc.service!.values().find((v) => v.id == "#atproto_pds")!;
6565+ } catch {
6666+ throw new Error("could not find #atproto_pds in doc");
6767+ }
6868+ let pds = svc.serviceEndpoint;
6969+ if (pds.endsWith("/")) pds = pds.substring(0, pds.length - 1);
7070+ info = `DID: '${did}', PDS: '${svc.serviceEndpoint}'`;
7171+ resolvedInputs.push(types.resolvedAtInput.parse({
7272+ raw: { name: name, input: input },
7373+ kind: "at",
7474+ pds: pds,
7575+ did: did,
7676+ }));
7777+ } else if (input.startsWith("git+")) {
7878+ const inner = new URL(cfg.gitInput.parse(input).substring(4));
7979+ const repo = inner.origin + inner.pathname;
8080+ resolvedInputs.push(types.resolvedGitInput.parse({
8181+ kind: "git",
8282+ raw: {
8383+ name: name,
8484+ input: input,
8585+ },
8686+ dir: inner.searchParams.get("dir"),
8787+ ref: inner.searchParams.get("ref") ?? undefined,
8888+ url: repo,
8989+ } as z.infer<typeof types.resolvedGitInput>));
9090+ }
9191+ console.log(`✅\tResolved '${name}' (\`${input}\`)` + (info ? `to [${info}]` : ""));
9292+ }),
9393+ );
9494+9595+ // literally just throwing if anything fucks up
9696+ console.log("⏳\tValidating inputs...");
9797+ await Promise.all(
9898+ resolvedInputs.map(async (input_) => {
9999+ const input = types.resolvedInput_z.parse(input_);
100100+ if (input.kind == "at") {
101101+ const probeRsp = await fetch(
102102+ `${input.pds}/xrpc/com.atproto.repo.listRecords?repo=${input.did}&collection=com.atproto.lexicon.schema`,
103103+ );
104104+ // {"records":[]}
105105+ const probeBody = await probeRsp.json() as { records: unknown[] };
106106+ if (probeBody.records.length == 0) {
107107+ throw new Error(`Repo for AT input '${input.raw.name}' does not contain any lexicons.`);
108108+ }
109109+ console.log(`✅\tFound lexicons in AT input '${input.raw.name}'`);
110110+ } else if (input.kind == "git") {
111111+ const proc = spawnSync("git", ["ls-remote", "--refs", input.url], { encoding: "utf-8" });
112112+ if (proc.status != 0) {
113113+ throw new Error(`Error checking git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`);
114114+ }
115115+ if (input.ref) {
116116+ // 91270de847fb763b1cb34ac8733c8bbc3991f820\trefs/heads/main => refs/heads/main
117117+ const refs = proc.output[1]!.split("\n").map((line) => line.split("\t")[1]);
118118+ // refs/heads/main => main
119119+ const heads = refs.map((ref) => ref.substring(ref.lastIndexOf("/") + 1));
120120+ if (!heads.includes(input.ref)) {
121121+ throw new Error(`Could not find ref '${input.ref}' in git input '${input.raw.name}'`);
122122+ }
123123+ console.log(`✅\tRemote for Git input '${input.raw.name}' exists and has ref '${input.ref}'`);
124124+ } else {
125125+ console.log(`✅\tRemote for Git input '${input.raw.name}' exists`);
126126+ }
127127+ }
128128+ }),
129129+ );
130130+ console.log("⏳\tRetrieving lexicons...");
131131+ await Promise.all(
132132+ resolvedInputs.map(async (input_) => {
133133+ const input = types.resolvedInput_z.parse(input_);
134134+ const myPath = dataDir + "/" + input.raw.name;
135135+ const myTmp = dataDir + "/.temp/" + input.raw.name;
136136+ if (input.kind == "at") {
137137+ let cursor: string | undefined;
138138+ const lexicons: lex.Lexicon[] = [];
139139+ // do/while because there is no cursor to start
140140+ do {
141141+ const params = new URLSearchParams();
142142+ params.append("repo", input.did);
143143+ params.append("collection", "com.atproto.lexicon.schema");
144144+ if (cursor) params.append("cursor", cursor);
145145+ const rsp = await fetch(
146146+ `${input.pds}/xrpc/com.atproto.repo.listRecords?${params.toString()}`,
147147+ );
148148+ if (!rsp.ok) throw new Error(`Error retrieving lexicons of input '${input.raw.name}': ${await rsp.text()}`);
149149+ const data = await rsp.json() as {
150150+ cursor?: string;
151151+ records: {
152152+ uri: string;
153153+ cid: string;
154154+ value: lex.Lexicon;
155155+ }[];
156156+ };
157157+ lexicons.push(...data.records.map((r) => r.value));
158158+ cursor = data.cursor;
159159+ } while (cursor);
160160+ await Promise.all(lexicons.map(async (lexicon) => {
161161+ // com.atproto.server.getAccountInviteCodes
162162+ // ^
163163+ const lastDot = lexicon.id.lastIndexOf(".");
164164+ // com.atproto.server => com/atproto/server
165165+ const dir = dataDir + `/${input.raw.name}/` + lexicon.id.substring(0, lastDot).split(".").join("/") + "/";
166166+ await fs.mkdir(dir, { recursive: true });
167167+ // getAccountInviteCodes
168168+ const name = lexicon.id.substring(lastDot + 1);
169169+ await fs.writeFile(`${dir}/${name}.json`, JSON.stringify(lexicon));
170170+ }));
171171+ console.log(`✅\tRetrieved ${lexicons.length} lexicons from AT input '${input.raw.name}'`);
172172+ } else if (input.kind == "git") {
173173+ await fs.mkdir(path.dirname(myTmp), { recursive: true });
174174+ let shouldCopy = false;
175175+ if (fss.existsSync(myTmp)) {
176176+ const proc = spawnSync("git", ["-C", myTmp, "pull"], { encoding: "utf-8" });
177177+ if (proc.status != 0) {
178178+ throw new Error(`Error pulling git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`);
179179+ }
180180+ if (proc.stdout.split("\n")[0].startsWith("Already up to date.")) {
181181+ console.log(`ℹ️\tGit input '${input.raw.name}' already up to date`);
182182+ } else {
183183+ shouldCopy = true;
184184+ console.log(`ℹ️\tUpdated Git input '${input.raw.name}'`);
185185+ }
186186+ } else {
187187+ const proc = spawnSync("git", ["clone", input.url, myTmp], { encoding: "utf-8" });
188188+ if (proc.status != 0) {
189189+ throw new Error(`Error cloning git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`);
190190+ }
191191+ console.log(`⏳\tCloned Git input '${input.raw.name}' to '${myTmp}'`);
192192+ shouldCopy = true;
193193+ }
194194+ if (shouldCopy) {
195195+ const src = `${myTmp}/${input.dir}`;
196196+ if (!(await fs.stat(src)).isDirectory()) {
197197+ throw new Error(`Path '${src}' for Git input '${input.raw.name}' is not a directory`);
198198+ }
199199+ await fs.cp(src, myPath, { recursive: true });
200200+ console.log(`✅\tCopied Git input '${input.raw.name}' to '${myPath}'`);
201201+ }
202202+ }
203203+ }),
204204+ );
205205+}
206206+207207+export async function convert(configPath: string = DEFAULT_CONFIG_PATH) {
208208+ const config = await configHelper(configPath);
209209+ console.log(`🏇\tConverting lexicons to TypeScript files at '${config.outputDir}'`);
210210+211211+ console.log("⏲️\tGenerating content...");
212212+ const inputListing = (await fs.readdir(config.dataDir, { recursive: true }))
213213+ .filter((x) => !x.startsWith(".temp/"));
214214+ await Promise.all(
215215+ inputListing
216216+ .filter((x) => x.endsWith(".json")).map(async (srcRel) => {
217217+ const dstRel = srcRel.substring(0, srcRel.lastIndexOf(".")) + ".ts";
218218+ const srcPath = path.resolve(config.dataDir, srcRel);
219219+ const dstPath = path.resolve(config.outputDir, dstRel.substring(dstRel.indexOf("/") + 1));
220220+221221+ const dstDir = path.dirname(dstPath);
222222+ await fs.mkdir(dstDir, { recursive: true });
223223+224224+ const code = gen.generateFile(await fs.readFile(srcPath, { encoding: "utf-8" }));
225225+ await fs.writeFile(dstPath, code);
226226+ }),
227227+ );
228228+229229+ console.log("🧐\tGenerating indexes...");
230230+ const generatedListing = (await fs.readdir(config.outputDir, { recursive: true, withFileTypes: true }))
231231+ .filter((x) => x.isDirectory())
232232+ .map((x) => `${x.parentPath}/${x.name}`);
233233+ await Promise.all(
234234+ generatedListing.map(async (dir) => {
235235+ await fs.writeFile(
236236+ `${dir}/_index.ts`,
237237+ await gen.generateIndex(dir),
238238+ );
239239+ }),
240240+ );
241241+ await fs.writeFile(config.outputDir + "/index.ts", await gen.generateIndex(config.outputDir));
242242+ console.log("♨️\t Finished!");
243243+}
244244+245245+export async function setup(configPath: string = DEFAULT_CONFIG_PATH) {
246246+ console.log();
247247+ if (fss.existsSync(configPath)) {
248248+ console.error(`⚠️ A config file already exists at ${configPath} !`);
249249+ console.error(` You'll need to remove it to create a new one.`);
250250+ return;
251251+ }
252252+ await fs.writeFile(
253253+ configPath,
254254+ JSON.stringify(
255255+ {
256256+ inputs: {
257257+ atproto: "at://did:plc:6msi3pj7krzih5qxqtryxlzw",
258258+ bsky: "at://did:plc:4v4y5r3lwsbtmsxhile2ljac",
259259+ },
260260+ dataDir: ".lxq",
261261+ outputDir: "generated",
262262+ } as cfg.Config,
263263+ null,
264264+ "\t",
265265+ ),
266266+ );
267267+ function manualImportMessage() {
268268+ console.log("It's recommended you add a bit to your `deno.json` or similar that looks like this:");
269269+ console.log(' "imports": {');
270270+ console.log(' "@/": "./generated/"');
271271+ console.log(" }");
272272+ console.log();
273273+ console.log('This makes your "gateway" import situation look like this:');
274274+ console.log(' import * as AT from "@/index.ts";');
275275+ console.log();
276276+ }
277277+278278+ console.log();
279279+ console.log("🌅 New lxq.json created at " + configPath);
280280+ console.log();
281281+ console.log("- Includes inputs for ATProto and Bluesky lexicons via PDS records");
282282+ console.log("- Data will be stored at `.lxq`");
283283+ console.log("- Generated code will land at `generated`");
284284+ console.log();
285285+ manualImportMessage();
286286+ console.log("Have fun, and make something awesome!");
287287+ console.log();
288288+}
289289+290290+async function getDidDoc(identifier: string): Promise<types.DIDDoc> {
291291+ const did = cfg.did_parts.decode(identifier as z.infer<typeof dp.did_z>);
292292+ if (did.method == "plc") {
293293+ const res = await fetch("https://plc.directory/" + identifier);
294294+ if (!res.ok) throw new Error(`failed to fetch did:plc. status: ${res.status}, body: '${await res.text()}'`);
295295+ return (await res.json()) as types.DIDDoc;
296296+ } else {
297297+ // did:web
298298+ const res = await fetch(`https://${did.identifier}/.well-known/did.json`);
299299+ if (!res.ok) throw new Error(`failed to fetch did:web. status: ${res.status}, body: '${await res.text()}'`);
300300+ return (await res.json()) as types.DIDDoc;
301301+ }
302302+}
303303+304304+const TXT_KEY = "did=";
305305+async function resolveHandle(handle: string): Promise<types.DID> {
306306+ try {
307307+ // _atproto.example.com
308308+ const txt = await dns.resolveTxt("_atproto." + handle);
309309+ const merged = txt.map((x) => x.join(" "));
310310+ const didEntry = merged.find((entry) => entry.startsWith(TXT_KEY));
311311+ if (didEntry) {
312312+ return dp.did_z.parse(didEntry.substring(TXT_KEY.length));
313313+ }
314314+ } catch { /* just going to fall back to http */ }
315315+ const http_rsp = await fetch(`https://${handle}/.well-known/atproto-did`);
316316+ if (http_rsp.ok) {
317317+ return dp.did_z.parse(await http_rsp.text());
318318+ } else {
319319+ throw new Error("Could not resolve handle to DID");
320320+ }
321321+}