create-better-t-stack 2.21.1 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1388 -1092
- package/package.json +5 -4
- /package/templates/base/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/nuxt/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/react/react-router/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/react/tanstack-router/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/react/tanstack-start/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/solid/{package.json → package.json.hbs} +0 -0
- /package/templates/frontend/svelte/{package.json → package.json.hbs} +0 -0
package/dist/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import fs from "fs-extra";
|
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { createCli, trpcServer, zod } from "trpc-cli";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import * as JSONC from "jsonc-parser";
|
|
9
10
|
import { $, execa } from "execa";
|
|
10
|
-
import os from "node:os";
|
|
11
11
|
import { globby } from "globby";
|
|
12
12
|
import handlebars from "handlebars";
|
|
13
|
+
import os from "node:os";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import { PostHog } from "posthog-node";
|
|
15
16
|
import gradient from "gradient-string";
|
|
@@ -102,6 +103,143 @@ const dependencyVersionMap = {
|
|
|
102
103
|
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
103
104
|
wrangler: "^4.20.0"
|
|
104
105
|
};
|
|
106
|
+
const ADDON_COMPATIBILITY = {
|
|
107
|
+
pwa: [
|
|
108
|
+
"tanstack-router",
|
|
109
|
+
"react-router",
|
|
110
|
+
"solid"
|
|
111
|
+
],
|
|
112
|
+
tauri: [
|
|
113
|
+
"tanstack-router",
|
|
114
|
+
"react-router",
|
|
115
|
+
"nuxt",
|
|
116
|
+
"svelte",
|
|
117
|
+
"solid"
|
|
118
|
+
],
|
|
119
|
+
biome: [],
|
|
120
|
+
husky: [],
|
|
121
|
+
turborepo: [],
|
|
122
|
+
starlight: [],
|
|
123
|
+
none: []
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/utils/addon-compatibility.ts
|
|
128
|
+
function validateAddonCompatibility(addon, frontend) {
|
|
129
|
+
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
|
130
|
+
if (compatibleFrontends.length === 0) return { isCompatible: true };
|
|
131
|
+
const hasCompatibleFrontend = frontend.some((f) => compatibleFrontends.includes(f));
|
|
132
|
+
if (!hasCompatibleFrontend) {
|
|
133
|
+
const frontendList = compatibleFrontends.join(", ");
|
|
134
|
+
return {
|
|
135
|
+
isCompatible: false,
|
|
136
|
+
reason: `${addon} addon requires one of these frontends: ${frontendList}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { isCompatible: true };
|
|
140
|
+
}
|
|
141
|
+
function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
|
|
142
|
+
return allAddons.filter((addon) => {
|
|
143
|
+
if (existingAddons.includes(addon)) return false;
|
|
144
|
+
if (addon === "none") return false;
|
|
145
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontend);
|
|
146
|
+
return isCompatible;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/utils/get-latest-cli-version.ts
|
|
152
|
+
const getLatestCLIVersion = () => {
|
|
153
|
+
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
154
|
+
const packageJsonContent = fs.readJSONSync(packageJsonPath);
|
|
155
|
+
return packageJsonContent.version ?? "1.0.0";
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/utils/bts-config.ts
|
|
160
|
+
const BTS_CONFIG_FILE = "bts.jsonc";
|
|
161
|
+
async function writeBtsConfig(projectConfig) {
|
|
162
|
+
const btsConfig = {
|
|
163
|
+
version: getLatestCLIVersion(),
|
|
164
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
165
|
+
database: projectConfig.database,
|
|
166
|
+
orm: projectConfig.orm,
|
|
167
|
+
backend: projectConfig.backend,
|
|
168
|
+
runtime: projectConfig.runtime,
|
|
169
|
+
frontend: projectConfig.frontend,
|
|
170
|
+
addons: projectConfig.addons,
|
|
171
|
+
examples: projectConfig.examples,
|
|
172
|
+
auth: projectConfig.auth,
|
|
173
|
+
packageManager: projectConfig.packageManager,
|
|
174
|
+
dbSetup: projectConfig.dbSetup,
|
|
175
|
+
api: projectConfig.api
|
|
176
|
+
};
|
|
177
|
+
const baseContent = {
|
|
178
|
+
$schema: "https://better-t-stack.dev/schema.json",
|
|
179
|
+
version: btsConfig.version,
|
|
180
|
+
createdAt: btsConfig.createdAt,
|
|
181
|
+
database: btsConfig.database,
|
|
182
|
+
orm: btsConfig.orm,
|
|
183
|
+
backend: btsConfig.backend,
|
|
184
|
+
runtime: btsConfig.runtime,
|
|
185
|
+
frontend: btsConfig.frontend,
|
|
186
|
+
addons: btsConfig.addons,
|
|
187
|
+
examples: btsConfig.examples,
|
|
188
|
+
auth: btsConfig.auth,
|
|
189
|
+
packageManager: btsConfig.packageManager,
|
|
190
|
+
dbSetup: btsConfig.dbSetup,
|
|
191
|
+
api: btsConfig.api
|
|
192
|
+
};
|
|
193
|
+
let configContent = JSON.stringify(baseContent);
|
|
194
|
+
const formatResult = JSONC.format(configContent, void 0, {
|
|
195
|
+
tabSize: 2,
|
|
196
|
+
insertSpaces: true,
|
|
197
|
+
eol: "\n"
|
|
198
|
+
});
|
|
199
|
+
configContent = JSONC.applyEdits(configContent, formatResult);
|
|
200
|
+
const finalContent = `// Better-T-Stack configuration file
|
|
201
|
+
// safe to delete
|
|
202
|
+
|
|
203
|
+
${configContent}`;
|
|
204
|
+
const configPath = path.join(projectConfig.projectDir, BTS_CONFIG_FILE);
|
|
205
|
+
await fs.writeFile(configPath, finalContent, "utf-8");
|
|
206
|
+
}
|
|
207
|
+
async function readBtsConfig(projectDir) {
|
|
208
|
+
try {
|
|
209
|
+
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
|
210
|
+
if (!await fs.pathExists(configPath)) return null;
|
|
211
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
212
|
+
const errors = [];
|
|
213
|
+
const config = JSONC.parse(configContent, errors, {
|
|
214
|
+
allowTrailingComma: true,
|
|
215
|
+
disallowComments: false
|
|
216
|
+
});
|
|
217
|
+
if (errors.length > 0) {
|
|
218
|
+
console.warn("Warning: Found errors parsing bts.jsonc:", errors);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return config;
|
|
222
|
+
} catch (_error) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function updateBtsConfig(projectDir, updates) {
|
|
227
|
+
try {
|
|
228
|
+
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
|
229
|
+
if (!await fs.pathExists(configPath)) return;
|
|
230
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
231
|
+
let modifiedContent = configContent;
|
|
232
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
233
|
+
const editResult = JSONC.modify(modifiedContent, [key], value, { formattingOptions: {
|
|
234
|
+
tabSize: 2,
|
|
235
|
+
insertSpaces: true,
|
|
236
|
+
eol: "\n"
|
|
237
|
+
} });
|
|
238
|
+
modifiedContent = JSONC.applyEdits(modifiedContent, editResult);
|
|
239
|
+
}
|
|
240
|
+
await fs.writeFile(configPath, modifiedContent, "utf-8");
|
|
241
|
+
} catch (_error) {}
|
|
242
|
+
}
|
|
105
243
|
|
|
106
244
|
//#endregion
|
|
107
245
|
//#region src/utils/add-package-deps.ts
|
|
@@ -232,17 +370,26 @@ async function setupTauri(config) {
|
|
|
232
370
|
|
|
233
371
|
//#endregion
|
|
234
372
|
//#region src/helpers/setup/addons-setup.ts
|
|
235
|
-
async function setupAddons(config) {
|
|
373
|
+
async function setupAddons(config, isAddCommand = false) {
|
|
236
374
|
const { addons, frontend, projectDir } = config;
|
|
237
375
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
238
376
|
const hasNuxtFrontend = frontend.includes("nuxt");
|
|
239
377
|
const hasSvelteFrontend = frontend.includes("svelte");
|
|
240
378
|
const hasSolidFrontend = frontend.includes("solid");
|
|
241
379
|
const hasNextFrontend = frontend.includes("next");
|
|
242
|
-
if (addons.includes("turborepo"))
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
380
|
+
if (addons.includes("turborepo")) {
|
|
381
|
+
await addPackageDependency({
|
|
382
|
+
devDependencies: ["turbo"],
|
|
383
|
+
projectDir
|
|
384
|
+
});
|
|
385
|
+
if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
|
|
386
|
+
|
|
387
|
+
${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
|
|
388
|
+
${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
|
|
389
|
+
|
|
390
|
+
${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
|
391
|
+
`);
|
|
392
|
+
}
|
|
246
393
|
if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
|
|
247
394
|
if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
|
|
248
395
|
if (addons.includes("biome")) await setupBiome(projectDir);
|
|
@@ -316,453 +463,922 @@ async function setupPwa(projectDir, frontends) {
|
|
|
316
463
|
}
|
|
317
464
|
|
|
318
465
|
//#endregion
|
|
319
|
-
//#region src/helpers/
|
|
320
|
-
async function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
466
|
+
//#region src/helpers/project-generation/detect-project-config.ts
|
|
467
|
+
async function detectProjectConfig(projectDir) {
|
|
468
|
+
try {
|
|
469
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
470
|
+
if (btsConfig) return {
|
|
471
|
+
projectDir,
|
|
472
|
+
projectName: path.basename(projectDir),
|
|
473
|
+
database: btsConfig.database,
|
|
474
|
+
orm: btsConfig.orm,
|
|
475
|
+
backend: btsConfig.backend,
|
|
476
|
+
runtime: btsConfig.runtime,
|
|
477
|
+
frontend: btsConfig.frontend,
|
|
478
|
+
addons: btsConfig.addons,
|
|
479
|
+
examples: btsConfig.examples,
|
|
480
|
+
auth: btsConfig.auth,
|
|
481
|
+
packageManager: btsConfig.packageManager,
|
|
482
|
+
dbSetup: btsConfig.dbSetup,
|
|
483
|
+
api: btsConfig.api
|
|
484
|
+
};
|
|
485
|
+
return null;
|
|
486
|
+
} catch (_error) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function isBetterTStackProject(projectDir) {
|
|
491
|
+
try {
|
|
492
|
+
return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
|
|
493
|
+
} catch (_error) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/helpers/project-generation/install-dependencies.ts
|
|
500
|
+
async function installDependencies({ projectDir, packageManager }) {
|
|
501
|
+
const s = spinner();
|
|
502
|
+
try {
|
|
503
|
+
s.start(`Running ${packageManager} install...`);
|
|
504
|
+
await $({
|
|
505
|
+
cwd: projectDir,
|
|
506
|
+
stderr: "inherit"
|
|
507
|
+
})`${packageManager} install`;
|
|
508
|
+
s.stop("Dependencies installed successfully");
|
|
509
|
+
} catch (error) {
|
|
510
|
+
s.stop(pc.red("Failed to install dependencies"));
|
|
511
|
+
if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/utils/template-processor.ts
|
|
517
|
+
/**
|
|
518
|
+
* Processes a Handlebars template file and writes the output to the destination.
|
|
519
|
+
* @param srcPath Path to the source .hbs template file.
|
|
520
|
+
* @param destPath Path to write the processed file.
|
|
521
|
+
* @param context Data to be passed to the Handlebars template.
|
|
522
|
+
*/
|
|
523
|
+
async function processTemplate(srcPath, destPath, context) {
|
|
524
|
+
try {
|
|
525
|
+
const templateContent = await fs.readFile(srcPath, "utf-8");
|
|
526
|
+
const template = handlebars.compile(templateContent);
|
|
527
|
+
const processedContent = template(context);
|
|
528
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
529
|
+
await fs.writeFile(destPath, processedContent);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
consola.error(`Error processing template ${srcPath}:`, error);
|
|
532
|
+
throw new Error(`Failed to process template ${srcPath}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
handlebars.registerHelper("eq", (a, b) => a === b);
|
|
536
|
+
handlebars.registerHelper("and", (a, b) => a && b);
|
|
537
|
+
handlebars.registerHelper("or", (a, b) => a || b);
|
|
538
|
+
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
539
|
+
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/helpers/project-generation/template-manager.ts
|
|
542
|
+
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
|
|
543
|
+
const sourceFiles = await globby(sourcePattern, {
|
|
544
|
+
cwd: baseSourceDir,
|
|
545
|
+
dot: true,
|
|
546
|
+
onlyFiles: true,
|
|
547
|
+
absolute: false
|
|
548
|
+
});
|
|
549
|
+
for (const relativeSrcPath of sourceFiles) {
|
|
550
|
+
const srcPath = path.join(baseSourceDir, relativeSrcPath);
|
|
551
|
+
let relativeDestPath = relativeSrcPath;
|
|
552
|
+
if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
|
|
553
|
+
const basename = path.basename(relativeSrcPath);
|
|
554
|
+
if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore");
|
|
555
|
+
else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".npmrc");
|
|
556
|
+
const destPath = path.join(destDir, relativeDestPath);
|
|
557
|
+
try {
|
|
558
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
559
|
+
if (!overwrite && await fs.pathExists(destPath)) continue;
|
|
560
|
+
if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
|
|
561
|
+
else await fs.copy(srcPath, destPath, { overwrite: true });
|
|
562
|
+
} catch (_error) {}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function copyBaseTemplate(projectDir, context) {
|
|
566
|
+
const templateDir = path.join(PKG_ROOT, "templates/base");
|
|
567
|
+
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
|
568
|
+
await fs.ensureDir(path.join(projectDir, "packages"));
|
|
569
|
+
}
|
|
570
|
+
async function setupFrontendTemplates(projectDir, context) {
|
|
571
|
+
const hasReactWeb = context.frontend.some((f) => [
|
|
328
572
|
"tanstack-router",
|
|
329
573
|
"react-router",
|
|
330
574
|
"tanstack-start",
|
|
331
575
|
"next"
|
|
332
576
|
].includes(f));
|
|
333
|
-
const hasNuxtWeb = frontend.includes("nuxt");
|
|
334
|
-
const hasSvelteWeb = frontend.includes("svelte");
|
|
335
|
-
const hasSolidWeb = frontend.includes("solid");
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
577
|
+
const hasNuxtWeb = context.frontend.includes("nuxt");
|
|
578
|
+
const hasSvelteWeb = context.frontend.includes("svelte");
|
|
579
|
+
const hasSolidWeb = context.frontend.includes("solid");
|
|
580
|
+
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
581
|
+
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
582
|
+
const _hasNative = hasNativeWind || hasUnistyles;
|
|
583
|
+
const isConvex = context.backend === "convex";
|
|
584
|
+
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
|
|
585
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
586
|
+
await fs.ensureDir(webAppDir);
|
|
587
|
+
if (hasReactWeb) {
|
|
588
|
+
const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
|
|
589
|
+
if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
|
590
|
+
const reactFramework = context.frontend.find((f) => [
|
|
591
|
+
"tanstack-router",
|
|
592
|
+
"react-router",
|
|
593
|
+
"tanstack-start",
|
|
594
|
+
"next"
|
|
595
|
+
].includes(f));
|
|
596
|
+
if (reactFramework) {
|
|
597
|
+
const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
|
|
598
|
+
if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
|
|
599
|
+
if (!isConvex && context.api !== "none") {
|
|
600
|
+
const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
|
|
601
|
+
if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
|
|
602
|
+
}
|
|
357
603
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
if (api === "orpc") await addPackageDependency({
|
|
379
|
-
dependencies: [
|
|
380
|
-
"@orpc/tanstack-query",
|
|
381
|
-
"@orpc/client",
|
|
382
|
-
"@orpc/server"
|
|
383
|
-
],
|
|
384
|
-
projectDir: webDir
|
|
385
|
-
});
|
|
386
|
-
} else if (hasSvelteWeb) {
|
|
387
|
-
if (api === "orpc") await addPackageDependency({
|
|
388
|
-
dependencies: [
|
|
389
|
-
"@orpc/tanstack-query",
|
|
390
|
-
"@orpc/client",
|
|
391
|
-
"@orpc/server",
|
|
392
|
-
"@tanstack/svelte-query"
|
|
393
|
-
],
|
|
394
|
-
projectDir: webDir
|
|
395
|
-
});
|
|
396
|
-
} else if (hasSolidWeb) {
|
|
397
|
-
if (api === "orpc") await addPackageDependency({
|
|
398
|
-
dependencies: [
|
|
399
|
-
"@orpc/tanstack-query",
|
|
400
|
-
"@orpc/client",
|
|
401
|
-
"@orpc/server",
|
|
402
|
-
"@tanstack/solid-query"
|
|
403
|
-
],
|
|
404
|
-
projectDir: webDir
|
|
405
|
-
});
|
|
604
|
+
} else if (hasNuxtWeb) {
|
|
605
|
+
const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
|
|
606
|
+
if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
|
|
607
|
+
if (!isConvex && context.api === "orpc") {
|
|
608
|
+
const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
|
|
609
|
+
if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
|
|
610
|
+
}
|
|
611
|
+
} else if (hasSvelteWeb) {
|
|
612
|
+
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
|
|
613
|
+
if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
|
|
614
|
+
if (!isConvex && context.api === "orpc") {
|
|
615
|
+
const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
|
|
616
|
+
if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
|
|
617
|
+
}
|
|
618
|
+
} else if (hasSolidWeb) {
|
|
619
|
+
const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
|
|
620
|
+
if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
|
|
621
|
+
if (!isConvex && context.api === "orpc") {
|
|
622
|
+
const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
|
|
623
|
+
if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
|
|
406
624
|
}
|
|
407
|
-
}
|
|
408
|
-
if (nativeDirExists) {
|
|
409
|
-
if (api === "trpc") await addPackageDependency({
|
|
410
|
-
dependencies: [
|
|
411
|
-
"@trpc/tanstack-react-query",
|
|
412
|
-
"@trpc/client",
|
|
413
|
-
"@trpc/server"
|
|
414
|
-
],
|
|
415
|
-
projectDir: nativeDir
|
|
416
|
-
});
|
|
417
|
-
else if (api === "orpc") await addPackageDependency({
|
|
418
|
-
dependencies: [
|
|
419
|
-
"@orpc/tanstack-query",
|
|
420
|
-
"@orpc/client",
|
|
421
|
-
"@orpc/server"
|
|
422
|
-
],
|
|
423
|
-
projectDir: nativeDir
|
|
424
|
-
});
|
|
425
625
|
}
|
|
426
626
|
}
|
|
427
|
-
|
|
428
|
-
"
|
|
627
|
+
if (hasNativeWind || hasUnistyles) {
|
|
628
|
+
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
629
|
+
await fs.ensureDir(nativeAppDir);
|
|
630
|
+
const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
|
|
631
|
+
if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
|
|
632
|
+
let nativeFrameworkPath = "";
|
|
633
|
+
if (hasNativeWind) nativeFrameworkPath = "nativewind";
|
|
634
|
+
else if (hasUnistyles) nativeFrameworkPath = "unistyles";
|
|
635
|
+
const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
|
|
636
|
+
if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
|
|
637
|
+
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
|
|
638
|
+
const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
|
|
639
|
+
if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async function setupBackendFramework(projectDir, context) {
|
|
644
|
+
if (context.backend === "none") return;
|
|
645
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
646
|
+
if (context.backend === "convex") {
|
|
647
|
+
if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
|
|
648
|
+
const convexBackendDestDir = path.join(projectDir, "packages/backend");
|
|
649
|
+
const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
|
|
650
|
+
await fs.ensureDir(convexBackendDestDir);
|
|
651
|
+
if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
await fs.ensureDir(serverAppDir);
|
|
655
|
+
const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
|
|
656
|
+
if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
|
|
657
|
+
const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
|
|
658
|
+
if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
|
|
659
|
+
if (context.api !== "none") {
|
|
660
|
+
const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
|
|
661
|
+
if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
|
|
662
|
+
const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
|
|
663
|
+
if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function setupDbOrmTemplates(projectDir, context) {
|
|
667
|
+
if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
|
|
668
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
669
|
+
await fs.ensureDir(serverAppDir);
|
|
670
|
+
const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
|
|
671
|
+
if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
|
|
672
|
+
}
|
|
673
|
+
async function setupAuthTemplate(projectDir, context) {
|
|
674
|
+
if (context.backend === "convex" || !context.auth) return;
|
|
675
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
676
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
677
|
+
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
678
|
+
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
|
679
|
+
const webAppDirExists = await fs.pathExists(webAppDir);
|
|
680
|
+
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
|
681
|
+
const hasReactWeb = context.frontend.some((f) => [
|
|
429
682
|
"tanstack-router",
|
|
683
|
+
"react-router",
|
|
430
684
|
"tanstack-start",
|
|
431
|
-
"next"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
const
|
|
444
|
-
if (await fs.pathExists(
|
|
445
|
-
await addPackageDependency({
|
|
446
|
-
dependencies: reactQueryDeps,
|
|
447
|
-
devDependencies: reactQueryDevDeps,
|
|
448
|
-
projectDir: webDir
|
|
449
|
-
});
|
|
450
|
-
} catch (_error) {}
|
|
685
|
+
"next"
|
|
686
|
+
].includes(f));
|
|
687
|
+
const hasNuxtWeb = context.frontend.includes("nuxt");
|
|
688
|
+
const hasSvelteWeb = context.frontend.includes("svelte");
|
|
689
|
+
const hasSolidWeb = context.frontend.includes("solid");
|
|
690
|
+
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
691
|
+
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
692
|
+
const hasNative = hasNativeWind || hasUnistyles;
|
|
693
|
+
if (serverAppDirExists) {
|
|
694
|
+
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
|
|
695
|
+
if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
|
|
696
|
+
if (context.backend === "next") {
|
|
697
|
+
const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
|
|
698
|
+
if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
|
|
451
699
|
}
|
|
452
|
-
if (
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
700
|
+
if (context.orm !== "none" && context.database !== "none") {
|
|
701
|
+
const orm = context.orm;
|
|
702
|
+
const db = context.database;
|
|
703
|
+
let authDbSrc = "";
|
|
704
|
+
if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/drizzle/${db}`);
|
|
705
|
+
else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/prisma/${db}`);
|
|
706
|
+
else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/mongoose/${db}`);
|
|
707
|
+
if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
|
708
|
+
else if (authDbSrc) {}
|
|
460
709
|
}
|
|
461
710
|
}
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
711
|
+
if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
|
|
712
|
+
if (hasReactWeb) {
|
|
713
|
+
const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/react/base");
|
|
714
|
+
if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
|
715
|
+
const reactFramework = context.frontend.find((f) => [
|
|
716
|
+
"tanstack-router",
|
|
717
|
+
"react-router",
|
|
718
|
+
"tanstack-start",
|
|
719
|
+
"next"
|
|
720
|
+
].includes(f));
|
|
721
|
+
if (reactFramework) {
|
|
722
|
+
const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/web/react/${reactFramework}`);
|
|
723
|
+
if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
|
|
724
|
+
}
|
|
725
|
+
} else if (hasNuxtWeb) {
|
|
726
|
+
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
|
|
727
|
+
if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
|
728
|
+
} else if (hasSvelteWeb) {
|
|
729
|
+
if (context.api === "orpc") {
|
|
730
|
+
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
|
|
731
|
+
if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
|
|
732
|
+
}
|
|
733
|
+
} else if (hasSolidWeb) {
|
|
734
|
+
if (context.api === "orpc") {
|
|
735
|
+
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
|
|
736
|
+
if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
|
|
737
|
+
}
|
|
474
738
|
}
|
|
475
739
|
}
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
projectDir: webDir
|
|
486
|
-
});
|
|
487
|
-
} catch (_error) {}
|
|
488
|
-
}
|
|
489
|
-
if (nativeDirExists) {
|
|
490
|
-
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
491
|
-
if (await fs.pathExists(nativePkgJsonPath)) try {
|
|
492
|
-
await addPackageDependency({
|
|
493
|
-
dependencies: ["convex"],
|
|
494
|
-
projectDir: nativeDir
|
|
495
|
-
});
|
|
496
|
-
} catch (_error) {}
|
|
497
|
-
}
|
|
498
|
-
const backendPackageName = `@${projectName}/backend`;
|
|
499
|
-
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
500
|
-
const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => {
|
|
501
|
-
try {
|
|
502
|
-
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
503
|
-
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
504
|
-
if (pkgJson.dependencies[depName] !== depVersion) {
|
|
505
|
-
pkgJson.dependencies[depName] = depVersion;
|
|
506
|
-
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
507
|
-
}
|
|
508
|
-
} catch (_error) {}
|
|
509
|
-
};
|
|
510
|
-
if (webDirExists) {
|
|
511
|
-
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
512
|
-
if (await fs.pathExists(webPkgJsonPath)) await addWorkspaceDepManually(webPkgJsonPath, backendPackageName, backendWorkspaceVersion);
|
|
740
|
+
if (hasNative && nativeAppDirExists) {
|
|
741
|
+
const authNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/native/native-base");
|
|
742
|
+
if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
|
|
743
|
+
let nativeFrameworkAuthPath = "";
|
|
744
|
+
if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
|
|
745
|
+
else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
|
|
746
|
+
if (nativeFrameworkAuthPath) {
|
|
747
|
+
const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/native/${nativeFrameworkAuthPath}`);
|
|
748
|
+
if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
|
|
513
749
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function setupAddonsTemplate(projectDir, context) {
|
|
753
|
+
if (!context.addons || context.addons.length === 0) return;
|
|
754
|
+
for (const addon of context.addons) {
|
|
755
|
+
if (addon === "none") continue;
|
|
756
|
+
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
|
757
|
+
let addonDestDir = projectDir;
|
|
758
|
+
if (addon === "pwa") {
|
|
759
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
760
|
+
if (!await fs.pathExists(webAppDir)) continue;
|
|
761
|
+
addonDestDir = webAppDir;
|
|
762
|
+
if (context.frontend.includes("next")) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
|
|
763
|
+
else if (context.frontend.some((f) => [
|
|
764
|
+
"tanstack-router",
|
|
765
|
+
"react-router",
|
|
766
|
+
"solid"
|
|
767
|
+
].includes(f))) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
|
|
768
|
+
else continue;
|
|
517
769
|
}
|
|
770
|
+
if (await fs.pathExists(addonSrcDir)) await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
|
|
518
771
|
}
|
|
519
772
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
773
|
+
async function setupExamplesTemplate(projectDir, context) {
|
|
774
|
+
if (!context.examples || context.examples.length === 0 || context.examples[0] === "none") return;
|
|
775
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
776
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
777
|
+
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
|
778
|
+
const webAppDirExists = await fs.pathExists(webAppDir);
|
|
779
|
+
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
780
|
+
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
|
781
|
+
const hasReactWeb = context.frontend.some((f) => [
|
|
782
|
+
"tanstack-router",
|
|
783
|
+
"react-router",
|
|
784
|
+
"tanstack-start",
|
|
785
|
+
"next"
|
|
786
|
+
].includes(f));
|
|
787
|
+
const hasNuxtWeb = context.frontend.includes("nuxt");
|
|
788
|
+
const hasSvelteWeb = context.frontend.includes("svelte");
|
|
789
|
+
const hasSolidWeb = context.frontend.includes("solid");
|
|
790
|
+
for (const example of context.examples) {
|
|
791
|
+
if (example === "none") continue;
|
|
792
|
+
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
|
|
793
|
+
if (serverAppDirExists && context.backend !== "convex" && context.backend !== "none") {
|
|
794
|
+
const exampleServerSrc = path.join(exampleBaseDir, "server");
|
|
795
|
+
if (example === "ai" && context.backend === "next") {
|
|
796
|
+
const aiNextServerSrc = path.join(exampleServerSrc, "next");
|
|
797
|
+
if (await fs.pathExists(aiNextServerSrc)) await processAndCopyFiles("**/*", aiNextServerSrc, serverAppDir, context, false);
|
|
798
|
+
}
|
|
799
|
+
if (context.orm !== "none" && context.database !== "none") {
|
|
800
|
+
const exampleOrmBaseSrc = path.join(exampleServerSrc, context.orm, "base");
|
|
801
|
+
if (await fs.pathExists(exampleOrmBaseSrc)) await processAndCopyFiles("**/*", exampleOrmBaseSrc, serverAppDir, context, false);
|
|
802
|
+
const exampleDbSchemaSrc = path.join(exampleServerSrc, context.orm, context.database);
|
|
803
|
+
if (await fs.pathExists(exampleDbSchemaSrc)) await processAndCopyFiles("**/*", exampleDbSchemaSrc, serverAppDir, context, false);
|
|
804
|
+
}
|
|
805
|
+
const ignorePatterns = [`${context.orm}/**`];
|
|
806
|
+
if (example === "ai" && context.backend === "next") ignorePatterns.push("next/**");
|
|
807
|
+
const generalServerFiles = await globby(["**/*.ts", "**/*.hbs"], {
|
|
808
|
+
cwd: exampleServerSrc,
|
|
809
|
+
onlyFiles: true,
|
|
810
|
+
deep: 1,
|
|
811
|
+
ignore: ignorePatterns
|
|
558
812
|
});
|
|
813
|
+
for (const file of generalServerFiles) {
|
|
814
|
+
const srcPath = path.join(exampleServerSrc, file);
|
|
815
|
+
const destPath = path.join(serverAppDir, file.replace(".hbs", ""));
|
|
816
|
+
try {
|
|
817
|
+
if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
|
|
818
|
+
else if (!await fs.pathExists(destPath)) await fs.copy(srcPath, destPath, { overwrite: false });
|
|
819
|
+
} catch (_error) {}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (webAppDirExists) {
|
|
823
|
+
if (hasReactWeb) {
|
|
824
|
+
const exampleWebSrc = path.join(exampleBaseDir, "web/react");
|
|
825
|
+
if (await fs.pathExists(exampleWebSrc)) {
|
|
826
|
+
const reactFramework = context.frontend.find((f) => [
|
|
827
|
+
"next",
|
|
828
|
+
"react-router",
|
|
829
|
+
"tanstack-router",
|
|
830
|
+
"tanstack-start"
|
|
831
|
+
].includes(f));
|
|
832
|
+
if (reactFramework) {
|
|
833
|
+
const exampleWebFrameworkSrc = path.join(exampleWebSrc, reactFramework);
|
|
834
|
+
if (await fs.pathExists(exampleWebFrameworkSrc)) await processAndCopyFiles("**/*", exampleWebFrameworkSrc, webAppDir, context, false);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} else if (hasNuxtWeb) {
|
|
838
|
+
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
|
|
839
|
+
if (await fs.pathExists(exampleWebNuxtSrc)) await processAndCopyFiles("**/*", exampleWebNuxtSrc, webAppDir, context, false);
|
|
840
|
+
} else if (hasSvelteWeb) {
|
|
841
|
+
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
|
|
842
|
+
if (await fs.pathExists(exampleWebSvelteSrc)) await processAndCopyFiles("**/*", exampleWebSvelteSrc, webAppDir, context, false);
|
|
843
|
+
} else if (hasSolidWeb) {
|
|
844
|
+
const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
|
|
845
|
+
if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (nativeAppDirExists) {
|
|
849
|
+
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
850
|
+
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
851
|
+
if (hasNativeWind || hasUnistyles) {
|
|
852
|
+
let nativeFramework = "";
|
|
853
|
+
if (hasNativeWind) nativeFramework = "nativewind";
|
|
854
|
+
else if (hasUnistyles) nativeFramework = "unistyles";
|
|
855
|
+
const exampleNativeSrc = path.join(exampleBaseDir, `native/${nativeFramework}`);
|
|
856
|
+
if (await fs.pathExists(exampleNativeSrc)) await processAndCopyFiles("**/*", exampleNativeSrc, nativeAppDir, context, false);
|
|
857
|
+
}
|
|
559
858
|
}
|
|
560
|
-
} catch (error) {
|
|
561
|
-
consola.error(pc.red("Failed to configure authentication dependencies"));
|
|
562
|
-
if (error instanceof Error) consola.error(pc.red(error.message));
|
|
563
859
|
}
|
|
564
860
|
}
|
|
565
|
-
function
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
861
|
+
async function handleExtras(projectDir, context) {
|
|
862
|
+
const extrasDir = path.join(PKG_ROOT, "templates/extras");
|
|
863
|
+
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
864
|
+
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
865
|
+
const hasNative = hasNativeWind || hasUnistyles;
|
|
866
|
+
if (context.packageManager === "pnpm") {
|
|
867
|
+
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
|
|
868
|
+
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
|
869
|
+
if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
|
870
|
+
}
|
|
871
|
+
if (context.packageManager === "pnpm" && (hasNative || context.frontend.includes("nuxt"))) {
|
|
872
|
+
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
873
|
+
const npmrcDest = path.join(projectDir, ".npmrc");
|
|
874
|
+
if (await fs.pathExists(npmrcTemplateSrc)) await processTemplate(npmrcTemplateSrc, npmrcDest, context);
|
|
875
|
+
}
|
|
876
|
+
if (context.runtime === "workers") {
|
|
877
|
+
const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers");
|
|
878
|
+
if (await fs.pathExists(runtimeWorkersDir)) await processAndCopyFiles("**/*", runtimeWorkersDir, projectDir, context, false);
|
|
879
|
+
}
|
|
571
880
|
}
|
|
572
881
|
|
|
573
882
|
//#endregion
|
|
574
|
-
//#region src/helpers/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
883
|
+
//#region src/helpers/project-generation/add-addons.ts
|
|
884
|
+
function exitWithError(message) {
|
|
885
|
+
cancel(pc.red(message));
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
async function addAddonsToProject(input) {
|
|
889
|
+
try {
|
|
890
|
+
const projectDir = input.projectDir || process.cwd();
|
|
891
|
+
const isBetterTStack = await isBetterTStackProject(projectDir);
|
|
892
|
+
if (!isBetterTStack) exitWithError("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
|
|
893
|
+
const detectedConfig = await detectProjectConfig(projectDir);
|
|
894
|
+
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
895
|
+
const config = {
|
|
896
|
+
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
897
|
+
projectDir,
|
|
898
|
+
relativePath: ".",
|
|
899
|
+
database: detectedConfig.database || "none",
|
|
900
|
+
orm: detectedConfig.orm || "none",
|
|
901
|
+
backend: detectedConfig.backend || "none",
|
|
902
|
+
runtime: detectedConfig.runtime || "none",
|
|
903
|
+
frontend: detectedConfig.frontend || [],
|
|
904
|
+
addons: input.addons,
|
|
905
|
+
examples: detectedConfig.examples || [],
|
|
906
|
+
auth: detectedConfig.auth || false,
|
|
907
|
+
git: false,
|
|
908
|
+
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
909
|
+
install: input.install || false,
|
|
910
|
+
dbSetup: detectedConfig.dbSetup || "none",
|
|
911
|
+
api: detectedConfig.api || "none"
|
|
912
|
+
};
|
|
913
|
+
for (const addon of input.addons) {
|
|
914
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
915
|
+
if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
|
|
595
916
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
917
|
+
log.info(pc.green(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`));
|
|
918
|
+
await setupAddonsTemplate(projectDir, config);
|
|
919
|
+
await setupAddons(config, true);
|
|
920
|
+
const currentAddons = detectedConfig.addons || [];
|
|
921
|
+
const mergedAddons = [...new Set([...currentAddons, ...input.addons])];
|
|
922
|
+
await updateBtsConfig(projectDir, { addons: mergedAddons });
|
|
923
|
+
if (config.install) await installDependencies({
|
|
924
|
+
projectDir,
|
|
925
|
+
packageManager: config.packageManager
|
|
926
|
+
});
|
|
927
|
+
else log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
928
|
+
} catch (error) {
|
|
929
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
930
|
+
exitWithError(`Error adding addons: ${message}`);
|
|
603
931
|
}
|
|
604
|
-
if (runtime === "bun") devDependencies.push("@types/bun");
|
|
605
|
-
if (dependencies.length > 0 || devDependencies.length > 0) await addPackageDependency({
|
|
606
|
-
dependencies,
|
|
607
|
-
devDependencies,
|
|
608
|
-
projectDir: serverDir
|
|
609
|
-
});
|
|
610
932
|
}
|
|
611
933
|
|
|
612
934
|
//#endregion
|
|
613
|
-
//#region src/helpers/
|
|
614
|
-
async function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
935
|
+
//#region src/helpers/setup/api-setup.ts
|
|
936
|
+
async function setupApi(config) {
|
|
937
|
+
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
938
|
+
const isConvex = backend === "convex";
|
|
939
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
940
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
941
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
942
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
943
|
+
const hasReactWeb = frontend.some((f) => [
|
|
944
|
+
"tanstack-router",
|
|
945
|
+
"react-router",
|
|
946
|
+
"tanstack-start",
|
|
947
|
+
"next"
|
|
948
|
+
].includes(f));
|
|
949
|
+
const hasNuxtWeb = frontend.includes("nuxt");
|
|
950
|
+
const hasSvelteWeb = frontend.includes("svelte");
|
|
951
|
+
const hasSolidWeb = frontend.includes("solid");
|
|
952
|
+
if (!isConvex && api !== "none") {
|
|
953
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
954
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
955
|
+
if (serverDirExists) {
|
|
956
|
+
if (api === "orpc") await addPackageDependency({
|
|
957
|
+
dependencies: ["@orpc/server", "@orpc/client"],
|
|
958
|
+
projectDir: serverDir
|
|
959
|
+
});
|
|
960
|
+
else if (api === "trpc") {
|
|
961
|
+
await addPackageDependency({
|
|
962
|
+
dependencies: ["@trpc/server", "@trpc/client"],
|
|
963
|
+
projectDir: serverDir
|
|
964
|
+
});
|
|
965
|
+
if (config.backend === "hono") await addPackageDependency({
|
|
966
|
+
dependencies: ["@hono/trpc-server"],
|
|
967
|
+
projectDir: serverDir
|
|
968
|
+
});
|
|
969
|
+
else if (config.backend === "elysia") await addPackageDependency({
|
|
970
|
+
dependencies: ["@elysiajs/trpc"],
|
|
971
|
+
projectDir: serverDir
|
|
972
|
+
});
|
|
630
973
|
}
|
|
631
|
-
} else {
|
|
632
|
-
contentToAdd += `${key}=${valueToWrite}\n`;
|
|
633
|
-
modified = true;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (contentToAdd) {
|
|
637
|
-
if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n";
|
|
638
|
-
envContent += contentToAdd;
|
|
639
|
-
}
|
|
640
|
-
if (modified) await fs.writeFile(filePath, envContent.trimEnd());
|
|
641
|
-
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
|
|
642
|
-
let exampleEnvContent = "";
|
|
643
|
-
if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
|
|
644
|
-
let exampleModified = false;
|
|
645
|
-
let exampleContentToAdd = "";
|
|
646
|
-
for (const exampleVar of exampleVariables) {
|
|
647
|
-
const key = exampleVar.split("=")[0];
|
|
648
|
-
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
649
|
-
if (!regex.test(exampleEnvContent)) {
|
|
650
|
-
exampleContentToAdd += `${exampleVar}\n`;
|
|
651
|
-
exampleModified = true;
|
|
652
974
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
975
|
+
if (webDirExists) {
|
|
976
|
+
if (hasReactWeb) {
|
|
977
|
+
if (api === "orpc") await addPackageDependency({
|
|
978
|
+
dependencies: [
|
|
979
|
+
"@orpc/tanstack-query",
|
|
980
|
+
"@orpc/client",
|
|
981
|
+
"@orpc/server"
|
|
982
|
+
],
|
|
983
|
+
projectDir: webDir
|
|
984
|
+
});
|
|
985
|
+
else if (api === "trpc") await addPackageDependency({
|
|
986
|
+
dependencies: [
|
|
987
|
+
"@trpc/tanstack-react-query",
|
|
988
|
+
"@trpc/client",
|
|
989
|
+
"@trpc/server"
|
|
990
|
+
],
|
|
991
|
+
projectDir: webDir
|
|
992
|
+
});
|
|
993
|
+
} else if (hasNuxtWeb) {
|
|
994
|
+
if (api === "orpc") await addPackageDependency({
|
|
995
|
+
dependencies: [
|
|
996
|
+
"@orpc/tanstack-query",
|
|
997
|
+
"@orpc/client",
|
|
998
|
+
"@orpc/server"
|
|
999
|
+
],
|
|
1000
|
+
projectDir: webDir
|
|
1001
|
+
});
|
|
1002
|
+
} else if (hasSvelteWeb) {
|
|
1003
|
+
if (api === "orpc") await addPackageDependency({
|
|
1004
|
+
dependencies: [
|
|
1005
|
+
"@orpc/tanstack-query",
|
|
1006
|
+
"@orpc/client",
|
|
1007
|
+
"@orpc/server",
|
|
1008
|
+
"@tanstack/svelte-query"
|
|
1009
|
+
],
|
|
1010
|
+
projectDir: webDir
|
|
1011
|
+
});
|
|
1012
|
+
} else if (hasSolidWeb) {
|
|
1013
|
+
if (api === "orpc") await addPackageDependency({
|
|
1014
|
+
dependencies: [
|
|
1015
|
+
"@orpc/tanstack-query",
|
|
1016
|
+
"@orpc/client",
|
|
1017
|
+
"@orpc/server",
|
|
1018
|
+
"@tanstack/solid-query"
|
|
1019
|
+
],
|
|
1020
|
+
projectDir: webDir
|
|
1021
|
+
});
|
|
684
1022
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1023
|
+
}
|
|
1024
|
+
if (nativeDirExists) {
|
|
1025
|
+
if (api === "trpc") await addPackageDependency({
|
|
1026
|
+
dependencies: [
|
|
1027
|
+
"@trpc/tanstack-react-query",
|
|
1028
|
+
"@trpc/client",
|
|
1029
|
+
"@trpc/server"
|
|
1030
|
+
],
|
|
1031
|
+
projectDir: nativeDir
|
|
1032
|
+
});
|
|
1033
|
+
else if (api === "orpc") await addPackageDependency({
|
|
1034
|
+
dependencies: [
|
|
1035
|
+
"@orpc/tanstack-query",
|
|
1036
|
+
"@orpc/client",
|
|
1037
|
+
"@orpc/server"
|
|
1038
|
+
],
|
|
1039
|
+
projectDir: nativeDir
|
|
1040
|
+
});
|
|
691
1041
|
}
|
|
692
1042
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1043
|
+
const reactBasedFrontends = [
|
|
1044
|
+
"react-router",
|
|
1045
|
+
"tanstack-router",
|
|
1046
|
+
"tanstack-start",
|
|
1047
|
+
"next",
|
|
1048
|
+
"native-nativewind",
|
|
1049
|
+
"native-unistyles"
|
|
1050
|
+
];
|
|
1051
|
+
const needsSolidQuery = frontend.includes("solid");
|
|
1052
|
+
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
1053
|
+
if (needsReactQuery && !isConvex) {
|
|
1054
|
+
const reactQueryDeps = ["@tanstack/react-query"];
|
|
1055
|
+
const reactQueryDevDeps = ["@tanstack/react-query-devtools"];
|
|
1056
|
+
const hasReactWeb$1 = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
1057
|
+
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
1058
|
+
if (hasReactWeb$1 && webDirExists) {
|
|
1059
|
+
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
1060
|
+
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
1061
|
+
await addPackageDependency({
|
|
1062
|
+
dependencies: reactQueryDeps,
|
|
1063
|
+
devDependencies: reactQueryDevDeps,
|
|
1064
|
+
projectDir: webDir
|
|
1065
|
+
});
|
|
1066
|
+
} catch (_error) {}
|
|
1067
|
+
}
|
|
1068
|
+
if (hasNative && nativeDirExists) {
|
|
1069
|
+
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
1070
|
+
if (await fs.pathExists(nativePkgJsonPath)) try {
|
|
1071
|
+
await addPackageDependency({
|
|
1072
|
+
dependencies: reactQueryDeps,
|
|
1073
|
+
projectDir: nativeDir
|
|
1074
|
+
});
|
|
1075
|
+
} catch (_error) {}
|
|
708
1076
|
}
|
|
709
1077
|
}
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
databaseUrl = "mysql://root:password@localhost:3306/mydb";
|
|
724
|
-
break;
|
|
725
|
-
case "mongodb":
|
|
726
|
-
databaseUrl = "mongodb://localhost:27017/mydatabase";
|
|
727
|
-
break;
|
|
728
|
-
case "sqlite":
|
|
729
|
-
if (config.runtime === "workers") databaseUrl = "http://127.0.0.1:8080";
|
|
730
|
-
else databaseUrl = "file:./local.db";
|
|
731
|
-
break;
|
|
1078
|
+
if (needsSolidQuery && !isConvex) {
|
|
1079
|
+
const solidQueryDeps = ["@tanstack/solid-query"];
|
|
1080
|
+
const solidQueryDevDeps = ["@tanstack/solid-query-devtools"];
|
|
1081
|
+
if (webDirExists) {
|
|
1082
|
+
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
1083
|
+
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
1084
|
+
await addPackageDependency({
|
|
1085
|
+
dependencies: solidQueryDeps,
|
|
1086
|
+
devDependencies: solidQueryDevDeps,
|
|
1087
|
+
projectDir: webDir
|
|
1088
|
+
});
|
|
1089
|
+
} catch (_error) {}
|
|
1090
|
+
}
|
|
732
1091
|
}
|
|
733
|
-
|
|
734
|
-
{
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1092
|
+
if (isConvex) {
|
|
1093
|
+
if (webDirExists) {
|
|
1094
|
+
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
1095
|
+
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
1096
|
+
const webDepsToAdd = ["convex"];
|
|
1097
|
+
if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
|
|
1098
|
+
if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
|
|
1099
|
+
await addPackageDependency({
|
|
1100
|
+
dependencies: webDepsToAdd,
|
|
1101
|
+
projectDir: webDir
|
|
1102
|
+
});
|
|
1103
|
+
} catch (_error) {}
|
|
1104
|
+
}
|
|
1105
|
+
if (nativeDirExists) {
|
|
1106
|
+
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
1107
|
+
if (await fs.pathExists(nativePkgJsonPath)) try {
|
|
1108
|
+
await addPackageDependency({
|
|
1109
|
+
dependencies: ["convex"],
|
|
1110
|
+
projectDir: nativeDir
|
|
1111
|
+
});
|
|
1112
|
+
} catch (_error) {}
|
|
1113
|
+
}
|
|
1114
|
+
const backendPackageName = `@${projectName}/backend`;
|
|
1115
|
+
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
1116
|
+
const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => {
|
|
1117
|
+
try {
|
|
1118
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
1119
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
1120
|
+
if (pkgJson.dependencies[depName] !== depVersion) {
|
|
1121
|
+
pkgJson.dependencies[depName] = depVersion;
|
|
1122
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
1123
|
+
}
|
|
1124
|
+
} catch (_error) {}
|
|
1125
|
+
};
|
|
1126
|
+
if (webDirExists) {
|
|
1127
|
+
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
1128
|
+
if (await fs.pathExists(webPkgJsonPath)) await addWorkspaceDepManually(webPkgJsonPath, backendPackageName, backendWorkspaceVersion);
|
|
1129
|
+
}
|
|
1130
|
+
if (nativeDirExists) {
|
|
1131
|
+
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
1132
|
+
if (await fs.pathExists(nativePkgJsonPath)) await addWorkspaceDepManually(nativePkgJsonPath, backendPackageName, backendWorkspaceVersion);
|
|
758
1133
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region src/helpers/setup/auth-setup.ts
|
|
1139
|
+
async function setupAuth(config) {
|
|
1140
|
+
const { auth, frontend, backend, projectDir } = config;
|
|
1141
|
+
if (backend === "convex" || !auth) return;
|
|
1142
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
1143
|
+
const clientDir = path.join(projectDir, "apps/web");
|
|
1144
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
1145
|
+
const clientDirExists = await fs.pathExists(clientDir);
|
|
1146
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
1147
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
1148
|
+
try {
|
|
1149
|
+
if (serverDirExists) await addPackageDependency({
|
|
1150
|
+
dependencies: ["better-auth"],
|
|
1151
|
+
projectDir: serverDir
|
|
1152
|
+
});
|
|
1153
|
+
const hasWebFrontend = frontend.some((f) => [
|
|
1154
|
+
"react-router",
|
|
1155
|
+
"tanstack-router",
|
|
1156
|
+
"tanstack-start",
|
|
1157
|
+
"next",
|
|
1158
|
+
"nuxt",
|
|
1159
|
+
"svelte",
|
|
1160
|
+
"solid"
|
|
1161
|
+
].includes(f));
|
|
1162
|
+
if (hasWebFrontend && clientDirExists) await addPackageDependency({
|
|
1163
|
+
dependencies: ["better-auth"],
|
|
1164
|
+
projectDir: clientDir
|
|
1165
|
+
});
|
|
1166
|
+
if ((frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) && nativeDirExists) {
|
|
1167
|
+
await addPackageDependency({
|
|
1168
|
+
dependencies: ["better-auth", "@better-auth/expo"],
|
|
1169
|
+
projectDir: nativeDir
|
|
1170
|
+
});
|
|
1171
|
+
if (serverDirExists) await addPackageDependency({
|
|
1172
|
+
dependencies: ["@better-auth/expo"],
|
|
1173
|
+
projectDir: serverDir
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
consola.error(pc.red("Failed to configure authentication dependencies"));
|
|
1178
|
+
if (error instanceof Error) consola.error(pc.red(error.message));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function generateAuthSecret(length = 32) {
|
|
1182
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1183
|
+
let result = "";
|
|
1184
|
+
const charactersLength = characters.length;
|
|
1185
|
+
for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/helpers/setup/backend-setup.ts
|
|
1191
|
+
async function setupBackendDependencies(config) {
|
|
1192
|
+
const { backend, runtime, api, projectDir } = config;
|
|
1193
|
+
if (backend === "convex") return;
|
|
1194
|
+
const framework = backend;
|
|
1195
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
1196
|
+
const dependencies = [];
|
|
1197
|
+
const devDependencies = [];
|
|
1198
|
+
if (framework === "hono") {
|
|
1199
|
+
dependencies.push("hono");
|
|
1200
|
+
if (api === "trpc") dependencies.push("@hono/trpc-server");
|
|
1201
|
+
if (runtime === "node") {
|
|
1202
|
+
dependencies.push("@hono/node-server");
|
|
1203
|
+
devDependencies.push("tsx", "@types/node");
|
|
1204
|
+
}
|
|
1205
|
+
} else if (framework === "elysia") {
|
|
1206
|
+
dependencies.push("elysia", "@elysiajs/cors");
|
|
1207
|
+
if (api === "trpc") dependencies.push("@elysiajs/trpc");
|
|
1208
|
+
if (runtime === "node") {
|
|
1209
|
+
dependencies.push("@elysiajs/node");
|
|
1210
|
+
devDependencies.push("tsx", "@types/node");
|
|
1211
|
+
}
|
|
1212
|
+
} else if (framework === "express") {
|
|
1213
|
+
dependencies.push("express", "cors");
|
|
1214
|
+
devDependencies.push("@types/express", "@types/cors");
|
|
1215
|
+
if (runtime === "node") devDependencies.push("tsx", "@types/node");
|
|
1216
|
+
} else if (framework === "fastify") {
|
|
1217
|
+
dependencies.push("fastify", "@fastify/cors");
|
|
1218
|
+
if (runtime === "node") devDependencies.push("tsx", "@types/node");
|
|
1219
|
+
}
|
|
1220
|
+
if (runtime === "bun") devDependencies.push("@types/bun");
|
|
1221
|
+
if (dependencies.length > 0 || devDependencies.length > 0) await addPackageDependency({
|
|
1222
|
+
dependencies,
|
|
1223
|
+
devDependencies,
|
|
1224
|
+
projectDir: serverDir
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
//#endregion
|
|
1229
|
+
//#region src/helpers/project-generation/env-setup.ts
|
|
1230
|
+
async function addEnvVariablesToFile(filePath, variables) {
|
|
1231
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
1232
|
+
let envContent = "";
|
|
1233
|
+
if (await fs.pathExists(filePath)) envContent = await fs.readFile(filePath, "utf8");
|
|
1234
|
+
let modified = false;
|
|
1235
|
+
let contentToAdd = "";
|
|
1236
|
+
const exampleVariables = [];
|
|
1237
|
+
for (const { key, value, condition } of variables) if (condition) {
|
|
1238
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
1239
|
+
const valueToWrite = value ?? "";
|
|
1240
|
+
exampleVariables.push(`${key}=`);
|
|
1241
|
+
if (regex.test(envContent)) {
|
|
1242
|
+
const existingMatch = envContent.match(regex);
|
|
1243
|
+
if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
|
|
1244
|
+
envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
|
|
1245
|
+
modified = true;
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
contentToAdd += `${key}=${valueToWrite}\n`;
|
|
1249
|
+
modified = true;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (contentToAdd) {
|
|
1253
|
+
if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n";
|
|
1254
|
+
envContent += contentToAdd;
|
|
1255
|
+
}
|
|
1256
|
+
if (modified) await fs.writeFile(filePath, envContent.trimEnd());
|
|
1257
|
+
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
|
|
1258
|
+
let exampleEnvContent = "";
|
|
1259
|
+
if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
|
|
1260
|
+
let exampleModified = false;
|
|
1261
|
+
let exampleContentToAdd = "";
|
|
1262
|
+
for (const exampleVar of exampleVariables) {
|
|
1263
|
+
const key = exampleVar.split("=")[0];
|
|
1264
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
1265
|
+
if (!regex.test(exampleEnvContent)) {
|
|
1266
|
+
exampleContentToAdd += `${exampleVar}\n`;
|
|
1267
|
+
exampleModified = true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (exampleContentToAdd) {
|
|
1271
|
+
if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) exampleEnvContent += "\n";
|
|
1272
|
+
exampleEnvContent += exampleContentToAdd;
|
|
1273
|
+
}
|
|
1274
|
+
if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
1275
|
+
}
|
|
1276
|
+
async function setupEnvironmentVariables(config) {
|
|
1277
|
+
const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
|
|
1278
|
+
const hasReactRouter = frontend.includes("react-router");
|
|
1279
|
+
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
1280
|
+
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
1281
|
+
const hasNextJs = frontend.includes("next");
|
|
1282
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
1283
|
+
const hasSvelte = frontend.includes("svelte");
|
|
1284
|
+
const hasSolid = frontend.includes("solid");
|
|
1285
|
+
const hasWebFrontend = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || hasSolid || hasSvelte;
|
|
1286
|
+
if (hasWebFrontend) {
|
|
1287
|
+
const clientDir = path.join(projectDir, "apps/web");
|
|
1288
|
+
if (await fs.pathExists(clientDir)) {
|
|
1289
|
+
let envVarName = "VITE_SERVER_URL";
|
|
1290
|
+
let serverUrl = "http://localhost:3000";
|
|
1291
|
+
if (hasNextJs) envVarName = "NEXT_PUBLIC_SERVER_URL";
|
|
1292
|
+
else if (hasNuxt) envVarName = "NUXT_PUBLIC_SERVER_URL";
|
|
1293
|
+
else if (hasSvelte) envVarName = "PUBLIC_SERVER_URL";
|
|
1294
|
+
if (backend === "convex") {
|
|
1295
|
+
if (hasNextJs) envVarName = "NEXT_PUBLIC_CONVEX_URL";
|
|
1296
|
+
else if (hasNuxt) envVarName = "NUXT_PUBLIC_CONVEX_URL";
|
|
1297
|
+
else if (hasSvelte) envVarName = "PUBLIC_CONVEX_URL";
|
|
1298
|
+
else envVarName = "VITE_CONVEX_URL";
|
|
1299
|
+
serverUrl = "https://<YOUR_CONVEX_URL>";
|
|
1300
|
+
}
|
|
1301
|
+
const clientVars = [{
|
|
1302
|
+
key: envVarName,
|
|
1303
|
+
value: serverUrl,
|
|
1304
|
+
condition: true
|
|
1305
|
+
}];
|
|
1306
|
+
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) {
|
|
1310
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
1311
|
+
if (await fs.pathExists(nativeDir)) {
|
|
1312
|
+
let envVarName = "EXPO_PUBLIC_SERVER_URL";
|
|
1313
|
+
let serverUrl = "http://localhost:3000";
|
|
1314
|
+
if (backend === "convex") {
|
|
1315
|
+
envVarName = "EXPO_PUBLIC_CONVEX_URL";
|
|
1316
|
+
serverUrl = "https://<YOUR_CONVEX_URL>";
|
|
1317
|
+
}
|
|
1318
|
+
const nativeVars = [{
|
|
1319
|
+
key: envVarName,
|
|
1320
|
+
value: serverUrl,
|
|
1321
|
+
condition: true
|
|
1322
|
+
}];
|
|
1323
|
+
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (backend === "convex") return;
|
|
1327
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
1328
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
1329
|
+
const envPath = path.join(serverDir, ".env");
|
|
1330
|
+
let corsOrigin = "http://localhost:3001";
|
|
1331
|
+
if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173";
|
|
1332
|
+
let databaseUrl = null;
|
|
1333
|
+
const specializedSetup = dbSetup === "turso" || dbSetup === "prisma-postgres" || dbSetup === "mongodb-atlas" || dbSetup === "neon" || dbSetup === "supabase" || dbSetup === "d1";
|
|
1334
|
+
if (database !== "none" && !specializedSetup) switch (database) {
|
|
1335
|
+
case "postgres":
|
|
1336
|
+
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
|
|
1337
|
+
break;
|
|
1338
|
+
case "mysql":
|
|
1339
|
+
databaseUrl = "mysql://root:password@localhost:3306/mydb";
|
|
1340
|
+
break;
|
|
1341
|
+
case "mongodb":
|
|
1342
|
+
databaseUrl = "mongodb://localhost:27017/mydatabase";
|
|
1343
|
+
break;
|
|
1344
|
+
case "sqlite":
|
|
1345
|
+
if (config.runtime === "workers") databaseUrl = "http://127.0.0.1:8080";
|
|
1346
|
+
else databaseUrl = "file:./local.db";
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
const serverVars = [
|
|
1350
|
+
{
|
|
1351
|
+
key: "CORS_ORIGIN",
|
|
1352
|
+
value: corsOrigin,
|
|
1353
|
+
condition: true
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
key: "BETTER_AUTH_SECRET",
|
|
1357
|
+
value: generateAuthSecret(),
|
|
1358
|
+
condition: !!auth
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
key: "BETTER_AUTH_URL",
|
|
1362
|
+
value: "http://localhost:3000",
|
|
1363
|
+
condition: !!auth
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
key: "DATABASE_URL",
|
|
1367
|
+
value: databaseUrl,
|
|
1368
|
+
condition: database !== "none" && !specializedSetup
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1372
|
+
value: "",
|
|
1373
|
+
condition: examples?.includes("ai") || false
|
|
1374
|
+
}
|
|
1375
|
+
];
|
|
1376
|
+
await addEnvVariablesToFile(envPath, serverVars);
|
|
1377
|
+
if (config.runtime === "workers") {
|
|
1378
|
+
const devVarsPath = path.join(serverDir, ".dev.vars");
|
|
1379
|
+
try {
|
|
1380
|
+
await fs.copy(envPath, devVarsPath);
|
|
1381
|
+
} catch (_err) {}
|
|
766
1382
|
}
|
|
767
1383
|
}
|
|
768
1384
|
|
|
@@ -1891,23 +2507,6 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
|
|
|
1891
2507
|
return scripts;
|
|
1892
2508
|
}
|
|
1893
2509
|
|
|
1894
|
-
//#endregion
|
|
1895
|
-
//#region src/helpers/project-generation/install-dependencies.ts
|
|
1896
|
-
async function installDependencies({ projectDir, packageManager }) {
|
|
1897
|
-
const s = spinner();
|
|
1898
|
-
try {
|
|
1899
|
-
s.start(`Running ${packageManager} install...`);
|
|
1900
|
-
await $({
|
|
1901
|
-
cwd: projectDir,
|
|
1902
|
-
stderr: "inherit"
|
|
1903
|
-
})`${packageManager} install`;
|
|
1904
|
-
s.stop("Dependencies installed successfully");
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
s.stop(pc.red("Failed to install dependencies"));
|
|
1907
|
-
if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
2510
|
//#endregion
|
|
1912
2511
|
//#region src/helpers/project-generation/post-installation.ts
|
|
1913
2512
|
function displayPostInstallInstructions(config) {
|
|
@@ -2018,549 +2617,182 @@ function getStarlightInstructions(runCmd) {
|
|
|
2018
2617
|
function getNoOrmWarning() {
|
|
2019
2618
|
return `\n${pc.yellow("WARNING:")} Database selected without an ORM. Features requiring database access (e.g., examples, auth) need manual setup.`;
|
|
2020
2619
|
}
|
|
2021
|
-
function getBunWebNativeWarning() {
|
|
2022
|
-
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
//#endregion
|
|
2026
|
-
//#region src/helpers/project-generation/project-config.ts
|
|
2027
|
-
async function updatePackageConfigurations(projectDir, options) {
|
|
2028
|
-
await updateRootPackageJson(projectDir, options);
|
|
2029
|
-
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
2030
|
-
else await updateConvexPackageJson(projectDir, options);
|
|
2031
|
-
}
|
|
2032
|
-
async function updateRootPackageJson(projectDir, options) {
|
|
2033
|
-
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
|
2034
|
-
if (!await fs.pathExists(rootPackageJsonPath)) return;
|
|
2035
|
-
const packageJson = await fs.readJson(rootPackageJsonPath);
|
|
2036
|
-
packageJson.name = options.projectName;
|
|
2037
|
-
if (!packageJson.scripts) packageJson.scripts = {};
|
|
2038
|
-
const scripts = packageJson.scripts;
|
|
2039
|
-
const backendPackageName = options.backend === "convex" ? `@${options.projectName}/backend` : "server";
|
|
2040
|
-
let serverDevScript = "";
|
|
2041
|
-
if (options.addons.includes("turborepo")) serverDevScript = `turbo -F ${backendPackageName} dev`;
|
|
2042
|
-
else if (options.packageManager === "bun") serverDevScript = `bun run --filter ${backendPackageName} dev`;
|
|
2043
|
-
else if (options.packageManager === "pnpm") serverDevScript = `pnpm --filter ${backendPackageName} dev`;
|
|
2044
|
-
else if (options.packageManager === "npm") serverDevScript = `npm run dev --workspace ${backendPackageName}`;
|
|
2045
|
-
let devScript = "";
|
|
2046
|
-
if (options.packageManager === "pnpm") devScript = "pnpm -r dev";
|
|
2047
|
-
else if (options.packageManager === "npm") devScript = "npm run dev --workspaces";
|
|
2048
|
-
else if (options.packageManager === "bun") devScript = "bun run --filter '*' dev";
|
|
2049
|
-
const needsDbScripts = options.backend !== "convex" && options.database !== "none" && options.orm !== "none" && options.orm !== "mongoose";
|
|
2050
|
-
if (options.addons.includes("turborepo")) {
|
|
2051
|
-
scripts.dev = "turbo dev";
|
|
2052
|
-
scripts.build = "turbo build";
|
|
2053
|
-
scripts["check-types"] = "turbo check-types";
|
|
2054
|
-
scripts["dev:native"] = "turbo -F native dev";
|
|
2055
|
-
scripts["dev:web"] = "turbo -F web dev";
|
|
2056
|
-
scripts["dev:server"] = serverDevScript;
|
|
2057
|
-
if (options.backend === "convex") scripts["dev:setup"] = `turbo -F ${backendPackageName} setup`;
|
|
2058
|
-
if (needsDbScripts) {
|
|
2059
|
-
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
|
|
2060
|
-
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
|
|
2061
|
-
if (options.orm === "prisma") {
|
|
2062
|
-
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
|
2063
|
-
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
|
2064
|
-
} else if (options.orm === "drizzle") {
|
|
2065
|
-
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
|
2066
|
-
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
} else if (options.packageManager === "pnpm") {
|
|
2070
|
-
scripts.dev = devScript;
|
|
2071
|
-
scripts.build = "pnpm -r build";
|
|
2072
|
-
scripts["check-types"] = "pnpm -r check-types";
|
|
2073
|
-
scripts["dev:native"] = "pnpm --filter native dev";
|
|
2074
|
-
scripts["dev:web"] = "pnpm --filter web dev";
|
|
2075
|
-
scripts["dev:server"] = serverDevScript;
|
|
2076
|
-
if (options.backend === "convex") scripts["dev:setup"] = `pnpm --filter ${backendPackageName} setup`;
|
|
2077
|
-
if (needsDbScripts) {
|
|
2078
|
-
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
|
|
2079
|
-
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
|
|
2080
|
-
if (options.orm === "prisma") {
|
|
2081
|
-
scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
|
|
2082
|
-
scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
|
|
2083
|
-
} else if (options.orm === "drizzle") {
|
|
2084
|
-
scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
|
|
2085
|
-
scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
} else if (options.packageManager === "npm") {
|
|
2089
|
-
scripts.dev = devScript;
|
|
2090
|
-
scripts.build = "npm run build --workspaces";
|
|
2091
|
-
scripts["check-types"] = "npm run check-types --workspaces";
|
|
2092
|
-
scripts["dev:native"] = "npm run dev --workspace native";
|
|
2093
|
-
scripts["dev:web"] = "npm run dev --workspace web";
|
|
2094
|
-
scripts["dev:server"] = serverDevScript;
|
|
2095
|
-
if (options.backend === "convex") scripts["dev:setup"] = `npm run setup --workspace ${backendPackageName}`;
|
|
2096
|
-
if (needsDbScripts) {
|
|
2097
|
-
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
|
|
2098
|
-
scripts["db:studio"] = `npm run db:studio --workspace ${backendPackageName}`;
|
|
2099
|
-
if (options.orm === "prisma") {
|
|
2100
|
-
scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
|
|
2101
|
-
scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
|
|
2102
|
-
} else if (options.orm === "drizzle") {
|
|
2103
|
-
scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
|
|
2104
|
-
scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
} else if (options.packageManager === "bun") {
|
|
2108
|
-
scripts.dev = devScript;
|
|
2109
|
-
scripts.build = "bun run --filter '*' build";
|
|
2110
|
-
scripts["check-types"] = "bun run --filter '*' check-types";
|
|
2111
|
-
scripts["dev:native"] = "bun run --filter native dev";
|
|
2112
|
-
scripts["dev:web"] = "bun run --filter web dev";
|
|
2113
|
-
scripts["dev:server"] = serverDevScript;
|
|
2114
|
-
if (options.backend === "convex") scripts["dev:setup"] = `bun run --filter ${backendPackageName} setup`;
|
|
2115
|
-
if (needsDbScripts) {
|
|
2116
|
-
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
|
|
2117
|
-
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
|
|
2118
|
-
if (options.orm === "prisma") {
|
|
2119
|
-
scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
|
|
2120
|
-
scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
|
|
2121
|
-
} else if (options.orm === "drizzle") {
|
|
2122
|
-
scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
|
|
2123
|
-
scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
if (options.addons.includes("biome")) scripts.check = "biome check --write .";
|
|
2128
|
-
if (options.addons.includes("husky")) {
|
|
2129
|
-
scripts.prepare = "husky";
|
|
2130
|
-
packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
|
|
2131
|
-
}
|
|
2132
|
-
try {
|
|
2133
|
-
const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir });
|
|
2134
|
-
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
|
2135
|
-
} catch (_e) {
|
|
2136
|
-
log.warn(`Could not determine ${options.packageManager} version.`);
|
|
2137
|
-
}
|
|
2138
|
-
if (!packageJson.workspaces) packageJson.workspaces = [];
|
|
2139
|
-
const workspaces = packageJson.workspaces;
|
|
2140
|
-
if (options.backend === "convex") {
|
|
2141
|
-
if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
|
|
2142
|
-
const needsAppsDir = options.frontend.length > 0 || options.addons.includes("starlight");
|
|
2143
|
-
if (needsAppsDir && !workspaces.includes("apps/*")) workspaces.push("apps/*");
|
|
2144
|
-
} else {
|
|
2145
|
-
if (!workspaces.includes("apps/*")) workspaces.push("apps/*");
|
|
2146
|
-
if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
|
|
2147
|
-
}
|
|
2148
|
-
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
|
2149
|
-
}
|
|
2150
|
-
async function updateServerPackageJson(projectDir, options) {
|
|
2151
|
-
const serverPackageJsonPath = path.join(projectDir, "apps/server/package.json");
|
|
2152
|
-
if (!await fs.pathExists(serverPackageJsonPath)) return;
|
|
2153
|
-
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
|
2154
|
-
if (!serverPackageJson.scripts) serverPackageJson.scripts = {};
|
|
2155
|
-
const scripts = serverPackageJson.scripts;
|
|
2156
|
-
if (options.database !== "none") {
|
|
2157
|
-
if (options.database === "sqlite" && options.orm === "drizzle") scripts["db:local"] = "turso dev --db-file local.db";
|
|
2158
|
-
if (options.orm === "prisma") {
|
|
2159
|
-
scripts["db:push"] = "prisma db push --schema ./prisma/schema";
|
|
2160
|
-
scripts["db:studio"] = "prisma studio";
|
|
2161
|
-
scripts["db:generate"] = "prisma generate --schema ./prisma/schema";
|
|
2162
|
-
scripts["db:migrate"] = "prisma migrate dev";
|
|
2163
|
-
} else if (options.orm === "drizzle") {
|
|
2164
|
-
scripts["db:push"] = "drizzle-kit push";
|
|
2165
|
-
scripts["db:studio"] = "drizzle-kit studio";
|
|
2166
|
-
scripts["db:generate"] = "drizzle-kit generate";
|
|
2167
|
-
scripts["db:migrate"] = "drizzle-kit migrate";
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
await fs.writeJson(serverPackageJsonPath, serverPackageJson, { spaces: 2 });
|
|
2171
|
-
}
|
|
2172
|
-
async function updateConvexPackageJson(projectDir, options) {
|
|
2173
|
-
const convexPackageJsonPath = path.join(projectDir, "packages/backend/package.json");
|
|
2174
|
-
if (!await fs.pathExists(convexPackageJsonPath)) return;
|
|
2175
|
-
const convexPackageJson = await fs.readJson(convexPackageJsonPath);
|
|
2176
|
-
convexPackageJson.name = `@${options.projectName}/backend`;
|
|
2177
|
-
if (!convexPackageJson.scripts) convexPackageJson.scripts = {};
|
|
2178
|
-
await fs.writeJson(convexPackageJsonPath, convexPackageJson, { spaces: 2 });
|
|
2179
|
-
}
|
|
2180
|
-
async function initializeGit(projectDir, useGit) {
|
|
2181
|
-
if (!useGit) return;
|
|
2182
|
-
const gitVersionResult = await $({
|
|
2183
|
-
cwd: projectDir,
|
|
2184
|
-
reject: false,
|
|
2185
|
-
stderr: "pipe"
|
|
2186
|
-
})`git --version`;
|
|
2187
|
-
if (gitVersionResult.exitCode !== 0) {
|
|
2188
|
-
log.warn(pc.yellow("Git is not installed"));
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
const result = await $({
|
|
2192
|
-
cwd: projectDir,
|
|
2193
|
-
reject: false,
|
|
2194
|
-
stderr: "pipe"
|
|
2195
|
-
})`git init`;
|
|
2196
|
-
if (result.exitCode !== 0) throw new Error(`Git initialization failed: ${result.stderr}`);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
//#endregion
|
|
2200
|
-
//#region src/utils/template-processor.ts
|
|
2201
|
-
/**
|
|
2202
|
-
* Processes a Handlebars template file and writes the output to the destination.
|
|
2203
|
-
* @param srcPath Path to the source .hbs template file.
|
|
2204
|
-
* @param destPath Path to write the processed file.
|
|
2205
|
-
* @param context Data to be passed to the Handlebars template.
|
|
2206
|
-
*/
|
|
2207
|
-
async function processTemplate(srcPath, destPath, context) {
|
|
2208
|
-
try {
|
|
2209
|
-
const templateContent = await fs.readFile(srcPath, "utf-8");
|
|
2210
|
-
const template = handlebars.compile(templateContent);
|
|
2211
|
-
const processedContent = template(context);
|
|
2212
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
2213
|
-
await fs.writeFile(destPath, processedContent);
|
|
2214
|
-
} catch (error) {
|
|
2215
|
-
consola.error(`Error processing template ${srcPath}:`, error);
|
|
2216
|
-
throw new Error(`Failed to process template ${srcPath}`);
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2220
|
-
handlebars.registerHelper("and", (a, b) => a && b);
|
|
2221
|
-
handlebars.registerHelper("or", (a, b) => a || b);
|
|
2222
|
-
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
2223
|
-
|
|
2224
|
-
//#endregion
|
|
2225
|
-
//#region src/helpers/project-generation/template-manager.ts
|
|
2226
|
-
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
|
|
2227
|
-
const sourceFiles = await globby(sourcePattern, {
|
|
2228
|
-
cwd: baseSourceDir,
|
|
2229
|
-
dot: true,
|
|
2230
|
-
onlyFiles: true,
|
|
2231
|
-
absolute: false
|
|
2232
|
-
});
|
|
2233
|
-
for (const relativeSrcPath of sourceFiles) {
|
|
2234
|
-
const srcPath = path.join(baseSourceDir, relativeSrcPath);
|
|
2235
|
-
let relativeDestPath = relativeSrcPath;
|
|
2236
|
-
if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
|
|
2237
|
-
const basename = path.basename(relativeSrcPath);
|
|
2238
|
-
if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore");
|
|
2239
|
-
else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".npmrc");
|
|
2240
|
-
const destPath = path.join(destDir, relativeDestPath);
|
|
2241
|
-
try {
|
|
2242
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
2243
|
-
if (!overwrite && await fs.pathExists(destPath)) continue;
|
|
2244
|
-
if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
|
|
2245
|
-
else await fs.copy(srcPath, destPath, { overwrite: true });
|
|
2246
|
-
} catch (_error) {}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
async function copyBaseTemplate(projectDir, context) {
|
|
2250
|
-
const templateDir = path.join(PKG_ROOT, "templates/base");
|
|
2251
|
-
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
|
2252
|
-
await fs.ensureDir(path.join(projectDir, "packages"));
|
|
2253
|
-
}
|
|
2254
|
-
async function setupFrontendTemplates(projectDir, context) {
|
|
2255
|
-
const hasReactWeb = context.frontend.some((f) => [
|
|
2256
|
-
"tanstack-router",
|
|
2257
|
-
"react-router",
|
|
2258
|
-
"tanstack-start",
|
|
2259
|
-
"next"
|
|
2260
|
-
].includes(f));
|
|
2261
|
-
const hasNuxtWeb = context.frontend.includes("nuxt");
|
|
2262
|
-
const hasSvelteWeb = context.frontend.includes("svelte");
|
|
2263
|
-
const hasSolidWeb = context.frontend.includes("solid");
|
|
2264
|
-
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
2265
|
-
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
2266
|
-
const _hasNative = hasNativeWind || hasUnistyles;
|
|
2267
|
-
const isConvex = context.backend === "convex";
|
|
2268
|
-
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
|
|
2269
|
-
const webAppDir = path.join(projectDir, "apps/web");
|
|
2270
|
-
await fs.ensureDir(webAppDir);
|
|
2271
|
-
if (hasReactWeb) {
|
|
2272
|
-
const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
|
|
2273
|
-
if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
|
2274
|
-
const reactFramework = context.frontend.find((f) => [
|
|
2275
|
-
"tanstack-router",
|
|
2276
|
-
"react-router",
|
|
2277
|
-
"tanstack-start",
|
|
2278
|
-
"next"
|
|
2279
|
-
].includes(f));
|
|
2280
|
-
if (reactFramework) {
|
|
2281
|
-
const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
|
|
2282
|
-
if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
|
|
2283
|
-
if (!isConvex && context.api !== "none") {
|
|
2284
|
-
const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
|
|
2285
|
-
if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
} else if (hasNuxtWeb) {
|
|
2289
|
-
const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
|
|
2290
|
-
if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
|
|
2291
|
-
if (!isConvex && context.api === "orpc") {
|
|
2292
|
-
const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
|
|
2293
|
-
if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
|
|
2294
|
-
}
|
|
2295
|
-
} else if (hasSvelteWeb) {
|
|
2296
|
-
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
|
|
2297
|
-
if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
|
|
2298
|
-
if (!isConvex && context.api === "orpc") {
|
|
2299
|
-
const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
|
|
2300
|
-
if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
|
|
2301
|
-
}
|
|
2302
|
-
} else if (hasSolidWeb) {
|
|
2303
|
-
const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
|
|
2304
|
-
if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
|
|
2305
|
-
if (!isConvex && context.api === "orpc") {
|
|
2306
|
-
const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
|
|
2307
|
-
if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
if (hasNativeWind || hasUnistyles) {
|
|
2312
|
-
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
2313
|
-
await fs.ensureDir(nativeAppDir);
|
|
2314
|
-
const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
|
|
2315
|
-
if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
|
|
2316
|
-
let nativeFrameworkPath = "";
|
|
2317
|
-
if (hasNativeWind) nativeFrameworkPath = "nativewind";
|
|
2318
|
-
else if (hasUnistyles) nativeFrameworkPath = "unistyles";
|
|
2319
|
-
const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
|
|
2320
|
-
if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
|
|
2321
|
-
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
|
|
2322
|
-
const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
|
|
2323
|
-
if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
async function setupBackendFramework(projectDir, context) {
|
|
2328
|
-
if (context.backend === "none") return;
|
|
2329
|
-
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2330
|
-
if (context.backend === "convex") {
|
|
2331
|
-
if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
|
|
2332
|
-
const convexBackendDestDir = path.join(projectDir, "packages/backend");
|
|
2333
|
-
const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
|
|
2334
|
-
await fs.ensureDir(convexBackendDestDir);
|
|
2335
|
-
if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
|
|
2336
|
-
return;
|
|
2337
|
-
}
|
|
2338
|
-
await fs.ensureDir(serverAppDir);
|
|
2339
|
-
const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
|
|
2340
|
-
if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
|
|
2341
|
-
const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
|
|
2342
|
-
if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
|
|
2343
|
-
if (context.api !== "none") {
|
|
2344
|
-
const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
|
|
2345
|
-
if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
|
|
2346
|
-
const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
|
|
2347
|
-
if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
async function setupDbOrmTemplates(projectDir, context) {
|
|
2351
|
-
if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
|
|
2352
|
-
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2353
|
-
await fs.ensureDir(serverAppDir);
|
|
2354
|
-
const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
|
|
2355
|
-
if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
|
|
2356
|
-
}
|
|
2357
|
-
async function setupAuthTemplate(projectDir, context) {
|
|
2358
|
-
if (context.backend === "convex" || !context.auth) return;
|
|
2359
|
-
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2360
|
-
const webAppDir = path.join(projectDir, "apps/web");
|
|
2361
|
-
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
2362
|
-
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
|
2363
|
-
const webAppDirExists = await fs.pathExists(webAppDir);
|
|
2364
|
-
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
|
2365
|
-
const hasReactWeb = context.frontend.some((f) => [
|
|
2366
|
-
"tanstack-router",
|
|
2367
|
-
"react-router",
|
|
2368
|
-
"tanstack-start",
|
|
2369
|
-
"next"
|
|
2370
|
-
].includes(f));
|
|
2371
|
-
const hasNuxtWeb = context.frontend.includes("nuxt");
|
|
2372
|
-
const hasSvelteWeb = context.frontend.includes("svelte");
|
|
2373
|
-
const hasSolidWeb = context.frontend.includes("solid");
|
|
2374
|
-
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
2375
|
-
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
2376
|
-
const hasNative = hasNativeWind || hasUnistyles;
|
|
2377
|
-
if (serverAppDirExists) {
|
|
2378
|
-
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
|
|
2379
|
-
if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
|
|
2380
|
-
if (context.backend === "next") {
|
|
2381
|
-
const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
|
|
2382
|
-
if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
|
|
2383
|
-
}
|
|
2384
|
-
if (context.orm !== "none" && context.database !== "none") {
|
|
2385
|
-
const orm = context.orm;
|
|
2386
|
-
const db = context.database;
|
|
2387
|
-
let authDbSrc = "";
|
|
2388
|
-
if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/drizzle/${db}`);
|
|
2389
|
-
else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/prisma/${db}`);
|
|
2390
|
-
else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/mongoose/${db}`);
|
|
2391
|
-
if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
|
2392
|
-
else if (authDbSrc) {}
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
|
|
2396
|
-
if (hasReactWeb) {
|
|
2397
|
-
const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/react/base");
|
|
2398
|
-
if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
|
2399
|
-
const reactFramework = context.frontend.find((f) => [
|
|
2400
|
-
"tanstack-router",
|
|
2401
|
-
"react-router",
|
|
2402
|
-
"tanstack-start",
|
|
2403
|
-
"next"
|
|
2404
|
-
].includes(f));
|
|
2405
|
-
if (reactFramework) {
|
|
2406
|
-
const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/web/react/${reactFramework}`);
|
|
2407
|
-
if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
|
|
2408
|
-
}
|
|
2409
|
-
} else if (hasNuxtWeb) {
|
|
2410
|
-
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
|
|
2411
|
-
if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
|
2412
|
-
} else if (hasSvelteWeb) {
|
|
2413
|
-
if (context.api === "orpc") {
|
|
2414
|
-
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
|
|
2415
|
-
if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
|
|
2416
|
-
}
|
|
2417
|
-
} else if (hasSolidWeb) {
|
|
2418
|
-
if (context.api === "orpc") {
|
|
2419
|
-
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
|
|
2420
|
-
if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
if (hasNative && nativeAppDirExists) {
|
|
2425
|
-
const authNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/native/native-base");
|
|
2426
|
-
if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
|
|
2427
|
-
let nativeFrameworkAuthPath = "";
|
|
2428
|
-
if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
|
|
2429
|
-
else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
|
|
2430
|
-
if (nativeFrameworkAuthPath) {
|
|
2431
|
-
const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/native/${nativeFrameworkAuthPath}`);
|
|
2432
|
-
if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2620
|
+
function getBunWebNativeWarning() {
|
|
2621
|
+
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
|
|
2435
2622
|
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
const webAppDir = path.join(projectDir, "apps/web");
|
|
2444
|
-
if (!await fs.pathExists(webAppDir)) continue;
|
|
2445
|
-
addonDestDir = webAppDir;
|
|
2446
|
-
if (context.frontend.includes("next")) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
|
|
2447
|
-
else if (context.frontend.some((f) => [
|
|
2448
|
-
"tanstack-router",
|
|
2449
|
-
"react-router",
|
|
2450
|
-
"solid"
|
|
2451
|
-
].includes(f))) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
|
|
2452
|
-
else continue;
|
|
2453
|
-
}
|
|
2454
|
-
if (await fs.pathExists(addonSrcDir)) await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
|
|
2455
|
-
}
|
|
2623
|
+
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region src/helpers/project-generation/project-config.ts
|
|
2626
|
+
async function updatePackageConfigurations(projectDir, options) {
|
|
2627
|
+
await updateRootPackageJson(projectDir, options);
|
|
2628
|
+
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
2629
|
+
else await updateConvexPackageJson(projectDir, options);
|
|
2456
2630
|
}
|
|
2457
|
-
async function
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
const
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2631
|
+
async function updateRootPackageJson(projectDir, options) {
|
|
2632
|
+
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
|
2633
|
+
if (!await fs.pathExists(rootPackageJsonPath)) return;
|
|
2634
|
+
const packageJson = await fs.readJson(rootPackageJsonPath);
|
|
2635
|
+
packageJson.name = options.projectName;
|
|
2636
|
+
if (!packageJson.scripts) packageJson.scripts = {};
|
|
2637
|
+
const scripts = packageJson.scripts;
|
|
2638
|
+
const backendPackageName = options.backend === "convex" ? `@${options.projectName}/backend` : "server";
|
|
2639
|
+
let serverDevScript = "";
|
|
2640
|
+
if (options.addons.includes("turborepo")) serverDevScript = `turbo -F ${backendPackageName} dev`;
|
|
2641
|
+
else if (options.packageManager === "bun") serverDevScript = `bun run --filter ${backendPackageName} dev`;
|
|
2642
|
+
else if (options.packageManager === "pnpm") serverDevScript = `pnpm --filter ${backendPackageName} dev`;
|
|
2643
|
+
else if (options.packageManager === "npm") serverDevScript = `npm run dev --workspace ${backendPackageName}`;
|
|
2644
|
+
let devScript = "";
|
|
2645
|
+
if (options.packageManager === "pnpm") devScript = "pnpm -r dev";
|
|
2646
|
+
else if (options.packageManager === "npm") devScript = "npm run dev --workspaces";
|
|
2647
|
+
else if (options.packageManager === "bun") devScript = "bun run --filter '*' dev";
|
|
2648
|
+
const needsDbScripts = options.backend !== "convex" && options.database !== "none" && options.orm !== "none" && options.orm !== "mongoose";
|
|
2649
|
+
if (options.addons.includes("turborepo")) {
|
|
2650
|
+
scripts.dev = "turbo dev";
|
|
2651
|
+
scripts.build = "turbo build";
|
|
2652
|
+
scripts["check-types"] = "turbo check-types";
|
|
2653
|
+
scripts["dev:native"] = "turbo -F native dev";
|
|
2654
|
+
scripts["dev:web"] = "turbo -F web dev";
|
|
2655
|
+
scripts["dev:server"] = serverDevScript;
|
|
2656
|
+
if (options.backend === "convex") scripts["dev:setup"] = `turbo -F ${backendPackageName} setup`;
|
|
2657
|
+
if (needsDbScripts) {
|
|
2658
|
+
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
|
|
2659
|
+
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
|
|
2660
|
+
if (options.orm === "prisma") {
|
|
2661
|
+
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
|
2662
|
+
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
|
2663
|
+
} else if (options.orm === "drizzle") {
|
|
2664
|
+
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
|
2665
|
+
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
|
2488
2666
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2667
|
+
}
|
|
2668
|
+
} else if (options.packageManager === "pnpm") {
|
|
2669
|
+
scripts.dev = devScript;
|
|
2670
|
+
scripts.build = "pnpm -r build";
|
|
2671
|
+
scripts["check-types"] = "pnpm -r check-types";
|
|
2672
|
+
scripts["dev:native"] = "pnpm --filter native dev";
|
|
2673
|
+
scripts["dev:web"] = "pnpm --filter web dev";
|
|
2674
|
+
scripts["dev:server"] = serverDevScript;
|
|
2675
|
+
if (options.backend === "convex") scripts["dev:setup"] = `pnpm --filter ${backendPackageName} setup`;
|
|
2676
|
+
if (needsDbScripts) {
|
|
2677
|
+
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
|
|
2678
|
+
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
|
|
2679
|
+
if (options.orm === "prisma") {
|
|
2680
|
+
scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
|
|
2681
|
+
scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
|
|
2682
|
+
} else if (options.orm === "drizzle") {
|
|
2683
|
+
scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
|
|
2684
|
+
scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
|
|
2504
2685
|
}
|
|
2505
2686
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
if (await fs.pathExists(exampleWebNuxtSrc)) await processAndCopyFiles("**/*", exampleWebNuxtSrc, webAppDir, context, false);
|
|
2524
|
-
} else if (hasSvelteWeb) {
|
|
2525
|
-
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
|
|
2526
|
-
if (await fs.pathExists(exampleWebSvelteSrc)) await processAndCopyFiles("**/*", exampleWebSvelteSrc, webAppDir, context, false);
|
|
2527
|
-
} else if (hasSolidWeb) {
|
|
2528
|
-
const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
|
|
2529
|
-
if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
|
|
2687
|
+
} else if (options.packageManager === "npm") {
|
|
2688
|
+
scripts.dev = devScript;
|
|
2689
|
+
scripts.build = "npm run build --workspaces";
|
|
2690
|
+
scripts["check-types"] = "npm run check-types --workspaces";
|
|
2691
|
+
scripts["dev:native"] = "npm run dev --workspace native";
|
|
2692
|
+
scripts["dev:web"] = "npm run dev --workspace web";
|
|
2693
|
+
scripts["dev:server"] = serverDevScript;
|
|
2694
|
+
if (options.backend === "convex") scripts["dev:setup"] = `npm run setup --workspace ${backendPackageName}`;
|
|
2695
|
+
if (needsDbScripts) {
|
|
2696
|
+
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
|
|
2697
|
+
scripts["db:studio"] = `npm run db:studio --workspace ${backendPackageName}`;
|
|
2698
|
+
if (options.orm === "prisma") {
|
|
2699
|
+
scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
|
|
2700
|
+
scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
|
|
2701
|
+
} else if (options.orm === "drizzle") {
|
|
2702
|
+
scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
|
|
2703
|
+
scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
|
|
2530
2704
|
}
|
|
2531
2705
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2706
|
+
} else if (options.packageManager === "bun") {
|
|
2707
|
+
scripts.dev = devScript;
|
|
2708
|
+
scripts.build = "bun run --filter '*' build";
|
|
2709
|
+
scripts["check-types"] = "bun run --filter '*' check-types";
|
|
2710
|
+
scripts["dev:native"] = "bun run --filter native dev";
|
|
2711
|
+
scripts["dev:web"] = "bun run --filter web dev";
|
|
2712
|
+
scripts["dev:server"] = serverDevScript;
|
|
2713
|
+
if (options.backend === "convex") scripts["dev:setup"] = `bun run --filter ${backendPackageName} setup`;
|
|
2714
|
+
if (needsDbScripts) {
|
|
2715
|
+
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
|
|
2716
|
+
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
|
|
2717
|
+
if (options.orm === "prisma") {
|
|
2718
|
+
scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
|
|
2719
|
+
scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
|
|
2720
|
+
} else if (options.orm === "drizzle") {
|
|
2721
|
+
scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
|
|
2722
|
+
scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
|
|
2541
2723
|
}
|
|
2542
2724
|
}
|
|
2543
2725
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
2549
|
-
const hasNative = hasNativeWind || hasUnistyles;
|
|
2550
|
-
if (context.packageManager === "pnpm") {
|
|
2551
|
-
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
|
|
2552
|
-
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
|
2553
|
-
if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
|
2726
|
+
if (options.addons.includes("biome")) scripts.check = "biome check --write .";
|
|
2727
|
+
if (options.addons.includes("husky")) {
|
|
2728
|
+
scripts.prepare = "husky";
|
|
2729
|
+
packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
|
|
2554
2730
|
}
|
|
2555
|
-
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2558
|
-
|
|
2731
|
+
try {
|
|
2732
|
+
const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir });
|
|
2733
|
+
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
|
2734
|
+
} catch (_e) {
|
|
2735
|
+
log.warn(`Could not determine ${options.packageManager} version.`);
|
|
2559
2736
|
}
|
|
2560
|
-
if (
|
|
2561
|
-
|
|
2562
|
-
|
|
2737
|
+
if (!packageJson.workspaces) packageJson.workspaces = [];
|
|
2738
|
+
const workspaces = packageJson.workspaces;
|
|
2739
|
+
if (options.backend === "convex") {
|
|
2740
|
+
if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
|
|
2741
|
+
const needsAppsDir = options.frontend.length > 0 || options.addons.includes("starlight");
|
|
2742
|
+
if (needsAppsDir && !workspaces.includes("apps/*")) workspaces.push("apps/*");
|
|
2743
|
+
} else {
|
|
2744
|
+
if (!workspaces.includes("apps/*")) workspaces.push("apps/*");
|
|
2745
|
+
if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
|
|
2746
|
+
}
|
|
2747
|
+
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
|
2748
|
+
}
|
|
2749
|
+
async function updateServerPackageJson(projectDir, options) {
|
|
2750
|
+
const serverPackageJsonPath = path.join(projectDir, "apps/server/package.json");
|
|
2751
|
+
if (!await fs.pathExists(serverPackageJsonPath)) return;
|
|
2752
|
+
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
|
2753
|
+
if (!serverPackageJson.scripts) serverPackageJson.scripts = {};
|
|
2754
|
+
const scripts = serverPackageJson.scripts;
|
|
2755
|
+
if (options.database !== "none") {
|
|
2756
|
+
if (options.database === "sqlite" && options.orm === "drizzle") scripts["db:local"] = "turso dev --db-file local.db";
|
|
2757
|
+
if (options.orm === "prisma") {
|
|
2758
|
+
scripts["db:push"] = "prisma db push --schema ./prisma/schema";
|
|
2759
|
+
scripts["db:studio"] = "prisma studio";
|
|
2760
|
+
scripts["db:generate"] = "prisma generate --schema ./prisma/schema";
|
|
2761
|
+
scripts["db:migrate"] = "prisma migrate dev";
|
|
2762
|
+
} else if (options.orm === "drizzle") {
|
|
2763
|
+
scripts["db:push"] = "drizzle-kit push";
|
|
2764
|
+
scripts["db:studio"] = "drizzle-kit studio";
|
|
2765
|
+
scripts["db:generate"] = "drizzle-kit generate";
|
|
2766
|
+
scripts["db:migrate"] = "drizzle-kit migrate";
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
await fs.writeJson(serverPackageJsonPath, serverPackageJson, { spaces: 2 });
|
|
2770
|
+
}
|
|
2771
|
+
async function updateConvexPackageJson(projectDir, options) {
|
|
2772
|
+
const convexPackageJsonPath = path.join(projectDir, "packages/backend/package.json");
|
|
2773
|
+
if (!await fs.pathExists(convexPackageJsonPath)) return;
|
|
2774
|
+
const convexPackageJson = await fs.readJson(convexPackageJsonPath);
|
|
2775
|
+
convexPackageJson.name = `@${options.projectName}/backend`;
|
|
2776
|
+
if (!convexPackageJson.scripts) convexPackageJson.scripts = {};
|
|
2777
|
+
await fs.writeJson(convexPackageJsonPath, convexPackageJson, { spaces: 2 });
|
|
2778
|
+
}
|
|
2779
|
+
async function initializeGit(projectDir, useGit) {
|
|
2780
|
+
if (!useGit) return;
|
|
2781
|
+
const gitVersionResult = await $({
|
|
2782
|
+
cwd: projectDir,
|
|
2783
|
+
reject: false,
|
|
2784
|
+
stderr: "pipe"
|
|
2785
|
+
})`git --version`;
|
|
2786
|
+
if (gitVersionResult.exitCode !== 0) {
|
|
2787
|
+
log.warn(pc.yellow("Git is not installed"));
|
|
2788
|
+
return;
|
|
2563
2789
|
}
|
|
2790
|
+
const result = await $({
|
|
2791
|
+
cwd: projectDir,
|
|
2792
|
+
reject: false,
|
|
2793
|
+
stderr: "pipe"
|
|
2794
|
+
})`git init`;
|
|
2795
|
+
if (result.exitCode !== 0) throw new Error(`Git initialization failed: ${result.stderr}`);
|
|
2564
2796
|
}
|
|
2565
2797
|
|
|
2566
2798
|
//#endregion
|
|
@@ -2592,6 +2824,7 @@ async function createProject(options) {
|
|
|
2592
2824
|
await setupEnvironmentVariables(options);
|
|
2593
2825
|
await updatePackageConfigurations(projectDir, options);
|
|
2594
2826
|
await createReadme(projectDir, options);
|
|
2827
|
+
await writeBtsConfig(options);
|
|
2595
2828
|
await initializeGit(projectDir, options.git);
|
|
2596
2829
|
log.success("Project template successfully scaffolded!");
|
|
2597
2830
|
if (options.install) {
|
|
@@ -2619,48 +2852,145 @@ async function createProject(options) {
|
|
|
2619
2852
|
}
|
|
2620
2853
|
}
|
|
2621
2854
|
|
|
2855
|
+
//#endregion
|
|
2856
|
+
//#region src/types.ts
|
|
2857
|
+
const DatabaseSchema = z.enum([
|
|
2858
|
+
"none",
|
|
2859
|
+
"sqlite",
|
|
2860
|
+
"postgres",
|
|
2861
|
+
"mysql",
|
|
2862
|
+
"mongodb"
|
|
2863
|
+
]).describe("Database type");
|
|
2864
|
+
const ORMSchema = z.enum([
|
|
2865
|
+
"drizzle",
|
|
2866
|
+
"prisma",
|
|
2867
|
+
"mongoose",
|
|
2868
|
+
"none"
|
|
2869
|
+
]).describe("ORM type");
|
|
2870
|
+
const BackendSchema = z.enum([
|
|
2871
|
+
"hono",
|
|
2872
|
+
"express",
|
|
2873
|
+
"fastify",
|
|
2874
|
+
"next",
|
|
2875
|
+
"elysia",
|
|
2876
|
+
"convex",
|
|
2877
|
+
"none"
|
|
2878
|
+
]).describe("Backend framework");
|
|
2879
|
+
const RuntimeSchema = z.enum([
|
|
2880
|
+
"bun",
|
|
2881
|
+
"node",
|
|
2882
|
+
"workers",
|
|
2883
|
+
"none"
|
|
2884
|
+
]).describe("Runtime environment (workers only available with hono backend and drizzle orm)");
|
|
2885
|
+
const FrontendSchema = z.enum([
|
|
2886
|
+
"tanstack-router",
|
|
2887
|
+
"react-router",
|
|
2888
|
+
"tanstack-start",
|
|
2889
|
+
"next",
|
|
2890
|
+
"nuxt",
|
|
2891
|
+
"native-nativewind",
|
|
2892
|
+
"native-unistyles",
|
|
2893
|
+
"svelte",
|
|
2894
|
+
"solid",
|
|
2895
|
+
"none"
|
|
2896
|
+
]).describe("Frontend framework");
|
|
2897
|
+
const AddonsSchema = z.enum([
|
|
2898
|
+
"pwa",
|
|
2899
|
+
"tauri",
|
|
2900
|
+
"starlight",
|
|
2901
|
+
"biome",
|
|
2902
|
+
"husky",
|
|
2903
|
+
"turborepo",
|
|
2904
|
+
"none"
|
|
2905
|
+
]).describe("Additional addons");
|
|
2906
|
+
const ExamplesSchema = z.enum([
|
|
2907
|
+
"todo",
|
|
2908
|
+
"ai",
|
|
2909
|
+
"none"
|
|
2910
|
+
]).describe("Example templates to include");
|
|
2911
|
+
const PackageManagerSchema = z.enum([
|
|
2912
|
+
"npm",
|
|
2913
|
+
"pnpm",
|
|
2914
|
+
"bun"
|
|
2915
|
+
]).describe("Package manager");
|
|
2916
|
+
const DatabaseSetupSchema = z.enum([
|
|
2917
|
+
"turso",
|
|
2918
|
+
"neon",
|
|
2919
|
+
"prisma-postgres",
|
|
2920
|
+
"mongodb-atlas",
|
|
2921
|
+
"supabase",
|
|
2922
|
+
"d1",
|
|
2923
|
+
"none"
|
|
2924
|
+
]).describe("Database hosting setup");
|
|
2925
|
+
const APISchema = z.enum([
|
|
2926
|
+
"trpc",
|
|
2927
|
+
"orpc",
|
|
2928
|
+
"none"
|
|
2929
|
+
]).describe("API type");
|
|
2930
|
+
const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(255, "Project name must be less than 255 characters").refine((name) => name === "." || !name.startsWith("."), "Project name cannot start with a dot (except for '.')").refine((name) => name === "." || !name.startsWith("-"), "Project name cannot start with a dash").refine((name) => {
|
|
2931
|
+
const invalidChars = [
|
|
2932
|
+
"<",
|
|
2933
|
+
">",
|
|
2934
|
+
":",
|
|
2935
|
+
"\"",
|
|
2936
|
+
"|",
|
|
2937
|
+
"?",
|
|
2938
|
+
"*"
|
|
2939
|
+
];
|
|
2940
|
+
return !invalidChars.some((char) => name.includes(char));
|
|
2941
|
+
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
|
|
2942
|
+
|
|
2622
2943
|
//#endregion
|
|
2623
2944
|
//#region src/prompts/addons.ts
|
|
2945
|
+
function getAddonDisplay(addon, isRecommended = false) {
|
|
2946
|
+
let label;
|
|
2947
|
+
let hint;
|
|
2948
|
+
if (addon === "turborepo") {
|
|
2949
|
+
label = isRecommended ? "Turborepo (Recommended)" : "Turborepo";
|
|
2950
|
+
hint = "High-performance build system for JavaScript and TypeScript";
|
|
2951
|
+
} else if (addon === "pwa") {
|
|
2952
|
+
label = "PWA (Progressive Web App)";
|
|
2953
|
+
hint = "Make your app installable and work offline";
|
|
2954
|
+
} else if (addon === "tauri") {
|
|
2955
|
+
label = isRecommended ? "Tauri Desktop App" : "Tauri";
|
|
2956
|
+
hint = "Build native desktop apps from your web frontend";
|
|
2957
|
+
} else if (addon === "biome") {
|
|
2958
|
+
label = "Biome";
|
|
2959
|
+
hint = isRecommended ? "Add Biome for linting and formatting" : "Fast formatter and linter for JavaScript, TypeScript, JSX";
|
|
2960
|
+
} else if (addon === "husky") {
|
|
2961
|
+
label = "Husky";
|
|
2962
|
+
hint = isRecommended ? "Add Git hooks with Husky, lint-staged (requires Biome)" : "Git hooks made easy";
|
|
2963
|
+
} else if (addon === "starlight") {
|
|
2964
|
+
label = "Starlight";
|
|
2965
|
+
hint = isRecommended ? "Add Astro Starlight documentation site" : "Documentation site with Astro";
|
|
2966
|
+
} else {
|
|
2967
|
+
label = addon;
|
|
2968
|
+
hint = `Add ${addon}`;
|
|
2969
|
+
}
|
|
2970
|
+
return {
|
|
2971
|
+
label,
|
|
2972
|
+
hint
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2624
2975
|
async function getAddonsChoice(addons, frontends) {
|
|
2625
2976
|
if (addons !== void 0) return addons;
|
|
2626
|
-
const
|
|
2627
|
-
const
|
|
2628
|
-
const
|
|
2629
|
-
{
|
|
2630
|
-
|
|
2631
|
-
label
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
hint: "Add Astro Starlight documentation site"
|
|
2638
|
-
},
|
|
2639
|
-
{
|
|
2640
|
-
value: "biome",
|
|
2641
|
-
label: "Biome",
|
|
2642
|
-
hint: "Add Biome for linting and formatting"
|
|
2643
|
-
},
|
|
2644
|
-
{
|
|
2645
|
-
value: "husky",
|
|
2646
|
-
label: "Husky",
|
|
2647
|
-
hint: "Add Git hooks with Husky, lint-staged (requires Biome)"
|
|
2648
|
-
},
|
|
2649
|
-
{
|
|
2650
|
-
value: "pwa",
|
|
2651
|
-
label: "PWA (Progressive Web App)",
|
|
2652
|
-
hint: "Make your app installable and work offline"
|
|
2653
|
-
},
|
|
2654
|
-
{
|
|
2655
|
-
value: "tauri",
|
|
2656
|
-
label: "Tauri Desktop App",
|
|
2657
|
-
hint: "Build native desktop apps from your web frontend"
|
|
2977
|
+
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
|
|
2978
|
+
const allPossibleOptions = [];
|
|
2979
|
+
for (const addon of allAddons) {
|
|
2980
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontends || []);
|
|
2981
|
+
if (isCompatible) {
|
|
2982
|
+
const { label, hint } = getAddonDisplay(addon, true);
|
|
2983
|
+
allPossibleOptions.push({
|
|
2984
|
+
value: addon,
|
|
2985
|
+
label,
|
|
2986
|
+
hint
|
|
2987
|
+
});
|
|
2658
2988
|
}
|
|
2659
|
-
|
|
2660
|
-
const options = allPossibleOptions.
|
|
2661
|
-
if (
|
|
2662
|
-
if (
|
|
2663
|
-
return
|
|
2989
|
+
}
|
|
2990
|
+
const options = allPossibleOptions.sort((a, b) => {
|
|
2991
|
+
if (a.value === "turborepo") return -1;
|
|
2992
|
+
if (b.value === "turborepo") return 1;
|
|
2993
|
+
return 0;
|
|
2664
2994
|
});
|
|
2665
2995
|
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => options.some((opt) => opt.value === addonValue));
|
|
2666
2996
|
const response = await multiselect({
|
|
@@ -2676,6 +3006,30 @@ async function getAddonsChoice(addons, frontends) {
|
|
|
2676
3006
|
if (response.includes("husky") && !response.includes("biome")) response.push("biome");
|
|
2677
3007
|
return response;
|
|
2678
3008
|
}
|
|
3009
|
+
async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
3010
|
+
const options = [];
|
|
3011
|
+
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
|
|
3012
|
+
const compatibleAddons = getCompatibleAddons(allAddons, frontend, existingAddons);
|
|
3013
|
+
for (const addon of compatibleAddons) {
|
|
3014
|
+
const { label, hint } = getAddonDisplay(addon, false);
|
|
3015
|
+
options.push({
|
|
3016
|
+
value: addon,
|
|
3017
|
+
label,
|
|
3018
|
+
hint
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
if (options.length === 0) return [];
|
|
3022
|
+
const response = await multiselect({
|
|
3023
|
+
message: "Select addons",
|
|
3024
|
+
options,
|
|
3025
|
+
required: true
|
|
3026
|
+
});
|
|
3027
|
+
if (isCancel(response)) {
|
|
3028
|
+
cancel(pc.red("Operation cancelled"));
|
|
3029
|
+
process.exit(0);
|
|
3030
|
+
}
|
|
3031
|
+
return response;
|
|
3032
|
+
}
|
|
2679
3033
|
|
|
2680
3034
|
//#endregion
|
|
2681
3035
|
//#region src/prompts/api.ts
|
|
@@ -3233,94 +3587,6 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
3233
3587
|
};
|
|
3234
3588
|
}
|
|
3235
3589
|
|
|
3236
|
-
//#endregion
|
|
3237
|
-
//#region src/types.ts
|
|
3238
|
-
const DatabaseSchema = z.enum([
|
|
3239
|
-
"none",
|
|
3240
|
-
"sqlite",
|
|
3241
|
-
"postgres",
|
|
3242
|
-
"mysql",
|
|
3243
|
-
"mongodb"
|
|
3244
|
-
]).describe("Database type");
|
|
3245
|
-
const ORMSchema = z.enum([
|
|
3246
|
-
"drizzle",
|
|
3247
|
-
"prisma",
|
|
3248
|
-
"mongoose",
|
|
3249
|
-
"none"
|
|
3250
|
-
]).describe("ORM type");
|
|
3251
|
-
const BackendSchema = z.enum([
|
|
3252
|
-
"hono",
|
|
3253
|
-
"express",
|
|
3254
|
-
"fastify",
|
|
3255
|
-
"next",
|
|
3256
|
-
"elysia",
|
|
3257
|
-
"convex",
|
|
3258
|
-
"none"
|
|
3259
|
-
]).describe("Backend framework");
|
|
3260
|
-
const RuntimeSchema = z.enum([
|
|
3261
|
-
"bun",
|
|
3262
|
-
"node",
|
|
3263
|
-
"workers",
|
|
3264
|
-
"none"
|
|
3265
|
-
]).describe("Runtime environment (workers only available with hono backend and drizzle orm)");
|
|
3266
|
-
const FrontendSchema = z.enum([
|
|
3267
|
-
"tanstack-router",
|
|
3268
|
-
"react-router",
|
|
3269
|
-
"tanstack-start",
|
|
3270
|
-
"next",
|
|
3271
|
-
"nuxt",
|
|
3272
|
-
"native-nativewind",
|
|
3273
|
-
"native-unistyles",
|
|
3274
|
-
"svelte",
|
|
3275
|
-
"solid",
|
|
3276
|
-
"none"
|
|
3277
|
-
]).describe("Frontend framework");
|
|
3278
|
-
const AddonsSchema = z.enum([
|
|
3279
|
-
"pwa",
|
|
3280
|
-
"tauri",
|
|
3281
|
-
"starlight",
|
|
3282
|
-
"biome",
|
|
3283
|
-
"husky",
|
|
3284
|
-
"turborepo",
|
|
3285
|
-
"none"
|
|
3286
|
-
]).describe("Additional addons");
|
|
3287
|
-
const ExamplesSchema = z.enum([
|
|
3288
|
-
"todo",
|
|
3289
|
-
"ai",
|
|
3290
|
-
"none"
|
|
3291
|
-
]).describe("Example templates to include");
|
|
3292
|
-
const PackageManagerSchema = z.enum([
|
|
3293
|
-
"npm",
|
|
3294
|
-
"pnpm",
|
|
3295
|
-
"bun"
|
|
3296
|
-
]).describe("Package manager");
|
|
3297
|
-
const DatabaseSetupSchema = z.enum([
|
|
3298
|
-
"turso",
|
|
3299
|
-
"neon",
|
|
3300
|
-
"prisma-postgres",
|
|
3301
|
-
"mongodb-atlas",
|
|
3302
|
-
"supabase",
|
|
3303
|
-
"d1",
|
|
3304
|
-
"none"
|
|
3305
|
-
]).describe("Database hosting setup");
|
|
3306
|
-
const APISchema = z.enum([
|
|
3307
|
-
"trpc",
|
|
3308
|
-
"orpc",
|
|
3309
|
-
"none"
|
|
3310
|
-
]).describe("API type");
|
|
3311
|
-
const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(255, "Project name must be less than 255 characters").refine((name) => name === "." || !name.startsWith("."), "Project name cannot start with a dot (except for '.')").refine((name) => name === "." || !name.startsWith("-"), "Project name cannot start with a dash").refine((name) => {
|
|
3312
|
-
const invalidChars = [
|
|
3313
|
-
"<",
|
|
3314
|
-
">",
|
|
3315
|
-
":",
|
|
3316
|
-
"\"",
|
|
3317
|
-
"|",
|
|
3318
|
-
"?",
|
|
3319
|
-
"*"
|
|
3320
|
-
];
|
|
3321
|
-
return !invalidChars.some((char) => name.includes(char));
|
|
3322
|
-
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
|
|
3323
|
-
|
|
3324
3590
|
//#endregion
|
|
3325
3591
|
//#region src/prompts/project-name.ts
|
|
3326
3592
|
function validateDirectoryName(name) {
|
|
@@ -3372,14 +3638,6 @@ async function getProjectName(initialName) {
|
|
|
3372
3638
|
return projectPath;
|
|
3373
3639
|
}
|
|
3374
3640
|
|
|
3375
|
-
//#endregion
|
|
3376
|
-
//#region src/utils/get-latest-cli-version.ts
|
|
3377
|
-
const getLatestCLIVersion = () => {
|
|
3378
|
-
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
3379
|
-
const packageJsonContent = fs.readJSONSync(packageJsonPath);
|
|
3380
|
-
return packageJsonContent.version ?? "1.0.0";
|
|
3381
|
-
};
|
|
3382
|
-
|
|
3383
3641
|
//#endregion
|
|
3384
3642
|
//#region src/utils/analytics.ts
|
|
3385
3643
|
const POSTHOG_API_KEY = "phc_8ZUxEwwfKMajJLvxz1daGd931dYbQrwKNficBmsdIrs";
|
|
@@ -3957,6 +4215,35 @@ async function createProjectHandler(input) {
|
|
|
3957
4215
|
process.exit(1);
|
|
3958
4216
|
}
|
|
3959
4217
|
}
|
|
4218
|
+
async function addAddonsHandler(input) {
|
|
4219
|
+
try {
|
|
4220
|
+
if (!input.addons || input.addons.length === 0) {
|
|
4221
|
+
const projectDir = input.projectDir || process.cwd();
|
|
4222
|
+
const detectedConfig = await detectProjectConfig(projectDir);
|
|
4223
|
+
if (!detectedConfig) {
|
|
4224
|
+
cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
|
|
4225
|
+
process.exit(1);
|
|
4226
|
+
}
|
|
4227
|
+
const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
|
|
4228
|
+
if (addonsPrompt.length === 0) {
|
|
4229
|
+
outro(pc.yellow("No addons to add or all compatible addons are already present."));
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
input.addons = addonsPrompt;
|
|
4233
|
+
}
|
|
4234
|
+
if (!input.addons || input.addons.length === 0) {
|
|
4235
|
+
outro(pc.yellow("No addons specified to add."));
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
await addAddonsToProject({
|
|
4239
|
+
...input,
|
|
4240
|
+
addons: input.addons
|
|
4241
|
+
});
|
|
4242
|
+
} catch (error) {
|
|
4243
|
+
console.error(error);
|
|
4244
|
+
process.exit(1);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
3960
4247
|
const router = t.router({
|
|
3961
4248
|
init: t.procedure.meta({
|
|
3962
4249
|
description: "Create a new Better-T Stack project",
|
|
@@ -3984,6 +4271,15 @@ const router = t.router({
|
|
|
3984
4271
|
};
|
|
3985
4272
|
await createProjectHandler(combinedInput);
|
|
3986
4273
|
}),
|
|
4274
|
+
add: t.procedure.meta({ description: "Add addons to an existing Better-T Stack project" }).input(zod.tuple([zod.object({
|
|
4275
|
+
addons: zod.array(AddonsSchema).optional().default([]),
|
|
4276
|
+
projectDir: zod.string().optional(),
|
|
4277
|
+
install: zod.boolean().optional().default(false).describe("Install dependencies after adding addons"),
|
|
4278
|
+
packageManager: PackageManagerSchema.optional()
|
|
4279
|
+
}).optional().default({})])).mutation(async ({ input }) => {
|
|
4280
|
+
const [options] = input;
|
|
4281
|
+
await addAddonsHandler(options);
|
|
4282
|
+
}),
|
|
3987
4283
|
sponsors: t.procedure.meta({ description: "Show Better-T Stack sponsors" }).mutation(async () => {
|
|
3988
4284
|
try {
|
|
3989
4285
|
renderTitle();
|