create-lumen 2.0.0-alpha.1 → 2.0.0-alpha.2

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 (63) hide show
  1. package/README.md +15 -7
  2. package/bin/cli.js +1 -0
  3. package/package.json +2 -2
  4. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
  5. package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
  6. package/schema/lumen.config.v2.json +0 -1
  7. package/src/cli-args.js +65 -3
  8. package/src/configure.js +4 -0
  9. package/src/css.js +14 -30
  10. package/src/dependencies.js +28 -18
  11. package/src/engine/capabilities.js +174 -4
  12. package/src/engine/composition.js +644 -0
  13. package/src/engine/index.js +1 -0
  14. package/src/injector.js +294 -446
  15. package/src/main.js +6 -7
  16. package/src/manifest/schema.js +1 -1
  17. package/src/prompts.js +459 -247
  18. package/src/readme.js +23 -2
  19. package/src/scaffold.js +17 -0
  20. package/templates/architectures/hybrid/main.jsx +10 -0
  21. package/templates/architectures/hybrid/main.tsx +10 -0
  22. package/templates/architectures/hybrid/src/App.jsx +20 -0
  23. package/templates/architectures/hybrid/src/App.tsx +20 -0
  24. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
  25. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
  26. package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
  27. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
  28. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
  29. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
  30. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  31. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  32. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  33. package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
  34. package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
  35. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
  36. package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
  37. package/templates/architectures/none/main.jsx +10 -0
  38. package/templates/architectures/none/main.tsx +10 -0
  39. package/templates/architectures/none/src/App.jsx +15 -0
  40. package/templates/architectures/none/src/App.tsx +15 -0
  41. package/templates/architectures/none/src/styles/.gitkeep +1 -0
  42. package/templates/architectures/none/src/types/index.js +20 -0
  43. package/templates/architectures/none/src/types/index.ts +16 -0
  44. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
  45. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
  46. package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
  47. package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
  48. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
  49. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
  50. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
  51. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
  52. package/templates/css/tailwind/src/globals.css +10 -0
  53. package/templates/css/tailwind/src/themes.css +86 -4
  54. package/templates/ui/shadcn/lib/utils.js +6 -0
  55. package/templates/ui/shadcn/lib/utils.ts +6 -0
  56. package/templates/ui/shadcn/primitives/Button.jsx +48 -0
  57. package/templates/ui/shadcn/primitives/Button.tsx +59 -0
  58. package/templates/ui/shadcn/primitives/Card.jsx +55 -0
  59. package/templates/ui/shadcn/primitives/Card.tsx +73 -0
  60. package/templates/ui/shadcn/primitives/index.js +9 -0
  61. package/templates/ui/shadcn/primitives/index.ts +9 -0
  62. package/templates/css/bootstrap/src/themes.css +0 -14
  63. /package/templates/{css/bootstrap/src/globals.css → architectures/hybrid/src/features/.gitkeep} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-lumen
2
2
 
3
- A scaffolder that generates production-ready React + Vite projects (feature-based or type-based).
3
+ A scaffolder that generates production-ready React + Vite projects (feature-based, type-based, hybrid, or none).
4
4
 
5
5
  Note: this repository is the CLI scaffolder itself, not a generated app. Generated projects live in the target folder you create.
6
6
 
@@ -34,6 +34,12 @@ npm create lumen my-app -- --template react-ts
34
34
  npm create lumen my-app -- -t react-type-js
35
35
  ```
36
36
 
37
+ Choose framework and architecture directly:
38
+
39
+ ```bash
40
+ npm create lumen my-app -- --framework react --arch none
41
+ ```
42
+
37
43
  Drive scaffolding directly from a manifest file or inline JSON (manifest v2):
38
44
 
39
45
  ```bash
@@ -64,9 +70,9 @@ create-lumen my-app # after npm link
64
70
 
65
71
  ## Features
66
72
 
67
- - Architecture choice: feature-based or type-based (with Next.js hybrid/none in v2)
73
+ - Architecture choice: feature-based, type-based, hybrid, or none (per-framework availability)
68
74
  - TypeScript or JavaScript output
