@ultimat3/cli 29.3.2 → 30.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 +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 +13 -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 +28 -8
- 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 +42 -138
- 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 +17 -27
- 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 +1313 -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
package/src/prerender.ts
CHANGED
|
@@ -16,29 +16,37 @@ import {
|
|
|
16
16
|
import type { RouteEntry } from '@ultimat3/render';
|
|
17
17
|
import { describePages, routeEntries } from '@ultimat3/render';
|
|
18
18
|
import { renderStatic } from '@ultimat3/render/server';
|
|
19
|
+
import { loadAppConfig } from './app-config-load';
|
|
19
20
|
import { loadApp } from './app-load';
|
|
20
21
|
import { appManifest } from './app-manifest';
|
|
21
22
|
import type { RouteStats } from './budgets';
|
|
22
23
|
import { measureDocumentJs, routeStatsRow, writeBuildStats } from './budgets';
|
|
24
|
+
import { clientErrorsStaticWarning } from './client-errors-routes';
|
|
23
25
|
import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
|
|
24
26
|
import { PrerenderLoadFailedError } from './errors';
|
|
25
27
|
import { FAVICON_PATH, faviconBytes } from './favicon';
|
|
26
|
-
import type { IslandBundle } from './island-bundle';
|
|
27
28
|
import { buildIslands, writeIslands } from './island-bundle';
|
|
28
29
|
import { measureDatabase } from './measure-database';
|
|
29
30
|
import { measurePaths } from './measure-paths';
|
|
30
|
-
import { measurementActorLabel, measureScope, withAppUrl } from './measure-scope';
|
|
31
|
+
import { measurementActorLabel, measureScope, publicScope, withAppUrl } from './measure-scope';
|
|
31
32
|
import { loadNavigation, pageNavigation } from './page-navigation';
|
|
32
33
|
import { loadSpeculation, otherLocaleSegments, pageSpeculation } from './page-speculation';
|
|
34
|
+
import { asksPrerender, enumerateOnce, type RouteEntries } from './prerender-entries';
|
|
35
|
+
import { heaviestSource } from './prerender-heaviest';
|
|
33
36
|
import { localizedArtifacts } from './prerender-locales';
|
|
34
37
|
import { clearPrerenderOut } from './prerender-out';
|
|
35
38
|
import type { PrerenderedPage, PrerenderReport } from './prerender-report';
|
|
39
|
+
import { collectSeoReads } from './prerender-seo-reads';
|
|
36
40
|
import type { ServedMeasure } from './prerender-served';
|
|
37
41
|
import { servedMeasure } from './prerender-served';
|
|
42
|
+
import { clientVitalsStaticWarning, vitalsBuild } from './prerender-vitals';
|
|
38
43
|
import { loadPwaArtifacts, writePwaIcons } from './pwa-artifacts';
|
|
39
44
|
import { routeDocument, servedDocument } from './runtime-render';
|
|
40
45
|
import { writeSiteAssets } from './site-assets';
|
|
41
46
|
import { loadSiteSettings, originWarning, publicOrigin } from './site-config';
|
|
47
|
+
import type { LlmsPages } from './site-llms-pages';
|
|
48
|
+
import { newestSourceMtime } from './source-mtime';
|
|
49
|
+
import { staticCapture } from './static-capture';
|
|
42
50
|
import type { SkippedRoute, UnmeasuredRoute } from './static-report';
|
|
43
51
|
import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
|
|
44
52
|
import { styleBundle, writeStyles } from './style-bundle';
|
|
@@ -80,29 +88,8 @@ export interface PrerenderOptions {
|
|
|
80
88
|
* on a production build, because a placeholder canonical is one a search engine indexes.
|
|
81
89
|
*/
|
|
82
90
|
readonly origin?: string;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The heaviest thing the document actually boots, named by the source file an author can open.
|
|
87
|
-
* An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
|
|
88
|
-
* through the bundle is what turns `X_BUDGET_EXCEEDED` from a number into an instruction.
|
|
89
|
-
*/
|
|
90
|
-
function heaviestSource(
|
|
91
|
-
bundle: IslandBundle,
|
|
92
|
-
entries: readonly { readonly url: string; readonly bytes: number }[],
|
|
93
|
-
): readonly string[] | undefined {
|
|
94
|
-
const heaviest = entries.reduce<{ url: string; bytes: number } | undefined>(
|
|
95
|
-
(max, entry) => (max === undefined || entry.bytes > max.bytes ? entry : max),
|
|
96
|
-
undefined,
|
|
97
|
-
);
|
|
98
|
-
if (heaviest === undefined) return undefined;
|
|
99
|
-
const chunk = bundle.chunkAt(heaviest.url);
|
|
100
|
-
if (chunk !== undefined) return [chunk.file];
|
|
101
|
-
// A shared chunk has no file of its own: the chain is an island on THIS page that imports it.
|
|
102
|
-
const importer = bundle.chunks.find(
|
|
103
|
-
(one) => one.imports.includes(heaviest.url) && entries.some((entry) => entry.url === one.url),
|
|
104
|
-
);
|
|
105
|
-
return importer === undefined ? [heaviest.url] : [importer.file, heaviest.url];
|
|
91
|
+
/** Photograph declared social cards. Absent: on unless `ULTIMATE_BUILD_CAPTURE=off` (`--no-capture`). */
|
|
92
|
+
readonly capture?: boolean;
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
export const DEFAULT_ORIGIN = 'https://localhost';
|
|
@@ -130,6 +117,9 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
130
117
|
// The load `x dev` and `x manifest` perform: importing the app's modules fills the route registry.
|
|
131
118
|
// A module that would not import registered nothing, so its page would vanish from the export
|
|
132
119
|
// under a green build — refused BEFORE the export is emptied, so the last good artifact survives.
|
|
120
|
+
// Read BEFORE the app loads: a source saved while this build runs is newer than this, so the
|
|
121
|
+
// gate's `budgets` step rebuilds rather than weighing bytes that predate the save.
|
|
122
|
+
const sourcesAt = await newestSourceMtime(options.root);
|
|
133
123
|
const loaded = await loadApp(options.root);
|
|
134
124
|
if (loaded.findings.length > 0) throw new PrerenderLoadFailedError(loaded.findings);
|
|
135
125
|
// Emptied before anything is written: a deleted route's HTML kept shipping when it only grew.
|
|
@@ -140,7 +130,14 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
140
130
|
const origin = declaredOrigin ?? DEFAULT_ORIGIN;
|
|
141
131
|
// Written to stderr as well as returned: an app's `prerender.ts` prints its report as ONE JSON
|
|
142
132
|
// line on stdout, and a build that parses it must not meet a warning inside it.
|
|
143
|
-
const
|
|
133
|
+
const clientErrors = (await loadAppConfig(options.root))?.clientErrors;
|
|
134
|
+
// Real-user timings as served: a site/ page that lists them is exported carrying the script.
|
|
135
|
+
const vitals = vitalsBuild(clientErrors?.vitals);
|
|
136
|
+
const warnings = [
|
|
137
|
+
...originWarning(tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT, declaredOrigin),
|
|
138
|
+
...clientErrorsStaticWarning(clientErrors),
|
|
139
|
+
...clientVitalsStaticWarning(vitals),
|
|
140
|
+
];
|
|
144
141
|
for (const warning of warnings) writeErrorLine(`warning: ${warning}`);
|
|
145
142
|
// The app's locales as `loadApp` read them, ONCE, off `@ultimat3/i18n`'s one reader — the same
|
|
146
143
|
// answer the PWA manifest and the service worker below are built from. The default first, so
|
|
@@ -211,6 +208,10 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
211
208
|
if (navigation.script !== undefined) {
|
|
212
209
|
await Bun.write(join(options.out, navigation.script.url.slice(1)), navigation.script.code);
|
|
213
210
|
}
|
|
211
|
+
// Before any site/ document names it, for the router's reason: weighed off disk, and charged.
|
|
212
|
+
if (vitals.siteHead !== undefined && vitals.script !== undefined) {
|
|
213
|
+
await Bun.write(join(options.out, vitals.script.url.slice(1)), vitals.script.code);
|
|
214
|
+
}
|
|
214
215
|
// The registration TAG now, the worker itself after the render loop — the two halves are wanted
|
|
215
216
|
// at different moments and used to be taken at the same one. Every document below has to name
|
|
216
217
|
// `/x-sw-register.js`, and the worker's precache manifest is built from the content hash of
|
|
@@ -263,8 +264,9 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
263
264
|
// process, because an `app/` page's `load` calls policy-guarded queries over its typed client
|
|
264
265
|
// and no server is listening during a build. `renderStatic` keeps `ctx`: its output is a
|
|
265
266
|
// published file, and a `site/` load a policy refuses must fail the build, never render another
|
|
266
|
-
// actor's rows into it.
|
|
267
|
+
// actor's rows into it. `published` authenticates nobody: sitemap and llms.txt reads run in it.
|
|
267
268
|
const measure = await measureScope({ origin, buildId });
|
|
269
|
+
const published = publicScope({ origin, buildId });
|
|
268
270
|
// Started on the first route that needs weighing, and released however the loop ends.
|
|
269
271
|
const database = measureDatabase(options.root);
|
|
270
272
|
const documentOptions = {
|
|
@@ -281,6 +283,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
281
283
|
...(pwa === undefined
|
|
282
284
|
? {}
|
|
283
285
|
: { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
|
|
286
|
+
...(vitals.siteHead === undefined ? {} : { siteHead: vitals.siteHead }),
|
|
284
287
|
};
|
|
285
288
|
const render = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
|
|
286
289
|
routeDocument(entry, data, documentOptions);
|
|
@@ -288,7 +291,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
288
291
|
// composed on the first route that needs it, so a build weighing none builds no page boot.
|
|
289
292
|
let weighing: Promise<ServedMeasure> | undefined;
|
|
290
293
|
const served = (): Promise<ServedMeasure> => {
|
|
291
|
-
weighing ??= servedMeasure(options.root, buildId, islands);
|
|
294
|
+
weighing ??= servedMeasure(options.root, buildId, islands, vitals);
|
|
292
295
|
return weighing;
|
|
293
296
|
};
|
|
294
297
|
const document = (
|
|
@@ -300,6 +303,11 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
300
303
|
render(entry, data),
|
|
301
304
|
);
|
|
302
305
|
|
|
306
|
+
// The seo seam is installed for the loop and only the loop (`static-capture.ts`).
|
|
307
|
+
const capture = await staticCapture(options.out, options.capture);
|
|
308
|
+
let entries: RouteEntries = new Map();
|
|
309
|
+
const enumerate = enumerateOnce();
|
|
310
|
+
let llmsPages: LlmsPages = new Map();
|
|
303
311
|
try {
|
|
304
312
|
for (const entry of routeEntries()) {
|
|
305
313
|
const facts = { surface: entry.surface, render: entry.config.render, route: entry.path };
|
|
@@ -314,10 +322,13 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
314
322
|
try {
|
|
315
323
|
// A dynamic route is rendered at the paths its own `prerender()` lists, and the row holds
|
|
316
324
|
// the heaviest — a static route's rule. One that lists none is its own finding.
|
|
317
|
-
//
|
|
318
|
-
//
|
|
325
|
+
// `prerender()` reads the app's own data as its `load` does — as nobody for a route the
|
|
326
|
+
// sitemap asks, because the one memo hands this answer to `collectSeoReads`.
|
|
319
327
|
await database.ready();
|
|
320
|
-
const
|
|
328
|
+
const enumerateIn = asksPrerender(entry) ? published : measure.run;
|
|
329
|
+
const plan = await withAppUrl(origin, () =>
|
|
330
|
+
enumerateIn(() => measurePaths(entry, enumerate)),
|
|
331
|
+
);
|
|
321
332
|
if ('unmeasured' in plan) {
|
|
322
333
|
unmeasured.push(plan.unmeasured);
|
|
323
334
|
continue;
|
|
@@ -413,9 +424,22 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
413
424
|
}
|
|
414
425
|
if (heaviest !== undefined) routes.push(heaviest);
|
|
415
426
|
}
|
|
427
|
+
// Before the scope and the database are released: `siteSeo` reads these instead of asking
|
|
428
|
+
// `prerender()` or a page's `load` again, when nothing answers the app's own query any more.
|
|
429
|
+
({ entries, llmsPages } = await collectSeoReads({
|
|
430
|
+
root: options.root,
|
|
431
|
+
origin,
|
|
432
|
+
defaultLocale,
|
|
433
|
+
scope: (fn) => withAppUrl(origin, () => published(fn)),
|
|
434
|
+
before: () => database.ready(),
|
|
435
|
+
enumerate,
|
|
436
|
+
}));
|
|
416
437
|
} finally {
|
|
438
|
+
capture.close();
|
|
417
439
|
await database.close();
|
|
418
440
|
}
|
|
441
|
+
// The variants and cards the exported documents name, before the worker precaches them.
|
|
442
|
+
await capture.write(pages);
|
|
419
443
|
// The worker, LAST: every document it precaches has now been rendered, hashed and weighed. A
|
|
420
444
|
// static host runs no route table, so both files go into the artifact — a
|
|
421
445
|
// `<script src="/x-sw-register.js">` in every document is a 404 otherwise, which is the same
|
|
@@ -437,7 +461,10 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
437
461
|
if (serviceWorker !== undefined) {
|
|
438
462
|
await Bun.write(join(options.out, SERVICE_WORKER_PATH.slice(1)), serviceWorker.source);
|
|
439
463
|
}
|
|
440
|
-
const stats = await writeBuildStats(options.root, {
|
|
464
|
+
const stats = await writeBuildStats(options.root, {
|
|
465
|
+
routes,
|
|
466
|
+
...(sourcesAt === undefined ? {} : { sourcesAt }),
|
|
467
|
+
});
|
|
441
468
|
// Written LAST and by the same call that writes the stats, so an app whose `prerender.ts` does
|
|
442
469
|
// not reach `prerenderSite` produces neither — and `x verify`'s `budgets` step already reds that
|
|
443
470
|
// app with `X_BUDGET_UNMEASURED`, which is why this side needs no second code of its own.
|
|
@@ -467,5 +494,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
467
494
|
serviceWorkerWarnings: serviceWorker?.warnings ?? [],
|
|
468
495
|
warnings,
|
|
469
496
|
origin,
|
|
497
|
+
entries,
|
|
498
|
+
llmsPages,
|
|
470
499
|
};
|
|
471
500
|
}
|
package/src/pwa-artifacts.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { join } from 'node:path';
|
|
|
20
20
|
import type { PwaColors, PwaOfflineConfig } from '@ultimat3/core';
|
|
21
21
|
import { escapeHtml, localeSegment, pushWired } from '@ultimat3/core';
|
|
22
22
|
import type { CacheHint, RequestContext, Route, UltimateRequest } from '@ultimat3/http';
|
|
23
|
-
import { applyCacheHeaders } from '@ultimat3/http';
|
|
23
|
+
import { applyCacheHeaders, textResponse } from '@ultimat3/http';
|
|
24
24
|
import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
25
25
|
import {
|
|
26
26
|
appleTouchLinks,
|
|
@@ -180,7 +180,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
|
|
|
180
180
|
* `/assets/pwa/wide.png` — answers the content-hashed URL the site actually serves, exactly as
|
|
181
181
|
* `asset()` does in a page. The site serves an asset ONLY at its hashed URL, and the manifest took
|
|
182
182
|
* `src` verbatim, so an app had to hash its own screenshots to avoid shipping a 404 in the install
|
|
183
|
-
* sheet (
|
|
183
|
+
* sheet (one adopter's `pwa-install.ts`). A missing file is `X_ASSET_MISSING` at boot, never a
|
|
184
184
|
* broken image in an install prompt; any other `src` passes untouched — an absolute path outside
|
|
185
185
|
* `/assets/`, an already hashed one, or a shortcut icon's off-site URL (core screens screenshots and
|
|
186
186
|
* shortcut urls to absolute paths, not icon sources).
|
|
@@ -262,7 +262,8 @@ export const pwaManifestRoute = (artifacts: PwaArtifacts): Route => ({
|
|
|
262
262
|
},
|
|
263
263
|
handler: async (_request: UltimateRequest, ctx: RequestContext): Promise<Response> =>
|
|
264
264
|
applyCacheHeaders(
|
|
265
|
-
|
|
265
|
+
// `textResponse` for the length it declares: `compress` reads nothing undeclared.
|
|
266
|
+
textResponse(manifestIn(artifacts.manifests, ctx.locale)?.body ?? artifacts.body, {
|
|
266
267
|
headers: { 'content-type': 'application/manifest+json; charset=utf-8' },
|
|
267
268
|
}),
|
|
268
269
|
MANIFEST_CACHE,
|
|
@@ -26,7 +26,7 @@ const strings = (value: unknown): readonly string[] =>
|
|
|
26
26
|
/**
|
|
27
27
|
* The manifests' locales, off `appLocaleSet`'s answer (`@ultimat3/i18n/app-catalogs`) — an app
|
|
28
28
|
* with no catalogs is `UNDECLARED_LOCALES`, the framework default alone. The manifest's `lang` was
|
|
29
|
-
* the generator's hardcoded `'en'` for an `es-co` app (
|
|
29
|
+
* the generator's hardcoded `'en'` for an `es-co` app (one adopter, 22.3.2). Deduplicated by URL
|
|
30
30
|
* segment, the unit a locale prefix is spelled in.
|
|
31
31
|
*/
|
|
32
32
|
export function appLocales(set: AppLocaleSet): AppLocales {
|
package/src/registry.ts
CHANGED
|
@@ -11,21 +11,27 @@ import { authSpec } from './cmd-auth-spec';
|
|
|
11
11
|
import { buildSpec } from './cmd-build-spec';
|
|
12
12
|
import { ciSpec } from './cmd-ci-spec';
|
|
13
13
|
import { cleanSpec } from './cmd-clean-spec';
|
|
14
|
+
import { clientSpec } from './cmd-client-spec';
|
|
14
15
|
import { dbSpec } from './cmd-db-spec';
|
|
15
16
|
import { deploySpec } from './cmd-deploy-spec';
|
|
16
17
|
import { devSpec } from './cmd-dev-spec';
|
|
17
18
|
import { docsSpec } from './cmd-docs-spec';
|
|
18
19
|
import { doctorSpec } from './cmd-doctor-spec';
|
|
20
|
+
import { drillSpec } from './cmd-drill-spec';
|
|
19
21
|
import { envSpec } from './cmd-env-spec';
|
|
20
22
|
import { errorsSpec } from './cmd-errors-spec';
|
|
23
|
+
import { exerciseSpec } from './cmd-exercise-spec';
|
|
21
24
|
import { fixSpec } from './cmd-fix-spec';
|
|
22
25
|
import { generateSpec } from './cmd-generate-spec';
|
|
26
|
+
import { graphSpec } from './cmd-graph-spec';
|
|
23
27
|
// Registered here, not by whichever command body happens to load: `dispatch` renders a CLI code's
|
|
24
28
|
// title for a refusal raised before any body is imported.
|
|
25
29
|
import './error-codes';
|
|
26
30
|
import { createHelpCommand, createVersionCommand } from './cmd-help';
|
|
27
31
|
import { i18nSpec } from './cmd-i18n-spec';
|
|
32
|
+
import { impactSpec } from './cmd-impact-spec';
|
|
28
33
|
import { jobsSpec } from './cmd-jobs-spec';
|
|
34
|
+
import { mailSpec } from './cmd-mail-spec';
|
|
29
35
|
import { manifestSpec } from './cmd-manifest-spec';
|
|
30
36
|
import { mcpSpec } from './cmd-mcp-spec';
|
|
31
37
|
import { newSpec } from './cmd-new-spec';
|
|
@@ -33,11 +39,14 @@ import { plannedCommands } from './cmd-planned';
|
|
|
33
39
|
import { policySpec } from './cmd-policy-spec';
|
|
34
40
|
import { prSpec } from './cmd-pr-spec';
|
|
35
41
|
import { actionsSpec, entitiesSpec, queriesSpec } from './cmd-registries-spec';
|
|
42
|
+
import { reproSpec } from './cmd-repro-spec';
|
|
36
43
|
import { routesSpec } from './cmd-routes-spec';
|
|
37
44
|
import { secretsSpec } from './cmd-secrets-spec';
|
|
38
45
|
import { shotSpec } from './cmd-shot-spec';
|
|
46
|
+
import { stopSpec } from './cmd-stop-spec';
|
|
39
47
|
import { tasksSpec } from './cmd-tasks-spec';
|
|
40
48
|
import { testSpec } from './cmd-test-spec';
|
|
49
|
+
import { upgradeSpec } from './cmd-upgrade-spec';
|
|
41
50
|
import { vapidSpec } from './cmd-vapid-spec';
|
|
42
51
|
import { verifySpec } from './cmd-verify-spec';
|
|
43
52
|
import type { CliCommand } from './command';
|
|
@@ -78,6 +87,7 @@ const lazy = (spec: CommandSpec, load: () => Promise<CliCommand>): LazyCommand =
|
|
|
78
87
|
export const LAZY_COMMANDS: readonly LazyCommand[] = [
|
|
79
88
|
lazy(newSpec, async () => (await import('./cmd-new')).newCommand),
|
|
80
89
|
lazy(devSpec, async () => (await import('./cmd-dev')).devCommand),
|
|
90
|
+
lazy(stopSpec, async () => (await import('./cmd-stop')).stopCommand),
|
|
81
91
|
lazy(buildSpec, async () => (await import('./cmd-build')).buildCommand),
|
|
82
92
|
lazy(testSpec, async () => (await import('./cmd-test')).testCommand),
|
|
83
93
|
lazy(verifySpec, async () => (await import('./cmd-verify')).verifyCommand),
|
|
@@ -99,14 +109,22 @@ export const LAZY_COMMANDS: readonly LazyCommand[] = [
|
|
|
99
109
|
lazy(jobsSpec, async () => (await import('./cmd-jobs')).jobsCommand),
|
|
100
110
|
lazy(tasksSpec, async () => (await import('./cmd-tasks')).tasksCommand),
|
|
101
111
|
lazy(policySpec, async () => (await import('./cmd-policy')).policyCommand),
|
|
112
|
+
lazy(exerciseSpec, async () => (await import('./cmd-exercise')).exerciseCommand),
|
|
113
|
+
lazy(drillSpec, async () => (await import('./cmd-drill')).drillCommand),
|
|
102
114
|
lazy(i18nSpec, async () => (await import('./cmd-i18n')).i18nCommand),
|
|
115
|
+
lazy(mailSpec, async () => (await import('./cmd-mail')).mailCommand),
|
|
103
116
|
lazy(errorsSpec, async () => (await import('./cmd-errors')).errorsCommand),
|
|
117
|
+
lazy(reproSpec, async () => (await import('./cmd-repro')).reproCommand),
|
|
104
118
|
lazy(docsSpec, async () => (await import('./cmd-docs')).docsCommand),
|
|
105
119
|
lazy(fixSpec, async () => (await import('./cmd-fix')).fixCommand),
|
|
106
120
|
lazy(affectedSpec, async () => (await import('./cmd-affected')).affectedCommand),
|
|
107
121
|
lazy(shotSpec, async () => (await import('./cmd-shot')).shotCommand),
|
|
108
122
|
lazy(prSpec, async () => (await import('./cmd-pr')).prCommand),
|
|
109
123
|
lazy(ciSpec, async () => (await import('./cmd-ci')).ciCommand),
|
|
124
|
+
lazy(upgradeSpec, async () => (await import('./cmd-upgrade')).upgradeCommand),
|
|
125
|
+
lazy(clientSpec, async () => (await import('./cmd-client')).clientCommand),
|
|
126
|
+
lazy(graphSpec, async () => (await import('./cmd-graph')).graphCommand),
|
|
127
|
+
lazy(impactSpec, async () => (await import('./cmd-impact')).impactCommand),
|
|
110
128
|
];
|
|
111
129
|
|
|
112
130
|
/**
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Single responsibility: what `x repro <id>` is about — a dead job (`x_jobs` and its steps, with or
|
|
2
|
+
// without capture), a captured request (`x_failures` by bundle, request or trace id, else the newest
|
|
3
|
+
// bundle of a fingerprint), or a bundle file (`--from`). Reads only; refuses with `X_REPRO_*`.
|
|
4
|
+
|
|
5
|
+
import type { FailureBundle } from '@ultimat3/core';
|
|
6
|
+
import { isJsonObject } from '@ultimat3/core';
|
|
7
|
+
import { postgresFailureStore } from '@ultimat3/db';
|
|
8
|
+
import type { JobDriver } from '@ultimat3/jobs';
|
|
9
|
+
import { inspectJob } from '@ultimat3/jobs';
|
|
10
|
+
import type { JobFailure } from '@ultimat3/testing';
|
|
11
|
+
import { ReproCaptureOffError, ReproUnknownError } from './repro-errors';
|
|
12
|
+
import { jobFailureOf } from './repro-job';
|
|
13
|
+
|
|
14
|
+
export type ReproSubject =
|
|
15
|
+
| { readonly kind: 'job'; readonly failure: JobFailure }
|
|
16
|
+
| { readonly kind: 'request'; readonly bundle: FailureBundle };
|
|
17
|
+
|
|
18
|
+
/** A uuid in any spelling `x_jobs.id` accepts — `cmd-jobs.ts`' rule, so no statement is wasted. */
|
|
19
|
+
const JOB_ID = /^\{?[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}\}?$/i;
|
|
20
|
+
|
|
21
|
+
const isBundle = (value: unknown): value is FailureBundle =>
|
|
22
|
+
isJsonObject(value) &&
|
|
23
|
+
typeof value['name'] === 'string' &&
|
|
24
|
+
(value['primitive'] === 'action' || value['primitive'] === 'query') &&
|
|
25
|
+
Array.isArray(value['reads']);
|
|
26
|
+
|
|
27
|
+
const isJobFailure = (value: unknown): value is JobFailure =>
|
|
28
|
+
isJsonObject(value) &&
|
|
29
|
+
typeof value['jobId'] === 'string' &&
|
|
30
|
+
typeof value['name'] === 'string' &&
|
|
31
|
+
Array.isArray(value['steps']);
|
|
32
|
+
|
|
33
|
+
/** `--from <file>`: a bundle `x repro <id> --json` printed (`data.bundle` or `data.failure`), or one bare. */
|
|
34
|
+
export async function subjectFromFile(path: string): Promise<ReproSubject> {
|
|
35
|
+
const file = Bun.file(path);
|
|
36
|
+
if (!(await file.exists())) throw new ReproUnknownError({ id: path });
|
|
37
|
+
const parsed: unknown = await file.json();
|
|
38
|
+
const data = isJsonObject(parsed) && isJsonObject(parsed['data']) ? parsed['data'] : parsed;
|
|
39
|
+
const inner = isJsonObject(data) ? (data['bundle'] ?? data['failure'] ?? data) : data;
|
|
40
|
+
if (isBundle(inner)) return { kind: 'request', bundle: inner };
|
|
41
|
+
if (isJobFailure(inner)) return { kind: 'job', failure: inner };
|
|
42
|
+
throw new ReproUnknownError({ id: path });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A job first — its row exists whether or not capture is on — then the captured bundles. Not found
|
|
47
|
+
* anywhere: `X_REPRO_CAPTURE_OFF` when the app never stored bundles, `X_REPRO_UNKNOWN` otherwise.
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveSubject(
|
|
50
|
+
driver: JobDriver,
|
|
51
|
+
id: string,
|
|
52
|
+
captureOn: boolean,
|
|
53
|
+
): Promise<ReproSubject> {
|
|
54
|
+
if (JOB_ID.test(id)) {
|
|
55
|
+
const trace = await inspectJob(driver, id);
|
|
56
|
+
if (trace !== undefined) {
|
|
57
|
+
return { kind: 'job', failure: jobFailureOf(trace, await driver.steps.list(trace.runId)) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const store = postgresFailureStore();
|
|
61
|
+
const bundle = (await store.find(id)) ?? (await store.latest(id));
|
|
62
|
+
if (bundle !== undefined) return { kind: 'request', bundle };
|
|
63
|
+
if (!captureOn) throw new ReproCaptureOffError({ id });
|
|
64
|
+
throw new ReproUnknownError({ id });
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The three ways `x repro` refuses: an id nothing holds, a capture that was never on, and a written
|
|
2
|
+
// test that ran and did not fail the way the original did. Registered in `error-codes.ts`.
|
|
3
|
+
|
|
4
|
+
import { renderFixShellArg, UltimateError } from '@ultimat3/core';
|
|
5
|
+
|
|
6
|
+
export class ReproUnknownError extends UltimateError {
|
|
7
|
+
constructor(input: { readonly id: string }) {
|
|
8
|
+
super({
|
|
9
|
+
code: 'X_REPRO_UNKNOWN',
|
|
10
|
+
cause: `no dead job, captured request, trace or fingerprint matches "${input.id}"`,
|
|
11
|
+
fix: 'x repro --list --json',
|
|
12
|
+
meta: { id: input.id },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ReproCaptureOffError extends UltimateError {
|
|
18
|
+
constructor(input: { readonly id: string }) {
|
|
19
|
+
super({
|
|
20
|
+
code: 'X_REPRO_CAPTURE_OFF',
|
|
21
|
+
cause: `"${input.id}" is no job, and failureCapture is disabled in app.config.ts, so no request bundle was ever stored`,
|
|
22
|
+
fix: 'x jobs list --json # a job id replays with capture off; for requests set failureCapture: { enabled: true } in app.config.ts',
|
|
23
|
+
meta: { id: input.id },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NotReproducedInput {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly expected: string;
|
|
31
|
+
readonly got: string | null;
|
|
32
|
+
readonly masked: readonly string[];
|
|
33
|
+
readonly file: string;
|
|
34
|
+
readonly build: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ReproNotReproducedError extends UltimateError {
|
|
38
|
+
constructor(input: NotReproducedInput) {
|
|
39
|
+
const masked = input.masked.length === 0 ? 'none' : input.masked.join(', ');
|
|
40
|
+
super({
|
|
41
|
+
code: 'X_REPRO_NOT_REPRODUCED',
|
|
42
|
+
cause: `${input.file} ended in ${input.got ?? 'a pass'}, not ${input.expected}; masked fields: ${masked}; captured on build ${input.build ?? 'unknown'}`,
|
|
43
|
+
fix: `x repro ${renderFixShellArg(input.id, '<id>')} --json, then set the listed masked fields and unread rows by hand in ${renderFixShellArg(input.file, '<the written test>')}`,
|
|
44
|
+
meta: { ...input },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/repro-job.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Single responsibility: a dead job as a replay — what `inspectJob` and the run's step records say,
|
|
2
|
+
// folded into the `JobFailure` `reproJob` takes, and the `.job.test.ts` that replays it. Pure: the
|
|
3
|
+
// reads are `repro-bundle.ts`', the write is `repro-write.ts`'.
|
|
4
|
+
|
|
5
|
+
import type { ErrorReport } from '@ultimat3/core';
|
|
6
|
+
import { captureValue, errorFingerprint, singleLine } from '@ultimat3/core';
|
|
7
|
+
import type { JobTrace, StepRecord } from '@ultimat3/jobs';
|
|
8
|
+
import type { JobFailure } from '@ultimat3/testing';
|
|
9
|
+
import { tsLiteral } from './repro-literal';
|
|
10
|
+
|
|
11
|
+
const CODE = /X_[A-Z0-9_]+/;
|
|
12
|
+
|
|
13
|
+
/** `inspectJob`'s trace (input already masked as `x jobs show` masks it) plus the run's steps. */
|
|
14
|
+
export function jobFailureOf(trace: JobTrace, records: readonly StepRecord[]): JobFailure {
|
|
15
|
+
const { value, masked } = captureValue(trace.input);
|
|
16
|
+
return {
|
|
17
|
+
jobId: trace.id,
|
|
18
|
+
name: trace.name,
|
|
19
|
+
input: value,
|
|
20
|
+
masked,
|
|
21
|
+
// Completed steps only, in the order the run took them: each one replays from its output.
|
|
22
|
+
steps: records
|
|
23
|
+
.filter((record) => record.status === 'completed')
|
|
24
|
+
.map((record) => ({ name: record.name, output: record.output ?? null })),
|
|
25
|
+
code: CODE.exec(trace.lastError ?? '')?.[0] ?? null,
|
|
26
|
+
step: records.find((record) => record.status === 'failed')?.name ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The group a job failure reports under — `source: 'job'`, the job's name as the operation. */
|
|
31
|
+
export function jobFingerprint(failure: JobFailure): string | null {
|
|
32
|
+
if (failure.code === null) return null;
|
|
33
|
+
const report: Pick<ErrorReport, 'code' | 'source' | 'scope'> = {
|
|
34
|
+
code: failure.code,
|
|
35
|
+
source: 'job',
|
|
36
|
+
scope: { operation: failure.name },
|
|
37
|
+
};
|
|
38
|
+
// `errorFingerprint` reads three fields through total accessors; the rest of a report is not read.
|
|
39
|
+
return errorFingerprint(report as ErrorReport);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ReproSourceMeta {
|
|
43
|
+
/** Bare id, as the user typed it — the comment says how the file came to be. */
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/** Module specifiers imported for their registrations (the app's API index), relative. */
|
|
46
|
+
readonly imports: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const header = (lines: readonly string[]): string =>
|
|
50
|
+
lines.map((line) => `// ${singleLine(line)}`).join('\n');
|
|
51
|
+
|
|
52
|
+
/** A `.job.test.ts` that is RED until the fault is fixed: the run must settle with no error. */
|
|
53
|
+
export function jobTestSource(failure: JobFailure, meta: ReproSourceMeta): string {
|
|
54
|
+
const masked = failure.masked.length === 0 ? 'none' : failure.masked.join(', ');
|
|
55
|
+
return `${header([
|
|
56
|
+
`x repro ${meta.id}: job "${failure.name}" died with ${failure.code ?? 'an uncoded failure'} at step "${failure.step ?? '(the body)'}".`,
|
|
57
|
+
'Written by `x repro --write`. Red until the fault is fixed: the completed steps replay from',
|
|
58
|
+
`their stored output, then the job runs once more. Masked input fields (${masked}) are stood in`,
|
|
59
|
+
'for from the schema — set them here when the failure needs their real shape.',
|
|
60
|
+
])}
|
|
61
|
+
|
|
62
|
+
import { expect, type JobFailure, jobTest, reproJob } from '@ultimat3/testing';
|
|
63
|
+
${meta.imports.map((specifier) => `import '${specifier}';`).join('\n')}
|
|
64
|
+
|
|
65
|
+
const FAILURE: JobFailure = ${tsLiteral(failure)};
|
|
66
|
+
|
|
67
|
+
jobTest(${tsLiteral(`repro ${meta.id.slice(0, 8)}: ${failure.name} settles`)}, async ({ runJobs }) => {
|
|
68
|
+
const outcome = await reproJob(runJobs, FAILURE);
|
|
69
|
+
expect(outcome.failure).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Single responsibility: a JSON value as a TypeScript literal in the repo's own style — single
|
|
2
|
+
// quotes, two spaces, trailing commas, keys bare where they can be — so a test `x repro --write`
|
|
3
|
+
// emits passes `lint` without a formatter and reads like a test someone wrote.
|
|
4
|
+
|
|
5
|
+
const BARE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
6
|
+
|
|
7
|
+
const quoted = (text: string): string =>
|
|
8
|
+
`'${text
|
|
9
|
+
.replaceAll('\\', '\\\\')
|
|
10
|
+
.replaceAll("'", "\\'")
|
|
11
|
+
.replaceAll('\n', '\\n')
|
|
12
|
+
.replaceAll('\r', '\\r')
|
|
13
|
+
.replaceAll('
', '\\u2028')
|
|
14
|
+
.replaceAll('
', '\\u2029')}'`;
|
|
15
|
+
|
|
16
|
+
/** `__proto__` written bare or quoted SETS the prototype; only a computed key stays a key. */
|
|
17
|
+
const keyOf = (key: string): string =>
|
|
18
|
+
key === '__proto__' ? `[${quoted(key)}]` : BARE_KEY.test(key) ? key : quoted(key);
|
|
19
|
+
|
|
20
|
+
/** `value` rendered at `depth` (its first line is not indented; nested lines are). */
|
|
21
|
+
export function tsLiteral(value: unknown, depth = 0): string {
|
|
22
|
+
if (value === null || value === undefined) return 'null';
|
|
23
|
+
if (typeof value === 'string') return quoted(value);
|
|
24
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'null';
|
|
25
|
+
if (typeof value === 'boolean') return String(value);
|
|
26
|
+
const pad = ' '.repeat(depth + 1);
|
|
27
|
+
const close = ' '.repeat(depth);
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
if (value.length === 0) return '[]';
|
|
30
|
+
return `[\n${value.map((entry) => `${pad}${tsLiteral(entry, depth + 1)},`).join('\n')}\n${close}]`;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'object') {
|
|
33
|
+
const entries = Object.entries(value);
|
|
34
|
+
if (entries.length === 0) return '{}';
|
|
35
|
+
const lines = entries.map(
|
|
36
|
+
([key, entry]) => `${pad}${keyOf(key)}: ${tsLiteral(entry, depth + 1)},`,
|
|
37
|
+
);
|
|
38
|
+
return `{\n${lines.join('\n')}\n${close}}`;
|
|
39
|
+
}
|
|
40
|
+
return 'null';
|
|
41
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Single responsibility: a captured request bundle as the `.contract.test.ts` that replays it —
|
|
2
|
+
// rows seeded through the app's own driver, the clock frozen at the failure, the actor and input
|
|
3
|
+
// as captured. Pure: the read is `repro-bundle.ts`', the write `repro-write.ts`'.
|
|
4
|
+
|
|
5
|
+
import type { FailureBundle } from '@ultimat3/core';
|
|
6
|
+
import type { ReproSourceMeta } from './repro-job';
|
|
7
|
+
import { header } from './repro-job';
|
|
8
|
+
import { tsLiteral } from './repro-literal';
|
|
9
|
+
|
|
10
|
+
export interface RequestSourceMeta extends ReproSourceMeta {
|
|
11
|
+
/** `@<app>/db` — the package exporting `driver`; `null` when the app has none (no rows seeded). */
|
|
12
|
+
readonly dbPackage: string | null;
|
|
13
|
+
/** The build that is about to run the test, to say so when it is not the captured one. */
|
|
14
|
+
readonly build: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function requestTestSource(bundle: FailureBundle, meta: RequestSourceMeta): string {
|
|
18
|
+
const masked = bundle.masked.length === 0 ? 'none' : bundle.masked.join(', ');
|
|
19
|
+
const drift =
|
|
20
|
+
bundle.build !== null && meta.build !== null && bundle.build !== meta.build
|
|
21
|
+
? `Captured on build ${bundle.build}, replayed on ${meta.build}: a different outcome may be the fix already shipped.`
|
|
22
|
+
: `Captured on build ${bundle.build ?? 'unknown'}.`;
|
|
23
|
+
const lines = [
|
|
24
|
+
`x repro ${meta.id}: ${bundle.primitive} "${bundle.name}" answered ${bundle.code} (fingerprint ${bundle.fingerprint}).`,
|
|
25
|
+
'Written by `x repro --write`. Red until the fault is fixed: the rows it read are seeded, the',
|
|
26
|
+
'clock is frozen at the failure, and it runs again as the same actor with the same input.',
|
|
27
|
+
`Masked input fields (${masked}) are stood in for from the schema. ${drift}`,
|
|
28
|
+
...(bundle.truncated
|
|
29
|
+
? ['The bundle was truncated at capture: some rows it read are not here.']
|
|
30
|
+
: []),
|
|
31
|
+
];
|
|
32
|
+
const driverImport =
|
|
33
|
+
meta.dbPackage === null ? '' : `import { driver } from '${meta.dbPackage}';\n`;
|
|
34
|
+
const options = meta.dbPackage === null ? '' : ', { driver }';
|
|
35
|
+
return `${header(lines)}
|
|
36
|
+
|
|
37
|
+
import type { FailureBundle } from '@ultimat3/core';
|
|
38
|
+
import { expect, reproduce, test } from '@ultimat3/testing';
|
|
39
|
+
${driverImport}${meta.imports.map((specifier) => `import '${specifier}';`).join('\n')}
|
|
40
|
+
|
|
41
|
+
const BUNDLE: FailureBundle = ${tsLiteral(bundle)};
|
|
42
|
+
|
|
43
|
+
test(${tsLiteral(`repro ${meta.id.slice(0, 8)}: ${bundle.name} answers without ${bundle.code}`)}, async () => {
|
|
44
|
+
const outcome = await reproduce(BUNDLE${options});
|
|
45
|
+
expect(outcome.failure).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
`;
|
|
48
|
+
}
|