better-ts-stack 2.0.2 → 2.1.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.
Files changed (40) hide show
  1. package/README.md +114 -113
  2. package/dist/args.js +24 -20
  3. package/dist/builder/buildPlanner.js +1 -1
  4. package/dist/builder/configGenerator.js +1 -5
  5. package/dist/builder/index.js +15 -3
  6. package/dist/builder/moduleSelector.js +4 -2
  7. package/dist/builder/readmeGenerator.js +101 -0
  8. package/dist/builder/templateContext.js +30 -0
  9. package/dist/index.js +7 -1
  10. package/dist/output/nextSteps.js +19 -22
  11. package/dist/prompts/index.js +2 -1
  12. package/dist/prompts/stack.js +21 -2
  13. package/dist/types/index.js +1 -0
  14. package/dist/ui/messages.js +2 -1
  15. package/dist/validators/index.js +5 -2
  16. package/package.json +1 -1
  17. package/templates/backend/express/config.json +42 -40
  18. package/templates/backend/express/src/env.ts.hbs +39 -0
  19. package/templates/backend/express/src/index.ts.hbs +111 -75
  20. package/templates/backend/nestjs/config.json +43 -41
  21. package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
  22. package/templates/backend/nestjs/src/env.ts.hbs +39 -0
  23. package/templates/backend/nestjs/src/main.ts +3 -2
  24. package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
  25. package/templates/frontend/tanstack/config.json +2 -2
  26. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
  27. package/templates/frontend/tanstack/vite.config.ts +22 -14
  28. package/templates/modules/auth/express/config.json +20 -20
  29. package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
  30. package/templates/modules/auth/nestjs/config.json +20 -20
  31. package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
  32. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
  33. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
  34. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
  35. package/templates/modules/docker/.dockerignore +1 -0
  36. package/templates/modules/docker/Dockerfile.hbs +147 -136
  37. package/templates/modules/docker/docker-compose.yml.hbs +96 -1
  38. package/templates/modules/rate-limit/express/config.json +12 -0
  39. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +21 -0
  40. package/templates/modules/rate-limit/nestjs/config.json +12 -0
