create-lumen 2.0.0-beta.2 → 2.0.0-beta.4
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 +23 -22
- package/bin/cli.js +1 -6
- package/package.json +2 -2
- package/schema/fixtures/v2/valid/next-app-router-feature-based.json +1 -1
- package/schema/fixtures/v2/valid/next-app-router-none.json +1 -1
- package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +1 -1
- package/schema/fixtures/v2/valid/next-pages-router-none.json +1 -1
- package/schema/lumen.config.v2.json +2 -3
- package/src/cleanup.js +21 -5
- package/src/cli-args.js +53 -2
- package/src/configure.js +36 -8
- package/src/css.js +17 -7
- package/src/dependencies.js +9 -2
- package/src/engine/capabilities.js +2 -2
- package/src/engine/composition.js +48 -10
- package/src/env.js +149 -12
- package/src/format.js +14 -6
- package/src/injector.js +66 -14
- package/src/main.js +57 -12
- package/src/manifest/emit.js +23 -7
- package/src/manifest/paths.js +1 -25
- package/src/manifest/schema.js +3 -3
- package/src/prompts.js +25 -8
- package/src/readme.js +0 -16
- package/src/utils/dev-server.js +40 -8
- package/templates/.env.example +1 -1
- package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
- package/templates/conditional/linting/biome/biome.json +17 -11
- package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
- package/templates/conditional/testing/vitest/next/vitest.config.js +1 -1
- package/templates/conditional/testing/vitest/next/vitest.config.ts +1 -1
- package/templates/css/tailwind/postcss.config.mjs +3 -1
- package/schema/fixtures/v2/valid/next-app-router-hybrid.json +0 -35
- package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +0 -35
- package/templates/architectures/hybrid/main.jsx +0 -10
- package/templates/architectures/hybrid/main.tsx +0 -10
- package/templates/architectures/hybrid/src/App.jsx +0 -20
- package/templates/architectures/hybrid/src/App.tsx +0 -20
- package/templates/architectures/hybrid/src/features/.gitkeep +0 -1
- package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +0 -31
- package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +0 -36
- package/templates/architectures/hybrid/src/shared/components/ui/index.js +0 -1
- package/templates/architectures/hybrid/src/shared/components/ui/index.ts +0 -1
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +0 -28
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +0 -28
- 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 +0 -20
- package/templates/architectures/hybrid/src/shared/types/index.ts +0 -16
- package/templates/architectures/hybrid/src/shared/utils/cn.jsx +0 -3
- package/templates/architectures/hybrid/src/shared/utils/cn.ts +0 -5
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +0 -11
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +0 -11
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +0 -10
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +0 -10
package/src/env.js
CHANGED
|
@@ -9,13 +9,38 @@ const ENV_GITIGNORE = `
|
|
|
9
9
|
!.env.example
|
|
10
10
|
`;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export async function resolveFramework(projectPath, responses = {}) {
|
|
13
|
+
if (responses.framework || responses.frameworkName) {
|
|
14
|
+
return normalizeFramework(
|
|
15
|
+
responses.framework || {
|
|
16
|
+
name: responses.frameworkName,
|
|
17
|
+
variant: responses.frameworkVariant,
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
// Try reading lumen.config.json if already written
|
|
23
|
+
try {
|
|
24
|
+
const manifestPath = path.join(projectPath, "lumen.config.json");
|
|
25
|
+
const content = await fsp.readFile(manifestPath, "utf8");
|
|
26
|
+
const parsed = JSON.parse(content);
|
|
27
|
+
if (parsed.framework) return normalizeFramework(parsed.framework);
|
|
28
|
+
} catch {}
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
// Try checking package.json
|
|
31
|
+
try {
|
|
32
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
33
|
+
const content = await fsp.readFile(pkgPath, "utf8");
|
|
34
|
+
const pkg = JSON.parse(content);
|
|
35
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) {
|
|
36
|
+
return { name: "next", variant: "app-router" };
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
40
|
+
return { name: "react", variant: "vite" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildEnvExample(responses = {}, existingContent = "") {
|
|
19
44
|
const fw = normalizeFramework(
|
|
20
45
|
responses.framework || {
|
|
21
46
|
name: responses.frameworkName,
|
|
@@ -23,16 +48,128 @@ export async function copyEnvExample(projectPath, templatesDir, responses = {})
|
|
|
23
48
|
}
|
|
24
49
|
);
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
const isNext = fw.name === "next";
|
|
52
|
+
const lines = [];
|
|
53
|
+
|
|
54
|
+
if (isNext) {
|
|
55
|
+
const routerDocs =
|
|
56
|
+
fw.variant === "pages-router"
|
|
57
|
+
? "https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables"
|
|
58
|
+
: "https://nextjs.org/docs/app/building-your-application/configuring/environment-variables";
|
|
59
|
+
|
|
60
|
+
lines.push("# Copy this file to `.env.local` (or `.env`) and fill in your values.");
|
|
61
|
+
lines.push(`# See ${routerDocs} for details.`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
|
|
64
|
+
// Public API client URL
|
|
65
|
+
lines.push("# Public API URL (accessible in browser and server via process.env.NEXT_PUBLIC_API_URL)");
|
|
66
|
+
lines.push("NEXT_PUBLIC_API_URL=https://api.example.com");
|
|
67
|
+
|
|
68
|
+
// Database (Prisma)
|
|
69
|
+
if (responses.database === "prisma" || existingContent.includes("DATABASE_URL")) {
|
|
70
|
+
const isSupabase =
|
|
71
|
+
responses.databaseProvider === "supabase" ||
|
|
72
|
+
responses.dbProvider === "supabase" ||
|
|
73
|
+
(!responses.databaseProvider && existingContent.includes("pooler.supabase.com"));
|
|
74
|
+
|
|
75
|
+
lines.push("");
|
|
76
|
+
if (isSupabase) {
|
|
77
|
+
lines.push("# Database (Prisma + Supabase PostgreSQL)");
|
|
78
|
+
lines.push("# Transaction connection pooler (port 6543) with PgBouncer - used by Next.js Server Components / Actions:");
|
|
79
|
+
lines.push('DATABASE_URL="postgresql://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true"');
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("# Direct connection (port 5432) - used by Prisma CLI for migrations:");
|
|
82
|
+
lines.push('DIRECT_URL="postgresql://postgres.[YOUR-PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres"');
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("# Local development fallback (local PostgreSQL / Docker compose / Supabase local CLI):");
|
|
85
|
+
lines.push('# DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
|
|
86
|
+
lines.push('# DIRECT_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("# Supabase API credentials (optional, for Supabase Auth / Storage)");
|
|
89
|
+
lines.push('# NEXT_PUBLIC_SUPABASE_URL="https://[YOUR-PROJECT-REF].supabase.co"');
|
|
90
|
+
lines.push('# NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOi..."');
|
|
91
|
+
} else {
|
|
92
|
+
lines.push("# Database (Prisma + PostgreSQL)");
|
|
93
|
+
lines.push("# Local development with PostgreSQL or Docker compose (docker compose up -d db):");
|
|
94
|
+
lines.push('DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
|
|
95
|
+
lines.push('DIRECT_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"');
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("# Supabase / cloud PostgreSQL with connection pooling (Transaction mode / port 6543):");
|
|
98
|
+
lines.push('# DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"');
|
|
99
|
+
lines.push('# DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Docker
|
|
104
|
+
if (responses.docker === true || existingContent.includes("PORT=")) {
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push("# Server / Container Port (optional, defaults to 3000)");
|
|
107
|
+
lines.push("# PORT=3000");
|
|
108
|
+
}
|
|
28
109
|
} else {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
110
|
+
// React / Vite
|
|
111
|
+
lines.push("# Copy this file to `.env.local` (or `.env`) and fill in your values.");
|
|
112
|
+
lines.push("# See https://vite.dev/guide/env-and-mode for how env vars are exposed.");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push("# Application API URL (accessible in Vite via import.meta.env.VITE_API_URL)");
|
|
115
|
+
lines.push("VITE_API_URL=https://api.example.com");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push("");
|
|
119
|
+
let result = lines.join("\n");
|
|
120
|
+
|
|
121
|
+
// If there was existing custom content, preserve custom non-framework variables
|
|
122
|
+
if (existingContent && existingContent.trim()) {
|
|
123
|
+
const existingLines = existingContent.split("\n");
|
|
124
|
+
const preservedLines = [];
|
|
125
|
+
for (const line of existingLines) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
128
|
+
|
|
129
|
+
// Purge Vite references from Next.js scaffolds
|
|
130
|
+
if (
|
|
131
|
+
isNext &&
|
|
132
|
+
(trimmed.startsWith("VITE_") ||
|
|
133
|
+
trimmed.includes("vitejs.dev") ||
|
|
134
|
+
trimmed.includes("vite.dev"))
|
|
135
|
+
) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Purge Next.js references from React scaffolds
|
|
139
|
+
if (!isNext && trimmed.startsWith("NEXT_PUBLIC_")) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check variable name
|
|
144
|
+
const eqIdx = trimmed.indexOf("=");
|
|
145
|
+
if (eqIdx !== -1) {
|
|
146
|
+
const varName = trimmed.slice(0, eqIdx).trim();
|
|
147
|
+
if (!result.includes(varName + "=")) {
|
|
148
|
+
preservedLines.push(line);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (preservedLines.length > 0) {
|
|
153
|
+
result = result.trimEnd() + "\n\n# Custom environment variables\n" + preservedLines.join("\n") + "\n";
|
|
154
|
+
}
|
|
34
155
|
}
|
|
35
156
|
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function copyEnvExample(projectPath, templatesDir, responses = {}) {
|
|
161
|
+
const fw = await resolveFramework(projectPath, responses);
|
|
162
|
+
const resolvedResponses = { ...responses, framework: fw };
|
|
163
|
+
|
|
164
|
+
const envPath = path.join(projectPath, ".env.example");
|
|
165
|
+
let existingContent = "";
|
|
166
|
+
try {
|
|
167
|
+
existingContent = await fsp.readFile(envPath, "utf8");
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
const envContent = buildEnvExample(resolvedResponses, existingContent);
|
|
171
|
+
await fsp.writeFile(envPath, envContent, "utf8");
|
|
172
|
+
|
|
36
173
|
// Ensure the project gitignore excludes env files but keeps .env.example.
|
|
37
174
|
const gitignore = path.join(projectPath, ".gitignore");
|
|
38
175
|
try {
|
package/src/format.js
CHANGED
|
@@ -17,12 +17,20 @@ async function exists(p) {
|
|
|
17
17
|
// is already on PATH (e.g. verification harnesses resolving from a vendored
|
|
18
18
|
// node_modules symlink).
|
|
19
19
|
export async function runProjectFormat(projectPath, responses) {
|
|
20
|
-
if (
|
|
20
|
+
if (responses.formatter && responses.formatter !== "none") {
|
|
21
|
+
const formatter = responses.formatter;
|
|
22
|
+
const bin = path.join(projectPath, "node_modules", ".bin", formatter);
|
|
23
|
+
const command = (await exists(bin)) ? bin : formatter;
|
|
24
|
+
const args = formatter === "prettier" ? ["--write", "."] : ["."];
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const args = formatter === "prettier" ? ["--write", "."] : ["."];
|
|
26
|
+
await execa(command, args, { cwd: projectPath, stdio: "pipe" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
// When Biome is the linter and no separate formatter is chosen, use Biome's native check --write
|
|
31
|
+
if (responses.linter === "biome") {
|
|
32
|
+
const bin = path.join(projectPath, "node_modules", ".bin", "biome");
|
|
33
|
+
const command = (await exists(bin)) ? bin : "biome";
|
|
34
|
+
await execa(command, ["check", "--write", "."], { cwd: projectPath, stdio: "pipe" });
|
|
35
|
+
}
|
|
28
36
|
}
|
package/src/injector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fsp } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { normalizeFramework } from "./engine/capabilities.js";
|
|
3
4
|
import {
|
|
4
5
|
FRAGMENT_SCOPES,
|
|
5
6
|
resolveFragments,
|
|
@@ -31,9 +32,25 @@ export async function injectArchitecture(
|
|
|
31
32
|
templatesDir,
|
|
32
33
|
architecture,
|
|
33
34
|
language,
|
|
34
|
-
cssFramework
|
|
35
|
+
cssFramework,
|
|
36
|
+
frameworkOptions = {}
|
|
35
37
|
) {
|
|
36
|
-
const
|
|
38
|
+
const fwObj =
|
|
39
|
+
typeof frameworkOptions === "object" && frameworkOptions !== null
|
|
40
|
+
? frameworkOptions
|
|
41
|
+
: typeof frameworkOptions === "string"
|
|
42
|
+
? { name: frameworkOptions }
|
|
43
|
+
: {};
|
|
44
|
+
const framework = normalizeFramework(fwObj);
|
|
45
|
+
const responses = {
|
|
46
|
+
architecture,
|
|
47
|
+
language,
|
|
48
|
+
cssFramework,
|
|
49
|
+
framework,
|
|
50
|
+
frameworkName: framework.name,
|
|
51
|
+
frameworkVariant: framework.variant,
|
|
52
|
+
...fwObj,
|
|
53
|
+
};
|
|
37
54
|
const allFragments = resolveFragments(responses, templatesDir);
|
|
38
55
|
const archFragments = allFragments.filter(
|
|
39
56
|
(f) =>
|
|
@@ -150,35 +167,59 @@ export function getComponentsJson({
|
|
|
150
167
|
architecture = "feature-based",
|
|
151
168
|
language = "ts",
|
|
152
169
|
}) {
|
|
153
|
-
const
|
|
170
|
+
const fwName =
|
|
171
|
+
typeof framework === "object" && framework !== null
|
|
172
|
+
? framework.name
|
|
173
|
+
: String(framework);
|
|
174
|
+
const fwVariant =
|
|
175
|
+
typeof framework === "object" && framework !== null
|
|
176
|
+
? framework.variant
|
|
177
|
+
: "app-router";
|
|
178
|
+
const isNext = fwName === "next";
|
|
154
179
|
const isTs = language === "ts";
|
|
180
|
+
const isFeatureBased = architecture === "feature-based";
|
|
155
181
|
|
|
156
182
|
if (isNext) {
|
|
183
|
+
const cssPath =
|
|
184
|
+
fwVariant === "pages-router"
|
|
185
|
+
? "src/styles/globals.css"
|
|
186
|
+
: "src/app/globals.css";
|
|
187
|
+
|
|
157
188
|
// Next App Router contract stub
|
|
158
189
|
return {
|
|
159
190
|
$schema: "https://ui.shadcn.com/schema.json",
|
|
160
191
|
style: "new-york",
|
|
161
|
-
rsc:
|
|
192
|
+
rsc: fwVariant !== "pages-router",
|
|
162
193
|
tsx: isTs,
|
|
163
194
|
tailwind: {
|
|
164
195
|
config: "",
|
|
165
|
-
css:
|
|
196
|
+
css: cssPath,
|
|
166
197
|
baseColor: "neutral",
|
|
167
198
|
cssVariables: true,
|
|
168
199
|
prefix: "",
|
|
169
200
|
},
|
|
170
|
-
aliases:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
201
|
+
aliases: isFeatureBased
|
|
202
|
+
? {
|
|
203
|
+
components: "@/shared/components",
|
|
204
|
+
utils: "@/lib/utils",
|
|
205
|
+
ui: "@/shared/components/ui",
|
|
206
|
+
lib: "@/lib",
|
|
207
|
+
hooks: "@/shared/hooks",
|
|
208
|
+
}
|
|
209
|
+
: {
|
|
210
|
+
components: "@/components",
|
|
211
|
+
utils: "@/lib/utils",
|
|
212
|
+
ui: "@/components/ui",
|
|
213
|
+
lib: "@/lib",
|
|
214
|
+
hooks: "@/hooks",
|
|
215
|
+
},
|
|
177
216
|
iconLibrary: "lucide",
|
|
217
|
+
registries: {
|
|
218
|
+
"@lumen": "https://raw.githubusercontent.com/strophix-software/lumen-registry/main/public/r",
|
|
219
|
+
},
|
|
178
220
|
};
|
|
179
221
|
}
|
|
180
222
|
|
|
181
|
-
const isFeatureBased = architecture === "feature-based";
|
|
182
223
|
return {
|
|
183
224
|
$schema: "https://ui.shadcn.com/schema.json",
|
|
184
225
|
style: "new-york",
|
|
@@ -209,6 +250,9 @@ export function getComponentsJson({
|
|
|
209
250
|
hooks: "@/hooks",
|
|
210
251
|
},
|
|
211
252
|
iconLibrary: "lucide",
|
|
253
|
+
registries: {
|
|
254
|
+
"@lumen": "https://raw.githubusercontent.com/strophix-software/lumen-registry/main/public/r",
|
|
255
|
+
},
|
|
212
256
|
};
|
|
213
257
|
}
|
|
214
258
|
|
|
@@ -232,6 +276,7 @@ export async function injectUiKit(
|
|
|
232
276
|
const codeExt = isTs ? "tsx" : "jsx";
|
|
233
277
|
const utilExt = isTs ? "ts" : "js";
|
|
234
278
|
const framework = responses.framework || responses.frameworkName || "vite";
|
|
279
|
+
const isNext = typeof framework === "object" ? framework.name === "next" : framework === "next";
|
|
235
280
|
|
|
236
281
|
// 1. Write components.json
|
|
237
282
|
const componentsJson = getComponentsJson({
|
|
@@ -313,7 +358,14 @@ export async function injectUiKit(
|
|
|
313
358
|
|
|
314
359
|
try {
|
|
315
360
|
await fsp.access(buttonTemplate);
|
|
316
|
-
await fsp.
|
|
361
|
+
let buttonContent = await fsp.readFile(buttonTemplate, "utf8");
|
|
362
|
+
if (isNext) {
|
|
363
|
+
buttonContent = buttonContent.replace(
|
|
364
|
+
/\/\/ eslint-disable-next-line react-refresh\/only-export-components\r?\n/,
|
|
365
|
+
""
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
await fsp.writeFile(buttonDest, buttonContent, "utf8");
|
|
317
369
|
} catch {}
|
|
318
370
|
|
|
319
371
|
try {
|
package/src/main.js
CHANGED
|
@@ -26,9 +26,9 @@ import { normalizeFramework } from "@/engine/capabilities.js";
|
|
|
26
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
27
|
const __dirname = path.dirname(__filename);
|
|
28
28
|
const TEMPLATES_DIR = path.join(__dirname, "../templates");
|
|
29
|
-
const CURRENT_DIR = process.cwd();
|
|
30
29
|
|
|
31
|
-
export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd
|
|
30
|
+
export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd, rawArgs } = {}) {
|
|
31
|
+
const currentDir = cwd || process.cwd();
|
|
32
32
|
const cli = parseCliArgs(rawArgs || process.argv.slice(2));
|
|
33
33
|
const cliArg = nameArg || cli.projectName || null;
|
|
34
34
|
|
|
@@ -37,7 +37,7 @@ export function resolveProjectName({ quickSetup = false, projectName: nameArg, c
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (quickSetup || cli.quickSetup || cli.manifest || cli.template) {
|
|
40
|
-
const defaultName = path.basename(
|
|
40
|
+
const defaultName = path.basename(currentDir).trim() || "my-app";
|
|
41
41
|
return defaultName === "." ? "my-app" : defaultName;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -52,16 +52,31 @@ export async function main(options = {}) {
|
|
|
52
52
|
template: templateName,
|
|
53
53
|
framework: frameworkArg,
|
|
54
54
|
arch: archArg,
|
|
55
|
+
language: languageArg,
|
|
56
|
+
cssFramework: cssArg,
|
|
57
|
+
uiKit: uiKitArg,
|
|
58
|
+
database: databaseArg,
|
|
59
|
+
databaseProvider: dbProviderArg,
|
|
60
|
+
docker: dockerArg,
|
|
61
|
+
testing: testingArg,
|
|
62
|
+
linter: linterArg,
|
|
63
|
+
formatter: formatterArg,
|
|
64
|
+
routeGroups: routeGroupsArg,
|
|
65
|
+
bundler: bundlerArg,
|
|
66
|
+
adapter: adapterArg,
|
|
67
|
+
reactCompiler: reactCompilerArg,
|
|
68
|
+
agentDocs: agentDocsArg,
|
|
55
69
|
} = options;
|
|
56
70
|
const pkg = getPkgManager();
|
|
57
71
|
|
|
58
72
|
const isNonInteractive = Boolean(quickSetup || manifestSource || templateName);
|
|
73
|
+
const currentDir = options.cwd || process.cwd();
|
|
59
74
|
|
|
60
75
|
// 1. Project name
|
|
61
76
|
let projectName = resolveProjectName({
|
|
62
77
|
quickSetup: isNonInteractive,
|
|
63
78
|
projectName: nameArg,
|
|
64
|
-
cwd:
|
|
79
|
+
cwd: currentDir,
|
|
65
80
|
});
|
|
66
81
|
|
|
67
82
|
if (projectName) {
|
|
@@ -83,7 +98,7 @@ export async function main(options = {}) {
|
|
|
83
98
|
// 3. Collect user preferences (interactive or non-interactive)
|
|
84
99
|
let responses;
|
|
85
100
|
if (manifestSource) {
|
|
86
|
-
const manifestObj = loadManifestSource(manifestSource,
|
|
101
|
+
const manifestObj = loadManifestSource(manifestSource, currentDir);
|
|
87
102
|
responses = manifestToResponses(manifestObj, projectName);
|
|
88
103
|
} else if (templateName) {
|
|
89
104
|
const preset = PRESETS[templateName];
|
|
@@ -100,7 +115,23 @@ export async function main(options = {}) {
|
|
|
100
115
|
responses = await getUserInputs(projectName, { quickSetup, initialArch: archArg, framework: frameworkArg });
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
|
|
118
|
+
// Overlay explicit CLI flags if provided
|
|
119
|
+
if (languageArg) responses.language = languageArg;
|
|
120
|
+
if (cssArg) responses.cssFramework = cssArg;
|
|
121
|
+
if (uiKitArg) responses.uiKit = uiKitArg;
|
|
122
|
+
if (linterArg) responses.linter = linterArg;
|
|
123
|
+
if (formatterArg) responses.formatter = formatterArg;
|
|
124
|
+
if (databaseArg) responses.database = databaseArg;
|
|
125
|
+
if (dbProviderArg) responses.databaseProvider = dbProviderArg;
|
|
126
|
+
if (testingArg) responses.testing = testingArg;
|
|
127
|
+
if (dockerArg) responses.docker = true;
|
|
128
|
+
if (routeGroupsArg) responses.routeGroups = true;
|
|
129
|
+
if (bundlerArg) responses.bundler = bundlerArg;
|
|
130
|
+
if (adapterArg) responses.adapter = adapterArg;
|
|
131
|
+
if (reactCompilerArg !== null && reactCompilerArg !== undefined) responses.reactCompiler = reactCompilerArg;
|
|
132
|
+
if (agentDocsArg !== null && agentDocsArg !== undefined) responses.agentDocs = agentDocsArg;
|
|
133
|
+
|
|
134
|
+
const projectPath = path.resolve(currentDir, projectName);
|
|
104
135
|
|
|
105
136
|
// 4. Scaffold the base project (provider-owned bases; Vite shells out, Next copies)
|
|
106
137
|
const fw = normalizeFramework(
|
|
@@ -168,7 +199,8 @@ export async function main(options = {}) {
|
|
|
168
199
|
TEMPLATES_DIR,
|
|
169
200
|
responses.architecture,
|
|
170
201
|
responses.language,
|
|
171
|
-
responses.cssFramework
|
|
202
|
+
responses.cssFramework,
|
|
203
|
+
responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant }
|
|
172
204
|
);
|
|
173
205
|
archSpin.stop(chalk.green("Architecture scaffolding complete."));
|
|
174
206
|
} catch (err) {
|
|
@@ -233,6 +265,7 @@ export async function main(options = {}) {
|
|
|
233
265
|
architecture: responses.architecture,
|
|
234
266
|
ext: responses.language === "ts" ? "tsx" : "jsx",
|
|
235
267
|
pkg,
|
|
268
|
+
framework: responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant },
|
|
236
269
|
});
|
|
237
270
|
if (responses.cssFramework !== "none") {
|
|
238
271
|
cssSpin.stop(chalk.green(`${cssLabel} setup complete.`));
|
|
@@ -246,7 +279,12 @@ export async function main(options = {}) {
|
|
|
246
279
|
const configSpin = spinner();
|
|
247
280
|
configSpin.start("Configuring project...");
|
|
248
281
|
try {
|
|
249
|
-
await configureProject(
|
|
282
|
+
await configureProject(
|
|
283
|
+
projectPath,
|
|
284
|
+
responses.language,
|
|
285
|
+
responses.cssFramework,
|
|
286
|
+
responses.framework || { name: responses.frameworkName, variant: responses.frameworkVariant }
|
|
287
|
+
);
|
|
250
288
|
configSpin.stop(chalk.green("Project configured."));
|
|
251
289
|
} catch (err) {
|
|
252
290
|
configSpin.stop(chalk.red("Failed to configure project."));
|
|
@@ -254,7 +292,7 @@ export async function main(options = {}) {
|
|
|
254
292
|
}
|
|
255
293
|
|
|
256
294
|
// 12. Cleanup boilerplate
|
|
257
|
-
await cleanupBoilerplate(projectPath);
|
|
295
|
+
await cleanupBoilerplate(projectPath, responses);
|
|
258
296
|
|
|
259
297
|
// 12.5 Copy .env.example into the project
|
|
260
298
|
await copyEnvExample(projectPath, TEMPLATES_DIR, responses);
|
|
@@ -297,9 +335,16 @@ export async function main(options = {}) {
|
|
|
297
335
|
}
|
|
298
336
|
|
|
299
337
|
// 13.5 Format the freshly generated project so it is canonical on init
|
|
300
|
-
|
|
338
|
+
const shouldFormat =
|
|
339
|
+
(responses.formatter && responses.formatter !== "none") ||
|
|
340
|
+
responses.linter === "biome";
|
|
341
|
+
if (shouldFormat) {
|
|
342
|
+
const fmtName =
|
|
343
|
+
responses.formatter && responses.formatter !== "none"
|
|
344
|
+
? responses.formatter
|
|
345
|
+
: "biome";
|
|
301
346
|
const fmtPassSpin = spinner();
|
|
302
|
-
fmtPassSpin.start(`Formatting project with ${
|
|
347
|
+
fmtPassSpin.start(`Formatting project with ${fmtName}...`);
|
|
303
348
|
try {
|
|
304
349
|
await runProjectFormat(projectPath, responses);
|
|
305
350
|
fmtPassSpin.stop(chalk.green("Project formatted."));
|
|
@@ -317,5 +362,5 @@ export async function main(options = {}) {
|
|
|
317
362
|
);
|
|
318
363
|
|
|
319
364
|
// 14. Offer dev server
|
|
320
|
-
await handleDevServer(pkg, projectName);
|
|
365
|
+
await handleDevServer(pkg, projectPath, { isNonInteractive, projectName });
|
|
321
366
|
}
|
package/src/manifest/emit.js
CHANGED
|
@@ -15,25 +15,34 @@ const FORMAT_COMMANDS = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Build default harness commands from tooling choices.
|
|
18
|
+
* Build default harness commands from tooling choices and test framework.
|
|
19
19
|
* Skips lint/format when tooling is "none"; skips types when language is not "ts".
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Skips test when testing is "none" (or not configured).
|
|
21
|
+
* Build is always included so commands.length >= 1.
|
|
22
|
+
* @param {{ linter?: string, formatter?: string, language?: string, testing?: string }} tooling
|
|
23
|
+
* @param {string} [testing]
|
|
22
24
|
* @returns {Array<{ name: string, command: string, required: boolean }>}
|
|
23
25
|
*/
|
|
24
|
-
export function defaultHarnessCommands(tooling = {}) {
|
|
26
|
+
export function defaultHarnessCommands(tooling = {}, testing = null) {
|
|
25
27
|
const { linter = "none", formatter = "none", language = "ts" } = tooling;
|
|
28
|
+
const testFramework = testing ?? tooling.testing ?? "none";
|
|
26
29
|
const commands = [];
|
|
27
30
|
if (linter && linter !== "none" && LINT_COMMANDS[linter]) {
|
|
28
31
|
commands.push({ name: "lint", command: LINT_COMMANDS[linter], required: true });
|
|
29
32
|
}
|
|
30
33
|
if (formatter && formatter !== "none" && FORMAT_COMMANDS[formatter]) {
|
|
31
34
|
commands.push({ name: "format", command: FORMAT_COMMANDS[formatter], required: true });
|
|
35
|
+
} else if (linter === "biome" && (!formatter || formatter === "none" || formatter === "biome")) {
|
|
36
|
+
commands.push({ name: "format", command: "biome check --write .", required: true });
|
|
32
37
|
}
|
|
33
38
|
if (language === "ts") {
|
|
34
39
|
commands.push({ name: "types", command: "tsc -b", required: true });
|
|
35
40
|
}
|
|
36
|
-
|
|
41
|
+
if (testFramework === "vitest") {
|
|
42
|
+
commands.push({ name: "test", command: "vitest run", required: true });
|
|
43
|
+
} else if (testFramework === "jest") {
|
|
44
|
+
commands.push({ name: "test", command: "npm test", required: true });
|
|
45
|
+
}
|
|
37
46
|
commands.push({ name: "build", command: "npm run build", required: true });
|
|
38
47
|
return commands.length > 0 ? commands : [...HARNESS_DEFAULT_COMMANDS];
|
|
39
48
|
}
|
|
@@ -99,8 +108,15 @@ export function buildManifest(responses, opts = {}) {
|
|
|
99
108
|
formatter: responses.formatter || "none",
|
|
100
109
|
};
|
|
101
110
|
|
|
111
|
+
const resolvedTesting =
|
|
112
|
+
responses.testing !== undefined
|
|
113
|
+
? responses.testing
|
|
114
|
+
: framework.name === "next"
|
|
115
|
+
? "none"
|
|
116
|
+
: "vitest";
|
|
117
|
+
|
|
102
118
|
const harness = responses.harness ?? {
|
|
103
|
-
commands: defaultHarnessCommands(tooling),
|
|
119
|
+
commands: defaultHarnessCommands(tooling, resolvedTesting),
|
|
104
120
|
};
|
|
105
121
|
|
|
106
122
|
const manifest = {
|
|
@@ -120,7 +136,7 @@ export function buildManifest(responses, opts = {}) {
|
|
|
120
136
|
if (responses.database && responses.database !== "none") {
|
|
121
137
|
manifest.database = {
|
|
122
138
|
orm: responses.database,
|
|
123
|
-
provider: "postgresql",
|
|
139
|
+
provider: responses.databaseProvider || "postgresql",
|
|
124
140
|
};
|
|
125
141
|
}
|
|
126
142
|
if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
|
package/src/manifest/paths.js
CHANGED
|
@@ -16,7 +16,7 @@ export function resolvePaths({ architecture, framework = {} } = {}) {
|
|
|
16
16
|
const variant = framework.variant || (fwName === "next" ? "app-router" : "vite");
|
|
17
17
|
|
|
18
18
|
// Helper to pick Next.js pages root based on variant
|
|
19
|
-
const nextPagesForVariant = (v) => (v === "pages-router" ? "pages" : "app");
|
|
19
|
+
const nextPagesForVariant = (v) => (v === "pages-router" ? "src/pages" : "src/app");
|
|
20
20
|
const isNext = fwName === "next";
|
|
21
21
|
|
|
22
22
|
// -- feature-based (React + Vite canonical, also Next feature-based)
|
|
@@ -69,30 +69,6 @@ export function resolvePaths({ architecture, framework = {} } = {}) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// -- hybrid (Next.js: feature + shared). For React, fallback to feature-based
|
|
73
|
-
if (arch === "hybrid") {
|
|
74
|
-
if (isNext) {
|
|
75
|
-
const nextRoot = nextPagesForVariant(variant);
|
|
76
|
-
return {
|
|
77
|
-
features: "src/features",
|
|
78
|
-
components: "src/shared/components",
|
|
79
|
-
services: "src/shared/services",
|
|
80
|
-
hooks: "src/shared/hooks",
|
|
81
|
-
pages: nextRoot,
|
|
82
|
-
ui: "src/shared/components/ui",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// React hybrid = hybrid not valid, but map to feature-based for determinism
|
|
86
|
-
return {
|
|
87
|
-
features: "src/features",
|
|
88
|
-
components: "src/shared/components",
|
|
89
|
-
services: "src/shared/services",
|
|
90
|
-
hooks: "src/shared/hooks",
|
|
91
|
-
pages: "src/app/router",
|
|
92
|
-
ui: "src/shared/components/ui",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
72
|
// -- none (minimal flat src/ — no imposed convention)
|
|
97
73
|
if (arch === "none") {
|
|
98
74
|
if (isNext) {
|
package/src/manifest/schema.js
CHANGED
|
@@ -13,7 +13,7 @@ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
|
|
|
13
13
|
export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
|
|
14
14
|
|
|
15
15
|
export const STYLING_ENGINES = ["tailwind", "none"];
|
|
16
|
-
export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "
|
|
16
|
+
export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "none"];
|
|
17
17
|
// Legacy alias: docs used architecture.type, canonical is architecture.preset.
|
|
18
18
|
// Keep both in sync; parseManifest normalizes to expose both keys.
|
|
19
19
|
export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
|
|
@@ -24,7 +24,7 @@ export const TOOLING_LANGUAGES = ["ts", "js"];
|
|
|
24
24
|
export const TOOLING_LINTERS = ["eslint", "oxlint", "biome", "none"];
|
|
25
25
|
export const TOOLING_FORMATTERS = ["prettier", "oxfmt", "none"];
|
|
26
26
|
export const DATABASE_ORMS = ["prisma", "none"];
|
|
27
|
-
export const DATABASE_PROVIDERS = ["postgresql"];
|
|
27
|
+
export const DATABASE_PROVIDERS = ["postgresql", "supabase"];
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Sub-schemas
|
|
@@ -212,7 +212,7 @@ export const manifestSchemaV2 = z
|
|
|
212
212
|
const allowedByFramework =
|
|
213
213
|
framework.name === "react"
|
|
214
214
|
? ["feature-based", "type-based", "none"]
|
|
215
|
-
: ["feature-based", "
|
|
215
|
+
: ["feature-based", "none"];
|
|
216
216
|
if (!allowedByFramework.includes(archValue)) {
|
|
217
217
|
ctx.addIssue({
|
|
218
218
|
code: "custom",
|