@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("");
|
package/dist/index.js
CHANGED
|
@@ -111,33 +111,6 @@ var PLUGIN_CATALOG = [
|
|
|
111
111
|
];
|
|
112
112
|
var PLUGIN_IDS = PLUGIN_CATALOG.map((entry) => entry.id);
|
|
113
113
|
|
|
114
|
-
// ../integrations/dist/catalog/resolve-version.js
|
|
115
|
-
import { get } from "node:https";
|
|
116
|
-
function resolveVersionFromNpmRegistry(npmPackage) {
|
|
117
|
-
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(npmPackage)}`;
|
|
118
|
-
return new Promise((resolve2, reject) => {
|
|
119
|
-
get(registryUrl, (response) => {
|
|
120
|
-
let body = "";
|
|
121
|
-
response.on("data", (chunk) => {
|
|
122
|
-
body += chunk;
|
|
123
|
-
});
|
|
124
|
-
response.on("end", () => {
|
|
125
|
-
try {
|
|
126
|
-
const parsed = JSON.parse(body);
|
|
127
|
-
const latest = parsed?.["dist-tags"]?.["latest"];
|
|
128
|
-
if (typeof latest !== "string") {
|
|
129
|
-
reject(new Error(`Could not resolve the latest version of ${npmPackage} from the npm registry.`));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
resolve2(latest);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}).on("error", reject);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
114
|
// ../integrations/dist/hosts/claude-code/detect.js
|
|
142
115
|
import { existsSync, readFileSync } from "node:fs";
|
|
143
116
|
import { join } from "node:path";
|
|
@@ -178,14 +151,14 @@ var WORKSPACE_FILES_BY_SCOPE = {
|
|
|
178
151
|
local: [".claude/settings.local.json"]
|
|
179
152
|
};
|
|
180
153
|
var DEFAULT_PLUGIN_SCOPE = "project";
|
|
181
|
-
function createClaudeMarketplacePlan(entry
|
|
154
|
+
function createClaudeMarketplacePlan(entry) {
|
|
182
155
|
return async (options) => {
|
|
183
156
|
const distribution = entry.hosts["claude-code"];
|
|
184
157
|
if (options.targetEnvironment !== "claude-code" || distribution === void 0) {
|
|
185
158
|
const supported = Object.entries(entry.hosts).filter(([, value]) => value !== void 0).map(([host]) => host).join(", ");
|
|
186
159
|
throw new Error(`Target environment "${options.targetEnvironment}" is not supported by this integration. Supported: ${supported}.`);
|
|
187
160
|
}
|
|
188
|
-
const resolvedVersion = options.versionRange ??
|
|
161
|
+
const resolvedVersion = options.versionRange ?? VERSION_DECIDED_BY_MARKETPLACE;
|
|
189
162
|
const scope = options.scope ?? DEFAULT_PLUGIN_SCOPE;
|
|
190
163
|
return {
|
|
191
164
|
officialCommand: {
|
|
@@ -603,10 +576,6 @@ function createClaudeMarketplaceVerifier(entry, deps) {
|
|
|
603
576
|
}
|
|
604
577
|
|
|
605
578
|
// ../integrations/dist/catalog/create-catalog-integration.js
|
|
606
|
-
function defaultResolveVersion(entry) {
|
|
607
|
-
const npmPackage = entry.npmPackage;
|
|
608
|
-
return npmPackage === void 0 ? async () => VERSION_DECIDED_BY_MARKETPLACE : () => resolveVersionFromNpmRegistry(npmPackage);
|
|
609
|
-
}
|
|
610
579
|
function createCatalogIntegration(entry, deps) {
|
|
611
580
|
return {
|
|
612
581
|
id: entry.id,
|
|
@@ -616,7 +585,7 @@ function createCatalogIntegration(entry, deps) {
|
|
|
616
585
|
license: entry.license,
|
|
617
586
|
supportedEnvironments: PLUGIN_HOSTS.filter((host) => entry.hosts[host] !== void 0),
|
|
618
587
|
detect: (context) => detectWorkspacePackage(entry, context),
|
|
619
|
-
plan: createClaudeMarketplacePlan(entry
|
|
588
|
+
plan: createClaudeMarketplacePlan(entry),
|
|
620
589
|
install: createClaudeMarketplaceInstall(entry, { runner: deps.runner }),
|
|
621
590
|
verify: createClaudeMarketplaceVerifier(entry, { runner: deps.runner }),
|
|
622
591
|
uninstallInstructions: async () => [...entry.uninstallSteps]
|
|
@@ -693,7 +662,7 @@ var NodeCommandRunner = class {
|
|
|
693
662
|
// src/runtime/integrations-composition-root.ts
|
|
694
663
|
function createIntegrationsCompositionRoot(deps = {}) {
|
|
695
664
|
const runner = deps.runner ?? new NodeCommandRunner();
|
|
696
|
-
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner
|
|
665
|
+
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner }));
|
|
697
666
|
return {
|
|
698
667
|
registry,
|
|
699
668
|
runner,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clearlydev/nx-archweave",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Nx generators that make a workspace ready to work on in one preset: FSD Lite structure with enforced module boundaries, AI agent rules, GitHub templates, and optional AI PR review (Claude, Gemini, GPT, Cursor)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "clearlydev (https://github.com/clearlydev)",
|
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
|
-
"@nx/devkit": "^23.1.0"
|
|
96
|
-
|
|
95
|
+
"@nx/devkit": "^23.1.0"
|
|
96
|
+
},
|
|
97
|
+
"devDependencies": {
|
|
98
|
+
"jsonc-parser": "3.3.1"
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"type": "boolean",
|
|
34
34
|
"description": "Include the Team Rules section (collaboration rules for a shared repository). Defaults to toolkit.config.json's agentRules.teamRules, else false"
|
|
35
35
|
},
|
|
36
|
+
"overwriteBlock": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"default": false,
|
|
39
|
+
"description": "Replace the nx-archweave block even when it was edited by hand (otherwise an edited block is kept and the report shows the difference)"
|
|
40
|
+
},
|
|
36
41
|
"dryRun": {
|
|
37
42
|
"type": "boolean",
|
|
38
43
|
"default": false,
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"frontend": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"enum": ["react-vite", "next"],
|
|
11
|
-
"description": "Target frontend — decides the layer set the guide prints. Next.js has no FSD 'app' layer (its App Router directory is it) and its pages layer is the _pages/ directory (FSD's Next.js guide: Next.js reads src/pages as its Pages Router). Omitted: toolkit.config.json's frontend (init records it); required when none is recorded"
|
|
10
|
+
"enum": ["react-vite", "next", "expo", "generic"],
|
|
11
|
+
"description": "Target frontend — decides the layer set the guide prints. Next.js has no FSD 'app' layer (its App Router directory is it) and its pages layer is the _pages/ directory (FSD's Next.js guide: Next.js reads src/pages as its Pages Router). 'generic' is any other framework: the common FSD rules, no renamed layers. Omitted: toolkit.config.json's frontend (init records it); required when none is recorded"
|
|
12
12
|
},
|
|
13
13
|
"project": {
|
|
14
14
|
"type": "string",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"versionRange": {
|
|
25
25
|
"type": "string",
|
|
26
|
-
"description": "
|
|
26
|
+
"description": "Version to show as requested. Without it, the preview says the marketplace decides — it installs its own latest. No network access."
|
|
27
27
|
},
|
|
28
28
|
"dryRun": {
|
|
29
29
|
"type": "boolean",
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"name": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"description": "Slice name in kebab-case (e.g. \"user\") — becomes <app>/src/entities/<name>. Invalid values are refused with a report, not thrown"
|
|
10
|
+
"description": "Slice name in kebab-case (e.g. \"user\") — becomes <app>/src/entities/<name>. Invalid values are refused with a report, not thrown",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
11
12
|
},
|
|
12
13
|
"publicApiName": {
|
|
13
14
|
"type": "string",
|
|
@@ -18,6 +19,15 @@
|
|
|
18
19
|
"items": { "type": "string" },
|
|
19
20
|
"description": "Extra segments to start the slice with, each as <segment>/index.ts (e.g. ui,api). model always exists — it holds the stub. Names are by purpose: ui, api, lib, config … (components/hooks/utils/types are refused)"
|
|
20
21
|
},
|
|
22
|
+
"group": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "A slice group folder to put the slice in: <layer>/<group>/<name>/ (e.g. --group=mypage). The group folder itself gets no files — FSD keeps each slice in it separate"
|
|
25
|
+
},
|
|
26
|
+
"unitTestRunner": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["auto", "jest", "vitest", "none"],
|
|
29
|
+
"description": "Whether the stub gets a .spec.ts. auto (default): when the app or workspace has jest or vitest (a test target, a runner config, or the dependency); toolkit.config.json's fsd.unitTestRunner comes before detection"
|
|
30
|
+
},
|
|
21
31
|
"project": {
|
|
22
32
|
"type": "string",
|
|
23
33
|
"description": "The frontend app whose source root holds the FSD layers (run add-fsd-lite first). Omitted: the workspace's one frontend application; with more than one, the run is refused with the candidates listed"
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"name": {
|
|
9
9
|
"type": "string",
|
|
10
|
-
"description": "Slice name in kebab-case (e.g. \"add-to-cart\") — becomes <app>/src/features/<name>. Invalid values are refused with a report, not thrown"
|
|
10
|
+
"description": "Slice name in kebab-case (e.g. \"add-to-cart\") — becomes <app>/src/features/<name>. Invalid values are refused with a report, not thrown",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
11
12
|
},
|
|
12
13
|
"publicApiName": {
|
|
13
14
|
"type": "string",
|
|
@@ -18,6 +19,15 @@
|
|
|
18
19
|
"items": { "type": "string" },
|
|
19
20
|
"description": "Extra segments to start the slice with, each as <segment>/index.ts (e.g. ui,api). model always exists — it holds the stub. Names are by purpose: ui, api, lib, config … (components/hooks/utils/types are refused)"
|
|
20
21
|
},
|
|
22
|
+
"group": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "A slice group folder to put the slice in: <layer>/<group>/<name>/ (e.g. --group=mypage). The group folder itself gets no files — FSD keeps each slice in it separate"
|
|
25
|
+
},
|
|
26
|
+
"unitTestRunner": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["auto", "jest", "vitest", "none"],
|
|
29
|
+
"description": "Whether the stub gets a .spec.ts. auto (default): when the app or workspace has jest or vitest (a test target, a runner config, or the dependency); toolkit.config.json's fsd.unitTestRunner comes before detection"
|
|
30
|
+
},
|
|
21
31
|
"project": {
|
|
22
32
|
"type": "string",
|
|
23
33
|
"description": "The frontend app whose source root holds the FSD layers (run add-fsd-lite first). Omitted: the workspace's one frontend application; with more than one, the run is refused with the candidates listed"
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
"enum": [
|
|
20
20
|
"react-vite",
|
|
21
21
|
"next",
|
|
22
|
+
"expo",
|
|
23
|
+
"generic",
|
|
22
24
|
"none"
|
|
23
25
|
],
|
|
24
|
-
"description": "Target frontend; 'none' is a backend-only workspace (needs --backend; FSD is skipped). Recorded in toolkit.config.json; omitted: the recorded one (required when none is recorded)"
|
|
26
|
+
"description": "Target frontend; 'generic' is a framework nx-archweave does not recognise (common FSD rules, no renamed layers); 'none' is a backend-only workspace (needs --backend; FSD is skipped). Recorded in toolkit.config.json; omitted: the recorded one (required when none is recorded)"
|
|
25
27
|
},
|
|
26
28
|
"backend": {
|
|
27
29
|
"type": "string",
|
|
@@ -74,10 +76,19 @@
|
|
|
74
76
|
"default": false,
|
|
75
77
|
"description": "Preview the full pipeline: zero writes, and the post-run validation callback never runs"
|
|
76
78
|
},
|
|
79
|
+
"project": {
|
|
80
|
+
"oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }],
|
|
81
|
+
"description": "The frontend app(s) the FSD section sets up, comma-separated (web,admin). Needed when the workspace has more than one frontend; each gets the rule and is recorded in toolkit.config.json's frontends"
|
|
82
|
+
},
|
|
83
|
+
"strict": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"default": false,
|
|
86
|
+
"description": "Fail the run (non-zero exit) when a section aborts or is refused. Files the other sections wrote stay; the report lists them. Without it init always succeeds and reports"
|
|
87
|
+
},
|
|
77
88
|
"allowDirty": {
|
|
78
89
|
"type": "boolean",
|
|
79
90
|
"default": false,
|
|
80
|
-
"description": "Run even when the working tree has uncommitted changes. The report then warns that
|
|
91
|
+
"description": "Run even when the working tree has uncommitted changes. The report then warns that restoring the files init modified also reverts those pre-existing changes. Used by create-nx-archweave, whose own plugin-install step dirties the fresh workspace right before init runs"
|
|
81
92
|
},
|
|
82
93
|
"applyEslint": {
|
|
83
94
|
"type": "boolean",
|