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.
Files changed (87) hide show
  1. package/README.md +23 -22
  2. package/bin/cli.js +1 -6
  3. package/package.json +2 -2
  4. package/schema/fixtures/v2/valid/next-app-router-feature-based.json +1 -1
  5. package/schema/fixtures/v2/valid/next-app-router-none.json +1 -1
  6. package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +1 -1
  7. package/schema/fixtures/v2/valid/next-pages-router-none.json +1 -1
  8. package/schema/lumen.config.v2.json +30 -2
  9. package/src/agent-context.js +71 -47
  10. package/src/cleanup.js +34 -1
  11. package/src/cli-args.js +79 -2
  12. package/src/configure.js +36 -8
  13. package/src/css.js +3 -4
  14. package/src/dependencies.js +42 -15
  15. package/src/engine/capabilities.js +51 -2
  16. package/src/engine/composition.js +282 -23
  17. package/src/env.js +159 -4
  18. package/src/format.js +14 -6
  19. package/src/injector.js +66 -14
  20. package/src/main.js +62 -17
  21. package/src/manifest/emit.js +35 -6
  22. package/src/manifest/paths.js +1 -25
  23. package/src/manifest/schema.js +32 -2
  24. package/src/prompts.js +101 -11
  25. package/src/readme.js +0 -16
  26. package/src/utils/dev-server.js +6 -1
  27. package/templates/.env.example +1 -1
  28. package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
  29. package/templates/conditional/database/prisma/db.js +14 -0
  30. package/templates/conditional/database/prisma/db.ts +16 -0
  31. package/templates/conditional/database/prisma/schema.prisma +9 -0
  32. package/templates/conditional/database/prisma/seed.js +16 -0
  33. package/templates/conditional/database/prisma/seed.ts +16 -0
  34. package/templates/conditional/docker/.dockerignore +10 -0
  35. package/templates/conditional/docker/Dockerfile +57 -0
  36. package/templates/conditional/docker/docker-compose.yml +35 -0
  37. package/templates/conditional/linting/biome/biome.json +17 -11
  38. package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
  39. package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
  40. package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
  41. package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
  42. package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
  43. package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
  44. package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
  45. package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
  46. package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
  47. package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
  48. package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
  49. package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
  50. package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
  51. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
  52. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
  53. package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
  54. package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
  55. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
  56. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
  57. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
  58. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
  59. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
  60. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
  61. package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
  62. package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
  63. package/templates/css/tailwind/postcss.config.mjs +3 -1
  64. package/schema/fixtures/v2/valid/next-app-router-hybrid.json +0 -35
  65. package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +0 -35
  66. package/templates/architectures/hybrid/main.jsx +0 -10
  67. package/templates/architectures/hybrid/main.tsx +0 -10
  68. package/templates/architectures/hybrid/src/App.jsx +0 -20
  69. package/templates/architectures/hybrid/src/App.tsx +0 -20
  70. package/templates/architectures/hybrid/src/features/.gitkeep +0 -1
  71. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +0 -31
  72. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +0 -36
  73. package/templates/architectures/hybrid/src/shared/components/ui/index.js +0 -1
  74. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +0 -1
  75. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +0 -28
  76. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +0 -28
  77. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  78. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  79. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  80. package/templates/architectures/hybrid/src/shared/types/index.js +0 -20
  81. package/templates/architectures/hybrid/src/shared/types/index.ts +0 -16
  82. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +0 -3
  83. package/templates/architectures/hybrid/src/shared/utils/cn.ts +0 -5
  84. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +0 -11
  85. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +0 -11
  86. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +0 -10
  87. 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" || architecture === "hybrid"
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" || architecture === "hybrid"
102
+ architecture === "feature-based"
104
103
  ? `./shared/styles/${mainFileName}`
105
104
  : `./styles/${mainFileName}`;
106
105
  const wrong =
107
- architecture === "feature-based" || architecture === "hybrid"
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}'`);
@@ -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 fwName =
44
- typeof responses.framework === "string"
45
- ? responses.framework
46
- : (responses.framework?.name ?? responses.frameworkName ?? "react");
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
- testingDevDeps.push(
96
- "jest",
97
- "jest-environment-jsdom",
98
- "@testing-library/react",
99
- "@testing-library/jest-dom",
100
- "jsdom",
101
- "babel-jest",
102
- "@babel/preset-env",
103
- "@babel/preset-react",
104
- "@babel/preset-typescript"
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", "hybrid", "none"],
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", "hybrid", "none"],
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 / hybrid / none)
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
- const configSrc = path.join(testingDir, `${testingFramework}.config.${extConfig}`);
339
- try {
340
- await fsp.access(configSrc);
341
- await fsp.copyFile(configSrc, path.join(projectPath, `${testingFramework}.config.${extConfig}`));
342
- } catch {}
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
- const babelSrc = path.join(testingDir, "babel.config.cjs");
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(babelSrc);
361
- await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
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.8 Formatter
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";