better-ts-stack 2.0.0 → 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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/args.js +2 -2
  4. package/dist/builder/buildPlanner.js +71 -0
  5. package/dist/builder/configGenerator.js +9 -7
  6. package/dist/builder/dependencyInstaller.js +0 -1
  7. package/dist/builder/fileProcessor.js +47 -44
  8. package/dist/builder/gitInitializer.js +20 -1
  9. package/dist/builder/index.js +19 -38
  10. package/dist/builder/moduleSelector.js +0 -1
  11. package/dist/builder/templateContext.js +0 -22
  12. package/dist/index.js +4 -3
  13. package/dist/modules/registry.js +4 -6
  14. package/dist/output/nextSteps.js +0 -1
  15. package/dist/prompts/index.js +0 -1
  16. package/dist/prompts/stack.js +4 -1
  17. package/dist/types/index.js +7 -3
  18. package/dist/ui/index.js +0 -1
  19. package/dist/ui/messages.js +25 -1
  20. package/dist/ui/prompts.js +0 -1
  21. package/dist/ui/theme.js +0 -1
  22. package/dist/validators/index.js +10 -41
  23. package/package.json +2 -1
  24. package/templates/backend/express/.prettierrc +2 -2
  25. package/templates/backend/express/config.json +1 -5
  26. package/templates/backend/express/eslint.config.mjs +40 -40
  27. package/templates/backend/express/gitignore +36 -36
  28. package/templates/backend/express/src/{index.ts → index.ts.hbs} +28 -29
  29. package/templates/backend/express/src/routes/health.ts +12 -12
  30. package/templates/backend/express/tsconfig.json +25 -25
  31. package/templates/backend/nestjs/.prettierrc +16 -16
  32. package/templates/backend/nestjs/config.json +1 -4
  33. package/templates/backend/nestjs/eslint.config.mjs +40 -40
  34. package/templates/backend/nestjs/gitignore +36 -36
  35. package/templates/backend/nestjs/nest-cli.json +9 -9
  36. package/templates/backend/nestjs/src/app.module.ts.hbs +34 -34
  37. package/templates/backend/nestjs/src/health/health.controller.ts +12 -12
  38. package/templates/backend/nestjs/src/main.ts +23 -23
  39. package/templates/backend/nestjs/tsconfig.json +25 -25
  40. package/templates/frontend/nextjs/components.json +19 -19
  41. package/templates/frontend/nextjs/config.json +1 -2
  42. package/templates/frontend/nextjs/gitignore +42 -42
  43. package/templates/frontend/nextjs/lib/utils.ts +6 -6
  44. package/templates/frontend/nextjs/proxy.ts.hbs +24 -24
  45. package/templates/frontend/nextjs/tsconfig.json +34 -34
  46. package/templates/frontend/tanstack/config.json +1 -4
  47. package/templates/frontend/tanstack/src/routeTree.gen.ts +68 -68
  48. package/templates/frontend/tanstack/src/styles/app.css +85 -85
  49. package/templates/modules/auth/express/config.json +1 -8
  50. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +116 -116
  51. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
  52. package/templates/modules/auth/express/src/services/userStore.ts.hbs +117 -117
  53. package/templates/modules/auth/nestjs/config.json +1 -5
  54. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -46
  55. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -36
  56. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -22
  57. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -35
  58. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -70
  59. package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -27
  60. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -144
  61. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -16
  62. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +96 -96
  63. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +35 -35
  64. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +35 -35
  65. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +132 -132
  66. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +50 -50
  67. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +152 -152
  68. package/templates/modules/auth/nextjs/config.json +2 -15
  69. package/templates/modules/auth/nextjs/lib/auth-schema.ts +39 -39
  70. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +37 -37
  71. package/templates/modules/auth/tanstack/config.json +2 -16
  72. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +4 -4
  73. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +140 -140
  74. package/templates/modules/docker/.dockerignore +14 -14
  75. package/templates/modules/docker/Dockerfile.hbs +1 -3
  76. package/templates/modules/docker/config.json +1 -5
  77. package/templates/modules/docker/docker-compose.yml.hbs +9 -9
  78. package/templates/modules/drizzle/express/config.json +1 -2
  79. package/templates/modules/drizzle/express/drizzle.config.ts +13 -13
  80. package/templates/modules/drizzle/express/src/lib/db.ts +13 -13
  81. package/templates/modules/drizzle/express/src/lib/schema.ts +10 -10
  82. package/templates/modules/drizzle/nestjs/config.json +1 -2
  83. package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -13
  84. package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -30
  85. package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -10
  86. package/templates/modules/drizzle/nextjs/config.json +1 -4
  87. package/templates/modules/drizzle/nextjs/lib/db.ts +13 -13
  88. package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +84 -84
  89. package/templates/modules/drizzle/tanstack/config.json +5 -5
  90. package/templates/modules/mongoose/express/config.json +1 -2
  91. package/templates/modules/mongoose/nestjs/config.json +1 -2
  92. package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -21
  93. package/templates/modules/mongoose/nextjs/config.json +1 -2
  94. package/templates/modules/mongoose/nextjs/lib/db.ts +36 -36
  95. package/templates/modules/mongoose/tanstack/config.json +1 -2
  96. package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -37
  97. package/templates/modules/prisma/express/config.json +0 -1
  98. package/templates/modules/prisma/express/prisma/schema.prisma +1 -1
  99. package/templates/modules/prisma/express/prisma.config.ts +12 -12
  100. package/templates/modules/prisma/express/src/lib/prisma.ts +24 -24
  101. package/templates/modules/prisma/nestjs/config.json +0 -1
  102. package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -23
  103. package/templates/modules/prisma/nestjs/prisma.config.ts +12 -12
  104. package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -11
  105. package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -17
  106. package/templates/modules/prisma/nextjs/config.json +0 -1
  107. package/templates/modules/prisma/nextjs/prisma/schema.prisma.hbs +91 -91
  108. package/templates/modules/prisma/tanstack/config.json +0 -1
  109. package/dist/args.d.ts +0 -12
  110. package/dist/args.d.ts.map +0 -1
  111. package/dist/args.js.map +0 -1
  112. package/dist/builder/configGenerator.d.ts +0 -7
  113. package/dist/builder/configGenerator.d.ts.map +0 -1
  114. package/dist/builder/configGenerator.js.map +0 -1
  115. package/dist/builder/dependencyInstaller.d.ts +0 -3
  116. package/dist/builder/dependencyInstaller.d.ts.map +0 -1
  117. package/dist/builder/dependencyInstaller.js.map +0 -1
  118. package/dist/builder/fileProcessor.d.ts +0 -6
  119. package/dist/builder/fileProcessor.d.ts.map +0 -1
  120. package/dist/builder/fileProcessor.js.map +0 -1
  121. package/dist/builder/gitInitializer.d.ts +0 -2
  122. package/dist/builder/gitInitializer.d.ts.map +0 -1
  123. package/dist/builder/gitInitializer.js.map +0 -1
  124. package/dist/builder/index.d.ts +0 -3
  125. package/dist/builder/index.d.ts.map +0 -1
  126. package/dist/builder/index.js.map +0 -1
  127. package/dist/builder/moduleSelector.d.ts +0 -6
  128. package/dist/builder/moduleSelector.d.ts.map +0 -1
  129. package/dist/builder/moduleSelector.js.map +0 -1
  130. package/dist/builder/templateContext.d.ts +0 -6
  131. package/dist/builder/templateContext.d.ts.map +0 -1
  132. package/dist/builder/templateContext.js.map +0 -1
  133. package/dist/index.d.ts +0 -2
  134. package/dist/index.d.ts.map +0 -1
  135. package/dist/index.js.map +0 -1
  136. package/dist/modules/registry.d.ts +0 -6
  137. package/dist/modules/registry.d.ts.map +0 -1
  138. package/dist/modules/registry.js.map +0 -1
  139. package/dist/output/nextSteps.d.ts +0 -3
  140. package/dist/output/nextSteps.d.ts.map +0 -1
  141. package/dist/output/nextSteps.js.map +0 -1
  142. package/dist/prompts/index.d.ts +0 -4
  143. package/dist/prompts/index.d.ts.map +0 -1
  144. package/dist/prompts/index.js.map +0 -1
  145. package/dist/prompts/stack.d.ts +0 -4
  146. package/dist/prompts/stack.d.ts.map +0 -1
  147. package/dist/prompts/stack.js.map +0 -1
  148. package/dist/types/index.d.ts +0 -137
  149. package/dist/types/index.d.ts.map +0 -1
  150. package/dist/types/index.js.map +0 -1
  151. package/dist/ui/index.d.ts +0 -3
  152. package/dist/ui/index.d.ts.map +0 -1
  153. package/dist/ui/index.js.map +0 -1
  154. package/dist/ui/messages.d.ts +0 -9
  155. package/dist/ui/messages.d.ts.map +0 -1
  156. package/dist/ui/messages.js.map +0 -1
  157. package/dist/ui/prompts.d.ts +0 -20
  158. package/dist/ui/prompts.d.ts.map +0 -1
  159. package/dist/ui/prompts.js.map +0 -1
  160. package/dist/ui/theme.d.ts +0 -9
  161. package/dist/ui/theme.d.ts.map +0 -1
  162. package/dist/ui/theme.js.map +0 -1
  163. package/dist/validators/index.d.ts +0 -6
  164. package/dist/validators/index.d.ts.map +0 -1
  165. package/dist/validators/index.js.map +0 -1
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
@@ -39,7 +39,7 @@ npx better-ts-stack . --framework nextjs --database drizzle --pm pnpm
39
39
  | `--git` / `--no-git` | Initialize a git repository |
