@ultimat3/cli 29.4.0 → 30.1.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 +64 -40
- package/README.md +61 -11
- package/package.json +42 -28
- package/src/affected-tests.ts +97 -0
- package/src/affected-unowned.ts +43 -0
- package/src/affected.ts +25 -6
- package/src/app-boundaries.ts +29 -1
- package/src/app-evals.ts +12 -4
- package/src/app-load-unresolved.ts +62 -0
- package/src/app-load.ts +30 -37
- package/src/app-mcp.ts +14 -2
- package/src/app-module-graph.ts +232 -0
- package/src/app-reload-graph.ts +2 -2
- package/src/app-root.ts +37 -1
- package/src/bin.ts +1 -0
- package/src/boundary-findings.ts +6 -0
- package/src/budgets-freshness.ts +32 -0
- package/src/budgets.ts +20 -1
- package/src/build-capture-env.ts +9 -0
- package/src/cdp-shot-a11y.ts +2 -39
- package/src/ci-tested.ts +243 -0
- package/src/client-emit-customer-runtime.ts +139 -0
- package/src/client-emit-customer.ts +108 -0
- package/src/client-emit-drift.ts +72 -0
- package/src/client-emit-header.ts +67 -0
- package/src/client-emit-schemas.ts +175 -0
- package/src/client-emit-types.ts +121 -0
- package/src/client-emit-vendor.ts +146 -0
- package/src/client-errors-routes.ts +229 -0
- package/src/client-errors-script.ts +74 -0
- package/src/client-vitals-route.ts +197 -0
- package/src/client-vitals-script.ts +56 -0
- package/src/cmd-affected.ts +23 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +35 -9
- package/src/cmd-ci-spec.ts +42 -6
- package/src/cmd-ci.ts +99 -2
- package/src/cmd-client-customer.ts +134 -0
- package/src/cmd-client-source.ts +81 -0
- package/src/cmd-client-spec.ts +48 -0
- package/src/cmd-client-write.ts +41 -0
- package/src/cmd-client.ts +111 -0
- package/src/cmd-db-recount.ts +120 -0
- package/src/cmd-db-spec.ts +36 -6
- package/src/cmd-db.ts +11 -3
- package/src/cmd-deploy-compose.ts +61 -3
- package/src/cmd-deploy-drain.ts +10 -2
- package/src/cmd-deploy-helm.ts +11 -0
- package/src/cmd-deploy-preflight.ts +118 -0
- package/src/cmd-deploy-spec.ts +58 -6
- package/src/cmd-deploy-verify.ts +380 -0
- package/src/cmd-deploy.ts +42 -9
- package/src/cmd-docs.ts +34 -4
- package/src/cmd-doctor.ts +66 -9
- package/src/cmd-drill-spec.ts +25 -0
- package/src/cmd-drill.ts +176 -0
- package/src/cmd-env-init.ts +102 -0
- package/src/cmd-env-spec.ts +10 -3
- package/src/cmd-env.ts +15 -10
- package/src/cmd-errors-recent.ts +60 -0
- package/src/cmd-errors-spec.ts +9 -1
- package/src/cmd-errors.ts +4 -0
- package/src/cmd-exercise-spec.ts +19 -0
- package/src/cmd-exercise.ts +129 -0
- package/src/cmd-generate-spec.ts +9 -1
- package/src/cmd-generate.ts +32 -27
- package/src/cmd-graph-load.ts +97 -0
- package/src/cmd-graph-spec.ts +16 -0
- package/src/cmd-graph.ts +73 -0
- package/src/cmd-i18n.ts +4 -1
- package/src/cmd-impact-spec.ts +13 -0
- package/src/cmd-impact.ts +64 -0
- package/src/cmd-jobs-resolve.ts +76 -0
- package/src/cmd-jobs-spec.ts +21 -2
- package/src/cmd-jobs.ts +10 -6
- package/src/cmd-mail-spec.ts +28 -0
- package/src/cmd-mail.ts +70 -0
- package/src/cmd-mcp-lock.ts +69 -0
- package/src/cmd-mcp-spec.ts +23 -4
- package/src/cmd-mcp.ts +17 -3
- package/src/cmd-new-spec.ts +8 -1
- package/src/cmd-new.ts +61 -9
- package/src/cmd-planned.ts +0 -6
- package/src/cmd-repro-spec.ts +31 -0
- package/src/cmd-repro.ts +174 -0
- package/src/cmd-stop-spec.ts +21 -0
- package/src/cmd-stop.ts +166 -0
- package/src/cmd-test-spec.ts +17 -4
- package/src/cmd-test.ts +70 -14
- package/src/cmd-upgrade-spec.ts +26 -0
- package/src/cmd-upgrade.ts +274 -0
- package/src/cmd-verify-spec.ts +3 -3
- package/src/db-backfill.ts +6 -0
- package/src/db-diagnostics.ts +106 -0
- package/src/db-generate.ts +9 -0
- package/src/db-reset-template.ts +1 -1
- package/src/db-rolling.ts +45 -0
- package/src/db-seed-generate.ts +188 -0
- package/src/deploy-verify-findings.ts +98 -0
- package/src/deploy-verify-flags.ts +109 -0
- package/src/deploy-verify-grade.ts +185 -0
- package/src/deploy-verify-queues.ts +53 -0
- package/src/dev-boot.ts +35 -15
- package/src/dev-child-watch.ts +16 -4
- package/src/dev-dashboard-guard.ts +8 -1
- package/src/dev-dashboard.ts +7 -0
- package/src/dev-lock-identity.ts +61 -0
- package/src/dev-lock-port-holder.ts +80 -0
- package/src/dev-lock-steal.ts +124 -0
- package/src/dev-lock.ts +92 -120
- package/src/dev-mail-templates.ts +51 -0
- package/src/dev-rebuild.ts +26 -2
- package/src/dev-route-table.ts +17 -0
- package/src/dev-supervisor.ts +5 -2
- package/src/dev-watch-tree.ts +11 -10
- package/src/dev-watch.ts +1 -1
- package/src/dispatch.ts +29 -6
- package/src/docs-section.ts +69 -0
- package/src/docs-sources.ts +100 -0
- package/src/doctor-auth.ts +11 -2
- package/src/doctor-chart-alerts.ts +55 -0
- package/src/doctor-pooler.ts +107 -0
- package/src/doctor-rolling.ts +35 -0
- package/src/doctor-row-security.ts +40 -0
- package/src/document-options.ts +13 -0
- package/src/drill-sync-lane.ts +25 -0
- package/src/drill-targets.ts +39 -0
- package/src/duplicate-packages.ts +1 -1
- package/src/env-production.ts +93 -0
- package/src/error-codes-shot.ts +59 -0
- package/src/error-codes-tooling.ts +29 -0
- package/src/error-codes.ts +100 -54
- package/src/error-contract.ts +2 -2
- package/src/errors.ts +22 -3
- package/src/exercise-database.ts +99 -0
- package/src/exercise-findings.ts +159 -0
- package/src/exercise-overrides.ts +42 -0
- package/src/exercise-registration.ts +58 -0
- package/src/exercise-static.ts +88 -0
- package/src/exercise-subject.ts +69 -0
- package/src/failure-capture-boot.ts +30 -0
- package/src/framework-env.ts +7 -1
- package/src/framework-schema.ts +72 -5
- package/src/generate-catalog-locales.ts +19 -0
- package/src/generate-chart-alerts.ts +156 -0
- package/src/generate-feature-required.ts +65 -0
- package/src/generate-feature.ts +83 -6
- package/src/generate-files.ts +24 -0
- package/src/generate-format-written.ts +32 -0
- package/src/generate-kinds.ts +6 -0
- package/src/generate-sign-in-config.ts +100 -0
- package/src/generate-sign-in-manifest.ts +28 -0
- package/src/generate-sign-in-mode.ts +101 -0
- package/src/generate-sign-in.ts +250 -0
- package/src/generate-write.ts +44 -7
- package/src/hand-rolled-socket.ts +72 -0
- package/src/i18n-app-key-shapes.ts +29 -0
- package/src/i18n-audit.ts +10 -3
- package/src/i18n-registration.ts +18 -3
- package/src/index.ts +2 -0
- package/src/island-bundle.ts +4 -3
- package/src/island-disposer.ts +116 -0
- package/src/island-realtime-state.ts +34 -6
- package/src/island-realtime.ts +6 -2
- package/src/island-routes.ts +3 -2
- package/src/isr-query-advice.ts +6 -11
- package/src/isr-query-guard.ts +4 -3
- package/src/job-budget.ts +95 -0
- package/src/jobs-json.ts +11 -0
- package/src/lint-findings.ts +104 -0
- package/src/local-cli.ts +34 -4
- package/src/loopback-host.ts +2 -2
- package/src/mail-preview-findings.ts +24 -0
- package/src/mcp-errors.ts +82 -2
- package/src/mcp-host.ts +11 -1
- package/src/mcp-lock-findings.ts +57 -0
- package/src/measure-paths.ts +12 -4
- package/src/measure-scope.ts +27 -13
- package/src/messages-client.ts +23 -0
- package/src/messages-db.ts +56 -0
- package/src/messages-gate.ts +107 -0
- package/src/messages-repro.ts +11 -0
- package/src/messages-shot.ts +51 -0
- package/src/messages-upgrade.ts +45 -0
- package/src/messages.ts +53 -178
- package/src/metrics-endpoint.ts +23 -5
- package/src/new-shapes-generate.ts +71 -0
- package/src/new-shapes.ts +63 -0
- package/src/openapi-compile-ir.ts +68 -0
- package/src/openapi-compile-schema.ts +284 -0
- package/src/openapi-compile.ts +273 -0
- package/src/openapi-refuse.ts +45 -0
- package/src/otlp-export.ts +2 -1
- package/src/output-findings.ts +123 -0
- package/src/output.ts +32 -8
- package/src/package-boundaries.ts +110 -0
- package/src/page-pins.ts +23 -0
- package/src/plain-routes.ts +13 -0
- package/src/prerender-entries.ts +59 -0
- package/src/prerender-heaviest.ts +27 -0
- package/src/prerender-preload.ts +6 -0
- package/src/prerender-report.ts +12 -0
- package/src/prerender-seo-reads.ts +38 -0
- package/src/prerender-served.ts +22 -7
- package/src/prerender-vitals.ts +42 -0
- package/src/prerender.ts +61 -32
- package/src/pwa-artifacts.ts +4 -3
- package/src/pwa-manifest-locales.ts +1 -1
- package/src/registry.ts +18 -0
- package/src/repro-bundle.ts +65 -0
- package/src/repro-errors.ts +47 -0
- package/src/repro-job.ts +72 -0
- package/src/repro-literal.ts +41 -0
- package/src/repro-request.ts +48 -0
- package/src/repro-write.ts +100 -0
- package/src/retain-sweep.ts +27 -0
- package/src/retained-assets-boot.ts +113 -0
- package/src/retained-assets-layout.ts +110 -0
- package/src/retained-assets-read.ts +142 -0
- package/src/retained-assets-sweep.ts +70 -0
- package/src/retained-assets.ts +171 -0
- package/src/revalidated-response.ts +8 -5
- package/src/role-replicator.ts +7 -1
- package/src/role-start-devices.ts +119 -0
- package/src/role-start-types.ts +6 -0
- package/src/role-start.ts +22 -5
- package/src/role-sync.ts +7 -1
- package/src/route-islands.ts +42 -0
- package/src/route-table.ts +2 -1
- package/src/row-security-boot.ts +58 -0
- package/src/row-security-wanted.ts +32 -0
- package/src/runtime-admin.ts +0 -1
- package/src/runtime-assets-site-cache.ts +55 -0
- package/src/runtime-assets-site.ts +136 -0
- package/src/runtime-assets.ts +99 -55
- package/src/runtime-bindings.ts +19 -3
- package/src/runtime-cache.ts +23 -14
- package/src/runtime-isr-outcome.ts +114 -60
- package/src/runtime-purge.ts +14 -2
- package/src/runtime-queue.ts +31 -4
- package/src/runtime-render-document.ts +204 -0
- package/src/runtime-render.ts +48 -198
- package/src/runtime-replica-position.ts +62 -0
- package/src/runtime-replica.ts +18 -3
- package/src/runtime-s3-path-style.ts +17 -0
- package/src/runtime-services.ts +19 -6
- package/src/runtime-statement-bound.ts +22 -0
- package/src/runtime-storage-upload.ts +9 -0
- package/src/runtime-storage.ts +41 -81
- package/src/schema-diff-views.ts +54 -0
- package/src/schema-diff.ts +21 -3
- package/src/schema-drift.ts +28 -9
- package/src/schema-dump-drift.ts +91 -7
- package/src/script-csp.ts +1 -1
- package/src/seo-cards.ts +26 -0
- package/src/seo-routes.ts +22 -8
- package/src/serve-boot.ts +54 -21
- package/src/serve-drain.ts +1 -0
- package/src/serve-env.ts +26 -8
- package/src/serve-retire.ts +12 -6
- package/src/serve-web.ts +42 -4
- package/src/serve.ts +9 -7
- package/src/site-asset-routes.ts +7 -1
- package/src/site-config.ts +4 -0
- package/src/site-llms-pages.ts +106 -0
- package/src/site-llms.ts +64 -0
- package/src/site-seo-write.ts +4 -2
- package/src/site-seo.ts +55 -6
- package/src/source-mtime.ts +50 -0
- package/src/static-capture-errors.ts +41 -0
- package/src/static-capture.ts +56 -0
- package/src/static-cards-serve.ts +39 -0
- package/src/static-cards.ts +93 -0
- package/src/static-document.ts +14 -13
- package/src/static-images.ts +115 -0
- package/src/stream-hole-scope.ts +55 -0
- package/src/style-bundle.ts +1 -1
- package/src/style-routes.ts +3 -2
- package/src/sw-precache-plan.ts +2 -2
- package/src/sw-routes.ts +2 -2
- package/src/templates/action-tests.ts +306 -0
- package/src/templates/action.ts +81 -269
- package/src/templates/backfill.ts +1 -1
- package/src/templates/entity.ts +21 -5
- package/src/templates/github/ci.yml.ts +8 -9
- package/src/templates/guard-bare-error.ts +2 -2
- package/src/templates/guard-error-declared.ts +263 -0
- package/src/templates/guard-native-confirm.ts +132 -0
- package/src/templates/guard-raw-fetch.ts +151 -0
- package/src/templates/guard-raw-interval.ts +121 -0
- package/src/templates/guard-repo-raw-sql.ts +71 -2
- package/src/templates/job.ts +1 -1
- package/src/templates/naming.ts +5 -0
- package/src/templates/pinned/docker-compose.yml +6 -0
- package/src/templates/pinned-images.ts +8 -0
- package/src/templates/policy.ts +85 -2
- package/src/templates/query.ts +70 -13
- package/src/templates/resource-create.ts +3 -1
- package/src/templates/resource-form-island.ts +15 -4
- package/src/templates/resource-page-db.ts +83 -0
- package/src/templates/resource-page.ts +161 -52
- package/src/templates/route.ts +4 -6
- package/src/templates/scaffold-app.ts +3 -0
- package/src/templates/scaffold-auth.ts +2 -1
- package/src/templates/scaffold-bin.ts +5 -52
- package/src/templates/scaffold-claude-agents.ts +19 -19
- package/src/templates/scaffold-claude-commands.ts +28 -31
- package/src/templates/scaffold-claude.ts +26 -23
- package/src/templates/scaffold-container-compose.ts +6 -1
- package/src/templates/scaffold-container.ts +24 -4
- package/src/templates/scaffold-dashboard-bare.ts +3 -3
- package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
- package/src/templates/scaffold-dashboard-example-view.ts +156 -0
- package/src/templates/scaffold-dashboard-example.ts +43 -131
- package/src/templates/scaffold-dashboard-shared.ts +3 -7
- package/src/templates/scaffold-db-client.ts +20 -6
- package/src/templates/scaffold-db-package.ts +5 -14
- package/src/templates/scaffold-docs.ts +73 -57
- package/src/templates/scaffold-domain-package.ts +18 -14
- package/src/templates/scaffold-entries.ts +7 -2
- package/src/templates/scaffold-exercise.ts +37 -0
- package/src/templates/scaffold-guards.ts +13 -1
- package/src/templates/scaffold-helm-alerts.ts +234 -0
- package/src/templates/scaffold-helm-templates.ts +7 -5
- package/src/templates/scaffold-helm.ts +46 -12
- package/src/templates/scaffold-i18n.ts +6 -2
- package/src/templates/scaffold-mcp-endpoint.ts +182 -0
- package/src/templates/scaffold-mcp-json.ts +31 -0
- package/src/templates/scaffold-mcp-package.ts +59 -56
- package/src/templates/scaffold-repo.ts +7 -2
- package/src/templates/scaffold-shell.ts +45 -79
- package/src/templates/scaffold-test-services.ts +13 -5
- package/src/templates/shapes/api-keys.ts +170 -0
- package/src/templates/shapes/api.ts +101 -0
- package/src/templates/shapes/blog-site.ts +340 -0
- package/src/templates/shapes/blog-slice.ts +265 -0
- package/src/templates/shapes/blog.ts +114 -0
- package/src/templates/shapes/dashboard-detail.ts +242 -0
- package/src/templates/shapes/dashboard.ts +53 -0
- package/src/templates/shapes/index.ts +17 -0
- package/src/templates/shapes/overlay.ts +99 -0
- package/src/templates/shapes/shared-edits.ts +46 -0
- package/src/templates/sign-in-action-tests.ts +237 -0
- package/src/templates/sign-in-authenticator.ts +112 -0
- package/src/templates/sign-in-oidc.ts +189 -0
- package/src/templates/sign-in-pages.ts +148 -0
- package/src/templates/sign-in-password.ts +216 -0
- package/src/templates/sign-in-shared.ts +268 -0
- package/src/templates/slice-foundation.ts +70 -15
- package/src/test-batches.ts +1 -1
- package/src/test-database.ts +1 -1
- package/src/test-shards.ts +7 -1
- package/src/test-workers.ts +18 -12
- package/src/tool-gate-names.ts +34 -0
- package/src/upgrade-agents-link.ts +49 -0
- package/src/upgrade-bun-floor.ts +225 -0
- package/src/upgrade-gate.ts +228 -0
- package/src/upgrade-pins.ts +63 -0
- package/src/upgrade-plan.ts +214 -0
- package/src/upgrade-registry.ts +53 -0
- package/src/upgrade-report.ts +139 -0
- package/src/upgrade-root.ts +74 -0
- package/src/verify-budgets-build.ts +118 -0
- package/src/verify-checks.ts +67 -24
- package/src/verify-coverage-run.ts +1 -1
- package/src/verify-e2e-lane.ts +41 -0
- package/src/verify-exercise.ts +64 -0
- package/src/verify-floor.ts +41 -0
- package/src/verify-merge.ts +19 -4
- package/src/verify-role-load.ts +2 -0
- package/src/verify-run.ts +28 -8
- package/src/verify-shard.ts +12 -2
- package/src/verify-skip-ceiling.ts +87 -0
- package/src/verify-step.ts +5 -0
- package/src/verify-tests.ts +25 -5
- package/src/view-drift.ts +55 -0
- package/src/worker-bundle.ts +3 -2
- package/src/workspace-graph.ts +38 -19
- package/src/write-generated.ts +43 -0
- package/src/zone-boot.ts +20 -0
- package/wiki/Actions.md +342 -0
- package/wiki/Admin-Dashboard.md +432 -0
- package/wiki/Agents.md +958 -0
- package/wiki/Auth.md +195 -0
- package/wiki/Bare-VM.md +111 -0
- package/wiki/Batching-And-Preloading.md +136 -0
- package/wiki/Building-Your-Own-Base.md +203 -0
- package/wiki/CI-Parallel-Gate.md +220 -0
- package/wiki/CLI-Reference.md +1894 -0
- package/wiki/Caching-And-Invalidation.md +255 -0
- package/wiki/Client-Data.md +194 -0
- package/wiki/Client-Navigation.md +513 -0
- package/wiki/Compared.md +93 -0
- package/wiki/Configuration.md +868 -0
- package/wiki/Contributing.md +12 -0
- package/wiki/Deploy-Recipes.md +122 -0
- package/wiki/Deployment.md +508 -0
- package/wiki/Entities-And-Migrations.md +1116 -0
- package/wiki/FAQ.md +175 -0
- package/wiki/Feature-Flags.md +65 -0
- package/wiki/Getting-Started.md +221 -0
- package/wiki/Home.md +160 -0
- package/wiki/I18n.md +239 -0
- package/wiki/Installation.md +206 -0
- package/wiki/Interface-Rules.md +339 -0
- package/wiki/Jobs-And-Workflows.md +783 -0
- package/wiki/Known-Gaps.md +185 -0
- package/wiki/MCP-And-AI.md +753 -0
- package/wiki/Mail.md +138 -0
- package/wiki/Migrating-An-Existing-App.md +594 -0
- package/wiki/Migrating-From-Django.md +54 -0
- package/wiki/Migrating-From-Laravel.md +53 -0
- package/wiki/Migrating-From-Node-Drizzle.md +53 -0
- package/wiki/Migrating-From-Rails.md +53 -0
- package/wiki/Migrations-And-Backfills.md +552 -0
- package/wiki/Money.md +338 -0
- package/wiki/N-Plus-One-Detection.md +197 -0
- package/wiki/Notify.md +208 -0
- package/wiki/Observability.md +245 -0
- package/wiki/Outbound-HTTP.md +109 -0
- package/wiki/PWA-And-Offline.md +343 -0
- package/wiki/Policies-And-Authz.md +251 -0
- package/wiki/Project-Layout.md +150 -0
- package/wiki/Queries-And-Live-Queries.md +429 -0
- package/wiki/Realtime.md +734 -0
- package/wiki/Resource-Management.md +121 -0
- package/wiki/Routes-And-Render-Modes.md +521 -0
- package/wiki/SEO.md +288 -0
- package/wiki/Scheduled-Tasks.md +175 -0
- package/wiki/Scraping.md +493 -0
- package/wiki/Shapes.md +63 -0
- package/wiki/Stability.md +97 -0
- package/wiki/Static-Assets.md +166 -0
- package/wiki/Storage-And-Uploads.md +404 -0
- package/wiki/Testing.md +1314 -0
- package/wiki/The-Eight-Primitives.md +247 -0
- package/wiki/Theming.md +362 -0
- package/wiki/Timezones-And-Dates.md +194 -0
- package/wiki/Troubleshooting.md +163 -0
- package/wiki/Tutorial-01-First-App.md +289 -0
- package/wiki/Tutorial-02-First-Feature.md +339 -0
- package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
- package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
- package/wiki/Tutorial-05-Deploy-Free.md +231 -0
- package/wiki/Tutorial-06-Growing-Up.md +158 -0
- package/wiki/UI-Components.md +156 -0
- package/wiki/Upgrading.md +3667 -0
- package/src/stored-object-headers.ts +0 -43
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The app's module graph as `@ultimat3/manifest`'s `ModuleFacts`: every source and test file, the
|
|
2
|
+
// app files each one imports — type-only imports included, since a type change breaks a test —
|
|
3
|
+
// a computed `import()` marking its file opaque, and which file declares which primitive.
|
|
4
|
+
|
|
5
|
+
// why: Bun ships no path API; a specifier resolves against its importing file's directory.
|
|
6
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
7
|
+
import { describeActions, getAction } from '@ultimat3/action';
|
|
8
|
+
import { maskLiterals } from '@ultimat3/core';
|
|
9
|
+
import { registeredEntities } from '@ultimat3/entity';
|
|
10
|
+
import { registeredJobs, registeredTasks } from '@ultimat3/jobs';
|
|
11
|
+
import type { ModuleFacts } from '@ultimat3/manifest';
|
|
12
|
+
import { describeQueries, getQuery } from '@ultimat3/query';
|
|
13
|
+
import { routeEntries } from '@ultimat3/render';
|
|
14
|
+
import { scanAllImports } from './import-scan';
|
|
15
|
+
import { hasPathSegment } from './path-segments';
|
|
16
|
+
import { isTest } from './source-files';
|
|
17
|
+
|
|
18
|
+
/** Every authored TypeScript file an app holds: its apps, packages, guards, scripts and root. */
|
|
19
|
+
const FILE_GLOBS = [
|
|
20
|
+
'apps/**/*.{ts,tsx}',
|
|
21
|
+
'packages/**/*.{ts,tsx}',
|
|
22
|
+
'guards/**/*.{ts,tsx}',
|
|
23
|
+
'scripts/**/*.{ts,tsx}',
|
|
24
|
+
'*.{ts,tsx}',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** A computed `import()` — any argument that does not open with a quote — read off masked code. */
|
|
28
|
+
const COMPUTED_IMPORT = /(?<![\w$.])import\s*\(\s*(?!['"])/;
|
|
29
|
+
|
|
30
|
+
const toPosix = (path: string): string => path.replace(/\\/g, '/');
|
|
31
|
+
|
|
32
|
+
const skipped = (path: string): boolean =>
|
|
33
|
+
hasPathSegment(path, 'node_modules') ||
|
|
34
|
+
hasPathSegment(path, 'dist') ||
|
|
35
|
+
hasPathSegment(path, '.x') ||
|
|
36
|
+
path.endsWith('.d.ts');
|
|
37
|
+
|
|
38
|
+
/** The app's files, root-relative POSIX, sorted. */
|
|
39
|
+
export async function appSourceFiles(root: string): Promise<readonly string[]> {
|
|
40
|
+
const found = new Set<string>();
|
|
41
|
+
for (const pattern of FILE_GLOBS) {
|
|
42
|
+
for await (const path of new Bun.Glob(pattern).scan({ cwd: root, absolute: false })) {
|
|
43
|
+
const posix = toPosix(path);
|
|
44
|
+
if (!skipped(posix)) found.add(posix);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...found].sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One file's app-internal imports (root-relative) and whether it computes one. */
|
|
51
|
+
export function fileImports(
|
|
52
|
+
root: string,
|
|
53
|
+
path: string,
|
|
54
|
+
source: string,
|
|
55
|
+
): { readonly imports: readonly string[]; readonly opaque: boolean } {
|
|
56
|
+
// Absolute: `Bun.resolveSync` reads a relative directory against nothing useful.
|
|
57
|
+
const base = resolve(root);
|
|
58
|
+
const masked = maskLiterals(source);
|
|
59
|
+
let specifiers: readonly string[];
|
|
60
|
+
try {
|
|
61
|
+
specifiers = scanAllImports({ path, source }, masked);
|
|
62
|
+
} catch {
|
|
63
|
+
// A file that does not parse imports nothing we can read: it may import anything.
|
|
64
|
+
return { imports: [], opaque: true };
|
|
65
|
+
}
|
|
66
|
+
const imports = new Set<string>();
|
|
67
|
+
for (const specifier of specifiers) {
|
|
68
|
+
let resolved: string;
|
|
69
|
+
try {
|
|
70
|
+
resolved = Bun.resolveSync(specifier, dirname(join(base, path)));
|
|
71
|
+
} catch {
|
|
72
|
+
// Unresolvable: the module's own import fails first, and loudly.
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const inside = toPosix(relative(base, resolved));
|
|
76
|
+
if (!inside.startsWith('../') && !skipped(inside)) imports.add(inside);
|
|
77
|
+
}
|
|
78
|
+
return { imports: [...imports].sort(), opaque: COMPUTED_IMPORT.test(masked) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The app's own test preloads (`bunfig.toml`'s `[test] preload`): Bun imports each before EVERY
|
|
83
|
+
* test file, so each is an import of every test — the edge no source spells.
|
|
84
|
+
*/
|
|
85
|
+
async function testPreloads(root: string): Promise<readonly string[]> {
|
|
86
|
+
const bunfig = Bun.file(join(root, 'bunfig.toml'));
|
|
87
|
+
if (!(await bunfig.exists())) return [];
|
|
88
|
+
let parsed: unknown;
|
|
89
|
+
try {
|
|
90
|
+
parsed = Bun.TOML.parse(await bunfig.text());
|
|
91
|
+
} catch {
|
|
92
|
+
// An unparseable bunfig fails every `bun test` first; no edge is better than a guessed one.
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const preload = (parsed as { readonly test?: { readonly preload?: unknown } }).test?.preload;
|
|
96
|
+
const listed = Array.isArray(preload) ? preload : typeof preload === 'string' ? [preload] : [];
|
|
97
|
+
return fileImports(
|
|
98
|
+
root,
|
|
99
|
+
'bunfig.ts',
|
|
100
|
+
listed.map((entry) => `import ${JSON.stringify(String(entry))};`).join('\n'),
|
|
101
|
+
).imports;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Every file's facts. `primitives` is filled by `primitiveFiles` once the app has loaded. */
|
|
105
|
+
export async function moduleFiles(root: string): Promise<ModuleFacts['files']> {
|
|
106
|
+
const preloads = await testPreloads(root);
|
|
107
|
+
const files: ModuleFacts['files'][number][] = [];
|
|
108
|
+
for (const path of await appSourceFiles(root)) {
|
|
109
|
+
const source = await Bun.file(join(root, path)).text();
|
|
110
|
+
const found = fileImports(root, path, source);
|
|
111
|
+
const test = isTest(path);
|
|
112
|
+
const imports = test ? [...new Set([...found.imports, ...preloads])].sort() : found.imports;
|
|
113
|
+
files.push({ path, imports, opaque: found.opaque, test });
|
|
114
|
+
}
|
|
115
|
+
return files;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type Primitive = ModuleFacts['primitives'][number];
|
|
119
|
+
|
|
120
|
+
const BARREL = /(?:^|\/)index\.tsx?$/;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The modules asked for their exports: the ones `loadApp` imported, and the app's own workspace
|
|
124
|
+
* libraries (`packages/<name>/src/`, where an entity is declared). Never a script or a guard —
|
|
125
|
+
* importing one RUNS it.
|
|
126
|
+
*/
|
|
127
|
+
const IMPORTABLE = /^(?:apps\/[^/]+\/(?:app|site|api|shared)\/|packages\/[^/]+\/src\/).*\.tsx?$/;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Which file declares which primitive, by IDENTITY: each loaded module's exports compared with the
|
|
131
|
+
* handles the registries hold — never a parse of the source for a call. Routes carry their file.
|
|
132
|
+
* Call after `loadApp`: Bun's module cache answers each import without evaluating it again.
|
|
133
|
+
*/
|
|
134
|
+
export async function primitiveFiles(
|
|
135
|
+
root: string,
|
|
136
|
+
files: readonly string[],
|
|
137
|
+
): Promise<readonly Primitive[]> {
|
|
138
|
+
const handles = new Map<unknown, Omit<Primitive, 'file'>>();
|
|
139
|
+
for (const { name } of describeActions()) handles.set(getAction(name), { kind: 'action', name });
|
|
140
|
+
for (const { name } of describeQueries()) handles.set(getQuery(name), { kind: 'query', name });
|
|
141
|
+
for (const job of registeredJobs()) handles.set(job, { kind: 'job', name: job.name });
|
|
142
|
+
for (const task of registeredTasks()) handles.set(task, { kind: 'task', name: task.name });
|
|
143
|
+
for (const entity of registeredEntities()) {
|
|
144
|
+
if (entity.core !== undefined) handles.set(entity.core, { kind: 'entity', name: entity.name });
|
|
145
|
+
}
|
|
146
|
+
const found = new Map<string, Primitive>();
|
|
147
|
+
for (const route of routeEntries()) {
|
|
148
|
+
const file = toPosix(route.file);
|
|
149
|
+
found.set(`route:${route.path}`, { kind: 'route', name: route.path, file });
|
|
150
|
+
}
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
if (isTest(file) || !IMPORTABLE.test(file)) continue;
|
|
153
|
+
let module: Readonly<Record<string, unknown>>;
|
|
154
|
+
try {
|
|
155
|
+
module = (await import(join(root, file))) as Readonly<Record<string, unknown>>;
|
|
156
|
+
} catch {
|
|
157
|
+
// A module that will not load declares nothing here; `x verify`'s manifest step names it.
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const value of Object.values(module)) {
|
|
161
|
+
const handle = handles.get(value);
|
|
162
|
+
const key = handle === undefined ? undefined : `${handle.kind}:${handle.name}`;
|
|
163
|
+
if (handle === undefined || key === undefined) continue;
|
|
164
|
+
// A barrel re-exports the handle its declaring module exports: the declaring file wins.
|
|
165
|
+
const held = found.get(key);
|
|
166
|
+
if (held === undefined || (BARREL.test(held.file) && !BARREL.test(file))) {
|
|
167
|
+
found.set(key, { ...handle, file });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return [...found.values()].sort((a, b) =>
|
|
172
|
+
`${a.kind}:${a.name}` < `${b.kind}:${b.name}` ? -1 : 1,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Root-level files the framework READS rather than imports: a change re-checks everything. */
|
|
177
|
+
const READ_NOT_IMPORTED = new Set([
|
|
178
|
+
'app.config.ts',
|
|
179
|
+
'bunfig.toml',
|
|
180
|
+
'package.json',
|
|
181
|
+
'tsconfig.json',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
export interface TestSelection {
|
|
185
|
+
/** Root-relative test files, sorted. Every test when `all`. */
|
|
186
|
+
readonly tests: readonly string[];
|
|
187
|
+
/** `true` when a changed file decides nothing by import alone, so every test runs. */
|
|
188
|
+
readonly all: boolean;
|
|
189
|
+
/** The changed files that forced `all`. */
|
|
190
|
+
readonly forcing: readonly string[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The tests a change can break: a test runs unless its import closure misses every changed file.
|
|
195
|
+
* A superset by construction — a test importing an opaque file always runs, and a changed file
|
|
196
|
+
* the import graph cannot speak for (config read at boot, a catalog, a lockfile, anything outside
|
|
197
|
+
* the app) runs everything. A `.md` reaches nothing.
|
|
198
|
+
*/
|
|
199
|
+
export function selectTests(
|
|
200
|
+
files: ModuleFacts['files'],
|
|
201
|
+
changed: readonly string[],
|
|
202
|
+
): TestSelection {
|
|
203
|
+
const known = new Set(files.map((file) => file.path));
|
|
204
|
+
// Outside the app, read at boot, deleted, or no source at all: the import graph cannot speak for
|
|
205
|
+
// it. A deleted module's importers are no longer on record either, so it forces too.
|
|
206
|
+
const forcing = changed.filter(
|
|
207
|
+
(path) =>
|
|
208
|
+
!path.endsWith('.md') &&
|
|
209
|
+
(path.startsWith('../') || READ_NOT_IMPORTED.has(path) || !known.has(path)),
|
|
210
|
+
);
|
|
211
|
+
const allTests = files.filter((file) => file.test).map((file) => file.path);
|
|
212
|
+
if (forcing.length > 0) return { tests: allTests, all: true, forcing };
|
|
213
|
+
const importers = new Map<string, string[]>();
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
for (const target of file.imports) {
|
|
216
|
+
const list = importers.get(target);
|
|
217
|
+
if (list === undefined) importers.set(target, [file.path]);
|
|
218
|
+
else list.push(file.path);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const reached = new Set(changed.filter((path) => known.has(path)));
|
|
222
|
+
for (const file of files) if (file.opaque) reached.add(file.path);
|
|
223
|
+
const queue = [...reached];
|
|
224
|
+
for (let at = queue.shift(); at !== undefined; at = queue.shift()) {
|
|
225
|
+
for (const importer of importers.get(at) ?? []) {
|
|
226
|
+
if (reached.has(importer)) continue;
|
|
227
|
+
reached.add(importer);
|
|
228
|
+
queue.push(importer);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { tests: allTests.filter((test) => reached.has(test)), all: false, forcing: [] };
|
|
232
|
+
}
|
package/src/app-reload-graph.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 2026-09-27 `x dev` re-imported only the edited ROUTE module, under `?x-reload=<hash>`, and every
|
|
5
5
|
// module that route imports resolved to the instance already cached: a save to a component, its
|
|
6
6
|
// `.module.scss` or a helper logged "reloaded" and rendered the old code until a restart
|
|
7
|
-
// (
|
|
7
|
+
// (one adopter: the panel's balance card and the home page, observed twice on one day).
|
|
8
8
|
//
|
|
9
9
|
// The rule is a reverse-dependency closure. A rescan hashes every file this graph knows; each
|
|
10
10
|
// changed file, and every module that imports it transitively, leaves the registry, and the scan's
|
|
@@ -62,7 +62,7 @@ export function takeStalePins(): readonly StalePin[] {
|
|
|
62
62
|
return taken;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Edges cost a transpile scan and a resolve per import (~300 ms over
|
|
65
|
+
* Edges cost a transpile scan and a resolve per import (~300 ms over one adopter's 1200 modules),
|
|
66
66
|
* so only the one process that rescans pays it: `x dev` turns this on before its first scan. Off,
|
|
67
67
|
* a rescan still re-evaluates each changed module itself — a route saved in place reloads — but
|
|
68
68
|
* nothing it is imported by.
|
package/src/app-root.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Locating the app. `app.config.ts` is the one config file, so it is also the one root marker —
|
|
2
2
|
// commands that need an app resolve it here and nowhere else, and the failure names the fix.
|
|
3
3
|
|
|
4
|
+
// why: a refusal is synchronous and Bun.file reads are not; this is the one read on that path.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
4
6
|
import { join, resolve } from 'node:path';
|
|
5
7
|
import { APP_CONFIG_FILE, appDirOf } from '@ultimat3/core';
|
|
6
8
|
import { BunVersionError, NotInAppError } from './errors';
|
|
@@ -54,10 +56,44 @@ export function findAppRoot(from: string): AppRoot | undefined {
|
|
|
54
56
|
|
|
55
57
|
export function requireAppRoot(command: string, from: string): AppRoot {
|
|
56
58
|
const root = findAppRoot(from);
|
|
57
|
-
if (root === undefined)
|
|
59
|
+
if (root === undefined) {
|
|
60
|
+
const dir = resolve(from);
|
|
61
|
+
throw new NotInAppError({
|
|
62
|
+
command,
|
|
63
|
+
from: dir,
|
|
64
|
+
below: appDirsBelow(dir),
|
|
65
|
+
adopter: dependsOnFramework(dir),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
58
68
|
return root;
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
/** Bounded: this runs on a refusal, and `from` may be a home directory. */
|
|
72
|
+
const APP_CONFIG_BELOW = ['*', '*/*', '*/*/*'].map(
|
|
73
|
+
(depth) => new Bun.Glob(`${depth}/${APP_CONFIG_FILE}`),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/** The app directories up to three levels under `from`, for a refusal to name. Never vendored. */
|
|
77
|
+
export function appDirsBelow(from: string): readonly string[] {
|
|
78
|
+
try {
|
|
79
|
+
return APP_CONFIG_BELOW.flatMap((glob) => [...glob.scanSync({ cwd: from })])
|
|
80
|
+
.map((path) => path.replaceAll('\\', '/').slice(0, -`/${APP_CONFIG_FILE}`.length))
|
|
81
|
+
.filter((dir) => !dir.split('/').includes('node_modules'))
|
|
82
|
+
.sort();
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether `from`'s own manifest names a framework package, read as text: a refusal must not throw. */
|
|
89
|
+
function dependsOnFramework(from: string): boolean {
|
|
90
|
+
try {
|
|
91
|
+
return readFileSync(join(from, 'package.json'), 'utf8').includes('"@ultimat3/');
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
61
97
|
/** Semver-lite compare, sufficient because both sides are plain `major.minor.patch`. */
|
|
62
98
|
export function versionAtLeast(found: string, required: string): boolean {
|
|
63
99
|
const parse = (input: string): readonly number[] =>
|
package/src/bin.ts
CHANGED
|
@@ -22,6 +22,7 @@ const local = resolveLocalCli({
|
|
|
22
22
|
cwd: cwdFromArgv(Bun.argv.slice(2), process.cwd()),
|
|
23
23
|
selfPath: import.meta.path,
|
|
24
24
|
env: Bun.env,
|
|
25
|
+
command: Bun.argv[2],
|
|
25
26
|
});
|
|
26
27
|
if (local !== undefined) {
|
|
27
28
|
writeErrorLine(`x: using the app's own @ultimat3/cli at ${local}`);
|
package/src/boundary-findings.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { appImportGraph, checkImportRules, readAppSources } from './app-boundari
|
|
|
8
8
|
import { siblingModuleFindings } from './app-layout';
|
|
9
9
|
import { appTransportFindings } from './app-transport';
|
|
10
10
|
import { planBoundaryCuts } from './boundary-cuts';
|
|
11
|
+
import { handRolledSocketFindings } from './hand-rolled-socket';
|
|
12
|
+
import { islandDisposerFindings } from './island-disposer';
|
|
11
13
|
import type { Finding } from './output';
|
|
14
|
+
import { packageBoundaryFindings } from './package-boundaries';
|
|
12
15
|
|
|
13
16
|
/** A cut and a finding name one violation when they share its code, its importer and its chain. */
|
|
14
17
|
const keyOf = (code: string, at: string | undefined, cause: string): string =>
|
|
@@ -46,5 +49,8 @@ export async function appBoundaryFindings(
|
|
|
46
49
|
...withCutEdits(checkImportRules(files), appImportGraph(files)),
|
|
47
50
|
...(await appTransportFindings(root, files)),
|
|
48
51
|
...siblingModuleFindings(files),
|
|
52
|
+
...islandDisposerFindings(files),
|
|
53
|
+
...handRolledSocketFindings(files),
|
|
54
|
+
...(await packageBoundaryFindings(root)),
|
|
49
55
|
];
|
|
50
56
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Whether `x verify`'s budgets step may read the stats on disk, or has to build first. Pure: the
|
|
2
|
+
// stats as `readBuildStats` returned them and two mtimes in, one state out. The reads that gather
|
|
3
|
+
// the mtimes are `verify-budgets-build.ts`'s.
|
|
4
|
+
|
|
5
|
+
import type { BuildStats } from './budgets';
|
|
6
|
+
|
|
7
|
+
/** `fresh` is the only state the step reads without building first. */
|
|
8
|
+
export type BudgetsFreshness = 'fresh' | 'missing' | 'old-rule' | 'older-than-source';
|
|
9
|
+
|
|
10
|
+
export interface FreshnessInput {
|
|
11
|
+
/** `readBuildStats`' answer: `undefined` is no file, `stale: true` a file of an earlier rule. */
|
|
12
|
+
readonly stats: BuildStats | undefined;
|
|
13
|
+
/** The stats file's mtime; `undefined` when it could not be read. */
|
|
14
|
+
readonly statsMtimeMs: number | undefined;
|
|
15
|
+
/** The newest mtime over the app's source set; `undefined` when the set is empty. */
|
|
16
|
+
readonly newestSourceMtimeMs?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mtimes, never a content hash: a wrong answer here costs one rebuild, never a wrong verdict, and
|
|
21
|
+
* the rebuild writes the stats again. Equal mtimes are fresh, because a build finishing in the same
|
|
22
|
+
* clock tick as the last save read that save. A stats file with no readable mtime is not trusted.
|
|
23
|
+
*/
|
|
24
|
+
export function budgetsFreshness(input: FreshnessInput): BudgetsFreshness {
|
|
25
|
+
if (input.stats === undefined || input.statsMtimeMs === undefined) return 'missing';
|
|
26
|
+
if (input.stats.stale === true) return 'old-rule';
|
|
27
|
+
const newest = input.newestSourceMtimeMs;
|
|
28
|
+
// What the build READ, when it said (`sourcesAt`): a save landing during the build is older than
|
|
29
|
+
// the stats file and newer than that. A file an older build wrote has only its own mtime.
|
|
30
|
+
const read = input.stats.sourcesAt ?? input.statsMtimeMs;
|
|
31
|
+
return newest !== undefined && newest > read ? 'older-than-source' : 'fresh';
|
|
32
|
+
}
|
package/src/budgets.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface BuildStats {
|
|
|
61
61
|
readonly measuredBy?: number;
|
|
62
62
|
/** Set by `readBuildStats` on a file an earlier rule wrote: there are no numbers to read. */
|
|
63
63
|
readonly stale?: true;
|
|
64
|
+
/**
|
|
65
|
+
* The newest source mtime (ms) the build read before it rendered (`source-mtime.ts`): `budgets`
|
|
66
|
+
* calls the stats fresh only if no source is newer, so a save during the build is not missed.
|
|
67
|
+
*/
|
|
68
|
+
readonly sourcesAt?: number;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
/**
|
|
@@ -148,7 +153,10 @@ function unmeasuredFinding(
|
|
|
148
153
|
}
|
|
149
154
|
return {
|
|
150
155
|
code: 'X_BUDGET_UNMEASURED',
|
|
151
|
-
|
|
156
|
+
// Inside the gate this is a build that ran and still wrote nothing: `x verify`'s budgets step
|
|
157
|
+
// builds whenever the file is missing (`verify-budgets-build.ts`), so the cause names the one
|
|
158
|
+
// writer that did not run. The fix is the build alone, which prints why.
|
|
159
|
+
cause: `${url} declares a ${declared} budget and no build has written ${BUILD_STATS_FILE} in this repo — x verify builds when it is missing, so a static build that ran wrote none: apps/web/prerender.ts must call prerenderSite()`,
|
|
152
160
|
// `--target static` is load-bearing and `x build` alone was a fix that changes nothing: the
|
|
153
161
|
// flag defaults to `docker`, and only the static target runs `apps/web/prerender.ts`, which is
|
|
154
162
|
// the one caller of `writeBuildStats`.
|
|
@@ -292,6 +300,17 @@ export interface MeasuredJs {
|
|
|
292
300
|
* excluded as data, by `carriesJson`, not by this set. A further entry joins this set by a
|
|
293
301
|
* decision, here, with the register's argument: every document carries it and no author can
|
|
294
302
|
* remove it.
|
|
303
|
+
*
|
|
304
|
+
* `/x-client-errors.js` (`clientErrors.enabled`, plan 101, 24) is NOT here, and needs no entry: it
|
|
305
|
+
* rides `app/` documents a served process renders, and the static export this measures never names
|
|
306
|
+
* it (no route there receives a report). Measured 2026-11-03: 1,961 B (962 B gzip) per served
|
|
307
|
+
* `app/` document of an app that turned it on, and 0 B for every other document.
|
|
308
|
+
*
|
|
309
|
+
* `/x-client-vitals.js` (`clientErrors.vitals`, plan 101, 41) is NOT here either, and is CHARGED:
|
|
310
|
+
* the app opted in per surface and can opt out, the page boot's argument. A weighed `app/` page
|
|
311
|
+
* is measured as served (`prerender-served.ts` hands its bytes in `served`), and a `site/` page
|
|
312
|
+
* that lists it is exported carrying it, so it is weighed off disk. Measured 2026-11-06: 1,202 B
|
|
313
|
+
* (633 B gzip) per document on a listed surface, 0 B everywhere else.
|
|
295
314
|
*/
|
|
296
315
|
export const FRAMEWORK_SCRIPTS: ReadonlySet<string> = new Set([SW_REGISTER_PATH]);
|
|
297
316
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The one name `x build --no-capture` and the static export agree on. Its own module so `x build`
|
|
2
|
+
// reads it without loading the export's image and card writers.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How `x build --no-capture` reaches the app's own `prerender.ts`, which `x build` spawns and which
|
|
6
|
+
* calls `prerenderSite` with no flag of its own: `off` stamps the card tags and takes no picture.
|
|
7
|
+
* An env var, not an argv flag, so every app's existing `prerender.ts` honours it unedited.
|
|
8
|
+
*/
|
|
9
|
+
export const BUILD_CAPTURE_ENV = 'ULTIMATE_BUILD_CAPTURE';
|
package/src/cdp-shot-a11y.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The accessibility tree, read over the page's own CDP session: what the BROWSER computed for a
|
|
2
2
|
// screen reader, after ARIA, label association and `aria-hidden` pruning. A `<div onclick>`
|
|
3
3
|
// answers no role and no name, which is the finding — no read of the markup can reproduce it.
|
|
4
|
+
// The node READER is `@ultimat3/testing`'s, the one the e2e audit reads a whole tree with.
|
|
5
|
+
import { toAxNode } from '@ultimat3/testing';
|
|
4
6
|
import type { AxNode } from './browser-launcher-port';
|
|
5
7
|
|
|
6
8
|
/** One session-scoped CDP call. Every answer is somebody else's JSON, read and never cast. */
|
|
@@ -12,45 +14,6 @@ export type SessionSend = (
|
|
|
12
14
|
const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
13
15
|
typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
14
16
|
|
|
15
|
-
/** CDP's `AXValue.value` is any scalar; a reader is told text. */
|
|
16
|
-
const textOf = (wrapped: unknown): string | undefined => {
|
|
17
|
-
const value = record(wrapped)?.['value'];
|
|
18
|
-
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
19
|
-
? String(value)
|
|
20
|
-
: undefined;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const flagOf = (node: Readonly<Record<string, unknown>>, name: string): boolean | undefined => {
|
|
24
|
-
const properties = node['properties'];
|
|
25
|
-
if (!Array.isArray(properties)) return undefined;
|
|
26
|
-
const found = properties.map(record).find((property) => property?.['name'] === name);
|
|
27
|
-
return found === undefined ? undefined : record(found['value'])?.['value'] === true;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* `nodes[0]` is the element's own node when `fetchRelatives` is false. An EMPTY list is a real
|
|
32
|
-
* answer — an element that never entered the tree — and is kept as ignored, so the count the
|
|
33
|
-
* caller gets is the count of matches.
|
|
34
|
-
*/
|
|
35
|
-
export const toAxNode = (answer: Readonly<Record<string, unknown>> | undefined): AxNode => {
|
|
36
|
-
const nodes = answer?.['nodes'];
|
|
37
|
-
const node = Array.isArray(nodes) ? record(nodes[0]) : undefined;
|
|
38
|
-
if (node === undefined) return { role: '', name: '', ignored: true };
|
|
39
|
-
const description = textOf(node['description']);
|
|
40
|
-
const value = textOf(node['value']);
|
|
41
|
-
const focused = flagOf(node, 'focused');
|
|
42
|
-
const disabled = flagOf(node, 'disabled');
|
|
43
|
-
return {
|
|
44
|
-
role: textOf(node['role']) ?? '',
|
|
45
|
-
name: textOf(node['name']) ?? '',
|
|
46
|
-
...(description === undefined ? {} : { description }),
|
|
47
|
-
...(value === undefined ? {} : { value }),
|
|
48
|
-
...(focused === undefined ? {} : { focused }),
|
|
49
|
-
...(disabled === undefined ? {} : { disabled }),
|
|
50
|
-
ignored: node['ignored'] === true,
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
|
|
54
17
|
/** Every match of `selector`, in document order, at most `max` — one round trip per match. */
|
|
55
18
|
export async function axNodesFor(
|
|
56
19
|
send: SessionSend,
|