create-lumen 2.0.0-alpha.1 → 2.0.0-alpha.2
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 +15 -7
- package/bin/cli.js +1 -0
- package/package.json +2 -2
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
- package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
- package/schema/lumen.config.v2.json +0 -1
- package/src/cli-args.js +65 -3
- package/src/configure.js +4 -0
- package/src/css.js +14 -30
- package/src/dependencies.js +28 -18
- package/src/engine/capabilities.js +174 -4
- package/src/engine/composition.js +644 -0
- package/src/engine/index.js +1 -0
- package/src/injector.js +294 -446
- package/src/main.js +6 -7
- package/src/manifest/schema.js +1 -1
- package/src/prompts.js +459 -247
- package/src/readme.js +23 -2
- package/src/scaffold.js +17 -0
- package/templates/architectures/hybrid/main.jsx +10 -0
- package/templates/architectures/hybrid/main.tsx +10 -0
- package/templates/architectures/hybrid/src/App.jsx +20 -0
- package/templates/architectures/hybrid/src/App.tsx +20 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
- 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 +20 -0
- package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
- package/templates/architectures/none/main.jsx +10 -0
- package/templates/architectures/none/main.tsx +10 -0
- package/templates/architectures/none/src/App.jsx +15 -0
- package/templates/architectures/none/src/App.tsx +15 -0
- package/templates/architectures/none/src/styles/.gitkeep +1 -0
- package/templates/architectures/none/src/types/index.js +20 -0
- package/templates/architectures/none/src/types/index.ts +16 -0
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
- package/templates/css/tailwind/src/globals.css +10 -0
- package/templates/css/tailwind/src/themes.css +86 -4
- package/templates/ui/shadcn/lib/utils.js +6 -0
- package/templates/ui/shadcn/lib/utils.ts +6 -0
- package/templates/ui/shadcn/primitives/Button.jsx +48 -0
- package/templates/ui/shadcn/primitives/Button.tsx +59 -0
- package/templates/ui/shadcn/primitives/Card.jsx +55 -0
- package/templates/ui/shadcn/primitives/Card.tsx +73 -0
- package/templates/ui/shadcn/primitives/index.js +9 -0
- package/templates/ui/shadcn/primitives/index.ts +9 -0
- package/templates/css/bootstrap/src/themes.css +0 -14
- /package/templates/{css/bootstrap/src/globals.css → architectures/hybrid/src/features/.gitkeep} +0 -0
package/src/injector.js
CHANGED
|
@@ -1,492 +1,340 @@
|
|
|
1
1
|
import { promises as fsp } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"architectures",
|
|
44
|
-
architecture,
|
|
45
|
-
`main.${ext}`
|
|
3
|
+
import {
|
|
4
|
+
FRAGMENT_SCOPES,
|
|
5
|
+
resolveFragments,
|
|
6
|
+
applyFragments,
|
|
7
|
+
langFileFilter,
|
|
8
|
+
pruneRedundantGitkeeps,
|
|
9
|
+
} from "./engine/composition.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
FRAGMENT_SCOPES,
|
|
13
|
+
resolveFragments,
|
|
14
|
+
applyFragments,
|
|
15
|
+
langFileFilter,
|
|
16
|
+
pruneRedundantGitkeeps,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inject architecture templates into projectPath from composed fragments.
|
|
21
|
+
* Composes framework entry points, architecture layout, and styling component overlays.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} projectPath
|
|
24
|
+
* @param {string} templatesDir
|
|
25
|
+
* @param {string} architecture
|
|
26
|
+
* @param {string} language
|
|
27
|
+
* @param {string} cssFramework
|
|
28
|
+
*/
|
|
29
|
+
export async function injectArchitecture(
|
|
30
|
+
projectPath,
|
|
31
|
+
templatesDir,
|
|
32
|
+
architecture,
|
|
33
|
+
language,
|
|
34
|
+
cssFramework
|
|
35
|
+
) {
|
|
36
|
+
const responses = { architecture, language, cssFramework };
|
|
37
|
+
const allFragments = resolveFragments(responses, templatesDir);
|
|
38
|
+
const archFragments = allFragments.filter(
|
|
39
|
+
(f) =>
|
|
40
|
+
f.scope === FRAGMENT_SCOPES.FRAMEWORK ||
|
|
41
|
+
f.scope === FRAGMENT_SCOPES.ARCH ||
|
|
42
|
+
f.scope === FRAGMENT_SCOPES.STYLING
|
|
46
43
|
);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Remove the opposite language's main file (Vite creates main.jsx by default)
|
|
55
|
-
const oppositeExt = language === "ts" ? "jsx" : "tsx";
|
|
56
|
-
const oppositeMain = path.join(projectPath, "src", `main.${oppositeExt}`);
|
|
57
|
-
try { await fsp.rm(oppositeMain, { force: true }); } catch {}
|
|
58
|
-
|
|
59
|
-
// Apply the chosen CSS framework's component styling on top of the base
|
|
60
|
-
// (only where the base ships framework-agnostic inline markup). Runs here,
|
|
61
|
-
// before conditional overlays, so icons/router can still override the files
|
|
62
|
-
// they own.
|
|
63
|
-
if (cssFramework === "tailwind" || cssFramework === "bootstrap") {
|
|
64
|
-
const variantSrc = path.join(
|
|
65
|
-
templatesDir,
|
|
66
|
-
"css",
|
|
67
|
-
"component-styles",
|
|
68
|
-
cssFramework,
|
|
69
|
-
architecture,
|
|
70
|
-
"src"
|
|
71
|
-
);
|
|
72
|
-
try {
|
|
73
|
-
await fsp.access(variantSrc);
|
|
74
|
-
await copyDirRecursive(
|
|
75
|
-
variantSrc,
|
|
76
|
-
path.join(projectPath, "src"),
|
|
77
|
-
langFileFilter(language)
|
|
78
|
-
);
|
|
79
|
-
} catch {
|
|
80
|
-
// no component-styles for this arch/framework
|
|
81
|
-
}
|
|
82
|
-
}
|
|
44
|
+
await applyFragments(projectPath, archFragments, {
|
|
45
|
+
language,
|
|
46
|
+
templatesDir,
|
|
47
|
+
responses,
|
|
48
|
+
});
|
|
83
49
|
}
|
|
84
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Inject code formatter configuration and wiring from composed fragments.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectPath
|
|
55
|
+
* @param {string} templatesDir
|
|
56
|
+
* @param {object} responses
|
|
57
|
+
*/
|
|
85
58
|
export async function injectFormatter(projectPath, templatesDir, responses) {
|
|
86
59
|
if (!responses.formatter || responses.formatter === "none") return;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
responses.formatter === "prettier" ? ".prettierrc" : ".oxfmtrc.json";
|
|
91
|
-
const configSrc = path.join(
|
|
92
|
-
templatesDir,
|
|
93
|
-
"conditional",
|
|
94
|
-
"formatter",
|
|
95
|
-
responses.formatter,
|
|
96
|
-
configFile
|
|
60
|
+
const allFragments = resolveFragments(responses, templatesDir);
|
|
61
|
+
const formatterFragments = allFragments.filter(
|
|
62
|
+
(f) => f.scope === FRAGMENT_SCOPES.TOOLING && f.category === "formatter"
|
|
97
63
|
);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Append format script(s) to package.json
|
|
104
|
-
const pkgPath = path.join(projectPath, "package.json");
|
|
105
|
-
try {
|
|
106
|
-
const content = await fsp.readFile(pkgPath, "utf8");
|
|
107
|
-
const pkg = JSON.parse(content);
|
|
108
|
-
if (!pkg.scripts) pkg.scripts = {};
|
|
109
|
-
if (responses.formatter === "prettier") {
|
|
110
|
-
pkg.scripts.format = "prettier --write .";
|
|
111
|
-
pkg.scripts["format:check"] = "prettier --check .";
|
|
112
|
-
} else if (responses.formatter === "oxfmt") {
|
|
113
|
-
pkg.scripts.format = "oxfmt .";
|
|
114
|
-
pkg.scripts["format:check"] = "oxfmt --check .";
|
|
115
|
-
}
|
|
116
|
-
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
117
|
-
} catch {}
|
|
118
|
-
|
|
119
|
-
// Wire eslint-config-prettier only for the eslint + prettier combination
|
|
120
|
-
if (responses.linter === "eslint" && responses.formatter === "prettier") {
|
|
121
|
-
await wireEslintPrettier(projectPath, responses.language, responses.framework);
|
|
122
|
-
}
|
|
64
|
+
await applyFragments(projectPath, formatterFragments, {
|
|
65
|
+
language: responses.language,
|
|
66
|
+
templatesDir,
|
|
67
|
+
responses,
|
|
68
|
+
});
|
|
123
69
|
}
|
|
124
70
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Inject conditional tooling features (state, router, icons, api client, testing, linter, scripts)
|
|
73
|
+
* using the composed fragment pipeline.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} projectPath
|
|
76
|
+
* @param {string} templatesDir
|
|
77
|
+
* @param {object} responses
|
|
78
|
+
* @param {string} architecture
|
|
79
|
+
* @param {string} language
|
|
80
|
+
*/
|
|
81
|
+
export async function injectConditionals(
|
|
82
|
+
projectPath,
|
|
83
|
+
templatesDir,
|
|
84
|
+
responses,
|
|
85
|
+
architecture,
|
|
86
|
+
language
|
|
87
|
+
) {
|
|
88
|
+
const mergedResponses = { ...responses, architecture, language };
|
|
89
|
+
const allFragments = resolveFragments(mergedResponses, templatesDir);
|
|
90
|
+
const conditionalFragments = allFragments.filter(
|
|
91
|
+
(f) => f.scope === FRAGMENT_SCOPES.TOOLING && f.category !== "formatter"
|
|
92
|
+
);
|
|
93
|
+
await applyFragments(projectPath, conditionalFragments, {
|
|
94
|
+
language,
|
|
95
|
+
templatesDir,
|
|
96
|
+
responses: mergedResponses,
|
|
97
|
+
});
|
|
147
98
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// The JS template exports an array (`export default [...]`); the TS
|
|
153
|
-
// template uses a function call (`export default tseslint.config(...)`).
|
|
154
|
-
const pushLast = (_, head, tail) =>
|
|
155
|
-
head.replace(/,\s*$/, "") + ",\n prettier," + tail;
|
|
156
|
-
if (result.includes("tseslint.config(")) {
|
|
157
|
-
result = result.replace(
|
|
158
|
-
/(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
|
|
159
|
-
pushLast
|
|
160
|
-
);
|
|
161
|
-
} else if (result.includes("const eslintConfig = [")) {
|
|
162
|
-
result = result.replace(
|
|
163
|
-
/(const eslintConfig = \[[\s\S]*?)(\n\s*\];)/,
|
|
164
|
-
pushLast
|
|
165
|
-
);
|
|
166
|
-
} else {
|
|
167
|
-
result = result.replace(
|
|
168
|
-
/(export default \[[\s\S]*?)(\n\s*\];)/,
|
|
169
|
-
pushLast
|
|
170
|
-
);
|
|
171
|
-
}
|
|
99
|
+
// 8. UI Kit (shadcn gated to tailwind) — applied after fragments so its
|
|
100
|
+
// components.json, cn helper and primitives land on the composed tree.
|
|
101
|
+
await injectUiKit(projectPath, templatesDir, mergedResponses, architecture, language);
|
|
102
|
+
}
|
|
172
103
|
|
|
173
|
-
|
|
174
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Inject linter configuration and package scripts using the composed fragment pipeline.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} projectPath
|
|
108
|
+
* @param {string} templatesDir
|
|
109
|
+
* @param {string} linter
|
|
110
|
+
* @param {string} language
|
|
111
|
+
* @param {string|object} framework
|
|
112
|
+
*/
|
|
113
|
+
export async function injectLinter(
|
|
114
|
+
projectPath,
|
|
115
|
+
templatesDir,
|
|
116
|
+
linter,
|
|
117
|
+
language,
|
|
118
|
+
framework
|
|
119
|
+
) {
|
|
120
|
+
const responses = { linter, language, framework };
|
|
121
|
+
const allFragments = resolveFragments(responses, templatesDir);
|
|
122
|
+
const linterFragments = allFragments.filter(
|
|
123
|
+
(f) => f.scope === FRAGMENT_SCOPES.TOOLING && f.category === "linter"
|
|
124
|
+
);
|
|
125
|
+
await applyFragments(projectPath, linterFragments, {
|
|
126
|
+
language,
|
|
127
|
+
templatesDir,
|
|
128
|
+
responses,
|
|
129
|
+
});
|
|
175
130
|
}
|
|
176
131
|
|
|
177
|
-
|
|
178
|
-
|
|
132
|
+
/**
|
|
133
|
+
* High-level composition API: inject all capability fragments in documented precedence order.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} projectPath
|
|
136
|
+
* @param {string} templatesDir
|
|
137
|
+
* @param {object} responses
|
|
138
|
+
*/
|
|
139
|
+
export async function injectComposedFragments(projectPath, templatesDir, responses) {
|
|
140
|
+
const fragments = resolveFragments(responses, templatesDir);
|
|
141
|
+
await applyFragments(projectPath, fragments, {
|
|
142
|
+
language: responses.language,
|
|
143
|
+
templatesDir,
|
|
144
|
+
responses,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
179
147
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
148
|
+
export function getComponentsJson({
|
|
149
|
+
framework = "vite",
|
|
150
|
+
architecture = "feature-based",
|
|
151
|
+
language = "ts",
|
|
152
|
+
}) {
|
|
153
|
+
const isNext = framework === "next";
|
|
154
|
+
const isTs = language === "ts";
|
|
155
|
+
|
|
156
|
+
if (isNext) {
|
|
157
|
+
// Next App Router contract stub
|
|
158
|
+
return {
|
|
159
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
160
|
+
style: "new-york",
|
|
161
|
+
rsc: true,
|
|
162
|
+
tsx: isTs,
|
|
163
|
+
tailwind: {
|
|
164
|
+
config: "",
|
|
165
|
+
css: "app/globals.css",
|
|
166
|
+
baseColor: "neutral",
|
|
167
|
+
cssVariables: true,
|
|
168
|
+
prefix: "",
|
|
169
|
+
},
|
|
170
|
+
aliases: {
|
|
171
|
+
components: "@/components",
|
|
172
|
+
utils: "@/lib/utils",
|
|
173
|
+
ui: "@/components/ui",
|
|
174
|
+
lib: "@/lib",
|
|
175
|
+
hooks: "@/hooks",
|
|
176
|
+
},
|
|
177
|
+
iconLibrary: "lucide",
|
|
178
|
+
};
|
|
196
179
|
}
|
|
197
180
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
181
|
+
const isFeatureBased = architecture === "feature-based";
|
|
182
|
+
return {
|
|
183
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
184
|
+
style: "new-york",
|
|
185
|
+
rsc: false,
|
|
186
|
+
tsx: isTs,
|
|
187
|
+
tailwind: {
|
|
188
|
+
config: "",
|
|
189
|
+
css: isFeatureBased
|
|
190
|
+
? "src/shared/styles/globals.css"
|
|
191
|
+
: "src/styles/globals.css",
|
|
192
|
+
baseColor: "neutral",
|
|
193
|
+
cssVariables: true,
|
|
194
|
+
prefix: "",
|
|
195
|
+
},
|
|
196
|
+
aliases: isFeatureBased
|
|
197
|
+
? {
|
|
198
|
+
components: "@/shared/components",
|
|
199
|
+
utils: "@/lib/utils",
|
|
200
|
+
ui: "@/shared/components/ui",
|
|
201
|
+
lib: "@/lib",
|
|
202
|
+
hooks: "@/shared/hooks",
|
|
203
|
+
}
|
|
204
|
+
: {
|
|
205
|
+
components: "@/components",
|
|
206
|
+
utils: "@/lib/utils",
|
|
207
|
+
ui: "@/ui",
|
|
208
|
+
lib: "@/lib",
|
|
209
|
+
hooks: "@/hooks",
|
|
210
|
+
},
|
|
211
|
+
iconLibrary: "lucide",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
208
214
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
export async function injectUiKit(
|
|
216
|
+
projectPath,
|
|
217
|
+
templatesDir,
|
|
218
|
+
responses,
|
|
219
|
+
architecture,
|
|
220
|
+
language
|
|
221
|
+
) {
|
|
222
|
+
const uiKit = responses.uiKit || responses.ui?.kit || responses.ui || "none";
|
|
223
|
+
if (uiKit !== "shadcn") return;
|
|
224
|
+
|
|
225
|
+
if (responses.cssFramework !== "tailwind") {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`ui.kit "shadcn" requires styling.engine "tailwind" (got "${responses.cssFramework}")`
|
|
217
228
|
);
|
|
218
229
|
}
|
|
219
230
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
templatesDir,
|
|
225
|
-
`conditional/axios/${architecture}`,
|
|
226
|
-
language
|
|
227
|
-
);
|
|
228
|
-
} else if (responses.apiClient === "fetch") {
|
|
229
|
-
await injectOverlay(
|
|
230
|
-
projectPath,
|
|
231
|
-
templatesDir,
|
|
232
|
-
`conditional/fetch/${architecture}`,
|
|
233
|
-
language
|
|
234
|
-
);
|
|
235
|
-
}
|
|
231
|
+
const isTs = language === "ts";
|
|
232
|
+
const codeExt = isTs ? "tsx" : "jsx";
|
|
233
|
+
const utilExt = isTs ? "ts" : "js";
|
|
234
|
+
const framework = responses.framework || responses.frameworkName || "vite";
|
|
236
235
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
236
|
+
// 1. Write components.json
|
|
237
|
+
const componentsJson = getComponentsJson({
|
|
238
|
+
framework,
|
|
239
|
+
architecture,
|
|
240
|
+
language,
|
|
241
|
+
});
|
|
242
|
+
await fsp.writeFile(
|
|
243
|
+
path.join(projectPath, "components.json"),
|
|
244
|
+
JSON.stringify(componentsJson, null, 2) + "\n",
|
|
245
|
+
"utf8"
|
|
246
|
+
);
|
|
243
247
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
// 2. Write cn helper to src/lib/utils.{ts,js}
|
|
249
|
+
const libDir = path.join(projectPath, "src", "lib");
|
|
250
|
+
await fsp.mkdir(libDir, { recursive: true });
|
|
251
|
+
const utilsSrc = path.join(
|
|
252
|
+
templatesDir,
|
|
253
|
+
"ui",
|
|
254
|
+
"shadcn",
|
|
255
|
+
"lib",
|
|
256
|
+
`utils.${utilExt}`
|
|
257
|
+
);
|
|
258
|
+
const utilsDest = path.join(libDir, `utils.${utilExt}`);
|
|
259
|
+
try {
|
|
260
|
+
await fsp.access(utilsSrc);
|
|
261
|
+
await fsp.copyFile(utilsSrc, utilsDest);
|
|
262
|
+
} catch {
|
|
263
|
+
const code = isTs
|
|
264
|
+
? `import { clsx, type ClassValue } from "clsx";\nimport { twMerge } from "tailwind-merge";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`
|
|
265
|
+
: `import { clsx } from "clsx";\nimport { twMerge } from "tailwind-merge";\n\nexport function cn(...inputs) {\n return twMerge(clsx(inputs));\n}\n`;
|
|
266
|
+
await fsp.writeFile(utilsDest, code, "utf8");
|
|
249
267
|
}
|
|
250
268
|
|
|
251
|
-
//
|
|
269
|
+
// Also sync architecture's cn helper if present
|
|
252
270
|
if (architecture === "feature-based") {
|
|
253
|
-
|
|
271
|
+
const sharedUtilsDir = path.join(projectPath, "src", "shared", "utils");
|
|
272
|
+
await fsp.mkdir(sharedUtilsDir, { recursive: true });
|
|
273
|
+
await fsp.copyFile(utilsDest, path.join(sharedUtilsDir, `cn.${codeExt === "tsx" ? "ts" : "jsx"}`));
|
|
274
|
+
} else {
|
|
275
|
+
const typeUtilsDir = path.join(projectPath, "src", "utils");
|
|
276
|
+
await fsp.mkdir(typeUtilsDir, { recursive: true });
|
|
277
|
+
await fsp.copyFile(utilsDest, path.join(typeUtilsDir, `cn.${codeExt === "tsx" ? "ts" : "jsx"}`));
|
|
254
278
|
}
|
|
255
279
|
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
// 3. Inject Button and Card primitives
|
|
281
|
+
const isFeatureBased = architecture === "feature-based";
|
|
282
|
+
const targetUiDir = isFeatureBased
|
|
283
|
+
? path.join(projectPath, "src", "shared", "components", "ui")
|
|
284
|
+
: path.join(projectPath, "src", "ui");
|
|
260
285
|
|
|
261
|
-
|
|
262
|
-
// files (e.g. type-based `services/.gitkeep` after the axios/fetch client
|
|
263
|
-
// overlay lands). A folder with nothing but its placeholder keeps it.
|
|
264
|
-
async function pruneRedundantGitkeeps(projectPath) {
|
|
265
|
-
const walk = async (dir) => {
|
|
266
|
-
let entries;
|
|
267
|
-
try {
|
|
268
|
-
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
269
|
-
} catch {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
for (const entry of entries) {
|
|
273
|
-
if (entry.isDirectory()) {
|
|
274
|
-
await walk(path.join(dir, entry.name));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (entries.some((e) => e.name === ".gitkeep")) {
|
|
278
|
-
const hasRealFiles = entries.some((e) => e.isFile() && e.name !== ".gitkeep");
|
|
279
|
-
if (hasRealFiles) {
|
|
280
|
-
await fsp.rm(path.join(dir, ".gitkeep"), { force: true });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
await walk(path.join(projectPath, "src"));
|
|
285
|
-
}
|
|
286
|
+
await fsp.mkdir(targetUiDir, { recursive: true });
|
|
286
287
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// markup defaults used for cssFramework "none" — then overlay the framework's
|
|
291
|
-
// subfolder so its markup-specific files (transitive layouts, home pages)
|
|
292
|
-
// win; `copyDirRecursive` overwrites on name collision.
|
|
293
|
-
async function injectOverlayVariant(projectPath, templatesDir, rel, cssFramework, language) {
|
|
294
|
-
const base = `conditional/${rel}`;
|
|
295
|
-
await injectOverlay(projectPath, templatesDir, base, language);
|
|
296
|
-
const variant =
|
|
297
|
-
cssFramework === "tailwind" || cssFramework === "bootstrap"
|
|
298
|
-
? cssFramework
|
|
299
|
-
: "";
|
|
300
|
-
if (variant) {
|
|
301
|
-
await injectOverlay(
|
|
302
|
-
projectPath,
|
|
303
|
-
templatesDir,
|
|
304
|
-
`${base}/${variant}`,
|
|
305
|
-
language
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
288
|
+
const buttonDest = path.join(targetUiDir, `Button.${codeExt}`);
|
|
289
|
+
const cardDest = path.join(targetUiDir, `Card.${codeExt}`);
|
|
290
|
+
const indexDest = path.join(targetUiDir, `index.${utilExt}`);
|
|
309
291
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
292
|
+
const buttonTemplate = path.join(
|
|
293
|
+
templatesDir,
|
|
294
|
+
"ui",
|
|
295
|
+
"shadcn",
|
|
296
|
+
"primitives",
|
|
297
|
+
`Button.${codeExt}`
|
|
298
|
+
);
|
|
299
|
+
const cardTemplate = path.join(
|
|
300
|
+
templatesDir,
|
|
301
|
+
"ui",
|
|
302
|
+
"shadcn",
|
|
303
|
+
"primitives",
|
|
304
|
+
`Card.${codeExt}`
|
|
305
|
+
);
|
|
306
|
+
const indexTemplate = path.join(
|
|
307
|
+
templatesDir,
|
|
308
|
+
"ui",
|
|
309
|
+
"shadcn",
|
|
310
|
+
"primitives",
|
|
311
|
+
`index.${utilExt}`
|
|
312
|
+
);
|
|
326
313
|
|
|
327
|
-
// Copy config files to project root
|
|
328
|
-
const configExt = language === "ts" ? "ts" : "js";
|
|
329
|
-
const configSrc = path.join(testingDir, `${framework}.config.${configExt}`);
|
|
330
314
|
try {
|
|
331
|
-
await fsp.access(
|
|
332
|
-
await fsp.copyFile(
|
|
315
|
+
await fsp.access(buttonTemplate);
|
|
316
|
+
await fsp.copyFile(buttonTemplate, buttonDest);
|
|
333
317
|
} catch {}
|
|
334
318
|
|
|
335
|
-
// Copy setup file
|
|
336
|
-
const setupSrc = path.join(testingDir, `src/test/setup.${configExt}`);
|
|
337
319
|
try {
|
|
338
|
-
await fsp.access(
|
|
339
|
-
|
|
340
|
-
await writeFileRecursive(setupDest, await fsp.readFile(setupSrc, "utf8"));
|
|
320
|
+
await fsp.access(cardTemplate);
|
|
321
|
+
await fsp.copyFile(cardTemplate, cardDest);
|
|
341
322
|
} catch {}
|
|
342
323
|
|
|
343
|
-
// Jest keeps its setup at the template root (jest.setup.ts/js), referenced by
|
|
344
|
-
// jest.config as <rootDir>/jest.setup.ts — copy it to the project root.
|
|
345
|
-
if (framework === "jest") {
|
|
346
|
-
const jestSetupSrc = path.join(testingDir, `jest.setup.${configExt}`);
|
|
347
|
-
try {
|
|
348
|
-
await fsp.access(jestSetupSrc);
|
|
349
|
-
await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${configExt}`));
|
|
350
|
-
} catch {}
|
|
351
|
-
|
|
352
|
-
// Unified Babel transform config for Jest (env + react + typescript presets).
|
|
353
|
-
const babelSrc = path.join(testingDir, "babel.config.cjs");
|
|
354
|
-
try {
|
|
355
|
-
await fsp.access(babelSrc);
|
|
356
|
-
await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
|
|
357
|
-
} catch {}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Copy test file (architecture-scoped so the App import path resolves)
|
|
361
|
-
const testExt = language === "ts" ? "tsx" : "jsx";
|
|
362
|
-
const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${testExt}`);
|
|
363
324
|
try {
|
|
364
|
-
await fsp.access(
|
|
365
|
-
|
|
366
|
-
await writeFileRecursive(testDest, await fsp.readFile(testSrc, "utf8"));
|
|
325
|
+
await fsp.access(indexTemplate);
|
|
326
|
+
await fsp.copyFile(indexTemplate, indexDest);
|
|
367
327
|
} catch {}
|
|
368
328
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const pkg = JSON.parse(content);
|
|
374
|
-
if (!pkg.scripts) pkg.scripts = {};
|
|
375
|
-
if (framework === "vitest") {
|
|
376
|
-
pkg.scripts.test = "vitest";
|
|
377
|
-
pkg.scripts["test:run"] = "vitest run";
|
|
378
|
-
} else {
|
|
379
|
-
pkg.scripts.test = "jest";
|
|
380
|
-
pkg.scripts["test:watch"] = "jest --watch";
|
|
381
|
-
}
|
|
382
|
-
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
383
|
-
} catch {}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export async function injectLinter(projectPath, templatesDir, linter, language, framework) {
|
|
387
|
-
const lintingDir = path.join(templatesDir, "conditional", "linting", linter);
|
|
388
|
-
|
|
389
|
-
if (linter === "eslint") {
|
|
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
|
-
}
|
|
404
|
-
} else if (linter === "oxlint") {
|
|
405
|
-
const configSrc = path.join(lintingDir, "oxlintrc.json");
|
|
329
|
+
// If type-based, also populate src/components/ui for maximum tooling compatibility
|
|
330
|
+
if (!isFeatureBased) {
|
|
331
|
+
const fallbackUiDir = path.join(projectPath, "src", "components", "ui");
|
|
332
|
+
await fsp.mkdir(fallbackUiDir, { recursive: true });
|
|
406
333
|
try {
|
|
407
|
-
await fsp.
|
|
408
|
-
await fsp.copyFile(
|
|
334
|
+
await fsp.copyFile(buttonDest, path.join(fallbackUiDir, `Button.${codeExt}`));
|
|
335
|
+
await fsp.copyFile(cardDest, path.join(fallbackUiDir, `Card.${codeExt}`));
|
|
336
|
+
await fsp.copyFile(indexDest, path.join(fallbackUiDir, `index.${utilExt}`));
|
|
409
337
|
} catch {}
|
|
410
338
|
}
|
|
411
|
-
|
|
412
|
-
// Add lint script to package.json
|
|
413
|
-
const pkgPath = path.join(projectPath, "package.json");
|
|
414
|
-
try {
|
|
415
|
-
const content = await fsp.readFile(pkgPath, "utf8");
|
|
416
|
-
const pkg = JSON.parse(content);
|
|
417
|
-
if (!pkg.scripts) pkg.scripts = {};
|
|
418
|
-
if (linter === "eslint") {
|
|
419
|
-
pkg.scripts.lint = "eslint .";
|
|
420
|
-
pkg.scripts["lint:fix"] = "eslint . --fix";
|
|
421
|
-
} else {
|
|
422
|
-
pkg.scripts.lint = "oxlint .";
|
|
423
|
-
}
|
|
424
|
-
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
425
|
-
} catch {}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function injectCreateFeatureScript(projectPath, templatesDir, language) {
|
|
429
|
-
const scriptSrc = path.join(templatesDir, "architectures", "feature-based", "scripts", "create-feature.mjs");
|
|
430
|
-
const scriptDest = path.join(projectPath, "scripts", "create-feature.mjs");
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
await fsp.access(scriptSrc);
|
|
434
|
-
let content = await fsp.readFile(scriptSrc, "utf8");
|
|
435
|
-
|
|
436
|
-
// Adapt to JS if needed
|
|
437
|
-
if (language === "js") {
|
|
438
|
-
// Replace .ts extensions with .js in the initialFiles
|
|
439
|
-
content = content
|
|
440
|
-
.replace(/services\/index\.ts/g, "services/index.js")
|
|
441
|
-
.replace(/store\/index\.ts/g, "store/index.js")
|
|
442
|
-
.replace(/hooks\/index\.ts/g, "hooks/index.js")
|
|
443
|
-
.replace(/pages\/index\.ts/g, "pages/index.js")
|
|
444
|
-
.replace(/types\/index\.ts/g, "types/index.js");
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
await writeFileRecursive(scriptDest, content);
|
|
448
|
-
} catch {}
|
|
449
|
-
|
|
450
|
-
// Add script to package.json
|
|
451
|
-
const pkgPath = path.join(projectPath, "package.json");
|
|
452
|
-
try {
|
|
453
|
-
const content = await fsp.readFile(pkgPath, "utf8");
|
|
454
|
-
const pkg = JSON.parse(content);
|
|
455
|
-
if (!pkg.scripts) pkg.scripts = {};
|
|
456
|
-
pkg.scripts["create:feature"] = "node scripts/create-feature.mjs";
|
|
457
|
-
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
458
|
-
} catch {}
|
|
459
339
|
}
|
|
460
340
|
|
|
461
|
-
async function updateMainWithProvider(projectPath, stateType, architecture, ext) {
|
|
462
|
-
const mainPath = path.join(projectPath, "src", `main.${ext}`);
|
|
463
|
-
try {
|
|
464
|
-
let content = await fsp.readFile(mainPath, "utf8");
|
|
465
|
-
|
|
466
|
-
if (architecture === "feature-based") {
|
|
467
|
-
if (!content.includes("StoreProvider")) {
|
|
468
|
-
content = content.replace(
|
|
469
|
-
/import App from ['"]@\/app\/App['"]\n?/,
|
|
470
|
-
"import App from '@/app/App'\nimport { StoreProvider } from '@/app/providers/StoreProvider'\n"
|
|
471
|
-
);
|
|
472
|
-
content = content.replace(
|
|
473
|
-
/<App \/>/,
|
|
474
|
-
"<StoreProvider>\n <App />\n </StoreProvider>"
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
} else {
|
|
478
|
-
if (!content.includes("StoreProvider")) {
|
|
479
|
-
content = content.replace(
|
|
480
|
-
/import App from ['"]\.\/App['"]\n?/,
|
|
481
|
-
"import App from './App'\nimport { StoreProvider } from './providers/StoreProvider'\n"
|
|
482
|
-
);
|
|
483
|
-
content = content.replace(
|
|
484
|
-
/<App \/>/,
|
|
485
|
-
"<StoreProvider>\n <App />\n </StoreProvider>"
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
await fsp.writeFile(mainPath, content, "utf8");
|
|
491
|
-
} catch {}
|
|
492
|
-
}
|