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/css.js
CHANGED
|
@@ -17,7 +17,6 @@ export async function setupCssFramework({
|
|
|
17
17
|
pkg: _pkg,
|
|
18
18
|
framework = "vite",
|
|
19
19
|
}) {
|
|
20
|
-
process.chdir(projectPath);
|
|
21
20
|
const fw = normalizeFramework(framework);
|
|
22
21
|
const isFilesystemRouted = hasCapability(fw, CAPABILITIES.FILESYSTEM_ROUTING);
|
|
23
22
|
const cssDir = path.join(templatesDir, "css");
|
|
@@ -26,7 +25,7 @@ export async function setupCssFramework({
|
|
|
26
25
|
// main.css). The chosen framework's styles land in the file the architecture
|
|
27
26
|
// actually imports.
|
|
28
27
|
const stylesDir =
|
|
29
|
-
architecture === "feature-based"
|
|
28
|
+
architecture === "feature-based"
|
|
30
29
|
? path.join(projectPath, "src", "shared", "styles")
|
|
31
30
|
: path.join(projectPath, "src", "styles");
|
|
32
31
|
|
|
@@ -100,11 +99,11 @@ async function syncMainCssImport(projectPath, ext, architecture, mainFileName) {
|
|
|
100
99
|
try {
|
|
101
100
|
let content = await fsp.readFile(mainPath, "utf8");
|
|
102
101
|
const expected =
|
|
103
|
-
architecture === "feature-based"
|
|
102
|
+
architecture === "feature-based"
|
|
104
103
|
? `./shared/styles/${mainFileName}`
|
|
105
104
|
: `./styles/${mainFileName}`;
|
|
106
105
|
const wrong =
|
|
107
|
-
architecture === "feature-based"
|
|
106
|
+
architecture === "feature-based"
|
|
108
107
|
? /import ['"]\.\/shared\/styles\/(globals|main)\.css['"]/
|
|
109
108
|
: /import ['"]\.\/styles\/(globals|main)\.css['"]/;
|
|
110
109
|
content = content.replace(wrong, `import '${expected}'`);
|
package/src/dependencies.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
import { normalizeFramework } from "./engine/capabilities.js";
|
|
2
3
|
|
|
3
4
|
export async function installDeps(pkgManager, dev, packages, cwd) {
|
|
4
5
|
let cmd, args;
|
|
@@ -40,11 +41,15 @@ export async function installAllDeps(responses, pkg, cwd) {
|
|
|
40
41
|
// the conditional wiring stays verifiable without a network install.
|
|
41
42
|
export function computeDeps(responses) {
|
|
42
43
|
const deps = [];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
44
|
+
const fw = normalizeFramework(
|
|
45
|
+
responses.framework || {
|
|
46
|
+
name: responses.frameworkName,
|
|
47
|
+
variant: responses.frameworkVariant,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
const fwName = fw.name;
|
|
47
51
|
const cssDevDeps = [];
|
|
52
|
+
const databaseDevDeps = [];
|
|
48
53
|
const testingDevDeps = [];
|
|
49
54
|
const linterDevDeps = [];
|
|
50
55
|
const formatterDevDeps = [];
|
|
@@ -83,6 +88,15 @@ export function computeDeps(responses) {
|
|
|
83
88
|
deps.push("axios");
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
// Database
|
|
92
|
+
if (responses.database === "prisma") {
|
|
93
|
+
deps.push("@prisma/client@^6");
|
|
94
|
+
databaseDevDeps.push("prisma@^6");
|
|
95
|
+
if (responses.language === "ts") {
|
|
96
|
+
databaseDevDeps.push("tsx");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
86
100
|
// Testing
|
|
87
101
|
if (responses.testing === "vitest") {
|
|
88
102
|
testingDevDeps.push(
|
|
@@ -92,17 +106,29 @@ export function computeDeps(responses) {
|
|
|
92
106
|
"jsdom"
|
|
93
107
|
);
|
|
94
108
|
} else if (responses.testing === "jest") {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
if (fwName === "next") {
|
|
110
|
+
testingDevDeps.push(
|
|
111
|
+
"jest",
|
|
112
|
+
"jest-environment-jsdom",
|
|
113
|
+
"@testing-library/react",
|
|
114
|
+
"@testing-library/jest-dom"
|
|
115
|
+
);
|
|
116
|
+
if (responses.language === "ts") {
|
|
117
|
+
testingDevDeps.push("@types/jest");
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
testingDevDeps.push(
|
|
121
|
+
"jest",
|
|
122
|
+
"jest-environment-jsdom",
|
|
123
|
+
"@testing-library/react",
|
|
124
|
+
"@testing-library/jest-dom",
|
|
125
|
+
"jsdom",
|
|
126
|
+
"babel-jest",
|
|
127
|
+
"@babel/preset-env",
|
|
128
|
+
"@babel/preset-react",
|
|
129
|
+
"@babel/preset-typescript"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
// Linter
|
|
@@ -136,6 +162,7 @@ export function computeDeps(responses) {
|
|
|
136
162
|
|
|
137
163
|
const devDepBatches = [
|
|
138
164
|
cssDevDeps,
|
|
165
|
+
databaseDevDeps,
|
|
139
166
|
testingDevDeps,
|
|
140
167
|
linterDevDeps,
|
|
141
168
|
formatterDevDeps,
|
|
@@ -18,6 +18,9 @@ export const CAPABILITIES = {
|
|
|
18
18
|
NEXT_IMAGE: "next-image",
|
|
19
19
|
SPA_FALLBACK: "spa-fallback",
|
|
20
20
|
AGENT_DOCS: "agent-docs",
|
|
21
|
+
DATABASE: "database",
|
|
22
|
+
DOCKER: "docker",
|
|
23
|
+
ROUTE_GROUPS: "route-groups",
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export const FRAMEWORK_CAPABILITIES = {
|
|
@@ -37,6 +40,7 @@ export const FRAMEWORK_CAPABILITIES = {
|
|
|
37
40
|
allowedFormatters: ["prettier", "oxfmt", "none"],
|
|
38
41
|
allowedBundlers: [],
|
|
39
42
|
allowedAdapters: [],
|
|
43
|
+
allowedDatabases: [],
|
|
40
44
|
supportsRouterPrompt: true,
|
|
41
45
|
},
|
|
42
46
|
"next:app-router": {
|
|
@@ -52,8 +56,11 @@ export const FRAMEWORK_CAPABILITIES = {
|
|
|
52
56
|
CAPABILITIES.NEXT_FONT,
|
|
53
57
|
CAPABILITIES.NEXT_IMAGE,
|
|
54
58
|
CAPABILITIES.AGENT_DOCS,
|
|
59
|
+
CAPABILITIES.DATABASE,
|
|
60
|
+
CAPABILITIES.DOCKER,
|
|
61
|
+
CAPABILITIES.ROUTE_GROUPS,
|
|
55
62
|
],
|
|
56
|
-
allowedArchitectures: ["feature-based", "
|
|
63
|
+
allowedArchitectures: ["feature-based", "none"],
|
|
57
64
|
allowedStyling: ["tailwind", "none"],
|
|
58
65
|
allowedUiKits: ["shadcn", "none"],
|
|
59
66
|
allowedLanguages: ["ts", "js"],
|
|
@@ -61,6 +68,7 @@ export const FRAMEWORK_CAPABILITIES = {
|
|
|
61
68
|
allowedFormatters: ["prettier", "none"],
|
|
62
69
|
allowedBundlers: ["turbopack", "webpack"],
|
|
63
70
|
allowedAdapters: ["node", "vercel", "cloudflare", "static"],
|
|
71
|
+
allowedDatabases: ["prisma", "none"],
|
|
64
72
|
supportsRouterPrompt: false, // filesystem routing is built-in
|
|
65
73
|
},
|
|
66
74
|
"next:pages-router": {
|
|
@@ -74,8 +82,10 @@ export const FRAMEWORK_CAPABILITIES = {
|
|
|
74
82
|
CAPABILITIES.NEXT_FONT,
|
|
75
83
|
CAPABILITIES.NEXT_IMAGE,
|
|
76
84
|
CAPABILITIES.AGENT_DOCS,
|
|
85
|
+
CAPABILITIES.DATABASE,
|
|
86
|
+
CAPABILITIES.DOCKER,
|
|
77
87
|
],
|
|
78
|
-
allowedArchitectures: ["feature-based", "
|
|
88
|
+
allowedArchitectures: ["feature-based", "none"],
|
|
79
89
|
allowedStyling: ["tailwind", "none"],
|
|
80
90
|
allowedUiKits: ["shadcn", "none"],
|
|
81
91
|
allowedLanguages: ["ts", "js"],
|
|
@@ -83,6 +93,7 @@ export const FRAMEWORK_CAPABILITIES = {
|
|
|
83
93
|
allowedFormatters: ["prettier", "none"],
|
|
84
94
|
allowedBundlers: ["turbopack", "webpack"],
|
|
85
95
|
allowedAdapters: ["node", "vercel", "cloudflare", "static"],
|
|
96
|
+
allowedDatabases: ["prisma", "none"],
|
|
86
97
|
supportsRouterPrompt: false, // filesystem routing is built-in
|
|
87
98
|
},
|
|
88
99
|
};
|
|
@@ -168,6 +179,22 @@ export function isOptionCompatible(framework, optionKey, optionValue) {
|
|
|
168
179
|
return hasCapability(framework, CAPABILITIES.REACT_COMPILER);
|
|
169
180
|
}
|
|
170
181
|
return true;
|
|
182
|
+
case "database":
|
|
183
|
+
case "db":
|
|
184
|
+
if (!desc.allowedDatabases || desc.allowedDatabases.length === 0) {
|
|
185
|
+
return optionValue === "none" || !optionValue;
|
|
186
|
+
}
|
|
187
|
+
return desc.allowedDatabases.includes(optionValue);
|
|
188
|
+
case "docker":
|
|
189
|
+
if (optionValue === true) {
|
|
190
|
+
return hasCapability(framework, CAPABILITIES.DOCKER);
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
case "routeGroups":
|
|
194
|
+
if (optionValue === true) {
|
|
195
|
+
return hasCapability(framework, CAPABILITIES.ROUTE_GROUPS);
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
171
198
|
default:
|
|
172
199
|
return true;
|
|
173
200
|
}
|
|
@@ -192,6 +219,7 @@ export function getCompatibleOptions(framework) {
|
|
|
192
219
|
formatters: [...desc.allowedFormatters],
|
|
193
220
|
bundlers: [...desc.allowedBundlers],
|
|
194
221
|
adapters: [...desc.allowedAdapters],
|
|
222
|
+
databases: [...(desc.allowedDatabases || [])],
|
|
195
223
|
supportsRouterPrompt: desc.supportsRouterPrompt,
|
|
196
224
|
};
|
|
197
225
|
}
|
|
@@ -289,6 +317,12 @@ export function isPromptVisible(framework, promptName) {
|
|
|
289
317
|
return hasCapability(fw, CAPABILITIES.ADAPTERS);
|
|
290
318
|
case "reactCompiler":
|
|
291
319
|
return hasCapability(fw, CAPABILITIES.REACT_COMPILER);
|
|
320
|
+
case "database":
|
|
321
|
+
return hasCapability(fw, CAPABILITIES.DATABASE);
|
|
322
|
+
case "docker":
|
|
323
|
+
return hasCapability(fw, CAPABILITIES.DOCKER);
|
|
324
|
+
case "routeGroups":
|
|
325
|
+
return hasCapability(fw, CAPABILITIES.ROUTE_GROUPS);
|
|
292
326
|
default:
|
|
293
327
|
return true;
|
|
294
328
|
}
|
|
@@ -334,6 +368,9 @@ export function getDefaultResponses(framework, { projectName = "my-app" } = {})
|
|
|
334
368
|
stateManagement: "none",
|
|
335
369
|
iconLibrary: "none",
|
|
336
370
|
apiClient: "none",
|
|
371
|
+
database: "none",
|
|
372
|
+
docker: false,
|
|
373
|
+
routeGroups: false,
|
|
337
374
|
linter: "eslint",
|
|
338
375
|
formatter: "prettier",
|
|
339
376
|
docsLanguage: "en",
|
|
@@ -355,6 +392,9 @@ export function getDefaultResponses(framework, { projectName = "my-app" } = {})
|
|
|
355
392
|
stateManagement: "none",
|
|
356
393
|
iconLibrary: "none",
|
|
357
394
|
apiClient: "none",
|
|
395
|
+
database: "none",
|
|
396
|
+
docker: false,
|
|
397
|
+
routeGroups: false,
|
|
358
398
|
linter: "eslint",
|
|
359
399
|
formatter: "prettier",
|
|
360
400
|
docsLanguage: "en",
|
|
@@ -390,6 +430,15 @@ export function sanitizeResponsesForFramework(framework, responses = {}) {
|
|
|
390
430
|
sanitized.reactCompiler = false;
|
|
391
431
|
}
|
|
392
432
|
}
|
|
433
|
+
if (!isPromptVisible(fw, "database")) {
|
|
434
|
+
sanitized.database = "none";
|
|
435
|
+
}
|
|
436
|
+
if (!isPromptVisible(fw, "docker")) {
|
|
437
|
+
sanitized.docker = false;
|
|
438
|
+
}
|
|
439
|
+
if (!isPromptVisible(fw, "routeGroups")) {
|
|
440
|
+
sanitized.routeGroups = false;
|
|
441
|
+
}
|
|
393
442
|
|
|
394
443
|
for (const [key, val] of Object.entries(sanitized)) {
|
|
395
444
|
if (val === undefined || val === null) continue;
|
|
@@ -41,10 +41,13 @@ export const TOOLING_ORDER = [
|
|
|
41
41
|
"router",
|
|
42
42
|
"icons",
|
|
43
43
|
"api",
|
|
44
|
+
"database",
|
|
44
45
|
"testing",
|
|
45
46
|
"linter",
|
|
46
47
|
"feature-script",
|
|
47
48
|
"next-config",
|
|
49
|
+
"route-groups",
|
|
50
|
+
"docker",
|
|
48
51
|
"formatter",
|
|
49
52
|
];
|
|
50
53
|
|
|
@@ -163,7 +166,7 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
163
166
|
});
|
|
164
167
|
|
|
165
168
|
// =========================================================================
|
|
166
|
-
// Layer 3: Architecture (feature-based / type-based /
|
|
169
|
+
// Layer 3: Architecture (feature-based / type-based / none)
|
|
167
170
|
// =========================================================================
|
|
168
171
|
const archSrcDir = path.join(templatesDir, "architectures", architecture, "src");
|
|
169
172
|
const archMainSrc = path.join(templatesDir, "architectures", architecture, `main.${ext}`);
|
|
@@ -174,6 +177,24 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
174
177
|
sourceDir: archSrcDir,
|
|
175
178
|
targetDir: "src",
|
|
176
179
|
action: async ({ projectPath }) => {
|
|
180
|
+
if (fw.name === "next") {
|
|
181
|
+
try { await fsp.rm(path.join(projectPath, "src", "main.tsx"), { force: true }); } catch {}
|
|
182
|
+
try { await fsp.rm(path.join(projectPath, "src", "main.jsx"), { force: true }); } catch {}
|
|
183
|
+
try { await fsp.rm(path.join(projectPath, "src", "App.tsx"), { force: true }); } catch {}
|
|
184
|
+
try { await fsp.rm(path.join(projectPath, "src", "App.jsx"), { force: true }); } catch {}
|
|
185
|
+
try { await fsp.rm(path.join(projectPath, "src", "App.css"), { force: true }); } catch {}
|
|
186
|
+
if (fw.variant === "pages-router") {
|
|
187
|
+
try { await fsp.rm(path.join(projectPath, "src", "app"), { recursive: true, force: true }); } catch {}
|
|
188
|
+
} else {
|
|
189
|
+
// Next App Router: preserve file-system routes; purge React/Vite-specific application layer
|
|
190
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "App.tsx"), { force: true }); } catch {}
|
|
191
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "App.jsx"), { force: true }); } catch {}
|
|
192
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "hooks"), { recursive: true, force: true }); } catch {}
|
|
193
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "contexts"), { recursive: true, force: true }); } catch {}
|
|
194
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "providers"), { recursive: true, force: true }); } catch {}
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
177
198
|
// Copy architecture main entry
|
|
178
199
|
try {
|
|
179
200
|
await fsp.access(archMainSrc);
|
|
@@ -204,6 +225,16 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
204
225
|
id: `styling:${cssFramework}:components`,
|
|
205
226
|
sourceDir: componentStylesDir,
|
|
206
227
|
targetDir: "src",
|
|
228
|
+
action: async ({ projectPath }) => {
|
|
229
|
+
if (fw.name === "next") {
|
|
230
|
+
if (fw.variant === "pages-router") {
|
|
231
|
+
try { await fsp.rm(path.join(projectPath, "src", "app"), { recursive: true, force: true }); } catch {}
|
|
232
|
+
} else {
|
|
233
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "App.tsx"), { force: true }); } catch {}
|
|
234
|
+
try { await fsp.rm(path.join(projectPath, "src", "app", "App.jsx"), { force: true }); } catch {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
207
238
|
});
|
|
208
239
|
}
|
|
209
240
|
|
|
@@ -220,6 +251,7 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
220
251
|
sourceDir: path.join(templatesDir, "conditional", "state", "redux", architecture, "src"),
|
|
221
252
|
targetDir: "src",
|
|
222
253
|
action: async ({ projectPath }) => {
|
|
254
|
+
if (fw.name === "next") return;
|
|
223
255
|
await updateMainWithProvider(projectPath, "redux", architecture, ext);
|
|
224
256
|
},
|
|
225
257
|
});
|
|
@@ -316,6 +348,111 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
316
348
|
id: `tooling:api:${responses.apiClient}`,
|
|
317
349
|
sourceDir: apiDir,
|
|
318
350
|
targetDir: "src",
|
|
351
|
+
action: async ({ projectPath }) => {
|
|
352
|
+
if (fw.name === "next") {
|
|
353
|
+
const configCandidates = [
|
|
354
|
+
path.join(projectPath, "src", "config", `axios.config.${extConfig}`),
|
|
355
|
+
path.join(projectPath, "src", "shared", "lib", "axios", `api.config.${extConfig}`),
|
|
356
|
+
path.join(projectPath, "src", "config", `api.config.${extConfig}`),
|
|
357
|
+
path.join(projectPath, "src", "shared", "api", `api.config.${extConfig}`),
|
|
358
|
+
];
|
|
359
|
+
for (const cfgPath of configCandidates) {
|
|
360
|
+
try {
|
|
361
|
+
let content = await fsp.readFile(cfgPath, "utf8");
|
|
362
|
+
if (content.includes("import.meta.env.VITE_API_URL")) {
|
|
363
|
+
content = content.replace(
|
|
364
|
+
/import\.meta\.env\.VITE_API_URL/g,
|
|
365
|
+
"process.env.NEXT_PUBLIC_API_URL"
|
|
366
|
+
);
|
|
367
|
+
await fsp.writeFile(cfgPath, content, "utf8");
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 5.45 Database (Prisma)
|
|
377
|
+
if (responses.database === "prisma") {
|
|
378
|
+
fragments.push({
|
|
379
|
+
scope: FRAGMENT_SCOPES.TOOLING,
|
|
380
|
+
category: "database",
|
|
381
|
+
id: "tooling:database:prisma",
|
|
382
|
+
scripts: {
|
|
383
|
+
"db:generate": "prisma generate",
|
|
384
|
+
"db:push": "prisma db push",
|
|
385
|
+
"db:seed": "prisma db seed",
|
|
386
|
+
"db:studio": "prisma studio",
|
|
387
|
+
},
|
|
388
|
+
action: async ({ projectPath, templatesDir: tDir }) => {
|
|
389
|
+
const prismaTplDir = path.join(tDir, "conditional", "database", "prisma");
|
|
390
|
+
const prismaDir = path.join(projectPath, "prisma");
|
|
391
|
+
await fsp.mkdir(prismaDir, { recursive: true });
|
|
392
|
+
|
|
393
|
+
// Copy schema.prisma
|
|
394
|
+
const schemaSrc = path.join(prismaTplDir, "schema.prisma");
|
|
395
|
+
const schemaDest = path.join(prismaDir, "schema.prisma");
|
|
396
|
+
try {
|
|
397
|
+
await fsp.access(schemaSrc);
|
|
398
|
+
await fsp.copyFile(schemaSrc, schemaDest);
|
|
399
|
+
} catch {}
|
|
400
|
+
|
|
401
|
+
// Copy seed file: prisma/seed.ts or prisma/seed.js
|
|
402
|
+
const seedFileName = language === "ts" ? "seed.ts" : "seed.js";
|
|
403
|
+
const seedSrc = path.join(prismaTplDir, seedFileName);
|
|
404
|
+
const seedDest = path.join(prismaDir, seedFileName);
|
|
405
|
+
try {
|
|
406
|
+
await fsp.access(seedSrc);
|
|
407
|
+
await fsp.copyFile(seedSrc, seedDest);
|
|
408
|
+
} catch {}
|
|
409
|
+
|
|
410
|
+
// Copy client singleton: src/lib/db.ts or src/lib/db.js
|
|
411
|
+
const libDir = path.join(projectPath, "src", "lib");
|
|
412
|
+
await fsp.mkdir(libDir, { recursive: true });
|
|
413
|
+
const dbFileName = language === "ts" ? "db.ts" : "db.js";
|
|
414
|
+
const dbSrc = path.join(prismaTplDir, dbFileName);
|
|
415
|
+
const dbDest = path.join(libDir, dbFileName);
|
|
416
|
+
try {
|
|
417
|
+
await fsp.access(dbSrc);
|
|
418
|
+
await fsp.copyFile(dbSrc, dbDest);
|
|
419
|
+
} catch {}
|
|
420
|
+
|
|
421
|
+
// Configure prisma.seed in package.json
|
|
422
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
423
|
+
try {
|
|
424
|
+
const content = await fsp.readFile(pkgPath, "utf8");
|
|
425
|
+
const pkg = JSON.parse(content);
|
|
426
|
+
pkg.prisma = {
|
|
427
|
+
seed: language === "ts" ? "tsx prisma/seed.ts" : "node prisma/seed.js",
|
|
428
|
+
};
|
|
429
|
+
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
430
|
+
} catch {}
|
|
431
|
+
|
|
432
|
+
// Append DATABASE_URL and DIRECT_URL to .env.example
|
|
433
|
+
const envExamplePath = path.join(projectPath, ".env.example");
|
|
434
|
+
const envLines = [
|
|
435
|
+
"",
|
|
436
|
+
"# Database (Prisma + PostgreSQL)",
|
|
437
|
+
"# Local development with PostgreSQL or Docker compose (docker compose up -d db):",
|
|
438
|
+
'DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"',
|
|
439
|
+
'DIRECT_URL="postgresql://postgres:postgres@localhost:5432/app?schema=public"',
|
|
440
|
+
"",
|
|
441
|
+
"# Supabase / cloud PostgreSQL with connection pooling (Transaction mode / port 6543):",
|
|
442
|
+
'# DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"',
|
|
443
|
+
'# DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"',
|
|
444
|
+
].join("\n");
|
|
445
|
+
try {
|
|
446
|
+
let envContent = "";
|
|
447
|
+
try {
|
|
448
|
+
envContent = await fsp.readFile(envExamplePath, "utf8");
|
|
449
|
+
} catch {}
|
|
450
|
+
if (!envContent.includes("DATABASE_URL")) {
|
|
451
|
+
envContent = envContent.trimEnd() + "\n" + envLines.trimStart() + "\n";
|
|
452
|
+
await writeFileRecursive(envExamplePath, envContent);
|
|
453
|
+
}
|
|
454
|
+
} catch {}
|
|
455
|
+
},
|
|
319
456
|
});
|
|
320
457
|
}
|
|
321
458
|
|
|
@@ -325,7 +462,7 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
325
462
|
const testingDir = path.join(templatesDir, "conditional", "testing", testingFramework);
|
|
326
463
|
const testScripts =
|
|
327
464
|
testingFramework === "vitest"
|
|
328
|
-
? { test: "vitest", "test:run": "vitest run" }
|
|
465
|
+
? { test: "vitest", "test:run": "vitest run", "test:watch": "vitest" }
|
|
329
466
|
: { test: "jest", "test:watch": "jest --watch" };
|
|
330
467
|
|
|
331
468
|
fragments.push({
|
|
@@ -335,11 +472,26 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
335
472
|
scripts: testScripts,
|
|
336
473
|
action: async ({ projectPath }) => {
|
|
337
474
|
// Config file to project root
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
475
|
+
if (testingFramework === "jest" && fw.name === "next") {
|
|
476
|
+
const nextConfigExt = language === "ts" ? "ts" : "mjs";
|
|
477
|
+
const configSrc = path.join(testingDir, "next", `jest.config.${nextConfigExt}`);
|
|
478
|
+
try {
|
|
479
|
+
await fsp.access(configSrc);
|
|
480
|
+
await fsp.copyFile(configSrc, path.join(projectPath, `jest.config.${nextConfigExt}`));
|
|
481
|
+
} catch {}
|
|
482
|
+
} else if (testingFramework === "vitest" && fw.name === "next") {
|
|
483
|
+
const configSrc = path.join(testingDir, "next", `vitest.config.${extConfig}`);
|
|
484
|
+
try {
|
|
485
|
+
await fsp.access(configSrc);
|
|
486
|
+
await fsp.copyFile(configSrc, path.join(projectPath, `vitest.config.${extConfig}`));
|
|
487
|
+
} catch {}
|
|
488
|
+
} else {
|
|
489
|
+
const configSrc = path.join(testingDir, `${testingFramework}.config.${extConfig}`);
|
|
490
|
+
try {
|
|
491
|
+
await fsp.access(configSrc);
|
|
492
|
+
await fsp.copyFile(configSrc, path.join(projectPath, `${testingFramework}.config.${extConfig}`));
|
|
493
|
+
} catch {}
|
|
494
|
+
}
|
|
343
495
|
|
|
344
496
|
// Setup file to src/test/setup.*
|
|
345
497
|
const setupSrc = path.join(testingDir, `src/test/setup.${extConfig}`);
|
|
@@ -355,20 +507,43 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
355
507
|
await fsp.access(jestSetupSrc);
|
|
356
508
|
await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${extConfig}`));
|
|
357
509
|
} catch {}
|
|
358
|
-
|
|
510
|
+
if (fw.name !== "next") {
|
|
511
|
+
const babelSrc = path.join(testingDir, "babel.config.cjs");
|
|
512
|
+
try {
|
|
513
|
+
await fsp.access(babelSrc);
|
|
514
|
+
await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
|
|
515
|
+
} catch {}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (fw.name === "next") {
|
|
520
|
+
const testFileName = fw.variant === "pages-router" ? "index" : "page";
|
|
521
|
+
const testSrc = path.join(
|
|
522
|
+
testingDir,
|
|
523
|
+
"next",
|
|
524
|
+
fw.variant,
|
|
525
|
+
"src",
|
|
526
|
+
"__tests__",
|
|
527
|
+
`${testFileName}.test.${ext}`
|
|
528
|
+
);
|
|
529
|
+
try {
|
|
530
|
+
await fsp.access(testSrc);
|
|
531
|
+
const testDest = path.join(projectPath, "src", "__tests__", `${testFileName}.test.${ext}`);
|
|
532
|
+
let testContent = await fsp.readFile(testSrc, "utf8");
|
|
533
|
+
if (responses.routeGroups === true && fw.variant === "app-router") {
|
|
534
|
+
testContent = testContent.replace("@/app/page", "@/app/(public)/page");
|
|
535
|
+
}
|
|
536
|
+
await writeFileRecursive(testDest, testContent);
|
|
537
|
+
} catch {}
|
|
538
|
+
} else {
|
|
539
|
+
// Architecture-scoped test file
|
|
540
|
+
const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${ext}`);
|
|
359
541
|
try {
|
|
360
|
-
await fsp.access(
|
|
361
|
-
|
|
542
|
+
await fsp.access(testSrc);
|
|
543
|
+
const testDest = path.join(projectPath, "src", "__tests__", `App.test.${ext}`);
|
|
544
|
+
await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
|
|
362
545
|
} catch {}
|
|
363
546
|
}
|
|
364
|
-
|
|
365
|
-
// Architecture-scoped test file
|
|
366
|
-
const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${ext}`);
|
|
367
|
-
try {
|
|
368
|
-
await fsp.access(testSrc);
|
|
369
|
-
const testDest = path.join(projectPath, "src", "__tests__", `App.test.${ext}`);
|
|
370
|
-
await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
|
|
371
|
-
} catch {}
|
|
372
547
|
},
|
|
373
548
|
});
|
|
374
549
|
}
|
|
@@ -417,14 +592,24 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
417
592
|
},
|
|
418
593
|
});
|
|
419
594
|
} else if (responses.linter === "biome") {
|
|
595
|
+
const isBiomeFormatter =
|
|
596
|
+
!responses.formatter ||
|
|
597
|
+
responses.formatter === "none" ||
|
|
598
|
+
responses.formatter === "biome";
|
|
599
|
+
const biomeScripts = {
|
|
600
|
+
lint: "biome check .",
|
|
601
|
+
"lint:fix": "biome check --write .",
|
|
602
|
+
};
|
|
603
|
+
if (isBiomeFormatter) {
|
|
604
|
+
biomeScripts.format = "biome check --write .";
|
|
605
|
+
biomeScripts["format:check"] = "biome check .";
|
|
606
|
+
}
|
|
607
|
+
|
|
420
608
|
fragments.push({
|
|
421
609
|
scope: FRAGMENT_SCOPES.TOOLING,
|
|
422
610
|
category: "linter",
|
|
423
611
|
id: "tooling:linter:biome",
|
|
424
|
-
scripts:
|
|
425
|
-
lint: "biome check .",
|
|
426
|
-
"lint:fix": "biome check --write .",
|
|
427
|
-
},
|
|
612
|
+
scripts: biomeScripts,
|
|
428
613
|
action: async ({ projectPath }) => {
|
|
429
614
|
const configSrc = path.join(templatesDir, "conditional", "linting", "biome", "biome.json");
|
|
430
615
|
try {
|
|
@@ -525,11 +710,85 @@ export function resolveFragments(responses = {}, templatesDir) {
|
|
|
525
710
|
}
|
|
526
711
|
} catch {}
|
|
527
712
|
}
|
|
713
|
+
|
|
714
|
+
// Apply standalone output to next.config if docker is enabled
|
|
715
|
+
if (responses.docker === true) {
|
|
716
|
+
const targetConfigPath = path.join(projectPath, `next.config.${configExt}`);
|
|
717
|
+
try {
|
|
718
|
+
let content = await fsp.readFile(targetConfigPath, "utf8");
|
|
719
|
+
if (!content.includes('"standalone"') && !content.includes("'standalone'")) {
|
|
720
|
+
content = content.replace(
|
|
721
|
+
/const nextConfig(?::\s*NextConfig)?\s*=\s*\{/,
|
|
722
|
+
(match) => `${match}\n output: "standalone",`
|
|
723
|
+
);
|
|
724
|
+
await fsp.writeFile(targetConfigPath, content, "utf8");
|
|
725
|
+
}
|
|
726
|
+
} catch {}
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 5.8 Route Groups Preset & Auth Proxy Template (#64)
|
|
733
|
+
if (responses.routeGroups === true && fw.name === "next" && fw.variant === "app-router") {
|
|
734
|
+
fragments.push({
|
|
735
|
+
scope: FRAGMENT_SCOPES.TOOLING,
|
|
736
|
+
category: "route-groups",
|
|
737
|
+
id: "tooling:next:route-groups",
|
|
738
|
+
sourceDir: path.join(templatesDir, "conditional", "next", "route-groups", language),
|
|
739
|
+
targetDir: "",
|
|
740
|
+
action: async ({ projectPath }) => {
|
|
741
|
+
// Remove colliding root page so it does not collide with (public)/page
|
|
742
|
+
try {
|
|
743
|
+
await fsp.rm(path.join(projectPath, "src", "app", `page.${ext}`), { force: true });
|
|
744
|
+
} catch {}
|
|
745
|
+
const oppositeExt = language === "ts" ? "jsx" : "tsx";
|
|
746
|
+
try {
|
|
747
|
+
await fsp.rm(path.join(projectPath, "src", "app", `page.${oppositeExt}`), { force: true });
|
|
748
|
+
} catch {}
|
|
749
|
+
// Also remove page.js / page.tsx in case base provider was named page.js
|
|
750
|
+
try {
|
|
751
|
+
await fsp.rm(path.join(projectPath, "src", "app", "page.js"), { force: true });
|
|
752
|
+
} catch {}
|
|
753
|
+
try {
|
|
754
|
+
await fsp.rm(path.join(projectPath, "src", "app", "page.tsx"), { force: true });
|
|
755
|
+
} catch {}
|
|
756
|
+
// Adjust existing test file if it imported @/app/page before route groups was applied
|
|
757
|
+
for (const testExt of ["tsx", "jsx", "ts", "js"]) {
|
|
758
|
+
const testPath = path.join(projectPath, "src", "__tests__", `page.test.${testExt}`);
|
|
759
|
+
try {
|
|
760
|
+
const content = await fsp.readFile(testPath, "utf8");
|
|
761
|
+
if (content.includes("@/app/page")) {
|
|
762
|
+
await fsp.writeFile(testPath, content.replace(/@\/app\/page/g, "@/app/(public)/page"), "utf8");
|
|
763
|
+
}
|
|
764
|
+
} catch {}
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 5.9 Docker (Multi-stage Dockerfile and docker-compose for Next.js standalone)
|
|
771
|
+
if (responses.docker === true && hasCapability(fw, CAPABILITIES.DOCKER)) {
|
|
772
|
+
fragments.push({
|
|
773
|
+
scope: FRAGMENT_SCOPES.TOOLING,
|
|
774
|
+
category: "docker",
|
|
775
|
+
id: "tooling:docker",
|
|
776
|
+
action: async ({ projectPath }) => {
|
|
777
|
+
const dockerSrcDir = path.join(templatesDir, "conditional", "docker");
|
|
778
|
+
const filesToCopy = ["Dockerfile", ".dockerignore", "docker-compose.yml"];
|
|
779
|
+
for (const file of filesToCopy) {
|
|
780
|
+
const srcFile = path.join(dockerSrcDir, file);
|
|
781
|
+
const destFile = path.join(projectPath, file);
|
|
782
|
+
try {
|
|
783
|
+
await fsp.access(srcFile);
|
|
784
|
+
await fsp.copyFile(srcFile, destFile);
|
|
785
|
+
} catch {}
|
|
786
|
+
}
|
|
528
787
|
},
|
|
529
788
|
});
|
|
530
789
|
}
|
|
531
790
|
|
|
532
|
-
// 5.
|
|
791
|
+
// 5.10 Formatter
|
|
533
792
|
if (responses.formatter && responses.formatter !== "none") {
|
|
534
793
|
const isPrettier = responses.formatter === "prettier";
|
|
535
794
|
const configFile = isPrettier ? ".prettierrc" : ".oxfmtrc.json";
|