ITADN

incrementalBuild: Fonts API embeds a random ephemeral port in generated code, making dependencyHash non-deterministic and blocking all caching

#17626Openchrispecoraro 创建于 23 天前
pkg: astro- P3: minor bugtriage: fix verified
C
chrispecorarocommented
### Astro Info ```block Astro v7.2.0 Node v22.21.1 System Linux (x64) Package Manager pnpm Output static Adapter none Integrations @astrojs/tailwind (v6.0.2) @astrojs/react (v6.0.2) @astrojs/svelte @astrojs/sitemap (v3.7.3) astro-robots-txt (v1.0.0) ``` ### Describe the Bug With `experimental.incrementalBuild: true`, `build.concurrency: 1`, and the Fonts API (`fonts: [...]` using `fontProviders.fontsource()` / `fontProviders.google()`) all enabled together, **no page is ever skipped on a rebuild**, even with zero source changes between builds. Root cause: the generated `virtual:astro:assets/fonts/runtime/font-file-url-resolver` module embeds a live, per-build ephemeral socket address/port into its compiled code, which feeds into the incremental build's per-route `dependencyHash` and makes it non-deterministic build over build. Since every page in a typical site imports a shared layout that renders `<Font />`, this one module poisons `dependencyHash` for effectively every route, defeating the cache site-wide. **How I found it:** I temporarily instrumented `hashModules()` in `node_modules/astro/dist/core/build/plugins/plugin-incremental.js` to log each transitively-included module's id plus a hash of `graph.getModuleInfo(id)?.code`, across two consecutive builds with identical source. Every module hashed identically **except one**: `virtual:astro:assets/fonts/runtime/font-file-url-resolver`. Diffing that module's actual generated code between the two builds: ```diff - address: {"address":"::","family":"IPv6","port":34261}, + address: {"address":"::","family":"IPv6","port":41219}, ``` That's a literal ephemeral socket port baked into the built module's source text — almost certainly from whatever local server/connection the font-fetching pipeline opens during the build (e.g. to fetch/proxy font files from the configured provider). Since this value is different every process invocation by definition, any route that transitively imports this module can never produce a stable `dependencyHash`. **Confirmation — removing the Fonts API alone fixes it.** To isolate the variable, I removed the `fonts` config and all `<Font />` usage from the same site (~4,122 pages, otherwise unchanged) and re-ran the same test: | | Run 1 (cold) | Run 2 (warm, nothing cleaned) | Run 3 (warm again) | |---|---|---|---| | With Fonts API (`incrementalBuild: true`) | 38.9s | 33.0s — **0 / 4,122 cached** | 36.5s — 0 / 4,122 | | Without Fonts API (`incrementalBuild: true`) | 37.6s | **14.9s — 4,117 / 4,122 cached** | **21.4s — 4,117 / 4,122 cached** | | Without Fonts API (`incrementalBuild: false`, control) | 31.0s | 30.8s (no change, as expected) | — | With the Fonts API out of the module graph, `incrementalBuild` performs exactly as documented — roughly a 60% reduction on the warm build. The control (`incrementalBuild: false` on the same fonts-free codebase) shows no such improvement, ruling out warm OS/network caches as the explanation. **Impact:** any project using the Fonts API from a shared layout — the common case — cannot benefit from `incrementalBuild` at all, silently. There's no warning; the feature just does nothing while still paying its bookkeeping overhead (computing/writing `incremental-build.json`, hashing every module graph on every build). **Related, smaller footgun found in the same investigation** (happy to file separately if preferred): custom content loaders implementing `Loader.load({ store, parseData, generateDigest, ... })` get zero incremental-build benefit if they call `store.set({ id, data })` without an explicit `digest` — `entry.digest` stays `undefined`, and any downstream code doing `cacheKey: String(entry.digest)` silently gets the literal string `"undefined"` for every entry rather than an error or a type warning. Nothing signals that `digest` was expected; a JSDoc note on `LoaderContext.generateDigest`, or a dev-mode warning when `load()` never invokes it, would help. ### What's the expected result? On the second build, pages whose `cacheKey` and dependency graph are unchanged should be skipped and logged with `(cached)` or `(restored)` (per `generatePathWithPrerenderer` in `node_modules/astro/dist/core/build/generate.js`). One of: - The font-file-url-resolver module doesn't embed ephemeral connection info in its generated code (resolve it at runtime instead of baking it into the built module's source text). - If the port is genuinely only needed in dev, it's excluded from output that participates in `hashModules()` for production/prerender builds. - At minimum, a warning is surfaced when `incrementalBuild` is enabled alongside config known to produce non-deterministic build output, instead of silently caching nothing. ### Link to Minimal Reproducible Example I don't have a hosted StackBlitz/GitHub minimal reproduction yet -- this was found investigating a real production site. Steps to reproduce from a fresh project: 1. `astro.config.mjs`: ```js export default defineConfig({ experimental: { incrementalBuild: true }, fonts: [ { provider: fontProviders.fontsource(), name: "SomeFont", cssVariable: "--font-some" }, ], build: { concurrency: 1 }, }); ``` 2. Render `<Font cssVariable="--font-some" />` from a shared layout used by all pages, and return a stable, non-empty `cacheKey` from `getStaticPaths()` for at least one route (e.g. a content digest). 3. Run `astro build` twice in a row, touching nothing between runs (leave `dist/` and `node_modules/.astro/` alone). 4. Inspect `node_modules/.astro/incremental-build.json` after each run -- `routes["<route file>"].dependencyHash` differs between the two. I can build and host a proper minimal reproduction if that's needed to move this forward -- let me know. ### Participation - [ ] I am willing to submit a pull request for this issue.
3 条评论