@clearlydev/nx-archweave 0.0.0-stage → 0.1.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -2
  3. package/dist/bin/review-verdict.js +330 -0
  4. package/dist/eslint/fsd-boundaries.d.ts +119 -0
  5. package/dist/eslint/fsd-boundaries.js +144 -0
  6. package/dist/eslint/index.d.ts +78 -0
  7. package/dist/eslint/index.js +256 -0
  8. package/dist/eslint/tsconfig-aliases.d.ts +18 -0
  9. package/dist/eslint/tsconfig-aliases.js +114 -0
  10. package/dist/generators/add-agent-rules/generator.js +550 -0
  11. package/dist/generators/add-agent-rules/render.js +46 -0
  12. package/dist/generators/add-backend/generator.js +784 -0
  13. package/dist/generators/add-backend/render.js +102 -0
  14. package/dist/generators/add-fsd-lite/generator.js +553 -0
  15. package/dist/generators/add-fsd-lite/render.js +72 -0
  16. package/dist/generators/add-github-templates/files/en/codeowners/.github/CODEOWNERS.template +4 -0
  17. package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
  18. package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
  19. package/dist/generators/add-github-templates/files/en/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
  20. package/dist/generators/add-github-templates/files/ko/codeowners/.github/CODEOWNERS.template +4 -0
  21. package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
  22. package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
  23. package/dist/generators/add-github-templates/files/ko/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
  24. package/dist/generators/add-github-templates/generator.js +125 -0
  25. package/dist/generators/add-github-templates/render.js +26 -0
  26. package/dist/generators/add-integration/generator.js +849 -0
  27. package/dist/generators/add-integration/render.js +34 -0
  28. package/dist/generators/add-review/generator.js +921 -0
  29. package/dist/generators/add-review/render.js +96 -0
  30. package/dist/generators/allow-builds/generator.js +436 -0
  31. package/dist/generators/allow-builds/render.js +82 -0
  32. package/dist/generators/doctor/generator.js +2462 -0
  33. package/dist/generators/doctor/integration-report.js +70 -0
  34. package/dist/generators/doctor/render.js +10 -0
  35. package/dist/generators/entity/generator.js +395 -0
  36. package/dist/generators/feature/generator.js +395 -0
  37. package/dist/generators/init/generator.js +3211 -0
  38. package/dist/generators/init/render.js +125 -0
  39. package/dist/generators/init/validation.js +74 -0
  40. package/dist/generators/install-integration/generator.js +1265 -0
  41. package/dist/generators/install-integration/render.js +128 -0
  42. package/dist/generators/page/generator.js +395 -0
  43. package/dist/generators/record-integration/generator.js +883 -0
  44. package/dist/generators/record-integration/render.js +31 -0
  45. package/dist/generators/shared/dry-run.js +12 -0
  46. package/dist/generators/shared/frontend-app.js +57 -0
  47. package/dist/generators/shared/generator.js +395 -0
  48. package/dist/generators/shared/project-type.js +39 -0
  49. package/dist/generators/shared/record-pipeline.js +34 -0
  50. package/dist/generators/shared/run-nx.js +24 -0
  51. package/dist/generators/shared/template-files.js +38 -0
  52. package/dist/generators/shared/toolkit-config-file.js +110 -0
  53. package/dist/generators/slice/core.js +185 -0
  54. package/dist/generators/slice/files/index/index.ts.template +14 -0
  55. package/dist/generators/slice/files/module/__name__.spec.ts.template +11 -0
  56. package/dist/generators/slice/files/module/__name__.ts.template +11 -0
  57. package/dist/generators/slice/files/segment/index.ts.template +20 -0
  58. package/dist/generators/slice/render.js +70 -0
  59. package/dist/generators/widget/generator.js +395 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +711 -0
  62. package/dist/lib/nx-plugin.d.ts +2 -0
  63. package/dist/lib/nx-plugin.js +7 -0
  64. package/dist/runtime/integrations-composition-root.js +20 -0
  65. package/dist/runtime/node-command-runner.js +81 -0
  66. package/generators.json +85 -0
  67. package/package.json +96 -4
  68. package/src/generators/add-agent-rules/schema.json +43 -0
  69. package/src/generators/add-backend/schema.json +39 -0
  70. package/src/generators/add-fsd-lite/schema.json +43 -0
  71. package/src/generators/add-github-templates/schema.json +35 -0
  72. package/src/generators/add-integration/schema.json +35 -0
  73. package/src/generators/add-review/schema.json +72 -0
  74. package/src/generators/allow-builds/schema.json +20 -0
  75. package/src/generators/doctor/schema.json +20 -0
  76. package/src/generators/entity/schema.json +37 -0
  77. package/src/generators/feature/schema.json +37 -0
  78. package/src/generators/init/schema.json +98 -0
  79. package/src/generators/install-integration/schema.json +35 -0
  80. package/src/generators/page/schema.json +53 -0
  81. package/src/generators/record-integration/schema.json +26 -0
  82. package/src/generators/shared/schema.json +32 -0
  83. package/src/generators/widget/schema.json +45 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `archweave/fsd-boundaries` — canonical FSD import rules inside one frontend app.
