create-lumen 2.0.0-alpha.0 → 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 +35 -12
- package/bin/cli.js +1 -0
- package/package.json +3 -2
- package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
- package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
- package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
- package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
- package/schema/fixtures/v2/valid/harness-full.json +44 -0
- package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
- package/schema/fixtures/v2/valid/validation-none.json +41 -0
- package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
- package/schema/lumen.config.v2.json +54 -2
- package/skills/create-lumen/SKILL.md +110 -0
- package/skills/create-lumen/references/manifest-v2.md +9 -0
- package/src/agent-context.js +114 -0
- 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 +20 -7
- package/src/manifest/emit.js +49 -3
- package/src/manifest/schema.js +65 -7
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildManifest } from "./manifest/emit.js";
|
|
4
|
+
|
|
5
|
+
const VALIDATION_NOTES = {
|
|
6
|
+
strict: "strict: architecture boundaries are enforced — do not import across features except via barrels or shared paths; violations must be fixed.",
|
|
7
|
+
relaxed: "relaxed: architecture boundaries are advisory — prefer barrels and shared paths, but cross-imports only warn.",
|
|
8
|
+
none: "none: no architecture validation — paths are still the canonical locations.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function resolveManifest(manifestOrResponses) {
|
|
12
|
+
if (manifestOrResponses && typeof manifestOrResponses.manifestVersion !== "undefined") {
|
|
13
|
+
return manifestOrResponses;
|
|
14
|
+
}
|
|
15
|
+
if (manifestOrResponses && manifestOrResponses._manifestSource) {
|
|
16
|
+
return manifestOrResponses._manifestSource;
|
|
17
|
+
}
|
|
18
|
+
return buildManifest(manifestOrResponses);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render the short agent conventions markdown from a manifest.
|
|
23
|
+
* Deterministic: derived only from stable manifest fields.
|
|
24
|
+
* @param {object} manifest - validated manifest v2
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function renderConventions(manifest) {
|
|
28
|
+
const preset = manifest.architecture?.preset ?? manifest.architecture?.type ?? "feature-based";
|
|
29
|
+
const validation = manifest.architecture?.validation ?? "strict";
|
|
30
|
+
const paths = manifest.paths ?? {};
|
|
31
|
+
const harness = manifest.harness ?? { commands: [] };
|
|
32
|
+
const barrelFile = manifest.tooling?.language === "js" ? "index.js" : "index.ts";
|
|
33
|
+
|
|
34
|
+
const pathLines = Object.entries(paths)
|
|
35
|
+
.map(([key, value]) => `- ${key}: \`${value}\``)
|
|
36
|
+
.join("\n");
|
|
37
|
+
|
|
38
|
+
const harnessLines = (harness.commands ?? [])
|
|
39
|
+
.map((c) => `- \`${c.name}\`: \`${c.command}\` (${c.required === false ? "optional" : "required"})`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
|
|
42
|
+
const harnessSummary = (harness.commands ?? []).map((c) => `\`${c.command}\``).join(", ");
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
`# Lumen Agent Conventions\n` +
|
|
46
|
+
`\n` +
|
|
47
|
+
`Generated from \`lumen.config.json\`. Resolve every file location via \`paths.*\` in \`.lumen/architecture.json\` (preset: \`${preset}\`, validation: \`${validation}\`).\n` +
|
|
48
|
+
`\n` +
|
|
49
|
+
`## Rules\n` +
|
|
50
|
+
`\n` +
|
|
51
|
+
`- Resolve all paths via \`paths.*\` — never hardcode alternative \`src/...\` locations.\n` +
|
|
52
|
+
`- Feature barrel: every \`src/features/<feature>/\` must re-export its public API via \`${barrelFile}\`.\n` +
|
|
53
|
+
`- Shared code: import shared utilities via \`@/shared\` (plus the \`@/...\` aliases from tsconfig/jsconfig).\n` +
|
|
54
|
+
`- Harness: before finishing, run each command in \`.lumen/project.json\` \`harness.commands\` in order (${harnessSummary}).\n` +
|
|
55
|
+
`- Validation mode \`${validation}\`: ${VALIDATION_NOTES[validation] ?? VALIDATION_NOTES.strict}\n` +
|
|
56
|
+
`\n` +
|
|
57
|
+
`## Paths\n` +
|
|
58
|
+
`\n` +
|
|
59
|
+
`${pathLines}\n` +
|
|
60
|
+
`\n` +
|
|
61
|
+
`## Harness\n` +
|
|
62
|
+
`\n` +
|
|
63
|
+
`${harnessLines}\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the deterministic agent-context payload from a validated manifest v2.
|
|
69
|
+
* No timestamps — output must be byte-stable.
|
|
70
|
+
* @param {object} manifest - validated manifest v2
|
|
71
|
+
* @returns {{ project: object, architecture: object, conventionsMd: string }}
|
|
72
|
+
*/
|
|
73
|
+
export function buildAgentContext(manifest) {
|
|
74
|
+
const project = {
|
|
75
|
+
manifestVersion: manifest.manifestVersion,
|
|
76
|
+
framework: manifest.framework,
|
|
77
|
+
styling: manifest.styling,
|
|
78
|
+
tooling: manifest.tooling,
|
|
79
|
+
harness: manifest.harness,
|
|
80
|
+
docs: manifest.docs,
|
|
81
|
+
};
|
|
82
|
+
const architecture = {
|
|
83
|
+
preset: manifest.architecture?.preset ?? manifest.architecture?.type,
|
|
84
|
+
validation: manifest.architecture?.validation ?? "strict",
|
|
85
|
+
paths: manifest.paths,
|
|
86
|
+
};
|
|
87
|
+
const conventionsMd = renderConventions(manifest);
|
|
88
|
+
return { project, architecture, conventionsMd };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeJson(filePath, value) {
|
|
92
|
+
return fsp.writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Emit the .lumen agent context dir into a scaffolded project.
|
|
97
|
+
* Accepts either a validated manifest v2 or scaffolder responses
|
|
98
|
+
* (uses responses._manifestSource when present, else buildManifest).
|
|
99
|
+
* Writes .lumen/project.json, .lumen/architecture.json, .lumen/conventions.md
|
|
100
|
+
* with 2-space indent + trailing newline. Byte-deterministic (no timestamps).
|
|
101
|
+
* @param {string} projectPath - absolute path to scaffolded project
|
|
102
|
+
* @param {object} manifestOrResponses
|
|
103
|
+
* @returns {Promise<string>} absolute path to the .lumen dir
|
|
104
|
+
*/
|
|
105
|
+
export async function emitAgentContext(projectPath, manifestOrResponses) {
|
|
106
|
+
const manifest = resolveManifest(manifestOrResponses);
|
|
107
|
+
const { project, architecture, conventionsMd } = buildAgentContext(manifest);
|
|
108
|
+
const dir = path.join(projectPath, ".lumen");
|
|
109
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
110
|
+
await writeJson(path.join(dir, "project.json"), project);
|
|
111
|
+
await writeJson(path.join(dir, "architecture.json"), architecture);
|
|
112
|
+
await fsp.writeFile(path.join(dir, "conventions.md"), conventionsMd, "utf8");
|
|
113
|
+
return dir;
|
|
114
|
+
}
|
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
|
+
|