Skip to content

Commit f6f8b4d

Browse files
committed
fix(web): address strict build review comments
1 parent 2cf2a43 commit f6f8b4d

2 files changed

Lines changed: 177 additions & 13 deletions

File tree

packages/app/vite.web.config.ts

Lines changed: 166 additions & 8 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 Plugin, type PluginOption, type UserConfig } 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)
@@ -166,7 +166,32 @@ const terminalWebSocketProxyPlugin = (apiTarget: string): PluginOption => ({
166166
})
167167

168168
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>
169172

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
170195
const removeDeprecatedOptimizeDepsOptions = (
171196
optimizeDeps: UserConfig["optimizeDeps"]
172197
): UserConfig["optimizeDeps"] => {
@@ -178,6 +203,28 @@ const removeDeprecatedOptimizeDepsOptions = (
178203
return remainingOptions
179204
}
180205

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
181228
const removeDeprecatedGridlandOptions = (
182229
config: VitePluginConfig | null | void
183230
): VitePluginConfig | null | void => {
@@ -195,23 +242,134 @@ const removeDeprecatedGridlandOptions = (
195242
}
196243
}
197244

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
198267
const isVitePlugin = (plugin: PluginOption): plugin is Plugin =>
199268
typeof plugin === "object" && plugin !== null && !Array.isArray(plugin) && "name" in plugin
200269

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
201345
const gridlandWebPluginWithoutDeprecatedOptions = (): ReadonlyArray<PluginOption> =>
202346
gridlandWebPlugin().map((plugin) => {
203-
if (!isVitePlugin(plugin) || plugin.name !== "gridland-web-aliases" || typeof plugin.config !== "function") {
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)) {
204362
return plugin
205363
}
206364

207-
const resolveGridlandConfig = plugin.config
365+
const resolveGridlandConfig = gridlandConfig.handler
208366
return {
209367
...plugin,
210-
config(config, env) {
211-
const result = resolveGridlandConfig.call(this, config, env)
212-
return result instanceof Promise
213-
? result.then(removeDeprecatedGridlandOptions)
214-
: removeDeprecatedGridlandOptions(result)
368+
config: {
369+
...gridlandConfig,
370+
handler(config, env) {
371+
return sanitizeGridlandConfigResult(resolveGridlandConfig.call(this, config, env))
372+
}
215373
}
216374
}
217375
})

scripts/ci/check-web-build-output.mjs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const forbiddenOutput = [
1414
},
1515
{
1616
label: "Deprecated build option warning",
17-
pattern: /\bdeprecated\b/iu
17+
pattern: /(?:\[vite\]\s+warning:[^\n]*\bdeprecated\b|\(!\)[^\n]*\bdeprecated\b)/iu
1818
},
1919
{
2020
label: "Chunk size warning",
@@ -27,19 +27,25 @@ const result = spawnSync(runtime, ["run", "--cwd", "packages/app", "build:web"],
2727
encoding: "utf8"
2828
})
2929

30-
process.stdout.write(result.stdout)
31-
process.stderr.write(result.stderr)
32-
3330
if (result.error !== undefined) {
3431
console.error(result.error)
3532
process.exit(1)
3633
}
3734

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+
3844
if (result.status !== 0) {
3945
process.exit(result.status ?? 1)
4046
}
4147

42-
const output = `${result.stdout}\n${result.stderr}`
48+
const output = `${stdout}\n${stderr}`
4349
const matches = forbiddenOutput.filter(({ pattern }) => pattern.test(output))
4450
if (matches.length > 0) {
4551
console.error("Web build emitted forbidden warning output:")

0 commit comments

Comments
 (0)