create-better-t-stack 2.27.1 → 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 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.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 (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 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"
@@ -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.27.1",
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.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
  >
@@ -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
  }