Skip to content

fix(asset): resolve dynamic new URL paths without a leading ./ (fix #10032) - #23433

Open
kakiuwang-ui wants to merge 1 commit into
vitejs:mainfrom
kakiuwang-ui:fix/asset-import-meta-url-bare-relative
Open

fix(asset): resolve dynamic new URL paths without a leading ./ (fix #10032)#23433
kakiuwang-ui wants to merge 1 commit into
vitejs:mainfrom
kakiuwang-ui:fix/asset-import-meta-url-bare-relative

Conversation

@kakiuwang-ui

Copy link
Copy Markdown
Contributor

Fixes #10032.

new URL('pets/cat.txt', import.meta.url) and new URL('./pets/cat.txt', import.meta.url) resolve to the same file, because the first argument of new URL() is always resolved against the base URL — it is never a bare specifier. The template literal form does not agree: new URL(`pets/${name}.txt`, import.meta.url) is turned into import.meta.glob("pets/*.txt", ...), and toAbsoluteGlob rejects a pattern that starts with neither / nor ./.

The symptom has changed since the issue was filed, and both modes are now worse than the build error that was reported:

  • vite dev throws Invalid glob: "pets/*.txt", so the module 500s.
  • vite build only warns and emits the untransformed call into the bundle: new URL(import.meta.glob(`pets/*.txt`, {...})[`pets/${e}.txt`], import.meta.url). import.meta.glob does not exist at runtime, so the chunk throws a TypeError in the browser. Reverting this patch and running playground/assets shows the blast radius: 144 failures in serve mode and 5 in build mode, most of them neighbours taken down by the one broken module.

Implementation

assetImportMetaUrl now prefixes ./ to both the glob pattern and the runtime index key (they have to stay in sync, or the lookup returns undefined silently).

The prefix is only added when nothing else can claim the specifier, so that the pattern resolves in the same order as the static branch below it (alias, then relative, then bare):

  • an alias wins over a sibling directory of the same name. With alias: { pets: './other/pets' } and a real src/pets/, the static form resolves to other/pets because @rollup/plugin-alias runs ahead of the oxc resolver and preferRelative only applies after it. An unconditional prefix would make the dynamic form pick src/pets instead — trading one inconsistency for another. The check is an aliasOnly resolve, and it only runs when the sibling directory actually exists, so it is not on the path of every dynamic new URL.
  • #-prefixed patterns are left alone; toAbsoluteGlob has explicit subpath-imports handling for them.
  • a non-absolute importer (virtual module) has no directory to be relative to, so it is skipped rather than being resolved against process.cwd().
  • a bare package specifier with no directory of that name next to the importer keeps its current behaviour, i.e. the Invalid glob error.

assetResolver's lazy initialisation is pulled into getAssetResolver() so the two call sites share it.

Notes for review

  • globBaseExists uses statSync, which is case-insensitive on macOS and Windows, while the glob runs with caseSensitiveMatch: true. new URL(`FOO/${n}.js`, import.meta.url) next to a foo/ directory is therefore rewritten and then matches nothing, where it used to raise Invalid glob. I left it that way on purpose: `./FOO/${n}.js` already fails silently today, and making only the un-prefixed form loud would recreate the inconsistency this PR removes. Happy to add a case-exact check if you would rather have the error back.
  • The transform result now depends on filesystem state at transform time, and the probed directory is not watched. A directory created after the first transform will not re-trigger it on its own. The static branch below has the same property (tryFsResolve plus a warnOnce fallback), so I did not add a watch for it here.
  • Adjacent bug I did not touch: an aliased dynamic path resolves the glob but not the index key, so new URL(`@/${n}.png`, import.meta.url) produces keys like /src/foo.png while the lookup uses `@/${n}.png` and evaluates to undefined. That is pre-existing and unchanged here; happy to open a separate issue or PR.
  • The import.meta.glob call leaking into the production bundle instead of failing comes from builtin:vite-import-glob warning where the JS plugin throws. This PR removes the case that reaches it, but the leak itself is still there for alias and bare patterns.

Test

packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts covers the rewrite plus each of the four cases that must not be rewritten. The transform helper now takes an importer, because the check needs a real directory; the new cases point at fixtures/asset-import-meta-url/.

I checked the tests discriminate rather than just pass, by breaking the implementation one guard at a time and confirming each failure lands on the intended assertion: dropping the existence check fails the bare-specifier case, prefixing the pattern without the index key fails the relative case, and dropping the #, path.isAbsolute and alias guards each fail their own case.

playground/assets gets a dynamic new URL without a leading ./, which fails in serve, build and bundled-dev without this patch.

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.

vite build rejects new URL(url, import.meta.url) with template strings that don't begin with / or ./

1 participant