package/README.md CHANGED
@@ -1,113 +1,114 @@
1
- # better-ts-stack
2
-
3
- > A CLI that scaffolds TypeScript backend and full-stack projects.
4
-
5
- `better-ts-stack` creates an Express, NestJS, Next.js, or TanStack Start project with TypeScript, ESLint, and Prettier set up. Database, authentication, Docker, and git are optional.
6
-
7
- ## Quick Start
8
-
9
- ```bash
10
- # Run instantly with npx
11
- npx better-ts-stack
12
-
13
- # Or install globally
14
- npm install -g better-ts-stack
15
- better-ts-stack
16
- ```
17
-
18
- Run the command and answer the prompts. The CLI creates the project folder.
19
-
20
- ## Flags
21
-
22
- Every prompt has a flag. Options you leave out are asked interactively, unless you pass `--yes`.
23
-
24
- ```bash
25
- # No prompts at all
26
- npx better-ts-stack my-api --framework nestjs --database prisma --auth --yes
27
-
28
- # Create the project in the current (empty) directory
29
- npx better-ts-stack . --framework nextjs --database drizzle --pm pnpm
30
- ```
31
-
32
- | Flag | Values |
33
- | ---------------------------- | -------------------------------------------- |
34
- | `--framework <name>` | `express`, `nestjs`, `nextjs`, `tanstack` |
35
- | `--database <name>` | `none`, `prisma`, `drizzle`, `mongoose` |
36
- | `--pm <name>` | `npm`, `pnpm`, `bun` |
37
- | `--auth` / `--no-auth` | Add authentication |
38
- | `--docker` / `--no-docker` | Add Docker files |
39
- | `--git` / `--no-git` | Initialize a git repository |
40
- | `--install` / `--no-install` | Install dependencies |
41
- | `-y`, `--yes` | Use defaults for missing options, no prompts |
42
- | `--dry-run` | List what would be created, write nothing |
43
- | `-h`, `--help` | Show help |
44
- | `-v`, `--version` | Show the version |
45
-
46
- `--database` names the ORM, which also selects the database: `prisma` and `drizzle` use PostgreSQL, `mongoose` uses MongoDB.
47
-
48
- With `--yes`, the defaults are Express, no database, no auth, no Docker, npm, git initialized, and dependencies not installed.
49
-
50
- The project path can be a name (`my-app`), a nested path (`apps/api`), or `.` for the current directory.
51
-
52
- ## What It Can Scaffold
53
-
54
- | Project type | Stack | Optional setup |
55
- | -------------- | --------------------------------------- | ------------------------------------------------------------------------ |
56
- | Backend API | Express 5 or NestJS 12 + TypeScript | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, JWT auth, Docker, git |
57
- | Full-stack app | Next.js 16 or TanStack Start + React 19 | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, Better Auth, Docker, git |
58
-
59
- Notes:
60
-
61
- - Better Auth is only offered for full-stack projects that use a database.
62
-
63
- ## Interactive Prompt Flow
64
-
65
- Without flags, the CLI asks, in order:
66
-
67
- 1. Project name or path
68
- 2. Application type: `Backend API` or `Full-stack App`
69
- 3. Framework
70
- - Backend: `Express` or `NestJS`
71
- - Full-stack: `Next.js` or `TanStack Start`
72
- 4. Database: `none`, `PostgreSQL`, or `MongoDB`
73
- 5. ORM/ODM when a database is selected
74
- - PostgreSQL: `Prisma` or `Drizzle`
75
- - MongoDB: `Mongoose`
76
- 6. Package manager: `npm`, `pnpm`, or `bun`
77
- 7. Docker setup
78
- 8. Authentication
79
- - Express and NestJS apps: JWT-based auth
80
- - Next.js and TanStack Start apps: Better Auth, only when a database is selected
81
- 9. Git initialization
82
- 10. Dependency installation
83
-
84
- The CLI does not ask for a port. Backend projects default to `PORT=3000`.
85
-
86
- ## What You Get
87
-
88
- Every generated project includes:
89
-
90
- - TypeScript with strict mode enabled
91
- - ESLint and Prettier pre-configured
92
- - `.env.example` and `.env` setup
93
- - Ready-to-run project scripts
94
- - Health check endpoint at `/health` for backend projects
95
- - shadcn-compatible UI components for Next.js and TanStack Start projects
96
- - Database wiring and schema setup when selected
97
- - Authentication scaffolding when selected
98
- - Docker files when selected
99
- - Optional git repository initialization
100
-
101
- ## Requirements
102
-
103
- - Node.js 24 or later
104
-
105
- ## Links
106
-
107
- - [Documentation](https://better-ts-stack.abdullahtech.me/docs)
108
- - [GitHub Repository](https://github.com/Abdullah-dev0/better-ts-stack)
109
- - [Issue Tracker](https://github.com/Abdullah-dev0/better-ts-stack/issues)
110
-
111
- ## License
112
-
113
- MIT
1
+ # better-ts-stack
2
+
3
+ > A CLI that scaffolds TypeScript backend and full-stack projects.
4
+
5
+ `better-ts-stack` creates an Express, NestJS, Next.js, or TanStack Start project with TypeScript, ESLint, and Prettier set up. Database, authentication, Docker, and git are optional.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Run instantly with npx
11
+ npx better-ts-stack
12
+
13
+ # Or install globally
14
+ npm install -g better-ts-stack
15
+ better-ts-stack
16
+ ```
17
+
18
+ Run the command and answer the prompts. The CLI creates the project folder.
19
+
20
+ ## Flags
21
+
22
+ Every prompt has a flag. Options you leave out are asked interactively, unless you pass `--yes`.
23
+
24
+ ```bash
25
+ # No prompts at all
26
+ npx better-ts-stack my-api --framework nestjs --database prisma --auth --yes
27
+
28
+ # Create the project in the current (empty) directory
29
+ npx better-ts-stack . --framework nextjs --database drizzle --pm pnpm
30
+ ```
31
+
32
+ | Flag | Values |
33
+ | ---------------------------------- | -------------------------------------------- |
34
+ | `--framework <name>` | `express`, `nestjs`, `nextjs`, `tanstack` |
35
+ | `--database <name>` | `none`, `prisma`, `drizzle`, `mongoose` |
36
+ | `--pm <name>` | `npm`, `pnpm`, `bun` |
37
+ | `--auth` / `--no-auth` | Add authentication |
38
+ | `--docker` / `--no-docker` | Add Docker files |
39
+ | `--rate-limit` / `--no-rate-limit` | Add rate limiting (Express and NestJS) |
40
+ | `--git` / `--no-git` | Initialize a git repository |
41
+ | `--install` / `--no-install` | Install dependencies |
42
+ | `-y`, `--yes` | Use defaults for missing options, no prompts |
43
+ | `--dry-run` | List what would be created, write nothing |
44
+ | `-h`, `--help` | Show help |
45
+ | `-v`, `--version` | Show the version |
46
+
47
+ `--database` names the ORM, which also selects the database: `prisma` and `drizzle` use PostgreSQL, `mongoose` uses MongoDB.
48
+
49
+ With `--yes`, the defaults are Express, no database, no auth, no rate limiting, no Docker, git initialized, and dependencies not installed. The package manager defaults to the one that ran the CLI: `pnpm` for `pnpm dlx`, `bun` for `bunx`, and `npm` otherwise.
50
+
51
+ The project path can be a name (`my-app`), a nested path (`apps/api`), or `.` for the current directory.
52
+
53
+ ## What It Can Scaffold
54
+
55
+ | Project type | Stack | Optional setup |
56
+ | -------------- | --------------------------------------- | ------------------------------------------------------------------------ |
57
+ | Backend API | Express 5 or NestJS 12 + TypeScript | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, JWT auth, Docker, git |
58
+ | Full-stack app | Next.js 16 or TanStack Start + React 19 | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, Better Auth, Docker, git |
59
+
60
+ Notes:
61
+
62
+ - Better Auth is only offered for full-stack projects that use a database.
63
+
64
+ ## Interactive Prompt Flow
65
+
66
+ Without flags, the CLI asks, in order:
67
+
68
+ 1. Project name or path
69
+ 2. Application type: `Backend API` or `Full-stack App`
70
+ 3. Framework
71
+ - Backend: `Express` or `NestJS`
72
+ - Full-stack: `Next.js` or `TanStack Start`
73
+ 4. Database: `none`, `PostgreSQL`, or `MongoDB`
74
+ 5. ORM/ODM when a database is selected
75
+ - PostgreSQL: `Prisma` or `Drizzle`
76
+ - MongoDB: `Mongoose`
77
+ 6. Package manager: `npm`, `pnpm`, or `bun`
78
+ 7. Docker setup
79
+ 8. Authentication
80
+ - Express and NestJS apps: JWT-based auth
81
+ - Next.js and TanStack Start apps: Better Auth, only when a database is selected
82
+ 9. Git initialization
83
+ 10. Dependency installation
84
+
85
+ The CLI does not ask for a port. Backend projects default to `PORT=3000`.
86
+
87
+ ## What You Get
88
+
89
+ Every generated project includes:
90
+
91
+ - TypeScript with strict mode enabled
92
+ - ESLint and Prettier pre-configured
93
+ - `.env.example` and `.env` setup
94
+ - Ready-to-run project scripts
95
+ - Health check endpoint at `/health` for backend projects
96
+ - shadcn-compatible UI components for Next.js and TanStack Start projects
97
+ - Database wiring and schema setup when selected
98
+ - Authentication scaffolding when selected
99
+ - Docker files when selected
100
+ - Optional git repository initialization
101
+
102
+ ## Requirements
103
+
104
+ - Node.js 24 or later
105
+
106
+ ## Links
107
+
108
+ - [Documentation](https://better-ts-stack.abdullahtech.me/docs)
109
+ - [GitHub Repository](https://github.com/Abdullah-dev0/better-ts-stack)
110
+ - [Issue Tracker](https://github.com/Abdullah-dev0/better-ts-stack/issues)
111
+
112
+ ## License
113
+
114
+ MIT
package/dist/args.js CHANGED
@@ -11,26 +11,28 @@ const FRAMEWORKS = [
11
11
  ].map((option) => option.value);
12
12
  const DATABASES = ["none", "prisma", "drizzle", "mongoose"];
13
13
  const PACKAGE_MANAGERS = types_1.packageManagerOptions.map((option) => option.value);
14
- exports.HELP_TEXT = `Usage: better-ts-stack [project-path] [options]
15
-
16
- Options left out are asked interactively, unless --yes is given.
17
-
18
- --framework <name> ${FRAMEWORKS.join(" | ")}
19
- --database <name> ${DATABASES.join(" | ")}
20
- --pm <name> ${PACKAGE_MANAGERS.join(" | ")}
21
- --auth, --no-auth Add authentication
22
- --docker, --no-docker Add Docker files
23
- --git, --no-git Initialize a git repository
24
- --install, --no-install Install dependencies
25
- -y, --yes Use defaults for missing options and skip confirmation
26
- --dry-run List the files, dependencies, scripts, environment
27
- variables, and commands without writing anything
28
- -h, --help Show this help
29
- -v, --version Show the version
30
-
31
- Examples:
32
- better-ts-stack my-api --framework nestjs --database prisma --auth --yes
33
- better-ts-stack . --framework nextjs --database drizzle --pm pnpm
14
+ exports.HELP_TEXT = `Usage: better-ts-stack [project-path] [options]
15
+
16
+ Options left out are asked interactively, unless --yes is given.
17
+
18
+ --framework <name> ${FRAMEWORKS.join(" | ")}
19
+ --database <name> ${DATABASES.join(" | ")}
20
+ --pm <name> ${PACKAGE_MANAGERS.join(" | ")}
21
+ --auth, --no-auth Add authentication
22
+ --docker, --no-docker Add Docker files
23
+ --rate-limit, --no-rate-limit
24
+ Add rate limiting (Express and NestJS)
25
+ --git, --no-git Initialize a git repository
26
+ --install, --no-install Install dependencies
27
+ -y, --yes Use defaults for missing options and skip confirmation
28
+ --dry-run List the files, dependencies, scripts, environment
29
+ variables, and commands without writing anything
30
+ -h, --help Show this help
31
+ -v, --version Show the version
32
+
33
+ Examples:
34
+ better-ts-stack my-api --framework nestjs --database prisma --auth --yes
35
+ better-ts-stack . --framework nextjs --database drizzle --pm pnpm
34
36
  `;
35
37
  function parseChoice(flag, value, allowed) {
36
38
  if (value === undefined)
@@ -53,6 +55,7 @@ function parseCliArgs(argv) {
53
55
  pm: { type: "string" },
54
56
  auth: { type: "boolean" },
55
57
  docker: { type: "boolean" },
58
+ "rate-limit": { type: "boolean" },
56
59
  git: { type: "boolean" },
57
60
  install: { type: "boolean" },
58
61
  yes: { type: "boolean", short: "y" },
@@ -75,6 +78,7 @@ function parseCliArgs(argv) {
75
78
  packageManager: parseChoice("pm", values.pm, PACKAGE_MANAGERS),
76
79
  useAuth: values.auth,
77
80
  useDocker: values.docker,
81
+ useRateLimit: values["rate-limit"],
78
82
  initGit: values.git,
79
83
  installDeps: values.install,
80
84
  };
@@ -15,7 +15,7 @@ const gitInitializer_1 = require("./gitInitializer");
15
15
  const moduleSelector_1 = require("./moduleSelector");
16
16
  const templateContext_1 = require("./templateContext");
17
17
  // Files the builder writes itself instead of copying them from a module
18
- const GENERATED_FILES = ["package.json", ".env", ".env.example"];
18
+ const GENERATED_FILES = ["package.json", ".env", ".env.example", "README.md"];
19
19
  // Loads the selected modules and merges their configurations
20
20
  async function loadBuildInputs(config) {
21
21
  const selection = (0, moduleSelector_1.selectModules)(config);
@@ -69,11 +69,7 @@ async function generatePackageJson(targetDir, mergedConfig, config) {
69
69
  name: config.projectName,
70
70
  version: "1.0.0",
71
71
  private: true,
72
- // TanStack's srvx static serving needs Node 24.20+ on Windows (earlier
73
- // 24.x reports mismatched file stats there, so every asset 404s)
74
- engines: {
75
- node: config.framework === "tanstack" ? ">=24.20.0" : ">=24.0.0",
76
- },
72
+ engines: { node: ">=24.0.0" },
77
73
  description: `Project created with better-ts-stack using ${config.database !== "none" ? config.database : "no database"}`,
78
74
  ...(ESM_FRAMEWORKS.includes(config.framework) ? { type: "module" } : {}),
79
75
  ...(isFullstack ? {} : { main: BACKEND_ENTRY_FILES[config.framework] }),
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.planBuild = void 0;
7
7
  exports.build = build;
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
9
10
  const nextSteps_1 = require("../output/nextSteps");
10
11
  const types_1 = require("../types");
11
12
  const validators_1 = require("../validators");
@@ -14,8 +15,17 @@ const configGenerator_1 = require("./configGenerator");
14
15
  const dependencyInstaller_1 = require("./dependencyInstaller");
15
16
  const fileProcessor_1 = require("./fileProcessor");
16
17
  const gitInitializer_1 = require("./gitInitializer");
18
+ const readmeGenerator_1 = require("./readmeGenerator");
17
19
  var buildPlanner_2 = require("./buildPlanner");
18
20
  Object.defineProperty(exports, "planBuild", { enumerable: true, get: function () { return buildPlanner_2.planBuild; } });
21
+ // Empties a directory except for the entries it may have had before the build
22
+ async function removeCreatedEntries(dir) {
23
+ for (const entry of await fs_extra_1.default.readdir(dir)) {
24
+ if (!validators_1.PRESERVED_ENTRIES.includes(entry)) {
25
+ await fs_extra_1.default.remove(node_path_1.default.join(dir, entry));
26
+ }
27
+ }
28
+ }
19
29
  // Main function to orchestrate the project construction process
20
30
  async function build(config, targetDir) {
21
31
  // 1. Validate target directory
@@ -42,8 +52,9 @@ async function build(config, targetDir) {
42
52
  if (config.packageManager === "pnpm") {
43
53
  await (0, configGenerator_1.generatePnpmWorkspace)(targetDir, mergedConfig.overrides);
44
54
  }
45
- // 7. Generate environment files
55
+ // 7. Generate environment files and the README
46
56
  await (0, configGenerator_1.generateEnvFile)(targetDir, mergedConfig.envVars);
57
+ await (0, readmeGenerator_1.generateReadme)(targetDir, config, mergedConfig);
47
58
  // 8. Install dependencies and initialize git (both optional); a failure
48
59
  // in either is reported as a warning and does not fail the build
49
60
  const depsInstalled = config.installDeps &&
@@ -57,8 +68,9 @@ async function build(config, targetDir) {
57
68
  }
58
69
  catch (error) {
59
70
  // Remove the partial project so a retry is not blocked by a non-empty
60
- // directory; a directory that already existed is emptied, not deleted
61
- await (didDirectoryExist ? fs_extra_1.default.emptyDir(targetDir) : fs_extra_1.default.remove(targetDir)).catch(() => undefined);
71
+ // directory. A directory that already existed is emptied, not deleted,
72
+ // and keeps what it had before, such as .git
73
+ await (didDirectoryExist ? removeCreatedEntries(targetDir) : fs_extra_1.default.remove(targetDir)).catch(() => undefined);
62
74
  // Errors raised by the build steps already say what failed
63
75
  throw (0, types_1.isBuildError)(error)
64
76
  ? error
@@ -9,13 +9,15 @@ function selectModules(config) {
9
9
  ? `backend/${framework}`
10
10
  : `frontend/${framework}`;
11
11
  const modules = [];
12
- // Database and auth modules have one variant per framework (prisma/express,
13
- // auth/nextjs, ...); the Docker module is shared
12
+ // Database, auth, and rate-limit modules have one variant per framework
13
+ // (prisma/express, auth/nextjs, ...); the Docker module is shared
14
14
  if (config.database !== "none")
15
15
  modules.push(`${config.database}/${framework}`);
16
16
  if (config.useDocker)
17
17
  modules.push("docker");
18
18
  if (config.useAuth)
19
19
  modules.push(`auth/${framework}`);
20
+ if (config.useRateLimit)
21
+ modules.push(`rate-limit/${framework}`);
20
22
  return { base, modules };
21
23
  }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderReadme = renderReadme;
7
+ exports.generateReadme = generateReadme;
8
+ // Writes the README.md of a generated project
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const nextSteps_1 = require("../output/nextSteps");
12
+ const types_1 = require("../types");
13
+ const configGenerator_1 = require("./configGenerator");
14
+ const DOCS_URL = "https://better-ts-stack.abdullahtech.me/docs";
15
+ const FRAMEWORK_LABELS = {
16
+ express: "Express 5",
17
+ nestjs: "NestJS 12",
18
+ nextjs: "Next.js 16 (App Router)",
19
+ tanstack: "TanStack Start",
20
+ };
21
+ const DATABASE_LABELS = {
22
+ prisma: "Prisma with PostgreSQL",
23
+ drizzle: "Drizzle with PostgreSQL",
24
+ mongoose: "Mongoose with MongoDB",
25
+ };
26
+ // Renders rows as a Markdown table; pipes inside cells are escaped
27
+ function table(headers, rows) {
28
+ const cell = (text) => text.replace(/\|/g, "\\|");
29
+ return [
30
+ `| ${headers.join(" | ")} |`,
31
+ `| ${headers.map(() => "---").join(" | ")} |`,
32
+ ...rows.map((row) => `| ${row.map(cell).join(" | ")} |`),
33
+ ];
34
+ }
35
+ // Builds the README content from the project's choices and merged config
36
+ function renderReadme(config, mergedConfig, targetDir) {
37
+ const isFullstack = config.applicationType === "fullstack";
38
+ const lines = [
39
+ `# ${config.projectName}`,
40
+ "",
41
+ `Created with [better-ts-stack](${DOCS_URL}).`,
42
+ "",
43
+ "## Stack",
44
+ "",
45
+ `- **Framework:** ${FRAMEWORK_LABELS[config.framework]}`,
46
+ ];
47
+ if (config.database !== "none") {
48
+ lines.push(`- **Database:** ${DATABASE_LABELS[config.database]}`);
49
+ }
50
+ if (config.useAuth) {
51
+ lines.push(isFullstack
52
+ ? "- **Auth:** Better Auth with email and password"
53
+ : "- **Auth:** JWT with `/auth/register`, `/auth/login`, and `/auth/me`");
54
+ }
55
+ if (config.useDocker)
56
+ lines.push("- **Docker:** Dockerfile and Compose file");
57
+ lines.push(`- **Package manager:** ${config.packageManager}`, "");
58
+ const steps = (0, nextSteps_1.generateNextSteps)(config, targetDir, false, true);
59
+ lines.push("## Getting started", "", ...steps.map((step, index) => {
60
+ const isCommand = /^(npm|pnpm|bun|docker) /.test(step);
61
+ return `${index + 1}. ${isCommand ? `\`${step}\`` : step}`;
62
+ }), "");
63
+ // "pre" and "post" hooks run on their own, so only the commands to type
64
+ // are listed
65
+ const scripts = Object.entries(mergedConfig.scripts).filter(([name]) => !/^(pre|post)./.test(name));
66
+ if (scripts.length > 0) {
67
+ lines.push("## Scripts", "", ...table(["Command", "Runs"], scripts.map(([name, command]) => [
68
+ `\`${config.packageManager} run ${name}\``,
69
+ `\`${command}\``,
70
+ ])), "");
71
+ }
72
+ const envVars = Object.entries(mergedConfig.envVars);
73
+ if (envVars.length > 0) {
74
+ lines.push("## Environment variables", "", "`.env` holds your local values and is not committed. `.env.example` lists the same variables for other developers.", "", ...table(["Variable", "Default"], envVars.map(([key, value]) => [
75
+ `\`${key}\``,
76
+ configGenerator_1.GENERATED_SECRETS.includes(key)
77
+ ? "A random value generated in `.env`"
78
+ : `\`${value}\``,
79
+ ])), "");
80
+ }
81
+ lines.push("## Learn more", "", `- [${isFullstack ? "Full-stack" : "Backend"} guide](${DOCS_URL}/modules/${isFullstack ? "frontend" : "backend"})`);
82
+ if (config.database !== "none") {
83
+ lines.push(`- [Database guide](${DOCS_URL}/modules/database)`);
84
+ }
85
+ if (config.useAuth)
86
+ lines.push(`- [Auth guide](${DOCS_URL}/modules/auth)`);
87
+ if (config.useDocker) {
88
+ lines.push(`- [Docker guide](${DOCS_URL}/modules/docker)`);
89
+ }
90
+ lines.push(`- [Troubleshooting](${DOCS_URL}/troubleshooting)`, "");
91
+ return lines.join("\n");
92
+ }
93
+ // Writes README.md into the project
94
+ async function generateReadme(targetDir, config, mergedConfig) {
95
+ try {
96
+ await fs_extra_1.default.writeFile(node_path_1.default.join(targetDir, "README.md"), renderReadme(config, mergedConfig, targetDir), "utf-8");
97
+ }
98
+ catch (error) {
99
+ throw (0, types_1.buildError)(error, "README_ERROR", "Failed to generate README.md");
100
+ }
101
+ }
@@ -3,6 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildTemplateContext = buildTemplateContext;
4
4
  // Context builder for Handlebars, providing variables and helpers for templates
5
5
  const types_1 = require("../types");
6
+ // The lockfile is optional: a project built before its first install has
7
+ // none, so each install falls back to a plain install without it
8
+ const DOCKER_COMMANDS = {
9
+ npm: {
10
+ setup: "",
11
+ manifests: "package.json package-lock.json*",
12
+ install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi",
13
+ installProduction: "if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi",
14
+ cleanCache: "npm cache clean --force",
15
+ run: "npm run",
16
+ },
17
+ pnpm: {
18
+ setup: "RUN npm install -g pnpm@11",
19
+ manifests: "package.json pnpm-lock.yaml* pnpm-workspace.yaml",
20
+ install: "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi",
21
+ installProduction: "if [ -f pnpm-lock.yaml ]; then pnpm install --prod --frozen-lockfile; else pnpm install --prod; fi",
22
+ cleanCache: "pnpm store prune",
23
+ run: "pnpm run",
24
+ },
25
+ bun: {
26
+ setup: "COPY --from=oven/bun:1-alpine /usr/local/bin/bun /usr/local/bin/bun",
27
+ manifests: "package.json bun.lock*",
28
+ install: "if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi",
29
+ installProduction: "if [ -f bun.lock ]; then bun install --production --frozen-lockfile; else bun install --production; fi",
30
+ cleanCache: "rm -rf /root/.bun/install/cache",
31
+ run: "bun run",
32
+ },
33
+ };
6
34
  function buildTemplateContext(config) {
7
35
  return {
8
36
  projectName: config.projectName,
@@ -12,6 +40,8 @@ function buildTemplateContext(config) {
12
40
  port: types_1.DEFAULT_PORT,
13
41
  useDocker: config.useDocker,
14
42
  useAuth: config.useAuth,
43
+ useRateLimit: config.useRateLimit,
44
+ docker: DOCKER_COMMANDS[config.packageManager],
15
45
  helpers: {
16
46
  eq: (a, b) => a === b,
17
47
  runner: () => {
package/dist/index.js CHANGED
@@ -32,7 +32,13 @@ async function main() {
32
32
  const directoryError = await (0, validators_1.validateDirectoryEmpty)(targetDir);
33
33
  if (directoryError)
34
34
  throw (0, types_1.buildError)(directoryError, "DIRECTORY_NOT_EMPTY");
35
- const choices = await (0, prompts_1.collectStackChoices)(args.yes ? { ...types_1.DEFAULT_CHOICES, ...args.choices } : args.choices);
35
+ const choices = await (0, prompts_1.collectStackChoices)(args.yes
36
+ ? {
37
+ ...types_1.DEFAULT_CHOICES,
38
+ packageManager: (0, prompts_1.detectPackageManager)(),
39
+ ...args.choices,
40
+ }
41
+ : args.choices);
36
42
  const config = {
37
43
  projectName: node_path_1.default.basename(targetDir),
38
44
  applicationType: (0, types_1.getApplicationType)(choices.framework),
@@ -7,23 +7,16 @@ exports.generateNextSteps = generateNextSteps;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const node_process_1 = require("node:process");
9
9
  const MONGODB_AUTH_SETUP_URL = "https://better-ts-stack.abdullahtech.me/docs/modules/database#mongoose-and-mongodb";
10
- // Node 24 releases before 24.20 report mismatched file stats on Windows, which
11
- // makes srvx (TanStack's production server) reject every static asset
12
- const MIN_TANSTACK_WINDOWS_NODE = [24, 20];
13
- function isNodeBelow([major, minor]) {
14
- const [currentMajor, currentMinor] = process.versions.node
15
- .split(".")
16
- .map(Number);
17
- return (currentMajor < major || (currentMajor === major && currentMinor < minor));
18
- }
19
- // Generates a list of instructions for the user to follow after project creation
20
- function generateNextSteps(config, targetDir, depsInstalled) {
10
+ // Generates a list of instructions for the user to follow after project
11
+ // creation. The README version leaves out the cd step, which only makes
12
+ // sense in the terminal
13
+ function generateNextSteps(config, targetDir, depsInstalled, isForReadme = false) {
21
14
  const steps = [];
22
15
  const isFullstack = config.applicationType === "fullstack";
23
16
  // Step 1: cd into project directory, unless it was created in place
24
17
  // Quoted when it has spaces, so the command can be pasted as it is
25
18
  const relativeDir = node_path_1.default.relative((0, node_process_1.cwd)(), targetDir);
26
- if (relativeDir) {
19
+ if (relativeDir && !isForReadme) {
27
20
  steps.push(/\s/.test(relativeDir) ? `cd "${relativeDir}"` : `cd ${relativeDir}`);
28
21
  }
29
22
  // Step 2: Set environment variables
@@ -35,17 +28,15 @@ function generateNextSteps(config, targetDir, depsInstalled) {
35
28
  ? "Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env"
36
29
  : "Review the generated JWT_SECRET in .env");
37
30
  }
38
- if (config.framework === "tanstack" &&
39
- process.platform === "win32" &&
40
- isNodeBelow(MIN_TANSTACK_WINDOWS_NODE)) {
41
- steps.push(`Upgrade Node.js to ${MIN_TANSTACK_WINDOWS_NODE.join(".")} or newer: on Windows, ` +
42
- `Node ${process.versions.node} makes "npm start" return 404 for CSS, JS, and other static files`);
43
- }
44
31
  // Step 3: Install dependencies if not already done
45
32
  if (!depsInstalled) {
46
33
  steps.push(`${config.packageManager} install`);
47
34
  }
48
- // Step 4: Database-specific steps
35
+ // Step 4: Database-specific steps. With Docker, Compose runs the database
36
+ const hasDockerDatabase = config.useDocker && config.database !== "none";
37
+ if (hasDockerDatabase) {
38
+ steps.push("docker compose up -d db");
39
+ }
49
40
  if (config.database === "prisma") {
50
41
  steps.push(`${config.packageManager} run prisma:generate`);
51
42
  steps.push(`${config.packageManager} run prisma:migrate`);
@@ -55,9 +46,12 @@ function generateNextSteps(config, targetDir, depsInstalled) {
55
46
  steps.push(`${config.packageManager} run db:migrate`);
56
47
  }
57
48
  else if (config.database === "mongoose") {
58
- steps.push("Ensure MongoDB is running locally or update MONGODB_URI in .env");
59
- // Better Auth's MongoDB adapter uses transactions, which need a replica set
60
- if (config.useAuth && isFullstack) {
49
+ if (!hasDockerDatabase) {
50
+ steps.push("Ensure MongoDB is running locally or update MONGODB_URI in .env");
51
+ }
52
+ // Better Auth's MongoDB adapter uses transactions, which need a replica
53
+ // set; the Compose database already runs as one
54
+ if (config.useAuth && isFullstack && !hasDockerDatabase) {
61
55
  steps.push("Better Auth uses MongoDB transactions: run MongoDB as a replica set (or use Atlas) " +
62
56
  'and create the "user", "session", "account", and "verification" collections before signing up. ' +
63
57
  `See ${MONGODB_AUTH_SETUP_URL}`);
@@ -70,5 +64,8 @@ function generateNextSteps(config, targetDir, depsInstalled) {
70
64
  ? "Visit /sign-up to create your first account"
71
65
  : "Create a user via POST /auth/register then login with /auth/login");
72
66
  }
67
+ if (config.useDocker) {
68
+ steps.push(`To run the app in Docker instead: ${config.packageManager} run docker:up`);
69
+ }
73
70
  return steps;
74
71
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.collectStackChoices = void 0;
3
+ exports.detectPackageManager = exports.collectStackChoices = void 0;
4
4
  exports.collectProjectPath = collectProjectPath;
5
5
  exports.confirmBuild = confirmBuild;
6
6
  const types_1 = require("../types");
@@ -9,6 +9,7 @@ const validators_1 = require("../validators");
9
9
  const stack_1 = require("./stack");
10
10
  var stack_2 = require("./stack");
11
11
  Object.defineProperty(exports, "collectStackChoices", { enumerable: true, get: function () { return stack_2.collectStackChoices; } });
12
+ Object.defineProperty(exports, "detectPackageManager", { enumerable: true, get: function () { return stack_2.detectPackageManager; } });
12
13
  // Returns the project path from the command line, or asks for it
13
14
  async function collectProjectPath(preset, isNonInteractive) {
14
15
  if (preset !== undefined) {