@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
|
@@ -247,33 +247,6 @@ function recordMetadataFor(entry) {
|
|
|
247
247
|
return { source: claude.marketplace, repository: entry.repository, method: CLAUDE_MARKETPLACE_METHOD };
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// ../integrations/dist/catalog/resolve-version.js
|
|
251
|
-
import { get } from "node:https";
|
|
252
|
-
function resolveVersionFromNpmRegistry(npmPackage) {
|
|
253
|
-
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(npmPackage)}`;
|
|
254
|
-
return new Promise((resolve2, reject) => {
|
|
255
|
-
get(registryUrl, (response) => {
|
|
256
|
-
let body = "";
|
|
257
|
-
response.on("data", (chunk) => {
|
|
258
|
-
body += chunk;
|
|
259
|
-
});
|
|
260
|
-
response.on("end", () => {
|
|
261
|
-
try {
|
|
262
|
-
const parsed = JSON.parse(body);
|
|
263
|
-
const latest = parsed?.["dist-tags"]?.["latest"];
|
|
264
|
-
if (typeof latest !== "string") {
|
|
265
|
-
reject(new Error(`Could not resolve the latest version of ${npmPackage} from the npm registry.`));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
resolve2(latest);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}).on("error", reject);
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
250
|
// ../integrations/dist/hosts/claude-code/detect.js
|
|
278
251
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
279
252
|
import { join as join3 } from "node:path";
|
|
@@ -314,14 +287,14 @@ var WORKSPACE_FILES_BY_SCOPE = {
|
|
|
314
287
|
local: [".claude/settings.local.json"]
|
|
315
288
|
};
|
|
316
289
|
var DEFAULT_PLUGIN_SCOPE = "project";
|
|
317
|
-
function createClaudeMarketplacePlan(entry
|
|
290
|
+
function createClaudeMarketplacePlan(entry) {
|
|
318
291
|
return async (options) => {
|
|
319
292
|
const distribution = entry.hosts["claude-code"];
|
|
320
293
|
if (options.targetEnvironment !== "claude-code" || distribution === void 0) {
|
|
321
294
|
const supported = Object.entries(entry.hosts).filter(([, value]) => value !== void 0).map(([host]) => host).join(", ");
|
|
322
295
|
throw new Error(`Target environment "${options.targetEnvironment}" is not supported by this integration. Supported: ${supported}.`);
|
|
323
296
|
}
|
|
324
|
-
const resolvedVersion = options.versionRange ??
|
|
297
|
+
const resolvedVersion = options.versionRange ?? VERSION_DECIDED_BY_MARKETPLACE;
|
|
325
298
|
const scope = options.scope ?? DEFAULT_PLUGIN_SCOPE;
|
|
326
299
|
return {
|
|
327
300
|
officialCommand: {
|
|
@@ -752,10 +725,6 @@ function createClaudeMarketplaceVerifier(entry, deps) {
|
|
|
752
725
|
}
|
|
753
726
|
|
|
754
727
|
// ../integrations/dist/catalog/create-catalog-integration.js
|
|
755
|
-
function defaultResolveVersion(entry) {
|
|
756
|
-
const npmPackage = entry.npmPackage;
|
|
757
|
-
return npmPackage === void 0 ? async () => VERSION_DECIDED_BY_MARKETPLACE : () => resolveVersionFromNpmRegistry(npmPackage);
|
|
758
|
-
}
|
|
759
728
|
function createCatalogIntegration(entry, deps) {
|
|
760
729
|
return {
|
|
761
730
|
id: entry.id,
|
|
@@ -765,7 +734,7 @@ function createCatalogIntegration(entry, deps) {
|
|
|
765
734
|
license: entry.license,
|
|
766
735
|
supportedEnvironments: PLUGIN_HOSTS.filter((host) => entry.hosts[host] !== void 0),
|
|
767
736
|
detect: (context) => detectWorkspacePackage(entry, context),
|
|
768
|
-
plan: createClaudeMarketplacePlan(entry
|
|
737
|
+
plan: createClaudeMarketplacePlan(entry),
|
|
769
738
|
install: createClaudeMarketplaceInstall(entry, { runner: deps.runner }),
|
|
770
739
|
verify: createClaudeMarketplaceVerifier(entry, { runner: deps.runner }),
|
|
771
740
|
uninstallInstructions: async () => [...entry.uninstallSteps]
|
|
@@ -1074,7 +1043,7 @@ var NodeCommandRunner = class {
|
|
|
1074
1043
|
// src/runtime/integrations-composition-root.ts
|
|
1075
1044
|
function createIntegrationsCompositionRoot(deps = {}) {
|
|
1076
1045
|
const runner = deps.runner ?? new NodeCommandRunner();
|
|
1077
|
-
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner
|
|
1046
|
+
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner }));
|
|
1078
1047
|
return {
|
|
1079
1048
|
registry,
|
|
1080
1049
|
runner,
|
|
@@ -1110,8 +1079,7 @@ async function runInstallIntegration(tree, options, deps = {}) {
|
|
|
1110
1079
|
const plan = await integration.plan({
|
|
1111
1080
|
targetEnvironment: target,
|
|
1112
1081
|
versionRange: options.versionRange,
|
|
1113
|
-
scope
|
|
1114
|
-
resolveLatestVersion: false
|
|
1082
|
+
scope
|
|
1115
1083
|
});
|
|
1116
1084
|
const pre = await verifyViaClaudeCli(root.runner, pluginId, tree.root);
|
|
1117
1085
|
let currentState;
|
|
@@ -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("");
|
|
@@ -221,33 +221,6 @@ function recordMetadataFor(entry) {
|
|
|
221
221
|
return { source: claude.marketplace, repository: entry.repository, method: CLAUDE_MARKETPLACE_METHOD };
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// ../integrations/dist/catalog/resolve-version.js
|
|
225
|
-
import { get } from "node:https";
|
|
226
|
-
function resolveVersionFromNpmRegistry(npmPackage) {
|
|
227
|
-
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(npmPackage)}`;
|
|
228
|
-
return new Promise((resolve2, reject) => {
|
|
229
|
-
get(registryUrl, (response) => {
|
|
230
|
-
let body = "";
|
|
231
|
-
response.on("data", (chunk) => {
|
|
232
|
-
body += chunk;
|
|
233
|
-
});
|
|
234
|
-
response.on("end", () => {
|
|
235
|
-
try {
|
|
236
|
-
const parsed = JSON.parse(body);
|
|
237
|
-
const latest = parsed?.["dist-tags"]?.["latest"];
|
|
238
|
-
if (typeof latest !== "string") {
|
|
239
|
-
reject(new Error(`Could not resolve the latest version of ${npmPackage} from the npm registry.`));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
resolve2(latest);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
}).on("error", reject);
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
224
|
// ../integrations/dist/hosts/claude-code/detect.js
|
|
252
225
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
253
226
|
import { join as join2 } from "node:path";
|
|
@@ -288,14 +261,14 @@ var WORKSPACE_FILES_BY_SCOPE = {
|
|
|
288
261
|
local: [".claude/settings.local.json"]
|
|
289
262
|
};
|
|
290
263
|
var DEFAULT_PLUGIN_SCOPE = "project";
|
|
291
|
-
function createClaudeMarketplacePlan(entry
|
|
264
|
+
function createClaudeMarketplacePlan(entry) {
|
|
292
265
|
return async (options) => {
|
|
293
266
|
const distribution = entry.hosts["claude-code"];
|
|
294
267
|
if (options.targetEnvironment !== "claude-code" || distribution === void 0) {
|
|
295
268
|
const supported = Object.entries(entry.hosts).filter(([, value]) => value !== void 0).map(([host]) => host).join(", ");
|
|
296
269
|
throw new Error(`Target environment "${options.targetEnvironment}" is not supported by this integration. Supported: ${supported}.`);
|
|
297
270
|
}
|
|
298
|
-
const resolvedVersion = options.versionRange ??
|
|
271
|
+
const resolvedVersion = options.versionRange ?? VERSION_DECIDED_BY_MARKETPLACE;
|
|
299
272
|
const scope = options.scope ?? DEFAULT_PLUGIN_SCOPE;
|
|
300
273
|
return {
|
|
301
274
|
officialCommand: {
|
|
@@ -713,10 +686,6 @@ function createClaudeMarketplaceVerifier(entry, deps) {
|
|
|
713
686
|
}
|
|
714
687
|
|
|
715
688
|
// ../integrations/dist/catalog/create-catalog-integration.js
|
|
716
|
-
function defaultResolveVersion(entry) {
|
|
717
|
-
const npmPackage = entry.npmPackage;
|
|
718
|
-
return npmPackage === void 0 ? async () => VERSION_DECIDED_BY_MARKETPLACE : () => resolveVersionFromNpmRegistry(npmPackage);
|
|
719
|
-
}
|
|
720
689
|
function createCatalogIntegration(entry, deps) {
|
|
721
690
|
return {
|
|
722
691
|
id: entry.id,
|
|
@@ -726,7 +695,7 @@ function createCatalogIntegration(entry, deps) {
|
|
|
726
695
|
license: entry.license,
|
|
727
696
|
supportedEnvironments: PLUGIN_HOSTS.filter((host) => entry.hosts[host] !== void 0),
|
|
728
697
|
detect: (context) => detectWorkspacePackage(entry, context),
|
|
729
|
-
plan: createClaudeMarketplacePlan(entry
|
|
698
|
+
plan: createClaudeMarketplacePlan(entry),
|
|
730
699
|
install: createClaudeMarketplaceInstall(entry, { runner: deps.runner }),
|
|
731
700
|
verify: createClaudeMarketplaceVerifier(entry, { runner: deps.runner }),
|
|
732
701
|
uninstallInstructions: async () => [...entry.uninstallSteps]
|
|
@@ -803,7 +772,7 @@ var NodeCommandRunner = class {
|
|
|
803
772
|
// src/runtime/integrations-composition-root.ts
|
|
804
773
|
function createIntegrationsCompositionRoot(deps = {}) {
|
|
805
774
|
const runner = deps.runner ?? new NodeCommandRunner();
|
|
806
|
-
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner
|
|
775
|
+
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner }));
|
|
807
776
|
return {
|
|
808
777
|
registry,
|
|
809
778
|
runner,
|
|
@@ -858,7 +827,7 @@ async function recordIntegrationGenerator(tree, options, deps = {}) {
|
|
|
858
827
|
if (!metadata) {
|
|
859
828
|
throw new Error(`No manifest metadata is defined for "${options.tool}".`);
|
|
860
829
|
}
|
|
861
|
-
await integration.plan({ targetEnvironment: target
|
|
830
|
+
await integration.plan({ targetEnvironment: target });
|
|
862
831
|
const outcome = await runRecordPipeline({
|
|
863
832
|
id: integration.id,
|
|
864
833
|
runner: root.runner,
|