vitejs / vite

Next generation frontend tooling. It's fast!
http://vite.dev
MIT License
68.43k stars 6.17k forks source link

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

Open castholm opened 2 years ago

castholm commented 2 years ago

Describe the bug

When running vite dev and visiting the site, all of the following methods of resolving assets work:

// 1: Static, without leading './'
const static1res = await fetch(new URL("pets/cat.txt", import.meta.url))

// 2: Static, with leading './'
const static2res = await fetch(new URL("./pets/cat.txt", import.meta.url))

// 3: Dynamic, without leading './'
function fetchDynamic1(species: string): Promise<Response> {
  return fetch(new URL(`pets/${species}.txt`, import.meta.url))
}
const dynamic1res = await fetchDynamic1("cat")

// 4: Dynamic, with leading './'
function fetchDynamic2(species: string): Promise<Response> {
  return fetch(new URL(`./pets/${species}.txt`, import.meta.url))
}
const dynamic2res = await fetchDynamic2("cat")

However, when running vite build, the third case (new URL(`pets/${species}.txt`, import.meta.url)) causes the build to fail with the message

Invalid glob: "pets/**.txt" (resolved: "pets/**.txt"). It must start with '/' or './'

It seems a bit inconsistent that static relative URLs without a leading ./ work but not dynamic ones. Ideally, all four should work.

I understand that the JavaScript spec require relative module imports to begin with a leading ./, but this is just a URL to a generic asset. path/to/asset and ./path/to/asset should be equivalent.

Reproduction

https://stackblitz.com/edit/vitejs-vite-9ymymw

System Info

System:
    OS: Linux 5.0 undefined
    CPU: (4) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 7.17.0 - /usr/local/bin/npm
  npmPackages:
    vite: ^3.1.0 => 3.1.0

Used Package Manager

npm

Logs

