@savvy-web/silk-effects 3.3.1 → 4.0.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/changesets/changelog/getReleaseLine.js +1 -1
- package/changesets/constants.js +0 -18
- package/changesets/index.js +10 -24
- package/changesets/schemas/changeset.js +8 -4
- package/changesets/schemas/dependency-table.js +15 -4
- package/changesets/schemas/git.js +7 -2
- package/changesets/schemas/github.js +4 -4
- package/changesets/schemas/options.js +3 -3
- package/changesets/schemas/package-scope.js +2 -5
- package/changesets/schemas/primitives.js +2 -2
- package/changesets/schemas/release-plan.js +12 -8
- package/changesets/schemas/version-files.js +4 -4
- package/changesets/services/branch-analyzer.js +60 -191
- package/changesets/services/changelog.js +2 -15
- package/changesets/services/config-inspector.js +109 -75
- package/changesets/services/deps-regen.js +58 -32
- package/changesets/services/github.js +3 -16
- package/changesets/services/maintenance-reason.js +5 -1
- package/changesets/services/markdown.js +3 -16
- package/changesets/services/release-planner.js +4 -13
- package/changesets/utils/commit-parser.js +0 -18
- package/changesets/utils/dep-diff.js +0 -27
- package/changesets/utils/git.js +15 -50
- package/changesets/utils/issue-refs.js +0 -13
- package/changesets/utils/logger.js +0 -13
- package/changesets/utils/markdown-link.js +0 -14
- package/changesets/utils/publishability.js +11 -24
- package/changesets/utils/strip-frontmatter.js +0 -19
- package/changesets/utils/version-files.js +85 -51
- package/commitlint/config/schema.js +9 -5
- package/commitlint/detection/scopes.js +2 -7
- package/commitlint/formatter/messages.js +0 -5
- package/commitlint/hook/diagnostics/branch.js +12 -12
- package/commitlint/hook/diagnostics/open-issues.js +13 -7
- package/commitlint/hook/diagnostics/signing.js +30 -37
- package/commitlint/hook/envelope.js +2 -8
- package/commitlint/hook/silence-logger.js +14 -12
- package/index.d.ts +897 -932
- package/lint/cli/templates/markdownlint.gen.js +0 -9
- package/lint/handlers/PnpmWorkspace.js +26 -12
- package/lint/utils/Filter.js +0 -13
- package/lint/utils/Workspace.js +10 -6
- package/package.json +11 -10
- package/repos/index.js +3 -5
- package/repos/schemas/manifest.js +7 -13
- package/repos/schemas/reports.js +5 -1
- package/repos/services/config-store.js +6 -10
- package/repos/services/manager.js +52 -112
- package/schemas/CommentStyle.js +1 -1
- package/schemas/ResolvedTool.js +52 -17
- package/schemas/SectionBlock.js +1 -1
- package/schemas/SectionDefinition.js +2 -2
- package/schemas/TagStrategySchemas.js +1 -1
- package/schemas/ToolDefinition.js +1 -1
- package/schemas/ToolResults.js +1 -1
- package/schemas/VersioningSchemas.js +28 -9
- package/schemas/WorkspaceAnalysisSchemas.js +27 -17
- package/services/BiomeSchemaSync.js +7 -8
- package/services/ChangesetConfig.js +2 -2
- package/services/ChangesetConfigReader.js +7 -8
- package/services/ConfigDiscovery.js +5 -6
- package/services/ManagedSection.js +3 -4
- package/services/SilkPublishability.js +38 -15
- package/services/SilkWorkspaceAnalyzer.js +7 -9
- package/services/TagStrategy.js +3 -3
- package/services/ToolDiscovery.js +22 -21
- package/services/VersioningStrategy.js +2 -2
- package/tsdoc-metadata.json +1 -1
- package/turbo/schemas/DryRun.js +8 -14
- package/turbo/schemas/results.js +8 -8
- package/turbo/services/TurboInspector.js +30 -23
- package/utils/ToolCommand.js +38 -12
|
@@ -1,77 +1,35 @@
|
|
|
1
1
|
import { GitError } from "../errors.js";
|
|
2
2
|
import { ClassificationReasonSchema, ConfigInspector } from "./config-inspector.js";
|
|
3
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
-
import {
|
|
3
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
4
|
+
import { Git } from "@effected/git";
|
|
5
5
|
|
|
6
6
|
//#region src/changesets/services/branch-analyzer.ts
|
|
7
|
-
/**
|
|
8
|
-
* `BranchAnalyzer` service — combine a git diff against the base branch with
|
|
9
|
-
* the per-file classification produced by {@link ConfigInspector}.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* This is the single call that the changeset-manager agent uses during its
|
|
13
|
-
* inventory step: one invocation returns the diff, the per-file package
|
|
14
|
-
* attribution, the set of packages affected by the branch, and the list of
|
|
15
|
-
* paths that did not map to any known release surface (so the agent can ask
|
|
16
|
-
* the user about them rather than silently excluding).
|
|
17
|
-
*
|
|
18
|
-
* Base-branch resolution order (highest priority first):
|
|
19
|
-
*
|
|
20
|
-
* 1. Explicit `opts.baseBranch` passed to {@link BranchAnalyzerShape.analyzeBranch}.
|
|
21
|
-
* 2. `baseBranch` from `.changeset/config.json` (surfaced via the inspector).
|
|
22
|
-
* 3. The branch `origin/HEAD` points to (`git symbolic-ref refs/remotes/origin/HEAD`).
|
|
23
|
-
* 4. `"main"` as a final fallback.
|
|
24
|
-
*
|
|
25
|
-
* Diff resolution covers everything the user might commit before merging:
|
|
26
|
-
*
|
|
27
|
-
* 1. `git merge-base <base> HEAD` finds the common ancestor.
|
|
28
|
-
* 2. `git diff --name-status <merge-base>` (working tree vs merge-base)
|
|
29
|
-
* returns every committed, staged, AND unstaged change since the
|
|
30
|
-
* branch diverged. The two-arg `<merge-base>...HEAD` form would miss
|
|
31
|
-
* work-in-progress — what the user has open in their editor right
|
|
32
|
-
* now is exactly the state the agent needs to document.
|
|
33
|
-
* 3. `git ls-files --others --exclude-standard` adds untracked files
|
|
34
|
-
* (entirely new files not yet `git add`-ed). Each is reported with
|
|
35
|
-
* status `"added"`.
|
|
36
|
-
*
|
|
37
|
-
* The two results are deduped by path before classification. Renames
|
|
38
|
-
* are reported as `"renamed"` with the new path; the old path is
|
|
39
|
-
* discarded since the classifier needs a single canonical path per
|
|
40
|
-
* file.
|
|
41
|
-
*
|
|
42
|
-
* @see {@link BranchAnalyzer} for the service tag
|
|
43
|
-
* @see {@link BranchAnalyzerLive} for the production layer
|
|
44
|
-
* @see {@link ConfigInspector} for the underlying classification service
|
|
45
|
-
*
|
|
46
|
-
*/
|
|
47
7
|
/** Git diff status as reported by `--name-status`. @public */
|
|
48
|
-
const FileStatusSchema = Schema.
|
|
8
|
+
const FileStatusSchema = Schema.Literals([
|
|
9
|
+
"added",
|
|
10
|
+
"modified",
|
|
11
|
+
"deleted",
|
|
12
|
+
"renamed",
|
|
13
|
+
"copied",
|
|
14
|
+
"typechange",
|
|
15
|
+
"unmerged",
|
|
16
|
+
"unknown"
|
|
17
|
+
]).annotate({ identifier: "FileStatus" });
|
|
49
18
|
/** One file entry in the branch analysis output. @public */
|
|
50
19
|
const BranchFileEntrySchema = Schema.Struct({
|
|
51
|
-
path: Schema.String.
|
|
20
|
+
path: Schema.String.annotate({ description: "Repo-relative path (the new path in the case of renames)." }),
|
|
52
21
|
status: FileStatusSchema,
|
|
53
|
-
package: Schema.NullOr(Schema.String).
|
|
22
|
+
package: Schema.NullOr(Schema.String).annotate({ description: "Owning package, or null if outside every known release surface." }),
|
|
54
23
|
reason: ClassificationReasonSchema
|
|
55
|
-
}).
|
|
24
|
+
}).annotate({ identifier: "BranchFileEntry" });
|
|
56
25
|
/** Structured result of analyzing the current branch against its base. @public */
|
|
57
26
|
const BranchAnalysisSchema = Schema.Struct({
|
|
58
27
|
baseBranch: Schema.String,
|
|
59
28
|
mergeBaseSha: Schema.String,
|
|
60
29
|
files: Schema.Array(BranchFileEntrySchema),
|
|
61
30
|
packagesAffected: Schema.Array(Schema.String),
|
|
62
|
-
unmappedFiles: Schema.Array(Schema.String).
|
|
63
|
-
}).
|
|
64
|
-
const _tag = Context.Tag("BranchAnalyzer");
|
|
65
|
-
/**
|
|
66
|
-
* Base class for {@link BranchAnalyzer}.
|
|
67
|
-
*
|
|
68
|
-
* @privateRemarks
|
|
69
|
-
* Effect's `Context.Tag` creates an anonymous base class that api-extractor
|
|
70
|
-
* cannot follow without an explicit export. Do not delete.
|
|
71
|
-
*
|
|
72
|
-
* @internal
|
|
73
|
-
*/
|
|
74
|
-
const BranchAnalyzerBase = _tag();
|
|
31
|
+
unmappedFiles: Schema.Array(Schema.String).annotate({ description: "Repo-relative paths whose package is null — candidates for an AskUserQuestion." })
|
|
32
|
+
}).annotate({ identifier: "BranchAnalysis" });
|
|
75
33
|
/**
|
|
76
34
|
* Effect service tag for {@link BranchAnalyzerShape}.
|
|
77
35
|
*
|
|
@@ -90,165 +48,74 @@ const BranchAnalyzerBase = _tag();
|
|
|
90
48
|
* program.pipe(
|
|
91
49
|
* Effect.provide(BranchAnalyzerLive),
|
|
92
50
|
* Effect.provide(ConfigInspectorLive),
|
|
93
|
-
* // ... + ChangesetConfigReaderLive +
|
|
51
|
+
* // ... + ChangesetConfigReaderLive + kit workspace layers + NodeServices.layer
|
|
94
52
|
* ),
|
|
95
53
|
* );
|
|
96
54
|
* ```
|
|
97
55
|
*
|
|
98
56
|
* @public
|
|
99
57
|
*/
|
|
100
|
-
var BranchAnalyzer = class extends
|
|
58
|
+
var BranchAnalyzer = class extends Context.Service()("BranchAnalyzer") {};
|
|
101
59
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* command, cwd, and captured stderr.
|
|
60
|
+
* Fold a `@effected/git` typed failure into this package's {@link GitError},
|
|
61
|
+
* preserving the public `ConfigurationError | GitError` error channel.
|
|
105
62
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
stdio: [
|
|
112
|
-
"ignore",
|
|
113
|
-
"pipe",
|
|
114
|
-
"pipe"
|
|
115
|
-
]
|
|
116
|
-
}),
|
|
117
|
-
catch: (error) => {
|
|
118
|
-
const e = error;
|
|
119
|
-
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "";
|
|
120
|
-
return new GitError({
|
|
121
|
-
command: `git ${args.join(" ")}`,
|
|
122
|
-
cwd,
|
|
123
|
-
reason: stderr.trim() || e.message || String(error)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
const STATUS_MAP = {
|
|
129
|
-
A: "added",
|
|
130
|
-
M: "modified",
|
|
131
|
-
D: "deleted",
|
|
132
|
-
R: "renamed",
|
|
133
|
-
C: "copied",
|
|
134
|
-
T: "typechange",
|
|
135
|
-
U: "unmerged"
|
|
136
|
-
};
|
|
137
|
-
function statusFromCode(code) {
|
|
138
|
-
const head = code.charAt(0);
|
|
139
|
-
return STATUS_MAP[head] ?? "unknown";
|
|
140
|
-
}
|
|
63
|
+
const toGitError = (command, cwd) => (e) => new GitError({
|
|
64
|
+
command,
|
|
65
|
+
cwd,
|
|
66
|
+
reason: e.message
|
|
67
|
+
});
|
|
141
68
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* (status, old path, new path) instead of two; everything else is two.
|
|
69
|
+
* Map `@effected/git`'s `NameStatusEntry` status vocabulary onto this
|
|
70
|
+
* package's public {@link FileStatus} literals. The kit spells
|
|
71
|
+
* `"typeChanged"` where the public schema (stable since 0.x) says
|
|
72
|
+
* `"typechange"`, and adds `"broken"` (git's `B`), which the public schema
|
|
73
|
+
* folds into `"unknown"`.
|
|
148
74
|
*/
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
for (let i = 0; i < tokens.length;) {
|
|
155
|
-
const code = tokens[i] ?? "";
|
|
156
|
-
if (code.length === 0) {
|
|
157
|
-
/* v8 ignore next 2 -- defensive guard; trailing empties stripped at parse time */
|
|
158
|
-
i += 1;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const status = statusFromCode(code);
|
|
162
|
-
if (status === "renamed" || status === "copied") {
|
|
163
|
-
const newPath = tokens[i + 2] ?? "";
|
|
164
|
-
if (newPath.length > 0) entries.push({
|
|
165
|
-
path: newPath,
|
|
166
|
-
status
|
|
167
|
-
});
|
|
168
|
-
i += 3;
|
|
169
|
-
} else {
|
|
170
|
-
const path = tokens[i + 1] ?? "";
|
|
171
|
-
if (path.length > 0) entries.push({
|
|
172
|
-
path,
|
|
173
|
-
status
|
|
174
|
-
});
|
|
175
|
-
i += 2;
|
|
176
|
-
}
|
|
75
|
+
function toFileStatus(status) {
|
|
76
|
+
switch (status) {
|
|
77
|
+
case "typeChanged": return "typechange";
|
|
78
|
+
case "broken": return "unknown";
|
|
79
|
+
default: return status;
|
|
177
80
|
}
|
|
178
|
-
return entries;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Parse a NUL-separated list of paths (e.g., `git ls-files -z` output) into
|
|
182
|
-
* an array of strings, dropping any trailing empty entry left by the final
|
|
183
|
-
* NUL byte.
|
|
184
|
-
*/
|
|
185
|
-
function parseNulSeparatedPaths(output) {
|
|
186
|
-
if (output.length === 0) return [];
|
|
187
|
-
const tokens = output.split("\0");
|
|
188
|
-
if (tokens[tokens.length - 1] === "") tokens.pop();
|
|
189
|
-
return tokens.filter((t) => t.length > 0);
|
|
190
81
|
}
|
|
191
82
|
/**
|
|
192
83
|
* Resolve the base branch using the documented priority order.
|
|
193
84
|
*/
|
|
194
|
-
function resolveBaseBranch(opts) {
|
|
85
|
+
function resolveBaseBranch(git, opts) {
|
|
195
86
|
if (opts.explicit && opts.explicit.length > 0) return Effect.succeed(opts.explicit);
|
|
196
87
|
if (opts.configBaseBranch && opts.configBaseBranch !== "main") return Effect.succeed(opts.configBaseBranch);
|
|
197
|
-
return
|
|
198
|
-
"symbolic-ref",
|
|
199
|
-
"--quiet",
|
|
200
|
-
"--short",
|
|
201
|
-
"refs/remotes/origin/HEAD"
|
|
202
|
-
]).pipe(
|
|
203
|
-
/* v8 ignore start -- callback only reachable with a remote that exposes origin/HEAD */
|
|
204
|
-
Effect.map((stdout) => {
|
|
205
|
-
const trimmed = stdout.trim();
|
|
206
|
-
return trimmed.length > 0 ? trimmed.replace(/^origin\//, "") : opts.configBaseBranch;
|
|
207
|
-
}),
|
|
208
|
-
/* v8 ignore stop */
|
|
209
|
-
Effect.catchAll(() => Effect.succeed(opts.configBaseBranch))
|
|
210
|
-
);
|
|
88
|
+
return git.defaultBranch(opts.cwd).pipe(Effect.map(Option.getOrElse(() => opts.configBaseBranch)), Effect.catch(() => Effect.succeed(opts.configBaseBranch)));
|
|
211
89
|
}
|
|
212
|
-
function makeShape(inspector) {
|
|
90
|
+
function makeShape(inspector, git) {
|
|
213
91
|
const analyzeBranch = (cwd, opts) => Effect.gen(function* () {
|
|
214
92
|
const inspected = yield* inspector.inspect(cwd);
|
|
215
|
-
const baseBranch = yield* resolveBaseBranch({
|
|
93
|
+
const baseBranch = yield* resolveBaseBranch(git, {
|
|
216
94
|
explicit: opts?.baseBranch,
|
|
217
95
|
configBaseBranch: inspected.baseBranch,
|
|
218
96
|
cwd
|
|
219
97
|
});
|
|
220
|
-
const mergeBaseSha = (yield*
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"HEAD"
|
|
224
|
-
])).trim();
|
|
225
|
-
const diffEntries = parseNameStatus(yield* runGit(cwd, [
|
|
226
|
-
"diff",
|
|
227
|
-
"--name-status",
|
|
228
|
-
"-z",
|
|
229
|
-
mergeBaseSha
|
|
230
|
-
]));
|
|
231
|
-
const untrackedEntries = parseNulSeparatedPaths(yield* runGit(cwd, [
|
|
232
|
-
"ls-files",
|
|
233
|
-
"-z",
|
|
234
|
-
"--others",
|
|
235
|
-
"--exclude-standard"
|
|
236
|
-
])).map((path) => ({
|
|
237
|
-
path,
|
|
238
|
-
status: "added"
|
|
239
|
-
}));
|
|
98
|
+
const mergeBaseSha = (yield* git.mergeBase(cwd, baseBranch, "HEAD").pipe(Effect.mapError(toGitError(`git merge-base ${baseBranch} HEAD`, cwd)))).trim();
|
|
99
|
+
const diffEntries = yield* git.nameStatus(cwd, { base: mergeBaseSha }).pipe(Effect.mapError(toGitError(`git diff --name-status ${mergeBaseSha}`, cwd)));
|
|
100
|
+
const untracked = yield* git.untrackedFiles(cwd).pipe(Effect.mapError(toGitError("git ls-files --others --exclude-standard", cwd)));
|
|
240
101
|
const isOwnChangeset = (path) => path.startsWith(".changeset/") && path.endsWith(".md");
|
|
241
102
|
const seen = /* @__PURE__ */ new Set();
|
|
242
103
|
const rawEntries = [];
|
|
243
104
|
for (const e of diffEntries) {
|
|
244
105
|
if (seen.has(e.path) || isOwnChangeset(e.path)) continue;
|
|
245
106
|
seen.add(e.path);
|
|
246
|
-
rawEntries.push(
|
|
107
|
+
rawEntries.push({
|
|
108
|
+
path: e.path,
|
|
109
|
+
status: toFileStatus(e.status)
|
|
110
|
+
});
|
|
247
111
|
}
|
|
248
|
-
for (const
|
|
249
|
-
if (seen.has(
|
|
250
|
-
seen.add(
|
|
251
|
-
rawEntries.push(
|
|
112
|
+
for (const path of untracked) {
|
|
113
|
+
if (seen.has(path) || isOwnChangeset(path)) continue;
|
|
114
|
+
seen.add(path);
|
|
115
|
+
rawEntries.push({
|
|
116
|
+
path,
|
|
117
|
+
status: "added"
|
|
118
|
+
});
|
|
252
119
|
}
|
|
253
120
|
const paths = rawEntries.map((e) => e.path);
|
|
254
121
|
const classifications = yield* inspector.classify(cwd, paths);
|
|
@@ -275,13 +142,15 @@ function makeShape(inspector) {
|
|
|
275
142
|
* Live layer for {@link BranchAnalyzer}.
|
|
276
143
|
*
|
|
277
144
|
* Requires {@link ConfigInspector} (which in turn requires
|
|
278
|
-
* `ChangesetConfigReader` and `WorkspaceDiscovery`)
|
|
145
|
+
* `ChangesetConfigReader` and `WorkspaceDiscovery`) and a
|
|
146
|
+
* `ChildProcessSpawner` (satisfied by `NodeServices.layer`) for the
|
|
147
|
+
* internally-composed `@effected/git` layer.
|
|
279
148
|
*
|
|
280
149
|
* @public
|
|
281
150
|
*/
|
|
282
151
|
const BranchAnalyzerLive = Layer.effect(BranchAnalyzer, Effect.gen(function* () {
|
|
283
|
-
return makeShape(yield* ConfigInspector);
|
|
284
|
-
}));
|
|
152
|
+
return makeShape(yield* ConfigInspector, yield* Git);
|
|
153
|
+
})).pipe(Layer.provide(Git.layer));
|
|
285
154
|
/**
|
|
286
155
|
* Test factory — build a {@link BranchAnalyzer} that returns a fixed
|
|
287
156
|
* {@link BranchAnalysis} for any input.
|
|
@@ -293,4 +162,4 @@ function makeBranchAnalyzerTest(fixed) {
|
|
|
293
162
|
}
|
|
294
163
|
|
|
295
164
|
//#endregion
|
|
296
|
-
export { BranchAnalysisSchema, BranchAnalyzer,
|
|
165
|
+
export { BranchAnalysisSchema, BranchAnalyzer, BranchAnalyzerLive, BranchFileEntrySchema, FileStatusSchema, makeBranchAnalyzerTest };
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { Context } from "effect";
|
|
2
2
|
|
|
3
3
|
//#region src/changesets/services/changelog.ts
|
|
4
|
-
const _tag = Context.Tag("ChangelogService");
|
|
5
|
-
/**
|
|
6
|
-
* Base class for ChangelogService.
|
|
7
|
-
*
|
|
8
|
-
* @privateRemarks
|
|
9
|
-
* This export is required for api-extractor documentation generation.
|
|
10
|
-
* Effect's Context.Tag creates an anonymous base class that must be
|
|
11
|
-
* explicitly exported to avoid "forgotten export" warnings. Do not delete.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
const ChangelogServiceBase = _tag();
|
|
16
4
|
/**
|
|
17
5
|
* Effect service tag for changelog formatting.
|
|
18
6
|
*
|
|
@@ -40,11 +28,10 @@ const ChangelogServiceBase = _tag();
|
|
|
40
28
|
* ```
|
|
41
29
|
*
|
|
42
30
|
* @see {@link ChangelogServiceShape} for the service interface
|
|
43
|
-
* @see {@link ChangelogServiceBase} for the api-extractor base class
|
|
44
31
|
*
|
|
45
32
|
* @public
|
|
46
33
|
*/
|
|
47
|
-
var ChangelogService = class extends
|
|
34
|
+
var ChangelogService = class extends Context.Service()("ChangelogService") {};
|
|
48
35
|
|
|
49
36
|
//#endregion
|
|
50
|
-
export { ChangelogService
|
|
37
|
+
export { ChangelogService };
|
|
@@ -2,11 +2,11 @@ import { ConfigurationError } from "../errors.js";
|
|
|
2
2
|
import { ChangesetOptionsSchema } from "../schemas/options.js";
|
|
3
3
|
import { ChangesetConfigReader } from "../../services/ChangesetConfigReader.js";
|
|
4
4
|
import { SilkPublishability, readTargetsBinding } from "../../services/SilkPublishability.js";
|
|
5
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
5
|
+
import { Context, Effect, FileSystem, Layer, Path, Result, Schema } from "effect";
|
|
6
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { WorkspaceDiscovery } from "workspaces
|
|
7
|
+
import { GlobPattern, GlobPatternOptions } from "@effected/glob";
|
|
8
|
+
import { descend } from "@effected/walker";
|
|
9
|
+
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
10
10
|
|
|
11
11
|
//#region src/changesets/services/config-inspector.ts
|
|
12
12
|
/**
|
|
@@ -45,9 +45,9 @@ import { WorkspaceDiscovery } from "workspaces-effect";
|
|
|
45
45
|
/** A `versionFiles` entry expanded to its absolute target paths. @public */
|
|
46
46
|
const ResolvedVersionFileSchema = Schema.Struct({
|
|
47
47
|
glob: Schema.String,
|
|
48
|
-
paths: Schema.Array(Schema.String).
|
|
48
|
+
paths: Schema.Array(Schema.String).annotate({ description: "JSONPath expressions to update (defaults to [\"$.version\"])." }),
|
|
49
49
|
matchedFiles: Schema.Array(Schema.String)
|
|
50
|
-
}).
|
|
50
|
+
}).annotate({ identifier: "ResolvedVersionFile" });
|
|
51
51
|
/** A package's resolved release surface. @public */
|
|
52
52
|
const ResolvedPackageScopeSchema = Schema.Struct({
|
|
53
53
|
name: Schema.String,
|
|
@@ -56,43 +56,37 @@ const ResolvedPackageScopeSchema = Schema.Struct({
|
|
|
56
56
|
additionalScopes: Schema.Array(Schema.String),
|
|
57
57
|
additionalScopeFiles: Schema.Array(Schema.String),
|
|
58
58
|
versionFiles: Schema.Array(ResolvedVersionFileSchema)
|
|
59
|
-
}).
|
|
59
|
+
}).annotate({ identifier: "ResolvedPackageScope" });
|
|
60
60
|
/** Structured representation of a resolved `.changeset/config.json`. @public */
|
|
61
61
|
const InspectedConfigSchema = Schema.Struct({
|
|
62
62
|
configPath: Schema.String,
|
|
63
|
-
projectDir: Schema.String.
|
|
63
|
+
projectDir: Schema.String.annotate({ description: "Absolute project root (the directory containing .changeset/)." }),
|
|
64
64
|
changelog: Schema.NullOr(Schema.String),
|
|
65
65
|
baseBranch: Schema.String,
|
|
66
|
-
access: Schema.
|
|
66
|
+
access: Schema.Literals(["public", "restricted"]),
|
|
67
67
|
ignore: Schema.Array(Schema.String),
|
|
68
68
|
packages: Schema.Array(ResolvedPackageScopeSchema),
|
|
69
69
|
legacyVersionFilesUsed: Schema.Boolean
|
|
70
|
-
}).
|
|
70
|
+
}).annotate({ identifier: "InspectedConfig" });
|
|
71
71
|
/** Reason a path was attributed to a package (or left unmapped). @public */
|
|
72
|
-
const ClassificationReasonSchema = Schema.Union(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const ClassificationReasonSchema = Schema.Union([
|
|
73
|
+
Schema.Literal("workspace"),
|
|
74
|
+
Schema.Struct({
|
|
75
|
+
kind: Schema.Literal("additionalScope"),
|
|
76
|
+
glob: Schema.String
|
|
77
|
+
}),
|
|
78
|
+
Schema.Struct({
|
|
79
|
+
kind: Schema.Literal("versionFile"),
|
|
80
|
+
glob: Schema.String
|
|
81
|
+
}),
|
|
82
|
+
Schema.Null
|
|
83
|
+
]).annotate({ identifier: "ClassificationReason" });
|
|
79
84
|
/** The result of classifying a single path against a resolved config. @public */
|
|
80
85
|
const ClassificationSchema = Schema.Struct({
|
|
81
86
|
path: Schema.String,
|
|
82
87
|
package: Schema.NullOr(Schema.String),
|
|
83
88
|
reason: ClassificationReasonSchema
|
|
84
|
-
}).
|
|
85
|
-
const _tag = Context.Tag("ConfigInspector");
|
|
86
|
-
/**
|
|
87
|
-
* Base class for {@link ConfigInspector}.
|
|
88
|
-
*
|
|
89
|
-
* @privateRemarks
|
|
90
|
-
* Effect's `Context.Tag` creates an anonymous base class that api-extractor
|
|
91
|
-
* cannot follow without an explicit export. Do not delete.
|
|
92
|
-
*
|
|
93
|
-
* @internal
|
|
94
|
-
*/
|
|
95
|
-
const ConfigInspectorBase = _tag();
|
|
89
|
+
}).annotate({ identifier: "Classification" });
|
|
96
90
|
/**
|
|
97
91
|
* Effect service tag for {@link ConfigInspectorShape}.
|
|
98
92
|
*
|
|
@@ -112,7 +106,7 @@ const ConfigInspectorBase = _tag();
|
|
|
112
106
|
*
|
|
113
107
|
* @public
|
|
114
108
|
*/
|
|
115
|
-
var ConfigInspector = class extends
|
|
109
|
+
var ConfigInspector = class extends Context.Service()("ConfigInspector") {};
|
|
116
110
|
/**
|
|
117
111
|
* Pull the changelog formatter ID and its options object out of the raw
|
|
118
112
|
* `.changeset/config.json` shape (where `changelog` may be a tuple, a string,
|
|
@@ -175,16 +169,48 @@ function normalizeLegacyOptions(options, configPath) {
|
|
|
175
169
|
legacyUsed: true
|
|
176
170
|
};
|
|
177
171
|
}
|
|
172
|
+
/** Match dotfiles, mirroring the former tinyglobby `dot: true` behavior. */
|
|
173
|
+
const GLOB_OPTIONS = GlobPatternOptions.make({ dot: true });
|
|
174
|
+
/**
|
|
175
|
+
* Compile `glob` via `@effected/glob`, folding a compile-guard trip
|
|
176
|
+
* (over-length pattern, brace-expansion budget, nesting depth) into a
|
|
177
|
+
* {@link ConfigurationError} naming the offending glob on the typed channel.
|
|
178
|
+
*/
|
|
179
|
+
function compileGlob(glob) {
|
|
180
|
+
return GlobPattern.compile(glob, GLOB_OPTIONS).pipe(Effect.mapError((error) => new ConfigurationError({
|
|
181
|
+
field: "glob",
|
|
182
|
+
reason: `Invalid glob pattern ${JSON.stringify(glob)}: ${error.message}`
|
|
183
|
+
})));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Pure attribution helper: does `glob` (under this service's `dot: true`
|
|
187
|
+
* semantics) match the repo-relative POSIX path `rel`? An uncompilable
|
|
188
|
+
* pattern matches nothing.
|
|
189
|
+
*/
|
|
190
|
+
function globMatchesRel(glob, rel) {
|
|
191
|
+
const result = Effect.runSync(Effect.result(GlobPattern.compile(glob, GLOB_OPTIONS)));
|
|
192
|
+
return Result.isSuccess(result) && result.success.matches(rel);
|
|
193
|
+
}
|
|
178
194
|
/**
|
|
179
195
|
* Materialize a glob against `cwd` and return the matched file paths as
|
|
180
|
-
* repo-relative strings
|
|
196
|
+
* repo-relative POSIX strings, sorted by relative path. Matches dotfiles (the
|
|
197
|
+
* former tinyglobby `dot: true` semantics); the walk is `@effected/walker`'s
|
|
198
|
+
* `descend` (literal fast path, `enumerationPrefix` bounding,
|
|
199
|
+
* `node_modules`/`.git` pruning), with `onUnreadable: "skip"` preserving the
|
|
200
|
+
* previous silent-skip policy for unreadable directories. The one remaining
|
|
201
|
+
* `DescendError` (depth cap) folds into a {@link ConfigurationError} naming
|
|
202
|
+
* the glob — this service's single typed failure. `descend`'s `Path`
|
|
203
|
+
* requirement is satisfied internally with the core POSIX `Path.layer` (this
|
|
204
|
+
* module already speaks POSIX-relative match paths).
|
|
181
205
|
*/
|
|
182
206
|
function materializeGlob(glob, cwd) {
|
|
183
|
-
return
|
|
207
|
+
return compileGlob(glob).pipe(Effect.flatMap((pattern) => descend(pattern, {
|
|
184
208
|
cwd,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
onUnreadable: "skip"
|
|
210
|
+
}).pipe(Effect.mapError((error) => new ConfigurationError({
|
|
211
|
+
field: "glob",
|
|
212
|
+
reason: `Failed to materialize glob ${JSON.stringify(glob)}: ${error.message}`
|
|
213
|
+
})), Effect.provide(Path.layer))));
|
|
188
214
|
}
|
|
189
215
|
/**
|
|
190
216
|
* Determine whether `child` is the same directory as `parent` or sits inside
|
|
@@ -199,33 +225,41 @@ function isInside(parent, child) {
|
|
|
199
225
|
* options with workspace info.
|
|
200
226
|
*/
|
|
201
227
|
function buildResolvedScopes(params) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
return Effect.gen(function* () {
|
|
229
|
+
const { options, workspaces, projectDir, configPath } = params;
|
|
230
|
+
const packages = options.packages ?? {};
|
|
231
|
+
const workspacesByName = new Map(workspaces.map((w) => [w.name, w]));
|
|
232
|
+
const scopes = [];
|
|
233
|
+
for (const [pkgName, scope] of Object.entries(packages)) {
|
|
234
|
+
const ws = workspacesByName.get(pkgName);
|
|
235
|
+
if (!ws) return yield* Effect.fail(new ConfigurationError({
|
|
236
|
+
field: `packages["${pkgName}"]`,
|
|
237
|
+
reason: `Unknown package "${pkgName}" in ${configPath}. Known workspace packages: ${workspaces.map((w) => w.name).join(", ") || "(none)"}.`
|
|
238
|
+
}));
|
|
239
|
+
const additionalScopes = scope.additionalScopes ?? [];
|
|
240
|
+
const additionalScopeFiles = [];
|
|
241
|
+
for (const g of additionalScopes) additionalScopeFiles.push(...yield* materializeGlob(g, projectDir));
|
|
242
|
+
const versionFileEntries = scope.versionFiles ?? [];
|
|
243
|
+
const resolvedVersionFiles = [];
|
|
244
|
+
for (const entry of versionFileEntries) {
|
|
245
|
+
const matched = yield* materializeGlob(entry.glob, projectDir);
|
|
246
|
+
resolvedVersionFiles.push({
|
|
247
|
+
glob: entry.glob,
|
|
248
|
+
paths: entry.paths ?? ["$.version"],
|
|
249
|
+
matchedFiles: matched.map((rel) => join(projectDir, rel))
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
scopes.push({
|
|
253
|
+
name: pkgName,
|
|
254
|
+
workspaceDir: ws.path,
|
|
255
|
+
version: ws.version,
|
|
256
|
+
additionalScopes,
|
|
257
|
+
additionalScopeFiles: additionalScopeFiles.map((rel) => join(projectDir, rel)),
|
|
258
|
+
versionFiles: resolvedVersionFiles
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return scopes;
|
|
262
|
+
});
|
|
229
263
|
}
|
|
230
264
|
/**
|
|
231
265
|
* Read a workspace package's raw package.json. A genuinely missing manifest
|
|
@@ -237,8 +271,8 @@ function buildResolvedScopes(params) {
|
|
|
237
271
|
*/
|
|
238
272
|
function readRawPackageJson(fs, pkgDir) {
|
|
239
273
|
const pkgJsonPath = join(pkgDir, "package.json");
|
|
240
|
-
return fs.readFileString(pkgJsonPath).pipe(Effect.flatMap((content) => Effect.try(() => JSON.parse(content))), Effect.
|
|
241
|
-
if (err.reason === "NotFound") return Effect.succeed(null);
|
|
274
|
+
return fs.readFileString(pkgJsonPath).pipe(Effect.flatMap((content) => Effect.try(() => JSON.parse(content))), Effect.catch((err) => {
|
|
275
|
+
if (err.reason?._tag === "NotFound") return Effect.succeed(null);
|
|
242
276
|
return Effect.fail(new ConfigurationError({
|
|
243
277
|
field: "workspace",
|
|
244
278
|
reason: `Failed to read or parse ${pkgJsonPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -356,8 +390,8 @@ function makeShape(reader, discovery, fs) {
|
|
|
356
390
|
if (e instanceof ConfigurationError) return yield* Effect.fail(e);
|
|
357
391
|
throw e;
|
|
358
392
|
}
|
|
359
|
-
const decodedOptions = yield* Schema.
|
|
360
|
-
const workspaces = (yield* discovery.listPackages(
|
|
393
|
+
const decodedOptions = yield* Schema.decodeUnknownEffect(ChangesetOptionsSchema)(normalized).pipe(Effect.mapError((parseError) => configErrorFromParseError(parseError, configPath)));
|
|
394
|
+
const workspaces = (yield* discovery.listPackages().pipe(Effect.mapError((err) => new ConfigurationError({
|
|
361
395
|
field: "workspace",
|
|
362
396
|
reason: `Workspace discovery failed for ${projectDir}: ${err.message}`
|
|
363
397
|
})))).map((w) => ({
|
|
@@ -368,14 +402,13 @@ function makeShape(reader, discovery, fs) {
|
|
|
368
402
|
const hasExplicitPackages = Object.keys(decodedOptions.packages ?? {}).length > 0;
|
|
369
403
|
let scopes;
|
|
370
404
|
if (hasExplicitPackages) {
|
|
371
|
-
|
|
405
|
+
const explicitScopes = yield* buildResolvedScopes({
|
|
406
|
+
options: decodedOptions,
|
|
407
|
+
workspaces,
|
|
408
|
+
projectDir,
|
|
409
|
+
configPath
|
|
410
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs));
|
|
372
411
|
try {
|
|
373
|
-
explicitScopes = buildResolvedScopes({
|
|
374
|
-
options: decodedOptions,
|
|
375
|
-
workspaces,
|
|
376
|
-
projectDir,
|
|
377
|
-
configPath
|
|
378
|
-
});
|
|
379
412
|
checkConflicts(explicitScopes, workspaces, projectDir, configPath);
|
|
380
413
|
} catch (e) {
|
|
381
414
|
if (e instanceof ConfigurationError) return yield* Effect.fail(e);
|
|
@@ -431,7 +464,8 @@ function classifyOne(inspected, path) {
|
|
|
431
464
|
reason: "workspace"
|
|
432
465
|
};
|
|
433
466
|
for (const s of inspected.packages) if (s.additionalScopeFiles.includes(abs)) {
|
|
434
|
-
const
|
|
467
|
+
const rel = relative(inspected.projectDir, abs).replaceAll("\\", "/");
|
|
468
|
+
const glob = s.additionalScopes.find((g) => globMatchesRel(g, rel));
|
|
435
469
|
return {
|
|
436
470
|
path,
|
|
437
471
|
package: s.name,
|
|
@@ -485,4 +519,4 @@ function makeConfigInspectorTest(fixed) {
|
|
|
485
519
|
}
|
|
486
520
|
|
|
487
521
|
//#endregion
|
|
488
|
-
export { ClassificationReasonSchema, ClassificationSchema, ConfigInspector,
|
|
522
|
+
export { ClassificationReasonSchema, ClassificationSchema, ConfigInspector, ConfigInspectorLive, InspectedConfigSchema, ResolvedPackageScopeSchema, ResolvedVersionFileSchema, makeConfigInspectorTest };
|