@savvy-web/silk-effects 7.5.3 → 8.0.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/changesets/api/changelog.js +10 -5
- package/changesets/changelog/getReleaseLine.js +2 -5
- package/changesets/changelog/index.js +39 -19
- package/changesets/index.js +5 -2
- package/changesets/utils/logger.js +47 -20
- package/index.d.ts +171 -1336
- package/index.js +2 -10
- package/lint/cli/sections.js +1 -1
- package/package.json +6 -5
- package/services/BiomeSchemaSync.js +3 -3
- package/services/ChangesetConfigReader.js +3 -4
- package/services/ConfigDiscovery.js +2 -2
- package/services/SilkPublishability.js +2 -3
- package/services/SilkWorkspaceAnalyzer.js +2 -3
- package/errors/BiomeSyncError.js +0 -22
- package/errors/ChangesetConfigError.js +0 -21
- package/errors/ConfigNotFoundError.js +0 -22
- package/errors/PublishTargetBindingError.js +0 -30
- package/errors/WorkspaceAnalysisError.js +0 -22
- package/pr-body/body.js +0 -145
- package/pr-body/diagnostics.js +0 -104
- package/pr-body/index.js +0 -22
- package/pr-body/linked-issue.js +0 -44
- package/pr-body/markers.js +0 -103
- package/pr-body/references.js +0 -157
- package/pr-body/region.js +0 -105
- package/schemas/SavvyInstallSection.js +0 -232
- package/schemas/SavvySections.js +0 -194
- package/schemas/VersioningSchemas.js +0 -87
- package/schemas/WorkspaceAnalysisSchemas.js +0 -209
- package/utils/TrailingSlash.js +0 -18
package/pr-body/markers.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { Region } from "./region.js";
|
|
2
|
-
|
|
3
|
-
//#region src/pr-body/markers.ts
|
|
4
|
-
/**
|
|
5
|
-
* The frozen `silk-release` marker vocabulary — the wire format of the shared
|
|
6
|
-
* PR-body contract.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* **The `silk-release:` token is frozen and names the CONTRACT, not the
|
|
10
|
-
* emitting action.** `silk-update-action` PRs carry the same markers as
|
|
11
|
-
* release PRs, deliberately: every live document, the `pr-body` plugin skill,
|
|
12
|
-
* and every agent that edits a managed PR description key on these exact
|
|
13
|
-
* byte sequences. Do not parameterize the token per action and do not rename
|
|
14
|
-
* it — either forks the wire format for zero gain and orphans every open PR
|
|
15
|
-
* (ruled in savvy-web/systems#419).
|
|
16
|
-
*
|
|
17
|
-
* These constants are the single source of truth for the marker grammar.
|
|
18
|
-
* The agent-facing documentation in the silk plugin (`pr-body` and
|
|
19
|
-
* `commit-create` skills) duplicates the literals for readability; a drift
|
|
20
|
-
* lint in this package's test suite asserts the copies stay in sync.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* The marker constants of the `silk-release` PR-body contract.
|
|
24
|
-
*
|
|
25
|
-
* @public
|
|
26
|
-
*/
|
|
27
|
-
var Markers = class {
|
|
28
|
-
constructor() {}
|
|
29
|
-
/**
|
|
30
|
-
* Opening marker of the whole managed region.
|
|
31
|
-
*
|
|
32
|
-
* @remarks
|
|
33
|
-
* Everything between this and {@link Markers.MANAGED_END} that is not
|
|
34
|
-
* inside the summary or references region is regenerated wholesale on
|
|
35
|
-
* every run; everything outside the pair is human territory and survives
|
|
36
|
-
* every regeneration.
|
|
37
|
-
*
|
|
38
|
-
* @public
|
|
39
|
-
*/
|
|
40
|
-
static MANAGED_START = Region.start("silk-release");
|
|
41
|
-
/**
|
|
42
|
-
* Closing marker of the whole managed region.
|
|
43
|
-
*
|
|
44
|
-
* @public
|
|
45
|
-
*/
|
|
46
|
-
static MANAGED_END = Region.end("silk-release");
|
|
47
|
-
/**
|
|
48
|
-
* Opening marker of the region an AI summariser owns.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* The generating action never writes into this region — it only reserves
|
|
52
|
-
* it and carries its content through on regeneration.
|
|
53
|
-
*
|
|
54
|
-
* @public
|
|
55
|
-
*/
|
|
56
|
-
static SUMMARY_START = Region.start("silk-release:summary");
|
|
57
|
-
/**
|
|
58
|
-
* Closing marker of the summariser's region.
|
|
59
|
-
*
|
|
60
|
-
* @public
|
|
61
|
-
*/
|
|
62
|
-
static SUMMARY_END = Region.end("silk-release:summary");
|
|
63
|
-
/**
|
|
64
|
-
* The PLAIN opening marker of the closing-reference region — the form an
|
|
65
|
-
* author writes by hand.
|
|
66
|
-
*
|
|
67
|
-
* @remarks
|
|
68
|
-
* A generating run emits the ATTRIBUTED form instead (the plain prefix
|
|
69
|
-
* plus an `owned="…"` attribute). Never locate the region by matching
|
|
70
|
-
* this constant — match {@link Markers.REFERENCES_START_PREFIX}, or a
|
|
71
|
-
* region a run wrote will not be found.
|
|
72
|
-
*
|
|
73
|
-
* @public
|
|
74
|
-
*/
|
|
75
|
-
static REFERENCES_START = Region.start("silk-release:references");
|
|
76
|
-
/**
|
|
77
|
-
* Closing marker of the closing-reference region.
|
|
78
|
-
*
|
|
79
|
-
* @public
|
|
80
|
-
*/
|
|
81
|
-
static REFERENCES_END = Region.end("silk-release:references");
|
|
82
|
-
/**
|
|
83
|
-
* The references opening marker up to its attributes, for locating a
|
|
84
|
-
* region whose `owned` list is unknown.
|
|
85
|
-
*
|
|
86
|
-
* @public
|
|
87
|
-
*/
|
|
88
|
-
static REFERENCES_START_PREFIX = "<!-- silk-release:references:start";
|
|
89
|
-
/**
|
|
90
|
-
* The fence language for the proposed squash-commit block.
|
|
91
|
-
*
|
|
92
|
-
* @remarks
|
|
93
|
-
* Not a GFM language and apparently undocumented, but GitHub renders it.
|
|
94
|
-
* It is a target for AI integrations to read and rewrite into the
|
|
95
|
-
* eventual squash-commit message. **Do not "correct" it to `text`.**
|
|
96
|
-
*
|
|
97
|
-
* @public
|
|
98
|
-
*/
|
|
99
|
-
static SQUASH_FENCE_LANGUAGE = "proposed-squash-commit";
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
//#endregion
|
|
103
|
-
export { Markers };
|
package/pr-body/references.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { Markers } from "./markers.js";
|
|
2
|
-
import { LinkedIssueRef } from "./linked-issue.js";
|
|
3
|
-
import { Schema } from "effect";
|
|
4
|
-
import { parseBareLines } from "@effected/github-references";
|
|
5
|
-
|
|
6
|
-
//#region src/pr-body/references.ts
|
|
7
|
-
/**
|
|
8
|
-
* An ordered list of issue ids destined for closing references, with the two
|
|
9
|
-
* renderers whose difference is the whole point of this module.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* The same issues appear twice in a managed PR body, spelled differently, and
|
|
13
|
-
* **neither consumer accepts the other's spelling**:
|
|
14
|
-
*
|
|
15
|
-
* - commitlint reads ONE comma-joined trailer (`Closes #1, #2`) inside the
|
|
16
|
-
* proposed-squash-commit fence — {@link ClosingReferences.renderTrailer};
|
|
17
|
-
* - GitHub's linker reads one bare `Closes #N` line each, OUTSIDE every
|
|
18
|
-
* fence — {@link ClosingReferences.renderBareLines}. A reference inside a
|
|
19
|
-
* fenced block is inert to GitHub.
|
|
20
|
-
*
|
|
21
|
-
* The duplication is load-bearing. Never "simplify" a body by emitting one
|
|
22
|
-
* form in both places: comma-joined bare lines link nothing (verified by hand
|
|
23
|
-
* against live pull requests — `savvy-web/silk-integration` #242/#232 with no
|
|
24
|
-
* bare line reported `closingIssuesReferences: []`, #243 with one reported
|
|
25
|
-
* `[168]`), and per-line trailers inside the fence break the commit contract.
|
|
26
|
-
*
|
|
27
|
-
* Ids are stored exactly as given — construction neither deduplicates nor
|
|
28
|
-
* sorts. Call {@link ClosingReferences.dedupe} where uniqueness is wanted;
|
|
29
|
-
* the split exists because the squash trailer historically renders duplicates
|
|
30
|
-
* as-given while the references region deduplicates, and byte-compatibility
|
|
31
|
-
* with live PR bodies pins that behavior.
|
|
32
|
-
*
|
|
33
|
-
* @public
|
|
34
|
-
*/
|
|
35
|
-
var ClosingReferences = class ClosingReferences extends Schema.Class("ClosingReferences")({ ids: Schema.Array(Schema.Number.check(Schema.isInt())) }) {
|
|
36
|
-
/**
|
|
37
|
-
* The open issues' ids, in input order, duplicates preserved.
|
|
38
|
-
*
|
|
39
|
-
* @remarks
|
|
40
|
-
* Closedness is decided by `LinkedIssueRef.isClosed` — the only
|
|
41
|
-
* sanctioned test, case-insensitive so REST (`closed`) and GraphQL
|
|
42
|
-
* (`CLOSED`) payloads classify identically.
|
|
43
|
-
*
|
|
44
|
-
* @public
|
|
45
|
-
*/
|
|
46
|
-
static fromIssues(issues) {
|
|
47
|
-
return ClosingReferences.make({ ids: issues.filter((issue) => !LinkedIssueRef.isClosed(issue)).map((issue) => issue.number) });
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Issue ids carried by a region's bare closing lines.
|
|
51
|
-
*
|
|
52
|
-
* @remarks
|
|
53
|
-
* The region is read with `@effected/github-references`' `parseBareLines`
|
|
54
|
-
* — per line, the whole line, after trimming, must be
|
|
55
|
-
* `<keyword>[:] #<number>`, so a number mentioned in passing is never
|
|
56
|
-
* mistaken for a closing reference. Every keyword GitHub accepts counts,
|
|
57
|
-
* not just the present-tense plural this contract emits, because a
|
|
58
|
-
* reference the parser fails to recognise is one the next regeneration
|
|
59
|
-
* silently deletes.
|
|
60
|
-
*
|
|
61
|
-
* @public
|
|
62
|
-
*/
|
|
63
|
-
static parseBare(region) {
|
|
64
|
-
return parseBareLines(region).map((ref) => ref.issueNumber);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* A copy with duplicate ids removed, first occurrence winning.
|
|
68
|
-
*
|
|
69
|
-
* @public
|
|
70
|
-
*/
|
|
71
|
-
dedupe() {
|
|
72
|
-
return ClosingReferences.make({ ids: [...new Set(this.ids)] });
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* The comma-joined trailer the squash-commit message carries, or `""`
|
|
76
|
-
* when there is nothing to close.
|
|
77
|
-
*
|
|
78
|
-
* @remarks
|
|
79
|
-
* `Closes #1, #2` on ONE line — the spelling commitlint reads and
|
|
80
|
-
* GitHub's linker ignores. See the class remarks before changing either
|
|
81
|
-
* renderer.
|
|
82
|
-
*
|
|
83
|
-
* @public
|
|
84
|
-
*/
|
|
85
|
-
renderTrailer() {
|
|
86
|
-
if (this.ids.length === 0) return "";
|
|
87
|
-
return `Closes ${this.ids.map((id) => `#${id}`).join(", ")}`;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* One bare `Closes #N` line per id, or `""` when empty.
|
|
91
|
-
*
|
|
92
|
-
* @remarks
|
|
93
|
-
* The spelling GitHub's linker reads — each line must sit OUTSIDE every
|
|
94
|
-
* fenced block to link. See the class remarks before changing either
|
|
95
|
-
* renderer.
|
|
96
|
-
*
|
|
97
|
-
* @public
|
|
98
|
-
*/
|
|
99
|
-
renderBareLines() {
|
|
100
|
-
return this.ids.map((id) => `Closes #${id}`).join("\n");
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
/**
|
|
104
|
-
* The `owned="…"` attribute on the references region's opening marker.
|
|
105
|
-
*
|
|
106
|
-
* @remarks
|
|
107
|
-
* Records the issue ids a generating run emitted itself, so the next run can
|
|
108
|
-
* tell its own references from ones an agent or human added. "Not in this
|
|
109
|
-
* run's linked set" is NOT enough: a reference the previous run emitted also
|
|
110
|
-
* disappears from the linked set when the release stops tracking that issue,
|
|
111
|
-
* and treating it as agent-authored would preserve it forever — re-linking,
|
|
112
|
-
* and on merge auto-closing, an issue the release deliberately dropped.
|
|
113
|
-
*
|
|
114
|
-
* **Never hand-edit the attribute.** A wrong value makes the next run delete
|
|
115
|
-
* a real reference or resurrect a dropped one.
|
|
116
|
-
*
|
|
117
|
-
* @public
|
|
118
|
-
*/
|
|
119
|
-
var OwnedAttribute = class {
|
|
120
|
-
constructor() {}
|
|
121
|
-
/**
|
|
122
|
-
* The attribute as emitted on the opening marker.
|
|
123
|
-
*
|
|
124
|
-
* @public
|
|
125
|
-
*/
|
|
126
|
-
static render(ids) {
|
|
127
|
-
return `owned="${ids.join(",")}"`;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* The ids the prior body's opening marker claims as the previous run's
|
|
131
|
-
* own.
|
|
132
|
-
*
|
|
133
|
-
* @remarks
|
|
134
|
-
* An absent or malformed attribute reads as "none", which degrades to
|
|
135
|
-
* treating every reference in the region as agent-authored. That
|
|
136
|
-
* preserves too much rather than deleting someone's work — the safe
|
|
137
|
-
* direction to fail. The match is anchored to an attribute boundary: an
|
|
138
|
-
* unanchored match also finds `data-owned="…"` and `unowned="…"`, which
|
|
139
|
-
* would let an unrelated attribute claim an agent's reference and get it
|
|
140
|
-
* dropped on the next run.
|
|
141
|
-
*
|
|
142
|
-
* @public
|
|
143
|
-
*/
|
|
144
|
-
static parse(priorBody) {
|
|
145
|
-
const from = priorBody.indexOf(Markers.REFERENCES_START_PREFIX);
|
|
146
|
-
if (from === -1) return /* @__PURE__ */ new Set();
|
|
147
|
-
const openEnd = priorBody.indexOf("-->", from);
|
|
148
|
-
if (openEnd === -1) return /* @__PURE__ */ new Set();
|
|
149
|
-
const attributes = priorBody.slice(from + Markers.REFERENCES_START_PREFIX.length, openEnd);
|
|
150
|
-
const owned = /(?:^|\s)owned="([\d,\s]*)"/.exec(attributes)?.[1];
|
|
151
|
-
if (owned === void 0) return /* @__PURE__ */ new Set();
|
|
152
|
-
return new Set(owned.split(",").map((part) => part.trim()).filter((part) => part !== "").map(Number));
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
//#endregion
|
|
157
|
-
export { ClosingReferences, OwnedAttribute };
|
package/pr-body/region.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
//#region src/pr-body/region.ts
|
|
2
|
-
/**
|
|
3
|
-
* The generic marker-delimited region grammar every silk-managed document
|
|
4
|
-
* uses: `<!-- token:start -->` … `<!-- token:end -->`.
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* Extracted from `silk-release-action` (its `pr-body.ts` and
|
|
8
|
-
* `managed-sections.ts` both carried a private copy) so the grammar has one
|
|
9
|
-
* owner. **Every marker is a pair.** A lone opening marker can only be located
|
|
10
|
-
* by scanning forward to whatever happens to follow it, which makes the
|
|
11
|
-
* region's extent a function of its neighbours rather than of itself — moving
|
|
12
|
-
* anything nearby silently redefines it. The token is free-form; `:start` and
|
|
13
|
-
* `:end` are the whole contract, so pairs nest and a region can contain
|
|
14
|
-
* sub-regions without either needing to know about the other.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Pure helpers over the `<!-- token:start -->` / `<!-- token:end -->` region
|
|
18
|
-
* grammar.
|
|
19
|
-
*
|
|
20
|
-
* @remarks
|
|
21
|
-
* Every operation is total: a body with no region (or a broken pair) degrades
|
|
22
|
-
* to the documented fail-safe result rather than failing, because the callers
|
|
23
|
-
* are regenerating actions that must still produce a body when the prior one
|
|
24
|
-
* is malformed. Use `PrBodyDiagnostic.scan` when a caller wants to be told
|
|
25
|
-
* about a broken pair instead of silently tolerating it.
|
|
26
|
-
*
|
|
27
|
-
* @public
|
|
28
|
-
*/
|
|
29
|
-
var Region = class Region {
|
|
30
|
-
constructor() {}
|
|
31
|
-
/**
|
|
32
|
-
* The opening delimiter for a named region.
|
|
33
|
-
*
|
|
34
|
-
* @public
|
|
35
|
-
*/
|
|
36
|
-
static start(token) {
|
|
37
|
-
return `<!-- ${token}:start -->`;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* The closing delimiter for a named region.
|
|
41
|
-
*
|
|
42
|
-
* @public
|
|
43
|
-
*/
|
|
44
|
-
static end(token) {
|
|
45
|
-
return `<!-- ${token}:end -->`;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The content between a region's delimiters, or `undefined` when absent.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* Finds the FIRST opening marker and the matching close after it, so a
|
|
52
|
-
* nested region of a different token is returned as part of the content
|
|
53
|
-
* rather than truncating it.
|
|
54
|
-
*
|
|
55
|
-
* @public
|
|
56
|
-
*/
|
|
57
|
-
static read(body, token) {
|
|
58
|
-
const from = body.indexOf(Region.start(token));
|
|
59
|
-
const to = body.indexOf(Region.end(token), from === -1 ? 0 : from);
|
|
60
|
-
if (from === -1 || to === -1 || to < from) return void 0;
|
|
61
|
-
return body.slice(from + Region.start(token).length, to);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Everything outside a region, with the region and its delimiters removed.
|
|
65
|
-
*
|
|
66
|
-
* @remarks
|
|
67
|
-
* A body without the region comes back unchanged — removal of an absent
|
|
68
|
-
* region is a no-op, not an error.
|
|
69
|
-
*
|
|
70
|
-
* @public
|
|
71
|
-
*/
|
|
72
|
-
static strip(body, token) {
|
|
73
|
-
const from = body.indexOf(Region.start(token));
|
|
74
|
-
const to = body.indexOf(Region.end(token), from === -1 ? 0 : from);
|
|
75
|
-
if (from === -1 || to === -1 || to < from) return body;
|
|
76
|
-
return `${body.slice(0, from)}${body.slice(to + Region.end(token).length)}`;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Put `rendered` (a fully rendered region, its own markers included) into
|
|
80
|
-
* `body`, replacing a previous region of the same token and leaving
|
|
81
|
-
* everything else alone.
|
|
82
|
-
*
|
|
83
|
-
* @remarks
|
|
84
|
-
* **Human edits outside the markers survive.** A predecessor spliced on a
|
|
85
|
-
* markdown heading, which silently ate any content a human happened to put
|
|
86
|
-
* under a heading of that name and could not tell generated text from
|
|
87
|
-
* theirs. An explicit marker pair can.
|
|
88
|
-
*
|
|
89
|
-
* A body with no markers keeps its content and gains the region **below**
|
|
90
|
-
* it, so an existing hand-written document is not displaced. The result is
|
|
91
|
-
* trimmed.
|
|
92
|
-
*
|
|
93
|
-
* @public
|
|
94
|
-
*/
|
|
95
|
-
static upsert(body, token, rendered) {
|
|
96
|
-
const start = body.indexOf(Region.start(token));
|
|
97
|
-
const end = body.indexOf(Region.end(token));
|
|
98
|
-
if (start !== -1 && end !== -1 && end > start) return `${body.slice(0, start)}${rendered}${body.slice(end + Region.end(token).length)}`.trim();
|
|
99
|
-
const trimmed = body.trim();
|
|
100
|
-
return trimmed === "" ? rendered : `${trimmed}\n\n${rendered}`;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
//#endregion
|
|
105
|
-
export { Region };
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { CommentStyle, SectionId } from "@effected/templates";
|
|
2
|
-
|
|
3
|
-
//#region src/schemas/SavvyInstallSection.ts
|
|
4
|
-
/**
|
|
5
|
-
* Section identity for the dependency auto-install block.
|
|
6
|
-
*
|
|
7
|
-
* `toolName` is `"savvy-install"`; pair with {@link savvyInstallDeps}.
|
|
8
|
-
*
|
|
9
|
-
* @since 7.5.0
|
|
10
|
-
* @public
|
|
11
|
-
*/
|
|
12
|
-
const SavvyInstallSection = SectionId.make({
|
|
13
|
-
key: "SAVVY-INSTALL",
|
|
14
|
-
commentStyle: CommentStyle.hash
|
|
15
|
-
});
|
|
16
|
-
/**
|
|
17
|
-
* Pathspecs whose change across a checkout or merge means `node_modules` is stale.
|
|
18
|
-
*
|
|
19
|
-
* @remarks
|
|
20
|
-
* Git's default pathspec globbing does not stop a wildcard at a slash, so the
|
|
21
|
-
* one nested-manifest entry below covers every workspace manifest at any depth
|
|
22
|
-
* without needing a recursive spelling. Manifests are listed alongside lockfiles
|
|
23
|
-
* because a dependency edit that has not been installed yet moves the manifest
|
|
24
|
-
* first and the lockfile not at all.
|
|
25
|
-
*/
|
|
26
|
-
const DEPENDENCY_PATHS = [
|
|
27
|
-
"package.json",
|
|
28
|
-
"*/package.json",
|
|
29
|
-
"pnpm-lock.yaml",
|
|
30
|
-
"pnpm-workspace.yaml",
|
|
31
|
-
"package-lock.json",
|
|
32
|
-
"npm-shrinkwrap.json",
|
|
33
|
-
"yarn.lock",
|
|
34
|
-
"bun.lock",
|
|
35
|
-
"bun.lockb"
|
|
36
|
-
].map((path) => `'${path}'`).join(" ");
|
|
37
|
-
/**
|
|
38
|
-
* Dependency auto-install shared across Silk Suite hook files.
|
|
39
|
-
*
|
|
40
|
-
* @remarks
|
|
41
|
-
* Brings `node_modules` back in line after a branch switch or a pull, so a batch
|
|
42
|
-
* of repos pulled to align their dependencies does not each need a manual
|
|
43
|
-
* install. Installs with lifecycle scripts skipped — `--ignore-scripts`, or
|
|
44
|
-
* `--mode=skip-build` for Yarn Berry, which dropped the former — because the goal
|
|
45
|
-
* is to get the dependency tree on disk, not to run a full postinstall.
|
|
46
|
-
*
|
|
47
|
-
* **That flag has a cost, and one shape of repo cannot pay it.** Where a package
|
|
48
|
-
* publishes through a built link directory — `publishConfig.directory` with
|
|
49
|
-
* `linkDirectory: true`, which is how this monorepo wires `dist/dev/pkg` — the
|
|
50
|
-
* workspace resolves its own dependencies through a directory that a `prepare`
|
|
51
|
-
* script has to build, and skipping scripts yields a populated `node_modules`
|
|
52
|
-
* pointing at nothing.
|
|
53
|
-
*
|
|
54
|
-
* Those repos ask for a full install through the LOCAL git config key
|
|
55
|
-
* `savvy.installLifecycleScripts`, which `savvy init` sets when it finds the
|
|
56
|
-
* shape. The decision deliberately does NOT read the checked-out tree. Doing so
|
|
57
|
-
* failed in both directions at once: a branch that merely declared the shape
|
|
58
|
-
* could turn lifecycle scripts back on just by being checked out, making
|
|
59
|
-
* `git checkout` of an untrusted revision a code-execution path; and the `jq`
|
|
60
|
-
* the scan needed is absent on stock macOS and Ubuntu, where the missing answer
|
|
61
|
-
* silently skipped scripts in precisely the repos that cannot survive it.
|
|
62
|
-
* `.git/config` is neither checked out nor parsed with `jq`, so it has neither
|
|
63
|
-
* failure mode.
|
|
64
|
-
*
|
|
65
|
-
* The package managers gate dependency scripts themselves on top of this —
|
|
66
|
-
* pnpm's `strictDepBuilds` with an `allowBuilds` allowlist being the case in
|
|
67
|
-
* point — but workspace and root lifecycle scripts still run, which is why the
|
|
68
|
-
* opt-in is local rather than inferred. With the flag on, the hook says on the
|
|
69
|
-
* way out that scripts were skipped rather than leaving it to be discovered.
|
|
70
|
-
*
|
|
71
|
-
* Deliberately self-contained, like `savvyToolchainCheck`: its homes carry
|
|
72
|
-
* `SavvyHooksSection` but no `SavvyBaseSection`, so it defines its own root, CI
|
|
73
|
-
* and package-manager lookups rather than depending on `ROOT`, `in_ci` or `PM`.
|
|
74
|
-
*
|
|
75
|
-
* Four guards keep it from firing on the many checkouts that are not dependency
|
|
76
|
-
* events, each a silent no-op:
|
|
77
|
-
*
|
|
78
|
-
* - CI, where the runtime action owns installation by construction.
|
|
79
|
-
* - `SAVVY_SKIP_INSTALL`, the escape hatch for a bisect or a scripted sweep that
|
|
80
|
-
* does not want an install between steps.
|
|
81
|
-
* - `post-checkout` only: a branch-flag of `0`, which is `git checkout -- <file>`
|
|
82
|
-
* and not a move between commits.
|
|
83
|
-
* - **The gate that makes this affordable:** nothing dependency-related actually
|
|
84
|
-
* changed across the move. Without it every branch switch would pay for a full
|
|
85
|
-
* install. A missing `node_modules` overrides it, since there is nothing to be
|
|
86
|
-
* stale.
|
|
87
|
-
*
|
|
88
|
-
* A fresh clone is not among the cases it handles, and cannot be: husky sets
|
|
89
|
-
* `core.hooksPath` from its own `prepare` script, so until the first manual
|
|
90
|
-
* install has run there is no hook installed to fire.
|
|
91
|
-
*
|
|
92
|
-
* The install's exit status is swallowed and its output goes to stderr. Git
|
|
93
|
-
* ignores what `post-checkout` and `post-merge` return, so a failure here must
|
|
94
|
-
* not look like a failed checkout; the hint names the escape hatch instead.
|
|
95
|
-
*
|
|
96
|
-
* Every probe that can legitimately fail — no `ORIG_HEAD` to compare against, an
|
|
97
|
-
* unresolvable range — is neutralised with `|| true` rather than left to its own
|
|
98
|
-
* status. These blocks are co-owned and consumer content can sit below them, so
|
|
99
|
-
* under a hook running `set -e` a bare failing substitution would abort the whole
|
|
100
|
-
* file and take those later sections with it.
|
|
101
|
-
*
|
|
102
|
-
* @param hook - Which hook the block is being generated for; decides the
|
|
103
|
-
* argument guard and how the comparison range is recovered.
|
|
104
|
-
* @returns The install shell, with no surrounding markers or trailing newline.
|
|
105
|
-
*
|
|
106
|
-
* @since 7.5.0
|
|
107
|
-
* @public
|
|
108
|
-
*/
|
|
109
|
-
function savvyInstallDeps(hook) {
|
|
110
|
-
const skip = `! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ] || [ -n "$SAVVY_SKIP_INSTALL" ]; }`;
|
|
111
|
-
return `if ${hook === "post-checkout" ? `${skip} && [ "$3" = "1" ]` : skip}; then
|
|
112
|
-
install_root=$(git rev-parse --show-toplevel 2>/dev/null) || true
|
|
113
|
-
${hook === "post-checkout" ? ` install_from="$1"
|
|
114
|
-
install_to="$2"` : ` install_from=$(git rev-parse --verify --quiet ORIG_HEAD 2>/dev/null) || true
|
|
115
|
-
install_to="HEAD"`}
|
|
116
|
-
install_pm=""
|
|
117
|
-
if [ -n "$install_root" ] && [ -f "$install_root/package.json" ]; then
|
|
118
|
-
if command -v jq >/dev/null 2>&1; then
|
|
119
|
-
install_pm=$(jq -r '.packageManager // empty' "$install_root/package.json" 2>/dev/null | cut -d'@' -f1)
|
|
120
|
-
fi
|
|
121
|
-
if [ -z "$install_pm" ]; then
|
|
122
|
-
if [ -f "$install_root/pnpm-lock.yaml" ]; then install_pm="pnpm"
|
|
123
|
-
elif [ -f "$install_root/yarn.lock" ]; then install_pm="yarn"
|
|
124
|
-
elif [ -f "$install_root/bun.lock" ] || [ -f "$install_root/bun.lockb" ]; then install_pm="bun"
|
|
125
|
-
else install_pm="npm"; fi
|
|
126
|
-
fi
|
|
127
|
-
fi
|
|
128
|
-
# packageManager comes from the checked-out revision, so it names an executable
|
|
129
|
-
# the tree controls. 'command -v' proves a binary exists, not that it is a
|
|
130
|
-
# package manager, so the name is checked against the supported four first.
|
|
131
|
-
case "$install_pm" in
|
|
132
|
-
npm|pnpm|yarn|bun) ;;
|
|
133
|
-
*) install_pm="" ;;
|
|
134
|
-
esac
|
|
135
|
-
# Nothing to bring up to date unless a manifest or lockfile actually moved.
|
|
136
|
-
# A missing node_modules skips the diff outright: there is no tree to be stale.
|
|
137
|
-
install_stale=""
|
|
138
|
-
if [ -n "$install_pm" ]; then
|
|
139
|
-
if [ ! -d "$install_root/node_modules" ]; then
|
|
140
|
-
install_stale=1
|
|
141
|
-
elif [ -n "$install_from" ] && [ -n "$install_to" ]; then
|
|
142
|
-
install_stale=$(git diff --name-only "$install_from" "$install_to" -- ${DEPENDENCY_PATHS} 2>/dev/null | head -n 1) || true
|
|
143
|
-
fi
|
|
144
|
-
fi
|
|
145
|
-
if [ -n "$install_stale" ] && command -v "$install_pm" >/dev/null 2>&1; then
|
|
146
|
-
# Whether lifecycle scripts run is a LOCAL decision, read from .git/config,
|
|
147
|
-
# which no checkout can rewrite. Default off. 'savvy init' turns it on for a
|
|
148
|
-
# workspace that publishes through built link directories, where the links
|
|
149
|
-
# point at directories a prepare script has to produce.
|
|
150
|
-
install_scripts=$(git -C "$install_root" config --bool --get savvy.installLifecycleScripts 2>/dev/null) || true
|
|
151
|
-
if [ "$install_scripts" = "true" ]; then
|
|
152
|
-
install_flag=""
|
|
153
|
-
else
|
|
154
|
-
# Berry dropped --ignore-scripts for the install mode; Classic never knew the
|
|
155
|
-
# mode. An unreadable major reads as Berry, the likelier of the two today.
|
|
156
|
-
install_flag="--ignore-scripts"
|
|
157
|
-
if [ "$install_pm" = "yarn" ]; then
|
|
158
|
-
case "$(yarn --version 2>/dev/null | cut -d. -f1)" in
|
|
159
|
-
1) ;;
|
|
160
|
-
*) install_flag="--mode=skip-build" ;;
|
|
161
|
-
esac
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
# ':+' so a full install announces itself as "pnpm install", not "pnpm install ".
|
|
165
|
-
printf '↻ dependencies changed, running %s install%s\\n' "$install_pm" "\${install_flag:+ $install_flag}" >&2
|
|
166
|
-
# Unquoted on purpose: empty must expand to NO argument rather than an empty
|
|
167
|
-
# one. Safe because every value is a static flag literal from this generator.
|
|
168
|
-
if ( cd "$install_root" && "$install_pm" install $install_flag ) >&2; then
|
|
169
|
-
if [ -n "$install_flag" ]; then
|
|
170
|
-
printf ' Lifecycle scripts were skipped. Run a full install if you need build outputs.\\n' >&2
|
|
171
|
-
fi
|
|
172
|
-
else
|
|
173
|
-
printf '⚠ %s install failed; run it yourself to see why.\\n' "$install_pm" >&2
|
|
174
|
-
printf ' Set SAVVY_SKIP_INSTALL=1 to stop this hook from trying.\\n' >&2
|
|
175
|
-
fi
|
|
176
|
-
fi
|
|
177
|
-
unset install_root install_from install_to install_pm install_stale install_scripts install_flag
|
|
178
|
-
fi`;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The local git config key that authorizes lifecycle scripts during a hook install.
|
|
182
|
-
*
|
|
183
|
-
* @remarks
|
|
184
|
-
* Local scope only. It lives in `.git/config`, which is never checked out, so no
|
|
185
|
-
* incoming revision can set it — that is the whole point of reading the decision
|
|
186
|
-
* from here rather than from a manifest in the tree.
|
|
187
|
-
*
|
|
188
|
-
* @since 7.5.0
|
|
189
|
-
* @public
|
|
190
|
-
*/
|
|
191
|
-
const LIFECYCLE_SCRIPTS_CONFIG_KEY = "savvy.installLifecycleScripts";
|
|
192
|
-
/**
|
|
193
|
-
* Whether `manifest` publishes through a built link directory.
|
|
194
|
-
*
|
|
195
|
-
* @remarks
|
|
196
|
-
* `publishConfig.directory` with `linkDirectory: true` means consumers of this
|
|
197
|
-
* package resolve it through a directory that a `prepare` script has to produce,
|
|
198
|
-
* so an install that skips lifecycle scripts leaves the link pointing at nothing.
|
|
199
|
-
* A workspace containing any such package is one whose owner probably wants
|
|
200
|
-
* {@link LIFECYCLE_SCRIPTS_CONFIG_KEY} set.
|
|
201
|
-
*
|
|
202
|
-
* Reporting the shape is deliberately separate from acting on it: this answers
|
|
203
|
-
* "does this repo need scripts", and a human still decides whether hook-time
|
|
204
|
-
* installs may run them.
|
|
205
|
-
*
|
|
206
|
-
* @param manifest - A parsed `package.json`; any non-object reads as `false`.
|
|
207
|
-
*
|
|
208
|
-
* @since 7.5.0
|
|
209
|
-
* @public
|
|
210
|
-
*/
|
|
211
|
-
function publishesBuiltLinkDirectory(manifest) {
|
|
212
|
-
if (typeof manifest !== "object" || manifest === null) return false;
|
|
213
|
-
const { publishConfig } = manifest;
|
|
214
|
-
if (typeof publishConfig !== "object" || publishConfig === null) return false;
|
|
215
|
-
const { directory, linkDirectory } = publishConfig;
|
|
216
|
-
return typeof directory === "string" && directory.length > 0 && linkDirectory === true;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Build the dependency auto-install block for `hook`.
|
|
220
|
-
*
|
|
221
|
-
* @param hook - Which hook the section is destined for.
|
|
222
|
-
* @returns A shell `Section` (`commentStyle: hash`) keyed `SAVVY-INSTALL`.
|
|
223
|
-
*
|
|
224
|
-
* @since 7.5.0
|
|
225
|
-
* @public
|
|
226
|
-
*/
|
|
227
|
-
function savvyInstallBlock(hook) {
|
|
228
|
-
return SavvyInstallSection.section(savvyInstallDeps(hook));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
//#endregion
|
|
232
|
-
export { LIFECYCLE_SCRIPTS_CONFIG_KEY, SavvyInstallSection, publishesBuiltLinkDirectory, savvyInstallBlock, savvyInstallDeps };
|