@teamscala/service-boot 0.23.443 → 0.23.444
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
|
@@ -51,7 +51,7 @@ describe("verifyClientBuildAssets", () => {
|
|
|
51
51
|
if (!result.ok) expect(result.reason).toContain("never ran or its output is gone");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
test("the first-boot tombstone shape (unhashed
|
|
54
|
+
test("the first-boot tombstone shape (unhashed paths, no files) refuses", async () => {
|
|
55
55
|
await writeManifest(join(dir, "dist", "client"), {
|
|
56
56
|
stylesPath: "/assets/styles.css",
|
|
57
57
|
hydratePath: "/assets/hydrate.js",
|
|
@@ -61,6 +61,18 @@ describe("verifyClientBuildAssets", () => {
|
|
|
61
61
|
if (!result.ok) expect(result.reason).toContain("unbuilt assets");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("a path-less tombstone (the shape build-client-assets now writes) refuses as unbuilt", async () => {
|
|
65
|
+
await writeManifest(join(dir, "dist", "client"), {
|
|
66
|
+
inputFingerprint: "failed:2026-09-01T00:00:00.000Z",
|
|
67
|
+
buildFailed: true,
|
|
68
|
+
buildFailedReason: "hydrate build failed: something",
|
|
69
|
+
buildFailedAt: "2026-09-01T00:00:00.000Z",
|
|
70
|
+
} as never);
|
|
71
|
+
const result = await verifyClientBuildAssets(dir);
|
|
72
|
+
expect(result.ok).toBe(false);
|
|
73
|
+
if (!result.ok) expect(result.reason).toContain("never ran or its output is gone");
|
|
74
|
+
});
|
|
75
|
+
|
|
64
76
|
test("a manifest naming a file that was deleted refuses (stale-bundle shape)", async () => {
|
|
65
77
|
await writeManifest(join(dir, "dist", "client"), {
|
|
66
78
|
stylesPath: "/assets/styles-abc.css",
|
|
@@ -21,8 +21,13 @@ import type { SourceMapUploadResult } from "./sourcemaps-upload.ts";
|
|
|
21
21
|
export const MANIFEST_FILE = "asset-manifest.json";
|
|
22
22
|
|
|
23
23
|
export interface AssetManifest {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Optional because a TOMBSTONE from a build that failed with no prior
|
|
25
|
+
// success carries NO paths — there are no real assets to name, and inventing
|
|
26
|
+
// URLs nothing built is the deleted fallback class (no-shims).
|
|
27
|
+
// `readManifest` returns null unless both are present, so a path-less
|
|
28
|
+
// tombstone reads as unbuilt everywhere (verification, readiness, render).
|
|
29
|
+
stylesPath?: string;
|
|
30
|
+
hydratePath?: string;
|
|
26
31
|
// Content fingerprint of the CLIENT-BUNDLE DEPENDENCY CLOSURE (source entrypoints + @teamscala/* JS/CSS in the closure + third-party closure-dep
|
|
27
32
|
// versions), hashed by CONTENT — never mtime (cache-key-hashes-source- inputs-only). The boot/build freshness check rebuilds iff this differs
|
|
28
33
|
// from the current closure. mtime comparison is fundamentally broken on Railway: deploy-build bakes the bundle at image-build time, and under the
|
|
@@ -53,15 +58,6 @@ export interface AssetManifest {
|
|
|
53
58
|
sourceMaps?: SourceMapUploadResult;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
// Last-resort paths for a tombstone when no prior successful manifest exists
|
|
57
|
-
// (first-ever boot fails). Points at the unhashed build names so an operator
|
|
58
|
-
// inspecting the manifest sees a real (if 404-ing) target rather than a
|
|
59
|
-
// mystery sentinel.
|
|
60
|
-
export const FALLBACK_MANIFEST: { stylesPath: string; hydratePath: string } = {
|
|
61
|
-
stylesPath: "/assets/styles.css",
|
|
62
|
-
hydratePath: "/assets/hydrate.js",
|
|
63
|
-
};
|
|
64
|
-
|
|
65
61
|
/**
|
|
66
62
|
* Read ONLY the tombstone state of the current manifest — the runtime
|
|
67
63
|
* (read-html-template, ui-service) calls this to decide whether to surface a
|
|
@@ -125,10 +121,9 @@ export async function writeManifest(
|
|
|
125
121
|
* Verify the CURRENT manifest against the files it names — the capability
|
|
126
122
|
* probe the readiness flip gates on (`a-component-may-not-report-a-state-it-
|
|
127
123
|
* has-not-verified`). A manifest is servable only when BOTH referenced assets
|
|
128
|
-
* exist on disk.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* must read as NOT built, never as a legitimate bundle. Measured 2026-09-01:
|
|
124
|
+
* exist on disk. An absent or unreadable manifest, a first-boot tombstone
|
|
125
|
+
* carrying no paths, and any manifest naming files that do not exist must all
|
|
126
|
+
* read as NOT built, never as a legitimate bundle. Measured 2026-09-01:
|
|
132
127
|
* proposals.scala.business served a client-facing proposal unstyled while
|
|
133
128
|
* Railway reported the deploy SUCCESS, because "client build never ran" was
|
|
134
129
|
* indistinguishable from "client build fine" at every layer that reports.
|
|
@@ -138,7 +133,7 @@ export async function verifyClientBuildAssets(
|
|
|
138
133
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
139
134
|
const outDir = join(cwd, "dist", "client");
|
|
140
135
|
const manifest = await readManifest(outDir);
|
|
141
|
-
if (!manifest) {
|
|
136
|
+
if (!manifest || !manifest.stylesPath || !manifest.hydratePath) {
|
|
142
137
|
return {
|
|
143
138
|
ok: false,
|
|
144
139
|
reason: `no readable asset manifest at ${join(outDir, MANIFEST_FILE)} — the client build never ran or its output is gone`,
|
|
@@ -60,7 +60,6 @@ import { CLIENT_BUILD_LOCK, getClientBuildLockTimeoutMs } from "./build-client-a
|
|
|
60
60
|
import { buildHydrateBundle } from "./build-client-assets/hydrate-bundle.ts";
|
|
61
61
|
import { computeInputFingerprint } from "./build-client-assets/input-fingerprint.ts";
|
|
62
62
|
import {
|
|
63
|
-
FALLBACK_MANIFEST,
|
|
64
63
|
readManifest,
|
|
65
64
|
writeManifest,
|
|
66
65
|
} from "./build-client-assets/manifest.ts";
|
|
@@ -85,7 +84,9 @@ export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
|
85
84
|
// permanently lost the release's maps — client stacks stayed minified
|
|
86
85
|
// until the next real input change forced a rebuild.
|
|
87
86
|
const recorded = existingManifest.sourceMaps?.status;
|
|
88
|
-
const release =
|
|
87
|
+
const release = existingManifest.hydratePath
|
|
88
|
+
? releaseFromHydratePath(existingManifest.hydratePath)
|
|
89
|
+
: null;
|
|
89
90
|
if (recorded !== "uploaded" && release) {
|
|
90
91
|
log.info(
|
|
91
92
|
`[build-client-assets:${serviceSlug}] manifest up-to-date (skip); source maps previously ${recorded ?? "unrecorded"} — retrying upload for release ${release}`,
|
|
@@ -143,10 +144,19 @@ async function performBuild(
|
|
|
143
144
|
try {
|
|
144
145
|
await mkdir(outDir, { recursive: true });
|
|
145
146
|
await writeManifest(outDir, {
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
// NO invented paths: when a prior successful build exists its real
|
|
148
|
+
// paths are preserved (SSR keeps serving the prior bundle — degraded
|
|
149
|
+
// but coherent); when none exists the tombstone carries NO asset
|
|
150
|
+
// paths at all. Writing the unhashed build names here was the last
|
|
151
|
+
// remnant of the deleted fallback class — paths that name files
|
|
152
|
+
// nothing built must not exist on disk any more than in served HTML.
|
|
153
|
+
// A path-less tombstone reads as unbuilt everywhere (readManifest →
|
|
154
|
+
// null → verification refuses → readiness never flips), and its
|
|
155
|
+
// buildFailedReason below is the operator's diagnostic — the failure
|
|
156
|
+
// is read via readClientBuildStatus, which never needed the paths.
|
|
157
|
+
...(existingManifest?.stylesPath && existingManifest?.hydratePath
|
|
158
|
+
? { stylesPath: existingManifest.stylesPath, hydratePath: existingManifest.hydratePath }
|
|
159
|
+
: {}),
|
|
150
160
|
// Failure sentinel: the next boot's fingerprint check reads
|
|
151
161
|
// this and cannot match any real fingerprint → forces a retry.
|
|
152
162
|
inputFingerprint: `failed:${new Date().toISOString()}`,
|