@teamscala/service-boot 0.23.535 → 0.23.537
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/package.json +306 -305
- package/src/agreement-gate/signing-session.ts +1 -1
- package/src/apply-logger-config.ts +2 -1
- package/src/build-client-assets/hydrate-bundle.ts +3 -2
- package/src/build-client-assets/manifest.ts +0 -38
- package/src/build-client-assets/sourcemaps-upload.test.ts +2 -4
- package/src/build-client-assets/sourcemaps-upload.ts +244 -138
- package/src/build-client-assets/styles-build.ts +2 -2
- package/src/build-client-assets.ts +9 -46
- package/src/load-service-install-row.ts +1 -1
- package/src/mount-server-packages.ts +6 -12
- package/src/orpc-contract-rows.json +34 -17
- package/src/orpc-contract-rows.test.ts +1450 -0
- package/src/pg-boss-mount.ts +124 -15
- package/src/read-html-template.ts +1 -25
- package/src/register-page-server-client.ts +2 -2
- package/src/register-registry-page-route/live-loader-config.test.ts +0 -12
- package/src/register-registry-page-route/live-loader-config.ts +1 -1
- package/src/register-registry-page-route/resolve-request-graph.ts +1 -1
- package/src/register-registry-page-route.ts +3 -3
- package/src/two-phase-full-app.ts +3 -3
- package/src/ui-service.ts +11 -36
- package/src/build-client-assets/source-candidates.test.ts +0 -60
- package/src/build-client-assets/source-candidates.ts +0 -76
- package/src/orpc-openapi-gate.test.ts +0 -751
|
@@ -3,30 +3,13 @@
|
|
|
3
3
|
* @status handwritten
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
// boot — SSR can still serve HTML; only client interactivity degrades. No fallback path for failed bundle builds — per `no-shims`, never write a
|
|
8
|
-
// manifest entry that points at a file we don't actually serve. If a build fails, write a TOMBSTONE manifest (no-silent-reverts) and throw.
|
|
9
|
-
//
|
|
10
|
-
// The tombstone preserves the LAST-SUCCESSFUL paths so SSR can keep serving the old bundle (better than a 404 hydrate), but marks itself
|
|
11
|
-
// `buildFailed: true` + the error + `failedAt`, AND — because every boot
|
|
12
|
-
// now rebuilds unconditionally — the failure is retried by construction;
|
|
13
|
-
// the tombstone is the on-disk failure signal; ui-service-boot adds the
|
|
14
|
-
// runtime signal (recordEvent → event-log → GlitchTip) and read-html-template stamps the
|
|
15
|
-
// served HTML with the failure status. Without the tombstone, a build failure left the OLD manifest untouched and the dashboard served stale
|
|
16
|
-
// code with NO signal that the published fix never reached the bundle (scala-agents 2026-07-18: ui-foundation@3.4.5 installed but the bundle
|
|
17
|
-
// kept serving the old bytes across restarts while node_modules was mid-install — invisible until the operator hit the white-on-white
|
|
18
|
-
// toolbar regression the fix was supposed to close).
|
|
19
|
-
|
|
20
|
-
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
21
7
|
import { join } from "node:path";
|
|
22
8
|
import { withFileLock } from "@teamscala/file-lock/with-lock";
|
|
23
9
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
24
10
|
import { CLIENT_BUILD_LOCK, getClientBuildLockTimeoutMs } from "./build-client-assets/build-lock.ts";
|
|
25
11
|
import { buildHydrateBundle } from "./build-client-assets/hydrate-bundle.ts";
|
|
26
|
-
import {
|
|
27
|
-
readManifest,
|
|
28
|
-
writeManifest,
|
|
29
|
-
} from "./build-client-assets/manifest.ts";
|
|
12
|
+
import { MANIFEST_FILE, writeManifest } from "./build-client-assets/manifest.ts";
|
|
30
13
|
import { buildTailwindCss } from "./build-client-assets/styles-build.ts";
|
|
31
14
|
|
|
32
15
|
export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
@@ -34,12 +17,6 @@ export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
|
34
17
|
const cwd = process.cwd();
|
|
35
18
|
const outDir = join(cwd, "dist", "client");
|
|
36
19
|
|
|
37
|
-
// Serialise the build across every UI service on this host
|
|
38
|
-
// (client-asset-builds-serialize-via-lock) so a publish-driven restart storm
|
|
39
|
-
// becomes a short queue instead of N concurrent host-saturating builds. The
|
|
40
|
-
// lock FAILS OPEN: a lock-acquisition timeout builds unlocked rather than blocking
|
|
41
|
-
// boot. A real build failure inside performBuild throws and propagates (the
|
|
42
|
-
// service refuses to start); only the file-lock timeout falls through.
|
|
43
20
|
const lockTimeoutMs = await getClientBuildLockTimeoutMs();
|
|
44
21
|
try {
|
|
45
22
|
await withFileLock(
|
|
@@ -67,31 +44,17 @@ async function performBuild(
|
|
|
67
44
|
): Promise<void> {
|
|
68
45
|
const t0 = Date.now();
|
|
69
46
|
|
|
70
|
-
|
|
47
|
+
await rm(join(outDir, MANIFEST_FILE), { force: true });
|
|
48
|
+
|
|
71
49
|
const writeTombstone = async (reason: string): Promise<void> => {
|
|
72
50
|
try {
|
|
73
51
|
await mkdir(outDir, { recursive: true });
|
|
74
52
|
await writeManifest(outDir, {
|
|
75
|
-
// NO invented paths: when a prior successful build exists its real
|
|
76
|
-
// paths are preserved (SSR keeps serving the prior bundle — degraded
|
|
77
|
-
// but coherent); when none exists the tombstone carries NO asset
|
|
78
|
-
// paths at all. Writing the unhashed build names here was the last
|
|
79
|
-
// remnant of the deleted fallback class — paths that name files
|
|
80
|
-
// nothing built must not exist on disk any more than in served HTML.
|
|
81
|
-
// A path-less tombstone reads as unbuilt everywhere (readManifest →
|
|
82
|
-
// null → verification refuses → readiness never flips), and its
|
|
83
|
-
// buildFailedReason below is the operator's diagnostic — the failure
|
|
84
|
-
// is read via readClientBuildStatus, which never needed the paths.
|
|
85
|
-
...(existingManifest?.stylesPath && existingManifest?.hydratePath
|
|
86
|
-
? { stylesPath: existingManifest.stylesPath, hydratePath: existingManifest.hydratePath }
|
|
87
|
-
: {}),
|
|
88
53
|
buildFailed: true,
|
|
89
54
|
buildFailedReason: reason.slice(0, 2000),
|
|
90
55
|
buildFailedAt: new Date().toISOString(),
|
|
91
56
|
});
|
|
92
57
|
} catch (tombstoneErr) {
|
|
93
|
-
// Best-effort: if even the tombstone write fails (read-only FS,
|
|
94
|
-
// full disk), the throw below still propagates the build failure.
|
|
95
58
|
log.warn(
|
|
96
59
|
`[build-client-assets:${serviceSlug}] failed to write failure tombstone (build error still propagates)`,
|
|
97
60
|
{ tombstoneError: tombstoneErr instanceof Error ? tombstoneErr.message : String(tombstoneErr) },
|
|
@@ -115,8 +78,8 @@ async function performBuild(
|
|
|
115
78
|
const reason = hydrateResult.reason instanceof Error ? hydrateResult.reason.message : String(hydrateResult.reason);
|
|
116
79
|
await writeTombstone(`hydrate build failed: ${reason}`);
|
|
117
80
|
throw new Error(
|
|
118
|
-
`[build-client-assets:${serviceSlug}] hydrate bundle build failed —
|
|
119
|
-
`Fix the underlying import/resolution errors and
|
|
81
|
+
`[build-client-assets:${serviceSlug}] hydrate bundle build failed — no servable client build exists. ` +
|
|
82
|
+
`Fix the underlying import/resolution errors and redeploy. Original: ${reason}`,
|
|
120
83
|
);
|
|
121
84
|
}
|
|
122
85
|
if (stylesResult.status === "rejected") {
|
|
@@ -127,8 +90,8 @@ async function performBuild(
|
|
|
127
90
|
const reason = stylesResult.reason instanceof Error ? stylesResult.reason.message : String(stylesResult.reason);
|
|
128
91
|
await writeTombstone(`styles build failed: ${reason}`);
|
|
129
92
|
throw new Error(
|
|
130
|
-
`[build-client-assets:${serviceSlug}] styles bundle build failed —
|
|
131
|
-
`Fix the underlying tailwind/CSS errors and
|
|
93
|
+
`[build-client-assets:${serviceSlug}] styles bundle build failed — no servable client build exists. ` +
|
|
94
|
+
`Fix the underlying tailwind/CSS errors and redeploy. Original: ${reason}`,
|
|
132
95
|
);
|
|
133
96
|
}
|
|
134
97
|
|
|
@@ -138,7 +101,7 @@ async function performBuild(
|
|
|
138
101
|
const reason = `bundle build returned empty path (hydrate=${hydrate?.hydratePath ?? "<null>"}, styles=${stylesPath})`;
|
|
139
102
|
await writeTombstone(reason);
|
|
140
103
|
throw new Error(
|
|
141
|
-
`[build-client-assets:${serviceSlug}] ${reason} —
|
|
104
|
+
`[build-client-assets:${serviceSlug}] ${reason} — no servable client build exists.`,
|
|
142
105
|
);
|
|
143
106
|
}
|
|
144
107
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
enforceExplicitRenderMode,
|
|
14
14
|
ServiceInstallConfigSchema,
|
|
15
15
|
ServiceInstallValidationError,
|
|
16
|
-
} from "@teamscala/
|
|
16
|
+
} from "@teamscala/registry-schemas/registry-schemas/service-install";
|
|
17
17
|
import * as v from "valibot";
|
|
18
18
|
|
|
19
19
|
const BOOT_ROW_KEYS = [
|
|
@@ -58,10 +58,6 @@ export async function mountServerPackages(
|
|
|
58
58
|
const mountRegistry = createMountRegistry();
|
|
59
59
|
const mountCtx = createMountContext(serviceSlug, config, app, mountRegistry, getSharedDb);
|
|
60
60
|
const serverPackages = (config.server_packages ?? []) as string[];
|
|
61
|
-
/** Packages that loaded but whose mount threw — unavailable, not fatal.
|
|
62
|
-
* Named in the summary so a degraded boot is legible without grepping for
|
|
63
|
-
* the per-package error line. */
|
|
64
|
-
const degraded: string[] = [];
|
|
65
61
|
|
|
66
62
|
if (serverPackages.length === 0) {
|
|
67
63
|
log.info(`[scala-install:${serviceSlug}] No server_packages declared`);
|
|
@@ -119,10 +115,11 @@ export async function mountServerPackages(
|
|
|
119
115
|
}
|
|
120
116
|
} catch (err) {
|
|
121
117
|
if (err instanceof BootFatalError) throw err;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
throw new BootFatalError(
|
|
119
|
+
"mount-server-packages",
|
|
120
|
+
serviceSlug,
|
|
121
|
+
`server_package "${pkg}" mount failed: ${err instanceof Error ? err.message : err}`,
|
|
122
|
+
err,
|
|
126
123
|
);
|
|
127
124
|
}
|
|
128
125
|
}
|
|
@@ -130,10 +127,7 @@ export async function mountServerPackages(
|
|
|
130
127
|
log.info(
|
|
131
128
|
`[scala-install:${serviceSlug}] Registered ${mountRegistry.handlers.size} handler(s), ` +
|
|
132
129
|
`${mountRegistry.wsRoutes.size} ws route(s), ` +
|
|
133
|
-
`${mountRegistry.bootHooks.size} boot hook(s)
|
|
134
|
-
(degraded.length > 0
|
|
135
|
-
? `, ${degraded.length} server_package(s) UNAVAILABLE: ${degraded.join(", ")}`
|
|
136
|
-
: ""),
|
|
130
|
+
`${mountRegistry.bootHooks.size} boot hook(s)`,
|
|
137
131
|
);
|
|
138
132
|
|
|
139
133
|
return mountRegistry;
|
|
@@ -452,6 +452,7 @@
|
|
|
452
452
|
"note": "The html text claims (absent Take me home, present no-page-at-this-link) stay hand-written: then-step expects carry structural verbs only.",
|
|
453
453
|
"then": [
|
|
454
454
|
{
|
|
455
|
+
"args": [],
|
|
455
456
|
"call": "text"
|
|
456
457
|
}
|
|
457
458
|
],
|
|
@@ -480,6 +481,7 @@
|
|
|
480
481
|
"note": "The html text claims (Take me home, doesn't exist, href, noindex) stay hand-written: the text lives behind the async Response.text() member call, and then-step expects carry the structural verbs only, so no row can read the body. The then step here proves text() resolves.",
|
|
481
482
|
"then": [
|
|
482
483
|
{
|
|
484
|
+
"args": [],
|
|
483
485
|
"call": "text"
|
|
484
486
|
}
|
|
485
487
|
],
|
|
@@ -509,6 +511,7 @@
|
|
|
509
511
|
"note": "The html text claims stay hand-written: then-step expects carry structural verbs only (no contains_text), so no row can read the async body.",
|
|
510
512
|
"then": [
|
|
511
513
|
{
|
|
514
|
+
"args": [],
|
|
512
515
|
"call": "text"
|
|
513
516
|
}
|
|
514
517
|
],
|
|
@@ -537,6 +540,7 @@
|
|
|
537
540
|
"note": "The html copy claims stay hand-written: async body, structural then-steps only.",
|
|
538
541
|
"then": [
|
|
539
542
|
{
|
|
543
|
+
"args": [],
|
|
540
544
|
"call": "text"
|
|
541
545
|
}
|
|
542
546
|
],
|
|
@@ -562,6 +566,7 @@
|
|
|
562
566
|
"note": "The html copy claims stay hand-written: async body, structural then-steps only.",
|
|
563
567
|
"then": [
|
|
564
568
|
{
|
|
569
|
+
"args": [],
|
|
565
570
|
"call": "text"
|
|
566
571
|
}
|
|
567
572
|
],
|
|
@@ -585,6 +590,7 @@
|
|
|
585
590
|
"note": "The html copy claims stay hand-written: async body, structural then-steps only.",
|
|
586
591
|
"then": [
|
|
587
592
|
{
|
|
593
|
+
"args": [],
|
|
588
594
|
"call": "text"
|
|
589
595
|
}
|
|
590
596
|
],
|
|
@@ -627,6 +633,7 @@
|
|
|
627
633
|
"expect": null
|
|
628
634
|
},
|
|
629
635
|
{
|
|
636
|
+
"args": [],
|
|
630
637
|
"call": "text"
|
|
631
638
|
}
|
|
632
639
|
],
|
|
@@ -685,6 +692,7 @@
|
|
|
685
692
|
"expect": "no-store, no-transform"
|
|
686
693
|
},
|
|
687
694
|
{
|
|
695
|
+
"args": [],
|
|
688
696
|
"call": "text"
|
|
689
697
|
}
|
|
690
698
|
],
|
|
@@ -1522,6 +1530,7 @@
|
|
|
1522
1530
|
"slug": "service-boot:extract-org-id-from-callback-path-scala-projects-single-uuid-shape-segment-0"
|
|
1523
1531
|
},
|
|
1524
1532
|
{
|
|
1533
|
+
"args": [],
|
|
1525
1534
|
"note": "The generated router shape is what matters, not mere key presence.",
|
|
1526
1535
|
"hoist": [
|
|
1527
1536
|
{
|
|
@@ -1554,6 +1563,7 @@
|
|
|
1554
1563
|
"slug": "service-boot:get-server-client-capability-fault-a-model-present-without-findfirst-still-counts-as-missing"
|
|
1555
1564
|
},
|
|
1556
1565
|
{
|
|
1566
|
+
"args": [],
|
|
1557
1567
|
"note": "Daemon/portal shells own no DB schema and are healthy with an empty client.",
|
|
1558
1568
|
"hoist": [
|
|
1559
1569
|
{
|
|
@@ -1580,6 +1590,7 @@
|
|
|
1580
1590
|
"slug": "service-boot:get-server-client-capability-fault-a-service-declaring-no-models-is-never-faulted"
|
|
1581
1591
|
},
|
|
1582
1592
|
{
|
|
1593
|
+
"args": [],
|
|
1583
1594
|
"hoist": [
|
|
1584
1595
|
{
|
|
1585
1596
|
"args": [
|
|
@@ -1609,6 +1620,7 @@
|
|
|
1609
1620
|
"slug": "service-boot:get-server-client-capability-fault-an-unregistered-client-is-itself-the-fault"
|
|
1610
1621
|
},
|
|
1611
1622
|
{
|
|
1623
|
+
"args": [],
|
|
1612
1624
|
"note": "THE OUTAGE, AS A HEALTH ANSWER. A client was registered and carried only the platform routers, so every process-existence signal was green while every data-backed page 500d. /health must be able to say this.",
|
|
1613
1625
|
"hoist": [
|
|
1614
1626
|
{
|
|
@@ -1644,6 +1656,7 @@
|
|
|
1644
1656
|
"slug": "service-boot:get-server-client-capability-fault-names-the-models-the-client-cannot-serve"
|
|
1645
1657
|
},
|
|
1646
1658
|
{
|
|
1659
|
+
"args": [],
|
|
1647
1660
|
"hoist": [
|
|
1648
1661
|
{
|
|
1649
1662
|
"args": [
|
|
@@ -1685,6 +1698,7 @@
|
|
|
1685
1698
|
"slug": "service-boot:get-server-client-capability-fault-no-fault-when-every-declared-model-is-servable"
|
|
1686
1699
|
},
|
|
1687
1700
|
{
|
|
1701
|
+
"args": [],
|
|
1688
1702
|
"note": "The band asserts reference identity (toBe) across two calls; the row spells the shape that comes back, since the runner names no cross-call reference. The single module slot is the mechanism that makes them the same object.",
|
|
1689
1703
|
"hoist": [
|
|
1690
1704
|
{
|
|
@@ -1715,6 +1729,7 @@
|
|
|
1715
1729
|
"slug": "service-boot:get-server-client-returns-the-registered-client-once-boot-sets-one"
|
|
1716
1730
|
},
|
|
1717
1731
|
{
|
|
1732
|
+
"args": [],
|
|
1718
1733
|
"note": "The old fallback returned a truthy client, so callers could not tell registration had never happened. Both band spellings (the no-registration message and the registerPageServerClient naming) are declared as the substrings the rejection must carry.",
|
|
1719
1734
|
"hoist": [
|
|
1720
1735
|
{
|
|
@@ -1895,15 +1910,8 @@
|
|
|
1895
1910
|
"slug": "service-boot:is-module-not-found-treats-an-unexported-subpath-as-not-found"
|
|
1896
1911
|
},
|
|
1897
1912
|
{
|
|
1898
|
-
"
|
|
1899
|
-
|
|
1900
|
-
"args": [
|
|
1901
|
-
null
|
|
1902
|
-
],
|
|
1903
|
-
"export": "setServerClient"
|
|
1904
|
-
}
|
|
1905
|
-
],
|
|
1906
|
-
"label": "isServerClientRegistered tracks registration state (unregistered)",
|
|
1913
|
+
"args": [],
|
|
1914
|
+
"label": "isServerClientRegistered is false in a process where boot has not registered a client — the module's own initial state, so the row needs no setup",
|
|
1907
1915
|
"expect": false,
|
|
1908
1916
|
"export": "isServerClientRegistered",
|
|
1909
1917
|
"module": "src/page-server-orpc/server-client.ts",
|
|
@@ -1912,6 +1920,7 @@
|
|
|
1912
1920
|
"slug": "service-boot:is-server-client-registered-is-false-before-boot-registers"
|
|
1913
1921
|
},
|
|
1914
1922
|
{
|
|
1923
|
+
"args": [],
|
|
1915
1924
|
"hoist": [
|
|
1916
1925
|
{
|
|
1917
1926
|
"args": [
|
|
@@ -2174,7 +2183,7 @@
|
|
|
2174
2183
|
"expect": {
|
|
2175
2184
|
"member_equals": {
|
|
2176
2185
|
"title": "scala-quiz oRPC procedures",
|
|
2177
|
-
"servers.0.url": "http://localhost/api/
|
|
2186
|
+
"servers.0.url": "http://localhost/api/openapi"
|
|
2178
2187
|
}
|
|
2179
2188
|
},
|
|
2180
2189
|
"export": "openApiContractOptions",
|
|
@@ -2193,7 +2202,7 @@
|
|
|
2193
2202
|
"expect": {
|
|
2194
2203
|
"member_equals": {
|
|
2195
2204
|
"title": "scala-portal oRPC procedures",
|
|
2196
|
-
"servers.0.url": "http://scala-portal.internal:5200/api/
|
|
2205
|
+
"servers.0.url": "http://scala-portal.internal:5200/api/openapi"
|
|
2197
2206
|
}
|
|
2198
2207
|
},
|
|
2199
2208
|
"export": "openApiContractOptions",
|
|
@@ -2636,8 +2645,7 @@
|
|
|
2636
2645
|
"website": {
|
|
2637
2646
|
"name": "launcha Website",
|
|
2638
2647
|
"slug": "launcha-website",
|
|
2639
|
-
"header_settings": null
|
|
2640
|
-
"organisation_id": null
|
|
2648
|
+
"header_settings": null
|
|
2641
2649
|
},
|
|
2642
2650
|
"puckData": {
|
|
2643
2651
|
"root": {
|
|
@@ -2658,8 +2666,7 @@
|
|
|
2658
2666
|
},
|
|
2659
2667
|
"brand": {
|
|
2660
2668
|
"companyName": "GMG APAC",
|
|
2661
|
-
"primaryColor": "#FF4500"
|
|
2662
|
-
"companyFriendlyName": null
|
|
2669
|
+
"primaryColor": "#FF4500"
|
|
2663
2670
|
},
|
|
2664
2671
|
"website": {
|
|
2665
2672
|
"name": "launcha Website",
|
|
@@ -3319,7 +3326,12 @@
|
|
|
3319
3326
|
3021
|
|
3320
3327
|
],
|
|
3321
3328
|
"label": "an unparseable or out-of-range PORT never displaces a valid row (resolvedPort narrowing: 0)",
|
|
3322
|
-
"expect":
|
|
3329
|
+
"expect": {
|
|
3330
|
+
"member_equals": {
|
|
3331
|
+
"port": 3021,
|
|
3332
|
+
"source": "service_routing"
|
|
3333
|
+
}
|
|
3334
|
+
},
|
|
3323
3335
|
"export": "resolveServicePort",
|
|
3324
3336
|
"module": "src/resolve-service-port.ts",
|
|
3325
3337
|
"package": "@teamscala/service-boot",
|
|
@@ -3348,7 +3360,12 @@
|
|
|
3348
3360
|
3021
|
|
3349
3361
|
],
|
|
3350
3362
|
"label": "an unparseable or out-of-range PORT never displaces a valid row (resolvedPort narrowing: 70000)",
|
|
3351
|
-
"expect":
|
|
3363
|
+
"expect": {
|
|
3364
|
+
"member_equals": {
|
|
3365
|
+
"port": 3021,
|
|
3366
|
+
"source": "service_routing"
|
|
3367
|
+
}
|
|
3368
|
+
},
|
|
3352
3369
|
"export": "resolveServicePort",
|
|
3353
3370
|
"module": "src/resolve-service-port.ts",
|
|
3354
3371
|
"package": "@teamscala/service-boot",
|