oxlint-architecture-rules 0.1.0-beta.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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/build/dts/adapters/cli/main.d.ts +3 -0
  3. package/build/dts/adapters/cli/main.d.ts.map +1 -0
  4. package/build/dts/adapters/cli/run.d.ts +19 -0
  5. package/build/dts/adapters/cli/run.d.ts.map +1 -0
  6. package/build/dts/adapters/cli/source-facts.d.ts +9 -0
  7. package/build/dts/adapters/cli/source-facts.d.ts.map +1 -0
  8. package/build/dts/adapters/cli/source-files.d.ts +2 -0
  9. package/build/dts/adapters/cli/source-files.d.ts.map +1 -0
  10. package/build/dts/adapters/oxlint/config-loader.d.ts +23 -0
  11. package/build/dts/adapters/oxlint/config-loader.d.ts.map +1 -0
  12. package/build/dts/adapters/oxlint/exports-rule.d.ts +4 -0
  13. package/build/dts/adapters/oxlint/exports-rule.d.ts.map +1 -0
  14. package/build/dts/adapters/oxlint/imports-rule.d.ts +4 -0
  15. package/build/dts/adapters/oxlint/imports-rule.d.ts.map +1 -0
  16. package/build/dts/adapters/oxlint/members-rule.d.ts +4 -0
  17. package/build/dts/adapters/oxlint/members-rule.d.ts.map +1 -0
  18. package/build/dts/adapters/oxlint/oxlint-api.d.ts +19 -0
  19. package/build/dts/adapters/oxlint/oxlint-api.d.ts.map +1 -0
  20. package/build/dts/adapters/oxlint/plugin.d.ts +15 -0
  21. package/build/dts/adapters/oxlint/plugin.d.ts.map +1 -0
  22. package/build/dts/adapters/oxlint/structure-rule.d.ts +4 -0
  23. package/build/dts/adapters/oxlint/structure-rule.d.ts.map +1 -0
  24. package/build/dts/core/baseline.d.ts +16 -0
  25. package/build/dts/core/baseline.d.ts.map +1 -0
  26. package/build/dts/core/exports.d.ts +39 -0
  27. package/build/dts/core/exports.d.ts.map +1 -0
  28. package/build/dts/core/imports.d.ts +27 -0
  29. package/build/dts/core/imports.d.ts.map +1 -0
  30. package/build/dts/core/members.d.ts +28 -0
  31. package/build/dts/core/members.d.ts.map +1 -0
  32. package/build/dts/core/patterns.d.ts +19 -0
  33. package/build/dts/core/patterns.d.ts.map +1 -0
  34. package/build/dts/core/structure.d.ts +48 -0
  35. package/build/dts/core/structure.d.ts.map +1 -0
  36. package/build/dts/domain/architecture-config.d.ts +177 -0
  37. package/build/dts/domain/architecture-config.d.ts.map +1 -0
  38. package/build/dts/domain/architecture-error.d.ts +27 -0
  39. package/build/dts/domain/architecture-error.d.ts.map +1 -0
  40. package/build/dts/domain/violation.d.ts +11 -0
  41. package/build/dts/domain/violation.d.ts.map +1 -0
  42. package/build/dts/index.d.ts +10 -0
  43. package/build/dts/index.d.ts.map +1 -0
  44. package/build/dts/infrastructure/file-system-fake.d.ts +3 -0
  45. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -0
  46. package/build/dts/infrastructure/file-system-live.d.ts +3 -0
  47. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -0
  48. package/build/dts/infrastructure/module-resolver-fake.d.ts +3 -0
  49. package/build/dts/infrastructure/module-resolver-fake.d.ts.map +1 -0
  50. package/build/dts/infrastructure/module-resolver-live.d.ts +4 -0
  51. package/build/dts/infrastructure/module-resolver-live.d.ts.map +1 -0
  52. package/build/dts/manifest/compile.d.ts +16 -0
  53. package/build/dts/manifest/compile.d.ts.map +1 -0
  54. package/build/dts/manifest/glob.d.ts +14 -0
  55. package/build/dts/manifest/glob.d.ts.map +1 -0
  56. package/build/dts/manifest/manifest.d.ts +96 -0
  57. package/build/dts/manifest/manifest.d.ts.map +1 -0
  58. package/build/dts/ports/file-system.d.ts +4 -0
  59. package/build/dts/ports/file-system.d.ts.map +1 -0
  60. package/build/dts/ports/module-resolver.d.ts +11 -0
  61. package/build/dts/ports/module-resolver.d.ts.map +1 -0
  62. package/build/esm/adapters/cli/main.js +17 -0
  63. package/build/esm/adapters/cli/main.js.map +1 -0
  64. package/build/esm/adapters/cli/run.js +188 -0
  65. package/build/esm/adapters/cli/run.js.map +1 -0
  66. package/build/esm/adapters/cli/source-facts.js +117 -0
  67. package/build/esm/adapters/cli/source-facts.js.map +1 -0
  68. package/build/esm/adapters/cli/source-files.js +40 -0
  69. package/build/esm/adapters/cli/source-files.js.map +1 -0
  70. package/build/esm/adapters/oxlint/config-loader.js +81 -0
  71. package/build/esm/adapters/oxlint/config-loader.js.map +1 -0
  72. package/build/esm/adapters/oxlint/exports-rule.js +104 -0
  73. package/build/esm/adapters/oxlint/exports-rule.js.map +1 -0
  74. package/build/esm/adapters/oxlint/imports-rule.js +55 -0
  75. package/build/esm/adapters/oxlint/imports-rule.js.map +1 -0
  76. package/build/esm/adapters/oxlint/members-rule.js +75 -0
  77. package/build/esm/adapters/oxlint/members-rule.js.map +1 -0
  78. package/build/esm/adapters/oxlint/oxlint-api.js +7 -0
  79. package/build/esm/adapters/oxlint/oxlint-api.js.map +1 -0
  80. package/build/esm/adapters/oxlint/plugin.js +23 -0
  81. package/build/esm/adapters/oxlint/plugin.js.map +1 -0
  82. package/build/esm/adapters/oxlint/structure-rule.js +37 -0
  83. package/build/esm/adapters/oxlint/structure-rule.js.map +1 -0
  84. package/build/esm/core/baseline.js +30 -0
  85. package/build/esm/core/baseline.js.map +1 -0
  86. package/build/esm/core/exports.js +96 -0
  87. package/build/esm/core/exports.js.map +1 -0
  88. package/build/esm/core/imports.js +85 -0
  89. package/build/esm/core/imports.js.map +1 -0
  90. package/build/esm/core/members.js +84 -0
  91. package/build/esm/core/members.js.map +1 -0
  92. package/build/esm/core/patterns.js +78 -0
  93. package/build/esm/core/patterns.js.map +1 -0
  94. package/build/esm/core/structure.js +256 -0
  95. package/build/esm/core/structure.js.map +1 -0
  96. package/build/esm/domain/architecture-config.js +172 -0
  97. package/build/esm/domain/architecture-config.js.map +1 -0
  98. package/build/esm/domain/architecture-error.js +29 -0
  99. package/build/esm/domain/architecture-error.js.map +1 -0
  100. package/build/esm/domain/violation.js +8 -0
  101. package/build/esm/domain/violation.js.map +1 -0
  102. package/build/esm/index.js +8 -0
  103. package/build/esm/index.js.map +1 -0
  104. package/build/esm/infrastructure/file-system-fake.js +5 -0
  105. package/build/esm/infrastructure/file-system-fake.js.map +1 -0
  106. package/build/esm/infrastructure/file-system-live.js +19 -0
  107. package/build/esm/infrastructure/file-system-live.js.map +1 -0
  108. package/build/esm/infrastructure/module-resolver-fake.js +23 -0
  109. package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
  110. package/build/esm/infrastructure/module-resolver-live.js +71 -0
  111. package/build/esm/infrastructure/module-resolver-live.js.map +1 -0
  112. package/build/esm/manifest/compile.js +503 -0
  113. package/build/esm/manifest/compile.js.map +1 -0
  114. package/build/esm/manifest/glob.js +77 -0
  115. package/build/esm/manifest/glob.js.map +1 -0
  116. package/build/esm/manifest/manifest.js +120 -0
  117. package/build/esm/manifest/manifest.js.map +1 -0
  118. package/build/esm/ports/file-system.js +2 -0
  119. package/build/esm/ports/file-system.js.map +1 -0
  120. package/build/esm/ports/module-resolver.js +2 -0
  121. package/build/esm/ports/module-resolver.js.map +1 -0
  122. package/package.json +73 -0
  123. package/src/adapters/cli/main.ts +20 -0
  124. package/src/adapters/cli/run.ts +270 -0
  125. package/src/adapters/cli/source-facts.ts +137 -0
  126. package/src/adapters/cli/source-files.ts +42 -0
  127. package/src/adapters/oxlint/config-loader.ts +136 -0
  128. package/src/adapters/oxlint/exports-rule.ts +144 -0
  129. package/src/adapters/oxlint/imports-rule.ts +67 -0
  130. package/src/adapters/oxlint/members-rule.ts +114 -0
  131. package/src/adapters/oxlint/oxlint-api.ts +25 -0
  132. package/src/adapters/oxlint/plugin.ts +35 -0
  133. package/src/adapters/oxlint/structure-rule.ts +39 -0
  134. package/src/core/baseline.ts +62 -0
  135. package/src/core/exports.ts +164 -0
  136. package/src/core/imports.ts +138 -0
  137. package/src/core/members.ts +131 -0
  138. package/src/core/patterns.ts +122 -0
  139. package/src/core/structure.ts +348 -0
  140. package/src/domain/architecture-config.ts +206 -0
  141. package/src/domain/architecture-error.ts +43 -0
  142. package/src/domain/violation.ts +24 -0
  143. package/src/index.ts +28 -0
  144. package/src/infrastructure/file-system-fake.ts +6 -0
  145. package/src/infrastructure/file-system-live.ts +20 -0
  146. package/src/infrastructure/module-resolver-fake.ts +29 -0
  147. package/src/infrastructure/module-resolver-live.ts +100 -0
  148. package/src/manifest/compile.ts +736 -0
  149. package/src/manifest/glob.ts +108 -0
  150. package/src/manifest/manifest.ts +178 -0
  151. package/src/ports/file-system.ts +5 -0
  152. package/src/ports/module-resolver.ts +23 -0