40
40
  | `--install` / `--no-install` | Install dependencies |
41
41
  | `-y`, `--yes` | Use defaults for missing options, no prompts |
42
- | `--dry-run` | Show the summary without writing files |
42
+ | `--dry-run` | List what would be created, write nothing |
43
43
  | `-h`, `--help` | Show help |
44
44
  | `-v`, `--version` | Show the version |
45
45
 
package/dist/args.js CHANGED
@@ -23,7 +23,8 @@ Options left out are asked interactively, unless --yes is given.
23
23
  --git, --no-git Initialize a git repository
24
24
  --install, --no-install Install dependencies
25
25
  -y, --yes Use defaults for missing options and skip confirmation
26
- --dry-run Show what would be created without writing files
26
+ --dry-run List the files, dependencies, scripts, environment
27
+ variables, and commands without writing anything
27
28
  -h, --help Show this help
28
29
  -v, --version Show the version
29
30
 
@@ -87,4 +88,3 @@ function parseCliArgs(argv) {
87
88
  version: values.version ?? false,
88
89
  };
89
90
  }
90
- //# sourceMappingURL=args.js.map
@@ -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,7 +13,7 @@ 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");
17
18
  // Frameworks whose generated project uses ES modules
18
19
  const ESM_FRAMEWORKS = ["tanstack", "nestjs"];
