@drkmttr/adhere 0.13.0 → 0.13.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/README.md +45 -0
- package/package.json +6 -6
- package/skills/adhere/SKILL.md +7 -0
- package/src/config.ts +14 -0
- package/src/markdown.ts +9 -1
- package/src/services/AdhereConfig.ts +13 -1
- package/src/services/Jev.http.ts +11 -5
- package/src/services/Jev.ts +9 -7
- package/src/workflows/audit.ts +12 -4
- package/src/workspaces.ts +141 -0
package/README.md
CHANGED
|
@@ -676,6 +676,51 @@ its matchers dropped, with their scores.
|
|
|
676
676
|
Keeping the scope out of the description keeps the description to what the
|
|
677
677
|
rule asks for, which the report shows, and gives each part a score of its own.
|
|
678
678
|
|
|
679
|
+
### Workspace packages
|
|
680
|
+
|
|
681
|
+
A rule can turn on whose package an import is of: the repository's own, or
|
|
682
|
+
one installed from a registry. The file alone does not show that, so a rule
|
|
683
|
+
can ask for the workspace's packages, with `includeWorkspacePackages: true`
|
|
684
|
+
in its front matter, or as a field of a rule in TypeScript or a config:
|
|
685
|
+
|
|
686
|
+
````md
|
|
687
|
+
---
|
|
688
|
+
description: A function from one of this repository's own packages, which `workspacePackages` names, must be called through the package's namespace, never imported by its own name.
|
|
689
|
+
includeWorkspacePackages: true
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Must
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import * as Orders from "orders-core";
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## Never
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
import { parse } from "orders-core";
|
|
702
|
+
```
|
|
703
|
+
````
|
|
704
|
+
|
|
705
|
+
Jev then reads, beside the code, `workspacePackages`: each package's name
|
|
706
|
+
with its directory from the working directory, as in
|
|
707
|
+
`{ "orders-core": "packages/orders" }`. The description can name that key in
|
|
708
|
+
backticks, as the questions name `code`. A package that is not in it is an
|
|
709
|
+
installed one.
|
|
710
|
+
|
|
711
|
+
The packages are read on every run, when a rule asks for them, from the
|
|
712
|
+
working directory's `package.json`, whose `workspaces` npm, Bun, and Yarn
|
|
713
|
+
read, and from `pnpm-workspace.yaml`'s `packages`. In a pattern, `*` is any
|
|
714
|
+
directory and `**` any depth of them, and one that starts with `!` leaves out
|
|
715
|
+
what it matches. Each matched directory's `package.json` gives the name.
|
|
716
|
+
Nothing need be installed, so a run in CI reads the same packages as one on a
|
|
717
|
+
laptop. A working directory that is not a workspace's root has none.
|
|
718
|
+
|
|
719
|
+
Such a rule goes to Jev in requests of its own, as a rule with `appendState`
|
|
720
|
+
does, so no other rule reads the packages. They are part of what its
|
|
721
|
+
judgments are cached under: when a package is added, renamed, or moved, that
|
|
722
|
+
rule is judged again for every file, and no other rule is.
|
|
723
|
+
|
|
679
724
|
### Rule writing tips
|
|
680
725
|
|
|
681
726
|
We've evaluated different ways of giving Jev a rule, to catch the most
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@drkmttr/adhere-darwin-arm64": "0.13.
|
|
72
|
-
"@drkmttr/adhere-darwin-x64": "0.13.
|
|
73
|
-
"@drkmttr/adhere-linux-arm64": "0.13.
|
|
74
|
-
"@drkmttr/adhere-linux-x64": "0.13.
|
|
75
|
-
"@drkmttr/adhere-win32-x64": "0.13.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.13.1",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.13.1",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.13.1",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.13.1",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.13.1"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -102,6 +102,13 @@ Front matter besides `description`, all optional:
|
|
|
102
102
|
one line, scope a rule without widening its description: a finding stands
|
|
103
103
|
only when Jev says the code that breaks the rule is as every `appliesTo`
|
|
104
104
|
describes and as no `excludeIf` does.
|
|
105
|
+
- `includeWorkspacePackages: true` gives Jev `workspacePackages` beside the
|
|
106
|
+
code: each of the repository's own packages, by name, with its directory,
|
|
107
|
+
read from the root `package.json`'s `workspaces` or `pnpm-workspace.yaml`.
|
|
108
|
+
Use it for a rule that turns on whether an import is of the repository's
|
|
109
|
+
own package or an installed one, and name `workspacePackages` in the
|
|
110
|
+
description. The rule costs a request of its own per file, and a changed
|
|
111
|
+
workspace judges it again.
|
|
105
112
|
|
|
106
113
|
A rule's directory can hold a `RULE.ts` instead, which default-exports
|
|
107
114
|
`defineRule({...})` from `@drkmttr/adhere`, with the fields a config's inline
|
package/src/config.ts
CHANGED
|
@@ -67,6 +67,14 @@ const RuleFields = Schema.Struct({
|
|
|
67
67
|
* that breaks the rule is as it describes, and a yes to any drops the finding.
|
|
68
68
|
*/
|
|
69
69
|
excludeIf: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
70
|
+
/**
|
|
71
|
+
* Whether Jev reads, beside the code, the workspace's packages as
|
|
72
|
+
* `workspacePackages`: each one's name with its directory, so the rule can
|
|
73
|
+
* tell an import of the repository's own package from an installed one's.
|
|
74
|
+
* The rule's questions go in requests of their own, and a change to the
|
|
75
|
+
* packages judges it again.
|
|
76
|
+
*/
|
|
77
|
+
includeWorkspacePackages: Schema.optionalKey(Schema.Boolean),
|
|
70
78
|
/**
|
|
71
79
|
* A hook on the state Jev reads for the rule, in a rule written in
|
|
72
80
|
* TypeScript (see `AppendState`). The rule's questions go in requests of
|
|
@@ -296,6 +304,12 @@ export interface ResolvedConfig {
|
|
|
296
304
|
readonly exclude?: ReadonlyArray<string>;
|
|
297
305
|
/** Whether Jev reads every comment; unset, only the `@adhere` notes. */
|
|
298
306
|
readonly includeComments?: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* The workspace's packages, each name with its directory from the working
|
|
309
|
+
* directory, read from its manifests on every run, when a rule asks for
|
|
310
|
+
* them with `includeWorkspacePackages`.
|
|
311
|
+
*/
|
|
312
|
+
readonly workspacePackages?: Readonly<Record<string, string>>;
|
|
299
313
|
}
|
|
300
314
|
|
|
301
315
|
/** Command-line values that apply on top of the config file. */
|
package/src/markdown.ts
CHANGED
|
@@ -44,6 +44,7 @@ const FrontMatter = Schema.Struct({
|
|
|
44
44
|
level: Schema.optionalKey(Schema.Literals(["error", "warning"])),
|
|
45
45
|
appliesTo: Schema.optionalKey(Matchers),
|
|
46
46
|
excludeIf: Schema.optionalKey(Matchers),
|
|
47
|
+
includeWorkspacePackages: Schema.optionalKey(Schema.Literals(["true", "false"])),
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
@@ -206,5 +207,12 @@ export const parseRuleMarkdown = (
|
|
|
206
207
|
message: `${file}: the body needs code, in a fence under a Must, Never, Should, or Should not heading, or untagged`,
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
const { includeWorkspacePackages, ...fields } = front;
|
|
211
|
+
return {
|
|
212
|
+
...fields,
|
|
213
|
+
...(includeWorkspacePackages === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { includeWorkspacePackages: includeWorkspacePackages === "true" }),
|
|
216
|
+
...code,
|
|
217
|
+
};
|
|
210
218
|
});
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
shownId,
|
|
22
22
|
withOverrides,
|
|
23
23
|
} from "#rules.ts";
|
|
24
|
+
import { workspacePackagesOf } from "#workspaces.ts";
|
|
24
25
|
import { Context, Effect, FileSystem, Layer, Path, Record } from "effect";
|
|
25
26
|
|
|
26
27
|
export class AdhereConfig extends Context.Service<AdhereConfig, ResolvedConfig>()(
|
|
@@ -129,6 +130,17 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
const scopedRules = withOverrides(entries, overrides);
|
|
134
|
+
// The manifests are read on every run, so a changed workspace is seen, and only when a rule reads them.
|
|
135
|
+
const workspacePackages = scopedRules.some(
|
|
136
|
+
(entry) => entry.rule.includeWorkspacePackages === true,
|
|
137
|
+
)
|
|
138
|
+
? yield* workspacePackagesOf(cwd)
|
|
139
|
+
: undefined;
|
|
140
|
+
return AdhereConfig.of({
|
|
141
|
+
...resolved,
|
|
142
|
+
scopedRules,
|
|
143
|
+
...(workspacePackages === undefined ? {} : { workspacePackages }),
|
|
144
|
+
});
|
|
133
145
|
}),
|
|
134
146
|
);
|
package/src/services/Jev.http.ts
CHANGED
|
@@ -68,14 +68,19 @@ const refused = (message: string) => JevUnavailable.make({ message });
|
|
|
68
68
|
const bun = (globalThis as { readonly Bun?: unknown }).Bun as Parameters<AppendState>[2];
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* A body as it goes out: when its rule has `
|
|
72
|
-
*
|
|
71
|
+
* A body as it goes out: when its rule has `includeWorkspacePackages`, with
|
|
72
|
+
* the workspace's packages in its state, and when it has `appendState`, with
|
|
73
|
+
* what the hook returns spread over that. A hook that fails refuses the run.
|
|
73
74
|
*/
|
|
74
75
|
const hooked = (
|
|
75
|
-
|
|
76
|
+
sent: Body & { readonly state: JevState },
|
|
76
77
|
group: Rules,
|
|
77
78
|
file: JudgedFile,
|
|
79
|
+
workspacePackages: Readonly<Record<string, string>>,
|
|
78
80
|
): Effect.Effect<Body, JevUnavailable> => {
|
|
81
|
+
const body = Object.values(group).some((rule) => rule.includeWorkspacePackages === true)
|
|
82
|
+
? { ...sent, state: { ...sent.state, workspacePackages } }
|
|
83
|
+
: sent;
|
|
79
84
|
const [hook] = Object.entries(group).flatMap(([id, rule]) =>
|
|
80
85
|
rule.appendState === undefined ? [] : [[id, rule.appendState] as const],
|
|
81
86
|
);
|
|
@@ -273,8 +278,9 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
273
278
|
) =>
|
|
274
279
|
Effect.map(
|
|
275
280
|
Effect.forEach(requestGroups(rules), (group) =>
|
|
276
|
-
Effect.flatMap(
|
|
277
|
-
|
|
281
|
+
Effect.flatMap(
|
|
282
|
+
hooked(bodyOf(group), group, file, config.workspacePackages ?? {}),
|
|
283
|
+
(body) => answersTo(kind, body, Answers),
|
|
278
284
|
),
|
|
279
285
|
),
|
|
280
286
|
(groups) =>
|
package/src/services/Jev.ts
CHANGED
|
@@ -477,18 +477,20 @@ export const requestsOf = (body: Body): ReadonlyArray<Body> => {
|
|
|
477
477
|
return requests.map(({ questions }) => ({ ...body, questions }));
|
|
478
478
|
};
|
|
479
479
|
|
|
480
|
+
/** Whether a rule reads state of its own: what its `appendState` adds, or the workspace's packages. */
|
|
481
|
+
const readsOwnState = (rule: Rule): boolean =>
|
|
482
|
+
rule.appendState !== undefined || rule.includeWorkspacePackages === true;
|
|
483
|
+
|
|
480
484
|
/**
|
|
481
|
-
* The rules as requests carry them: every rule
|
|
482
|
-
* body, and each rule with
|
|
483
|
-
*
|
|
485
|
+
* The rules as requests carry them: every rule that reads only the code in
|
|
486
|
+
* one body, and each rule with state of its own in a body of its own, so
|
|
487
|
+
* that what is added to the state for it only its own questions read.
|
|
484
488
|
*/
|
|
485
489
|
export const requestGroups = (rules: Rules): ReadonlyArray<Rules> => {
|
|
486
|
-
const shared = Record.filter(rules, (rule) => rule
|
|
490
|
+
const shared = Record.filter(rules, (rule) => !readsOwnState(rule));
|
|
487
491
|
return [
|
|
488
492
|
...(Record.isEmptyRecord(shared) ? [] : [shared]),
|
|
489
|
-
...Object.entries(rules).flatMap(([id, rule]) =>
|
|
490
|
-
rule.appendState === undefined ? [] : [{ [id]: rule }],
|
|
491
|
-
),
|
|
493
|
+
...Object.entries(rules).flatMap(([id, rule]) => (readsOwnState(rule) ? [{ [id]: rule }] : [])),
|
|
492
494
|
];
|
|
493
495
|
};
|
|
494
496
|
|
package/src/workflows/audit.ts
CHANGED
|
@@ -112,14 +112,22 @@ const LAYOUT = "sections";
|
|
|
112
112
|
* judge question, and its matchers' when it has any. An edited rule or
|
|
113
113
|
* matcher re-judges the rule, and a question asked differently re-judges
|
|
114
114
|
* every rule. So does an edited `appendState`, by its source; what the hook
|
|
115
|
-
* reads outside the file is not known until it runs, so a change there does
|
|
115
|
+
* reads outside the file is not known until it runs, so a change there does
|
|
116
|
+
* not. A rule that reads the workspace's packages depends on them too, so a
|
|
117
|
+
* package added, renamed, or moved re-judges that rule and no other.
|
|
116
118
|
*/
|
|
117
|
-
const fingerprintOf = (
|
|
119
|
+
const fingerprintOf = (
|
|
120
|
+
model: string,
|
|
121
|
+
rule: Rule,
|
|
122
|
+
workspacePackages: Readonly<Record<string, string>> = {},
|
|
123
|
+
) => {
|
|
118
124
|
const matchers = matcherQuestions("", rule).map(([, question]) => question);
|
|
119
125
|
const scope = matchers.length === 0 ? "" : `\u0000${JSON.stringify(matchers)}`;
|
|
120
126
|
const hook = rule.appendState === undefined ? "" : `\u0000${rule.appendState.toString()}`;
|
|
127
|
+
const packages =
|
|
128
|
+
rule.includeWorkspacePackages === true ? `\u0000${JSON.stringify(workspacePackages)}` : "";
|
|
121
129
|
return sha256(
|
|
122
|
-
`${model}\u0000${LAYOUT}\u0000${JSON.stringify(judgeQuestion(rule))}${scope}${hook}`,
|
|
130
|
+
`${model}\u0000${LAYOUT}\u0000${JSON.stringify(judgeQuestion(rule))}${scope}${hook}${packages}`,
|
|
123
131
|
);
|
|
124
132
|
};
|
|
125
133
|
|
|
@@ -314,7 +322,7 @@ export const planAudit = (
|
|
|
314
322
|
const prepared: Record<RuleId, PreparedRule> = yield* Effect.forEach(
|
|
315
323
|
Object.entries(rules),
|
|
316
324
|
([id, rule]) =>
|
|
317
|
-
Effect.map(fingerprintOf(config.model, rule), (fingerprint) => {
|
|
325
|
+
Effect.map(fingerprintOf(config.model, rule, config.workspacePackages), (fingerprint) => {
|
|
318
326
|
const preset = presetOfRule.get(rule);
|
|
319
327
|
return [
|
|
320
328
|
id,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
|
|
3
|
+
/** A workspace's packages: each one's name, with its directory from the root. */
|
|
4
|
+
export type WorkspacePackages = Readonly<Record<string, string>>;
|
|
5
|
+
|
|
6
|
+
/** What JSON text holds, or nothing when it is not JSON. */
|
|
7
|
+
const parsed = (text: string): unknown => {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(text);
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** The strings of a value that is an array of them, and none of any other value. */
|
|
16
|
+
const strings = (value: unknown): ReadonlyArray<string> =>
|
|
17
|
+
Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A root package.json's workspace patterns: `workspaces` as an array, which
|
|
21
|
+
* npm, Bun, and Yarn read, or as Yarn's `{ packages: [...] }`.
|
|
22
|
+
*/
|
|
23
|
+
const manifestPatterns = (manifest: unknown): ReadonlyArray<string> => {
|
|
24
|
+
const workspaces = (manifest as { readonly workspaces?: unknown } | undefined)?.workspaces;
|
|
25
|
+
return Array.isArray(workspaces)
|
|
26
|
+
? strings(workspaces)
|
|
27
|
+
: strings((workspaces as { readonly packages?: unknown } | undefined)?.packages);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The patterns under `packages:` in a pnpm-workspace.yaml: the list items
|
|
32
|
+
* that follow it, up to the next key. Read line by line, since that list is
|
|
33
|
+
* all of the file adhere needs and the executable carries no YAML parser.
|
|
34
|
+
*/
|
|
35
|
+
const pnpmPatterns = (yaml: string): ReadonlyArray<string> => {
|
|
36
|
+
const lines = yaml.split(/\r?\n/);
|
|
37
|
+
const start = lines.findIndex((line) => /^packages:\s*$/.test(line));
|
|
38
|
+
if (start < 0) return [];
|
|
39
|
+
const rest = lines.slice(start + 1);
|
|
40
|
+
const end = rest.findIndex((line) => /^\S/.test(line) && !line.startsWith("-"));
|
|
41
|
+
return (end < 0 ? rest : rest.slice(0, end)).flatMap((line) => {
|
|
42
|
+
const [, item] = /^\s*-\s*(.+?)\s*$/.exec(line) ?? [];
|
|
43
|
+
return item === undefined ? [] : [item.replace(/^["']|["']$/g, "")];
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Whether a directory's name matches a pattern segment, where `*` stands for any run of characters. */
|
|
48
|
+
const matches = (segment: string, name: string): boolean =>
|
|
49
|
+
new RegExp(
|
|
50
|
+
`^${segment
|
|
51
|
+
.split("*")
|
|
52
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
53
|
+
.join(".*")}$`,
|
|
54
|
+
).test(name);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The packages of the workspace whose root is `root`, by name, sorted, so
|
|
58
|
+
* the same workspace always reads the same: from the root package.json's
|
|
59
|
+
* `workspaces` and from pnpm-workspace.yaml's `packages`. A pattern's `*` is
|
|
60
|
+
* any directory and `**` any depth of them, outside `node_modules` and dot
|
|
61
|
+
* directories, and one that starts with `!` leaves out what it matches. A
|
|
62
|
+
* root that has neither file, or names no workspaces, has no packages.
|
|
63
|
+
*/
|
|
64
|
+
export const workspacePackagesOf = Effect.fn("workspacePackagesOf")(function* (root: string) {
|
|
65
|
+
const fs = yield* FileSystem.FileSystem;
|
|
66
|
+
const path = yield* Path.Path;
|
|
67
|
+
const read = (file: string) =>
|
|
68
|
+
fs.readFileString(path.join(root, file)).pipe(Effect.orElseSucceed(() => ""));
|
|
69
|
+
const children = (directory: string) =>
|
|
70
|
+
fs.readDirectory(path.join(root, directory)).pipe(
|
|
71
|
+
Effect.orElseSucceed((): ReadonlyArray<string> => []),
|
|
72
|
+
Effect.map((names) =>
|
|
73
|
+
names.filter((name) => name !== "node_modules" && !name.startsWith(".")),
|
|
74
|
+
),
|
|
75
|
+
Effect.flatMap((names) =>
|
|
76
|
+
Effect.filter(names, (name) =>
|
|
77
|
+
fs.stat(path.join(root, directory, name)).pipe(
|
|
78
|
+
Effect.map((info) => info.type === "Directory"),
|
|
79
|
+
Effect.orElseSucceed(() => false),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
Effect.map((names) =>
|
|
84
|
+
names.map((name) => (directory === "" ? name : `${directory}/${name}`)),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
/** The directories under `directory` that the pattern's remaining segments lead to. */
|
|
88
|
+
const expand = (
|
|
89
|
+
directory: string,
|
|
90
|
+
segments: ReadonlyArray<string>,
|
|
91
|
+
): Effect.Effect<ReadonlyArray<string>> => {
|
|
92
|
+
const [segment, ...rest] = segments;
|
|
93
|
+
if (segment === undefined) return Effect.succeed([directory]);
|
|
94
|
+
if (segment === "**") {
|
|
95
|
+
return Effect.gen(function* () {
|
|
96
|
+
const below = yield* Effect.forEach(yield* children(directory), (child) =>
|
|
97
|
+
expand(child, segments),
|
|
98
|
+
);
|
|
99
|
+
return [...(yield* expand(directory, rest)), ...below.flat()];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (!segment.includes("*")) {
|
|
103
|
+
return expand(directory === "" ? segment : `${directory}/${segment}`, rest);
|
|
104
|
+
}
|
|
105
|
+
return Effect.flatMap(children(directory), (names) =>
|
|
106
|
+
Effect.map(
|
|
107
|
+
Effect.forEach(
|
|
108
|
+
names.filter((name) => matches(segment, name.slice(name.lastIndexOf("/") + 1))),
|
|
109
|
+
(child) => expand(child, rest),
|
|
110
|
+
),
|
|
111
|
+
(found) => found.flat(),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
const segmentsOf = (pattern: string) =>
|
|
116
|
+
pattern.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
117
|
+
const patterns = [
|
|
118
|
+
...manifestPatterns(parsed(yield* read("package.json"))),
|
|
119
|
+
...pnpmPatterns(yield* read("pnpm-workspace.yaml")),
|
|
120
|
+
];
|
|
121
|
+
const directoriesOf = (chosen: ReadonlyArray<string>) =>
|
|
122
|
+
Effect.map(
|
|
123
|
+
Effect.forEach(chosen, (pattern) => expand("", segmentsOf(pattern))),
|
|
124
|
+
(found) => new Set(found.flat()),
|
|
125
|
+
);
|
|
126
|
+
const included = yield* directoriesOf(patterns.filter((pattern) => !pattern.startsWith("!")));
|
|
127
|
+
const excluded = yield* directoriesOf(
|
|
128
|
+
patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)),
|
|
129
|
+
);
|
|
130
|
+
const named = yield* Effect.forEach(
|
|
131
|
+
[...included].filter((directory) => !excluded.has(directory)),
|
|
132
|
+
(directory) =>
|
|
133
|
+
Effect.map(read(`${directory}/package.json`), (text) => {
|
|
134
|
+
const name = (parsed(text) as { readonly name?: unknown } | undefined)?.name;
|
|
135
|
+
return typeof name === "string" ? [[name, directory] as const] : [];
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
named.flat().sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
140
|
+
) as WorkspacePackages;
|
|
141
|
+
});
|