From 488d62f9e0bdf7ef031664d64b3841d6e314bc19 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 12 Jul 2021 17:53:51 -0400 Subject: [PATCH] feat(html): use `resolveBuiltUrl` on scripts/stylesheets not included in the facade module This commit allows for assets in ./public/ to have their URLs transformed at build time, making plugins like vite-plugin-public and vite-plugin-rehost possible. Depends on #1675 --- packages/vite/src/node/index.ts | 6 +- packages/vite/src/node/plugins/html.ts | 227 ++++++++++++++----------- 2 files changed, 135 insertions(+), 98 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 01312cb7515c6d..fb4cb27c3118eb 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -7,7 +7,11 @@ export { send } from './server/send' export { createLogger } from './logger' export { transformWithEsbuild } from './plugins/esbuild' export { resolvePackageData, resolvePackageEntry } from './plugins/resolve' -export { applyHtmlTransforms, resolveHtmlTransforms } from './plugins/html' +export { + applyHtmlTransforms, + resolveHtmlTransforms, + transformLocalUrls +} from './plugins/html' export { normalizePath } from './utils' // additional types diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 672f0a6ae02b21..fc181a54aafbdc 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -1,7 +1,7 @@ import path from 'path' import { Plugin } from '../plugin' import { ViteDevServer } from '../server' -import { OutputAsset, OutputBundle, OutputChunk } from 'rollup' +import { OutputAsset, OutputBundle, OutputChunk, PluginContext } from 'rollup' import { slash, cleanUrl, @@ -142,14 +142,116 @@ function formatParseError(e: any, id: string, html: string): Error { return e } +async function transformLocalUrls( + html: string, + importer: string, + config: ResolvedConfig, + ctx: PluginContext, + events: { + onModule?: ( + url: string | undefined, + node: any, + isAsync: boolean + ) => boolean | void + onStyleSheet?: (url: string) => boolean | void + } = {} +) { + const s = new MagicString(html) + const localUrls: AttributeNode[] = [] + const isExcludedUrl = (url: string) => + url.startsWith('#') || isExternalUrl(url) || isDataUrl(url) + + await traverseHtml(html, importer, (node) => { + if (node.type !== NodeTypes.ELEMENT) { + return + } + + let shouldRemove = false + + // For asset references in index.html, also generate an import + // statement for each - this will be handled by the asset plugin + const assetAttrs = assetAttrsConfig[node.tag] + if (assetAttrs) { + for (const p of node.props) { + if ( + p.type === NodeTypes.ATTRIBUTE && + p.value && + assetAttrs.includes(p.name) + ) { + const url = p.value.content + if (isExcludedUrl(url)) { + return + } + if (checkPublicFile(url, config)) { + localUrls.push(p) + } else if ( + node.tag === 'link' && + isCSSRequest(url) && + events.onStyleSheet?.(url) + ) { + shouldRemove = true + } else { + localUrls.push(p) + } + } + } + } + // script tags + else if (node.tag === 'script') { + const { src, isModule, isAsync } = getScriptInfo(node) + const url = src?.value?.content + if (url && isExcludedUrl(url)) { + return + } + if (url && checkPublicFile(url, config)) { + localUrls.push(src!) + } else if (isModule && events.onModule?.(url, node, isAsync)) { + shouldRemove = true + } else if (src) { + localUrls.push(src) + } + } + + if (shouldRemove) { + s.remove(node.loc.start.offset, node.loc.end.offset) + } + }) + + // for each encountered asset url, rewrite original html so that it + // references the post-build location. + for (const attr of localUrls) { + const value = attr.value! + try { + const processedUrl = + attr.name === 'srcset' + ? await processSrcSet(value.content, ({ url }) => + urlToBuiltUrl(url, importer, config, ctx) + ) + : await urlToBuiltUrl(value.content, importer, config, ctx) + + s.overwrite( + value.loc.start.offset, + value.loc.end.offset, + `"${processedUrl}"` + ) + } catch (e) { + // #1885 preload may be pointing to urls that do not exist + // locally on disk + if (e.code !== 'ENOENT') { + throw e + } + } + } + + return s.toString() +} + /** * Compiles index.html into an entry js module */ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const [preHooks, postHooks] = resolveHtmlTransforms(config.plugins) const processedHtml = new Map() - const isExcludedUrl = (url: string) => - url.startsWith('#') || isExternalUrl(url) || isDataUrl(url) return { name: 'vite:build-html', @@ -168,8 +270,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }) let js = '' - const s = new MagicString(html) - const assetUrls: AttributeNode[] = [] let inlineModuleIndex = -1 let htmlProxy: string | undefined @@ -177,109 +277,42 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { let someScriptsAreAsync = false let someScriptsAreDefer = false - await traverseHtml(html, id, (node) => { - if (node.type !== NodeTypes.ELEMENT) { - return - } - - let shouldRemove = false - - // script tags - if (node.tag === 'script') { - const { src, isModule, isAsync } = getScriptInfo(node) - - const url = src && src.value && src.value.content - if (url && checkPublicFile(url, config)) { - assetUrls.push(src!) - } else if (isModule) { - inlineModuleIndex++ - if (url && !isExcludedUrl(url)) { - // - js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` - htmlProxy = html - shouldRemove = true - } - - everyScriptIsAsync &&= isAsync - someScriptsAreAsync ||= isAsync - someScriptsAreDefer ||= !isAsync + html = await transformLocalUrls(html, id, config, this, { + onModule(url, node, isAsync) { + inlineModuleIndex++ + if (url) { + // + js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` + htmlProxy = html + return true } - } - - if (shouldRemove) { - // remove the script tag from the html. we are going to inject new - // ones in the end. - s.remove(node.loc.start.offset, node.loc.end.offset) + everyScriptIsAsync &&= isAsync + someScriptsAreAsync ||= isAsync + someScriptsAreDefer ||= !isAsync + }, + onStyleSheet(url) { + // CSS references, convert to import + js += `\nimport ${JSON.stringify(url)}` + return true } }) - + processedHtml.set(id, html) isAsyncScriptMap.get(config)!.set(id, everyScriptIsAsync) if (someScriptsAreAsync && someScriptsAreDefer) { config.logger.warn( - `\nMixed async and defer script modules in ${id}, output script will fallback to defer. Every script, including inline ones, need to be marked as async for your output script to be async.` + `\nMixed async and defer script modules in ${id}, output script ` + + `will fallback to defer. Every script, including inline ones, ` + + `need to be marked as async for your output script to be async.` ) } - // for each encountered asset url, rewrite original html so that it - // references the post-build location. - for (const attr of assetUrls) { - const value = attr.value! - try { - const url = - attr.name === 'srcset' - ? await processSrcSet(value.content, ({ url }) => - urlToBuiltUrl(url, id, config, this) - ) - : await urlToBuiltUrl(value.content, id, config, this) - - s.overwrite( - value.loc.start.offset, - value.loc.end.offset, - `"${url}"` - ) - } catch (e) { - // #1885 preload may be pointing to urls that do not exist - // locally on disk - if (e.code !== 'ENOENT') { - throw e - } - } - } - - processedHtml.set(id, s.toString()) - // inject module preload polyfill only when configured and needed if ( config.build.polyfillModulePreload &&