69
- - CSS frameworks: Tailwind CSS v4 (CSS-first `@theme`, v3 dropped), Bootstrap 5.3+, or none
75
+ - CSS frameworks: Tailwind CSS v4 (CSS-first `@theme`, v3 dropped) or none
70
76
  - Optional state management: Zustand or Redux Toolkit
71
77
  - Optional router (React Router)
72
78
  - Optional testing: Vitest or Jest
@@ -148,8 +154,8 @@ src/
148
154
  ├── layouts
149
155
  ├── stores
150
156
  ├── styles
151
- │   ├── globals.css # Tailwind/Bootstrap directives (or main.css for vanilla)
152
- │   └── themes.css # Theme tokens (Tailwind @theme / Bootstrap data-bs-theme / CSS vars)
157
+ │   ├── globals.css # Tailwind directives (or main.css for vanilla)
158
+ │   └── themes.css # Theme tokens (Tailwind @theme / CSS vars)
153
159
  ├── types
154
160
  └── utils
155
161
  ```
@@ -177,8 +183,8 @@ src/
177
183
  ├── services
178
184
  ├── store
179
185
  ├── styles
180
- │   ├── globals.css # Tailwind/Bootstrap directives (or main.css for vanilla)
181
- │   └── themes.css # Theme tokens (Tailwind @theme / Bootstrap data-bs-theme / CSS vars)
186
+ │   ├── globals.css # Tailwind directives (or main.css for vanilla)
187
+ │   └── themes.css # Theme tokens (Tailwind @theme / CSS vars)
182
188
  ├── test
183
189
  └── utils
184
190
  ```
@@ -199,6 +205,8 @@ The manifest now includes:
199
205
 
200
206
  ### Ecosystem Documentation & Contracts
201
207
 
208
+ - **[Documentation Index](./docs/README.md):** Complete overview of specifications, ADRs, and developer guides.
209
+ - **[Generator Engine & Capabilities Guide (M2)](./docs/engine-capabilities.md):** Capability model, template composition, shadcn/ui on Tailwind v4, architecture presets, and Bootstrap removal.
202
210
  - **[Manifest v2 Overview](./docs/manifest-v2.md):** Specification of the nested schema, validation behavior, and the new harness and architecture validation fields.
203
211
  - **[Shared Contract: `create-lumen` ↔ `lumen-cli`](./docs/contracts/manifest-v2-contract.md):** The agreed contract for path mapping, barrel conventions, version negotiation, and the harness/configuration interface.
204
212
  - **[v1 to v2 Migration Guide](./docs/migration/v1-to-v2.md):** Complete guide for migrating generated projects and legacy flat configs to v2.
