Skip to content

Commit

Permalink
feat(html): use resolveBuiltUrl on scripts/stylesheets not included…
Browse files Browse the repository at this point in the history
… 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 vitejs#1675
  • Loading branch information
aleclarson committed Oct 4, 2021
1 parent d14ca7b commit 488d62f
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 98 deletions.
6 changes: 5 additions & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
227 changes: 130 additions & 97 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<string, string>()
const isExcludedUrl = (url: string) =>
url.startsWith('#') || isExternalUrl(url) || isDataUrl(url)

return {
name: 'vite:build-html',
Expand All @@ -168,118 +270,49 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
})

let js = ''
const s = new MagicString(html)
const assetUrls: AttributeNode[] = []
let inlineModuleIndex = -1
let htmlProxy: string | undefined

let everyScriptIsAsync = true
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)) {
// <script type="module" src="..."/>
// add it as an import
js += `\nimport ${JSON.stringify(url)}`
shouldRemove = true
} else if (node.children.length) {
// <script type="module">...</script>
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) {
// <script type="module" src="..."/>
// add it as an import
js += `\nimport ${JSON.stringify(url)}`
return true
}
}

// 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 (checkPublicFile(url, config)) {
assetUrls.push(p)
} else if (!isExcludedUrl(url)) {
if (node.tag === 'link' && isCSSRequest(url)) {
// CSS references, convert to import
js += `\nimport ${JSON.stringify(url)}`
shouldRemove = true
} else {
assetUrls.push(p)
}
}
}
if (node.children.length) {
// <script type="module">...</script>
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 &&
Expand Down

0 comments on commit 488d62f

Please sign in to comment.