checkly 8.23.0 → 8.23.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/dist/ai-context/skills-command/references/configure-playwright-checks.md +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/services/check-parser/bundler.js +48 -3
- package/dist/services/check-parser/bundler.js.map +1 -1
- package/dist/services/check-parser/package-prune.d.ts +180 -21
- package/dist/services/check-parser/package-prune.js +470 -92
- package/dist/services/check-parser/package-prune.js.map +1 -1
- package/dist/services/checkly-config-loader.d.ts +47 -8
- package/dist/services/checkly-config-loader.js.map +1 -1
- package/oclif.manifest.json +406 -406
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import Debug from 'debug';
|
|
3
|
-
import {
|
|
3
|
+
import { quotedList } from '../embedded-packages/diagnostics.js';
|
|
4
|
+
import { parsePackageNamePattern, patternsSelectName, specMatchesPackageName, } from '../embedded-packages/spec.js';
|
|
4
5
|
const debug = Debug('checkly:cli:services:check-parser:package-prune');
|
|
5
6
|
export const DEPENDENCY_CLASSES = [
|
|
6
7
|
'dependencies',
|
|
@@ -9,35 +10,41 @@ export const DEPENDENCY_CLASSES = [
|
|
|
9
10
|
'optionalDependencies',
|
|
10
11
|
];
|
|
11
12
|
const SHAPE_ERROR = `must be an array of package name patterns or an object keyed by dependency class`;
|
|
13
|
+
const ENTRY_SHAPE_ERROR = `each entry must be a package name pattern`
|
|
14
|
+
+ ` or a member-scoped object with 'member' and one of 'remove' or 'keep'`;
|
|
15
|
+
const CATCH_ALL_PATTERN = parsePackageNamePattern('**');
|
|
16
|
+
function isPlainObject(value) {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
// A non-plain object (a Set, a Map, a Date) has no own enumerable
|
|
21
|
+
// string keys, so without this check it would silently normalize to
|
|
22
|
+
// "nothing to do" instead of being rejected.
|
|
23
|
+
const proto = Object.getPrototypeOf(value);
|
|
24
|
+
return proto === Object.prototype || proto === null;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
27
|
+
* Parses one element of a pattern list. A plain-object element gets a
|
|
28
|
+
* pointed rejection: the likely mistake is a member-scoped entry nested
|
|
29
|
+
* inside a class-keyed map or another entry's selection, which would
|
|
30
|
+
* otherwise fail as an unreadable `'[object Object]' is not a valid
|
|
31
|
+
* pattern`. Anything else — arrays included, which the hint would only
|
|
32
|
+
* misdirect — falls through to the generic pattern error.
|
|
19
33
|
*/
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let perClass;
|
|
25
|
-
if (Array.isArray(raw)) {
|
|
26
|
-
perClass = Object.fromEntries(DEPENDENCY_CLASSES.map(dependencyClass => [dependencyClass, raw]));
|
|
27
|
-
}
|
|
28
|
-
else if (raw !== null && typeof raw === 'object') {
|
|
29
|
-
// A non-plain object (a Set, a Map, a Date) has no own enumerable
|
|
30
|
-
// string keys, so without this check it would silently normalize to
|
|
31
|
-
// "nothing to do" instead of being rejected.
|
|
32
|
-
const proto = Object.getPrototypeOf(raw);
|
|
33
|
-
if (proto !== Object.prototype && proto !== null) {
|
|
34
|
-
throw new Error(SHAPE_ERROR);
|
|
35
|
-
}
|
|
36
|
-
perClass = raw;
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
throw new Error(SHAPE_ERROR);
|
|
34
|
+
function parseNamePatternEntry(entry, context) {
|
|
35
|
+
if (isPlainObject(entry)) {
|
|
36
|
+
throw new Error(`'${context}' entries must be package name patterns`
|
|
37
|
+
+ ` (member-scoped objects are only valid in the top-level prune array)`);
|
|
40
38
|
}
|
|
39
|
+
return parsePackageNamePattern(entry);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates and normalizes a class-keyed pattern map. `true` values are
|
|
43
|
+
* kept as `true` for the standalone class-keyed form and desugared to the
|
|
44
|
+
* catch-all `'**'` pattern inside member-scoped entries, where selections
|
|
45
|
+
* compose in listed order and a later exclusion may subtract from them.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeClassMap(perClass, desugarTrue) {
|
|
41
48
|
const normalized = {};
|
|
42
49
|
for (const [key, value] of Object.entries(perClass)) {
|
|
43
50
|
if (!DEPENDENCY_CLASSES.includes(key)) {
|
|
@@ -47,7 +54,7 @@ export function normalizePackagePrune(raw) {
|
|
|
47
54
|
continue;
|
|
48
55
|
}
|
|
49
56
|
if (value === true) {
|
|
50
|
-
normalized[key] = true;
|
|
57
|
+
normalized[key] = desugarTrue ? [CATCH_ALL_PATTERN] : true;
|
|
51
58
|
continue;
|
|
52
59
|
}
|
|
53
60
|
if (Array.isArray(value)) {
|
|
@@ -57,19 +64,402 @@ export function normalizePackagePrune(raw) {
|
|
|
57
64
|
// Parsed per class even for the array shape, so no two classes share
|
|
58
65
|
// one pattern array instance and a consumer mutating one cannot
|
|
59
66
|
// silently change the others.
|
|
60
|
-
normalized[key] = value.map(entry =>
|
|
67
|
+
normalized[key] = value.map(entry => parseNamePatternEntry(entry, key));
|
|
61
68
|
continue;
|
|
62
69
|
}
|
|
63
70
|
throw new Error(`'${key}' must be true or an array of package name patterns`);
|
|
64
71
|
}
|
|
65
|
-
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Normalizes a member-scoped `remove`/`keep` value: a flat pattern list
|
|
76
|
+
* fans out to every dependency class, a class-keyed map stays per class.
|
|
77
|
+
* For `keep`, an absent class means "keep nothing from it" — so an empty
|
|
78
|
+
* flat list or map legitimately describes a member kept dependency-free.
|
|
79
|
+
*/
|
|
80
|
+
function normalizeSelection(value, field) {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
const patterns = value.map(entry => parseNamePatternEntry(entry, field));
|
|
83
|
+
if (patterns.length === 0) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
return Object.fromEntries(DEPENDENCY_CLASSES.map(dependencyClass => [dependencyClass, patterns.slice()]));
|
|
87
|
+
}
|
|
88
|
+
if (isPlainObject(value)) {
|
|
89
|
+
// With `true` desugared no `true` values remain, only pattern arrays.
|
|
90
|
+
return normalizeClassMap(value, true);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`'${field}' ${SHAPE_ERROR}`);
|
|
93
|
+
}
|
|
94
|
+
function normalizeMemberPatterns(raw) {
|
|
95
|
+
const list = typeof raw === 'string' ? [raw] : raw;
|
|
96
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
97
|
+
throw new Error(`'member' must be a workspace member name pattern or a non-empty array of them`);
|
|
98
|
+
}
|
|
99
|
+
return list.map(element => {
|
|
100
|
+
if (element === '.' || element === '!.') {
|
|
101
|
+
return { root: true, exclude: element === '!.' };
|
|
102
|
+
}
|
|
103
|
+
if (element !== null && typeof element === 'object') {
|
|
104
|
+
throw new Error(`'member' entries must be member name patterns or the '.' root token`);
|
|
105
|
+
}
|
|
106
|
+
return parsePackageNamePattern(element);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function normalizeMemberScopedEntry(entry) {
|
|
110
|
+
if (!isPlainObject(entry)) {
|
|
111
|
+
throw new Error(ENTRY_SHAPE_ERROR);
|
|
112
|
+
}
|
|
113
|
+
for (const key of Object.keys(entry)) {
|
|
114
|
+
if (key !== 'member' && key !== 'remove' && key !== 'keep') {
|
|
115
|
+
throw new Error(`'${key}' is not a member-scoped prune entry field (expected member, remove, keep)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const member = normalizeMemberPatterns(entry.member);
|
|
119
|
+
const hasRemove = entry.remove !== undefined;
|
|
120
|
+
const hasKeep = entry.keep !== undefined;
|
|
121
|
+
if (hasRemove === hasKeep) {
|
|
122
|
+
throw new Error(`a member-scoped prune entry must have exactly one of 'remove' and 'keep'`);
|
|
123
|
+
}
|
|
124
|
+
if (hasRemove) {
|
|
125
|
+
return { kind: 'remove', member, remove: normalizeSelection(entry.remove, 'remove') };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
kind: 'keep',
|
|
129
|
+
member,
|
|
130
|
+
keep: normalizeSelection(entry.keep, 'keep'),
|
|
131
|
+
flat: Array.isArray(entry.keep),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Validates and normalizes a `bundle.packages.prune` value. Returns
|
|
136
|
+
* `undefined` when there is nothing to do — the value is absent, or every
|
|
137
|
+
* shape it carries is empty. Throws `InvalidPackageNamePatternError` on an
|
|
138
|
+
* invalid pattern and a plain `Error` on an invalid shape; the config
|
|
139
|
+
* loader relies on that to reject plain-JS configs that bypass the
|
|
140
|
+
* TypeScript type.
|
|
141
|
+
*/
|
|
142
|
+
export function normalizePackagePrune(raw) {
|
|
143
|
+
if (raw === undefined) {
|
|
66
144
|
return undefined;
|
|
67
145
|
}
|
|
68
|
-
|
|
146
|
+
if (Array.isArray(raw)) {
|
|
147
|
+
const entries = [];
|
|
148
|
+
for (const entry of raw) {
|
|
149
|
+
if (typeof entry === 'string') {
|
|
150
|
+
entries.push({ kind: 'global', pattern: parsePackageNamePattern(entry) });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
entries.push(normalizeMemberScopedEntry(entry));
|
|
154
|
+
}
|
|
155
|
+
if (entries.length === 0) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return { form: 'entries', entries };
|
|
159
|
+
}
|
|
160
|
+
if (!isPlainObject(raw)) {
|
|
161
|
+
throw new Error(SHAPE_ERROR);
|
|
162
|
+
}
|
|
163
|
+
const classes = normalizeClassMap(raw, false);
|
|
164
|
+
if (Object.keys(classes).length === 0) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return { form: 'classes', classes };
|
|
168
|
+
}
|
|
169
|
+
function memberPatternsSelect(patterns, member) {
|
|
170
|
+
let selected = false;
|
|
171
|
+
for (const pattern of patterns) {
|
|
172
|
+
const matches = 'root' in pattern
|
|
173
|
+
? member.root
|
|
174
|
+
: typeof member.name === 'string' && member.name !== ''
|
|
175
|
+
&& specMatchesPackageName(pattern, member.name);
|
|
176
|
+
if (matches) {
|
|
177
|
+
selected = !pattern.exclude;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return selected;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Reduces a normalized prune to what applies to the given manifest:
|
|
184
|
+
* global patterns and matching member-scoped `remove` selections combine
|
|
185
|
+
* per class in listed order (so a later `!` exclusion subtracts from
|
|
186
|
+
* earlier entries' selections), unless a `keep` entry matches the member,
|
|
187
|
+
* in which case the keep selections govern alone. Returns `undefined`
|
|
188
|
+
* when nothing targets the manifest.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveManifestPrune(prune, member) {
|
|
191
|
+
if (prune.form === 'classes') {
|
|
192
|
+
return { mode: 'remove', classes: prune.classes };
|
|
193
|
+
}
|
|
194
|
+
const keeps = [];
|
|
195
|
+
for (const entry of prune.entries) {
|
|
196
|
+
if (entry.kind === 'keep' && memberPatternsSelect(entry.member, member)) {
|
|
197
|
+
keeps.push(entry.keep);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (keeps.length > 0) {
|
|
201
|
+
return { mode: 'keep', keeps };
|
|
202
|
+
}
|
|
203
|
+
const classes = {};
|
|
204
|
+
const append = (dependencyClass, patterns) => {
|
|
205
|
+
(classes[dependencyClass] ??= []).push(...patterns);
|
|
206
|
+
};
|
|
207
|
+
for (const entry of prune.entries) {
|
|
208
|
+
if (entry.kind === 'global') {
|
|
209
|
+
for (const dependencyClass of DEPENDENCY_CLASSES) {
|
|
210
|
+
append(dependencyClass, [entry.pattern]);
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (entry.kind === 'remove' && memberPatternsSelect(entry.member, member)) {
|
|
215
|
+
for (const dependencyClass of DEPENDENCY_CLASSES) {
|
|
216
|
+
const patterns = entry.remove[dependencyClass];
|
|
217
|
+
if (patterns !== undefined) {
|
|
218
|
+
append(dependencyClass, patterns);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (Object.keys(classes).length === 0) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
return { mode: 'remove', classes };
|
|
227
|
+
}
|
|
228
|
+
/** Formats a member pattern back to its config spelling. */
|
|
229
|
+
function formatMemberPattern(pattern) {
|
|
230
|
+
return `${pattern.exclude ? '!' : ''}${'root' in pattern ? '.' : pattern.name}`;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* How a member is named in warnings. Only the root can lack a `name` at
|
|
234
|
+
* runtime (see {@link PruneMemberIdentity.name}), so its token stands in.
|
|
235
|
+
*/
|
|
236
|
+
function displayMemberName(member) {
|
|
237
|
+
return member.name ?? '.';
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether the pattern contributes anything to the manifest's kept set:
|
|
241
|
+
* some entry in one of the given classes both matches the pattern and is
|
|
242
|
+
* ultimately retained — judged against the union of every keep entry
|
|
243
|
+
* matching the member, exactly what {@link applyPrune} keeps. Judged
|
|
244
|
+
* against retention rather than the raw match so a keep list whose later
|
|
245
|
+
* `!` exclusions cancel everything a pattern selected still reports
|
|
246
|
+
* (that silent gutting is exactly what this net exists to catch), while
|
|
247
|
+
* a name another matching keep entry rescues does not.
|
|
248
|
+
*/
|
|
249
|
+
function patternReaches(pattern, keeps, parsed, dependencyClasses) {
|
|
250
|
+
return dependencyClasses.some(dependencyClass => {
|
|
251
|
+
const section = parsed[dependencyClass];
|
|
252
|
+
return isPlainSection(section) && Object.keys(section).some(name => specMatchesPackageName(pattern, name) && keepsSelectName(keeps, dependencyClass, name));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Lints member-scoped prune entries. Two nets, both warnings rather than
|
|
257
|
+
* errors because a pattern reaching nothing is legal in every other
|
|
258
|
+
* `bundle.packages` position: a `member` selector matching no workspace
|
|
259
|
+
* member at all (typo net), and a `keep` pattern selecting nothing in a
|
|
260
|
+
* matched bundled member (the auto-enrollment net — a member swept in by
|
|
261
|
+
* a wildcard later almost never declares another member's keep set, so
|
|
262
|
+
* the gutting self-reports on the next run; a right name in the wrong
|
|
263
|
+
* class reports the same way). The typo net is judged against the whole
|
|
264
|
+
* discovered workspace, not the bundle: which members land in a bundle
|
|
265
|
+
* varies per run with the check filter (`checkly test --grep`, a single
|
|
266
|
+
* `checkly pw-test` config), and a selector for a member the current run
|
|
267
|
+
* merely did not bundle is not a typo — that state shows up in the debug
|
|
268
|
+
* reach log instead. The keep net runs on the bundled physical manifests
|
|
269
|
+
* only — faux shims carry no real dependency classes — and only on ones
|
|
270
|
+
* the caller could read, so a symlinked or unreadable manifest's own
|
|
271
|
+
* failure warns separately.
|
|
272
|
+
*/
|
|
273
|
+
export class MemberPruneDiagnostics {
|
|
274
|
+
/** One record per scoped entry, in entry order. */
|
|
275
|
+
#state = [];
|
|
276
|
+
/**
|
|
277
|
+
* Keep misses keyed by entry, place and pattern, accumulating the
|
|
278
|
+
* missing members: a shared keep list over a wildcard selector then
|
|
279
|
+
* warns once per pattern naming every member it missed, instead of
|
|
280
|
+
* bursting one line per member on every run.
|
|
281
|
+
*/
|
|
282
|
+
#keepMisses = new Map();
|
|
283
|
+
constructor(prune) {
|
|
284
|
+
if (prune?.form === 'entries') {
|
|
285
|
+
this.#state = prune.entries
|
|
286
|
+
.filter(entry => entry.kind !== 'global')
|
|
287
|
+
.map(entry => ({ entry, workspaceMatched: new Set(), bundledMatched: new Set() }));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Marks the scoped entries whose member selector selects this workspace
|
|
292
|
+
* member, bundled or not — the input of the typo net.
|
|
293
|
+
*/
|
|
294
|
+
observeWorkspaceMember(member) {
|
|
295
|
+
for (const state of this.#state) {
|
|
296
|
+
if (memberPatternsSelect(state.entry.member, member)) {
|
|
297
|
+
state.workspaceMatched.add(displayMemberName(member));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Marks the scoped entries whose member selector selects this bundled
|
|
303
|
+
* physical manifest — the input of the debug reach log.
|
|
304
|
+
*/
|
|
305
|
+
observeBundledMember(member) {
|
|
306
|
+
for (const state of this.#state) {
|
|
307
|
+
if (memberPatternsSelect(state.entry.member, member)) {
|
|
308
|
+
state.bundledMatched.add(displayMemberName(member));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Checks matching keep selections against the manifest's contents. */
|
|
313
|
+
observeManifestContent(member, content) {
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = JSON.parse(content);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!isPlainSection(parsed)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Reach is judged against the union of every matching keep entry —
|
|
325
|
+
// the member's actual retention — so an entry's pattern whose kept
|
|
326
|
+
// names another entry supplies does not misreport.
|
|
327
|
+
const matched = [];
|
|
328
|
+
const keeps = [];
|
|
329
|
+
for (const [index, state] of this.#state.entries()) {
|
|
330
|
+
if (state.entry.kind === 'keep' && memberPatternsSelect(state.entry.member, member)) {
|
|
331
|
+
matched.push({ index, entry: state.entry });
|
|
332
|
+
keeps.push(state.entry.keep);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const { index, entry } of matched) {
|
|
336
|
+
this.#checkKeepEntry(index, entry, keeps, member, parsed);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
#checkKeepEntry(entryIndex, entry, keeps, member, parsed) {
|
|
340
|
+
const memberName = displayMemberName(member);
|
|
341
|
+
const check = (patterns, classes, where) => {
|
|
342
|
+
for (const pattern of patterns) {
|
|
343
|
+
if (this.#exemptFromMatchCheck(pattern)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!patternReaches(pattern, keeps, parsed, classes)) {
|
|
347
|
+
const key = `${entryIndex}\u0000${where}\u0000${pattern.name}`;
|
|
348
|
+
let miss = this.#keepMisses.get(key);
|
|
349
|
+
if (miss === undefined) {
|
|
350
|
+
this.#keepMisses.set(key, miss = { pattern: pattern.name, where, members: [] });
|
|
351
|
+
}
|
|
352
|
+
if (!miss.members.includes(memberName)) {
|
|
353
|
+
miss.members.push(memberName);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
if (entry.flat) {
|
|
359
|
+
// A flat list fans out to every class identically, so its patterns
|
|
360
|
+
// are checked across all classes and warn once — a per-class report
|
|
361
|
+
// would repeat every miss four times.
|
|
362
|
+
check(entry.keep.dependencies ?? [], DEPENDENCY_CLASSES, 'any dependency class');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const dependencyClass of DEPENDENCY_CLASSES) {
|
|
366
|
+
check(entry.keep[dependencyClass] ?? [], [dependencyClass], dependencyClass);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Exclusions only subtract, so one deselecting nothing is benign — the
|
|
371
|
+
* same rule embed applies. The bare catch-all is exempt too: it is what
|
|
372
|
+
* `keep: { <class>: true }` desugars to, and "everything in an empty
|
|
373
|
+
* class" matching nothing is not a signal worth a warning.
|
|
374
|
+
*/
|
|
375
|
+
#exemptFromMatchCheck(pattern) {
|
|
376
|
+
return pattern.exclude || pattern.name === '**';
|
|
377
|
+
}
|
|
378
|
+
#selector(entry) {
|
|
379
|
+
return entry.member.map(formatMemberPattern).join(`', '`);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Logs each scoped entry's resolved member set — workspace-wide and
|
|
383
|
+
* within this run's bundle — mirroring embed's pattern reach logging.
|
|
384
|
+
* This is also where "the selector is fine, the member just is not in
|
|
385
|
+
* this bundle" is visible. A separate call rather than a side effect of
|
|
386
|
+
* {@link MemberPruneDiagnostics.warnings}: a caller that gates or drops
|
|
387
|
+
* the warnings must not silently lose the reach log, which is the tool
|
|
388
|
+
* for answering the very question the warnings raise.
|
|
389
|
+
*/
|
|
390
|
+
logMemberReach() {
|
|
391
|
+
for (const { entry, workspaceMatched, bundledMatched } of this.#state) {
|
|
392
|
+
debug(`Prune entry for member '${this.#selector(entry)}' matched workspace members:`
|
|
393
|
+
+ ` ${[...workspaceMatched].join(', ') || '(none)'};`
|
|
394
|
+
+ ` bundled: ${[...bundledMatched].join(', ') || '(none)'}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** The accumulated lint messages, without the `Warning:` prefix. */
|
|
398
|
+
warnings() {
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const { entry, workspaceMatched } of this.#state) {
|
|
401
|
+
if (workspaceMatched.size === 0) {
|
|
402
|
+
out.push(`bundle.packages.prune entry for member '${this.#selector(entry)}'`
|
|
403
|
+
+ ` matched no workspace member`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const { pattern, where, members } of this.#keepMisses.values()) {
|
|
407
|
+
const consequence = members.length > 1
|
|
408
|
+
? 'their bundled manifests do not keep it'
|
|
409
|
+
: 'its bundled manifest does not keep it';
|
|
410
|
+
out.push(`bundle.packages.prune keep pattern '${pattern}' matched nothing`
|
|
411
|
+
+ ` in ${where} of ${quotedList(members)}; ${consequence}`);
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
69
415
|
}
|
|
70
416
|
function isPlainSection(section) {
|
|
71
417
|
return section !== null && typeof section === 'object' && !Array.isArray(section);
|
|
72
418
|
}
|
|
419
|
+
/** Whether any of the matched keep selections keeps the given entry. */
|
|
420
|
+
function keepsSelectName(keeps, dependencyClass, name) {
|
|
421
|
+
return keeps.some(keep => {
|
|
422
|
+
const patterns = keep[dependencyClass];
|
|
423
|
+
return patterns !== undefined && patternsSelectName(patterns, name);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Deletes the class's entries the predicate selects, recording removals
|
|
428
|
+
* as `class:name` labels. Carries the structural rules shared by both
|
|
429
|
+
* prune modes: a removed peer takes its `peerDependenciesMeta` entry with
|
|
430
|
+
* it, and a section or meta object emptied by these removals is deleted —
|
|
431
|
+
* one that was already empty is none of this feature's business.
|
|
432
|
+
*/
|
|
433
|
+
function removeSelectedNames(parsed, dependencyClass, selects, removed) {
|
|
434
|
+
const section = parsed[dependencyClass];
|
|
435
|
+
if (!isPlainSection(section)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
let lost = false;
|
|
439
|
+
for (const name of Object.keys(section)) {
|
|
440
|
+
if (!selects(name)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
delete section[name];
|
|
444
|
+
removed.push(`${dependencyClass}:${name}`);
|
|
445
|
+
lost = true;
|
|
446
|
+
if (dependencyClass === 'peerDependencies') {
|
|
447
|
+
const meta = parsed.peerDependenciesMeta;
|
|
448
|
+
if (isPlainSection(meta)) {
|
|
449
|
+
delete meta[name];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (lost && Object.keys(section).length === 0) {
|
|
454
|
+
delete parsed[dependencyClass];
|
|
455
|
+
}
|
|
456
|
+
if (lost && dependencyClass === 'peerDependencies'
|
|
457
|
+
&& isPlainSection(parsed.peerDependenciesMeta)
|
|
458
|
+
&& Object.keys(parsed.peerDependenciesMeta).length === 0) {
|
|
459
|
+
delete parsed.peerDependenciesMeta;
|
|
460
|
+
}
|
|
461
|
+
return lost;
|
|
462
|
+
}
|
|
73
463
|
/**
|
|
74
464
|
* Deletes the pruned entries from a parsed manifest, recording removals as
|
|
75
465
|
* `class:name` labels. The verification below replays the same edit from
|
|
@@ -78,21 +468,27 @@ function isPlainSection(section) {
|
|
|
78
468
|
* not a plain object is never touched; `true` deletes the class (and, for
|
|
79
469
|
* `peerDependencies`, the whole `peerDependenciesMeta`); a removed peer
|
|
80
470
|
* takes its meta entry with it; and a section or meta object emptied by
|
|
81
|
-
* these removals is deleted.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
471
|
+
* these removals is deleted. In keep mode the selection inverts — an entry
|
|
472
|
+
* no matched keep selects is removed — under the same structural rules.
|
|
473
|
+
* `dependenciesMeta` is deliberately not followed: only the peer meta
|
|
474
|
+
* matters for the auto-install-peers promotion this feature exists to
|
|
475
|
+
* counter, and a dangling `dependenciesMeta` entry is inert for the
|
|
476
|
+
* regenerated lockfile.
|
|
85
477
|
*/
|
|
86
|
-
function applyPrune(parsed,
|
|
478
|
+
function applyPrune(parsed, resolved) {
|
|
87
479
|
const removed = [];
|
|
88
480
|
let changed = false;
|
|
89
481
|
for (const dependencyClass of DEPENDENCY_CLASSES) {
|
|
90
|
-
|
|
482
|
+
if (resolved.mode === 'keep') {
|
|
483
|
+
changed = removeSelectedNames(parsed, dependencyClass, name => !keepsSelectName(resolved.keeps, dependencyClass, name), removed) || changed;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const matcher = resolved.classes[dependencyClass];
|
|
91
487
|
if (matcher === undefined) {
|
|
92
488
|
continue;
|
|
93
489
|
}
|
|
94
|
-
const section = parsed[dependencyClass];
|
|
95
490
|
if (matcher === true) {
|
|
491
|
+
const section = parsed[dependencyClass];
|
|
96
492
|
if (isPlainSection(section)) {
|
|
97
493
|
for (const name of Object.keys(section)) {
|
|
98
494
|
removed.push(`${dependencyClass}:${name}`);
|
|
@@ -110,47 +506,19 @@ function applyPrune(parsed, prune) {
|
|
|
110
506
|
}
|
|
111
507
|
continue;
|
|
112
508
|
}
|
|
113
|
-
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
let lost = false;
|
|
117
|
-
for (const name of Object.keys(section)) {
|
|
118
|
-
if (!patternsSelectName(matcher, name)) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
delete section[name];
|
|
122
|
-
removed.push(`${dependencyClass}:${name}`);
|
|
123
|
-
changed = true;
|
|
124
|
-
lost = true;
|
|
125
|
-
if (dependencyClass === 'peerDependencies') {
|
|
126
|
-
const meta = parsed.peerDependenciesMeta;
|
|
127
|
-
if (isPlainSection(meta)) {
|
|
128
|
-
delete meta[name];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Only a section (or meta object) this pass emptied is dropped; one
|
|
133
|
-
// that was already empty is none of this feature's business.
|
|
134
|
-
if (lost && Object.keys(section).length === 0) {
|
|
135
|
-
delete parsed[dependencyClass];
|
|
136
|
-
}
|
|
137
|
-
if (lost && dependencyClass === 'peerDependencies'
|
|
138
|
-
&& isPlainSection(parsed.peerDependenciesMeta)
|
|
139
|
-
&& Object.keys(parsed.peerDependenciesMeta).length === 0) {
|
|
140
|
-
delete parsed.peerDependenciesMeta;
|
|
141
|
-
}
|
|
509
|
+
changed = removeSelectedNames(parsed, dependencyClass, name => patternsSelectName(matcher, name), removed) || changed;
|
|
142
510
|
}
|
|
143
511
|
return { removed, changed };
|
|
144
512
|
}
|
|
145
513
|
/**
|
|
146
|
-
* Applies a
|
|
147
|
-
* contents. Returns the rewritten content and the
|
|
148
|
-
* `undefined` when the content cannot be parsed or the
|
|
149
|
-
* verification — the caller ships the original in that case.
|
|
150
|
-
* changes nothing returns the original content with
|
|
151
|
-
* the caller can leave the bundle entry untouched.
|
|
514
|
+
* Applies a resolved per-manifest prune (see {@link resolveManifestPrune})
|
|
515
|
+
* to a package.json's contents. Returns the rewritten content and the
|
|
516
|
+
* removed entries, or `undefined` when the content cannot be parsed or the
|
|
517
|
+
* rewrite fails verification — the caller ships the original in that case.
|
|
518
|
+
* A prune that changes nothing returns the original content with
|
|
519
|
+
* `changed: false`, so the caller can leave the bundle entry untouched.
|
|
152
520
|
*/
|
|
153
|
-
export function prunePackageJson(content,
|
|
521
|
+
export function prunePackageJson(content, resolved) {
|
|
154
522
|
let parsed;
|
|
155
523
|
try {
|
|
156
524
|
parsed = JSON.parse(content);
|
|
@@ -163,7 +531,7 @@ export function prunePackageJson(content, prune) {
|
|
|
163
531
|
debug(`Refusing to prune a package.json that is not an object`);
|
|
164
532
|
return undefined;
|
|
165
533
|
}
|
|
166
|
-
const { removed, changed } = applyPrune(parsed,
|
|
534
|
+
const { removed, changed } = applyPrune(parsed, resolved);
|
|
167
535
|
if (!changed) {
|
|
168
536
|
return { content, removed, changed };
|
|
169
537
|
}
|
|
@@ -172,7 +540,7 @@ export function prunePackageJson(content, prune) {
|
|
|
172
540
|
// text — and matching the original's formatting would mean carrying a
|
|
173
541
|
// JSON editor for no behavioral gain.
|
|
174
542
|
const rewritten = JSON.stringify(parsed, null, 2);
|
|
175
|
-
if (verifyPrunedManifest(content, rewritten,
|
|
543
|
+
if (verifyPrunedManifest(content, rewritten, resolved, removed) === undefined) {
|
|
176
544
|
return undefined;
|
|
177
545
|
}
|
|
178
546
|
return { content: rewritten, removed, changed };
|
|
@@ -181,13 +549,16 @@ export function prunePackageJson(content, prune) {
|
|
|
181
549
|
* Asserts that a prune rewrite changed nothing but the reported entries,
|
|
182
550
|
* by replaying the deletion on a fresh parse of the original — from the
|
|
183
551
|
* `class:name` labels rather than the pattern matching, with each label
|
|
184
|
-
* additionally checked against the
|
|
552
|
+
* additionally checked against the resolved prune — so a matcher that
|
|
185
553
|
* deleted the wrong entry, deleted from a class the prune never
|
|
186
|
-
* configured,
|
|
187
|
-
* comparison instead of shipping. The
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
554
|
+
* configured, deleted a keep-selected entry, or strayed outside the
|
|
555
|
+
* dependency classes fails the comparison instead of shipping. The
|
|
556
|
+
* verification is inherently per manifest — its inputs are one manifest's
|
|
557
|
+
* original and rewrite — so the resolved model already carries any member
|
|
558
|
+
* scoping and the labels need no member qualifier. The structural rules
|
|
559
|
+
* (`true` deleting a class, emptied sections and meta dropping out) are
|
|
560
|
+
* mirrored from applyPrune rather than independently derived, so only the
|
|
561
|
+
* per-entry path carries independent evidence; a maintainer extending the
|
|
191
562
|
* structural rules must extend both sides. What the comparison cannot see
|
|
192
563
|
* is a field whose value is already altered by JSON.parse itself (an
|
|
193
564
|
* integer beyond 2^53 loses precision on both sides alike); asymmetric
|
|
@@ -196,7 +567,7 @@ export function prunePackageJson(content, prune) {
|
|
|
196
567
|
* fail-closed verifier in patched-dependencies.ts (`verifyRewrite`);
|
|
197
568
|
* hardening either against a manifest quirk likely applies to both.
|
|
198
569
|
*/
|
|
199
|
-
export function verifyPrunedManifest(original, rewritten,
|
|
570
|
+
export function verifyPrunedManifest(original, rewritten, resolved, removed) {
|
|
200
571
|
let expected;
|
|
201
572
|
let actual;
|
|
202
573
|
try {
|
|
@@ -207,18 +578,25 @@ export function verifyPrunedManifest(original, rewritten, prune, removed) {
|
|
|
207
578
|
debug(`Could not reparse a pruned package.json for verification: ${err}`);
|
|
208
579
|
return undefined;
|
|
209
580
|
}
|
|
581
|
+
const classTrue = (dependencyClass) => resolved.mode === 'remove' && resolved.classes[dependencyClass] === true;
|
|
582
|
+
// Whether the resolved prune selects the entry for removal. Only called
|
|
583
|
+
// with the known dependency classes — the caller checks membership
|
|
584
|
+
// first, so a label like `__proto__:x` fails closed instead of reading
|
|
585
|
+
// a matcher off Object.prototype.
|
|
586
|
+
const selectsRemoval = (dependencyClass, name) => {
|
|
587
|
+
if (resolved.mode === 'keep') {
|
|
588
|
+
return !keepsSelectName(resolved.keeps, dependencyClass, name);
|
|
589
|
+
}
|
|
590
|
+
const matcher = resolved.classes[dependencyClass];
|
|
591
|
+
return matcher !== undefined && (matcher === true || patternsSelectName(matcher, name));
|
|
592
|
+
};
|
|
210
593
|
// Every reported removal must be one the configuration allows: its class
|
|
211
|
-
// a known dependency class
|
|
212
|
-
// by `true` or a configured pattern. The class-membership check comes
|
|
213
|
-
// first so a label like `__proto__:x` fails closed instead of reading a
|
|
214
|
-
// matcher off Object.prototype.
|
|
594
|
+
// a known dependency class, and its name one the resolved prune selects.
|
|
215
595
|
for (const label of removed) {
|
|
216
596
|
const separator = label.indexOf(':');
|
|
217
597
|
const dependencyClass = label.slice(0, separator);
|
|
218
598
|
const name = label.slice(separator + 1);
|
|
219
|
-
|
|
220
|
-
if (matcher === undefined
|
|
221
|
-
|| (matcher !== true && !patternsSelectName(matcher, name))) {
|
|
599
|
+
if (!DEPENDENCY_CLASSES.includes(dependencyClass) || !selectsRemoval(dependencyClass, name)) {
|
|
222
600
|
debug(`A pruned package.json removed '${label}', which the configuration does not select;`
|
|
223
601
|
+ ` leaving the bundle alone`);
|
|
224
602
|
return undefined;
|
|
@@ -238,7 +616,7 @@ export function verifyPrunedManifest(original, rewritten, prune, removed) {
|
|
|
238
616
|
}
|
|
239
617
|
delete section[name];
|
|
240
618
|
lostClasses.add(dependencyClass);
|
|
241
|
-
if (dependencyClass === 'peerDependencies' &&
|
|
619
|
+
if (dependencyClass === 'peerDependencies' && !classTrue('peerDependencies')) {
|
|
242
620
|
lostPeers = true;
|
|
243
621
|
const meta = expected.peerDependenciesMeta;
|
|
244
622
|
if (isPlainSection(meta)) {
|
|
@@ -247,7 +625,7 @@ export function verifyPrunedManifest(original, rewritten, prune, removed) {
|
|
|
247
625
|
}
|
|
248
626
|
}
|
|
249
627
|
for (const dependencyClass of DEPENDENCY_CLASSES) {
|
|
250
|
-
if (
|
|
628
|
+
if (classTrue(dependencyClass)) {
|
|
251
629
|
if (isPlainSection(expected[dependencyClass])) {
|
|
252
630
|
delete expected[dependencyClass];
|
|
253
631
|
}
|