@intentius/chant 0.44.14 → 0.45.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/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +11 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +57 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +144 -5
- package/src/cli/commands/build.ts +66 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +19 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +1 -1
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +22 -2
- package/src/cli/handlers/lifecycle.test.ts +117 -0
- package/src/cli/handlers/lifecycle.ts +16 -0
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +55 -6
- package/src/cli/main.ts +5 -3
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +3 -1
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +16 -3
- package/src/codegen/validate.ts +17 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.ts +101 -7
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/policy.ts +14 -3
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/ownership.test.ts +63 -1
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -72,8 +72,8 @@ export interface RemovedEntry {
|
|
|
72
72
|
entry: SurfaceEntry;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
/** Structural differences between two versions of one entry. */
|
|
76
|
+
export interface EntryChanges {
|
|
77
77
|
/** New resourceType (breaking). */
|
|
78
78
|
resourceTypeChanged?: { before: string; after: string };
|
|
79
79
|
/** kind changed from resource to property or vice-versa (breaking). */
|
|
@@ -96,14 +96,40 @@ export interface ChangedEntry {
|
|
|
96
96
|
taggableChanged?: { before?: boolean; after?: boolean };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export interface ChangedEntry extends EntryChanges {
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An entry whose TS export name changed while its kind and resourceType
|
|
105
|
+
* stayed the same (#1460). Paired from one removal and one addition.
|
|
106
|
+
*/
|
|
107
|
+
export interface RenamedEntry {
|
|
108
|
+
/** TS export name in the baseline. */
|
|
109
|
+
from: string;
|
|
110
|
+
/** TS export name in the fresh surface. */
|
|
111
|
+
to: string;
|
|
112
|
+
/** The entry as it now appears. */
|
|
113
|
+
entry: SurfaceEntry;
|
|
114
|
+
/** Structural differences between the two, when any. */
|
|
115
|
+
changes?: EntryChanges;
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
export interface SurfaceDelta {
|
|
100
119
|
added: AddedEntry[];
|
|
101
120
|
changed: ChangedEntry[];
|
|
102
121
|
removed: RemovedEntry[];
|
|
122
|
+
/**
|
|
123
|
+
* Removals and additions paired by kind + resourceType. A rename still
|
|
124
|
+
* breaks the old import, so it counts as breaking; it is reported apart
|
|
125
|
+
* from removals so a reader can tell the two kinds of break apart.
|
|
126
|
+
*/
|
|
127
|
+
renamed: RenamedEntry[];
|
|
103
128
|
/**
|
|
104
129
|
* Rolled-up severity. "none" means no surface changes at all.
|
|
105
130
|
* "additive" means only new resources / new optional props / new attrs.
|
|
106
|
-
* "breaking" means any removal or change that could break
|
|
131
|
+
* "breaking" means any removal, rename, or change that could break
|
|
132
|
+
* existing consumers.
|
|
107
133
|
*/
|
|
108
134
|
severity: ChangeSeverity;
|
|
109
135
|
}
|
|
@@ -314,9 +340,9 @@ export function extractPropsFromDts(dts: string): Map<string, string[]> {
|
|
|
314
340
|
* - taggable: false→true or undefined→true → additive
|
|
315
341
|
*/
|
|
316
342
|
export function diffSurface(baseline: SurfaceSnapshot, fresh: SurfaceSnapshot): SurfaceDelta {
|
|
317
|
-
const added: AddedEntry[] = [];
|
|
318
343
|
const changed: ChangedEntry[] = [];
|
|
319
|
-
|
|
344
|
+
let added: AddedEntry[] = [];
|
|
345
|
+
let removed: RemovedEntry[] = [];
|
|
320
346
|
|
|
321
347
|
const baseNames = new Set(Object.keys(baseline.entries));
|
|
322
348
|
const freshNames = new Set(Object.keys(fresh.entries));
|
|
@@ -335,119 +361,253 @@ export function diffSurface(baseline: SurfaceSnapshot, fresh: SurfaceSnapshot):
|
|
|
335
361
|
}
|
|
336
362
|
}
|
|
337
363
|
|
|
364
|
+
// Renamed (#1460): a removal and an addition that carry the same
|
|
365
|
+
// resourceType and kind are one entry under a new TS name. Pair them
|
|
366
|
+
// before classifying so the report shows what upstream actually did.
|
|
367
|
+
const renamed: RenamedEntry[] = [];
|
|
368
|
+
const paired = pairRenames(removed, added);
|
|
369
|
+
for (const { from, to } of paired) {
|
|
370
|
+
const entry: RenamedEntry = { from: from.name, to: to.name, entry: to.entry };
|
|
371
|
+
const changes = diffEntries(from.entry, to.entry);
|
|
372
|
+
if (changes) entry.changes = changes;
|
|
373
|
+
renamed.push(entry);
|
|
374
|
+
}
|
|
375
|
+
if (paired.length > 0) {
|
|
376
|
+
const fromNames = new Set(paired.map((p) => p.from.name));
|
|
377
|
+
const toNames = new Set(paired.map((p) => p.to.name));
|
|
378
|
+
removed = removed.filter((r) => !fromNames.has(r.name));
|
|
379
|
+
added = added.filter((a) => !toNames.has(a.name));
|
|
380
|
+
}
|
|
381
|
+
renamed.sort((a, b) => a.from.localeCompare(b.from));
|
|
382
|
+
|
|
338
383
|
// Changed
|
|
339
384
|
for (const name of baseNames) {
|
|
340
385
|
if (!freshNames.has(name)) continue;
|
|
386
|
+
const changes = diffEntries(baseline.entries[name], fresh.entries[name]);
|
|
387
|
+
if (changes) changed.push({ name, ...changes });
|
|
388
|
+
}
|
|
341
389
|
|
|
342
|
-
|
|
343
|
-
|
|
390
|
+
// Severity
|
|
391
|
+
let severity: ChangeSeverity = "none";
|
|
344
392
|
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
if (added.length > 0) {
|
|
394
|
+
severity = "additive";
|
|
395
|
+
}
|
|
347
396
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
hasChange = true;
|
|
351
|
-
}
|
|
397
|
+
// Additive-only changes in the changed[] set
|
|
398
|
+
const hasAdditiveChanges = changed.some(isAdditiveChange);
|
|
352
399
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
400
|
+
if (hasAdditiveChanges && severity === "none") {
|
|
401
|
+
severity = "additive";
|
|
402
|
+
}
|
|
357
403
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
365
|
-
const afterProps = new Map(
|
|
366
|
-
(after.props ?? []).map((p) => {
|
|
367
|
-
const [nm, req] = splitProp(p);
|
|
368
|
-
return [nm, req === "true"];
|
|
369
|
-
}),
|
|
370
|
-
);
|
|
404
|
+
// A rename is still breaking for the consumer: the old import no longer
|
|
405
|
+
// resolves, whether the entry is a resource or a property type. It is
|
|
406
|
+
// reported separately so the reader can tell a rename from a deletion.
|
|
407
|
+
const hasBreaking =
|
|
408
|
+
removed.length > 0 ||
|
|
409
|
+
renamed.length > 0 ||
|
|
410
|
+
changed.some(isBreakingChange);
|
|
371
411
|
|
|
372
|
-
|
|
373
|
-
const addedProps = [...afterProps.keys()].filter((k) => !beforeProps.has(k));
|
|
374
|
-
const nowRequired = [...beforeProps.keys()].filter(
|
|
375
|
-
(k) => afterProps.has(k) && !beforeProps.get(k) && afterProps.get(k),
|
|
376
|
-
);
|
|
377
|
-
const nowOptional = [...beforeProps.keys()].filter(
|
|
378
|
-
(k) => afterProps.has(k) && beforeProps.get(k) && !afterProps.get(k),
|
|
379
|
-
);
|
|
412
|
+
if (hasBreaking) severity = "breaking";
|
|
380
413
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (nowRequired.length > 0) { change.nowRequired = nowRequired.sort(); hasChange = true; }
|
|
384
|
-
if (nowOptional.length > 0) { change.nowOptional = nowOptional.sort(); hasChange = true; }
|
|
385
|
-
|
|
386
|
-
// Attrs diff
|
|
387
|
-
const beforeAttrs = new Set(before.attrs ?? []);
|
|
388
|
-
const afterAttrs = new Set(after.attrs ?? []);
|
|
389
|
-
const removedAttrs = [...beforeAttrs].filter((a) => !afterAttrs.has(a));
|
|
390
|
-
const addedAttrs = [...afterAttrs].filter((a) => !beforeAttrs.has(a));
|
|
391
|
-
if (removedAttrs.length > 0) { change.removedAttrs = removedAttrs.sort(); hasChange = true; }
|
|
392
|
-
if (addedAttrs.length > 0) { change.addedAttrs = addedAttrs.sort(); hasChange = true; }
|
|
393
|
-
|
|
394
|
-
// createOnly diff
|
|
395
|
-
const beforeCO = JSON.stringify((before.createOnly ?? []).sort());
|
|
396
|
-
const afterCO = JSON.stringify((after.createOnly ?? []).sort());
|
|
397
|
-
if (beforeCO !== afterCO) {
|
|
398
|
-
change.createOnlyChanged = {
|
|
399
|
-
before: before.createOnly ?? [],
|
|
400
|
-
after: after.createOnly ?? [],
|
|
401
|
-
};
|
|
402
|
-
hasChange = true;
|
|
403
|
-
}
|
|
414
|
+
return { added, changed, removed, renamed, severity };
|
|
415
|
+
}
|
|
404
416
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Diff two entries that are (or are taken to be) the same thing. Returns
|
|
419
|
+
* `null` when nothing differs.
|
|
420
|
+
*/
|
|
421
|
+
function diffEntries(before: SurfaceEntry, after: SurfaceEntry): EntryChanges | null {
|
|
422
|
+
const change: EntryChanges = {};
|
|
423
|
+
let hasChange = false;
|
|
410
424
|
|
|
411
|
-
|
|
425
|
+
if (before.resourceType !== after.resourceType) {
|
|
426
|
+
change.resourceTypeChanged = { before: before.resourceType, after: after.resourceType };
|
|
427
|
+
hasChange = true;
|
|
412
428
|
}
|
|
413
429
|
|
|
414
|
-
|
|
415
|
-
|
|
430
|
+
if (before.kind !== after.kind) {
|
|
431
|
+
change.kindChanged = { before: before.kind, after: after.kind };
|
|
432
|
+
hasChange = true;
|
|
433
|
+
}
|
|
416
434
|
|
|
417
|
-
|
|
418
|
-
|
|
435
|
+
// Props diff (only meaningful for resource entries)
|
|
436
|
+
const beforeProps = new Map(
|
|
437
|
+
(before.props ?? []).map((p) => {
|
|
438
|
+
const [nm, req] = splitProp(p);
|
|
439
|
+
return [nm, req === "true"];
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
const afterProps = new Map(
|
|
443
|
+
(after.props ?? []).map((p) => {
|
|
444
|
+
const [nm, req] = splitProp(p);
|
|
445
|
+
return [nm, req === "true"];
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const removedProps = [...beforeProps.keys()].filter((k) => !afterProps.has(k));
|
|
450
|
+
const addedProps = [...afterProps.keys()].filter((k) => !beforeProps.has(k));
|
|
451
|
+
const nowRequired = [...beforeProps.keys()].filter(
|
|
452
|
+
(k) => afterProps.has(k) && !beforeProps.get(k) && afterProps.get(k),
|
|
453
|
+
);
|
|
454
|
+
const nowOptional = [...beforeProps.keys()].filter(
|
|
455
|
+
(k) => afterProps.has(k) && beforeProps.get(k) && !afterProps.get(k),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
if (removedProps.length > 0) { change.removedProps = removedProps.sort(); hasChange = true; }
|
|
459
|
+
if (addedProps.length > 0) { change.addedProps = addedProps.sort(); hasChange = true; }
|
|
460
|
+
if (nowRequired.length > 0) { change.nowRequired = nowRequired.sort(); hasChange = true; }
|
|
461
|
+
if (nowOptional.length > 0) { change.nowOptional = nowOptional.sort(); hasChange = true; }
|
|
462
|
+
|
|
463
|
+
// Attrs diff
|
|
464
|
+
const beforeAttrs = new Set(before.attrs ?? []);
|
|
465
|
+
const afterAttrs = new Set(after.attrs ?? []);
|
|
466
|
+
const removedAttrs = [...beforeAttrs].filter((a) => !afterAttrs.has(a));
|
|
467
|
+
const addedAttrs = [...afterAttrs].filter((a) => !beforeAttrs.has(a));
|
|
468
|
+
if (removedAttrs.length > 0) { change.removedAttrs = removedAttrs.sort(); hasChange = true; }
|
|
469
|
+
if (addedAttrs.length > 0) { change.addedAttrs = addedAttrs.sort(); hasChange = true; }
|
|
470
|
+
|
|
471
|
+
// createOnly diff
|
|
472
|
+
const beforeCO = JSON.stringify((before.createOnly ?? []).sort());
|
|
473
|
+
const afterCO = JSON.stringify((after.createOnly ?? []).sort());
|
|
474
|
+
if (beforeCO !== afterCO) {
|
|
475
|
+
change.createOnlyChanged = {
|
|
476
|
+
before: before.createOnly ?? [],
|
|
477
|
+
after: after.createOnly ?? [],
|
|
478
|
+
};
|
|
479
|
+
hasChange = true;
|
|
419
480
|
}
|
|
420
481
|
|
|
421
|
-
//
|
|
422
|
-
|
|
482
|
+
// taggable diff
|
|
483
|
+
if (before.taggable !== after.taggable) {
|
|
484
|
+
change.taggableChanged = { before: before.taggable, after: after.taggable };
|
|
485
|
+
hasChange = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return hasChange ? change : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isAdditiveChange(c: EntryChanges): boolean {
|
|
492
|
+
return (
|
|
423
493
|
(c.addedProps?.length ?? 0) > 0 ||
|
|
424
494
|
(c.nowOptional?.length ?? 0) > 0 ||
|
|
425
495
|
(c.addedAttrs?.length ?? 0) > 0 ||
|
|
426
496
|
// gaining tagging is additive
|
|
427
|
-
(c.taggableChanged !== undefined && c.taggableChanged.after === true && c.taggableChanged.before !== true)
|
|
497
|
+
(c.taggableChanged !== undefined && c.taggableChanged.after === true && c.taggableChanged.before !== true)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isBreakingChange(c: EntryChanges): boolean {
|
|
502
|
+
return (
|
|
503
|
+
c.resourceTypeChanged !== undefined ||
|
|
504
|
+
c.kindChanged !== undefined ||
|
|
505
|
+
(c.removedProps?.length ?? 0) > 0 ||
|
|
506
|
+
(c.nowRequired?.length ?? 0) > 0 ||
|
|
507
|
+
(c.removedAttrs?.length ?? 0) > 0 ||
|
|
508
|
+
c.createOnlyChanged !== undefined ||
|
|
509
|
+
// losing taggable is breaking
|
|
510
|
+
(c.taggableChanged !== undefined && c.taggableChanged.before === true && c.taggableChanged.after !== true)
|
|
428
511
|
);
|
|
512
|
+
}
|
|
429
513
|
|
|
430
|
-
|
|
431
|
-
|
|
514
|
+
// ── Rename pairing ───────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
interface RenamePair {
|
|
517
|
+
from: RemovedEntry;
|
|
518
|
+
to: AddedEntry;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Pair removed and added entries that share a kind and resourceType.
|
|
523
|
+
*
|
|
524
|
+
* One removal and one addition under a resourceType pair directly. When a
|
|
525
|
+
* resourceType has several of each (property types of one renamed parent
|
|
526
|
+
* resource, for instance), pairs are chosen greedily by name similarity and
|
|
527
|
+
* whatever is left over stays a plain removal or addition. Entries with an
|
|
528
|
+
* empty resourceType are never paired; there is nothing to key them on.
|
|
529
|
+
*/
|
|
530
|
+
function pairRenames(removed: RemovedEntry[], added: AddedEntry[]): RenamePair[] {
|
|
531
|
+
const key = (e: SurfaceEntry): string => `${e.kind} ${e.resourceType}`;
|
|
532
|
+
|
|
533
|
+
const addedByKey = new Map<string, AddedEntry[]>();
|
|
534
|
+
for (const a of added) {
|
|
535
|
+
if (!a.entry.resourceType) continue;
|
|
536
|
+
const k = key(a.entry);
|
|
537
|
+
const bucket = addedByKey.get(k);
|
|
538
|
+
if (bucket) bucket.push(a);
|
|
539
|
+
else addedByKey.set(k, [a]);
|
|
432
540
|
}
|
|
433
541
|
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
542
|
+
const removedByKey = new Map<string, RemovedEntry[]>();
|
|
543
|
+
for (const r of removed) {
|
|
544
|
+
if (!r.entry.resourceType) continue;
|
|
545
|
+
const k = key(r.entry);
|
|
546
|
+
if (!addedByKey.has(k)) continue;
|
|
547
|
+
const bucket = removedByKey.get(k);
|
|
548
|
+
if (bucket) bucket.push(r);
|
|
549
|
+
else removedByKey.set(k, [r]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const pairs: RenamePair[] = [];
|
|
553
|
+
for (const [k, olds] of removedByKey) {
|
|
554
|
+
const news = addedByKey.get(k) ?? [];
|
|
555
|
+
if (olds.length === 1 && news.length === 1) {
|
|
556
|
+
pairs.push({ from: olds[0], to: news[0] });
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Ambiguous: rank every (old, new) combination by similarity and take
|
|
561
|
+
// the best remaining pair until one side runs out.
|
|
562
|
+
const candidates: Array<{ from: RemovedEntry; to: AddedEntry; score: number }> = [];
|
|
563
|
+
for (const from of olds) {
|
|
564
|
+
for (const to of news) {
|
|
565
|
+
candidates.push({ from, to, score: nameSimilarity(from.name, to.name) });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
candidates.sort((a, b) =>
|
|
569
|
+
b.score - a.score ||
|
|
570
|
+
a.from.name.localeCompare(b.from.name) ||
|
|
571
|
+
a.to.name.localeCompare(b.to.name),
|
|
446
572
|
);
|
|
573
|
+
const usedOld = new Set<string>();
|
|
574
|
+
const usedNew = new Set<string>();
|
|
575
|
+
for (const cand of candidates) {
|
|
576
|
+
if (usedOld.has(cand.from.name) || usedNew.has(cand.to.name)) continue;
|
|
577
|
+
usedOld.add(cand.from.name);
|
|
578
|
+
usedNew.add(cand.to.name);
|
|
579
|
+
pairs.push({ from: cand.from, to: cand.to });
|
|
580
|
+
if (usedOld.size === olds.length || usedNew.size === news.length) break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
447
583
|
|
|
448
|
-
|
|
584
|
+
return pairs;
|
|
585
|
+
}
|
|
449
586
|
|
|
450
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Dice similarity over character bigrams, in [0, 1]. Enough to tell
|
|
589
|
+
* `Space` apart from `Space_Tag` when pairing the property types of a
|
|
590
|
+
* renamed parent; nothing heavier is needed.
|
|
591
|
+
*/
|
|
592
|
+
function nameSimilarity(a: string, b: string): number {
|
|
593
|
+
if (a === b) return 1;
|
|
594
|
+
if (a.length < 2 || b.length < 2) return 0;
|
|
595
|
+
const bigrams = (s: string): Map<string, number> => {
|
|
596
|
+
const m = new Map<string, number>();
|
|
597
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
598
|
+
const bg = s.slice(i, i + 2);
|
|
599
|
+
m.set(bg, (m.get(bg) ?? 0) + 1);
|
|
600
|
+
}
|
|
601
|
+
return m;
|
|
602
|
+
};
|
|
603
|
+
const ba = bigrams(a);
|
|
604
|
+
const bb = bigrams(b);
|
|
605
|
+
let overlap = 0;
|
|
606
|
+
for (const [bg, n] of ba) {
|
|
607
|
+
const m = bb.get(bg);
|
|
608
|
+
if (m) overlap += Math.min(n, m);
|
|
609
|
+
}
|
|
610
|
+
return (2 * overlap) / (a.length - 1 + b.length - 1);
|
|
451
611
|
}
|
|
452
612
|
|
|
453
613
|
// ── Serialization ────────────────────────────────────────────────────
|
|
@@ -489,42 +649,19 @@ export function formatDelta(delta: SurfaceDelta): string {
|
|
|
489
649
|
}
|
|
490
650
|
}
|
|
491
651
|
|
|
652
|
+
if (delta.renamed.length > 0) {
|
|
653
|
+
lines.push(`Renamed (${delta.renamed.length}):`);
|
|
654
|
+
for (const r of delta.renamed) {
|
|
655
|
+
lines.push(` ~ ${r.from} -> ${r.to} [${r.entry.kind}] (${r.entry.resourceType})`);
|
|
656
|
+
if (r.changes) lines.push(...formatChanges(r.changes));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
492
660
|
if (delta.changed.length > 0) {
|
|
493
661
|
lines.push(`Changed (${delta.changed.length}):`);
|
|
494
662
|
for (const c of delta.changed) {
|
|
495
663
|
lines.push(` ~ ${c.name}`);
|
|
496
|
-
|
|
497
|
-
lines.push(` resourceType: ${c.resourceTypeChanged.before} -> ${c.resourceTypeChanged.after}`);
|
|
498
|
-
}
|
|
499
|
-
if (c.kindChanged) {
|
|
500
|
-
lines.push(` kind: ${c.kindChanged.before} -> ${c.kindChanged.after}`);
|
|
501
|
-
}
|
|
502
|
-
if (c.removedProps?.length) {
|
|
503
|
-
lines.push(` removed props: ${c.removedProps.join(", ")}`);
|
|
504
|
-
}
|
|
505
|
-
if (c.nowRequired?.length) {
|
|
506
|
-
lines.push(` now required: ${c.nowRequired.join(", ")}`);
|
|
507
|
-
}
|
|
508
|
-
if (c.addedProps?.length) {
|
|
509
|
-
lines.push(` added props: ${c.addedProps.join(", ")}`);
|
|
510
|
-
}
|
|
511
|
-
if (c.nowOptional?.length) {
|
|
512
|
-
lines.push(` now optional: ${c.nowOptional.join(", ")}`);
|
|
513
|
-
}
|
|
514
|
-
if (c.removedAttrs?.length) {
|
|
515
|
-
lines.push(` removed attrs: ${c.removedAttrs.join(", ")}`);
|
|
516
|
-
}
|
|
517
|
-
if (c.addedAttrs?.length) {
|
|
518
|
-
lines.push(` added attrs: ${c.addedAttrs.join(", ")}`);
|
|
519
|
-
}
|
|
520
|
-
if (c.createOnlyChanged) {
|
|
521
|
-
const before = c.createOnlyChanged.before.join(", ") || "(none)";
|
|
522
|
-
const after = c.createOnlyChanged.after.join(", ") || "(none)";
|
|
523
|
-
lines.push(` createOnly: [${before}] -> [${after}]`);
|
|
524
|
-
}
|
|
525
|
-
if (c.taggableChanged) {
|
|
526
|
-
lines.push(` taggable: ${c.taggableChanged.before ?? "(unset)"} -> ${c.taggableChanged.after ?? "(unset)"}`);
|
|
527
|
-
}
|
|
664
|
+
lines.push(...formatChanges(c));
|
|
528
665
|
}
|
|
529
666
|
}
|
|
530
667
|
|
|
@@ -538,6 +675,44 @@ export function formatDelta(delta: SurfaceDelta): string {
|
|
|
538
675
|
return lines.join("\n");
|
|
539
676
|
}
|
|
540
677
|
|
|
678
|
+
/** Indented detail lines for one entry's structural changes. */
|
|
679
|
+
function formatChanges(c: EntryChanges): string[] {
|
|
680
|
+
const lines: string[] = [];
|
|
681
|
+
if (c.resourceTypeChanged) {
|
|
682
|
+
lines.push(` resourceType: ${c.resourceTypeChanged.before} -> ${c.resourceTypeChanged.after}`);
|
|
683
|
+
}
|
|
684
|
+
if (c.kindChanged) {
|
|
685
|
+
lines.push(` kind: ${c.kindChanged.before} -> ${c.kindChanged.after}`);
|
|
686
|
+
}
|
|
687
|
+
if (c.removedProps?.length) {
|
|
688
|
+
lines.push(` removed props: ${c.removedProps.join(", ")}`);
|
|
689
|
+
}
|
|
690
|
+
if (c.nowRequired?.length) {
|
|
691
|
+
lines.push(` now required: ${c.nowRequired.join(", ")}`);
|
|
692
|
+
}
|
|
693
|
+
if (c.addedProps?.length) {
|
|
694
|
+
lines.push(` added props: ${c.addedProps.join(", ")}`);
|
|
695
|
+
}
|
|
696
|
+
if (c.nowOptional?.length) {
|
|
697
|
+
lines.push(` now optional: ${c.nowOptional.join(", ")}`);
|
|
698
|
+
}
|
|
699
|
+
if (c.removedAttrs?.length) {
|
|
700
|
+
lines.push(` removed attrs: ${c.removedAttrs.join(", ")}`);
|
|
701
|
+
}
|
|
702
|
+
if (c.addedAttrs?.length) {
|
|
703
|
+
lines.push(` added attrs: ${c.addedAttrs.join(", ")}`);
|
|
704
|
+
}
|
|
705
|
+
if (c.createOnlyChanged) {
|
|
706
|
+
const before = c.createOnlyChanged.before.join(", ") || "(none)";
|
|
707
|
+
const after = c.createOnlyChanged.after.join(", ") || "(none)";
|
|
708
|
+
lines.push(` createOnly: [${before}] -> [${after}]`);
|
|
709
|
+
}
|
|
710
|
+
if (c.taggableChanged) {
|
|
711
|
+
lines.push(` taggable: ${c.taggableChanged.before ?? "(unset)"} -> ${c.taggableChanged.after ?? "(unset)"}`);
|
|
712
|
+
}
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
715
|
+
|
|
541
716
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
542
717
|
|
|
543
718
|
function splitProp(encoded: string): [string, string] {
|
|
@@ -106,7 +106,7 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
106
106
|
return dir;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const run = (basePath: string, checkSurfaceSnapshot: boolean, armed = true) =>
|
|
109
|
+
const run = (basePath: string, checkSurfaceSnapshot: boolean | "always", armed = true) =>
|
|
110
110
|
validateLexiconArtifacts({
|
|
111
111
|
lexiconJsonFilename: "lexicon-test.json",
|
|
112
112
|
requiredNames: [],
|
|
@@ -141,8 +141,8 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("is off unless the lexicon opts in", async () => {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
144
|
+
// A lexicon with no baseline of its own, or one that generates from a
|
|
145
|
+
// moving upstream it has not pinned, must not be gated by default.
|
|
146
146
|
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
147
147
|
const result = await run(fixture({ snapshot: stale }), false);
|
|
148
148
|
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
@@ -158,6 +158,19 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
158
158
|
expect(result.success).toBe(true);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
test("\"always\" runs outside a release — a pinned upstream cannot move on its own (#1475)", async () => {
|
|
162
|
+
// k8s and azure generate from immutable refs, so drift on a PR can only
|
|
163
|
+
// come from this repo and is exactly what should fail the build.
|
|
164
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
165
|
+
const result = await run(fixture({ snapshot: stale }), "always", false);
|
|
166
|
+
const check = result.checks.find((c) => c.name === "surface-matches-snapshot");
|
|
167
|
+
expect(check?.ok).toBe(false);
|
|
168
|
+
expect(result.success).toBe(false);
|
|
169
|
+
|
|
170
|
+
const ok = await run(fixture({ snapshot: await matchingSnapshot() }), "always", false);
|
|
171
|
+
expect(ok.checks.find((c) => c.name === "surface-matches-snapshot")?.ok).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
161
174
|
test("is skipped for a lexicon with no committed snapshot", async () => {
|
|
162
175
|
// A new lexicon before its first baseline must still be able to build.
|
|
163
176
|
const result = await run(fixture(), true);
|
package/src/codegen/validate.ts
CHANGED
|
@@ -62,10 +62,18 @@ export interface LexiconValidationConfig {
|
|
|
62
62
|
* What must never happen is *publishing* a surface nobody reviewed. That is
|
|
63
63
|
* a release-time property, so it is checked at release time.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* `"always"` drops the env half (#1475). It is for a lexicon whose upstream
|
|
66
|
+
* is pinned to an immutable ref — k8s generates from a kubernetes release
|
|
67
|
+
* tag plus vendored CRDs, azure from a commit sha of the
|
|
68
|
+
* resource-manager-schemas repo — so a fresh `generate` on a PR is
|
|
69
|
+
* deterministic and the only way the surface can move is a change in this
|
|
70
|
+
* repo. For those, drift on a PR is exactly the thing to fail on: the CRD
|
|
71
|
+
* batches #1319/#1320/#1321 left the k8s baseline 393 entries behind, and
|
|
72
|
+
* the #1144 pin left azure 483 behind, because nothing compared the two
|
|
73
|
+
* until a release was attempted. Never use `"always"` for a lexicon that
|
|
74
|
+
* fetches a moving upstream.
|
|
67
75
|
*/
|
|
68
|
-
checkSurfaceSnapshot?: boolean;
|
|
76
|
+
checkSurfaceSnapshot?: boolean | "always";
|
|
69
77
|
/** Environment to read {@link RELEASE_GATE_ENV} from. Defaults to `process.env`; overridden in tests. */
|
|
70
78
|
env?: NodeJS.ProcessEnv;
|
|
71
79
|
/** Path to the generated directory (defaults to basePath/src/generated) */
|
|
@@ -194,12 +202,14 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
194
202
|
// skipped for a lexicon with no committed snapshot, which is the case for a
|
|
195
203
|
// new lexicon before its first baseline.
|
|
196
204
|
const snapshotPath = join(config.basePath, "surface.snapshot.json");
|
|
197
|
-
const
|
|
198
|
-
|
|
205
|
+
const surfaceGate =
|
|
206
|
+
config.checkSurfaceSnapshot === "always" ||
|
|
207
|
+
(config.checkSurfaceSnapshot === true && (config.env ?? process.env)[RELEASE_GATE_ENV] === "1");
|
|
208
|
+
if (surfaceGate && lexiconData && existsSync(snapshotPath) && existsSync(dtsPath)) {
|
|
199
209
|
try {
|
|
200
210
|
const fresh = extractSurface(readFileSync(lexiconPath, "utf-8"), readFileSync(dtsPath, "utf-8"));
|
|
201
211
|
const delta = diffSurface(parseSnapshot(readFileSync(snapshotPath, "utf-8")), fresh);
|
|
202
|
-
const moved = delta.added.length + delta.removed.length + delta.changed.length;
|
|
212
|
+
const moved = delta.added.length + delta.removed.length + delta.renamed.length + delta.changed.length;
|
|
203
213
|
checks.push(
|
|
204
214
|
moved === 0
|
|
205
215
|
? { name: "surface-matches-snapshot", ok: true }
|
|
@@ -208,7 +218,7 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
208
218
|
ok: false,
|
|
209
219
|
error:
|
|
210
220
|
`The generated API differs from the reviewed surface.snapshot.json ` +
|
|
211
|
-
`(${delta.added.length} added, ${delta.removed.length} removed, ${delta.changed.length} changed). ` +
|
|
221
|
+
`(${delta.added.length} added, ${delta.removed.length} removed, ${delta.renamed.length} renamed, ${delta.changed.length} changed). ` +
|
|
212
222
|
`Accept it deliberately with \`chant dev surface-diff <lexicon> --update-snapshot --bump\`, ` +
|
|
213
223
|
`never as a side effect of a release.\n${formatDelta(delta)}`,
|
|
214
224
|
},
|
|
@@ -42,7 +42,7 @@ import type { DriverStepRecord } from "./driver";
|
|
|
42
42
|
* `extract-config-bom` likewise *describes* an artifact rather than promoting
|
|
43
43
|
* one. Those must not trigger a release record (#665) — a release means "we
|
|
44
44
|
* promoted this by digest to a location", not "we hashed some bytes". The
|
|
45
|
-
* driver's own `
|
|
45
|
+
* driver's own `collectComponentOutputs` stays looser (uri/digest/key) because it
|
|
46
46
|
* only feeds `@<component>.publish.*` wiring, where a false positive is harmless.
|
|
47
47
|
*/
|
|
48
48
|
function isPromotedArtifact(output: unknown): output is { uri: string; digest?: string } {
|
|
@@ -44,7 +44,7 @@ import type { Capability } from "./capability";
|
|
|
44
44
|
export interface CapabilityManifest {
|
|
45
45
|
/** Package/plugin name (e.g. "aws", "gcp"), not a capability `kind`. */
|
|
46
46
|
name: string;
|
|
47
|
-
/**
|
|
47
|
+
/** The plugin package's version (semver), read from its `package.json`. */
|
|
48
48
|
version: string;
|
|
49
49
|
/** Minimum/compatible chant core version, checked the same way as `LexiconManifest.chantVersion` (see ../lexicon-manifest.ts's `checkVersionCompatibility`). */
|
|
50
50
|
chantVersion?: string;
|
|
@@ -69,7 +69,13 @@ export interface CapabilityPlugin {
|
|
|
69
69
|
/** Human-readable plugin/package name (e.g. "aws", "gcp"), not a capability `kind`. */
|
|
70
70
|
readonly name: string;
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The plugin package's version (semver), read from its `package.json`
|
|
74
|
+
* (chant #1505). Mirrors `LexiconManifest.version`. Informational — nothing
|
|
75
|
+
* gates on it. The built-in plugins expose it as a lazy getter over
|
|
76
|
+
* `ownPackageVersion`, so the read happens on first access and never at
|
|
77
|
+
* module scope.
|
|
78
|
+
*/
|
|
73
79
|
readonly version: string;
|
|
74
80
|
|
|
75
81
|
/** Return every `Capability` this plugin contributes, keyed for registration by its own `kind`. */
|
|
@@ -124,8 +130,23 @@ export function isCapabilityPlugin(value: unknown): value is CapabilityPlugin {
|
|
|
124
130
|
*
|
|
125
131
|
* Returns `"0.0.0"` when no versioned `package.json` is found — a visible
|
|
126
132
|
* sentinel rather than a guess; nothing gates on the field.
|
|
133
|
+
*
|
|
134
|
+
* Call it lazily (from a `get version()` accessor, as the built-in plugins
|
|
135
|
+
* do), not at module scope: the read touches the filesystem, and a plugin
|
|
136
|
+
* module must stay importable where `fs` is absent (workerd, chant #1081).
|
|
137
|
+
* The result is cached per module URL, so the walk happens once.
|
|
127
138
|
*/
|
|
128
139
|
export function ownPackageVersion(moduleUrl: string): string {
|
|
140
|
+
const cached = ownVersionCache.get(moduleUrl);
|
|
141
|
+
if (cached !== undefined) return cached;
|
|
142
|
+
const found = findPackageVersion(moduleUrl);
|
|
143
|
+
ownVersionCache.set(moduleUrl, found);
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ownVersionCache = new Map<string, string>();
|
|
148
|
+
|
|
149
|
+
function findPackageVersion(moduleUrl: string): string {
|
|
129
150
|
let dir = dirname(fileURLToPath(moduleUrl));
|
|
130
151
|
for (;;) {
|
|
131
152
|
try {
|