create-better-t-stack 2.27.1 → 2.28.1
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/README.md +6 -6
- package/dist/index.js +352 -85
- package/package.json +1 -1
- package/templates/addons/biome/biome.json.hbs +3 -2
- package/templates/addons/ultracite/biome.json.hbs +22 -0
- package/templates/auth/web/react/next/src/components/sign-in-form.tsx +1 -1
- package/templates/auth/web/react/next/src/components/sign-up-form.tsx +1 -1
- package/templates/auth/web/react/react-router/src/components/sign-in-form.tsx +1 -1
- package/templates/auth/web/react/react-router/src/components/sign-up-form.tsx +1 -1
- package/templates/auth/web/react/tanstack-router/src/components/sign-in-form.tsx +1 -1
- package/templates/auth/web/react/tanstack-router/src/components/sign-up-form.tsx +1 -1
- package/templates/auth/web/react/tanstack-start/src/components/sign-in-form.tsx +1 -1
- package/templates/auth/web/react/tanstack-start/src/components/sign-up-form.tsx +1 -1
- package/templates/auth/web/solid/src/components/sign-in-form.tsx +1 -1
- package/templates/auth/web/solid/src/components/sign-up-form.tsx +1 -1
- package/templates/backend/server/next/package.json.hbs +2 -1
- package/templates/backend/server/server-base/package.json.hbs +1 -6
- package/templates/db/prisma/mongodb/prisma.config.ts.hbs +3 -1
- package/templates/db/prisma/mysql/{prisma.config.ts → prisma.config.ts.hbs} +3 -1
- package/templates/db/prisma/postgres/prisma.config.ts.hbs +5 -5
- package/templates/db/prisma/sqlite/{prisma.config.ts → prisma.config.ts.hbs} +3 -1
- package/templates/extras/bunfig.toml +2 -0
- package/templates/frontend/react/tanstack-router/package.json.hbs +12 -11
package/README.md
CHANGED
|
@@ -21,6 +21,12 @@ pnpm create better-t-stack@latest
|
|
|
21
21
|
|
|
22
22
|
Follow the prompts to configure your project or use the `--yes` flag for defaults.
|
|
23
23
|
|
|
24
|
+
## Sponsors
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://sponsors.amanv.dev/sponsors.png" alt="Sponsors">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
24
30
|
## Features
|
|
25
31
|
|
|
26
32
|
| Category | Options |
|
|
@@ -211,9 +217,3 @@ my-better-t-app/
|
|
|
211
217
|
```
|
|
212
218
|
|
|
213
219
|
After project creation, you'll receive detailed instructions for next steps and additional setup requirements.
|
|
214
|
-
|
|
215
|
-
## Sponsors
|
|
216
|
-
|
|
217
|
-
<p align="center">
|
|
218
|
-
<img src="https://sponsors.amanv.dev/sponsors.png" alt="Sponsors" width="300">
|
|
219
|
-
</p>
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
2
|
+
import { cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
3
3
|
import consola, { consola as consola$1 } from "consola";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { createCli, trpcServer } from "trpc-cli";
|
|
@@ -49,8 +49,8 @@ const DEFAULT_CONFIG = {
|
|
|
49
49
|
webDeploy: "none"
|
|
50
50
|
};
|
|
51
51
|
const dependencyVersionMap = {
|
|
52
|
-
"better-auth": "^1.3.
|
|
53
|
-
"@better-auth/expo": "^1.3.
|
|
52
|
+
"better-auth": "^1.3.4",
|
|
53
|
+
"@better-auth/expo": "^1.3.4",
|
|
54
54
|
"drizzle-orm": "^0.44.2",
|
|
55
55
|
"drizzle-kit": "^0.31.2",
|
|
56
56
|
"@libsql/client": "^0.15.9",
|
|
@@ -60,16 +60,18 @@ const dependencyVersionMap = {
|
|
|
60
60
|
"@types/ws": "^8.18.1",
|
|
61
61
|
ws: "^8.18.3",
|
|
62
62
|
mysql2: "^3.14.0",
|
|
63
|
-
"@prisma/client": "^6.
|
|
64
|
-
prisma: "^6.
|
|
63
|
+
"@prisma/client": "^6.13.0",
|
|
64
|
+
prisma: "^6.13.0",
|
|
65
65
|
"@prisma/extension-accelerate": "^2.0.2",
|
|
66
66
|
mongoose: "^8.14.0",
|
|
67
67
|
"vite-plugin-pwa": "^1.0.1",
|
|
68
68
|
"@vite-pwa/assets-generator": "^1.0.0",
|
|
69
69
|
"@tauri-apps/cli": "^2.4.0",
|
|
70
|
-
"@biomejs/biome": "^2.
|
|
70
|
+
"@biomejs/biome": "^2.1.2",
|
|
71
|
+
oxlint: "^1.8.0",
|
|
72
|
+
ultracite: "5.1.1",
|
|
71
73
|
husky: "^9.1.7",
|
|
72
|
-
"lint-staged": "^
|
|
74
|
+
"lint-staged": "^16.1.2",
|
|
73
75
|
tsx: "^4.19.2",
|
|
74
76
|
"@types/node": "^22.13.11",
|
|
75
77
|
"@types/bun": "^1.2.6",
|
|
@@ -130,6 +132,9 @@ const ADDON_COMPATIBILITY = {
|
|
|
130
132
|
husky: [],
|
|
131
133
|
turborepo: [],
|
|
132
134
|
starlight: [],
|
|
135
|
+
ultracite: [],
|
|
136
|
+
oxlint: [],
|
|
137
|
+
fumadocs: [],
|
|
133
138
|
none: []
|
|
134
139
|
};
|
|
135
140
|
const WEB_FRAMEWORKS = [
|
|
@@ -191,6 +196,9 @@ const AddonsSchema = z.enum([
|
|
|
191
196
|
"biome",
|
|
192
197
|
"husky",
|
|
193
198
|
"turborepo",
|
|
199
|
+
"fumadocs",
|
|
200
|
+
"ultracite",
|
|
201
|
+
"oxlint",
|
|
194
202
|
"none"
|
|
195
203
|
]).describe("Additional addons");
|
|
196
204
|
const ExamplesSchema = z.enum([
|
|
@@ -258,87 +266,136 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
|
|
|
258
266
|
|
|
259
267
|
//#endregion
|
|
260
268
|
//#region src/prompts/addons.ts
|
|
261
|
-
function getAddonDisplay(addon
|
|
269
|
+
function getAddonDisplay(addon) {
|
|
262
270
|
let label;
|
|
263
271
|
let hint;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
272
|
+
switch (addon) {
|
|
273
|
+
case "turborepo":
|
|
274
|
+
label = "Turborepo";
|
|
275
|
+
hint = "High-performance build system";
|
|
276
|
+
break;
|
|
277
|
+
case "pwa":
|
|
278
|
+
label = "PWA";
|
|
279
|
+
hint = "Make your app installable and work offline";
|
|
280
|
+
break;
|
|
281
|
+
case "tauri":
|
|
282
|
+
label = "Tauri";
|
|
283
|
+
hint = "Build native desktop apps from your web frontend";
|
|
284
|
+
break;
|
|
285
|
+
case "biome":
|
|
286
|
+
label = "Biome";
|
|
287
|
+
hint = "Format, lint, and more";
|
|
288
|
+
break;
|
|
289
|
+
case "oxlint":
|
|
290
|
+
label = "Oxlint";
|
|
291
|
+
hint = "Rust-powered linter";
|
|
292
|
+
break;
|
|
293
|
+
case "ultracite":
|
|
294
|
+
label = "Ultracite";
|
|
295
|
+
hint = "Zero-config Biome preset with AI integration";
|
|
296
|
+
break;
|
|
297
|
+
case "husky":
|
|
298
|
+
label = "Husky";
|
|
299
|
+
hint = "Modern native Git hooks made easy";
|
|
300
|
+
break;
|
|
301
|
+
case "starlight":
|
|
302
|
+
label = "Starlight";
|
|
303
|
+
hint = "Build stellar docs with astro";
|
|
304
|
+
break;
|
|
305
|
+
case "fumadocs":
|
|
306
|
+
label = "Fumadocs";
|
|
307
|
+
hint = "Build excellent documentation site";
|
|
308
|
+
break;
|
|
309
|
+
default:
|
|
310
|
+
label = addon;
|
|
311
|
+
hint = `Add ${addon}`;
|
|
285
312
|
}
|
|
286
313
|
return {
|
|
287
314
|
label,
|
|
288
315
|
hint
|
|
289
316
|
};
|
|
290
317
|
}
|
|
318
|
+
const ADDON_GROUPS = {
|
|
319
|
+
Documentation: ["starlight", "fumadocs"],
|
|
320
|
+
Linting: [
|
|
321
|
+
"biome",
|
|
322
|
+
"oxlint",
|
|
323
|
+
"ultracite"
|
|
324
|
+
],
|
|
325
|
+
Other: [
|
|
326
|
+
"turborepo",
|
|
327
|
+
"pwa",
|
|
328
|
+
"tauri",
|
|
329
|
+
"husky"
|
|
330
|
+
]
|
|
331
|
+
};
|
|
291
332
|
async function getAddonsChoice(addons, frontends) {
|
|
292
333
|
if (addons !== void 0) return addons;
|
|
293
334
|
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
|
|
294
|
-
const
|
|
335
|
+
const groupedOptions = {
|
|
336
|
+
Documentation: [],
|
|
337
|
+
Linting: [],
|
|
338
|
+
Other: []
|
|
339
|
+
};
|
|
340
|
+
const frontendsArray = frontends || [];
|
|
295
341
|
for (const addon of allAddons) {
|
|
296
|
-
const { isCompatible } = validateAddonCompatibility(addon,
|
|
297
|
-
if (isCompatible)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
|
|
343
|
+
if (!isCompatible) continue;
|
|
344
|
+
const { label, hint } = getAddonDisplay(addon);
|
|
345
|
+
const option = {
|
|
346
|
+
value: addon,
|
|
347
|
+
label,
|
|
348
|
+
hint
|
|
349
|
+
};
|
|
350
|
+
if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
|
|
351
|
+
else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
|
|
352
|
+
else if (ADDON_GROUPS.Other.includes(addon)) groupedOptions.Other.push(option);
|
|
305
353
|
}
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
if (b.value === "turborepo") return 1;
|
|
309
|
-
return 0;
|
|
354
|
+
Object.keys(groupedOptions).forEach((group$1) => {
|
|
355
|
+
if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
|
|
310
356
|
});
|
|
311
|
-
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => options.some((opt) => opt.value === addonValue));
|
|
312
|
-
const response = await
|
|
357
|
+
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue)));
|
|
358
|
+
const response = await groupMultiselect({
|
|
313
359
|
message: "Select addons",
|
|
314
|
-
options,
|
|
360
|
+
options: groupedOptions,
|
|
315
361
|
initialValues,
|
|
316
|
-
required: false
|
|
362
|
+
required: false,
|
|
363
|
+
selectableGroups: false
|
|
317
364
|
});
|
|
318
365
|
if (isCancel(response)) {
|
|
319
366
|
cancel(pc.red("Operation cancelled"));
|
|
320
367
|
process.exit(0);
|
|
321
368
|
}
|
|
322
|
-
if (response.includes("husky") && !response.includes("biome")) response.push("biome");
|
|
323
369
|
return response;
|
|
324
370
|
}
|
|
325
371
|
async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
372
|
+
const groupedOptions = {
|
|
373
|
+
Documentation: [],
|
|
374
|
+
Linting: [],
|
|
375
|
+
Other: []
|
|
376
|
+
};
|
|
377
|
+
const frontendArray = frontend || [];
|
|
378
|
+
const compatibleAddons = getCompatibleAddons(AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons);
|
|
329
379
|
for (const addon of compatibleAddons) {
|
|
330
|
-
const { label, hint } = getAddonDisplay(addon
|
|
331
|
-
|
|
380
|
+
const { label, hint } = getAddonDisplay(addon);
|
|
381
|
+
const option = {
|
|
332
382
|
value: addon,
|
|
333
383
|
label,
|
|
334
384
|
hint
|
|
335
|
-
}
|
|
385
|
+
};
|
|
386
|
+
if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
|
|
387
|
+
else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
|
|
388
|
+
else if (ADDON_GROUPS.Other.includes(addon)) groupedOptions.Other.push(option);
|
|
336
389
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
390
|
+
Object.keys(groupedOptions).forEach((group$1) => {
|
|
391
|
+
if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
|
|
392
|
+
});
|
|
393
|
+
if (Object.keys(groupedOptions).length === 0) return [];
|
|
394
|
+
const response = await groupMultiselect({
|
|
395
|
+
message: "Select addons to add",
|
|
396
|
+
options: groupedOptions,
|
|
397
|
+
required: false,
|
|
398
|
+
selectableGroups: false
|
|
342
399
|
});
|
|
343
400
|
if (isCancel(response)) {
|
|
344
401
|
cancel(pc.red("Operation cancelled"));
|
|
@@ -640,7 +697,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
640
697
|
async function getFrontendChoice(frontendOptions, backend) {
|
|
641
698
|
if (frontendOptions !== void 0) return frontendOptions;
|
|
642
699
|
const frontendTypes = await multiselect({
|
|
643
|
-
message: "Select
|
|
700
|
+
message: "Select project type",
|
|
644
701
|
options: [{
|
|
645
702
|
value: "web",
|
|
646
703
|
label: "Web",
|
|
@@ -1682,6 +1739,70 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
1682
1739
|
}
|
|
1683
1740
|
}
|
|
1684
1741
|
|
|
1742
|
+
//#endregion
|
|
1743
|
+
//#region src/helpers/setup/fumadocs-setup.ts
|
|
1744
|
+
const TEMPLATES = {
|
|
1745
|
+
"next-mdx": {
|
|
1746
|
+
label: "Next.js: Fumadocs MDX",
|
|
1747
|
+
hint: "Recommended template with MDX support",
|
|
1748
|
+
value: "+next+fuma-docs-mdx"
|
|
1749
|
+
},
|
|
1750
|
+
"next-content-collections": {
|
|
1751
|
+
label: "Next.js: Content Collections",
|
|
1752
|
+
hint: "Template using Next.js content collections",
|
|
1753
|
+
value: "+next+content-collections"
|
|
1754
|
+
},
|
|
1755
|
+
"react-router-mdx-remote": {
|
|
1756
|
+
label: "React Router: MDX Remote",
|
|
1757
|
+
hint: "Template for React Router with MDX remote",
|
|
1758
|
+
value: "react-router"
|
|
1759
|
+
},
|
|
1760
|
+
"tanstack-start-mdx-remote": {
|
|
1761
|
+
label: "Tanstack Start: MDX Remote",
|
|
1762
|
+
hint: "Template for Tanstack Start with MDX remote",
|
|
1763
|
+
value: "tanstack-start"
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
async function setupFumadocs(config) {
|
|
1767
|
+
const { packageManager, projectDir } = config;
|
|
1768
|
+
try {
|
|
1769
|
+
log.info("Setting up Fumadocs...");
|
|
1770
|
+
const template = await select({
|
|
1771
|
+
message: "Choose a template",
|
|
1772
|
+
options: Object.entries(TEMPLATES).map(([key, template$1]) => ({
|
|
1773
|
+
value: key,
|
|
1774
|
+
label: template$1.label,
|
|
1775
|
+
hint: template$1.hint
|
|
1776
|
+
})),
|
|
1777
|
+
initialValue: "next-mdx"
|
|
1778
|
+
});
|
|
1779
|
+
if (isCancel(template)) {
|
|
1780
|
+
cancel(pc.red("Operation cancelled"));
|
|
1781
|
+
process.exit(0);
|
|
1782
|
+
}
|
|
1783
|
+
const templateArg = TEMPLATES[template].value;
|
|
1784
|
+
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint`;
|
|
1785
|
+
const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
1786
|
+
await execa(fumadocsInitCommand, {
|
|
1787
|
+
cwd: path.join(projectDir, "apps"),
|
|
1788
|
+
env: { CI: "true" },
|
|
1789
|
+
shell: true
|
|
1790
|
+
});
|
|
1791
|
+
const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
|
|
1792
|
+
const packageJsonPath = path.join(fumadocsDir, "package.json");
|
|
1793
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
1794
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
1795
|
+
packageJson.name = "fumadocs";
|
|
1796
|
+
if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
|
|
1797
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1798
|
+
}
|
|
1799
|
+
log.success("Fumadocs setup successfully!");
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
log.error(pc.red("Failed to set up Fumadocs"));
|
|
1802
|
+
if (error instanceof Error) consola.error(pc.red(error.message));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1685
1806
|
//#endregion
|
|
1686
1807
|
//#region src/helpers/setup/starlight-setup.ts
|
|
1687
1808
|
async function setupStarlight(config) {
|
|
@@ -1770,6 +1891,106 @@ async function setupTauri(config) {
|
|
|
1770
1891
|
}
|
|
1771
1892
|
}
|
|
1772
1893
|
|
|
1894
|
+
//#endregion
|
|
1895
|
+
//#region src/helpers/setup/ultracite-setup.ts
|
|
1896
|
+
const EDITORS = {
|
|
1897
|
+
vscode: {
|
|
1898
|
+
label: "VSCode / Cursor / Windsurf",
|
|
1899
|
+
hint: "Visual Studio Code editor configuration"
|
|
1900
|
+
},
|
|
1901
|
+
zed: {
|
|
1902
|
+
label: "Zed",
|
|
1903
|
+
hint: "Zed editor configuration"
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
const RULES = {
|
|
1907
|
+
"vscode-copilot": {
|
|
1908
|
+
label: "VS Code Copilot",
|
|
1909
|
+
hint: "GitHub Copilot integration for VS Code"
|
|
1910
|
+
},
|
|
1911
|
+
cursor: {
|
|
1912
|
+
label: "Cursor",
|
|
1913
|
+
hint: "Cursor AI editor configuration"
|
|
1914
|
+
},
|
|
1915
|
+
windsurf: {
|
|
1916
|
+
label: "Windsurf",
|
|
1917
|
+
hint: "Windsurf editor configuration"
|
|
1918
|
+
},
|
|
1919
|
+
zed: {
|
|
1920
|
+
label: "Zed",
|
|
1921
|
+
hint: "Zed editor rules"
|
|
1922
|
+
},
|
|
1923
|
+
claude: {
|
|
1924
|
+
label: "Claude",
|
|
1925
|
+
hint: "Claude AI integration"
|
|
1926
|
+
},
|
|
1927
|
+
codex: {
|
|
1928
|
+
label: "Codex",
|
|
1929
|
+
hint: "Codex AI integration"
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
async function setupUltracite(config, hasHusky) {
|
|
1933
|
+
const { packageManager, projectDir } = config;
|
|
1934
|
+
try {
|
|
1935
|
+
log.info("Setting up Ultracite...");
|
|
1936
|
+
await setupBiome(projectDir);
|
|
1937
|
+
const editors = await multiselect({
|
|
1938
|
+
message: "Choose editors",
|
|
1939
|
+
options: Object.entries(EDITORS).map(([key, editor]) => ({
|
|
1940
|
+
value: key,
|
|
1941
|
+
label: editor.label,
|
|
1942
|
+
hint: editor.hint
|
|
1943
|
+
})),
|
|
1944
|
+
required: false
|
|
1945
|
+
});
|
|
1946
|
+
if (isCancel(editors)) {
|
|
1947
|
+
cancel(pc.red("Operation cancelled"));
|
|
1948
|
+
process.exit(0);
|
|
1949
|
+
}
|
|
1950
|
+
const rules = await multiselect({
|
|
1951
|
+
message: "Choose rules",
|
|
1952
|
+
options: Object.entries(RULES).map(([key, rule]) => ({
|
|
1953
|
+
value: key,
|
|
1954
|
+
label: rule.label,
|
|
1955
|
+
hint: rule.hint
|
|
1956
|
+
})),
|
|
1957
|
+
required: false
|
|
1958
|
+
});
|
|
1959
|
+
if (isCancel(rules)) {
|
|
1960
|
+
cancel(pc.red("Operation cancelled"));
|
|
1961
|
+
process.exit(0);
|
|
1962
|
+
}
|
|
1963
|
+
const ultraciteArgs = [
|
|
1964
|
+
"init",
|
|
1965
|
+
"--pm",
|
|
1966
|
+
packageManager
|
|
1967
|
+
];
|
|
1968
|
+
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
1969
|
+
if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
|
|
1970
|
+
if (hasHusky) ultraciteArgs.push("--features", "husky", "lint-staged");
|
|
1971
|
+
const ultraciteArgsString = ultraciteArgs.join(" ");
|
|
1972
|
+
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
|
1973
|
+
const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
1974
|
+
await execa(ultraciteInitCommand, {
|
|
1975
|
+
cwd: projectDir,
|
|
1976
|
+
env: { CI: "true" },
|
|
1977
|
+
shell: true
|
|
1978
|
+
});
|
|
1979
|
+
if (hasHusky) await addPackageDependency({
|
|
1980
|
+
devDependencies: ["husky", "lint-staged"],
|
|
1981
|
+
projectDir
|
|
1982
|
+
});
|
|
1983
|
+
await addPackageDependency({
|
|
1984
|
+
devDependencies: ["ultracite"],
|
|
1985
|
+
projectDir
|
|
1986
|
+
});
|
|
1987
|
+
log.success("Ultracite setup successfully!");
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
log.error(pc.red("Failed to set up Ultracite"));
|
|
1990
|
+
if (error instanceof Error) console.error(pc.red(error.message));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1773
1994
|
//#endregion
|
|
1774
1995
|
//#region src/utils/ts-morph.ts
|
|
1775
1996
|
const tsProject = new Project({
|
|
@@ -1824,7 +2045,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
|
1824
2045
|
//#endregion
|
|
1825
2046
|
//#region src/helpers/setup/addons-setup.ts
|
|
1826
2047
|
async function setupAddons(config, isAddCommand = false) {
|
|
1827
|
-
const { addons, frontend, projectDir } = config;
|
|
2048
|
+
const { addons, frontend, projectDir, packageManager } = config;
|
|
1828
2049
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
1829
2050
|
const hasNuxtFrontend = frontend.includes("nuxt");
|
|
1830
2051
|
const hasSvelteFrontend = frontend.includes("svelte");
|
|
@@ -1845,9 +2066,23 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
|
|
1845
2066
|
}
|
|
1846
2067
|
if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
|
|
1847
2068
|
if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
|
|
1848
|
-
|
|
1849
|
-
|
|
2069
|
+
const hasUltracite = addons.includes("ultracite");
|
|
2070
|
+
const hasBiome = addons.includes("biome");
|
|
2071
|
+
const hasHusky = addons.includes("husky");
|
|
2072
|
+
const hasOxlint = addons.includes("oxlint");
|
|
2073
|
+
if (hasUltracite) await setupUltracite(config, hasHusky);
|
|
2074
|
+
else {
|
|
2075
|
+
if (hasBiome) await setupBiome(projectDir);
|
|
2076
|
+
if (hasHusky) {
|
|
2077
|
+
let linter;
|
|
2078
|
+
if (hasOxlint) linter = "oxlint";
|
|
2079
|
+
else if (hasBiome) linter = "biome";
|
|
2080
|
+
await setupHusky(projectDir, linter);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
|
|
1850
2084
|
if (addons.includes("starlight")) await setupStarlight(config);
|
|
2085
|
+
if (addons.includes("fumadocs")) await setupFumadocs(config);
|
|
1851
2086
|
}
|
|
1852
2087
|
function getWebAppDir(projectDir, frontends) {
|
|
1853
2088
|
if (frontends.some((f) => [
|
|
@@ -1874,7 +2109,7 @@ async function setupBiome(projectDir) {
|
|
|
1874
2109
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1875
2110
|
}
|
|
1876
2111
|
}
|
|
1877
|
-
async function setupHusky(projectDir) {
|
|
2112
|
+
async function setupHusky(projectDir, linter) {
|
|
1878
2113
|
await addPackageDependency({
|
|
1879
2114
|
devDependencies: ["husky", "lint-staged"],
|
|
1880
2115
|
projectDir
|
|
@@ -1886,7 +2121,9 @@ async function setupHusky(projectDir) {
|
|
|
1886
2121
|
...packageJson.scripts,
|
|
1887
2122
|
prepare: "husky"
|
|
1888
2123
|
};
|
|
1889
|
-
packageJson["lint-staged"] = { "
|
|
2124
|
+
if (linter === "oxlint") packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint" };
|
|
2125
|
+
else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
|
|
2126
|
+
else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
|
|
1890
2127
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1891
2128
|
}
|
|
1892
2129
|
}
|
|
@@ -1916,6 +2153,27 @@ async function setupPwa(projectDir, frontends) {
|
|
|
1916
2153
|
const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
|
|
1917
2154
|
if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
|
|
1918
2155
|
}
|
|
2156
|
+
async function setupOxlint(projectDir, packageManager) {
|
|
2157
|
+
await addPackageDependency({
|
|
2158
|
+
devDependencies: ["oxlint"],
|
|
2159
|
+
projectDir
|
|
2160
|
+
});
|
|
2161
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
2162
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
2163
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2164
|
+
packageJson.scripts = {
|
|
2165
|
+
...packageJson.scripts,
|
|
2166
|
+
check: "oxlint"
|
|
2167
|
+
};
|
|
2168
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2169
|
+
}
|
|
2170
|
+
const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
|
|
2171
|
+
await execa(oxlintInitCommand, {
|
|
2172
|
+
cwd: projectDir,
|
|
2173
|
+
env: { CI: "true" },
|
|
2174
|
+
shell: true
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
1919
2177
|
|
|
1920
2178
|
//#endregion
|
|
1921
2179
|
//#region src/helpers/project-generation/detect-project-config.ts
|
|
@@ -2319,6 +2577,11 @@ async function handleExtras(projectDir, context) {
|
|
|
2319
2577
|
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
|
2320
2578
|
if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
|
2321
2579
|
}
|
|
2580
|
+
if (context.packageManager === "bun") {
|
|
2581
|
+
const bunfigSrc = path.join(extrasDir, "bunfig.toml");
|
|
2582
|
+
const bunfigDest = path.join(projectDir, "bunfig.toml");
|
|
2583
|
+
if (await fs.pathExists(bunfigSrc)) await fs.copy(bunfigSrc, bunfigDest);
|
|
2584
|
+
}
|
|
2322
2585
|
if (context.packageManager === "pnpm" && (hasNative || context.frontend.includes("nuxt"))) {
|
|
2323
2586
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
2324
2587
|
const npmrcDest = path.join(projectDir, ".npmrc");
|
|
@@ -3334,6 +3597,10 @@ const NEON_REGIONS = [
|
|
|
3334
3597
|
label: "AWS Asia Pacific (Singapore)",
|
|
3335
3598
|
value: "aws-ap-southeast-1"
|
|
3336
3599
|
},
|
|
3600
|
+
{
|
|
3601
|
+
label: "AWS South America East 1 (São Paulo)",
|
|
3602
|
+
value: "aws-sa-east-1"
|
|
3603
|
+
},
|
|
3337
3604
|
{
|
|
3338
3605
|
label: "AWS Asia Pacific (Sydney)",
|
|
3339
3606
|
value: "aws-ap-southeast-2"
|
|
@@ -3472,7 +3739,7 @@ async function setupNeonPostgres(config) {
|
|
|
3472
3739
|
//#region src/helpers/database-providers/prisma-postgres-setup.ts
|
|
3473
3740
|
async function setupWithCreateDb(serverDir, packageManager, orm) {
|
|
3474
3741
|
try {
|
|
3475
|
-
log.info("Starting Prisma
|
|
3742
|
+
log.info("Starting Prisma Postgres setup. Please follow the instructions below:");
|
|
3476
3743
|
const createDbCommand = getPackageExecutionCommand(packageManager, "create-db@latest -i");
|
|
3477
3744
|
await execa(createDbCommand, {
|
|
3478
3745
|
cwd: serverDir,
|
|
@@ -3539,6 +3806,16 @@ async function writeEnvFile$1(projectDir, config) {
|
|
|
3539
3806
|
consola$1.error("Failed to update environment configuration");
|
|
3540
3807
|
}
|
|
3541
3808
|
}
|
|
3809
|
+
async function addDotenvImportToPrismaConfig(projectDir) {
|
|
3810
|
+
try {
|
|
3811
|
+
const prismaConfigPath = path.join(projectDir, "apps/server/prisma.config.ts");
|
|
3812
|
+
let content = await fs.readFile(prismaConfigPath, "utf8");
|
|
3813
|
+
content = `import "dotenv/config";\n${content}`;
|
|
3814
|
+
await fs.writeFile(prismaConfigPath, content);
|
|
3815
|
+
} catch (_error) {
|
|
3816
|
+
consola$1.error("Failed to update prisma.config.ts");
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3542
3819
|
function displayManualSetupInstructions$1() {
|
|
3543
3820
|
log.info(`Manual Prisma PostgreSQL Setup Instructions:
|
|
3544
3821
|
|
|
@@ -3596,7 +3873,7 @@ async function setupPrismaPostgres(config) {
|
|
|
3596
3873
|
hint: "More control (requires auth)"
|
|
3597
3874
|
});
|
|
3598
3875
|
const setupMethod = await select({
|
|
3599
|
-
message: "Choose your Prisma setup method:",
|
|
3876
|
+
message: "Choose your Prisma Postgres setup method:",
|
|
3600
3877
|
options: setupOptions,
|
|
3601
3878
|
initialValue: "create-db"
|
|
3602
3879
|
});
|
|
@@ -3609,20 +3886,15 @@ async function setupPrismaPostgres(config) {
|
|
|
3609
3886
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
3610
3887
|
if (prismaConfig) {
|
|
3611
3888
|
await writeEnvFile$1(projectDir, prismaConfig);
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
}
|
|
3616
|
-
log.success(pc.green("Prisma PostgreSQL database configured successfully!"));
|
|
3889
|
+
await addDotenvImportToPrismaConfig(projectDir);
|
|
3890
|
+
if (orm === "prisma") await addPrismaAccelerateExtension(serverDir);
|
|
3891
|
+
log.success(pc.green("Prisma Postgres database configured successfully!"));
|
|
3617
3892
|
} else {
|
|
3618
|
-
const fallbackSpinner = spinner();
|
|
3619
|
-
fallbackSpinner.start("Setting up fallback configuration...");
|
|
3620
3893
|
await writeEnvFile$1(projectDir);
|
|
3621
|
-
fallbackSpinner.stop("Fallback configuration ready");
|
|
3622
3894
|
displayManualSetupInstructions$1();
|
|
3623
3895
|
}
|
|
3624
3896
|
} catch (error) {
|
|
3625
|
-
consola$1.error(pc.red(`Error during Prisma
|
|
3897
|
+
consola$1.error(pc.red(`Error during Prisma Postgres setup: ${error instanceof Error ? error.message : String(error)}`));
|
|
3626
3898
|
try {
|
|
3627
3899
|
await writeEnvFile$1(projectDir);
|
|
3628
3900
|
displayManualSetupInstructions$1();
|
|
@@ -4553,6 +4825,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
4553
4825
|
else if (!hasNative && !addons?.includes("starlight")) output += `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`;
|
|
4554
4826
|
if (!isConvex) output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
|
|
4555
4827
|
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
4828
|
+
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
4556
4829
|
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
|
4557
4830
|
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
|
4558
4831
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
@@ -4599,8 +4872,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4599
4872
|
instructions.push("");
|
|
4600
4873
|
}
|
|
4601
4874
|
if (orm === "prisma") {
|
|
4602
|
-
if (
|
|
4603
|
-
if (runtime === "bun") instructions.push(`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`);
|
|
4875
|
+
if (dbSetup === "turso") instructions.push(`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`, `Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso`);
|
|
4604
4876
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination may not work.`);
|
|
4605
4877
|
if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
|
|
4606
4878
|
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
@@ -4760,11 +5032,6 @@ async function updateRootPackageJson(projectDir, options) {
|
|
|
4760
5032
|
scripts["db:down"] = `bun run --filter ${backendPackageName} db:down`;
|
|
4761
5033
|
}
|
|
4762
5034
|
}
|
|
4763
|
-
if (options.addons.includes("biome")) scripts.check = "biome check --write .";
|
|
4764
|
-
if (options.addons.includes("husky")) {
|
|
4765
|
-
scripts.prepare = "husky";
|
|
4766
|
-
packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
|
|
4767
|
-
}
|
|
4768
5035
|
try {
|
|
4769
5036
|
const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir });
|
|
4770
5037
|
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
|
@@ -4792,9 +5059,9 @@ async function updateServerPackageJson(projectDir, options) {
|
|
|
4792
5059
|
if (options.database !== "none") {
|
|
4793
5060
|
if (options.database === "sqlite" && options.orm === "drizzle" && options.dbSetup !== "d1") scripts["db:local"] = "turso dev --db-file local.db";
|
|
4794
5061
|
if (options.orm === "prisma") {
|
|
4795
|
-
scripts["db:push"] = "prisma db push
|
|
5062
|
+
scripts["db:push"] = "prisma db push";
|
|
4796
5063
|
scripts["db:studio"] = "prisma studio";
|
|
4797
|
-
scripts["db:generate"] = "prisma generate
|
|
5064
|
+
scripts["db:generate"] = "prisma generate";
|
|
4798
5065
|
scripts["db:migrate"] = "prisma migrate dev";
|
|
4799
5066
|
} else if (options.orm === "drizzle") {
|
|
4800
5067
|
scripts["db:push"] = "drizzle-kit push";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.1",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
|
3
3
|
"vcs": {
|
|
4
4
|
"enabled": false,
|
|
5
5
|
"clientKind": "git",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"!**/.nuxt",
|
|
21
21
|
"!bts.jsonc",
|
|
22
22
|
"!**/.expo",
|
|
23
|
-
"!**/.wrangler"
|
|
23
|
+
"!**/.wrangler",
|
|
24
|
+
"!**/.source"
|
|
24
25
|
]
|
|
25
26
|
},
|
|
26
27
|
"formatter": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"ignoreUnknown": false,
|
|
5
|
+
"includes": [
|
|
6
|
+
"**",
|
|
7
|
+
"!**/.next",
|
|
8
|
+
"!**/dist",
|
|
9
|
+
"!**/.turbo",
|
|
10
|
+
"!**/dev-dist",
|
|
11
|
+
"!**/.zed",
|
|
12
|
+
"!**/.vscode",
|
|
13
|
+
"!**/routeTree.gen.ts",
|
|
14
|
+
"!**/src-tauri",
|
|
15
|
+
"!**/.nuxt",
|
|
16
|
+
"!bts.jsonc",
|
|
17
|
+
"!**/.expo",
|
|
18
|
+
"!**/.wrangler",
|
|
19
|
+
"!**/.source"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"next": "15.3.0",
|
|
12
12
|
"react": "^19.0.0",
|
|
13
13
|
"react-dom": "^19.0.0",
|
|
14
|
-
"dotenv": "^
|
|
14
|
+
"dotenv": "^17.2.1"
|
|
15
15
|
},
|
|
16
16
|
{{#if (eq dbSetup 'supabase')}}
|
|
17
17
|
"trustedDependencies": [
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20",
|
|
23
23
|
"@types/react": "^19",
|
|
24
|
+
"zod": "^4.0.13",
|
|
24
25
|
"typescript": "^5"
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -7,13 +7,8 @@
|
|
|
7
7
|
"check-types": "tsc -b",
|
|
8
8
|
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server"
|
|
9
9
|
},
|
|
10
|
-
{{#if (eq orm 'prisma')}}
|
|
11
|
-
"prisma": {
|
|
12
|
-
"schema": "./schema"
|
|
13
|
-
},
|
|
14
|
-
{{/if}}
|
|
15
10
|
"dependencies": {
|
|
16
|
-
"dotenv": "^
|
|
11
|
+
"dotenv": "^17.2.1",
|
|
17
12
|
"zod": "^4.0.2"
|
|
18
13
|
},
|
|
19
14
|
{{#if (eq dbSetup 'supabase')}}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
{{#
|
|
2
|
-
// import "dotenv/config"; uncomment this to load .env
|
|
3
|
-
{{else}}
|
|
1
|
+
{{#unless (eq dbSetup "prisma-postgres")}}
|
|
4
2
|
import "dotenv/config";
|
|
5
|
-
{{/
|
|
3
|
+
{{/unless}}
|
|
6
4
|
import path from "node:path";
|
|
7
5
|
import type { PrismaConfig } from "prisma";
|
|
8
6
|
|
|
9
7
|
export default {
|
|
10
|
-
earlyAccess: true,
|
|
11
8
|
schema: path.join("prisma", "schema"),
|
|
9
|
+
migrations: {
|
|
10
|
+
path: path.join("prisma", "migrations"),
|
|
11
|
+
}
|
|
12
12
|
} satisfies PrismaConfig;
|
|
@@ -10,17 +10,6 @@
|
|
|
10
10
|
"start": "vite",
|
|
11
11
|
"check-types": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@tanstack/react-router-devtools": "^1.114.27",
|
|
15
|
-
"@tanstack/router-plugin": "^1.114.27",
|
|
16
|
-
"@types/node": "^22.13.13",
|
|
17
|
-
"@types/react": "^19.0.12",
|
|
18
|
-
"@types/react-dom": "^19.0.4",
|
|
19
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
20
|
-
"postcss": "^8.5.3",
|
|
21
|
-
"tailwindcss": "^4.0.15",
|
|
22
|
-
"vite": "^6.2.2"
|
|
23
|
-
},
|
|
24
13
|
"dependencies": {
|
|
25
14
|
"@hookform/resolvers": "^5.1.1",
|
|
26
15
|
"radix-ui": "^1.4.2",
|
|
@@ -37,5 +26,17 @@
|
|
|
37
26
|
"tailwind-merge": "^3.3.1",
|
|
38
27
|
"tw-animate-css": "^1.2.5",
|
|
39
28
|
"zod": "^4.0.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tanstack/react-router-devtools": "^1.114.27",
|
|
32
|
+
"@tanstack/router-plugin": "^1.114.27",
|
|
33
|
+
"@types/node": "^22.13.13",
|
|
34
|
+
"@types/react": "^19.0.12",
|
|
35
|
+
"@types/react-dom": "^19.0.4",
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
37
|
+
"postcss": "^8.5.3",
|
|
38
|
+
"typescript": "^5.8.3",
|
|
39
|
+
"tailwindcss": "^4.0.15",
|
|
40
|
+
"vite": "^6.2.2"
|
|
40
41
|
}
|
|
41
42
|
}
|