Astro Info
Astro v7.3.1
Node v24.13.0
System Linux (x64)
Package Manager npm
Output static
Adapter none
Integrations none
Also verified against upstream main at 9870f9560 with the repository's frozen lockfile.
If this issue only occurs in one browser, which browser is a problem?
Not browser-specific; the reproduction invokes the server-side crawler directly.
Describe the Bug
crawlGraph() normalizes its input with unwrapId() and records that normalized ID in scanned, but checks recursive imports with scanned.has(importedModule.id) without normalizing them.
If a graph exposes both \0virtual:cycle and /@id/__x00__virtual:cycle, a wrapped import of an already visited module passes that check. The recursive call then unwraps the ID and traverses the original module again. A self-cycle can repeat indefinitely; a shared dependency can be yielded more than once.
Reproduction scope: the linked example calls the published Astro implementation with a minimal in-memory module graph. It is a graph-level reproduction, not a standalone Astro/Vue application that naturally generates this graph. Ordinary Vite virtual-module resolution can normalize IDs before they reach the crawler. A downstream Astro 7.2.0 application has carried this one-line patch for a reported dev-server stack overflow, but I could not reproduce that application's original crash in the current environment, so I am not claiming a confirmed Vue regression in 7.3.1.
Steps:
git clone --single-branch --branch codex/repro-crawlgraph-wrapped-ids https://github.com/rwv/astro.git astro-crawlgraph-repro
cd astro-crawlgraph-repro
npm ci
npm test
Actual result on unpatched Astro 7.3.1:
AssertionError [ERR_ASSERTION]: The crawler revisited "/@id/__x00__virtual:cycle" after visiting its unwrapped ID
false !== true
The script checks one generator iteration and closes it so the failure is deterministic and does not intentionally exhaust the stack.
Changing the lookup to scanned.has(unwrapId(importedModule.id)) makes the same reproduction pass. I have also prepared regression tests for wrapped/unwrapped cycles, shared imports, and preserving an unseen wrapped import while traversing its normalized module.
Source: packages/astro/src/vite-plugin-astro-server/vite.ts.
What's the expected result?
Use the same normalized ID representation when recording and checking visits, so already visited modules are skipped regardless of wrapping. An unseen wrapped import should still be yielded with its original ID.
Link to Minimal Reproducible Example
https://github.com/rwv/astro/tree/41ebe0e
Participation
Astro Info
Also verified against upstream
mainat9870f9560with the repository's frozen lockfile.If this issue only occurs in one browser, which browser is a problem?
Not browser-specific; the reproduction invokes the server-side crawler directly.
Describe the Bug
crawlGraph()normalizes its input withunwrapId()and records that normalized ID inscanned, but checks recursive imports withscanned.has(importedModule.id)without normalizing them.If a graph exposes both
\0virtual:cycleand/@id/__x00__virtual:cycle, a wrapped import of an already visited module passes that check. The recursive call then unwraps the ID and traverses the original module again. A self-cycle can repeat indefinitely; a shared dependency can be yielded more than once.Reproduction scope: the linked example calls the published Astro implementation with a minimal in-memory module graph. It is a graph-level reproduction, not a standalone Astro/Vue application that naturally generates this graph. Ordinary Vite virtual-module resolution can normalize IDs before they reach the crawler. A downstream Astro 7.2.0 application has carried this one-line patch for a reported dev-server stack overflow, but I could not reproduce that application's original crash in the current environment, so I am not claiming a confirmed Vue regression in 7.3.1.
Steps:
Actual result on unpatched Astro 7.3.1:
The script checks one generator iteration and closes it so the failure is deterministic and does not intentionally exhaust the stack.
Changing the lookup to
scanned.has(unwrapId(importedModule.id))makes the same reproduction pass. I have also prepared regression tests for wrapped/unwrapped cycles, shared imports, and preserving an unseen wrapped import while traversing its normalized module.Source:
packages/astro/src/vite-plugin-astro-server/vite.ts.What's the expected result?
Use the same normalized ID representation when recording and checking visits, so already visited modules are skipped regardless of wrapping. An unseen wrapped import should still be yielded with its original ID.
Link to Minimal Reproducible Example
https://github.com/rwv/astro/tree/41ebe0e
Participation