@clearlydev/nx-archweave 0.1.0 → 0.2.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.
- package/README.md +2 -2
- package/dist/eslint/fsd-boundaries.d.ts +27 -10
- package/dist/eslint/fsd-policy.d.ts +38 -0
- package/dist/eslint/index.d.ts +10 -9
- package/dist/eslint/index.js +227 -34
- package/dist/eslint/slice-groups.d.ts +12 -0
- package/dist/generators/add-agent-rules/generator.js +1175 -23
- package/dist/generators/add-backend/generator.js +1127 -75
- package/dist/generators/add-fsd-lite/generator.js +1274 -96
- package/dist/generators/add-integration/generator.js +4 -35
- package/dist/generators/add-review/files/dependabot.yml +7 -0
- package/dist/generators/add-review/generator.js +1100 -46
- package/dist/generators/doctor/generator.js +784 -131
- package/dist/generators/entity/generator.js +251 -32
- package/dist/generators/feature/generator.js +251 -32
- package/dist/generators/init/generator.js +1955 -549
- package/dist/generators/install-integration/generator.js +5 -37
- package/dist/generators/page/generator.js +251 -32
- package/dist/generators/record-integration/generator.js +5 -36
- package/dist/generators/shared/generator.js +251 -32
- package/dist/generators/widget/generator.js +251 -32
- package/dist/index.js +4 -35
- package/package.json +5 -3
- package/src/generators/add-agent-rules/schema.json +5 -0
- package/src/generators/add-fsd-lite/schema.json +2 -2
- package/src/generators/add-integration/schema.json +1 -1
- package/src/generators/entity/schema.json +11 -1
- package/src/generators/feature/schema.json +11 -1
- package/src/generators/init/schema.json +13 -2
- package/src/generators/page/schema.json +15 -3
- package/src/generators/shared/schema.json +7 -1
- package/src/generators/widget/schema.json +11 -1
- package/dist/eslint/fsd-boundaries.js +0 -144
- package/dist/eslint/tsconfig-aliases.js +0 -114
- package/dist/generators/add-agent-rules/render.js +0 -46
- package/dist/generators/add-backend/render.js +0 -102
- package/dist/generators/add-fsd-lite/render.js +0 -72
- package/dist/generators/add-github-templates/render.js +0 -26
- package/dist/generators/add-integration/render.js +0 -34
- package/dist/generators/add-review/render.js +0 -96
- package/dist/generators/allow-builds/render.js +0 -82
- package/dist/generators/doctor/integration-report.js +0 -70
- package/dist/generators/doctor/render.js +0 -10
- package/dist/generators/init/render.js +0 -125
- package/dist/generators/init/validation.js +0 -74
- package/dist/generators/install-integration/render.js +0 -128
- package/dist/generators/record-integration/render.js +0 -31
- package/dist/generators/shared/dry-run.js +0 -12
- package/dist/generators/shared/frontend-app.js +0 -57
- package/dist/generators/shared/project-type.js +0 -39
- package/dist/generators/shared/record-pipeline.js +0 -34
- package/dist/generators/shared/run-nx.js +0 -24
- package/dist/generators/shared/template-files.js +0 -38
- package/dist/generators/shared/toolkit-config-file.js +0 -110
- package/dist/generators/slice/core.js +0 -185
- package/dist/generators/slice/render.js +0 -70
- package/dist/lib/nx-plugin.js +0 -7
- package/dist/runtime/integrations-composition-root.js +0 -20
- package/dist/runtime/node-command-runner.js +0 -81
|
@@ -1,6 +1,113 @@
|
|
|
1
1
|
// src/generators/slice/core.ts
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
5
|
+
var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
|
|
6
|
+
function readBoundaryTags(config) {
|
|
7
|
+
const boundaries = config?.["boundaries"];
|
|
8
|
+
const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
|
|
9
|
+
const pick = (role) => {
|
|
10
|
+
const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
|
|
11
|
+
return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
|
|
12
|
+
};
|
|
13
|
+
return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
|
|
14
|
+
}
|
|
15
|
+
function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
16
|
+
return [
|
|
17
|
+
{ sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
|
|
18
|
+
{ sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
|
|
19
|
+
{ sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
var SCOPE_CONSTRAINTS = scopeConstraints();
|
|
23
|
+
|
|
24
|
+
// ../template-engine/dist/frameworks/profiles.js
|
|
25
|
+
var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
|
|
26
|
+
var NEXT = {
|
|
27
|
+
id: "next",
|
|
28
|
+
recognised: true,
|
|
29
|
+
// Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
|
|
30
|
+
// names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
|
|
31
|
+
// src/pages as its routers.
|
|
32
|
+
layers: ["pages", "features", "entities", "shared"],
|
|
33
|
+
layerDirs: { app: "_app", pages: "_pages" },
|
|
34
|
+
buildOutputDirs: [".next", "out"],
|
|
35
|
+
platformSuffixes: [],
|
|
36
|
+
routerDirs: ["pages"]
|
|
37
|
+
};
|
|
38
|
+
var REACT_VITE = {
|
|
39
|
+
id: "react-vite",
|
|
40
|
+
recognised: true,
|
|
41
|
+
layers: ALL_LAYERS,
|
|
42
|
+
layerDirs: {},
|
|
43
|
+
buildOutputDirs: ["dist"],
|
|
44
|
+
platformSuffixes: [],
|
|
45
|
+
routerDirs: []
|
|
46
|
+
};
|
|
47
|
+
var EXPO_ROUTER = {
|
|
48
|
+
id: "expo",
|
|
49
|
+
recognised: true,
|
|
50
|
+
layers: ALL_LAYERS,
|
|
51
|
+
layerDirs: { app: "_app" },
|
|
52
|
+
buildOutputDirs: [".expo", "dist"],
|
|
53
|
+
platformSuffixes: ["ios", "android", "native", "web"],
|
|
54
|
+
routerDirs: []
|
|
55
|
+
};
|
|
56
|
+
var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
|
|
57
|
+
var GENERIC = {
|
|
58
|
+
id: "generic",
|
|
59
|
+
recognised: false,
|
|
60
|
+
layers: ALL_LAYERS,
|
|
61
|
+
layerDirs: {},
|
|
62
|
+
buildOutputDirs: ["dist", "build"],
|
|
63
|
+
platformSuffixes: [],
|
|
64
|
+
routerDirs: []
|
|
65
|
+
};
|
|
66
|
+
var PROFILES = { next: NEXT, "react-vite": REACT_VITE, expo: EXPO_ROUTER, generic: GENERIC };
|
|
67
|
+
function profileById(id) {
|
|
68
|
+
return PROFILES[id];
|
|
69
|
+
}
|
|
70
|
+
var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
|
|
71
|
+
var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
72
|
+
var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
|
|
73
|
+
function hasExpoKey(input, file) {
|
|
74
|
+
const text = input.read(file);
|
|
75
|
+
if (typeof text !== "string")
|
|
76
|
+
return false;
|
|
77
|
+
try {
|
|
78
|
+
return typeof JSON.parse(text)["expo"] === "object";
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function dependsOn(input, manifest, name) {
|
|
84
|
+
const text = input.read(manifest);
|
|
85
|
+
if (typeof text !== "string")
|
|
86
|
+
return false;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(text);
|
|
89
|
+
return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function detectFramework(input) {
|
|
95
|
+
const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
|
|
96
|
+
const executors = input.executors ?? [];
|
|
97
|
+
if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
|
|
98
|
+
return NEXT;
|
|
99
|
+
}
|
|
100
|
+
const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
|
|
101
|
+
const usesExpo = executors.some((executor) => executor.startsWith("@nx/expo:")) || hasExpoKey(input, `${prefix}app.json`) || EXPO_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || dependsOn(input, `${prefix}package.json`, "expo");
|
|
102
|
+
if (usesExpo)
|
|
103
|
+
return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
|
|
104
|
+
const usesReact = dependsAnywhere("react");
|
|
105
|
+
const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
|
|
106
|
+
if (usesVite && usesReact)
|
|
107
|
+
return REACT_VITE;
|
|
108
|
+
return GENERIC;
|
|
109
|
+
}
|
|
110
|
+
|
|
4
111
|
// ../template-engine/dist/fsd/slice-rules.js
|
|
5
112
|
function toCamelCase(kebabName) {
|
|
6
113
|
return kebabName.split("-").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
@@ -102,7 +209,14 @@ function resolveLocaleOption(option, config) {
|
|
|
102
209
|
const locale = config?.["locale"];
|
|
103
210
|
return locale === "ko" || locale === "en" ? locale : "en";
|
|
104
211
|
}
|
|
105
|
-
var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
|
|
212
|
+
var RECORDED_FRONTENDS = ["react-vite", "next", "expo", "generic", "none"];
|
|
213
|
+
function recordedFrontends(config) {
|
|
214
|
+
const frontends = config?.["frontends"];
|
|
215
|
+
if (!Array.isArray(frontends)) return [];
|
|
216
|
+
return frontends.flatMap(
|
|
217
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry["project"] === "string" ? [entry.project] : []
|
|
218
|
+
);
|
|
219
|
+
}
|
|
106
220
|
|
|
107
221
|
// src/generators/shared/frontend-app.ts
|
|
108
222
|
import { getProjects } from "@nx/devkit";
|
|
@@ -130,12 +244,27 @@ function isEndToEnd(root) {
|
|
|
130
244
|
const dir = baseNameOf(root);
|
|
131
245
|
return dir === "e2e" || dir.endsWith("-e2e");
|
|
132
246
|
}
|
|
133
|
-
function isBackend(root, config) {
|
|
134
|
-
return baseNameOf(root) === "api" || (config.tags ?? []).includes(
|
|
247
|
+
function isBackend(root, config, backendTag) {
|
|
248
|
+
return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
|
|
135
249
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
250
|
+
function prefixOf(root) {
|
|
251
|
+
return root === "" || root === "." ? "" : `${root}/`;
|
|
252
|
+
}
|
|
253
|
+
function sourceRootOf(tree, config) {
|
|
254
|
+
const prefix = prefixOf(config.root);
|
|
255
|
+
const declared = config.sourceRoot?.replace(/\/$/, "");
|
|
256
|
+
const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
|
|
257
|
+
if (declared && !atRoot) return declared;
|
|
258
|
+
if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
|
|
259
|
+
return `${prefix}src`;
|
|
260
|
+
}
|
|
261
|
+
function frameworkOf(tree, config) {
|
|
262
|
+
return detectFramework({
|
|
263
|
+
exists: (path) => tree.exists(path),
|
|
264
|
+
read: (path) => tree.read(path, "utf-8"),
|
|
265
|
+
projectRoot: config.root,
|
|
266
|
+
executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
|
|
267
|
+
});
|
|
139
268
|
}
|
|
140
269
|
function resolveFrontendApp(tree, explicitProject) {
|
|
141
270
|
let projects;
|
|
@@ -146,27 +275,51 @@ function resolveFrontendApp(tree, explicitProject) {
|
|
|
146
275
|
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
147
276
|
return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
|
|
148
277
|
}
|
|
149
|
-
const found = (project2, config2) =>
|
|
150
|
-
|
|
151
|
-
project: project2,
|
|
152
|
-
|
|
153
|
-
sourceRoot: sourceRootOf(config2)
|
|
154
|
-
});
|
|
278
|
+
const found = (project2, config2) => {
|
|
279
|
+
const profile = frameworkOf(tree, config2);
|
|
280
|
+
return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
|
|
281
|
+
};
|
|
155
282
|
if (explicitProject !== void 0) {
|
|
156
283
|
const config2 = projects.get(explicitProject);
|
|
157
|
-
|
|
284
|
+
if (config2) return found(explicitProject, config2);
|
|
285
|
+
const rootApp = inferredRootApp(tree, projects);
|
|
286
|
+
return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
|
|
158
287
|
}
|
|
159
|
-
const
|
|
288
|
+
const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
|
|
289
|
+
const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
|
|
160
290
|
const declared = eligible.filter(([, config2]) => config2.projectType === "application");
|
|
161
|
-
const inferred = eligible.filter(
|
|
162
|
-
(
|
|
163
|
-
|
|
291
|
+
const inferred = eligible.filter(([, config2]) => {
|
|
292
|
+
if (config2.projectType !== void 0) return false;
|
|
293
|
+
if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
|
|
294
|
+
return resolveProjectType(tree, config2.root, void 0) === "application";
|
|
295
|
+
});
|
|
164
296
|
const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
|
|
165
|
-
if (candidates.length === 0)
|
|
297
|
+
if (candidates.length === 0) {
|
|
298
|
+
const rootApp = inferredRootApp(tree, projects);
|
|
299
|
+
if (rootApp !== null) return rootApp;
|
|
300
|
+
const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
|
|
301
|
+
return { kind: "none", otherProjects };
|
|
302
|
+
}
|
|
166
303
|
if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
|
|
167
304
|
const [project, config] = candidates[0];
|
|
168
305
|
return found(project, config);
|
|
169
306
|
}
|
|
307
|
+
function inferredRootApp(tree, projects) {
|
|
308
|
+
if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
|
|
309
|
+
const raw = tree.read("package.json", "utf-8");
|
|
310
|
+
if (raw === null) return null;
|
|
311
|
+
let name;
|
|
312
|
+
try {
|
|
313
|
+
name = JSON.parse(raw).name;
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (typeof name !== "string" || name === "") return null;
|
|
318
|
+
const config = { name, root: "." };
|
|
319
|
+
const framework = frameworkOf(tree, config);
|
|
320
|
+
if (!framework.recognised) return null;
|
|
321
|
+
return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
|
|
322
|
+
}
|
|
170
323
|
|
|
171
324
|
// src/generators/shared/template-files.ts
|
|
172
325
|
import { existsSync } from "node:fs";
|
|
@@ -192,7 +345,7 @@ function renderTemplateFiles(srcFolder, target, substitutions) {
|
|
|
192
345
|
// src/generators/slice/core.ts
|
|
193
346
|
function composeSlice(input) {
|
|
194
347
|
const files = generatorFilesDir("slice", import.meta.url);
|
|
195
|
-
const directory = `${input.sourceRoot}/${input.layerDirName}/${input.name}`;
|
|
348
|
+
const directory = `${input.sourceRoot}/${input.layerDirName}/${input.group === void 0 ? "" : `${input.group}/`}${input.name}`;
|
|
196
349
|
const shared = input.layer === "shared";
|
|
197
350
|
const values = {
|
|
198
351
|
name: input.name,
|
|
@@ -201,7 +354,7 @@ function composeSlice(input) {
|
|
|
201
354
|
locale: input.locale,
|
|
202
355
|
modulePath: shared ? `./${input.name}` : `./model/${input.name}`
|
|
203
356
|
};
|
|
204
|
-
const module = renderTemplateFiles(join(files, "module"), shared ? directory : `${directory}/model`, values).sort((a, b) => Number(a.filePath.endsWith(".spec.ts")) - Number(b.filePath.endsWith(".spec.ts")));
|
|
357
|
+
const module = renderTemplateFiles(join(files, "module"), shared ? directory : `${directory}/model`, values).filter((file) => input.withSpec !== false || !file.filePath.endsWith(".spec.ts")).sort((a, b) => Number(a.filePath.endsWith(".spec.ts")) - Number(b.filePath.endsWith(".spec.ts")));
|
|
205
358
|
const segments = shared ? [] : Array.from(new Set(input.segments)).filter((segment) => segment !== "model").flatMap((segment) => renderTemplateFiles(join(files, "segment"), `${directory}/${segment}`, { ...values, segment }));
|
|
206
359
|
return {
|
|
207
360
|
directory,
|
|
@@ -211,20 +364,18 @@ function composeSlice(input) {
|
|
|
211
364
|
}
|
|
212
365
|
var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
213
366
|
function layerDirNameFor(layer, frontend) {
|
|
214
|
-
return layer
|
|
367
|
+
return profileById(frontend).layerDirs[layer] ?? layer;
|
|
215
368
|
}
|
|
216
369
|
function layerDirFor(layer, sourceRoot, frontend = "react-vite") {
|
|
217
370
|
return `${sourceRoot}/${layerDirNameFor(layer, frontend)}`;
|
|
218
371
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (option === "react-vite" || option === "next") return option;
|
|
372
|
+
function resolveFrontendForApp(option, config, detected) {
|
|
373
|
+
if (option !== void 0) return option;
|
|
222
374
|
const recorded = config?.["frontend"];
|
|
223
375
|
if (typeof recorded === "string" && RECORDED_FRONTENDS.includes(recorded) && recorded !== "none") {
|
|
224
376
|
return recorded;
|
|
225
377
|
}
|
|
226
|
-
|
|
227
|
-
return NEXT_CONFIG_FILES.some((file) => tree.exists(`${prefix}${file}`)) ? "next" : "react-vite";
|
|
378
|
+
return detected;
|
|
228
379
|
}
|
|
229
380
|
function listFilesUnder(tree, dir, prefix = "") {
|
|
230
381
|
return tree.children(dir).flatMap((child) => {
|
|
@@ -279,15 +430,23 @@ function runFsdSlice(tree, layer, options) {
|
|
|
279
430
|
if (badSegment !== void 0) return refusal("invalid-segment", dryRun, layer, options.name, unresolved, { detail: badSegment });
|
|
280
431
|
const essenceSegment = segments.find((segment) => isEssenceSegmentName(segment));
|
|
281
432
|
if (essenceSegment !== void 0) return refusal("segment-by-essence", dryRun, layer, options.name, unresolved, { detail: essenceSegment });
|
|
433
|
+
if (options.group !== void 0 && layer === "shared") return refusal("group-on-shared", dryRun, layer, options.name, unresolved);
|
|
434
|
+
if (options.group !== void 0 && (!KEBAB_CASE.test(options.group) || SEGMENT_FOLDERS.includes(options.group) || isEssenceSegmentName(options.group))) {
|
|
435
|
+
return refusal("invalid-group", dryRun, layer, options.name, unresolved, { detail: options.group });
|
|
436
|
+
}
|
|
282
437
|
const app = resolveFrontendApp(tree, options.project);
|
|
283
438
|
if (app.kind === "not-found") return refusal("project-not-found", dryRun, layer, options.name, { project: app.project, sourceRoot: unresolved.sourceRoot });
|
|
284
|
-
if (app.kind === "ambiguous")
|
|
439
|
+
if (app.kind === "ambiguous") {
|
|
440
|
+
return refusal("ambiguous-frontend", dryRun, layer, options.name, unresolved, { candidates: app.candidates, recordedFrontends: recordedFrontends(readToolkitConfig(tree)) });
|
|
441
|
+
}
|
|
285
442
|
if (app.kind === "invalid-project-json") return refusal("invalid-project-json", dryRun, layer, options.name, unresolved, { detail: app.detail });
|
|
286
|
-
if (app.kind === "none") return refusal("no-frontend-app", dryRun, layer, options.name, unresolved);
|
|
443
|
+
if (app.kind === "none") return refusal("no-frontend-app", dryRun, layer, options.name, unresolved, { candidates: app.otherProjects });
|
|
287
444
|
const config = readToolkitConfig(tree);
|
|
288
|
-
const frontend = resolveFrontendForApp(options.frontend, config, app.
|
|
445
|
+
const frontend = resolveFrontendForApp(options.frontend, config, app.framework);
|
|
289
446
|
const layerDir = layerDirFor(layer, app.sourceRoot, frontend);
|
|
290
447
|
const locale = resolveLocaleOption(options.locale, config);
|
|
448
|
+
const requested = options.unitTestRunner !== void 0 && options.unitTestRunner !== "auto" ? options.unitTestRunner : recordedUnitTestRunner(config);
|
|
449
|
+
const runner = requested ?? detectUnitTestRunner(tree, app.root);
|
|
291
450
|
const plan = composeSlice({
|
|
292
451
|
layer,
|
|
293
452
|
name: options.name,
|
|
@@ -295,7 +454,9 @@ function runFsdSlice(tree, layer, options) {
|
|
|
295
454
|
locale,
|
|
296
455
|
sourceRoot: app.sourceRoot,
|
|
297
456
|
layerDirName: layerDirNameFor(layer, frontend),
|
|
298
|
-
segments
|
|
457
|
+
segments,
|
|
458
|
+
group: options.group,
|
|
459
|
+
withSpec: runner !== "none"
|
|
299
460
|
});
|
|
300
461
|
const action = computeSliceAction(tree, plan);
|
|
301
462
|
if (action === "created" && !dryRun) {
|
|
@@ -312,9 +473,53 @@ function runFsdSlice(tree, layer, options) {
|
|
|
312
473
|
refused: false,
|
|
313
474
|
refusalReason: null,
|
|
314
475
|
action,
|
|
315
|
-
files: plan.files
|
|
476
|
+
files: plan.files,
|
|
477
|
+
...runner === "none" ? { specSkipped: requested === "none" ? "none-requested" : "no-test-runner" } : {}
|
|
316
478
|
};
|
|
317
479
|
}
|
|
480
|
+
var SEGMENT_FOLDERS = ["ui", "model", "api", "lib", "config"];
|
|
481
|
+
function recordedUnitTestRunner(config) {
|
|
482
|
+
const fsd = config?.["fsd"];
|
|
483
|
+
const value = typeof fsd === "object" && fsd !== null ? fsd["unitTestRunner"] : void 0;
|
|
484
|
+
return value === "jest" || value === "vitest" || value === "none" ? value : void 0;
|
|
485
|
+
}
|
|
486
|
+
function hasDependency(tree, manifest, name) {
|
|
487
|
+
const raw = tree.read(manifest, "utf-8");
|
|
488
|
+
if (raw === null) return false;
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(raw);
|
|
491
|
+
return parsed["dependencies"]?.[name] !== void 0 || parsed["devDependencies"]?.[name] !== void 0;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function detectUnitTestRunner(tree, projectRoot) {
|
|
497
|
+
const prefix = projectRoot === "" || projectRoot === "." ? "" : `${projectRoot}/`;
|
|
498
|
+
const project = tree.read(`${prefix}project.json`, "utf-8");
|
|
499
|
+
if (project !== null) {
|
|
500
|
+
try {
|
|
501
|
+
const targets = JSON.parse(project).targets ?? {};
|
|
502
|
+
for (const target of Object.values(targets)) {
|
|
503
|
+
if (target?.executor?.startsWith("@nx/jest:")) return "jest";
|
|
504
|
+
if (target?.executor?.startsWith("@nx/vite:test") || target?.executor?.startsWith("@nx/vitest:")) return "vitest";
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const dir of prefix === "" ? [""] : [prefix, ""]) {
|
|
510
|
+
if (["ts", "mts", "cts", "js", "mjs", "cjs"].some((ext) => tree.exists(`${dir}jest.config.${ext}`))) return "jest";
|
|
511
|
+
if (["ts", "mts", "js", "mjs"].some((ext) => tree.exists(`${dir}vitest.config.${ext}`) || tree.exists(`${dir}vitest.workspace.${ext}`))) return "vitest";
|
|
512
|
+
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
513
|
+
const vite = tree.read(`${dir}vite.config.${ext}`, "utf-8");
|
|
514
|
+
if (vite !== null && /\btest\s*:/.test(vite)) return "vitest";
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const manifest of prefix === "" ? ["package.json"] : [`${prefix}package.json`, "package.json"]) {
|
|
518
|
+
if (hasDependency(tree, manifest, "vitest")) return "vitest";
|
|
519
|
+
if (hasDependency(tree, manifest, "jest")) return "jest";
|
|
520
|
+
}
|
|
521
|
+
return "none";
|
|
522
|
+
}
|
|
318
523
|
|
|
319
524
|
// src/generators/slice/render.ts
|
|
320
525
|
var ACTION_LINES = {
|
|
@@ -340,13 +545,22 @@ function renderSliceReport(report) {
|
|
|
340
545
|
} else if (report.refusalReason === "ambiguous-frontend") {
|
|
341
546
|
lines.push(" More than one frontend app was found \u2014 rerun with --project=<one of them>:");
|
|
342
547
|
for (const name of report.candidates ?? []) lines.push(` - ${name}`);
|
|
548
|
+
if ((report.recordedFrontends ?? []).length > 0) lines.push(` (toolkit.config.json records: ${(report.recordedFrontends ?? []).join(", ")})`);
|
|
343
549
|
} else if (report.refusalReason === "invalid-project-json") {
|
|
344
550
|
lines.push(` A project.json could not be parsed (${report.detail ?? "unknown"}) \u2014 fix it, then rerun.`);
|
|
551
|
+
} else if (report.refusalReason === "no-frontend-app" && (report.candidates ?? []).length > 0) {
|
|
552
|
+
lines.push(" No project was recognised as a frontend app. If one of these is it, rerun with --project=<name>:");
|
|
553
|
+
for (const name of report.candidates ?? []) lines.push(` - ${name}`);
|
|
345
554
|
} else if (report.refusalReason === "no-frontend-app") {
|
|
346
|
-
lines.push(" The workspace has no
|
|
555
|
+
lines.push(" The workspace has no projects to hold FSD layers \u2014 create a frontend app first.");
|
|
347
556
|
} else if (report.refusalReason === "invalid-segment") {
|
|
348
557
|
lines.push(` --segments: "${report.detail ?? ""}" is not a valid segment name.`);
|
|
349
558
|
lines.push(" Use lowercase letters, digits, and single hyphens, starting with a letter (e.g. ui, api, lib).");
|
|
559
|
+
} else if (report.refusalReason === "group-on-shared") {
|
|
560
|
+
lines.push(" --group does not apply to shared: shared holds segments, not slices.");
|
|
561
|
+
} else if (report.refusalReason === "invalid-group") {
|
|
562
|
+
lines.push(` --group: "${report.detail ?? ""}" is not a usable group name.`);
|
|
563
|
+
lines.push(' Use kebab-case (e.g. "mypage"), and not a segment name (ui, model, api, lib, config).');
|
|
350
564
|
} else if (report.refusalReason === "segments-on-shared") {
|
|
351
565
|
lines.push(" --segments does not apply to shared: a shared segment holds no segments of its own.");
|
|
352
566
|
lines.push(" Run the generator once per segment instead (e.g. --name=ui, then --name=api).");
|
|
@@ -369,6 +583,11 @@ function renderSliceReport(report) {
|
|
|
369
583
|
lines.push(" \u2014 or remove the existing directory manually first if you intended to replace it.");
|
|
370
584
|
return lines.join("\n");
|
|
371
585
|
}
|
|
586
|
+
if (report.specSkipped === "no-test-runner") {
|
|
587
|
+
lines.push(" no test runner found (jest or vitest) \u2014 no .spec.ts was written; pass --unitTestRunner=jest|vitest to add one.");
|
|
588
|
+
} else if (report.specSkipped === "none-requested") {
|
|
589
|
+
lines.push(" no .spec.ts was written (unitTestRunner: none).");
|
|
590
|
+
}
|
|
372
591
|
if (report.dryRun) {
|
|
373
592
|
for (const file of report.files) {
|
|
374
593
|
lines.push("");
|