@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// How the build weighs an app/ page it renders only to measure: as the document a request is
|
|
2
|
+
// SERVED — the sync head and page boot both boots compose (`pageSync`), and the runtime chunk that
|
|
3
|
+
// boot makes unfetched. An unscoped render measured a runtime chunk no scoped page downloads and
|
|
4
|
+
// never the boot every one of them does, so `/feed` and `/runs` were budgets about nothing (#505).
|
|
5
|
+
|
|
6
|
+
import type { ClientSyncHead } from '@ultimat3/render';
|
|
7
|
+
import { loadAppConfig } from './app-config-load';
|
|
8
|
+
import type { MeasureOptions } from './budgets';
|
|
9
|
+
import type { IslandBundle } from './island-bundle';
|
|
10
|
+
import { isRuntimeChunk } from './island-runtime';
|
|
11
|
+
import { pageSync } from './page-sync';
|
|
12
|
+
import { realtimeConfigOf } from './runtime-realtime';
|
|
13
|
+
import { PAGE_BOOT_BASE_PATH } from './worker-bundle';
|
|
14
|
+
|
|
15
|
+
/** What a served-document measurement adds: to the document's options, and to the measurer's. */
|
|
16
|
+
export interface ServedMeasure {
|
|
17
|
+
readonly document: {
|
|
18
|
+
readonly sync?: ClientSyncHead;
|
|
19
|
+
readonly persisted?: () => readonly string[];
|
|
20
|
+
};
|
|
21
|
+
readonly measure: MeasureOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `realtime.enabled: false` serves no sync head and no boot (`pageSync`), and then the measurement
|
|
26
|
+
* adds nothing: such a page's realtime islands load the runtime chunk, and are charged for it.
|
|
27
|
+
*/
|
|
28
|
+
export async function servedMeasure(
|
|
29
|
+
root: string,
|
|
30
|
+
buildId: string,
|
|
31
|
+
islands: IslandBundle,
|
|
32
|
+
): Promise<ServedMeasure> {
|
|
33
|
+
const sync = await pageSync(
|
|
34
|
+
root,
|
|
35
|
+
process.env,
|
|
36
|
+
buildId,
|
|
37
|
+
realtimeConfigOf(await loadAppConfig(root)),
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
document: {
|
|
41
|
+
...(sync.head === undefined ? {} : { sync: sync.head }),
|
|
42
|
+
persisted: sync.persisted,
|
|
43
|
+
},
|
|
44
|
+
measure: {
|
|
45
|
+
served: new Map(sync.scripts.map((script) => [script.url, script.code])),
|
|
46
|
+
boot: {
|
|
47
|
+
prefix: `${PAGE_BOOT_BASE_PATH}/`,
|
|
48
|
+
supplies: new Set(islands.shared.map((one) => one.url).filter(isRuntimeChunk)),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/prerender.ts
CHANGED
|
@@ -5,34 +5,38 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
ctxOf,
|
|
9
9
|
DEFAULT_ENVIRONMENT,
|
|
10
10
|
isUltimateError,
|
|
11
|
+
localizePath,
|
|
11
12
|
renderThrowable,
|
|
12
13
|
runWithContext,
|
|
13
14
|
tryResolveEnvironment,
|
|
14
15
|
} from '@ultimat3/core';
|
|
15
|
-
import { localeConfig, localizedPath, routedLocales } from '@ultimat3/i18n';
|
|
16
16
|
import type { RouteEntry } from '@ultimat3/render';
|
|
17
|
-
import {
|
|
17
|
+
import { describePages, routeEntries } from '@ultimat3/render';
|
|
18
18
|
import { renderStatic } from '@ultimat3/render/server';
|
|
19
19
|
import { loadApp } from './app-load';
|
|
20
20
|
import { appManifest } from './app-manifest';
|
|
21
21
|
import type { RouteStats } from './budgets';
|
|
22
|
-
import { measureDocumentJs, writeBuildStats } from './budgets';
|
|
22
|
+
import { measureDocumentJs, routeStatsRow, writeBuildStats } from './budgets';
|
|
23
23
|
import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
|
|
24
|
+
import { PrerenderLoadFailedError } from './errors';
|
|
24
25
|
import { FAVICON_PATH, faviconBytes } from './favicon';
|
|
25
26
|
import type { IslandBundle } from './island-bundle';
|
|
26
27
|
import { buildIslands, writeIslands } from './island-bundle';
|
|
27
28
|
import { measureDatabase } from './measure-database';
|
|
28
29
|
import { measurePaths } from './measure-paths';
|
|
29
|
-
import { measureScope, withAppUrl } from './measure-scope';
|
|
30
|
+
import { measurementActorLabel, measureScope, withAppUrl } from './measure-scope';
|
|
30
31
|
import { loadNavigation, pageNavigation } from './page-navigation';
|
|
31
|
-
import { loadSpeculation, pageSpeculation } from './page-speculation';
|
|
32
|
+
import { loadSpeculation, otherLocaleSegments, pageSpeculation } from './page-speculation';
|
|
32
33
|
import { localizedArtifacts } from './prerender-locales';
|
|
33
34
|
import { clearPrerenderOut } from './prerender-out';
|
|
35
|
+
import type { PrerenderedPage, PrerenderReport } from './prerender-report';
|
|
36
|
+
import type { ServedMeasure } from './prerender-served';
|
|
37
|
+
import { servedMeasure } from './prerender-served';
|
|
34
38
|
import { loadPwaArtifacts, writePwaIcons } from './pwa-artifacts';
|
|
35
|
-
import { routeDocument } from './runtime-render';
|
|
39
|
+
import { routeDocument, servedDocument } from './runtime-render';
|
|
36
40
|
import { writeSiteAssets } from './site-assets';
|
|
37
41
|
import { loadSiteSettings, originWarning, publicOrigin } from './site-config';
|
|
38
42
|
import type { SkippedRoute, UnmeasuredRoute } from './static-report';
|
|
@@ -47,9 +51,13 @@ import {
|
|
|
47
51
|
serviceWorkerRegistration,
|
|
48
52
|
} from './sw-artifacts';
|
|
49
53
|
import { loadThemeMode, themeBoot } from './theme-boot';
|
|
54
|
+
import { loadThemeBrand } from './theme-brand';
|
|
55
|
+
import { writeErrorLine } from './write-line';
|
|
50
56
|
|
|
57
|
+
export type { PrerenderedPage, PrerenderReport } from './prerender-report';
|
|
51
58
|
// Re-exported, never re-declared: `static-report.ts` owns the shape because the report on disk
|
|
52
|
-
// carries it, and this file already imports that module.
|
|
59
|
+
// carries it, and this file already imports that module. The report's own shapes are
|
|
60
|
+
// `prerender-report.ts`'s, re-exported here for the same reason: callers ask this module.
|
|
53
61
|
export type { UnmeasuredRoute };
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -74,59 +82,6 @@ export interface PrerenderOptions {
|
|
|
74
82
|
readonly origin?: string;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
export interface PrerenderedPage {
|
|
78
|
-
/** The DECLARED route, `/blog/:slug` — one route can write many pages, and the report groups them. */
|
|
79
|
-
readonly route: string;
|
|
80
|
-
/** The URL the page is served at — `/en/pricing` for a non-default locale. */
|
|
81
|
-
readonly path: string;
|
|
82
|
-
/** The locale the page was rendered in. */
|
|
83
|
-
readonly locale: string;
|
|
84
|
-
/** Relative to `out`, POSIX — under `<locale>/` for a non-default locale. */
|
|
85
|
-
readonly file: string;
|
|
86
|
-
readonly hash: string;
|
|
87
|
-
readonly bytes: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface PrerenderReport {
|
|
91
|
-
readonly out: string;
|
|
92
|
-
readonly buildId: string;
|
|
93
|
-
readonly pages: readonly PrerenderedPage[];
|
|
94
|
-
/**
|
|
95
|
-
* Every declared route that wrote no file, WITH the cause. A bare path list was the whole of
|
|
96
|
-
* #242: `.x/static/` held a partial site, the report said only which paths were missing, and a
|
|
97
|
-
* screenshot tool pointed at the directory filed "the island did not mount" against a route that
|
|
98
|
-
* had never been in the artifact. The reason is what tells an author whether an edit exists.
|
|
99
|
-
*/
|
|
100
|
-
readonly skipped: readonly SkippedRoute[];
|
|
101
|
-
/**
|
|
102
|
-
* Routes whose budget this build could not weigh, with the reason. `X_BUDGET_UNMEASURED` is what
|
|
103
|
-
* the gate then reports for each; this is the half that says WHY, which a per-route finding read
|
|
104
|
-
* off a stats file cannot know.
|
|
105
|
-
*/
|
|
106
|
-
readonly unmeasured: readonly UnmeasuredRoute[];
|
|
107
|
-
/** Where the measured stats landed, for the `budgets` gate step to read. */
|
|
108
|
-
readonly stats: string;
|
|
109
|
-
/** Where the emitted/skipped inventory landed, for `x build --target static` to read back. */
|
|
110
|
-
readonly report: string;
|
|
111
|
-
/** Client entries emitted, one chunk each. Reported so "which JS shipped?" needs no unzip. */
|
|
112
|
-
readonly islands: readonly string[];
|
|
113
|
-
/** Surface stylesheets emitted, one file each — the CSS half of the same question. */
|
|
114
|
-
readonly styles: readonly string[];
|
|
115
|
-
/**
|
|
116
|
-
* What the service worker could not express, and what its precache manifest weighs too much of.
|
|
117
|
-
*
|
|
118
|
-
* `PrecacheManifest.warnings` had no reader anywhere in the tree — the precache budget was, in
|
|
119
|
-
* `wiki/Troubleshooting.md`'s own words, "a designed thing that is not one" (#390). An install
|
|
120
|
-
* that stalls on a bad connection is invisible on a laptop and fatal on a phone, so the number
|
|
121
|
-
* has to reach the build's own report. Empty for an app with no service worker.
|
|
122
|
-
*/
|
|
123
|
-
readonly serviceWorkerWarnings: readonly string[];
|
|
124
|
-
/** What this build could not get right on its own — today, a production build with no origin. */
|
|
125
|
-
readonly warnings: readonly string[];
|
|
126
|
-
/** The origin every absolute URL in the export was built against — hand it to `siteSeo`. */
|
|
127
|
-
readonly origin: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
85
|
/**
|
|
131
86
|
* The heaviest thing the document actually boots, named by the source file an author can open.
|
|
132
87
|
* An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
|
|
@@ -168,15 +123,17 @@ const NOT_FOUND_STATUS = 404;
|
|
|
168
123
|
*/
|
|
169
124
|
const declaresBudget = (entry: RouteEntry): boolean => {
|
|
170
125
|
const budget = entry.config.budget;
|
|
171
|
-
return budget
|
|
126
|
+
return budget?.js !== undefined;
|
|
172
127
|
};
|
|
173
128
|
|
|
174
129
|
export async function prerenderSite(options: PrerenderOptions): Promise<PrerenderReport> {
|
|
175
|
-
// The
|
|
176
|
-
//
|
|
177
|
-
//
|
|
130
|
+
// The load `x dev` and `x manifest` perform: importing the app's modules fills the route registry.
|
|
131
|
+
// A module that would not import registered nothing, so its page would vanish from the export
|
|
132
|
+
// under a green build — refused BEFORE the export is emptied, so the last good artifact survives.
|
|
133
|
+
const loaded = await loadApp(options.root);
|
|
134
|
+
if (loaded.findings.length > 0) throw new PrerenderLoadFailedError(loaded.findings);
|
|
135
|
+
// Emptied before anything is written: a deleted route's HTML kept shipping when it only grew.
|
|
178
136
|
await clearPrerenderOut(options.out, options.root);
|
|
179
|
-
await loadApp(options.root);
|
|
180
137
|
const buildId = (await appManifest(options.root)).manifest.buildId;
|
|
181
138
|
const declaredOrigin =
|
|
182
139
|
options.origin ?? publicOrigin(process.env, await loadSiteSettings(options.root));
|
|
@@ -184,11 +141,12 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
184
141
|
// Written to stderr as well as returned: an app's `prerender.ts` prints its report as ONE JSON
|
|
185
142
|
// line on stdout, and a build that parses it must not meet a warning inside it.
|
|
186
143
|
const warnings = originWarning(tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT, declaredOrigin);
|
|
187
|
-
for (const warning of warnings)
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
const
|
|
144
|
+
for (const warning of warnings) writeErrorLine(`warning: ${warning}`);
|
|
145
|
+
// The app's locales as `loadApp` read them, ONCE, off `@ultimat3/i18n`'s one reader — the same
|
|
146
|
+
// answer the PWA manifest and the service worker below are built from. The default first, so
|
|
147
|
+
// every other pass writes beside a tree that already holds the unprefixed one.
|
|
148
|
+
const appLocales = loaded.locales;
|
|
149
|
+
const { locales, defaultLocale } = appLocales;
|
|
192
150
|
const pages: PrerenderedPage[] = [];
|
|
193
151
|
const skipped: SkippedRoute[] = [];
|
|
194
152
|
const routes: RouteStats[] = [];
|
|
@@ -235,6 +193,8 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
235
193
|
// names it either.
|
|
236
194
|
const pwa = await loadPwaArtifacts(options.root);
|
|
237
195
|
const theme = themeBoot(await loadThemeMode(options.root));
|
|
196
|
+
// The brand the served process carries, so an exported page and a served one paint alike.
|
|
197
|
+
const brand = await loadThemeBrand(options.root);
|
|
238
198
|
// The client router, for the surfaces that opted in: written BEFORE any document names it, for
|
|
239
199
|
// the register's reason below — `measureDocumentJs` weighs it off disk, and it is charged to
|
|
240
200
|
// the route (the interactivity the app asked for), so it must be there to be weighed.
|
|
@@ -244,6 +204,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
244
204
|
const speculation = pageSpeculation({
|
|
245
205
|
config: await loadSpeculation(options.root),
|
|
246
206
|
client: declared.surfaces,
|
|
207
|
+
localeSegments: otherLocaleSegments(appLocales),
|
|
247
208
|
});
|
|
248
209
|
if (navigation.script !== undefined) {
|
|
249
210
|
await Bun.write(join(options.out, navigation.script.url.slice(1)), navigation.script.code);
|
|
@@ -289,11 +250,11 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
289
250
|
// build. One context for the build, `role: 'web'` because that is the role serving these
|
|
290
251
|
// documents, and this build's own id so a component reading `ctx.buildId` stamps the artifact
|
|
291
252
|
// with the id the report and the stats carry.
|
|
292
|
-
// ONE PER LOCALE, and the locale is the context's: `
|
|
253
|
+
// ONE PER LOCALE, and the locale is the context's: `ctxOf` defaults it to core's `en`, so a
|
|
293
254
|
// Spanish-default site was prerendered in English — `<html lang="en">` over English copy — while
|
|
294
255
|
// the served process answered Spanish. Each `site/` page is rendered once per routed locale.
|
|
295
256
|
const contexts = new Map(
|
|
296
|
-
locales.map((locale) => [locale,
|
|
257
|
+
locales.map((locale) => [locale, ctxOf({ role: 'web', buildId, locale })]),
|
|
297
258
|
);
|
|
298
259
|
// A SECOND scope, for the branch below that renders only to weigh (`measure-scope.ts`): a
|
|
299
260
|
// request context holding the app's measurement actor, with the app's own API answered in
|
|
@@ -304,23 +265,32 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
304
265
|
const measure = await measureScope({ origin, buildId });
|
|
305
266
|
// Started on the first route that needs weighing, and released however the loop ends.
|
|
306
267
|
const database = measureDatabase(options.root);
|
|
268
|
+
const documentOptions = {
|
|
269
|
+
resolveIsland: (file: string) => islands.resolverFor(file),
|
|
270
|
+
themeHead: theme.head,
|
|
271
|
+
...(brand === undefined ? {} : { brandHead: brand.head }),
|
|
272
|
+
...(speculation === undefined ? {} : { speculationHead: speculation.head }),
|
|
273
|
+
origin,
|
|
274
|
+
...(navigation.head === undefined ? {} : { navigation: navigation.head }),
|
|
275
|
+
...(pwa === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
|
|
278
|
+
};
|
|
307
279
|
const render = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
|
|
308
|
-
routeDocument(entry, data,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
: { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
|
|
317
|
-
});
|
|
280
|
+
routeDocument(entry, data, documentOptions);
|
|
281
|
+
// The weigh-only branch measures the document a request is SERVED (`prerender-served.ts`):
|
|
282
|
+
// composed on the first route that needs it, so a build weighing none builds no page boot.
|
|
283
|
+
let weighing: Promise<ServedMeasure> | undefined;
|
|
284
|
+
const served = (): Promise<ServedMeasure> => {
|
|
285
|
+
weighing ??= servedMeasure(options.root, buildId, islands);
|
|
286
|
+
return weighing;
|
|
287
|
+
};
|
|
318
288
|
const document = (
|
|
319
289
|
locale: string,
|
|
320
290
|
entry: RouteEntry,
|
|
321
291
|
data: { url: string; params: Record<string, string> },
|
|
322
292
|
) =>
|
|
323
|
-
runWithContext(contexts.get(locale) ??
|
|
293
|
+
runWithContext(contexts.get(locale) ?? ctxOf({ role: 'web', buildId, locale }), () =>
|
|
324
294
|
render(entry, data),
|
|
325
295
|
);
|
|
326
296
|
|
|
@@ -349,16 +319,19 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
349
319
|
let row: RouteStats | undefined;
|
|
350
320
|
for (const { path, params } of plan.paths) {
|
|
351
321
|
const data = { url: new URL(path, origin).href, params };
|
|
352
|
-
const
|
|
353
|
-
const
|
|
322
|
+
const as = await served();
|
|
323
|
+
const html = await withAppUrl(origin, () =>
|
|
324
|
+
measure.run(() =>
|
|
325
|
+
servedDocument(entry, data, measure.actor, {
|
|
326
|
+
...documentOptions,
|
|
327
|
+
...as.document,
|
|
328
|
+
buildId,
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
const measured = await measureDocumentJs(html, options.out, as.measure);
|
|
354
333
|
if (row !== undefined && measured.jsBytes <= row.jsBytes) continue;
|
|
355
|
-
|
|
356
|
-
row = {
|
|
357
|
-
path: entry.path,
|
|
358
|
-
jsBytes: measured.jsBytes,
|
|
359
|
-
frameworkJsBytes: measured.frameworkBytes,
|
|
360
|
-
...(chain === undefined ? {} : { heaviestChain: chain }),
|
|
361
|
-
};
|
|
334
|
+
row = routeStatsRow(entry.path, measured, heaviestSource(islands, measured.entries));
|
|
362
335
|
}
|
|
363
336
|
if (row !== undefined) routes.push(row);
|
|
364
337
|
} catch (error) {
|
|
@@ -374,16 +347,17 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
374
347
|
...(isUltimateError(error)
|
|
375
348
|
? { code: error.code, cause: error.cause, fix: error.fix }
|
|
376
349
|
: {}),
|
|
350
|
+
actor: measurementActorLabel(measure.actor),
|
|
377
351
|
});
|
|
378
352
|
}
|
|
379
353
|
continue;
|
|
380
354
|
}
|
|
381
|
-
const artifacts = await localizedArtifacts(
|
|
355
|
+
const artifacts = await localizedArtifacts(appLocales, (locale) =>
|
|
382
356
|
renderStatic(
|
|
383
357
|
entry,
|
|
384
358
|
({ path, params }) =>
|
|
385
359
|
document(locale, entry, {
|
|
386
|
-
url: new URL(
|
|
360
|
+
url: new URL(localizePath(path, locale, locales, defaultLocale), origin).href,
|
|
387
361
|
params,
|
|
388
362
|
}),
|
|
389
363
|
{ buildId },
|
|
@@ -428,14 +402,8 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
428
402
|
bytes,
|
|
429
403
|
assets: measured.entries.map((entry) => entry.url),
|
|
430
404
|
});
|
|
431
|
-
const chain = heaviestSource(islands, measured.entries);
|
|
432
405
|
if (heaviest !== undefined && heaviest.jsBytes >= measured.jsBytes) continue;
|
|
433
|
-
heaviest =
|
|
434
|
-
path: entry.path,
|
|
435
|
-
jsBytes: measured.jsBytes,
|
|
436
|
-
frameworkJsBytes: measured.frameworkBytes,
|
|
437
|
-
...(chain === undefined ? {} : { heaviestChain: chain }),
|
|
438
|
-
};
|
|
406
|
+
heaviest = routeStatsRow(entry.path, measured, heaviestSource(islands, measured.entries));
|
|
439
407
|
}
|
|
440
408
|
if (heaviest !== undefined) routes.push(heaviest);
|
|
441
409
|
}
|
|
@@ -452,7 +420,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
452
420
|
: serviceWorkerArtifacts({
|
|
453
421
|
pwa,
|
|
454
422
|
buildId,
|
|
455
|
-
routes:
|
|
423
|
+
routes: describePages(),
|
|
456
424
|
islands,
|
|
457
425
|
styles,
|
|
458
426
|
documents,
|
package/src/pwa-artifacts.ts
CHANGED
|
@@ -15,17 +15,13 @@
|
|
|
15
15
|
// build behind them — `wiki/PWA-And-Offline.md` says so — and a bad `sw.js` is sticky in a way a
|
|
16
16
|
// manifest is not, so the worker lands behind a real browser check rather than beside this.
|
|
17
17
|
|
|
18
|
-
// why: Bun exposes no
|
|
19
|
-
// `app-auth.ts` and `runtime-cache.ts` each make before importing an app's config — a root with no
|
|
20
|
-
// `app.config.ts` is an ordinary answer here (a scratch root, `x build` outside an app).
|
|
21
|
-
import { existsSync } from 'node:fs';
|
|
22
|
-
// why: Bun exposes no path-join primitive, and `APP_CONFIG_FILE` is app-root-relative — the same
|
|
23
|
-
// necessity `favicon.ts` and `runtime-assets.ts` each record for their own root-relative constant.
|
|
18
|
+
// why: Bun exposes no path-join primitive, and the export writes each icon under an out directory.
|
|
24
19
|
import { join } from 'node:path';
|
|
25
20
|
import type { PwaColors, PwaOfflineConfig } from '@ultimat3/core';
|
|
26
|
-
import { localeSegment } from '@ultimat3/core';
|
|
21
|
+
import { escapeHtml, localeSegment } from '@ultimat3/core';
|
|
27
22
|
import type { CacheHint, RequestContext, Route, UltimateRequest } from '@ultimat3/http';
|
|
28
23
|
import { applyCacheHeaders } from '@ultimat3/http';
|
|
24
|
+
import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
29
25
|
import {
|
|
30
26
|
appleTouchLinks,
|
|
31
27
|
generateWebManifest,
|
|
@@ -34,9 +30,7 @@ import {
|
|
|
34
30
|
} from '@ultimat3/pwa';
|
|
35
31
|
import type { AssetPath } from '@ultimat3/render';
|
|
36
32
|
import { assetPathProblem } from '@ultimat3/render';
|
|
37
|
-
import {
|
|
38
|
-
import { APP_CONFIG_EXPORT } from './app-auth';
|
|
39
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
33
|
+
import { loadAppConfig } from './app-config-load';
|
|
40
34
|
import { hasSourceIcon, iconPlan, iconRenderer } from './icon-assets';
|
|
41
35
|
import type { AppLocales } from './pwa-manifest-locales';
|
|
42
36
|
import { appLocales, localeMembers, spellIn } from './pwa-manifest-locales';
|
|
@@ -80,45 +74,17 @@ export interface PwaArtifacts {
|
|
|
80
74
|
/** The `<head>` for a document in `locale` — it links that locale's manifest. */
|
|
81
75
|
headFor(locale: string): string;
|
|
82
76
|
/**
|
|
83
|
-
* The three `pwa` keys the SERVICE WORKER needs, carried here
|
|
84
|
-
*
|
|
85
|
-
* bundle as well, and a second `await import` of `app.config.ts` would be a second answer to
|
|
86
|
-
* "what did this app declare".
|
|
77
|
+
* The three `pwa` keys the SERVICE WORKER needs, carried here so `sw-artifacts.ts` — which needs
|
|
78
|
+
* the route table and the island bundle as well — reads them off the same resolved block.
|
|
87
79
|
*/
|
|
88
80
|
readonly offline: PwaOfflineConfig;
|
|
89
81
|
readonly backgroundSync: boolean;
|
|
90
82
|
readonly push: boolean;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const text = (value: unknown): string | undefined =>
|
|
97
|
-
typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* `colors` read structurally, for `loadSignInPath`'s reason: `defineConfig` returns a plain object
|
|
101
|
-
* and a config resolved through an older core simply has no such key. `validate()` already refused
|
|
102
|
-
* a blank one an `await import` above this line, so anything this rejects is a hand-written config
|
|
103
|
-
* object — and the honest answer for one is no manifest at all, never a colour we invented.
|
|
104
|
-
*/
|
|
105
|
-
function colorsOf(value: unknown): PwaColors | undefined {
|
|
106
|
-
if (!isRecord(value)) return undefined;
|
|
107
|
-
const light = isRecord(value['light']) ? value['light'] : undefined;
|
|
108
|
-
const dark = isRecord(value['dark']) ? value['dark'] : undefined;
|
|
109
|
-
if (light === undefined || dark === undefined) return undefined;
|
|
110
|
-
const [lt, lb, dt, db] = [
|
|
111
|
-
text(light['themeColor']),
|
|
112
|
-
text(light['backgroundColor']),
|
|
113
|
-
text(dark['themeColor']),
|
|
114
|
-
text(dark['backgroundColor']),
|
|
115
|
-
];
|
|
116
|
-
if (lt === undefined || lb === undefined || dt === undefined || db === undefined)
|
|
117
|
-
return undefined;
|
|
118
|
-
return {
|
|
119
|
-
light: { themeColor: lt, backgroundColor: lb },
|
|
120
|
-
dark: { themeColor: dt, backgroundColor: db },
|
|
121
|
-
};
|
|
83
|
+
/**
|
|
84
|
+
* The routed locales the manifests above were built from, default first — `appLocaleSet`'s one
|
|
85
|
+
* answer, carried here so the service worker prefixes exactly the locales the manifests name.
|
|
86
|
+
*/
|
|
87
|
+
readonly locales: AppLocales;
|
|
122
88
|
}
|
|
123
89
|
|
|
124
90
|
/** The `pwa` block, as much of it as this file needs, or `undefined` when the app declares none. */
|
|
@@ -133,60 +99,30 @@ interface InstallableApp {
|
|
|
133
99
|
readonly push: boolean;
|
|
134
100
|
}
|
|
135
101
|
|
|
136
|
-
async function loadInstallable(root: string): Promise<InstallableApp | undefined> {
|
|
137
|
-
const configPath = join(root, APP_CONFIG_FILE);
|
|
138
|
-
if (!existsSync(configPath)) return undefined;
|
|
139
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
140
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
141
|
-
if (!isRecord(config)) return undefined;
|
|
142
|
-
const pwa = config['pwa'];
|
|
143
|
-
// `pwa.enabled`, read. Not a truthiness test: `enabled` is the key this whole file exists to
|
|
144
|
-
// give a reader, and `=== true` is what makes a hand-written `enabled: 'yes'` produce no
|
|
145
|
-
// manifest rather than one nobody asked for.
|
|
146
|
-
if (!isRecord(pwa) || pwa['enabled'] !== true) return undefined;
|
|
147
|
-
const name = text(pwa['name']);
|
|
148
|
-
const colors = colorsOf(pwa['colors']);
|
|
149
|
-
if (name === undefined || colors === undefined) return undefined;
|
|
150
|
-
return {
|
|
151
|
-
name,
|
|
152
|
-
colors,
|
|
153
|
-
block: pwa,
|
|
154
|
-
locales: appLocales(config),
|
|
155
|
-
offline: offlineOf(pwa['offline']),
|
|
156
|
-
...flags(pwa),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
102
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* then emits no worker at all — never a path the framework invented, which offline would be a
|
|
165
|
-
* cached 404 answering every navigation.
|
|
103
|
+
* `pwa.enabled`, read, off the one loader (`app-config-load.ts`). Core's validator refuses an
|
|
104
|
+
* enabled block without a name, both colour schemes or an absolute `offline.fallback`, so nothing
|
|
105
|
+
* here invents a colour or a fallback route; `colors` is optional only in the type.
|
|
166
106
|
*/
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
107
|
+
async function loadInstallable(root: string): Promise<InstallableApp | undefined> {
|
|
108
|
+
const config = await loadAppConfig(root);
|
|
109
|
+
const pwa = config?.pwa;
|
|
110
|
+
if (config === undefined || pwa === undefined || !pwa.enabled || pwa.colors === undefined) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
171
113
|
return {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
114
|
+
name: pwa.name,
|
|
115
|
+
colors: pwa.colors,
|
|
116
|
+
block: { ...pwa },
|
|
117
|
+
locales: appLocales(await appLocaleSet(root)),
|
|
118
|
+
offline: pwa.offline,
|
|
119
|
+
backgroundSync: pwa.backgroundSync,
|
|
120
|
+
push: pwa.push,
|
|
179
121
|
};
|
|
180
122
|
}
|
|
181
123
|
|
|
182
|
-
/** `=== true` for `enabled`'s reason: a hand-written `backgroundSync: 'yes'` wires nothing. */
|
|
183
|
-
const flags = (pwa: Record<string, unknown>): { backgroundSync: boolean; push: boolean } => ({
|
|
184
|
-
backgroundSync: pwa['backgroundSync'] === true,
|
|
185
|
-
push: pwa['push'] === true,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
124
|
/**
|
|
189
|
-
* Resolved ONCE at boot, like `loadSignInPath` and
|
|
125
|
+
* Resolved ONCE at boot, like `loadSignInPath` and unlike `faviconBytes`: the loader's
|
|
190
126
|
* `await import` caches the module, so re-reading per request would answer the same object at a
|
|
191
127
|
* per-request cost, and the head string has to be available synchronously while a document renders.
|
|
192
128
|
*/
|
|
@@ -219,7 +155,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
|
|
|
219
155
|
path,
|
|
220
156
|
body: serializeWebManifest(result.manifest),
|
|
221
157
|
head:
|
|
222
|
-
`<link rel="manifest" href="${
|
|
158
|
+
`<link rel="manifest" href="${escapeHtml(path)}">` +
|
|
223
159
|
renderThemeColorMeta(result.themeColorMeta) +
|
|
224
160
|
(icons === undefined ? '' : appleTouchLinks(icons)),
|
|
225
161
|
};
|
|
@@ -231,6 +167,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
|
|
|
231
167
|
offline: app.offline,
|
|
232
168
|
backgroundSync: app.backgroundSync,
|
|
233
169
|
push: app.push,
|
|
170
|
+
locales: app.locales,
|
|
234
171
|
body: primary.body,
|
|
235
172
|
head: primary.head,
|
|
236
173
|
manifests,
|
|
@@ -244,7 +181,9 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
|
|
|
244
181
|
* `asset()` does in a page. The site serves an asset ONLY at its hashed URL, and the manifest took
|
|
245
182
|
* `src` verbatim, so an app had to hash its own screenshots to avoid shipping a 404 in the install
|
|
246
183
|
* sheet (notificado.co's `pwa-install.ts`). A missing file is `X_ASSET_MISSING` at boot, never a
|
|
247
|
-
* broken image in an install prompt; any other `src`
|
|
184
|
+
* broken image in an install prompt; any other `src` passes untouched — an absolute path outside
|
|
185
|
+
* `/assets/`, an already hashed one, or a shortcut icon's off-site URL (core screens screenshots and
|
|
186
|
+
* shortcut urls to absolute paths, not icon sources).
|
|
248
187
|
*/
|
|
249
188
|
function withHashedAssets<M extends ReturnType<typeof localeMembers>>(members: M, root: string): M {
|
|
250
189
|
const resolve = (src: string): string => {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// routed locale from this; the `pwa` block is read structurally, for `colorsOf`'s reason there.
|
|
4
4
|
|
|
5
5
|
import { localeSegment, localizePath } from '@ultimat3/core';
|
|
6
|
+
import type { AppLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
6
7
|
import type {
|
|
7
8
|
ManifestIcon,
|
|
8
9
|
ManifestScreenshot,
|
|
@@ -10,7 +11,7 @@ import type {
|
|
|
10
11
|
WebManifestInput,
|
|
11
12
|
} from '@ultimat3/pwa';
|
|
12
13
|
|
|
13
|
-
/** The app's locales as
|
|
14
|
+
/** The app's locales as its catalogs declare them (`defineCatalogs`), default first. */
|
|
14
15
|
export interface AppLocales {
|
|
15
16
|
readonly routed: readonly string[];
|
|
16
17
|
readonly fallback: string;
|
|
@@ -23,15 +24,14 @@ const strings = (value: unknown): readonly string[] =>
|
|
|
23
24
|
Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* The manifests' locales, off `appLocaleSet`'s answer (`@ultimat3/i18n/app-catalogs`) — an app
|
|
28
|
+
* with no catalogs is `UNDECLARED_LOCALES`, the framework default alone. The manifest's `lang` was
|
|
29
|
+
* the generator's hardcoded `'en'` for an `es-co` app (notificado.co, 22.3.2). Deduplicated by URL
|
|
30
|
+
* segment, the unit a locale prefix is spelled in.
|
|
29
31
|
*/
|
|
30
|
-
export function appLocales(
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const fallback = typeof named === 'string' && named !== '' ? named : (declared[0] ?? 'en');
|
|
34
|
-
const others = declared.filter((locale) => localeSegment(locale) !== localeSegment(fallback));
|
|
32
|
+
export function appLocales(set: AppLocaleSet): AppLocales {
|
|
33
|
+
const fallback = set.defaultLocale;
|
|
34
|
+
const others = set.locales.filter((locale) => localeSegment(locale) !== localeSegment(fallback));
|
|
35
35
|
return { routed: [fallback, ...others], fallback };
|
|
36
36
|
}
|
|
37
37
|
|
package/src/registry.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// dev server it never started (plan 101 slice 12 d).
|
|
8
8
|
|
|
9
9
|
import { affectedSpec } from './cmd-affected-spec';
|
|
10
|
+
import { authSpec } from './cmd-auth-spec';
|
|
10
11
|
import { buildSpec } from './cmd-build-spec';
|
|
11
12
|
import { ciSpec } from './cmd-ci-spec';
|
|
12
13
|
import { dbSpec } from './cmd-db-spec';
|
|
@@ -85,6 +86,7 @@ export const LAZY_COMMANDS: readonly LazyCommand[] = [
|
|
|
85
86
|
lazy(deploySpec, async () => (await import('./cmd-deploy')).deployCommand),
|
|
86
87
|
lazy(envSpec, async () => (await import('./cmd-env')).envCommand),
|
|
87
88
|
lazy(secretsSpec, async () => (await import('./cmd-secrets')).secretsCommand),
|
|
89
|
+
lazy(authSpec, async () => (await import('./cmd-auth')).authCommand),
|
|
88
90
|
lazy(manifestSpec, async () => (await import('./cmd-manifest')).manifestCommand),
|
|
89
91
|
lazy(routesSpec, async () => (await import('./cmd-routes')).routesCommand),
|
|
90
92
|
lazy(actionsSpec, async () => (await import('./cmd-registries')).actionsCommand),
|
package/src/retry-memo.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface RetryMemo<T> {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* `start` runs at most once per SUCCESS. `@ultimat3/db`'s `
|
|
18
|
+
* `start` runs at most once per SUCCESS. `@ultimat3/db`'s `pgliteClient` states the rule this
|
|
19
19
|
* generalises — "a failed boot must not be cached" — and the clearing handler is attached at
|
|
20
20
|
* creation for the reason that matters: it therefore runs ahead of every caller's own `await`
|
|
21
21
|
* continuation, so by the time anyone sees the rejection the slot is already empty and the next
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// name that outlives its bytes is a file no client refreshes for a year.
|
|
5
5
|
|
|
6
6
|
import type { UltimateRequest } from '@ultimat3/http';
|
|
7
|
+
import { contentHash } from '@ultimat3/render/server';
|
|
7
8
|
import { etagMatches } from './runtime-storage';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -14,8 +15,8 @@ import { etagMatches } from './runtime-storage';
|
|
|
14
15
|
export const REVALIDATE_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* A strong validator from the bytes —
|
|
18
|
-
* taken here over bytes because an icon is not a string. Revalidating costs a request and no body.
|
|
18
|
+
* A strong validator from the bytes — `@ultimat3/render`'s `contentHash`, the function a document's
|
|
19
|
+
* ETag is, taken here over bytes because an icon is not a string. Revalidating costs a request and no body.
|
|
19
20
|
*/
|
|
20
21
|
export function revalidatedResponse(
|
|
21
22
|
request: UltimateRequest,
|
|
@@ -25,7 +26,7 @@ export function revalidatedResponse(
|
|
|
25
26
|
// Copied, not passed through: a `Uint8Array<ArrayBufferLike>` may be backed by a
|
|
26
27
|
// `SharedArrayBuffer`, which `Response` does not accept — `runtime-assets.ts`'s rule, verbatim.
|
|
27
28
|
const bytes = typeof body === 'string' ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
28
|
-
const etag = `"${
|
|
29
|
+
const etag = `"${contentHash(bytes)}"`;
|
|
29
30
|
const headers = {
|
|
30
31
|
'content-type': contentType,
|
|
31
32
|
'cache-control': REVALIDATE_CACHE_CONTROL,
|