Click to expand! ```shell ❯ npm run build -- --debug $ tsc && vite build --debug vite:config no config file found. +0ms vite:esbuild init tsconfck (root: /home/projects/vitejs-vite-9ymymw) +0ms vite:esbuild init tsconfck (root: /home/projects/vitejs-vite-9ymymw) +1ms vite:esbuild init tsconfck (root: /home/projects/vitejs-vite-9ymymw) +0ms vite:esbuild init tsconfck (root: /home/projects/vitejs-vite-9ymymw) +0ms vite:esbuild init tsconfck end +1ms vite:esbuild init tsconfck end +0ms vite:esbuild init tsconfck end +0ms vite:esbuild init tsconfck end +0ms vite:config using resolved config: { vite:config root: '/home/projects/vitejs-vite-9ymymw', vite:config base: '/', vite:config mode: 'production', vite:config configFile: undefined, vite:config logLevel: undefined, vite:config clearScreen: undefined, vite:config optimizeDeps: { vite:config disabled: 'build', vite:config force: undefined, vite:config esbuildOptions: { preserveSymlinks: undefined } vite:config }, vite:config build: { vite:config target: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari13' ], vite:config polyfillModulePreload: true, vite:config outDir: 'dist', vite:config assetsDir: 'assets', vite:config assetsInlineLimit: 4096, vite:config cssCodeSplit: true, vite:config cssTarget: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari13' ], vite:config sourcemap: false, vite:config rollupOptions: {}, vite:config minify: 'esbuild', vite:config terserOptions: {}, vite:config write: true, vite:config emptyOutDir: null, vite:config manifest: false, vite:config lib: false, vite:config ssr: false, vite:config ssrManifest: false, vite:config reportCompressedSize: true, vite:config chunkSizeWarningLimit: 500, vite:config watch: null, vite:config commonjsOptions: { include: [Array], extensions: [Array] }, vite:config dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] } vite:config }, vite:config configFileDependencies: [], vite:config inlineConfig: { vite:config root: undefined, vite:config base: undefined, vite:config mode: undefined, vite:config configFile: undefined, vite:config logLevel: undefined, vite:config clearScreen: undefined, vite:config optimizeDeps: { force: undefined }, vite:config build: {} vite:config }, vite:config resolve: { alias: [ [Object], [Object] ] }, vite:config publicDir: '/home/projects/vitejs-vite-9ymymw/public', vite:config cacheDir: '/home/projects/vitejs-vite-9ymymw/node_modules/.vite', vite:config command: 'build', vite:config ssr: { vite:config format: 'esm', vite:config target: 'node', vite:config optimizeDeps: { disabled: true, esbuildOptions: [Object] } vite:config }, vite:config isWorker: false, vite:config mainConfig: null, vite:config isProduction: true, vite:config plugins: [ vite:config 'vite:build-metadata', vite:config 'vite:pre-alias', vite:config 'alias', vite:config 'vite:modulepreload-polyfill', vite:config 'vite:resolve', vite:config 'vite:html-inline-proxy', vite:config 'vite:css', vite:config 'vite:esbuild', vite:config 'vite:json', vite:config 'vite:wasm-helper', vite:config 'vite:worker', vite:config 'vite:asset', vite:config 'vite:wasm-fallback', vite:config 'vite:define', vite:config 'vite:css-post', vite:config 'vite:build-html', vite:config 'vite:worker-import-meta-url', vite:config 'vite:force-systemjs-wrap-complete', vite:config 'vite:watch-package-data', vite:config 'commonjs', vite:config 'vite:data-uri', vite:config 'vite:asset-import-meta-url', vite:config 'vite:dynamic-import-vars', vite:config 'vite:import-glob', vite:config 'vite:build-import-analysis', vite:config 'vite:esbuild-transpile', vite:config 'vite:terser', vite:config 'vite:reporter', vite:config 'vite:load-fallback' vite:config ], vite:config server: { vite:config preTransformRequests: true, vite:config middlewareMode: false, vite:config fs: { strict: true, allow: [Array], deny: [Array] } vite:config }, vite:config preview: { vite:config port: undefined, vite:config strictPort: undefined, vite:config host: undefined, vite:config https: undefined, vite:config open: undefined, vite:config proxy: undefined, vite:config cors: undefined, vite:config headers: undefined vite:config }, vite:config env: { BASE_URL: '/', MODE: 'production', DEV: false, PROD: true }, vite:config assetsInclude: [Function: assetsInclude], vite:config logger: { vite:config hasWarned: false, vite:config info: [Function: info], vite:config warn: [Function: warn], vite:config warnOnce: [Function: warnOnce], vite:config error: [Function: error], vite:config clearScreen: [Function: clearScreen], vite:config hasErrorLogged: [Function: hasErrorLogged] vite:config }, vite:config packageCache: Map(0) { set: [Function (anonymous)] }, vite:config createResolver: [Function: createResolver], vite:config worker: { vite:config format: 'iife', vite:config plugins: [ vite:config 'vite:build-metadata', vite:config 'vite:pre-alias', vite:config 'alias', vite:config 'vite:modulepreload-polyfill', vite:config 'vite:resolve', vite:config 'vite:html-inline-proxy', vite:config 'vite:css', vite:config 'vite:esbuild', vite:config 'vite:json', vite:config 'vite:wasm-helper', vite:config 'vite:worker', vite:config 'vite:asset', vite:config 'vite:wasm-fallback', vite:config 'vite:define', vite:config 'vite:css-post', vite:config 'vite:build-html', vite:config 'vite:worker-import-meta-url', vite:config 'vite:force-systemjs-wrap-complete', vite:config 'vite:watch-package-data', vite:config 'commonjs', vite:config 'vite:data-uri', vite:config 'vite:asset-import-meta-url', vite:config 'vite:dynamic-import-vars', vite:config 'vite:import-glob', vite:config 'vite:build-import-analysis', vite:config 'vite:esbuild-transpile', vite:config 'vite:terser', vite:config 'vite:reporter', vite:config 'vite:load-fallback' vite:config ], vite:config rollupOptions: {}, vite:config getSortedPlugins: [Function: getSortedPlugins], vite:config getSortedPluginHooks: [Function: getSortedPluginHooks] vite:config }, vite:config appType: 'spa', vite:config experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false }, vite:config getSortedPlugins: [Function: getSortedPlugins], vite:config getSortedPluginHooks: [Function: getSortedPluginHooks] vite:config } +6ms vite v3.1.0 building for production... ✓ 2 modules transformed. [vite:import-glob] Invalid glob: "pets/**.txt" (resolved: "pets/**.txt"). It must start with '/' or './' file: /home/projects/vitejs-vite-9ymymw/src/main.ts error during build: Error: Invalid glob: "pets/**.txt" (resolved: "pets/**.txt"). It must start with '/' or './' at toAbsoluteGlob (file:///home/projects/vitejs-vite-9ymymw/node_modules/vite/dist/node/chunks/dep-665b0112.js:36015:11) at async Promise.all (index 0) at async eval (file:///home/projects/vitejs-vite-9ymymw/node_modules/vite/dist/node/chunks/dep-665b0112.js:35848:31) at async Promise.all (index 0) at async parseImportGlob (file:///home/projects/vitejs-vite-9ymymw/node_modules/vite/dist/node/chunks/dep-665b0112.js:35862:13) at async transformGlobImport (file:///home/projects/vitejs-vite-9ymymw/node_modules/vite/dist/node/chunks/dep-665b0112.js:35874:21) at async Object.transform (file:///home/projects/vitejs-vite-9ymymw/node_modules/vite/dist/node/chunks/dep-665b0112.js:35680:28) at async transform (file:///home/projects/vitejs-vite-9ymymw/node_modules/rollup/dist/es/shared/rollup.js:21958:16) at async ModuleLoader.addModuleSource (file:///home/projects/vitejs-vite-9ymymw/node_modules/rollup/dist/es/shared/rollup.js:22183:30) ```

Validations

castholm commented 2 years ago

Locally, I have used patch-package to apply a patch that would be the equivalent of making the following changes to assetImportMetaUrl.ts:

@@ -44,7 +44,14 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {

           // potential dynamic template string
           if (rawUrl[0] === '`' && /\$\{/.test(rawUrl)) {
-            const ast = this.parse(rawUrl)
+            let fixedUrl
+            if (!/^`(\.?\.?\/|\${)/.test(rawUrl)) {
+              // Normalize relative URLs to begin with './' to prevent issues later down the plugin pipeline.
+              fixedUrl = '`./' + rawUrl.slice(1)
+            } else {
+              fixedUrl = rawUrl
+            }
+            const ast = this.parse(fixedUrl)
             const templateLiteral = (ast as any).body[0].expression
             if (templateLiteral.expressions.length) {
               const pattern = JSON.stringify(buildGlobPattern(templateLiteral))
@@ -55,7 +62,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
               s.overwrite(
                 index,
                 index + exp.length,
-                `new URL((import.meta.glob(${pattern}, { eager: true, import: 'default', as: 'url' }))[${rawUrl}], self.location)`,
+                `new URL((import.meta.glob(${pattern}, { eager: true, import: 'default', as: 'url' }))[${fixedUrl}], self.location)`,
                 { contentOnly: true }
               )
               continue

If you believe this would be okay as a fix, let me know and I'll open a PR.

poyoho commented 2 years ago

It seems resolve by #7837, the PR will call the resolve plugin to resolve url. So the url can resolve as bare package imports or absolute fs paths.

castholm commented 2 years ago

Does #7837 handle template strings? I will have to build and test it myself later to confirm, but it doesn't seem like that PR changes how template strings are handled.