A tool for conquest of ATProto lexicons. https://jsr.io/@hotsocket/lexiconqueror

giving up on npm

+2201
+1
.envrc
··· 1 + use flake
+10
.gitignore
··· 1 + package-lock.json 2 + bun.lock 3 + node_modules 4 + npm 5 + .DS_Store 6 + .direnv 7 + 8 + .lxq 9 + generated 10 + lxq.json
+13
.licenserc.json
··· 1 + { 2 + "**/*.ts": [ 3 + "This Source Code Form is subject to the terms of the Mozilla Public", 4 + "License, v. 2.0. If a copy of the MPL was not distributed with this", 5 + "file, You can obtain one at https://mozilla.org/MPL/2.0/." 6 + ], 7 + "ignore": [ 8 + "example", 9 + "npm", 10 + "generated", 11 + ".lxq" 12 + ] 13 + }
+373
LICENSE
··· 1 + Mozilla Public License Version 2.0 2 + ================================== 3 + 4 + 1. Definitions 5 + -------------- 6 + 7 + 1.1. "Contributor" 8 + means each individual or legal entity that creates, contributes to 9 + the creation of, or owns Covered Software. 10 + 11 + 1.2. "Contributor Version" 12 + means the combination of the Contributions of others (if any) used 13 + by a Contributor and that particular Contributor's Contribution. 14 + 15 + 1.3. "Contribution" 16 + means Covered Software of a particular Contributor. 17 + 18 + 1.4. "Covered Software" 19 + means Source Code Form to which the initial Contributor has attached 20 + the notice in Exhibit A, the Executable Form of such Source Code 21 + Form, and Modifications of such Source Code Form, in each case 22 + including portions thereof. 23 + 24 + 1.5. "Incompatible With Secondary Licenses" 25 + means 26 + 27 + (a) that the initial Contributor has attached the notice described 28 + in Exhibit B to the Covered Software; or 29 + 30 + (b) that the Covered Software was made available under the terms of 31 + version 1.1 or earlier of the License, but not also under the 32 + terms of a Secondary License. 33 + 34 + 1.6. "Executable Form" 35 + means any form of the work other than Source Code Form. 36 + 37 + 1.7. "Larger Work" 38 + means a work that combines Covered Software with other material, in 39 + a separate file or files, that is not Covered Software. 40 + 41 + 1.8. "License" 42 + means this document. 43 + 44 + 1.9. "Licensable" 45 + means having the right to grant, to the maximum extent possible, 46 + whether at the time of the initial grant or subsequently, any and 47 + all of the rights conveyed by this License. 48 + 49 + 1.10. "Modifications" 50 + means any of the following: 51 + 52 + (a) any file in Source Code Form that results from an addition to, 53 + deletion from, or modification of the contents of Covered 54 + Software; or 55 + 56 + (b) any new file in Source Code Form that contains any Covered 57 + Software. 58 + 59 + 1.11. "Patent Claims" of a Contributor 60 + means any patent claim(s), including without limitation, method, 61 + process, and apparatus claims, in any patent Licensable by such 62 + Contributor that would be infringed, but for the grant of the 63 + License, by the making, using, selling, offering for sale, having 64 + made, import, or transfer of either its Contributions or its 65 + Contributor Version. 66 + 67 + 1.12. "Secondary License" 68 + means either the GNU General Public License, Version 2.0, the GNU 69 + Lesser General Public License, Version 2.1, the GNU Affero General 70 + Public License, Version 3.0, or any later versions of those 71 + licenses. 72 + 73 + 1.13. "Source Code Form" 74 + means the form of the work preferred for making modifications. 75 + 76 + 1.14. "You" (or "Your") 77 + means an individual or a legal entity exercising rights under this 78 + License. For legal entities, "You" includes any entity that 79 + controls, is controlled by, or is under common control with You. For 80 + purposes of this definition, "control" means (a) the power, direct 81 + or indirect, to cause the direction or management of such entity, 82 + whether by contract or otherwise, or (b) ownership of more than 83 + fifty percent (50%) of the outstanding shares or beneficial 84 + ownership of such entity. 85 + 86 + 2. License Grants and Conditions 87 + -------------------------------- 88 + 89 + 2.1. Grants 90 + 91 + Each Contributor hereby grants You a world-wide, royalty-free, 92 + non-exclusive license: 93 + 94 + (a) under intellectual property rights (other than patent or trademark) 95 + Licensable by such Contributor to use, reproduce, make available, 96 + modify, display, perform, distribute, and otherwise exploit its 97 + Contributions, either on an unmodified basis, with Modifications, or 98 + as part of a Larger Work; and 99 + 100 + (b) under Patent Claims of such Contributor to make, use, sell, offer 101 + for sale, have made, import, and otherwise transfer either its 102 + Contributions or its Contributor Version. 103 + 104 + 2.2. Effective Date 105 + 106 + The licenses granted in Section 2.1 with respect to any Contribution 107 + become effective for each Contribution on the date the Contributor first 108 + distributes such Contribution. 109 + 110 + 2.3. Limitations on Grant Scope 111 + 112 + The licenses granted in this Section 2 are the only rights granted under 113 + this License. No additional rights or licenses will be implied from the 114 + distribution or licensing of Covered Software under this License. 115 + Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 + Contributor: 117 + 118 + (a) for any code that a Contributor has removed from Covered Software; 119 + or 120 + 121 + (b) for infringements caused by: (i) Your and any other third party's 122 + modifications of Covered Software, or (ii) the combination of its 123 + Contributions with other software (except as part of its Contributor 124 + Version); or 125 + 126 + (c) under Patent Claims infringed by Covered Software in the absence of 127 + its Contributions. 128 + 129 + This License does not grant any rights in the trademarks, service marks, 130 + or logos of any Contributor (except as may be necessary to comply with 131 + the notice requirements in Section 3.4). 132 + 133 + 2.4. Subsequent Licenses 134 + 135 + No Contributor makes additional grants as a result of Your choice to 136 + distribute the Covered Software under a subsequent version of this 137 + License (see Section 10.2) or under the terms of a Secondary License (if 138 + permitted under the terms of Section 3.3). 139 + 140 + 2.5. Representation 141 + 142 + Each Contributor represents that the Contributor believes its 143 + Contributions are its original creation(s) or it has sufficient rights 144 + to grant the rights to its Contributions conveyed by this License. 145 + 146 + 2.6. Fair Use 147 + 148 + This License is not intended to limit any rights You have under 149 + applicable copyright doctrines of fair use, fair dealing, or other 150 + equivalents. 151 + 152 + 2.7. Conditions 153 + 154 + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 + in Section 2.1. 156 + 157 + 3. Responsibilities 158 + ------------------- 159 + 160 + 3.1. Distribution of Source Form 161 + 162 + All distribution of Covered Software in Source Code Form, including any 163 + Modifications that You create or to which You contribute, must be under 164 + the terms of this License. You must inform recipients that the Source 165 + Code Form of the Covered Software is governed by the terms of this 166 + License, and how they can obtain a copy of this License. You may not 167 + attempt to alter or restrict the recipients' rights in the Source Code 168 + Form. 169 + 170 + 3.2. Distribution of Executable Form 171 + 172 + If You distribute Covered Software in Executable Form then: 173 + 174 + (a) such Covered Software must also be made available in Source Code 175 + Form, as described in Section 3.1, and You must inform recipients of 176 + the Executable Form how they can obtain a copy of such Source Code 177 + Form by reasonable means in a timely manner, at a charge no more 178 + than the cost of distribution to the recipient; and 179 + 180 + (b) You may distribute such Executable Form under the terms of this 181 + License, or sublicense it under different terms, provided that the 182 + license for the Executable Form does not attempt to limit or alter 183 + the recipients' rights in the Source Code Form under this License. 184 + 185 + 3.3. Distribution of a Larger Work 186 + 187 + You may create and distribute a Larger Work under terms of Your choice, 188 + provided that You also comply with the requirements of this License for 189 + the Covered Software. If the Larger Work is a combination of Covered 190 + Software with a work governed by one or more Secondary Licenses, and the 191 + Covered Software is not Incompatible With Secondary Licenses, this 192 + License permits You to additionally distribute such Covered Software 193 + under the terms of such Secondary License(s), so that the recipient of 194 + the Larger Work may, at their option, further distribute the Covered 195 + Software under the terms of either this License or such Secondary 196 + License(s). 197 + 198 + 3.4. Notices 199 + 200 + You may not remove or alter the substance of any license notices 201 + (including copyright notices, patent notices, disclaimers of warranty, 202 + or limitations of liability) contained within the Source Code Form of 203 + the Covered Software, except that You may alter any license notices to 204 + the extent required to remedy known factual inaccuracies. 205 + 206 + 3.5. Application of Additional Terms 207 + 208 + You may choose to offer, and to charge a fee for, warranty, support, 209 + indemnity or liability obligations to one or more recipients of Covered 210 + Software. However, You may do so only on Your own behalf, and not on 211 + behalf of any Contributor. You must make it absolutely clear that any 212 + such warranty, support, indemnity, or liability obligation is offered by 213 + You alone, and You hereby agree to indemnify every Contributor for any 214 + liability incurred by such Contributor as a result of warranty, support, 215 + indemnity or liability terms You offer. You may include additional 216 + disclaimers of warranty and limitations of liability specific to any 217 + jurisdiction. 218 + 219 + 4. Inability to Comply Due to Statute or Regulation 220 + --------------------------------------------------- 221 + 222 + If it is impossible for You to comply with any of the terms of this 223 + License with respect to some or all of the Covered Software due to 224 + statute, judicial order, or regulation then You must: (a) comply with 225 + the terms of this License to the maximum extent possible; and (b) 226 + describe the limitations and the code they affect. Such description must 227 + be placed in a text file included with all distributions of the Covered 228 + Software under this License. Except to the extent prohibited by statute 229 + or regulation, such description must be sufficiently detailed for a 230 + recipient of ordinary skill to be able to understand it. 231 + 232 + 5. Termination 233 + -------------- 234 + 235 + 5.1. The rights granted under this License will terminate automatically 236 + if You fail to comply with any of its terms. However, if You become 237 + compliant, then the rights granted under this License from a particular 238 + Contributor are reinstated (a) provisionally, unless and until such 239 + Contributor explicitly and finally terminates Your grants, and (b) on an 240 + ongoing basis, if such Contributor fails to notify You of the 241 + non-compliance by some reasonable means prior to 60 days after You have 242 + come back into compliance. Moreover, Your grants from a particular 243 + Contributor are reinstated on an ongoing basis if such Contributor 244 + notifies You of the non-compliance by some reasonable means, this is the 245 + first time You have received notice of non-compliance with this License 246 + from such Contributor, and You become compliant prior to 30 days after 247 + Your receipt of the notice. 248 + 249 + 5.2. If You initiate litigation against any entity by asserting a patent 250 + infringement claim (excluding declaratory judgment actions, 251 + counter-claims, and cross-claims) alleging that a Contributor Version 252 + directly or indirectly infringes any patent, then the rights granted to 253 + You by any and all Contributors for the Covered Software under Section 254 + 2.1 of this License shall terminate. 255 + 256 + 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 + end user license agreements (excluding distributors and resellers) which 258 + have been validly granted by You or Your distributors under this License 259 + prior to termination shall survive termination. 260 + 261 + ************************************************************************ 262 + * * 263 + * 6. Disclaimer of Warranty * 264 + * ------------------------- * 265 + * * 266 + * Covered Software is provided under this License on an "as is" * 267 + * basis, without warranty of any kind, either expressed, implied, or * 268 + * statutory, including, without limitation, warranties that the * 269 + * Covered Software is free of defects, merchantable, fit for a * 270 + * particular purpose or non-infringing. The entire risk as to the * 271 + * quality and performance of the Covered Software is with You. * 272 + * Should any Covered Software prove defective in any respect, You * 273 + * (not any Contributor) assume the cost of any necessary servicing, * 274 + * repair, or correction. This disclaimer of warranty constitutes an * 275 + * essential part of this License. No use of any Covered Software is * 276 + * authorized under this License except under this disclaimer. * 277 + * * 278 + ************************************************************************ 279 + 280 + ************************************************************************ 281 + * * 282 + * 7. Limitation of Liability * 283 + * -------------------------- * 284 + * * 285 + * Under no circumstances and under no legal theory, whether tort * 286 + * (including negligence), contract, or otherwise, shall any * 287 + * Contributor, or anyone who distributes Covered Software as * 288 + * permitted above, be liable to You for any direct, indirect, * 289 + * special, incidental, or consequential damages of any character * 290 + * including, without limitation, damages for lost profits, loss of * 291 + * goodwill, work stoppage, computer failure or malfunction, or any * 292 + * and all other commercial damages or losses, even if such party * 293 + * shall have been informed of the possibility of such damages. This * 294 + * limitation of liability shall not apply to liability for death or * 295 + * personal injury resulting from such party's negligence to the * 296 + * extent applicable law prohibits such limitation. Some * 297 + * jurisdictions do not allow the exclusion or limitation of * 298 + * incidental or consequential damages, so this exclusion and * 299 + * limitation may not apply to You. * 300 + * * 301 + ************************************************************************ 302 + 303 + 8. Litigation 304 + ------------- 305 + 306 + Any litigation relating to this License may be brought only in the 307 + courts of a jurisdiction where the defendant maintains its principal 308 + place of business and such litigation shall be governed by laws of that 309 + jurisdiction, without reference to its conflict-of-law provisions. 310 + Nothing in this Section shall prevent a party's ability to bring 311 + cross-claims or counter-claims. 312 + 313 + 9. Miscellaneous 314 + ---------------- 315 + 316 + This License represents the complete agreement concerning the subject 317 + matter hereof. If any provision of this License is held to be 318 + unenforceable, such provision shall be reformed only to the extent 319 + necessary to make it enforceable. Any law or regulation which provides 320 + that the language of a contract shall be construed against the drafter 321 + shall not be used to construe this License against a Contributor. 322 + 323 + 10. Versions of the License 324 + --------------------------- 325 + 326 + 10.1. New Versions 327 + 328 + Mozilla Foundation is the license steward. Except as provided in Section 329 + 10.3, no one other than the license steward has the right to modify or 330 + publish new versions of this License. Each version will be given a 331 + distinguishing version number. 332 + 333 + 10.2. Effect of New Versions 334 + 335 + You may distribute the Covered Software under the terms of the version 336 + of the License under which You originally received the Covered Software, 337 + or under the terms of any subsequent version published by the license 338 + steward. 339 + 340 + 10.3. Modified Versions 341 + 342 + If you create software not governed by this License, and you want to 343 + create a new license for such software, you may create and use a 344 + modified version of this License if you rename the license and remove 345 + any references to the name of the license steward (except to note that 346 + such modified license differs from this License). 347 + 348 + 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 + Licenses 350 + 351 + If You choose to distribute Source Code Form that is Incompatible With 352 + Secondary Licenses under the terms of this version of the License, the 353 + notice described in Exhibit B of this License must be attached. 354 + 355 + Exhibit A - Source Code Form License Notice 356 + ------------------------------------------- 357 + 358 + This Source Code Form is subject to the terms of the Mozilla Public 359 + License, v. 2.0. If a copy of the MPL was not distributed with this 360 + file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 + 362 + If it is not possible or desirable to put the notice in a particular 363 + file, then You may include the notice in a location (such as a LICENSE 364 + file in a relevant directory) where a recipient would be likely to look 365 + for such a notice. 366 + 367 + You may add additional accurate notices of copyright ownership. 368 + 369 + Exhibit B - "Incompatible With Secondary Licenses" Notice 370 + --------------------------------------------------------- 371 + 372 + This Source Code Form is "Incompatible With Secondary Licenses", as 373 + defined by the Mozilla Public License, v. 2.0.
+18
README.md
··· 1 + # Lexiconqueror 2 + 3 + _SUBJUGATE YOUR SCHEMAS_ 4 + 5 + > To get started, run `deno run -A jsr:@hotsocket/lexiconqueror` 6 + 7 + Lexiconqueror is a utility for using ATProto lexicons. Its primary goal is to generate TypeScript code that feels not 8 + unlike the source material. For example, this ref: 9 + 10 + ```txt 11 + com.atproto.admin.defs#repoRef 12 + ``` 13 + 14 + Becomes: 15 + 16 + ```ts 17 + AT.com.atproto.admin.defs.$repoRef; 18 + ```
+40
deno.json
··· 1 + { 2 + "name": "@hotsocket/lexiconqueror", 3 + "version": "0.1.0", 4 + "license": "GPL-3.0-or-later", 5 + "tasks": { 6 + "npm": "deno run -A npm.ts", 7 + "license": "deno run --allow-read jsr:@kt3k/license-checker@3.3.1/main" 8 + }, 9 + "exports": { 10 + ".": "./mod.ts" 11 + }, 12 + "fmt": { 13 + "useTabs": true, 14 + "lineWidth": 120, 15 + "exclude": [ 16 + "*/.lxq/*" 17 + ] 18 + }, 19 + "lint": { 20 + "rules": { 21 + "exclude": [ 22 + "verbatim-module-syntax" 23 + ] 24 + } 25 + }, 26 + "imports": { 27 + "@deno/dnt": "jsr:@deno/dnt@^0.42.3", 28 + "@hotsocket/atproto-common": "jsr:@hotsocket/atproto-common@^0.2.4", 29 + "@hotsocket/dhmo": "jsr:@hotsocket/dhmo@^0.1.1", 30 + "@std/assert": "jsr:@std/assert@^1.0.15", 31 + "@zod/zod": "jsr:@zod/zod@^4.1.12", 32 + "typescript": "npm:typescript@^5.9.3", 33 + "@/": "./example/generated/" 34 + }, 35 + "compilerOptions": { 36 + "lib": [ 37 + "deno.window" 38 + ] 39 + } 40 + }
+117
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "jsr:@david/code-block-writer@^13.0.3": "13.0.3", 5 + "jsr:@deno/dnt@~0.42.3": "0.42.3", 6 + "jsr:@hotsocket/atproto-common@~0.2.4": "0.2.4", 7 + "jsr:@hotsocket/dhmo@~0.1.1": "0.1.1", 8 + "jsr:@std/assert@^1.0.15": "1.0.15", 9 + "jsr:@std/fmt@1": "1.0.8", 10 + "jsr:@std/fs@1": "1.0.19", 11 + "jsr:@std/internal@^1.0.10": "1.0.12", 12 + "jsr:@std/internal@^1.0.12": "1.0.12", 13 + "jsr:@std/internal@^1.0.9": "1.0.12", 14 + "jsr:@std/path@1": "1.1.2", 15 + "jsr:@std/path@^1.1.1": "1.1.2", 16 + "jsr:@ts-morph/bootstrap@0.27": "0.27.0", 17 + "jsr:@ts-morph/common@0.27": "0.27.0", 18 + "jsr:@zod/zod@^4.1.12": "4.1.12", 19 + "npm:@types/node@*": "24.2.0", 20 + "npm:atpkgs@*": "0.0.2", 21 + "npm:typescript@^5.9.3": "5.9.3" 22 + }, 23 + "jsr": { 24 + "@david/code-block-writer@13.0.3": { 25 + "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f" 26 + }, 27 + "@deno/dnt@0.42.3": { 28 + "integrity": "62a917a0492f3c8af002dce90605bb0d41f7d29debc06aca40dba72ab65d8ae3", 29 + "dependencies": [ 30 + "jsr:@david/code-block-writer", 31 + "jsr:@std/fmt", 32 + "jsr:@std/fs", 33 + "jsr:@std/path@1", 34 + "jsr:@ts-morph/bootstrap" 35 + ] 36 + }, 37 + "@hotsocket/atproto-common@0.2.4": { 38 + "integrity": "0886e30ac32d8ae46a90dfb5044c0fba9917dd26b71c76f1ef0d137d159deb0f", 39 + "dependencies": [ 40 + "jsr:@hotsocket/dhmo" 41 + ] 42 + }, 43 + "@hotsocket/dhmo@0.1.1": { 44 + "integrity": "dd3db38a5e1e7df1064e9ef3dac1b51986e4e1c86b2b45a6988a98fedb7847e8" 45 + }, 46 + "@std/assert@1.0.15": { 47 + "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 48 + "dependencies": [ 49 + "jsr:@std/internal@^1.0.12" 50 + ] 51 + }, 52 + "@std/fmt@1.0.8": { 53 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 54 + }, 55 + "@std/fs@1.0.19": { 56 + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", 57 + "dependencies": [ 58 + "jsr:@std/internal@^1.0.9", 59 + "jsr:@std/path@^1.1.1" 60 + ] 61 + }, 62 + "@std/internal@1.0.12": { 63 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 64 + }, 65 + "@std/path@1.1.2": { 66 + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", 67 + "dependencies": [ 68 + "jsr:@std/internal@^1.0.10" 69 + ] 70 + }, 71 + "@ts-morph/bootstrap@0.27.0": { 72 + "integrity": "b8d7bc8f7942ce853dde4161b28f9aa96769cef3d8eebafb379a81800b9e2448", 73 + "dependencies": [ 74 + "jsr:@ts-morph/common" 75 + ] 76 + }, 77 + "@ts-morph/common@0.27.0": { 78 + "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052", 79 + "dependencies": [ 80 + "jsr:@std/fs", 81 + "jsr:@std/path@1" 82 + ] 83 + }, 84 + "@zod/zod@4.1.12": { 85 + "integrity": "5876ed4c6d44673faf5120f0a461a2ada2eb6c735329d3ebaf5ba1fc08387695" 86 + } 87 + }, 88 + "npm": { 89 + "@types/node@24.2.0": { 90 + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 91 + "dependencies": [ 92 + "undici-types" 93 + ] 94 + }, 95 + "atpkgs@0.0.2": { 96 + "integrity": "sha512-bHKjLgCzJTzxBjEqy8UpAWxwPODY19TwO0O3uAlbhuNOxuGJAMpjzR0OoxUNR3RtQl4CxfZznkc+Wxe7ho7Hzw==", 97 + "bin": true 98 + }, 99 + "typescript@5.9.3": { 100 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 101 + "bin": true 102 + }, 103 + "undici-types@7.10.0": { 104 + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" 105 + } 106 + }, 107 + "workspace": { 108 + "dependencies": [ 109 + "jsr:@deno/dnt@~0.42.3", 110 + "jsr:@hotsocket/atproto-common@~0.2.4", 111 + "jsr:@hotsocket/dhmo@~0.1.1", 112 + "jsr:@std/assert@^1.0.15", 113 + "jsr:@zod/zod@^4.1.12", 114 + "npm:typescript@^5.9.3" 115 + ] 116 + } 117 + }
+37
extra/lxq.schema.json
··· 1 + { 2 + "$schema": "http://json-schema.org/draft-2020-12/schema#", 3 + "title": "Lexiconqueror Configuration", 4 + "description": "The configuration file used by Lexiconqueror.", 5 + "type": "object", 6 + "properties": { 7 + "inputs": { 8 + "type": "object", 9 + "minProperties": 1, 10 + "additionalProperties": { 11 + "type": "string", 12 + "description": "Input reference.", 13 + "pattern": "^(?:at:\\/\\/(?:(?:did:(?:plc:[a-z0-9]{24}|web:(?:\\S+\\.)+\\S+))|(?:\\S+\\.)+\\S+)|git\\+https?:\\/\\/(?:\\S+\\.)+\\S+(?:\\/[\\S]+)+)$", 14 + "format": "Input Reference" 15 + } 16 + }, 17 + "dataDir": { 18 + "type": "string", 19 + "description": "Directory for storing lexicons, maybe other data.", 20 + "default": ".lxq" 21 + }, 22 + "outputDir": { 23 + "type": "string", 24 + "description": "Path to generated file output", 25 + "default": "generated" 26 + }, 27 + "$schema": { 28 + "type": "string", 29 + "description": "literally just here so your stuff doesn't throw a fit", 30 + "format": "url" 31 + } 32 + }, 33 + "required": [ 34 + "inputs" 35 + ], 36 + "additionalProperties": false 37 + }
+61
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-utils": { 4 + "inputs": { 5 + "systems": "systems" 6 + }, 7 + "locked": { 8 + "lastModified": 1731533236, 9 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 + "owner": "numtide", 11 + "repo": "flake-utils", 12 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 + "type": "github" 14 + }, 15 + "original": { 16 + "owner": "numtide", 17 + "repo": "flake-utils", 18 + "type": "github" 19 + } 20 + }, 21 + "nixpkgs": { 22 + "locked": { 23 + "lastModified": 1761672384, 24 + "narHash": "sha256-o9KF3DJL7g7iYMZq9SWgfS1BFlNbsm6xplRjVlOCkXI=", 25 + "owner": "nixos", 26 + "repo": "nixpkgs", 27 + "rev": "08dacfca559e1d7da38f3cf05f1f45ee9bfd213c", 28 + "type": "github" 29 + }, 30 + "original": { 31 + "owner": "nixos", 32 + "ref": "nixos-unstable", 33 + "repo": "nixpkgs", 34 + "type": "github" 35 + } 36 + }, 37 + "root": { 38 + "inputs": { 39 + "flake-utils": "flake-utils", 40 + "nixpkgs": "nixpkgs" 41 + } 42 + }, 43 + "systems": { 44 + "locked": { 45 + "lastModified": 1681028828, 46 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 + "owner": "nix-systems", 48 + "repo": "default", 49 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 + "type": "github" 51 + }, 52 + "original": { 53 + "owner": "nix-systems", 54 + "repo": "default", 55 + "type": "github" 56 + } 57 + } 58 + }, 59 + "root": "root", 60 + "version": 7 61 + }
+19
flake.nix
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 + flake-utils.url = "github:numtide/flake-utils"; 5 + }; 6 + outputs = { self, nixpkgs, flake-utils }: 7 + flake-utils.lib.eachDefaultSystem (system: 8 + let 9 + pkgs = import nixpkgs { inherit system; }; 10 + in 11 + { 12 + devShells.default = pkgs.mkShell { 13 + buildInputs = with pkgs; [ 14 + git 15 + deno 16 + ]; 17 + }; 18 + }); 19 + }
+14
mod.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + export * from "./src/config.ts"; 8 + 9 + // running as program 10 + import { argv, exit } from "node:process"; 11 + import { main } from "./src/main.ts"; 12 + if (import.meta.main) { 13 + exit(await main(argv)); 14 + }
+132
npm.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import { build, emptyDir } from "@deno/dnt"; 8 + import { default as denoJson } from "./deno.json" with { type: "json" }; 9 + import { PackageMappedSpecifier, SpecifierMappings } from "@deno/dnt/transform"; 10 + 11 + const OUT_DIR = "./npm"; 12 + await emptyDir(OUT_DIR); 13 + 14 + const imports = denoJson.imports as Record<string, string>; 15 + 16 + await build({ 17 + filterDiagnostic(diag) { 18 + return diag.code != 2345; // fuck off its fine 19 + }, 20 + postBuild() { 21 + Deno.copyFileSync("./tsconfig.json", OUT_DIR + "/tsconfig.json"); 22 + Deno.copyFileSync("./LICENSE", OUT_DIR + "/LICENSE"); 23 + Deno.copyFileSync("./readme-npm.md", OUT_DIR + "/readme.md"); 24 + }, 25 + entryPoints: [...Object.values(denoJson.exports)], 26 + outDir: OUT_DIR, 27 + package: { 28 + name: "lxq", 29 + description: "A tool for conquest of ATProto lexicons.", 30 + version: denoJson.version, 31 + license: denoJson.license, 32 + // bin: { 33 + // lxq: "esm/src/npx.js", 34 + // }, 35 + repository: { 36 + type: "git", 37 + url: "git+https://github.com/hotsocket-fyi/lexiconqueror", 38 + }, 39 + bugs: "https://github.com/hotsocket-fyi/lexiconqueror/issues", 40 + author: { 41 + name: "HotSocket", 42 + url: "https://hotsocket.fyi/", 43 + }, 44 + dependencies: { 45 + "@types/node": "^24.9.2", 46 + }, 47 + } as PackageJson, 48 + compilerOptions: { 49 + lib: [ 50 + "DOM", 51 + "ESNext", 52 + ], 53 + }, 54 + // don't do commonjs, kids! 55 + scriptModule: false, 56 + 57 + // https://my.clevelandclinic.org/health/diseases/22131-migraine-aura 58 + shims: { 59 + undici: false, 60 + }, 61 + test: false, 62 + }); 63 + 64 + // function convertImports(imports: Record<string, string>): Record<string, string> { 65 + // return Object.fromEntries( 66 + // Object.entries(imports).map( 67 + // ([k, v]): [string, string] | void => { 68 + // if (v.startsWith("./")) return; 69 + // const [registry, name_] = v.split(":", 2); 70 + // const lastAt = name_.lastIndexOf("@"); 71 + // const name = 72 + // let newKey: string; 73 + // console.log(newKey); 74 + // if (registry == "jsr") { 75 + // } 76 + // }, 77 + // ).filter((x) => !!x), 78 + // ); 79 + // } 80 + // function findVersion(pkg: string): string { 81 + // return (denoJson.imports as Record<string, string>)[pkg].split("^")[1]; 82 + // } 83 + 84 + // something with imports fucked up so i copied in the code for my editing pleasure 85 + // https://jsr.io/@deno/dnt/0.42.3/lib/types.ts 86 + interface PackageJsonPerson { 87 + name: string; 88 + email?: string; 89 + url?: string; 90 + } 91 + 92 + interface PackageJsonBugs { 93 + url?: string; 94 + email?: string; 95 + } 96 + interface PackageJson { 97 + name: string; 98 + version: string; 99 + description?: string; 100 + keywords?: string[]; 101 + homepage?: string; 102 + bugs?: PackageJsonBugs | string; 103 + /** 104 + * Check https://spdx.org/licenses/ for valid licences 105 + */ 106 + license?: "MIT" | "ISC" | "UNLICENSED" | string; 107 + author?: PackageJsonPerson | string; 108 + contributors?: (PackageJsonPerson | string)[]; 109 + main?: string; 110 + types?: string; 111 + scripts?: { [key: string]: string }; 112 + repository?: string | { type: string; url: string; directory?: string }; 113 + dependencies?: { [packageName: string]: string }; 114 + devDependencies?: { [packageName: string]: string }; 115 + peerDependencies?: { [packageName: string]: string }; 116 + bundleDependencies?: { [packageName: string]: string }; 117 + optionalDependencies?: { [packageName: string]: string }; 118 + engines?: { [engineName: string]: string }; 119 + /** 120 + * A list of os like "darwin", "linux", "win32", OS names can be prefix by a "!" 121 + */ 122 + os?: string[]; 123 + /** 124 + * A list of cpu like "x64", "ia32", "arm", "mips", CPU names can be prefix by a "!" 125 + */ 126 + cpu?: string[]; 127 + private?: boolean; 128 + /** 129 + * rest of the fields 130 + */ 131 + [propertyName: string]: any; 132 + }
+18
readme-npm.md
··· 1 + # Lexiconqueror: NPM Edition 2 + 3 + _SUBJUGATE YOUR SCHEMAS_ 4 + 5 + > To get started, run `npx lxq` 6 + 7 + Lexiconqueror is a utility for using ATProto lexicons. Its primary goal is to generate TypeScript code that feels not 8 + unlike the original lexicons. For example, this ref: 9 + 10 + ```txt 11 + com.atproto.admin.defs#repoRef 12 + ``` 13 + 14 + Becomes: 15 + 16 + ```ts 17 + AT.com.atproto.admin.defs.$repoRef; 18 + ```
+50
src/config.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import * as z from "@zod/zod"; 8 + import { did_z, didMethods, didPipe } from "./did_parts.ts"; 9 + 10 + /** zod codec for getting did method and identifier */ 11 + export const did_parts = z.codec( 12 + did_z, 13 + z.object({ 14 + method: didMethods, 15 + identifier: z.string(), 16 + }), 17 + { 18 + decode: (str) => { 19 + const parts = str.split(":"); 20 + return { 21 + method: parts[1]! as z.infer<typeof didMethods>, 22 + identifier: parts[2]!, 23 + }; 24 + }, 25 + encode: (obj) => didPipe.parse(`did:${obj.method}:${obj.identifier}`), 26 + }, 27 + ); 28 + 29 + /** git input format */ 30 + export const gitInput = z.union([ 31 + z.url({ protocol: /^git\+https?$/, normalize: true }), 32 + ]); 33 + 34 + /** at:// input format */ 35 + export const atInput = z.union([ 36 + z.templateLiteral(["at://", did_z]), 37 + z.templateLiteral(["at://", z.hostname()]), 38 + ]); 39 + /** zod union of git and at:// input formats */ 40 + export const anyInput = z.union([atInput, gitInput]); 41 + 42 + /** lxq.json format */ 43 + export const config_z = z.object({ 44 + inputs: z.record(z.string().regex(/^(?!\.).*$/), anyInput), 45 + dataDir: z.string(), 46 + outputDir: z.string(), 47 + }); 48 + 49 + /** actual type for lxq.json format */ 50 + export type Config = z.infer<typeof config_z>;
+39
src/did_parts.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import * as z from "@zod/zod"; 8 + 9 + export const didMethods = z.enum(["plc", "web"]); 10 + export const did_z = z.templateLiteral([ 11 + "did:", 12 + didMethods, 13 + ":", 14 + z.union([ 15 + z.stringFormat("plc-id", /^([a-z0-9]{24})$/), 16 + z.hostname().refine((val) => !val.includes(":"), { 17 + message: "hostname has colon(s)", 18 + }), 19 + ]), 20 + ]); 21 + export const didPipe = z.string().pipe(did_z); 22 + 23 + export const did_parts = z.codec( 24 + did_z, 25 + z.object({ 26 + method: didMethods, 27 + identifier: z.string(), 28 + }), 29 + { 30 + decode: (str) => { 31 + const parts = str.split(":"); 32 + return { 33 + method: parts[1]! as z.infer<typeof didMethods>, 34 + identifier: parts[2]!, 35 + }; 36 + }, 37 + encode: (obj) => didPipe.parse(`did:${obj.method}:${obj.identifier}`), 38 + }, 39 + );
+352
src/generator/generateFile.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import ts, { factory as f } from "typescript"; 8 + import type { lexicon as lex } from "@hotsocket/atproto-common"; 9 + import { 10 + addComments, 11 + addNamedImport, 12 + CONSTRAINT_SERIALIZABLEOBJECT, 13 + convertProperty, 14 + convertSchemaObject, 15 + finalizeImports, 16 + type ImportInfo, 17 + PRINTER, 18 + propsContainUnknown, 19 + } from "./shared.ts"; 20 + 21 + export function generateFile(data: string): string { 22 + // so basically load the lexicon into the typed "Lexicon" object deal 23 + const lex: lex.Lexicon = JSON.parse(data); 24 + const lexName = lex.id.substring(lex.id.lastIndexOf(".") + 1); 25 + const imports: Record<string, ImportInfo> = {}; 26 + const body: ts.Statement[] = []; 27 + // used to turn specified errors into something easily throwable 28 + const nsExtraDefs: ts.Statement[] = []; 29 + 30 + if (lex.defs["main"]) { 31 + const main = lex.defs["main"]; 32 + let mainStatement: ts.Statement; 33 + const mainIdent = f.createIdentifier(lexName); 34 + // outer part is to avoid duplicate code 35 + // got a main def? check if its a proc/query 36 + if ( 37 + main.type == "query" || main.type == "procedure" || 38 + main.type == "subscription" 39 + ) { 40 + const rpcMain = main as lex.Query | lex.Procedure | lex.Subscription; 41 + // if its a proc/query, we're essentially filling in their respective templates 42 + // ^ remember to import functions for this 43 + // imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 44 + // body.push( 45 + // f.createImportDeclaration( 46 + // undefined, 47 + // f.createImportClause( 48 + // undefined, 49 + // undefined, 50 + // f.createNamespaceImport(f.createIdentifier("common")), 51 + // ), 52 + // f.createStringLiteral("@hotsocket/atproto-common"), 53 + // ), 54 + // ); 55 + const callFunctionName = f.createIdentifier("common.requests." + main.type); 56 + 57 + // parameters for function 58 + const method = f.createStringLiteral(lex.id); 59 + const service = f.createIdentifier("service"); 60 + const headers = f.createIdentifier("headers"); 61 + const input = f.createIdentifier("input"); 62 + const parameters = f.createIdentifier("parameters"); 63 + // only used as a type, so not needed 64 + // const output = f.createIdentifier("output"); 65 + 66 + //${lexName}.[something] 67 + const inputTypeID = f.createIdentifier(`${lexName}._input`); 68 + const outputTypeID = f.createIdentifier(`${lexName}._output`); 69 + const parametersTypeID = f.createIdentifier(`${lexName}._parameters`); 70 + 71 + const rpcParameters = [ 72 + f.createParameterDeclaration( 73 + undefined, 74 + undefined, 75 + service, 76 + undefined, 77 + f.createTypeReferenceNode("URL"), 78 + ), 79 + ]; 80 + const rpcBuildFlags = { 81 + hasInput: false, 82 + inputHasUnknown: false, 83 + hasParameters: false, 84 + parametersHasUnknown: false, 85 + hasOutput: false, 86 + outputHasUnknown: false, 87 + }; 88 + 89 + // inputs, pushed to function parameters 90 + if (rpcMain.parameters) { 91 + rpcBuildFlags.hasParameters = true; 92 + rpcBuildFlags.parametersHasUnknown = propsContainUnknown( 93 + rpcMain.parameters.properties, 94 + ); 95 + rpcParameters.push(f.createParameterDeclaration( 96 + undefined, 97 + undefined, 98 + parameters, 99 + undefined, 100 + f.createTypeReferenceNode(parametersTypeID), 101 + )); 102 + lex.defs["_parameters"] = rpcMain.parameters!; 103 + } 104 + if (rpcMain.type == "procedure" && rpcMain.input) { 105 + rpcBuildFlags.hasInput = true; 106 + if (rpcMain.input.encoding == "application/json") { 107 + rpcBuildFlags.inputHasUnknown = rpcMain.input.schema?.type == "object" && 108 + propsContainUnknown(rpcMain.input.schema.properties); 109 + rpcParameters.push(f.createParameterDeclaration( 110 + undefined, 111 + undefined, 112 + input, 113 + undefined, 114 + f.createTypeReferenceNode( 115 + inputTypeID, 116 + rpcBuildFlags.inputHasUnknown 117 + ? [ 118 + f.createTypeReferenceNode("T"), 119 + ] 120 + : undefined, 121 + ), 122 + )); 123 + lex.defs["_input"] = rpcMain.input.schema!; 124 + } else { 125 + rpcParameters.push(f.createParameterDeclaration( 126 + undefined, 127 + undefined, 128 + input, 129 + undefined, 130 + f.createTypeReferenceNode("Blob"), 131 + )); 132 + } 133 + } 134 + 135 + // output stuff. 136 + let outputType: ts.TypeNode | undefined; 137 + if (rpcMain.output) { 138 + rpcBuildFlags.hasOutput = true; 139 + if (rpcMain.output.encoding == "application/json") { 140 + rpcBuildFlags.outputHasUnknown = rpcMain.output.schema!.type == "object" && 141 + propsContainUnknown(rpcMain.output.schema!.properties); 142 + outputType = f.createTypeReferenceNode( 143 + outputTypeID, 144 + rpcBuildFlags.outputHasUnknown ? [f.createTypeReferenceNode("T")] : undefined, 145 + ); 146 + lex.defs["_output"] = rpcMain.output.schema!; 147 + } else { 148 + outputType = f.createTypeReferenceNode("Blob"); 149 + } 150 + } 151 + if (rpcMain.errors) { 152 + for (const error of rpcMain.errors) { 153 + const errArgs: ts.Expression[] = [f.createStringLiteral(error.name)]; 154 + if (error.description) { 155 + errArgs.push( 156 + f.createObjectLiteralExpression([ 157 + f.createPropertyAssignment( 158 + "cause", 159 + f.createStringLiteral(error.description), 160 + ), 161 + ]), 162 + ); 163 + } 164 + nsExtraDefs.push( 165 + f.createVariableStatement( 166 + [f.createModifier(ts.SyntaxKind.ExportKeyword)], 167 + f.createVariableDeclarationList([ 168 + f.createVariableDeclaration( 169 + error.name.replaceAll(" ", "_"), 170 + undefined, 171 + undefined, 172 + f.createNewExpression( 173 + f.createIdentifier("Error"), 174 + undefined, 175 + errArgs, 176 + ), 177 + ), 178 + ], ts.NodeFlags.Const), 179 + ), 180 + ); 181 + } 182 + } 183 + 184 + const xErrorRef = f.createTypeReferenceNode("common.types.XError"); 185 + const modifiers: ts.ModifierLike[] = [ 186 + f.createModifier(ts.SyntaxKind.ExportKeyword), 187 + ]; 188 + let returnType; 189 + let innerReturnType; 190 + if (rpcMain.type != "subscription") { 191 + addNamedImport(imports, "Result", "@hotsocket/dhmo"); 192 + innerReturnType = f.createTypeReferenceNode("Result", [ 193 + rpcBuildFlags.hasOutput ? outputType! : f.createTypeReferenceNode("never"), 194 + xErrorRef, 195 + ]); 196 + returnType = f.createTypeReferenceNode( 197 + "Promise", 198 + [innerReturnType], 199 + ); 200 + rpcParameters.push(f.createParameterDeclaration( 201 + undefined, 202 + undefined, 203 + headers, 204 + undefined, 205 + f.createTypeReferenceNode("Headers"), 206 + f.createNewExpression( 207 + f.createIdentifier("Headers"), 208 + undefined, 209 + [], 210 + ), 211 + )); 212 + modifiers.push(f.createModifier(ts.SyntaxKind.AsyncKeyword)); 213 + } else { 214 + returnType = f.createTypeReferenceNode("WebSocket"); 215 + } 216 + const callExpr = f.createCallExpression( 217 + callFunctionName, 218 + rpcBuildFlags.hasOutput ? [outputType!] : undefined, 219 + [ 220 + f.createObjectLiteralExpression( 221 + [ 222 + f.createPropertyAssignment("method", method), 223 + f.createPropertyAssignment("service", service), 224 + rpcBuildFlags.hasInput ? f.createPropertyAssignment("input", input) : undefined, 225 + rpcBuildFlags.hasParameters ? f.createPropertyAssignment("parameters", parameters) : undefined, 226 + rpcMain.type != "subscription" ? f.createPropertyAssignment("headers", headers) : undefined, 227 + ].filter((x) => !!x), 228 + true, 229 + ), 230 + // ^ filter drops anything undefined 231 + ], 232 + ); 233 + imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 234 + mainStatement = f.createFunctionDeclaration( 235 + modifiers, 236 + undefined, 237 + mainIdent, 238 + (rpcBuildFlags.inputHasUnknown || rpcBuildFlags.outputHasUnknown) 239 + ? [ 240 + CONSTRAINT_SERIALIZABLEOBJECT, 241 + ] 242 + : undefined, 243 + rpcParameters, 244 + returnType, 245 + f.createBlock([ 246 + f.createReturnStatement( 247 + rpcMain.type == "subscription" 248 + ? callExpr 249 + : f.createAsExpression(f.createAwaitExpression(callExpr), innerReturnType!), 250 + ), 251 + ], true), 252 + ); 253 + 254 + // also (_input|_params)/_output objects should be figured out and added to defs 255 + // } else if (main.type == "subscription") { 256 + // console.warn(`⚠️ dropped subscription ${lex.id}`); 257 + // return ""; 258 + } else { 259 + if (main.type == "object") { 260 + mainStatement = convertSchemaObject(lex, lexName, main, imports)!; 261 + } else if (main.type == "record") { 262 + mainStatement = convertSchemaObject( 263 + lex, 264 + lexName, 265 + main.record, 266 + imports, 267 + )!; 268 + } else throw new Error(`unsupported ${main.type} for main in ${lex.id}`); 269 + } 270 + 271 + addComments(main, mainStatement); 272 + body.push(mainStatement); 273 + 274 + // code's generated for main, so the def is deleted 275 + delete lex.defs["main"]; 276 + } 277 + 278 + const modStatements: ts.Statement[] = []; 279 + 280 + for (const originalName in lex.defs) { 281 + const def = lex.defs[originalName]!; 282 + // if (def.type != "object" && def.type != "params") throw new Error("hell nah " + lex.id); 283 + const defName = originalName.startsWith("_") ? originalName : "$" + originalName; 284 + let statement: ts.Statement; 285 + if (def.type == "object" || def.type == "params") { 286 + statement = convertSchemaObject(lex, defName, def as lex.Object, imports)!; 287 + } else { 288 + statement = convertProperty(lex, defName, def as lex.FieldTypes, imports); 289 + } 290 + addComments(def, statement); 291 + modStatements.push(statement); 292 + // body.push(statement); 293 + } 294 + 295 + // if (isRPC) { 296 + if (nsExtraDefs.length > 0) { 297 + const errorNS = f.createModuleDeclaration( 298 + [f.createModifier(ts.SyntaxKind.ExportKeyword)], 299 + f.createIdentifier("errors"), 300 + f.createModuleBlock(nsExtraDefs), 301 + ts.NodeFlags.Namespace, 302 + ); 303 + ts.addSyntheticLeadingComment( 304 + errorNS, 305 + ts.SyntaxKind.SingleLineCommentTrivia, 306 + "deno-lint-ignore no-namespace", 307 + true, 308 + ); 309 + modStatements.push(errorNS); 310 + } 311 + if (modStatements.length > 0) { 312 + const ns = f.createModuleDeclaration( 313 + [f.createModifier(ts.SyntaxKind.ExportKeyword)], 314 + f.createIdentifier(lexName), 315 + f.createModuleBlock(modStatements), 316 + ts.NodeFlags.Namespace, 317 + ); 318 + ts.addSyntheticLeadingComment( 319 + ns, 320 + ts.SyntaxKind.SingleLineCommentTrivia, 321 + "deno-lint-ignore no-namespace", 322 + true, 323 + ); 324 + body.push(ns); 325 + } 326 + // } else { 327 + // body.push(...modStatements); 328 + // } 329 + // if (hasMain) { 330 + // if (isRPC) { 331 + body.push( 332 + f.createExportDefault(f.createIdentifier(lexName)), 333 + ); 334 + // } 335 + // } else { 336 + // body.push( 337 + // f.createExportDefault(f.createObjectLiteralExpression([])), 338 + // ); 339 + // } 340 + // } 341 + 342 + const finalImports: ts.Statement[] = finalizeImports(imports); 343 + const sourceFile = f.createSourceFile( 344 + [ 345 + ...finalImports, 346 + ...body, 347 + ], 348 + f.createToken(ts.SyntaxKind.EndOfFileToken), 349 + ts.NodeFlags.None, 350 + ); 351 + return PRINTER.printFile(sourceFile); 352 + }
+87
src/generator/generateIndex.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import ts, { factory as f } from "typescript"; 8 + import { PRINTER } from "./shared.ts"; 9 + import fs from "node:fs/promises"; 10 + import type { Dirent } from "node:fs"; 11 + 12 + // this machine creates _index.ts 13 + export async function generateIndex(dir: string): Promise<string> { 14 + const dirName = dir.endsWith("/") ? dir : dir + "/"; 15 + const listing = await fs.readdir(dir, { withFileTypes: true }); 16 + const entries: Dirent[] = []; 17 + // const subdirs: Deno.DirEntry[] = []; 18 + const pairs: Record<string, boolean> = {}; 19 + for await (const file of listing) { 20 + if ( 21 + (file.isFile() && !file.name.startsWith("_") && 22 + file.name.endsWith(".ts")) || file.isDirectory() 23 + ) { 24 + entries.push(file); 25 + } 26 + } 27 + // detect xrpc calls (and therefore the existence of runtime values) 28 + await Promise.all(entries.map(async (file) => { 29 + let hasRuntime = false; 30 + let name = file.name; 31 + if (file.isFile()) { 32 + if (file.name == "index.ts") return; 33 + const content = await fs.readFile(dirName + file.name, "utf-8"); 34 + hasRuntime = content.indexOf("export async function") != -1; 35 + } 36 + if (file.isDirectory()) name = name + "/_index.ts"; 37 + pairs[name] = hasRuntime; 38 + })); 39 + 40 + // bang out the imports 41 + const body: ts.Statement[] = []; 42 + Object.keys(pairs).forEach((fileName) => { 43 + const hasRuntime = pairs[fileName]; 44 + const name = fileName.replace(".ts", "").replace("/_index", ""); 45 + if (!fileName.includes("_")) { 46 + // import { something as somethingNS } from "./something.ts"; 47 + const defaultExportName = name.replaceAll("-", "_") + "_default"; 48 + body.push(f.createImportDeclaration( 49 + undefined, 50 + f.createImportClause( 51 + undefined, 52 + f.createIdentifier(defaultExportName), 53 + undefined, // f.createNamespaceImport(f.createIdentifier(name + "NS")), 54 + ), 55 + f.createStringLiteral("./" + fileName), 56 + )); 57 + body.push(f.createExportDeclaration( 58 + undefined, 59 + !hasRuntime, 60 + f.createNamedExports([ 61 + f.createExportSpecifier( 62 + false, 63 + defaultExportName, 64 + f.createIdentifier(name), 65 + ), 66 + ]), 67 + )); 68 + } else { 69 + body.push(f.createExportDeclaration( 70 + undefined, 71 + false, 72 + f.createNamespaceExport(f.createIdentifier(name.replaceAll("-", "_"))), 73 + f.createStringLiteral("./" + fileName), 74 + )); 75 + } 76 + }); 77 + 78 + // spit out the content 79 + const sourceFile = f.createSourceFile( 80 + [ 81 + ...body, 82 + ], 83 + f.createToken(ts.SyntaxKind.EndOfFileToken), 84 + ts.NodeFlags.None, 85 + ); 86 + return PRINTER.printFile(sourceFile); 87 + }
+30
src/generator/generateRootIndex.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import * as fs from "node:fs/promises"; 8 + import * as ts from "typescript"; 9 + import { factory as f } from "typescript"; 10 + import { PRINTER } from "./shared.ts"; 11 + 12 + export async function generateRootIndex(dir: string): Promise<string> { 13 + const ls = (await fs.readdir(dir, { withFileTypes: true })).filter((x) => x.isDirectory()); 14 + const body: ts.Statement[] = []; 15 + ls.forEach((dir) => { 16 + body.push(f.createExportDeclaration( 17 + undefined, 18 + false, 19 + undefined, 20 + f.createStringLiteral(`./${dir.name}/_index.ts`), 21 + )); 22 + }); 23 + 24 + const sourceFile = f.createSourceFile( 25 + body, 26 + f.createToken(ts.SyntaxKind.EndOfFileToken), 27 + ts.NodeFlags.None, 28 + ); 29 + return PRINTER.printFile(sourceFile); 30 + }
+9
src/generator/generator.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + export * from "./generateFile.ts"; 8 + export * from "./generateIndex.ts"; 9 + export * from "./generator.ts";
+268
src/generator/shared.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import ts, { factory as f, SyntaxKind } from "typescript"; 8 + import type { lexicon as lex } from "@hotsocket/atproto-common"; 9 + 10 + /* need to work out the flow 11 + 12 + if theres a main def, wrap the rest in a namespace, since the default export needs to be the main def 13 + ^> If a main definition exists, it can be referenced without a fragment, just using the NSID. 14 + > For references in the $type fields in data objects themselves (eg, records or contents of a union), 15 + > this is a "must" (use of a #main suffix is invalid). For example, com.example.record not com.example.record#main. 16 + 17 + then we rename defs not starting with _ to start with $, to create that visual differentiation like lexicon refs 18 + ^ com.atproto.label.defs#selfLabels -> AT.com.atproto.label.defs.$selfLabels 19 + 20 + also with defs containing an "unknown" type, add a type parameter to the output type, and set the field type T 21 + 22 + */ 23 + 24 + export const PRINTER = ts.createPrinter({ 25 + newLine: ts.NewLineKind.LineFeed, 26 + noEmitHelpers: false, 27 + omitTrailingSemicolon: false, 28 + removeComments: false, 29 + }); 30 + 31 + export const CONSTRAINT_SERIALIZABLEOBJECT = f.createTypeParameterDeclaration( 32 + undefined, 33 + "T", 34 + f.createTypeReferenceNode("common.types.SerializableObject"), 35 + f.createTypeReferenceNode("common.types.SerializableObject"), 36 + ); 37 + export const CONSTRAINT_SERIALIZABLEPARAMS = f.createTypeParameterDeclaration( 38 + undefined, 39 + "T", 40 + f.createTypeReferenceNode("common.types.SerializableParams"), 41 + ); 42 + 43 + export type ImportInfo = { 44 + defaultImport?: string; 45 + namedImports?: ts.ImportSpecifier[]; 46 + nsImport?: string; 47 + }; 48 + export function finalizeImports( 49 + imports: Record<string, ImportInfo>, 50 + ): ts.Statement[] { 51 + const output: ts.Statement[] = []; 52 + for ( 53 + const [module, { defaultImport, namedImports, nsImport }] of Object.entries(imports) 54 + ) { 55 + if (nsImport) { 56 + output.push( 57 + f.createImportDeclaration( 58 + undefined, 59 + f.createImportClause( 60 + undefined, 61 + undefined, 62 + f.createNamespaceImport(f.createIdentifier(nsImport)), 63 + ), 64 + f.createStringLiteral(module), 65 + ), 66 + ); 67 + } else { 68 + output.push(f.createImportDeclaration( 69 + undefined, 70 + f.createImportClause( 71 + undefined, 72 + defaultImport ? f.createIdentifier(defaultImport) : undefined, 73 + namedImports ? f.createNamedImports(namedImports) : undefined, 74 + ), 75 + f.createStringLiteral(module), 76 + )); 77 + } 78 + } 79 + return output; 80 + } 81 + 82 + export function addComments<T extends lex.SchemaObject>( 83 + def: T, 84 + declaration: ts.Statement | ts.Declaration, 85 + ) { 86 + const commentParts: string[] = []; 87 + if (def.description) commentParts.push(def.description); 88 + if ( 89 + def.type == "query" || def.type == "procedure" || def.type == "subscription" 90 + ) commentParts.push("@" + def.type); 91 + if (def.type === "string" && "format" in def && (def as lex.String).format) { 92 + commentParts.push("@format " + (def as lex.String).format); 93 + } 94 + if (commentParts.length > 0) { 95 + ts.addSyntheticLeadingComment( 96 + declaration, 97 + ts.SyntaxKind.MultiLineCommentTrivia, 98 + "* " + commentParts.join("\n * ") + " ", 99 + true, 100 + ); 101 + } 102 + } 103 + export function addNamedImport( 104 + imports: Record<string, ImportInfo>, 105 + name: string, 106 + file: string, 107 + ) { 108 + if (!imports[file]) imports[file] = {}; 109 + if (!imports[file].namedImports) imports[file].namedImports = []; 110 + if (imports[file].namedImports.find((x) => x.name.text == name)) return; 111 + imports[file].namedImports.push( 112 + f.createImportSpecifier(false, undefined, f.createIdentifier(name)), 113 + ); 114 + } 115 + 116 + export function lastNSIDPart(nsid: string): string { 117 + return nsid.substring(nsid.lastIndexOf(".") + 1); 118 + } 119 + 120 + export function refToNode( 121 + ref: string, 122 + lex: lex.Lexicon, 123 + imports: Record<string, ImportInfo>, 124 + ) { 125 + const fixedRef = ref.replace("#", ".$"); 126 + // #something means it's local to "here" 127 + if (ref.startsWith("#")) { 128 + if (Object.hasOwn(lex.defs, "main")) { 129 + //${lastNSIDPart(lex.id)}${fixedRef} 130 + return f.createTypeReferenceNode(`${lastNSIDPart(lex.id)}${fixedRef}`); 131 + } else { 132 + return f.createTypeReferenceNode(fixedRef.replace(".", "")); 133 + } 134 + } else { 135 + imports["@/index.ts"] = { nsImport: "AT" }; 136 + return f.createTypeReferenceNode("AT." + fixedRef); 137 + } 138 + } 139 + 140 + export function lookupType( 141 + object: lex.AnySchemaObject, 142 + lex: lex.Lexicon, 143 + name: string, 144 + imports: Record<string, ImportInfo>, 145 + ): ts.TypeNode { 146 + switch (object.type) { 147 + case "cid-link": 148 + case "bytes": 149 + case "token": 150 + case "string": 151 + return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); 152 + case "blob": 153 + imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 154 + return f.createTypeReferenceNode("common.types.XBlob"); 155 + case "boolean": 156 + return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); 157 + case "integer": 158 + return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); 159 + case "unknown": 160 + return f.createTypeReferenceNode("T"); 161 + case "ref": 162 + return refToNode(object.ref, lex, imports); 163 + case "union": 164 + // TODO: Handle union types properly 165 + if (object.refs.length == 0) { 166 + // Record<PropertyKey, never> 167 + return f.createTypeReferenceNode( 168 + "Record", 169 + [ 170 + f.createTypeReferenceNode("PropertyKey"), 171 + f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), 172 + ], 173 + ); 174 + } else { 175 + return f.createUnionTypeNode( 176 + object.refs.map((ref) => refToNode(ref, lex, imports)), 177 + ); 178 + } 179 + case "array": 180 + // TODO: Handle array types properly 181 + return f.createArrayTypeNode( 182 + lookupType(object.items, lex, name + ">array", imports), 183 + ); 184 + // objects, params are handled differently 185 + default: 186 + throw new Error(`unhandled type ${object.type} for ${lex.id}#${name}`); 187 + } 188 + } 189 + 190 + export function propsContainUnknown( 191 + props: Record<string, lex.AnySchemaObject>, 192 + ): boolean { 193 + for (const name in props) { 194 + const prop = props[name]!; 195 + if (prop.type == "unknown") return true; 196 + if (prop.type == "array" && prop.items.type == "unknown") return true; 197 + } 198 + return false; 199 + } 200 + 201 + export function convertProperty( 202 + lex: lex.Lexicon, 203 + name: string, 204 + def: lex.FieldTypes, 205 + imports: Record<string, ImportInfo>, 206 + ): ts.Statement { 207 + return f.createTypeAliasDeclaration( 208 + [ 209 + f.createModifier(ts.SyntaxKind.ExportKeyword), 210 + ], 211 + name, 212 + undefined, 213 + lookupType(def, lex, name, imports), 214 + ); 215 + } 216 + 217 + export function convertSchemaObject( 218 + lex: lex.Lexicon, 219 + name: string, 220 + def: lex.Object, 221 + imports: Record<string, ImportInfo>, 222 + ): ts.Statement | void { 223 + const members: ts.TypeElement[] = []; 224 + imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 225 + // members.push( 226 + // `\t[key: string]: ${name == "_parameters" ? "string | number | boolean | null | undefined" : "Serializable"};`, 227 + // ); 228 + function requiredLookup(propName: string): ts.QuestionToken | undefined { 229 + if (def.required && def.required.indexOf(propName) != -1) { 230 + return undefined; 231 + } else { 232 + return f.createToken(ts.SyntaxKind.QuestionToken); 233 + } 234 + } 235 + // detect whether any property contains `unknown` (including arrays of unknown) 236 + // so we can add the generic `T` type parameter to the interface when needed. 237 + const hasUnknown = propsContainUnknown(def.properties); 238 + for (const propName in def.properties) { 239 + const prop = def.properties[propName]!; 240 + 241 + const member = f.createPropertySignature( 242 + undefined, 243 + propName, 244 + requiredLookup(propName), 245 + lookupType(prop, lex, propName, imports), 246 + ); 247 + addComments(prop, member); 248 + members.push(member); 249 + } 250 + let inheritID; 251 + if (name == "_parameters") { 252 + inheritID = f.createIdentifier("common.types.SerializableParams"); 253 + } else { 254 + inheritID = f.createIdentifier("common.types.SerializableObject"); 255 + } 256 + return f.createInterfaceDeclaration( 257 + [f.createModifier(ts.SyntaxKind.ExportKeyword)], 258 + name, 259 + hasUnknown ? [CONSTRAINT_SERIALIZABLEOBJECT] : undefined, 260 + [f.createHeritageClause(SyntaxKind.ExtendsKeyword, [ 261 + f.createExpressionWithTypeArguments( 262 + inheritID, 263 + undefined, 264 + ), 265 + ])], 266 + members, 267 + ); 268 + }
+58
src/genmain.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import { Dirent } from "node:fs"; 8 + import { generateFile } from "./generator/generateFile.ts"; 9 + import { generateIndex } from "./generator/generateIndex.ts"; 10 + import * as fs from "node:fs/promises"; 11 + 12 + const XSRCPATH = "./_external/atproto/lexicons"; 13 + const SRCPATH = "./lexicons"; 14 + const OUTPATH = "./support/atproto/generated"; 15 + 16 + async function convertFiles(rootDir: string, parentDir: string, entry: Dirent) { 17 + const inputPath = parentDir + "/" + entry.name; 18 + const outputPath = inputPath.replace(rootDir, OUTPATH); 19 + 20 + if (entry.isFile()) { 21 + if (entry.name.endsWith(".json")) { 22 + await fs.writeFile( 23 + outputPath.substring(0, outputPath.lastIndexOf(".json")) + ".ts", 24 + generateFile(await fs.readFile(inputPath, { encoding: "utf-8" })), 25 + ); 26 + } 27 + } else { 28 + await fs.mkdir(outputPath, { recursive: true }); 29 + await Promise.all( 30 + (await fs.readdir(inputPath, { withFileTypes: true })).map((name) => convertFiles(rootDir, inputPath, name)), 31 + ); 32 + } 33 + } 34 + 35 + await Promise.all([ 36 + Promise.all( 37 + (await fs.readdir(XSRCPATH, { withFileTypes: true })).map(async (root) => { 38 + await convertFiles(XSRCPATH, XSRCPATH, root); 39 + }), 40 + ), 41 + Promise.all( 42 + (await fs.readdir(SRCPATH, { withFileTypes: true })).map(async (root) => { 43 + await convertFiles(SRCPATH, SRCPATH, root); 44 + }), 45 + ), 46 + ]); 47 + 48 + async function createIndexes(parentDir: string, entry: Dirent) { 49 + const path = parentDir + "/" + entry.name; 50 + 51 + if (entry.isDirectory()) { 52 + await fs.writeFile(path + "/_index.ts", await generateIndex(path)); 53 + await Promise.all((await fs.readdir(path, { withFileTypes: true })).map((name) => createIndexes(path, name))); 54 + } 55 + } 56 + 57 + await fs.writeFile(OUTPATH + "/_index.ts", await generateIndex(OUTPATH)); 58 + await Promise.all((await fs.readdir(OUTPATH, { withFileTypes: true })).map((name) => createIndexes(OUTPATH, name)));
+51
src/internaltypes.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import * as z from "@zod/zod"; 8 + import * as cfg from "./config.ts"; 9 + import * as dp from "./did_parts.ts"; 10 + 11 + export const serviceProperty_z = z.object({ 12 + id: z.string(), 13 + type: z.string(), 14 + serviceEndpoint: z.string(), 15 + }); 16 + export type ServiceProperty = z.infer<typeof serviceProperty_z>; 17 + // incomplete but like come on i dont need all that 18 + export const didDoc_z = z.object({ 19 + id: z.string(), 20 + alsoKnownAs: z.optional(z.set(z.string())), 21 + controller: z.optional(z.union([z.string(), z.set(z.string())])), 22 + service: z.optional(z.set(serviceProperty_z)), 23 + }); 24 + export type DID = z.infer<typeof dp.did_z>; 25 + export type DIDDoc = z.infer<typeof didDoc_z>; 26 + 27 + const anyResolvedInput = z.object({ 28 + raw: z.object({ 29 + name: z.string(), 30 + input: cfg.anyInput, 31 + }), 32 + }); 33 + export const resolvedAtInput = anyResolvedInput.safeExtend({ 34 + kind: z.literal("at"), 35 + pds: z.httpUrl(), 36 + did: z.string(), 37 + }); 38 + 39 + export const resolvedGitInput = anyResolvedInput.safeExtend({ 40 + kind: z.literal("git"), 41 + url: z.url(), 42 + dir: z.string(), 43 + // rev: z.optional(z.string()), 44 + ref: z.optional(z.string()), 45 + }); 46 + 47 + export const resolvedInput_z = z.discriminatedUnion("kind", [ 48 + resolvedAtInput, 49 + resolvedGitInput, 50 + ]); 51 + export type ResolvedInput = z.infer<typeof resolvedInput_z>;
+55
src/main.ts
··· 1 + #!/usr/bin/env node 2 + /* 3 + * This Source Code Form is subject to the terms of the Mozilla Public 4 + * License, v. 2.0. If a copy of the MPL was not distributed with this 5 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 + */ 7 + 8 + import * as run from "./run.ts"; 9 + import { default as denoJson } from "../deno.json" with { type: "json" }; 10 + 11 + export async function main(argv: (string | undefined)[]): Promise<number> { 12 + const act = argv[2]; 13 + const cfg = argv[3]; 14 + // GRAAAAHHHHHHHHHHHH 15 + console.log(" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"); 16 + console.log(`🔥🔥 ⚔️ LEXICON-QUEROR ⚔️ 🔥🔥`); 17 + console.log(" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"); 18 + console.log(`VERSION ${denoJson.version}`); 19 + console.log(`SUBJUGATE YOUR SCHEMAS`); 20 + switch (act) { 21 + case "download": 22 + await run.download(cfg); 23 + break; 24 + case "convert": 25 + await run.convert(cfg); 26 + break; 27 + case "setup": 28 + await run.setup(cfg); 29 + break; 30 + 31 + default: 32 + // console.log( 33 + // `unknown action ${act ? `'${act}'` : "<none>"}, showing help...`, 34 + // ); 35 + // /* falls through */ 36 + // case "help": 37 + console.log(" ___________________________________________________"); 38 + console.log("|_______________________ HELP ______________________|"); 39 + console.log("| |"); 40 + console.log("| CMD: <run lxq> <action> [args] [path/to/lxq.json] |"); 41 + console.log("| |"); 42 + console.log("|__________________ ACTIONS LIST __________________|"); 43 + console.log("| [ action ] [ description ] |"); 44 + console.log("| download 🛜 Downloads configured inputs |"); 45 + console.log("| convert 🔀 Converts lexicons to TypeScript |"); 46 + console.log("| setup 🌅 Gives you an lxq.json to work with |"); 47 + // console.log("| new ✨ Creates a new lexicon definition in |"); 48 + // console.log("| your workspace. Has args: |"); 49 + // console.log("| - group/input name |"); 50 + // console.log("| - NSID (com.example.something) |"); 51 + console.log("|___________________________________________________|"); 52 + break; 53 + } 54 + return 0; 55 + }
+321
src/run.ts
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + 7 + import * as z from "@zod/zod"; 8 + import * as dns from "node:dns/promises"; 9 + import * as fs from "node:fs/promises"; 10 + import * as fss from "node:fs"; 11 + import * as path from "node:path"; 12 + import * as cfg from "./config.ts"; 13 + import * as types from "./internaltypes.ts"; 14 + import * as dp from "./did_parts.ts"; 15 + import { lexicon as lex } from "@hotsocket/atproto-common"; 16 + import { spawnSync } from "node:child_process"; 17 + import * as gen from "./generator/generator.ts"; 18 + 19 + // not using generated code to avoid potential circular dependency type trouble 20 + 21 + export const DEFAULT_CONFIG_PATH = "./lxq.json"; 22 + const DEFAULT_DATA_PATH = ".lxq"; 23 + const DEFAULT_OUTPUT_PATH = "generated/lxq"; 24 + async function configHelper(configPath: string): Promise<cfg.Config & { root: string }> { 25 + console.log(); 26 + const configDir = path.dirname(path.resolve(configPath)); 27 + const raw = JSON.parse(await fs.readFile(path.resolve(configPath), { encoding: "utf-8" })); 28 + console.log("⏳\tValidating config..."); 29 + const config = cfg.config_z.parse(raw); 30 + const dataDir = path.resolve(configDir, config.dataDir ?? path.resolve(configDir, DEFAULT_DATA_PATH)); 31 + if (!config.dataDir) console.log("👀\tUsing fallback dataDir"); 32 + console.log("🗄️\tAbsolute path of dataDir: " + dataDir); 33 + const outputDir = path.resolve(configDir, config.outputDir ?? path.resolve(configDir, DEFAULT_OUTPUT_PATH)); 34 + if (!config.outputDir) console.log("👀\tUsing fallback outputDir"); 35 + console.log("🗄️\tAbsolute path of outputDir: " + outputDir); 36 + return { 37 + inputs: config.inputs, 38 + dataDir: dataDir, 39 + outputDir: outputDir, 40 + root: configDir, 41 + }; 42 + } 43 + 44 + export async function download(configPath: string = "./lxq.json") { 45 + const config = await configHelper(configPath); 46 + const dataDir = config.dataDir; 47 + console.log("⏳\tResolving inputs..."); 48 + const resolvedInputs: types.ResolvedInput[] = []; 49 + await Promise.all( 50 + Object.keys(config.inputs).map(async (name) => { 51 + const input = cfg.anyInput.parse(config.inputs[name]); 52 + let info = ""; 53 + if (input.startsWith("at://")) { // pulls straight from published in repo 54 + const inner = cfg.atInput.parse(input).substring(5); 55 + let did: types.DID; 56 + if (inner.startsWith("did:")) { 57 + did = inner as types.DID; 58 + } else { 59 + did = await resolveHandle(inner); 60 + } 61 + const doc = await getDidDoc(did); 62 + let svc: types.ServiceProperty; 63 + try { 64 + svc = doc.service!.values().find((v) => v.id == "#atproto_pds")!; 65 + } catch { 66 + throw new Error("could not find #atproto_pds in doc"); 67 + } 68 + let pds = svc.serviceEndpoint; 69 + if (pds.endsWith("/")) pds = pds.substring(0, pds.length - 1); 70 + info = `DID: '${did}', PDS: '${svc.serviceEndpoint}'`; 71 + resolvedInputs.push(types.resolvedAtInput.parse({ 72 + raw: { name: name, input: input }, 73 + kind: "at", 74 + pds: pds, 75 + did: did, 76 + })); 77 + } else if (input.startsWith("git+")) { 78 + const inner = new URL(cfg.gitInput.parse(input).substring(4)); 79 + const repo = inner.origin + inner.pathname; 80 + resolvedInputs.push(types.resolvedGitInput.parse({ 81 + kind: "git", 82 + raw: { 83 + name: name, 84 + input: input, 85 + }, 86 + dir: inner.searchParams.get("dir"), 87 + ref: inner.searchParams.get("ref") ?? undefined, 88 + url: repo, 89 + } as z.infer<typeof types.resolvedGitInput>)); 90 + } 91 + console.log(`✅\tResolved '${name}' (\`${input}\`)` + (info ? `to [${info}]` : "")); 92 + }), 93 + ); 94 + 95 + // literally just throwing if anything fucks up 96 + console.log("⏳\tValidating inputs..."); 97 + await Promise.all( 98 + resolvedInputs.map(async (input_) => { 99 + const input = types.resolvedInput_z.parse(input_); 100 + if (input.kind == "at") { 101 + const probeRsp = await fetch( 102 + `${input.pds}/xrpc/com.atproto.repo.listRecords?repo=${input.did}&collection=com.atproto.lexicon.schema`, 103 + ); 104 + // {"records":[]} 105 + const probeBody = await probeRsp.json() as { records: unknown[] }; 106 + if (probeBody.records.length == 0) { 107 + throw new Error(`Repo for AT input '${input.raw.name}' does not contain any lexicons.`); 108 + } 109 + console.log(`✅\tFound lexicons in AT input '${input.raw.name}'`); 110 + } else if (input.kind == "git") { 111 + const proc = spawnSync("git", ["ls-remote", "--refs", input.url], { encoding: "utf-8" }); 112 + if (proc.status != 0) { 113 + throw new Error(`Error checking git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`); 114 + } 115 + if (input.ref) { 116 + // 91270de847fb763b1cb34ac8733c8bbc3991f820\trefs/heads/main => refs/heads/main 117 + const refs = proc.output[1]!.split("\n").map((line) => line.split("\t")[1]); 118 + // refs/heads/main => main 119 + const heads = refs.map((ref) => ref.substring(ref.lastIndexOf("/") + 1)); 120 + if (!heads.includes(input.ref)) { 121 + throw new Error(`Could not find ref '${input.ref}' in git input '${input.raw.name}'`); 122 + } 123 + console.log(`✅\tRemote for Git input '${input.raw.name}' exists and has ref '${input.ref}'`); 124 + } else { 125 + console.log(`✅\tRemote for Git input '${input.raw.name}' exists`); 126 + } 127 + } 128 + }), 129 + ); 130 + console.log("⏳\tRetrieving lexicons..."); 131 + await Promise.all( 132 + resolvedInputs.map(async (input_) => { 133 + const input = types.resolvedInput_z.parse(input_); 134 + const myPath = dataDir + "/" + input.raw.name; 135 + const myTmp = dataDir + "/.temp/" + input.raw.name; 136 + if (input.kind == "at") { 137 + let cursor: string | undefined; 138 + const lexicons: lex.Lexicon[] = []; 139 + // do/while because there is no cursor to start 140 + do { 141 + const params = new URLSearchParams(); 142 + params.append("repo", input.did); 143 + params.append("collection", "com.atproto.lexicon.schema"); 144 + if (cursor) params.append("cursor", cursor); 145 + const rsp = await fetch( 146 + `${input.pds}/xrpc/com.atproto.repo.listRecords?${params.toString()}`, 147 + ); 148 + if (!rsp.ok) throw new Error(`Error retrieving lexicons of input '${input.raw.name}': ${await rsp.text()}`); 149 + const data = await rsp.json() as { 150 + cursor?: string; 151 + records: { 152 + uri: string; 153 + cid: string; 154 + value: lex.Lexicon; 155 + }[]; 156 + }; 157 + lexicons.push(...data.records.map((r) => r.value)); 158 + cursor = data.cursor; 159 + } while (cursor); 160 + await Promise.all(lexicons.map(async (lexicon) => { 161 + // com.atproto.server.getAccountInviteCodes 162 + // ^ 163 + const lastDot = lexicon.id.lastIndexOf("."); 164 + // com.atproto.server => com/atproto/server 165 + const dir = dataDir + `/${input.raw.name}/` + lexicon.id.substring(0, lastDot).split(".").join("/") + "/"; 166 + await fs.mkdir(dir, { recursive: true }); 167 + // getAccountInviteCodes 168 + const name = lexicon.id.substring(lastDot + 1); 169 + await fs.writeFile(`${dir}/${name}.json`, JSON.stringify(lexicon)); 170 + })); 171 + console.log(`✅\tRetrieved ${lexicons.length} lexicons from AT input '${input.raw.name}'`); 172 + } else if (input.kind == "git") { 173 + await fs.mkdir(path.dirname(myTmp), { recursive: true }); 174 + let shouldCopy = false; 175 + if (fss.existsSync(myTmp)) { 176 + const proc = spawnSync("git", ["-C", myTmp, "pull"], { encoding: "utf-8" }); 177 + if (proc.status != 0) { 178 + throw new Error(`Error pulling git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`); 179 + } 180 + if (proc.stdout.split("\n")[0].startsWith("Already up to date.")) { 181 + console.log(`ℹ️\tGit input '${input.raw.name}' already up to date`); 182 + } else { 183 + shouldCopy = true; 184 + console.log(`ℹ️\tUpdated Git input '${input.raw.name}'`); 185 + } 186 + } else { 187 + const proc = spawnSync("git", ["clone", input.url, myTmp], { encoding: "utf-8" }); 188 + if (proc.status != 0) { 189 + throw new Error(`Error cloning git input '${input.raw.name}': ${proc.output[2]?.split("\n")[0]}`); 190 + } 191 + console.log(`⏳\tCloned Git input '${input.raw.name}' to '${myTmp}'`); 192 + shouldCopy = true; 193 + } 194 + if (shouldCopy) { 195 + const src = `${myTmp}/${input.dir}`; 196 + if (!(await fs.stat(src)).isDirectory()) { 197 + throw new Error(`Path '${src}' for Git input '${input.raw.name}' is not a directory`); 198 + } 199 + await fs.cp(src, myPath, { recursive: true }); 200 + console.log(`✅\tCopied Git input '${input.raw.name}' to '${myPath}'`); 201 + } 202 + } 203 + }), 204 + ); 205 + } 206 + 207 + export async function convert(configPath: string = DEFAULT_CONFIG_PATH) { 208 + const config = await configHelper(configPath); 209 + console.log(`🏇\tConverting lexicons to TypeScript files at '${config.outputDir}'`); 210 + 211 + console.log("⏲️\tGenerating content..."); 212 + const inputListing = (await fs.readdir(config.dataDir, { recursive: true })) 213 + .filter((x) => !x.startsWith(".temp/")); 214 + await Promise.all( 215 + inputListing 216 + .filter((x) => x.endsWith(".json")).map(async (srcRel) => { 217 + const dstRel = srcRel.substring(0, srcRel.lastIndexOf(".")) + ".ts"; 218 + const srcPath = path.resolve(config.dataDir, srcRel); 219 + const dstPath = path.resolve(config.outputDir, dstRel.substring(dstRel.indexOf("/") + 1)); 220 + 221 + const dstDir = path.dirname(dstPath); 222 + await fs.mkdir(dstDir, { recursive: true }); 223 + 224 + const code = gen.generateFile(await fs.readFile(srcPath, { encoding: "utf-8" })); 225 + await fs.writeFile(dstPath, code); 226 + }), 227 + ); 228 + 229 + console.log("🧐\tGenerating indexes..."); 230 + const generatedListing = (await fs.readdir(config.outputDir, { recursive: true, withFileTypes: true })) 231 + .filter((x) => x.isDirectory()) 232 + .map((x) => `${x.parentPath}/${x.name}`); 233 + await Promise.all( 234 + generatedListing.map(async (dir) => { 235 + await fs.writeFile( 236 + `${dir}/_index.ts`, 237 + await gen.generateIndex(dir), 238 + ); 239 + }), 240 + ); 241 + await fs.writeFile(config.outputDir + "/index.ts", await gen.generateIndex(config.outputDir)); 242 + console.log("♨️\t Finished!"); 243 + } 244 + 245 + export async function setup(configPath: string = DEFAULT_CONFIG_PATH) { 246 + console.log(); 247 + if (fss.existsSync(configPath)) { 248 + console.error(`⚠️ A config file already exists at ${configPath} !`); 249 + console.error(` You'll need to remove it to create a new one.`); 250 + return; 251 + } 252 + await fs.writeFile( 253 + configPath, 254 + JSON.stringify( 255 + { 256 + inputs: { 257 + atproto: "at://did:plc:6msi3pj7krzih5qxqtryxlzw", 258 + bsky: "at://did:plc:4v4y5r3lwsbtmsxhile2ljac", 259 + }, 260 + dataDir: ".lxq", 261 + outputDir: "generated", 262 + } as cfg.Config, 263 + null, 264 + "\t", 265 + ), 266 + ); 267 + function manualImportMessage() { 268 + console.log("It's recommended you add a bit to your `deno.json` or similar that looks like this:"); 269 + console.log(' "imports": {'); 270 + console.log(' "@/": "./generated/"'); 271 + console.log(" }"); 272 + console.log(); 273 + console.log('This makes your "gateway" import situation look like this:'); 274 + console.log(' import * as AT from "@/index.ts";'); 275 + console.log(); 276 + } 277 + 278 + console.log(); 279 + console.log("🌅 New lxq.json created at " + configPath); 280 + console.log(); 281 + console.log("- Includes inputs for ATProto and Bluesky lexicons via PDS records"); 282 + console.log("- Data will be stored at `.lxq`"); 283 + console.log("- Generated code will land at `generated`"); 284 + console.log(); 285 + manualImportMessage(); 286 + console.log("Have fun, and make something awesome!"); 287 + console.log(); 288 + } 289 + 290 + async function getDidDoc(identifier: string): Promise<types.DIDDoc> { 291 + const did = cfg.did_parts.decode(identifier as z.infer<typeof dp.did_z>); 292 + if (did.method == "plc") { 293 + const res = await fetch("https://plc.directory/" + identifier); 294 + if (!res.ok) throw new Error(`failed to fetch did:plc. status: ${res.status}, body: '${await res.text()}'`); 295 + return (await res.json()) as types.DIDDoc; 296 + } else { 297 + // did:web 298 + const res = await fetch(`https://${did.identifier}/.well-known/did.json`); 299 + if (!res.ok) throw new Error(`failed to fetch did:web. status: ${res.status}, body: '${await res.text()}'`); 300 + return (await res.json()) as types.DIDDoc; 301 + } 302 + } 303 + 304 + const TXT_KEY = "did="; 305 + async function resolveHandle(handle: string): Promise<types.DID> { 306 + try { 307 + // _atproto.example.com 308 + const txt = await dns.resolveTxt("_atproto." + handle); 309 + const merged = txt.map((x) => x.join(" ")); 310 + const didEntry = merged.find((entry) => entry.startsWith(TXT_KEY)); 311 + if (didEntry) { 312 + return dp.did_z.parse(didEntry.substring(TXT_KEY.length)); 313 + } 314 + } catch { /* just going to fall back to http */ } 315 + const http_rsp = await fetch(`https://${handle}/.well-known/atproto-did`); 316 + if (http_rsp.ok) { 317 + return dp.did_z.parse(await http_rsp.text()); 318 + } else { 319 + throw new Error("Could not resolve handle to DID"); 320 + } 321 + }
+28
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "esnext", 4 + "module": "nodenext", 5 + "moduleResolution": "nodenext", 6 + "lib": [ 7 + "esnext", 8 + "DOM" 9 + ], 10 + "strict": true, 11 + "esModuleInterop": true, 12 + "skipLibCheck": true, 13 + "forceConsistentCasingInFileNames": true, 14 + "noUnusedLocals": true, 15 + "noUnusedParameters": true, 16 + "noFallthroughCasesInSwitch": true, 17 + "noImplicitReturns": true, 18 + "noUncheckedIndexedAccess": true, 19 + "noImplicitOverride": true, 20 + "exactOptionalPropertyTypes": true 21 + }, 22 + "include": [ 23 + "**/*.ts" 24 + ], 25 + "exclude": [ 26 + "node_modules" 27 + ] 28 + }