diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index d3488d5bb092c9..c09d5067086c37 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -168,8 +168,24 @@ const noInlineLinkRels = new Set([ 'apple-touch-icon', 'apple-touch-startup-image', 'manifest', + 'modulepreload', + 'preload', + 'prefetch', ]) +// If the node is a link, check if it can be inlined. If not, return `false` to +// force no inline. `undefined` leaves it to the default heuristics. +function getLinkShouldInline( + node: DefaultTreeAdapterMap['element'], + attributes: Record, +): false | undefined { + const isNoInlineLink = + node.nodeName === 'link' && + attributes.rel && + parseRelAttr(attributes.rel).some((v) => noInlineLinkRels.has(v)) + return isNoInlineLink ? false : undefined +} + export const isAsyncScriptMap: WeakMap< ResolvedConfig, Map @@ -636,7 +652,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { decodedUrl !== undefined && !isExcludedUrl(decodedUrl) ) { - const result = await processAssetUrl(url) + const result = await processAssetUrl( + url, + getLinkShouldInline(node, attr.attributes), + ) return result !== decodedUrl ? encodeURIPath(result) : url @@ -675,20 +694,11 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }) js += importExpression } else { - // If the node is a link, check if it can be inlined. If not, set `shouldInline` - // to `false` to force no inline. If `undefined`, it leaves to the default heuristics. - const isNoInlineLink = - node.nodeName === 'link' && - attr.attributes.rel && - parseRelAttr(attr.attributes.rel).some((v) => - noInlineLinkRels.has(v), - ) - const shouldInline = isNoInlineLink ? false : undefined assetUrlsPromises.push( (async () => { const processedUrl = await processAssetUrl( url, - shouldInline, + getLinkShouldInline(node, attr.attributes), ) if (processedUrl !== url) { overwriteAttrValue( diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index 222ae0ba88cafe..318d32158b0080 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -328,6 +328,36 @@ describe('css url() references', () => { expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match) }) + test('no base64 inline for modulepreload links', async () => { + const el = await page.$(`link[rel="modulepreload"]`) + const href = await el.getAttribute('href') + expect(href).toMatch( + isBundled + ? /\/foo\/bar\/assets\/preload-module-[-\w]{8}\.js/ + : 'preload-module.js', + ) + }) + + test('no base64 inline for preload and prefetch links', async () => { + const preloadAssetMatch = isBundled + ? /\/foo\/bar\/assets\/preload-asset-[-\w]{8}\.png/ + : '/foo/bar/nested/preload-asset.png' + + const preloadEl = await page.$('link.preload-href') + expect(await preloadEl.getAttribute('href')).toMatch(preloadAssetMatch) + + const prefetchEl = await page.$('link.prefetch-href') + expect(await prefetchEl.getAttribute('href')).toMatch(preloadAssetMatch) + + // `imagesrcset` goes through the srcset branch, which has to honour the + // same no-inline decision as `href` + const imageSrcSetEl = await page.$('link.preload-imagesrcset') + const imageSrcSet = await imageSrcSetEl.getAttribute('imagesrcset') + imageSrcSet.split(', ').forEach((s) => { + expect(s).toMatch(preloadAssetMatch) + }) + }) + test('no base64 inline for icon and manifest links', async () => { const iconEl = await page.$(`link.ico`) const href = await iconEl.getAttribute('href') diff --git a/playground/assets/index.html b/playground/assets/index.html index d9b395be1c12a1..601863db0dcb34 100644 --- a/playground/assets/index.html +++ b/playground/assets/index.html @@ -4,6 +4,24 @@ + + + + , small enough to hit assetsInlineLimit +export const preloadedModule = 'preloaded'