@ultimat3/cli 20.2.1 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
14
14
|
| `x verify` | **the gate** | 20 named steps, each with pass/fail + duration |
|
|
15
15
|
| `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
|
|
16
16
|
| `x db gen\|migrate\|reset\|branch\|backfill` | everything DB | `branch` = copy-on-write clone + preview URL; `backfill` dry-runs unless `--write`. `x db studio` is **planned** — it parses, and exits `X_NOT_IMPLEMENTED` naming `/_x`'s db panel |
|
|
17
|
-
| `x mcp serve` | `@ultimat3/mcp`'s
|
|
17
|
+
| `x mcp serve` | `@ultimat3/mcp`'s 18 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
|
|
18
18
|
| `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
|
|
19
19
|
| `x deploy` | container deploy plan | compose or helm; zero platform primitives |
|
|
20
20
|
| `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
|
|
@@ -118,7 +118,7 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
|
|
|
118
118
|
| `app-runtime.ts` | the app's `RuntimeOverrides`: `apps/<app>/runtime.ts` exports `runtime`, read by `x dev` and by `runRole` when its caller passed none |
|
|
119
119
|
| `local-cli.ts` | which `x` runs: a global CLI inside an app hands over to the app's own, because a second module instance is an empty registry |
|
|
120
120
|
| `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
|
|
121
|
-
| `
|
|
121
|
+
| `runtime-live-feed.ts` | what feeds the sync node this process booted: the in-process row observer under the embedded database, the WAL decoder with a real one, nothing without the role — `live=` on the ready line |
|
|
122
122
|
| `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
|
|
123
123
|
| `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
|
|
124
124
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
@@ -160,3 +160,41 @@ Every emitted source has a `<file>.test.ts` beside it that passes on the first r
|
|
|
160
160
|
|
|
161
161
|
`X_CLI_UNKNOWN_COMMAND` `X_CLI_BAD_FLAG` `X_VERIFY_FAILED` `X_NOT_IN_APP` `X_BUN_VERSION`
|
|
162
162
|
`X_NOT_IMPLEMENTED` `X_GUARD_INVALID` `X_GUARD_FAILED` `X_GUARD_FINDING_INVALID`
|
|
163
|
+
|
|
164
|
+
### Error classes
|
|
165
|
+
|
|
166
|
+
Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
|
|
167
|
+
a job boundary the class is gone and the `code` is what survives — match on that.
|
|
168
|
+
|
|
169
|
+
| Class | Code | Declared in |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
172
|
+
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build-errors.ts` |
|
|
173
|
+
| `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
|
|
174
|
+
| `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
|
|
175
|
+
| `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
|
|
176
|
+
| `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
|
|
177
|
+
| `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
|
|
178
|
+
| `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
|
|
179
|
+
| `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
|
|
180
|
+
| `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
|
|
181
|
+
| `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
|
|
182
|
+
| `E2eEvaluateCapturedError` | `X_E2E_EVALUATE_CAPTURED` | `src/e2e-errors.ts` |
|
|
183
|
+
| `E2eEvaluateThrewError` | `X_E2E_EVALUATE_THREW` | `src/e2e-errors.ts` |
|
|
184
|
+
| `E2eEvaluateUnsupportedError` | `X_E2E_EVALUATE_UNSUPPORTED` | `src/e2e-errors.ts` |
|
|
185
|
+
| `E2eLocatorAmbiguousError` | `X_E2E_LOCATOR_AMBIGUOUS` | `src/e2e-errors.ts` |
|
|
186
|
+
| `E2eLocatorEmptyError` | `X_E2E_LOCATOR_EMPTY` | `src/e2e-errors.ts` |
|
|
187
|
+
| `E2eServiceWorkerAbsentError` | `X_E2E_SERVICE_WORKER_ABSENT` | `src/e2e-errors.ts` |
|
|
188
|
+
| `ErrorCodeUnknownError` | `X_ERROR_CODE_UNKNOWN` | `src/errors.ts` |
|
|
189
|
+
| `FixTargetUnknownError` | `X_FIX_TARGET_UNKNOWN` | `src/errors.ts` |
|
|
190
|
+
| `FrameworkSchemaFailedError` | `X_FRAMEWORK_SCHEMA_FAILED` | `src/schema-errors.ts` |
|
|
191
|
+
| `JobUnknownError` | `X_JOB_UNKNOWN` | `src/errors.ts` |
|
|
192
|
+
| `MissingPositionalError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
193
|
+
| `MissingSubcommandError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
194
|
+
| `NoTestFilesError` | `X_TEST_NO_FILES` | `src/errors.ts` |
|
|
195
|
+
| `NotInAppError` | `X_NOT_IN_APP` | `src/errors.ts` |
|
|
196
|
+
| `PortInvalidError` | `X_PORT_INVALID` | `src/errors.ts` |
|
|
197
|
+
| `QuerySubscribesUnknownError` | `X_QUERY_SUBSCRIBES_UNKNOWN` | `src/db-subscribes.ts` |
|
|
198
|
+
| `RoleUnknownError` | `X_ROLE_UNKNOWN` | `src/errors.ts` |
|
|
199
|
+
| `UnknownCommandError` | `X_CLI_UNKNOWN_COMMAND` | `src/errors.ts` |
|
|
200
|
+
| `VerifyFailedError` | `X_VERIFY_FAILED` | `src/errors.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.0.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"provenance": true
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./src/index.ts"
|
|
17
|
+
".": "./src/index.ts",
|
|
18
|
+
"./serve": "./src/serve-entry.ts"
|
|
18
19
|
},
|
|
19
20
|
"bin": {
|
|
20
21
|
"x": "./src/bin.ts"
|
|
@@ -36,35 +37,34 @@
|
|
|
36
37
|
"dev": "bun run src/bin.ts dev"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@babel/core": "^
|
|
40
|
-
"@ultimat3/action": "
|
|
41
|
-
"@ultimat3/admin": "
|
|
42
|
-
"@ultimat3/ai": "
|
|
43
|
-
"@ultimat3/auth": "
|
|
44
|
-
"@ultimat3/cache": "
|
|
45
|
-
"@ultimat3/core": "
|
|
46
|
-
"@ultimat3/db": "
|
|
47
|
-
"@ultimat3/entity": "
|
|
48
|
-
"@ultimat3/flags": "
|
|
49
|
-
"@ultimat3/http": "
|
|
50
|
-
"@ultimat3/i18n": "
|
|
51
|
-
"@ultimat3/jobs": "
|
|
52
|
-
"@ultimat3/mail": "
|
|
53
|
-
"@ultimat3/manifest": "
|
|
54
|
-
"@ultimat3/mcp": "
|
|
55
|
-
"@ultimat3/money": "
|
|
56
|
-
"@ultimat3/notify": "
|
|
57
|
-
"@ultimat3/policy": "
|
|
58
|
-
"@ultimat3/pwa": "
|
|
59
|
-
"@ultimat3/query": "
|
|
60
|
-
"@ultimat3/realtime": "
|
|
61
|
-
"@ultimat3/render": "
|
|
62
|
-
"@ultimat3/schema": "
|
|
63
|
-
"@ultimat3/
|
|
64
|
-
"@ultimat3/
|
|
65
|
-
"@ultimat3/
|
|
66
|
-
"@ultimat3/
|
|
67
|
-
"@ultimat3/time": "20.2.1",
|
|
40
|
+
"@babel/core": "^8.0.1",
|
|
41
|
+
"@ultimat3/action": "22.0.0",
|
|
42
|
+
"@ultimat3/admin": "22.0.0",
|
|
43
|
+
"@ultimat3/ai": "22.0.0",
|
|
44
|
+
"@ultimat3/auth": "22.0.0",
|
|
45
|
+
"@ultimat3/cache": "22.0.0",
|
|
46
|
+
"@ultimat3/core": "22.0.0",
|
|
47
|
+
"@ultimat3/db": "22.0.0",
|
|
48
|
+
"@ultimat3/entity": "22.0.0",
|
|
49
|
+
"@ultimat3/flags": "22.0.0",
|
|
50
|
+
"@ultimat3/http": "22.0.0",
|
|
51
|
+
"@ultimat3/i18n": "22.0.0",
|
|
52
|
+
"@ultimat3/jobs": "22.0.0",
|
|
53
|
+
"@ultimat3/mail": "22.0.0",
|
|
54
|
+
"@ultimat3/manifest": "22.0.0",
|
|
55
|
+
"@ultimat3/mcp": "22.0.0",
|
|
56
|
+
"@ultimat3/money": "22.0.0",
|
|
57
|
+
"@ultimat3/notify": "22.0.0",
|
|
58
|
+
"@ultimat3/policy": "22.0.0",
|
|
59
|
+
"@ultimat3/pwa": "22.0.0",
|
|
60
|
+
"@ultimat3/query": "22.0.0",
|
|
61
|
+
"@ultimat3/realtime": "22.0.0",
|
|
62
|
+
"@ultimat3/render": "22.0.0",
|
|
63
|
+
"@ultimat3/schema": "22.0.0",
|
|
64
|
+
"@ultimat3/seo": "22.0.0",
|
|
65
|
+
"@ultimat3/storage": "22.0.0",
|
|
66
|
+
"@ultimat3/testing": "22.0.0",
|
|
67
|
+
"@ultimat3/time": "22.0.0",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// `x g job` / `x g task` / `x g resource` hand what they wrote to `apps/web/api/index.ts`. The scan
|
|
2
|
+
// registers actions and queries by export name on its own and registers NO job: a job module
|
|
3
|
+
// nothing lists keeps the positional `anonymous-job-2` that `job()` minted, on the queue row, in
|
|
4
|
+
// `x.manifest.json` and in every dead-letter trace — under a green gate (plan 101 slice 11 b).
|
|
5
|
+
|
|
6
|
+
import { containedPath } from './generate-write';
|
|
7
|
+
import { camel } from './templates/naming';
|
|
8
|
+
import { wrapList } from './templates/wrap';
|
|
9
|
+
|
|
10
|
+
/** The one file `defineApi` is called from in a scaffolded app. */
|
|
11
|
+
export const API_INDEX = 'apps/web/api/index.ts';
|
|
12
|
+
|
|
13
|
+
/** One module to import as a namespace and list under a `defineApi` key. */
|
|
14
|
+
export interface ApiEntry {
|
|
15
|
+
readonly key: 'jobs' | 'tasks';
|
|
16
|
+
/** The namespace binding: the file's own name, camelCased — `reindexPost`. */
|
|
17
|
+
readonly binding: string;
|
|
18
|
+
/** From `apps/web/api/`: `../app/post/jobs/reindex-post`. */
|
|
19
|
+
readonly specifier: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PRIMITIVE_PATH =
|
|
23
|
+
/^apps\/web\/(?<rest>[^/]+\/[^/]+\/(?<dir>jobs|tasks)\/(?<file>[a-z0-9-]+))\.ts$/;
|
|
24
|
+
|
|
25
|
+
/** The job and task modules among the paths a generator wrote. Tests are not modules to list. */
|
|
26
|
+
export function apiEntriesFor(written: readonly string[]): readonly ApiEntry[] {
|
|
27
|
+
return written.flatMap((path) => {
|
|
28
|
+
const groups = PRIMITIVE_PATH.exec(path)?.groups;
|
|
29
|
+
if (groups === undefined) return [];
|
|
30
|
+
const { rest = '', dir, file = '' } = groups;
|
|
31
|
+
return [
|
|
32
|
+
{ key: dir === 'tasks' ? 'tasks' : 'jobs', binding: camel(file), specifier: `../${rest}` },
|
|
33
|
+
];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every `[...]` entry of a `key: [...]` list in the `defineApi({ ... })` call. */
|
|
38
|
+
const listOf = (
|
|
39
|
+
source: string,
|
|
40
|
+
key: string,
|
|
41
|
+
): { start: number; end: number; items: string[] } | undefined => {
|
|
42
|
+
const open = new RegExp(`\\n(\\s*)${key}: \\[`).exec(source);
|
|
43
|
+
if (open === null) return undefined;
|
|
44
|
+
const start = open.index + open[0].length;
|
|
45
|
+
const end = source.indexOf(']', start);
|
|
46
|
+
if (end === -1) return undefined;
|
|
47
|
+
const items = source
|
|
48
|
+
.slice(start, end)
|
|
49
|
+
.split(',')
|
|
50
|
+
.map((item) => item.trim())
|
|
51
|
+
.filter((item) => item.length > 0);
|
|
52
|
+
return { start, end, items };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `source` with each entry imported and listed. The import joins the `import * as` block in sorted
|
|
57
|
+
* position; the binding joins its list, which is created after `actions: [...]` when the call has
|
|
58
|
+
* none. An index that is not the scaffold's shape — no `defineApi({`, no `actions:` list — is
|
|
59
|
+
* returned untouched with the entries in `skipped`, and the `manifest` step's X_JOB_UNREGISTERED
|
|
60
|
+
* names the edit.
|
|
61
|
+
*/
|
|
62
|
+
export function insertApiEntries(
|
|
63
|
+
source: string,
|
|
64
|
+
entries: readonly ApiEntry[],
|
|
65
|
+
): { readonly source: string; readonly skipped: readonly ApiEntry[] } {
|
|
66
|
+
let next = source;
|
|
67
|
+
const skipped: ApiEntry[] = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const importLine = `import * as ${entry.binding} from '${entry.specifier}';`;
|
|
70
|
+
const call = next.indexOf('defineApi({');
|
|
71
|
+
const actions = listOf(next, 'actions');
|
|
72
|
+
if (call === -1 || actions === undefined) {
|
|
73
|
+
skipped.push(entry);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const list = listOf(next, entry.key);
|
|
77
|
+
if (list?.items.includes(entry.binding) === true) continue;
|
|
78
|
+
if (list === undefined) {
|
|
79
|
+
// After `actions: [...],` — the order `x new` writes: actions, queries, jobs, tasks.
|
|
80
|
+
const after = next.indexOf('\n', actions.end);
|
|
81
|
+
const line = ` ${entry.key}: [${entry.binding}],`;
|
|
82
|
+
next = `${next.slice(0, after + 1)}${line}\n${next.slice(after + 1)}`;
|
|
83
|
+
} else {
|
|
84
|
+
const items = [...list.items, entry.binding];
|
|
85
|
+
const lineStart = next.lastIndexOf('\n', list.start) + 1;
|
|
86
|
+
const indent = /^\s*/.exec(next.slice(lineStart))?.[0] ?? '';
|
|
87
|
+
const rewritten = wrapList(indent, `${entry.key}: [`, items, ']');
|
|
88
|
+
next = `${next.slice(0, lineStart)}${rewritten}${next.slice(list.end + 1)}`;
|
|
89
|
+
}
|
|
90
|
+
if (!next.includes(importLine)) next = withImport(next, importLine);
|
|
91
|
+
}
|
|
92
|
+
return { source: next, skipped };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The import, in the `import * as` block Biome keeps sorted by specifier. Placed before the first
|
|
97
|
+
* relative import whose specifier sorts after it, or after the last one.
|
|
98
|
+
*/
|
|
99
|
+
function withImport(source: string, importLine: string): string {
|
|
100
|
+
const specifier = /from '([^']+)'/.exec(importLine)?.[1] ?? '';
|
|
101
|
+
const lines = source.split('\n');
|
|
102
|
+
const relative = lines
|
|
103
|
+
.map((line, index) => ({ line, index, from: /^import .* from '(\.[^']*)';$/.exec(line)?.[1] }))
|
|
104
|
+
.filter((row) => row.from !== undefined);
|
|
105
|
+
const before = relative.find((row) => (row.from ?? '') > specifier);
|
|
106
|
+
const at = before?.index ?? (relative.at(-1)?.index ?? -1) + 1;
|
|
107
|
+
lines.splice(at, 0, importLine);
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Performs `insertApiEntries` on the app's index. Answers the paths it rewrote. */
|
|
112
|
+
export async function registerGeneratedPrimitives(
|
|
113
|
+
root: string,
|
|
114
|
+
written: readonly string[],
|
|
115
|
+
): Promise<readonly string[]> {
|
|
116
|
+
const entries = apiEntriesFor(written);
|
|
117
|
+
const file = containedPath(root, API_INDEX);
|
|
118
|
+
if (entries.length === 0 || !(await Bun.file(file).exists())) return [];
|
|
119
|
+
const before = await Bun.file(file).text();
|
|
120
|
+
const { source } = insertApiEntries(before, entries);
|
|
121
|
+
if (source === before) return [];
|
|
122
|
+
await Bun.write(file, source);
|
|
123
|
+
return [API_INDEX];
|
|
124
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The two generated contracts an app commits — `x.manifest.json` and `openapi.json` — written by one
|
|
2
|
+
// function, because two commands wrote them and only one wrote both: `x g` refreshed the manifest
|
|
3
|
+
// and left `openapi.json` behind, so its own output failed the `contract-diff` step next.
|
|
4
|
+
|
|
5
|
+
import { join } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
6
|
+
import type { Manifest } from '@ultimat3/manifest';
|
|
7
|
+
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
8
|
+
import { writeAppManifest } from './app-manifest';
|
|
9
|
+
import { OPENAPI_FILE, openApiJson } from './app-openapi';
|
|
10
|
+
|
|
11
|
+
export interface ArtifactOptions {
|
|
12
|
+
/** `x manifest --no-openapi` writes the manifest alone. */
|
|
13
|
+
readonly openapi: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Refresh only what is already committed. `x g` never introduces a generated file an app did not
|
|
16
|
+
* ask to maintain; `x manifest` is the asking.
|
|
17
|
+
*/
|
|
18
|
+
readonly onlyExisting: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Writes the app's generated contracts from one projection. Answers the app-relative paths. */
|
|
22
|
+
export async function writeAppArtifacts(
|
|
23
|
+
root: string,
|
|
24
|
+
manifest: Manifest,
|
|
25
|
+
options: ArtifactOptions,
|
|
26
|
+
): Promise<readonly string[]> {
|
|
27
|
+
const written: string[] = [];
|
|
28
|
+
const present = async (file: string): Promise<boolean> =>
|
|
29
|
+
!options.onlyExisting || (await Bun.file(join(root, file)).exists());
|
|
30
|
+
if (await present(MANIFEST_FILENAME)) {
|
|
31
|
+
await writeAppManifest(root, manifest);
|
|
32
|
+
written.push(MANIFEST_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
if (options.openapi && (await present(OPENAPI_FILE))) {
|
|
35
|
+
await Bun.write(join(root, OPENAPI_FILE), openApiJson(manifest));
|
|
36
|
+
written.push(OPENAPI_FILE);
|
|
37
|
+
}
|
|
38
|
+
return written;
|
|
39
|
+
}
|
package/src/app-boundaries.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { dirname, join, normalize, relative } from 'node:path/posix';
|
|
|
17
17
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
18
18
|
import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
|
|
19
19
|
import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
|
|
20
|
+
import { scanRuntimeImports, stripShebang } from './import-scan';
|
|
20
21
|
import type { Finding } from './output';
|
|
21
22
|
import { hasPathSegment } from './path-segments';
|
|
22
23
|
import { quoteArg } from './shell-quote';
|
|
@@ -27,6 +28,7 @@ export const BOUNDARY_CODES = [
|
|
|
27
28
|
'X_BOUNDARY_APP_TO_API',
|
|
28
29
|
'X_BOUNDARY_ROUTE_TO_DB',
|
|
29
30
|
'X_BOUNDARY_SERVICE_TO_HTTP',
|
|
31
|
+
'X_BOUNDARY_SURFACE_IMPORT',
|
|
30
32
|
] as const;
|
|
31
33
|
|
|
32
34
|
export type BoundaryCode = (typeof BOUNDARY_CODES)[number];
|
|
@@ -41,6 +43,8 @@ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
|
|
|
41
43
|
'site-imports-app': 'X_BOUNDARY_SITE_TO_APP',
|
|
42
44
|
'shared-is-a-leaf': 'X_BOUNDARY_SHARED_LEAF',
|
|
43
45
|
'app-imports-api-at-runtime': 'X_BOUNDARY_APP_TO_API',
|
|
46
|
+
// Every other crossing `SURFACE_SPECS` does not allow: the table is the rule (`@ultimat3/render`).
|
|
47
|
+
'surface-imports-surface': 'X_BOUNDARY_SURFACE_IMPORT',
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -68,16 +72,7 @@ const isDbSpecifier = (specifier: string): boolean =>
|
|
|
68
72
|
const isHttpSpecifier = (specifier: string): boolean =>
|
|
69
73
|
specifier === '@ultimat3/http' || /(^|[/:])https?($|\/)/.test(specifier);
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
export const stripShebang = (source: string): string =>
|
|
73
|
-
source.startsWith('#!') ? source.slice(source.indexOf('\n') + 1) : source;
|
|
74
|
-
|
|
75
|
-
/** Bun's transpiler is the parser; a regex fallback would miss re-exports and dynamic imports. */
|
|
76
|
-
export function scanRuntimeImports(file: SourceFile): readonly string[] {
|
|
77
|
-
const loader = file.path.endsWith('x') ? 'tsx' : 'ts';
|
|
78
|
-
const transpiler = new Bun.Transpiler({ loader });
|
|
79
|
-
return transpiler.scanImports(stripShebang(file.source)).map((entry) => entry.path);
|
|
80
|
-
}
|
|
75
|
+
export { scanRuntimeImports, stripShebang };
|
|
81
76
|
|
|
82
77
|
const CANDIDATE_SUFFIXES = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'] as const;
|
|
83
78
|
|
package/src/app-env.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// `app.config.ts`, the `.env.example` projected from it, and the drift between the two. One
|
|
3
3
|
// declaration, both files (axiom 2) — nothing here holds a second list of variable names.
|
|
4
4
|
|
|
5
|
-
// Bun ships no
|
|
6
|
-
// host-separator path to the two files this module reads.
|
|
5
|
+
// why: Bun ships no synchronous exists; `existsSync` answers whether this root is an app.
|
|
7
6
|
import { existsSync } from 'node:fs';
|
|
7
|
+
// why: Bun exposes no path API — the two files this module reads are joined to the app root.
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
10
10
|
import {
|
package/src/app-load.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
// Bun ships no `Bun.*` path API: `relative`/`sep` turn an absolute scan hit into the app-root-
|
|
7
7
|
// relative POSIX path every finding and every manifest fact is keyed by.
|
|
8
|
-
import { relative, sep } from 'node:path';
|
|
9
|
-
import { registerActions } from '@ultimat3/action';
|
|
8
|
+
import { join, relative, sep } from 'node:path';
|
|
9
|
+
import { listActions, registerActions } from '@ultimat3/action';
|
|
10
|
+
import { describeEntities } from '@ultimat3/entity';
|
|
10
11
|
import { localeConfig } from '@ultimat3/i18n';
|
|
12
|
+
import { registeredJobs, registeredTasks } from '@ultimat3/jobs';
|
|
11
13
|
import type { ErrorCodeFact } from '@ultimat3/manifest';
|
|
12
|
-
import { registerQueries } from '@ultimat3/query';
|
|
14
|
+
import { listQueries, registerQueries } from '@ultimat3/query';
|
|
13
15
|
import type { RouteConfig } from '@ultimat3/render';
|
|
14
|
-
import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render';
|
|
16
|
+
import { isRouteConfig, pageComponentOf, registerRoute, routeEntries } from '@ultimat3/render';
|
|
15
17
|
// For the SIDE EFFECT, and it is this module's to hold: importing `@ultimat3/render/server`
|
|
16
18
|
// installs the `.tsx`/`.scss` Bun plugin, a plugin only transforms modules loaded AFTER it, and
|
|
17
19
|
// every app module below is loaded by the dynamic `import()` in this file. Before the render
|
|
@@ -19,10 +21,12 @@ import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render'
|
|
|
19
21
|
// here is six hops through `error-contract` → `fix-command` → the command registry, which is an
|
|
20
22
|
// accident one refactor away from compiling every app's `.tsx` to `React.createElement`.
|
|
21
23
|
import '@ultimat3/render/server';
|
|
24
|
+
import { APP_CONFIG_FILE } from './app-root';
|
|
22
25
|
import { collectDeclaredCodes } from './error-contract';
|
|
23
26
|
import type { Finding } from './output';
|
|
24
27
|
import { findingFrom } from './output';
|
|
25
28
|
import { hasPathSegment } from './path-segments';
|
|
29
|
+
import { isTest } from './source-files';
|
|
26
30
|
|
|
27
31
|
/** Every place an app keeps code the framework has to see. */
|
|
28
32
|
const APP_GLOBS = [
|
|
@@ -112,8 +116,10 @@ export async function loadApp(root: string): Promise<LoadedApp> {
|
|
|
112
116
|
// A SEGMENT, never a substring: an app checked out under
|
|
113
117
|
// `~/dev/node_modules-experiments/myapp` answered `includes('node_modules')` for every
|
|
114
118
|
// file it holds, so this loop imported none of them and the app registered nothing.
|
|
115
|
-
|
|
119
|
+
// The ROOT-RELATIVE path is tested, never the absolute one: an app checked out under
|
|
120
|
+
// `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
|
|
116
121
|
const file = relative(root, absolute).split(sep).join('/');
|
|
122
|
+
if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
|
|
117
123
|
if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
@@ -138,6 +144,16 @@ export async function loadApp(root: string): Promise<LoadedApp> {
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
files.sort();
|
|
147
|
+
// An app that imported NOTHING and reported nothing is a registry every later step reads as
|
|
148
|
+
// empty-and-fine. With an `app.config.ts` beside it, that is never what the author meant.
|
|
149
|
+
if (
|
|
150
|
+
files.length === 0 &&
|
|
151
|
+
findings.length === 0 &&
|
|
152
|
+
registersNothing() &&
|
|
153
|
+
(await Bun.file(join(root, APP_CONFIG_FILE)).exists())
|
|
154
|
+
) {
|
|
155
|
+
findings.push(emptyAppFinding(root));
|
|
156
|
+
}
|
|
141
157
|
// Read after the loop, never before it: `configureLocales` runs on the app's own import.
|
|
142
158
|
return {
|
|
143
159
|
root,
|
|
@@ -148,6 +164,22 @@ export async function loadApp(root: string): Promise<LoadedApp> {
|
|
|
148
164
|
};
|
|
149
165
|
}
|
|
150
166
|
|
|
167
|
+
/** No primitive in any registry this scan fills — a process that registered some itself is not empty. */
|
|
168
|
+
const registersNothing = (): boolean =>
|
|
169
|
+
listActions().length === 0 &&
|
|
170
|
+
listQueries().length === 0 &&
|
|
171
|
+
routeEntries().length === 0 &&
|
|
172
|
+
registeredJobs().length === 0 &&
|
|
173
|
+
registeredTasks().length === 0 &&
|
|
174
|
+
describeEntities().length === 0;
|
|
175
|
+
|
|
176
|
+
const emptyAppFinding = (root: string): Finding => ({
|
|
177
|
+
code: 'X_APP_EMPTY',
|
|
178
|
+
cause: `${root} has an ${APP_CONFIG_FILE} and loadApp imported no module under ${APP_GLOBS.join(', ')}, so every step reading the registries would check nothing`,
|
|
179
|
+
fix: 'x doctor --json',
|
|
180
|
+
at: APP_CONFIG_FILE,
|
|
181
|
+
});
|
|
182
|
+
|
|
151
183
|
/**
|
|
152
184
|
* Registers a module once; every later call replays whatever the first one reported — except for
|
|
153
185
|
* a route module, which a later call re-registers from disk when its source has changed.
|
package/src/app-openapi.ts
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
// OpenAPI builder to drift from the ones the packages serve. Until 2026-09 only the actions were
|
|
6
6
|
// here, and every `GET /_x/query/<name>` the server mounted was a route the spec had never heard of.
|
|
7
7
|
|
|
8
|
+
// why: Bun ships no path-join primitive.
|
|
9
|
+
import { join } from 'node:path';
|
|
8
10
|
import { buildOpenApi, serializeOpenApi } from '@ultimat3/action';
|
|
11
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
9
12
|
import type { Manifest } from '@ultimat3/manifest';
|
|
10
13
|
import { queryOpenApiPaths } from '@ultimat3/query';
|
|
14
|
+
import type { Finding } from './output';
|
|
11
15
|
|
|
12
16
|
export const OPENAPI_FILE = 'openapi.json';
|
|
13
17
|
|
|
@@ -18,3 +22,26 @@ export const openApiJson = (manifest: Manifest): string => {
|
|
|
18
22
|
// can never shadow one with the other. `serializeOpenApi` sorts the merged keys.
|
|
19
23
|
return serializeOpenApi({ ...document, paths: { ...document.paths, ...queryOpenApiPaths() } });
|
|
20
24
|
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. One
|
|
28
|
+
* check, read by `x verify`'s `manifest` step AND `x manifest --check` — which looked at
|
|
29
|
+
* `x.manifest.json` alone and answered "fresh" over a spec the gate refused.
|
|
30
|
+
*/
|
|
31
|
+
export async function openApiStaleness(
|
|
32
|
+
root: string,
|
|
33
|
+
manifest: Manifest,
|
|
34
|
+
): Promise<readonly Finding[]> {
|
|
35
|
+
const path = join(root, OPENAPI_FILE);
|
|
36
|
+
if (!(await Bun.file(path).exists())) return [];
|
|
37
|
+
if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
code: 'X_MANIFEST_STALE',
|
|
41
|
+
cause: `${OPENAPI_FILE} does not match the actions the code registers`,
|
|
42
|
+
fix: 'x manifest',
|
|
43
|
+
docs: ERROR_DOCS_URL,
|
|
44
|
+
at: OPENAPI_FILE,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
package/src/app-permissions.ts
CHANGED
|
Binary file
|
package/src/app-root.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const MANIFEST_FILE = 'x.manifest.json';
|
|
|
24
24
|
* (`bun run typecheck` is clean against `@types/bun@1.3.14`), and `>=1.4.0` therefore bars Bun 1.3
|
|
25
25
|
* users for a capability the framework does not use. What refused it is a Bun 1.3.14 defect, not
|
|
26
26
|
* the paperwork: a service shutdown against a destroyed database never resolves there
|
|
27
|
-
* (`queue.stop()`, reproduced by `
|
|
27
|
+
* (`queue.stop()`, reproduced by `runtime-services.live.test.ts`), so an app on a runtime this line
|
|
28
28
|
* declared supported would hang on graceful shutdown the moment its database went away. The full
|
|
29
29
|
* measurement is in `.github/actions/setup/action.yml`; read it before lowering this.
|
|
30
30
|
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// A route whose `Page` is an `async` function and which declares no `load`. It renders — the
|
|
2
|
+
// renderer awaits whatever the component returns — so no suite fails on it, and its data read
|
|
3
|
+
// silently skips everything `load` is for: the route's cache, `meta` computed from the data, the
|
|
4
|
+
// streaming boundary and the measurement render `x build` weighs. The social demo shipped eleven
|
|
5
|
+
// (plan 101 slice 11 l); nothing asked.
|
|
6
|
+
|
|
7
|
+
import type { RouteEntry } from '@ultimat3/render';
|
|
8
|
+
import { routeEntries } from '@ultimat3/render';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
|
|
11
|
+
/** `async function Page` and `async () =>` both construct one; a sync one never does. */
|
|
12
|
+
const isAsync = (value: unknown): boolean =>
|
|
13
|
+
typeof value === 'function' && value.constructor.name === 'AsyncFunction';
|
|
14
|
+
|
|
15
|
+
export function asyncPageFinding(file: string): Finding {
|
|
16
|
+
return {
|
|
17
|
+
code: 'X_ROUTE_ASYNC_PAGE',
|
|
18
|
+
cause: `${file} exports an async Page, so its data read bypasses load's caching and meta`,
|
|
19
|
+
fix: `in ${file}: move each await in Page into export const load = (ctx) => …, and read it with the page's data prop`,
|
|
20
|
+
at: file,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every registered route whose page component is async and whose config declares no `load`. */
|
|
25
|
+
export function asyncPageFindings(
|
|
26
|
+
entries: readonly Pick<RouteEntry, 'file' | 'config' | 'component'>[] = routeEntries(),
|
|
27
|
+
): readonly Finding[] {
|
|
28
|
+
return entries
|
|
29
|
+
.filter((entry) => entry.config.load === undefined && isAsync(entry.component))
|
|
30
|
+
.map((entry) => asyncPageFinding(entry.file))
|
|
31
|
+
.sort((a, b) => (a.at ?? '').localeCompare(b.at ?? ''));
|
|
32
|
+
}
|
package/src/bin.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { dispatch } from './dispatch';
|
|
6
6
|
import { resolveLocalCli } from './local-cli';
|
|
7
|
+
import { cwdFromArgv } from './root-env';
|
|
7
8
|
// The writes themselves are `write-line.ts`: `create-ultimate`'s entry point needs the identical
|
|
8
9
|
// one, and a second copy of a note about pipe truncation is a second copy that drifts. Two sinks,
|
|
9
10
|
// because fd 1 is not always this process's to write on — `x mcp serve --transport stdio` hands it
|
|
@@ -15,7 +16,12 @@ import { writeErrorLine, writeLine } from './write-line';
|
|
|
15
16
|
// `--json` consumer reading fd 1 sees exactly the child's one document. `process.execPath` is the
|
|
16
17
|
// Bun that is already running, never a `PATH` lookup: the child's `import.meta.path` resolves to
|
|
17
18
|
// the app's file, so its own `resolveLocalCli` answers "same file" and the chain stops at one hop.
|
|
18
|
-
|
|
19
|
+
// `--cwd` decides WHICH app's CLI, exactly as it decides which app `dispatch` runs against.
|
|
20
|
+
const local = resolveLocalCli({
|
|
21
|
+
cwd: cwdFromArgv(Bun.argv.slice(2), process.cwd()),
|
|
22
|
+
selfPath: import.meta.path,
|
|
23
|
+
env: Bun.env,
|
|
24
|
+
});
|
|
19
25
|
if (local !== undefined) {
|
|
20
26
|
writeErrorLine(`x: using the app's own @ultimat3/cli at ${local}`);
|
|
21
27
|
const child = Bun.spawn([process.execPath, local, ...Bun.argv.slice(2)], {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The surface findings `x verify` reports, each carrying the CONCRETE edit as its `fix:`. It said
|
|
2
|
+
// `x fix boundary <file>`, which printed a second fix and wrote nothing — a fix whose whole content
|
|
3
|
+
// was another command to run for the real one (plan 101 slice 11 j). The planner is the same one
|
|
4
|
+
// `x fix boundary` prints from, so the gate and the command cannot disagree about the cut.
|
|
5
|
+
|
|
6
|
+
import { appImportGraph, checkImportRules, readAppSources } from './app-boundaries';
|
|
7
|
+
import { planBoundaryCuts } from './boundary-cuts';
|
|
8
|
+
import type { Finding } from './output';
|
|
9
|
+
|
|
10
|
+
/** A cut and a finding name one violation when they share its code, its importer and its chain. */
|
|
11
|
+
const keyOf = (code: string, at: string | undefined, cause: string): string =>
|
|
12
|
+
[code, at ?? '', cause].join('\u0000');
|
|
13
|
+
|
|
14
|
+
/** `findings` with every surface violation's `fix` replaced by the cut that clears it. */
|
|
15
|
+
export function withCutEdits(
|
|
16
|
+
findings: readonly Finding[],
|
|
17
|
+
graph: Parameters<typeof planBoundaryCuts>[1],
|
|
18
|
+
): readonly Finding[] {
|
|
19
|
+
const edits = new Map<string, string>();
|
|
20
|
+
for (const finding of findings) {
|
|
21
|
+
if (finding.at === undefined) continue;
|
|
22
|
+
for (const cut of planBoundaryCuts(finding.at, graph)) {
|
|
23
|
+
edits.set(keyOf(cut.code, cut.at, cut.cause), cut.edit);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return findings.map((finding) => {
|
|
27
|
+
const edit = edits.get(keyOf(finding.code, finding.at, finding.cause));
|
|
28
|
+
return edit === undefined ? finding : { ...finding, fix: edit };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read the app's sources once, check them, and hand back findings whose fix is the edit. */
|
|
33
|
+
export async function appBoundaryFindings(root: string): Promise<readonly Finding[]> {
|
|
34
|
+
const files = await readAppSources(root);
|
|
35
|
+
return withCutEdits(checkImportRules(files), appImportGraph(files));
|
|
36
|
+
}
|