create-lumen 2.0.0-beta.1 → 2.0.0-beta.3
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 +30 -2
- package/src/agent-context.js +71 -47
- package/src/cleanup.js +34 -1
- package/src/cli-args.js +79 -2
- package/src/configure.js +36 -8
- package/src/css.js +3 -4
- package/src/dependencies.js +42 -15
- package/src/engine/capabilities.js +51 -2
- package/src/engine/composition.js +282 -23
- package/src/env.js +159 -4
- package/src/format.js +14 -6
- package/src/injector.js +66 -14
- package/src/main.js +62 -17
- package/src/manifest/emit.js +35 -6
- package/src/manifest/paths.js +1 -25
- package/src/manifest/schema.js +32 -2
- package/src/prompts.js +101 -11
- package/src/readme.js +0 -16
- package/src/utils/dev-server.js +6 -1
- package/templates/.env.example +1 -1
- package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
- package/templates/conditional/database/prisma/db.js +14 -0
- package/templates/conditional/database/prisma/db.ts +16 -0
- package/templates/conditional/database/prisma/schema.prisma +9 -0
- package/templates/conditional/database/prisma/seed.js +16 -0
- package/templates/conditional/database/prisma/seed.ts +16 -0
- package/templates/conditional/docker/.dockerignore +10 -0
- package/templates/conditional/docker/Dockerfile +57 -0
- package/templates/conditional/docker/docker-compose.yml +35 -0
- package/templates/conditional/linting/biome/biome.json +17 -11
- package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
- package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
- package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
- package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
- package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
- package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
- package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
- package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
- package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
- package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
- package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
- package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { promises as fsp } from "fs";
|
|
3
|
+
import { normalizeFramework } from "./engine/capabilities.js";
|
|
3
4
|
|
|
4
5
|
const ENV_GITIGNORE = `
|
|
5
6
|
# Environment variables
|
|
@@ -8,13 +9,167 @@ const ENV_GITIGNORE = `
|
|
|
8
9
|
!.env.example
|
|
9
10
|
`;
|
|
10
11
|
|
|
11
|
-
export async function
|
|
12
|
-
|
|
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
|
+
}
|
|
21
|
+
|
|
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 {}
|
|
29
|
+
|
|
30
|
+
// Try checking package.json
|
|
13
31
|
try {
|
|
14
|
-
|
|
15
|
-
await fsp.
|
|
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
|
+
}
|
|
16
38
|
} catch {}
|
|
17
39
|
|
|
40
|
+
return { name: "react", variant: "vite" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildEnvExample(responses = {}, existingContent = "") {
|
|
44
|
+
const fw = normalizeFramework(
|
|
45
|
+
responses.framework || {
|
|
46
|
+
name: responses.frameworkName,
|
|
47
|
+
variant: responses.frameworkVariant,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
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
|
+
}
|
|
109
|
+
} else {
|
|
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
|
+
}
|
|
155
|
+
}
|
|
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
|
+
|
|
18
173
|
// Ensure the project gitignore excludes env files but keeps .env.example.
|
|
19
174
|
const gitignore = path.join(projectPath, ".gitignore");
|
|
20
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,10 +292,10 @@ 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
|
-
await copyEnvExample(projectPath, TEMPLATES_DIR);
|
|
298
|
+
await copyEnvExample(projectPath, TEMPLATES_DIR, responses);
|
|
261
299
|
|
|
262
300
|
// 12.6 Emit lumen.config.json (manifest v2) — byte-deterministic, Zod-validated
|
|
263
301
|
const manifestSpin = spinner();
|
|
@@ -270,15 +308,15 @@ export async function main(options = {}) {
|
|
|
270
308
|
throw err;
|
|
271
309
|
}
|
|
272
310
|
|
|
273
|
-
// 12.7 Emit .
|
|
311
|
+
// 12.7 Emit AGENTS.md at root (only when agentDocs is enabled)
|
|
274
312
|
if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
|
|
275
313
|
const agentSpin = spinner();
|
|
276
|
-
agentSpin.start("Writing .
|
|
314
|
+
agentSpin.start("Writing AGENTS.md...");
|
|
277
315
|
try {
|
|
278
316
|
await emitAgentContext(projectPath, responses._manifestSource ?? responses);
|
|
279
|
-
agentSpin.stop(chalk.green(".
|
|
317
|
+
agentSpin.stop(chalk.green("AGENTS.md written."));
|
|
280
318
|
} catch (err) {
|
|
281
|
-
agentSpin.stop(chalk.red("Failed to write .
|
|
319
|
+
agentSpin.stop(chalk.red("Failed to write AGENTS.md."));
|
|
282
320
|
throw err;
|
|
283
321
|
}
|
|
284
322
|
}
|
|
@@ -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, projectName, { isNonInteractive });
|
|
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 = {
|
|
@@ -117,8 +133,18 @@ export function buildManifest(responses, opts = {}) {
|
|
|
117
133
|
};
|
|
118
134
|
|
|
119
135
|
// Optional cross-cutting flags — only include if explicitly set (keeps minimal manifest byte-stable)
|
|
136
|
+
if (responses.database && responses.database !== "none") {
|
|
137
|
+
manifest.database = {
|
|
138
|
+
orm: responses.database,
|
|
139
|
+
provider: responses.databaseProvider || "postgresql",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
|
|
143
|
+
if (typeof responses.routeGroups === "boolean") manifest.routeGroups = responses.routeGroups;
|
|
120
144
|
if (typeof responses.reactCompiler === "boolean") manifest.reactCompiler = responses.reactCompiler;
|
|
145
|
+
if (typeof responses.routeGroups === "boolean") manifest.routeGroups = responses.routeGroups;
|
|
121
146
|
if (typeof responses.agentDocs === "boolean") manifest.agentDocs = responses.agentDocs;
|
|
147
|
+
if (typeof responses.docker === "boolean") manifest.docker = responses.docker;
|
|
122
148
|
|
|
123
149
|
// Enforce stable top-level key order for determinism (like formatter pass)
|
|
124
150
|
const ordered = {};
|
|
@@ -133,6 +159,9 @@ export function buildManifest(responses, opts = {}) {
|
|
|
133
159
|
"docs",
|
|
134
160
|
"paths",
|
|
135
161
|
"tooling",
|
|
162
|
+
"database",
|
|
163
|
+
"docker",
|
|
164
|
+
"routeGroups",
|
|
136
165
|
"reactCompiler",
|
|
137
166
|
"agentDocs",
|
|
138
167
|
];
|
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) {
|