@@ -88,7 +89,7 @@ async function generatePackageJson(targetDir, mergedConfig, config) {
88
89
  devDependencies: mergedConfig.devDependencies,
89
90
  overrides,
90
91
  };
91
- const packageJsonPath = path_1.default.join(targetDir, "package.json");
92
+ const packageJsonPath = node_path_1.default.join(targetDir, "package.json");
92
93
  await fs_extra_1.default.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
93
94
  }
94
95
  catch (error) {
@@ -124,12 +125,14 @@ async function generatePnpmWorkspace(targetDir, overrides) {
124
125
  lines.push(` - "${name}"`);
125
126
  }
126
127
  try {
127
- 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");
128
129
  }
129
130
  catch (error) {
130
131
  throw (0, types_1.buildError)(error, "PNPM_WORKSPACE_ERROR", "Failed to generate pnpm-workspace.yaml");
131
132
  }
132
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"];
133
136
  // Generates .env and .env.example files from merged environment variables
134
137
  async function generateEnvFile(targetDir, envVars) {
135
138
  try {
@@ -140,21 +143,20 @@ async function generateEnvFile(targetDir, envVars) {
140
143
  let localEnvContent = envContent;
141
144
  // Add each environment variable
142
145
  for (const [key, value] of Object.entries(envVars)) {
143
- const resolvedValue = ["JWT_SECRET", "BETTER_AUTH_SECRET"].includes(key)
146
+ const resolvedValue = exports.GENERATED_SECRETS.includes(key)
144
147
  ? (0, node_crypto_1.randomBytes)(32).toString("hex")
145
148
  : value;
146
149
  envContent += `${key}=${value}\n`;
147
150
  localEnvContent += `${key}=${resolvedValue}\n`;
148
151
  }
149
152
  // Write .env.example
150
- const envExamplePath = path_1.default.join(targetDir, ".env.example");
153
+ const envExamplePath = node_path_1.default.join(targetDir, ".env.example");
151
154
  await fs_extra_1.default.writeFile(envExamplePath, envContent, "utf-8");
152
155
  // Keep generated secrets out of the shareable example file.
153
- const envPath = path_1.default.join(targetDir, ".env");
156
+ const envPath = node_path_1.default.join(targetDir, ".env");
154
157
  await fs_extra_1.default.writeFile(envPath, localEnvContent, "utf-8");
155
158
  }
156
159
  catch (error) {
157
160
  throw (0, types_1.buildError)(error, "ENV_FILE_ERROR", "Failed to generate environment files");
158
161
  }
159
162
  }
160
- //# sourceMappingURL=configGenerator.js.map
@@ -21,4 +21,3 @@ function installDependencies(packageManager, cwd) {
21
21
  return false;
22
22
  }
23
23
  }
24
- //# 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
@@ -1,10 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInsideGitRepository = isInsideGitRepository;
3
4
  exports.initializeGitRepository = initializeGitRepository;
4
5
  const node_child_process_1 = require("node:child_process");
5
6
  const ui_1 = require("../ui");
7
+ // True when the directory is already tracked by a git repository above it
8
+ function isInsideGitRepository(cwd) {
9
+ try {
10
+ const output = (0, node_child_process_1.execSync)("git rev-parse --is-inside-work-tree", {
11
+ cwd,
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ });
14
+ return output.toString().trim() === "true";
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
6
20
  // Initializes a git repository in the given directory
7
21
  function initializeGitRepository(cwd) {
22
+ // A project created inside a repository (apps/api in a monorepo) must not
23
+ // get a nested repository of its own
24
+ if (isInsideGitRepository(cwd)) {
25
+ (0, ui_1.info)("Already inside a git repository, skipped git init");
26
+ return false;
27
+ }
8
28
  try {
9
29
  (0, node_child_process_1.execSync)("git init", { cwd, stdio: "ignore" });
10
30
  (0, ui_1.success)("Git repository initialized");
@@ -16,4 +36,3 @@ function initializeGitRepository(cwd) {
16
36
  return false;
17
37
  }
18
38
  }
19
- //# sourceMappingURL=gitInitializer.js.map
@@ -3,18 +3,19 @@ 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.planBuild = void 0;
6
7
  exports.build = build;
7
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
- const registry_1 = require("../modules/registry");
9
9
  const nextSteps_1 = require("../output/nextSteps");
10
10
  const types_1 = require("../types");
11
11
  const validators_1 = require("../validators");
12
+ const buildPlanner_1 = require("./buildPlanner");
12
13
  const configGenerator_1 = require("./configGenerator");
13
14
  const dependencyInstaller_1 = require("./dependencyInstaller");
14
15
  const fileProcessor_1 = require("./fileProcessor");
15
16
  const gitInitializer_1 = require("./gitInitializer");
16
- const moduleSelector_1 = require("./moduleSelector");
17
- const templateContext_1 = require("./templateContext");
17
+ var buildPlanner_2 = require("./buildPlanner");
18
+ Object.defineProperty(exports, "planBuild", { enumerable: true, get: function () { return buildPlanner_2.planBuild; } });
18
19
  // Main function to orchestrate the project construction process
19
20
  async function build(config, targetDir) {
20
21
  // 1. Validate target directory
@@ -27,51 +28,29 @@ async function build(config, targetDir) {
27
28
  await fs_extra_1.default.ensureDir(targetDir).catch((e) => {
28
29
  throw (0, types_1.buildError)(e, "DIRECTORY_CREATE_ERROR", "Failed to create directory");
29
30
  });
30
- // 3. Select and load modules
31
- const selection = (0, moduleSelector_1.selectModules)(config);
32
- const modules = [];
33
- try {
34
- for (const id of [selection.base, ...selection.modules]) {
35
- modules.push(await (0, registry_1.getModule)(id));
36
- }
31
+ // 3. Load the selected modules, build the template context, and merge
32
+ // the module configurations
33
+ const { modules, templateContext, mergedConfig } = await (0, buildPlanner_1.loadBuildInputs)(config);
34
+ // 4. Copy module files
35
+ for (const module of modules) {
36
+ await (0, fileProcessor_1.copyModuleFiles)(module.path, targetDir);
37
37
  }
38
- catch (e) {
39
- throw (0, types_1.buildError)(e, "MODULE_LOADING_ERROR", "Failed to load modules");
40
- }
41
- // 4. Build template context and merge module configurations
42
- const templateContext = (0, templateContext_1.buildTemplateContext)(config);
43
- const mergedConfig = (0, configGenerator_1.mergeConfigurations)(modules.map((m) => m.config), templateContext);
44
- // 5. Copy module files
45
- try {
46
- for (const module of modules) {
47
- await (0, fileProcessor_1.copyModuleFiles)(module.path, targetDir);
48
- }
49
- }
50
- catch (e) {
51
- throw (0, types_1.buildError)(e, "FILE_COPY_ERROR", "Failed to copy module files");
52
- }
53
- // 6. Process Handlebars templates
54
- try {
55
- await (0, fileProcessor_1.processTemplateFiles)(targetDir, templateContext, modules.flatMap((m) => m.config.templateFiles));
56
- }
57
- catch (e) {
58
- throw (0, types_1.buildError)(e, "TEMPLATE_PROCESSING_ERROR", "Failed to process template files");
59
- }
60
- // 7. Generate package.json
38
+ // 5. Process Handlebars templates
39
+ await (0, fileProcessor_1.processTemplateFiles)(targetDir, templateContext);
40
+ // 6. Generate package.json
61
41
  await (0, configGenerator_1.generatePackageJson)(targetDir, mergedConfig, config);
62
42
  if (config.packageManager === "pnpm") {
63
43
  await (0, configGenerator_1.generatePnpmWorkspace)(targetDir, mergedConfig.overrides);
64
44
  }
65
- // 8. Generate environment files
45
+ // 7. Generate environment files
66
46
  await (0, configGenerator_1.generateEnvFile)(targetDir, mergedConfig.envVars);
67
- // 9. Install dependencies and initialize git (both optional); a failure
47
+ // 8. Install dependencies and initialize git (both optional); a failure
68
48
  // in either is reported as a warning and does not fail the build
69
49
  const depsInstalled = config.installDeps &&
70
50
  (0, dependencyInstaller_1.installDependencies)(config.packageManager, targetDir);
71
51
  if (config.initGit)
72
52
  (0, gitInitializer_1.initializeGitRepository)(targetDir);
73
53
  return {
74
- success: true,
75
54
  projectPath: targetDir,
76
55
  nextSteps: (0, nextSteps_1.generateNextSteps)(config, targetDir, depsInstalled),
77
56
  };
@@ -80,7 +59,9 @@ async function build(config, targetDir) {
80
59
  // Remove the partial project so a retry is not blocked by a non-empty
81
60
  // directory; a directory that already existed is emptied, not deleted
82
61
  await (didDirectoryExist ? fs_extra_1.default.emptyDir(targetDir) : fs_extra_1.default.remove(targetDir)).catch(() => undefined);
83
- throw (0, types_1.buildError)(error, "BUILDING_ERROR", "Building failed");
62
+ // Errors raised by the build steps already say what failed
63
+ throw (0, types_1.isBuildError)(error)
64
+ ? error
65
+ : (0, types_1.buildError)(error, "BUILDING_ERROR", "Building failed");
84
66
  }
85
67
  }
86
- //# sourceMappingURL=index.js.map
@@ -19,4 +19,3 @@ function selectModules(config) {
19
19
  modules.push(`auth/${framework}`);
20
20
  return { base, modules };
21
21
  }
22
- //# sourceMappingURL=moduleSelector.js.map
@@ -1,26 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lowercase = lowercase;
4
- exports.uppercase = uppercase;
5
- exports.kebabCase = kebabCase;
6
3
  exports.buildTemplateContext = buildTemplateContext;
7
4
  // Context builder for Handlebars, providing variables and helpers for templates
8
5
  const types_1 = require("../types");
9
- // Converts string to lowercase
10
- function lowercase(str) {
11
- return str.toLowerCase();
12
- }
13
- // Converts string to uppercase
14
- function uppercase(str) {
15
- return str.toUpperCase();
16
- }
17
- // Converts string to kebab-case
18
- function kebabCase(str) {
19
- return str
20
- .replace(/([a-z])([A-Z])/g, "$1-$2") // camelCase to kebab-case
21
- .replace(/[\s_]+/g, "-") // spaces and underscores to hyphens
22
- .toLowerCase();
23
- }
24
6
  function buildTemplateContext(config) {
25
7
  return {
26
8
  projectName: config.projectName,
@@ -31,9 +13,6 @@ function buildTemplateContext(config) {
31
13
  useDocker: config.useDocker,
32
14
  useAuth: config.useAuth,
33
15
  helpers: {
34
- lowercase,
35
- uppercase,
36
- kebabCase,
37
16
  eq: (a, b) => a === b,
38
17
  runner: () => {
39
18
  if (config.packageManager === "bun")
@@ -43,4 +22,3 @@ function buildTemplateContext(config) {
43
22
  },
44
23
  };
45
24
  }
46
- //# sourceMappingURL=templateContext.js.map
package/dist/index.js CHANGED
@@ -40,10 +40,12 @@ async function main() {
40
40
  };
41
41
  (0, ui_1.showSummary)(config, targetDir);
42
42
  if (args.dryRun) {
43
+ (0, ui_1.showPlan)(await (0, builder_1.planBuild)(config, targetDir));
43
44
  (0, ui_1.info)("Dry run: no files were written.");
44
45
  return;
45
46
  }
46
- if (!args.yes)
47
+ // Without a terminal nobody can answer, and every option came from a flag
48
+ if (!args.yes && process.stdin.isTTY)
47
49
  await (0, prompts_1.confirmBuild)();
48
50
  const result = await (0, builder_1.build)(config, targetDir);
49
51
  (0, ui_1.success)("Project created");
@@ -53,7 +55,6 @@ main()
53
55
  .then(() => process.exit(0))
54
56
  .catch((err) => {
55
57
  // Errors raised by the CLI carry a code and a message meant for the user
56
- (0, ui_1.error)(err instanceof Error && "code" in err ? err.message : String(err));
58
+ (0, ui_1.error)((0, types_1.isBuildError)(err) ? err.message : String(err));
57
59
  process.exit(1);
58
60
  });
59
- //# sourceMappingURL=index.js.map
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getModule = getModule;
7
7
  // Module registry for accessing base templates and feature modules
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const path_1 = __importDefault(require("path"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
10
  const zod_1 = require("zod");
11
11
  const types_1 = require("../types");
12
12
  const moduleConfigSchema = zod_1.z.object({
@@ -19,20 +19,19 @@ const moduleConfigSchema = zod_1.z.object({
19
19
  overrides: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
20
20
  scripts: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
21
21
  envVars: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
22
- templateFiles: zod_1.z.array(zod_1.z.string()).default([]),
23
22
  });
24
23
  // Determines the absolute filesystem path for a given module ID
25
24
  function resolveModulePath(id) {
26
25
  // Base templates: backend/express, frontend/nextjs
27
26
  if (id.startsWith("backend/") || id.startsWith("frontend/")) {
28
- return path_1.default.join(__dirname, `../../templates/${id}`);
27
+ return node_path_1.default.join(__dirname, `../../templates/${id}`);
29
28
  }
30
29
  // Feature modules: auth/express, prisma/nextjs, docker, mongoose, etc.
31
- return path_1.default.join(__dirname, `../../templates/modules/${id}`);
30
+ return node_path_1.default.join(__dirname, `../../templates/modules/${id}`);
32
31
  }
33
32
  // Reads and parses a module's config.json file
34
33
  async function loadModuleConfig(modulePath) {
35
- const configPath = path_1.default.join(modulePath, "config.json");
34
+ const configPath = node_path_1.default.join(modulePath, "config.json");
36
35
  try {
37
36
  const configContent = await fs_extra_1.default.readFile(configPath, "utf-8");
38
37
  const rawConfig = JSON.parse(configContent);
@@ -58,4 +57,3 @@ async function getModule(id) {
58
57
  path: modulePath,
59
58
  };
60
59
  }
61
- //# sourceMappingURL=registry.js.map
@@ -70,4 +70,3 @@ function generateNextSteps(config, targetDir, depsInstalled) {
70
70
  }
71
71
  return steps;
72
72
  }
73
- //# sourceMappingURL=nextSteps.js.map
@@ -32,4 +32,3 @@ async function confirmBuild() {
32
32
  process.exit(0);
33
33
  }
34
34
  }
35
- //# sourceMappingURL=index.js.map
@@ -9,6 +9,10 @@ const ui_1 = require("../ui");
9
9
  async function ask(preset, prompt) {
10
10
  if (preset !== undefined)
11
11
  return preset;
12
+ // Without a terminal a prompt reads end-of-input and looks like a cancel
13
+ if (!process.stdin.isTTY) {
14
+ throw (0, types_1.buildError)("Cannot ask questions without an interactive terminal. Pass --yes, or give every option as a flag (see --help)", "NOT_INTERACTIVE");
15
+ }
12
16
  const answer = await prompt();
13
17
  if ((0, core_1.isCancel)(answer)) {
14
18
  (0, ui_1.info)("Cancelled. Nothing was created.");
@@ -85,4 +89,3 @@ async function collectStackChoices(preset) {
85
89
  installDeps,
86
90
  };
87
91
  }
88
- //# sourceMappingURL=stack.js.map
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.packageManagerOptions = exports.postgresqlOrmOptions = exports.mongodbOrmOptions = exports.databaseSystemOptions = exports.frontendFrameworkOptions = exports.backendFrameworkOptions = exports.applicationTypeOptions = exports.DEFAULT_PORT = exports.DEFAULT_CHOICES = void 0;
5
5
  exports.buildError = buildError;
6
+ exports.isBuildError = isBuildError;
6
7
  exports.getApplicationType = getApplicationType;
7
8
  // Used for every choice not given as a flag when --yes is passed
8
9
  exports.DEFAULT_CHOICES = {
@@ -17,13 +18,17 @@ exports.DEFAULT_CHOICES = {
17
18
  // Port the generated server listens on
18
19
  exports.DEFAULT_PORT = 3000;
19
20
  // Unified error creation function
20
- function buildError(errorOrMessage, code, context, exitCode = 1) {
21
+ function buildError(errorOrMessage, code, context) {
21
22
  const message = errorOrMessage instanceof Error
22
23
  ? errorOrMessage.message
23
24
  : String(errorOrMessage);
24
25
  const fullMessage = context ? `${context}: ${message}` : message;
25
26
  const error = new Error(fullMessage);
26
- return Object.assign(error, { code, exitCode });
27
+ return Object.assign(error, { code });
28
+ }
29
+ // True for errors created by buildError(), whose message is meant for the user
30
+ function isBuildError(error) {
31
+ return error instanceof Error && "code" in error;
27
32
  }
28
33
  // Type-safe option definitions
29
34
  exports.applicationTypeOptions = [
@@ -76,4 +81,3 @@ function getApplicationType(framework) {
76
81
  ? "backend"
77
82
  : "fullstack";
78
83
  }
79
- //# sourceMappingURL=index.js.map
package/dist/ui/index.js CHANGED
@@ -16,4 +16,3 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./messages"), exports);
18
18
  __exportStar(require("./prompts"), exports);
19
- //# sourceMappingURL=index.js.map