Skip to content

crawlGraph revisits wrapped module IDs after visiting their unwrapped IDs #17934

Description

@rwv

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

  • I am willing to submit a pull request for this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)pkg: astroRelated to the core `astro` package (scope)triage: fix pendingReporter needs to verify the triage bot fix works

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions