create-lumen 1.2.0 → 2.0.0-alpha.0

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 (136) hide show
  1. package/README.md +56 -9
  2. package/bin/cli.js +18 -4
  3. package/package.json +14 -4
  4. package/schema/fixtures/index.js +62 -0
  5. package/schema/fixtures/v2/invalid/invalid-manifest-version.json +33 -0
  6. package/schema/fixtures/v2/invalid/malformed-paths.json +33 -0
  7. package/schema/fixtures/v2/invalid/missing-required-fields.json +17 -0
  8. package/schema/fixtures/v2/invalid/next-type-based.json +33 -0
  9. package/schema/fixtures/v2/invalid/next-vite.json +33 -0
  10. package/schema/fixtures/v2/invalid/react-bundler.json +34 -0
  11. package/schema/fixtures/v2/invalid/react-hybrid.json +33 -0
  12. package/schema/fixtures/v2/invalid/react-variant-mismatch.json +33 -0
  13. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +33 -0
  14. package/schema/fixtures/v2/invalid/v1-flat-config.json +11 -0
  15. package/schema/fixtures/v2/valid/full-kitchen-sink.json +35 -0
  16. package/schema/fixtures/v2/valid/next-app-router-feature-based.json +35 -0
  17. package/schema/fixtures/v2/valid/next-app-router-hybrid.json +35 -0
  18. package/schema/fixtures/v2/valid/next-app-router-none.json +33 -0
  19. package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +35 -0
  20. package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +35 -0
  21. package/schema/fixtures/v2/valid/next-pages-router-none.json +33 -0
  22. package/schema/fixtures/v2/valid/react-vite-feature-based.json +33 -0
  23. package/schema/fixtures/v2/valid/react-vite-none.json +33 -0
  24. package/schema/fixtures/v2/valid/react-vite-type-based.json +33 -0
  25. package/schema/lumen.config.v2.json +216 -0
  26. package/src/cli-args.js +294 -0
  27. package/src/css.js +20 -9
  28. package/src/dependencies.js +12 -6
  29. package/src/engine/capabilities.js +218 -0
  30. package/src/engine/index.js +2 -0
  31. package/src/engine/providers/base-provider.js +219 -0
  32. package/src/injector.js +26 -13
  33. package/src/main.js +52 -10
  34. package/src/manifest/emit.js +120 -0
  35. package/src/manifest/index.js +11 -0
  36. package/src/manifest/paths.js +128 -0
  37. package/src/manifest/schema.js +212 -0
  38. package/src/prompts.js +26 -6
  39. package/templates/conditional/linting/eslint/next/eslint.config.mjs +16 -0
  40. package/templates/css/tailwind/postcss.config.mjs +5 -0
  41. /package/templates/architectures/{component-based → type-based}/main.jsx +0 -0
  42. /package/templates/architectures/{component-based → type-based}/main.tsx +0 -0
  43. /package/templates/architectures/{component-based → type-based}/src/App.jsx +0 -0
  44. /package/templates/architectures/{component-based → type-based}/src/App.tsx +0 -0
  45. /package/templates/architectures/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  46. /package/templates/architectures/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  47. /package/templates/architectures/{component-based → type-based}/src/components/common/index.js +0 -0
  48. /package/templates/architectures/{component-based → type-based}/src/components/common/index.ts +0 -0
  49. /package/templates/architectures/{component-based → type-based}/src/components/form/.gitkeep +0 -0
  50. /package/templates/architectures/{component-based → type-based}/src/config/.gitkeep +0 -0
  51. /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.js +0 -0
  52. /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.ts +0 -0
  53. /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.jsx +0 -0
  54. /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.ts +0 -0
  55. /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  56. /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  57. /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  58. /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  59. /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.js +0 -0
  60. /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.ts +0 -0
  61. /package/templates/architectures/{component-based → type-based}/src/providers/.gitkeep +0 -0
  62. /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.jsx +0 -0
  63. /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.tsx +0 -0
  64. /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.js +0 -0
  65. /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.ts +0 -0
  66. /package/templates/architectures/{component-based → type-based}/src/services/.gitkeep +0 -0
  67. /package/templates/architectures/{component-based → type-based}/src/store/.gitkeep +0 -0
  68. /package/templates/architectures/{component-based → type-based}/src/styles/.gitkeep +0 -0
  69. /package/templates/architectures/{component-based → type-based}/src/types/index.js +0 -0
  70. /package/templates/architectures/{component-based → type-based}/src/types/index.ts +0 -0
  71. /package/templates/architectures/{component-based → type-based}/src/utils/cn.jsx +0 -0
  72. /package/templates/architectures/{component-based → type-based}/src/utils/cn.ts +0 -0
  73. /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.js +0 -0
  74. /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.ts +0 -0
  75. /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.js +0 -0
  76. /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.ts +0 -0
  77. /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.js +0 -0
  78. /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.ts +0 -0
  79. /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.js +0 -0
  80. /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.ts +0 -0
  81. /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.js +0 -0
  82. /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.ts +0 -0
  83. /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.js +0 -0
  84. /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.ts +0 -0
  85. /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
  86. /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
  87. /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  88. /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  89. /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
  90. /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
  91. /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
  92. /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
  93. /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  94. /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  95. /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
  96. /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
  97. /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.jsx +0 -0
  98. /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.tsx +0 -0
  99. /package/templates/conditional/router/{component-based → type-based}/src/App.jsx +0 -0
  100. /package/templates/conditional/router/{component-based → type-based}/src/App.tsx +0 -0
  101. /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  102. /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  103. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.jsx +0 -0
  104. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.tsx +0 -0
  105. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.jsx +0 -0
  106. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.tsx +0 -0
  107. /package/templates/conditional/router/{component-based → type-based}/src/router/index.jsx +0 -0
  108. /package/templates/conditional/router/{component-based → type-based}/src/router/index.tsx +0 -0
  109. /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.jsx +0 -0
  110. /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.tsx +0 -0
  111. /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.jsx +0 -0
  112. /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.ts +0 -0
  113. /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.jsx +0 -0
  114. /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.tsx +0 -0
  115. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.js +0 -0
  116. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.ts +0 -0
  117. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.js +0 -0
  118. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.ts +0 -0
  119. /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.jsx +0 -0
  120. /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.ts +0 -0
  121. /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
  122. /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
  123. /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
  124. /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
  125. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  126. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  127. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  128. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  129. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  130. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  131. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  132. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  133. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  134. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  135. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  136. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
@@ -0,0 +1,219 @@
1
+ import path from "node:path";
2
+ import {
3
+ FRAMEWORK_CAPABILITIES,
4
+ getFrameworkKey,
5
+ } from "../capabilities.js";
6
+
7
+ /**
8
+ * Abstract BaseProvider seam (ADR 0001 / create-lumen#25).
9
+ * Encapsulates framework capabilities, base template file plans,
10
+ * default package sets, and post-write lifecycle hooks.
11
+ */
12
+ export class BaseProvider {
13
+ /**
14
+ * @param {object} config
15
+ * @param {string} config.name - framework name (e.g. "react", "next")
16
+ * @param {string} config.variant - framework variant (e.g. "vite", "app-router", "pages-router")
17
+ * @param {string[]} [config.capabilities]
18
+ */
19
+ constructor({ name, variant, capabilities = [] }) {
20
+ if (new.target === BaseProvider) {
21
+ throw new TypeError("Cannot instantiate abstract BaseProvider directly");
22
+ }
23
+ this.name = name;
24
+ this.variant = variant;
25
+ this.capabilities = new Set(capabilities);
26
+ }
27
+
28
+ /**
29
+ * Return array of declared capabilities.
30
+ * @returns {string[]}
31
+ */
32
+ getCapabilities() {
33
+ return Array.from(this.capabilities);
34
+ }
35
+
36
+ /**
37
+ * Check if provider has a capability.
38
+ * @param {string} capability
39
+ * @returns {boolean}
40
+ */
41
+ hasCapability(capability) {
42
+ return this.capabilities.has(capability);
43
+ }
44
+
45
+ /**
46
+ * Return relative file path pattern to the base template directory.
47
+ * Pattern: templates/bases/<provider>/<variant>/<lang>/
48
+ * @param {{ lang?: string }} [opts]
49
+ * @returns {string}
50
+ */
51
+ getFilePlan({ lang = "ts" } = {}) {
52
+ return path.posix.join("templates", "bases", this.name, this.variant, lang);
53
+ }
54
+
55
+ /**
56
+ * Return base package dependencies and devDependencies.
57
+ * @param {object} [_options]
58
+ * @returns {{ dependencies: Record<string, string>, devDependencies: Record<string, string> }}
59
+ */
60
+ getPackageSet(_options = {}) {
61
+ return {
62
+ dependencies: {},
63
+ devDependencies: {},
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Return post-write hooks to execute after files are written.
69
+ * @param {object} [_options]
70
+ * @returns {Array<{ name: string, run: (context: object) => Promise<void> | void }>}
71
+ */
72
+ getPostWriteHooks(_options = {}) {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ /**
78
+ * React + Vite concrete provider.
79
+ */
80
+ export class ViteReactProvider extends BaseProvider {
81
+ constructor() {
82
+ const desc = FRAMEWORK_CAPABILITIES["react:vite"];
83
+ super({
84
+ name: "react",
85
+ variant: "vite",
86
+ capabilities: desc.capabilities,
87
+ });
88
+ }
89
+
90
+ getPackageSet({ options = {} } = {}) {
91
+ const isTs = options.language === "ts";
92
+ const deps = {
93
+ react: "^19.0.0",
94
+ "react-dom": "^19.0.0",
95
+ };
96
+ const devDeps = {
97
+ vite: "^6.0.0",
98
+ "@vitejs/plugin-react": "^4.3.0",
99
+ };
100
+ if (isTs) {
101
+ devDeps.typescript = "^5.7.0";
102
+ devDeps["@types/react"] = "^19.0.0";
103
+ devDeps["@types/react-dom"] = "^19.0.0";
104
+ }
105
+ return {
106
+ dependencies: deps,
107
+ devDependencies: devDeps,
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Next.js App Router concrete provider.
114
+ */
115
+ export class NextAppRouterProvider extends BaseProvider {
116
+ constructor() {
117
+ const desc = FRAMEWORK_CAPABILITIES["next:app-router"];
118
+ super({
119
+ name: "next",
120
+ variant: "app-router",
121
+ capabilities: desc.capabilities,
122
+ });
123
+ }
124
+
125
+ getPackageSet({ options = {} } = {}) {
126
+ const isTs = options.language === "ts";
127
+ const deps = {
128
+ next: "^15.0.0",
129
+ react: "^19.0.0",
130
+ "react-dom": "^19.0.0",
131
+ };
132
+ const devDeps = {};
133
+ if (isTs) {
134
+ devDeps.typescript = "^5.7.0";
135
+ devDeps["@types/react"] = "^19.0.0";
136
+ devDeps["@types/react-dom"] = "^19.0.0";
137
+ devDeps["@types/node"] = "^20.0.0";
138
+ }
139
+ return {
140
+ dependencies: deps,
141
+ devDependencies: devDeps,
142
+ };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Next.js Pages Router concrete provider.
148
+ */
149
+ export class NextPagesRouterProvider extends BaseProvider {
150
+ constructor() {
151
+ const desc = FRAMEWORK_CAPABILITIES["next:pages-router"];
152
+ super({
153
+ name: "next",
154
+ variant: "pages-router",
155
+ capabilities: desc.capabilities,
156
+ });
157
+ }
158
+
159
+ getPackageSet({ options = {} } = {}) {
160
+ const isTs = options.language === "ts";
161
+ const deps = {
162
+ next: "^15.0.0",
163
+ react: "^19.0.0",
164
+ "react-dom": "^19.0.0",
165
+ };
166
+ const devDeps = {};
167
+ if (isTs) {
168
+ devDeps.typescript = "^5.7.0";
169
+ devDeps["@types/react"] = "^19.0.0";
170
+ devDeps["@types/react-dom"] = "^19.0.0";
171
+ devDeps["@types/node"] = "^20.0.0";
172
+ }
173
+ return {
174
+ dependencies: deps,
175
+ devDependencies: devDeps,
176
+ };
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Provider Registry
182
+ // ---------------------------------------------------------------------------
183
+ const providerRegistry = new Map();
184
+
185
+ /**
186
+ * Register a BaseProvider instance in the registry.
187
+ * @param {BaseProvider} provider
188
+ */
189
+ export function registerProvider(provider) {
190
+ if (!(provider instanceof BaseProvider)) {
191
+ throw new TypeError("provider must be an instance of BaseProvider");
192
+ }
193
+ const key = getFrameworkKey(provider.name, provider.variant);
194
+ providerRegistry.set(key, provider);
195
+ }
196
+
197
+ /**
198
+ * Retrieve registered provider by framework name and variant.
199
+ * @param {string} name
200
+ * @param {string} variant
201
+ * @returns {BaseProvider | null}
202
+ */
203
+ export function getProvider(name, variant) {
204
+ const key = getFrameworkKey(name, variant);
205
+ return providerRegistry.get(key) || null;
206
+ }
207
+
208
+ /**
209
+ * List all registered providers.
210
+ * @returns {BaseProvider[]}
211
+ */
212
+ export function listProviders() {
213
+ return Array.from(providerRegistry.values());
214
+ }
215
+
216
+ // Auto-register default providers
217
+ registerProvider(new ViteReactProvider());
218
+ registerProvider(new NextAppRouterProvider());
219
+ registerProvider(new NextPagesRouterProvider());
package/src/injector.js CHANGED
@@ -118,12 +118,12 @@ export async function injectFormatter(projectPath, templatesDir, responses) {
118
118
 
119
119
  // Wire eslint-config-prettier only for the eslint + prettier combination
120
120
  if (responses.linter === "eslint" && responses.formatter === "prettier") {
121
- await wireEslintPrettier(projectPath, responses.language);
121
+ await wireEslintPrettier(projectPath, responses.language, responses.framework);
122
122
  }
123
123
  }
124
124
 
125
- async function wireEslintPrettier(projectPath, language) {
126
- const ext = language === "ts" ? "ts" : "js";
125
+ async function wireEslintPrettier(projectPath, language, framework) {
126
+ const ext = framework === "next" ? "mjs" : language === "ts" ? "ts" : "js";
127
127
  const eslintConfigPath = path.join(projectPath, `eslint.config.${ext}`);
128
128
  try {
129
129
  let content = await fsp.readFile(eslintConfigPath, "utf8");
@@ -158,6 +158,11 @@ async function wireEslintPrettier(projectPath, language) {
158
158
  /(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
159
159
  pushLast
160
160
  );
161
+ } else if (result.includes("const eslintConfig = [")) {
162
+ result = result.replace(
163
+ /(const eslintConfig = \[[\s\S]*?)(\n\s*\];)/,
164
+ pushLast
165
+ );
161
166
  } else {
162
167
  result = result.replace(
163
168
  /(export default \[[\s\S]*?)(\n\s*\];)/,
@@ -238,9 +243,9 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
238
243
 
239
244
  // 6. Linter
240
245
  if (responses.linter === "eslint") {
241
- await injectLinter(projectPath, templatesDir, "eslint", language);
246
+ await injectLinter(projectPath, templatesDir, "eslint", language, responses.framework);
242
247
  } else if (responses.linter === "oxlint") {
243
- await injectLinter(projectPath, templatesDir, "oxlint", language);
248
+ await injectLinter(projectPath, templatesDir, "oxlint", language, responses.framework);
244
249
  }
245
250
 
246
251
  // 7. Create feature script (feature-based only)
@@ -254,7 +259,7 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
254
259
  }
255
260
 
256
261
  // Remove .gitkeep placeholders from src/ directories that now contain real
257
- // files (e.g. component-based `services/.gitkeep` after the axios/fetch client
262
+ // files (e.g. type-based `services/.gitkeep` after the axios/fetch client
258
263
  // overlay lands). A folder with nothing but its placeholder keeps it.
259
264
  async function pruneRedundantGitkeeps(projectPath) {
260
265
  const walk = async (dir) => {
@@ -378,16 +383,24 @@ async function injectTesting(projectPath, templatesDir, framework, language, arc
378
383
  } catch {}
379
384
  }
380
385
 
381
- async function injectLinter(projectPath, templatesDir, linter, language) {
386
+ export async function injectLinter(projectPath, templatesDir, linter, language, framework) {
382
387
  const lintingDir = path.join(templatesDir, "conditional", "linting", linter);
383
388
 
384
389
  if (linter === "eslint") {
385
- const configExt = language === "ts" ? "ts" : "js";
386
- const configSrc = path.join(lintingDir, `eslint.config.${configExt}`);
387
- try {
388
- await fsp.access(configSrc);
389
- await fsp.copyFile(configSrc, path.join(projectPath, `eslint.config.${configExt}`));
390
- } catch {}
390
+ if (framework === "next") {
391
+ const configSrc = path.join(lintingDir, "next", "eslint.config.mjs");
392
+ try {
393
+ await fsp.access(configSrc);
394
+ await fsp.copyFile(configSrc, path.join(projectPath, "eslint.config.mjs"));
395
+ } catch {}
396
+ } else {
397
+ const configExt = language === "ts" ? "ts" : "js";
398
+ const configSrc = path.join(lintingDir, `eslint.config.${configExt}`);
399
+ try {
400
+ await fsp.access(configSrc);
401
+ await fsp.copyFile(configSrc, path.join(projectPath, `eslint.config.${configExt}`));
402
+ } catch {}
403
+ }
391
404
  } else if (linter === "oxlint") {
392
405
  const configSrc = path.join(lintingDir, "oxlintrc.json");
393
406
  try {
package/src/main.js CHANGED
@@ -16,21 +16,24 @@ import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
16
16
  import { installAllDeps } from "@/dependencies.js";
17
17
  import { generateReadme } from "@/readme.js";
18
18
  import { runProjectFormat } from "@/format.js";
19
+ import { emitManifest } from "@/manifest/emit.js";
20
+
21
+ import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
19
22
 
20
23
  const __filename = fileURLToPath(import.meta.url);
21
24
  const __dirname = path.dirname(__filename);
22
25
  const TEMPLATES_DIR = path.join(__dirname, "../templates");
23
26
  const CURRENT_DIR = process.cwd();
24
27
 
25
- export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR } = {}) {
26
- const cliArgs = (process.argv.slice(2) ?? []).filter((arg) => arg && !arg.startsWith("-"));
27
- const cliArg = nameArg || cliArgs[0] || null;
28
+ export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR, rawArgs } = {}) {
29
+ const cli = parseCliArgs(rawArgs || process.argv.slice(2));
30
+ const cliArg = nameArg || cli.projectName || null;
28
31
 
29
32
  if (cliArg && cliArg.trim()) {
30
33
  return cliArg.trim();
31
34
  }
32
35
 
33
- if (quickSetup) {
36
+ if (quickSetup || cli.quickSetup || cli.manifest || cli.template) {
34
37
  const defaultName = path.basename(cwd).trim() || "my-app";
35
38
  return defaultName === "." ? "my-app" : defaultName;
36
39
  }
@@ -39,11 +42,22 @@ export function resolveProjectName({ quickSetup = false, projectName: nameArg, c
39
42
  }
40
43
 
41
44
  export async function main(options = {}) {
42
- const { quickSetup = false, projectName: nameArg } = options;
45
+ const {
46
+ quickSetup = false,
47
+ projectName: nameArg,
48
+ manifest: manifestSource,
49
+ template: templateName,
50
+ } = options;
43
51
  const pkg = getPkgManager();
44
52
 
53
+ const isNonInteractive = Boolean(quickSetup || manifestSource || templateName);
54
+
45
55
  // 1. Project name
46
- let projectName = resolveProjectName({ quickSetup, projectName: nameArg, cwd: CURRENT_DIR });
56
+ let projectName = resolveProjectName({
57
+ quickSetup: isNonInteractive,
58
+ projectName: nameArg,
59
+ cwd: CURRENT_DIR,
60
+ });
47
61
 
48
62
  if (projectName) {
49
63
  log.step(chalk.gray(`Project name: ${chalk.bold(projectName)}`));
@@ -61,8 +75,25 @@ export async function main(options = {}) {
61
75
  // 2. Handle existing folder
62
76
  await confirmEmptyFolder(projectName);
63
77
 
64
- // 3. Collect user preferences
65
- const responses = await getUserInputs(projectName, { quickSetup });
78
+ // 3. Collect user preferences (interactive or non-interactive)
79
+ let responses;
80
+ if (manifestSource) {
81
+ const manifestObj = loadManifestSource(manifestSource, CURRENT_DIR);
82
+ responses = manifestToResponses(manifestObj, projectName);
83
+ } else if (templateName) {
84
+ const preset = PRESETS[templateName];
85
+ if (!preset) {
86
+ throw new Error(
87
+ `Unknown template preset "${templateName}". Available presets: ${Object.keys(PRESETS).join(", ")}`
88
+ );
89
+ }
90
+ responses = {
91
+ projectName,
92
+ ...preset,
93
+ };
94
+ } else {
95
+ responses = await getUserInputs(projectName, { quickSetup });
96
+ }
66
97
 
67
98
  const projectPath = path.resolve(CURRENT_DIR, projectName);
68
99
 
@@ -104,7 +135,7 @@ export async function main(options = {}) {
104
135
  // 7. Inject architecture templates
105
136
  const archSpin = spinner();
106
137
  archSpin.start(
107
- `Setting up ${responses.architecture === "feature-based" ? "feature-based" : "component-based"} architecture...`
138
+ `Setting up ${responses.architecture === "feature-based" ? "feature-based" : "type-based"} architecture...`
108
139
  );
109
140
  try {
110
141
  await injectArchitecture(
@@ -205,6 +236,17 @@ export async function main(options = {}) {
205
236
  // 12.5 Copy .env.example into the project
206
237
  await copyEnvExample(projectPath, TEMPLATES_DIR);
207
238
 
239
+ // 12.6 Emit lumen.config.json (manifest v2) — byte-deterministic, Zod-validated
240
+ const manifestSpin = spinner();
241
+ manifestSpin.start("Writing lumen.config.json...");
242
+ try {
243
+ await emitManifest(projectPath, responses);
244
+ manifestSpin.stop(chalk.green("lumen.config.json written."));
245
+ } catch (err) {
246
+ manifestSpin.stop(chalk.red("Failed to write lumen.config.json."));
247
+ throw err;
248
+ }
249
+
208
250
  // 13. Generate README.md + LICENSE (if requested)
209
251
  if (responses.readme) {
210
252
  const readmeSpin = spinner();
@@ -234,7 +276,7 @@ export async function main(options = {}) {
234
276
  log.step(chalk.green("\nProject setup complete!"));
235
277
  log.message(
236
278
  chalk.gray(
237
- ` Architecture: ${responses.architecture === "feature-based" ? "Feature-based" : "Component-based"}`
279
+ ` Architecture: ${responses.architecture === "feature-based" ? "Feature-based" : "Type-based"}`
238
280
  )
239
281
  );
240
282
 
@@ -0,0 +1,120 @@
1
+ import { promises as fsp } from "fs";
2
+ import path from "path";
3
+ import { parseManifest, SCHEMA_URL, MANIFEST_VERSION } from "./schema.js";
4
+ import { resolvePaths } from "./paths.js";
5
+
6
+ /**
7
+ * Build an ordered manifest v2 object from scaffolder responses.
8
+ * Stable key ordering for byte-determinism.
9
+ * @param {object} responses - result from getUserInputs (includes docsLanguage, cssFramework, etc.)
10
+ * @param {object} [opts]
11
+ * @param {string} [opts.frameworkName] - default "react"
12
+ * @param {string} [opts.frameworkVariant] - default "vite" or derived from framework
13
+ * @returns {object} ordered manifest object ready to JSON.stringify
14
+ */
15
+ export function buildManifest(responses, opts = {}) {
16
+ if (responses.rawManifest) {
17
+ return parseManifest(responses.rawManifest);
18
+ }
19
+
20
+ const frameworkName = opts.frameworkName || responses.frameworkName || "react";
21
+ const frameworkVariant =
22
+ opts.frameworkVariant ||
23
+ responses.frameworkVariant ||
24
+ (frameworkName === "next" ? "app-router" : "vite");
25
+
26
+ const framework = {
27
+ name: frameworkName,
28
+ variant: frameworkVariant,
29
+ };
30
+ // Include optional axes only if present in responses (future overlays)
31
+ if (responses.bundler) framework.bundler = responses.bundler;
32
+ if (responses.adapter) framework.adapter = responses.adapter;
33
+
34
+ const styling = {
35
+ engine: responses.cssFramework || "tailwind",
36
+ };
37
+
38
+ const rawArch = responses.architecture || "feature-based";
39
+ const architecture = {
40
+ type: rawArch === "component-based" ? "type-based" : rawArch,
41
+ };
42
+
43
+ const ui = {
44
+ kit: responses.uiKit || responses.ui || "none",
45
+ };
46
+
47
+ const docs = {
48
+ language: responses.docsLanguage || "en",
49
+ };
50
+
51
+ const paths = resolvePaths({
52
+ architecture: architecture.type,
53
+ framework,
54
+ });
55
+
56
+ const tooling = {
57
+ language: responses.language || "ts",
58
+ linter: responses.linter || "none",
59
+ formatter: responses.formatter || "none",
60
+ };
61
+
62
+ const manifest = {
63
+ $schema: SCHEMA_URL,
64
+ manifestVersion: MANIFEST_VERSION,
65
+ framework,
66
+ styling,
67
+ architecture,
68
+ ui,
69
+ docs,
70
+ paths,
71
+ tooling,
72
+ };
73
+
74
+ // Optional cross-cutting flags — only include if explicitly set (keeps minimal manifest byte-stable)
75
+ if (typeof responses.reactCompiler === "boolean") manifest.reactCompiler = responses.reactCompiler;
76
+ if (typeof responses.agentDocs === "boolean") manifest.agentDocs = responses.agentDocs;
77
+
78
+ // Enforce stable top-level key order for determinism (like formatter pass)
79
+ const ordered = {};
80
+ const topOrder = [
81
+ "$schema",
82
+ "manifestVersion",
83
+ "framework",
84
+ "styling",
85
+ "architecture",
86
+ "ui",
87
+ "docs",
88
+ "paths",
89
+ "tooling",
90
+ "reactCompiler",
91
+ "agentDocs",
92
+ ];
93
+ for (const k of topOrder) {
94
+ if (k in manifest) ordered[k] = manifest[k];
95
+ }
96
+ // Include any extra keys not in order (should be none, but for forward compat)
97
+ for (const k of Object.keys(manifest)) {
98
+ if (!(k in ordered)) ordered[k] = manifest[k];
99
+ }
100
+
101
+ // Validate via Zod before returning (throws with user-friendly message on invalid)
102
+ // Use parse to apply defaults ($schema) and ensure strictness
103
+ return parseManifest(ordered);
104
+ }
105
+
106
+ /**
107
+ * Emit lumen.config.json to projectPath.
108
+ * Validates via Zod, writes with 2-space indent + trailing newline.
109
+ * @param {string} projectPath - absolute path to scaffolded project
110
+ * @param {object} responses
111
+ * @param {object} [opts] - same as buildManifest opts
112
+ * @returns {Promise<string>} absolute path to written file
113
+ */
114
+ export async function emitManifest(projectPath, responses, opts = {}) {
115
+ const manifest = responses._manifestSource || buildManifest(responses, opts);
116
+ const outPath = path.join(projectPath, "lumen.config.json");
117
+ const content = JSON.stringify(manifest, null, 2) + "\n";
118
+ await fsp.writeFile(outPath, content, "utf8");
119
+ return outPath;
120
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ manifestSchemaV2,
3
+ parseManifest,
4
+ isV1Manifest,
5
+ formatZodError,
6
+ MANIFEST_VERSION,
7
+ SCHEMA_URL,
8
+ } from "./schema.js";
9
+
10
+ export { resolvePaths, PATH_KEYS } from "./paths.js";
11
+ export { buildManifest, emitManifest } from "./emit.js";
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Path mapping for lumen.config.json — #15
3
+ * Resolves the 6 generator target directories from framework + architecture.
4
+ * Paths are relative to project root and use posix separators.
5
+ */
6
+
7
+ export const PATH_KEYS = ["features", "components", "services", "hooks", "pages", "ui"];
8
+
9
+ /**
10
+ * @param {{ architecture: string, framework?: { name?: string, variant?: string } }} opts
11
+ * @returns {{ features: string, components: string, services: string, hooks: string, pages: string, ui: string }}
12
+ */
13
+ export function resolvePaths({ architecture, framework = {} } = {}) {
14
+ const arch = architecture || "feature-based";
15
+ const fwName = framework.name || "react";
16
+ const variant = framework.variant || (fwName === "next" ? "app-router" : "vite");
17
+
18
+ // Helper to pick Next.js pages root based on variant
19
+ const nextPagesForVariant = (v) => (v === "pages-router" ? "pages" : "app");
20
+ const isNext = fwName === "next";
21
+
22
+ // -- feature-based (React + Vite canonical, also Next feature-based)
23
+ if (arch === "feature-based") {
24
+ if (isNext) {
25
+ const nextRoot = nextPagesForVariant(variant);
26
+ // Next keeps features in src/features, shared code in src/shared, routing in app/ or pages/
27
+ // Use posix paths regardless of OS
28
+ return {
29
+ features: "src/features",
30
+ components: "src/shared/components",
31
+ services: "src/shared/services",
32
+ hooks: "src/shared/hooks",
33
+ pages: nextRoot, // app/ or pages/ at project root for Next
34
+ ui: "src/shared/components/ui",
35
+ };
36
+ }
37
+ return {
38
+ features: "src/features",
39
+ components: "src/shared/components",
40
+ services: "src/shared/services",
41
+ hooks: "src/shared/hooks",
42
+ pages: "src/app/router",
43
+ ui: "src/shared/components/ui",
44
+ };
45
+ }
46
+
47
+ // -- type-based (React + Vite only; component-based supported as legacy alias)
48
+ if (arch === "type-based" || arch === "component-based") {
49
+ if (isNext) {
50
+ // Not a valid combo per schema, but provide a deterministic fallback
51
+ // to keep resolver total (hybrid should be used for Next)
52
+ const nextRoot = nextPagesForVariant(variant);
53
+ return {
54
+ features: "src/components",
55
+ components: "src/components",
56
+ services: "src/services",
57
+ hooks: "src/hooks",
58
+ pages: nextRoot,
59
+ ui: "src/components/ui",
60
+ };
61
+ }
62
+ return {
63
+ features: "src/features",
64
+ components: "src/components",
65
+ services: "src/services",
66
+ hooks: "src/hooks",
67
+ pages: "src/pages",
68
+ ui: "src/ui",
69
+ };
70
+ }
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
+ // -- none (minimal flat src/ — no imposed convention)
97
+ if (arch === "none") {
98
+ if (isNext) {
99
+ const nextRoot = nextPagesForVariant(variant);
100
+ return {
101
+ features: "src",
102
+ components: "src/components",
103
+ services: "src/services",
104
+ hooks: "src/hooks",
105
+ pages: nextRoot,
106
+ ui: "src/components/ui",
107
+ };
108
+ }
109
+ return {
110
+ features: "src",
111
+ components: "src/components",
112
+ services: "src/services",
113
+ hooks: "src/hooks",
114
+ pages: "src/pages",
115
+ ui: "src/components/ui",
116
+ };
117
+ }
118
+
119
+ // Fallback: treat unknown as feature-based
120
+ return {
121
+ features: "src/features",
122
+ components: "src/shared/components",
123
+ services: "src/shared/services",
124
+ hooks: "src/shared/hooks",
125
+ pages: isNext ? nextPagesForVariant(variant) : "src/app/router",
126
+ ui: "src/shared/components/ui",
127
+ };
128
+ }