@ultimat3/cli 23.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- 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 +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- 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 +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- 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 +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- 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-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- 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 +2 -6
- package/src/island-bundle.ts +57 -79
- 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 +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- 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 +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- 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-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- 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 +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- 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 -33
- 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-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -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 +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -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/naming.ts +28 -0
- 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 +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- 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-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- 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-client.ts +4 -2
- 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-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- 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 +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- 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/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
|
@@ -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.
|
|
@@ -31,19 +40,52 @@ image:
|
|
|
31
40
|
|
|
32
41
|
# Non-secret configuration. Secrets come from an existing Secret, never from this file:
|
|
33
42
|
# kubectl create secret generic ${app.kebab}-secrets --from-literal=DATABASE_URL=...
|
|
43
|
+
#
|
|
44
|
+
# APP_URL — the pages' public origin — is what the sync node admits a websocket from. With the
|
|
45
|
+
# ingress below enabled the chart SETS it on the sync role from ingress.host (https when
|
|
46
|
+
# ingress.tls), because an undeclared node behind a TLS ingress also admits the plain-http page.
|
|
47
|
+
# Set it here when the pages are served from another host or port than that (a CDN domain, a
|
|
48
|
+
# SYNC_URL of its own): this value wins, and without it every websocket from those pages is
|
|
49
|
+
# 403 X_SOCKET_ORIGIN_REFUSED.
|
|
34
50
|
env:
|
|
35
51
|
NODE_ENV: production
|
|
36
52
|
|
|
37
|
-
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
|
|
54
|
+
|
|
55
|
+
# The release-wide Secret above is every role's DEFAULT: \`existingSecret\` under a role
|
|
56
|
+
# (roles.web.existingSecret, migrate.existingSecret) names that role's own, so the migrate Job's
|
|
57
|
+
# schema-owning DATABASE_URL need not sit on a web pod.
|
|
58
|
+
|
|
59
|
+
# The /tmp every pod mounts (the root filesystem is read-only), bounded: past this the kubelet
|
|
60
|
+
# evicts the pod that filled it, never its neighbours for node disk pressure.
|
|
61
|
+
tmp:
|
|
62
|
+
sizeLimit: 512Mi
|
|
63
|
+
|
|
64
|
+
# One NetworkPolicy per serving role (templates/networkpolicy.yaml): \`http\` from httpFrom,
|
|
65
|
+
# \`metrics\` from metricsFrom, nothing else. Inert on a CNI that does not enforce NetworkPolicy.
|
|
66
|
+
# httpFrom empty = any source; narrow it to your ingress controller's namespace, e.g.
|
|
67
|
+
# [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
|
|
68
|
+
# metricsFrom default: any pod in the cluster and no address outside it
|
|
69
|
+
# egress OFF while empty; a list turns on egress filtering with exactly those rules —
|
|
70
|
+
# the database, NATS, the object store and DNS are your addresses, so list all of them
|
|
71
|
+
networkPolicy:
|
|
72
|
+
enabled: true
|
|
73
|
+
httpFrom: []
|
|
74
|
+
metricsFrom:
|
|
75
|
+
- namespaceSelector: {}
|
|
76
|
+
egress: []
|
|
38
77
|
|
|
39
78
|
# The scrape listener every serving role opens, on its own port and never the app's: the ingress
|
|
40
79
|
# routes / to web, so /metrics beside /healthz on 3000 is /metrics on the internet. This is
|
|
41
80
|
# METRICS_PORT in the container; move one and the other follows.
|
|
42
81
|
metricsPort: 9090
|
|
43
82
|
|
|
83
|
+
# The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
|
|
44
84
|
podSecurityContext:
|
|
45
85
|
runAsNonRoot: true
|
|
46
|
-
runAsUser:
|
|
86
|
+
runAsUser: ${RUNTIME_UID}
|
|
87
|
+
runAsGroup: ${RUNTIME_UID}
|
|
88
|
+
fsGroup: ${RUNTIME_UID}
|
|
47
89
|
seccompProfile: { type: RuntimeDefault }
|
|
48
90
|
|
|
49
91
|
securityContext:
|
|
@@ -52,10 +94,20 @@ securityContext:
|
|
|
52
94
|
capabilities: { drop: [ALL] }
|
|
53
95
|
|
|
54
96
|
# The stop sequence: out of the Service, then this preStop sleep (Kubernetes 1.30+ only), then
|
|
55
|
-
# SIGTERM, then the framework's readiness grace and drain.
|
|
56
|
-
#
|
|
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
|
|
57
103
|
drain:
|
|
58
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
|
|
59
111
|
|
|
60
112
|
# The release phase. Runs to completion before any serving role starts.
|
|
61
113
|
migrate:
|
|
@@ -112,6 +164,11 @@ roles:
|
|
|
112
164
|
worker:
|
|
113
165
|
enabled: true
|
|
114
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
|
|
115
172
|
resources:
|
|
116
173
|
requests: { cpu: 200m, memory: 256Mi }
|
|
117
174
|
limits: { memory: 1Gi }
|
|
@@ -153,7 +210,7 @@ ingress:
|
|
|
153
210
|
`;
|
|
154
211
|
|
|
155
212
|
/**
|
|
156
|
-
* The chart for a new app:
|
|
213
|
+
* The chart for a new app: six kinds of object, all of them core Kubernetes. A ServiceMonitor and
|
|
157
214
|
* a PodDisruptionBudget are deliberately absent — the first needs a CRD `helm install` fails on
|
|
158
215
|
* where no Prometheus operator is installed, and both are cluster policy rather than app topology.
|
|
159
216
|
*/
|
|
@@ -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
|
|
@@ -41,10 +43,18 @@ const BIOME_VERSION = '2.5.8';
|
|
|
41
43
|
*/
|
|
42
44
|
const TYPESCRIPT_VERSION = '^7.0.2';
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The Solid runtime every island and `@ultimat3/ui` component renders with, exact for the reason
|
|
48
|
+
* `BIOME_VERSION` is: the CLI compiles islands with `babel-preset-solid ^1.9.15`, and a scaffold
|
|
49
|
+
* that installed an older runtime than the compiler targets drifted as a buried literal (1.9.14
|
|
50
|
+
* beside the repository's 1.9.15). `scaffold-repo.test.ts` holds it equal to the root manifest's.
|
|
51
|
+
*/
|
|
52
|
+
const SOLID_VERSION = '1.9.15';
|
|
53
|
+
|
|
44
54
|
// `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
|
|
45
55
|
// (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
|
|
46
56
|
// BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
|
|
47
|
-
// 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
|
|
48
58
|
// floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
|
|
49
59
|
// disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
|
|
50
60
|
// `github/ci.yml.ts` pins the workflow's Bun to the same constant.
|
|
@@ -62,9 +72,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
62
72
|
"packages/*"
|
|
63
73
|
],
|
|
64
74
|
"scripts": {
|
|
65
|
-
"setup": "
|
|
75
|
+
"setup": "${binScript(BIN_SCRIPTS.setup)}",
|
|
66
76
|
"dev": "x dev",
|
|
67
|
-
"check": "
|
|
77
|
+
"check": "${binScript(BIN_SCRIPTS.check)}",
|
|
68
78
|
"verify": "x verify",
|
|
69
79
|
"typecheck": "tsc -p . --pretty",
|
|
70
80
|
"lint": "biome check .",
|
|
@@ -82,6 +92,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
82
92
|
"dependencies": {
|
|
83
93
|
"@ultimat3/action": "^${version}",
|
|
84
94
|
"@ultimat3/admin": "^${version}",
|
|
95
|
+
"@ultimat3/auth": "^${version}",
|
|
85
96
|
"@ultimat3/cache": "^${version}",
|
|
86
97
|
"@ultimat3/cli": "^${version}",
|
|
87
98
|
"@ultimat3/core": "^${version}",
|
|
@@ -99,7 +110,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
99
110
|
"@ultimat3/schema": "^${version}",
|
|
100
111
|
"@ultimat3/seo": "^${version}",
|
|
101
112
|
"@ultimat3/ui": "^${version}",
|
|
102
|
-
"solid-js": "
|
|
113
|
+
"solid-js": "${SOLID_VERSION}"
|
|
103
114
|
},
|
|
104
115
|
"engines": {
|
|
105
116
|
"bun": ">=${REQUIRED_BUN}"
|
|
@@ -176,9 +187,8 @@ export const env = defineEnv(envSchema);`;
|
|
|
176
187
|
* author has no use for a comment about keys their config does not name.
|
|
177
188
|
*
|
|
178
189
|
* `tier` is the one that got through, and it says what the list was worth. It accepted
|
|
179
|
-
* `'channels' | 'live-queries' | 'local-first'`,
|
|
180
|
-
*
|
|
181
|
-
* 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()`
|
|
182
192
|
* topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
|
|
183
193
|
* dead names to remember; it resolves every key path this literal writes against what `defineConfig`
|
|
184
194
|
* really returns, so the fourteenth deletion fails there with no edit here.
|
|
@@ -195,10 +205,8 @@ ${envDeclaration()}
|
|
|
195
205
|
|
|
196
206
|
export const config = defineConfig({
|
|
197
207
|
name: '${app.kebab}',
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
defaultTimeZone: 'UTC',
|
|
201
|
-
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.
|
|
202
210
|
// Env KEYS, never the value: the same image deploys to every environment. The database is
|
|
203
211
|
// configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
|
|
204
212
|
// The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
|
|
@@ -215,7 +223,7 @@ export const config = defineConfig({
|
|
|
215
223
|
enabled: true,
|
|
216
224
|
// The document an offline navigation gets when the cache has no answer, and the path
|
|
217
225
|
// \`apps/web/site/offline/page.tsx\` serves. Required once \`enabled\` is true: an installable
|
|
218
|
-
// app that shows the browser
|
|
226
|
+
// app that shows the browser error page offline is the failure the block exists to prevent.
|
|
219
227
|
offline: { fallback: '/offline' },
|
|
220
228
|
name: '${titleCase(app.raw)}',
|
|
221
229
|
colors: {
|
|
@@ -227,7 +235,7 @@ export const config = defineConfig({
|
|
|
227
235
|
// it, and a choice stored by the theme toggle wins over it on every later visit; 'system' follows
|
|
228
236
|
// the OS.
|
|
229
237
|
theme: { defaultMode: 'dark' },
|
|
230
|
-
ai: { mcp: { expose: true
|
|
238
|
+
ai: { mcp: { expose: true } },
|
|
231
239
|
});
|
|
232
240
|
|
|
233
241
|
/**
|
|
@@ -287,7 +295,8 @@ const biome = (): string => `{
|
|
|
287
295
|
"rules": {
|
|
288
296
|
"preset": "recommended",
|
|
289
297
|
"suspicious": { "noExplicitAny": "error" },
|
|
290
|
-
"correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" }
|
|
298
|
+
"correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" },
|
|
299
|
+
"style": { "useNodejsImportProtocol": "error" }
|
|
291
300
|
}
|
|
292
301
|
},
|
|
293
302
|
"javascript": {
|
|
@@ -356,7 +365,7 @@ export const SCAFFOLD_COVERAGE_EXCLUDE: readonly CoverageExclude[] = [
|
|
|
356
365
|
},
|
|
357
366
|
{
|
|
358
367
|
glob: 'apps/*/server.ts',
|
|
359
|
-
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',
|
|
360
369
|
},
|
|
361
370
|
{
|
|
362
371
|
glob: 'apps/*/prerender.ts',
|
|
@@ -462,6 +471,8 @@ export function repoFiles(
|
|
|
462
471
|
{ path: VERIFY_FLOOR_FILE, contents: verifyFloor(example) },
|
|
463
472
|
{ path: 'types/scss.d.ts', contents: scssTypes() },
|
|
464
473
|
{ path: '.gitignore', contents: gitignore() },
|
|
474
|
+
// Every text file LF whatever `core.autocrlf` says — a Windows checkout is otherwise CRLF.
|
|
475
|
+
rootGitattributesFile(),
|
|
465
476
|
{ path: '.env.development', contents: envDevelopment() },
|
|
466
477
|
// Committed, and generated: `x env example` rewrites this file from `envSchema`, and the
|
|
467
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);
|
|
@@ -272,17 +286,11 @@ const toggleIsland =
|
|
|
272
286
|
// browser downloads. Named by SPECIFIER from the page, never by import:
|
|
273
287
|
// const ThemeSwitch = island({ src: '../../shared/theme-toggle.island.tsx', props: ['locale'] });
|
|
274
288
|
//
|
|
275
|
-
// The control itself is the catalog's \`ThemeToggle\`; this file is the runtime it needs
|
|
276
|
-
//
|
|
289
|
+
// The control itself is the catalog's \`ThemeToggle\`; this file is only the runtime it needs. It
|
|
290
|
+
// reads the browser on its own: the theme the boot stamped on \`<html>\`, then \`theme.defaultMode\`
|
|
291
|
+
// (stamped beside it), then the OS.
|
|
277
292
|
|
|
278
|
-
import {
|
|
279
|
-
browserThemeEnv,
|
|
280
|
-
setSolidRuntime,
|
|
281
|
-
THEME_ATTRIBUTE,
|
|
282
|
-
type ThemeEnv,
|
|
283
|
-
ThemeToggle,
|
|
284
|
-
UiProvider,
|
|
285
|
-
} from '@ultimat3/ui';
|
|
293
|
+
import { setSolidRuntime, ThemeToggle, UiProvider } from '@ultimat3/ui';
|
|
286
294
|
import {
|
|
287
295
|
createContext,
|
|
288
296
|
createEffect,
|
|
@@ -298,17 +306,6 @@ export interface ThemeToggleIslandProps {
|
|
|
298
306
|
readonly locale: string;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
/**
|
|
302
|
-
* With no stored choice the catalog's \`resolveTheme\` asks the OS — but the document already wears
|
|
303
|
-
* the theme the framework's boot script stamped from \`theme.defaultMode\` (dark, in this app), so
|
|
304
|
-
* a toggle that asked the OS would show one theme while the page wore another. The boot's verdict
|
|
305
|
-
* is on \`<html>\` before this runs: read it once, and let a stored choice win as it does there.
|
|
306
|
-
*/
|
|
307
|
-
export const bootedEnv = (): ThemeEnv => {
|
|
308
|
-
const booted = document.documentElement.getAttribute(THEME_ATTRIBUTE) === 'dark';
|
|
309
|
-
return { ...browserThemeEnv(), prefersDark: () => booted };
|
|
310
|
-
};
|
|
311
|
-
|
|
312
309
|
/**
|
|
313
310
|
* The one export the hydration runtime calls. \`setSolidRuntime\` comes FIRST and is not optional:
|
|
314
311
|
* \`@ultimat3/ui\` imports types from solid-js and never a runtime, so the reactive graph a
|
|
@@ -318,12 +315,11 @@ export const bootedEnv = (): ThemeEnv => {
|
|
|
318
315
|
*/
|
|
319
316
|
export function mount(el: HTMLElement, props: ThemeToggleIslandProps): void {
|
|
320
317
|
setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
|
|
321
|
-
const env = bootedEnv();
|
|
322
318
|
el.textContent = '';
|
|
323
319
|
render(
|
|
324
320
|
() => (
|
|
325
321
|
<UiProvider locale={props.locale}>
|
|
326
|
-
<ThemeToggle mode="toggle"
|
|
322
|
+
<ThemeToggle mode="toggle" />
|
|
327
323
|
</UiProvider>
|
|
328
324
|
),
|
|
329
325
|
el,
|
|
@@ -353,15 +349,14 @@ export const themeToggleStates = defineIslandStates({
|
|
|
353
349
|
|
|
354
350
|
const toggleTest =
|
|
355
351
|
(): string => `// The toggle the browser actually runs: built with the same \`buildIslands\` as \`x build\`, mounted
|
|
356
|
-
// against a DOM small enough to read, and clicked. What it pins
|
|
357
|
-
//
|
|
358
|
-
// key the boot script reads back on the next load.
|
|
352
|
+
// against a DOM small enough to read, and clicked. What it pins: the boot's verdict on \`<html>\` is
|
|
353
|
+
// what the control starts from, and a click writes the key the boot script reads back next load.
|
|
359
354
|
|
|
360
355
|
import { join } from 'node:path';
|
|
361
356
|
import { buildIslands } from '@ultimat3/cli';
|
|
357
|
+
import { THEME_STORAGE_KEY } from '@ultimat3/core';
|
|
362
358
|
import { describeIslandState, expect, test } from '@ultimat3/testing';
|
|
363
|
-
import { THEME_ATTRIBUTE,
|
|
364
|
-
import { bootedEnv } from './theme-toggle.island';
|
|
359
|
+
import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE } from '@ultimat3/ui';
|
|
365
360
|
import { themeToggleStates } from './theme-toggle.island.states';
|
|
366
361
|
|
|
367
362
|
/** What the island reads through \`browserThemeEnv\`: storage and the OS query, both fakes. */
|
|
@@ -371,11 +366,19 @@ const localStorage = {
|
|
|
371
366
|
setItem: (key: string, value: string): void => void stored.set(key, value),
|
|
372
367
|
removeItem: (key: string): void => void stored.delete(key),
|
|
373
368
|
};
|
|
369
|
+
/** One OS query, shared by every \`matchMedia\` call: \`flipOs\` changes it and tells the listeners. */
|
|
370
|
+
const os = { dark: false, listeners: new Set<() => void>() };
|
|
374
371
|
const matchMedia = (): Record<string, unknown> => ({
|
|
375
|
-
matches:
|
|
376
|
-
|
|
377
|
-
|
|
372
|
+
get matches(): boolean {
|
|
373
|
+
return os.dark;
|
|
374
|
+
},
|
|
375
|
+
addEventListener: (_type: string, listener: () => void) => void os.listeners.add(listener),
|
|
376
|
+
removeEventListener: (_type: string, listener: () => void) => void os.listeners.delete(listener),
|
|
378
377
|
});
|
|
378
|
+
const flipOs = (dark: boolean): void => {
|
|
379
|
+
os.dark = dark;
|
|
380
|
+
for (const listener of os.listeners) listener();
|
|
381
|
+
};
|
|
379
382
|
|
|
380
383
|
// The state \`x shot --island theme-toggle\` photographs is the state this mounts: one manifest,
|
|
381
384
|
// so the picture and the assertion cannot be of two different components.
|
|
@@ -404,12 +407,27 @@ describeIslandState(themeToggleStates, 'default', island, (mounted) => {
|
|
|
404
407
|
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
405
408
|
});
|
|
406
409
|
|
|
407
|
-
test('
|
|
408
|
-
// The fake
|
|
410
|
+
test('with nothing stored, a click flips the theme the boot stamped — not the OS', () => {
|
|
411
|
+
// The OS fake says light. The boot stamped this app's \`defaultMode\`, dark, so the visible
|
|
412
|
+
// theme is dark and the first click must go light; a toggle that asked the OS would write the
|
|
413
|
+
// dark already on screen and look dead.
|
|
414
|
+
stored.clear();
|
|
415
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
|
|
409
416
|
mounted().documentElement.setAttribute(THEME_ATTRIBUTE, 'dark');
|
|
410
|
-
expect(
|
|
411
|
-
|
|
412
|
-
expect(
|
|
417
|
+
expect(mounted().fire('button', 'click')).toBe(true);
|
|
418
|
+
expect(stored.get(THEME_STORAGE_KEY)).toBe('light');
|
|
419
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('light');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("an OS flip is followed while nothing is stored and the app's default is 'system'", () => {
|
|
423
|
+
stored.clear();
|
|
424
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'system');
|
|
425
|
+
flipOs(true);
|
|
426
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
427
|
+
// A fixed default is the app's own opinion: the OS does not override it.
|
|
428
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
|
|
429
|
+
flipOs(false);
|
|
430
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
413
431
|
});
|
|
414
432
|
});
|
|
415
433
|
`;
|
package/src/test-counts.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { ExecResult } from './exec';
|
|
6
6
|
import { execOutput } from './exec';
|
|
7
7
|
import { parseBunTest } from './mcp-test-output';
|
|
8
|
+
// One reader of bun's `N errors` line, shared with MCP `tests.run` — never a second regex.
|
|
9
|
+
import { errorsIn } from './mcp-test-run';
|
|
8
10
|
|
|
9
11
|
export interface TestCounts {
|
|
10
12
|
/** Tests that executed — passed plus failed. A failed test is a test that ran. */
|
|
@@ -20,12 +22,6 @@ export interface TestCounts {
|
|
|
20
22
|
readonly errors?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/** Bun's own `N error` / `N errors` summary line: the last one, as `parseBunTest` reads the rest. */
|
|
24
|
-
const errorsIn = (output: string): number => {
|
|
25
|
-
const last = [...output.matchAll(/^\s*(\d+)\s+errors?\s*$/gm)].at(-1);
|
|
26
|
-
return last === undefined ? 0 : Number.parseInt(last[1] ?? '0', 10);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
25
|
/**
|
|
30
26
|
* Summed across every process the step spawned, because a step is one line in the gate whether it
|
|
31
27
|
* ran on one worker or eight.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The ONE policy for which paths are never a test the gate runs here, projected twice: the path
|
|
2
|
+
// predicate `discoverTests` filters by and the `--path-ignore-patterns` the serial steps pass to
|
|
3
|
+
// `bun test`. A leaf, so `test-select.ts` and `verify-tests.ts` (which import each other) share it.
|
|
4
|
+
|
|
5
|
+
/** Build output and installed packages: never a test, at ANY depth — a workspace's `dist/` too. */
|
|
6
|
+
const IGNORED_ANYWHERE = ['dist', 'node_modules'] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Nested projects with their own gate (`examples/`, `dummy/`, run by
|
|
10
|
+
* `scripts/reference-app-gate.ts`) and the root `build/` — skipped AT THE ROOT only. Matched at any
|
|
11
|
+
* depth they swallowed an app's own slice: `x g resource build` lost 14 of its 16 test files and
|
|
12
|
+
* `x g resource example` its `examples/` page tests, from `x test` and the gate alike.
|
|
13
|
+
*/
|
|
14
|
+
const IGNORED_AT_ROOT = ['build', 'examples', 'dummy'] as const;
|
|
15
|
+
|
|
16
|
+
/** Whether a root-relative POSIX path is excluded from every test selection. */
|
|
17
|
+
export const isIgnoredTestPath = (path: string): boolean =>
|
|
18
|
+
IGNORED_ANYWHERE.some((dir) => `/${path}`.includes(`/${dir}/`)) ||
|
|
19
|
+
IGNORED_AT_ROOT.some((dir) => path.startsWith(`${dir}/`));
|
|
20
|
+
|
|
21
|
+
/** The same policy as `bun test --path-ignore-patterns` globs, relative to the run's root. */
|
|
22
|
+
export const IGNORED_TEST_PATTERNS: readonly string[] = [
|
|
23
|
+
...IGNORED_ANYWHERE.map((dir) => `**/${dir}/**`),
|
|
24
|
+
...IGNORED_AT_ROOT.map((dir) => `${dir}/**`),
|
|
25
|
+
];
|