A CLI for scaffolding ATProto web applications
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

formatting

+367 -121
+1 -1
.github/workflows/test.yml
··· 16 16 uses: actions/setup-node@v6 17 17 with: 18 18 node-version-file: package.json 19 - cache: 'pnpm' 19 + cache: "pnpm" 20 20 - name: install dependencies 21 21 run: pnpm install --frozen-lockfile 22 22 - name: test
+8
.oxfmtrc.json
··· 1 + { 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + "ignorePatterns": ["dist/*", "src/templates/*"], 4 + "experimentalSortImports": { 5 + "order": "asc" 6 + }, 7 + "printWidth": 80 8 + }
+16 -14
package.json
··· 2 2 "name": "create-atproto-app", 3 3 "version": "0.0.1", 4 4 "description": "CLI to bootstrap ATProto projects", 5 - "type": "module", 6 - "main": "dist/index.js", 5 + "keywords": [ 6 + "atproto", 7 + "cli" 8 + ], 9 + "license": "MIT", 10 + "author": "Dane MIller <me@dane.computer>", 7 11 "bin": { 8 12 "create-atproto-app": "dist/index.js" 9 13 }, 14 + "type": "module", 15 + "main": "dist/index.js", 10 16 "scripts": { 11 17 "dev": "tsx src/index.ts", 12 18 "build": "tsc", 13 19 "start": "node dist/index.js", 20 + "fmt": "oxfmt", 14 21 "test": "vitest", 15 22 "test:run": "vitest run" 16 23 }, 17 - "keywords": [ 18 - "atproto", 19 - "cli" 20 - ], 21 - "author": "Dane MIller <me@dane.computer>", 22 - "license": "MIT", 23 - "packageManager": "pnpm@10.15.0", 24 - "engines": { 25 - "node": ">= 24.12.0" 24 + "dependencies": { 25 + "@clack/prompts": "^1.0.1" 26 26 }, 27 27 "devDependencies": { 28 28 "@types/node": "^22.0.0", 29 29 "citty": "^0.2.1", 30 30 "execa": "^9.6.1", 31 + "oxfmt": "^0.32.0", 31 32 "tsx": "^4.19.0", 32 33 "typescript": "^5.6.0", 33 34 "vitest": "^4.0.18" 34 35 }, 35 - "dependencies": { 36 - "@clack/prompts": "^1.0.1" 37 - } 36 + "engines": { 37 + "node": ">= 24.12.0" 38 + }, 39 + "packageManager": "pnpm@10.15.0" 38 40 }
+209
pnpm-lock.yaml
··· 21 21 execa: 22 22 specifier: ^9.6.1 23 23 version: 9.6.1 24 + oxfmt: 25 + specifier: ^0.32.0 26 + version: 0.32.0 24 27 tsx: 25 28 specifier: ^4.19.0 26 29 version: 4.21.0 ··· 198 201 '@jridgewell/sourcemap-codec@1.5.5': 199 202 resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 200 203 204 + '@oxfmt/binding-android-arm-eabi@0.32.0': 205 + resolution: {integrity: sha512-DpVyuVzgLH6/MvuB/YD3vXO9CN/o9EdRpA0zXwe/tagP6yfVSFkFWkPqTROdqp0mlzLH5Yl+/m+hOrcM601EbA==} 206 + engines: {node: ^20.19.0 || >=22.12.0} 207 + cpu: [arm] 208 + os: [android] 209 + 210 + '@oxfmt/binding-android-arm64@0.32.0': 211 + resolution: {integrity: sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ==} 212 + engines: {node: ^20.19.0 || >=22.12.0} 213 + cpu: [arm64] 214 + os: [android] 215 + 216 + '@oxfmt/binding-darwin-arm64@0.32.0': 217 + resolution: {integrity: sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg==} 218 + engines: {node: ^20.19.0 || >=22.12.0} 219 + cpu: [arm64] 220 + os: [darwin] 221 + 222 + '@oxfmt/binding-darwin-x64@0.32.0': 223 + resolution: {integrity: sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw==} 224 + engines: {node: ^20.19.0 || >=22.12.0} 225 + cpu: [x64] 226 + os: [darwin] 227 + 228 + '@oxfmt/binding-freebsd-x64@0.32.0': 229 + resolution: {integrity: sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q==} 230 + engines: {node: ^20.19.0 || >=22.12.0} 231 + cpu: [x64] 232 + os: [freebsd] 233 + 234 + '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': 235 + resolution: {integrity: sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw==} 236 + engines: {node: ^20.19.0 || >=22.12.0} 237 + cpu: [arm] 238 + os: [linux] 239 + 240 + '@oxfmt/binding-linux-arm-musleabihf@0.32.0': 241 + resolution: {integrity: sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g==} 242 + engines: {node: ^20.19.0 || >=22.12.0} 243 + cpu: [arm] 244 + os: [linux] 245 + 246 + '@oxfmt/binding-linux-arm64-gnu@0.32.0': 247 + resolution: {integrity: sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg==} 248 + engines: {node: ^20.19.0 || >=22.12.0} 249 + cpu: [arm64] 250 + os: [linux] 251 + 252 + '@oxfmt/binding-linux-arm64-musl@0.32.0': 253 + resolution: {integrity: sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw==} 254 + engines: {node: ^20.19.0 || >=22.12.0} 255 + cpu: [arm64] 256 + os: [linux] 257 + 258 + '@oxfmt/binding-linux-ppc64-gnu@0.32.0': 259 + resolution: {integrity: sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ==} 260 + engines: {node: ^20.19.0 || >=22.12.0} 261 + cpu: [ppc64] 262 + os: [linux] 263 + 264 + '@oxfmt/binding-linux-riscv64-gnu@0.32.0': 265 + resolution: {integrity: sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ==} 266 + engines: {node: ^20.19.0 || >=22.12.0} 267 + cpu: [riscv64] 268 + os: [linux] 269 + 270 + '@oxfmt/binding-linux-riscv64-musl@0.32.0': 271 + resolution: {integrity: sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ==} 272 + engines: {node: ^20.19.0 || >=22.12.0} 273 + cpu: [riscv64] 274 + os: [linux] 275 + 276 + '@oxfmt/binding-linux-s390x-gnu@0.32.0': 277 + resolution: {integrity: sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA==} 278 + engines: {node: ^20.19.0 || >=22.12.0} 279 + cpu: [s390x] 280 + os: [linux] 281 + 282 + '@oxfmt/binding-linux-x64-gnu@0.32.0': 283 + resolution: {integrity: sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA==} 284 + engines: {node: ^20.19.0 || >=22.12.0} 285 + cpu: [x64] 286 + os: [linux] 287 + 288 + '@oxfmt/binding-linux-x64-musl@0.32.0': 289 + resolution: {integrity: sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw==} 290 + engines: {node: ^20.19.0 || >=22.12.0} 291 + cpu: [x64] 292 + os: [linux] 293 + 294 + '@oxfmt/binding-openharmony-arm64@0.32.0': 295 + resolution: {integrity: sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA==} 296 + engines: {node: ^20.19.0 || >=22.12.0} 297 + cpu: [arm64] 298 + os: [openharmony] 299 + 300 + '@oxfmt/binding-win32-arm64-msvc@0.32.0': 301 + resolution: {integrity: sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA==} 302 + engines: {node: ^20.19.0 || >=22.12.0} 303 + cpu: [arm64] 304 + os: [win32] 305 + 306 + '@oxfmt/binding-win32-ia32-msvc@0.32.0': 307 + resolution: {integrity: sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ==} 308 + engines: {node: ^20.19.0 || >=22.12.0} 309 + cpu: [ia32] 310 + os: [win32] 311 + 312 + '@oxfmt/binding-win32-x64-msvc@0.32.0': 313 + resolution: {integrity: sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg==} 314 + engines: {node: ^20.19.0 || >=22.12.0} 315 + cpu: [x64] 316 + os: [win32] 317 + 201 318 '@rollup/rollup-android-arm-eabi@4.57.1': 202 319 resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} 203 320 cpu: [arm] ··· 467 584 obug@2.1.1: 468 585 resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 469 586 587 + oxfmt@0.32.0: 588 + resolution: {integrity: sha512-KArQhGzt/Y8M1eSAX98Y8DLtGYYDQhkR55THUPY5VNcpFQ+9nRZkL3ULXhagHMD2hIvjy8JSeEQEP5/yYJSrLA==} 589 + engines: {node: ^20.19.0 || >=22.12.0} 590 + hasBin: true 591 + 470 592 parse-ms@4.0.0: 471 593 resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} 472 594 engines: {node: '>=18'} ··· 547 669 tinyglobby@0.2.15: 548 670 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 549 671 engines: {node: '>=12.0.0'} 672 + 673 + tinypool@2.1.0: 674 + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} 675 + engines: {node: ^20.0.0 || >=22.0.0} 550 676 551 677 tinyrainbow@3.0.3: 552 678 resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} ··· 750 876 751 877 '@jridgewell/sourcemap-codec@1.5.5': {} 752 878 879 + '@oxfmt/binding-android-arm-eabi@0.32.0': 880 + optional: true 881 + 882 + '@oxfmt/binding-android-arm64@0.32.0': 883 + optional: true 884 + 885 + '@oxfmt/binding-darwin-arm64@0.32.0': 886 + optional: true 887 + 888 + '@oxfmt/binding-darwin-x64@0.32.0': 889 + optional: true 890 + 891 + '@oxfmt/binding-freebsd-x64@0.32.0': 892 + optional: true 893 + 894 + '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': 895 + optional: true 896 + 897 + '@oxfmt/binding-linux-arm-musleabihf@0.32.0': 898 + optional: true 899 + 900 + '@oxfmt/binding-linux-arm64-gnu@0.32.0': 901 + optional: true 902 + 903 + '@oxfmt/binding-linux-arm64-musl@0.32.0': 904 + optional: true 905 + 906 + '@oxfmt/binding-linux-ppc64-gnu@0.32.0': 907 + optional: true 908 + 909 + '@oxfmt/binding-linux-riscv64-gnu@0.32.0': 910 + optional: true 911 + 912 + '@oxfmt/binding-linux-riscv64-musl@0.32.0': 913 + optional: true 914 + 915 + '@oxfmt/binding-linux-s390x-gnu@0.32.0': 916 + optional: true 917 + 918 + '@oxfmt/binding-linux-x64-gnu@0.32.0': 919 + optional: true 920 + 921 + '@oxfmt/binding-linux-x64-musl@0.32.0': 922 + optional: true 923 + 924 + '@oxfmt/binding-openharmony-arm64@0.32.0': 925 + optional: true 926 + 927 + '@oxfmt/binding-win32-arm64-msvc@0.32.0': 928 + optional: true 929 + 930 + '@oxfmt/binding-win32-ia32-msvc@0.32.0': 931 + optional: true 932 + 933 + '@oxfmt/binding-win32-x64-msvc@0.32.0': 934 + optional: true 935 + 753 936 '@rollup/rollup-android-arm-eabi@4.57.1': 754 937 optional: true 755 938 ··· 990 1173 991 1174 obug@2.1.1: {} 992 1175 1176 + oxfmt@0.32.0: 1177 + dependencies: 1178 + tinypool: 2.1.0 1179 + optionalDependencies: 1180 + '@oxfmt/binding-android-arm-eabi': 0.32.0 1181 + '@oxfmt/binding-android-arm64': 0.32.0 1182 + '@oxfmt/binding-darwin-arm64': 0.32.0 1183 + '@oxfmt/binding-darwin-x64': 0.32.0 1184 + '@oxfmt/binding-freebsd-x64': 0.32.0 1185 + '@oxfmt/binding-linux-arm-gnueabihf': 0.32.0 1186 + '@oxfmt/binding-linux-arm-musleabihf': 0.32.0 1187 + '@oxfmt/binding-linux-arm64-gnu': 0.32.0 1188 + '@oxfmt/binding-linux-arm64-musl': 0.32.0 1189 + '@oxfmt/binding-linux-ppc64-gnu': 0.32.0 1190 + '@oxfmt/binding-linux-riscv64-gnu': 0.32.0 1191 + '@oxfmt/binding-linux-riscv64-musl': 0.32.0 1192 + '@oxfmt/binding-linux-s390x-gnu': 0.32.0 1193 + '@oxfmt/binding-linux-x64-gnu': 0.32.0 1194 + '@oxfmt/binding-linux-x64-musl': 0.32.0 1195 + '@oxfmt/binding-openharmony-arm64': 0.32.0 1196 + '@oxfmt/binding-win32-arm64-msvc': 0.32.0 1197 + '@oxfmt/binding-win32-ia32-msvc': 0.32.0 1198 + '@oxfmt/binding-win32-x64-msvc': 0.32.0 1199 + 993 1200 parse-ms@4.0.0: {} 994 1201 995 1202 path-key@3.1.1: {} ··· 1073 1280 dependencies: 1074 1281 fdir: 6.5.0(picomatch@4.0.3) 1075 1282 picomatch: 4.0.3 1283 + 1284 + tinypool@2.1.0: {} 1076 1285 1077 1286 tinyrainbow@3.0.3: {} 1078 1287
+2
pnpm-workspace.yaml
··· 1 + onlyBuiltDependencies: 2 + - esbuild
+66 -47
src/commands/create.ts
··· 1 - import { intro, outro, text, select, confirm, spinner, isCancel, cancel } from '@clack/prompts'; 2 - import type { CommandDef } from 'citty'; 3 - import { generateProject } from '../utils/generate.js'; 4 - import { FRONTEND_LIBRARIES, TEMPLATES } from "../utils/constants.js" 1 + import type { CommandDef } from "citty"; 2 + 3 + import { 4 + intro, 5 + outro, 6 + text, 7 + select, 8 + confirm, 9 + spinner, 10 + isCancel, 11 + cancel, 12 + } from "@clack/prompts"; 13 + 14 + import { FRONTEND_LIBRARIES, TEMPLATES } from "../utils/constants.js"; 15 + import { generateProject } from "../utils/generate.js"; 5 16 6 17 export const createCommand: CommandDef = { 7 18 meta: { 8 - name: 'init', 9 - description: 'Create a new ATProto project', 19 + name: "init", 20 + description: "Create a new ATProto project", 10 21 }, 11 22 args: { 12 23 name: { 13 - type: 'positional', 14 - description: 'Project name', 24 + type: "positional", 25 + description: "Project name", 15 26 required: false, 16 27 }, 17 28 template: { 18 - type: 'string', 19 - description: 'Template to use', 20 - alias: 't', 29 + type: "string", 30 + description: "Template to use", 31 + alias: "t", 21 32 }, 22 33 library: { 23 34 type: "string", 24 35 description: "Which frontend library to use", 25 - alias: "t" 36 + alias: "t", 26 37 }, 27 38 framework: { 28 39 type: "string", 29 40 description: "Which frontend framework to use (if using SSR)", 30 - alias: "f" 31 - } 41 + alias: "f", 42 + }, 32 43 }, 33 44 async run({ args }) { 34 45 intro(`create-atproto-app`); 35 46 36 - const projectName = args.name ?? await text({ 37 - message: 'Please provide a name for this project:', 38 - placeholder: 'my-atproto-app', 39 - validate: (value) => { 40 - if (!value || !value.trim()) return 'Project name is required'; 41 - if (!/^[a-z0-9-]+$/.test(value)) { 42 - return 'Use only lowercase letters, numbers, and hyphens'; 43 - } 44 - }, 45 - }); 47 + const projectName = 48 + args.name ?? 49 + (await text({ 50 + message: "Please provide a name for this project:", 51 + placeholder: "my-atproto-app", 52 + validate: (value) => { 53 + if (!value || !value.trim()) return "Project name is required"; 54 + if (!/^[a-z0-9-]+$/.test(value)) { 55 + return "Use only lowercase letters, numbers, and hyphens"; 56 + } 57 + }, 58 + })); 46 59 47 60 if (isCancel(projectName)) { 48 - cancel('Operation cancelled'); 61 + cancel("Operation cancelled"); 49 62 return; 50 63 } 51 64 52 - const template = args.template ?? await select({ 53 - message: 'Select the type of app you would like to create:', 54 - options: TEMPLATES, 55 - }); 65 + const template = 66 + args.template ?? 67 + (await select({ 68 + message: "Select the type of app you would like to create:", 69 + options: TEMPLATES, 70 + })); 56 71 57 72 if (isCancel(template)) { 58 - cancel('Operation cancelled'); 73 + cancel("Operation cancelled"); 59 74 return; 60 75 } 61 76 62 77 let clientSideRenderedOption; 63 - if (template === 'web-application-csr-no-auth') { 64 - clientSideRenderedOption = args.library ?? await select({ 65 - message: "Select a frontend library", 66 - options: FRONTEND_LIBRARIES 67 - }) 78 + if (template === "web-application-csr-no-auth") { 79 + clientSideRenderedOption = 80 + args.library ?? 81 + (await select({ 82 + message: "Select a frontend library", 83 + options: FRONTEND_LIBRARIES, 84 + })); 68 85 69 86 if (isCancel(clientSideRenderedOption)) { 70 - cancel('Operation cancelled'); 87 + cancel("Operation cancelled"); 71 88 return; 72 89 } 73 90 } 74 91 75 - if (template === 'web-application-csr-with-auth') { 76 - clientSideRenderedOption = args.library ?? await select({ 77 - message: "Select a frontend library", 78 - options: FRONTEND_LIBRARIES 79 - }) 92 + if (template === "web-application-csr-with-auth") { 93 + clientSideRenderedOption = 94 + args.library ?? 95 + (await select({ 96 + message: "Select a frontend library", 97 + options: FRONTEND_LIBRARIES, 98 + })); 80 99 81 100 if (isCancel(clientSideRenderedOption)) { 82 - cancel('Operation cancelled'); 101 + cancel("Operation cancelled"); 83 102 return; 84 103 } 85 104 } 86 105 87 106 const useTypeScript = await confirm({ 88 - message: 'Use TypeScript?', 107 + message: "Use TypeScript?", 89 108 initialValue: true, 90 109 }); 91 110 92 111 if (isCancel(useTypeScript)) { 93 - cancel('Operation cancelled'); 112 + cancel("Operation cancelled"); 94 113 return; 95 114 } 96 115 97 116 const s = spinner(); 98 - s.start('Scaffolding project...'); 117 + s.start("Scaffolding project..."); 99 118 100 119 try { 101 120 await generateProject({ ··· 103 122 template: template, 104 123 typescript: useTypeScript, 105 124 }); 106 - s.stop('Project created!'); 125 + s.stop("Project created!"); 107 126 } catch (err) { 108 - s.stop('Failed to create project'); 127 + s.stop("Failed to create project"); 109 128 console.error(err); 110 129 return; 111 130 }
+6 -5
src/index.ts
··· 1 - import { defineCommand, runMain } from 'citty'; 2 - import { createCommand } from './commands/create.js'; 3 - import { version } from "../package.json" with { type: "json" } 1 + import { defineCommand, runMain } from "citty"; 2 + 3 + import { version } from "../package.json" with { type: "json" }; 4 + import { createCommand } from "./commands/create.js"; 4 5 5 6 const main = defineCommand({ 6 7 meta: { 7 - name: 'create-atproto-app', 8 + name: "create-atproto-app", 8 9 version, 9 - description: 'Bootstrap a new ATProto project', 10 + description: "Bootstrap a new ATProto project", 10 11 }, 11 12 subCommands: { 12 13 create: createCommand,
+1 -1
src/templates/web-application-csr-no-auth/src/app.ts
··· 1 1 export function createApp() { 2 - console.log('Hello ATProto!'); 2 + console.log("Hello ATProto!"); 3 3 }
+1 -1
src/templates/web-application-csr-no-auth/src/index.ts
··· 1 - import { createApp } from './app.js'; 1 + import { createApp } from "./app.js"; 2 2 3 3 createApp();
+12 -12
src/utils/constants.ts
··· 1 1 export const TEMPLATES = [ 2 2 { 3 - value: 'web-application-csr-no-auth', 4 - label: 'Web Application (CSR + No OAuth)', 5 - hint: 'Minimal, Client Side Rendered app' 3 + value: "web-application-csr-no-auth", 4 + label: "Web Application (CSR + No OAuth)", 5 + hint: "Minimal, Client Side Rendered app", 6 6 }, 7 7 { 8 - value: 'web-application-csr-with-auth', 9 - label: 'Web Application (CSR + OAuth)', 10 - hint: 'Minimal, Client Side Rendered app with OAuth configured' 8 + value: "web-application-csr-with-auth", 9 + label: "Web Application (CSR + OAuth)", 10 + hint: "Minimal, Client Side Rendered app with OAuth configured", 11 11 }, 12 12 // { 13 13 // value: 'web-application-ssr-with-auth', 14 14 // label: 'Web Application (SSR + OAuth)', 15 15 // hint: 'Minimal, Server Side Rendered app that has OAuth configured' 16 16 // }, 17 - ] 17 + ]; 18 18 19 19 export const FRONTEND_LIBRARIES = [ 20 20 // { ··· 30 30 // label: "https://www.solidjs.com" 31 31 // }, 32 32 { 33 - value: 'react', 33 + value: "react", 34 34 label: "React", 35 - hint: "https://react.dev" 35 + hint: "https://react.dev", 36 36 }, 37 37 // { 38 38 // value: "Tanstack Router", 39 39 // label: "https://tanstack.com/router/latest" 40 40 // } 41 - ] 41 + ]; 42 42 43 43 export const FRONTEND_FRAMEWORKS = [ 44 44 // { ··· 55 55 // }, 56 56 { 57 57 value: "React Router", 58 - label: "https://reactrouter.com/start/framework/installation" 58 + label: "https://reactrouter.com/start/framework/installation", 59 59 }, 60 60 // { 61 61 // value: "Tanstack Start", 62 62 // label: "https://tanstack.com/start/latest" 63 63 // } 64 - ] 64 + ];
+11 -11
src/utils/generate.ts
··· 1 - import { cp, mkdir, writeFile, readFile } from 'node:fs/promises'; 2 - import { join, dirname } from 'node:path'; 3 - import { fileURLToPath } from 'node:url'; 1 + import { cp, mkdir, writeFile, readFile } from "node:fs/promises"; 2 + import { join, dirname } from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 4 5 5 const __dirname = dirname(fileURLToPath(import.meta.url)); 6 6 7 7 export interface GenerateOptions { 8 8 name: string; 9 - template: 'web-application-csr-no-auth' | 'web-application-csr-with-auth'; 9 + template: "web-application-csr-no-auth" | "web-application-csr-with-auth"; 10 10 typescript: boolean; 11 11 } 12 12 13 13 export async function generateProject(opts: GenerateOptions): Promise<void> { 14 14 const targetDir = join(process.cwd(), opts.name); 15 - const templateDir = join(__dirname, '../templates', opts.template); 15 + const templateDir = join(__dirname, "../templates", opts.template); 16 16 17 17 await mkdir(targetDir, { recursive: true }); 18 18 19 19 await cp(templateDir, targetDir, { recursive: true }); 20 20 21 - const packageJsonPath = join(targetDir, 'package.json'); 22 - const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); 21 + const packageJsonPath = join(targetDir, "package.json"); 22 + const packageJsonContent = await readFile(packageJsonPath, "utf-8"); 23 23 const packageJson = JSON.parse(packageJsonContent); 24 24 packageJson.name = opts.name; 25 25 await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); 26 26 27 27 if (!opts.typescript) { 28 - const jsDir = join(targetDir, 'src'); 29 - const tsFiles = ['index.ts', 'app.ts']; 28 + const jsDir = join(targetDir, "src"); 29 + const tsFiles = ["index.ts", "app.ts"]; 30 30 for (const file of tsFiles) { 31 31 const tsPath = join(jsDir, file); 32 - const jsPath = join(jsDir, file.replace('.ts', '.js')); 32 + const jsPath = join(jsDir, file.replace(".ts", ".js")); 33 33 try { 34 - const content = await readFile(tsPath, 'utf-8'); 34 + const content = await readFile(tsPath, "utf-8"); 35 35 await writeFile(jsPath, content); 36 36 await cp(jsPath, tsPath); 37 37 } catch {}
+34 -29
tests/generate.test.ts
··· 1 - import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 - import { generateProject } from '../src/utils/generate.js'; 3 - import { readFile, rm } from 'node:fs/promises'; 4 - import { join } from 'node:path'; 5 - import { existsSync } from 'node:fs'; 1 + import { existsSync } from "node:fs"; 2 + import { readFile, rm } from "node:fs/promises"; 3 + import { join } from "node:path"; 4 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 5 + 6 + import { generateProject } from "../src/utils/generate.js"; 6 7 7 - const TEST_PROJECTS = ['test-project', 'my-custom-app', 'js-project']; 8 + const TEST_PROJECTS = ["test-project", "my-custom-app", "js-project"]; 8 9 9 - describe('generateProject', () => { 10 + describe("generateProject", () => { 10 11 beforeEach(async () => { 11 12 for (const proj of TEST_PROJECTS) { 12 13 if (existsSync(proj)) { ··· 23 24 } 24 25 }); 25 26 26 - it('creates project directory', async () => { 27 + it("creates project directory", async () => { 27 28 await generateProject({ 28 - name: 'test-project', 29 - template: 'web-application-csr-no-auth', 29 + name: "test-project", 30 + template: "web-application-csr-no-auth", 30 31 typescript: true, 31 32 }); 32 33 33 - expect(existsSync(join(process.cwd(), 'test-project'))).toBe(true); 34 + expect(existsSync(join(process.cwd(), "test-project"))).toBe(true); 34 35 }); 35 36 36 - it('copies template files to project', async () => { 37 + it("copies template files to project", async () => { 37 38 await generateProject({ 38 - name: 'test-project', 39 - template: 'web-application-csr-no-auth', 39 + name: "test-project", 40 + template: "web-application-csr-no-auth", 40 41 typescript: true, 41 42 }); 42 43 43 - const projectDir = join(process.cwd(), 'test-project'); 44 - expect(existsSync(join(projectDir, 'package.json'))).toBe(true); 45 - expect(existsSync(join(projectDir, 'src', 'index.ts'))).toBe(true); 44 + const projectDir = join(process.cwd(), "test-project"); 45 + expect(existsSync(join(projectDir, "package.json"))).toBe(true); 46 + expect(existsSync(join(projectDir, "src", "index.ts"))).toBe(true); 46 47 }); 47 48 48 - it('updates package.json name', async () => { 49 + it("updates package.json name", async () => { 49 50 await generateProject({ 50 - name: 'my-custom-app', 51 - template: 'web-application-csr-no-auth', 51 + name: "my-custom-app", 52 + template: "web-application-csr-no-auth", 52 53 typescript: true, 53 54 }); 54 55 55 - const packageJsonPath = join(process.cwd(), 'my-custom-app', 'package.json'); 56 - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); 57 - expect(packageJson.name).toBe('my-custom-app'); 56 + const packageJsonPath = join( 57 + process.cwd(), 58 + "my-custom-app", 59 + "package.json", 60 + ); 61 + const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); 62 + expect(packageJson.name).toBe("my-custom-app"); 58 63 }); 59 64 60 - it('creates JS files when typescript is false', async () => { 65 + it("creates JS files when typescript is false", async () => { 61 66 await generateProject({ 62 - name: 'js-project', 63 - template: 'web-application-csr-no-auth', 67 + name: "js-project", 68 + template: "web-application-csr-no-auth", 64 69 typescript: false, 65 70 }); 66 71 67 - const projectDir = join(process.cwd(), 'js-project'); 68 - expect(existsSync(join(projectDir, 'src', 'index.js'))).toBe(true); 69 - expect(existsSync(join(projectDir, 'src', 'app.js'))).toBe(true); 72 + const projectDir = join(process.cwd(), "js-project"); 73 + expect(existsSync(join(projectDir, "src", "index.js"))).toBe(true); 74 + expect(existsSync(join(projectDir, "src", "app.js"))).toBe(true); 70 75 }); 71 76 });