@teamscala/service-boot 0.23.440 → 0.23.441
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
CHANGED
|
@@ -73,4 +73,24 @@ describe("readHtmlTemplate manifest-keyed cache", () => {
|
|
|
73
73
|
expect(html).toContain("/assets/hydrate.js");
|
|
74
74
|
expect(html).toContain("/assets/styles.css");
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
// The fallback is correct as a LAST RESORT and dangerous as a SILENT one:
|
|
78
|
+
// the unhashed names it emits were never built, so the page 200s while its
|
|
79
|
+
// stylesheet 404s. Measured 2026-09-01 — a client-facing proposal served
|
|
80
|
+
// unstyled while the deploy reported SUCCESS, because nothing anywhere in
|
|
81
|
+
// the stack distinguished "client build never ran" from "client build fine".
|
|
82
|
+
// The stamp is what makes the two distinguishable to an operator, a probe,
|
|
83
|
+
// or a deploy gate, so it is asserted rather than left to inspection.
|
|
84
|
+
test("stamps the page when no manifest was built, so a dead deploy is visible", async () => {
|
|
85
|
+
const html = await readHtmlTemplate();
|
|
86
|
+
expect(html).toContain('name="x-client-build-status"');
|
|
87
|
+
expect(html).toContain('content="missing-manifest"');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("does not stamp missing-manifest once a real manifest exists", async () => {
|
|
91
|
+
await writeManifest("/assets/hydrate-abc.js", "/assets/styles-abc.css");
|
|
92
|
+
const html = await readHtmlTemplate();
|
|
93
|
+
expect(html).toContain("/assets/hydrate-abc.js");
|
|
94
|
+
expect(html).not.toContain('content="missing-manifest"');
|
|
95
|
+
});
|
|
76
96
|
});
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
43
|
+
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
43
44
|
import { readClientBuildStatus } from "./build-client-assets/manifest.ts";
|
|
44
45
|
import { join } from "node:path";
|
|
45
46
|
|
|
@@ -93,7 +94,7 @@ async function manifestMtimeMs(): Promise<number> {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
async function loadManifest(): Promise<AssetManifest> {
|
|
97
|
+
async function loadManifest(): Promise<AssetManifest | null> {
|
|
97
98
|
const path = manifestPath();
|
|
98
99
|
if (await Bun.file(path).exists()) {
|
|
99
100
|
try {
|
|
@@ -101,10 +102,21 @@ async function loadManifest(): Promise<AssetManifest> {
|
|
|
101
102
|
if (parsed.stylesPath && parsed.hydratePath) {
|
|
102
103
|
return { stylesPath: parsed.stylesPath, hydratePath: parsed.hydratePath };
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
getAppLogger().warn(
|
|
106
|
+
`[read-html-template] asset manifest at ${path} is missing stylesPath/hydratePath — serving unbuilt fallback URLs`,
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
getAppLogger().warn(
|
|
110
|
+
`[read-html-template] asset manifest at ${path} is unreadable — serving unbuilt fallback URLs`,
|
|
111
|
+
error,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
106
115
|
}
|
|
107
|
-
|
|
116
|
+
getAppLogger().warn(
|
|
117
|
+
`[read-html-template] no asset manifest at ${path} — the client build did not run for this deployment; serving unbuilt fallback URLs that will 404`,
|
|
118
|
+
);
|
|
119
|
+
return null;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
export async function readHtmlTemplate(): Promise<string> {
|
|
@@ -113,8 +125,24 @@ export async function readHtmlTemplate(): Promise<string> {
|
|
|
113
125
|
if (cached && cached.manifestMtime === manifestMtime) return cached.html;
|
|
114
126
|
const manifest = await loadManifest();
|
|
115
127
|
let html = PAGE_DOCUMENT
|
|
116
|
-
.replace(/\{\{stylesPath\}\}/g, manifest.stylesPath)
|
|
117
|
-
.replace(/\{\{hydratePath\}\}/g, manifest.hydratePath);
|
|
128
|
+
.replace(/\{\{stylesPath\}\}/g, (manifest ?? FALLBACK).stylesPath)
|
|
129
|
+
.replace(/\{\{hydratePath\}\}/g, (manifest ?? FALLBACK).hydratePath);
|
|
130
|
+
// An ABSENT manifest is a failed client build wearing a working service's
|
|
131
|
+
// clothes: boot succeeds, SSR renders, every response is 200, and the only
|
|
132
|
+
// symptom is a stylesheet URL that was never built. Measured 2026-09-01 —
|
|
133
|
+
// proposals.scala.business served a client-facing proposal unstyled while
|
|
134
|
+
// Railway reported the deploy SUCCESS, because the fallback silently
|
|
135
|
+
// substitutes unhashed names for assets no build produced. The tombstone
|
|
136
|
+
// below already exists for a build that FAILED loudly; a build that never
|
|
137
|
+
// ran must be equally visible, or a broken deployment is indistinguishable
|
|
138
|
+
// from a working one at every layer that reports on it
|
|
139
|
+
// (`a-component-may-not-report-a-state-it-has-not-verified`).
|
|
140
|
+
if (!manifest) {
|
|
141
|
+
const meta = `<meta name="x-client-build-status" content="missing-manifest" data-reason="no dist/client/asset-manifest.json; asset URLs below are unbuilt and will 404">`;
|
|
142
|
+
html = html.includes("</head>")
|
|
143
|
+
? html.replace("</head>", `${meta}</head>`)
|
|
144
|
+
: `${meta}${html}`;
|
|
145
|
+
}
|
|
118
146
|
// Tombstone stamp — surfaces a failed client build at the served layer.
|
|
119
147
|
// `<meta>` in <head> is the minimum-invasive vehicle (no layout shift, no
|
|
120
148
|
// JS dependency, visible via view-source / devtools / a future status
|