diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 1648fad4d45334..28256d395d8d2a 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -127,7 +127,7 @@ function preload( Promise.resolve() // @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later if (__VITE_IS_MODERN__ && deps && deps.length > 0) { - const links = document.getElementsByTagName('link') + let preloadedHrefs: { all: Set; styles: Set } | undefined const cspNonceMeta = document.querySelector( 'meta[property=csp-nonce]', ) @@ -170,20 +170,29 @@ function preload( seen[dep.href] = true const isCss = isCssPreloadUrl(dep) - // check if the file is already preloaded by SSR markup - // `dep` is already converted to an absolute URL by the `assetsURL` function - for (let i = links.length - 1; i >= 0; i--) { - const link = links[i] - // The `links[i].href` is an absolute URL thanks to browser doing the work - // for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5 - if ( - link.href === dep.href && - (!isCss || link.rel === 'stylesheet') - ) { - return + if (preloadedHrefs === undefined) { + preloadedHrefs = { all: new Set(), styles: new Set() } + const links = document.getElementsByTagName('link') + for (let i = links.length - 1; i >= 0; i--) { + const link = links[i] + // The `links[i].href` is an absolute URL thanks to browser doing the work + // for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5 + preloadedHrefs.all.add(link.href) + if (link.rel === 'stylesheet') { + preloadedHrefs.styles.add(link.href) + } } } + // check if the file is already preloaded by SSR markup + // `importMetaResolve` converts `dep` to an absolute URL + const preloadedHrefSet = isCss + ? preloadedHrefs.styles + : preloadedHrefs.all + if (preloadedHrefSet.has(dep.href)) { + return + } + const link = document.createElement('link') link.rel = isCss ? 'stylesheet' : scriptRel if (!isCss) { diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts index 87de1450b31187..c37c022c16e464 100644 --- a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts +++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts @@ -361,7 +361,7 @@ describe.runIf(isBuild)('build tests', () => { SourceMap { content: { "debugId": "00000000-0000-0000-0000-000000000000", - "mappings": ";mvCAAAA,MAAA,OAAO,qDAEP,QAAQ,IAAI,uBAAuB", + "mappings": ";w1CAAAA,MAAA,OAAO,qDAEP,QAAQ,IAAI,uBAAuB", "names": [ "__vitePreload", ], @@ -376,7 +376,7 @@ describe.runIf(isBuild)('build tests', () => { ], "version": 3, }, - visualization: "https://evanw.github.io/source-map-visualization/#MTYzOABjb25zdCBfX3ZpdGVfX21hcERlcHM9KGksbT1fX3ZpdGVfX21hcERlcHMsZD0obS5mfHwobS5mPVsiYXNzZXRzL2R5bmFtaWMtZm9vLUJ3aFpUa3RCLmpzIiwiYXNzZXRzL2R5bmFtaWMtZm9vLURzcUtSckV5LmNzcyJdKSkpPT5pLm1hcChpPT5kW2ldKTsKdmFyIGU9ZnVuY3Rpb24oZSl7cmV0dXJuYC9gK2V9LHQ9e30sbj1mdW5jdGlvbihlKXtyZXR1cm4gZS5wYXRobmFtZS5lbmRzV2l0aChgLmNzc2ApfSxyPWZ1bmN0aW9uKHIsaSxhKXtsZXQgbz1Qcm9taXNlLnJlc29sdmUoKTtpZihpJiZpLmxlbmd0aD4wKXtsZXQgcj1kb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZShgbGlua2ApLHM9ZG9jdW1lbnQucXVlcnlTZWxlY3RvcihgbWV0YVtwcm9wZXJ0eT1jc3Atbm9uY2VdYCksYz1zPy5ub25jZXx8cz8uZ2V0QXR0cmlidXRlKGBub25jZWApO2Z1bmN0aW9uIGwoZSl7cmV0dXJuIFByb21pc2UuYWxsKGUubWFwKGU9PlByb21pc2UucmVzb2x2ZShlKS50aGVuKGU9Pih7c3RhdHVzOmBmdWxmaWxsZWRgLHZhbHVlOmV9KSxlPT4oe3N0YXR1czpgcmVqZWN0ZWRgLHJlYXNvbjplfSkpKSl9ZnVuY3Rpb24gdShlKXtyZXR1cm4gaW1wb3J0Lm1ldGEucmVzb2x2ZT9uZXcgVVJMKGltcG9ydC5tZXRhLnJlc29sdmUoZSkpOm5ldyBVUkwoZSxpbXBvcnQubWV0YS51cmwpfW89bChpLm1hcChpPT57aT1lKGksYSk7bGV0IG89dShpKTtpZihvLmhyZWYgaW4gdClyZXR1cm47dFtvLmhyZWZdPSEwO2xldCBzPW4obyk7Zm9yKGxldCBlPXIubGVuZ3RoLTE7ZT49MDtlLS0pe2xldCB0PXJbZV07aWYodC5ocmVmPT09by5ocmVmJiYoIXN8fHQucmVsPT09YHN0eWxlc2hlZXRgKSlyZXR1cm59bGV0IGw9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudChgbGlua2ApO2lmKGwucmVsPXM/YHN0eWxlc2hlZXRgOmBtb2R1bGVwcmVsb2FkYCxzfHwobC5hcz1gc2NyaXB0YCksbC5jcm9zc09yaWdpbj1gYCxsLmhyZWY9by5ocmVmLGMmJmwuc2V0QXR0cmlidXRlKGBub25jZWAsYyksZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChsKSxzKXJldHVybiBuZXcgUHJvbWlzZSgoZSx0KT0+e2wuYWRkRXZlbnRMaXN0ZW5lcihgbG9hZGAsZSksbC5hZGRFdmVudExpc3RlbmVyKGBlcnJvcmAsKCk9PnQoRXJyb3IoYFVuYWJsZSB0byBwcmVsb2FkIENTUyBmb3IgJHtvfWApKSl9KX0pLmZpbHRlcihlPT5lIT09dm9pZCAwKSl9ZnVuY3Rpb24gcyhlKXtsZXQgdD1uZXcgRXZlbnQoYHZpdGU6cHJlbG9hZEVycm9yYCx7Y2FuY2VsYWJsZTohMH0pO2lmKHQucGF5bG9hZD1lLHdpbmRvdy5kaXNwYXRjaEV2ZW50KHQpLCF0LmRlZmF1bHRQcmV2ZW50ZWQpdGhyb3cgZX1yZXR1cm4gby50aGVuKGU9Pntmb3IobGV0IHQgb2YgZXx8W10pdC5zdGF0dXM9PT1gcmVqZWN0ZWRgJiZzKHQucmVhc29uKTtyZXR1cm4gcigpLmNhdGNoKHMpfSl9O3IoKCk9PmltcG9ydChgLi9keW5hbWljLWZvby1Cd2haVGt0Qi5qc2ApLF9fdml0ZV9fbWFwRGVwcyhbMCwxXSkpLGNvbnNvbGUubG9nKGBhZnRlciBwcmVsb2FkIGR5bmFtaWNgKTtleHBvcnR7ciBhcyB0fTsKLy8jIGRlYnVnSWQ9ZWE1MTE0YWYtN2JmMi00NmExLTk3YzQtOTVlNzJlYmIyZTNkCi8vIyBzb3VyY2VNYXBwaW5nVVJMPWFmdGVyLXByZWxvYWQtZHluYW1pYy1EcVVSU3ZlXy5qcy5tYXAyODMAeyJkZWJ1Z0lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwibWFwcGluZ3MiOiI7bXZDQUFBQSxNQUFBLE9BQU8scURBRVAsUUFBUSxJQUFJLHVCQUF1QiIsIm5hbWVzIjpbIl9fdml0ZVByZWxvYWQiXSwic291cmNlcyI6WyIuLi8uLi9hZnRlci1wcmVsb2FkLWR5bmFtaWMuanMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0KCcuL2R5bmFtaWMvZHluYW1pYy1mb28nKVxuXG5jb25zb2xlLmxvZygnYWZ0ZXIgcHJlbG9hZCBkeW5hbWljJylcbiJdLCJ2ZXJzaW9uIjozfQ==" + visualization: "https://evanw.github.io/source-map-visualization/#MTczOQBjb25zdCBfX3ZpdGVfX21hcERlcHM9KGksbT1fX3ZpdGVfX21hcERlcHMsZD0obS5mfHwobS5mPVsiYXNzZXRzL2R5bmFtaWMtZm9vLUJ3aFpUa3RCLmpzIiwiYXNzZXRzL2R5bmFtaWMtZm9vLURzcUtSckV5LmNzcyJdKSkpPT5pLm1hcChpPT5kW2ldKTsKdmFyIGU9ZnVuY3Rpb24oZSl7cmV0dXJuYC9gK2V9LHQ9e30sbj1mdW5jdGlvbihlKXtyZXR1cm4gZS5wYXRobmFtZS5lbmRzV2l0aChgLmNzc2ApfSxyPWZ1bmN0aW9uKHIsaSxhKXtsZXQgbz1Qcm9taXNlLnJlc29sdmUoKTtpZihpJiZpLmxlbmd0aD4wKXtsZXQgcixzPWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoYG1ldGFbcHJvcGVydHk9Y3NwLW5vbmNlXWApLGM9cz8ubm9uY2V8fHM/LmdldEF0dHJpYnV0ZShgbm9uY2VgKTtmdW5jdGlvbiBsKGUpe3JldHVybiBQcm9taXNlLmFsbChlLm1hcChlPT5Qcm9taXNlLnJlc29sdmUoZSkudGhlbihlPT4oe3N0YXR1czpgZnVsZmlsbGVkYCx2YWx1ZTplfSksZT0+KHtzdGF0dXM6YHJlamVjdGVkYCxyZWFzb246ZX0pKSkpfWZ1bmN0aW9uIHUoZSl7cmV0dXJuIGltcG9ydC5tZXRhLnJlc29sdmU/bmV3IFVSTChpbXBvcnQubWV0YS5yZXNvbHZlKGUpKTpuZXcgVVJMKGUsaW1wb3J0Lm1ldGEudXJsKX1vPWwoaS5tYXAoaT0+e2k9ZShpLGEpO2xldCBvPXUoaSk7aWYoby5ocmVmIGluIHQpcmV0dXJuO3Rbby5ocmVmXT0hMDtsZXQgcz1uKG8pO2lmKHI9PT12b2lkIDApe3I9e2FsbDpuZXcgU2V0LHN0eWxlczpuZXcgU2V0fTtsZXQgZT1kb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZShgbGlua2ApO2ZvcihsZXQgdD1lLmxlbmd0aC0xO3Q+PTA7dC0tKXtsZXQgbj1lW3RdO3IuYWxsLmFkZChuLmhyZWYpLG4ucmVsPT09YHN0eWxlc2hlZXRgJiZyLnN0eWxlcy5hZGQobi5ocmVmKX19aWYoKHM/ci5zdHlsZXM6ci5hbGwpLmhhcyhvLmhyZWYpKXJldHVybjtsZXQgbD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KGBsaW5rYCk7aWYobC5yZWw9cz9gc3R5bGVzaGVldGA6YG1vZHVsZXByZWxvYWRgLHN8fChsLmFzPWBzY3JpcHRgKSxsLmNyb3NzT3JpZ2luPWBgLGwuaHJlZj1vLmhyZWYsYyYmbC5zZXRBdHRyaWJ1dGUoYG5vbmNlYCxjKSxkb2N1bWVudC5oZWFkLmFwcGVuZENoaWxkKGwpLHMpcmV0dXJuIG5ldyBQcm9taXNlKChlLHQpPT57bC5hZGRFdmVudExpc3RlbmVyKGBsb2FkYCxlKSxsLmFkZEV2ZW50TGlzdGVuZXIoYGVycm9yYCwoKT0+dChFcnJvcihgVW5hYmxlIHRvIHByZWxvYWQgQ1NTIGZvciAke299YCkpKX0pfSkuZmlsdGVyKGU9PmUhPT12b2lkIDApKX1mdW5jdGlvbiBzKGUpe2xldCB0PW5ldyBFdmVudChgdml0ZTpwcmVsb2FkRXJyb3JgLHtjYW5jZWxhYmxlOiEwfSk7aWYodC5wYXlsb2FkPWUsd2luZG93LmRpc3BhdGNoRXZlbnQodCksIXQuZGVmYXVsdFByZXZlbnRlZCl0aHJvdyBlfXJldHVybiBvLnRoZW4oZT0+e2ZvcihsZXQgdCBvZiBlfHxbXSl0LnN0YXR1cz09PWByZWplY3RlZGAmJnModC5yZWFzb24pO3JldHVybiByKCkuY2F0Y2gocyl9KX07cigoKT0+aW1wb3J0KGAuL2R5bmFtaWMtZm9vLUJ3aFpUa3RCLmpzYCksX192aXRlX19tYXBEZXBzKFswLDFdKSksY29uc29sZS5sb2coYGFmdGVyIHByZWxvYWQgZHluYW1pY2ApO2V4cG9ydHtyIGFzIHR9OwovLyMgZGVidWdJZD1lOTgwYmM3Ny00NzY2LTRlODctYTgxMS1hNjZlZGFmZGI2NDIKLy8jIHNvdXJjZU1hcHBpbmdVUkw9YWZ0ZXItcHJlbG9hZC1keW5hbWljLURwZ0x4M1IyLmpzLm1hcDI4MwB7ImRlYnVnSWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJtYXBwaW5ncyI6Ijt3MUNBQUFBLE1BQUEsT0FBTyxxREFFUCxRQUFRLElBQUksdUJBQXVCIiwibmFtZXMiOlsiX192aXRlUHJlbG9hZCJdLCJzb3VyY2VzIjpbIi4uLy4uL2FmdGVyLXByZWxvYWQtZHluYW1pYy5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQoJy4vZHluYW1pYy9keW5hbWljLWZvbycpXG5cbmNvbnNvbGUubG9nKCdhZnRlciBwcmVsb2FkIGR5bmFtaWMnKVxuIl0sInZlcnNpb24iOjN9" } `) // verify sourcemap comment is preserved at the last line diff --git a/playground/preload/__tests__/preload.spec.ts b/playground/preload/__tests__/preload.spec.ts index 4c055d01e770e0..57e2ff377e62a7 100644 --- a/playground/preload/__tests__/preload.spec.ts +++ b/playground/preload/__tests__/preload.spec.ts @@ -25,4 +25,18 @@ describe.runIf(isBuild)('build', () => { /link rel="stylesheet".*?href=".*?\/assets\/hello-[-\w]{8}\.css"/, ) }) + + test('does not re-preload a chunk already preloaded by the HTML', async () => { + const chunkPreloads = () => + page.$$eval('link[rel="modulepreload"]', (links) => + links + .map((l) => (l as HTMLLinkElement).href) + .filter((href) => /\/assets\/chunk-[-\w]{8}\.js$/.test(href)), + ) + expect(await chunkPreloads()).toHaveLength(1) + + await page.click('#about .load') + await page.waitForSelector('#about output') + expect(await chunkPreloads()).toHaveLength(1) + }) }) diff --git a/playground/preload/src/about.js b/playground/preload/src/about.js index 56b5fd6e8bcfe1..2b57b274c9ec1f 100644 --- a/playground/preload/src/about.js +++ b/playground/preload/src/about.js @@ -1,3 +1,4 @@ import { msg } from '@vitejs/test-dep-including-a' +import chunkMsg from './chunk' -document.querySelector('#about .msg').textContent = msg +document.querySelector('#about .msg').textContent = `${msg} ${chunkMsg}` diff --git a/playground/preload/vite.config.ts b/playground/preload/vite.config.ts index 18f5b5445c7116..a5ac06a6ef0372 100644 --- a/playground/preload/vite.config.ts +++ b/playground/preload/vite.config.ts @@ -14,11 +14,14 @@ export default defineConfig({ }, rolldownOptions: { output: { - // manualChunks(id) { - // if (id.includes('chunk.js')) { - // return 'chunk' - // } - // }, + codeSplitting: { + groups: [ + { + name: 'chunk', + test: 'chunk.js', + }, + ], + }, }, }, },