@clearlydev/nx-archweave 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/bin/review-verdict.js +5 -248
  2. package/dist/eslint/index.js +28 -321
  3. package/dist/generators/_chunks/chunk-2WSX5YNB.js +274 -0
  4. package/dist/generators/_chunks/chunk-34V6HAJT.js +55 -0
  5. package/dist/generators/_chunks/chunk-3A6RZUCZ.js +114 -0
  6. package/dist/generators/_chunks/chunk-47T7NOPP.js +30 -0
  7. package/dist/generators/_chunks/chunk-5MI3O4VZ.js +99 -0
  8. package/dist/generators/_chunks/chunk-67INEVWL.js +1068 -0
  9. package/dist/generators/_chunks/chunk-6ZTIUBB2.js +139 -0
  10. package/dist/generators/_chunks/chunk-AIJCJ7JK.js +203 -0
  11. package/dist/generators/_chunks/chunk-AQHV7DJS.js +656 -0
  12. package/dist/generators/_chunks/chunk-C2VVUWBA.js +386 -0
  13. package/dist/generators/_chunks/chunk-EXYZLL7A.js +234 -0
  14. package/dist/generators/_chunks/chunk-FQHFT5IH.js +50 -0
  15. package/dist/generators/_chunks/chunk-H222FPLK.js +80 -0
  16. package/dist/generators/_chunks/chunk-IIDOBI42.js +616 -0
  17. package/dist/generators/_chunks/chunk-JHRTVURZ.js +189 -0
  18. package/dist/generators/_chunks/chunk-L2RZ2LBO.js +132 -0
  19. package/dist/generators/_chunks/chunk-MQNQJOWA.js +217 -0
  20. package/dist/generators/_chunks/chunk-QQNDYJTR.js +243 -0
  21. package/dist/generators/_chunks/chunk-SFXU6IUS.js +0 -0
  22. package/dist/generators/_chunks/chunk-SNBIVIMI.js +89 -0
  23. package/dist/generators/_chunks/chunk-TUN34LAQ.js +8 -0
  24. package/dist/generators/_chunks/chunk-TXAQ2GJC.js +20 -0
  25. package/dist/generators/_chunks/chunk-U6LAFHSF.js +49 -0
  26. package/dist/generators/_chunks/chunk-URV6FLUH.js +73 -0
  27. package/dist/generators/_chunks/chunk-YNMLB4DL.js +25 -0
  28. package/dist/generators/_chunks/chunk-YY2UESLM.js +282 -0
  29. package/dist/generators/add-agent-rules/generator.js +11 -842
  30. package/dist/generators/add-backend/generator.js +11 -906
  31. package/dist/generators/add-fsd-lite/generator.js +12 -801
  32. package/dist/generators/add-github-templates/generator.js +13 -116
  33. package/dist/generators/add-integration/generator.js +15 -743
  34. package/dist/generators/add-review/generator.js +12 -1231
  35. package/dist/generators/allow-builds/generator.js +7 -71
  36. package/dist/generators/doctor/generator.js +74 -2042
  37. package/dist/generators/entity/generator.js +15 -604
  38. package/dist/generators/feature/generator.js +15 -604
  39. package/dist/generators/init/generator.js +68 -3376
  40. package/dist/generators/install-integration/generator.js +35 -871
  41. package/dist/generators/page/generator.js +15 -604
  42. package/dist/generators/record-integration/generator.js +12 -778
  43. package/dist/generators/shared/generator.js +15 -604
  44. package/dist/generators/widget/generator.js +15 -604
  45. package/dist/index.js +5 -663
  46. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ import {
2
+ composeReviewSetup,
3
+ judgeReviewFileConflict
4
+ } from "./chunk-67INEVWL.js";
5
+ import {
6
+ DEPENDABOT_PATH,
7
+ renderAddReviewReport,
8
+ renderLineDiff
9
+ } from "./chunk-3A6RZUCZ.js";
10
+ import {
11
+ generatorFilesDir,
12
+ renderTemplateFiles
13
+ } from "./chunk-YNMLB4DL.js";
14
+ import {
15
+ resolveDryRun
16
+ } from "./chunk-TUN34LAQ.js";
17
+ import {
18
+ applyJsonEdits,
19
+ treePackageManager
20
+ } from "./chunk-EXYZLL7A.js";
21
+
22
+ // src/generators/add-review/generator.ts
23
+ var CONFIG_PATH = "toolkit.config.json";
24
+ function deriveAltWorkflowPath(mainWorkflowPath) {
25
+ return mainWorkflowPath.replace(/\.yml$/, ".archweave.yml");
26
+ }
27
+ function readConfig(tree) {
28
+ const raw = tree.read(CONFIG_PATH, "utf-8");
29
+ if (raw === null) return { raw: null, parsed: null, error: null };
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
33
+ return { raw, parsed: null, error: `${CONFIG_PATH} must contain a top-level JSON object` };
34
+ }
35
+ return { raw, parsed, error: null };
36
+ } catch {
37
+ return { raw, parsed: null, error: `${CONFIG_PATH} is not valid JSON` };
38
+ }
39
+ }
40
+ function resolveLocale(option, config) {
41
+ if (option) return option;
42
+ const fromConfig = config.parsed?.["locale"];
43
+ return fromConfig === "ko" || fromConfig === "en" ? fromConfig : "en";
44
+ }
45
+ function deepEqual(a, b) {
46
+ if (a === b) return true;
47
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
48
+ if (Array.isArray(a) || Array.isArray(b)) {
49
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
50
+ }
51
+ const aKeys = Object.keys(a).sort();
52
+ const bKeys = Object.keys(b).sort();
53
+ if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
54
+ return aKeys.every((key) => deepEqual(a[key], b[key]));
55
+ }
56
+ var PROVIDERS = ["claude", "gemini", "gpt", "cursor"];
57
+ var BETA_PROVIDERS = ["gemini", "gpt", "cursor"];
58
+ var MODES = ["fast", "standard", "thorough"];
59
+ function recordedReview(config) {
60
+ const review = config?.["review"];
61
+ if (typeof review !== "object" || review === null || Array.isArray(review)) return {};
62
+ const record = review;
63
+ const pick = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : void 0;
64
+ const rounds = record["maxRounds"];
65
+ return {
66
+ provider: pick(record["provider"], PROVIDERS),
67
+ mode: pick(record["mode"], MODES),
68
+ maxRounds: rounds === 1 || rounds === 2 || rounds === 3 || rounds === 4 || rounds === 5 ? rounds : void 0
69
+ };
70
+ }
71
+ function runAddReview(tree, options) {
72
+ const config = readConfig(tree);
73
+ const detectedPm = treePackageManager(tree, config.parsed);
74
+ const packageManager = options.packageManager ?? detectedPm ?? "pnpm";
75
+ const packageManagerNote = options.packageManager === void 0 && detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm lockfile) \u2014 the workflow was rendered for pnpm; re-run with --packageManager=npm|yarn|bun if that is wrong." : void 0;
76
+ const recorded = recordedReview(config.parsed);
77
+ const provider = options.provider ?? recorded.provider ?? "claude";
78
+ const plan = composeReviewSetup({
79
+ enabled: true,
80
+ provider,
81
+ mode: options.mode ?? recorded.mode ?? "standard",
82
+ locale: resolveLocale(options.locale, config),
83
+ maxRounds: options.maxRounds ?? recorded.maxRounds ?? 3,
84
+ packageManager
85
+ });
86
+ if (plan === null) throw new Error("unreachable: add-review always composes with enabled: true");
87
+ if (config.error !== null) {
88
+ return {
89
+ dryRun: options.dryRun ?? false,
90
+ workflowPath: plan.workflowFilePath,
91
+ workflowAction: "aborted",
92
+ configAction: "aborted",
93
+ requiredSecretName: plan.requiredSecretName,
94
+ diff: null,
95
+ workflowYaml: null,
96
+ configSnippetJson: null,
97
+ configError: config.error,
98
+ dependabot: "not-applied",
99
+ dependabotProposal: null
100
+ };
101
+ }
102
+ const existing = tree.read(plan.workflowFilePath, "utf-8");
103
+ const verdict = judgeReviewFileConflict(existing, plan.workflowYaml);
104
+ let workflowAction;
105
+ let workflowPath = plan.workflowFilePath;
106
+ let writeWorkflowTo = null;
107
+ let diff = null;
108
+ let mainFileMatchesOptions;
109
+ if (verdict.kind === "create") {
110
+ workflowAction = "created";
111
+ writeWorkflowTo = plan.workflowFilePath;
112
+ mainFileMatchesOptions = true;
113
+ } else if (verdict.kind === "unchanged") {
114
+ workflowAction = "unchanged";
115
+ mainFileMatchesOptions = true;
116
+ } else {
117
+ switch (options.onConflict) {
118
+ case "overwrite":
119
+ workflowAction = "overwritten";
120
+ writeWorkflowTo = plan.workflowFilePath;
121
+ mainFileMatchesOptions = true;
122
+ break;
123
+ case "skip":
124
+ workflowAction = "skipped";
125
+ mainFileMatchesOptions = false;
126
+ break;
127
+ case "alternate-filename": {
128
+ const altWorkflowPath = deriveAltWorkflowPath(plan.workflowFilePath);
129
+ workflowAction = "alternate";
130
+ workflowPath = altWorkflowPath;
131
+ writeWorkflowTo = altWorkflowPath;
132
+ mainFileMatchesOptions = false;
133
+ break;
134
+ }
135
+ default:
136
+ workflowAction = "proposed";
137
+ diff = renderLineDiff(verdict.existing, verdict.generated);
138
+ mainFileMatchesOptions = false;
139
+ }
140
+ }
141
+ let configAction;
142
+ let newConfigContent = null;
143
+ if (!mainFileMatchesOptions) {
144
+ configAction = "not-updated";
145
+ } else {
146
+ const existingReview = config.parsed?.["review"];
147
+ if (deepEqual(existingReview, plan.configSnippet)) {
148
+ configAction = "unchanged";
149
+ } else {
150
+ configAction = config.raw === null ? "created" : "updated";
151
+ newConfigContent = applyJsonEdits(config.raw, [[["review"], plan.configSnippet]]);
152
+ }
153
+ }
154
+ const [dependabotFile] = renderTemplateFiles(generatorFilesDir("add-review", import.meta.url), ".github", {});
155
+ const existingDependabot = tree.read(DEPENDABOT_PATH, "utf-8");
156
+ const dependabot = existingDependabot === null ? "created" : /github-actions/.test(existingDependabot) ? "present" : "proposed";
157
+ const dryRun = options.dryRun ?? false;
158
+ if (!dryRun) {
159
+ if (writeWorkflowTo !== null) tree.write(writeWorkflowTo, plan.workflowYaml);
160
+ if (newConfigContent !== null) tree.write(CONFIG_PATH, newConfigContent);
161
+ if (dependabot === "created") tree.write(DEPENDABOT_PATH, dependabotFile.content);
162
+ }
163
+ return {
164
+ dryRun,
165
+ workflowPath,
166
+ workflowAction,
167
+ configAction,
168
+ requiredSecretName: plan.requiredSecretName,
169
+ diff,
170
+ workflowYaml: dryRun ? plan.workflowYaml : null,
171
+ configSnippetJson: dryRun ? JSON.stringify(plan.configSnippet, null, 2) : null,
172
+ configError: null,
173
+ dependabot,
174
+ dependabotProposal: dependabot === "proposed" ? dependabotFile.content : null,
175
+ ...packageManagerNote !== void 0 ? { packageManagerNote } : {},
176
+ ...BETA_PROVIDERS.includes(provider) ? { betaProvider: provider } : {}
177
+ };
178
+ }
179
+ async function addReviewGenerator(tree, options) {
180
+ console.log(renderAddReviewReport(runAddReview(tree, { ...options, dryRun: resolveDryRun(options) })));
181
+ }
182
+ var generator_default = addReviewGenerator;
183
+
184
+ export {
185
+ recordedReview,
186
+ runAddReview,
187
+ addReviewGenerator,
188
+ generator_default
189
+ };
@@ -0,0 +1,132 @@
1
+ import {
2
+ maskComments
3
+ } from "./chunk-6ZTIUBB2.js";
4
+
5
+ // ../templates/dist/fsd/fsd-eslint.js
6
+ var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
7
+ function buildFsdEslintImportLine() {
8
+ return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
9
+ }
10
+ function buildFsdEslintEntry(severity, root = "src", framework = {}) {
11
+ const list = (key, values) => values === void 0 || values.length === 0 ? "" : `, ${key}: [${values.map((value) => `'${value}'`).join(", ")}]`;
12
+ return `...fsdBoundaries({ severity: '${severity}', cwd: import.meta.dirname, root: '${root}'${list("platformSuffixes", framework.platformSuffixes)}${list("routerDirs", framework.routerDirs)} }),`;
13
+ }
14
+ function buildFsdEslintSnippet({ severity, locale, root, framework }) {
15
+ const lines = locale === "ko" ? [
16
+ "// nx-archweave FSD \uB808\uC774\uC5B4 \uADDC\uCE59 \u2014 \uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC758 eslint.config.mjs\uC5D0 \uC9C1\uC811 \uB123\uC73C\uC138\uC694:",
17
+ "// 1) \uB9C8\uC9C0\uB9C9 import \uB4A4\uC5D0 \uC544\uB798 import\uB97C,",
18
+ "// 2) \uC124\uC815 \uBC30\uC5F4(export default [...], \uB610\uB294 export default \uD558\uB294 \uBCC0\uC218\uC758 \uBC30\uC5F4)\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
19
+ ] : [
20
+ "// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
21
+ "// 1) the import after the last import,",
22
+ "// 2) the spread as the last element of the config array (export default [...], or the array your config exports by name)."
23
+ ];
24
+ return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity, root, framework)].join("\n");
25
+ }
26
+ var LAYER_GUIDE_EN = {
27
+ app: "composition root: providers, routing, global styles (the framework entry point lives here)",
28
+ pages: "one slice per screen, composed from the layers below",
29
+ features: "one slice per user-facing capability (add-to-cart, sign-in)",
30
+ entities: "one slice per business object (user, product)",
31
+ shared: "segments, not slices: ui, api, lib, model, config \u2014 named by purpose"
32
+ };
33
+ var LAYER_GUIDE_KO = {
34
+ app: "\uC870\uD569 \uB8E8\uD2B8: providers, \uB77C\uC6B0\uD305, \uC804\uC5ED \uC2A4\uD0C0\uC77C (\uD504\uB808\uC784\uC6CC\uD06C \uC9C4\uC785\uC810\uC774 \uC5EC\uAE30 \uC788\uC74C)",
35
+ pages: "\uD654\uBA74 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 \u2014 \uC544\uB798 \uB808\uC774\uC5B4\uB4E4\uC744 \uC870\uD569",
36
+ features: "\uC0AC\uC6A9\uC790 \uAE30\uB2A5 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (add-to-cart, sign-in)",
37
+ entities: "\uBE44\uC988\uB2C8\uC2A4 \uAC1D\uCCB4 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (user, product)",
38
+ shared: "\uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8: ui, api, lib, model, config \u2014 \uBAA9\uC801\uC73C\uB85C \uC774\uB984 \uC9D3\uAE30"
39
+ };
40
+ function buildFsdLayerGuide({ layers, directories, sourceRoot, locale }) {
41
+ const guide = locale === "ko" ? LAYER_GUIDE_KO : LAYER_GUIDE_EN;
42
+ const width = Math.max(...directories.map((d) => d.length));
43
+ const rows = layers.map((layer, index) => ` ${sourceRoot}/${directories[index].padEnd(width)} ${guide[layer] ?? ""}`);
44
+ const footer = locale === "ko" ? [
45
+ "\uB808\uC774\uC5B4 \uD3F4\uB354\uB294 \uCCAB \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC0DD\uAE38 \uB54C \uB9CC\uB4E4\uC5B4\uC9D1\uB2C8\uB2E4 (\uC815\uC2DD FSD):",
46
+ " nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity, shared\uB3C4 \uAC19\uC740 \uBC29\uC2DD; --segments=ui,api)",
47
+ " widgets/ \uB808\uC774\uC5B4\uB294 \uAE30\uBCF8 \uBC94\uC704 \uBC16\uC774\uC9C0\uB9CC nx g @clearlydev/nx-archweave:widget \uC73C\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
48
+ "\uC704\uC5D0\uC11C \uC544\uB798\uB85C\uB9CC import\uD560 \uC218 \uC788\uACE0, \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC\uB294 import\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2AC\uB77C\uC774\uC2A4\uB294",
49
+ "index.ts\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD569\uB2C8\uB2E4 \u2014 archweave/fsd-boundaries ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC90D\uB2C8\uB2E4."
50
+ ] : [
51
+ "Layer directories appear with their first slice (canonical FSD):",
52
+ " nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity and shared work the same way; --segments=ui,api)",
53
+ " widgets/ is outside the default scope but one command away: nx g @clearlydev/nx-archweave:widget",
54
+ "Imports go downward only, slices on one layer never import each other, and a slice is",
55
+ "imported through its index.ts \u2014 the archweave/fsd-boundaries ESLint rule reports violations."
56
+ ];
57
+ return [...rows, ...footer].join("\n");
58
+ }
59
+
60
+ // ../template-engine/dist/fsd/fsd-eslint-config.js
61
+ function classifyFsdEslintConfig(configText) {
62
+ return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
63
+ }
64
+ var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
65
+ var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
66
+ var CLOSING_BRACKET = /\]\s*;?\s*$/;
67
+ function closingBracketOf(masked, open) {
68
+ let depth = 0;
69
+ for (let i = open; i < masked.length; i++) {
70
+ const ch = masked[i];
71
+ if (ch === '"' || ch === "'" || ch === "`") {
72
+ for (i++; i < masked.length && masked[i] !== ch; i++)
73
+ if (masked[i] === "\\")
74
+ i++;
75
+ continue;
76
+ }
77
+ if (ch === "[")
78
+ depth++;
79
+ else if (ch === "]" && --depth === 0)
80
+ return i;
81
+ }
82
+ return -1;
83
+ }
84
+ function namedConfigArrayEnd(masked) {
85
+ const exported = [...masked.matchAll(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/g)];
86
+ if (exported.length !== 1)
87
+ return -1;
88
+ const declaration = new RegExp(`(?:const|let|var)\\s+${exported[0][1].replace(/\$/g, "\\$")}\\s*=\\s*(?:defineConfig\\s*\\(\\s*)?\\[`).exec(masked);
89
+ if (declaration === null)
90
+ return -1;
91
+ return closingBracketOf(masked, declaration.index + declaration[0].length - 1);
92
+ }
93
+ function applyFsdEslintConfig(configText, severity, root = "src", framework = {}) {
94
+ if (classifyFsdEslintConfig(configText) === "applied")
95
+ return { status: "already-applied" };
96
+ const masked = maskComments(configText);
97
+ EXPORT_DEFAULT_ARRAY.lastIndex = 0;
98
+ const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
99
+ const closing = CLOSING_BRACKET.exec(masked);
100
+ const closingIndex = exportMatches.length === 1 && closing !== null ? closing.index : namedConfigArrayEnd(masked);
101
+ if (closingIndex === -1)
102
+ return { status: "unrecognized" };
103
+ const eol = configText.includes("\r\n") ? "\r\n" : "\n";
104
+ const before = configText.slice(0, closingIndex);
105
+ const lastNonSpace = before.replace(/\s+$/, "");
106
+ const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
107
+ const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity, root, framework)}${eol}`;
108
+ const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
109
+ IMPORT_STATEMENT.lastIndex = 0;
110
+ let lastImportEnd = -1;
111
+ const maskedWithEntry = maskComments(withEntry);
112
+ for (let m = IMPORT_STATEMENT.exec(maskedWithEntry); m !== null; m = IMPORT_STATEMENT.exec(maskedWithEntry)) {
113
+ lastImportEnd = m.index + m[0].length;
114
+ }
115
+ const importLine = buildFsdEslintImportLine();
116
+ const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
117
+ return { status: "applied", text };
118
+ }
119
+ function buildFsdEslintPasteSnippet(severity, locale, root = "src", framework = {}) {
120
+ return buildFsdEslintSnippet({ severity, locale, root, framework });
121
+ }
122
+ function composeFsdLayerGuide({ profile, sourceRoot, locale }) {
123
+ const layers = [...profile.layers];
124
+ return buildFsdLayerGuide({ layers, directories: layers.map((layer) => profile.layerDirs[layer] ?? layer), sourceRoot, locale });
125
+ }
126
+
127
+ export {
128
+ classifyFsdEslintConfig,
129
+ applyFsdEslintConfig,
130
+ buildFsdEslintPasteSnippet,
131
+ composeFsdLayerGuide
132
+ };
@@ -0,0 +1,217 @@
1
+ import {
2
+ globToRegExp,
3
+ resolveFsdPolicy
4
+ } from "./chunk-5MI3O4VZ.js";
5
+
6
+ // src/eslint/fsd-boundaries.ts
7
+ import { posix, relative, resolve, sep } from "node:path";
8
+
9
+ // src/eslint/slice-groups.ts
10
+ import { readdirSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ var SEGMENTS = [
13
+ "ui",
14
+ "model",
15
+ "api",
16
+ "lib",
17
+ "config",
18
+ "components",
19
+ "hooks",
20
+ "types",
21
+ "utils",
22
+ "helpers",
23
+ "constants",
24
+ "consts",
25
+ "store",
26
+ "stores",
27
+ "styles",
28
+ "assets",
29
+ "services",
30
+ "context"
31
+ ];
32
+ var IGNORED = /^(\.|readme(\.|$))/i;
33
+ var listings = /* @__PURE__ */ new Map();
34
+ function list(dir) {
35
+ let mtimeMs;
36
+ try {
37
+ mtimeMs = statSync(dir).mtimeMs;
38
+ } catch {
39
+ listings.delete(dir);
40
+ return null;
41
+ }
42
+ const cached = listings.get(dir);
43
+ if (cached !== void 0 && cached.mtimeMs === mtimeMs) return cached.entries;
44
+ try {
45
+ const entries = readdirSync(dir, { withFileTypes: true }).map((entry) => ({ name: entry.name, directory: entry.isDirectory() }));
46
+ listings.set(dir, { mtimeMs, entries });
47
+ return entries;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function createGroupDetector(rootAbsolute, policy, isIndexFile, listDirectory = list) {
53
+ const groups = policy.sliceGroups;
54
+ if (groups === false) return () => false;
55
+ const segments = /* @__PURE__ */ new Set([...SEGMENTS, ...policy.segments]);
56
+ return (layerDir, name) => {
57
+ const key = `${layerDir}/${name}`;
58
+ if (groups.include.includes(key)) return true;
59
+ if (groups.exclude.includes(key) || !groups.auto) return false;
60
+ const entries = listDirectory(join(rootAbsolute, layerDir, name))?.filter((entry) => !IGNORED.test(entry.name)) ?? null;
61
+ if (entries === null || entries.length === 0) return false;
62
+ if (entries.some((entry) => !entry.directory || segments.has(entry.name))) return false;
63
+ return entries.some((entry) => listDirectory(join(rootAbsolute, layerDir, name, entry.name))?.some((child) => !child.directory && isIndexFile(child.name)) ?? false);
64
+ };
65
+ }
66
+
67
+ // src/eslint/fsd-boundaries.ts
68
+ var LAYER_ORDER = ["app", "pages", "widgets", "features", "entities", "shared"];
69
+ var LAYER_BY_DIR = {
70
+ app: "app",
71
+ _app: "app",
72
+ pages: "pages",
73
+ _pages: "pages",
74
+ widgets: "widgets",
75
+ features: "features",
76
+ entities: "entities",
77
+ shared: "shared"
78
+ };
79
+ var SLICED_LAYERS = /* @__PURE__ */ new Set(["pages", "widgets", "features", "entities"]);
80
+ function indexFilePattern(platformSuffixes) {
81
+ const platforms = platformSuffixes.length === 0 ? "" : `(\\.(${platformSuffixes.map((suffix) => suffix.replace(/[^a-z0-9-]/gi, "")).join("|")}))?`;
82
+ return new RegExp(`^index${platforms}(\\.[cm]?[jt]sx?)?$`);
83
+ }
84
+ var DEFAULT_POLICY = resolveFsdPolicy(void 0);
85
+ var NO_GROUPS = () => false;
86
+ function locate(rootRelativePath, policy = DEFAULT_POLICY, isGroup = NO_GROUPS) {
87
+ const segments = rootRelativePath.split("/").filter((s) => s.length > 0 && s !== ".");
88
+ if (segments.length === 0) return null;
89
+ const layer = policy.routerDirs.includes(segments[0]) ? "app" : LAYER_BY_DIR[segments[0]];
90
+ if (layer === void 0) return null;
91
+ let unit;
92
+ let rest;
93
+ if (layer === "app") {
94
+ unit = null;
95
+ rest = segments.slice(1);
96
+ } else if (layer === "shared") {
97
+ const component = policy.sharedPublicApi === "component" && segments.length > 2;
98
+ unit = segments[1] === void 0 ? null : component ? `${segments[1]}/${segments[2]}` : segments[1];
99
+ rest = policy.sharedPublicApi === "file" ? [] : segments.slice(component ? 3 : 2);
100
+ } else if (segments[1] !== void 0 && isGroup(segments[0], segments[1])) {
101
+ if (segments[2] === void 0) return { layer, unit: segments[1], rest: [], group: true };
102
+ unit = `${segments[1]}/${segments[2]}`;
103
+ rest = segments.slice(3);
104
+ } else {
105
+ unit = segments[1] ?? null;
106
+ rest = segments.slice(2);
107
+ }
108
+ if (rest.length > 0 && indexFilePattern(policy.platformSuffixes).test(rest[rest.length - 1])) rest.pop();
109
+ return { layer, unit, rest };
110
+ }
111
+ function judge(from, to) {
112
+ if (LAYER_ORDER.indexOf(to.layer) < LAYER_ORDER.indexOf(from.layer)) return { kind: "upward", from, to };
113
+ if (to.unit === null) return null;
114
+ if (to.group) return { kind: "group", from, to };
115
+ const sameUnit = from.layer === to.layer && from.unit === to.unit;
116
+ if (sameUnit) return null;
117
+ const importerSlice = from.unit?.split("/").pop();
118
+ const viaCrossImportApi = to.rest[0] === "@x" && to.rest[1] === importerSlice && from.layer === to.layer;
119
+ if (viaCrossImportApi) return null;
120
+ if (from.layer === to.layer && SLICED_LAYERS.has(to.layer)) return { kind: "cross-slice", from, to };
121
+ if (to.rest.length > 0) return { kind: "sidestep", from, to };
122
+ return null;
123
+ }
124
+ function describeLocation(location) {
125
+ return location.unit === null ? location.layer : `${location.layer}/${location.unit}`;
126
+ }
127
+ function resolveSpecifier(specifier, importerRootRelative, options) {
128
+ let rootRelative = null;
129
+ if (specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..") {
130
+ rootRelative = posix.normalize(posix.join(posix.dirname(importerRootRelative), specifier));
131
+ } else {
132
+ for (const [prefix, target] of Object.entries(options.aliases)) {
133
+ if (!specifier.startsWith(prefix)) continue;
134
+ const absolute = resolve(resolve(options.cwd, target), specifier.slice(prefix.length));
135
+ rootRelative = toPosix(relative(options.rootAbsolute, absolute)) || ".";
136
+ break;
137
+ }
138
+ }
139
+ if (rootRelative === null) return null;
140
+ if (rootRelative === ".." || rootRelative.startsWith("../")) return null;
141
+ return rootRelative;
142
+ }
143
+ function toPosix(path) {
144
+ return sep === "/" ? path : path.split(sep).join("/");
145
+ }
146
+ var fsdBoundariesRule = {
147
+ meta: {
148
+ type: "problem",
149
+ docs: {
150
+ description: "Enforce canonical Feature-Sliced Design import rules inside a frontend app",
151
+ url: "https://github.com/clearlydev/nx-archweave#fsd-lite"
152
+ },
153
+ schema: [
154
+ {
155
+ type: "object",
156
+ properties: {
157
+ root: { type: "string" },
158
+ aliases: { type: "object", additionalProperties: { type: "string" } },
159
+ policy: { type: "object" }
160
+ },
161
+ additionalProperties: false
162
+ }
163
+ ],
164
+ messages: {
165
+ upward: 'FSD: "{{from}}" may not import from "{{to}}" (a higher layer).',
166
+ crossSlice: 'FSD: "{{from}}" may not import a sibling slice "{{to}}" \u2014 move the shared part down a layer, or expose it through "{{to}}/@x/{{fromUnit}}".',
167
+ sidestep: 'FSD: import "{{to}}" through its public API (index.ts), not its internal file "{{path}}".',
168
+ group: 'FSD: "{{to}}" is a group of slices, not a slice \u2014 import one of its slices through its index.ts.'
169
+ }
170
+ },
171
+ create(context) {
172
+ const options = context.options[0] ?? {};
173
+ const root = options.root ?? "src";
174
+ const aliases = options.aliases ?? { "@/": root };
175
+ const cwd = context.cwd;
176
+ const rootAbsolute = resolve(cwd, root);
177
+ const importerRootRelative = toPosix(relative(rootAbsolute, context.filename));
178
+ if (importerRootRelative === "" || importerRootRelative.startsWith("..")) return {};
179
+ const policy = resolveFsdPolicy(void 0, options.policy ?? {});
180
+ const isIndexFile = (name) => indexFilePattern(policy.platformSuffixes).test(name);
181
+ const isGroup = createGroupDetector(rootAbsolute, policy, isIndexFile);
182
+ const from = locate(importerRootRelative, policy, isGroup);
183
+ if (from === null) return {};
184
+ const isTestFile = policy.testFiles !== false && policy.testFiles.some((glob) => globToRegExp(glob).test(importerRootRelative));
185
+ const check = (node, specifier) => {
186
+ if (typeof specifier !== "string") return;
187
+ const target = resolveSpecifier(specifier, importerRootRelative, { rootAbsolute, cwd, aliases });
188
+ if (target === null) return;
189
+ const to = locate(target, policy, isGroup);
190
+ if (to === null) return;
191
+ const violation = judge(from, to);
192
+ if (violation === null || isTestFile && violation.kind !== "upward") return;
193
+ const data = { from: describeLocation(from), to: describeLocation(to), fromUnit: from.unit?.split("/").pop() ?? "", path: target };
194
+ context.report({
195
+ node,
196
+ messageId: violation.kind === "cross-slice" ? "crossSlice" : violation.kind,
197
+ data
198
+ });
199
+ };
200
+ const fromSource = (node) => check(node, node.source?.value);
201
+ return {
202
+ ImportDeclaration: fromSource,
203
+ ExportNamedDeclaration: fromSource,
204
+ ExportAllDeclaration: fromSource,
205
+ ImportExpression: (node) => {
206
+ const argument = node.source;
207
+ if (argument && argument.type === "Literal") check(node, argument.value);
208
+ }
209
+ };
210
+ }
211
+ };
212
+
213
+ export {
214
+ createGroupDetector,
215
+ indexFilePattern,
216
+ fsdBoundariesRule
217
+ };