@ultimat3/cli 20.2.1 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
package/src/verify-run.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Run every step
|
|
17
|
+
* Run every step, never bailing early: an agent fixing three things at once needs all
|
|
18
18
|
* three findings from one run, not one per round-trip.
|
|
19
19
|
*
|
|
20
20
|
* `ctx.only` narrows the list to one step. The narrowing lives HERE rather than in `cmd-verify.ts`
|
|
@@ -26,61 +26,43 @@ export async function runVerify(
|
|
|
26
26
|
ctx: VerifyContext,
|
|
27
27
|
): Promise<CommandResult> {
|
|
28
28
|
const floor = await readVerifyFloor(ctx.root);
|
|
29
|
-
const results: StepResult[] = [];
|
|
30
29
|
const selected = ctx.only === undefined ? steps : steps.filter((step) => step.name === ctx.only);
|
|
30
|
+
const byName = new Map<string, StepResult>();
|
|
31
|
+
const began = performance.now();
|
|
32
|
+
// The static steps wait for the serial suites and then run BESIDE them — only when `live` is in
|
|
33
|
+
// the list, so a one-step run (`--only`) and a list with no serial suite keep today's order.
|
|
34
|
+
const overlapping = selected.some((step) => step.name === SERIAL_SUITES[0]);
|
|
35
|
+
const beside = overlapping ? selected.filter((step) => BESIDE_SERIAL_SUITES.has(step.name)) : [];
|
|
36
|
+
let pending: Promise<void> | undefined;
|
|
37
|
+
const join = async (): Promise<void> => {
|
|
38
|
+
await pending;
|
|
39
|
+
pending = undefined;
|
|
40
|
+
};
|
|
31
41
|
for (const step of selected) {
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
name: step.name,
|
|
42
|
-
ok: !required,
|
|
43
|
-
durationMs: 0,
|
|
44
|
-
skipped: !required,
|
|
45
|
-
findings: required ? [vanishedSuiteFinding(step.name)] : [],
|
|
46
|
-
});
|
|
47
|
-
continue;
|
|
42
|
+
if (beside.includes(step)) continue;
|
|
43
|
+
if (step.name === SERIAL_SUITES[0]) {
|
|
44
|
+
pending = Promise.all(
|
|
45
|
+
beside.map(async (other) => {
|
|
46
|
+
byName.set(other.name, await runStep(other, ctx, floor));
|
|
47
|
+
}),
|
|
48
|
+
).then(() => undefined);
|
|
49
|
+
} else if (!SERIAL_SUITES.includes(step.name)) {
|
|
50
|
+
await join();
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
const outcome = await step.run(ctx).catch(
|
|
51
|
-
(error: unknown): StepOutcome => ({
|
|
52
|
-
ok: false,
|
|
53
|
-
findings: [findingOf(error, step.name)],
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
// A suite that executed nothing did not run, whatever its exit code says: `bun test` exits 0
|
|
57
|
-
// over an all-skipped file, so the counts are the only channel that can tell the two apart.
|
|
58
|
-
// ONE definition of "nothing ran", read twice, because the floor decides which of the two
|
|
59
|
-
// things it means — exactly as it already does for a step whose `applies` said no.
|
|
60
|
-
const tests = outcome.tests;
|
|
61
|
-
const nothingRan = tests !== undefined && tests.ran === 0;
|
|
62
|
-
const required = floorRequires(floor, step.name);
|
|
63
|
-
// A step the floor requires whose suite executed nothing is the same vanished suite as a step
|
|
64
|
-
// with no files at all — the run just had to finish before it could be seen. Appended to the
|
|
65
|
-
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
66
|
-
const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
|
|
67
|
-
results.push({
|
|
68
|
-
name: step.name,
|
|
69
|
-
ok: outcome.ok && vanished.length === 0,
|
|
70
|
-
durationMs: Math.round(performance.now() - started),
|
|
71
|
-
// Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
|
|
72
|
-
// the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
|
|
73
|
-
// step table may never do — a reader cannot tell a lane that ran from a lane that did not.
|
|
74
|
-
skipped: nothingRan && !required,
|
|
75
|
-
findings: [...outcome.findings, ...vanished],
|
|
76
|
-
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
77
|
-
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
78
|
-
...(tests === undefined ? {} : { tests }),
|
|
79
|
-
});
|
|
52
|
+
byName.set(step.name, await runStep(step, ctx, floor));
|
|
80
53
|
}
|
|
54
|
+
await join();
|
|
55
|
+
// Reported in the declared order, whatever order the steps finished in: the table, `--json` and
|
|
56
|
+
// every gate parsing either read the same sequence they always did.
|
|
57
|
+
const results = selected.flatMap((step) => {
|
|
58
|
+
const result = byName.get(step.name);
|
|
59
|
+
return result === undefined ? [] : [result];
|
|
60
|
+
});
|
|
81
61
|
const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
|
|
82
62
|
const skippedSteps = results.filter((step) => step.skipped === true).map((step) => step.name);
|
|
83
|
-
|
|
63
|
+
// WALL time, not the sum of step times: with steps overlapping, the sum overstates what a run
|
|
64
|
+
// costs, and the wall clock is the number a CI job waits on.
|
|
65
|
+
const totalMs = Math.round(performance.now() - began);
|
|
84
66
|
const summary = verifySummary({
|
|
85
67
|
results,
|
|
86
68
|
failed: failedSteps,
|
|
@@ -139,6 +121,89 @@ function verifySummary(input: {
|
|
|
139
121
|
return msg(clean ? 'cli.verify.fail' : 'cli.verify.failSkipped', params);
|
|
140
122
|
}
|
|
141
123
|
|
|
124
|
+
/**
|
|
125
|
+
* `x verify`'s wall time was the SUM of 20 serial steps (#14, the DX ledger): measured locally, 395s,
|
|
126
|
+
* of which `lint`, `boundaries`, `filesize`, `package-shape` and `errors` were 127s spent while
|
|
127
|
+
* nothing else ran. They read the tree and write nothing a later step reads (`lint` is biome over
|
|
128
|
+
* files; the rest are in-process scans), so they run BESIDE the serial suites — `live` and `e2e`
|
|
129
|
+
* are one worker each, Postgres- and browser-bound, and mostly waiting. `typecheck` stays first
|
|
130
|
+
* and alone: `tsc -b` writes `.tsbuildinfo` and `dist/`, and `unit` saturates every core.
|
|
131
|
+
* `manifest` joined them 2026-09-23: it compares committed files against the code and writes
|
|
132
|
+
* nothing, and at the repo root its host checks are ~20 whole-tree reads that sat alone at the end.
|
|
133
|
+
*/
|
|
134
|
+
export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
|
|
135
|
+
'lint',
|
|
136
|
+
'boundaries',
|
|
137
|
+
'filesize',
|
|
138
|
+
'package-shape',
|
|
139
|
+
'errors',
|
|
140
|
+
'manifest',
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/** The consecutive run of steps the static group overlaps, in the order `VERIFY_STEP_NAMES` holds. */
|
|
144
|
+
export const SERIAL_SUITES: readonly string[] = ['live', 'job', 'e2e', 'eval'];
|
|
145
|
+
|
|
146
|
+
async function runStep(
|
|
147
|
+
step: VerifyStep,
|
|
148
|
+
ctx: VerifyContext,
|
|
149
|
+
floor: Awaited<ReturnType<typeof readVerifyFloor>>,
|
|
150
|
+
): Promise<StepResult> {
|
|
151
|
+
// Inside the step's own failure, like a throwing `run`: an `applies` that threw escaped every
|
|
152
|
+
// catch and aborted the whole gate, which is the one outcome `runVerify` exists to prevent.
|
|
153
|
+
let applies: boolean;
|
|
154
|
+
try {
|
|
155
|
+
applies = step.applies === undefined ? true : await step.applies(ctx);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { name: step.name, ok: false, durationMs: 0, findings: [findingOf(error, step.name)] };
|
|
158
|
+
}
|
|
159
|
+
if (!applies) {
|
|
160
|
+
// A skip this repo already ruled out is not a skip. The step ran here before — the floor is
|
|
161
|
+
// that claim, committed — so "nothing to check" now means the suite was deleted, and the
|
|
162
|
+
// gate says so on the step's own line rather than counting one more thing not to worry
|
|
163
|
+
// about. Recorded as failed and NOT as skipped, so every reader of a step table sees it:
|
|
164
|
+
// the summary, `data.failed`, and the reference-app gate's own red list.
|
|
165
|
+
const required = floorRequires(floor, step.name);
|
|
166
|
+
return {
|
|
167
|
+
name: step.name,
|
|
168
|
+
ok: !required,
|
|
169
|
+
durationMs: 0,
|
|
170
|
+
skipped: !required,
|
|
171
|
+
findings: required ? [vanishedSuiteFinding(step.name)] : [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const started = performance.now();
|
|
175
|
+
const outcome = await step.run(ctx).catch(
|
|
176
|
+
(error: unknown): StepOutcome => ({
|
|
177
|
+
ok: false,
|
|
178
|
+
findings: [findingOf(error, step.name)],
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
// A suite that executed nothing did not run, whatever its exit code says: `bun test` exits 0
|
|
182
|
+
// over an all-skipped file, so the counts are the only channel that can tell the two apart.
|
|
183
|
+
// ONE definition of "nothing ran", read twice, because the floor decides which of the two
|
|
184
|
+
// things it means — exactly as it already does for a step whose `applies` said no.
|
|
185
|
+
const tests = outcome.tests;
|
|
186
|
+
const nothingRan = tests !== undefined && tests.ran === 0;
|
|
187
|
+
const required = floorRequires(floor, step.name);
|
|
188
|
+
// A step the floor requires whose suite executed nothing is the same vanished suite as a step
|
|
189
|
+
// with no files at all — the run just had to finish before it could be seen. Appended to the
|
|
190
|
+
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
191
|
+
const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
|
|
192
|
+
return {
|
|
193
|
+
name: step.name,
|
|
194
|
+
ok: outcome.ok && vanished.length === 0,
|
|
195
|
+
durationMs: Math.round(performance.now() - started),
|
|
196
|
+
// Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
|
|
197
|
+
// the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
|
|
198
|
+
// step table may never do — a reader cannot tell a lane that ran from a lane that did not.
|
|
199
|
+
skipped: nothingRan && !required,
|
|
200
|
+
findings: [...outcome.findings, ...vanished],
|
|
201
|
+
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
202
|
+
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
203
|
+
...(tests === undefined ? {} : { tests }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
142
207
|
function findingOf(error: unknown, step: string): Finding {
|
|
143
208
|
// A step may throw anything, including an Error that fights being read: `instanceof` runs a
|
|
144
209
|
// Proxy's `getPrototypeOf` trap and `.message` runs a getter, so a hostile throw would take the
|
package/src/verify-step.ts
CHANGED
|
@@ -79,9 +79,9 @@ export interface VerifyContext {
|
|
|
79
79
|
*/
|
|
80
80
|
readonly workers?: number;
|
|
81
81
|
/**
|
|
82
|
-
* ONE step, by name — an iteration loop, and the one thing here that IS a narrowing.
|
|
83
|
-
*
|
|
84
|
-
* question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
|
|
82
|
+
* ONE step, by name — an iteration loop, and the one thing here that IS a narrowing. The
|
|
83
|
+
* whole gate costs minutes (3m19s at the framework root on 12 cores, `As of 2026-09-23`), which
|
|
84
|
+
* is the cost of asking a question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
|
|
85
85
|
* mechanical rather than remembered: a run with this set prints `NOT A GATE RUN` in the summary
|
|
86
86
|
* AND carries `notAGateRun` in `--json` (`verify-run.ts`), so no reader of either can mistake it
|
|
87
87
|
* for the gate; and nothing writes `x.verify.json`, so the suite floor cannot be lowered by a
|
package/src/verify-tests.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { countsOf } from './test-counts';
|
|
|
18
18
|
import { testEnvOverrides } from './test-dotenv';
|
|
19
19
|
import type { TestFile } from './test-select';
|
|
20
20
|
import { discoverTests } from './test-select';
|
|
21
|
-
import { defaultWorkers } from './test-workers';
|
|
21
|
+
import { defaultWorkers, SERIAL_TYPES } from './test-workers';
|
|
22
|
+
import { withE2eApp } from './verify-e2e';
|
|
22
23
|
import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
23
24
|
import { fromExec, fromFindings } from './verify-step';
|
|
24
25
|
import { runParallel } from './verify-test-run';
|
|
@@ -110,31 +111,6 @@ export const ownerOf = (path: string): TestType =>
|
|
|
110
111
|
const disownedBy = (type: TypedTest): readonly string[] =>
|
|
111
112
|
type === 'e2e' ? [`**/e2e/**/*.{${OUTRANKING_E2E.join(',')}}.test.*`] : [];
|
|
112
113
|
|
|
113
|
-
/**
|
|
114
|
-
* Which types run across worker processes, and why the other two cannot.
|
|
115
|
-
*
|
|
116
|
-
* Parallel is safe when the only thing a test file shares with another file is the database, and
|
|
117
|
-
* the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
|
|
118
|
-
* template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
|
|
119
|
-
* framework — the permission set, the roles, the entity/action/query registries, the error-code
|
|
120
|
-
* titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
|
|
121
|
-
*
|
|
122
|
-
* | Type | Why |
|
|
123
|
-
* |---|---|
|
|
124
|
-
* | `live` | **serial.** A logical replication slot and a publication are named at the Postgres
|
|
125
|
-
* CLUSTER level, not inside a database, and this repo's own feed tests hard-code
|
|
126
|
-
* `x_live_slot` / `x_live_pub` against `TEST_REPLICATION_URL` — the one server, never a per-worker
|
|
127
|
-
* clone. Two workers would race `pg_create_logical_replication_slot` and the loser's failure would
|
|
128
|
-
* read as a flake. A per-worker database does not isolate a cluster-wide object |
|
|
129
|
-
* | `e2e` | **serial.** It runs against the *built output*: one `dist/`, one service-worker
|
|
130
|
-
* registration, one browser profile. There is nothing per-worker to hand it, and the type is
|
|
131
|
-
* seconds at most, so a split would buy a race and no time |
|
|
132
|
-
*
|
|
133
|
-
* The two are named here rather than tested for, because "can this type be sharded?" is a design
|
|
134
|
-
* fact about the type, not something a run can discover about itself.
|
|
135
|
-
*/
|
|
136
|
-
export const SERIAL_TYPES: readonly TestType[] = ['live', 'e2e'];
|
|
137
|
-
|
|
138
114
|
const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
|
|
139
115
|
|
|
140
116
|
/**
|
|
@@ -162,6 +138,12 @@ const ignoreFlags = (patterns: readonly string[]): readonly string[] =>
|
|
|
162
138
|
export const typeFiltersOf = (type: Exclude<TestType, 'unit'>): readonly string[] =>
|
|
163
139
|
OWNERSHIP.filter(([owner]) => owner === type).map(([, filter]) => filter);
|
|
164
140
|
|
|
141
|
+
/**
|
|
142
|
+
* An e2e test's own budget: a first navigation waits on `x dev` compiling the route and its island,
|
|
143
|
+
* which bun's default 5 s does not cover — a timeout there reads as an app bug that is not one.
|
|
144
|
+
*/
|
|
145
|
+
export const E2E_TEST_TIMEOUT_MS = 60_000;
|
|
146
|
+
|
|
165
147
|
/** Unit is everything the typed suites do not claim, so no test can fall between two steps. */
|
|
166
148
|
export const testStepCommand = (type: TestType): readonly string[] =>
|
|
167
149
|
type === 'unit'
|
|
@@ -173,6 +155,7 @@ export const testStepCommand = (type: TestType): readonly string[] =>
|
|
|
173
155
|
: [
|
|
174
156
|
'bun',
|
|
175
157
|
'test',
|
|
158
|
+
...(type === 'e2e' ? [`--timeout=${String(E2E_TEST_TIMEOUT_MS)}`] : []),
|
|
176
159
|
...ignoreFlags([...NEVER_A_TEST, ...disownedBy(type)]),
|
|
177
160
|
...typeFiltersOf(type),
|
|
178
161
|
];
|
|
@@ -203,10 +186,19 @@ export const resetTestDiscovery = (): void => discovered.clear();
|
|
|
203
186
|
const runSerial = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome> => {
|
|
204
187
|
const command = testStepCommand(type);
|
|
205
188
|
const envOverrides = testEnvOverrides(ctx.root, ctx.env ?? Bun.env);
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
189
|
+
const exec = (e2e: { command: readonly string[]; env: typeof envOverrides }) =>
|
|
190
|
+
ctx.runner(e2e.command, {
|
|
191
|
+
cwd: ctx.root,
|
|
192
|
+
...(Object.keys(e2e.env).length === 0 ? {} : { env: e2e.env }),
|
|
193
|
+
});
|
|
194
|
+
// The e2e step drives a real browser against the app it spawns, when this machine has one.
|
|
195
|
+
const result =
|
|
196
|
+
type === 'e2e'
|
|
197
|
+
? await withE2eApp(
|
|
198
|
+
{ root: ctx.root, isApp: isApp(ctx.root), command, env: envOverrides },
|
|
199
|
+
exec,
|
|
200
|
+
)
|
|
201
|
+
: await exec({ command, env: envOverrides });
|
|
210
202
|
return {
|
|
211
203
|
...fromExec(result, {
|
|
212
204
|
code: 'X_TEST_FAILED',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Which `tsc` invocation the `typecheck` step runs. `tsc -b` decides "up to date" by mtime, and Bun
|
|
2
|
+
// installs a package as HARDLINKS out of its cache, carrying the cache's old mtimes — so upgrading
|
|
3
|
+
// a dependency to a version already in the cache left every input older than the buildinfo, `-b`
|
|
4
|
+
// skipped the program and reported green while a cold CI went red (#450). `-p` with `incremental`
|
|
5
|
+
// re-reads every input and compares content hashes, so it cannot be fooled by a date.
|
|
6
|
+
|
|
7
|
+
import { checkRootReferences } from './tsconfig-references';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A package dir no reference can name. `checkRootReferences` answers `[]` when the root has no
|
|
11
|
+
* `references` array at all and one finding for this probe when it has one, which is the one
|
|
12
|
+
* question asked here — without a second JSONC parser beside the one that file owns.
|
|
13
|
+
*/
|
|
14
|
+
const PROBE = '\u0000x-verify-typecheck-probe';
|
|
15
|
+
|
|
16
|
+
/** True when the root tsconfig declares `references`: a build graph that only `-b` can walk. */
|
|
17
|
+
export const usesProjectReferences = async (root: string): Promise<boolean> =>
|
|
18
|
+
(await checkRootReferences(root, [PROBE])).length > 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The argv after `bunx`. A root with references keeps `-b` — the framework's 32 projects are only
|
|
22
|
+
* reachable that way, and `tsc -b` rebuilds a project whose own sources moved. A root without them
|
|
23
|
+
* (every scaffolded app) is ONE program, and `-p .` is the content-hashed check of it.
|
|
24
|
+
*/
|
|
25
|
+
export async function typecheckArgs(root: string, bin: string): Promise<readonly string[]> {
|
|
26
|
+
const mode = (await usesProjectReferences(root)) ? ['-b'] : ['-p', '.'];
|
|
27
|
+
return [bin, ...mode, '--pretty', 'false'];
|
|
28
|
+
}
|
package/src/web-binding.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// How a process binds its sockets, and what it admits about itself. A LEAF: it imports nothing,
|
|
2
|
-
// so every role can read it without pulling `
|
|
3
|
-
// `
|
|
2
|
+
// so every role can read it without pulling `role-start` — which is what made this its own file.
|
|
3
|
+
// `role-sync` needs the default and `role-start` already imports `role-sync`, so reading it from
|
|
4
4
|
// there would be a runtime import cycle in the framework's own boot path.
|
|
5
5
|
|
|
6
6
|
export interface WebBinding {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The page's framework scripts (plan 101), each built as its own classic-script browser bundle,
|
|
2
|
+
// addressed by a hash of its source graph, served `immutable` by the same route in `x dev` and the
|
|
3
|
+
// container: the ONE sync worker (`@ultimat3/realtime/sync-worker`, slice 11) and the ONE page boot
|
|
4
|
+
// (`@ultimat3/realtime/boot` — the disk restore and the outbox, once per page instead of once per
|
|
5
|
+
// island). A new deploy is a new URL, so an old tab keeps what it started with.
|
|
6
|
+
|
|
7
|
+
// why: Bun ships no path API; the entry is resolved to a file and named in the cause.
|
|
8
|
+
import { dirname, join, relative } from 'node:path';
|
|
9
|
+
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
10
|
+
import { applyCacheHeaders, json } from '@ultimat3/http';
|
|
11
|
+
import { FrameworkScriptBuildFailedError, type FrameworkScriptKind } from './errors';
|
|
12
|
+
import { describeBuildError, graphHash, stripDebugId } from './island-bundle';
|
|
13
|
+
|
|
14
|
+
/** Under the dev namespace `/_x` — where the socket it opens (`/_x/sync`) already lives. */
|
|
15
|
+
export const SYNC_WORKER_BASE_PATH = '/_x/sync-worker';
|
|
16
|
+
|
|
17
|
+
/** What an app resolves: realtime's worker entry, from the APP's install, never the CLI's own. */
|
|
18
|
+
export const SYNC_WORKER_SPECIFIER = '@ultimat3/realtime/sync-worker';
|
|
19
|
+
|
|
20
|
+
/** Under the dev namespace too; one per document that carries a principal scope. */
|
|
21
|
+
export const PAGE_BOOT_BASE_PATH = '/_x/page-boot';
|
|
22
|
+
|
|
23
|
+
/** Realtime's page boot, from the APP's install. */
|
|
24
|
+
export const PAGE_BOOT_SPECIFIER = '@ultimat3/realtime/boot';
|
|
25
|
+
|
|
26
|
+
/** One framework script: the sync worker or the page boot. */
|
|
27
|
+
export interface FrameworkScript {
|
|
28
|
+
/** `<base>/<hash>.js` — what the document names. */
|
|
29
|
+
readonly url: string;
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly bytes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The worker, by the name every caller already uses. */
|
|
35
|
+
export type SyncWorkerChunk = FrameworkScript;
|
|
36
|
+
|
|
37
|
+
export interface BuildSyncWorkerOptions {
|
|
38
|
+
/** An absolute entry path. Absent resolves `SYNC_WORKER_SPECIFIER` from `root`. */
|
|
39
|
+
readonly entry?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `undefined` when the app cannot resolve realtime's worker: an app with no realtime has no
|
|
44
|
+
* socket to share, and the tab-side host falls back to an in-page engine by design (slice 11's
|
|
45
|
+
* transparent fallback) — absence is an answer, not a failure.
|
|
46
|
+
*/
|
|
47
|
+
export function buildSyncWorker(
|
|
48
|
+
root: string,
|
|
49
|
+
options: BuildSyncWorkerOptions = {},
|
|
50
|
+
): Promise<FrameworkScript | undefined> {
|
|
51
|
+
return buildFrameworkScript(
|
|
52
|
+
root,
|
|
53
|
+
'sync worker',
|
|
54
|
+
SYNC_WORKER_SPECIFIER,
|
|
55
|
+
SYNC_WORKER_BASE_PATH,
|
|
56
|
+
options.entry,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The page boot, or `undefined` for an app with no realtime (nothing to restore, no outbox). A
|
|
62
|
+
* document then carries no boot script, and `pageRealtime().booted` answers at once.
|
|
63
|
+
*/
|
|
64
|
+
export function buildPageBoot(
|
|
65
|
+
root: string,
|
|
66
|
+
options: BuildSyncWorkerOptions = {},
|
|
67
|
+
): Promise<FrameworkScript | undefined> {
|
|
68
|
+
return buildFrameworkScript(
|
|
69
|
+
root,
|
|
70
|
+
'page boot',
|
|
71
|
+
PAGE_BOOT_SPECIFIER,
|
|
72
|
+
PAGE_BOOT_BASE_PATH,
|
|
73
|
+
options.entry,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function buildFrameworkScript(
|
|
78
|
+
root: string,
|
|
79
|
+
what: FrameworkScriptKind,
|
|
80
|
+
specifier: string,
|
|
81
|
+
basePath: string,
|
|
82
|
+
explicit: string | undefined,
|
|
83
|
+
): Promise<FrameworkScript | undefined> {
|
|
84
|
+
const entry = explicit ?? resolveEntry(root, specifier);
|
|
85
|
+
if (entry === undefined) return undefined;
|
|
86
|
+
const label = relative(root, entry);
|
|
87
|
+
let built: Awaited<ReturnType<typeof Bun.build>>;
|
|
88
|
+
try {
|
|
89
|
+
built = await Bun.build({
|
|
90
|
+
entrypoints: [entry],
|
|
91
|
+
target: 'browser',
|
|
92
|
+
// A CLASSIC script: `new SharedWorker(url, { name })` with no `type: 'module'` runs it, and a
|
|
93
|
+
// `<script defer>` runs it before any island module that follows it in the document.
|
|
94
|
+
format: 'iife',
|
|
95
|
+
splitting: false,
|
|
96
|
+
minify: true,
|
|
97
|
+
// `island-bundle.ts`'s reasons, verbatim: a chunk is only ever built to be shipped, and the
|
|
98
|
+
// map's `sourcesContent` is the one stable identity a minified bundle has.
|
|
99
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
100
|
+
sourcemap: 'external',
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new FrameworkScriptBuildFailedError({
|
|
104
|
+
what,
|
|
105
|
+
entry: label,
|
|
106
|
+
logs: describeBuildError(error),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const output = built.outputs.find((artifact) => artifact.kind === 'entry-point');
|
|
110
|
+
const map = built.outputs.find((artifact) => artifact.kind === 'sourcemap');
|
|
111
|
+
if (!built.success || output === undefined || map === undefined) {
|
|
112
|
+
throw new FrameworkScriptBuildFailedError({
|
|
113
|
+
what,
|
|
114
|
+
entry: label,
|
|
115
|
+
logs: built.logs.map((log) => String(log)).join('; '),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const code = stripDebugId(await output.text());
|
|
119
|
+
return {
|
|
120
|
+
url: `${basePath}/${graphHash(specifier, await map.text())}.js`,
|
|
121
|
+
code,
|
|
122
|
+
bytes: new TextEncoder().encode(code).byteLength,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The app's realtime, from the root or — in a workspace app, where `@ultimat3/realtime` is a
|
|
128
|
+
* dependency of `apps/<app>` and not of the root (`examples/dummy`) — from the first app that has
|
|
129
|
+
* it: where its islands resolve it, which is the copy the scripts must match. Resolving from the
|
|
130
|
+
* root alone built no worker and no boot for such an app, silently.
|
|
131
|
+
*/
|
|
132
|
+
function resolveEntry(root: string, specifier: string): string | undefined {
|
|
133
|
+
for (const dir of [root, ...appDirs(root)]) {
|
|
134
|
+
try {
|
|
135
|
+
return Bun.resolveSync(specifier, dir);
|
|
136
|
+
} catch {
|
|
137
|
+
// Not installed here, or a realtime that predates the export: try the next app.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appDirs(root: string): readonly string[] {
|
|
144
|
+
const glob = new Bun.Glob('apps/*/package.json');
|
|
145
|
+
return [...glob.scanSync({ cwd: root, onlyFiles: true })]
|
|
146
|
+
.sort()
|
|
147
|
+
.map((file) => join(root, dirname(file)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The script a build produced, read per request so `x dev` and the container share one route. */
|
|
151
|
+
export type SyncWorkerSource = () => FrameworkScript | undefined;
|
|
152
|
+
|
|
153
|
+
export function syncWorkerRoutes(source: SyncWorkerSource): readonly Route[] {
|
|
154
|
+
return [scriptRoute(SYNC_WORKER_BASE_PATH, 'assets.sync-worker', 'sync worker', source)];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function pageBootRoutes(source: SyncWorkerSource): readonly Route[] {
|
|
158
|
+
return [scriptRoute(PAGE_BOOT_BASE_PATH, 'assets.page-boot', 'page boot', source)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function scriptRoute(
|
|
162
|
+
basePath: string,
|
|
163
|
+
name: string,
|
|
164
|
+
what: string,
|
|
165
|
+
source: SyncWorkerSource,
|
|
166
|
+
): Route {
|
|
167
|
+
return {
|
|
168
|
+
method: 'GET',
|
|
169
|
+
path: `${basePath}/:file`,
|
|
170
|
+
meta: { name, auth: 'public', tags: ['assets'] },
|
|
171
|
+
handler: (request: UltimateRequest): Response => {
|
|
172
|
+
const script = source();
|
|
173
|
+
if (script === undefined || script.url !== request.pathname) {
|
|
174
|
+
return json(
|
|
175
|
+
{
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: 'X_ROUTE_NOT_FOUND',
|
|
179
|
+
cause: `no ${what} is built at ${request.pathname} — the document that named it was rendered against another build`,
|
|
180
|
+
fix: `reload the page — this process serves only the ${what} it built`,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{ status: 404 },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return applyCacheHeaders(
|
|
187
|
+
new Response(script.code, { headers: { 'content-type': 'text/javascript' } }),
|
|
188
|
+
{ mode: 'immutable' },
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
package/src/workspace-graph.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
|
+
import { scanAllImports } from './import-scan';
|
|
8
9
|
import type { Finding } from './output';
|
|
9
10
|
import { eachSourceFile, isGenerated, isTest, isVendored } from './source-files';
|
|
10
|
-
import { maskLiterals } from './ts-scan';
|
|
11
11
|
|
|
12
12
|
export interface WorkspaceNode {
|
|
13
13
|
/** package.json "name". */
|
|
@@ -144,43 +144,20 @@ export function packageOfSpecifier(specifier: string): string | undefined {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Every
|
|
148
|
-
*
|
|
149
|
-
*
|
|
147
|
+
* Every package a source file imports, deduplicated, in first-appearance order — type-only imports
|
|
148
|
+
* included, because a type edge still needs its manifest line. The transpiler is the parser, so a
|
|
149
|
+
* generator's template literal (even one nested in a `${…}`, #493) is a string and never an import,
|
|
150
|
+
* and `from<Row>('posts')` is a call. A file it cannot parse throws rather than reading as empty.
|
|
150
151
|
*/
|
|
151
|
-
|
|
152
|
-
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|(?:^|[;{])\s*import\s+)['"]([^'"]*)['"]/g;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Every package a source file imports, deduplicated, in first-appearance order.
|
|
156
|
-
*
|
|
157
|
-
* Read from the MASKED text, because a generator's template literal holds whole programs: every
|
|
158
|
-
* `templates/*.ts` here emits `import … from '@ultimat3/ui'` as a string, and a scan that read
|
|
159
|
-
* those would bill the CLI for the imports of the app it writes. The line a masked hit falls on is
|
|
160
|
-
* then re-read from the real source — the specifier itself is what masking blanks.
|
|
161
|
-
*/
|
|
162
|
-
export function importedPackages(source: string): readonly string[] {
|
|
163
|
-
const masked = maskLiterals(source).split('\n');
|
|
164
|
-
const lines = source.split('\n');
|
|
152
|
+
export function importedPackages(source: string, path = 'source.ts'): readonly string[] {
|
|
165
153
|
const packages = new Set<string>();
|
|
166
|
-
for (const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (line === undefined || [...maskedLine.matchAll(IMPORT_FORM)].length === 0) continue;
|
|
170
|
-
for (const match of line.matchAll(IMPORT_FORM)) {
|
|
171
|
-
const name = packageOfSpecifier(match[1] ?? '');
|
|
172
|
-
if (name !== undefined) packages.add(name);
|
|
173
|
-
}
|
|
154
|
+
for (const specifier of scanAllImports({ path, source })) {
|
|
155
|
+
const name = packageOfSpecifier(specifier);
|
|
156
|
+
if (name !== undefined) packages.add(name);
|
|
174
157
|
}
|
|
175
158
|
return [...packages];
|
|
176
159
|
}
|
|
177
160
|
|
|
178
|
-
/**
|
|
179
|
-
* Borrowed, not twinned: `X_APP_PACKAGE_INVALID` already means "this package.json supplies no
|
|
180
|
-
* usable name", and a workspace manifest is that same file one directory down. `bun pm pkg set`
|
|
181
|
-
* is not the fix here — it parses the file it edits, so it fails on exactly the input this
|
|
182
|
-
* reports.
|
|
183
|
-
*/
|
|
184
161
|
export const unreadableWorkspaceFinding = (path: string): Finding => ({
|
|
185
162
|
code: 'X_APP_PACKAGE_INVALID',
|
|
186
163
|
cause: `${path} is claimed by the root "workspaces" globs and supplies no readable "name"`,
|
|
@@ -228,7 +205,7 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
|
|
|
228
205
|
if (isTest(path) || isGenerated(path)) continue;
|
|
229
206
|
const owner = owners.find((node) => path.startsWith(`${node.dir}/`));
|
|
230
207
|
if (owner === undefined) continue;
|
|
231
|
-
for (const name of importedPackages(await Bun.file(join(root, path)).text())) {
|
|
208
|
+
for (const name of importedPackages(await Bun.file(join(root, path)).text(), path)) {
|
|
232
209
|
const target = byName.get(name);
|
|
233
210
|
if (target === undefined || target.name === owner.name) continue;
|
|
234
211
|
if (owner.dependencies.includes(target.name)) continue;
|