create-better-t-stack 2.27.0 → 2.28.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/README.md +6 -6
- package/dist/index.js +332 -69
- 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/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";
|
|
@@ -67,9 +67,11 @@ const dependencyVersionMap = {
|
|
|
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 (Progressive Web App)";
|
|
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"
|
|
@@ -3588,12 +3855,12 @@ async function setupPrismaPostgres(config) {
|
|
|
3588
3855
|
const setupOptions = [{
|
|
3589
3856
|
label: "Quick setup with create-db",
|
|
3590
3857
|
value: "create-db",
|
|
3591
|
-
hint: "Fastest, automated database creation"
|
|
3858
|
+
hint: "Fastest, automated database creation (no auth)"
|
|
3592
3859
|
}];
|
|
3593
3860
|
if (orm === "prisma") setupOptions.push({
|
|
3594
|
-
label: "Custom setup with Prisma
|
|
3861
|
+
label: "Custom setup with Prisma Init",
|
|
3595
3862
|
value: "custom",
|
|
3596
|
-
hint: "More control
|
|
3863
|
+
hint: "More control (requires auth)"
|
|
3597
3864
|
});
|
|
3598
3865
|
const setupMethod = await select({
|
|
3599
3866
|
message: "Choose your Prisma setup method:",
|
|
@@ -4553,6 +4820,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
4553
4820
|
else if (!hasNative && !addons?.includes("starlight")) output += `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`;
|
|
4554
4821
|
if (!isConvex) output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
|
|
4555
4822
|
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
4823
|
+
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
4556
4824
|
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
|
4557
4825
|
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
|
4558
4826
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
@@ -4760,11 +5028,6 @@ async function updateRootPackageJson(projectDir, options) {
|
|
|
4760
5028
|
scripts["db:down"] = `bun run --filter ${backendPackageName} db:down`;
|
|
4761
5029
|
}
|
|
4762
5030
|
}
|
|
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
5031
|
try {
|
|
4769
5032
|
const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir });
|
|
4770
5033
|
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.0",
|
|
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
|
+
}
|
|
@@ -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
|
}
|