Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2f77366

Browse files
committed
feat: add transform srcset
1 parent 262bcdc commit 2f77366

File tree

5 files changed

+240
-22
lines changed

5 files changed

+240
-22
lines changed

packages/compiler/src/template/compose.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { vineErr, vineWarn } from '../diagnostics'
1515
import { appendToMapArray } from '../utils'
1616
import { transformAssetUrl } from './transform-asset-url'
1717
import { getTransformNegativeBoolPlugin } from './transform-negative-bool'
18+
import { transformSrcset } from './transform-srcset'
1819
import { walkVueTemplateAst } from './walk'
1920

2021
const SHOULD_ADD_SUFFIX_REGEXP = /(?<=<[a-z][^>/]*)$/i
@@ -103,7 +104,7 @@ export function compileVineTemplate(
103104

104105
const nodeTransforms = [
105106
...(__enableTransformAssetsURL
106-
? [transformAssetUrl]
107+
? [transformAssetUrl, transformSrcset]
107108
: []
108109
),
109110
getTransformNegativeBoolPlugin(

packages/compiler/src/template/transform-asset-url.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,16 @@ export interface AssetURLTagConfig {
99
[name: string]: string[]
1010
}
1111

12-
export interface AssetURLOptions {
13-
/**
14-
* If base is provided, instead of transforming relative asset urls into
15-
* imports, they will be directly rewritten to absolute urls.
16-
*/
17-
base?: string | null
18-
/**
19-
* If true, also processes absolute urls.
20-
*/
21-
includeAbsolute?: boolean
22-
tags?: AssetURLTagConfig
23-
}
24-
25-
export const assetUrlOptions: Required<AssetURLOptions> = {
12+
export const defaultAssetUrlOptions = {
2613
base: null,
27-
includeAbsolute: false,
2814
tags: {
2915
video: ['src', 'poster'],
3016
source: ['src'],
3117
img: ['src'],
3218
image: ['xlink:href', 'href'],
3319
use: ['xlink:href', 'href'],
3420
},
35-
}
21+
} as const
3622

3723
export const transformAssetUrl: NodeTransform = (
3824
node,
@@ -54,14 +40,13 @@ export const transformAssetUrl: NodeTransform = (
5440
? (context as unknown as VaporTransformContext).options.bindingMetadata
5541
: context.bindingMetadata
5642

57-
const tags = assetUrlOptions.tags
58-
const attrs = tags[node.tag]
59-
const wildCardAttrs = tags['*']
60-
if (!attrs && !wildCardAttrs) {
43+
const tags = defaultAssetUrlOptions.tags
44+
const attrs = tags[node.tag as keyof typeof tags]
45+
if (!attrs) {
6146
return
6247
}
6348

64-
const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
49+
const assetAttrs = (attrs || []) as readonly string[]
6550
node.props.forEach((attr, index) => {
6651
if (
6752
attr.type !== NodeTypes.ATTRIBUTE
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { ExpressionNode, NodeTransform, SimpleExpressionNode } from '@vue/compiler-core'
2+
import {
3+
ConstantTypes,
4+
createCompoundExpression,
5+
createSimpleExpression,
6+
NodeTypes,
7+
} from '@vue/compiler-core'
8+
import {
9+
isDataUrl,
10+
isExternalUrl,
11+
isRelativeUrl,
12+
parseUrl,
13+
} from '../utils/template'
14+
15+
const srcsetTags = ['img', 'source']
16+
17+
interface ImageCandidate {
18+
url: string
19+
descriptor: string
20+
}
21+
22+
// http://w3c.github.io/html/semantics-embedded-content.html#ref-for-image-candidate-string-5
23+
const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g
24+
25+
export const transformSrcset: NodeTransform = (
26+
node,
27+
context,
28+
) => {
29+
if (node.type === NodeTypes.ELEMENT) {
30+
if (srcsetTags.includes(node.tag) && node.props.length) {
31+
node.props.forEach((attr, index) => {
32+
if (attr.name === 'srcset' && attr.type === NodeTypes.ATTRIBUTE) {
33+
if (!attr.value)
34+
return
35+
const value = attr.value.content
36+
if (!value)
37+
return
38+
const imageCandidates: ImageCandidate[] = value.split(',').map((s) => {
39+
// The attribute value arrives here with all whitespace, except
40+
// normal spaces, represented by escape sequences
41+
const [url, descriptor] = s
42+
.replace(escapedSpaceCharacters, ' ')
43+
.trim()
44+
.split(' ', 2)
45+
return { url, descriptor }
46+
})
47+
48+
// data urls contains comma after the encoding so we need to re-merge
49+
// them
50+
for (let i = 0; i < imageCandidates.length; i++) {
51+
const { url } = imageCandidates[i]
52+
if (isDataUrl(url)) {
53+
imageCandidates[i + 1].url
54+
= `${url},${imageCandidates[i + 1].url}`
55+
imageCandidates.splice(i, 1)
56+
}
57+
}
58+
59+
const shouldProcessUrl = (url: string) => {
60+
return (
61+
!isExternalUrl(url)
62+
&& !isDataUrl(url)
63+
&& isRelativeUrl(url)
64+
)
65+
}
66+
// When srcset does not contain any qualified URLs, skip transforming
67+
if (!imageCandidates.some(({ url }) => shouldProcessUrl(url))) {
68+
return
69+
}
70+
71+
const compoundExpression = createCompoundExpression([], attr.loc)
72+
imageCandidates.forEach(({ url, descriptor }, index) => {
73+
if (shouldProcessUrl(url)) {
74+
const { path } = parseUrl(url)
75+
let exp: SimpleExpressionNode
76+
if (path) {
77+
const existingImportsIndex = context.imports.findIndex(
78+
i => i.path === path,
79+
)
80+
if (existingImportsIndex > -1) {
81+
exp = createSimpleExpression(
82+
`_imports_${existingImportsIndex}`,
83+
false,
84+
attr.loc,
85+
ConstantTypes.CAN_STRINGIFY,
86+
)
87+
}
88+
else {
89+
exp = createSimpleExpression(
90+
`_imports_${context.imports.length}`,
91+
false,
92+
attr.loc,
93+
ConstantTypes.CAN_STRINGIFY,
94+
)
95+
context.imports.push({ exp, path })
96+
}
97+
compoundExpression.children.push(exp)
98+
}
99+
}
100+
else {
101+
const exp = createSimpleExpression(
102+
`"${url}"`,
103+
false,
104+
attr.loc,
105+
ConstantTypes.CAN_STRINGIFY,
106+
)
107+
compoundExpression.children.push(exp)
108+
}
109+
const isNotLast = imageCandidates.length - 1 > index
110+
if (descriptor && isNotLast) {
111+
compoundExpression.children.push(` + ' ${descriptor}, ' + `)
112+
}
113+
else if (descriptor) {
114+
compoundExpression.children.push(` + ' ${descriptor}'`)
115+
}
116+
else if (isNotLast) {
117+
compoundExpression.children.push(` + ', ' + `)
118+
}
119+
})
120+
121+
let exp: ExpressionNode = compoundExpression
122+
if (context.hoistStatic) {
123+
exp = context.hoist(compoundExpression)
124+
exp.constType = ConstantTypes.CAN_STRINGIFY
125+
}
126+
127+
node.props[index] = {
128+
type: NodeTypes.DIRECTIVE,
129+
name: 'bind',
130+
arg: createSimpleExpression('srcset', true, attr.loc),
131+
exp,
132+
modifiers: [],
133+
loc: attr.loc,
134+
}
135+
}
136+
})
137+
}
138+
}
139+
}

packages/compiler/tests/__snapshots__/transform.spec.ts.snap

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,75 @@ typeof __VUE_HMR_RUNTIME__ !== "undefined" &&
13951395
"
13961396
`;
13971397
1398+
exports[`test transform > should transform asset url src & srcset 1`] = `
1399+
"import _imports_1 from "@/assets/[email protected]";
1400+
import _imports_0 from "@/assets/sample.png";
1401+
import {
1402+
defineComponent as _defineComponent,
1403+
useCssVars as _useCssVars,
1404+
unref as _unref,
1405+
createElementVNode as _createElementVNode,
1406+
Fragment as _Fragment,
1407+
openBlock as _openBlock,
1408+
createElementBlock as _createElementBlock,
1409+
} from "vue";
1410+
1411+
export const MyComp = (() => {
1412+
const _hoisted_1 = _imports_0 + " 1x, " + _imports_1 + " 2x";
1413+
const __vine = _defineComponent({
1414+
name: "MyComp",
1415+
/* No props */
1416+
/* No emits */
1417+
setup(__props, { expose: __expose }) {
1418+
__expose();
1419+
const props = __props;
1420+
1421+
return (_ctx, _cache) => {
1422+
return (
1423+
_openBlock(),
1424+
_createElementBlock(
1425+
_Fragment,
1426+
null,
1427+
[
1428+
_cache[0] ||
1429+
(_cache[0] = _createElementVNode(
1430+
"img",
1431+
{
1432+
src: _imports_0,
1433+
alt: "sample-src",
1434+
},
1435+
null,
1436+
-1 /* CACHED */,
1437+
)),
1438+
_cache[1] ||
1439+
(_cache[1] = _createElementVNode(
1440+
"img",
1441+
{
1442+
srcset: _hoisted_1,
1443+
alt: "sample-srcset",
1444+
},
1445+
null,
1446+
-1 /* CACHED */,
1447+
)),
1448+
],
1449+
64 /* STABLE_FRAGMENT */,
1450+
)
1451+
);
1452+
};
1453+
},
1454+
});
1455+
1456+
__vine.__vue_vine = true;
1457+
__vine.__hmrId = "06d7a8d8";
1458+
1459+
return __vine;
1460+
})();
1461+
1462+
typeof __VUE_HMR_RUNTIME__ !== "undefined" &&
1463+
__VUE_HMR_RUNTIME__.createRecord(MyComp.__hmrId, MyComp);
1464+
"
1465+
`;
1466+
13981467
exports[`test transform > should transform destructured props 1`] = `
13991468
"import { useDefaults as _useDefaults } from "vue-vine";
14001469
import {

packages/compiler/tests/transform.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,28 @@ function MyComp() {
514514
expect(updatedSource).toBe(sample)
515515
}
516516
})
517+
518+
it('should transform asset url src & srcset', async () => {
519+
const { mockCompilerCtx, mockCompilerHooks } = createMockTransformCtx({
520+
envMode: 'development',
521+
})
522+
const specContent = `
523+
function MyComp() {
524+
return vine\`
525+
<img src="@/assets/sample.png" alt="sample-src" />
526+
<img srcset="@/assets/sample.png 1x, @/assets/[email protected] 2x" alt="sample-srcset" />
527+
\`
528+
}
529+
`
530+
compileVineTypeScriptFile(specContent, 'testTransformAssetUrlSrcSet', { compilerHooks: mockCompilerHooks })
531+
expect(mockCompilerCtx.vineCompileErrors.length).toBe(0)
532+
533+
const fileCtx = mockCompilerCtx.fileCtxMap.get('testTransformAssetUrlSrcSet')
534+
const transformed = fileCtx?.fileMagicCode.toString() ?? ''
535+
const formated = await format(
536+
transformed,
537+
{ parser: 'babel-ts' },
538+
)
539+
expect(formated).toMatchSnapshot()
540+
})
517541
})

0 commit comments

Comments
 (0)