feat(compiler-sfc): add transformAssetUrlsBase option

This commit is contained in:
Evan You 2020-05-02 14:49:28 -04:00
parent 71a942b25a
commit 36972c20b5
6 changed files with 145 additions and 41 deletions

View File

@ -36,3 +36,16 @@ export function render(_ctx, _cache) {
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
exports[`compiler sfc: transform asset url with explicit base 1`] = `
"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"img\\", { src: \\"/foo/bar.png\\" }),
_createVNode(\\"img\\", { src: \\"/foo/bar.png\\" }),
_createVNode(\\"img\\", { src: \\"bar.png\\" }),
_createVNode(\\"img\\", { src: \\"@theme/bar.png\\" })
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -1,5 +1,8 @@
import { generate, baseParse, transform } from '@vue/compiler-core' import { generate, baseParse, transform } from '@vue/compiler-core'
import { transformAssetUrl } from '../src/templateTransformAssetUrl' import {
transformAssetUrl,
createAssetUrlTransformWithOptions
} from '../src/templateTransformAssetUrl'
import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformElement } from '../../compiler-core/src/transforms/transformElement'
import { transformBind } from '../../compiler-core/src/transforms/vBind' import { transformBind } from '../../compiler-core/src/transforms/vBind'
@ -46,4 +49,26 @@ describe('compiler sfc: transform asset url', () => {
expect(result.code).toMatchSnapshot() expect(result.code).toMatchSnapshot()
}) })
test('with explicit base', () => {
const ast = baseParse(
`<img src="./bar.png"></img>` + // -> /foo/bar.png
`<img src="~bar.png"></img>` + // -> /foo/bar.png
`<img src="bar.png"></img>` + // -> bar.png (untouched)
`<img src="@theme/bar.png"></img>` // -> @theme/bar.png (untouched)
)
transform(ast, {
nodeTransforms: [
createAssetUrlTransformWithOptions({
base: '/foo'
}),
transformElement
],
directiveTransforms: {
bind: transformBind
}
})
const { code } = generate(ast, { mode: 'module' })
expect(code).toMatchSnapshot()
})
}) })

View File

@ -40,8 +40,23 @@ export interface SFCTemplateCompileOptions {
compilerOptions?: CompilerOptions compilerOptions?: CompilerOptions
preprocessLang?: string preprocessLang?: string
preprocessOptions?: any preprocessOptions?: any
/**
* In some cases, compiler-sfc may not be inside the project root (e.g. when
* linked or globally installed). In such cases a custom `require` can be
* passed to correctly resolve the preprocessors.
*/
preprocessCustomRequire?: (id: string) => any preprocessCustomRequire?: (id: string) => any
/**
* Configure what tags/attributes to trasnform into relative asset url imports
* in the form of `{ [tag: string]: string[] }`, or disable the transform with
* `false`.
*/
transformAssetUrls?: AssetURLOptions | boolean transformAssetUrls?: AssetURLOptions | boolean
/**
* If base is provided, instead of transforming relative asset urls into
* imports, they will be directly rewritten to absolute urls.
*/
transformAssetUrlsBase?: string
} }
function preprocess( function preprocess(
@ -129,18 +144,24 @@ function doCompileTemplate({
ssr = false, ssr = false,
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM, compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
compilerOptions = {}, compilerOptions = {},
transformAssetUrls transformAssetUrls,
transformAssetUrlsBase
}: SFCTemplateCompileOptions): SFCTemplateCompileResults { }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
const errors: CompilerError[] = [] const errors: CompilerError[] = []
let nodeTransforms: NodeTransform[] = [] let nodeTransforms: NodeTransform[] = []
if (isObject(transformAssetUrls)) { if (transformAssetUrls !== false) {
nodeTransforms = [ if (transformAssetUrlsBase || isObject(transformAssetUrls)) {
createAssetUrlTransformWithOptions(transformAssetUrls), nodeTransforms = [
transformSrcset createAssetUrlTransformWithOptions({
] base: transformAssetUrlsBase,
} else if (transformAssetUrls !== false) { tags: isObject(transformAssetUrls) ? transformAssetUrls : undefined
nodeTransforms = [transformAssetUrl, transformSrcset] }),
transformSrcset
]
} else {
nodeTransforms = [transformAssetUrl, transformSrcset]
}
} }
let { code, map } = compiler.compile(source, { let { code, map } = compiler.compile(source, {

View File

@ -1,3 +1,4 @@
import path from 'path'
import { import {
createSimpleExpression, createSimpleExpression,
ExpressionNode, ExpressionNode,
@ -12,54 +13,97 @@ export interface AssetURLOptions {
[name: string]: string[] [name: string]: string[]
} }
const defaultOptions: AssetURLOptions = { export interface NormlaizedAssetURLOptions {
video: ['src', 'poster'], base?: string | null
source: ['src'], tags?: AssetURLOptions
img: ['src'], }
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href'] const defaultAssetUrlOptions: Required<NormlaizedAssetURLOptions> = {
base: null,
tags: {
video: ['src', 'poster'],
source: ['src'],
img: ['src'],
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
} }
export const createAssetUrlTransformWithOptions = ( export const createAssetUrlTransformWithOptions = (
options: AssetURLOptions options: NormlaizedAssetURLOptions
): NodeTransform => { ): NodeTransform => {
const mergedOptions = { const mergedOptions = {
...defaultOptions, ...defaultAssetUrlOptions,
...options ...options
} }
return (node, context) => return (node, context) =>
(transformAssetUrl as Function)(node, context, mergedOptions) (transformAssetUrl as Function)(node, context, mergedOptions)
} }
/**
* A `@vue/compiler-core` plugin that transforms relative asset urls into
* either imports or absolute urls.
*
* ``` js
* // Before
* createVNode('img', { src: './logo.png' })
*
* // After
* import _imports_0 from './logo.png'
* createVNode('img', { src: _imports_0 })
* ```
*/
export const transformAssetUrl: NodeTransform = ( export const transformAssetUrl: NodeTransform = (
node, node,
context, context,
options: AssetURLOptions = defaultOptions options: NormlaizedAssetURLOptions = defaultAssetUrlOptions
) => { ) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
for (const tag in options) { const tags = options.tags || defaultAssetUrlOptions.tags
for (const tag in tags) {
if ((tag === '*' || node.tag === tag) && node.props.length) { if ((tag === '*' || node.tag === tag) && node.props.length) {
const attributes = options[tag] const attributes = tags[tag]
attributes.forEach(item => { attributes.forEach(name => {
node.props.forEach((attr, index) => { node.props.forEach((attr, index) => {
if (attr.type !== NodeTypes.ATTRIBUTE) return if (
if (attr.name !== item) return attr.type !== NodeTypes.ATTRIBUTE ||
if (!attr.value) return attr.name !== name ||
if (!isRelativeUrl(attr.value.content)) return !attr.value ||
!isRelativeUrl(attr.value.content)
) {
return
}
const url = parseUrl(attr.value.content) const url = parseUrl(attr.value.content)
const exp = getImportsExpressionExp( if (options.base) {
url.path, // explicit base - directly rewrite the url into absolute url
url.hash, // does not apply to url that starts with `@` since they are
attr.loc, // aliases
context if (attr.value.content[0] !== '@') {
) // when packaged in the browser, path will be using the posix-
node.props[index] = { // only version provided by rollup-plugin-node-builtins.
type: NodeTypes.DIRECTIVE, attr.value.content = (path.posix || path).join(
name: 'bind', options.base,
arg: createSimpleExpression(item, true, attr.loc), url.path + (url.hash || '')
exp, )
modifiers: [], }
loc: attr.loc } else {
// otherwise, transform the url into an import.
// this assumes a bundler will resolve the import into the correct
// absolute url (e.g. webpack file-loader)
const exp = getImportsExpressionExp(
url.path,
url.hash,
attr.loc,
context
)
node.props[index] = {
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: createSimpleExpression(name, true, attr.loc),
exp,
modifiers: [],
loc: attr.loc
}
} }
}) })
}) })

View File

@ -6,8 +6,9 @@ export function isRelativeUrl(url: string): boolean {
return firstChar === '.' || firstChar === '~' || firstChar === '@' return firstChar === '.' || firstChar === '~' || firstChar === '@'
} }
// We need an extra transform context API for injecting arbitrary import /**
// statements. * Parses string url into URL object.
*/
export function parseUrl(url: string): UrlWithStringQuery { export function parseUrl(url: string): UrlWithStringQuery {
const firstChar = url.charAt(0) const firstChar = url.charAt(0)
if (firstChar === '~') { if (firstChar === '~') {

View File

@ -129,7 +129,7 @@ function createConfig(format, output, plugins = []) {
[ [
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}), ...Object.keys(pkg.peerDependencies || {}),
'url' // for @vue/compiler-sfc ...['path', 'url'] // for @vue/compiler-sfc
] ]
// the browser builds of @vue/compiler-sfc requires postcss to be available // the browser builds of @vue/compiler-sfc requires postcss to be available