@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
package/src/site-config.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
// The public origin and the crawler rules out of `app.config.ts` — `site.origin` and
|
|
2
2
|
// `seo.robots.disallow` — and the one resolution of "which origin is this site served on" every
|
|
3
|
-
// absolute URL a document, a sitemap or a robots file carries is built against.
|
|
4
|
-
// `app-
|
|
3
|
+
// absolute URL a document, a sitemap or a robots file carries is built against. Read off the one
|
|
4
|
+
// loader (`app-config-load.ts`).
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import type { AppConfig, Environment, SeoSitemapConfig, SitemapLastmod } from '@ultimat3/core';
|
|
9
|
-
import { SITEMAP_LASTMOD_SOURCES } from '@ultimat3/core';
|
|
10
|
-
import { APP_CONFIG_EXPORT } from './app-auth';
|
|
11
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
6
|
+
import type { Environment, SeoSitemapConfig } from '@ultimat3/core';
|
|
7
|
+
import { loadAppConfig } from './app-config-load';
|
|
12
8
|
|
|
13
9
|
export interface SiteSettings {
|
|
14
10
|
/** `site.origin`, trailing slash removed; `null` when the app declares none. */
|
|
@@ -25,51 +21,16 @@ export const NO_SITE_SETTINGS: SiteSettings = {
|
|
|
25
21
|
sitemap: { extra: [], lastmod: 'none' },
|
|
26
22
|
};
|
|
27
23
|
|
|
28
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
29
|
-
typeof value === 'object' && value !== null;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Structural, never `instanceof`, for `loadSignInPath`'s reason: a config that resolved through an
|
|
33
|
-
* older core simply has no `site` section, and that is the "not declared" answer, not a crash.
|
|
34
|
-
*/
|
|
35
|
-
const hasSiteSections = (value: unknown): value is Pick<AppConfig, 'site' | 'seo'> =>
|
|
36
|
-
isRecord(value) &&
|
|
37
|
-
isRecord(value['site']) &&
|
|
38
|
-
isRecord(value['seo']) &&
|
|
39
|
-
isRecord(value['seo']['robots']);
|
|
40
|
-
|
|
41
24
|
const trimSlash = (origin: string): string => origin.replace(/\/+$/, '');
|
|
42
25
|
|
|
43
26
|
export async function loadSiteSettings(root: string): Promise<SiteSettings> {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
const
|
|
47
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
48
|
-
if (!hasSiteSections(config)) return NO_SITE_SETTINGS;
|
|
49
|
-
const origin = config.site.origin;
|
|
27
|
+
const config = await loadAppConfig(root);
|
|
28
|
+
if (config === undefined) return NO_SITE_SETTINGS;
|
|
29
|
+
const { origin } = config.site;
|
|
50
30
|
return {
|
|
51
|
-
origin:
|
|
31
|
+
origin: origin === null ? null : trimSlash(origin),
|
|
52
32
|
disallow: [...config.seo.robots.disallow],
|
|
53
|
-
sitemap:
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Structural for `hasSiteSections`' reason: a config resolved through a core older than
|
|
59
|
-
* `seo.sitemap` has no such key, and that is the default, not a crash.
|
|
60
|
-
*/
|
|
61
|
-
function sitemapOf(seo: unknown): SeoSitemapConfig {
|
|
62
|
-
const declared = isRecord(seo) ? seo['sitemap'] : undefined;
|
|
63
|
-
if (!isRecord(declared)) return NO_SITE_SETTINGS.sitemap;
|
|
64
|
-
const extra = declared['extra'];
|
|
65
|
-
const lastmod = declared['lastmod'];
|
|
66
|
-
return {
|
|
67
|
-
extra: Array.isArray(extra)
|
|
68
|
-
? extra.filter((path): path is string => typeof path === 'string')
|
|
69
|
-
: [],
|
|
70
|
-
lastmod: SITEMAP_LASTMOD_SOURCES.includes(lastmod as SitemapLastmod)
|
|
71
|
-
? (lastmod as SitemapLastmod)
|
|
72
|
-
: 'none',
|
|
33
|
+
sitemap: config.seo.sitemap,
|
|
73
34
|
};
|
|
74
35
|
}
|
|
75
36
|
|
package/src/site-seo.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Environment, SeoSitemapConfig } from '@ultimat3/core';
|
|
6
6
|
import { UltimateError } from '@ultimat3/core';
|
|
7
|
-
import {
|
|
7
|
+
import { localizedPath, unlocalizedPath } from '@ultimat3/i18n';
|
|
8
|
+
import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
8
9
|
import type { RouteEntry } from '@ultimat3/render';
|
|
9
10
|
import { routeEntries } from '@ultimat3/render';
|
|
10
11
|
import { enumeratePrerender, fillPath } from '@ultimat3/render/server';
|
|
@@ -150,10 +151,11 @@ export async function siteSeo(options: SiteSeoOptions): Promise<SiteSeo> {
|
|
|
150
151
|
// Every routed locale, with `xhtml:link` alternates: one `<url>` per page per locale, each
|
|
151
152
|
// naming the whole cluster and `x-default`. A single-locale app passes none and gets the plain
|
|
152
153
|
// urlset it always had — an alternates cluster of one is noise.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const settings = options.sitemap ?? NO_SITEMAP_SETTINGS;
|
|
154
|
+
// The app's declared set — the one the manifest, the worker and the prerender read — never the
|
|
155
|
+
// ambient locale config, which a build process has not configured.
|
|
156
156
|
const root = options.root ?? process.cwd();
|
|
157
|
+
const { locales, defaultLocale } = await appLocaleSet(root);
|
|
158
|
+
const settings = options.sitemap ?? NO_SITEMAP_SETTINGS;
|
|
157
159
|
const stamp: Stamp = (file) => {
|
|
158
160
|
const lastmod = lastmodOf(file, settings.lastmod, root);
|
|
159
161
|
return lastmod === undefined ? {} : { lastmod };
|
|
@@ -173,7 +175,7 @@ export async function siteSeo(options: SiteSeoOptions): Promise<SiteSeo> {
|
|
|
173
175
|
localizedPath(path, locale, defaultLocale),
|
|
174
176
|
}),
|
|
175
177
|
});
|
|
176
|
-
// Past 50,000 URLs `files` are `/
|
|
178
|
+
// Past 50,000 URLs `files` are `/sitemaps/<n>.xml` and the index is `/sitemap.xml`; below it,
|
|
177
179
|
// `files` is that one file and there is no index.
|
|
178
180
|
const sitemaps = sitemap.index === undefined ? sitemap.files : [sitemap.index, ...sitemap.files];
|
|
179
181
|
const robots = buildRobots({
|
package/src/sitemap-lastmod.ts
CHANGED
|
@@ -14,6 +14,9 @@ let builtAt: string | undefined;
|
|
|
14
14
|
|
|
15
15
|
const cache = new Map<string, string | undefined>();
|
|
16
16
|
|
|
17
|
+
/** One `git log -1` over one path; a synchronous wait past this blocks the request serving it. */
|
|
18
|
+
const GIT_DATE_TIMEOUT_MS = 5_000;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* The `<lastmod>` for one route file, W3C Datetime (ISO 8601, UTC), or `undefined` when the source
|
|
19
22
|
* yields none — a sitemap entry without `<lastmod>` is valid, a wrong one misleads a crawler.
|
|
@@ -46,6 +49,8 @@ function gitDate(file: string, root: string): string | undefined {
|
|
|
46
49
|
cwd: root,
|
|
47
50
|
stdout: 'pipe',
|
|
48
51
|
stderr: 'ignore',
|
|
52
|
+
// A wedged `git` (a lock, a credential prompt) answers no date: the mtime fallback below.
|
|
53
|
+
timeout: GIT_DATE_TIMEOUT_MS,
|
|
49
54
|
});
|
|
50
55
|
if (run.exitCode !== 0) return undefined;
|
|
51
56
|
return isoOf(run.stdout.toString().trim());
|
package/src/solid-loader.ts
CHANGED
|
@@ -73,6 +73,10 @@ interface CacheEntry {
|
|
|
73
73
|
*/
|
|
74
74
|
const cache = new Map<string, CacheEntry>();
|
|
75
75
|
|
|
76
|
+
/** Transforms running now, and the `prepareStackTrace` the first of them found (see below). */
|
|
77
|
+
let inFlight = 0;
|
|
78
|
+
let foundStackTrace: typeof Error.prepareStackTrace;
|
|
79
|
+
|
|
76
80
|
/** Test seam: the cache is process-global because the dev server it serves is too. */
|
|
77
81
|
export function clearIslandTransformCache(): void {
|
|
78
82
|
cache.clear();
|
|
@@ -92,8 +96,12 @@ export async function transformIslandTsx(source: string, path: string): Promise<
|
|
|
92
96
|
|
|
93
97
|
// Babel installs its own `prepareStackTrace` on the first TRANSFORM — not on import, which is
|
|
94
98
|
// where this guard was first put — and leaving it installed makes `Error.captureStackTrace`
|
|
95
|
-
// strict for every unrelated module loaded later in the same process
|
|
96
|
-
|
|
99
|
+
// strict for every unrelated module loaded later in the same process, and names every error in
|
|
100
|
+
// it `Error` (a `TypeError`'s stack then opens `Error: …`). Saved by the FIRST transform in
|
|
101
|
+
// flight and restored by the LAST: `Bun.build` loads a second `.tsx` while the first compiles,
|
|
102
|
+
// and a per-call save taken inside that window saved Babel's own rewriter and restored it last.
|
|
103
|
+
if (inFlight === 0) foundStackTrace = Error.prepareStackTrace;
|
|
104
|
+
inFlight += 1;
|
|
97
105
|
let code: string | null | undefined;
|
|
98
106
|
try {
|
|
99
107
|
// `transformAsync` and never `transformFileAsync`: the latter is gated behind `@babel/core`'s
|
|
@@ -111,7 +119,8 @@ export async function transformIslandTsx(source: string, path: string): Promise<
|
|
|
111
119
|
});
|
|
112
120
|
code = result?.code;
|
|
113
121
|
} finally {
|
|
114
|
-
|
|
122
|
+
inFlight -= 1;
|
|
123
|
+
if (inFlight === 0) Error.prepareStackTrace = foundStackTrace;
|
|
115
124
|
}
|
|
116
125
|
// A parse error is NOT caught here: Babel's own message already names the file, the line and the
|
|
117
126
|
// column ("a.island.tsx: Unexpected token (2:23)"), and `buildOne` wraps whatever escapes in
|
package/src/source-files.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// means a finding one of them can never see.
|
|
4
4
|
|
|
5
5
|
import { hasPathSegment } from './path-segments';
|
|
6
|
+
import { toPosix } from './posix-path';
|
|
6
7
|
|
|
7
8
|
export const SOURCE_GLOBS = [
|
|
8
9
|
'packages/*/src/**/*.{ts,tsx}',
|
|
@@ -16,6 +17,12 @@ export const SOURCE_GLOBS = [
|
|
|
16
17
|
'api/**/*.{ts,tsx}',
|
|
17
18
|
'shared/**/*.{ts,tsx}',
|
|
18
19
|
'apps/*/{app,site,api,shared}/**/*.{ts,tsx}',
|
|
20
|
+
// An app's own entry files: authored, shipped, and outside every surface glob above — a 900-line
|
|
21
|
+
// `server.ts` or an unrunnable `fix:` in it was invisible to `filesize` and `errors`.
|
|
22
|
+
// And its guards: run on the gate, held to the error contract, and invisible to `filesize`.
|
|
23
|
+
'guards/*.ts',
|
|
24
|
+
'apps/*/server.ts',
|
|
25
|
+
'apps/*/prerender.ts',
|
|
19
26
|
] as const;
|
|
20
27
|
|
|
21
28
|
/**
|
|
@@ -32,7 +39,17 @@ export const isGenerated = (path: string): boolean => path.endsWith('.d.ts');
|
|
|
32
39
|
export const isTest = (path: string): boolean => /\.test\.tsx?$/.test(path);
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
42
|
+
* Glob answers as the POSIX paths every consumer keys by, in code-unit order of that spelling.
|
|
43
|
+
* The glob answers in the host's separator: on Windows `guards\untranslated-string.ts` missed
|
|
44
|
+
* `i18n`'s `startsWith('guards/')`, so the example `t('…')` in its fix line became a key every
|
|
45
|
+
* fresh scaffold was missing — and `^packages\/` rules matched nothing at all.
|
|
46
|
+
*/
|
|
47
|
+
export function sortedPosix(paths: readonly string[]): string[] {
|
|
48
|
+
return paths.map(toPosix).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every source file under `root`, repo-relative, POSIX and deduplicated across the globs — in a SORTED
|
|
36
53
|
* order per pattern, `As of 2026-09-05`. `Bun.Glob.scan` yields in the filesystem's `readdir`
|
|
37
54
|
* order, which ext4 hashes: the first offender a scan names was `index.ts` on one machine and
|
|
38
55
|
* `tools.ts` on a GitHub runner, so a finding's `cause` depended on which disk held the checkout
|
|
@@ -46,8 +63,7 @@ export async function* eachSourceFile(root: string): AsyncGenerator<string> {
|
|
|
46
63
|
for await (const path of new Bun.Glob(pattern).scan({ cwd: root, absolute: false })) {
|
|
47
64
|
if (!isVendored(path)) matches.push(path);
|
|
48
65
|
}
|
|
49
|
-
|
|
50
|
-
for (const path of matches) {
|
|
66
|
+
for (const path of sortedPosix(matches)) {
|
|
51
67
|
if (seen.has(path)) continue;
|
|
52
68
|
seen.add(path);
|
|
53
69
|
yield path;
|
package/src/static-report.ts
CHANGED
|
@@ -71,6 +71,11 @@ export type UnmeasuredRoute = {
|
|
|
71
71
|
readonly code?: string;
|
|
72
72
|
readonly cause?: string;
|
|
73
73
|
readonly fix?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Who the measurement render ran as, labelled (`measure-scope.ts`'s `measurementActorLabel`) —
|
|
76
|
+
* so a refusal OF that actor names it, and the edit that changes it (#675). Never its facts.
|
|
77
|
+
*/
|
|
78
|
+
readonly actor?: string;
|
|
74
79
|
/**
|
|
75
80
|
* `false` when no build can weigh the route by construction — a `render: 'ssr'` page with params,
|
|
76
81
|
* which may not declare `prerender()`, so the build holds no value to render it at. Not a defect
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// How a stored object may be presented when the app's own origin serves it (plan 101, s1-sec L6):
|
|
2
|
+
// inline only for a type a browser renders without running anything, a sandboxed download otherwise. Read by
|
|
3
|
+
// both surfaces that serve stored bytes — `/_storage` (`runtime-storage.ts`) and `/media`.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Raster images, audio and video: what an `<img>`, `<audio>` or `<video>` presents and nothing
|
|
7
|
+
* executes. NOT `image/svg+xml` (it carries `<script>`), `text/html`, `application/xml` or PDF —
|
|
8
|
+
* any of them opened from the app's origin runs with the app's cookies and its `'self'` sources.
|
|
9
|
+
*/
|
|
10
|
+
const INLINE_TYPES: ReadonlySet<string> = new Set([
|
|
11
|
+
'image/png',
|
|
12
|
+
'image/jpeg',
|
|
13
|
+
'image/gif',
|
|
14
|
+
'image/webp',
|
|
15
|
+
'image/avif',
|
|
16
|
+
'audio/mpeg',
|
|
17
|
+
'audio/ogg',
|
|
18
|
+
'audio/wav',
|
|
19
|
+
'audio/webm',
|
|
20
|
+
'video/mp4',
|
|
21
|
+
'video/webm',
|
|
22
|
+
'video/ogg',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** `image/PNG; charset=…` is `image/png`: parameters and case never widen the list. */
|
|
26
|
+
const essenceOf = (contentType: string): string =>
|
|
27
|
+
(contentType.split(';')[0] ?? '').trim().toLowerCase();
|
|
28
|
+
|
|
29
|
+
export const isInlineSafe = (contentType: string): boolean =>
|
|
30
|
+
INLINE_TYPES.has(essenceOf(contentType));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The headers a stored object is served with beyond its type. `attachment` makes a navigation to an
|
|
34
|
+
* uploaded `text/html` a download instead of a page on the app's origin; an embedding element
|
|
35
|
+
* (`<img>`, `<video>`) ignores it, so nothing that displays an object inline stops working. The
|
|
36
|
+
* `sandbox` policy is for the browser that renders it anyway: an opaque origin, no script, no
|
|
37
|
+
* cookies. `@ultimat3/http`'s security stage keeps it and adds the app's policy beside it.
|
|
38
|
+
*/
|
|
39
|
+
export function storedObjectHeaders(contentType: string): Readonly<Record<string, string>> {
|
|
40
|
+
return isInlineSafe(contentType)
|
|
41
|
+
? {}
|
|
42
|
+
: { 'content-disposition': 'attachment', 'content-security-policy': 'sandbox' };
|
|
43
|
+
}
|
package/src/style-routes.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// image.
|
|
5
5
|
|
|
6
6
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
7
|
-
import { applyCacheHeaders,
|
|
7
|
+
import { applyCacheHeaders, jsonResponse, NO_STORE } from '@ultimat3/http';
|
|
8
8
|
import type { StyleBundle } from './style-bundle';
|
|
9
9
|
import { STYLE_BASE_PATH } from './style-bundle';
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ export function styleRoutes(source: StyleSource): readonly Route[] {
|
|
|
32
32
|
handler: (request: UltimateRequest): Response => {
|
|
33
33
|
const chunk = source().chunkAt(request.pathname);
|
|
34
34
|
if (chunk === undefined) {
|
|
35
|
-
const missing =
|
|
35
|
+
const missing = jsonResponse(
|
|
36
36
|
{
|
|
37
37
|
ok: false,
|
|
38
38
|
error: {
|
package/src/sw-artifacts.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// until #390. Beside `pwa-artifacts.ts` and not inside it: that file needs a root and a config
|
|
4
4
|
// file, this one needs a booted app and a finished build.
|
|
5
5
|
|
|
6
|
-
import { localeConfig, routedLocales } from '@ultimat3/i18n';
|
|
7
6
|
import type { PrecacheManifest } from '@ultimat3/pwa';
|
|
8
7
|
import { generateServiceWorker } from '@ultimat3/pwa';
|
|
9
8
|
import type { RouteDescriptor } from '@ultimat3/render';
|
|
@@ -12,7 +11,7 @@ import { ISLAND_BASE_PATH } from './island-bundle';
|
|
|
12
11
|
import { msg } from './messages';
|
|
13
12
|
import type { PwaArtifacts } from './pwa-artifacts';
|
|
14
13
|
import type { StyleBundle } from './style-bundle';
|
|
15
|
-
import type { RenderedDocument
|
|
14
|
+
import type { RenderedDocument } from './sw-precache-plan';
|
|
16
15
|
import { localePrefixes, precacheAssets, pwaRoutes } from './sw-precache-plan';
|
|
17
16
|
|
|
18
17
|
export type { RenderedDocument, RoutedLocales } from './sw-precache-plan';
|
|
@@ -73,11 +72,6 @@ export interface ServiceWorkerInput {
|
|
|
73
72
|
* offline reload that cannot load the boot restores no record and shows the old count.
|
|
74
73
|
*/
|
|
75
74
|
readonly scripts?: readonly { readonly url: string; readonly bytes: number }[];
|
|
76
|
-
/**
|
|
77
|
-
* The routed locales, default first. Absent, `@ultimat3/i18n`'s own — the answer the server routes
|
|
78
|
-
* with, set once the app's catalogs are loaded, which every caller has done by now.
|
|
79
|
-
*/
|
|
80
|
-
readonly locales?: RoutedLocales;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
/**
|
|
@@ -157,7 +151,9 @@ export function serviceWorkerArtifacts(
|
|
|
157
151
|
// Absent when this pass did not render the fallback (no route serves it — `x doctor` reports
|
|
158
152
|
// that as `X_PWA_NO_OFFLINE_FALLBACK`), and then `@ultimat3/pwa` falls back to the build id.
|
|
159
153
|
const fallbackDocument = documents.get(fallback);
|
|
160
|
-
|
|
154
|
+
// The set the manifests were built from (`PwaArtifacts.locales`), never the ambient locale
|
|
155
|
+
// config: one answer, so a locale an installed manifest names is always one the worker prefixes.
|
|
156
|
+
const locales = pwa.locales;
|
|
161
157
|
const output = generateServiceWorker(
|
|
162
158
|
pwaRoutes(input.routes, documents, locales),
|
|
163
159
|
{
|
|
@@ -187,6 +183,10 @@ export function serviceWorkerArtifacts(
|
|
|
187
183
|
styles: input.styles,
|
|
188
184
|
scripts: input.scripts ?? [],
|
|
189
185
|
fallback,
|
|
186
|
+
placeholders: {
|
|
187
|
+
urls: [pwa.offline.image, pwa.offline.font].filter((url): url is string => url !== null),
|
|
188
|
+
buildId: input.buildId,
|
|
189
|
+
},
|
|
190
190
|
}),
|
|
191
191
|
},
|
|
192
192
|
input.buildId,
|
package/src/sw-precache-plan.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PrecacheAsset, PwaRoute } from '@ultimat3/pwa';
|
|
|
7
7
|
import { strategyFor } from '@ultimat3/pwa';
|
|
8
8
|
import type { RouteDescriptor } from '@ultimat3/render';
|
|
9
9
|
import type { IslandBundle } from './island-bundle';
|
|
10
|
+
import { parseHashedAssetUrl } from './site-assets';
|
|
10
11
|
import type { StyleBundle } from './style-bundle';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -94,6 +95,12 @@ export interface PrecacheAssetInput {
|
|
|
94
95
|
readonly scripts: readonly { readonly url: string; readonly bytes: number }[];
|
|
95
96
|
/** The offline document's path: its islands ride the precache whatever its own route says. */
|
|
96
97
|
readonly fallback: string;
|
|
98
|
+
/**
|
|
99
|
+
* `pwa.offline.image` / `pwa.offline.font`: the worker answers a failed image or font request
|
|
100
|
+
* from the PRECACHE only, so a placeholder nothing precached is a branch that matches nothing.
|
|
101
|
+
* `buildId` is the revision of one whose bytes this build cannot hash — the offline document's.
|
|
102
|
+
*/
|
|
103
|
+
readonly placeholders?: { readonly urls: readonly string[]; readonly buildId: string };
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
@@ -140,12 +147,35 @@ export function precacheAssets(input: PrecacheAssetInput): readonly PrecacheAsse
|
|
|
140
147
|
for (const chunk of input.islands.chunks) {
|
|
141
148
|
if (wanted.has(chunk.url)) for (const url of chunk.imports) wanted.add(url);
|
|
142
149
|
}
|
|
143
|
-
|
|
150
|
+
const chunks: PrecacheAsset[] = [
|
|
144
151
|
...input.islands.chunks.filter((chunk) => wanted.has(chunk.url)),
|
|
145
152
|
...input.islands.shared.filter((chunk) => wanted.has(chunk.url)),
|
|
146
153
|
...input.styles.chunks,
|
|
147
154
|
...input.scripts,
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
].map((chunk) => ({ url: chunk.url, revision: chunk.url, bytes: chunk.bytes }));
|
|
156
|
+
return [...chunks, ...placeholderAssets(input, new Set(chunks.map((chunk) => chunk.url)))].sort(
|
|
157
|
+
(a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The placeholders on the offline document's terms (`buildPrecacheManifest`'s `fallback` entry):
|
|
163
|
+
* never refused here — this pass cannot see the site assets, and the worker installs entry by
|
|
164
|
+
* entry, so a URL that 404s costs that entry and the branch falls through to the 503 — and
|
|
165
|
+
* stamped with the build id unless the URL is content-addressed, which is its own revision. Bytes
|
|
166
|
+
* unknown count 0, as the offline document's do. A chunk already listed keeps its own entry.
|
|
167
|
+
*/
|
|
168
|
+
function placeholderAssets(
|
|
169
|
+
input: PrecacheAssetInput,
|
|
170
|
+
listed: ReadonlySet<string>,
|
|
171
|
+
): PrecacheAsset[] {
|
|
172
|
+
const out = new Map<string, PrecacheAsset>();
|
|
173
|
+
const placeholders = input.placeholders;
|
|
174
|
+
if (placeholders === undefined) return [];
|
|
175
|
+
for (const url of placeholders.urls) {
|
|
176
|
+
if (listed.has(url) || out.has(url)) continue;
|
|
177
|
+
const hashed = parseHashedAssetUrl(url) !== undefined;
|
|
178
|
+
out.set(url, { url, revision: hashed ? url : placeholders.buildId, bytes: 0 });
|
|
179
|
+
}
|
|
180
|
+
return [...out.values()];
|
|
151
181
|
}
|
|
@@ -9,8 +9,8 @@ import { finiteCount, systemClock } from '@ultimat3/core';
|
|
|
9
9
|
import type { HttpConfig } from '@ultimat3/http';
|
|
10
10
|
import {
|
|
11
11
|
configuredAuthenticator,
|
|
12
|
-
createRequestContext,
|
|
13
12
|
defineHttpConfig,
|
|
13
|
+
requestContext,
|
|
14
14
|
UltimateRequest,
|
|
15
15
|
} from '@ultimat3/http';
|
|
16
16
|
import type { SyncAuthenticator, SyncGrant } from '@ultimat3/realtime/server';
|
|
@@ -84,7 +84,7 @@ export interface SyncAuthenticatorOptions {
|
|
|
84
84
|
/**
|
|
85
85
|
* What the sync node is given when the app configured an authenticator, and `undefined` when it
|
|
86
86
|
* did not — which keeps `x dev` anonymous and makes the node log that it is, exactly as
|
|
87
|
-
* `
|
|
87
|
+
* `syncNode` documents. A stub that answered `{ actor: anonymous }` would look configured.
|
|
88
88
|
*
|
|
89
89
|
* The grant carries an `expiresAt` and a `refresh`, and both are the app's own resolver asked
|
|
90
90
|
* again: `configureAuthenticator` says who is dialling, and the only honest way to learn that it
|
|
@@ -115,7 +115,7 @@ export function syncAuthenticator(
|
|
|
115
115
|
method: credential.method,
|
|
116
116
|
headers: credential.headers,
|
|
117
117
|
});
|
|
118
|
-
const ctx =
|
|
118
|
+
const ctx = requestContext({
|
|
119
119
|
url: new URL(credential.url),
|
|
120
120
|
method: credential.method,
|
|
121
121
|
role: 'sync',
|
package/src/sync-url.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ConfigInvalidError } from '@ultimat3/core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The sync node's own path (`
|
|
8
|
+
* The sync node's own path (`syncNode`'s default in `@ultimat3/realtime`). Same origin by
|
|
9
9
|
* default because every rung already serves it there: `x dev` and a combined-role container mount
|
|
10
10
|
* the node on the web port, and `docker/helm`'s ingress routes `/_x/sync` to the `sync` service.
|
|
11
11
|
*/
|
|
@@ -43,8 +43,8 @@ export function syncConnectSources(env: Readonly<Record<string, string | undefin
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* The page origins a sync node admits besides its own
|
|
47
|
-
*
|
|
46
|
+
* The page origins a sync node admits besides its own origin: `APP_URL`'s, when set. A deployment
|
|
47
|
+
* that serves the page on another port or host than the node sees needs it; an unparsable value is
|
|
48
48
|
* refused rather than read as "no page origin", which would refuse every socket in silence.
|
|
49
49
|
*/
|
|
50
50
|
export function syncOriginsFrom(env: Readonly<Record<string, string | undefined>>): string[] {
|
package/src/tasks-facts.ts
CHANGED
|
@@ -5,38 +5,12 @@
|
|
|
5
5
|
import type { TaskDescriptor, TaskHandle } from '@ultimat3/jobs';
|
|
6
6
|
import { getTask, registeredTasks } from '@ultimat3/jobs';
|
|
7
7
|
import type { CronPhrases } from '@ultimat3/time';
|
|
8
|
-
import {
|
|
9
|
-
describeCron,
|
|
10
|
-
fromEpochMs,
|
|
11
|
-
nextCronOccurrenceMs,
|
|
12
|
-
offsetLabel,
|
|
13
|
-
toZoned,
|
|
14
|
-
} from '@ultimat3/time';
|
|
8
|
+
import { describeCron, fromEpochMs, isoInZone, nextCronOccurrenceMs } from '@ultimat3/time';
|
|
15
9
|
import { BadFlagError } from './errors';
|
|
16
10
|
|
|
17
11
|
const DEFAULT_COUNT = 5;
|
|
18
12
|
const MAX_COUNT = 50;
|
|
19
13
|
|
|
20
|
-
const pad2 = (value: number): string => String(value).padStart(2, '0');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* ISO-8601 rendered with the ZONE's OWN offset (`2026-03-08T03:00:00-04:00`), never collapsed to
|
|
24
|
-
* UTC `Z` — an ambient zone is exactly the bug a task's `tz` exists to prevent. No single
|
|
25
|
-
* "instant in a zone, as ISO" export exists in `@ultimat3/time` to call instead: `toIso` is
|
|
26
|
-
* UTC-`Z` only, `isoDateInZone` is date-only, and `formatWithOffset` renders locale prose, not
|
|
27
|
-
* ISO. So this composes the package's own instant→zoned-wall-clock conversion (`toZoned`) and
|
|
28
|
-
* zone-label helper (`offsetLabel`) rather than a fresh `Intl.DateTimeFormat` call here.
|
|
29
|
-
*
|
|
30
|
-
* Exported for `cmd-tasks.ts`'s `last` column: the occurrence a task last fired and the one it
|
|
31
|
-
* fires next are one clock, so they are one function.
|
|
32
|
-
*/
|
|
33
|
-
export function isoInZone(ms: number, zone: string): string {
|
|
34
|
-
const zoned = toZoned(fromEpochMs(ms), zone);
|
|
35
|
-
const date = `${String(zoned.year).padStart(4, '0')}-${pad2(zoned.month)}-${pad2(zoned.day)}`;
|
|
36
|
-
const time = `${pad2(zoned.hour)}:${pad2(zoned.minute)}:${pad2(zoned.second)}`;
|
|
37
|
-
return `${date}T${time}${offsetLabel(zoned.offsetMinutes)}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
14
|
/** `x tasks list` row: the descriptor plus the next occurrence, ms and rendered alike. */
|
|
41
15
|
export interface TaskFact extends TaskDescriptor {
|
|
42
16
|
readonly nextMs: number;
|
|
@@ -46,7 +20,7 @@ export interface TaskFact extends TaskDescriptor {
|
|
|
46
20
|
function toFact(handle: TaskHandle, nowMs: number): TaskFact {
|
|
47
21
|
const descriptor = handle.describe();
|
|
48
22
|
const nextMs = nextCronOccurrenceMs(descriptor.cron, descriptor.tz, nowMs);
|
|
49
|
-
return { ...descriptor, nextMs, next: isoInZone(nextMs, descriptor.tz) };
|
|
23
|
+
return { ...descriptor, nextMs, next: isoInZone(fromEpochMs(nextMs), descriptor.tz) };
|
|
50
24
|
}
|
|
51
25
|
|
|
52
26
|
export function listTaskFacts(nowMs: number): readonly TaskFact[] {
|
|
@@ -110,7 +84,7 @@ export function taskShowFacts(
|
|
|
110
84
|
let cursor = nowMs;
|
|
111
85
|
for (let i = 0; i < count; i += 1) {
|
|
112
86
|
cursor = nextCronOccurrenceMs(descriptor.cron, descriptor.tz, cursor);
|
|
113
|
-
upcoming.push({ ms: cursor, at: isoInZone(cursor, descriptor.tz) });
|
|
87
|
+
upcoming.push({ ms: cursor, at: isoInZone(fromEpochMs(cursor), descriptor.tz) });
|
|
114
88
|
}
|
|
115
89
|
return { descriptor, describe: describeCron(descriptor.cron, 'en-US', phrases), upcoming };
|
|
116
90
|
}
|
package/src/templates/action.ts
CHANGED
|
@@ -82,6 +82,9 @@ export const ${name.camel} = mutator({
|
|
|
82
82
|
input: t.object({ id: t.uuid, orgId: t.uuid, title: t.string }),
|
|
83
83
|
output: t.object({ id: t.uuid, title: t.string }),
|
|
84
84
|
policy: can${feature.pascal}Write,
|
|
85
|
+
// Required on a mutator (X_MUTATOR_NOT_IDEMPOTENT): the client replays this write under one
|
|
86
|
+
// Idempotency-Key after a dropped response, and the replay answers the first result.
|
|
87
|
+
idempotent: true,
|
|
85
88
|
// No \`mcp\` until a real description is written: see \`x g action\`.
|
|
86
89
|
// tx.table(name) rather than tx.${feature.plural}: the typed accessor exists only once the app
|
|
87
90
|
// augments LocalTables, and generated code cannot assume that has happened yet. The name is the
|
|
@@ -151,7 +154,7 @@ const input = { id, orgId${isMutator ? ", title: 'a title'" : ''} };
|
|
|
151
154
|
|
|
152
155
|
// Named here because every projection needs a stable name and this file does not boot the app.
|
|
153
156
|
// At boot \`registerActions(await import('./actions'))\` stamps the same name onto the same
|
|
154
|
-
// object, so
|
|
157
|
+
// object, so each projection of it works there with nothing to remember.
|
|
155
158
|
const target = ${name.camel}.named('${name.camel}');
|
|
156
159
|
${
|
|
157
160
|
outsider
|
|
@@ -231,7 +234,7 @@ ${
|
|
|
231
234
|
? `
|
|
232
235
|
unitTest('the handler answers the row it found', async () => {
|
|
233
236
|
const draft = { orgId, title: 'kept', price: { minor: 0, currency: 'USD' } };
|
|
234
|
-
const row = await runWithContext(
|
|
237
|
+
const row = await runWithContext(ctxOf({ actor: writer }), () => repo.insert(draft));
|
|
235
238
|
const answer = await target.as(writer, ${call});
|
|
236
239
|
expect(answer).toEqual(${found});
|
|
237
240
|
});
|
|
@@ -260,7 +263,7 @@ const actionUnitTest = (name: NameSet, feature: NameSet, shape: HandlerShape): s
|
|
|
260
263
|
return `// ${name.camel}: its declared shape, the input it refuses, and what its handler answers — in
|
|
261
264
|
// process, so all three belong to the \`unit\` step. The contract projections are next door.
|
|
262
265
|
${sortedImports([
|
|
263
|
-
...(stores ? ["import {
|
|
266
|
+
...(stores ? ["import { ctxOf, runWithContext } from '@ultimat3/core';"] : []),
|
|
264
267
|
"import { testActor } from '@ultimat3/policy';",
|
|
265
268
|
`import { ${testing} } from '@ultimat3/testing';`,
|
|
266
269
|
...(stores ? [`import { driver } from '${shape.dbModule}';`] : []),
|
|
@@ -273,7 +276,7 @@ const input = { id, orgId${shape.isMutator ? ", title: 'a title'" : ''} };
|
|
|
273
276
|
${shape.isMutator ? `\n// The client store's table: the entity's own, so the twin and the server row share one key.\nconst TABLE = '${feature.table}';\n` : ''}
|
|
274
277
|
// Named here because every projection needs a stable name and this file does not boot the app.
|
|
275
278
|
// At boot \`registerActions(await import('./actions'))\` stamps the same name onto the same
|
|
276
|
-
// object, so
|
|
279
|
+
// object, so each projection of it works there with nothing to remember.
|
|
277
280
|
const target = ${name.camel}.named('${name.camel}');
|
|
278
281
|
|
|
279
282
|
// Holds the grant in the org the input names: past the policy, so what answers is the handler.
|
|
@@ -107,14 +107,14 @@ const backfillTest = (
|
|
|
107
107
|
): string => `// ${name.camel}'s durable identity: one live run per name, retried under the same key. The work
|
|
108
108
|
// itself — the pass over real rows and the projection it applies — is the unit suite's, next door.
|
|
109
109
|
|
|
110
|
-
import {
|
|
110
|
+
import { memoryJobDriver, resetJobDriver, setJobDriver } from '@ultimat3/jobs';
|
|
111
111
|
import { afterAll, beforeAll, expect, jobTest } from '@ultimat3/testing';
|
|
112
112
|
import { ${name.camel} } from './${name.kebab}';
|
|
113
113
|
|
|
114
114
|
// The driver is process-global, so it is installed and released around this file rather than
|
|
115
115
|
// left behind for whichever test happens to run next.
|
|
116
116
|
beforeAll(() => {
|
|
117
|
-
setJobDriver(
|
|
117
|
+
setJobDriver(memoryJobDriver());
|
|
118
118
|
});
|
|
119
119
|
afterAll(resetJobDriver);
|
|
120
120
|
|
|
@@ -162,7 +162,7 @@ const backfillUnitTest = (
|
|
|
162
162
|
// driver. A sweep rewrites rows no user asked it to, so what is worth failing on is that it reaches
|
|
163
163
|
// every tenant's rows, writes the projection and nothing else, and that a replay changes nothing.
|
|
164
164
|
${sortedImports([
|
|
165
|
-
"import {
|
|
165
|
+
"import { ctxOf, runWithContext } from '@ultimat3/core';",
|
|
166
166
|
"import { testActor } from '@ultimat3/policy';",
|
|
167
167
|
"import { afterEach, expect, unitTest } from '@ultimat3/testing';",
|
|
168
168
|
`import { db, driver } from '${dbModule}';`,
|
|
@@ -175,7 +175,7 @@ const orgB = '00000000-0000-4000-8000-000000000009';
|
|
|
175
175
|
|
|
176
176
|
/** What a request is to the handle: an actor, whose org every read and write runs under. */
|
|
177
177
|
const inOrg = <T>(orgId: string, run: () => Promise<T>): Promise<T> =>
|
|
178
|
-
runWithContext(
|
|
178
|
+
runWithContext(ctxOf({ actor: testActor('member', { orgId }).actor }), run);
|
|
179
179
|
|
|
180
180
|
const store = (orgId: string, title: string): Promise<${feature.pascal}> => {
|
|
181
181
|
const draft = { orgId, title, price: { minor: 1000, currency: 'USD' } };
|
package/src/templates/entity.ts
CHANGED
|
@@ -156,7 +156,7 @@ const repoTest = (
|
|
|
156
156
|
): string => `// The ${name.kebab} repo against the in-memory driver: the contract Postgres serves, with no
|
|
157
157
|
// database. What it pins is what the repo cannot show by being read — whose rows a call reaches.
|
|
158
158
|
${sortedImports([
|
|
159
|
-
"import {
|
|
159
|
+
"import { ctxOf, frozenClock, runWithContext } from '@ultimat3/core';",
|
|
160
160
|
"import { testActor } from '@ultimat3/policy';",
|
|
161
161
|
"import { afterEach, expect, unitTest } from '@ultimat3/testing';",
|
|
162
162
|
`import { driver } from '${dbModule}';`,
|
|
@@ -171,7 +171,7 @@ const clock = frozenClock('2026-01-01T00:00:00.000Z');
|
|
|
171
171
|
|
|
172
172
|
/** What a request is to the handle: an actor, whose org every read and write runs under. */
|
|
173
173
|
const inOrg = <T>(org: string, run: () => Promise<T>): Promise<T> =>
|
|
174
|
-
runWithContext(
|
|
174
|
+
runWithContext(ctxOf({ actor: testActor('member', { orgId: org }).actor, clock }), run);
|
|
175
175
|
|
|
176
176
|
const draft = (title: string) => ({ orgId, title, price: { minor: 1200, currency: 'USD' } });
|
|
177
177
|
|