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.
- package/README.md +15 -7
- package/bin/cli.js +1 -0
- package/package.json +2 -2
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
- package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
- package/schema/lumen.config.v2.json +0 -1
- package/src/cli-args.js +65 -3
- package/src/configure.js +4 -0
- package/src/css.js +14 -30
- package/src/dependencies.js +28 -18
- package/src/engine/capabilities.js +174 -4
- package/src/engine/composition.js +644 -0
- package/src/engine/index.js +1 -0
- package/src/injector.js +294 -446
- package/src/main.js +6 -7
- package/src/manifest/schema.js +1 -1
- package/src/prompts.js +459 -247
- package/src/readme.js +23 -2
- package/src/scaffold.js +17 -0
- package/templates/architectures/hybrid/main.jsx +10 -0
- package/templates/architectures/hybrid/main.tsx +10 -0
- package/templates/architectures/hybrid/src/App.jsx +20 -0
- package/templates/architectures/hybrid/src/App.tsx +20 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
- package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
- package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
- package/templates/architectures/none/main.jsx +10 -0
- package/templates/architectures/none/main.tsx +10 -0
- package/templates/architectures/none/src/App.jsx +15 -0
- package/templates/architectures/none/src/App.tsx +15 -0
- package/templates/architectures/none/src/styles/.gitkeep +1 -0
- package/templates/architectures/none/src/types/index.js +20 -0
- package/templates/architectures/none/src/types/index.ts +16 -0
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
- package/templates/css/tailwind/src/globals.css +10 -0
- package/templates/css/tailwind/src/themes.css +86 -4
- package/templates/ui/shadcn/lib/utils.js +6 -0
- package/templates/ui/shadcn/lib/utils.ts +6 -0
- package/templates/ui/shadcn/primitives/Button.jsx +48 -0
- package/templates/ui/shadcn/primitives/Button.tsx +59 -0
- package/templates/ui/shadcn/primitives/Card.jsx +55 -0
- package/templates/ui/shadcn/primitives/Card.tsx +73 -0
- package/templates/ui/shadcn/primitives/index.js +9 -0
- package/templates/ui/shadcn/primitives/index.ts +9 -0
- package/templates/css/bootstrap/src/themes.css +0 -14
- /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
|
|
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
|
|
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)
|
|
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
|
|
152
|
-
│ └── themes.css # Theme tokens (Tailwind @theme /
|
|
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
|
|
181
|
-
│ └── themes.css # Theme tokens (Tailwind @theme /
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lumen",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
4
|
-
"description": "Scaffold a React + Vite project with architecture choice: feature-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"
|
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
|
|
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:
|
|
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:
|
|
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
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
|
|
27
|
-
// globals.css (
|
|
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 (
|
|
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
|
|
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}'`);
|
package/src/dependencies.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
34
|
-
await installDeps(pkg, true,
|
|
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
|
|
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
|
-
|
|
51
|
+
cssDevDeps.push("tailwindcss", "@tailwindcss/postcss", "postcss");
|
|
49
52
|
} else {
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
linterDevDeps.push("eslint", "eslint-config-next");
|
|
107
108
|
} else {
|
|
108
|
-
|
|
109
|
+
linterDevDeps.push("eslint", "@eslint/js", "eslint-plugin-react-hooks", "eslint-plugin-react-refresh", "globals");
|
|
109
110
|
if (responses.language === "ts") {
|
|
110
|
-
|
|
111
|
+
linterDevDeps.push("typescript-eslint", "jiti");
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
} else if (responses.linter === "oxlint") {
|
|
114
|
-
|
|
115
|
+
linterDevDeps.push("oxlint");
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
// Formatter
|
|
118
119
|
if (responses.formatter === "prettier") {
|
|
119
|
-
|
|
120
|
+
formatterDevDeps.push("prettier");
|
|
120
121
|
if (responses.linter === "eslint") {
|
|
121
|
-
|
|
122
|
+
formatterDevDeps.push("eslint-config-prettier");
|
|
122
123
|
}
|
|
123
124
|
} else if (responses.formatter === "oxfmt") {
|
|
124
|
-
|
|
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
|
-
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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
|
+
|