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.
Files changed (23) hide show
  1. package/README.md +6 -6
  2. package/dist/index.js +352 -85
  3. package/package.json +1 -1
  4. package/templates/addons/biome/biome.json.hbs +3 -2
  5. package/templates/addons/ultracite/biome.json.hbs +22 -0
  6. package/templates/auth/web/react/next/src/components/sign-in-form.tsx +1 -1
  7. package/templates/auth/web/react/next/src/components/sign-up-form.tsx +1 -1
  8. package/templates/auth/web/react/react-router/src/components/sign-in-form.tsx +1 -1
  9. package/templates/auth/web/react/react-router/src/components/sign-up-form.tsx +1 -1
  10. package/templates/auth/web/react/tanstack-router/src/components/sign-in-form.tsx +1 -1
  11. package/templates/auth/web/react/tanstack-router/src/components/sign-up-form.tsx +1 -1
  12. package/templates/auth/web/react/tanstack-start/src/components/sign-in-form.tsx +1 -1
  13. package/templates/auth/web/react/tanstack-start/src/components/sign-up-form.tsx +1 -1
  14. package/templates/auth/web/solid/src/components/sign-in-form.tsx +1 -1
  15. package/templates/auth/web/solid/src/components/sign-up-form.tsx +1 -1
  16. package/templates/backend/server/next/package.json.hbs +2 -1
  17. package/templates/backend/server/server-base/package.json.hbs +1 -6
  18. package/templates/db/prisma/mongodb/prisma.config.ts.hbs +3 -1
  19. package/templates/db/prisma/mysql/{prisma.config.ts → prisma.config.ts.hbs} +3 -1
  20. package/templates/db/prisma/postgres/prisma.config.ts.hbs +5 -5
  21. package/templates/db/prisma/sqlite/{prisma.config.ts → prisma.config.ts.hbs} +3 -1
  22. package/templates/extras/bunfig.toml +2 -0
  23. 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.0",
