Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(build): mixed external and transpiled srcset #14888

Merged
merged 3 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 103 additions & 74 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
preHooks.push(htmlEnvHook(config))
postHooks.push(postImportMapHook())
const processedHtml = new Map<string, string>()

const isExcludedUrl = (url: string) =>
url[0] === '#' ||
isExternalUrl(url) ||
isDataUrl(url) ||
checkPublicFile(url, config)
url[0] === '#' || isExternalUrl(url) || isDataUrl(url)

// Same reason with `htmlInlineProxyPlugin`
isAsyncScriptMap.set(config, new Map())

Expand Down Expand Up @@ -367,10 +366,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {

let js = ''
const s = new MagicString(html)
const assetUrls: {
attr: Token.Attribute
sourceCodeLocation: Token.Location
}[] = []
const scriptUrls: ScriptAssetsUrl[] = []
const styleUrls: ScriptAssetsUrl[] = []
let inlineModuleIndex = -1
Expand All @@ -379,6 +374,31 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
let someScriptsAreAsync = false
let someScriptsAreDefer = false

const assetUrlsPromises: Promise<void>[] = []

// for each encountered asset url, rewrite original html so that it
// references the post-build location, ignoring empty attributes and
// attributes that directly reference named output.
const namedOutput = Object.keys(
config?.build?.rollupOptions?.input || {},
)
const processAssetUrl = async (url: string) => {
if (
url !== '' && // Empty attribute
!namedOutput.includes(url) && // Direct reference to named output
!namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path
) {
try {
return await urlToBuiltUrl(url, id, config, this)
} catch (e) {
if (e.code !== 'ENOENT') {
throw e
}
}
}
return url
}

await traverseHtml(html, id, (node) => {
if (!nodeIsElement(node)) {
return
Expand All @@ -404,7 +424,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {

if (isModule) {
inlineModuleIndex++
if (url && !isExcludedUrl(url)) {
if (url && !isExcludedUrl(url) && !isPublicFile) {
// <script type="module" src="..."/>
// add it as an import
js += `\nimport ${JSON.stringify(url)}`
Expand Down Expand Up @@ -447,41 +467,71 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
for (const p of node.attrs) {
const attrKey = getAttrKey(p)
if (p.value && assetAttrs.includes(attrKey)) {
const attrSourceCodeLocation =
node.sourceCodeLocation!.attrs![attrKey]
// assetsUrl may be encodeURI
const url = decodeURI(p.value)
if (!isExcludedUrl(url)) {
if (
node.nodeName === 'link' &&
isCSSRequest(url) &&
// should not be converted if following attributes are present (#6748)
!node.attrs.some(
(p) =>
p.prefix === undefined &&
(p.name === 'media' || p.name === 'disabled'),
if (attrKey === 'srcset') {
assetUrlsPromises.push(
(async () => {
const processedUrl = await processSrcSet(
p.value,
async ({ url }) => {
const decodedUrl = decodeURI(url)
if (!isExcludedUrl(decodedUrl)) {
const result = await processAssetUrl(url)
return result !== decodedUrl ? result : url
}
return url
sapphi-red marked this conversation as resolved.
Show resolved Hide resolved
},
)
if (processedUrl !== p.value) {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
)
}
})(),
)
} else {
const url = decodeURI(p.value)
if (checkPublicFile(url, config)) {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
toOutputPublicFilePath(url),
)
) {
// CSS references, convert to import
const importExpression = `\nimport ${JSON.stringify(url)}`
styleUrls.push({
url,
start: nodeStartWithLeadingWhitespace(node),
end: node.sourceCodeLocation!.endOffset,
})
js += importExpression
} else {
assetUrls.push({
attr: p,
sourceCodeLocation: attrSourceCodeLocation,
})
} else if (!isExcludedUrl(url)) {
if (
node.nodeName === 'link' &&
isCSSRequest(url) &&
// should not be converted if following attributes are present (#6748)
!node.attrs.some(
(p) =>
p.prefix === undefined &&
(p.name === 'media' || p.name === 'disabled'),
)
) {
// CSS references, convert to import
const importExpression = `\nimport ${JSON.stringify(url)}`
styleUrls.push({
url,
start: nodeStartWithLeadingWhitespace(node),
end: node.sourceCodeLocation!.endOffset,
})
js += importExpression
} else {
assetUrlsPromises.push(
(async () => {
const processedUrl = await processAssetUrl(url)
if (processedUrl !== url) {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
)
}
})(),
)
}
}
} else if (checkPublicFile(url, config)) {
overwriteAttrValue(
s,
attrSourceCodeLocation,
toOutputPublicFilePath(url),
)
}
}
}
Expand Down Expand Up @@ -543,42 +593,14 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
)
}

// for each encountered asset url, rewrite original html so that it
// references the post-build location, ignoring empty attributes and
// attributes that directly reference named output.
const namedOutput = Object.keys(
config?.build?.rollupOptions?.input || {},
)
for (const { attr, sourceCodeLocation } of assetUrls) {
// assetsUrl may be encodeURI
const content = decodeURI(attr.value)
if (
content !== '' && // Empty attribute
!namedOutput.includes(content) && // Direct reference to named output
!namedOutput.includes(removeLeadingSlash(content)) // Allow for absolute references as named output can't be an absolute path
) {
try {
const url =
attr.prefix === undefined && attr.name === 'srcset'
? await processSrcSet(content, ({ url }) =>
urlToBuiltUrl(url, id, config, this),
)
: await urlToBuiltUrl(content, id, config, this)
await Promise.all(assetUrlsPromises)

overwriteAttrValue(s, sourceCodeLocation, url)
} catch (e) {
if (e.code !== 'ENOENT') {
throw e
}
}
}
}
// emit <script>import("./aaa")</script> asset
for (const { start, end, url } of scriptUrls) {
if (!isExcludedUrl(url)) {
s.update(start, end, await urlToBuiltUrl(url, id, config, this))
} else if (checkPublicFile(url, config)) {
if (checkPublicFile(url, config)) {
s.update(start, end, toOutputPublicFilePath(url))
} else if (!isExcludedUrl(url)) {
s.update(start, end, await urlToBuiltUrl(url, id, config, this))
}
}

Expand Down Expand Up @@ -1331,3 +1353,10 @@ function incrementIndent(indent: string = '') {
export function getAttrKey(attr: Token.Attribute): string {
return attr.prefix === undefined ? attr.name : `${attr.prefix}:${attr.name}`
}

function getAttrSourceCodeLocation(
node: DefaultTreeAdapterMap['element'],
attrKey: string,
) {
return node.sourceCodeLocation!.attrs![attrKey]
}
3 changes: 1 addition & 2 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,7 @@ describe('image', () => {
})
})

// TODO: fix build
test.runIf(!isBuild)('srcset (mixed)', async () => {
test('srcset (mixed)', async () => {
const img = await page.$('.img-src-set-mixed')
const srcset = await img.getAttribute('srcset')
const srcs = srcset.split(', ')
Expand Down