mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
feat(html)!: support more asset sources (#11138)
This commit is contained in:
parent
826c81a40b
commit
8a7af50b5d
@ -170,9 +170,24 @@ Any HTML files in your project root can be directly accessed by its respective d
|
||||
- `<root>/about.html` -> `http://localhost:5173/about.html`
|
||||
- `<root>/blog/index.html` -> `http://localhost:5173/blog/index.html`
|
||||
|
||||
HTML elements such as `<script type="module">` and `<link href>` tags are processed by default, which enables using Vite features in the linked files. General asset elements, such as `<img src>`, `<video src>`, and `<source src>`, are also rebased to ensure they are optimized and linked to the right path.
|
||||
Files referenced by HTML elements such as `<script type="module">` and `<link href>` are processed and bundled as part of the app. General asset elements can also reference assets to be optimized by default, including:
|
||||
|
||||
```html
|
||||
- `<audio src>`
|
||||
- `<embed src>`
|
||||
- `<img src>` and `<img srcset>`
|
||||
- `<image src>`
|
||||
- `<input src>`
|
||||
- `<link href>` and `<link imagesrcet>`
|
||||
- `<object data>`
|
||||
- `<source src>` and `<source srcset>`
|
||||
- `<track src>`
|
||||
- `<use href>` and `<use xlink:href>`
|
||||
- `<video src>` and `<video poster>`
|
||||
- `<meta content>`
|
||||
- Only if `name` attribute matches `msapplication-tileimage`, `msapplication-square70x70logo`, `msapplication-square150x150logo`, `msapplication-wide310x150logo`, `msapplication-square310x310logo`, `msapplication-config`, or `twitter:image`
|
||||
- Or only if `property` attribute matches `og:image`, `og:image:url`, `og:image:secure_url`, `og:audio`, `og:audio:secure_url`, `og:video`, or `og:video:secure_url`
|
||||
|
||||
```html {4-5,8-9}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@ -180,7 +195,6 @@ HTML elements such as `<script type="module">` and `<link href>` tags are proces
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<img src="/src/images/logo.svg" alt="logo" />
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
97
packages/vite/src/node/__tests__/assetSource.spec.ts
Normal file
97
packages/vite/src/node/__tests__/assetSource.spec.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { type DefaultTreeAdapterMap, parseFragment } from 'parse5'
|
||||
import { getNodeAssetAttributes } from '../assetSource'
|
||||
|
||||
describe('getNodeAssetAttributes', () => {
|
||||
const getNode = (html: string) => {
|
||||
const ast = parseFragment(html, { sourceCodeLocationInfo: true })
|
||||
return ast.childNodes[0] as DefaultTreeAdapterMap['element']
|
||||
}
|
||||
|
||||
test('handles img src', () => {
|
||||
const node = getNode('<img src="foo.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(1)
|
||||
expect(attrs[0]).toHaveProperty('type', 'src')
|
||||
expect(attrs[0]).toHaveProperty('key', 'src')
|
||||
expect(attrs[0]).toHaveProperty('value', 'foo.jpg')
|
||||
expect(attrs[0].attributes).toEqual({ src: 'foo.jpg' })
|
||||
expect(attrs[0].location).toHaveProperty('startOffset', 5)
|
||||
expect(attrs[0].location).toHaveProperty('endOffset', 18)
|
||||
})
|
||||
|
||||
test('handles source srcset', () => {
|
||||
const node = getNode('<source srcset="foo.jpg 1x, bar.jpg 2x">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(1)
|
||||
expect(attrs[0]).toHaveProperty('type', 'srcset')
|
||||
expect(attrs[0]).toHaveProperty('key', 'srcset')
|
||||
expect(attrs[0]).toHaveProperty('value', 'foo.jpg 1x, bar.jpg 2x')
|
||||
expect(attrs[0].attributes).toEqual({ srcset: 'foo.jpg 1x, bar.jpg 2x' })
|
||||
})
|
||||
|
||||
test('handles video src and poster', () => {
|
||||
const node = getNode('<video src="video.mp4" poster="poster.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(2)
|
||||
expect(attrs[0]).toHaveProperty('type', 'src')
|
||||
expect(attrs[0]).toHaveProperty('key', 'src')
|
||||
expect(attrs[0]).toHaveProperty('value', 'video.mp4')
|
||||
expect(attrs[0].attributes).toEqual({
|
||||
src: 'video.mp4',
|
||||
poster: 'poster.jpg',
|
||||
})
|
||||
expect(attrs[1]).toHaveProperty('type', 'src')
|
||||
expect(attrs[1]).toHaveProperty('key', 'poster')
|
||||
expect(attrs[1]).toHaveProperty('value', 'poster.jpg')
|
||||
})
|
||||
|
||||
test('handles link with allowed rel', () => {
|
||||
const node = getNode('<link rel="stylesheet" href="style.css">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(1)
|
||||
expect(attrs[0]).toHaveProperty('type', 'src')
|
||||
expect(attrs[0]).toHaveProperty('key', 'href')
|
||||
expect(attrs[0]).toHaveProperty('value', 'style.css')
|
||||
expect(attrs[0].attributes).toEqual({
|
||||
rel: 'stylesheet',
|
||||
href: 'style.css',
|
||||
})
|
||||
})
|
||||
|
||||
test('handles meta with allowed name', () => {
|
||||
const node = getNode('<meta name="twitter:image" content="image.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(1)
|
||||
expect(attrs[0]).toHaveProperty('type', 'src')
|
||||
expect(attrs[0]).toHaveProperty('key', 'content')
|
||||
expect(attrs[0]).toHaveProperty('value', 'image.jpg')
|
||||
})
|
||||
|
||||
test('handles meta with allowed property', () => {
|
||||
const node = getNode('<meta property="og:image" content="image.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(1)
|
||||
expect(attrs[0]).toHaveProperty('type', 'src')
|
||||
expect(attrs[0]).toHaveProperty('key', 'content')
|
||||
expect(attrs[0]).toHaveProperty('value', 'image.jpg')
|
||||
})
|
||||
|
||||
test('does not handle meta with unknown name', () => {
|
||||
const node = getNode('<meta name="unknown" content="image.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('does not handle meta with unknown property', () => {
|
||||
const node = getNode('<meta property="unknown" content="image.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('does not handle meta with no known properties', () => {
|
||||
const node = getNode('<meta foo="bar" content="image.jpg">')
|
||||
const attrs = getNodeAssetAttributes(node)
|
||||
expect(attrs).toHaveLength(0)
|
||||
})
|
||||
})
|
151
packages/vite/src/node/assetSource.ts
Normal file
151
packages/vite/src/node/assetSource.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import type { DefaultTreeAdapterMap, Token } from 'parse5'
|
||||
|
||||
interface HtmlAssetSource {
|
||||
srcAttributes?: string[]
|
||||
srcsetAttributes?: string[]
|
||||
/**
|
||||
* Called before handling an attribute to determine if it should be processed.
|
||||
*/
|
||||
filter?: (data: HtmlAssetSourceFilterData) => boolean
|
||||
}
|
||||
|
||||
interface HtmlAssetSourceFilterData {
|
||||
key: string
|
||||
value: string
|
||||
attributes: Record<string, string>
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name
|
||||
// https://wiki.whatwg.org/wiki/MetaExtensions
|
||||
const ALLOWED_META_NAME = [
|
||||
'msapplication-tileimage',
|
||||
'msapplication-square70x70logo',
|
||||
'msapplication-square150x150logo',
|
||||
'msapplication-wide310x150logo',
|
||||
'msapplication-square310x310logo',
|
||||
'msapplication-config',
|
||||
'twitter:image',
|
||||
]
|
||||
|
||||
// https://ogp.me
|
||||
const ALLOWED_META_PROPERTY = [
|
||||
'og:image',
|
||||
'og:image:url',
|
||||
'og:image:secure_url',
|
||||
'og:audio',
|
||||
'og:audio:secure_url',
|
||||
'og:video',
|
||||
'og:video:secure_url',
|
||||
]
|
||||
|
||||
const DEFAULT_HTML_ASSET_SOURCES: Record<string, HtmlAssetSource> = {
|
||||
audio: {
|
||||
srcAttributes: ['src'],
|
||||
},
|
||||
embed: {
|
||||
srcAttributes: ['src'],
|
||||
},
|
||||
img: {
|
||||
srcAttributes: ['src'],
|
||||
srcsetAttributes: ['srcset'],
|
||||
},
|
||||
image: {
|
||||
srcAttributes: ['href', 'xlink:href'],
|
||||
},
|
||||
input: {
|
||||
srcAttributes: ['src'],
|
||||
},
|
||||
link: {
|
||||
srcAttributes: ['href'],
|
||||
srcsetAttributes: ['imagesrcset'],
|
||||
},
|
||||
object: {
|
||||
srcAttributes: ['data'],
|
||||
},
|
||||
source: {
|
||||
srcAttributes: ['src'],
|
||||
srcsetAttributes: ['srcset'],
|
||||
},
|
||||
track: {
|
||||
srcAttributes: ['src'],
|
||||
},
|
||||
use: {
|
||||
srcAttributes: ['href', 'xlink:href'],
|
||||
},
|
||||
video: {
|
||||
srcAttributes: ['src', 'poster'],
|
||||
},
|
||||
meta: {
|
||||
srcAttributes: ['content'],
|
||||
filter({ attributes }) {
|
||||
if (
|
||||
attributes.name &&
|
||||
ALLOWED_META_NAME.includes(attributes.name.trim().toLowerCase())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
attributes.property &&
|
||||
ALLOWED_META_PROPERTY.includes(attributes.property.trim().toLowerCase())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
interface HtmlAssetAttribute {
|
||||
type: 'src' | 'srcset' | 'remove'
|
||||
key: string
|
||||
value: string
|
||||
attributes: Record<string, string>
|
||||
location: Token.Location
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a HTML node, find all attributes that references an asset to be processed
|
||||
*/
|
||||
export function getNodeAssetAttributes(
|
||||
node: DefaultTreeAdapterMap['element'],
|
||||
): HtmlAssetAttribute[] {
|
||||
const matched = DEFAULT_HTML_ASSET_SOURCES[node.nodeName]
|
||||
if (!matched) return []
|
||||
|
||||
const attributes: Record<string, string> = {}
|
||||
for (const attr of node.attrs) {
|
||||
attributes[getAttrKey(attr)] = attr.value
|
||||
}
|
||||
|
||||
// If the node has a `vite-ignore` attribute, remove the attribute and early out
|
||||
// to skip processing any attributes
|
||||
if ('vite-ignore' in attributes) {
|
||||
return [
|
||||
{
|
||||
type: 'remove',
|
||||
key: 'vite-ignore',
|
||||
value: '',
|
||||
attributes,
|
||||
location: node.sourceCodeLocation!.attrs!['vite-ignore'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const actions: HtmlAssetAttribute[] = []
|
||||
function handleAttributeKey(key: string, type: 'src' | 'srcset') {
|
||||
const value = attributes[key]
|
||||
if (!value) return
|
||||
if (matched.filter && !matched.filter({ key, value, attributes })) return
|
||||
const location = node.sourceCodeLocation!.attrs![key]
|
||||
actions.push({ type, key, value, attributes, location })
|
||||
}
|
||||
matched.srcAttributes?.forEach((key) => handleAttributeKey(key, 'src'))
|
||||
matched.srcsetAttributes?.forEach((key) => handleAttributeKey(key, 'srcset'))
|
||||
return actions
|
||||
}
|
||||
|
||||
function getAttrKey(attr: Token.Attribute): string {
|
||||
return attr.prefix === undefined ? attr.name : `${attr.prefix}:${attr.name}`
|
||||
}
|
@ -33,6 +33,7 @@ import { resolveEnvPrefix } from '../env'
|
||||
import type { Logger } from '../logger'
|
||||
import { cleanUrl } from '../../shared/utils'
|
||||
import { usePerEnvironmentState } from '../environment'
|
||||
import { getNodeAssetAttributes } from '../assetSource'
|
||||
import {
|
||||
assetUrlRE,
|
||||
getPublicAssetFilename,
|
||||
@ -140,16 +141,6 @@ export function addToHTMLProxyTransformResult(
|
||||
htmlProxyResult.set(hash, code)
|
||||
}
|
||||
|
||||
// this extends the config in @vue/compiler-sfc with <link href>
|
||||
export const assetAttrsConfig: Record<string, string[]> = {
|
||||
link: ['href'],
|
||||
video: ['src', 'poster'],
|
||||
source: ['src', 'srcset'],
|
||||
img: ['src', 'srcset'],
|
||||
image: ['xlink:href', 'href'],
|
||||
use: ['xlink:href', 'href'],
|
||||
}
|
||||
|
||||
// Some `<link rel>` elements should not be inlined in build. Excluding:
|
||||
// - `shortcut` : only valid for IE <9, use `icon`
|
||||
// - `mask-icon` : deprecated since Safari 12 (for pinned tabs)
|
||||
@ -525,96 +516,80 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
|
||||
|
||||
// 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.nodeName]
|
||||
if (assetAttrs) {
|
||||
const nodeAttrs: Record<string, string> = {}
|
||||
for (const attr of node.attrs) {
|
||||
nodeAttrs[getAttrKey(attr)] = attr.value
|
||||
}
|
||||
const shouldIgnore =
|
||||
node.nodeName === 'link' && 'vite-ignore' in nodeAttrs
|
||||
if (shouldIgnore) {
|
||||
removeViteIgnoreAttr(s, node.sourceCodeLocation!)
|
||||
} else {
|
||||
for (const attrKey in nodeAttrs) {
|
||||
const attrValue = nodeAttrs[attrKey]
|
||||
if (attrValue && assetAttrs.includes(attrKey)) {
|
||||
if (attrKey === 'srcset') {
|
||||
assetUrlsPromises.push(
|
||||
(async () => {
|
||||
const processedEncodedUrl = await processSrcSet(
|
||||
attrValue,
|
||||
async ({ url }) => {
|
||||
const decodedUrl = decodeURI(url)
|
||||
if (!isExcludedUrl(decodedUrl)) {
|
||||
const result = await processAssetUrl(url)
|
||||
return result !== decodedUrl
|
||||
? encodeURIPath(result)
|
||||
: url
|
||||
}
|
||||
return url
|
||||
},
|
||||
)
|
||||
if (processedEncodedUrl !== attrValue) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
getAttrSourceCodeLocation(node, attrKey),
|
||||
processedEncodedUrl,
|
||||
)
|
||||
}
|
||||
})(),
|
||||
const assetAttributes = getNodeAssetAttributes(node)
|
||||
for (const attr of assetAttributes) {
|
||||
if (attr.type === 'remove') {
|
||||
s.remove(attr.location.startOffset, attr.location.endOffset)
|
||||
continue
|
||||
} else if (attr.type === 'srcset') {
|
||||
assetUrlsPromises.push(
|
||||
(async () => {
|
||||
const processedEncodedUrl = await processSrcSet(
|
||||
attr.value,
|
||||
async ({ url }) => {
|
||||
const decodedUrl = decodeURI(url)
|
||||
if (!isExcludedUrl(decodedUrl)) {
|
||||
const result = await processAssetUrl(url)
|
||||
return result !== decodedUrl
|
||||
? encodeURIPath(result)
|
||||
: url
|
||||
}
|
||||
return url
|
||||
},
|
||||
)
|
||||
if (processedEncodedUrl !== attr.value) {
|
||||
overwriteAttrValue(s, attr.location, processedEncodedUrl)
|
||||
}
|
||||
})(),
|
||||
)
|
||||
} else if (attr.type === 'src') {
|
||||
const url = decodeURI(attr.value)
|
||||
if (checkPublicFile(url, config)) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
attr.location,
|
||||
partialEncodeURIPath(toOutputPublicFilePath(url)),
|
||||
)
|
||||
} else if (!isExcludedUrl(url)) {
|
||||
if (
|
||||
node.nodeName === 'link' &&
|
||||
isCSSRequest(url) &&
|
||||
// should not be converted if following attributes are present (#6748)
|
||||
!('media' in attr.attributes || 'disabled' in attr.attributes)
|
||||
) {
|
||||
// CSS references, convert to import
|
||||
const importExpression = `\nimport ${JSON.stringify(url)}`
|
||||
styleUrls.push({
|
||||
url,
|
||||
start: nodeStartWithLeadingWhitespace(node),
|
||||
end: node.sourceCodeLocation!.endOffset,
|
||||
})
|
||||
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),
|
||||
)
|
||||
} else {
|
||||
const url = decodeURI(attrValue)
|
||||
if (checkPublicFile(url, config)) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
getAttrSourceCodeLocation(node, attrKey),
|
||||
partialEncodeURIPath(toOutputPublicFilePath(url)),
|
||||
const shouldInline = isNoInlineLink ? false : undefined
|
||||
assetUrlsPromises.push(
|
||||
(async () => {
|
||||
const processedUrl = await processAssetUrl(
|
||||
url,
|
||||
shouldInline,
|
||||
)
|
||||
} else if (!isExcludedUrl(url)) {
|
||||
if (
|
||||
node.nodeName === 'link' &&
|
||||
isCSSRequest(url) &&
|
||||
// should not be converted if following attributes are present (#6748)
|
||||
!('media' in nodeAttrs || 'disabled' in nodeAttrs)
|
||||
) {
|
||||
// CSS references, convert to import
|
||||
const importExpression = `\nimport ${JSON.stringify(url)}`
|
||||
styleUrls.push({
|
||||
url,
|
||||
start: nodeStartWithLeadingWhitespace(node),
|
||||
end: node.sourceCodeLocation!.endOffset,
|
||||
})
|
||||
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' &&
|
||||
nodeAttrs.rel &&
|
||||
parseRelAttr(nodeAttrs.rel).some((v) =>
|
||||
noInlineLinkRels.has(v),
|
||||
)
|
||||
const shouldInline = isNoInlineLink ? false : undefined
|
||||
assetUrlsPromises.push(
|
||||
(async () => {
|
||||
const processedUrl = await processAssetUrl(
|
||||
url,
|
||||
shouldInline,
|
||||
)
|
||||
if (processedUrl !== url) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
getAttrSourceCodeLocation(node, attrKey),
|
||||
partialEncodeURIPath(processedUrl),
|
||||
)
|
||||
}
|
||||
})(),
|
||||
if (processedUrl !== url) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
attr.location,
|
||||
partialEncodeURIPath(processedUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1565,14 +1540,3 @@ function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string {
|
||||
function incrementIndent(indent: string = '') {
|
||||
return `${indent}${indent[0] === '\t' ? '\t' : ' '}`
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
@ -9,10 +9,8 @@ import type { IndexHtmlTransformHook } from '../../plugins/html'
|
||||
import {
|
||||
addToHTMLProxyCache,
|
||||
applyHtmlTransforms,
|
||||
assetAttrsConfig,
|
||||
extractImportExpressionFromClassicScript,
|
||||
findNeedTransformStyleAttribute,
|
||||
getAttrKey,
|
||||
getScriptInfo,
|
||||
htmlEnvHook,
|
||||
htmlProxyResult,
|
||||
@ -45,6 +43,7 @@ import { checkPublicFile } from '../../publicDir'
|
||||
import { isCSSRequest } from '../../plugins/css'
|
||||
import { getCodeWithSourcemap, injectSourcesContent } from '../sourcemap'
|
||||
import { cleanUrl, unwrapId, wrapId } from '../../../shared/utils'
|
||||
import { getNodeAssetAttributes } from '../../assetSource'
|
||||
|
||||
interface AssetNode {
|
||||
start: number
|
||||
@ -277,7 +276,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
} else if (src) {
|
||||
const processedUrl = processNodeUrl(
|
||||
src.value,
|
||||
getAttrKey(src) === 'srcset',
|
||||
/* useSrcSetReplacer */ false,
|
||||
config,
|
||||
htmlPath,
|
||||
originalUrl,
|
||||
@ -332,35 +331,20 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
}
|
||||
|
||||
// elements with [href/src] attrs
|
||||
const assetAttrs = assetAttrsConfig[node.nodeName]
|
||||
if (assetAttrs) {
|
||||
const nodeAttrs: Record<string, string> = {}
|
||||
for (const attr of node.attrs) {
|
||||
nodeAttrs[getAttrKey(attr)] = attr.value
|
||||
}
|
||||
const shouldIgnore =
|
||||
node.nodeName === 'link' && 'vite-ignore' in nodeAttrs
|
||||
if (shouldIgnore) {
|
||||
removeViteIgnoreAttr(s, node.sourceCodeLocation!)
|
||||
const assetAttributes = getNodeAssetAttributes(node)
|
||||
for (const attr of assetAttributes) {
|
||||
if (attr.type === 'remove') {
|
||||
s.remove(attr.location.startOffset, attr.location.endOffset)
|
||||
} else {
|
||||
for (const attrKey in nodeAttrs) {
|
||||
const attrValue = nodeAttrs[attrKey]
|
||||
if (attrValue && assetAttrs.includes(attrKey)) {
|
||||
const processedUrl = processNodeUrl(
|
||||
attrValue,
|
||||
attrKey === 'srcset',
|
||||
config,
|
||||
htmlPath,
|
||||
originalUrl,
|
||||
)
|
||||
if (processedUrl !== attrValue) {
|
||||
overwriteAttrValue(
|
||||
s,
|
||||
node.sourceCodeLocation!.attrs![attrKey],
|
||||
processedUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
const processedUrl = processNodeUrl(
|
||||
attr.value,
|
||||
attr.type === 'srcset',
|
||||
config,
|
||||
htmlPath,
|
||||
originalUrl,
|
||||
)
|
||||
if (processedUrl !== attr.value) {
|
||||
overwriteAttrValue(s, attr.location, processedUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -347,6 +347,18 @@ describe('image', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('meta', () => {
|
||||
test('og image', async () => {
|
||||
const meta = await page.$('.meta-og-image')
|
||||
const content = await meta.getAttribute('content')
|
||||
expect(content).toMatch(
|
||||
isBuild
|
||||
? /\/foo\/bar\/assets\/asset-\w{8}\.png/
|
||||
: /\/foo\/bar\/nested\/asset.png/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('svg fragments', () => {
|
||||
// 404 is checked already, so here we just ensure the urls end with #fragment
|
||||
test('img url', async () => {
|
||||
|
@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link class="ico" rel="icon" type="image/svg+xml" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta
|
||||
class="meta-og-image"
|
||||
property="og:image"
|
||||
content="./nested/asset.png"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<link class="data-href" rel="icon" href="data:," />
|
||||
|
Loading…
Reference in New Issue
Block a user