3
+ * Three findings:
4
+ *
5
+ * - upward: a layer imports a layer above it (app > pages > widgets > features > entities > shared)
6
+ * - cross-slice: two slices on the same sliced layer import each other — unless through the
7
+ * target slice's `@x/<importer-slice>` public API (FSD's cross-import notation)
8
+ * - sidestep: a slice's (or a shared segment's) internal file is imported past its index
9
+ *
10
+ * Only relative specifiers and the configured aliases are judged; package specifiers
11
+ * (`react`, `firebase/app`) are never touched — string-matching layer names in them was the
12
+ * measured source of false positives. Locations are computed from path strings alone: the
13
+ * rule never reads the file system.
14
+ *
15
+ * Self-contained on purpose: no `eslint` import, so the plugin's runtime dependencies stay
16
+ * @nx/devkit + tslib. The handful of ESLint shapes it touches are typed structurally below.
17
+ */
18
+ export type FsdEnforcementSeverity = 'error' | 'warn';
19
+ export interface FsdBoundariesOptions {
20
+ /** The app's source root, relative to ESLint's cwd (Nx lints from the project root). */
21
+ root: string;
22
+ /** Import-prefix aliases → directories relative to cwd, e.g. `{ '@/': 'src' }`. */
23
+ aliases: Record<string, string>;
24
+ }
25
+ declare const LAYER_ORDER: readonly ["app", "pages", "widgets", "features", "entities", "shared"];
26
+ type FsdLayer = (typeof LAYER_ORDER)[number];
27
+ /** Where a file sits inside the FSD tree. `unit` is the slice on a sliced layer, the segment
28
+ * on `shared` (its segments carry a public API too), null on `app`. `rest` is the path below
29
+ * the unit, with a trailing `index[.ext]` dropped. */
30
+ interface Location {
31
+ layer: FsdLayer;
32
+ unit: string | null;
33
+ rest: string[];
34
+ }
35
+ export declare function locate(rootRelativePath: string): Location | null;
36
+ export type FsdViolation = {
37
+ kind: 'upward';
38
+ from: Location;
39
+ to: Location;
40
+ } | {
41
+ kind: 'cross-slice';
42
+ from: Location;
43
+ to: Location;
44
+ } | {
45
+ kind: 'sidestep';
46
+ from: Location;
47
+ to: Location;
48
+ };
49
+ /** Pure core: the violation (if any) of `from` importing `to`. */
50
+ export declare function judge(from: Location, to: Location): FsdViolation | null;
51
+ export declare function describeLocation(location: Location): string;
52
+ /** Resolves an import specifier to a path relative to the source root, or null when it is
53
+ * a package specifier or lands outside the root. */
54
+ export declare function resolveSpecifier(specifier: string, importerRootRelative: string, options: {
55
+ rootAbsolute: string;
56
+ cwd: string;
57
+ aliases: Record<string, string>;
58
+ }): string | null;
59
+ export interface SourceNode {
60
+ type: string;
61
+ value?: unknown;
62
+ }
63
+ export interface AstNode {
64
+ type: string;
65
+ source?: SourceNode | null;
66
+ arguments?: AstNode[];
67
+ }
68
+ export interface RuleContext {
69
+ filename: string;
70
+ cwd: string;
71
+ options: unknown[];
72
+ report(descriptor: {
73
+ node: AstNode;
74
+ messageId: string;
75
+ data?: Record<string, string>;
76
+ }): void;
77
+ }
78
+ export declare const fsdBoundariesRule: {
79
+ meta: {
80
+ type: "problem";
81
+ docs: {
82
+ description: string;
83
+ url: string;
84
+ };
85
+ schema: {
86
+ type: string;
87
+ properties: {
88
+ root: {
89
+ type: string;
90
+ };
91
+ aliases: {
92
+ type: string;
93
+ additionalProperties: {
94
+ type: string;
95
+ };
96
+ };
97
+ };
98
+ additionalProperties: boolean;
99
+ }[];
100
+ messages: {
101
+ upward: string;
102
+ crossSlice: string;
103
+ sidestep: string;
104
+ };
105
+ };
106
+ create(context: RuleContext): {
107
+ ImportDeclaration?: undefined;
108
+ ExportNamedDeclaration?: undefined;
109
+ ExportAllDeclaration?: undefined;
110
+ ImportExpression?: undefined;
111
+ } | {
112
+ ImportDeclaration: (node: AstNode) => void;
113
+ ExportNamedDeclaration: (node: AstNode) => void;
114
+ ExportAllDeclaration: (node: AstNode) => void;
115
+ ImportExpression: (node: AstNode) => void;
116
+ };
117
+ };
118
+ export {};
119
+ //# sourceMappingURL=fsd-boundaries.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { posix, relative, resolve, sep } from 'node:path';
2
+ const LAYER_ORDER = ['app', 'pages', 'widgets', 'features', 'entities', 'shared'];
3
+ /** Directory name → layer. `_app`/`_pages` are FSD's own Next.js naming (its routers own
4
+ * `app/` and `pages/`). */
5
+ const LAYER_BY_DIR = {
6
+ app: 'app',
7
+ _app: 'app',
8
+ pages: 'pages',
9
+ _pages: 'pages',
10
+ widgets: 'widgets',
11
+ features: 'features',
12
+ entities: 'entities',
13
+ shared: 'shared',
14
+ };
15
+ const SLICED_LAYERS = new Set(['pages', 'widgets', 'features', 'entities']);
16
+ const INDEX_FILE = /^index(\.[cm]?[jt]sx?)?$/;
17
+ export function locate(rootRelativePath) {
18
+ const segments = rootRelativePath.split('/').filter((s) => s.length > 0 && s !== '.');
19
+ if (segments.length === 0)
20
+ return null;
21
+ const layer = LAYER_BY_DIR[segments[0]];
22
+ if (layer === undefined)
23
+ return null;
24
+ const hasUnit = layer !== 'app';
25
+ const unit = hasUnit ? (segments[1] ?? null) : null;
26
+ const rest = segments.slice(hasUnit ? 2 : 1);
27
+ if (rest.length > 0 && INDEX_FILE.test(rest[rest.length - 1]))
28
+ rest.pop();
29
+ return { layer, unit, rest };
30
+ }
31
+ /** Pure core: the violation (if any) of `from` importing `to`. */
32
+ export function judge(from, to) {
33
+ if (LAYER_ORDER.indexOf(to.layer) < LAYER_ORDER.indexOf(from.layer))
34
+ return { kind: 'upward', from, to };
35
+ if (to.unit === null)
36
+ return null; // the app layer, or a bare layer directory — nothing finer to guard
37
+ const sameUnit = from.layer === to.layer && from.unit === to.unit;
38
+ if (sameUnit)
39
+ return null; // inside one slice/segment anything goes
40
+ const viaCrossImportApi = to.rest[0] === '@x' && to.rest[1] === from.unit && from.layer === to.layer;
41
+ if (viaCrossImportApi)
42
+ return null;
43
+ if (from.layer === to.layer && SLICED_LAYERS.has(to.layer))
44
+ return { kind: 'cross-slice', from, to };
45
+ if (to.rest.length > 0)
46
+ return { kind: 'sidestep', from, to };
47
+ return null;
48
+ }
49
+ export function describeLocation(location) {
50
+ return location.unit === null ? location.layer : `${location.layer}/${location.unit}`;
51
+ }
52
+ /** Resolves an import specifier to a path relative to the source root, or null when it is
53
+ * a package specifier or lands outside the root. */
54
+ export function resolveSpecifier(specifier, importerRootRelative, options) {
55
+ let rootRelative = null;
56
+ if (specifier.startsWith('./') || specifier.startsWith('../') || specifier === '.' || specifier === '..') {
57
+ rootRelative = posix.normalize(posix.join(posix.dirname(importerRootRelative), specifier));
58
+ }
59
+ else {
60
+ for (const [prefix, target] of Object.entries(options.aliases)) {
61
+ if (!specifier.startsWith(prefix))
62
+ continue;
63
+ const targetAbsolute = resolve(options.cwd, target);
64
+ const targetRootRelative = toPosix(relative(options.rootAbsolute, targetAbsolute));
65
+ rootRelative = posix.normalize(posix.join(targetRootRelative === '' ? '.' : targetRootRelative, specifier.slice(prefix.length)));
66
+ break;
67
+ }
68
+ }
69
+ if (rootRelative === null)
70
+ return null;
71
+ if (rootRelative === '..' || rootRelative.startsWith('../'))
72
+ return null;
73
+ return rootRelative;
74
+ }
75
+ function toPosix(path) {
76
+ return sep === '/' ? path : path.split(sep).join('/');
77
+ }
78
+ export const fsdBoundariesRule = {
79
+ meta: {
80
+ type: 'problem',
81
+ docs: {
82
+ description: 'Enforce canonical Feature-Sliced Design import rules inside a frontend app',
83
+ url: 'https://github.com/clearlydev/nx-archweave#fsd-lite',
84
+ },
85
+ schema: [
86
+ {
87
+ type: 'object',
88
+ properties: {
89
+ root: { type: 'string' },
90
+ aliases: { type: 'object', additionalProperties: { type: 'string' } },
91
+ },
92
+ additionalProperties: false,
93
+ },
94
+ ],
95
+ messages: {
96
+ upward: 'FSD: "{{from}}" may not import from "{{to}}" (a higher layer).',
97
+ crossSlice: 'FSD: "{{from}}" may not import a sibling slice "{{to}}" — move the shared part down a layer, or expose it through "{{to}}/@x/{{fromUnit}}".',
98
+ sidestep: 'FSD: import "{{to}}" through its public API (index.ts), not its internal file "{{path}}".',
99
+ },
100
+ },
101
+ create(context) {
102
+ const options = (context.options[0] ?? {});
103
+ const root = options.root ?? 'src';
104
+ const aliases = options.aliases ?? { '@/': root };
105
+ const cwd = context.cwd;
106
+ const rootAbsolute = resolve(cwd, root);
107
+ const importerRootRelative = toPosix(relative(rootAbsolute, context.filename));
108
+ if (importerRootRelative === '' || importerRootRelative.startsWith('..'))
109
+ return {};
110
+ const from = locate(importerRootRelative);
111
+ if (from === null)
112
+ return {};
113
+ const check = (node, specifier) => {
114
+ if (typeof specifier !== 'string')
115
+ return;
116
+ const target = resolveSpecifier(specifier, importerRootRelative, { rootAbsolute, cwd, aliases });
117
+ if (target === null)
118
+ return;
119
+ const to = locate(target);
120
+ if (to === null)
121
+ return;
122
+ const violation = judge(from, to);
123
+ if (violation === null)
124
+ return;
125
+ const data = { from: describeLocation(from), to: describeLocation(to), fromUnit: from.unit ?? '', path: target };
126
+ context.report({
127
+ node,
128
+ messageId: violation.kind === 'upward' ? 'upward' : violation.kind === 'cross-slice' ? 'crossSlice' : 'sidestep',
129
+ data,
130
+ });
131
+ };
132
+ const fromSource = (node) => check(node, node.source?.value);
133
+ return {
134
+ ImportDeclaration: fromSource,
135
+ ExportNamedDeclaration: fromSource,
136
+ ExportAllDeclaration: fromSource,
137
+ ImportExpression: (node) => {
138
+ const argument = node.source;
139
+ if (argument && argument.type === 'Literal')
140
+ check(node, argument.value);
141
+ },
142
+ };
143
+ },
144
+ };
@@ -0,0 +1,78 @@
1
+ import { type FsdEnforcementSeverity } from './fsd-boundaries.ts';
2
+ export { fsdBoundariesRule, type FsdBoundariesOptions, type FsdEnforcementSeverity } from './fsd-boundaries.ts';
3
+ export { aliasesFromTsconfig } from './tsconfig-aliases.ts';
4
+ /** The ESLint plugin object — `plugins: { archweave: archweavePlugin }`. */
5
+ export declare const archweavePlugin: {
6
+ meta: {
7
+ name: string;
8
+ version: string;
9
+ };
10
+ rules: {
11
+ 'fsd-boundaries': {
12
+ meta: {
13
+ type: "problem";
14
+ docs: {
15
+ description: string;
16
+ url: string;
17
+ };
18
+ schema: {
19
+ type: string;
20
+ properties: {
21
+ root: {
22
+ type: string;
23
+ };
24
+ aliases: {
25
+ type: string;
26
+ additionalProperties: {
27
+ type: string;
28
+ };
29
+ };
30
+ };
31
+ additionalProperties: boolean;
32
+ }[];
33
+ messages: {
34
+ upward: string;
35
+ crossSlice: string;
36
+ sidestep: string;
37
+ };
38
+ };
39
+ create(context: import("./fsd-boundaries.ts").RuleContext): {
40
+ ImportDeclaration?: undefined;
41
+ ExportNamedDeclaration?: undefined;
42
+ ExportAllDeclaration?: undefined;
43
+ ImportExpression?: undefined;
44
+ } | {
45
+ ImportDeclaration: (node: import("./fsd-boundaries.ts").AstNode) => void;
46
+ ExportNamedDeclaration: (node: import("./fsd-boundaries.ts").AstNode) => void;
47
+ ExportAllDeclaration: (node: import("./fsd-boundaries.ts").AstNode) => void;
48
+ ImportExpression: (node: import("./fsd-boundaries.ts").AstNode) => void;
49
+ };
50
+ };
51
+ };
52
+ };
53
+ export interface FsdBoundariesConfigOptions {
54
+ /** `error` (Team preset) or `warn` (Solo / Quick Start). Default `error`. */
55
+ severity?: FsdEnforcementSeverity;
56
+ /** The app's source root, relative to the config file's directory. Default `src`. */
57
+ root?: string;
58
+ /** Import-prefix aliases → directories relative to the config file's directory. Default:
59
+ * `{ '@/': <root> }` (the alias Nx's Next.js template ships) plus whatever the app's
60
+ * tsconfig `paths` declare (see `tsconfig`). Passing this replaces both. */
61
+ aliases?: Record<string, string>;
62
+ /** The tsconfig whose `compilerOptions.paths` (and its `extends` chain) add to the default
63
+ * aliases, relative to `cwd`. Default `tsconfig.json`; `false` turns the lookup off. */
64
+ tsconfig?: string | false;
65
+ /** The directory the config is evaluated from — Nx lints from the project root. Default
66
+ * `process.cwd()`. */
67
+ cwd?: string;
68
+ /** Override the files the rule applies to. Default: everything under `root`. */
69
+ files?: string[];
70
+ }
71
+ /**
72
+ * The one flat-config entry add-fsd-lite adds to a frontend app's eslint.config.mjs:
73
+ *
74
+ * import { fsdBoundaries } from '@clearlydev/nx-archweave/eslint';
75
+ * export default [...baseConfig, ...fsdBoundaries({ severity: 'error' })];
76
+ */
77
+ export declare function fsdBoundaries(options?: FsdBoundariesConfigOptions): object[];
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,256 @@
1
+ // src/eslint/index.ts
2
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
3
+ import { dirname as dirname2, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // src/eslint/fsd-boundaries.ts
7
+ import { posix, relative, resolve, sep } from "node:path";
8
+ var LAYER_ORDER = ["app", "pages", "widgets", "features", "entities", "shared"];
9
+ var LAYER_BY_DIR = {
10
+ app: "app",
11
+ _app: "app",
12
+ pages: "pages",
13
+ _pages: "pages",
14
+ widgets: "widgets",
15
+ features: "features",
16
+ entities: "entities",
17
+ shared: "shared"
18
+ };
19
+ var SLICED_LAYERS = /* @__PURE__ */ new Set(["pages", "widgets", "features", "entities"]);
20
+ var INDEX_FILE = /^index(\.[cm]?[jt]sx?)?$/;
21
+ function locate(rootRelativePath) {
22
+ const segments = rootRelativePath.split("/").filter((s) => s.length > 0 && s !== ".");
23
+ if (segments.length === 0) return null;
24
+ const layer = LAYER_BY_DIR[segments[0]];
25
+ if (layer === void 0) return null;
26
+ const hasUnit = layer !== "app";
27
+ const unit = hasUnit ? segments[1] ?? null : null;
28
+ const rest = segments.slice(hasUnit ? 2 : 1);
29
+ if (rest.length > 0 && INDEX_FILE.test(rest[rest.length - 1])) rest.pop();
30
+ return { layer, unit, rest };
31
+ }
32
+ function judge(from, to) {
33
+ if (LAYER_ORDER.indexOf(to.layer) < LAYER_ORDER.indexOf(from.layer)) return { kind: "upward", from, to };
34
+ if (to.unit === null) return null;
35
+ const sameUnit = from.layer === to.layer && from.unit === to.unit;
36
+ if (sameUnit) return null;
37
+ const viaCrossImportApi = to.rest[0] === "@x" && to.rest[1] === from.unit && from.layer === to.layer;
38
+ if (viaCrossImportApi) return null;
39
+ if (from.layer === to.layer && SLICED_LAYERS.has(to.layer)) return { kind: "cross-slice", from, to };
40
+ if (to.rest.length > 0) return { kind: "sidestep", from, to };
41
+ return null;
42
+ }
43
+ function describeLocation(location) {
44
+ return location.unit === null ? location.layer : `${location.layer}/${location.unit}`;
45
+ }
46
+ function resolveSpecifier(specifier, importerRootRelative, options) {
47
+ let rootRelative = null;
48
+ if (specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..") {
49
+ rootRelative = posix.normalize(posix.join(posix.dirname(importerRootRelative), specifier));
50
+ } else {
51
+ for (const [prefix, target] of Object.entries(options.aliases)) {
52
+ if (!specifier.startsWith(prefix)) continue;
53
+ const targetAbsolute = resolve(options.cwd, target);
54
+ const targetRootRelative = toPosix(relative(options.rootAbsolute, targetAbsolute));
55
+ rootRelative = posix.normalize(posix.join(targetRootRelative === "" ? "." : targetRootRelative, specifier.slice(prefix.length)));
56
+ break;
57
+ }
58
+ }
59
+ if (rootRelative === null) return null;
60
+ if (rootRelative === ".." || rootRelative.startsWith("../")) return null;
61
+ return rootRelative;
62
+ }
63
+ function toPosix(path) {
64
+ return sep === "/" ? path : path.split(sep).join("/");
65
+ }
66
+ var fsdBoundariesRule = {
67
+ meta: {
68
+ type: "problem",
69
+ docs: {
70
+ description: "Enforce canonical Feature-Sliced Design import rules inside a frontend app",
71
+ url: "https://github.com/clearlydev/nx-archweave#fsd-lite"
72
+ },
73
+ schema: [
74
+ {
75
+ type: "object",
76
+ properties: {
77
+ root: { type: "string" },
78
+ aliases: { type: "object", additionalProperties: { type: "string" } }
79
+ },
80
+ additionalProperties: false
81
+ }
82
+ ],
83
+ messages: {
84
+ upward: 'FSD: "{{from}}" may not import from "{{to}}" (a higher layer).',
85
+ 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}}".',
86
+ sidestep: 'FSD: import "{{to}}" through its public API (index.ts), not its internal file "{{path}}".'
87
+ }
88
+ },
89
+ create(context) {
90
+ const options = context.options[0] ?? {};
91
+ const root = options.root ?? "src";
92
+ const aliases = options.aliases ?? { "@/": root };
93
+ const cwd = context.cwd;
94
+ const rootAbsolute = resolve(cwd, root);
95
+ const importerRootRelative = toPosix(relative(rootAbsolute, context.filename));
96
+ if (importerRootRelative === "" || importerRootRelative.startsWith("..")) return {};
97
+ const from = locate(importerRootRelative);
98
+ if (from === null) return {};
99
+ const check = (node, specifier) => {
100
+ if (typeof specifier !== "string") return;
101
+ const target = resolveSpecifier(specifier, importerRootRelative, { rootAbsolute, cwd, aliases });
102
+ if (target === null) return;
103
+ const to = locate(target);
104
+ if (to === null) return;
105
+ const violation = judge(from, to);
106
+ if (violation === null) return;
107
+ const data = { from: describeLocation(from), to: describeLocation(to), fromUnit: from.unit ?? "", path: target };
108
+ context.report({
109
+ node,
110
+ messageId: violation.kind === "upward" ? "upward" : violation.kind === "cross-slice" ? "crossSlice" : "sidestep",
111
+ data
112
+ });
113
+ };
114
+ const fromSource = (node) => check(node, node.source?.value);
115
+ return {
116
+ ImportDeclaration: fromSource,
117
+ ExportNamedDeclaration: fromSource,
118
+ ExportAllDeclaration: fromSource,
119
+ ImportExpression: (node) => {
120
+ const argument = node.source;
121
+ if (argument && argument.type === "Literal") check(node, argument.value);
122
+ }
123
+ };
124
+ }
125
+ };
126
+
127
+ // src/eslint/tsconfig-aliases.ts
128
+ import { existsSync, readFileSync } from "node:fs";
129
+ import { dirname, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
130
+ function aliasesFromTsconfig({ cwd, tsconfig = "tsconfig.json", maxDepth = 8 }) {
131
+ const aliases = {};
132
+ let current = resolve2(cwd, tsconfig);
133
+ const seen = /* @__PURE__ */ new Set();
134
+ for (let depth = 0; current !== null && depth < maxDepth && !seen.has(current); depth++) {
135
+ seen.add(current);
136
+ const config = readJsonc(current);
137
+ if (config === null) break;
138
+ const compilerOptions = asRecord(config["compilerOptions"]);
139
+ const paths = asRecord(compilerOptions?.["paths"]);
140
+ if (paths) {
141
+ const baseUrl = typeof compilerOptions?.["baseUrl"] === "string" ? resolve2(dirname(current), compilerOptions["baseUrl"]) : dirname(current);
142
+ for (const [pattern, targets] of Object.entries(paths)) {
143
+ if (!pattern.endsWith("*")) continue;
144
+ const prefix = pattern.slice(0, -1);
145
+ if (prefix in aliases) continue;
146
+ const target = Array.isArray(targets) ? targets.find((t) => typeof t === "string") : void 0;
147
+ if (target === void 0 || !target.endsWith("/*")) continue;
148
+ const dir = toPosix2(relative2(cwd, resolve2(baseUrl, target.slice(0, -"/*".length))));
149
+ aliases[prefix] = dir === "" ? "." : dir;
150
+ }
151
+ }
152
+ const parent = config["extends"];
153
+ const parentPath = typeof parent === "string" ? parent : Array.isArray(parent) ? parent.find((p) => typeof p === "string") : void 0;
154
+ current = parentPath !== void 0 && (parentPath.startsWith(".") || isAbsolute(parentPath)) ? resolveExtends(dirname(current), parentPath) : null;
155
+ }
156
+ return aliases;
157
+ }
158
+ function resolveExtends(fromDir, target) {
159
+ const candidates = [resolve2(fromDir, target), resolve2(fromDir, `${target}.json`)];
160
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
161
+ }
162
+ function asRecord(value) {
163
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
164
+ }
165
+ function toPosix2(path) {
166
+ return sep2 === "/" ? path : path.split(sep2).join("/");
167
+ }
168
+ function readJsonc(path) {
169
+ let text;
170
+ try {
171
+ text = readFileSync(path, "utf-8");
172
+ } catch {
173
+ return null;
174
+ }
175
+ let out = "";
176
+ let mode = "code";
177
+ for (let i = 0; i < text.length; i++) {
178
+ const c = text[i];
179
+ const d = text[i + 1];
180
+ if (mode === "code") {
181
+ if (c === '"') {
182
+ mode = "string";
183
+ out += c;
184
+ } else if (c === "/" && d === "/") {
185
+ mode = "line";
186
+ i++;
187
+ } else if (c === "/" && d === "*") {
188
+ mode = "block";
189
+ i++;
190
+ } else {
191
+ out += c;
192
+ }
193
+ } else if (mode === "string") {
194
+ out += c;
195
+ if (c === "\\") {
196
+ out += d ?? "";
197
+ i++;
198
+ } else if (c === '"') {
199
+ mode = "code";
200
+ }
201
+ } else if (mode === "line") {
202
+ if (c === "\n") {
203
+ mode = "code";
204
+ out += c;
205
+ }
206
+ } else if (c === "*" && d === "/") {
207
+ mode = "code";
208
+ i++;
209
+ }
210
+ }
211
+ try {
212
+ const parsed = JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
213
+ return asRecord(parsed);
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ // src/eslint/index.ts
220
+ function ownVersion() {
221
+ let dir = dirname2(fileURLToPath(import.meta.url));
222
+ for (; ; ) {
223
+ const manifest = join(dir, "package.json");
224
+ if (existsSync2(manifest)) {
225
+ const pkg = JSON.parse(readFileSync2(manifest, "utf-8"));
226
+ if (pkg.name === "@clearlydev/nx-archweave" && pkg.version) return pkg.version;
227
+ }
228
+ const parent = dirname2(dir);
229
+ if (parent === dir) throw new Error("@clearlydev/nx-archweave: its own package.json was not found");
230
+ dir = parent;
231
+ }
232
+ }
233
+ var archweavePlugin = {
234
+ meta: { name: "@clearlydev/nx-archweave", version: ownVersion() },
235
+ rules: { "fsd-boundaries": fsdBoundariesRule }
236
+ };
237
+ function fsdBoundaries(options = {}) {
238
+ const root = options.root ?? "src";
239
+ const cwd = options.cwd ?? process.cwd();
240
+ const fromTsconfig = options.aliases === void 0 && options.tsconfig !== false ? aliasesFromTsconfig({ cwd, tsconfig: options.tsconfig }) : {};
241
+ const ruleOptions = { root, aliases: options.aliases ?? { "@/": root, ...fromTsconfig } };
242
+ return [
243
+ {
244
+ name: "nx-archweave/fsd-boundaries",
245
+ files: options.files ?? [`${root}/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`],
246
+ plugins: { archweave: archweavePlugin },
247
+ rules: { "archweave/fsd-boundaries": [options.severity ?? "error", ruleOptions] }
248
+ }
249
+ ];
250
+ }
251
+ export {
252
+ aliasesFromTsconfig,
253
+ archweavePlugin,
254
+ fsdBoundaries,
255
+ fsdBoundariesRule
256
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Import aliases from a tsconfig's `compilerOptions.paths`: every
3
+ * `"<prefix>*": ["<dir>/*"]` entry becomes `{ "<prefix>": "<dir>" }`, the directory relative
4
+ * to `cwd` (what the rule's `aliases` option takes). The `extends` chain is followed
5
+ * (relative paths only — a package preset is skipped), the nearest config winning per key;
6
+ * `baseUrl` of the config that declares the paths resolves the targets. Comments and
7
+ * trailing commas are tolerated, like tsc. Anything unreadable yields nothing: the rule
8
+ * then falls back to its default alias — a wrong alias map costs a missed report, never a
9
+ * false one, so this stays best-effort.
10
+ */
11
+ export declare function aliasesFromTsconfig({ cwd, tsconfig, maxDepth }: {
12
+ cwd: string;
13
+ tsconfig?: string;
14
+ maxDepth?: number;
15
+ }): Record<string, string>;
16
+ /** tsconfig is JSON with comments and trailing commas; strip both outside strings. */
17
+ export declare function readJsonc(path: string): Record<string, unknown> | null;
18
+ //# sourceMappingURL=tsconfig-aliases.d.ts.map