package/bin/cli.js CHANGED
@@ -36,6 +36,7 @@ main({
36
36
  projectName: cliArgs.projectName,
37
37
  manifest: cliArgs.manifest,
38
38
  template: cliArgs.template,
39
+ arch: cliArgs.arch,
39
40
  }).catch((e) => {
40
41
  console.error(chalk.red("\nError:"), e.message || e);
41
42
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-lumen",
3
- "version": "2.0.0-alpha.1",
4
- "description": "Scaffold a React + Vite project with architecture choice: feature-based or type-based.",
3
+ "version": "2.0.0-alpha.2",
4
+ "description": "Scaffold a React + Vite project with architecture choice: feature-based, type-based, hybrid, or none.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-lumen": "bin/cli.js"
@@ -6,7 +6,7 @@
6
6
  "variant": "vite"
7
7
  },
8
8
  "styling": {
9
- "engine": "bootstrap"
9
+ "engine": "none"
10
10
  },
11
11
  "architecture": {
12
12
  "type": "feature-based"
@@ -6,7 +6,7 @@
6
6
  "variant": "vite"
7
7
  },
8
8
  "styling": {
9
- "engine": "bootstrap"
9
+ "engine": "none"
10
10
  },
11
11
  "architecture": {
12
12
  "type": "type-based"
@@ -62,7 +62,6 @@
62
62
  "type": "string",
63
63
  "enum": [
64
64
  "tailwind",
65
- "bootstrap",
66
65
  "none"
67
66
  ]
68
67
  }
package/src/cli-args.js CHANGED
@@ -3,6 +3,11 @@ import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { parseManifest } from "./manifest/schema.js";
6
+ import {
7
+ normalizeFramework,
8
+ isPromptVisible,
9
+ getDefaultResponses,
10
+ } from "./engine/capabilities.js";
6
11
 
7
12
  export const PRESETS = {
8
13
  "react-ts": {
@@ -141,6 +146,40 @@ export const PRESETS = {
141
146
  gitInit: true,
142
147
  readme: true,
143
148
  },
149
+ "react-none-ts": {
150
+ frameworkName: "react",
151
+ frameworkVariant: "vite",
152
+ architecture: "none",
153
+ language: "ts",
154
+ cssFramework: "tailwind",
155
+ testing: "vitest",
156
+ router: false,
157
+ stateManagement: "none",
158
+ iconLibrary: "none",
159
+ apiClient: "none",
160
+ linter: "eslint",
161
+ formatter: "prettier",
162
+ docsLanguage: "en",
163
+ gitInit: true,
164
+ readme: true,
165
+ },
166
+ "react-none-js": {
167
+ frameworkName: "react",
168
+ frameworkVariant: "vite",
169
+ architecture: "none",
170
+ language: "js",
171
+ cssFramework: "tailwind",
172
+ testing: "vitest",
173
+ router: false,
174
+ stateManagement: "none",
175
+ iconLibrary: "none",
176
+ apiClient: "none",
177
+ linter: "eslint",
178
+ formatter: "prettier",
179
+ docsLanguage: "en",
180
+ gitInit: true,
181
+ readme: true,
182
+ },
144
183
  };
145
184
 
146
185
  /**
@@ -151,6 +190,8 @@ export const PRESETS = {
151
190
  * quickSetup: boolean,
152
191
  * manifest: string | null,
153
192
  * template: string | null,
193
+ * framework: string | null,
194
+ * arch: string | null,
154
195
  * help: boolean,
155
196
  * version: boolean,
156
197
  * positionals: string[]
@@ -163,6 +204,9 @@ export function parseCliArgs(argv = process.argv.slice(2)) {
163
204
  yes: { type: "boolean", short: "y", default: false },
164
205
  manifest: { type: "string", short: "m" },
165
206
  template: { type: "string", short: "t" },
207
+ framework: { type: "string", short: "f" },
208
+ arch: { type: "string", short: "a" },
209
+ architecture: { type: "string" },
166
210
  help: { type: "boolean", short: "h", default: false },
167
211
  version: { type: "boolean", short: "v", default: false },
168
212
  },
@@ -172,11 +216,20 @@ export function parseCliArgs(argv = process.argv.slice(2)) {
172
216
 
173
217
  const projectName = positionals[0] ? positionals[0].trim() : null;
174
218
 
219
+ let arch = values.arch || values.architecture || null;
220
+ if (arch) {
221
+ arch = arch.trim().toLowerCase();
222
+ if (arch === "feature") arch = "feature-based";
223
+ else if (arch === "component" || arch === "component-based" || arch === "type") arch = "type-based";
224
+ }
225
+
175
226
  return {
176
227
  projectName,
177
228
  quickSetup: Boolean(values.yes),
178
229
  manifest: values.manifest || null,
179
230
  template: values.template || null,
231
+ framework: values.framework ? values.framework.trim() : null,
232
+ arch,
180
233
  help: Boolean(values.help),
181
234
  version: Boolean(values.version),
182
235
  positionals,
@@ -222,13 +275,18 @@ export function loadManifestSource(source, cwd = process.cwd()) {
222
275
  export function manifestToResponses(manifest, projectName) {
223
276
  const frameworkName = manifest.framework?.name || "react";
224
277
  const frameworkVariant = manifest.framework?.variant || "vite";
225
- const isNext = frameworkName === "next";
278
+ const framework = normalizeFramework({ name: frameworkName, variant: frameworkVariant });
279
+ const canClientRoute = isPromptVisible(framework, "router");
280
+ const supportsBundler = isPromptVisible(framework, "bundler");
226
281
 
227
282
  return {
228
283
  projectName,
229
284
  frameworkName,
230
285
  frameworkVariant,
231
- bundler: manifest.framework?.bundler,
286
+ bundler:
287
+ supportsBundler && manifest.framework?.bundler
288
+ ? manifest.framework.bundler
289
+ : undefined,
232
290
  adapter: manifest.framework?.adapter,
233
291
  architecture: manifest.architecture?.type || "feature-based",
234
292
  language: manifest.tooling?.language || "ts",
@@ -241,7 +299,7 @@ export function manifestToResponses(manifest, projectName) {
241
299
  agentDocs: manifest.agentDocs,
242
300
  // Sensible defaults for scaffolder conditional passes
243
301
  testing: "vitest",
244
- router: isNext ? false : true,
302
+ router: canClientRoute,
245
303
  stateManagement: "none",
246
304
  iconLibrary: "none",
247
305
  apiClient: "none",
@@ -263,8 +321,10 @@ ${chalk.bold("USAGE")}
263
321
 
264
322
  ${chalk.bold("OPTIONS")}
265
323
  ${chalk.yellow("-y, --yes")} Quick setup with recommended defaults (TS + Tailwind + Router + ESLint + Prettier)
324
+ ${chalk.yellow("-a, --arch")} <name> Architecture preset (feature-based, type-based, none, hybrid)
266
325
  ${chalk.yellow("-m, --manifest")} <path> Drive scaffolding from a lumen.config.json or inline JSON
267
326
  ${chalk.yellow("-t, --template")} <name> Scaffold using a preset (${Object.keys(PRESETS).join(", ")})
327
+ ${chalk.yellow("-f, --framework")} <name> Target framework (react, next, react:vite, next:app-router, etc.)
268
328
  ${chalk.yellow("-h, --help")} Show this help message
269
329
  ${chalk.yellow("-v, --version")} Show version number
270
330
 
@@ -273,6 +333,8 @@ ${chalk.bold("PRESETS")}
273
333
  ${chalk.cyan("react-js")} React + Vite + JavaScript (feature-based)
274
334
  ${chalk.cyan("react-type-ts")} React + Vite + TypeScript (type-based)
275
335
  ${chalk.cyan("react-type-js")} React + Vite + JavaScript (type-based)
336
+ ${chalk.cyan("react-none-ts")} React + Vite + TypeScript (none)
337
+ ${chalk.cyan("react-none-js")} React + Vite + JavaScript (none)
276
338
 
277
339
  ${chalk.bold("EXAMPLES")}
278
340
  ${chalk.gray("# Interactive setup")}
package/src/configure.js CHANGED
@@ -88,6 +88,10 @@ async function configureTsconfig(projectPath) {
88
88
  const rootTsconfig = {
89
89
  files: [],
90
90
  references,
91
+ compilerOptions: {
92
+ baseUrl: ".",
93
+ paths,
94
+ },
91
95
  };
92
96
  await fsp.writeFile(
93
97
  rootTsconfigPath,
package/src/css.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
3
  import { deleteIfExists } from "@/utils/fs.js";
4
+ import {
5
+ normalizeFramework,
6
+ hasCapability,
7
+ CAPABILITIES,
8
+ } from "@/engine/capabilities.js";
4
9
 
5
10
  export async function setupCssFramework({
6
11
  projectPath,
@@ -13,18 +18,20 @@ export async function setupCssFramework({
13
18
  framework = "vite",
14
19
  }) {
15
20
  process.chdir(projectPath);
21
+ const fw = normalizeFramework(framework);
22
+ const isFilesystemRouted = hasCapability(fw, CAPABILITIES.FILESYSTEM_ROUTING);
16
23
  const cssDir = path.join(templatesDir, "css");
17
24
  // Feature-based main.* imports ./shared/styles/globals.css (or main.css when
18
25
  // framework is "none"); type-based imports ./styles/globals.css (or
19
26
  // main.css). The chosen framework's styles land in the file the architecture
20
27
  // actually imports.
21
28
  const stylesDir =
22
- architecture === "feature-based"
29
+ architecture === "feature-based" || architecture === "hybrid"
23
30
  ? path.join(projectPath, "src", "shared", "styles")
24
31
  : path.join(projectPath, "src", "styles");
25
32
 
26
- // Vanilla CSS gets a stylesheet named main.css; tailwind/bootstrap keep
27
- // globals.css (their idiomatic name — they inject framework directives).
33
+ // Vanilla CSS gets a stylesheet named main.css; tailwind keeps
34
+ // globals.css (its idiomatic name — it injects framework directives).
28
35
  const mainFileName = cssFramework === "none" ? "main.css" : "globals.css";
29
36
 
30
37
  async function writeCss(content, fileName) {
@@ -32,7 +39,7 @@ export async function setupCssFramework({
32
39
  }
33
40
 
34
41
  if (cssFramework === "tailwind") {
35
- if (framework === "next") {
42
+ if (isFilesystemRouted || fw.name === "next") {
36
43
  const postcssConfigFile = "postcss.config.mjs";
37
44
  const postcssConfigTarget = path.join(projectPath, postcssConfigFile);
38
45
  const postcssConfigContent = await fsp.readFile(
@@ -60,29 +67,6 @@ export async function setupCssFramework({
60
67
  );
61
68
  await writeCss(globalsContent, "globals.css");
62
69
  await writeCss(themesContent, "themes.css");
63
- } else if (cssFramework === "bootstrap") {
64
- const mainFile = path.join(projectPath, "src", `main.${ext}`);
65
- try {
66
- let mainContent = await fsp.readFile(mainFile, "utf8");
67
- if (
68
- !mainContent.includes("bootstrap/dist/css/bootstrap.min.css")
69
- ) {
70
- mainContent =
71
- "import 'bootstrap/dist/css/bootstrap.min.css';\n" + mainContent;
72
- await fsp.writeFile(mainFile, mainContent, "utf8");
73
- }
74
- } catch {}
75
-
76
- const globalsContent = await fsp.readFile(
77
- path.join(cssDir, "bootstrap", "src", "globals.css"),
78
- "utf8"
79
- );
80
- const themesContent = await fsp.readFile(
81
- path.join(cssDir, "bootstrap", "src", "themes.css"),
82
- "utf8"
83
- );
84
- await writeCss(globalsContent, "globals.css");
85
- await writeCss(themesContent, "themes.css");
86
70
  } else {
87
71
  const mainContent = await fsp.readFile(
88
72
  path.join(cssDir, "none", "src", "main.css"),
@@ -110,17 +94,17 @@ export async function setupCssFramework({
110
94
  }
111
95
 
112
96
  // Rewrite the CSS import in main.{ext} so it matches the file the framework
113
- // actually wrote (main.css for "none", globals.css for tailwind/bootstrap).
97
+ // actually wrote (main.css for "none", globals.css for tailwind).
114
98
  async function syncMainCssImport(projectPath, ext, architecture, mainFileName) {
115
99
  const mainPath = path.join(projectPath, "src", `main.${ext}`);
116
100
  try {
117
101
  let content = await fsp.readFile(mainPath, "utf8");
118
102
  const expected =
119
- architecture === "feature-based"
103
+ architecture === "feature-based" || architecture === "hybrid"
120
104
  ? `./shared/styles/${mainFileName}`
121
105
  : `./styles/${mainFileName}`;
122
106
  const wrong =
123
- architecture === "feature-based"
107
+ architecture === "feature-based" || architecture === "hybrid"
124
108
  ? /import ['"]\.\/shared\/styles\/(globals|main)\.css['"]/
125
109
  : /import ['"]\.\/styles\/(globals|main)\.css['"]/;
126
110
  content = content.replace(wrong, `import '${expected}'`);
@@ -25,13 +25,13 @@ export async function installDeps(pkgManager, dev, packages, cwd) {
25
25
  }
26
26
 
27
27
  export async function installAllDeps(responses, pkg, cwd) {
28
- const { deps, devDeps } = computeDeps(responses);
28
+ const { deps, devDepBatches } = computeDeps(responses);
29
29
 
30
30
  if (deps.length > 0) {
31
31
  await installDeps(pkg, false, deps, cwd);
32
32
  }
33
- if (devDeps.length > 0) {
34
- await installDeps(pkg, true, devDeps, cwd);
33
+ for (const batch of devDepBatches) {
34
+ await installDeps(pkg, true, batch, cwd);
35
35
  }
36
36
  }
37
37
 
@@ -40,18 +40,19 @@ export async function installAllDeps(responses, pkg, cwd) {
40
40
  // the conditional wiring stays verifiable without a network install.
41
41
  export function computeDeps(responses) {
42
42
  const deps = [];
43
- const devDeps = [];
43
+ const cssDevDeps = [];
44
+ const testingDevDeps = [];
45
+ const linterDevDeps = [];
46
+ const formatterDevDeps = [];
44
47
 
45
48
  // CSS framework
46
49
  if (responses.cssFramework === "tailwind") {
47
50
  if (responses.framework === "next") {
48
- devDeps.push("tailwindcss", "@tailwindcss/postcss", "postcss");
51
+ cssDevDeps.push("tailwindcss", "@tailwindcss/postcss", "postcss");
49
52
  } else {
50
- devDeps.push("tailwindcss", "@tailwindcss/vite");
53
+ cssDevDeps.push("tailwindcss", "@tailwindcss/vite");
51
54
  }
52
55
  deps.push("clsx", "tailwind-merge");
53
- } else if (responses.cssFramework === "bootstrap") {
54
- deps.push("bootstrap", "react-bootstrap");
55
56
  }
56
57
 
57
58
  // State management
@@ -80,14 +81,14 @@ export function computeDeps(responses) {
80
81
 
81
82
  // Testing
82
83
  if (responses.testing === "vitest") {
83
- devDeps.push(
84
+ testingDevDeps.push(
84
85
  "vitest",
85
86
  "@testing-library/react",
86
87
  "@testing-library/jest-dom",
87
88
  "jsdom"
88
89
  );
89
90
  } else if (responses.testing === "jest") {
90
- devDeps.push(
91
+ testingDevDeps.push(
91
92
  "jest",
92
93
  "jest-environment-jsdom",
93
94
  "@testing-library/react",
@@ -103,29 +104,38 @@ export function computeDeps(responses) {
103
104
  // Linter
104
105
  if (responses.linter === "eslint") {
105
106
  if (responses.framework === "next") {
106
- devDeps.push("eslint", "eslint-config-next");
107
+ linterDevDeps.push("eslint", "eslint-config-next");
107
108
  } else {
108
- devDeps.push("eslint", "@eslint/js", "eslint-plugin-react-hooks", "eslint-plugin-react-refresh", "globals");
109
+ linterDevDeps.push("eslint", "@eslint/js", "eslint-plugin-react-hooks", "eslint-plugin-react-refresh", "globals");
109
110
  if (responses.language === "ts") {
110
- devDeps.push("typescript-eslint", "jiti");
111
+ linterDevDeps.push("typescript-eslint", "jiti");
111
112
  }
112
113
  }
113
114
  } else if (responses.linter === "oxlint") {
114
- devDeps.push("oxlint");
115
+ linterDevDeps.push("oxlint");
115
116
  }
116
117
 
117
118
  // Formatter
118
119
  if (responses.formatter === "prettier") {
119
- devDeps.push("prettier");
120
+ formatterDevDeps.push("prettier");
120
121
  if (responses.linter === "eslint") {
121
- devDeps.push("eslint-config-prettier");
122
+ formatterDevDeps.push("eslint-config-prettier");
122
123
  }
123
124
  } else if (responses.formatter === "oxfmt") {
124
- devDeps.push("oxfmt");
125
+ formatterDevDeps.push("oxfmt");
125
126
  }
126
127
 
127
128
  // Always re-install react + react-dom for consistency
128
129
  deps.push("react", "react-dom");
129
130
 
130
- return { deps, devDeps };
131
+ const devDepBatches = [
132
+ cssDevDeps,
133
+ testingDevDeps,
134
+ linterDevDeps,
135
+ formatterDevDeps,
136
+ ].filter((batch) => batch.length > 0);
137
+
138
+ const devDeps = devDepBatches.flat();
139
+
140
+ return { deps, devDeps, devDepBatches };
131
141
  }
@@ -30,7 +30,7 @@ export const FRAMEWORK_CAPABILITIES = {
30
30
  CAPABILITIES.AGENT_DOCS,
31
31
  ],
32
32
  allowedArchitectures: ["feature-based", "type-based", "none"],
33
- allowedStyling: ["tailwind", "bootstrap", "none"],
33
+ allowedStyling: ["tailwind", "none"],
34
34
  allowedUiKits: ["shadcn", "none"],
35
35
  allowedLanguages: ["ts", "js"],
36
36
  allowedLinters: ["eslint", "oxlint", "biome", "none"],
@@ -76,7 +76,7 @@ export const FRAMEWORK_CAPABILITIES = {
76
76
  CAPABILITIES.AGENT_DOCS,
77
77
  ],
78
78
  allowedArchitectures: ["feature-based", "hybrid", "none"],
79
- allowedStyling: ["tailwind", "bootstrap", "none"],
79
+ allowedStyling: ["tailwind", "none"],
80
80
  allowedUiKits: ["shadcn", "none"],
81
81
  allowedLanguages: ["ts", "js"],
82
82
  allowedLinters: ["eslint", "biome", "none"],
@@ -132,8 +132,12 @@ export function isOptionCompatible(framework, optionKey, optionValue) {
132
132
  if (!desc) return false;
133
133
 
134
134
  switch (optionKey) {
135
- case "architecture":
136
- return desc.allowedArchitectures.includes(optionValue);
135
+ case "architecture": {
136
+ let val = optionValue;
137
+ if (val === "component" || val === "component-based") val = "type-based";
138
+ if (val === "feature") val = "feature-based";
139
+ return desc.allowedArchitectures.includes(val);
140
+ }
137
141
  case "styling":
138
142
  case "cssFramework":
139
143
  return desc.allowedStyling.includes(optionValue);
@@ -216,3 +220,169 @@ export function validateCompatibility(framework, options = {}) {
216
220
  errors,
217
221
  };
218
222
  }
223
+
224
+ /**
225
+ * Normalize framework input into a standard { name, variant } descriptor.
226
+ * @param {string | { name?: string, variant?: string } | null | undefined} framework
227
+ * @returns {{ name: string, variant: string }}
228
+ */
229
+ export function normalizeFramework(framework) {
230
+ if (!framework) {
231
+ return { name: "react", variant: "vite" };
232
+ }
233
+
234
+ if (typeof framework === "string") {
235
+ const trimmed = framework.trim();
236
+ if (!trimmed) {
237
+ return { name: "react", variant: "vite" };
238
+ }
239
+ if (trimmed.includes(":")) {
240
+ const [name, ...rest] = trimmed.split(":");
241
+ return { name, variant: rest.join(":") };
242
+ }
243
+ if (trimmed === "next") {
244
+ return { name: "next", variant: "app-router" };
245
+ }
246
+ if (trimmed === "react") {
247
+ return { name: "react", variant: "vite" };
248
+ }
249
+ return { name: trimmed, variant: "vite" };
250
+ }
251
+
252
+ if (typeof framework === "object") {
253
+ const name = framework.name || "react";
254
+ const variant =
255
+ framework.variant || (framework.name === "next" ? "app-router" : "vite");
256
+ return { name, variant };
257
+ }
258
+
259
+ return { name: "react", variant: "vite" };
260
+ }
261
+
262
+ /**
263
+ * Determine whether a prompt should be presented to the user based on framework capabilities.
264
+ * @param {string | object} framework
265
+ * @param {string} promptName
266
+ * @returns {boolean}
267
+ */
268
+ export function isPromptVisible(framework, promptName) {
269
+ const fw = normalizeFramework(framework);
270
+ const desc = getFrameworkDescriptor(fw);
271
+
272
+ switch (promptName) {
273
+ case "router":
274
+ return (
275
+ hasCapability(fw, CAPABILITIES.CLIENT_ROUTING) &&
276
+ Boolean(desc?.supportsRouterPrompt)
277
+ );
278
+ case "bundler":
279
+ return hasCapability(fw, CAPABILITIES.BUNDLER_SELECTION);
280
+ case "adapter":
281
+ return hasCapability(fw, CAPABILITIES.ADAPTERS);
282
+ case "reactCompiler":
283
+ return hasCapability(fw, CAPABILITIES.REACT_COMPILER);
284
+ default:
285
+ return true;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Filter an array of choice objects or values based on framework compatibility.
291
+ * @param {string | object} framework
292
+ * @param {string} optionKey
293
+ * @param {Array<object | string>} choices
294
+ * @returns {Array<object | string>}
295
+ */
296
+ export function filterCompatibleChoices(framework, optionKey, choices = []) {
297
+ const fw = normalizeFramework(framework);
298
+ if (!Array.isArray(choices)) return [];
299
+ return choices.filter((choice) =>
300
+ isOptionCompatible(fw, optionKey, choice?.value ?? choice)
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Get canonical default prompt responses for a given framework.
306
+ * @param {string | object} framework
307
+ * @param {{ projectName?: string }} [options]
308
+ * @returns {object}
309
+ */
310
+ export function getDefaultResponses(framework, { projectName = "my-app" } = {}) {
311
+ const fw = normalizeFramework(framework);
312
+
313
+ if (fw.name === "next") {
314
+ return {
315
+ projectName,
316
+ frameworkName: fw.name,
317
+ frameworkVariant: fw.variant,
318
+ architecture: "feature-based",
319
+ language: "ts",
320
+ cssFramework: "tailwind",
321
+ testing: "vitest",
322
+ router: false,
323
+ bundler: "turbopack",
324
+ adapter: "none",
325
+ stateManagement: "none",
326
+ iconLibrary: "none",
327
+ apiClient: "none",
328
+ linter: "eslint",
329
+ formatter: "prettier",
330
+ docsLanguage: "en",
331
+ gitInit: true,
332
+ readme: true,
333
+ };
334
+ }
335
+
336
+ return {
337
+ projectName,
338
+ frameworkName: "react",
339
+ frameworkVariant: "vite",
340
+ architecture: "feature-based",
341
+ language: "ts",
342
+ cssFramework: "tailwind",
343
+ testing: "vitest",
344
+ router: true,
345
+ stateManagement: "none",
346
+ iconLibrary: "none",
347
+ apiClient: "none",
348
+ linter: "eslint",
349
+ formatter: "prettier",
350
+ docsLanguage: "en",
351
+ gitInit: true,
352
+ readme: true,
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Sanitize prompt responses for a framework by adjusting or resetting incompatible options.
358
+ * @param {string | object} framework
359
+ * @param {object} [responses]
360
+ * @returns {object}
361
+ */
362
+ export function sanitizeResponsesForFramework(framework, responses = {}) {
363
+ const fw = normalizeFramework(framework);
364
+ const sanitized = { ...(responses || {}) };
365
+ const defaults = getDefaultResponses(fw, {
366
+ projectName: sanitized.projectName || "my-app",
367
+ });
368
+
369
+ if (!isPromptVisible(fw, "router")) {
370
+ sanitized.router = false;
371
+ }
372
+
373
+ for (const [key, val] of Object.entries(sanitized)) {
374
+ if (val === undefined || val === null) continue;
375
+ if (!isOptionCompatible(fw, key, val)) {
376
+ if (key in defaults) {
377
+ sanitized[key] = defaults[key];
378
+ } else if (key === "styling" && "cssFramework" in defaults) {
379
+ sanitized[key] = defaults.cssFramework;
380
+ } else {
381
+ sanitized[key] = defaults[key];
382
+ }
383
+ }
384
+ }
385
+
386
+ return sanitized;
387
+ }
388
+