@intentius/chant 0.124.16 → 0.124.18
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/__golden__/wave-loop/fan-out-applied.json +511 -0
- package/dist/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
- package/dist/__golden__/wave-loop/fan-out-gated.json +707 -0
- package/dist/__golden__/wave-loop/fan-out-no-members.json +898 -0
- package/dist/__golden__/wave-loop/fan-out-only.json +514 -0
- package/dist/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
- package/dist/__golden__/wave-loop/fan-out-same-root.json +311 -0
- package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +756 -0
- package/dist/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
- package/dist/__golden__/wave-loop/op-by-apply-pass.json +400 -0
- package/dist/__golden__/wave-loop/op-by-apply-skip.json +404 -0
- package/dist/__golden__/wave-loop/op-cost-member-clash.json +62 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +191 -0
- package/dist/__golden__/wave-loop/op-denied-source.json +407 -0
- package/dist/__golden__/wave-loop/op-denied.json +399 -0
- package/dist/__golden__/wave-loop/op-discover-rest-short.json +92 -0
- package/dist/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
- package/dist/__golden__/wave-loop/op-discover-rest.json +659 -0
- package/dist/__golden__/wave-loop/op-discover-wide.json +290 -0
- package/dist/__golden__/wave-loop/op-held-on-again.json +176 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
- package/dist/__golden__/wave-loop/op-held-on.json +1455 -0
- package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +1059 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
- package/dist/__golden__/wave-loop/op-hooks-pass.json +735 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +1225 -0
- package/dist/__golden__/wave-loop/op-outcome-held.json +202 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
- package/dist/__golden__/wave-loop/op-outcome-replan.json +259 -0
- package/dist/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
- package/dist/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
- package/dist/__golden__/wave-loop/op-overridden.json +508 -0
- package/dist/__golden__/wave-loop/op-per-run.json +518 -0
- package/dist/__golden__/wave-loop/op-point.json +495 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +397 -0
- package/dist/__golden__/wave-loop/op-sealed.json +125 -0
- package/dist/__golden__/wave-loop/op-spend-change-only.json +168 -0
- package/dist/__golden__/wave-loop/op-spend.json +328 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +1808 -0
- package/dist/__golden__/wave-loop/op-wide-refused.json +291 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +231 -0
- package/dist/__golden__/wave-loop/op-wide.json +440 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +2 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +3 -2
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/discover.js +2 -1
- package/dist/components/fan-out-run.d.ts +4 -3
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +138 -123
- package/dist/config-sandbox.d.ts.map +1 -1
- package/dist/config-sandbox.js +5 -1
- package/dist/discovery/import.d.ts.map +1 -1
- package/dist/discovery/import.js +2 -1
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.js +8 -3
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +4 -3
- package/dist/lint/policy-import.d.ts.map +1 -1
- package/dist/lint/policy-import.js +2 -1
- package/dist/lint/rule-loader.d.ts.map +1 -1
- package/dist/lint/rule-loader.js +2 -1
- package/dist/module-interop.d.ts +20 -0
- package/dist/module-interop.d.ts.map +1 -0
- package/dist/module-interop.js +39 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +3 -2
- package/dist/op/op-waves-run.d.ts +7 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +129 -72
- package/dist/wave-engine.d.ts +74 -5
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +47 -5
- package/dist/wave-runtime.mjs +218 -218
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.js +2 -1
- package/package.json +1 -1
- package/src/__golden__/wave-loop/fan-out-applied.json +511 -0
- package/src/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
- package/src/__golden__/wave-loop/fan-out-gated.json +707 -0
- package/src/__golden__/wave-loop/fan-out-no-members.json +898 -0
- package/src/__golden__/wave-loop/fan-out-only.json +514 -0
- package/src/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
- package/src/__golden__/wave-loop/fan-out-same-root.json +311 -0
- package/src/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +756 -0
- package/src/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
- package/src/__golden__/wave-loop/op-by-apply-pass.json +400 -0
- package/src/__golden__/wave-loop/op-by-apply-skip.json +404 -0
- package/src/__golden__/wave-loop/op-cost-member-clash.json +62 -0
- package/src/__golden__/wave-loop/op-cost-member.json +191 -0
- package/src/__golden__/wave-loop/op-denied-source.json +407 -0
- package/src/__golden__/wave-loop/op-denied.json +399 -0
- package/src/__golden__/wave-loop/op-discover-rest-short.json +92 -0
- package/src/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
- package/src/__golden__/wave-loop/op-discover-rest.json +659 -0
- package/src/__golden__/wave-loop/op-discover-wide.json +290 -0
- package/src/__golden__/wave-loop/op-held-on-again.json +176 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
- package/src/__golden__/wave-loop/op-held-on.json +1455 -0
- package/src/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
- package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +1059 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
- package/src/__golden__/wave-loop/op-hooks-pass.json +735 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +1225 -0
- package/src/__golden__/wave-loop/op-outcome-held.json +202 -0
- package/src/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
- package/src/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
- package/src/__golden__/wave-loop/op-outcome-replan.json +259 -0
- package/src/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
- package/src/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
- package/src/__golden__/wave-loop/op-overridden.json +508 -0
- package/src/__golden__/wave-loop/op-per-run.json +518 -0
- package/src/__golden__/wave-loop/op-point.json +495 -0
- package/src/__golden__/wave-loop/op-pr-review.json +397 -0
- package/src/__golden__/wave-loop/op-sealed.json +125 -0
- package/src/__golden__/wave-loop/op-spend-change-only.json +168 -0
- package/src/__golden__/wave-loop/op-spend.json +328 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +1808 -0
- package/src/__golden__/wave-loop/op-wide-refused.json +291 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +231 -0
- package/src/__golden__/wave-loop/op-wide.json +440 -0
- package/src/cli/commands/check-lexicon-plugin.ts +2 -1
- package/src/cli/commands/lint.ts +3 -2
- package/src/cli/compiled-cli.e2e.test.ts +42 -1
- package/src/components/discover.ts +2 -1
- package/src/components/fan-out-run.ts +156 -128
- package/src/config-sandbox.ts +5 -1
- package/src/discovery/import.ts +2 -1
- package/src/discovery/sandbox/driver.ts +8 -3
- package/src/lexicon-module.ts +4 -3
- package/src/lint/policy-import.ts +2 -1
- package/src/lint/rule-loader.ts +2 -1
- package/src/meta/peer-deps.test.ts +39 -0
- package/src/module-interop.test.ts +65 -0
- package/src/module-interop.ts +39 -0
- package/src/op/discover.ts +3 -2
- package/src/op/op-waves-run.ts +184 -68
- package/src/wave-engine.test.ts +38 -1
- package/src/wave-engine.ts +101 -5
- package/src/wave-loop-golden.test.ts +627 -0
- package/src/workspace/lineage-migrations.ts +2 -1
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
* ## Or one approval per wave
|
|
32
32
|
*
|
|
33
33
|
* `waveGate` (#3049) is the other mode. Each wave is planned once the waves
|
|
34
|
-
* before it applied, and its gate binds that wave's set digest.
|
|
35
|
-
* the digest and the gate decision come from the wave engine
|
|
36
|
-
* (`../wave-engine.ts`, #3766), which `chant run wave` uses too
|
|
34
|
+
* before it applied, and its gate binds that wave's set digest. The loop,
|
|
35
|
+
* gate names, the digest and the gate decision come from the wave engine
|
|
36
|
+
* (`../wave-engine.ts`, #3766), which `chant run wave` uses too; this module
|
|
37
|
+
* hands it the steps a fan-out plans, decides and applies a wave with. See
|
|
37
38
|
* {@link runGatedWaves}.
|
|
38
39
|
*/
|
|
39
40
|
|
|
@@ -46,13 +47,13 @@ import {
|
|
|
46
47
|
} from "./driver";
|
|
47
48
|
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
48
49
|
import { downstreamWithin, remainingFanOut, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
|
|
49
|
-
import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
|
|
50
|
+
import { evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "../op/gate";
|
|
50
51
|
import type { RunProgressEvent } from "./run-progress";
|
|
51
52
|
import type { CapabilityRegistry } from "./capability";
|
|
52
53
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
53
54
|
import type { DeployContext } from "./capability";
|
|
54
55
|
import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
|
|
55
|
-
import { decideWaveGate, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
|
|
56
|
+
import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* One gate per wave (#3049), each bound to its wave's set digest. The other
|
|
@@ -392,137 +393,164 @@ async function runGatedWaves(
|
|
|
392
393
|
|
|
393
394
|
options.onProgress?.({ type: "run-start", waves: plan.waves });
|
|
394
395
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
396
|
+
/** What planning one wave gave. */
|
|
397
|
+
interface Planned {
|
|
398
|
+
runnable: string[];
|
|
399
|
+
gateName: string;
|
|
400
|
+
contexts: Map<string, DeployContext>;
|
|
401
|
+
members: WaveMember[];
|
|
402
|
+
plansFor: Map<string, Record<string, unknown>>;
|
|
403
|
+
waveFailures: Array<{ component: string; error?: string }>;
|
|
404
|
+
}
|
|
405
|
+
/** `next`: go on to the next wave. `stop`: the run ends here. */
|
|
406
|
+
type Outcome = "next" | "stop";
|
|
407
|
+
|
|
408
|
+
await runWaveLoop<Planned, { check: Extract<GateCheck, { satisfied: true }>; record: WaveRecord; toRun: string[] }, Outcome>({
|
|
409
|
+
first: 1,
|
|
410
|
+
onward: (_k, outcome) => outcome === "next",
|
|
411
|
+
|
|
412
|
+
async plan({ k: waveNum }) {
|
|
413
|
+
const wave = plan.waves[waveNum - 1];
|
|
414
|
+
if (!wave) return { end: "stop" };
|
|
415
|
+
const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
|
|
416
|
+
if (waveGate.only !== undefined && waveNum < waveGate.only) {
|
|
417
|
+
if (runnable.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, runnable);
|
|
418
|
+
return { end: "next" };
|
|
418
419
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
420
|
+
if (waveGate.only !== undefined && waveNum > waveGate.only) return { end: "stop" };
|
|
421
|
+
if (runnable.length === 0) return { end: "next" };
|
|
422
|
+
|
|
423
|
+
const gateName = waveGateName(waveGate.gate, waveNum);
|
|
424
|
+
const contexts = new Map(runnable.map((name) => [name, fanOutContext(options, name)]));
|
|
425
|
+
const planned = await Promise.all(
|
|
426
|
+
runnable.map((name) => planComponent(byName.get(name)!, contexts.get(name)!, componentOutputs)),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const members: WaveMember[] = [];
|
|
430
|
+
const plansFor = new Map<string, Record<string, unknown>>();
|
|
431
|
+
const waveFailures: Array<{ component: string; error?: string }> = [];
|
|
432
|
+
for (const p of planned) {
|
|
433
|
+
if ("error" in p) {
|
|
434
|
+
waveFailures.push({ component: p.component, error: p.error });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
members.push(...p.members);
|
|
438
|
+
plansFor.set(p.component, p.plans);
|
|
439
|
+
}
|
|
440
|
+
return { go: { runnable, gateName, contexts, members, plansFor, waveFailures } };
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
digest(_at, { members, plansFor, waveFailures }) {
|
|
444
|
+
try {
|
|
445
|
+
// A wave where nothing planned has no set, and so no digest.
|
|
446
|
+
return members.length > 0 ? waveSetDigest(members) : "";
|
|
447
|
+
} catch (err) {
|
|
448
|
+
// Two components planning the same root: there is no one plan for it.
|
|
449
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
450
|
+
for (const name of plansFor.keys()) waveFailures.push({ component: name, error });
|
|
451
|
+
plansFor.clear();
|
|
452
|
+
return "";
|
|
453
|
+
}
|
|
454
|
+
},
|
|
439
455
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
digest,
|
|
447
|
-
members: [...members].sort((a, b) => a.member.localeCompare(b.member)),
|
|
448
|
-
status: "failed",
|
|
449
|
-
...(waveFailures.length > 0 ? { failed: waveFailures } : {}),
|
|
450
|
-
};
|
|
451
|
-
if (toRun.length === 0) {
|
|
452
|
-
waves.push(record);
|
|
453
|
-
options.onWaveSettled?.(record);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
+
async decide({ k: waveNum }, { runnable, gateName, members, plansFor, waveFailures }, digest) {
|
|
457
|
+
for (const f of waveFailures) {
|
|
458
|
+
failed.push(f.component);
|
|
459
|
+
results.push(planFailure(f.component, f.error ?? "plan failed"));
|
|
460
|
+
markBlocked(f.component);
|
|
461
|
+
}
|
|
456
462
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// would read outputs this wave has not written.
|
|
468
|
-
const stopped: WaveRecord = {
|
|
469
|
-
...record,
|
|
470
|
-
status: "gated",
|
|
471
|
-
...(check.mismatch?.approved ? { approved: check.mismatch.approved } : {}),
|
|
463
|
+
const toRun = runnable.filter((name) => plansFor.has(name));
|
|
464
|
+
const record: WaveRecord = {
|
|
465
|
+
wave: waveNum,
|
|
466
|
+
op: waveGate.op,
|
|
467
|
+
gate: gateName,
|
|
468
|
+
components: [...runnable].sort(),
|
|
469
|
+
digest,
|
|
470
|
+
members: [...members].sort((a, b) => a.member.localeCompare(b.member)),
|
|
471
|
+
status: "failed",
|
|
472
|
+
...(waveFailures.length > 0 ? { failed: waveFailures } : {}),
|
|
472
473
|
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
options.onProgress?.({ type: "wave-start", wave: waveNum, components: toRun });
|
|
480
|
-
const waveResults = await Promise.all(
|
|
481
|
-
toRun.map(async (name) => {
|
|
482
|
-
options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
|
|
483
|
-
const plans = plansFor.get(name)!;
|
|
484
|
-
const ctx = { ...contexts.get(name)!, ...(Object.keys(plans).length > 0 ? { plans } : {}) };
|
|
485
|
-
const result = await runComponentDeploy(byName.get(name)!, ctx, registry, componentOutputs, options.onProgress, gates);
|
|
486
|
-
options.onProgress?.({
|
|
487
|
-
type: "component-done",
|
|
488
|
-
wave: waveNum,
|
|
489
|
-
component: name,
|
|
490
|
-
status: result.status === "fail" ? "failed" : result.status,
|
|
491
|
-
});
|
|
492
|
-
options.onComponentSettled?.(result, componentOutputs[name]);
|
|
493
|
-
return result;
|
|
494
|
-
}),
|
|
495
|
-
);
|
|
496
|
-
results.push(...waveResults);
|
|
474
|
+
if (toRun.length === 0) {
|
|
475
|
+
waves.push(record);
|
|
476
|
+
options.onWaveSettled?.(record);
|
|
477
|
+
return { end: "next" };
|
|
478
|
+
}
|
|
497
479
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
480
|
+
const check = await decideWaveGate(gates.port, {
|
|
481
|
+
op: waveGate.op,
|
|
482
|
+
gate: gateName,
|
|
483
|
+
digest,
|
|
484
|
+
description: waveGate.description ?? `wave ${waveNum} of ${plan.waves.length}: ${toRun.join(", ")}`,
|
|
485
|
+
...(waveGate.timeout ? { timeout: waveGate.timeout } : {}),
|
|
486
|
+
...(options.now ? { now: options.now } : {}),
|
|
487
|
+
});
|
|
488
|
+
if (!check.satisfied) {
|
|
489
|
+
// Nothing in this wave ran, and no later wave is planned: its plan
|
|
490
|
+
// would read outputs this wave has not written.
|
|
491
|
+
const stopped: WaveRecord = {
|
|
492
|
+
...record,
|
|
493
|
+
status: "gated",
|
|
494
|
+
...(check.mismatch?.approved ? { approved: check.mismatch.approved } : {}),
|
|
495
|
+
};
|
|
496
|
+
waves.push(stopped);
|
|
497
|
+
options.onWaveSettled?.(stopped);
|
|
498
|
+
gatedFact = check.pending;
|
|
499
|
+
return { end: "stop" };
|
|
502
500
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
501
|
+
return { go: { check, record, toRun } };
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
async apply({ k: waveNum }, { contexts, plansFor, waveFailures }, { check, record, toRun }) {
|
|
505
|
+
options.onProgress?.({ type: "wave-start", wave: waveNum, components: toRun });
|
|
506
|
+
const waveResults = await Promise.all(
|
|
507
|
+
toRun.map(async (name) => {
|
|
508
|
+
options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
|
|
509
|
+
const plans = plansFor.get(name)!;
|
|
510
|
+
const ctx = { ...contexts.get(name)!, ...(Object.keys(plans).length > 0 ? { plans } : {}) };
|
|
511
|
+
const result = await runComponentDeploy(byName.get(name)!, ctx, registry, componentOutputs, options.onProgress, gates);
|
|
512
|
+
options.onProgress?.({
|
|
513
|
+
type: "component-done",
|
|
514
|
+
wave: waveNum,
|
|
515
|
+
component: name,
|
|
516
|
+
status: result.status === "fail" ? "failed" : result.status,
|
|
517
|
+
});
|
|
518
|
+
options.onComponentSettled?.(result, componentOutputs[name]);
|
|
519
|
+
return result;
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
results.push(...waveResults);
|
|
523
|
+
|
|
524
|
+
for (const result of waveResults) {
|
|
525
|
+
if (result.status === "ok") {
|
|
526
|
+
completed.push(result.component);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (result.status === "fail") {
|
|
530
|
+
failed.push(result.component);
|
|
531
|
+
waveFailures.push({ component: result.component });
|
|
532
|
+
} else {
|
|
533
|
+
gatedFact ??= result.gate;
|
|
534
|
+
}
|
|
535
|
+
markBlocked(result.component);
|
|
508
536
|
}
|
|
509
|
-
markBlocked(result.component);
|
|
510
|
-
}
|
|
511
537
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
538
|
+
const settled: WaveRecord = {
|
|
539
|
+
...record,
|
|
540
|
+
status: waveFailures.length > 0 ? "failed" : "applied",
|
|
541
|
+
approvedBy: check.resolution.resolvedBy,
|
|
542
|
+
...(waveFailures.length > 0 ? { failed: waveFailures } : {}),
|
|
543
|
+
};
|
|
544
|
+
waves.push(settled);
|
|
545
|
+
options.onWaveSettled?.(settled);
|
|
546
|
+
options.onProgress?.({
|
|
547
|
+
type: "wave-done",
|
|
548
|
+
wave: waveNum,
|
|
549
|
+
status: waveFailures.length > 0 ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
|
|
550
|
+
});
|
|
551
|
+
return "next";
|
|
552
|
+
},
|
|
553
|
+
});
|
|
526
554
|
|
|
527
555
|
const blocked: FanOutSkip[] = [...blockedBy.entries()]
|
|
528
556
|
.sort(([a], [b]) => a.localeCompare(b))
|
package/src/config-sandbox.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { importConfigModule, requireConfigModule, type ConfigModuleNamespace } from "./config-import";
|
|
3
|
+
import { esModuleNamespace } from "./module-interop";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* chant #1113 — decides WHERE a project's `chant.config.ts` is evaluated.
|
|
@@ -84,7 +85,10 @@ export function resetSandboxConfigEvaluationForTests(): void {
|
|
|
84
85
|
* evaluated, never how its result is read.
|
|
85
86
|
*/
|
|
86
87
|
export function selectConfigExport(namespace: ConfigModuleNamespace): unknown {
|
|
87
|
-
|
|
88
|
+
// A config tsx compiled as CommonJS arrives as `{ default: module.exports }`
|
|
89
|
+
// under a native import (#3897); read the module's own namespace instead.
|
|
90
|
+
const ns = esModuleNamespace(namespace);
|
|
91
|
+
return ns.default ?? ns.config ?? ns;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/**
|
package/src/discovery/import.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DiscoveryError } from "../errors";
|
|
2
2
|
import { ensureTypeScriptLoader } from "../ts-loader";
|
|
3
|
+
import { esModuleNamespace } from "../module-interop";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Modules whose evaluation threw, and the error each threw with (#2368).
|
|
@@ -59,7 +60,7 @@ export async function importModule(
|
|
|
59
60
|
|
|
60
61
|
try {
|
|
61
62
|
ensureTypeScriptLoader();
|
|
62
|
-
return await import(path);
|
|
63
|
+
return esModuleNamespace(await import(path));
|
|
63
64
|
} catch (error) {
|
|
64
65
|
const message =
|
|
65
66
|
error instanceof Error ? error.message : "Unknown import error";
|
|
@@ -59,6 +59,8 @@ const POST_SYNTH_MODULE = join(dirname(DISCOVERY_DIR), "lint", "post-synth.ts");
|
|
|
59
59
|
// file, so the driver's `setBuildParams` call below mutates the one object
|
|
60
60
|
// every bundled import observes.
|
|
61
61
|
const PARAMS_MODULE = join(dirname(DISCOVERY_DIR), "params.ts");
|
|
62
|
+
// #3897: a project file tsx compiled as CommonJS reads as its own namespace.
|
|
63
|
+
const INTEROP_MODULE = join(dirname(DISCOVERY_DIR), "module-interop.ts");
|
|
62
64
|
|
|
63
65
|
export interface GenerateDriverOptions {
|
|
64
66
|
/** Absolute paths to the run-fallback files this build decided NOT to fold — see `discover()`'s fold/taint loop in `../index.ts`. */
|
|
@@ -130,6 +132,7 @@ export function generateDriverSource(options: GenerateDriverOptions): string {
|
|
|
130
132
|
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
131
133
|
`import { getProvenance } from ${lit(PROVENANCE_MODULE)};`,
|
|
132
134
|
`import { setBuildParams } from ${lit(PARAMS_MODULE)};`,
|
|
135
|
+
`import { esModuleNamespace } from ${lit(INTEROP_MODULE)};`,
|
|
133
136
|
`import { writeSync } from "node:fs";`,
|
|
134
137
|
``,
|
|
135
138
|
`const BUILD_ROOT = ${lit(buildRoot)};`,
|
|
@@ -181,7 +184,7 @@ export function generateDriverSource(options: GenerateDriverOptions): string {
|
|
|
181
184
|
lines.push(
|
|
182
185
|
` importing = ${lit(file)};`,
|
|
183
186
|
` try {`,
|
|
184
|
-
` const mod = await import(${lit(file)});`,
|
|
187
|
+
` const mod = esModuleNamespace(await import(${lit(file)}));`,
|
|
185
188
|
` modules.push({ file: ${lit(file)}, exports: mod });`,
|
|
186
189
|
` } catch (err) {`,
|
|
187
190
|
` errors.push(classifyChildError(${lit(file)}, err).toJSON());`,
|
|
@@ -261,6 +264,7 @@ export function generateConfigDriverSource(configPath: string): string {
|
|
|
261
264
|
return [
|
|
262
265
|
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
263
266
|
`import { scanConfigWireSafety } from ${lit(CONFIG_WIRE_MODULE)};`,
|
|
267
|
+
`import { esModuleNamespace } from ${lit(INTEROP_MODULE)};`,
|
|
264
268
|
`import { writeSync } from "node:fs";`,
|
|
265
269
|
``,
|
|
266
270
|
...sendFunctionSource(),
|
|
@@ -268,7 +272,7 @@ export function generateConfigDriverSource(configPath: string): string {
|
|
|
268
272
|
`async function main() {`,
|
|
269
273
|
` let namespace;`,
|
|
270
274
|
` try {`,
|
|
271
|
-
` namespace = await import(${lit(configPath)});`,
|
|
275
|
+
` namespace = esModuleNamespace(await import(${lit(configPath)}));`,
|
|
272
276
|
` } catch (err) {`,
|
|
273
277
|
` await send({ kind: "chant-config", ok: false, error: classifyChildError(${lit(configPath)}, err).toJSON() });`,
|
|
274
278
|
` return;`,
|
|
@@ -337,6 +341,7 @@ export function generatePolicyDriverSource(policyPaths: readonly string[]): stri
|
|
|
337
341
|
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
338
342
|
`import { decodePolicyBuildResult, scanPolicyDiagnostics, PolicyWireError } from ${lit(POLICY_WIRE_MODULE)};`,
|
|
339
343
|
`import { runPostSynthChecks, isPostSynthCheck } from ${lit(POST_SYNTH_MODULE)};`,
|
|
344
|
+
`import { esModuleNamespace } from ${lit(INTEROP_MODULE)};`,
|
|
340
345
|
`import { writeSync } from "node:fs";`,
|
|
341
346
|
``,
|
|
342
347
|
...sendFunctionSource(),
|
|
@@ -382,7 +387,7 @@ export function generatePolicyDriverSource(policyPaths: readonly string[]): stri
|
|
|
382
387
|
for (const policyPath of policyPaths) {
|
|
383
388
|
lines.push(
|
|
384
389
|
` try {`,
|
|
385
|
-
` const mod = await import(${lit(policyPath)});`,
|
|
390
|
+
` const mod = esModuleNamespace(await import(${lit(policyPath)}));`,
|
|
386
391
|
` for (const value of Object.values(mod)) {`,
|
|
387
392
|
` if (isPostSynthCheck(value)) checks.push(guard(value, ${lit(policyPath)}));`,
|
|
388
393
|
` }`,
|
package/src/lexicon-module.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { createRequire } from "node:module";
|
|
|
28
28
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
30
30
|
import { ensureTypeScriptLoader } from "./ts-loader";
|
|
31
|
+
import { esModuleNamespace } from "./module-interop";
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* One `lexicons` entry: a package-backed name, or a name plus the module that
|
|
@@ -181,7 +182,7 @@ export async function importLexiconModule(name: string): Promise<Record<string,
|
|
|
181
182
|
if (path === undefined) return undefined;
|
|
182
183
|
try {
|
|
183
184
|
ensureTypeScriptLoader();
|
|
184
|
-
return (await import(path)) as Record<string, unknown
|
|
185
|
+
return esModuleNamespace((await import(path)) as Record<string, unknown>);
|
|
185
186
|
} catch (err) {
|
|
186
187
|
throw new Error(
|
|
187
188
|
`lexicon "${name}" is declared with module ${path}, which could not be loaded: ` +
|
|
@@ -262,12 +263,12 @@ export function resolveFromProject(spec: string, fromDir: string = process.cwd()
|
|
|
262
263
|
export async function importLexiconPackage(spec: string, fromDir: string = process.cwd()): Promise<Record<string, unknown>> {
|
|
263
264
|
try {
|
|
264
265
|
ensureTypeScriptLoader();
|
|
265
|
-
return (await import(spec)) as Record<string, unknown
|
|
266
|
+
return esModuleNamespace((await import(spec)) as Record<string, unknown>);
|
|
266
267
|
} catch (err) {
|
|
267
268
|
const pkg = packageNameOf(spec);
|
|
268
269
|
if (pkg === undefined || !isPackageNotFound(err, pkg)) throw err;
|
|
269
270
|
const resolved = resolveFromProject(spec, fromDir);
|
|
270
271
|
if (resolved === undefined) throw err;
|
|
271
|
-
return (await import(pathToFileURL(resolved).href)) as Record<string, unknown
|
|
272
|
+
return esModuleNamespace((await import(pathToFileURL(resolved).href)) as Record<string, unknown>);
|
|
272
273
|
}
|
|
273
274
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { ensureTypeScriptLoader } from "../ts-loader";
|
|
27
|
+
import { esModuleNamespace } from "../module-interop";
|
|
27
28
|
|
|
28
29
|
/** Whether this process must run project policy modules inside the sandbox boundary instead of here. */
|
|
29
30
|
let armed = false;
|
|
@@ -69,5 +70,5 @@ export async function importPolicyModule(policyPath: string): Promise<PolicyModu
|
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
ensureTypeScriptLoader();
|
|
72
|
-
return (await import(policyPath)) as PolicyModuleNamespace;
|
|
73
|
+
return esModuleNamespace((await import(policyPath)) as PolicyModuleNamespace);
|
|
73
74
|
}
|
package/src/lint/rule-loader.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readdirSync, existsSync } from "fs";
|
|
|
2
2
|
import { dirname, join, resolve } from "path";
|
|
3
3
|
import type { LintRule } from "./rule";
|
|
4
4
|
import { ensureTypeScriptLoader } from "../ts-loader";
|
|
5
|
+
import { esModuleNamespace } from "../module-interop";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Type guard to check if a value conforms to the LintRule interface.
|
|
@@ -84,7 +85,7 @@ export async function loadLocalRules(projectDir: string): Promise<LintRule[]> {
|
|
|
84
85
|
let mod: Record<string, unknown>;
|
|
85
86
|
try {
|
|
86
87
|
ensureTypeScriptLoader();
|
|
87
|
-
mod = await import(filePath);
|
|
88
|
+
mod = esModuleNamespace(await import(filePath));
|
|
88
89
|
} catch (err) {
|
|
89
90
|
throw new Error(
|
|
90
91
|
`Failed to load rule file "${file}": ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -124,3 +124,42 @@ describe("the provider-aware rule, over fixtures", () => {
|
|
|
124
124
|
expect(rangeViolations(skewed, { aws: { typescript: ">=5.0.0" } })).toEqual([]);
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Same rule over every dependency field of every publishable package (#3905).
|
|
130
|
+
* docker, fly, k8s, grafana and helm once depended on sibling lexicons at `*`,
|
|
131
|
+
* which follows `latest` instead of the release they shipped in. Both release
|
|
132
|
+
* recipes now write `^<provider version>`; this fails if any `@intentius/*`
|
|
133
|
+
* range in dependencies, peerDependencies or optionalDependencies is `*` or
|
|
134
|
+
* anything other than the provider's current version.
|
|
135
|
+
*/
|
|
136
|
+
describe("every in-family @intentius/* range pins its provider's current version", () => {
|
|
137
|
+
type Manifest = {
|
|
138
|
+
name: string;
|
|
139
|
+
version: string;
|
|
140
|
+
private?: boolean;
|
|
141
|
+
dependencies?: Record<string, string>;
|
|
142
|
+
peerDependencies?: Record<string, string>;
|
|
143
|
+
optionalDependencies?: Record<string, string>;
|
|
144
|
+
};
|
|
145
|
+
const dirs = [
|
|
146
|
+
...readdirSync(join(repoRoot, "packages")).map((n) => join("packages", n)),
|
|
147
|
+
...lexicons.map((n) => join("lexicons", n)),
|
|
148
|
+
].filter((d) => existsSync(join(repoRoot, d, "package.json")));
|
|
149
|
+
const manifests = dirs.map((d) => JSON.parse(readFileSync(join(repoRoot, d, "package.json"), "utf-8")) as Manifest);
|
|
150
|
+
const publishable = manifests.filter((m) => !m.private);
|
|
151
|
+
const versions = Object.fromEntries(publishable.map((m) => [m.name, m.version]));
|
|
152
|
+
|
|
153
|
+
test.each(publishable.map((m) => m.name))("%s", (name) => {
|
|
154
|
+
const m = publishable.find((p) => p.name === name)!;
|
|
155
|
+
const bad: string[] = [];
|
|
156
|
+
for (const field of ["dependencies", "peerDependencies", "optionalDependencies"] as const) {
|
|
157
|
+
for (const [dep, range] of Object.entries(m[field] ?? {})) {
|
|
158
|
+
if (!dep.startsWith("@intentius/")) continue;
|
|
159
|
+
const want = versions[dep] ? `^${versions[dep]}` : undefined;
|
|
160
|
+
if (range !== want) bad.push(`${field}.${dep} is "${range}", want ${want ?? "a published workspace package"}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
expect(bad).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { selectConfigExport } from "./config-sandbox";
|
|
3
|
+
import { esModuleNamespace } from "./module-interop";
|
|
4
|
+
|
|
5
|
+
/** What node's native import() hands back for a CommonJS module tsx compiled from `export default`. */
|
|
6
|
+
function commonJsNamespace(exports: Record<string, unknown>): Record<string, unknown> {
|
|
7
|
+
const moduleExports: Record<string, unknown> = { ...exports };
|
|
8
|
+
Object.defineProperty(moduleExports, "__esModule", { value: true });
|
|
9
|
+
return { ...exports, default: moduleExports };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("esModuleNamespace (#3897)", () => {
|
|
13
|
+
test("a CommonJS __esModule namespace reads as the module's own exports", () => {
|
|
14
|
+
const config = { lexicons: ["terraform"] };
|
|
15
|
+
const helper = () => 1;
|
|
16
|
+
const ns = esModuleNamespace(commonJsNamespace({ default: config, helper }));
|
|
17
|
+
expect(ns.default).toBe(config);
|
|
18
|
+
expect(ns.helper).toBe(helper);
|
|
19
|
+
expect(Object.keys(ns)).not.toContain("__esModule");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("a CommonJS module with no default export has none after unwrapping", () => {
|
|
23
|
+
const ns = esModuleNamespace(commonJsNamespace({ config: { lexicons: ["aws"] } }));
|
|
24
|
+
expect(ns.default).toBeUndefined();
|
|
25
|
+
expect(ns.config).toEqual({ lexicons: ["aws"] });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("an enumerable __esModule marker is left out too", () => {
|
|
29
|
+
const ns = esModuleNamespace({ default: { __esModule: true, default: 1 } });
|
|
30
|
+
expect(ns).toEqual({ default: 1 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("an ES module whose default export has a default key is unchanged", () => {
|
|
34
|
+
const namespace = { default: { default: { lexicons: ["aws"] } } };
|
|
35
|
+
expect(esModuleNamespace(namespace)).toBe(namespace);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a default without the marker set to true is unchanged", () => {
|
|
39
|
+
const namespace = { default: { __esModule: "yes", default: 1 } };
|
|
40
|
+
expect(esModuleNamespace(namespace)).toBe(namespace);
|
|
41
|
+
const primitive = { default: 3 };
|
|
42
|
+
expect(esModuleNamespace(primitive)).toBe(primitive);
|
|
43
|
+
const empty = {};
|
|
44
|
+
expect(esModuleNamespace(empty)).toBe(empty);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("selectConfigExport (#3897)", () => {
|
|
49
|
+
test("a config tsx compiled as CommonJS selects the config object, not {default: config}", () => {
|
|
50
|
+
const config = { lexicons: ["terraform"], terraform: { roots: { estate: { dir: "estate" } } } };
|
|
51
|
+
expect(selectConfigExport(commonJsNamespace({ default: config }))).toBe(config);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("an ES module config that legitimately has a default key keeps it", () => {
|
|
55
|
+
const config = { default: { lexicons: ["aws"] }, lexicons: ["aws"] };
|
|
56
|
+
expect(selectConfigExport({ default: config })).toBe(config);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a require()d CommonJS config still selects its default", () => {
|
|
60
|
+
const config = { lexicons: ["aws"] };
|
|
61
|
+
const moduleExports: Record<string, unknown> = { default: config };
|
|
62
|
+
Object.defineProperty(moduleExports, "__esModule", { value: true });
|
|
63
|
+
expect(selectConfigExport(moduleExports)).toBe(config);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The namespace a project module was authored with, whichever module format
|
|
3
|
+
* tsx compiled it to (#3897).
|
|
4
|
+
*
|
|
5
|
+
* A `.ts` file with no `"type": "module"` `package.json` above it is compiled
|
|
6
|
+
* by tsx as CommonJS: `exports.default = ...` plus a non-enumerable
|
|
7
|
+
* `__esModule: true` marker. The compiled CLI loads project files with a
|
|
8
|
+
* native `import()`, and Node hands a CommonJS module back as a namespace
|
|
9
|
+
* whose `default` is the whole `module.exports`. So `export default x` arrives
|
|
10
|
+
* as `namespace.default.default`. Under the source CLI tsx's ESM loader sits
|
|
11
|
+
* in front of the `import()` and does this interop itself, which is why the
|
|
12
|
+
* nesting only showed up once the CLI ran compiled (#3879).
|
|
13
|
+
*
|
|
14
|
+
* When `namespace.default` carries the `__esModule` marker, the module's real
|
|
15
|
+
* namespace is that exports object: this returns a copy of it with the marker
|
|
16
|
+
* left out. Anything else, including an ES module whose default export is an
|
|
17
|
+
* object that happens to have a `default` key, is returned unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function esModuleNamespace<T extends Record<string, unknown>>(namespace: T): T {
|
|
20
|
+
// `in` before the read: a vitest factory mock throws on reading an export
|
|
21
|
+
// it does not define, and most modules have no `default` at all.
|
|
22
|
+
if (namespace === null || typeof namespace !== "object" || !("default" in namespace)) return namespace;
|
|
23
|
+
const exported = namespace.default;
|
|
24
|
+
if (!isCommonJsEsModule(exported)) return namespace;
|
|
25
|
+
const unwrapped: Record<string, unknown> = {};
|
|
26
|
+
for (const key of Object.keys(exported)) {
|
|
27
|
+
if (key !== "__esModule") unwrapped[key] = exported[key];
|
|
28
|
+
}
|
|
29
|
+
return unwrapped as T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A CommonJS `module.exports` that a compiler marked as an ES module. */
|
|
33
|
+
function isCommonJsEsModule(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return (
|
|
35
|
+
value !== null &&
|
|
36
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
37
|
+
(value as { __esModule?: unknown }).__esModule === true
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/op/discover.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { hasDiscoveryMarkerSync } from "../discovery/files";
|
|
|
7
7
|
import { resolveDiscoveryGlobs } from "../config";
|
|
8
8
|
import { isStewardDeclaration, type StewardDeclaration } from "./steward";
|
|
9
9
|
import { ensureTypeScriptLoader } from "../ts-loader";
|
|
10
|
+
import { esModuleNamespace } from "../module-interop";
|
|
10
11
|
|
|
11
12
|
export interface DiscoveredOp {
|
|
12
13
|
config: OpConfig;
|
|
@@ -152,7 +153,7 @@ export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryR
|
|
|
152
153
|
for (const filePath of files) {
|
|
153
154
|
try {
|
|
154
155
|
ensureTypeScriptLoader();
|
|
155
|
-
const mod = (await import(filePath)) as Record<string, unknown
|
|
156
|
+
const mod = esModuleNamespace((await import(filePath)) as Record<string, unknown>);
|
|
156
157
|
const exported = opsExportedBy(mod);
|
|
157
158
|
|
|
158
159
|
// A file may hold a steward and no Op (#2731): the steward sits beside
|
|
@@ -223,7 +224,7 @@ export async function discoverStewards(opts?: { cwd?: string }): Promise<Steward
|
|
|
223
224
|
let mod: Record<string, unknown>;
|
|
224
225
|
try {
|
|
225
226
|
ensureTypeScriptLoader();
|
|
226
|
-
mod = (await import(filePath)) as Record<string, unknown
|
|
227
|
+
mod = esModuleNamespace((await import(filePath)) as Record<string, unknown>);
|
|
227
228
|
} catch (err) {
|
|
228
229
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
230
|
continue;
|