@clearlydev/nx-archweave 0.3.2 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/review-verdict.js +8 -249
  3. package/dist/eslint/index.js +28 -321
  4. package/dist/generators/_chunks/chunk-2WSX5YNB.js +274 -0
  5. package/dist/generators/_chunks/chunk-34V6HAJT.js +55 -0
  6. package/dist/generators/_chunks/chunk-3A6RZUCZ.js +114 -0
  7. package/dist/generators/_chunks/chunk-47T7NOPP.js +30 -0
  8. package/dist/generators/_chunks/chunk-4FJ7CLEU.js +189 -0
  9. package/dist/generators/_chunks/chunk-5MI3O4VZ.js +99 -0
  10. package/dist/generators/_chunks/chunk-6ZTIUBB2.js +139 -0
  11. package/dist/generators/_chunks/chunk-AIJCJ7JK.js +203 -0
  12. package/dist/generators/_chunks/chunk-ALSXKTLM.js +1098 -0
  13. package/dist/generators/_chunks/chunk-AQHV7DJS.js +656 -0
  14. package/dist/generators/_chunks/chunk-C2VVUWBA.js +386 -0
  15. package/dist/generators/_chunks/chunk-EXYZLL7A.js +234 -0
  16. package/dist/generators/_chunks/chunk-FQHFT5IH.js +50 -0
  17. package/dist/generators/_chunks/chunk-H222FPLK.js +80 -0
  18. package/dist/generators/_chunks/chunk-IIDOBI42.js +616 -0
  19. package/dist/generators/_chunks/chunk-L2RZ2LBO.js +132 -0
  20. package/dist/generators/_chunks/chunk-MQNQJOWA.js +217 -0
  21. package/dist/generators/_chunks/chunk-QQNDYJTR.js +243 -0
  22. package/dist/generators/_chunks/chunk-SFXU6IUS.js +0 -0
  23. package/dist/generators/_chunks/chunk-SNBIVIMI.js +89 -0
  24. package/dist/generators/_chunks/chunk-TUN34LAQ.js +8 -0
  25. package/dist/generators/_chunks/chunk-TXAQ2GJC.js +20 -0
  26. package/dist/generators/_chunks/chunk-U6LAFHSF.js +49 -0
  27. package/dist/generators/_chunks/chunk-URV6FLUH.js +73 -0
  28. package/dist/generators/_chunks/chunk-YNMLB4DL.js +25 -0
  29. package/dist/generators/_chunks/chunk-YY2UESLM.js +282 -0
  30. package/dist/generators/add-agent-rules/generator.js +11 -842
  31. package/dist/generators/add-backend/generator.js +11 -906
  32. package/dist/generators/add-fsd-lite/generator.js +12 -801
  33. package/dist/generators/add-github-templates/generator.js +13 -116
  34. package/dist/generators/add-integration/generator.js +15 -743
  35. package/dist/generators/add-review/generator.js +12 -1231
  36. package/dist/generators/allow-builds/generator.js +7 -71
  37. package/dist/generators/doctor/generator.js +106 -2044
  38. package/dist/generators/entity/generator.js +15 -604
  39. package/dist/generators/feature/generator.js +15 -604
  40. package/dist/generators/init/generator.js +68 -3376
  41. package/dist/generators/install-integration/generator.js +35 -871
  42. package/dist/generators/page/generator.js +15 -604
  43. package/dist/generators/record-integration/generator.js +12 -778
  44. package/dist/generators/shared/generator.js +15 -604
  45. package/dist/generators/widget/generator.js +15 -604
  46. package/dist/index.js +5 -663
  47. package/package.json +1 -1
