@ultimat3/cli 29.4.0 → 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 +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 +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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `x g resource <name>`'s page: the plural route a slice is reached at. It READS —
|
|
2
|
-
// slice's own query as the request's actor
|
|
3
|
-
//
|
|
1
|
+
// `x g resource <name>`'s page: the plural route a slice is reached at. It READS — a `<Hole>` calls
|
|
2
|
+
// the slice's own query a page at a time as the request's actor, after the shell left — renders the
|
|
3
|
+
// rows in a link-mode `DataTable` (loading, failed, empty, ready; paged by URL, no script), and
|
|
4
|
+
// mounts the slice's form island. `x g route`
|
|
4
5
|
// writes a page with a heading and nothing to read; a resource has rows and a form, and a page
|
|
5
6
|
// that showed neither was a slice nobody could reach without wiring it by hand.
|
|
6
7
|
|
|
@@ -8,7 +9,8 @@ import { sortedImports } from './imports';
|
|
|
8
9
|
import type { GeneratedFile, NameSet } from './naming';
|
|
9
10
|
import { titleKey } from './naming';
|
|
10
11
|
import { formIslandSpecifier } from './resource-form-island';
|
|
11
|
-
import {
|
|
12
|
+
import { pageDbTest } from './resource-page-db';
|
|
13
|
+
import { LINE_WIDTH, wrapImport } from './wrap';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* The page's JS budget — MEASURED, never a ceiling picked to pass. `x build --target static` on a
|
|
@@ -20,9 +22,18 @@ import { LINE_WIDTH } from './wrap';
|
|
|
20
22
|
* byte-identical since 1.4.1 (issue #354), so no headroom is held for run-to-run movement. An app
|
|
21
23
|
* that adds per-document scripts (`navigation: { client: [...] }` charges its router, ~18 kB, to
|
|
22
24
|
* every `app/` page) is over it on its first build, and `nextSteps` sends the author there.
|
|
25
|
+
*
|
|
26
|
+
* Raised to 68kb (69,632) on 2026-11-06, measured on Bun 1.4.4 the same way: 66,285 B — the island
|
|
27
|
+
* 65,430 + the runtime 855, over 64kb by 749 B. why: `@ultimat3/ui` grew a wave of components
|
|
28
|
+
* (pickers, lists, viewers, drag, charts) and the island carries ui's whole catalog-key list
|
|
29
|
+
* (`i18n-keys.ts`) beside the Form/Input/Button it renders. The same 3.3 kB of headroom.
|
|
30
|
+
*
|
|
31
|
+
* Lowered to 66kb (67,584) the same day, measured the same way: 64,167 B — the island 63,312 +
|
|
32
|
+
* the runtime 855. why: `UI_KEYS` became a namespace over `ui-keys.ts`, so the island keeps the
|
|
33
|
+
* three keys its components read, not all of them (−2,118 B). The same ~3.4 kB of headroom.
|
|
23
34
|
*/
|
|
24
|
-
export const RESOURCE_PAGE_BUDGET = '
|
|
25
|
-
export const RESOURCE_PAGE_MEASURED = '
|
|
35
|
+
export const RESOURCE_PAGE_BUDGET = '66kb';
|
|
36
|
+
export const RESOURCE_PAGE_MEASURED = '64,167 B';
|
|
26
37
|
|
|
27
38
|
/** How the page reaches a string — the same rule `route.ts` states, for the same issue (#249). */
|
|
28
39
|
const catalogImport = (module: string | undefined): string =>
|
|
@@ -36,11 +47,12 @@ const i18nImports = (module: string | undefined): readonly string[] =>
|
|
|
36
47
|
? ["import { currentLocale, t } from '@ultimat3/i18n';"]
|
|
37
48
|
: [catalogImport(module), "import { currentLocale } from '@ultimat3/i18n';"];
|
|
38
49
|
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
/** The read in `rows()`, as Biome prints it at any name length. */
|
|
51
|
+
const stateLine = (read: string): string => {
|
|
52
|
+
const line = ` const state = await rowsState(() => ${read}.page(inputOf(list), windowOf(list)));`;
|
|
53
|
+
return line.length <= LINE_WIDTH
|
|
54
|
+
? line
|
|
55
|
+
: ` const state = await rowsState(() =>\n ${read}.page(inputOf(list), windowOf(list)),\n );`;
|
|
44
56
|
};
|
|
45
57
|
|
|
46
58
|
export interface ResourcePageOptions {
|
|
@@ -59,6 +71,7 @@ const pageSource = (options: ResourcePageOptions): string => {
|
|
|
59
71
|
const { feature } = options;
|
|
60
72
|
const path = feature.pluralKebab;
|
|
61
73
|
const list = `${feature.camel}List`;
|
|
74
|
+
const translator = options.catalogModule === undefined ? '' : '\n const t = useT();\n';
|
|
62
75
|
const open = options.shell
|
|
63
76
|
? '<Shell>\n <div class={styles.page}>'
|
|
64
77
|
: '<main class={styles.page}>';
|
|
@@ -69,24 +82,25 @@ const pageSource = (options: ResourcePageOptions): string => {
|
|
|
69
82
|
.split('\n')
|
|
70
83
|
.map((line) => (line === '' ? line : `${indent}${line}`))
|
|
71
84
|
.join('\n');
|
|
72
|
-
return `// /${path}: the org's ${feature.pluralKebab}, newest first, and the form that adds one.
|
|
73
|
-
//
|
|
74
|
-
// the
|
|
85
|
+
return `// /${path}: the org's ${feature.pluralKebab}, newest first, and the form that adds one.
|
|
86
|
+
// The rows are read inside a \`<Hole>\`, through the slice's own query, so the shell and the form
|
|
87
|
+
// are on screen before the read answers; the policy, the tenancy and the order are the query's.
|
|
88
|
+
// The pager is links (\`listUrl\`), so paging needs no script; the form is the slice's island, the
|
|
89
|
+
// one module of this page a browser downloads.
|
|
75
90
|
|
|
76
91
|
${sortedImports([
|
|
77
92
|
...i18nImports(options.catalogModule),
|
|
78
93
|
"import { derivePath } from '@ultimat3/action';",
|
|
79
|
-
"import type { AsyncState } from '@ultimat3/core';",
|
|
94
|
+
"import type { AsyncState, Page as RowPage } from '@ultimat3/core';",
|
|
80
95
|
"import type { KnownPermission } from '@ultimat3/policy';",
|
|
81
|
-
"import { defineRoute, island } from '@ultimat3/render';",
|
|
82
|
-
"import {
|
|
96
|
+
"import { defineRoute, Hole, island } from '@ultimat3/render';",
|
|
97
|
+
"import {\n DataTable,\n type ListUrl,\n listUrl,\n Money,\n PageHeader,\n Section,\n Skeleton,\n} from '@ultimat3/ui';",
|
|
83
98
|
])}
|
|
84
99
|
${options.shell ? "import { Shell } from '../../shared/shell';\n" : ''}import type { ${feature.pascal} } from '../${feature.kebab}/entity';
|
|
85
|
-
|
|
86
|
-
import { ${feature.pascal}List } from '../${feature.kebab}/ui';
|
|
100
|
+
${wrapImport([list, `${list}Sorts`], `../${feature.kebab}/live/${feature.kebab}-list`)}
|
|
87
101
|
import styles from './page.module.scss';
|
|
88
102
|
|
|
89
|
-
/**
|
|
103
|
+
/** The most rows one page shows; \`?limit=\` may ask for fewer, never more. */
|
|
90
104
|
const PAGE_ROWS = 50;
|
|
91
105
|
|
|
92
106
|
/** The currency a price is typed in; the form sends integer minor units of it. */
|
|
@@ -94,14 +108,14 @@ const CURRENCY = 'USD';
|
|
|
94
108
|
|
|
95
109
|
type Row = ${feature.pascal};
|
|
96
110
|
|
|
97
|
-
/**
|
|
98
|
-
export type PageData = AsyncState<
|
|
111
|
+
/** The read's outcome as DATA, so the table renders each of its states. */
|
|
112
|
+
export type PageData = AsyncState<RowPage<Row>>;
|
|
99
113
|
|
|
100
114
|
/**
|
|
101
|
-
* The read, as a state. A refusal
|
|
102
|
-
* fix on screen, the form still there — never a 500 for the whole document.
|
|
115
|
+
* The read, as a state. A refusal, a failed read or a stale cursor is the table's error branch —
|
|
116
|
+
* code, cause and fix on screen, the form still there — never a 500 for the whole document.
|
|
103
117
|
*/
|
|
104
|
-
export async function rowsState(read: () => Promise<
|
|
118
|
+
export async function rowsState(read: () => Promise<RowPage<Row>>): Promise<PageData> {
|
|
105
119
|
try {
|
|
106
120
|
return { status: 'ready', data: await read() };
|
|
107
121
|
} catch (error) {
|
|
@@ -109,10 +123,62 @@ export async function rowsState(read: () => Promise<readonly Row[]>): Promise<Pa
|
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
126
|
+
/** The list's state in the URL: a sort among the query's own columns, a cursor, a page size. */
|
|
127
|
+
type ListState = ListUrl<(typeof ${list}Sorts)[number]>;
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
const listOf = (url: string | undefined): ListState =>
|
|
130
|
+
listUrl(url ?? '/${path}', {
|
|
131
|
+
sorts: ${list}Sorts,
|
|
132
|
+
limit: { default: PAGE_ROWS, max: PAGE_ROWS },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* No org: the query reads the ACTOR's, so a read can only ever be for the caller's own org. No
|
|
137
|
+
* sort in the URL is the query's own order, newest first; a cursor is minted for one sort alone,
|
|
138
|
+
* and \`listUrl\` drops it whenever the sort changes.
|
|
139
|
+
*/
|
|
140
|
+
const inputOf = (list: ListState) =>
|
|
141
|
+
list.sort === undefined
|
|
142
|
+
? { limit: PAGE_ROWS }
|
|
143
|
+
: { limit: PAGE_ROWS, sort: list.sort.key, dir: list.sort.direction };
|
|
144
|
+
|
|
145
|
+
/** The page the URL names: its size, and the cursor it reads after when it is not the first. */
|
|
146
|
+
const windowOf = (list: ListState) =>
|
|
147
|
+
list.cursor === undefined ? { first: list.limit } : { first: list.limit, after: list.cursor };
|
|
148
|
+
|
|
149
|
+
/** The rows, once they are read. Called by the \`<Hole>\`: it starts after the first flush. */
|
|
150
|
+
async function rows(list: ListState) {${translator}
|
|
151
|
+
${stateLine(list)}
|
|
152
|
+
const page = state.status === 'ready' ? state.data : undefined;
|
|
153
|
+
return (
|
|
154
|
+
<DataTable
|
|
155
|
+
caption={t('${titleKey(path)}')}
|
|
156
|
+
rows={page?.rows ?? []}
|
|
157
|
+
rowKey={(row: Row) => row.id}
|
|
158
|
+
error={state.status === 'failed' ? state.error : undefined}
|
|
159
|
+
emptyTitle={t('app.${feature.kebab}.empty')}
|
|
160
|
+
columns={[
|
|
161
|
+
{
|
|
162
|
+
key: 'title',
|
|
163
|
+
header: t('app.${feature.kebab}.titleLabel'),
|
|
164
|
+
sortable: true,
|
|
165
|
+
cell: (row) => row.title,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: 'price',
|
|
169
|
+
header: t('app.${feature.kebab}.priceLabel'),
|
|
170
|
+
numeric: true,
|
|
171
|
+
cell: (row) => <Money value={row.price} />,
|
|
172
|
+
},
|
|
173
|
+
]}
|
|
174
|
+
sort={list.sort}
|
|
175
|
+
sortHrefFor={list.sortHrefFor}
|
|
176
|
+
nextCursor={page?.nextCursor}
|
|
177
|
+
hrefFor={list.hrefFor}
|
|
178
|
+
firstHref={list.firstHref}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
116
182
|
|
|
117
183
|
// Declared ABOVE \`defineRoute\`: the route drains the islands declared before it. Named by
|
|
118
184
|
// SPECIFIER, never imported — a string has no import edge, so the page's bundle graph stays the
|
|
@@ -123,28 +189,24 @@ const ${feature.pascal}Form = island({
|
|
|
123
189
|
});
|
|
124
190
|
|
|
125
191
|
export const config = defineRoute({
|
|
126
|
-
render: '
|
|
192
|
+
render: 'stream',
|
|
127
193
|
hydrate: 'visible',
|
|
128
194
|
offline: 'runtime',
|
|
129
195
|
// An org's rows are not public: a route with no policy registers \`auth: 'public'\` and its
|
|
130
196
|
// response carries no \`vary: cookie\`. Which ORG is the query's decision, on every read.
|
|
131
197
|
policy: { permission: '${feature.kebab}:read' satisfies KnownPermission },
|
|
132
|
-
// measured: ${RESOURCE_PAGE_MEASURED} (\`x build --target static\`, Bun 1.4.
|
|
198
|
+
// measured: ${RESOURCE_PAGE_MEASURED} (\`x build --target static\`, Bun 1.4.4) — the form island and the
|
|
133
199
|
// \`visible\` runtime. why: the form is Solid, @ultimat3/ui's Form/Input/Button and the transport.
|
|
134
200
|
// A raise states its own measured bytes and why here, in the same diff (\`X_BUDGET_EXCEEDED\`).
|
|
135
201
|
budget: { js: '${RESOURCE_PAGE_BUDGET}' },
|
|
136
|
-
load,
|
|
137
202
|
meta: ({ t }) => ({
|
|
138
203
|
title: t('${titleKey(path)}'),
|
|
139
204
|
description: t('${titleKey(path).replace('.title', '.description')}'),
|
|
140
205
|
}),
|
|
141
206
|
});
|
|
142
207
|
|
|
143
|
-
export
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function Page(props: PageProps) {${options.catalogModule === undefined ? '' : '\n const t = useT();\n'}
|
|
208
|
+
export function Page(props: { readonly url?: string | undefined }) {${options.catalogModule === undefined ? '' : '\n const t = useT();\n'}
|
|
209
|
+
const list = listOf(props.url);
|
|
148
210
|
return (
|
|
149
211
|
${open}
|
|
150
212
|
${inner(`<PageHeader title={t('${titleKey(path)}')} />
|
|
@@ -164,12 +226,7 @@ ${inner(`<PageHeader title={t('${titleKey(path)}')} />
|
|
|
164
226
|
<Skeleton lines={3} />
|
|
165
227
|
</${feature.pascal}Form>
|
|
166
228
|
</Section>
|
|
167
|
-
<
|
|
168
|
-
state={props.data}
|
|
169
|
-
reserve={{ lines: 3 }}
|
|
170
|
-
empty={() => <EmptyState title={t('app.${feature.kebab}.empty')} />}
|
|
171
|
-
ready={(rows) => <${feature.pascal}List rows={rows} />}
|
|
172
|
-
/>`)}
|
|
229
|
+
<Hole fallback={<Skeleton lines={3} />}>{() => rows(list)}</Hole>`)}
|
|
173
230
|
${close}
|
|
174
231
|
);
|
|
175
232
|
}
|
|
@@ -182,9 +239,9 @@ const OTHER_ORG = '00000000-0000-4000-8000-000000000009';
|
|
|
182
239
|
const pageTest = (options: ResourcePageOptions): string => {
|
|
183
240
|
const { feature } = options;
|
|
184
241
|
const t = options.catalogModule === undefined ? '' : '\n const t = useT();';
|
|
185
|
-
return `// /${feature.pluralKebab}, rendered as a request renders it
|
|
186
|
-
// slice's query — the in-memory driver under test — and the
|
|
187
|
-
// or the refusal, with the form island beside them in
|
|
242
|
+
return `// /${feature.pluralKebab}, rendered as a request renders it, with the \`<Hole>\` awaited in line: the
|
|
243
|
+
// read names the actor's org through the slice's query — the in-memory driver under test — and the
|
|
244
|
+
// page shows the rows, the empty state or the refusal, with the form island beside them in each.
|
|
188
245
|
${sortedImports([
|
|
189
246
|
catalogImport(options.catalogModule),
|
|
190
247
|
`import { driver } from '${options.dbModule}';`,
|
|
@@ -224,7 +281,7 @@ unitTest('the page lists the org’s rows, newest first, beside the form', async
|
|
|
224
281
|
clock.advance(1000);
|
|
225
282
|
await store('newer');
|
|
226
283
|
const view = await renderRoute(page, { url, actor: member });
|
|
227
|
-
expect(view.
|
|
284
|
+
expect(view.text).toContain('newer');
|
|
228
285
|
expect(view.text.indexOf('newer')).toBeLessThan(view.text.indexOf('older'));
|
|
229
286
|
// The form is the one island, and it wakes when it is visible.
|
|
230
287
|
expect(view.islands.map((island) => island.strategy)).toEqual(['visible']);
|
|
@@ -233,30 +290,78 @@ unitTest('the page lists the org’s rows, newest first, beside the form', async
|
|
|
233
290
|
|
|
234
291
|
unitTest('an org with no rows reads the empty state, not an empty list', async () => {${t}
|
|
235
292
|
const view = await renderRoute(page, { url, actor: member });
|
|
236
|
-
expect(view.data).toEqual({ status: 'ready', data: [] });
|
|
237
293
|
expect(view.text).toContain(t('app.${feature.kebab}.empty'));
|
|
238
294
|
expect(view.meta.title).toBe(t('${titleKey(feature.pluralKebab)}'));
|
|
239
295
|
});
|
|
240
296
|
|
|
241
|
-
unitTest('a member of another org reads none of this org’s rows', async () => {
|
|
297
|
+
unitTest('a member of another org reads none of this org’s rows', async () => {${t}
|
|
242
298
|
await store('ours');
|
|
243
299
|
// The same page, the same URL: which org it reads is the actor's, never a parameter.
|
|
244
300
|
const outsider = testActor('outsider', { orgId: otherOrg, permissions: [read] }).actor;
|
|
245
301
|
const view = await renderRoute(page, { url, actor: outsider });
|
|
246
|
-
expect(view.
|
|
302
|
+
expect(view.text).not.toContain('ours');
|
|
303
|
+
expect(view.text).toContain(t('app.${feature.kebab}.empty'));
|
|
247
304
|
});
|
|
248
305
|
|
|
249
306
|
unitTest('a caller the read refuses sees the refusal, and the page around it', async () => {
|
|
250
307
|
// In the org, without the grant: the query's policy refuses before any row is read.
|
|
251
308
|
const stranger = testActor('stranger', { orgId }).actor;
|
|
252
309
|
const view = await renderRoute(page, { url, actor: stranger });
|
|
253
|
-
expect(view.data.status).toBe('failed');
|
|
254
310
|
expect(view.html).toContain('X_FORBIDDEN');
|
|
255
311
|
expect(view.islands).toHaveLength(1);
|
|
256
312
|
});
|
|
257
313
|
|
|
314
|
+
/** The anchor a pager side renders, read off the HTML: \`rel\` names the side. */
|
|
315
|
+
const linkTo = (html: string, rel: string): string | undefined => {
|
|
316
|
+
const anchor = new RegExp(\`<a\\\\b[^>]*\\\\brel="\${rel}"[^>]*>\`).exec(html)?.[0];
|
|
317
|
+
return /href="([^"]*)"/.exec(anchor ?? '')?.[1]?.replaceAll('&', '&');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
unitTest('the list pages by link: a cursor in the URL, no script', async () => {
|
|
321
|
+
await store('older');
|
|
322
|
+
clock.advance(1000);
|
|
323
|
+
await store('newer');
|
|
324
|
+
const first = await renderRoute(page, { url: \`\${url}?limit=1\`, actor: member });
|
|
325
|
+
expect(first.text).toContain('newer');
|
|
326
|
+
expect(first.text).not.toContain('older');
|
|
327
|
+
// The first page has nowhere back to, and a keyset page has no Previous.
|
|
328
|
+
expect(first.html).not.toContain('href="/${feature.pluralKebab}?limit=1"');
|
|
329
|
+
expect(linkTo(first.html, 'prev')).toBeUndefined();
|
|
330
|
+
const next = linkTo(first.html, 'next');
|
|
331
|
+
expect(next).toContain('after=');
|
|
332
|
+
|
|
333
|
+
const second = await renderRoute(page, { url: new URL(next ?? '', url).href, actor: member });
|
|
334
|
+
expect(second.text).toContain('older');
|
|
335
|
+
expect(second.text).not.toContain('newer');
|
|
336
|
+
// The last page links back to the first and on to nothing, never to \`after=null\`.
|
|
337
|
+
expect(second.html).toContain('href="/${feature.pluralKebab}?limit=1"');
|
|
338
|
+
expect(linkTo(second.html, 'next')).toBeUndefined();
|
|
339
|
+
expect(second.html).not.toContain('after=null');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
unitTest('the list sorts by link, and a sorted list pages in that order', async () => {
|
|
343
|
+
// Written so that newest-first and by-title disagree: a sort applied after the bound would show
|
|
344
|
+
// the two newest re-sorted, never the first two titles.
|
|
345
|
+
for (const title of ['beta', 'gamma', 'alpha']) {
|
|
346
|
+
await store(title);
|
|
347
|
+
clock.advance(1000);
|
|
348
|
+
}
|
|
349
|
+
const sorted = \`\${url}?sort=title&dir=asc&limit=2\`;
|
|
350
|
+
const first = await renderRoute(page, { url: sorted, actor: member });
|
|
351
|
+
expect(first.text.indexOf('alpha')).toBeLessThan(first.text.indexOf('beta'));
|
|
352
|
+
expect(first.text).not.toContain('gamma');
|
|
353
|
+
// The header links on to the next state of its cycle, the cursor dropped with the old order.
|
|
354
|
+
const descending = 'href="/${feature.pluralKebab}?sort=title&dir=desc&limit=2"';
|
|
355
|
+
expect(first.html).toContain(descending);
|
|
356
|
+
const next = linkTo(first.html, 'next');
|
|
357
|
+
expect(next).toContain('sort=title');
|
|
358
|
+
const second = await renderRoute(page, { url: new URL(next ?? '', url).href, actor: member });
|
|
359
|
+
expect(second.text).toContain('gamma');
|
|
360
|
+
expect(second.text).not.toContain('alpha');
|
|
361
|
+
});
|
|
362
|
+
|
|
258
363
|
unitTest('it is gated, rendered per request, and budgeted', () => {
|
|
259
|
-
expect(page.config.render).toBe('
|
|
364
|
+
expect(page.config.render).toBe('stream');
|
|
260
365
|
expect(page.config.policy?.permission).toBe(read);
|
|
261
366
|
expect(page.config.offline).toBe('runtime');
|
|
262
367
|
expect(page.config.budget.js).toBe('${RESOURCE_PAGE_BUDGET}');
|
|
@@ -264,8 +369,12 @@ unitTest('it is gated, rendered per request, and budgeted', () => {
|
|
|
264
369
|
`;
|
|
265
370
|
};
|
|
266
371
|
|
|
267
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* The resource's `page.tsx`, its unit test, and the same read over the migrated database. The
|
|
374
|
+
* stylesheet, e2e test and catalog are `x g route`'s.
|
|
375
|
+
*/
|
|
268
376
|
export const resourcePageFiles = (options: ResourcePageOptions): readonly GeneratedFile[] => [
|
|
269
377
|
{ path: `${options.pageDir}/page.tsx`, contents: pageSource(options) },
|
|
270
378
|
{ path: `${options.pageDir}/page.test.ts`, contents: pageTest(options) },
|
|
379
|
+
{ path: `${options.pageDir}/page-db.test.ts`, contents: pageDbTest(options) },
|
|
271
380
|
];
|
package/src/templates/route.ts
CHANGED
|
@@ -13,13 +13,11 @@ import { kebab, pascal, titleKey } from './naming';
|
|
|
13
13
|
export type Surface = 'site' | 'app';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* `
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
* NO route at all, so every `x g route --surface app` and every `x g resource` scaffolded a URL
|
|
20
|
-
* that was absent from `x routes`, from the manifest and from `budgets`. Ship the mode that works.
|
|
16
|
+
* `stream` on `app/`: the surface's default (`surfaces.ts`), and a stream is always
|
|
17
|
+
* `private, no-store` where an ungated `ssr` page may be public. A page with no `<Hole>` pays
|
|
18
|
+
* nothing for it (`renderStreamHtml`), and the first read an author moves into one streams.
|
|
21
19
|
*/
|
|
22
|
-
const RENDER: Record<Surface, string> = { site: 'isr', app: '
|
|
20
|
+
const RENDER: Record<Surface, string> = { site: 'isr', app: 'stream' };
|
|
23
21
|
/**
|
|
24
22
|
* `app` is `'visible'` on a page the generator writes with no island, and that is not an oversight.
|
|
25
23
|
* A declared strategy costs nothing when there is nothing to hydrate — `hydrateRuntime` is emitted
|
|
@@ -10,6 +10,7 @@ import { dashboardFiles } from './scaffold-dashboard';
|
|
|
10
10
|
import { demoOrgFiles } from './scaffold-demo-org';
|
|
11
11
|
import { entryFiles } from './scaffold-entries';
|
|
12
12
|
import { errorPageFiles } from './scaffold-errors';
|
|
13
|
+
import { exerciseFiles } from './scaffold-exercise';
|
|
13
14
|
import { httpFiles } from './scaffold-http';
|
|
14
15
|
import { icon } from './scaffold-icon';
|
|
15
16
|
import { rolesFiles } from './scaffold-roles';
|
|
@@ -407,6 +408,8 @@ export function appFiles(app: NameSet, example: boolean): readonly GeneratedFile
|
|
|
407
408
|
{ path: 'apps/web/tsconfig.json', contents: tsconfig() },
|
|
408
409
|
// The process a container starts and the artifact a CDN is handed — `scaffold-entries.ts`.
|
|
409
410
|
...entryFiles(),
|
|
411
|
+
// What `x exercise` cannot guess, empty until a finding says what to paste — `scaffold-exercise.ts`.
|
|
412
|
+
...exerciseFiles(),
|
|
410
413
|
{ path: 'apps/web/site/icon.png', contents: icon() },
|
|
411
414
|
// The landing page: hero, two calls to action, three feature cards — `scaffold-site.ts`.
|
|
412
415
|
...siteFiles(app),
|
|
@@ -98,7 +98,8 @@ export const devAuthenticate = (request: HeaderRequest): Actor | null => {
|
|
|
98
98
|
};
|
|
99
99
|
`;
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
/** `apps/web/app/auth/auth.ts`: also `x g sign-in`'s, written only where an older app lacks it. */
|
|
102
|
+
export const authModule =
|
|
102
103
|
(): string => `// This app's one \`@ultimat3/auth\` instance: sessions, passwords and the lockout, over the
|
|
103
104
|
// framework's own \`x_users\` and \`x_sessions\` tables — \`x dev\` and \`ROLE=migrate\` apply them.
|
|
104
105
|
//
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The script a scaffolded app is set up by — `bun run setup` — written as TypeScript that Bun runs,
|
|
2
|
+
// never as shell. A bash `bin/setup` was the first thing a PowerShell or cmd user hit after `x new`,
|
|
3
|
+
// and it failed there. `bun run dev` and `bun run verify` need no file: they are `x dev` and
|
|
4
|
+
// `x verify`, and `x verify` builds what its budgets step weighs (no `bin/check`, plan 101 21).
|
|
5
5
|
|
|
6
6
|
import type { GeneratedFile } from './naming';
|
|
7
7
|
|
|
8
8
|
/** Where each script lands, and what the root `package.json`'s `scripts` run — named once. */
|
|
9
9
|
export const BIN_SCRIPTS = {
|
|
10
10
|
setup: 'bin/setup.ts',
|
|
11
|
-
check: 'bin/check.ts',
|
|
12
11
|
} as const;
|
|
13
12
|
|
|
14
13
|
/** The `package.json` script line that runs a bin file: Bun itself, so no shell interprets it. */
|
|
@@ -70,53 +69,7 @@ await x('manifest');
|
|
|
70
69
|
console.log('setup complete — next: bun run dev');
|
|
71
70
|
`;
|
|
72
71
|
|
|
73
|
-
const binCheck =
|
|
74
|
-
(): string => `// The gate. Same steps as CI, because a check that lives only in CI cannot be run locally.
|
|
75
|
-
// \`bun run check\` — Bun runs this file, so it is the same gate under PowerShell, cmd or bash.
|
|
76
|
-
import { join } from 'node:path';
|
|
77
|
-
|
|
78
|
-
const root = join(import.meta.dir, '..');
|
|
79
|
-
const args = process.argv.slice(2);
|
|
80
|
-
|
|
81
|
-
/** The app's own CLI, as \`bunx x\` resolves it, from the app root; resolves to its exit code. */
|
|
82
|
-
const x = async (...argv: string[]): Promise<number> => {
|
|
83
|
-
const child = Bun.spawn([process.execPath, 'x', 'x', ...argv], {
|
|
84
|
-
cwd: root,
|
|
85
|
-
stdio: ['inherit', 'inherit', 'inherit'],
|
|
86
|
-
});
|
|
87
|
-
// The terminal already hands Ctrl-C to the child; this process only waits for its verdict. A
|
|
88
|
-
// SIGTERM is forwarded, so a CI timeout stops the gate instead of orphaning it mid-step.
|
|
89
|
-
const wait = (): void => {};
|
|
90
|
-
const stop = (): void => {
|
|
91
|
-
child.kill('SIGTERM');
|
|
92
|
-
};
|
|
93
|
-
process.on('SIGINT', wait);
|
|
94
|
-
process.on('SIGTERM', stop);
|
|
95
|
-
const code = await child.exited;
|
|
96
|
-
process.off('SIGINT', wait);
|
|
97
|
-
process.off('SIGTERM', stop);
|
|
98
|
-
return code;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// The build FIRST, and not as a convenience: \`x verify\`'s budgets step compares declared limits
|
|
102
|
-
// against measured bytes in .x/build-stats.json, so with no build it reports X_BUDGET_UNMEASURED
|
|
103
|
-
// and the very first gate anyone runs on a brand-new app is red for a reason that has nothing to
|
|
104
|
-
// do with their code.
|
|
105
|
-
//
|
|
106
|
-
// \`--json\` is forwarded to BOTH, or the contract breaks: the build's human renderer followed by
|
|
107
|
-
// the gate's JSON is neither on stdout. Both commands emit one object; a reader takes the last line.
|
|
108
|
-
// \`--no-preflight\`: the build's own preflight is x verify's first six steps (typecheck, lint,
|
|
109
|
-
// boundaries, filesize, package-shape, errors), and the gate below runs them anyway.
|
|
110
|
-
const json = args.some((arg) => arg === '--json' || arg === '-j') ? ['--json'] : [];
|
|
111
|
-
const built = await x('build', '--target', 'static', '--no-preflight', ...json);
|
|
112
|
-
if (built !== 0) process.exit(built);
|
|
113
|
-
process.exit(await x('verify', ...args));
|
|
114
|
-
`;
|
|
115
|
-
|
|
116
72
|
/** The bin scripts, in the order a newcomer runs them. */
|
|
117
73
|
export function binFiles(): readonly GeneratedFile[] {
|
|
118
|
-
return [
|
|
119
|
-
{ path: BIN_SCRIPTS.setup, contents: binSetup() },
|
|
120
|
-
{ path: BIN_SCRIPTS.check, contents: binCheck() },
|
|
121
|
-
];
|
|
74
|
+
return [{ path: BIN_SCRIPTS.setup, contents: binSetup() }];
|
|
122
75
|
}
|
|
@@ -7,7 +7,7 @@ import type { GeneratedFile, NameSet } from './naming';
|
|
|
7
7
|
|
|
8
8
|
const shape = (app: NameSet): string => `---
|
|
9
9
|
name: shape
|
|
10
|
-
description: Use BEFORE writing any code, at the idea stage — turns a product request into the one decision that governs everything after it: which of the eight primitives each piece is, which slice and surface it lives in, and the exact \`x g\` invocations. Read-only; it decides, it never builds. Also use when a request seems to need something the framework does not have.
|
|
10
|
+
description: Use BEFORE writing any code, at the idea stage — turns a product request into the one decision that governs everything after it: which of the eight primitives each piece is, which slice and surface it lives in, and the exact \`bunx x g\` invocations. Read-only; it decides, it never builds. Also use when a request seems to need something the framework does not have.
|
|
11
11
|
tools: Read, Glob, Grep, Bash
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -45,8 +45,8 @@ stop; that answer is worth more than a plan built on a shape the app cannot hold
|
|
|
45
45
|
| \`packages/db/\` | schema, migrations, seed |
|
|
46
46
|
| \`packages/ui/\` | components with no feature knowledge |
|
|
47
47
|
|
|
48
|
-
Read the tree before you place anything. \`x routes\`, \`x actions\`, \`x queries\`, \`x entities\`,
|
|
49
|
-
\`x jobs\`, \`x tasks\` and \`x policy list\` are the registries — check whether the thing already
|
|
48
|
+
Read the tree before you place anything. \`bunx x routes\`, \`bunx x actions\`, \`bunx x queries\`, \`bunx x entities\`,
|
|
49
|
+
\`bunx x jobs\`, \`bunx x tasks\` and \`bunx x policy list\` are the registries — check whether the thing already
|
|
50
50
|
exists before proposing it, and add \`--json\` to any of them.
|
|
51
51
|
|
|
52
52
|
## Report
|
|
@@ -54,7 +54,7 @@ exists before proposing it, and add \`--json\` to any of them.
|
|
|
54
54
|
\`\`\`
|
|
55
55
|
Build: yes | no — <one line>
|
|
56
56
|
Pieces: <primitive> <name> → <dir> (one line each)
|
|
57
|
-
Generate: x g <kind> <name> --feature <slice> (one line each, in dependency order)
|
|
57
|
+
Generate: bunx x g <kind> <name> --feature <slice> (one line each, in dependency order)
|
|
58
58
|
Existing: <what already covers part of this>
|
|
59
59
|
Refused: <anything that fits no primitive, and why>
|
|
60
60
|
\`\`\`
|
|
@@ -72,17 +72,17 @@ feature slice. Nothing else. A change outside that set is a collision — report
|
|
|
72
72
|
| Rule | Detail |
|
|
73
73
|
|---|---|
|
|
74
74
|
| The entity is the schema | columns, invariants, indexes and tenancy are declared there, never in SQL |
|
|
75
|
-
| Migrations are generated | edit \`entity.ts\`, then \`x db gen "what changed"\`. Never hand-write a file into \`packages/db/migrations/\` |
|
|
75
|
+
| Migrations are generated | edit \`entity.ts\`, then \`bunx x db gen "what changed"\`. Never hand-write a file into \`packages/db/migrations/\` |
|
|
76
76
|
| Destructive is declared | a migration that drops, truncates or retypes carries \`-- destructive: true\`, or the gate refuses it |
|
|
77
77
|
| \`repo.ts\` is the only door | every read and write goes through it; a raw statement anywhere else is authz bypassed |
|
|
78
|
-
| Branch before you break things | \`x db branch create <name>\`, never the shared dev database |
|
|
78
|
+
| Branch before you break things | \`bunx x db branch create <name>\`, never the shared dev database |
|
|
79
79
|
| Money | \`{ minor, currency }\` — integer minor units and an ISO code, both, always. Never a float |
|
|
80
80
|
| Time | store UTC; a formatted date always names an explicit IANA time zone |
|
|
81
81
|
|
|
82
|
-
Inspect before you change: \`x entities list\`, \`x entities show <name>\`, both with \`--json\`.
|
|
82
|
+
Inspect before you change: \`bunx x entities list\`, \`bunx x entities show <name>\`, both with \`--json\`.
|
|
83
83
|
|
|
84
84
|
Checks: \`bun test <path>/entity.test.ts\`, \`bunx biome check --write <paths>\`, and \`bun run typecheck\`
|
|
85
|
-
once when you are otherwise done. Never \`x verify\` — that belongs to whoever coordinates you.
|
|
85
|
+
once when you are otherwise done. Never \`bunx x verify\` — that belongs to whoever coordinates you.
|
|
86
86
|
`;
|
|
87
87
|
|
|
88
88
|
const server = (app: NameSet): string => `---
|
|
@@ -97,19 +97,19 @@ renders. A change outside that set is a collision — report it, do not make it.
|
|
|
97
97
|
|
|
98
98
|
| Rule | Detail |
|
|
99
99
|
|---|---|
|
|
100
|
-
| Generate, then edit | \`x g <kind> <name> --feature <slice>\` for every one of them — a hand-written primitive is missing from every registry that projects it |
|
|
100
|
+
| Generate, then edit | \`bunx x g <kind> <name> --feature <slice>\` for every one of them — a hand-written primitive is missing from every registry that projects it |
|
|
101
101
|
| One authz object | the policy decides; a route or a UI that re-checks is a second answer that will drift |
|
|
102
102
|
| Data through the repo | an action calls \`repo.ts\`, never the database |
|
|
103
|
-
| Errors are instructions | never \`throw new Error\` —
|
|
103
|
+
| Errors are instructions | never \`throw new Error\` — declare each code once with \`defineError({ code, title, status, cause, fix })\` from \`@ultimat3/core\`: a stable \`X_SCREAMING_SNAKE\` code, a cause and a \`fix:\` a caller can actually run. \`guards/error-declared.ts\` refuses a handwritten \`UltimateError\` subclass |
|
|
104
104
|
| Jobs run at least once | a handler must be idempotent — an upsert or a statement whose second run changes nothing, never \`count + 1\` |
|
|
105
105
|
| Tasks name a time zone | a cron schedule with an ambient zone is a different time twice a year |
|
|
106
106
|
| No \`any\` | \`unknown\` plus a schema parse |
|
|
107
107
|
|
|
108
|
-
Inspect before you change: \`x actions show <name>\`, \`x queries show <name>\`, \`x jobs list\`,
|
|
109
|
-
\`x tasks list\`, \`x policy explain <subject>\` — every one takes \`--json\`.
|
|
108
|
+
Inspect before you change: \`bunx x actions show <name>\`, \`bunx x queries show <name>\`, \`bunx x jobs list\`,
|
|
109
|
+
\`bunx x tasks list\`, \`bunx x policy explain <subject>\` — every one takes \`--json\`.
|
|
110
110
|
|
|
111
111
|
Checks: \`bun test <path>/<file>.test.ts\`, \`bunx biome check --write <paths>\`, and \`bun run typecheck\`
|
|
112
|
-
once when you are otherwise done. Never \`x verify\` — that belongs to whoever coordinates you.
|
|
112
|
+
once when you are otherwise done. Never \`bunx x verify\` — that belongs to whoever coordinates you.
|
|
113
113
|
`;
|
|
114
114
|
|
|
115
115
|
const web = (app: NameSet): string => `---
|
|
@@ -124,7 +124,7 @@ calls. A change outside that set is a collision — report it, do not make it.
|
|
|
124
124
|
|
|
125
125
|
| Rule | Detail |
|
|
126
126
|
|---|---|
|
|
127
|
-
| \`site/\` is 0kb JS | and may not import from \`app/\`. One interactive control on a static page is an island: \`x g island <name> --at <dir>\` |
|
|
127
|
+
| \`site/\` is 0kb JS | and may not import from \`app/\`. One interactive control on a static page is an island: \`bunx x g island <name> --at <dir>\` |
|
|
128
128
|
| \`shared/\` is a leaf | tokens, primitives, the actor type. It imports nothing of yours |
|
|
129
129
|
| The directory is the URL | \`page.tsx\` under \`site/\`/\`app/\`; \`api/\` holds no route file, only \`defineApi()\`'s actions and queries. The filename is never the path |
|
|
130
130
|
| Every string through \`t()\` | a literal in a component is a string no locale can ever translate |
|
|
@@ -135,16 +135,16 @@ calls. A change outside that set is a collision — report it, do not make it.
|
|
|
135
135
|
| The focus ring is replaced | \`outline: none\` on its own leaves a keyboard user with nothing. \`@include tokens.focus-ring\`, or a \`:focus-visible\` box-shadow beside it |
|
|
136
136
|
| Every image has a box | width + height, or an aspect-ratio. An unsized image moves everything under it when its bytes land, and the priority one is never \`loading="lazy"\` |
|
|
137
137
|
| Animate transform and opacity | nothing else composites: a \`top\`, a \`width\` or a \`box-shadow\` costs a layout pass every frame, and \`transition: all\` animates properties nobody chose. \`tokens.duration()\` / \`tokens.easing()\` are the values |
|
|
138
|
-
| An island declares its states | a sibling \`<name>.island.states.ts\`, so \`x shot --island <name> --json\` can photograph the failures nobody can click to |
|
|
138
|
+
| An island declares its states | a sibling \`<name>.island.states.ts\`, so \`bunx x shot --island <name> --json\` can photograph the failures nobody can click to |
|
|
139
139
|
|
|
140
|
-
Every one of the last five is a build error, not a preference — \`guards/\` holds them and \`x verify\`'s
|
|
140
|
+
Every one of the last five is a build error, not a preference — \`guards/\` holds them and \`bunx x verify\`'s
|
|
141
141
|
\`boundaries\` step runs them. Read the guard rather than guessing at the rule.
|
|
142
142
|
|
|
143
|
-
Inspect before you change: \`x routes --json\`, \`x i18n check\` for catalog gaps, and
|
|
144
|
-
\`x shot <route> --json\` when you need to see what you built.
|
|
143
|
+
Inspect before you change: \`bunx x routes --json\`, \`bunx x i18n check\` for catalog gaps, and
|
|
144
|
+
\`bunx x shot <route> --json\` when you need to see what you built.
|
|
145
145
|
|
|
146
146
|
Checks: \`bun test <path>/page.test.ts\`, \`bunx biome check --write <paths>\`, and \`bun run typecheck\`
|
|
147
|
-
once when you are otherwise done. Never \`x verify\` — that belongs to whoever coordinates you.
|
|
147
|
+
once when you are otherwise done. Never \`bunx x verify\` — that belongs to whoever coordinates you.
|
|
148
148
|
`;
|
|
149
149
|
|
|
150
150
|
/** One agent per boundary, plus the read-only one that runs before there is a boundary to hold. */
|