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.
- package/LICENSE +21 -0
- package/build/dts/adapters/cli/main.d.ts +3 -0
- package/build/dts/adapters/cli/main.d.ts.map +1 -0
- package/build/dts/adapters/cli/run.d.ts +19 -0
- package/build/dts/adapters/cli/run.d.ts.map +1 -0
- package/build/dts/adapters/cli/source-facts.d.ts +9 -0
- package/build/dts/adapters/cli/source-facts.d.ts.map +1 -0
- package/build/dts/adapters/cli/source-files.d.ts +2 -0
- package/build/dts/adapters/cli/source-files.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/config-loader.d.ts +23 -0
- package/build/dts/adapters/oxlint/config-loader.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/exports-rule.d.ts +4 -0
- package/build/dts/adapters/oxlint/exports-rule.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/imports-rule.d.ts +4 -0
- package/build/dts/adapters/oxlint/imports-rule.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/members-rule.d.ts +4 -0
- package/build/dts/adapters/oxlint/members-rule.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/oxlint-api.d.ts +19 -0
- package/build/dts/adapters/oxlint/oxlint-api.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/plugin.d.ts +15 -0
- package/build/dts/adapters/oxlint/plugin.d.ts.map +1 -0
- package/build/dts/adapters/oxlint/structure-rule.d.ts +4 -0
- package/build/dts/adapters/oxlint/structure-rule.d.ts.map +1 -0
- package/build/dts/core/baseline.d.ts +16 -0
- package/build/dts/core/baseline.d.ts.map +1 -0
- package/build/dts/core/exports.d.ts +39 -0
- package/build/dts/core/exports.d.ts.map +1 -0
- package/build/dts/core/imports.d.ts +27 -0
- package/build/dts/core/imports.d.ts.map +1 -0
- package/build/dts/core/members.d.ts +28 -0
- package/build/dts/core/members.d.ts.map +1 -0
- package/build/dts/core/patterns.d.ts +19 -0
- package/build/dts/core/patterns.d.ts.map +1 -0
- package/build/dts/core/structure.d.ts +48 -0
- package/build/dts/core/structure.d.ts.map +1 -0
- package/build/dts/domain/architecture-config.d.ts +177 -0
- package/build/dts/domain/architecture-config.d.ts.map +1 -0
- package/build/dts/domain/architecture-error.d.ts +27 -0
- package/build/dts/domain/architecture-error.d.ts.map +1 -0
- package/build/dts/domain/violation.d.ts +11 -0
- package/build/dts/domain/violation.d.ts.map +1 -0
- package/build/dts/index.d.ts +10 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/dts/infrastructure/file-system-fake.d.ts +3 -0
- package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -0
- package/build/dts/infrastructure/file-system-live.d.ts +3 -0
- package/build/dts/infrastructure/file-system-live.d.ts.map +1 -0
- package/build/dts/infrastructure/module-resolver-fake.d.ts +3 -0
- package/build/dts/infrastructure/module-resolver-fake.d.ts.map +1 -0
- package/build/dts/infrastructure/module-resolver-live.d.ts +4 -0
- package/build/dts/infrastructure/module-resolver-live.d.ts.map +1 -0
- package/build/dts/manifest/compile.d.ts +16 -0
- package/build/dts/manifest/compile.d.ts.map +1 -0
- package/build/dts/manifest/glob.d.ts +14 -0
- package/build/dts/manifest/glob.d.ts.map +1 -0
- package/build/dts/manifest/manifest.d.ts +96 -0
- package/build/dts/manifest/manifest.d.ts.map +1 -0
- package/build/dts/ports/file-system.d.ts +4 -0
- package/build/dts/ports/file-system.d.ts.map +1 -0
- package/build/dts/ports/module-resolver.d.ts +11 -0
- package/build/dts/ports/module-resolver.d.ts.map +1 -0
- package/build/esm/adapters/cli/main.js +17 -0
- package/build/esm/adapters/cli/main.js.map +1 -0
- package/build/esm/adapters/cli/run.js +188 -0
- package/build/esm/adapters/cli/run.js.map +1 -0
- package/build/esm/adapters/cli/source-facts.js +117 -0
- package/build/esm/adapters/cli/source-facts.js.map +1 -0
- package/build/esm/adapters/cli/source-files.js +40 -0
- package/build/esm/adapters/cli/source-files.js.map +1 -0
- package/build/esm/adapters/oxlint/config-loader.js +81 -0
- package/build/esm/adapters/oxlint/config-loader.js.map +1 -0
- package/build/esm/adapters/oxlint/exports-rule.js +104 -0
- package/build/esm/adapters/oxlint/exports-rule.js.map +1 -0
- package/build/esm/adapters/oxlint/imports-rule.js +55 -0
- package/build/esm/adapters/oxlint/imports-rule.js.map +1 -0
- package/build/esm/adapters/oxlint/members-rule.js +75 -0
- package/build/esm/adapters/oxlint/members-rule.js.map +1 -0
- package/build/esm/adapters/oxlint/oxlint-api.js +7 -0
- package/build/esm/adapters/oxlint/oxlint-api.js.map +1 -0
- package/build/esm/adapters/oxlint/plugin.js +23 -0
- package/build/esm/adapters/oxlint/plugin.js.map +1 -0
- package/build/esm/adapters/oxlint/structure-rule.js +37 -0
- package/build/esm/adapters/oxlint/structure-rule.js.map +1 -0
- package/build/esm/core/baseline.js +30 -0
- package/build/esm/core/baseline.js.map +1 -0
- package/build/esm/core/exports.js +96 -0
- package/build/esm/core/exports.js.map +1 -0
- package/build/esm/core/imports.js +85 -0
- package/build/esm/core/imports.js.map +1 -0
- package/build/esm/core/members.js +84 -0
- package/build/esm/core/members.js.map +1 -0
- package/build/esm/core/patterns.js +78 -0
- package/build/esm/core/patterns.js.map +1 -0
- package/build/esm/core/structure.js +256 -0
- package/build/esm/core/structure.js.map +1 -0
- package/build/esm/domain/architecture-config.js +172 -0
- package/build/esm/domain/architecture-config.js.map +1 -0
- package/build/esm/domain/architecture-error.js +29 -0
- package/build/esm/domain/architecture-error.js.map +1 -0
- package/build/esm/domain/violation.js +8 -0
- package/build/esm/domain/violation.js.map +1 -0
- package/build/esm/index.js +8 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/infrastructure/file-system-fake.js +5 -0
- package/build/esm/infrastructure/file-system-fake.js.map +1 -0
- package/build/esm/infrastructure/file-system-live.js +19 -0
- package/build/esm/infrastructure/file-system-live.js.map +1 -0
- package/build/esm/infrastructure/module-resolver-fake.js +23 -0
- package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
- package/build/esm/infrastructure/module-resolver-live.js +71 -0
- package/build/esm/infrastructure/module-resolver-live.js.map +1 -0
- package/build/esm/manifest/compile.js +503 -0
- package/build/esm/manifest/compile.js.map +1 -0
- package/build/esm/manifest/glob.js +77 -0
- package/build/esm/manifest/glob.js.map +1 -0
- package/build/esm/manifest/manifest.js +120 -0
- package/build/esm/manifest/manifest.js.map +1 -0
- package/build/esm/ports/file-system.js +2 -0
- package/build/esm/ports/file-system.js.map +1 -0
- package/build/esm/ports/module-resolver.js +2 -0
- package/build/esm/ports/module-resolver.js.map +1 -0
- package/package.json +73 -0
- package/src/adapters/cli/main.ts +20 -0
- package/src/adapters/cli/run.ts +270 -0
- package/src/adapters/cli/source-facts.ts +137 -0
- package/src/adapters/cli/source-files.ts +42 -0
- package/src/adapters/oxlint/config-loader.ts +136 -0
- package/src/adapters/oxlint/exports-rule.ts +144 -0
- package/src/adapters/oxlint/imports-rule.ts +67 -0
- package/src/adapters/oxlint/members-rule.ts +114 -0
- package/src/adapters/oxlint/oxlint-api.ts +25 -0
- package/src/adapters/oxlint/plugin.ts +35 -0
- package/src/adapters/oxlint/structure-rule.ts +39 -0
- package/src/core/baseline.ts +62 -0
- package/src/core/exports.ts +164 -0
- package/src/core/imports.ts +138 -0
- package/src/core/members.ts +131 -0
- package/src/core/patterns.ts +122 -0
- package/src/core/structure.ts +348 -0
- package/src/domain/architecture-config.ts +206 -0
- package/src/domain/architecture-error.ts +43 -0
- package/src/domain/violation.ts +24 -0
- package/src/index.ts +28 -0
- package/src/infrastructure/file-system-fake.ts +6 -0
- package/src/infrastructure/file-system-live.ts +20 -0
- package/src/infrastructure/module-resolver-fake.ts +29 -0
- package/src/infrastructure/module-resolver-live.ts +100 -0
- package/src/manifest/compile.ts +736 -0
- package/src/manifest/glob.ts +108 -0
- package/src/manifest/manifest.ts +178 -0
- package/src/ports/file-system.ts +5 -0
- 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,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
|
+
};
|