@ultimat3/cli 23.0.0 → 25.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +57 -79
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -33
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/naming.ts +28 -0
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Single responsibility: a declaration site — a stack FRAME `@ultimat3/policy` recorded for a
|
|
2
|
+
// `defineRoles()` or `definePermissions()` call — reduced to the file it names, absolute or
|
|
3
|
+
// app-root-relative. Both `policy` step questions key their findings by it.
|
|
4
|
+
|
|
5
|
+
// why: Bun ships no path API, and `posix.relative` is what turns a frame's absolute path into the
|
|
6
|
+
// app-root-relative one every finding is keyed by — on POSIX-normalised input, so a Windows frame
|
|
7
|
+
// and root compare the same way on every host.
|
|
8
|
+
import { posix } from 'node:path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The location at the END of a frame — `at fn (<path>:<line>:<col>)` or `at <path>:<line>:<col>` —
|
|
12
|
+
* read lazily from the first place a path can start, so a `(` INSIDE the path (`/srv/shop (copy)/`)
|
|
13
|
+
* is part of it, and a drive letter (`C:\…`) is a path start too. Anchored at the end because a
|
|
14
|
+
* frame's location is always its last token, and the function name before it is not a path.
|
|
15
|
+
*/
|
|
16
|
+
const FRAME_LOCATION = /(?:^|[\s(])((?:[A-Za-z]:)?[\\/].*?\.[cm]?tsx?)(?::\d+){0,2}\)?\s*$/;
|
|
17
|
+
|
|
18
|
+
/** The absolute source path a frame names, exactly as written, or `undefined`. */
|
|
19
|
+
export function frameFile(site: string): string | undefined {
|
|
20
|
+
return FRAME_LOCATION.exec(site)?.[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `\` → `/`, and a drive letter rooted like a POSIX path with its case folded (`C:\a` → `/c:/a`):
|
|
25
|
+
* `@ultimat3/policy`'s `declaration-site.ts` compares frames `/`-normalised for the same reason —
|
|
26
|
+
* on Windows neither a frame nor a root holds a `/`, and a shell spells the drive either way.
|
|
27
|
+
*/
|
|
28
|
+
const comparable = (path: string): string => {
|
|
29
|
+
const slashed = path.replaceAll('\\', '/');
|
|
30
|
+
const drive = /^([A-Za-z]):\//.exec(slashed);
|
|
31
|
+
return drive === null ? slashed : `/${(drive[1] ?? '').toLowerCase()}:${slashed.slice(2)}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** `file` relative to `root`, `/`-separated, whichever OS printed either of them. */
|
|
35
|
+
export const rootRelative = (root: string, file: string): string =>
|
|
36
|
+
posix.relative(comparable(root), comparable(file));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The frame's file relative to the app root, or `undefined` outside it: a locator nobody can act on
|
|
40
|
+
* is worse than none. The frame is the only place the declaring module's name exists by the time
|
|
41
|
+
* the declaration runs.
|
|
42
|
+
*/
|
|
43
|
+
export function siteFile(root: string, site: string): string | undefined {
|
|
44
|
+
const absolute = frameFile(site);
|
|
45
|
+
if (absolute === undefined) return undefined;
|
|
46
|
+
const rel = rootRelative(root, absolute);
|
|
47
|
+
return rel.startsWith('..') || rel.length === 0 || rel.startsWith('/') ? undefined : rel;
|
|
48
|
+
}
|
package/src/app-permissions.ts
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
// required it on `/dashboard`, declared it nowhere, and served HTTP 500 on two of its three routes
|
|
8
8
|
// with `x verify` green (#F1). This is the question nothing asked.
|
|
9
9
|
|
|
10
|
-
// why: Bun ships no path API, and `relative` is what turns a stack frame's absolute path into
|
|
11
|
-
// the app-root-relative one every finding is keyed by.
|
|
12
|
-
import { relative } from 'node:path';
|
|
13
10
|
import {
|
|
14
11
|
grantMatches,
|
|
15
12
|
isKnownPermission,
|
|
@@ -21,6 +18,8 @@ import {
|
|
|
21
18
|
import { routeEntries } from '@ultimat3/render';
|
|
22
19
|
import { definedStorage } from '@ultimat3/storage';
|
|
23
20
|
import { loadApp } from './app-load';
|
|
21
|
+
import { borrowedFinding, borrowedPermissions, permissionsFile } from './app-permissions-borrowed';
|
|
22
|
+
import { siteFile } from './app-permissions-site';
|
|
24
23
|
import type { DuplicateProbe } from './duplicate-packages';
|
|
25
24
|
import { duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
|
|
26
25
|
import type { Finding } from './output';
|
|
@@ -45,19 +44,6 @@ export interface PermissionReference {
|
|
|
45
44
|
/** What this check needs of a boot — the seam `i18n-registration.ts` already established. */
|
|
46
45
|
export type AppLoader = (root: string) => Promise<{ readonly findings: readonly Finding[] }>;
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
* A role's declaration site is a stack FRAME (`at foo (/abs/path.ts:3:1)`), which is the only
|
|
50
|
-
* place the declaring module's name exists by the time `defineRoles` runs. Reduced to the
|
|
51
|
-
* app-relative path an agent opens, or dropped: a locator nobody can act on is worse than none.
|
|
52
|
-
*/
|
|
53
|
-
export function siteFile(root: string, site: string): string | undefined {
|
|
54
|
-
const match = /\(?(\/[^():]+\.[cm]?tsx?)(?::\d+)?(?::\d+)?\)?/.exec(site);
|
|
55
|
-
const absolute = match?.[1];
|
|
56
|
-
if (absolute === undefined) return undefined;
|
|
57
|
-
const rel = relative(root, absolute).replaceAll('\\', '/');
|
|
58
|
-
return rel.startsWith('..') || rel.length === 0 ? undefined : rel;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
47
|
/** Every permission a registered role grants, with where that role was declared. */
|
|
62
48
|
export function grantedReferences(root: string): readonly PermissionReference[] {
|
|
63
49
|
const sites = roleDeclarationSites();
|
|
@@ -198,7 +184,11 @@ export async function policyFindings(
|
|
|
198
184
|
unknown.length === 0
|
|
199
185
|
? ungrantedRequirements().map((rule) => ungrantedFinding(rule, rolesAt))
|
|
200
186
|
: [];
|
|
201
|
-
|
|
187
|
+
// Asked of KNOWN names only (`borrowedPermissions` skips the rest), so it rides beside the
|
|
188
|
+
// unknown half rather than behind it: one fault, one finding.
|
|
189
|
+
const declaredIn = permissionsFile(root);
|
|
190
|
+
const borrowed = borrowedPermissions(root).map((entry) => borrowedFinding(entry, declaredIn));
|
|
191
|
+
const findings = [...duplicated, ...unknown, ...borrowed, ...ungranted];
|
|
202
192
|
return findings.length === 0 ? [] : [...findings, ...app.findings];
|
|
203
193
|
}
|
|
204
194
|
|
package/src/app-root.ts
CHANGED
|
@@ -20,10 +20,14 @@ export const MANIFEST_FILE = 'x.manifest.json';
|
|
|
20
20
|
* the gate's dominant step, with `x doctor` reporting the runtime as fine. `--parallel` arrived in
|
|
21
21
|
* the same release and is emitted now, so the floor may never fall below that patch.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `BUN_TEST_WORKER_ID`'s numbering, probed on 1.4
|
|
26
|
-
*
|
|
23
|
+
* A 1.4 patch rather than `1.3.13` because a floor is a claim about a runtime somebody TESTED: CI
|
|
24
|
+
* pins one exact patch, both images build on that patch's `oven/bun:1.4-*` digest, and the
|
|
25
|
+
* per-worker database rests on `BUN_TEST_WORKER_ID`'s numbering, probed on 1.4 and on nothing older.
|
|
26
|
+
* It moved 1.4.0 → 1.4.2 with CI's pin (plan 2026/10/04/101, sweep 3): from then on 1.4.0 was a
|
|
27
|
+
* runtime nothing here ran, and the bundler defects fixed in 1.4.1 (oven-sh/bun#40578, #40650,
|
|
28
|
+
* #40657) change what `x build` emits. `scaffold-repo.ts` and the scaffold's CI read this constant,
|
|
29
|
+
* so a new app's `engines.bun` and its CI runtime move with it. `scripts/bun-pin.test.ts` holds it
|
|
30
|
+
* to the exact patch CI runs.
|
|
27
31
|
*
|
|
28
32
|
* **Lowering it to 1.3.14 was tried on 2026-08-27 and refused**, and the argument for trying was
|
|
29
33
|
* sound — `--isolate` and `--parallel` are 1.3.13 features, no package here calls a 1.4-only API
|
|
@@ -34,7 +38,7 @@ export const MANIFEST_FILE = 'x.manifest.json';
|
|
|
34
38
|
* declared supported would hang on graceful shutdown the moment its database went away. The full
|
|
35
39
|
* measurement is in `.github/actions/setup/action.yml`; read it before lowering this.
|
|
36
40
|
*/
|
|
37
|
-
export const REQUIRED_BUN = '1.4.
|
|
41
|
+
export const REQUIRED_BUN = '1.4.2';
|
|
38
42
|
|
|
39
43
|
export interface AppRoot {
|
|
40
44
|
readonly dir: string;
|
package/src/bin.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { dispatch } from './dispatch';
|
|
6
6
|
import { resolveLocalCli } from './local-cli';
|
|
7
|
+
import { handOff } from './local-cli-handoff';
|
|
7
8
|
import { cwdFromArgv } from './root-env';
|
|
8
9
|
// The writes themselves are `write-line.ts`: `create-ultimate`'s entry point needs the identical
|
|
9
10
|
// one, and a second copy of a note about pipe truncation is a second copy that drifts. Two sinks,
|
|
@@ -24,13 +25,8 @@ const local = resolveLocalCli({
|
|
|
24
25
|
});
|
|
25
26
|
if (local !== undefined) {
|
|
26
27
|
writeErrorLine(`x: using the app's own @ultimat3/cli at ${local}`);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
stdout: 'inherit',
|
|
30
|
-
stderr: 'inherit',
|
|
31
|
-
env: Bun.env,
|
|
32
|
-
});
|
|
33
|
-
process.exit(await child.exited);
|
|
28
|
+
// Stop signals are forwarded and the child's code is ours: see local-cli-handoff.ts.
|
|
29
|
+
process.exit(await handOff([process.execPath, local, ...Bun.argv.slice(2)], Bun.env));
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
// `x dev` supervises a child that serves (`dev-supervisor.ts`): a save that reaches a module which
|
package/src/browser-launcher.ts
CHANGED
|
@@ -39,7 +39,8 @@ export class ShotChromeMissingError extends UltimateError {
|
|
|
39
39
|
// One literal, so `fix-scan.ts` can read it. A path is the one repair that works whatever the
|
|
40
40
|
// browser is and wherever the distribution put it; `--cdp-url` is the answer for a box that
|
|
41
41
|
// will never have one, and it is a flag `x shot` already ships.
|
|
42
|
-
|
|
42
|
+
// The `#` names the macOS and Windows spellings: one literal for every platform.
|
|
43
|
+
fix: 'export CHROME_PATH=/usr/bin/google-chrome # any Chrome, Chromium or Edge binary — macOS: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; Windows PowerShell: $env:CHROME_PATH = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" (Edge: "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"); a box that will never have one attaches instead: x shot / --cdp-url wss://cdp.example.com/session/abc',
|
|
43
44
|
meta: { tried: [...CHROME_CANDIDATES], vars: [...BROWSER_PATH_VARS] },
|
|
44
45
|
});
|
|
45
46
|
}
|
|
@@ -67,7 +68,8 @@ export const browserBinaryExists = (path: string): boolean => existsSync(path);
|
|
|
67
68
|
/**
|
|
68
69
|
* The path a run will launch, or `undefined` when this machine has no browser to launch.
|
|
69
70
|
*
|
|
70
|
-
* The last step is a PROBE — the
|
|
71
|
+
* The last step is a PROBE — the paths `cdp-launch.ts` tries for the e2e driver on THIS platform
|
|
72
|
+
* (`/usr/bin` on Linux, the app bundles on macOS, Chrome and Edge on Windows), imported
|
|
71
73
|
* rather than restated, because two lists of Chrome locations that must agree is the drift axiom 2
|
|
72
74
|
* refuses — so a box with no browser is refused before a dev-server boot, not one launch later.
|
|
73
75
|
*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What a route's measured JavaScript was made of, file by file: the clause and the `--json` facts
|
|
2
|
+
// `X_BUDGET_EXCEEDED` carries, so a reader can tell a heavier chunk from a byte counted twice
|
|
3
|
+
// without re-running the build. Split from `budgets.ts`, which measures and compares.
|
|
4
|
+
|
|
5
|
+
import type { JsonValue } from './output';
|
|
6
|
+
|
|
7
|
+
/** One app file a document made the browser fetch, and its bytes on disk. */
|
|
8
|
+
export interface ChargedFile {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly bytes: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Named in the cause; the rest are summed. A page carries one or two files in practice, and a
|
|
15
|
+
* sentence listing forty is one nobody reads — `meta.charged` still holds every one of them.
|
|
16
|
+
*/
|
|
17
|
+
const NAMED_FILES = 5;
|
|
18
|
+
|
|
19
|
+
/** Heaviest first, then by URL, so the clause and the stats file are reproducible. */
|
|
20
|
+
export function heaviestFirst(files: readonly ChargedFile[]): readonly ChargedFile[] {
|
|
21
|
+
return [...files].sort((a, b) =>
|
|
22
|
+
b.bytes !== a.bytes ? b.bytes - a.bytes : a.url < b.url ? -1 : a.url > b.url ? 1 : 0,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `; charged: /islands/post-form-04f75f18.js 81306 B, inline scripts 855 B` — exact bytes, never
|
|
28
|
+
* `formatBytes`: a 22 kB disagreement between two platforms is the question this answers, and a
|
|
29
|
+
* rounded figure hides the byte a duplicate shows up as. Empty for a row an older build wrote.
|
|
30
|
+
*/
|
|
31
|
+
export function chargedClause(
|
|
32
|
+
charged: readonly ChargedFile[] | undefined,
|
|
33
|
+
inlineBytes: number | undefined,
|
|
34
|
+
): string {
|
|
35
|
+
const files = heaviestFirst(charged ?? []);
|
|
36
|
+
const parts = files.slice(0, NAMED_FILES).map((one) => `${one.url} ${String(one.bytes)} B`);
|
|
37
|
+
const rest = files.slice(NAMED_FILES);
|
|
38
|
+
if (rest.length > 0) {
|
|
39
|
+
const bytes = rest.reduce((sum, one) => sum + one.bytes, 0);
|
|
40
|
+
parts.push(`${String(rest.length)} more files ${String(bytes)} B`);
|
|
41
|
+
}
|
|
42
|
+
if (inlineBytes !== undefined && inlineBytes > 0) {
|
|
43
|
+
parts.push(`inline scripts ${String(inlineBytes)} B`);
|
|
44
|
+
}
|
|
45
|
+
return parts.length === 0 ? '' : `; charged: ${parts.join(', ')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The same facts as data, or `undefined` when the row predates them — absent is not empty. */
|
|
49
|
+
export function chargedMeta(
|
|
50
|
+
jsBytes: number,
|
|
51
|
+
charged: readonly ChargedFile[] | undefined,
|
|
52
|
+
inlineBytes: number | undefined,
|
|
53
|
+
): { readonly [key: string]: JsonValue } | undefined {
|
|
54
|
+
if (charged === undefined && inlineBytes === undefined) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
jsBytes,
|
|
57
|
+
charged: heaviestFirst(charged ?? []).map((one) => ({ url: one.url, bytes: one.bytes })),
|
|
58
|
+
inlineJsBytes: inlineBytes ?? 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// How the budget measurer READS an emitted document: which tags are code, which attribute names an
|
|
2
|
+
// island entry, which path a URL is fetched at, and which files a module makes the browser load.
|
|
3
|
+
// Split from `budgets.ts`, which decides what of that is charged and compares it to the budget.
|
|
4
|
+
|
|
5
|
+
import { SPECULATION_RULES_TYPE } from '@ultimat3/render';
|
|
6
|
+
|
|
7
|
+
export const SCRIPT_TAG = /<script(?<attrs>[^>]*)>(?<body>[\s\S]*?)<\/script>/g;
|
|
8
|
+
export const SRC_ATTR = /\ssrc="(?<src>[^"]*)"/;
|
|
9
|
+
const TYPE_ATTR = /\stype="(?<type>[^"]*)"/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `application/ld+json`, `application/json`, any `…+json`: the body is data, not code — the rule
|
|
13
|
+
* `@ultimat3/render`'s `head.ts` already states, restated because its `carriesJson` reads a
|
|
14
|
+
* `HeadTag` and is not exported, and this side has an attribute string off the emitted document.
|
|
15
|
+
* Without it a page shipping only `meta.ld` structured data and island props measured 8kb of JS
|
|
16
|
+
* and failed a 2kb budget with a `fix:` naming an import chain that does not exist.
|
|
17
|
+
*/
|
|
18
|
+
export const carriesJson = (attrs: string): boolean => {
|
|
19
|
+
// Everything from the first `;` is a MIME PARAMETER and not the type: a real document writes
|
|
20
|
+
// `type="application/ld+json; charset=utf-8"`, which does not END with `json`, so the suffix
|
|
21
|
+
// test alone charged an SEO structured-data block as executable JavaScript all over again.
|
|
22
|
+
const [type = ''] = (TYPE_ATTR.exec(attrs)?.groups?.['type'] ?? '').split(';');
|
|
23
|
+
const declared = type.trim().toLowerCase();
|
|
24
|
+
// Speculation rules are JSON under a type that does not say so: read by the browser's
|
|
25
|
+
// prefetcher, never executed — the one script a 0kb page may carry for free.
|
|
26
|
+
return declared.endsWith('json') || declared === SPECULATION_RULES_TYPE;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An island's chunk is reached by `import()` from inside the hydration runtime, so it never appears
|
|
31
|
+
* as a `<script src>` — and a document weighed by script tags alone was charged for the runtime and
|
|
32
|
+
* never for the code that runtime boots. The entry attribute is that module URL, so it is read as
|
|
33
|
+
* exactly what it is: a file the browser will execute.
|
|
34
|
+
*/
|
|
35
|
+
export const ENTRY_ATTR = /\sdata-x-entry="(?<url>[^"]*)"/g;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Static imports and `import()` both. A chunk reached by `import()` was INLINED into its island
|
|
39
|
+
* before islands were split, so charging it keeps a route's number what it was; and it is fetched
|
|
40
|
+
* the moment the island needs it, which is before the page does what it was loaded for.
|
|
41
|
+
*/
|
|
42
|
+
const LOADS: ReadonlySet<string> = new Set(['import-statement', 'dynamic-import']);
|
|
43
|
+
|
|
44
|
+
/** The origin every artifact path is resolved against — a name that can never be a real host. */
|
|
45
|
+
const ARTIFACT_ORIGIN = 'https://artifact.invalid';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The path a browser fetches for a root-relative `url`, or `undefined` for one that is not this
|
|
49
|
+
* artifact's. Resolved with the WHATWG parser, the browser's own: it reads `\` as `/`, folds `.`
|
|
50
|
+
* and `..`, drops a query, and makes `//host/a.js` another origin. The dedupe below is keyed by
|
|
51
|
+
* this, because it is the FETCH that is counted once, and a raw string kept two spellings of one
|
|
52
|
+
* file apart — which `join` on Windows then turned back into one real path, charged twice.
|
|
53
|
+
*/
|
|
54
|
+
export function artifactPath(url: string): string | undefined {
|
|
55
|
+
if (!url.startsWith('/')) return undefined;
|
|
56
|
+
const resolved = URL.parse(url, ARTIFACT_ORIGIN);
|
|
57
|
+
return resolved?.origin === ARTIFACT_ORIGIN ? resolved.pathname : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Same-origin paths `code`, served at `url`, makes the browser load. Cross-origin and bare are not this build's. */
|
|
61
|
+
export function loadedBy(url: string, code: string): readonly string[] {
|
|
62
|
+
let imports: readonly { readonly path: string; readonly kind: string }[];
|
|
63
|
+
try {
|
|
64
|
+
imports = new Bun.Transpiler({ loader: 'js' }).scanImports(code);
|
|
65
|
+
} catch {
|
|
66
|
+
// A file that does not parse as JavaScript imports nothing this gate can name; its own bytes
|
|
67
|
+
// are already charged.
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const base = new URL(url, ARTIFACT_ORIGIN);
|
|
71
|
+
return imports
|
|
72
|
+
.filter((one) => LOADS.has(one.kind) && /^\.{0,2}\//.test(one.path))
|
|
73
|
+
.map((one) => new URL(one.path, base))
|
|
74
|
+
.filter((target) => target.origin === base.origin)
|
|
75
|
+
.map((target) => target.pathname);
|
|
76
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Single responsibility: the finding for a budgeted route a build RAN and could not weigh, read off
|
|
2
|
+
// the static report's `unmeasured` entry — under the failure's own code when that code is the
|
|
3
|
+
// instruction, as an actor edit when the measurement actor was refused, the report otherwise.
|
|
4
|
+
|
|
5
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
|
+
import type { Finding } from './output';
|
|
7
|
+
import type { UnmeasuredRoute } from './static-report';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ONE code a failed measurement is reported under by its own name rather than as
|
|
11
|
+
* `X_BUDGET_UNMEASURED`. Its cause is complete — the island, the prop, its bytes, the cap — and
|
|
12
|
+
* its fix is an edit to the page, so the step's own "run x build and read the list" would put a
|
|
13
|
+
* second command between the author and a sentence the build had already composed. Every other
|
|
14
|
+
* render failure keeps the generic finding: a `TypeError` from a `load` that wanted a request is
|
|
15
|
+
* a reason to read the report, not an instruction.
|
|
16
|
+
*
|
|
17
|
+
* ONE code and not "any coded error", deliberately. `X_NO_CONTEXT` and `X_DB_UNAVAILABLE` from a
|
|
18
|
+
* measurement render are facts about the BUILD's environment, and reporting them under their own
|
|
19
|
+
* codes would tell the author to fix a database the gate never had. The list grows by a decision,
|
|
20
|
+
* per code, here.
|
|
21
|
+
*/
|
|
22
|
+
const REPORTED_BY_OWN_CODE: ReadonlySet<string> = new Set([
|
|
23
|
+
'X_ISLAND_PROPS_INVALID',
|
|
24
|
+
// A dynamic route that lists no `prerender()` path: the edit is in the route, not the build.
|
|
25
|
+
'X_BUDGET_PARAMS_UNDECLARED',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Refusals of WHO the render ran as. The actor is the app's own declaration (core's
|
|
30
|
+
* `defineMeasurementActor`, in `app.config.ts`), so these ARE an instruction — but the refusal's
|
|
31
|
+
* own `fix:` was written for a request ("sign in", "mint the actor with its tenant at the request
|
|
32
|
+
* boundary") and names no build-time edit. The default actor is a service actor holding `*` with
|
|
33
|
+
* no org and no roles: a page whose policy names a role, or whose `load` reads a tenant-scoped
|
|
34
|
+
* entity, is refused under it (#675).
|
|
35
|
+
*/
|
|
36
|
+
const ACTOR_REFUSALS: ReadonlySet<string> = new Set([
|
|
37
|
+
'X_FORBIDDEN',
|
|
38
|
+
'X_UNAUTHENTICATED',
|
|
39
|
+
'X_TENANCY_ACTOR_ORG_REQUIRED',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function ownCodeFinding(url: string, entry: UnmeasuredRoute): Finding {
|
|
43
|
+
return {
|
|
44
|
+
code: entry.code ?? 'X_BUDGET_UNMEASURED',
|
|
45
|
+
cause: entry.cause ?? entry.reason,
|
|
46
|
+
fix: entry.fix ?? `x build --target static --json # its "unmeasured" list has ${url}`,
|
|
47
|
+
docs: ERROR_DOCS_URL,
|
|
48
|
+
at: url,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `CODE: cause` when the failure had a code, its rendered reason otherwise. */
|
|
53
|
+
const refusalOf = (entry: UnmeasuredRoute): string =>
|
|
54
|
+
entry.code === undefined ? entry.reason : `${entry.code}: ${entry.cause ?? entry.reason}`;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A build ran and has no row for `url`. `entry` is the report's account of why, when it has one:
|
|
58
|
+
* an instruction under its own code, an actor edit, or — for a failure that is a fact about the
|
|
59
|
+
* build's environment — the report, with the refusal already in the cause.
|
|
60
|
+
*/
|
|
61
|
+
export function builtUnmeasuredFinding(
|
|
62
|
+
url: string,
|
|
63
|
+
declared: string,
|
|
64
|
+
statsFile: string,
|
|
65
|
+
entry: UnmeasuredRoute | undefined,
|
|
66
|
+
): Finding {
|
|
67
|
+
if (entry?.code !== undefined && REPORTED_BY_OWN_CODE.has(entry.code)) {
|
|
68
|
+
return ownCodeFinding(url, entry);
|
|
69
|
+
}
|
|
70
|
+
const base = `${url} declares a ${declared} budget and ${statsFile} has no row for it, so the build ran and could not weigh it`;
|
|
71
|
+
if (entry?.code !== undefined && ACTOR_REFUSALS.has(entry.code)) {
|
|
72
|
+
const actor = entry.actor ?? 'the measurement actor';
|
|
73
|
+
return {
|
|
74
|
+
code: 'X_BUDGET_UNMEASURED',
|
|
75
|
+
cause: `${base}: rendered as ${actor}, it was refused ${refusalOf(entry)}`,
|
|
76
|
+
// The paste: one declaration in the one file every build imports.
|
|
77
|
+
fix: "defineMeasurementActor(() => userActor({ id: 'measure', orgId: '<an org your dev seed creates>', roles: ['<the role this page requires>'] })) # in app.config.ts",
|
|
78
|
+
docs: ERROR_DOCS_URL,
|
|
79
|
+
at: url,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
code: 'X_BUDGET_UNMEASURED',
|
|
84
|
+
cause: entry === undefined ? base : `${base}: ${refusalOf(entry)}`,
|
|
85
|
+
// When a build already ran, the report is where the rest of the answer is — it names every
|
|
86
|
+
// route it could not weigh, and why.
|
|
87
|
+
fix: `x build --target static --json # its "unmeasured" list says why ${url} could not be weighed`,
|
|
88
|
+
docs: ERROR_DOCS_URL,
|
|
89
|
+
at: url,
|
|
90
|
+
};
|
|
91
|
+
}
|