@ultimat3/cli 24.0.0 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +15 -8
- package/README.md +30 -5
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +3 -1
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +3 -3
- package/src/app-env.ts +21 -14
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +2 -2
- package/src/app-mcp.ts +25 -27
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +2 -8
- package/src/app-permissions-site.ts +31 -7
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +115 -121
- package/src/cdp-shot-driver.ts +17 -0
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth.ts +2 -2
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +89 -9
- package/src/cmd-db-backfill.ts +1 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +5 -2
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -357
- package/src/cmd-doctor.ts +16 -66
- package/src/cmd-env.ts +29 -14
- package/src/cmd-generate.ts +12 -2
- package/src/cmd-i18n.ts +11 -8
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +18 -137
- package/src/cmd-mcp.ts +25 -7
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +9 -0
- package/src/cmd-routes.ts +3 -3
- package/src/cmd-shot-matrix.ts +21 -4
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +35 -39
- package/src/cmd-test.ts +1 -1
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +19 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +1 -1
- package/src/dev-dashboard.ts +5 -1
- package/src/dev-lock.ts +96 -24
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +14 -7
- package/src/doctor-auth.ts +4 -4
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +2 -2
- package/src/drift.ts +29 -6
- package/src/error-codes.ts +6 -0
- package/src/error-page-csp.ts +14 -15
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-kinds.ts +3 -17
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +50 -1
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +21 -8
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +54 -64
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +32 -16
- package/src/local-cli-handoff.ts +90 -0
- package/src/mcp-errors.ts +8 -1
- package/src/mcp-host.ts +6 -5
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +27 -16
- package/src/metrics-endpoint.ts +3 -2
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +18 -0
- package/src/page-speculation.ts +14 -11
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +63 -98
- package/src/pwa-artifacts.ts +8 -1
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/retry-memo.ts +1 -1
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +110 -31
- package/src/role-start.ts +64 -85
- package/src/role-sync.ts +65 -6
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +15 -2
- package/src/runtime-bindings.ts +23 -3
- package/src/runtime-cache.ts +8 -8
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +2 -2
- package/src/runtime-jobs.ts +78 -18
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +2 -2
- package/src/runtime-overrides.ts +3 -3
- package/src/runtime-purge.ts +1 -1
- package/src/runtime-queue.ts +12 -12
- package/src/runtime-realtime.ts +5 -3
- package/src/runtime-render.ts +77 -82
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +33 -13
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +3 -3
- package/src/schema-diff.ts +1 -1
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +2 -2
- package/src/serve-boot.ts +22 -11
- package/src/serve-drain.ts +3 -1
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +19 -8
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -15
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-seo.ts +6 -4
- package/src/source-files.ts +13 -3
- package/src/static-report.ts +5 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +1 -1
- package/src/templates/action.ts +4 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +23 -8
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +2 -1
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container.ts +12 -6
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-templates.ts +74 -18
- package/src/templates/scaffold-helm.ts +31 -4
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +18 -15
- package/src/templates/scaffold-shell.ts +17 -2
- package/src/test-only-shipped.ts +193 -0
- package/src/test-select.ts +2 -1
- package/src/test-shards.ts +1 -4
- package/src/theme-boot.ts +2 -2
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/verify-checks.ts +12 -2
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +87 -10
- package/src/verify-merge.ts +48 -16
- package/src/verify-part-step.ts +10 -2
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +3 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +0 -1
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/jobs-drain.ts +0 -230
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
import type { GeneratedFile, NameSet } from './naming';
|
|
9
9
|
import { helmTemplateFiles } from './scaffold-helm-templates';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The uid AND gid the app's process runs as, in the image and in every pod: the `bun` user of the
|
|
13
|
+
* `oven/bun:1.4-alpine` base the scaffolded Dockerfile pins (`id bun` → `uid=1000 gid=1000`). One
|
|
14
|
+
* number, read by both templates, because two of them drifted — the chart ran distroless'
|
|
15
|
+
* `nonroot` (65532), a uid that image does not have. Pinned to the measurement by
|
|
16
|
+
* `scaffold-container.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
export const RUNTIME_UID = 1000;
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* `appVersion` is the default image tag (`image.tag: ""` below), so it must name a tag that can
|
|
13
22
|
* exist. `x deploy --image <ref>` overrides both keys, which is the path that actually ships.
|
|
@@ -41,7 +50,7 @@ image:
|
|
|
41
50
|
env:
|
|
42
51
|
NODE_ENV: production
|
|
43
52
|
|
|
44
|
-
existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET,
|
|
53
|
+
existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET, and .env.example's Framework section
|
|
45
54
|
|
|
46
55
|
# The release-wide Secret above is every role's DEFAULT: \`existingSecret\` under a role
|
|
47
56
|
# (roles.web.existingSecret, migrate.existingSecret) names that role's own, so the migrate Job's
|
|
@@ -71,9 +80,12 @@ networkPolicy:
|
|
|
71
80
|
# METRICS_PORT in the container; move one and the other follows.
|
|
72
81
|
metricsPort: 9090
|
|
73
82
|
|
|
83
|
+
# The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
|
|
74
84
|
podSecurityContext:
|
|
75
85
|
runAsNonRoot: true
|
|
76
|
-
runAsUser:
|
|
86
|
+
runAsUser: ${RUNTIME_UID}
|
|
87
|
+
runAsGroup: ${RUNTIME_UID}
|
|
88
|
+
fsGroup: ${RUNTIME_UID}
|
|
77
89
|
seccompProfile: { type: RuntimeDefault }
|
|
78
90
|
|
|
79
91
|
securityContext:
|
|
@@ -82,10 +94,20 @@ securityContext:
|
|
|
82
94
|
capabilities: { drop: [ALL] }
|
|
83
95
|
|
|
84
96
|
# The stop sequence: out of the Service, then this preStop sleep (Kubernetes 1.30+ only), then
|
|
85
|
-
# SIGTERM, then the framework's readiness grace and drain.
|
|
86
|
-
#
|
|
97
|
+
# SIGTERM, then the framework's readiness grace and drain. Each Deployment's
|
|
98
|
+
# terminationGracePeriodSeconds is DERIVED from this block per role — never set it by hand.
|
|
99
|
+
# readinessGraceSeconds app.config.ts drain.readinessGraceMs / 1000 (web and sync only)
|
|
100
|
+
# deadlineSeconds app.config.ts drain.deadlineMs / 1000 — x deploy --method helm sets
|
|
101
|
+
# both from app.config.ts, so a raised budget raises the grace with it
|
|
102
|
+
# teardownMarginSeconds the release after the drain, the telemetry flush, the pool close
|
|
87
103
|
drain:
|
|
88
104
|
preStopSleepSeconds: 5
|
|
105
|
+
readinessGraceSeconds: 5
|
|
106
|
+
deadlineSeconds: 25
|
|
107
|
+
teardownMarginSeconds: 10
|
|
108
|
+
|
|
109
|
+
# How long a new pod must stay Ready before a rollout counts it and stops an old one.
|
|
110
|
+
minReadySeconds: 10
|
|
89
111
|
|
|
90
112
|
# The release phase. Runs to completion before any serving role starts.
|
|
91
113
|
migrate:
|
|
@@ -142,6 +164,11 @@ roles:
|
|
|
142
164
|
worker:
|
|
143
165
|
enabled: true
|
|
144
166
|
replicas: 2
|
|
167
|
+
# The retire, OFF at 0. Set to the longest a held job may run, in seconds, and the worker's
|
|
168
|
+
# preStop sends its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0
|
|
169
|
+
# — for a job whose side effect must happen at most once and may outlast drain.deadlineSeconds.
|
|
170
|
+
# Added to this role's terminationGracePeriodSeconds, which the kubelet counts the preStop against.
|
|
171
|
+
retireSeconds: 0
|
|
145
172
|
resources:
|
|
146
173
|
requests: { cpu: 200m, memory: 256Mi }
|
|
147
174
|
limits: { memory: 1Gi }
|
|
@@ -13,7 +13,7 @@ const httpConfig = (
|
|
|
13
13
|
app: NameSet,
|
|
14
14
|
): string => `// What this app declares about HTTP. Module scope IS the wiring: the boot scan imports every
|
|
15
15
|
// module under \`apps/*\` before a listener binds, and \`x dev\` and the container both read the
|
|
16
|
-
// configured value back at start — the same seam \`app/auth/
|
|
16
|
+
// configured value back at start — the same seam \`app/auth/authenticator.ts\` installs through.
|
|
17
17
|
//
|
|
18
18
|
// The BOOT lays its own facts over whatever this says: \`port\`, \`hostname\`, \`dev\`, \`buildId\`,
|
|
19
19
|
// \`signInPath\`, \`trustProxy\`, \`trustedProxyHops\` and \`rateLimit.scope\` are all facts about the
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// blessed typed-catalog shape (modelled on examples/dummy/packages/i18n/src/index.ts) — split out
|
|
3
3
|
// of scaffold-repo.ts to stay under the file-size ceiling.
|
|
4
4
|
|
|
5
|
+
import { APP_CATALOGS_PATH } from '@ultimat3/i18n/app-catalogs';
|
|
5
6
|
import { catalogJson } from './catalog-json';
|
|
6
7
|
import type { GeneratedFile, NameSet } from './naming';
|
|
7
8
|
import { camel } from './naming';
|
|
@@ -121,7 +122,7 @@ const i18nCatalog = (app: NameSet): string =>
|
|
|
121
122
|
'site.home.secondary': 'Open the admin',
|
|
122
123
|
'site.home.f1Title': 'One command to a running app',
|
|
123
124
|
'site.home.f1Body':
|
|
124
|
-
'
|
|
125
|
+
'bun run setup installs, migrates and seeds; bun run dev runs every role in one process.',
|
|
125
126
|
'site.home.f2Title': 'Policies, not flags',
|
|
126
127
|
'site.home.f2Body':
|
|
127
128
|
'Every route, action and query declares who may call it, and one system decides.',
|
|
@@ -149,6 +150,18 @@ const i18nCatalog = (app: NameSet): string =>
|
|
|
149
150
|
'app.dashboard.columnTitle': 'Title',
|
|
150
151
|
'app.dashboard.columnCreated': 'Created',
|
|
151
152
|
'app.dashboard.emptyTitle': 'No posts yet',
|
|
153
|
+
'app.dashboard.statPaid': 'Paid posts',
|
|
154
|
+
'app.dashboard.hintPaid': 'priced above zero',
|
|
155
|
+
'app.dashboard.chartKey': 'Day',
|
|
156
|
+
'app.dashboard.chartSeries': 'Posts',
|
|
157
|
+
'app.dashboard.mixTitle': 'Free and paid',
|
|
158
|
+
'app.dashboard.mixLabel': 'Posts by price, free against paid',
|
|
159
|
+
'app.dashboard.mixFree': 'Free',
|
|
160
|
+
'app.dashboard.mixPaid': 'Paid',
|
|
161
|
+
'app.dashboard.mixTotal': 'posts',
|
|
162
|
+
'app.dashboard.mixKey': 'Price',
|
|
163
|
+
'app.dashboard.gaugeLabel': 'Created this week, of all posts',
|
|
164
|
+
'app.dashboard.columnPrice': 'Price',
|
|
152
165
|
'app.dashboard.statRoutes': 'Routes',
|
|
153
166
|
'app.dashboard.hintRoutes': 'registered at boot',
|
|
154
167
|
'app.dashboard.statLocales': 'Locales',
|
|
@@ -203,7 +216,7 @@ export function i18nFiles(app: NameSet, version: string): readonly GeneratedFile
|
|
|
203
216
|
'**/*.ts',
|
|
204
217
|
'catalogs/**/*',
|
|
205
218
|
]),
|
|
206
|
-
{ path:
|
|
219
|
+
{ path: APP_CATALOGS_PATH, contents: i18nIndex(['en']) },
|
|
207
220
|
{ path: 'packages/i18n/src/index.test.ts', contents: i18nTest() },
|
|
208
221
|
{ path: 'packages/i18n/catalogs/en.json', contents: i18nCatalog(app), merge: 'json' },
|
|
209
222
|
];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// never turn it into the fourteen `ICON_MATRIX` PNGs the generated web manifest declares.
|
|
5
5
|
|
|
6
6
|
import type { Raster } from '@ultimat3/core';
|
|
7
|
-
import {
|
|
7
|
+
import { blankRaster, encodeImage } from '@ultimat3/core';
|
|
8
8
|
|
|
9
9
|
/** `@ultimat3/pwa`'s own contract (`requireSourceIcon`'s fix line): square, 1024 or larger. */
|
|
10
10
|
const ICON_SIZE = 1024;
|
|
@@ -48,7 +48,7 @@ function paintMark(raster: Raster, inset: number): void {
|
|
|
48
48
|
* so `x new` output never depends on run order or the clock.
|
|
49
49
|
*/
|
|
50
50
|
export function icon(): Uint8Array {
|
|
51
|
-
const raster =
|
|
51
|
+
const raster = blankRaster(ICON_SIZE, ICON_SIZE, 'scaffold-icon');
|
|
52
52
|
paintMark(raster, Math.round(ICON_SIZE * MASKABLE_PADDING));
|
|
53
53
|
return encodeImage(raster, 'png');
|
|
54
54
|
}
|
|
@@ -12,11 +12,13 @@ import { VERIFY_FLOOR_FILE } from '../verify-floor';
|
|
|
12
12
|
import type { VerifyStepName } from '../verify-step';
|
|
13
13
|
import type { GeneratedFile, NameSet } from './naming';
|
|
14
14
|
import { titleCase } from './naming';
|
|
15
|
+
import { BIN_SCRIPTS, binScript } from './scaffold-bin';
|
|
15
16
|
import { dbPackageFiles } from './scaffold-db-package';
|
|
16
17
|
import { docsFiles } from './scaffold-docs';
|
|
17
18
|
import { domainPackageFiles } from './scaffold-domain-package';
|
|
18
19
|
import { envExampleSource, envSchemaSource } from './scaffold-env';
|
|
19
20
|
import { PWA_COLORS } from './scaffold-errors';
|
|
21
|
+
import { rootGitattributesFile } from './scaffold-gitattributes';
|
|
20
22
|
import { scaffoldGuardFiles } from './scaffold-guards';
|
|
21
23
|
import { i18nFiles } from './scaffold-i18n';
|
|
22
24
|
import { mcpPackageFiles } from './scaffold-mcp-package';
|
|
@@ -28,7 +30,7 @@ import { uiPackageFiles } from './scaffold-ui-package';
|
|
|
28
30
|
* config as out of date on every `bun run lint`. A formatter is a build input — a range that floats
|
|
29
31
|
* is a `lint` step whose verdict depends on the day the app was installed.
|
|
30
32
|
*/
|
|
31
|
-
const BIOME_VERSION = '2.5.
|
|
33
|
+
const BIOME_VERSION = '2.5.15';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* The checker a scaffolded app typechecks with, and it must not lag the one the FRAMEWORK is built
|
|
@@ -52,7 +54,7 @@ const SOLID_VERSION = '1.9.15';
|
|
|
52
54
|
// `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
|
|
53
55
|
// (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
|
|
54
56
|
// BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
|
|
55
|
-
// next line of `
|
|
57
|
+
// next line of `bun run setup` died `X_BUN_VERSION: Bun 1.3.14 is older than the required 1.4.0` — a
|
|
56
58
|
// floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
|
|
57
59
|
// disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
|
|
58
60
|
// `github/ci.yml.ts` pins the workflow's Bun to the same constant.
|
|
@@ -70,9 +72,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
70
72
|
"packages/*"
|
|
71
73
|
],
|
|
72
74
|
"scripts": {
|
|
73
|
-
"setup": "
|
|
75
|
+
"setup": "${binScript(BIN_SCRIPTS.setup)}",
|
|
74
76
|
"dev": "x dev",
|
|
75
|
-
"check": "
|
|
77
|
+
"check": "${binScript(BIN_SCRIPTS.check)}",
|
|
76
78
|
"verify": "x verify",
|
|
77
79
|
"typecheck": "tsc -p . --pretty",
|
|
78
80
|
"lint": "biome check .",
|
|
@@ -90,6 +92,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
90
92
|
"dependencies": {
|
|
91
93
|
"@ultimat3/action": "^${version}",
|
|
92
94
|
"@ultimat3/admin": "^${version}",
|
|
95
|
+
"@ultimat3/auth": "^${version}",
|
|
93
96
|
"@ultimat3/cache": "^${version}",
|
|
94
97
|
"@ultimat3/cli": "^${version}",
|
|
95
98
|
"@ultimat3/core": "^${version}",
|
|
@@ -184,9 +187,8 @@ export const env = defineEnv(envSchema);`;
|
|
|
184
187
|
* author has no use for a comment about keys their config does not name.
|
|
185
188
|
*
|
|
186
189
|
* `tier` is the one that got through, and it says what the list was worth. It accepted
|
|
187
|
-
* `'channels' | 'live-queries' | 'local-first'`,
|
|
188
|
-
*
|
|
189
|
-
* a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
|
|
190
|
+
* `'channels' | 'live-queries' | 'local-first'`, and nothing read the value — silent, and shaped
|
|
191
|
+
* like a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
|
|
190
192
|
* topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
|
|
191
193
|
* dead names to remember; it resolves every key path this literal writes against what `defineConfig`
|
|
192
194
|
* really returns, so the fourteenth deletion fails there with no edit here.
|
|
@@ -203,10 +205,8 @@ ${envDeclaration()}
|
|
|
203
205
|
|
|
204
206
|
export const config = defineConfig({
|
|
205
207
|
name: '${app.kebab}',
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
defaultTimeZone: 'UTC',
|
|
209
|
-
defaultCurrency: 'USD',
|
|
208
|
+
// Locales are declared by the catalogs (\`defineCatalogs\` in \`packages/i18n/src/index.ts\`); no
|
|
209
|
+
// zone or currency default exists — every format call names its zone, every \`Money\` its currency.
|
|
210
210
|
// Env KEYS, never the value: the same image deploys to every environment. The database is
|
|
211
211
|
// configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
|
|
212
212
|
// The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
|
|
@@ -223,7 +223,7 @@ export const config = defineConfig({
|
|
|
223
223
|
enabled: true,
|
|
224
224
|
// The document an offline navigation gets when the cache has no answer, and the path
|
|
225
225
|
// \`apps/web/site/offline/page.tsx\` serves. Required once \`enabled\` is true: an installable
|
|
226
|
-
// app that shows the browser
|
|
226
|
+
// app that shows the browser error page offline is the failure the block exists to prevent.
|
|
227
227
|
offline: { fallback: '/offline' },
|
|
228
228
|
name: '${titleCase(app.raw)}',
|
|
229
229
|
colors: {
|
|
@@ -235,7 +235,7 @@ export const config = defineConfig({
|
|
|
235
235
|
// it, and a choice stored by the theme toggle wins over it on every later visit; 'system' follows
|
|
236
236
|
// the OS.
|
|
237
237
|
theme: { defaultMode: 'dark' },
|
|
238
|
-
ai: { mcp: { expose: true
|
|
238
|
+
ai: { mcp: { expose: true } },
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
/**
|
|
@@ -295,7 +295,8 @@ const biome = (): string => `{
|
|
|
295
295
|
"rules": {
|
|
296
296
|
"preset": "recommended",
|
|
297
297
|
"suspicious": { "noExplicitAny": "error" },
|
|
298
|
-
"correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" }
|
|
298
|
+
"correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" },
|
|
299
|
+
"style": { "useNodejsImportProtocol": "error" }
|
|
299
300
|
}
|
|
300
301
|
},
|
|
301
302
|
"javascript": {
|
|
@@ -364,7 +365,7 @@ export const SCAFFOLD_COVERAGE_EXCLUDE: readonly CoverageExclude[] = [
|
|
|
364
365
|
},
|
|
365
366
|
{
|
|
366
367
|
glob: 'apps/*/server.ts',
|
|
367
|
-
why: 'the container entry point: it boots the roles and lives as long as the process —
|
|
368
|
+
why: 'the container entry point: it boots the roles and lives as long as the process — bun run dev and the image exercise it',
|
|
368
369
|
},
|
|
369
370
|
{
|
|
370
371
|
glob: 'apps/*/prerender.ts',
|
|
@@ -470,6 +471,8 @@ export function repoFiles(
|
|
|
470
471
|
{ path: VERIFY_FLOOR_FILE, contents: verifyFloor(example) },
|
|
471
472
|
{ path: 'types/scss.d.ts', contents: scssTypes() },
|
|
472
473
|
{ path: '.gitignore', contents: gitignore() },
|
|
474
|
+
// Every text file LF whatever `core.autocrlf` says — a Windows checkout is otherwise CRLF.
|
|
475
|
+
rootGitattributesFile(),
|
|
473
476
|
{ path: '.env.development', contents: envDevelopment() },
|
|
474
477
|
// Committed, and generated: `x env example` rewrites this file from `envSchema`, and the
|
|
475
478
|
// gate's `manifest` step fails with X_ENV_EXAMPLE_DRIFT when the two stop agreeing. A
|
|
@@ -90,7 +90,7 @@ export function Shell(props: ShellProps): JSX.Element {
|
|
|
90
90
|
<span class={styles.env}>
|
|
91
91
|
{/* The pulse is a CSS opacity animation: alive without a byte of script. */}
|
|
92
92
|
<span class={styles.envDot} aria-hidden="true" />
|
|
93
|
-
{environment}
|
|
93
|
+
<span class={styles.envLabel}>{environment}</span>
|
|
94
94
|
</span>
|
|
95
95
|
)}
|
|
96
96
|
{props.actions}
|
|
@@ -125,9 +125,13 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
|
|
|
125
125
|
justify-content: space-between;
|
|
126
126
|
gap: tokens.space(4);
|
|
127
127
|
inline-size: 100%;
|
|
128
|
+
// Free to shrink below its content: on a 390px phone the brand gives way, never the page.
|
|
129
|
+
min-inline-size: 0;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
// The controls keep their size; the brand is what truncates.
|
|
130
133
|
.end {
|
|
134
|
+
flex: none;
|
|
131
135
|
display: inline-flex;
|
|
132
136
|
align-items: center;
|
|
133
137
|
gap: tokens.space(3);
|
|
@@ -136,6 +140,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
|
|
|
136
140
|
.brand {
|
|
137
141
|
display: inline-flex;
|
|
138
142
|
align-items: center;
|
|
143
|
+
min-inline-size: 0;
|
|
139
144
|
gap: tokens.space(3);
|
|
140
145
|
color: tokens.role('fg-strong');
|
|
141
146
|
|
|
@@ -146,6 +151,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
|
|
|
146
151
|
|
|
147
152
|
// One accent tile. The only saturated surface in the chrome, so the eye has exactly one anchor.
|
|
148
153
|
.mark {
|
|
154
|
+
flex: none;
|
|
149
155
|
display: inline-grid;
|
|
150
156
|
place-items: center;
|
|
151
157
|
inline-size: tokens.space(8);
|
|
@@ -159,6 +165,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
|
|
|
159
165
|
|
|
160
166
|
.wordmark {
|
|
161
167
|
@include tokens.data-text;
|
|
168
|
+
@include tokens.truncate;
|
|
162
169
|
|
|
163
170
|
font-size: tokens.text('sm');
|
|
164
171
|
font-weight: tokens.weight('medium');
|
|
@@ -178,6 +185,13 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
|
|
|
178
185
|
font-size: tokens.text('xs');
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
// Below sm the pill is its dot: the word stays readable to assistive tech, not to the layout.
|
|
189
|
+
.envLabel {
|
|
190
|
+
@include tokens.respond-down(sm) {
|
|
191
|
+
@include tokens.visually-hidden;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
181
195
|
.envDot {
|
|
182
196
|
inline-size: tokens.space(2);
|
|
183
197
|
block-size: tokens.space(2);
|
|
@@ -340,8 +354,9 @@ const toggleTest =
|
|
|
340
354
|
|
|
341
355
|
import { join } from 'node:path';
|
|
342
356
|
import { buildIslands } from '@ultimat3/cli';
|
|
357
|
+
import { THEME_STORAGE_KEY } from '@ultimat3/core';
|
|
343
358
|
import { describeIslandState, expect, test } from '@ultimat3/testing';
|
|
344
|
-
import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE
|
|
359
|
+
import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE } from '@ultimat3/ui';
|
|
345
360
|
import { themeToggleStates } from './theme-toggle.island.states';
|
|
346
361
|
|
|
347
362
|
/** What the island reads through \`browserThemeEnv\`: storage and the OS query, both fakes. */
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// `package-shape`'s third rule of "test code does not ship": a `src/` module only tests (and
|
|
2
|
+
// fixtures) import is test code under a name `files` cannot see. The tarball excludes `*.test.ts`
|
|
3
|
+
// and `*-fixture.ts`, so a fake, a probe or a `bun:test` helper named anything else rode into
|
|
4
|
+
// every install — 20 of them across eleven packages until 2026-10. The defect is the name.
|
|
5
|
+
|
|
6
|
+
import { ERROR_DOCS_URL, renderFixShellArg } from '@ultimat3/core';
|
|
7
|
+
import type { ClosureHost } from './import-closure';
|
|
8
|
+
import { resolveSpec } from './import-closure';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
import { FIXTURE_SUFFIX } from './publish-closure';
|
|
11
|
+
import { eachSourceFile, isGenerated, isTest } from './source-files';
|
|
12
|
+
|
|
13
|
+
/** A published package: its directory under `packages/` and the files its manifest names. */
|
|
14
|
+
export interface ShippedPackage {
|
|
15
|
+
readonly dir: string;
|
|
16
|
+
/** Package-relative (`src/index.ts`), and may be a glob (`src/icons/glyphs/*.ts`). */
|
|
17
|
+
readonly entries: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TestOnlyModule {
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** Every file that imports or names it — all of them tests, fixtures or test-only modules. */
|
|
23
|
+
readonly importers: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every `src/` file a manifest names anywhere — `exports`, `bin`, the `browser` map, a `scripts`
|
|
28
|
+
* command (`bun run src/icons/build-icons.ts`). Read off the whole document rather than field by
|
|
29
|
+
* field: a module the manifest names is shipped on purpose, whichever field names it.
|
|
30
|
+
*/
|
|
31
|
+
export function entryPatternsOf(manifest: unknown): readonly string[] {
|
|
32
|
+
const text = JSON.stringify(manifest ?? null);
|
|
33
|
+
const found = [...text.matchAll(/(?:\.\/)?(src\/[\w./*-]+?\.tsx?)(?![\w.])/g)];
|
|
34
|
+
return [...new Set(found.map((match) => match[1] ?? ''))].sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Type-only imports count: the package ships `.ts` source, so a consumer's `tsc` opens the file a
|
|
38
|
+
// shipped `import type` names. One pattern for `from '.'`, `import('.')` and `import '.'`, relative
|
|
39
|
+
// specifiers only: over-matching an importer only ever clears a module, never flags one.
|
|
40
|
+
const RELATIVE = /(?:\bfrom|\bimport)\s*\(?\s*(['"])(\.\.?\/[^'"]*)\1/g;
|
|
41
|
+
/**
|
|
42
|
+
* A template-literal `import(\`./drivers/${n}\`)`: the static head up to the first `${` names a
|
|
43
|
+
* DIRECTORY prefix, and any module under it may be the one loaded. With no `${` the closing
|
|
44
|
+
* backtick ends it and it is an ordinary specifier.
|
|
45
|
+
*/
|
|
46
|
+
const TEMPLATE = /\bimport\s*\(\s*`(\.\.?\/[^`$]*)(\$\{|`)/g;
|
|
47
|
+
/** A file named in a string — `join(import.meta.dir, 'probe.ts')`, `new URL('./worker.ts', …)`. */
|
|
48
|
+
const NAMED = /['"`/]([\w.-]+\.tsx?)['"`]/g;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether the match at `index` sits on a line that IS a comment — `//`, a doc comment's `*`, a
|
|
52
|
+
* `/*` opener. Line-wise and not core's `stripComments`, which is exact and costs ~1.1 s over the
|
|
53
|
+
* repo's 6,000 files (measured 2026-10-05); asked only of a match, never of every line. The lines
|
|
54
|
+
* this skips are where a commented-out or quoted-in-prose import lives, and one it keeps can only
|
|
55
|
+
* clear a module, never flag one.
|
|
56
|
+
*/
|
|
57
|
+
function onCommentLine(text: string, index: number): boolean {
|
|
58
|
+
let at = text.lastIndexOf('\n', index - 1) + 1;
|
|
59
|
+
while (text[at] === ' ' || text[at] === '\t') at += 1;
|
|
60
|
+
const head = text.slice(at, at + 2);
|
|
61
|
+
return head === '//' || head === '/*' || head.startsWith('*');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `from`'s directory joined with a relative `head`, `.` and `..` folded; a trailing `/` kept. */
|
|
65
|
+
function prefixFrom(from: string, head: string): string {
|
|
66
|
+
const parts = from.split('/').slice(0, -1);
|
|
67
|
+
const segments = head.split('/');
|
|
68
|
+
const last = segments.pop() ?? '';
|
|
69
|
+
for (const segment of segments) {
|
|
70
|
+
if (segment === '..') parts.pop();
|
|
71
|
+
else if (segment !== '.' && segment !== '') parts.push(segment);
|
|
72
|
+
}
|
|
73
|
+
return `${parts.join('/')}/${last}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isFixture = (path: string): boolean => path.endsWith(FIXTURE_SUFFIX);
|
|
77
|
+
const baseName = (path: string): string => path.slice(path.lastIndexOf('/') + 1);
|
|
78
|
+
|
|
79
|
+
const add = (map: Map<string, Set<string>>, key: string, value: string): void => {
|
|
80
|
+
const set = map.get(key);
|
|
81
|
+
if (set === undefined) map.set(key, new Set([value]));
|
|
82
|
+
else set.add(value);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Who imports each file, and which files name each basename in a string. One pass per file. */
|
|
86
|
+
function referrersOf(sources: ReadonlyMap<string, string>): {
|
|
87
|
+
readonly imports: ReadonlyMap<string, ReadonlySet<string>>;
|
|
88
|
+
readonly named: ReadonlyMap<string, ReadonlySet<string>>;
|
|
89
|
+
} {
|
|
90
|
+
const host: ClosureHost = { read: (path) => sources.get(path), alias: () => undefined };
|
|
91
|
+
const imports = new Map<string, Set<string>>();
|
|
92
|
+
const named = new Map<string, Set<string>>();
|
|
93
|
+
const prefixes: { readonly prefix: string; readonly from: string }[] = [];
|
|
94
|
+
for (const [from, text] of sources) {
|
|
95
|
+
for (const match of text.matchAll(RELATIVE)) {
|
|
96
|
+
if (onCommentLine(text, match.index)) continue;
|
|
97
|
+
const target = resolveSpec(host, from, match[2] ?? '');
|
|
98
|
+
if (target !== undefined && target !== from) add(imports, target, from);
|
|
99
|
+
}
|
|
100
|
+
for (const match of text.matchAll(TEMPLATE)) {
|
|
101
|
+
if (onCommentLine(text, match.index)) continue;
|
|
102
|
+
const head = match[1] ?? '';
|
|
103
|
+
if (match[2] === '`') {
|
|
104
|
+
const target = resolveSpec(host, from, head);
|
|
105
|
+
if (target !== undefined && target !== from) add(imports, target, from);
|
|
106
|
+
} else prefixes.push({ prefix: prefixFrom(from, head), from });
|
|
107
|
+
}
|
|
108
|
+
for (const match of text.matchAll(NAMED)) {
|
|
109
|
+
if (!onCommentLine(text, match.index)) add(named, match[1] ?? '', from);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Few computed imports in any repo, so a scan of every path per prefix costs nothing measurable.
|
|
113
|
+
for (const { prefix, from } of prefixes) {
|
|
114
|
+
for (const path of sources.keys()) {
|
|
115
|
+
if (path !== from && path.startsWith(prefix)) add(imports, path, from);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { imports, named };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The shipped `.ts` modules of the given packages whose every importer is a test, a fixture or
|
|
123
|
+
* another such module — a greatest fixed point, so a helper only a fake imports is caught with the
|
|
124
|
+
* fake. A module nobody references is not this rule's question, and an entry point is shipped by
|
|
125
|
+
* definition. `.tsx` is not judged: `FIXTURE_EXCLUSION` matches `-fixture.ts` alone.
|
|
126
|
+
*/
|
|
127
|
+
export function testOnlyModules(
|
|
128
|
+
sources: ReadonlyMap<string, string>,
|
|
129
|
+
packages: readonly ShippedPackage[],
|
|
130
|
+
): readonly TestOnlyModule[] {
|
|
131
|
+
const { imports, named } = referrersOf(sources);
|
|
132
|
+
const entryGlobs = new Map(
|
|
133
|
+
packages.map((pkg) => [
|
|
134
|
+
pkg.dir,
|
|
135
|
+
pkg.entries.map((entry) => new Bun.Glob(`packages/${pkg.dir}/${entry}`)),
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
const referrers = new Map<string, ReadonlySet<string>>();
|
|
139
|
+
for (const path of sources.keys()) {
|
|
140
|
+
const dir = /^packages\/([^/]+)\/src\//.exec(path)?.[1];
|
|
141
|
+
const globs = dir === undefined ? undefined : entryGlobs.get(dir);
|
|
142
|
+
if (globs === undefined || !path.endsWith('.ts') || isGenerated(path)) continue;
|
|
143
|
+
if (isTest(path) || isFixture(path) || globs.some((glob) => glob.match(path))) continue;
|
|
144
|
+
const from = new Set([...(imports.get(path) ?? []), ...(named.get(baseName(path)) ?? [])]);
|
|
145
|
+
from.delete(path);
|
|
146
|
+
if (from.size > 0) referrers.set(path, from);
|
|
147
|
+
}
|
|
148
|
+
const testOnly = new Set(referrers.keys());
|
|
149
|
+
const live = (path: string): boolean => !isTest(path) && !isFixture(path) && !testOnly.has(path);
|
|
150
|
+
for (let changed = true; changed; ) {
|
|
151
|
+
changed = false;
|
|
152
|
+
for (const path of testOnly) {
|
|
153
|
+
if (![...(referrers.get(path) ?? [])].some(live)) continue;
|
|
154
|
+
testOnly.delete(path);
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [...testOnly].sort().map((path) => ({
|
|
159
|
+
path,
|
|
160
|
+
importers: [...(referrers.get(path) ?? [])].sort(),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** `X_PACKAGE_TEST_ONLY_SHIPPED`, its fix the registered rename with the real paths in it. */
|
|
165
|
+
export const testOnlyShippedFinding = (module: TestOnlyModule): Finding => {
|
|
166
|
+
const shown = module.importers.slice(0, 3).join(', ');
|
|
167
|
+
const more = module.importers.length > 3 ? ` and ${module.importers.length - 3} more` : '';
|
|
168
|
+
return {
|
|
169
|
+
code: 'X_PACKAGE_TEST_ONLY_SHIPPED',
|
|
170
|
+
cause: `${module.path} is imported only by tests and fixtures (${shown}${more}), and "files" publishes it — test scaffolding in every install`,
|
|
171
|
+
fix: `git mv ${renderFixShellArg(module.path, '<the module above>')} ${renderFixShellArg(module.path.replace(/\.ts$/, FIXTURE_SUFFIX), '<its name>-fixture.ts')} # and update its importers`,
|
|
172
|
+
docs: ERROR_DOCS_URL,
|
|
173
|
+
at: module.path,
|
|
174
|
+
meta: { importers: [...module.importers] },
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The rule over a repo on disk. Every source file is a possible importer — a script reading a
|
|
180
|
+
* package's module by relative path is a shipped use of it — so the walk is the gate's own
|
|
181
|
+
* (`eachSourceFile`), tests included. A repo with no published package reads nothing.
|
|
182
|
+
*/
|
|
183
|
+
export async function checkTestOnlyShipped(
|
|
184
|
+
root: string,
|
|
185
|
+
packages: readonly ShippedPackage[],
|
|
186
|
+
): Promise<readonly Finding[]> {
|
|
187
|
+
if (packages.length === 0) return [];
|
|
188
|
+
const paths: string[] = [];
|
|
189
|
+
for await (const path of eachSourceFile(root)) if (!isGenerated(path)) paths.push(path);
|
|
190
|
+
const texts = await Promise.all(paths.map((path) => Bun.file(`${root}/${path}`).text()));
|
|
191
|
+
const sources = new Map(paths.map((path, index) => [path, texts[index] ?? '']));
|
|
192
|
+
return testOnlyModules(sources, packages).map(testOnlyShippedFinding);
|
|
193
|
+
}
|
package/src/test-select.ts
CHANGED
|
@@ -6,12 +6,13 @@ import { join } from 'node:path';
|
|
|
6
6
|
// Bun ships no equivalent: `join` builds the host-separator path from the scan root to a hit.
|
|
7
7
|
// Sizing is Bun's own (`Bun.file().size`), so nothing here reaches for `node:fs`.
|
|
8
8
|
import { nearestName, renderFixShellArg } from '@ultimat3/core';
|
|
9
|
+
import { TEST_TYPES } from '@ultimat3/testing';
|
|
9
10
|
import { BadFlagError } from './errors';
|
|
10
11
|
import type { ParsedArgs } from './parse';
|
|
11
12
|
import { flagString } from './parse';
|
|
12
13
|
import { isIgnoredTestPath } from './test-ignores';
|
|
13
14
|
import type { TestType } from './verify-tests';
|
|
14
|
-
import { ownerOf
|
|
15
|
+
import { ownerOf } from './verify-tests';
|
|
15
16
|
|
|
16
17
|
export interface TestFile {
|
|
17
18
|
readonly path: string;
|
package/src/test-shards.ts
CHANGED
|
@@ -52,9 +52,6 @@ import type { CommandResult, Finding, JsonValue, StepResult } from './output';
|
|
|
52
52
|
import { quoteArg } from './shell-quote';
|
|
53
53
|
import { testEnvOverrides } from './test-dotenv';
|
|
54
54
|
|
|
55
|
-
/** `@ultimat3/testing`'s `ISOLATED_ENV` — the one spelling, which that package's preload reads. */
|
|
56
|
-
export const ISOLATED_TEST_ENV = ISOLATED_ENV;
|
|
57
|
-
|
|
58
55
|
/** Bun's per-file durations, cached under the run's root and refreshed by every parallel run. */
|
|
59
56
|
export const TEST_TIMINGS_FILE = '.x/test-timings.json';
|
|
60
57
|
|
|
@@ -258,7 +255,7 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
|
|
|
258
255
|
...testEnvOverrides(options.root, options.env ?? Bun.env),
|
|
259
256
|
// Only an isolated run: the preload clears `Bun.plugin` handlers after each file, which in a
|
|
260
257
|
// shared global would leave the next `.tsx` compiling with Bun's classic factory.
|
|
261
|
-
...(isolatedRun(options) ? { [
|
|
258
|
+
...(isolatedRun(options) ? { [ISOLATED_ENV]: '1' } : {}),
|
|
262
259
|
};
|
|
263
260
|
const passthrough = options.passthrough ?? [];
|
|
264
261
|
const watching = passthrough.includes('--watch');
|
package/src/theme-boot.ts
CHANGED
|
@@ -22,8 +22,8 @@ export interface ThemeBoot {
|
|
|
22
22
|
/**
|
|
23
23
|
* Tag and hash from ONE body: `themeScriptBody` is what `themeScript` inlines, so a policy hashed
|
|
24
24
|
* here admits the script the document carries and not a restatement of it. The storage key is
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* `@ultimat3/core`'s `THEME_STORAGE_KEY`, the one `@ultimat3/ui`'s toggle writes — so the toggle
|
|
26
|
+
* writes the key the boot reads.
|
|
27
27
|
*/
|
|
28
28
|
export function themeBoot(mode: ThemeMode): ThemeBoot {
|
|
29
29
|
const options = { fallback: mode };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The app's brand, declared once as `export const brand = defineTheme(…)` in
|
|
2
|
+
// `apps/web/shared/theme.ts`, read here and nowhere else: the `<style>` every document the boot
|
|
3
|
+
// renders carries after its stylesheet, and the body `style-src` admits. No module, no brand: an
|
|
4
|
+
// unthemed app emits no tag and extends no policy.
|
|
5
|
+
|
|
6
|
+
// why: Bun exposes no path-join primitive, and the module path is app-root-relative.
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { ConfigInvalidError, describeValue } from '@ultimat3/core';
|
|
9
|
+
import { renderHead } from '@ultimat3/render';
|
|
10
|
+
|
|
11
|
+
/** The conventional module, beside `shared/global.ts` — one fixed path, as `API_INDEX` is. */
|
|
12
|
+
export const APP_THEME_MODULE = 'apps/web/shared/theme.ts';
|
|
13
|
+
/** The named export the module declares — `@ultimat3/ui`'s README spells it `brand`. */
|
|
14
|
+
export const APP_THEME_EXPORT = 'brand';
|
|
15
|
+
|
|
16
|
+
export interface ThemeBrand {
|
|
17
|
+
/** The rendered `<style>`, for `DocumentOptions.brandHead`. */
|
|
18
|
+
readonly head: string;
|
|
19
|
+
/** Exactly the text between the tags — the body a boot hands `inlineStyleSources`. */
|
|
20
|
+
readonly style: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Code to paste, as `loadAppConfig`'s is: the module path is already in the cause. */
|
|
24
|
+
const FIX = `export const ${APP_THEME_EXPORT} = defineTheme({ preset: 'scifi' });`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `undefined` when the app has no theme module or its brand renders no CSS. A module that will not
|
|
28
|
+
* import throws as it did, so a palette `defineTheme()` refuses fails the boot with
|
|
29
|
+
* `X_UI_CONTRAST_INSUFFICIENT` (or its token code) and not a rewording of it.
|
|
30
|
+
*/
|
|
31
|
+
export async function loadThemeBrand(root: string): Promise<ThemeBrand | undefined> {
|
|
32
|
+
const path = join(root, APP_THEME_MODULE);
|
|
33
|
+
if (!(await Bun.file(path).exists())) return undefined;
|
|
34
|
+
const module = (await import(path)) as Record<string, unknown>;
|
|
35
|
+
const css = brandCss(path, module);
|
|
36
|
+
if (css.length === 0) return undefined;
|
|
37
|
+
// `renderHead` escapes `</` in a style body, which would make the tag's text differ from the
|
|
38
|
+
// hashed one — so a body that needs escaping is refused above rather than escaped here.
|
|
39
|
+
return { head: renderHead([{ kind: 'style', key: 'style:brand', content: css }]), style: css };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The brand's stylesheet, read structurally: `@ultimat3/ui` is a tier below and this package does
|
|
44
|
+
* not import it, so a `Brand` is `{ css: string }`. `defineTheme()` never emits `<` — its values
|
|
45
|
+
* are validated against grammars that exclude it — so one here came from a hand-built object, and
|
|
46
|
+
* it is the one character that could end the element and start markup.
|
|
47
|
+
*/
|
|
48
|
+
function brandCss(path: string, module: Record<string, unknown>): string {
|
|
49
|
+
const brand = Object.hasOwn(module, APP_THEME_EXPORT) ? module[APP_THEME_EXPORT] : undefined;
|
|
50
|
+
const css = typeof brand === 'object' && brand !== null && 'css' in brand ? brand.css : undefined;
|
|
51
|
+
if (typeof css !== 'string') {
|
|
52
|
+
throw new ConfigInvalidError({
|
|
53
|
+
cause: `${path} exports no ${APP_THEME_EXPORT} built by defineTheme() (${describeValue(brand)}), so no document can carry the app's theme — declare it with defineTheme from @ultimat3/ui`,
|
|
54
|
+
fix: FIX,
|
|
55
|
+
meta: { key: APP_THEME_EXPORT },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (css.includes('<')) {
|
|
59
|
+
throw new ConfigInvalidError({
|
|
60
|
+
cause: `${path}'s ${APP_THEME_EXPORT}.css contains "<", which defineTheme() never emits and which could close the <style> element it is inlined in`,
|
|
61
|
+
fix: FIX,
|
|
62
|
+
meta: { key: APP_THEME_EXPORT },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return css;
|
|
66
|
+
}
|