@@ -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
+ };
@@ -0,0 +1,243 @@
1
+ import {
2
+ DEFAULT_BOUNDARY_TAGS
3
+ } from "./chunk-6ZTIUBB2.js";
4
+
5
+ // ../templates/dist/backend/shared-types.js
6
+ function sharedTypesIndexContent() {
7
+ return `export * from './lib/api-types.js';
8
+ `;
9
+ }
10
+ function sharedTypesApiTypesContent() {
11
+ return [
12
+ "// Types shared between the frontend and the backend live here.",
13
+ "// Example: the api app returns this from GET /api/health, and the",
14
+ "// frontend consumes the same shape \u2014 one definition, no drift.",
15
+ "export interface ApiHealthResponse {",
16
+ " status: 'ok';",
17
+ "}",
18
+ ""
19
+ ].join("\n");
20
+ }
21
+ function sharedTypesPackageJsonContent(packageName, sharedTag = "scope:shared") {
22
+ return `${JSON.stringify({
23
+ name: packageName,
24
+ version: "0.0.1",
25
+ private: true,
26
+ type: "module",
27
+ main: "./src/index.ts",
28
+ types: "./src/index.ts",
29
+ exports: {
30
+ ".": {
31
+ types: "./src/index.ts",
32
+ import: "./src/index.ts",
33
+ default: "./src/index.ts"
34
+ }
35
+ },
36
+ nx: {
37
+ projectType: "library",
38
+ tags: [sharedTag],
39
+ targets: {}
40
+ }
41
+ }, null, 2)}
42
+ `;
43
+ }
44
+
45
+ // ../template-engine/dist/backend/compose-backend-wiring.js
46
+ var DEFAULT_SHARED_TYPES_ROOT = "libs/shared-types";
47
+ function readSharedTypesConfig(config) {
48
+ const value = config?.["sharedTypes"];
49
+ if (value === false)
50
+ return false;
51
+ if (typeof value !== "object" || value === null)
52
+ return { path: DEFAULT_SHARED_TYPES_ROOT };
53
+ const record = value;
54
+ const path = typeof record["path"] === "string" && record["path"].trim() !== "" ? record["path"].replace(/\/+$/, "") : DEFAULT_SHARED_TYPES_ROOT;
55
+ const importPath = typeof record["importPath"] === "string" && record["importPath"].trim() !== "" ? record["importPath"] : void 0;
56
+ return importPath === void 0 ? { path } : { path, importPath };
57
+ }
58
+ function composeBackendWiring(input) {
59
+ const scoped = /^@([^/]+)\//.exec(input.workspaceName);
60
+ const scope = scoped ? `@${scoped[1]}` : `@${input.workspaceName.split("/").pop() || "workspace"}`;
61
+ const sharedTypesRoot = input.sharedTypes?.path ?? DEFAULT_SHARED_TYPES_ROOT;
62
+ const sharedTypesPackageName = input.sharedTypes?.importPath ?? `${scope}/shared-types`;
63
+ const tagByRole = input.tags ?? DEFAULT_BOUNDARY_TAGS;
64
+ return {
65
+ sharedTypesFiles: [
66
+ { filePath: `${sharedTypesRoot}/package.json`, content: sharedTypesPackageJsonContent(sharedTypesPackageName, tagByRole.shared) },
67
+ { filePath: `${sharedTypesRoot}/src/index.ts`, content: sharedTypesIndexContent() },
68
+ { filePath: `${sharedTypesRoot}/src/lib/api-types.ts`, content: sharedTypesApiTypesContent() }
69
+ ],
70
+ sharedTypesPackageName,
71
+ sharedTypesRoot,
72
+ tagByRole
73
+ };
74
+ }
75
+ function addSharedTypesNpmWorkspace(packageJsonText, entry = DEFAULT_SHARED_TYPES_ROOT) {
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(packageJsonText);
79
+ } catch {
80
+ return { status: "unrecognized" };
81
+ }
82
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
83
+ return { status: "unrecognized" };
84
+ const pkg = parsed;
85
+ const field = pkg["workspaces"];
86
+ let target;
87
+ if (field === void 0) {
88
+ target = [];
89
+ } else if (Array.isArray(field)) {
90
+ target = field;
91
+ } else if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
92
+ target = field.packages;
93
+ } else {
94
+ return { status: "unrecognized" };
95
+ }
96
+ const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
97
+ if (target.some((existing) => existing === entry || existing === parentGlob))
98
+ return { status: "present" };
99
+ const surgical = insertSharedTypesWorkspace(packageJsonText, field === void 0 ? "absent" : Array.isArray(field) ? "array" : "object", JSON.stringify(entry));
100
+ if (surgical !== null)
101
+ return { status: "added", text: surgical };
102
+ if (field === void 0)
103
+ pkg["workspaces"] = target;
104
+ target.push(entry);
105
+ const indent = /\n([ \t]+)"/.exec(packageJsonText)?.[1] ?? " ";
106
+ const trailing = packageJsonText.endsWith("\n") ? "\n" : "";
107
+ return { status: "added", text: `${JSON.stringify(pkg, null, indent)}${trailing}` };
108
+ }
109
+ function skipJsonValue(text, start) {
110
+ if (text[start] === '"') {
111
+ let i = start + 1;
112
+ while (i < text.length && text[i] !== '"')
113
+ i += text[i] === "\\" ? 2 : 1;
114
+ return i + 1;
115
+ }
116
+ if (text[start] === "[" || text[start] === "{") {
117
+ let depth = 0;
118
+ let i = start;
119
+ while (i < text.length) {
120
+ const c = text[i];
121
+ if (c === '"') {
122
+ i = skipJsonValue(text, i);
123
+ continue;
124
+ }
125
+ if (c === "[" || c === "{")
126
+ depth++;
127
+ if (c === "]" || c === "}") {
128
+ depth--;
129
+ if (depth === 0)
130
+ return i + 1;
131
+ }
132
+ i++;
133
+ }
134
+ return -1;
135
+ }
136
+ const literal = /^[^,}\]\s]+/.exec(text.slice(start));
137
+ return literal ? start + literal[0].length : -1;
138
+ }
139
+ function objectMembers(text, open) {
140
+ const members = [];
141
+ let i = open + 1;
142
+ for (; ; ) {
143
+ while (/[\s,]/.test(text[i] ?? ""))
144
+ i++;
145
+ if (text[i] === "}")
146
+ return members;
147
+ if (text[i] !== '"')
148
+ return null;
149
+ const keyEnd = skipJsonValue(text, i);
150
+ const key = JSON.parse(text.slice(i, keyEnd));
151
+ i = keyEnd;
152
+ while (/\s/.test(text[i] ?? ""))
153
+ i++;
154
+ if (text[i] !== ":")
155
+ return null;
156
+ i++;
157
+ while (/\s/.test(text[i] ?? ""))
158
+ i++;
159
+ const valueEnd = skipJsonValue(text, i);
160
+ if (valueEnd === -1)
161
+ return null;
162
+ members.push({ key, valueStart: i, valueEnd });
163
+ i = valueEnd;
164
+ }
165
+ }
166
+ function appendToArray(text, open, close, eol, quoted) {
167
+ const inner = text.slice(open + 1, close - 1);
168
+ if (inner.trim() === "")
169
+ return `${text.slice(0, open + 1)}${quoted}${text.slice(close - 1)}`;
170
+ if (!inner.includes("\n")) {
171
+ const body = inner.replace(/\s+$/, "");
172
+ return `${text.slice(0, open + 1)}${body}, ${quoted}${inner.slice(body.length)}${text.slice(close - 1)}`;
173
+ }
174
+ const lastEntryLineStart = text.lastIndexOf("\n", open + 1 + inner.trimEnd().length - 1) + 1;
175
+ const itemIndent = /^[ \t]*/.exec(text.slice(lastEntryLineStart))?.[0] ?? " ";
176
+ const bodyEnd = open + 1 + inner.trimEnd().length;
177
+ return `${text.slice(0, bodyEnd)},${eol}${itemIndent}${quoted}${text.slice(bodyEnd)}`;
178
+ }
179
+ function insertSharedTypesWorkspace(text, form, quoted) {
180
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
181
+ const rootOpen = text.indexOf("{");
182
+ if (rootOpen === -1)
183
+ return null;
184
+ const members = objectMembers(text, rootOpen);
185
+ if (members === null)
186
+ return null;
187
+ const workspaces = members.find((member) => member.key === "workspaces");
188
+ if (form === "absent") {
189
+ const last = members[members.length - 1];
190
+ const indent = /\n([ \t]+)"/.exec(text)?.[1] ?? " ";
191
+ if (last === void 0) {
192
+ const close = skipJsonValue(text, rootOpen) - 1;
193
+ return `${text.slice(0, rootOpen + 1)}${eol}${indent}"workspaces": [${quoted}]${eol}${text.slice(close)}`;
194
+ }
195
+ return `${text.slice(0, last.valueEnd)},${eol}${indent}"workspaces": [${quoted}]${text.slice(last.valueEnd)}`;
196
+ }
197
+ if (workspaces === void 0)
198
+ return null;
199
+ if (form === "array") {
200
+ if (text[workspaces.valueStart] !== "[")
201
+ return null;
202
+ return appendToArray(text, workspaces.valueStart, workspaces.valueEnd, eol, quoted);
203
+ }
204
+ if (text[workspaces.valueStart] !== "{")
205
+ return null;
206
+ const packages = objectMembers(text, workspaces.valueStart)?.find((member) => member.key === "packages");
207
+ if (packages === void 0 || text[packages.valueStart] !== "[")
208
+ return null;
209
+ return appendToArray(text, packages.valueStart, packages.valueEnd, eol, quoted);
210
+ }
211
+ function addSharedTypesWorkspaceGlob(yamlText, entry = DEFAULT_SHARED_TYPES_ROOT) {
212
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
213
+ const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
214
+ if (new RegExp(`^\\s*-\\s*['"]?(${escape(entry)}|${escape(parentGlob)})['"]?\\s*$`, "m").test(yamlText)) {
215
+ return { status: "present" };
216
+ }
217
+ const lines = yamlText.split("\n");
218
+ const packagesIndex = lines.findIndex((line) => /^packages:\s*$/.test(line));
219
+ if (packagesIndex === -1)
220
+ return { status: "unrecognized" };
221
+ let lastEntry = packagesIndex;
222
+ for (let i = packagesIndex + 1; i < lines.length; i++) {
223
+ if (/^\s+-\s+\S/.test(lines[i])) {
224
+ lastEntry = i;
225
+ } else if (lines[i].trim() === "") {
226
+ continue;
227
+ } else {
228
+ break;
229
+ }
230
+ }
231
+ if (lastEntry === packagesIndex)
232
+ return { status: "unrecognized" };
233
+ const indent = /^(\s+)-/.exec(lines[lastEntry])?.[1] ?? " ";
234
+ lines.splice(lastEntry + 1, 0, `${indent}- '${entry}'`);
235
+ return { status: "added", text: lines.join("\n") };
236
+ }
237
+
238
+ export {
239
+ readSharedTypesConfig,
240
+ composeBackendWiring,
241
+ addSharedTypesNpmWorkspace,
242
+ addSharedTypesWorkspaceGlob
243
+ };
File without changes
@@ -0,0 +1,89 @@
1
+ import {
2
+ generatorFilesDir,
3
+ renderTemplateFiles
4
+ } from "./chunk-YNMLB4DL.js";
5
+ import {
6
+ resolveDryRun
7
+ } from "./chunk-TUN34LAQ.js";
8
+ import {
9
+ readToolkitConfig,
10
+ resolveLocaleOption
11
+ } from "./chunk-EXYZLL7A.js";
12
+
13
+ // src/generators/add-github-templates/generator.ts
14
+ import { join } from "node:path";
15
+
16
+ // src/generators/add-github-templates/render.ts
17
+ var ACTION_LINES = {
18
+ created: "created",
19
+ unchanged: "unchanged (already matches the generated version)",
20
+ skipped: "left as-is (a different file already exists there \u2014 never overwritten)"
21
+ };
22
+ function renderAddGithubTemplatesReport(report) {
23
+ const lines = [];
24
+ if (report.files.length === 0) {
25
+ lines.push("add-github-templates: nothing selected (issue/pr/codeowners all false) \u2014 no files to install.");
26
+ return lines.join("\n");
27
+ }
28
+ if (report.dryRun) {
29
+ lines.push("add-github-templates dry-run \u2014 no files were written.");
30
+ lines.push("");
31
+ for (const file of report.files) {
32
+ lines.push(`${file.filePath}:`);
33
+ lines.push(file.content);
34
+ lines.push("");
35
+ }
36
+ lines.push("Effective decision:");
37
+ }
38
+ for (const file of report.files) {
39
+ lines.push(` ${file.filePath}: ${ACTION_LINES[file.action]}`);
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+
44
+ // src/generators/add-github-templates/generator.ts
45
+ var BUG_REPORT_PATH = ".github/ISSUE_TEMPLATE/bug-report.yml";
46
+ var FEATURE_REQUEST_PATH = ".github/ISSUE_TEMPLATE/feature-request.yml";
47
+ var PULL_REQUEST_TEMPLATE_PATH = ".github/PULL_REQUEST_TEMPLATE.md";
48
+ var CODEOWNERS_PATH = ".github/CODEOWNERS";
49
+ function composeGithubTemplates(groups, locale) {
50
+ const files = generatorFilesDir("add-github-templates", import.meta.url);
51
+ return ["issue", "pr", "codeowners"].filter((group) => groups[group]).flatMap((group) => renderTemplateFiles(join(files, locale, group), ".", {}));
52
+ }
53
+ function runAddGithubTemplates(tree, options) {
54
+ const locale = resolveLocaleOption(options.locale, readToolkitConfig(tree));
55
+ const plan = composeGithubTemplates(
56
+ { issue: options.issue ?? false, pr: options.pr ?? true, codeowners: options.codeowners ?? false },
57
+ locale
58
+ );
59
+ const dryRun = options.dryRun ?? false;
60
+ const files = plan.map(({ filePath, content }) => {
61
+ const existing = tree.read(filePath, "utf-8");
62
+ let action;
63
+ if (existing === null) {
64
+ action = "created";
65
+ if (!dryRun) tree.write(filePath, content);
66
+ } else if (existing === content) {
67
+ action = "unchanged";
68
+ } else {
69
+ action = "skipped";
70
+ }
71
+ return { filePath, action, content };
72
+ });
73
+ return { dryRun, files };
74
+ }
75
+ async function addGithubTemplatesGenerator(tree, options) {
76
+ console.log(renderAddGithubTemplatesReport(runAddGithubTemplates(tree, { ...options, dryRun: resolveDryRun(options) })));
77
+ }
78
+ var generator_default = addGithubTemplatesGenerator;
79
+
80
+ export {
81
+ renderAddGithubTemplatesReport,
82
+ BUG_REPORT_PATH,
83
+ FEATURE_REQUEST_PATH,
84
+ PULL_REQUEST_TEMPLATE_PATH,
85
+ CODEOWNERS_PATH,
86
+ runAddGithubTemplates,
87
+ addGithubTemplatesGenerator,
88
+ generator_default
89
+ };