53
- "@better-auth/expo": "^1.3.0",
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.12.0",
64
- prisma: "^6.12.0",
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.0.0",
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": "^15.5.0",
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, isRecommended = false) {
269
+ function getAddonDisplay(addon) {
262
270
  let label;
263
271
  let hint;
264
- if (addon === "turborepo") {
265
- label = isRecommended ? "Turborepo (Recommended)" : "Turborepo";
266
- hint = "High-performance build system for JavaScript and TypeScript";
267
- } else if (addon === "pwa") {
268
- label = "PWA (Progressive Web App)";
269
- hint = "Make your app installable and work offline";
270
- } else if (addon === "tauri") {
271
- label = isRecommended ? "Tauri Desktop App" : "Tauri";
272
- hint = "Build native desktop apps from your web frontend";
273
- } else if (addon === "biome") {
274
- label = "Biome";
275
- hint = isRecommended ? "Add Biome for linting and formatting" : "Fast formatter and linter for JavaScript, TypeScript, JSX";
276
- } else if (addon === "husky") {
277
- label = "Husky";
278
- hint = isRecommended ? "Add Git hooks with Husky, lint-staged (requires Biome)" : "Git hooks made easy";
279
- } else if (addon === "starlight") {
280
- label = "Starlight";
281
- hint = isRecommended ? "Add Astro Starlight documentation site" : "Documentation site with Astro";
282
- } else {
283
- label = addon;
284
- hint = `Add ${addon}`;
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 allPossibleOptions = [];
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, frontends || []);
297
- if (isCompatible) {
298
- const { label, hint } = getAddonDisplay(addon, true);
299
- allPossibleOptions.push({
300
- value: addon,
301
- label,
302
- hint
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
- const options = allPossibleOptions.sort((a, b) => {
307
- if (a.value === "turborepo") return -1;
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 multiselect({
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 options = [];
327
- const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
328
- const compatibleAddons = getCompatibleAddons(allAddons, frontend, existingAddons);
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, false);
331
- options.push({
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
- if (options.length === 0) return [];
338
- const response = await multiselect({
339
- message: "Select addons",
340
- options,
341
- required: false
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 platforms to develop for",
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
- if (addons.includes("biome")) await setupBiome(projectDir);
1849
- if (addons.includes("husky")) await setupHusky(projectDir);
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"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
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 PostgreSQL setup. Please follow the instructions below:");
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
- if (orm === "prisma") {
3613
- await addPrismaAccelerateExtension(serverDir);
3614
- log.info(pc.cyan("NOTE: Make sure to uncomment `import \"dotenv/config\";` in `apps/server/src/prisma.config.ts` to load environment variables."));
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 PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`));
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 (database === "sqlite") 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`);
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 --schema ./prisma/schema";
5062
+ scripts["db:push"] = "prisma db push";
4796
5063
  scripts["db:studio"] = "prisma studio";
4797
- scripts["db:generate"] = "prisma generate --schema ./prisma/schema";
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.27.1",
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.0.0/schema.json",
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
+ }
@@ -58,7 +58,7 @@ export default function SignInForm({
58
58
  onSubmit={(e) => {
59
59
  e.preventDefault();
60
60
  e.stopPropagation();
61
- void form.handleSubmit();
61
+ form.handleSubmit();
62
62
  }}
63
63
  className="space-y-4"
64
64
  >
@@ -61,7 +61,7 @@ export default function SignUpForm({
61
61
  onSubmit={(e) => {
62
62
  e.preventDefault();
63
63
  e.stopPropagation();
64
- void form.handleSubmit();
64
+ form.handleSubmit();
65
65
  }}
66
66
  className="space-y-4"
67
67
  >
@@ -58,7 +58,7 @@ export default function SignInForm({
58
58
  onSubmit={(e) => {
59
59
  e.preventDefault();
60
60
  e.stopPropagation();
61
- void form.handleSubmit();
61
+ form.handleSubmit();
62
62
  }}
63
63
  className="space-y-4"
64
64
  >
@@ -61,7 +61,7 @@ export default function SignUpForm({
61
61
  onSubmit={(e) => {
62
62
  e.preventDefault();
63
63
  e.stopPropagation();
64
- void form.handleSubmit();
64
+ form.handleSubmit();
65
65
  }}
66
66
  className="space-y-4"
67
67
  >
@@ -62,7 +62,7 @@ export default function SignInForm({
62
62
  onSubmit={(e) => {
63
63
  e.preventDefault();
64
64
  e.stopPropagation();
65
- void form.handleSubmit();
65
+ form.handleSubmit();
66
66
  }}
67
67
  className="space-y-4"
68
68
  >
@@ -65,7 +65,7 @@ export default function SignUpForm({
65
65
  onSubmit={(e) => {
66
66
  e.preventDefault();
67
67
  e.stopPropagation();
68
- void form.handleSubmit();
68
+ form.handleSubmit();
69
69
  }}
70
70
  className="space-y-4"
71
71
  >
@@ -62,7 +62,7 @@ export default function SignInForm({
62
62
  onSubmit={(e) => {
63
63
  e.preventDefault();
64
64
  e.stopPropagation();
65
- void form.handleSubmit();
65
+ form.handleSubmit();
66
66
  }}
67
67
  className="space-y-4"
68
68
  >
@@ -65,7 +65,7 @@ export default function SignUpForm({
65
65
  onSubmit={(e) => {
66
66
  e.preventDefault();
67
67
  e.stopPropagation();
68
- void form.handleSubmit();
68
+ form.handleSubmit();
69
69
  }}
70
70
  className="space-y-4"
71
71
  >
@@ -53,7 +53,7 @@ export default function SignInForm({
53
53
  onSubmit={(e) => {
54
54
  e.preventDefault();
55
55
  e.stopPropagation();
56
- void form.handleSubmit();
56
+ form.handleSubmit();
57
57
  }}
58
58
  class="space-y-4"
59
59
  >
@@ -56,7 +56,7 @@ export default function SignUpForm({
56
56
  onSubmit={(e) => {
57
57
  e.preventDefault();
58
58
  e.stopPropagation();
59
- void form.handleSubmit();
59
+ form.handleSubmit();
60
60
  }}
61
61
  class="space-y-4"
62
62
  >
@@ -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": "^16.5.0"
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": "^16.4.7",
11
+ "dotenv": "^17.2.1",
17
12
  "zod": "^4.0.2"
18
13
  },
19
14
  {{#if (eq dbSetup 'supabase')}}
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import type { PrismaConfig } from "prisma";
4
4
 
5
5
  export default {
6
- earlyAccess: true,
7
6
  schema: path.join("prisma", "schema"),
7
+ migrations: {
8
+ path: path.join("prisma", "migrations"),
9
+ }
8
10
  } satisfies PrismaConfig;
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import type { PrismaConfig } from "prisma";
4
4
 
5
5
  export default {
6
- earlyAccess: true,
7
6
  schema: path.join("prisma", "schema"),
7
+ migrations: {
8
+ path: path.join("prisma", "migrations"),
9
+ }
8
10
  } satisfies PrismaConfig;
@@ -1,12 +1,12 @@
1
- {{#if (eq dbSetup "prisma-postgres")}}
2
- // import "dotenv/config"; uncomment this to load .env
3
- {{else}}
1
+ {{#unless (eq dbSetup "prisma-postgres")}}
4
2
  import "dotenv/config";
5
- {{/if}}
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;
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import type { PrismaConfig } from "prisma";
4
4
 
5
5
  export default {
6
- earlyAccess: true,
7
6
  schema: path.join("prisma", "schema"),
7
+ migrations: {
8
+ path: path.join("prisma", "migrations"),
9
+ }
8
10
  } satisfies PrismaConfig;
@@ -0,0 +1,2 @@
1
+ [install]
2
+ linker = "isolated"
@@ -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
  }