Skip to content

fix(build): support fetched import-meta assets - #2884

Open
james-elicx wants to merge 3 commits into
mainfrom
codex/fix-edge-blob-assets-31439707085
Open

fix(build): support fetched import-meta assets#2884
james-elicx wants to merge 3 commits into
mainfrom
codex/fix-edge-blob-assets-31439707085

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • inline assets referenced by new URL(..., import.meta.url) when they are fetched by server code
  • preserve normal file-URL behavior for Node and default Nitro output while rewriting nodeless Worker targets
  • register embedded files as Vite watch dependencies so asset-only edits invalidate the owning module
  • cover plain Node, default Nitro, Nitro Cloudflare, Vite dev invalidation, and real Workerd execution

Failing job

This addresses the three local-asset failures in run 31439707085, job 93624401572:

  • allows to fetch text assets
  • allows to fetch image assets
  • allows to assets from node_modules

The two remote-URL controls in that suite already passed and remain unchanged.

Validation

  • targeted Next.js E2E: test/e2e/edge-compiler-can-import-blob-assets/index.test.ts — 5/5 passed, retry 0/0
  • focused asset transform tests — 14/14 passed
  • production/dev/Nitro integration tests — 5/5 passed
  • default app-router Nitro Node build and runtime passed, including fileURLToPath() and .pathname preservation
  • Nitro cloudflare-module build/runtime passed with constructor-wide asset rewriting
  • real Wrangler/Workerd regression passed: /api/blob-asset returned 200 with the expected bytes
  • two independent review passes completed; final pass reported no findings

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2884
npm i https://pkg.pr.new/create-vinext-app@2884
npm i https://pkg.pr.new/@vinext/types@2884
npm i https://pkg.pr.new/vinext@2884

commit: 821e639

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 821e639 against base d558d62 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 135.2 KB 135.2 KB ⚫ -0.0%
Client entry size (gzip) vinext 122.7 KB 122.7 KB ⚫ -0.0%
Dev server cold start vinext 3.03 s 3.03 s ⚫ -0.2%
Production build time vinext 3.15 s 3.18 s ⚫ +0.9%
RSC entry closure size (gzip) vinext 114.2 KB 114.2 KB ⚫ -0.0%
Server bundle size (gzip) vinext 193.0 KB 193.0 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2884 (fix(build): support fetched import-meta assets)

Reviewed the new vinext:asset-import-meta-url plugin, its wiring in index.ts, and all four test surfaces (unit transform, prod integration, dev invalidation, Nitro Node/Cloudflare presets, plus the Workerd E2E spec). Ran the targeted suites locally:

  • tests/asset-import-meta-url.test.ts — 14/14 pass
  • tests/edge-blob-assets.test.ts — 5/5 pass (incl. Node preset + cloudflare-module preset)
  • vp check on the new source + test files — clean (format, lint, types)

Overall: solid. The design is coherent and the coverage is unusually thorough for this class of change. No correctness bugs found. A few observations below, none blocking.

