Skip to content

Commit a82e183

Browse files
authored
fix(web): silence Vite web build warnings (#356)
* fix(web): silence vite web build warnings * fix(web): address strict build review comments
1 parent 1e19bc5 commit a82e183

4 files changed

Lines changed: 275 additions & 11 deletions

File tree

.github/workflows/final-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: bun ./packages/docker-git-session-sync/dist/docker-git-session-sync.js --help
3333
- name: Verify browser UI and menu clone smoke checks
3434
run: |
35-
bun run --cwd packages/app build:web
35+
bun run --cwd packages/app build:web:strict
3636
bun scripts/final-build/browser-web-smoke.mjs
3737
bun run --cwd packages/app vitest run tests/docker-git/browser-frontend.test.ts tests/docker-git/app-ready-create.test.ts tests/docker-git/actions-project-create.test.ts
3838
- name: Prepare package artifacts directory

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"build": "bun run build:app && bun run build:docker-git",
1818
"build:app": "vite build --ssr src/app/main.ts",
1919
"build:web": "vite build --config vite.web.config.ts",
20+
"build:web:strict": "bun ../../scripts/ci/check-web-build-output.mjs",
2021
"prepack": "bun run build:docker-git",
2122
"dev": "vite build --watch --ssr src/app/main.ts",
2223
"dev:web": "vite --config vite.web.config.ts",

packages/app/vite.web.config.ts

Lines changed: 217 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"
55

66
import { gridlandWebPlugin } from "@gridland/web/vite-plugin"
77
import react from "@vitejs/plugin-react"
8-
import { defineConfig, loadEnv, type PluginOption } from "vite"
8+
import { defineConfig, loadEnv, type HookHandler, type Plugin, type PluginOption, type UserConfig } from "vite"
99
import { type RawData, WebSocket, WebSocketServer } from "ws"
1010

1111
const __filename = fileURLToPath(import.meta.url)
@@ -165,14 +165,223 @@ const terminalWebSocketProxyPlugin = (apiTarget: string): PluginOption => ({
165165
}
166166
})
167167

168+
type VitePluginConfig = Omit<UserConfig, "plugins">
169+
type ViteConfigHook = HookHandler<NonNullable<Plugin["config"]>>
170+
type ViteConfigObjectHook = Exclude<NonNullable<Plugin["config"]>, ViteConfigHook>
171+
type ViteConfigHookResult = ReturnType<ViteConfigHook>
172+
173+
/**
174+
* Removes the deprecated dependency optimizer esbuild bridge from optional Vite optimizeDeps config.
175+
*
176+
* @param optimizeDeps - Optional Vite dependency optimizer config emitted by a plugin.
177+
* @returns `undefined` when no config exists; otherwise a shallow copy without `esbuildOptions`.
178+
* @pure true
179+
* @precondition `optimizeDeps` is either undefined or a Vite optimizeDeps object.
180+
* @postcondition The result is undefined iff the input is undefined; otherwise `esbuildOptions` is absent.
181+
* @invariant Every non-`esbuildOptions` own field is preserved by key and value.
182+
* @complexity O(k) time / O(k) space, where k is the number of own optimizeDeps fields.
183+
* @throws Never.
184+
*/
185+
// CHANGE: Strip only the deprecated optimizeDeps.esbuildOptions field.
186+
// WHY: Vite 8 warns on that bridge while all other optimizer settings remain valid input.
187+
// QUOTE(ТЗ): "Что бы оно не писалось"
188+
// REF: PR #356 review 4388758572
189+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
190+
// FORMAT THEOREM: ∀o ∈ OptimizeDeps: strip(o) = o \ {esbuildOptions}
191+
// PURITY: CORE
192+
// EFFECT: none
193+
// INVARIANT: ∀key ≠ esbuildOptions: result[key] = optimizeDeps[key]
194+
// COMPLEXITY: O(k) time / O(k) space
195+
const removeDeprecatedOptimizeDepsOptions = (
196+
optimizeDeps: UserConfig["optimizeDeps"]
197+
): UserConfig["optimizeDeps"] => {
198+
if (optimizeDeps === undefined) {
199+
return undefined
200+
}
201+
202+
const { esbuildOptions: _esbuildOptions, ...remainingOptions } = optimizeDeps
203+
return remainingOptions
204+
}
205+
206+
/**
207+
* Removes deprecated top-level and nested Gridland Vite config options from an optional plugin config.
208+
*
209+
* @param config - Optional Vite config fragment returned by the Gridland aliases plugin.
210+
* @returns The original nullish value, or a shallow config copy without deprecated esbuild fields.
211+
* @pure true
212+
* @precondition `config` is nullish or a Vite plugin config fragment without a `plugins` field.
213+
* @postcondition Returned object has no top-level `esbuild`; nested `optimizeDeps.esbuildOptions` is absent.
214+
* @invariant All non-deprecated config fields are preserved by key and value.
215+
* @complexity O(k + n) time / O(k + n) space, where k is config fields and n is optimizeDeps fields.
216+
* @throws Never.
217+
*/
218+
// CHANGE: Normalize Gridland config fragments before Vite consumes them.
219+
// WHY: The deprecated fields are warning-only compatibility options, not required for web build semantics.
220+
// QUOTE(ТЗ): "Что бы оно не писалось"
221+
// REF: PR #356 review 4388758572
222+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
223+
// FORMAT THEOREM: ∀c ∈ Config: normalize(c).esbuild = undefined ∧ normalize(c).optimizeDeps.esbuildOptions = undefined
224+
// PURITY: CORE
225+
// EFFECT: none
226+
// INVARIANT: ∀key ∉ {esbuild,optimizeDeps.esbuildOptions}: normalize(c)[key] = c[key]
227+
// COMPLEXITY: O(k + n) time / O(k + n) space
228+
const removeDeprecatedGridlandOptions = (
229+
config: VitePluginConfig | null | void
230+
): VitePluginConfig | null | void => {
231+
if (config === undefined || config === null) {
232+
return config
233+
}
234+
235+
const { esbuild: _esbuild, optimizeDeps, ...remainingConfig } = config
236+
const sanitizedOptimizeDeps = removeDeprecatedOptimizeDepsOptions(optimizeDeps)
237+
return sanitizedOptimizeDeps === undefined
238+
? remainingConfig
239+
: {
240+
...remainingConfig,
241+
optimizeDeps: sanitizedOptimizeDeps
242+
}
243+
}
244+
245+
/**
246+
* Tests whether a Vite plugin option is a concrete plugin object with a name.
247+
*
248+
* @param plugin - Vite plugin option produced by a plugin factory.
249+
* @returns True when the option is an object plugin; false for arrays, null, booleans, and functions.
250+
* @pure true
251+
* @precondition `plugin` is any value accepted by Vite as PluginOption.
252+
* @postcondition A true result narrows `plugin` to `Plugin` for property-safe access.
253+
* @invariant The predicate has no side effects and does not mutate the inspected value.
254+
* @complexity O(1) time / O(1) space.
255+
* @throws Never.
256+
*/
257+
// CHANGE: Provide a pure predicate for concrete Vite plugin objects.
258+
// WHY: The wrapper must only inspect named object plugins and preserve every other plugin option shape.
259+
// QUOTE(ТЗ): "Что бы оно не писалось"
260+
// REF: PR #356 review 4388758572
261+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
262+
// FORMAT THEOREM: ∀p ∈ PluginOption: isVitePlugin(p) → "name" ∈ keys(p)
263+
// PURITY: CORE
264+
// EFFECT: none
265+
// INVARIANT: isVitePlugin(p) is a deterministic boolean predicate over p's runtime shape.
266+
// COMPLEXITY: O(1) time / O(1) space
267+
const isVitePlugin = (plugin: PluginOption): plugin is Plugin =>
268+
typeof plugin === "object" && plugin !== null && !Array.isArray(plugin) && "name" in plugin
269+
270+
/**
271+
* Tests whether a Vite config hook is declared in object-hook form.
272+
*
273+
* @param hook - Concrete Vite config hook from a plugin.
274+
* @returns True when the hook has a callable `handler` property.
275+
* @pure true
276+
* @precondition `hook` is a non-null Vite config hook.
277+
* @postcondition A true result narrows `hook` to object-hook form.
278+
* @invariant The predicate does not call or mutate the hook.
279+
* @complexity O(1) time / O(1) space.
280+
* @throws Never.
281+
*/
282+
// CHANGE: Recognize Vite object-hook config declarations.
283+
// WHY: Sanitization should be stable if Gridland changes from function hook to object hook.
284+
// QUOTE(ТЗ): "Что бы оно не писалось"
285+
// REF: PR #356 review 4388758572
286+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
287+
// FORMAT THEOREM: ∀h ∈ ConfigHook: isObjectHook(h) → callable(h.handler)
288+
// PURITY: CORE
289+
// EFFECT: none
290+
// INVARIANT: isViteConfigObjectHook(h) is deterministic over h's runtime shape.
291+
// COMPLEXITY: O(1) time / O(1) space
292+
const isViteConfigObjectHook = (
293+
hook: NonNullable<Plugin["config"]>
294+
): hook is ViteConfigObjectHook =>
295+
typeof hook === "object" && hook !== null && "handler" in hook && typeof hook.handler === "function"
296+
297+
/**
298+
* Sanitizes either synchronous or asynchronous Gridland config hook output.
299+
*
300+
* @param result - Result returned by the original Gridland aliases config hook.
301+
* @returns The same sync/async shape with deprecated options removed from the resolved config.
302+
* @pure true
303+
* @precondition `result` is a valid Vite config hook result.
304+
* @postcondition Nullish results remain nullish; config objects are normalized after resolution.
305+
* @invariant Promise shape is preserved: Promise input yields Promise output; sync input yields sync output.
306+
* @complexity O(k + n) time / O(k + n) space after the hook result resolves.
307+
* @throws Never.
308+
*/
309+
// CHANGE: Centralize sync and async Gridland config result normalization.
310+
// WHY: Function and object Vite hooks must share the same warning-suppression invariant.
311+
// QUOTE(ТЗ): "Что бы оно не писалось"
312+
// REF: PR #356 review 4388758572
313+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
314+
// FORMAT THEOREM: ∀r ∈ HookResult: sanitize(r) resolves to normalize(r)
315+
// PURITY: CORE
316+
// EFFECT: none
317+
// INVARIANT: Sync/async result shape is preserved while resolved config is normalized.
318+
// COMPLEXITY: O(k + n) time / O(k + n) space
319+
const sanitizeGridlandConfigResult = (result: ViteConfigHookResult): ViteConfigHookResult =>
320+
result instanceof Promise
321+
? result.then(removeDeprecatedGridlandOptions)
322+
: removeDeprecatedGridlandOptions(result)
323+
324+
/**
325+
* Produces Gridland web plugins with the aliases config hook wrapped to suppress deprecated Vite output.
326+
*
327+
* @returns Plugin options from `gridlandWebPlugin` with only `gridland-web-aliases` config sanitized.
328+
* @pure true
329+
* @precondition `gridlandWebPlugin` returns Vite plugin options.
330+
* @postcondition Non-object plugins and non-target plugins are preserved; target config output is normalized.
331+
* @invariant Plugin order and non-target plugin identity are preserved.
332+
* @complexity O(p) time / O(p) space, where p is the number of Gridland plugin options.
333+
* @throws Never.
334+
*/
335+
// CHANGE: Wrap only the Gridland aliases plugin config hook.
336+
// WHY: The build warning source is localized to that plugin; unrelated plugins must retain their behavior.
337+
// QUOTE(ТЗ): "Что бы оно не писалось"
338+
// REF: PR #356 review 4388758572
339+
// SOURCE: https://github.com/ProverCoderAI/docker-git/pull/356#pullrequestreview-4388758572
340+
// FORMAT THEOREM: ∀p ≠ aliases: wrap(p) = p; aliases config output is normalized.
341+
// PURITY: CORE
342+
// EFFECT: none
343+
// INVARIANT: Plugin array length and order are unchanged.
344+
// COMPLEXITY: O(p) time / O(p) space
345+
const gridlandWebPluginWithoutDeprecatedOptions = (): ReadonlyArray<PluginOption> =>
346+
gridlandWebPlugin().map((plugin) => {
347+
if (!isVitePlugin(plugin) || plugin.name !== "gridland-web-aliases" || plugin.config === undefined) {
348+
return plugin
349+
}
350+
351+
const gridlandConfig = plugin.config
352+
if (typeof gridlandConfig === "function") {
353+
return {
354+
...plugin,
355+
config(config, env) {
356+
return sanitizeGridlandConfigResult(gridlandConfig.call(this, config, env))
357+
}
358+
}
359+
}
360+
361+
if (!isViteConfigObjectHook(gridlandConfig)) {
362+
return plugin
363+
}
364+
365+
const resolveGridlandConfig = gridlandConfig.handler
366+
return {
367+
...plugin,
368+
config: {
369+
...gridlandConfig,
370+
handler(config, env) {
371+
return sanitizeGridlandConfigResult(resolveGridlandConfig.call(this, config, env))
372+
}
373+
}
374+
}
375+
})
376+
168377
export default defineConfig(({ mode }) => {
169378
const env = loadEnv(mode, __dirname, "")
170379
const apiTarget = env["DOCKER_GIT_API_URL"]?.trim() || defaultApiTarget
171380

172381
return {
173382
plugins: [
174383
terminalWebSocketProxyPlugin(apiTarget),
175-
...gridlandWebPlugin(),
384+
...gridlandWebPluginWithoutDeprecatedOptions(),
176385
react()
177386
],
178387
publicDir: false,
@@ -211,14 +420,12 @@ export default defineConfig(({ mode }) => {
211420
build: {
212421
target: "esnext",
213422
outDir: "dist-web",
214-
sourcemap: true
215-
},
216-
esbuild: {
217-
target: "esnext"
218-
},
219-
optimizeDeps: {
220-
esbuildOptions: {
221-
target: "esnext"
423+
sourcemap: true,
424+
chunkSizeWarningLimit: 1200,
425+
rolldownOptions: {
426+
checks: {
427+
invalidAnnotation: false
428+
}
222429
}
223430
}
224431
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { spawnSync } from "node:child_process"
2+
import { fileURLToPath } from "node:url"
3+
4+
const repoRoot = fileURLToPath(new URL("../..", import.meta.url))
5+
const runtime = process.versions.bun === undefined ? "bun" : process.execPath
6+
const forbiddenOutput = [
7+
{
8+
label: "Vite warning",
9+
pattern: /\[vite\]\s+warning:/iu
10+
},
11+
{
12+
label: "Rolldown invalid annotation warning",
13+
pattern: /\[INVALID_ANNOTATION\]/u
14+
},
15+
{
16+
label: "Deprecated build option warning",
17+
pattern: /(?:\[vite\]\s+warning:[^\n]*\bdeprecated\b|\(!\)[^\n]*\bdeprecated\b)/iu
18+
},
19+
{
20+
label: "Chunk size warning",
21+
pattern: /Some chunks are larger than/u
22+
}
23+
]
24+
25+
const result = spawnSync(runtime, ["run", "--cwd", "packages/app", "build:web"], {
26+
cwd: repoRoot,
27+
encoding: "utf8"
28+
})
29+
30+
if (result.error !== undefined) {
31+
console.error(result.error)
32+
process.exit(1)
33+
}
34+
35+
const stdout = result.stdout ?? ""
36+
const stderr = result.stderr ?? ""
37+
if (stdout.length > 0) {
38+
process.stdout.write(stdout)
39+
}
40+
if (stderr.length > 0) {
41+
process.stderr.write(stderr)
42+
}
43+
44+
if (result.status !== 0) {
45+
process.exit(result.status ?? 1)
46+
}
47+
48+
const output = `${stdout}\n${stderr}`
49+
const matches = forbiddenOutput.filter(({ pattern }) => pattern.test(output))
50+
if (matches.length > 0) {
51+
console.error("Web build emitted forbidden warning output:")
52+
for (const match of matches) {
53+
console.error(`- ${match.label}`)
54+
}
55+
process.exit(1)
56+
}

0 commit comments

Comments
 (0)