Repository navigation
Expand file tree
/
Copy pathvite.config.ts
More file actions
137 lines (133 loc) · 6.06 KB
/
Copy pathvite.config.ts
File metadata and controls
137 lines (133 loc) · 6.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// Bare "child_process", not "node:child_process" — see resolveBuildCommit below
// for why this is not Bun.spawnSync. The lint rule bans only the node:-prefixed
// specifier, so this form needs no disable directive.
import { execSync } from "child_process";
import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
/**
* The commit this BUNDLE was built from (mt#3241).
*
* The web bundle and the cockpit daemon are versioned INDEPENDENTLY: the tray's
* web watcher (mt#2297) rebuilds `dist/` on change without restarting the
* daemon, so what a reader is looking at can be many commits newer than the
* process serving it. `/api/health`'s `commit` names the DAEMON's provenance and
* cannot answer for the bundle, so the bundle carries its own.
*
* Resolved here, at build time, because that is the only moment the answer
* exists — the running bundle has no access to git. Falls back to `"unknown"`
* when git is unavailable or the tree is not a repo, mirroring `getGitCommit` in
* `src/cockpit/routes/health.ts`: a Docker build or a non-git checkout must
* degrade, never fail the build.
*
* **Why `execSync` and not `Bun.spawnSync`, despite `bun_over_node.mdc`.** `Bun`
* is not defined when vite evaluates this config — measured: the Bun form threw,
* the `catch` swallowed it, and every build silently baked in `"unknown"` while
* still exiting 0 with clean lint. Matches `src/cockpit/routes/health.ts:22`.
*
* If a future change makes `Bun` available here, switching back is fine — but
* verify by grepping `dist/assets/*.js` for the actual sha. A green build proves
* nothing about this line, because the fallback is silent by design.
*
* `stdio: "pipe"` keeps `fatal: not a git repository` out of the build output.
*/
function resolveBuildCommit(): string {
try {
const sha = String(
execSync("git rev-parse --short HEAD", { encoding: "utf-8", stdio: "pipe" })
).trim();
return sha.length > 0 ? sha : "unknown";
} catch {
return "unknown";
}
}
/**
* The bundle's identity, resolved ONCE per build (mt#5034).
*
* Both consumers below read this same constant: the `__BUILD_COMMIT__` define
* that `RailFooter` renders, and the `build-info.json` sidecar that
* `/api/health` reports. Calling `resolveBuildCommit()` twice would let the
* footer and the health payload disagree if HEAD moved mid-build — a drift with
* no symptom until someone compares them.
*/
const BUILD_COMMIT = resolveBuildCommit();
/**
* Emit `build-info.json` beside the bundle so the SERVER can read the bundle's
* identity (mt#5034).
*
* **Why a sidecar at all.** `__BUILD_COMMIT__` is a compile-time text
* substitution into the emitted JS, so it exists only inside the bundle. The
* cockpit daemon serves that bundle statically and cannot read a value baked
* into it — which is why `/api/health` could report the DAEMON's commit while
* serving a much newer bundle, and a shipped web change read as undeployed. The
* tray's web watcher (mt#2297) rebuilds `dist/` WITHOUT restarting the daemon,
* so the two identities genuinely diverge by design.
*
* **Why `generateBundle` + `this.emitFile`, not `closeBundle` + `fs.write`.**
* Rollup documents `this.emitFile` inside `generateBundle` as the mechanism for
* emitting an additional file, and describes `closeBundle` as a cleanup hook
* whose invocation "is the responsibility of users of the JavaScript API to
* manually call `bundle.close()`". A `closeBundle` that does not fire would
* leave a STALE `build-info.json` — the same stale-identity defect this exists
* to remove, in a form that is harder to catch, because the field would be
* present and look authoritative rather than obviously naming the wrong commit.
*
* **`now` is a FUNCTION, called at emit time — not a timestamp captured at
* construction (PR #3697 R1).** A plugin factory runs once when vite evaluates
* this config; `generateBundle` runs once per build. Defaulting a `nowMs: number`
* parameter would freeze `builtAt` at config-evaluation time, so under a resident
* build process (`vite build --watch`, or the JS API driving repeated builds)
* every rebuild would stamp the SAME timestamp — a bundle whose recorded build
* time never advances, which is precisely the stale-identity class this file
* exists to remove. Today's caller is safe by accident rather than by design: the
* tray spawns a fresh `bun run cockpit:build` per rebuild
* (`watcher_web.rs:193-200`), so the config is re-evaluated each time. Not worth
* depending on.
*
* @param now Injected clock (`testing-standards.mdc`); real default, called per emit.
*/
function emitBuildInfo(now: () => number = Date.now): Plugin {
return {
name: "minsky-cockpit-build-info",
generateBundle() {
this.emitFile({
type: "asset",
fileName: "build-info.json",
source: `${JSON.stringify(
{ commit: BUILD_COMMIT, builtAt: new Date(now()).toISOString() },
null,
2
)}\n`,
});
},
};
}
export default defineConfig({
root: "src/cockpit/web",
plugins: [react(), emitBuildInfo()],
define: {
__BUILD_COMMIT__: JSON.stringify(BUILD_COMMIT),
},
build: {
outDir: "dist",
emptyOutDir: true,
// Long-cached vendor chunks. Page chunks are produced automatically from
// React.lazy() dynamic imports in App.tsx; this manualChunks map only
// governs the shared vendor split. Keep this list tight — over-splitting
// adds HTTP request overhead without proportional cache benefit.
rollupOptions: {
output: {
manualChunks: {
"react-vendor": ["react", "react-dom"],
router: ["react-router-dom"],
tanstack: ["@tanstack/react-query"],
icons: ["lucide-react"],
// Markdown rendering (mt#2550): react-markdown + remark/rehype/unified
// toolchain. Isolated so the ~60-80KB gz pipeline is a long-cached chunk
// loaded only on pages that render prose.
markdown: ["react-markdown", "remark-gfm"],
},
},
},
},
server: { proxy: { "/api": "http://localhost:3737" } },
});