better-ts-stack 1.1.4 → 2.0.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -82
  3. package/dist/args.js +90 -0
  4. package/dist/builder/buildPlanner.js +71 -0
  5. package/dist/builder/configGenerator.js +19 -10
  6. package/dist/builder/dependencyInstaller.js +8 -15
  7. package/dist/builder/fileProcessor.js +47 -44
  8. package/dist/builder/gitInitializer.js +21 -35
  9. package/dist/builder/index.js +33 -91
  10. package/dist/builder/moduleSelector.js +11 -16
  11. package/dist/builder/templateContext.js +6 -27
  12. package/dist/index.js +47 -21
  13. package/dist/modules/registry.js +4 -6
  14. package/dist/output/nextSteps.js +10 -20
  15. package/dist/prompts/index.js +22 -59
  16. package/dist/prompts/stack.js +85 -71
  17. package/dist/types/index.js +47 -41
  18. package/dist/ui/index.js +18 -0
  19. package/dist/ui/messages.js +124 -0
  20. package/dist/ui/prompts.js +70 -0
  21. package/dist/ui/theme.js +23 -0
  22. package/dist/validators/index.js +15 -40
  23. package/package.json +71 -66
  24. package/templates/backend/express/.prettierrc +2 -2
  25. package/templates/backend/express/config.json +40 -44
  26. package/templates/backend/express/src/{index.ts → index.ts.hbs} +75 -76
  27. package/templates/backend/nestjs/.prettierignore +52 -0
  28. package/templates/backend/nestjs/.prettierrc +16 -0
  29. package/templates/backend/nestjs/config.json +41 -0
  30. package/templates/backend/nestjs/eslint.config.mjs +40 -0
  31. package/templates/backend/nestjs/gitignore +36 -0
  32. package/templates/backend/nestjs/nest-cli.json +9 -0
  33. package/templates/backend/nestjs/src/app.module.ts.hbs +34 -0
  34. package/templates/backend/nestjs/src/health/health.controller.ts +12 -0
  35. package/templates/backend/nestjs/src/main.ts +23 -0
  36. package/templates/backend/nestjs/tsconfig.eslint.json +9 -0
  37. package/templates/backend/nestjs/tsconfig.json +25 -0
  38. package/templates/frontend/nextjs/app/layout.tsx +13 -13
  39. package/templates/frontend/nextjs/app/page.tsx.hbs +97 -97
  40. package/templates/frontend/nextjs/components/ui/button.tsx +51 -51
  41. package/templates/frontend/nextjs/components/ui/card.tsx +60 -60
  42. package/templates/frontend/nextjs/components/ui/field.tsx +67 -67
  43. package/templates/frontend/nextjs/components/ui/input.tsx +18 -18
  44. package/templates/frontend/nextjs/config.json +37 -38
  45. package/templates/frontend/nextjs/eslint.config.mjs +11 -11
  46. package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
  47. package/templates/frontend/tanstack/config.json +45 -48
  48. package/templates/modules/auth/express/config.json +20 -26
  49. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +43 -57
  50. package/templates/modules/auth/nestjs/config.json +20 -0
  51. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -0
  52. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -0
  53. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -0
  54. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -0
  55. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -0
  56. package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -0
  57. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -0
  58. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -0
  59. package/templates/modules/auth/nextjs/config.json +18 -31
  60. package/templates/modules/auth/tanstack/config.json +2 -16
  61. package/templates/modules/docker/Dockerfile.hbs +135 -133
  62. package/templates/modules/docker/config.json +15 -19
  63. package/templates/modules/drizzle/express/config.json +26 -27
  64. package/templates/modules/drizzle/nestjs/config.json +26 -0
  65. package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -0
  66. package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -0
  67. package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -0
  68. package/templates/modules/drizzle/nextjs/config.json +26 -29
  69. package/templates/modules/drizzle/tanstack/config.json +5 -5
  70. package/templates/modules/mongoose/express/config.json +15 -16
  71. package/templates/modules/mongoose/nestjs/config.json +15 -0
  72. package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -0
  73. package/templates/modules/mongoose/nextjs/config.json +15 -16
  74. package/templates/modules/mongoose/tanstack/config.json +16 -17
  75. package/templates/modules/prisma/express/config.json +27 -28
  76. package/templates/modules/prisma/nestjs/config.json +27 -0
  77. package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -0
  78. package/templates/modules/prisma/nestjs/prisma.config.ts +12 -0
  79. package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -0
  80. package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -0
  81. package/templates/modules/prisma/nextjs/config.json +28 -29
  82. package/templates/modules/prisma/tanstack/config.json +5 -6
  83. package/dist/builder/configGenerator.d.ts +0 -7
  84. package/dist/builder/configGenerator.d.ts.map +0 -1
  85. package/dist/builder/configGenerator.js.map +0 -1
  86. package/dist/builder/dependencyInstaller.d.ts +0 -3
  87. package/dist/builder/dependencyInstaller.d.ts.map +0 -1
  88. package/dist/builder/dependencyInstaller.js.map +0 -1
  89. package/dist/builder/fileProcessor.d.ts +0 -6
  90. package/dist/builder/fileProcessor.d.ts.map +0 -1
  91. package/dist/builder/fileProcessor.js.map +0 -1
  92. package/dist/builder/gitInitializer.d.ts +0 -2
  93. package/dist/builder/gitInitializer.d.ts.map +0 -1
  94. package/dist/builder/gitInitializer.js.map +0 -1
  95. package/dist/builder/index.d.ts +0 -3
  96. package/dist/builder/index.d.ts.map +0 -1
  97. package/dist/builder/index.js.map +0 -1
  98. package/dist/builder/moduleSelector.d.ts +0 -9
  99. package/dist/builder/moduleSelector.d.ts.map +0 -1
  100. package/dist/builder/moduleSelector.js.map +0 -1
  101. package/dist/builder/templateContext.d.ts +0 -21
  102. package/dist/builder/templateContext.d.ts.map +0 -1
  103. package/dist/builder/templateContext.js.map +0 -1
  104. package/dist/index.d.ts +0 -2
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js.map +0 -1
  107. package/dist/intro.d.ts +0 -2
  108. package/dist/intro.d.ts.map +0 -1
  109. package/dist/intro.js +0 -27
  110. package/dist/intro.js.map +0 -1
  111. package/dist/modules/registry.d.ts +0 -6
  112. package/dist/modules/registry.d.ts.map +0 -1
  113. package/dist/modules/registry.js.map +0 -1
  114. package/dist/output/nextSteps.d.ts +0 -4
  115. package/dist/output/nextSteps.d.ts.map +0 -1
  116. package/dist/output/nextSteps.js.map +0 -1
  117. package/dist/prompts/index.d.ts +0 -4
  118. package/dist/prompts/index.d.ts.map +0 -1
  119. package/dist/prompts/index.js.map +0 -1
  120. package/dist/prompts/stack.d.ts +0 -3
  121. package/dist/prompts/stack.d.ts.map +0 -1
  122. package/dist/prompts/stack.js.map +0 -1
  123. package/dist/types/index.d.ts +0 -159
  124. package/dist/types/index.d.ts.map +0 -1
  125. package/dist/types/index.js.map +0 -1
  126. package/dist/validators/index.d.ts +0 -5
  127. package/dist/validators/index.d.ts.map +0 -1
  128. package/dist/validators/index.js.map +0 -1
  129. package/templates/backend/express/.eslintrc.js +0 -24
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdullah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,82 +1,113 @@
1
- # better-ts-stack
2
-
3
- > An interactive CLI that scaffolds TypeScript backend and full-stack projects.
4
-
5
- `better-ts-stack` creates an Express, 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
- ## What It Can Scaffold
21
-
22
- | Project type | Stack | Optional setup |
23
- | -------------- | --------------------------------------- | ------------------------------------------------------------------------ |
24
- | Backend API | Express 5 + TypeScript | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, JWT auth, Docker, git |
25
- | Full-stack app | Next.js 16 or TanStack Start + React 19 | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, Better Auth, Docker, git |
26
-
27
- Notes:
28
-
29
- - Express is the only backend framework for now. NestJS is listed in the prompt as "coming soon".
30
- - Better Auth is only offered for full-stack projects that use a database.
31
-
32
- ## Interactive Prompt Flow
33
-
34
- The CLI has no subcommands or flags. It asks, in order:
35
-
36
- 1. Project name
37
- 2. Application type: `Backend API` or `Full-stack App`
38
- 3. Framework
39
- - Backend: `Express` (`NestJS` is coming soon)
40
- - Full-stack: `Next.js` or `TanStack Start`
41
- 4. Database: `none`, `PostgreSQL`, or `MongoDB`
42
- 5. ORM/ODM when a database is selected
43
- - PostgreSQL: `Prisma` or `Drizzle`
44
- - MongoDB: `Mongoose`
45
- 6. Package manager: `npm`, `pnpm`, or `bun`
46
- 7. Docker setup
47
- 8. Authentication
48
- - Express apps: JWT-based auth
49
- - Next.js and TanStack Start apps: Better Auth, only when a database is selected
50
- 9. Git initialization
51
- 10. Dependency installation
52
-
53
- The CLI does not ask for a port. Backend projects default to `PORT=3000`.
54
-
55
- ## What You Get
56
-
57
- Every generated project includes:
58
-
59
- - TypeScript with strict mode enabled
60
- - ESLint and Prettier pre-configured
61
- - `.env.example` and `.env` setup
62
- - Ready-to-run project scripts
63
- - Health check endpoint at `/health` for backend projects
64
- - shadcn-compatible UI components for Next.js and TanStack Start projects
65
- - Database wiring and schema setup when selected
66
- - Authentication scaffolding when selected
67
- - Docker files when selected
68
- - Optional git repository initialization
69
-
70
- ## Requirements
71
-
72
- - Node.js 24 or later
73
-
74
- ## Links
75
-
76
- - [Documentation](https://better-ts-stack.abdullahtech.me/docs)
77
- - [GitHub Repository](https://github.com/Abdullah-dev0/better-ts-stack)
78
- - [Issue Tracker](https://github.com/Abdullah-dev0/better-ts-stack/issues)
79
-
80
- ## License
81
-
82
- 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
+ | `--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
package/dist/args.js ADDED
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP_TEXT = void 0;
4
+ exports.parseCliArgs = parseCliArgs;
5
+ // Command-line flag parsing; every flag is optional and falls back to a prompt
6
+ const node_util_1 = require("node:util");
7
+ const types_1 = require("./types");
8
+ const FRAMEWORKS = [
9
+ ...types_1.backendFrameworkOptions,
10
+ ...types_1.frontendFrameworkOptions,
11
+ ].map((option) => option.value);
12
+ const DATABASES = ["none", "prisma", "drizzle", "mongoose"];
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
34
+ `;
35
+ function parseChoice(flag, value, allowed) {
36
+ if (value === undefined)
37
+ return undefined;
38
+ if (!allowed.includes(value)) {
39
+ throw (0, types_1.buildError)(`Invalid value "${value}" for --${flag}. Use one of: ${allowed.join(", ")}`, "INVALID_ARGUMENT");
40
+ }
41
+ return value;
42
+ }
43
+ function parseCliArgs(argv) {
44
+ let parsed;
45
+ try {
46
+ parsed = (0, node_util_1.parseArgs)({
47
+ args: argv,
48
+ allowPositionals: true,
49
+ allowNegative: true,
50
+ options: {
51
+ framework: { type: "string" },
52
+ database: { type: "string" },
53
+ pm: { type: "string" },
54
+ auth: { type: "boolean" },
55
+ docker: { type: "boolean" },
56
+ git: { type: "boolean" },
57
+ install: { type: "boolean" },
58
+ yes: { type: "boolean", short: "y" },
59
+ "dry-run": { type: "boolean" },
60
+ help: { type: "boolean", short: "h" },
61
+ version: { type: "boolean", short: "v" },
62
+ },
63
+ });
64
+ }
65
+ catch (error) {
66
+ throw (0, types_1.buildError)(error, "INVALID_ARGUMENT");
67
+ }
68
+ const { values, positionals } = parsed;
69
+ if (positionals.length > 1) {
70
+ throw (0, types_1.buildError)(`Expected one project path, got: ${positionals.join(" ")}`, "INVALID_ARGUMENT");
71
+ }
72
+ const choices = {
73
+ framework: parseChoice("framework", values.framework, FRAMEWORKS),
74
+ database: parseChoice("database", values.database, DATABASES),
75
+ packageManager: parseChoice("pm", values.pm, PACKAGE_MANAGERS),
76
+ useAuth: values.auth,
77
+ useDocker: values.docker,
78
+ initGit: values.git,
79
+ installDeps: values.install,
80
+ };
81
+ return {
82
+ projectPath: positionals[0],
83
+ // Keep only the flags that were passed, so the rest can take defaults
84
+ choices: Object.fromEntries(Object.entries(choices).filter(([, value]) => value !== undefined)),
85
+ yes: values.yes ?? false,
86
+ dryRun: values["dry-run"] ?? false,
87
+ help: values.help ?? false,
88
+ version: values.version ?? false,
89
+ };
90
+ }
@@ -0,0 +1,71 @@
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.loadBuildInputs = loadBuildInputs;
7
+ exports.planBuild = planBuild;
8
+ // Works out what a build produces; nothing here writes to disk
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const registry_1 = require("../modules/registry");
12
+ const configGenerator_1 = require("./configGenerator");
13
+ const fileProcessor_1 = require("./fileProcessor");
14
+ const gitInitializer_1 = require("./gitInitializer");
15
+ const moduleSelector_1 = require("./moduleSelector");
16
+ const templateContext_1 = require("./templateContext");
17
+ // Files the builder writes itself instead of copying them from a module
18
+ const GENERATED_FILES = ["package.json", ".env", ".env.example"];
19
+ // Loads the selected modules and merges their configurations
20
+ async function loadBuildInputs(config) {
21
+ const selection = (0, moduleSelector_1.selectModules)(config);
22
+ const modules = [];
23
+ for (const id of [selection.base, ...selection.modules]) {
24
+ modules.push(await (0, registry_1.getModule)(id));
25
+ }
26
+ const templateContext = (0, templateContext_1.buildTemplateContext)(config);
27
+ const mergedConfig = (0, configGenerator_1.mergeConfigurations)(modules.map((m) => m.config), templateContext);
28
+ return { modules, templateContext, mergedConfig };
29
+ }
30
+ // The target directory may not exist yet, so git is asked about the closest
31
+ // directory that does
32
+ async function findExistingDirectory(dir) {
33
+ let current = dir;
34
+ while (!(await fs_extra_1.default.pathExists(current))) {
35
+ const parent = node_path_1.default.dirname(current);
36
+ if (parent === current)
37
+ break;
38
+ current = parent;
39
+ }
40
+ return current;
41
+ }
42
+ // Describes the files, package.json content, environment variables, and
43
+ // commands of a build, for --dry-run
44
+ async function planBuild(config, targetDir) {
45
+ const { modules, mergedConfig } = await loadBuildInputs(config);
46
+ // Later modules overwrite files of earlier ones, so each path is listed once
47
+ const files = new Set(GENERATED_FILES);
48
+ if (config.packageManager === "pnpm")
49
+ files.add("pnpm-workspace.yaml");
50
+ for (const module of modules) {
51
+ for (const file of await (0, fileProcessor_1.listModuleFiles)(module.path))
52
+ files.add(file);
53
+ }
54
+ const envVars = Object.fromEntries(Object.entries(mergedConfig.envVars).map(([key, value]) => [
55
+ key,
56
+ configGenerator_1.GENERATED_SECRETS.includes(key) ? "<random secret>" : value,
57
+ ]));
58
+ const commands = [];
59
+ if (config.installDeps)
60
+ commands.push(`${config.packageManager} install`);
61
+ if (config.initGit &&
62
+ !(0, gitInitializer_1.isInsideGitRepository)(await findExistingDirectory(targetDir))) {
63
+ commands.push("git init");
64
+ }
65
+ return {
66
+ ...mergedConfig,
67
+ envVars,
68
+ files: [...files].sort((a, b) => a.localeCompare(b)),
69
+ commands,
70
+ };
71
+ }
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GENERATED_SECRETS = void 0;
6
7
  exports.processScriptVariables = processScriptVariables;
7
8
  exports.mergeConfigurations = mergeConfigurations;
8
9
  exports.generatePackageJson = generatePackageJson;
@@ -12,8 +13,15 @@ exports.generateEnvFile = generateEnvFile;
12
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
13
14
  const handlebars_1 = __importDefault(require("handlebars"));
14
15
  const node_crypto_1 = require("node:crypto");
15
- const path_1 = __importDefault(require("path"));
16
+ const node_path_1 = __importDefault(require("node:path"));
16
17
  const types_1 = require("../types");
18
+ // Frameworks whose generated project uses ES modules
19
+ const ESM_FRAMEWORKS = ["tanstack", "nestjs"];
20
+ // Compiled entry file of each backend framework
21
+ const BACKEND_ENTRY_FILES = {
22
+ express: "dist/index.js",
23
+ nestjs: "dist/main.js",
24
+ };
17
25
  // Replaces Handlebars variables within script command strings
18
26
  function processScriptVariables(scripts, context) {
19
27
  const processedScripts = {};
@@ -67,21 +75,21 @@ async function generatePackageJson(targetDir, mergedConfig, config) {
67
75
  node: config.framework === "tanstack" ? ">=24.20.0" : ">=24.0.0",
68
76
  },
69
77
  description: `Project created with better-ts-stack using ${config.database !== "none" ? config.database : "no database"}`,
70
- ...(config.framework === "tanstack" ? { type: "module" } : {}),
71
- ...(isFullstack ? {} : { main: "dist/index.js" }),
78
+ ...(ESM_FRAMEWORKS.includes(config.framework) ? { type: "module" } : {}),
79
+ ...(isFullstack ? {} : { main: BACKEND_ENTRY_FILES[config.framework] }),
72
80
  scripts: mergedConfig.scripts,
73
81
  keywords: isFullstack
74
82
  ? config.framework === "tanstack"
75
83
  ? ["tanstack-start", "react", "typescript"]
76
84
  : ["nextjs", "react", "typescript"]
77
- : ["backend", "typescript", "express"],
85
+ : ["backend", "typescript", config.framework],
78
86
  author: "",
79
87
  license: "MIT",
80
88
  dependencies: mergedConfig.dependencies,
81
89
  devDependencies: mergedConfig.devDependencies,
82
90
  overrides,
83
91
  };
84
- const packageJsonPath = path_1.default.join(targetDir, "package.json");
92
+ const packageJsonPath = node_path_1.default.join(targetDir, "package.json");
85
93
  await fs_extra_1.default.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
86
94
  }
87
95
  catch (error) {
@@ -117,12 +125,14 @@ async function generatePnpmWorkspace(targetDir, overrides) {
117
125
  lines.push(` - "${name}"`);
118
126
  }
119
127
  try {
120
- await fs_extra_1.default.writeFile(path_1.default.join(targetDir, "pnpm-workspace.yaml"), lines.join("\n") + "\n", "utf-8");
128
+ await fs_extra_1.default.writeFile(node_path_1.default.join(targetDir, "pnpm-workspace.yaml"), lines.join("\n") + "\n", "utf-8");
121
129
  }
122
130
  catch (error) {
123
131
  throw (0, types_1.buildError)(error, "PNPM_WORKSPACE_ERROR", "Failed to generate pnpm-workspace.yaml");
124
132
  }
125
133
  }
134
+ // Variables that get a random value in .env instead of the module's default
135
+ exports.GENERATED_SECRETS = ["JWT_SECRET", "BETTER_AUTH_SECRET"];
126
136
  // Generates .env and .env.example files from merged environment variables
127
137
  async function generateEnvFile(targetDir, envVars) {
128
138
  try {
@@ -133,21 +143,20 @@ async function generateEnvFile(targetDir, envVars) {
133
143
  let localEnvContent = envContent;
134
144
  // Add each environment variable
135
145
  for (const [key, value] of Object.entries(envVars)) {
136
- const resolvedValue = ["JWT_SECRET", "BETTER_AUTH_SECRET"].includes(key)
146
+ const resolvedValue = exports.GENERATED_SECRETS.includes(key)
137
147
  ? (0, node_crypto_1.randomBytes)(32).toString("hex")
138
148
  : value;
139
149
  envContent += `${key}=${value}\n`;
140
150
  localEnvContent += `${key}=${resolvedValue}\n`;
141
151
  }
142
152
  // Write .env.example
143
- const envExamplePath = path_1.default.join(targetDir, ".env.example");
153
+ const envExamplePath = node_path_1.default.join(targetDir, ".env.example");
144
154
  await fs_extra_1.default.writeFile(envExamplePath, envContent, "utf-8");
145
155
  // Keep generated secrets out of the shareable example file.
146
- const envPath = path_1.default.join(targetDir, ".env");
156
+ const envPath = node_path_1.default.join(targetDir, ".env");
147
157
  await fs_extra_1.default.writeFile(envPath, localEnvContent, "utf-8");
148
158
  }
149
159
  catch (error) {
150
160
  throw (0, types_1.buildError)(error, "ENV_FILE_ERROR", "Failed to generate environment files");
151
161
  }
152
162
  }
153
- //# sourceMappingURL=configGenerator.js.map
@@ -1,30 +1,23 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.installDependencies = installDependencies;
7
- const child_process_1 = require("child_process");
8
- const consola_1 = __importDefault(require("consola"));
4
+ const node_child_process_1 = require("node:child_process");
5
+ const ui_1 = require("../ui");
9
6
  // Installs project dependencies in the target directory
10
7
  function installDependencies(packageManager, cwd) {
11
8
  const command = `${packageManager} install`;
12
- consola_1.default.info(`Installing dependencies with ${packageManager}...`);
13
- consola_1.default.debug(`Running: ${command}`);
9
+ (0, ui_1.info)(`Installing dependencies with ${packageManager}`);
14
10
  try {
15
- (0, child_process_1.execSync)(command, {
11
+ (0, node_child_process_1.execSync)(command, {
16
12
  cwd,
17
13
  stdio: "inherit", // Show installation progress in real-time
18
14
  });
19
- consola_1.default.success("Dependencies installed successfully");
15
+ (0, ui_1.success)("Dependencies installed");
20
16
  return true;
21
17
  }
22
- catch (error) {
23
- // Display helpful error message but don't throw - allow building to continue
24
- consola_1.default.warn("Failed to install dependencies");
25
- consola_1.default.debug(`Error: ${error instanceof Error ? error.message : String(error)}`);
26
- consola_1.default.info(`You can install dependencies manually later by running: ${command}`);
18
+ catch {
19
+ // Not fatal: the project is complete and the install can be run later
20
+ (0, ui_1.warn)(`Could not install dependencies. Run "${command}" yourself.`);
27
21
  return false;
28
22
  }
29
23
  }
30
- //# sourceMappingURL=dependencyInstaller.js.map
@@ -4,18 +4,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.copyModuleFiles = copyModuleFiles;
7
+ exports.listModuleFiles = listModuleFiles;
7
8
  exports.compileTemplate = compileTemplate;
8
9
  exports.processTemplateFiles = processTemplateFiles;
9
- exports.processTemplateFile = processTemplateFile;
10
10
  // Utilities for copying files and processing Handlebars templates
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const handlebars_1 = __importDefault(require("handlebars"));
13
- const path_1 = __importDefault(require("path"));
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_path_1 = __importDefault(require("node:path"));
14
15
  const types_1 = require("../types");
16
+ const TEMPLATE_EXTENSION = ".hbs";
17
+ const MODULE_CONFIG_FILE = "config.json";
18
+ // Names a template's .gitignore can be stored under, see restoreGitignore()
19
+ const GITIGNORE_TEMPLATE_NAMES = ["gitignore", ".npmignore"];
15
20
  async function restoreGitignore(targetDir) {
16
- const templateGitignorePath = path_1.default.join(targetDir, "gitignore");
17
- const npmIgnorePath = path_1.default.join(targetDir, ".npmignore");
18
- const gitIgnorePath = path_1.default.join(targetDir, ".gitignore");
21
+ const templateGitignorePath = node_path_1.default.join(targetDir, "gitignore");
22
+ const npmIgnorePath = node_path_1.default.join(targetDir, ".npmignore");
23
+ const gitIgnorePath = node_path_1.default.join(targetDir, ".gitignore");
19
24
  const [hasTemplateGitignore, hasNpmIgnore, hasGitIgnore] = await Promise.all([
20
25
  fs_extra_1.default.pathExists(templateGitignorePath),
21
26
  fs_extra_1.default.pathExists(npmIgnorePath),
@@ -41,8 +46,8 @@ async function copyModuleFiles(moduleDir, targetDir) {
41
46
  overwrite: true,
42
47
  filter: (src) => {
43
48
  // Skip config.json files
44
- const basename = path_1.default.basename(src);
45
- return basename !== "config.json";
49
+ const basename = node_path_1.default.basename(src);
50
+ return basename !== MODULE_CONFIG_FILE;
46
51
  },
47
52
  });
48
53
  await restoreGitignore(targetDir);
@@ -51,58 +56,56 @@ async function copyModuleFiles(moduleDir, targetDir) {
51
56
  throw (0, types_1.buildError)(error, "FILE_COPY_ERROR", `Failed to copy module files from ${moduleDir}`);
52
57
  }
53
58
  }
59
+ // Lists the files a module adds to a project, under the names they end up
60
+ // with: no .hbs extension, and the gitignore template as .gitignore
61
+ async function listModuleFiles(moduleDir) {
62
+ const entries = await (0, promises_1.readdir)(moduleDir, {
63
+ recursive: true,
64
+ withFileTypes: true,
65
+ });
66
+ return entries
67
+ .filter((entry) => entry.isFile() && entry.name !== MODULE_CONFIG_FILE)
68
+ .map((entry) => {
69
+ const file = node_path_1.default
70
+ .relative(moduleDir, node_path_1.default.join(entry.parentPath, entry.name))
71
+ .split(node_path_1.default.sep)
72
+ .join("/");
73
+ if (GITIGNORE_TEMPLATE_NAMES.includes(file))
74
+ return ".gitignore";
75
+ return file.endsWith(TEMPLATE_EXTENSION)
76
+ ? file.slice(0, -TEMPLATE_EXTENSION.length)
77
+ : file;
78
+ });
79
+ }
54
80
  // Compiles and renders template content with Handlebars and provided context
55
81
  function compileTemplate(content, context) {
56
82
  try {
57
- // Register helper functions with Handlebars
58
- Object.entries(context.helpers).forEach(([name, fn]) => {
59
- handlebars_1.default.registerHelper(name, fn);
60
- });
61
- // Compile the template
62
- const template = handlebars_1.default.compile(content);
63
- // Render with context
64
- return template(context);
83
+ // The output is source code, not HTML, so values are written unescaped
84
+ const template = handlebars_1.default.compile(content, { noEscape: true });
85
+ return template(context, { helpers: context.helpers });
65
86
  }
66
87
  catch (error) {
67
88
  throw (0, types_1.buildError)(error, "TEMPLATE_SYNTAX_ERROR", "Failed to compile Handlebars template");
68
89
  }
69
90
  }
70
- // Processes multiple template files by replacing variables and handling extensions
71
- async function processTemplateFiles(targetDir, context, templateFiles) {
72
- for (const templateFile of templateFiles) {
73
- await processTemplateFile(path_1.default.join(targetDir, templateFile), context);
91
+ // Renders every .hbs file in the project and removes the .hbs extension
92
+ async function processTemplateFiles(targetDir, context) {
93
+ const files = await (0, promises_1.readdir)(targetDir, { recursive: true });
94
+ for (const file of files) {
95
+ if (file.endsWith(TEMPLATE_EXTENSION)) {
96
+ await processTemplateFile(node_path_1.default.join(targetDir, file), context);
97
+ }
74
98
  }
75
99
  }
76
- // Processes a single template file, replaces variables, and renames/deletes .hbs files
100
+ // Renders a single .hbs file next to itself and deletes the template
77
101
  async function processTemplateFile(filePath, context) {
78
102
  try {
79
- // Check if file exists
80
- const exists = await fs_extra_1.default.pathExists(filePath);
81
- if (!exists) {
82
- return; // Skip if file doesn't exist
83
- }
84
- // Read template file
85
103
  const content = await fs_extra_1.default.readFile(filePath, "utf-8");
86
- // Compile and render template with Handlebars
87
104
  const rendered = compileTemplate(content, context);
88
- // Determine output path (remove .hbs extension if present)
89
- const outputPath = filePath.endsWith(".hbs")
90
- ? filePath.slice(0, -4)
91
- : filePath;
92
- // Write processed content
93
- await fs_extra_1.default.writeFile(outputPath, rendered, "utf-8");
94
- // Remove original .hbs file if it was renamed
95
- if (outputPath !== filePath) {
96
- await fs_extra_1.default.remove(filePath);
97
- }
105
+ await fs_extra_1.default.writeFile(filePath.slice(0, -TEMPLATE_EXTENSION.length), rendered, "utf-8");
106
+ await fs_extra_1.default.remove(filePath);
98
107
  }
99
108
  catch (error) {
100
- // Re-throw BuildErrors as-is
101
- if (error && typeof error === "object" && "code" in error) {
102
- throw error;
103
- }
104
- // Wrap other errors
105
- throw (0, types_1.buildError)(error, "TEMPLATE_WRITE_ERROR", `Failed to process template file ${filePath}`);
109
+ throw (0, types_1.buildError)(error, "TEMPLATE_PROCESSING_ERROR", `Failed to process template file ${filePath}`);
106
110
  }
107
111
  }
108
- //# sourceMappingURL=fileProcessor.js.map