Strengths

  • Correct dev/prod/Nitro parity. The second commit's refinement of isWorkerTarget from hasNitroPluginhasNodelessNitroTarget (nitro.options.node === false) is the right call: it preserves real file: URL semantics (.pathname, fileURLToPath()) for plain Node and default Nitro output, and only does the constructor-wide data-URL rewrite for nodeless targets where import.meta.url isn't a usable base. The Nitro Node-preset test asserts protocol: "file:" and a real filePath, which locks this in.
  • Proper scope analysis. Reusing ast-scope/ast-utils (functions, blocks, loops, switch, catch, static blocks) and the URL/fetch shadowing guards is much more robust than a regex. The const-only binding restriction for the bound-fetch path is a sound safety choice (a reassignable let/var couldn't be trusted).
  • Watch registration. this.addWatchFile(file) for the embedded (non-graph) asset correctly closes the dev-invalidation gap, and the dev test exercises it via a real watcher.emit("change", ...).
  • Containment guard. Routing relative and bare specifiers through OgAssetOwnership.resolveContainedAsset (with the "does not read outside the owning project" test) gives good defense-in-depth against path traversal.
  • Plugin ordering. Keeping this after createOgInlineFetchAssetsPlugin is correct — the OG plugin's narrower fetch(...).then(arrayBuffer()) IIFE shape is preserved, and this plugin handles the plain fetch(new URL(...)) remainder. They're complementary, not overlapping.

Non-blocking observations

  1. isWorkerTarget timing depends on Nitro's setup running before transform. hasNodelessNitroTarget is assigned inside the Nitro setup hook and read lazily at transform time. The cloudflare-module test confirms the ordering holds for production builds, so this is fine in practice — but it is an implicit lifecycle dependency. Worth a one-line comment at the closure (isWorkerTarget: () => ...) noting that hasNodelessNitroTarget is populated by the Nitro setup hook, so the two must not be reordered.

  2. addWatchFile runs before the read can fail. this.addWatchFile(file) (line 337) is called before fs.promises.readFile (line 343). If the read throws we continue, but the file is already watched. Harmless (watching a transiently-unreadable path is a no-op consequence), just slightly out of order — could move it after a successful read if you want strictness.

  3. Node-mode let/var-bound fetch inputs are intentionally not rewritten. Only const bindings feed the bound-fetch path, so let url = new URL(...); fetch(url) stays a runtime file: fetch (which fails on Node's fetch). This is stricter than necessary but a defensible safety choice and still strictly more capable than the prior OG-only handling. Flagging only so it's a conscious decision — no change requested.

Nice work — the parity testing across Node/Nitro/Workerd is exactly the bar this kind of build transform should clear.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2884.

Summary

I reviewed PR #2884 (fix(build): support fetched import-meta assets) — the new vinext:asset-import-meta-url plugin plus its wiring and tests. I ran the targeted suites locally:

  • tests/asset-import-meta-url.test.ts — 14/14 pass
  • tests/edge-blob-assets.test.ts — 5/5 pass (including Node preset and cloudflare-module preset)
  • vp check on new source + test files — clean

Verdict: solid, no blocking issues, no correctness bugs found.

Key positives:

  • Correct dev/prod/Nitro parity — the hasNitroPluginhasNodelessNitroTarget refinement preserves real file: URL semantics for Node/default Nitro and only rewrites constructors to data URLs for nodeless targets.
  • Proper AST scope analysis (reusing ast-scope/ast-utils) with URL/fetch shadowing guards, rather than a fragile regex.
  • Dev invalidation closed via addWatchFile on the embedded non-graph asset, exercised by a real watcher-change test.
  • Path-traversal containment via resolveContainedAsset.
  • Correct plugin ordering relative to the OG inline plugin (complementary, not overlapping).

Three non-blocking observations flagged in the review:

  1. isWorkerTarget has an implicit dependency on Nitro's setup hook running before transform — worth a one-line comment (validated by tests, so fine in practice).
  2. addWatchFile runs before the read that can fail (harmless, slightly out of order).
  3. Node-mode let/var-bound fetch inputs are intentionally not rewritten (a deliberate safety choice, just flagging it as conscious).

github run

@james-elicx

Copy link
Copy Markdown
Member Author

CI status note: App Router shard 3/3 has been rerun three times and each run hard-failed only the out-of-scope use cache directive-metadata HMR test; the other 189 tests passed, with redirect-once classified flaky and passing on retry. The exact cache HMR test passes locally in isolation (1/1, 13.1s).

The product-specific gates for this PR are clean: targeted Next.js parity 5/5, focused Node/Nitro/Worker/watcher coverage, real Wrangler/Workerd execution, two independent reviews, and Big Bonk with no blocking findings. Keeping the PR draft because the required CI shard is still red; no claim of fully green CI.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

Independent exact-head re-review of 821e639d5413bbaa2ce0acd1bd6da52400144feb: NO FINDINGS.

Reviewed the committed shared OgAssetOwnership lifecycle across plugin ordering, per-environment build recreation, dev state, standalone factory fallback, and the removal of the duplicate resolver hot path. The change preserves behavior while leaving only the existing OG resolver to configure/reset/populate the shared tracker.

Independent validation:

  • clean worktree and git diff --check
  • ownership/asset unit tests: 24/24
  • tests/edge-blob-assets.test.ts: 5/5
  • exact targeted Next.js test/e2e/edge-compiler-can-import-blob-assets/index.test.ts: 5/5 with concurrency 1

Fresh CI and paired performance results were still running when this review was posted.

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2884 (fix(build): support fetched import-meta assets)

Reviewed the new vinext:asset-import-meta-url plugin, the index.ts wiring (shared OgAssetOwnership, hasNodelessNitroTarget), the og-assets.ts ownership-injection change, and all four test surfaces. Ran the targeted suites locally on the head branch:

  • tests/asset-import-meta-url.test.ts — 15/15 pass
  • tests/edge-blob-assets.test.ts — 5/5 pass (Node preset + cloudflare-module preset, real prod server)

Verdict: solid, no blocking correctness issues found. My independent pass reached the same conclusion as the prior automated review; I confirmed a few things it did not spell out.

Confirmed correctness properties

  • No double-inlining vs the OG plugin. The OG transform runs first and rewrites its narrow fetch(new URL(...)).then(r => r.arrayBuffer()) shape into an IIFE that no longer contains new URL(...), so this plugin cannot re-match it. Where the OG regex fails to match (reformatted source), this plugin still rewrites the inner new URL to a data URL in worker mode — a strict improvement, not a conflict.
  • No overlapping MagicString overwrites. Worker mode returns early after the first NewExpression match (no child recursion), and the Node bound-fetch path rewrites the reference identifier (range = input) while leaving the const declaration intact, so sibling/duplicate fetches map to disjoint ranges.
  • Shared-ownership wiring is single-pass. With a shared ownership, managesOwnership is false, so the new plugin drops its configResolved/buildStart/resolveId hooks and defers configure/reset/import-recording to the OG plugin (asserted by the "shares ownership tracking" test: configure/reset called once, plugin.resolveId === undefined).
  • Lifecycle deps hold. hasCloudflarePlugin is set in configResolved and hasNodelessNitroTarget in the Nitro setup hook — both before build transform runs — so the lazy isWorkerTarget() closure reads populated values.
  • Guards verified: literal-only first arg, protocol/// rejection, URL/fetch shadowing (both worker and node), @vite-ignore, and resolveContainedAsset path-traversal containment.

Non-blocking observations

  1. Worker-mode constructor rewrite changes .pathname/fileURLToPath() semantics. In a nodeless target, every new URL("./x", import.meta.url) becomes a data: URL, even one used only for .pathname or fileURLToPath() rather than fetch. This is documented as intentional (worker import.meta.url is not a usable base, so the original would throw anyway) and matches the Node-mode restriction to the fetch path. Flagging only so the divergence stays a conscious decision — no change requested.

  2. Data-URL media type contains a space (data:text/plain; charset=utf-8;base64,...) because contentTypeForPath(".txt") is "text/plain; charset=utf-8". It is not strictly RFC 2397 clean, but workerd and Node fetch both parse it (the real-Workerd E2E returns 200 with the expected bytes), so it is fine in practice.

  3. addWatchFile before the read that can fail (line 351 vs 357) — harmless, already noted upstream.

  4. Node-mode let/var-bound fetch inputs are intentionally not rewritten — deliberate safety choice for reassignable bindings; still strictly more capable than the prior OG-only handling.

Nice work — the Node/default-Nitro/nodeless-Nitro/Workerd/dev-watcher parity coverage is exactly the bar this class of build transform should clear.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2884.

Summary

I reviewed PR #2884 (fix(build): support fetched import-meta assets) — the new vinext:asset-import-meta-url plugin, the index.ts wiring, the og-assets.ts shared-ownership change, and all four test surfaces. Ran the targeted suites locally on the head branch:

  • tests/asset-import-meta-url.test.ts — 15/15 pass
  • tests/edge-blob-assets.test.ts — 5/5 pass (Node + cloudflare-module presets, real prod server)

Verdict: solid, no blocking correctness bugs.

Beyond the prior automated review, I independently verified:

  • No double-inlining vs the OG plugin — the OG transform runs first and removes new URL(...) from its matched shape; this plugin acts as a fallback where OG's regex misses.
  • No overlapping MagicString overwrites — worker mode returns early without recursing; Node mode rewrites the reference identifier while leaving the declaration intact, so ranges stay disjoint.
  • Single-pass shared ownershipmanagesOwnership: false correctly drops the duplicate configResolved/buildStart/resolveId hooks.
  • Lifecycle deps holdhasCloudflarePlugin (configResolved) and hasNodelessNitroTarget (Nitro setup) are both set before build transform.

Four non-blocking observations flagged: worker-mode .pathname/fileURLToPath semantics change (intentional), the space in the data: media type (works in practice, real-Workerd E2E green), addWatchFile ordering, and the intentional const-only bound-fetch restriction. No changes requested.

github run

@james-elicx
james-elicx marked this pull request as ready for review August 11, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant