@goodbones/core 0.1.0-beta.1
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/core/baseline.d.ts +16 -0
- package/build/dts/core/baseline.d.ts.map +1 -0
- package/build/dts/core/coverage.d.ts +47 -0
- package/build/dts/core/coverage.d.ts.map +1 -0
- package/build/dts/core/exports.d.ts +38 -0
- package/build/dts/core/exports.d.ts.map +1 -0
- package/build/dts/core/graph.d.ts +40 -0
- package/build/dts/core/graph.d.ts.map +1 -0
- package/build/dts/core/imports.d.ts +29 -0
- package/build/dts/core/imports.d.ts.map +1 -0
- package/build/dts/core/members.d.ts +26 -0
- package/build/dts/core/members.d.ts.map +1 -0
- package/build/dts/core/patterns.d.ts +17 -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/core/surface.d.ts +39 -0
- package/build/dts/core/surface.d.ts.map +1 -0
- package/build/dts/domain/architecture-config.d.ts +322 -0
- package/build/dts/domain/architecture-config.d.ts.map +1 -0
- package/build/dts/domain/architecture-error.d.ts +35 -0
- package/build/dts/domain/architecture-error.d.ts.map +1 -0
- package/build/dts/domain/facts.d.ts +26 -0
- package/build/dts/domain/facts.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 +23 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/dts/infrastructure/fact-extractor-fake.d.ts +4 -0
- package/build/dts/infrastructure/fact-extractor-fake.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/manifest-file.d.ts +3 -0
- package/build/dts/infrastructure/manifest-file.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/walk.d.ts +4 -0
- package/build/dts/infrastructure/walk.d.ts.map +1 -0
- package/build/dts/load/policy.d.ts +42 -0
- package/build/dts/load/policy.d.ts.map +1 -0
- package/build/dts/manifest/compile.d.ts +26 -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 +208 -0
- package/build/dts/manifest/manifest.d.ts.map +1 -0
- package/build/dts/ports/fact-extractor.d.ts +5 -0
- package/build/dts/ports/fact-extractor.d.ts.map +1 -0
- package/build/dts/ports/file-system.d.ts +5 -0
- package/build/dts/ports/file-system.d.ts.map +1 -0
- package/build/dts/ports/language.d.ts +14 -0
- package/build/dts/ports/language.d.ts.map +1 -0
- package/build/dts/ports/module-resolver.d.ts +12 -0
- package/build/dts/ports/module-resolver.d.ts.map +1 -0
- package/build/dts/testing.d.ts +4 -0
- package/build/dts/testing.d.ts.map +1 -0
- package/build/esm/core/baseline.js +30 -0
- package/build/esm/core/baseline.js.map +1 -0
- package/build/esm/core/coverage.js +75 -0
- package/build/esm/core/coverage.js.map +1 -0
- package/build/esm/core/exports.js +102 -0
- package/build/esm/core/exports.js.map +1 -0
- package/build/esm/core/graph.js +259 -0
- package/build/esm/core/graph.js.map +1 -0
- package/build/esm/core/imports.js +103 -0
- package/build/esm/core/imports.js.map +1 -0
- package/build/esm/core/members.js +106 -0
- package/build/esm/core/members.js.map +1 -0
- package/build/esm/core/patterns.js +75 -0
- package/build/esm/core/patterns.js.map +1 -0
- package/build/esm/core/structure.js +257 -0
- package/build/esm/core/structure.js.map +1 -0
- package/build/esm/core/surface.js +137 -0
- package/build/esm/core/surface.js.map +1 -0
- package/build/esm/domain/architecture-config.js +312 -0
- package/build/esm/domain/architecture-config.js.map +1 -0
- package/build/esm/domain/architecture-error.js +41 -0
- package/build/esm/domain/architecture-error.js.map +1 -0
- package/build/esm/domain/facts.js +2 -0
- package/build/esm/domain/facts.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 +22 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/infrastructure/fact-extractor-fake.js +24 -0
- package/build/esm/infrastructure/fact-extractor-fake.js.map +1 -0
- package/build/esm/infrastructure/file-system-fake.js +10 -0
- package/build/esm/infrastructure/file-system-fake.js.map +1 -0
- package/build/esm/infrastructure/file-system-live.js +27 -0
- package/build/esm/infrastructure/file-system-live.js.map +1 -0
- package/build/esm/infrastructure/manifest-file.js +15 -0
- package/build/esm/infrastructure/manifest-file.js.map +1 -0
- package/build/esm/infrastructure/module-resolver-fake.js +33 -0
- package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
- package/build/esm/infrastructure/walk.js +45 -0
- package/build/esm/infrastructure/walk.js.map +1 -0
- package/build/esm/load/policy.js +207 -0
- package/build/esm/load/policy.js.map +1 -0
- package/build/esm/manifest/compile.js +706 -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 +303 -0
- package/build/esm/manifest/manifest.js.map +1 -0
- package/build/esm/ports/fact-extractor.js +2 -0
- package/build/esm/ports/fact-extractor.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/language.js +2 -0
- package/build/esm/ports/language.js.map +1 -0
- package/build/esm/ports/module-resolver.js +2 -0
- package/build/esm/ports/module-resolver.js.map +1 -0
- package/build/esm/testing.js +7 -0
- package/build/esm/testing.js.map +1 -0
- package/package.json +61 -0
- package/src/core/baseline.ts +62 -0
- package/src/core/coverage.ts +154 -0
- package/src/core/exports.ts +171 -0
- package/src/core/graph.ts +331 -0
- package/src/core/imports.ts +160 -0
- package/src/core/members.ts +161 -0
- package/src/core/patterns.ts +116 -0
- package/src/core/structure.ts +349 -0
- package/src/core/surface.ts +181 -0
- package/src/domain/architecture-config.ts +370 -0
- package/src/domain/architecture-error.ts +59 -0
- package/src/domain/facts.ts +47 -0
- package/src/domain/violation.ts +24 -0
- package/src/index.ts +136 -0
- package/src/infrastructure/fact-extractor-fake.ts +28 -0
- package/src/infrastructure/file-system-fake.ts +14 -0
- package/src/infrastructure/file-system-live.ts +27 -0
- package/src/infrastructure/manifest-file.ts +17 -0
- package/src/infrastructure/module-resolver-fake.ts +39 -0
- package/src/infrastructure/walk.ts +54 -0
- package/src/load/policy.ts +344 -0
- package/src/manifest/compile.ts +1011 -0
- package/src/manifest/glob.ts +108 -0
- package/src/manifest/manifest.ts +408 -0
- package/src/ports/fact-extractor.ts +12 -0
- package/src/ports/file-system.ts +8 -0
- package/src/ports/language.ts +39 -0
- package/src/ports/module-resolver.ts +28 -0
- package/src/testing.ts +6 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
// Every pattern in this config is a JavaScript regular-expression source string
|
|
4
|
+
// matched against a repo-relative, forward-slash path — the same vocabulary
|
|
5
|
+
// dependency-cruiser rules are written in, so a rule ported from there keeps its
|
|
6
|
+
// pattern character-for-character. A list matches when ANY member matches.
|
|
7
|
+
const PatternList = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
|
|
8
|
+
|
|
9
|
+
// An import edge is a violation when the importer matches `from` (and not
|
|
10
|
+
// `fromNot`) AND the resolved target matches `to` (and not `toNot`). Omitting
|
|
11
|
+
// `to` means "any target", which is how a rule expresses "this folder may not
|
|
12
|
+
// import anything outside its allowlist" as a single `toNot`.
|
|
13
|
+
// A synthetic edge this rule must report. Every rule carries one: a configured
|
|
14
|
+
// rule that reports nothing is indistinguishable from a clean codebase, and that
|
|
15
|
+
// is the failure this package exists to make impossible. `from` is a repo-relative
|
|
16
|
+
// importer path. `to` is the target in one of three forms, and the form is its
|
|
17
|
+
// dependency kind: a repo-relative resolved path is `local`, `{ external }`
|
|
18
|
+
// names a third-party package, `{ builtin }` names a runtime module.
|
|
19
|
+
const ImportProbeTarget = Schema.Union([
|
|
20
|
+
Schema.String,
|
|
21
|
+
Schema.Struct({ external: Schema.String }),
|
|
22
|
+
Schema.Struct({ builtin: Schema.String }),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const ImportProbe = Schema.Struct({
|
|
26
|
+
from: Schema.String,
|
|
27
|
+
to: ImportProbeTarget,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ImportRule = Schema.Struct({
|
|
31
|
+
name: Schema.String,
|
|
32
|
+
message: Schema.String,
|
|
33
|
+
probe: ImportProbe,
|
|
34
|
+
from: PatternList,
|
|
35
|
+
fromNot: Schema.optionalKey(PatternList),
|
|
36
|
+
to: Schema.optionalKey(PatternList),
|
|
37
|
+
toNot: Schema.optionalKey(PatternList),
|
|
38
|
+
// Third-party packages this rule permits, by package name. An external
|
|
39
|
+
// target is judged by its package, never by where the language's resolver
|
|
40
|
+
// happened to find it on disk — `to`/`toNot` patterns are for the
|
|
41
|
+
// repository's own files.
|
|
42
|
+
externals: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
43
|
+
// `external` is a third-party package, `builtin` a runtime module, `local`
|
|
44
|
+
// anything in the repository. Compared against what the resolver reports,
|
|
45
|
+
// never read off a path. Replaces dependency-cruiser's `dependencyTypes`,
|
|
46
|
+
// whose finer npm grades no rule in this repo distinguishes.
|
|
47
|
+
dependencyKind: Schema.optionalKey(Schema.Literals(["external", "local", "builtin"])),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Which language pack resolves and parses the files a scope covers. A monorepo
|
|
51
|
+
// needs several scopes even in one language — the web pass resolves `@/*` the
|
|
52
|
+
// server pass does not — and a second language is one more scope. `options`
|
|
53
|
+
// belong to the language: the policy carries them opaquely and the pack
|
|
54
|
+
// validates them at load, so nothing here knows what a tsconfig is.
|
|
55
|
+
const ResolveScope = Schema.Struct({
|
|
56
|
+
// A regular-expression source matched against the importing file's path.
|
|
57
|
+
files: Schema.String,
|
|
58
|
+
language: Schema.String,
|
|
59
|
+
options: Schema.optionalKey(Schema.Unknown),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const ResolveConfig = Schema.Struct({
|
|
63
|
+
scopes: Schema.Array(ResolveScope),
|
|
64
|
+
// An edge nobody can resolve is an edge no rule can police, which is the
|
|
65
|
+
// silent-vacuity failure this whole package exists to prevent. Default loud.
|
|
66
|
+
unresolved: Schema.optionalKey(Schema.Literals(["error", "off"])),
|
|
67
|
+
ignoreUnresolved: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// The autofix strategies a rule may name. Each is a rewrite in one language's
|
|
71
|
+
// module syntax, so a language pack lists the ones it implements.
|
|
72
|
+
export const ExportFix = Schema.Literals(["subpath-namespace-import"]);
|
|
73
|
+
|
|
74
|
+
// Which binding form an import site used. A rule that fences off a factory
|
|
75
|
+
// function cares about `named`; one steering people to namespace subpath imports
|
|
76
|
+
// cares that `named` was used at all.
|
|
77
|
+
const BindingKind = Schema.Literals(["named", "default", "namespace"]);
|
|
78
|
+
|
|
79
|
+
// `source`, when present, is a snippet the loading adapter parses: the probe
|
|
80
|
+
// then holds only if a binding named `symbol` comes out of the parser and the
|
|
81
|
+
// rule covers it, with every edge in the snippet taken to resolve to `to`.
|
|
82
|
+
// Without it the probe is a synthetic binding of `symbol` and `kind`.
|
|
83
|
+
const ExportProbe = Schema.Struct({
|
|
84
|
+
from: Schema.String,
|
|
85
|
+
to: Schema.String,
|
|
86
|
+
symbol: Schema.String,
|
|
87
|
+
kind: Schema.optionalKey(BindingKind),
|
|
88
|
+
source: Schema.optionalKey(Schema.String),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Where a given *exported symbol* may be imported. `imports` asks whether one
|
|
92
|
+
// file may reach another at all; this asks which names it may pull across when
|
|
93
|
+
// it does — the distinction a path rule cannot make, because every importer of a
|
|
94
|
+
// barrel resolves to the same file.
|
|
95
|
+
export const ExportRule = Schema.Struct({
|
|
96
|
+
name: Schema.String,
|
|
97
|
+
message: Schema.String,
|
|
98
|
+
probe: ExportProbe,
|
|
99
|
+
from: PatternList,
|
|
100
|
+
fromNot: Schema.optionalKey(PatternList),
|
|
101
|
+
to: PatternList,
|
|
102
|
+
toNot: Schema.optionalKey(PatternList),
|
|
103
|
+
// Exact exported names. Omit to mean "any name", which is how a rule bans a
|
|
104
|
+
// whole binding form (every named import from a package barrel, say).
|
|
105
|
+
symbols: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
106
|
+
// Defaults to ["named"] — the discriminating form for every rule of this shape
|
|
107
|
+
// written so far.
|
|
108
|
+
kinds: Schema.optionalKey(Schema.Array(BindingKind)),
|
|
109
|
+
// A named autofix strategy, which a language pack may or may not implement:
|
|
110
|
+
// the loader refuses a rule naming one no loaded language does.
|
|
111
|
+
// `subpath-namespace-import` is an ES-module rewrite — `import { A, B as C }
|
|
112
|
+
// from "pkg"` into `import * as A from "pkg/A"` / `import * as C from "pkg/B"`,
|
|
113
|
+
// for packages that publish each module as its own subpath. A rule carrying a
|
|
114
|
+
// fix reports once per declaration rather than once per symbol, because the
|
|
115
|
+
// fix rewrites the whole declaration.
|
|
116
|
+
fix: Schema.optionalKey(ExportFix),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// What a name was declared as. For an export site, the declaration that
|
|
120
|
+
// introduced it; for a member site, the declaration it is written in.
|
|
121
|
+
// `expression` is `export default <expr>`; `other` covers a namespace, an
|
|
122
|
+
// `export =`, and a re-export, whose declaration is somewhere else. A second
|
|
123
|
+
// language will want `struct`, `record`, `constant`, `module` here; they are
|
|
124
|
+
// added with the pack that reads them, not before.
|
|
125
|
+
export const DeclarationKind = Schema.Literals([
|
|
126
|
+
"function",
|
|
127
|
+
"class",
|
|
128
|
+
"variable",
|
|
129
|
+
"type",
|
|
130
|
+
"interface",
|
|
131
|
+
"enum",
|
|
132
|
+
"expression",
|
|
133
|
+
"other",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// What kind of name a rule is about. `members` are the names written in a
|
|
137
|
+
// named declaration — a type alias, an interface, a class body — under that
|
|
138
|
+
// declaration's name (a port's method vocabulary); `calls` are called
|
|
139
|
+
// identifiers (the hooks a tier may reach for). Which declarations a `members`
|
|
140
|
+
// rule speaks to is `declares`' to say, so the vocabulary carries no language's
|
|
141
|
+
// split between types and values.
|
|
142
|
+
const MemberSubject = Schema.Literals(["members", "calls"]);
|
|
143
|
+
|
|
144
|
+
// `source`, when present, is a snippet the loading adapter parses: the probe
|
|
145
|
+
// then holds only if a site named `name` comes out of the parser and the rule
|
|
146
|
+
// reports it — the declaration shape is the parser's to judge, not `in`'s.
|
|
147
|
+
// Without it the probe is a synthetic site of `name` inside `in`, declared as
|
|
148
|
+
// `declares`.
|
|
149
|
+
const MemberProbe = Schema.Struct({
|
|
150
|
+
from: Schema.String,
|
|
151
|
+
name: Schema.String,
|
|
152
|
+
in: Schema.optionalKey(Schema.String),
|
|
153
|
+
declares: Schema.optionalKey(DeclarationKind),
|
|
154
|
+
source: Schema.optionalKey(Schema.String),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Which names a file is allowed to declare or call. This is the one family that
|
|
158
|
+
// needs no module resolution: it is about the vocabulary inside a file, not the
|
|
159
|
+
// edges leaving it.
|
|
160
|
+
export const MemberRule = Schema.Struct({
|
|
161
|
+
name: Schema.String,
|
|
162
|
+
message: Schema.String,
|
|
163
|
+
probe: MemberProbe,
|
|
164
|
+
from: PatternList,
|
|
165
|
+
fromNot: Schema.optionalKey(PatternList),
|
|
166
|
+
subject: MemberSubject,
|
|
167
|
+
// `members` only: which declaration's members are governed, by its name.
|
|
168
|
+
in: Schema.optionalKey(PatternList),
|
|
169
|
+
// `members` only: which kinds of declaration are governed. Omit for every
|
|
170
|
+
// kind — a type alias, an interface and a class body alike.
|
|
171
|
+
declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
|
|
172
|
+
// Which names the rule speaks to at all. Omit for "every one".
|
|
173
|
+
match: Schema.optionalKey(PatternList),
|
|
174
|
+
matchNot: Schema.optionalKey(PatternList),
|
|
175
|
+
// Names that are fine. A name the rule speaks to and this does not admit is
|
|
176
|
+
// the violation.
|
|
177
|
+
allow: Schema.optionalKey(PatternList),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// One export site, as a probe states it: the name (`default` for a default
|
|
181
|
+
// export, `*` for `export *`), its binding kind, and optionally what it was
|
|
182
|
+
// declared as and whether it is a re-export.
|
|
183
|
+
const SurfaceSite = Schema.Struct({
|
|
184
|
+
name: Schema.String,
|
|
185
|
+
kind: BindingKind,
|
|
186
|
+
declares: Schema.optionalKey(DeclarationKind),
|
|
187
|
+
reexport: Schema.optionalKey(Schema.Boolean),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// A whole surface, because `count` is about the file rather than a site.
|
|
191
|
+
// `source`, when present, is parsed by the loading adapter instead.
|
|
192
|
+
const SurfaceProbe = Schema.Struct({
|
|
193
|
+
from: Schema.String,
|
|
194
|
+
sites: Schema.optionalKey(Schema.Array(SurfaceSite)),
|
|
195
|
+
source: Schema.optionalKey(Schema.String),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// What a file may export. The selectors (`kinds`, `declares`, `reexport`,
|
|
199
|
+
// `match`) say which sites the rule speaks to; exactly one demand says what is
|
|
200
|
+
// required of them. No demand means `forbid`: a selected site is the violation.
|
|
201
|
+
export const SurfaceRule = Schema.Struct({
|
|
202
|
+
name: Schema.String,
|
|
203
|
+
message: Schema.String,
|
|
204
|
+
probe: SurfaceProbe,
|
|
205
|
+
from: PatternList,
|
|
206
|
+
fromNot: Schema.optionalKey(PatternList),
|
|
207
|
+
kinds: Schema.optionalKey(Schema.Array(BindingKind)),
|
|
208
|
+
declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
|
|
209
|
+
reexport: Schema.optionalKey(Schema.Boolean),
|
|
210
|
+
match: Schema.optionalKey(PatternList),
|
|
211
|
+
matchNot: Schema.optionalKey(PatternList),
|
|
212
|
+
forbid: Schema.optionalKey(Schema.Boolean),
|
|
213
|
+
// Names that are fine; a selected site named otherwise is the violation.
|
|
214
|
+
allow: Schema.optionalKey(PatternList),
|
|
215
|
+
// A regular-expression source every selected name must match.
|
|
216
|
+
convention: Schema.optionalKey(Schema.String),
|
|
217
|
+
// How many selected sites the file may have.
|
|
218
|
+
count: Schema.optionalKey(
|
|
219
|
+
Schema.Struct({
|
|
220
|
+
min: Schema.optionalKey(Schema.Finite),
|
|
221
|
+
max: Schema.optionalKey(Schema.Finite),
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// A small synthetic graph the rule must report on: the edges, and any files
|
|
227
|
+
// that take part without an edge (an orphan has none).
|
|
228
|
+
const GraphProbe = Schema.Struct({
|
|
229
|
+
edges: Schema.Array(Schema.Tuple([Schema.String, Schema.String])),
|
|
230
|
+
files: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Rules about the shape of the whole import graph, which no single file can
|
|
234
|
+
// answer. The CLI evaluates them; the plugin, which sees one file at a time,
|
|
235
|
+
// compiles and probes them so a vacuous one still fails to load.
|
|
236
|
+
export const GraphCycleRule = Schema.Struct({
|
|
237
|
+
name: Schema.String,
|
|
238
|
+
message: Schema.String,
|
|
239
|
+
probe: GraphProbe,
|
|
240
|
+
within: PatternList,
|
|
241
|
+
withinNot: Schema.optionalKey(PatternList),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
export const GraphOrphanRule = Schema.Struct({
|
|
245
|
+
name: Schema.String,
|
|
246
|
+
message: Schema.String,
|
|
247
|
+
probe: GraphProbe,
|
|
248
|
+
within: PatternList,
|
|
249
|
+
withinNot: Schema.optionalKey(PatternList),
|
|
250
|
+
// Files that are imported by nothing by design — the program's entry points.
|
|
251
|
+
entry: PatternList,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export const GraphReachRule = Schema.Struct({
|
|
255
|
+
name: Schema.String,
|
|
256
|
+
message: Schema.String,
|
|
257
|
+
probe: GraphProbe,
|
|
258
|
+
from: PatternList,
|
|
259
|
+
fromNot: Schema.optionalKey(PatternList),
|
|
260
|
+
to: PatternList,
|
|
261
|
+
toNot: Schema.optionalKey(PatternList),
|
|
262
|
+
// The tier that was supposed to mediate: a path stepping onto a `via` file is
|
|
263
|
+
// allowed, so only a path that avoids every `via` is the violation.
|
|
264
|
+
via: Schema.optionalKey(PatternList),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export const GraphConfig = Schema.Struct({
|
|
268
|
+
cycles: Schema.optionalKey(Schema.Array(GraphCycleRule)),
|
|
269
|
+
orphans: Schema.optionalKey(Schema.Array(GraphOrphanRule)),
|
|
270
|
+
reach: Schema.optionalKey(Schema.Array(GraphReachRule)),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const PathProbe = Schema.Struct({ path: Schema.String });
|
|
274
|
+
|
|
275
|
+
// The file taxonomy, as three questions rather than one nested tree.
|
|
276
|
+
//
|
|
277
|
+
// `roots` marks the regions where layout is deny-by-default. `folders` says
|
|
278
|
+
// which basenames each folder admits. `parity` says which siblings a file owes.
|
|
279
|
+
// Keeping them apart is what removes the nested config's most fragile rule —
|
|
280
|
+
// that a specific pattern must beat a `*` catch-all — because an exemption is
|
|
281
|
+
// now a `fileNot` on the parity rule that would otherwise fire.
|
|
282
|
+
const StructureRoot = Schema.Struct({
|
|
283
|
+
name: Schema.String,
|
|
284
|
+
message: Schema.String,
|
|
285
|
+
probe: PathProbe,
|
|
286
|
+
// A file under this path whose folder no `folders` rule governs is a file in a
|
|
287
|
+
// folder the taxonomy does not know about.
|
|
288
|
+
path: PatternList,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const StructureFolder = Schema.Struct({
|
|
292
|
+
name: Schema.String,
|
|
293
|
+
message: Schema.String,
|
|
294
|
+
probe: PathProbe,
|
|
295
|
+
// Matched against the file's folder, repo-relative, with no trailing slash.
|
|
296
|
+
folder: PatternList,
|
|
297
|
+
// Basenames this folder admits. Anything else is the violation.
|
|
298
|
+
files: PatternList,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const StructureParity = Schema.Struct({
|
|
302
|
+
name: Schema.String,
|
|
303
|
+
message: Schema.String,
|
|
304
|
+
probe: PathProbe,
|
|
305
|
+
file: PatternList,
|
|
306
|
+
fileNot: Schema.optionalKey(PatternList),
|
|
307
|
+
// Paths that must exist, relative to the file's own folder. `{base}` is the
|
|
308
|
+
// filename minus its final extension — `create-todo.handler` for
|
|
309
|
+
// `create-todo.handler.ts`, `handler` for `handler.go` — so `{base}.test.ts`
|
|
310
|
+
// or `{base}_test.go` names the sibling test in either language.
|
|
311
|
+
requires: Schema.Array(Schema.String),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// What shape the variable part of a name may take. `folders` says which
|
|
315
|
+
// stereotypes a folder admits; this says what the concept name in front of the
|
|
316
|
+
// stereotype may look like — the degree of freedom a taxonomy alone leaves open.
|
|
317
|
+
const StructureNaming = Schema.Struct({
|
|
318
|
+
name: Schema.String,
|
|
319
|
+
message: Schema.String,
|
|
320
|
+
probe: PathProbe,
|
|
321
|
+
// Matched against the whole repo-relative path, and carrying capture groups:
|
|
322
|
+
// `subject` says which of them holds the name being judged.
|
|
323
|
+
file: PatternList,
|
|
324
|
+
fileNot: Schema.optionalKey(PatternList),
|
|
325
|
+
subject: Schema.Finite,
|
|
326
|
+
// The shape the subject must have. Exactly one of these.
|
|
327
|
+
convention: Schema.optionalKey(Schema.String),
|
|
328
|
+
// A capture group the subject must equal, for "named after its folder".
|
|
329
|
+
sameAs: Schema.optionalKey(Schema.Finite),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const StructureConfig = Schema.Struct({
|
|
333
|
+
roots: Schema.optionalKey(Schema.Array(StructureRoot)),
|
|
334
|
+
folders: Schema.optionalKey(Schema.Array(StructureFolder)),
|
|
335
|
+
parity: Schema.optionalKey(Schema.Array(StructureParity)),
|
|
336
|
+
naming: Schema.optionalKey(Schema.Array(StructureNaming)),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export type ImportRule = (typeof ImportRule)["Type"];
|
|
340
|
+
export type ResolveConfig = (typeof ResolveConfig)["Type"];
|
|
341
|
+
export type ResolveScope = (typeof ResolveScope)["Type"];
|
|
342
|
+
export type ImportProbe = (typeof ImportProbe)["Type"];
|
|
343
|
+
export type ImportProbeTarget = (typeof ImportProbeTarget)["Type"];
|
|
344
|
+
export type ExportRule = (typeof ExportRule)["Type"];
|
|
345
|
+
export type ExportProbe = (typeof ExportProbe)["Type"];
|
|
346
|
+
export type BindingKind = (typeof BindingKind)["Type"];
|
|
347
|
+
export type ExportFix = (typeof ExportFix)["Type"];
|
|
348
|
+
export type MemberRule = (typeof MemberRule)["Type"];
|
|
349
|
+
export type MemberProbe = (typeof MemberProbe)["Type"];
|
|
350
|
+
export type MemberSubject = (typeof MemberSubject)["Type"];
|
|
351
|
+
export type GraphProbe = (typeof GraphProbe)["Type"];
|
|
352
|
+
export type GraphCycleRule = (typeof GraphCycleRule)["Type"];
|
|
353
|
+
export type GraphOrphanRule = (typeof GraphOrphanRule)["Type"];
|
|
354
|
+
export type GraphReachRule = (typeof GraphReachRule)["Type"];
|
|
355
|
+
export type GraphConfig = (typeof GraphConfig)["Type"];
|
|
356
|
+
export type DeclarationKind = (typeof DeclarationKind)["Type"];
|
|
357
|
+
export type SurfaceRule = (typeof SurfaceRule)["Type"];
|
|
358
|
+
export type SurfaceProbe = (typeof SurfaceProbe)["Type"];
|
|
359
|
+
export type StructureConfig = (typeof StructureConfig)["Type"];
|
|
360
|
+
export type StructureRoot = (typeof StructureRoot)["Type"];
|
|
361
|
+
export type StructureFolder = (typeof StructureFolder)["Type"];
|
|
362
|
+
export type StructureParity = (typeof StructureParity)["Type"];
|
|
363
|
+
export type StructureNaming = (typeof StructureNaming)["Type"];
|
|
364
|
+
|
|
365
|
+
// The file pattern an open folder's layout rule carries: it admits any name,
|
|
366
|
+
// so it claims the folder without policing it. Coverage counts it apart.
|
|
367
|
+
export const OPEN_LAYOUT = "^.*$";
|
|
368
|
+
|
|
369
|
+
export const patternsOf = (patterns: string | ReadonlyArray<string>): ReadonlyArray<string> =>
|
|
370
|
+
typeof patterns === "string" ? [patterns] : patterns;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
// These errors surface through oxlint's plugin loader, which prints a stack and
|
|
4
|
+
// nothing else. The `message` override is what turns "ConfigInvalid" into an
|
|
5
|
+
// instruction the reader can act on.
|
|
6
|
+
|
|
7
|
+
export class ConfigInvalid extends Schema.TaggedErrorClass<ConfigInvalid>("ConfigInvalid")(
|
|
8
|
+
"ConfigInvalid",
|
|
9
|
+
{ configPath: Schema.String, detail: Schema.String },
|
|
10
|
+
) {
|
|
11
|
+
override get message(): string {
|
|
12
|
+
return `${this.configPath}: ${this.detail}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PatternInvalid extends Schema.TaggedErrorClass<PatternInvalid>("PatternInvalid")(
|
|
17
|
+
"PatternInvalid",
|
|
18
|
+
{
|
|
19
|
+
ruleName: Schema.String,
|
|
20
|
+
field: Schema.String,
|
|
21
|
+
pattern: Schema.String,
|
|
22
|
+
detail: Schema.String,
|
|
23
|
+
},
|
|
24
|
+
) {
|
|
25
|
+
override get message(): string {
|
|
26
|
+
return `rule "${this.ruleName}" has an uncompilable ${this.field} pattern ${JSON.stringify(
|
|
27
|
+
this.pattern,
|
|
28
|
+
)}: ${this.detail}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A `resolve.scopes` entry a language pack cannot build a resolver from: options
|
|
33
|
+
// it does not understand, or a language no pack answers to. Raised by the pack,
|
|
34
|
+
// without the config path, which the loader adds when it reports it.
|
|
35
|
+
export class ScopeInvalid extends Schema.TaggedErrorClass<ScopeInvalid>("ScopeInvalid")(
|
|
36
|
+
"ScopeInvalid",
|
|
37
|
+
{
|
|
38
|
+
files: Schema.String,
|
|
39
|
+
language: Schema.String,
|
|
40
|
+
detail: Schema.String,
|
|
41
|
+
},
|
|
42
|
+
) {
|
|
43
|
+
override get message(): string {
|
|
44
|
+
return `resolve scope ${JSON.stringify(this.files)} (${this.language}): ${this.detail}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ImportUnresolved extends Schema.TaggedErrorClass<ImportUnresolved>("ImportUnresolved")(
|
|
49
|
+
"ImportUnresolved",
|
|
50
|
+
{
|
|
51
|
+
fromFile: Schema.String,
|
|
52
|
+
specifier: Schema.String,
|
|
53
|
+
detail: Schema.String,
|
|
54
|
+
},
|
|
55
|
+
) {
|
|
56
|
+
override get message(): string {
|
|
57
|
+
return `${this.fromFile} imports "${this.specifier}", which does not resolve: ${this.detail}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BindingKind, DeclarationKind, MemberSubject } from "./architecture-config.js";
|
|
2
|
+
|
|
3
|
+
// What a policy can know about one source file, read once. Both adapters
|
|
4
|
+
// produce this vocabulary — the plugin from oxlint's syntax tree, the CLI from
|
|
5
|
+
// TypeScript's — which is what keeps them answerable to the same core rather
|
|
6
|
+
// than to each other. A rule is evaluated against these and nothing else.
|
|
7
|
+
|
|
8
|
+
// One name pulled across one import edge: `import { makeCommandBus } from "…"`
|
|
9
|
+
// is a single binding, and so is the `Effect` in `import { Effect } from "effect"`.
|
|
10
|
+
export type Binding = {
|
|
11
|
+
readonly symbol: string;
|
|
12
|
+
readonly kind: BindingKind;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// One declared or called name. For `members`, the declaration it is written in
|
|
16
|
+
// — its name and what it was declared as (`type`, `interface`, `class`); both
|
|
17
|
+
// absent for `calls`.
|
|
18
|
+
export type MemberSite = {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly subject: MemberSubject;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly in?: string;
|
|
23
|
+
readonly declares?: DeclarationKind;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// One name a file offers, at its top level: `default` for a default export,
|
|
27
|
+
// `*` for `export *`, the exported (not the local) name otherwise. `declares`
|
|
28
|
+
// is what it was declared as when the declaration is in this file, and `other`
|
|
29
|
+
// for a re-export, whose declaration is elsewhere.
|
|
30
|
+
export type ExportSite = {
|
|
31
|
+
readonly file: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly kind: BindingKind;
|
|
34
|
+
readonly declares: DeclarationKind;
|
|
35
|
+
readonly reexport: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SourceFacts = {
|
|
39
|
+
// One entry per import edge, in source order. An edge appears once even if it
|
|
40
|
+
// carries several bindings.
|
|
41
|
+
readonly specifiers: ReadonlyArray<string>;
|
|
42
|
+
// The names pulled across each edge, keyed by specifier.
|
|
43
|
+
readonly bindings: ReadonlyMap<string, ReadonlyArray<Binding>>;
|
|
44
|
+
readonly memberSites: ReadonlyArray<MemberSite>;
|
|
45
|
+
// In source order; a file's whole surface, top-level statements only.
|
|
46
|
+
readonly exportSites: ReadonlyArray<ExportSite>;
|
|
47
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ViolationKind = "import" | "export" | "structure" | "member" | "surface" | "graph";
|
|
2
|
+
|
|
3
|
+
export type Violation = {
|
|
4
|
+
readonly kind: ViolationKind;
|
|
5
|
+
readonly ruleName: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
// Repo-relative, forward slashes.
|
|
8
|
+
readonly file: string;
|
|
9
|
+
// The other end of the violated relationship: the resolved import target, the
|
|
10
|
+
// restricted symbol, the missing sibling, the disallowed member. `null` when
|
|
11
|
+
// the file alone is the violation.
|
|
12
|
+
readonly subject: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Deliberately excludes line and column so an entry survives ordinary edits to
|
|
16
|
+
// the file it names — a baseline keyed on positions would go stale on the first
|
|
17
|
+
// reformat and silently re-admit a violation it was never meant to cover.
|
|
18
|
+
export const fingerprintOf = (violation: Violation): string =>
|
|
19
|
+
[violation.kind, violation.ruleName, violation.file, violation.subject ?? ""].join("|");
|
|
20
|
+
|
|
21
|
+
// `{name}` in a message is replaced with the subject, so a rule can quote the
|
|
22
|
+
// offending symbol without the config needing a callback.
|
|
23
|
+
export const formatMessage = (violation: Violation): string =>
|
|
24
|
+
`[${violation.ruleName}] ${violation.message.replaceAll("{name}", violation.subject ?? "")}`;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The core's public surface: the manifest vocabulary, the evaluators per
|
|
2
|
+
// family, the ports a language pack implements, and the loader that turns a
|
|
3
|
+
// manifest into a policy. A host composes these with a language pack and the
|
|
4
|
+
// live file system; a language pack implements the ports. Nothing here names a
|
|
5
|
+
// language. The fakes are under `@goodbones/core/testing`.
|
|
6
|
+
export {
|
|
7
|
+
type Baseline,
|
|
8
|
+
type BaselineFilter,
|
|
9
|
+
baselineOf,
|
|
10
|
+
decodeBaseline,
|
|
11
|
+
EMPTY_BASELINE,
|
|
12
|
+
makeBaselineFilter,
|
|
13
|
+
serializeBaseline,
|
|
14
|
+
staleEntriesOf,
|
|
15
|
+
unbaselined,
|
|
16
|
+
} from "./core/baseline.js";
|
|
17
|
+
export {
|
|
18
|
+
type Coverage,
|
|
19
|
+
type CoverageFamily,
|
|
20
|
+
type CoverageFloors,
|
|
21
|
+
coverageOf,
|
|
22
|
+
coverageShortfalls,
|
|
23
|
+
fractionsOf,
|
|
24
|
+
} from "./core/coverage.js";
|
|
25
|
+
export {
|
|
26
|
+
type BindingEdge,
|
|
27
|
+
type CompiledExportRule,
|
|
28
|
+
compileExportRule,
|
|
29
|
+
compileExportRules,
|
|
30
|
+
evaluateBindingEdge,
|
|
31
|
+
evaluateSelectedBindings,
|
|
32
|
+
exportRulesFailingTheirProbe,
|
|
33
|
+
exportRulesSelecting,
|
|
34
|
+
type ExportViolation,
|
|
35
|
+
type SelectedExportRule,
|
|
36
|
+
} from "./core/exports.js";
|
|
37
|
+
export {
|
|
38
|
+
type CompiledGraph,
|
|
39
|
+
compileGraphRules,
|
|
40
|
+
EMPTY_GRAPH_RULES,
|
|
41
|
+
evaluateGraph,
|
|
42
|
+
type Graph,
|
|
43
|
+
graphRulesFailingTheirProbe,
|
|
44
|
+
hasGraphRules,
|
|
45
|
+
} from "./core/graph.js";
|
|
46
|
+
export {
|
|
47
|
+
type CompiledImportRule,
|
|
48
|
+
compileImportRule,
|
|
49
|
+
compileImportRules,
|
|
50
|
+
evaluateImportEdge,
|
|
51
|
+
evaluateSelectedEdge,
|
|
52
|
+
type ImportEdge,
|
|
53
|
+
probeTargetOf,
|
|
54
|
+
rulesFailingTheirProbe,
|
|
55
|
+
rulesSelecting,
|
|
56
|
+
type SelectedRule,
|
|
57
|
+
} from "./core/imports.js";
|
|
58
|
+
export {
|
|
59
|
+
type CompiledMemberRule,
|
|
60
|
+
compileMemberRules,
|
|
61
|
+
evaluateMemberSite,
|
|
62
|
+
memberRulesFailingTheirProbe,
|
|
63
|
+
memberRulesSelecting,
|
|
64
|
+
} from "./core/members.js";
|
|
65
|
+
export {
|
|
66
|
+
type CompiledStructure,
|
|
67
|
+
compileStructure,
|
|
68
|
+
EMPTY_STRUCTURE,
|
|
69
|
+
evaluateStructure,
|
|
70
|
+
requiredSiblingsOf,
|
|
71
|
+
structureRulesFailingTheirProbe,
|
|
72
|
+
} from "./core/structure.js";
|
|
73
|
+
export {
|
|
74
|
+
type CompiledSurfaceRule,
|
|
75
|
+
compileSurfaceRules,
|
|
76
|
+
evaluateSurface,
|
|
77
|
+
surfaceRulesFailingTheirProbe,
|
|
78
|
+
surfaceRulesSelecting,
|
|
79
|
+
} from "./core/surface.js";
|
|
80
|
+
export {
|
|
81
|
+
type BindingKind,
|
|
82
|
+
type DeclarationKind,
|
|
83
|
+
type ExportFix,
|
|
84
|
+
type ExportRule,
|
|
85
|
+
type GraphConfig,
|
|
86
|
+
type GraphCycleRule,
|
|
87
|
+
type GraphOrphanRule,
|
|
88
|
+
type GraphReachRule,
|
|
89
|
+
type ImportProbe,
|
|
90
|
+
type ImportProbeTarget,
|
|
91
|
+
type ImportRule,
|
|
92
|
+
type MemberRule,
|
|
93
|
+
type MemberSubject,
|
|
94
|
+
type ResolveConfig,
|
|
95
|
+
type ResolveScope,
|
|
96
|
+
type StructureConfig,
|
|
97
|
+
type SurfaceRule,
|
|
98
|
+
} from "./domain/architecture-config.js";
|
|
99
|
+
export {
|
|
100
|
+
ConfigInvalid,
|
|
101
|
+
ImportUnresolved,
|
|
102
|
+
PatternInvalid,
|
|
103
|
+
ScopeInvalid,
|
|
104
|
+
} from "./domain/architecture-error.js";
|
|
105
|
+
export {
|
|
106
|
+
type Binding,
|
|
107
|
+
type ExportSite,
|
|
108
|
+
type MemberSite,
|
|
109
|
+
type SourceFacts,
|
|
110
|
+
} from "./domain/facts.js";
|
|
111
|
+
export {
|
|
112
|
+
fingerprintOf,
|
|
113
|
+
formatMessage,
|
|
114
|
+
type Violation,
|
|
115
|
+
type ViolationKind,
|
|
116
|
+
} from "./domain/violation.js";
|
|
117
|
+
export { makeFileSystemLive } from "./infrastructure/file-system-live.js";
|
|
118
|
+
export { DEFAULT_CONFIG_FILENAME, readManifestFile } from "./infrastructure/manifest-file.js";
|
|
119
|
+
export { listSourceFiles, type WalkedLanguage } from "./infrastructure/walk.js";
|
|
120
|
+
export { type LoadedPolicy, loadPolicy, type LoadPolicyInput } from "./load/policy.js";
|
|
121
|
+
export { type LoweredRules, lowerManifest, type ProbeLanguage } from "./manifest/compile.js";
|
|
122
|
+
export {
|
|
123
|
+
type DecodedManifest,
|
|
124
|
+
decodeManifest,
|
|
125
|
+
type Manifest,
|
|
126
|
+
type ManifestNode,
|
|
127
|
+
Manifest as ManifestSchema,
|
|
128
|
+
} from "./manifest/manifest.js";
|
|
129
|
+
export { type FactExtractor } from "./ports/fact-extractor.js";
|
|
130
|
+
export { type FileSystem } from "./ports/file-system.js";
|
|
131
|
+
export { type Language } from "./ports/language.js";
|
|
132
|
+
export {
|
|
133
|
+
type DependencyKind,
|
|
134
|
+
type ModuleResolver,
|
|
135
|
+
type ResolvedTarget,
|
|
136
|
+
} from "./ports/module-resolver.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SourceFacts } from "../domain/facts.js";
|
|
2
|
+
import type { FactExtractor } from "../ports/fact-extractor.js";
|
|
3
|
+
|
|
4
|
+
const NOTHING: SourceFacts = {
|
|
5
|
+
specifiers: [],
|
|
6
|
+
bindings: new Map(),
|
|
7
|
+
memberSites: [],
|
|
8
|
+
exportSites: [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Keyed by the source text itself: a core test states what a snippet reads as
|
|
12
|
+
// and never the parse, which has its own tests against the real extractor. A
|
|
13
|
+
// snippet not staged reads as nothing — the shape of a form the extractor is
|
|
14
|
+
// blind to, which is the case a source probe exists to catch.
|
|
15
|
+
export const makeFactExtractorFake = (
|
|
16
|
+
facts: Readonly<Record<string, Partial<SourceFacts>>>,
|
|
17
|
+
): FactExtractor => ({
|
|
18
|
+
factsOf: (file, text) => {
|
|
19
|
+
const found = facts[text];
|
|
20
|
+
if (found === undefined) return NOTHING;
|
|
21
|
+
return {
|
|
22
|
+
...NOTHING,
|
|
23
|
+
...found,
|
|
24
|
+
memberSites: (found.memberSites ?? []).map((site) => ({ ...site, file })),
|
|
25
|
+
exportSites: (found.exportSites ?? []).map((site) => ({ ...site, file })),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FileSystem } from "../ports/file-system.js";
|
|
2
|
+
|
|
3
|
+
// The files that exist, and optionally what some of them say. A file staged
|
|
4
|
+
// with contents exists; one staged by name alone exists and reads as nothing.
|
|
5
|
+
export const makeFileSystemFake = (
|
|
6
|
+
present: Iterable<string>,
|
|
7
|
+
contents: Readonly<Record<string, string>> = {},
|
|
8
|
+
): FileSystem => {
|
|
9
|
+
const paths = new Set([...present, ...Object.keys(contents)]);
|
|
10
|
+
return {
|
|
11
|
+
exists: (repoRelativePath) => paths.has(repoRelativePath),
|
|
12
|
+
readText: (repoRelativePath) => contents[repoRelativePath] ?? null,
|
|
13
|
+
};
|
|
14
|
+
};
|