@@ -0,0 +1,108 @@
1
+ // The manifest is meant to read like a filesystem, so its patterns are globs
2
+ // rather than the regexes the rule IR speaks. This is the one translation.
3
+ //
4
+ // * one path segment, or part of one
5
+ // ** any number of segments
6
+ // /** "…or nothing" — `a/**` matches `a` itself as well as `a/b/c`
7
+ // {name} a named capture of one segment when it appears in a tree key;
8
+ // elsewhere, a back-reference to the ancestor that declared it
9
+ // [A-Z] a character class, passed through as written — the one place a
10
+ // pattern needs to say something about a single character
11
+ //
12
+ // Everything else is literal, including the dots that make up a stereotype.
13
+
14
+ const ESCAPE = /[.+^$()|[\]\\]/g;
15
+
16
+ // Split on the tokens first so `**` is not read as two `*`s, `{name}`'s braces
17
+ // are not escaped, and a character class keeps its brackets.
18
+ const TOKEN_SOURCE = "\\[\\^?[^\\]]+\\]|\\{[a-zA-Z][a-zA-Z0-9]*\\}|\\*\\*|\\*|\\?";
19
+
20
+ export type CaptureIndex = Readonly<Record<string, number>>;
21
+
22
+ const escapeLiteral = (value: string): string => value.replace(ESCAPE, "\\$&");
23
+
24
+ const TOKENS = new RegExp(`(${TOKEN_SOURCE})`, "g");
25
+
26
+ export type GlobCompilation = {
27
+ readonly source: string;
28
+ readonly captures: CaptureIndex;
29
+ // Group indices for the `*`s, in source order, when `capturing` asked for
30
+ // them. A naming rule judges what one of these matched.
31
+ readonly wildcards: ReadonlyArray<number>;
32
+ };
33
+
34
+ // `declaring` compiles `{name}` to a new capture group and records its position;
35
+ // otherwise `{name}` becomes a `$n` back-reference to the group an ancestor
36
+ // declared, which is what the rule IR substitutes at match time.
37
+ export const globToRegexSource = (
38
+ glob: string,
39
+ captures: CaptureIndex,
40
+ options: {
41
+ readonly declaring: boolean;
42
+ readonly nextGroup: number;
43
+ // Compile `*` to a capture rather than to `[^/]*`, so a caller can ask what
44
+ // the variable part of a name actually was. Off everywhere else: a stray
45
+ // group would renumber the back-references `{capture}` compiles to.
46
+ readonly capturing?: boolean;
47
+ },
48
+ ): GlobCompilation => {
49
+ const declared: Record<string, number> = { ...captures };
50
+ const wildcards: Array<number> = [];
51
+ let group = options.nextGroup;
52
+ let out = "";
53
+
54
+ for (const part of glob.split(TOKENS)) {
55
+ if (part === "") continue;
56
+
57
+ if (part === "**") {
58
+ out += ".*";
59
+ continue;
60
+ }
61
+ if (part === "*") {
62
+ if (options.capturing === true) {
63
+ wildcards.push(group);
64
+ out += "([^/]*)";
65
+ group += 1;
66
+ } else out += "[^/]*";
67
+ continue;
68
+ }
69
+ if (part === "?") {
70
+ out += "[^/]";
71
+ continue;
72
+ }
73
+
74
+ // A character class is the one construct that passes through as written.
75
+ if (part.startsWith("[") && part.endsWith("]")) {
76
+ out += part;
77
+ continue;
78
+ }
79
+
80
+ const capture = /^\{([a-zA-Z][a-zA-Z0-9]*)\}$/.exec(part);
81
+ if (capture !== null) {
82
+ const name = capture[1] ?? "";
83
+ if (options.declaring && declared[name] === undefined) {
84
+ declared[name] = group;
85
+ out += "([^/]+)";
86
+ group += 1;
87
+ } else {
88
+ const index = declared[name];
89
+ if (index === undefined) {
90
+ throw new Error(
91
+ `pattern "${glob}" references {${name}}, which no ancestor path declares`,
92
+ );
93
+ }
94
+ out += `$${String(index)}`;
95
+ }
96
+ continue;
97
+ }
98
+
99
+ out += escapeLiteral(part);
100
+ }
101
+
102
+ // `a/**` should match `a` itself, not just its descendants.
103
+ return { source: out.replace(/\/\.\*$/, "(/.*)?"), captures: declared, wildcards };
104
+ };
105
+
106
+ export const anchored = (source: string): string => `^${source}$`;
107
+
108
+ export const prefixed = (source: string): string => `^${source}`;
@@ -0,0 +1,178 @@
1
+ import * as Result from "effect/Result";
2
+ import * as Schema from "effect/Schema";
3
+
4
+ import { ResolveConfig } from "../domain/architecture-config.js";
5
+ import { ConfigInvalid } from "../domain/architecture-error.js";
6
+
7
+ // A manifest is a tree of nodes keyed by path pattern, where everything the
8
+ // architecture says about a part of the tree is written at that part of the tree.
9
+ // A node is a folder (it has `children` and/or `files`) or a file (its key names
10
+ // a filename pattern).
11
+ //
12
+ // The default is tight: a folder admits only the children it lists, and a node
13
+ // may import only what it or an ancestor allows. Laxity is opted into, by name,
14
+ // at the node that wants it.
15
+
16
+ const Globs = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
17
+
18
+ // A prohibition. `match`/`matchNot` describe the target; `except` names the
19
+ // importers it does not apply to. The exemption is declared by the author of the
20
+ // prohibition, in the same breath — a descendant still cannot opt itself out.
21
+ const Denial = Schema.Struct({
22
+ match: Globs,
23
+ matchNot: Schema.optionalKey(Globs),
24
+ except: Schema.optionalKey(Globs),
25
+ message: Schema.String,
26
+ });
27
+
28
+ // Outbound: what this part of the tree may reach.
29
+ const Imports = Schema.Struct({
30
+ message: Schema.optionalKey(Schema.String),
31
+ // Repo-relative globs, after alias expansion. A target matching none of these
32
+ // is the violation — so widening is something you do by name, in the open.
33
+ allow: Schema.optionalKey(Globs),
34
+ // npm package names. Omit to inherit; `[]` to forbid every external.
35
+ external: Schema.optionalKey(Schema.Array(Schema.String)),
36
+ // Checked before `allow`, and wins over it. This is how a rule keeps a
37
+ // specific, better message for a mistake the allowlist would also catch.
38
+ deny: Schema.optionalKey(Schema.Array(Denial)),
39
+ // The one widening mechanism: stop inheriting ancestors' allowances. Inherited
40
+ // `deny` entries are unaffected — a prohibition only ever accumulates.
41
+ reset: Schema.optionalKey(Schema.Boolean),
42
+ // "This tier has no allowlist yet; only the prohibitions below apply." Implies
43
+ // `reset`. Required whenever a node states `imports` without an `allow`, so
44
+ // that an untightened tier is a sentence someone wrote rather than a gap in
45
+ // the config — and so `grep unrestricted` is the adoption backlog.
46
+ unrestricted: Schema.optionalKey(Schema.Boolean),
47
+ });
48
+
49
+ // Inbound: who may reach this part of the tree. Four of the rules governing
50
+ // `domain/` are of this shape — "*.root-ops.ts is private to command handlers" —
51
+ // and stating them here is what stops them being a distant rule with a growing
52
+ // exclusion list on its `from` side.
53
+ const ImportedBy = Schema.Struct({
54
+ message: Schema.String,
55
+ allow: Globs,
56
+ // Targets inside this node's subtree the restriction does not cover — the
57
+ // module barrel is the obvious one: a module is private except through it.
58
+ matchNot: Schema.optionalKey(Globs),
59
+ });
60
+
61
+ // What shape the variable part of a name must have. A folder's `children` keys
62
+ // already say which stereotypes it admits; this says what the concept name in
63
+ // front of the stereotype may look like, which is the degree of freedom a
64
+ // taxonomy alone leaves open.
65
+ //
66
+ // `like` names an ancestor capture the name must equal — a subdomain folder's
67
+ // root is named after the folder, and nothing else could say so.
68
+ const Naming = Schema.Union([
69
+ Schema.Literals(["kebab-case", "camelCase", "PascalCase", "snake_case"]),
70
+ Schema.Struct({ regex: Schema.String, message: Schema.optionalKey(Schema.String) }),
71
+ Schema.Struct({ like: Schema.String, message: Schema.optionalKey(Schema.String) }),
72
+ ]);
73
+
74
+ const Members = Schema.Struct({
75
+ message: Schema.String,
76
+ subject: Schema.Literals(["type-members", "calls"]),
77
+ in: Schema.optionalKey(Globs),
78
+ match: Schema.optionalKey(Globs),
79
+ matchNot: Schema.optionalKey(Globs),
80
+ allow: Schema.optionalKey(Globs),
81
+ });
82
+
83
+ // Which importers may name a given exported symbol. A path rule cannot say
84
+ // this: every importer of a barrel resolves to the same file, so only the
85
+ // imported name separates a bus factory from the Tag beside it.
86
+ const ExportRestriction = Schema.Struct({
87
+ name: Schema.String,
88
+ message: Schema.String,
89
+ // The module the symbols come from, matched against its RESOLVED path.
90
+ module: Globs,
91
+ // Exact exported names. Omit to mean "any named import from that module",
92
+ // which is how a rule bans a binding form rather than a name.
93
+ symbols: Schema.optionalKey(Schema.Array(Schema.String)),
94
+ except: Schema.optionalKey(Globs),
95
+ fix: Schema.optionalKey(Schema.Literal("subpath-namespace-import")),
96
+ });
97
+
98
+ export type ManifestNode = {
99
+ readonly message?: string;
100
+ // Carry policy for the subtree without claiming to enumerate this folder's
101
+ // contents. What makes incremental adoption possible — and what lets a
102
+ // prototype cover one branch without rejecting every sibling.
103
+ readonly partial?: boolean;
104
+ // "This folder admits any source file; only its subfolders are enumerated."
105
+ // A tier that is deliberately permissive about file names — the component
106
+ // library is, by design — says so, rather than carrying a layout rule that
107
+ // could never reject anything.
108
+ readonly layout?: "open";
109
+ // Inherited by the subtree, like `imports`, so a tier states its convention
110
+ // once rather than on every stereotype it admits.
111
+ readonly name?: typeof Naming.Type;
112
+ readonly imports?: typeof Imports.Type;
113
+ readonly importedBy?: typeof ImportedBy.Type;
114
+ readonly members?: ReadonlyArray<typeof Members.Type>;
115
+ // Files this node must have beside it. `{base}` is this file's name minus its
116
+ // final extension; `../` resolves against the node's own folder.
117
+ readonly requires?: ReadonlyArray<string>;
118
+ // Filenames this node's `requires` does not apply to — the exemption lives on
119
+ // the obligation it exempts, rather than as a separate more-specific key that
120
+ // has to win a precedence contest.
121
+ readonly requiresNot?: ReadonlyArray<string>;
122
+ // Folder nodes only. Deny-by-default: a child matching no key is a violation.
123
+ readonly children?: Readonly<Record<string, ManifestNode>>;
124
+ };
125
+
126
+ const ManifestNodeSchema: Schema.Codec<ManifestNode> = Schema.suspend(() =>
127
+ Schema.Struct({
128
+ message: Schema.optionalKey(Schema.String),
129
+ partial: Schema.optionalKey(Schema.Boolean),
130
+ layout: Schema.optionalKey(Schema.Literal("open")),
131
+ name: Schema.optionalKey(Naming),
132
+ imports: Schema.optionalKey(Imports),
133
+ importedBy: Schema.optionalKey(ImportedBy),
134
+ members: Schema.optionalKey(Schema.Array(Members)),
135
+ requires: Schema.optionalKey(Schema.Array(Schema.String)),
136
+ requiresNot: Schema.optionalKey(Schema.Array(Schema.String)),
137
+ children: Schema.optionalKey(Schema.Record(Schema.String, ManifestNodeSchema)),
138
+ }),
139
+ );
140
+
141
+ export const Manifest = Schema.Struct({
142
+ // How an import specifier becomes a file. Every pattern below is matched
143
+ // against a resolved path, so this is what makes the rest of the file mean
144
+ // anything.
145
+ resolve: ResolveConfig,
146
+ baseline: Schema.optionalKey(Schema.String),
147
+ // Prohibitions that hold everywhere, declared once. `not-to-spec` and the
148
+ // "this driver lives in one package" rules are statements about the whole
149
+ // repo, not about a tier — putting them on every tree root would be six
150
+ // copies and a seventh forgotten.
151
+ deny: Schema.optionalKey(Schema.Array(Denial)),
152
+ exports: Schema.optionalKey(Schema.Array(ExportRestriction)),
153
+ // Shorthands expanded in every glob, so a pattern reads the way the repo's own
154
+ // imports do rather than repeating `packages/server/src` on every line.
155
+ aliases: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
156
+ tree: Schema.Record(Schema.String, ManifestNodeSchema),
157
+ });
158
+
159
+ export type Manifest = typeof Manifest.Type;
160
+ export type ImportsSpec = typeof Imports.Type;
161
+ export type ImportedBySpec = typeof ImportedBy.Type;
162
+ export type MembersSpec = typeof Members.Type;
163
+ export type NamingSpec = typeof Naming.Type;
164
+ export type ExportRestriction = typeof ExportRestriction.Type;
165
+
166
+ export const globsOf = (globs: string | ReadonlyArray<string>): ReadonlyArray<string> =>
167
+ typeof globs === "string" ? [globs] : globs;
168
+
169
+ const decode = Schema.decodeUnknownResult(Manifest);
170
+
171
+ export const decodeManifest = (
172
+ configPath: string,
173
+ input: unknown,
174
+ ): Result.Result<Manifest, ConfigInvalid> =>
175
+ Result.mapError(
176
+ decode(input),
177
+ (issue) => new ConfigInvalid({ configPath, detail: String(issue) }),
178
+ );
@@ -0,0 +1,5 @@
1
+ // Sibling-parity rules ask a question no AST can answer: does this other file
2
+ // exist? A port keeps that question testable without a fixture tree on disk.
3
+ export type FileSystem = {
4
+ readonly exists: (repoRelativePath: string) => boolean;
5
+ };
@@ -0,0 +1,23 @@
1
+ import type * as Result from "effect/Result";
2
+
3
+ import type { ImportUnresolved } from "../domain/architecture-error.js";
4
+
5
+ export type DependencyKind = "local" | "external" | "builtin";
6
+
7
+ export type ResolvedTarget = {
8
+ // Repo-relative with forward slashes for a file on disk (including one under
9
+ // `node_modules/`), or `node:<name>` for a runtime builtin. This is the exact
10
+ // vocabulary dependency-cruiser reports in, so ported `to` patterns — including
11
+ // `/node_modules/effect/` against a pnpm `.pnpm/…` path — match unchanged.
12
+ readonly path: string;
13
+ // `builtin` is its own kind rather than a flavour of external, because a rule
14
+ // that fences off npm dependencies is not talking about `node:crypto`.
15
+ readonly kind: DependencyKind;
16
+ };
17
+
18
+ export type ModuleResolver = {
19
+ readonly resolve: (
20
+ fromFile: string,
21
+ specifier: string,
22
+ ) => Result.Result<ResolvedTarget, ImportUnresolved>;
23
+ };