From 6a0be882d4ce95eb8d8093f273ea0e868acfcd24 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 May 2020 16:45:19 -0400 Subject: [PATCH] feat(compiler-sfc): support transforming absolute asset urls BREAKING CHANGE: `@vue/compiler-sfc`'s `transformAssetUrlsBase` option has been removed. It is merged into `trasnformAssetUrls` which now also accepts the format of ```ts { base?: string includeAbsolute?: string tags?: { [name: string]: string[] } } ``` --- .../templateTransformAssetUrl.spec.ts.snap | 14 +++ .../templateTransformSrcset.spec.ts.snap | 112 ++++++++++++++++++ .../__tests__/compileTemplate.spec.ts | 18 ++- .../templateTransformAssetUrl.spec.ts | 38 +++--- .../__tests__/templateTransformSrcset.spec.ts | 58 ++++++--- packages/compiler-sfc/src/compileTemplate.ts | 44 +++---- .../src/templateTransformAssetUrl.ts | 92 +++++++++----- .../src/templateTransformSrcset.ts | 48 +++++++- packages/compiler-sfc/src/templateUtils.ts | 5 + 9 files changed, 332 insertions(+), 97 deletions(-) diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap index e997cd09c1b..3373e885ff8 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap @@ -49,3 +49,17 @@ export function render(_ctx, _cache) { ], 64 /* STABLE_FRAGMENT */)) }" `; + +exports[`compiler sfc: transform asset url with includeAbsolute: true 1`] = ` +"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" +import _imports_0 from './bar.png' +import _imports_1 from '/bar.png' + + +export function render(_ctx, _cache) { + return (_openBlock(), _createBlock(_Fragment, null, [ + _createVNode(\\"img\\", { src: _imports_0 }), + _createVNode(\\"img\\", { src: _imports_1 }) + ], 64 /* STABLE_FRAGMENT */)) +}" +`; diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap index dc0c5ee412b..75c72ac1947 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap @@ -59,3 +59,115 @@ export function render(_ctx, _cache) { ], 64 /* STABLE_FRAGMENT */)) }" `; + +exports[`compiler sfc: transform srcset transform srcset w/ 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: \\"./logo.png\\", + srcset: \\"/foo/logo.png\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png 2x\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png 2x\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png, /foo/logo.png 2x\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png 2x, /foo/logo.png\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png 2x, /foo/logo.png 3x\\" + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: \\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\" + }), + _createVNode(\\"img\\", { + src: \\"/logo.png\\", + srcset: \\"/logo.png, /logo.png 2x\\" + }), + _createVNode(\\"img\\", { + src: \\"https://example.com/logo.png\\", + srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\" + }), + _createVNode(\\"img\\", { + src: \\"/logo.png\\", + srcset: \\"/logo.png, /foo/logo.png 2x\\" + }) + ], 64 /* STABLE_FRAGMENT */)) +}" +`; + +exports[`compiler sfc: transform srcset transform srcset w/ includeAbsolute: true 1`] = ` +"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" +import _imports_0 from './logo.png' +import _imports_1 from '/logo.png' + + +const _hoisted_1 = _imports_0 +const _hoisted_2 = _imports_0 + '2x' +const _hoisted_3 = _imports_0 + '2x' +const _hoisted_4 = _imports_0 + ', ' + _imports_0 + '2x' +const _hoisted_5 = _imports_0 + '2x, ' + _imports_0 +const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x' +const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x' +const _hoisted_8 = _imports_1 + ', ' + _imports_1 + '2x' +const _hoisted_9 = \\"https://example.com/logo.png\\" + ', ' + \\"https://example.com/logo.png\\" + '2x' +const _hoisted_10 = _imports_1 + ', ' + _imports_0 + '2x' + +export function render(_ctx, _cache) { + return (_openBlock(), _createBlock(_Fragment, null, [ + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_1 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_2 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_3 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_4 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_5 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_6 + }), + _createVNode(\\"img\\", { + src: \\"./logo.png\\", + srcset: _hoisted_7 + }), + _createVNode(\\"img\\", { + src: \\"/logo.png\\", + srcset: _hoisted_8 + }), + _createVNode(\\"img\\", { + src: \\"https://example.com/logo.png\\", + srcset: _hoisted_9 + }), + _createVNode(\\"img\\", { + src: \\"/logo.png\\", + srcset: _hoisted_10 + }) + ], 64 /* STABLE_FRAGMENT */)) +}" +`; diff --git a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts index e6188f3f6da..2a456d8ef5b 100644 --- a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts +++ b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts @@ -54,15 +54,27 @@ test('transform asset url options', () => { // Object option const { code: code1 } = compileTemplate({ ...input, - transformAssetUrls: { foo: ['bar'] } + transformAssetUrls: { + tags: { foo: ['bar'] } + } }) expect(code1).toMatch(`import _imports_0 from 'baz'\n`) - // false option + + // legacy object option (direct tags config) const { code: code2 } = compileTemplate({ + ...input, + transformAssetUrls: { + foo: ['bar'] + } + }) + expect(code2).toMatch(`import _imports_0 from 'baz'\n`) + + // false option + const { code: code3 } = compileTemplate({ ...input, transformAssetUrls: false }) - expect(code2).not.toMatch(`import _imports_0 from 'baz'\n`) + expect(code3).not.toMatch(`import _imports_0 from 'baz'\n`) }) test('source map', () => { diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts index 64a94c6a6e4..1102690fdcf 100644 --- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts @@ -1,15 +1,20 @@ import { generate, baseParse, transform } from '@vue/compiler-core' import { transformAssetUrl, - createAssetUrlTransformWithOptions + createAssetUrlTransformWithOptions, + AssetURLOptions, + normalizeOptions } from '../src/templateTransformAssetUrl' import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformBind } from '../../compiler-core/src/transforms/vBind' -function compileWithAssetUrls(template: string) { +function compileWithAssetUrls(template: string, options?: AssetURLOptions) { const ast = baseParse(template) + const t = options + ? createAssetUrlTransformWithOptions(normalizeOptions(options)) + : transformAssetUrl transform(ast, { - nodeTransforms: [transformAssetUrl, transformElement], + nodeTransforms: [t, transformElement], directiveTransforms: { bind: transformBind } @@ -51,24 +56,25 @@ describe('compiler sfc: transform asset url', () => { }) test('with explicit base', () => { - const ast = baseParse( + const { code } = compileWithAssetUrls( `` + // -> /foo/bar.png `` + // -> /foo/bar.png `` + // -> bar.png (untouched) - `` // -> @theme/bar.png (untouched) + ``, // -> @theme/bar.png (untouched) + { + base: '/foo' + } ) - transform(ast, { - nodeTransforms: [ - createAssetUrlTransformWithOptions({ - base: '/foo' - }), - transformElement - ], - directiveTransforms: { - bind: transformBind + expect(code).toMatchSnapshot() + }) + + test('with includeAbsolute: true', () => { + const { code } = compileWithAssetUrls( + `` + ``, + { + includeAbsolute: true } - }) - const { code } = generate(ast, { mode: 'module' }) + ) expect(code).toMatchSnapshot() }) }) diff --git a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts index 7b2e365493d..1bba07672b1 100644 --- a/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformSrcset.spec.ts @@ -1,12 +1,22 @@ import { generate, baseParse, transform } from '@vue/compiler-core' -import { transformSrcset } from '../src/templateTransformSrcset' +import { + transformSrcset, + createSrcsetTransformWithOptions +} from '../src/templateTransformSrcset' import { transformElement } from '../../compiler-core/src/transforms/transformElement' import { transformBind } from '../../compiler-core/src/transforms/vBind' +import { + AssetURLOptions, + normalizeOptions +} from '../src/templateTransformAssetUrl' -function compileWithSrcset(template: string) { +function compileWithSrcset(template: string, options?: AssetURLOptions) { const ast = baseParse(template) + const srcsetTrasnform = options + ? createSrcsetTransformWithOptions(normalizeOptions(options)) + : transformSrcset transform(ast, { - nodeTransforms: [transformSrcset, transformElement], + nodeTransforms: [srcsetTrasnform, transformElement], directiveTransforms: { bind: transformBind } @@ -14,21 +24,37 @@ function compileWithSrcset(template: string) { return generate(ast, { mode: 'module' }) } +const src = ` + + + + + + + + + + +` + describe('compiler sfc: transform srcset', () => { test('transform srcset', () => { - const result = compileWithSrcset(` - - - - - - - - - - - `) + expect(compileWithSrcset(src).code).toMatchSnapshot() + }) + + test('transform srcset w/ base', () => { + expect( + compileWithSrcset(src, { + base: '/foo' + }).code + ).toMatchSnapshot() + }) - expect(result.code).toMatchSnapshot() + test('transform srcset w/ includeAbsolute: true', () => { + expect( + compileWithSrcset(src, { + includeAbsolute: true + }).code + ).toMatchSnapshot() }) }) diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index df0051b0032..f7f858a4bd2 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -10,9 +10,14 @@ import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map' import { transformAssetUrl, AssetURLOptions, - createAssetUrlTransformWithOptions + createAssetUrlTransformWithOptions, + AssetURLTagConfig, + normalizeOptions } from './templateTransformAssetUrl' -import { transformSrcset } from './templateTransformSrcset' +import { + transformSrcset, + createSrcsetTransformWithOptions +} from './templateTransformSrcset' import { isObject } from '@vue/shared' import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerSSR from '@vue/compiler-ssr' @@ -47,16 +52,10 @@ export interface SFCTemplateCompileOptions { */ 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 - /** - * If base is provided, instead of transforming relative asset urls into - * imports, they will be directly rewritten to absolute urls. + * Configure what tags/attributes to trasnform into asset url imports, + * or disable the transform altogether with `false`. */ - transformAssetUrlsBase?: string + transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean } function preprocess( @@ -144,24 +143,19 @@ function doCompileTemplate({ ssr = false, compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM, compilerOptions = {}, - transformAssetUrls, - transformAssetUrlsBase + transformAssetUrls }: SFCTemplateCompileOptions): SFCTemplateCompileResults { const errors: CompilerError[] = [] let nodeTransforms: NodeTransform[] = [] - if (transformAssetUrls !== false) { - if (transformAssetUrlsBase || isObject(transformAssetUrls)) { - nodeTransforms = [ - createAssetUrlTransformWithOptions({ - base: transformAssetUrlsBase, - tags: isObject(transformAssetUrls) ? transformAssetUrls : undefined - }), - transformSrcset - ] - } else { - nodeTransforms = [transformAssetUrl, transformSrcset] - } + if (isObject(transformAssetUrls)) { + const assetOptions = normalizeOptions(transformAssetUrls) + nodeTransforms = [ + createAssetUrlTransformWithOptions(assetOptions), + createSrcsetTransformWithOptions(assetOptions) + ] + } else if (transformAssetUrls !== false) { + nodeTransforms = [transformAssetUrl, transformSrcset] } let { code, map } = compiler.compile(source, { diff --git a/packages/compiler-sfc/src/templateTransformAssetUrl.ts b/packages/compiler-sfc/src/templateTransformAssetUrl.ts index 40189be86af..b491de6c930 100644 --- a/packages/compiler-sfc/src/templateTransformAssetUrl.ts +++ b/packages/compiler-sfc/src/templateTransformAssetUrl.ts @@ -8,18 +8,28 @@ import { TransformContext } from '@vue/compiler-core' import { isRelativeUrl, parseUrl } from './templateUtils' +import { isArray } from '@vue/shared' -export interface AssetURLOptions { +export interface AssetURLTagConfig { [name: string]: string[] } -export interface NormlaizedAssetURLOptions { +export interface AssetURLOptions { + /** + * If base is provided, instead of transforming relative asset urls into + * imports, they will be directly rewritten to absolute urls. + */ base?: string | null - tags?: AssetURLOptions + /** + * If true, also processes absolute urls. + */ + includeAbsolute?: boolean + tags?: AssetURLTagConfig } -const defaultAssetUrlOptions: Required = { +export const defaultAssetUrlOptions: Required = { base: null, + includeAbsolute: false, tags: { video: ['src', 'poster'], source: ['src'], @@ -29,15 +39,27 @@ const defaultAssetUrlOptions: Required = { } } -export const createAssetUrlTransformWithOptions = ( - options: NormlaizedAssetURLOptions -): NodeTransform => { - const mergedOptions = { +export const normalizeOptions = ( + options: AssetURLOptions | AssetURLTagConfig +): Required => { + if (Object.keys(options).some(key => isArray((options as any)[key]))) { + // legacy option format which directly passes in tags config + return { + ...defaultAssetUrlOptions, + tags: options as any + } + } + return { ...defaultAssetUrlOptions, ...options } +} + +export const createAssetUrlTransformWithOptions = ( + options: Required +): NodeTransform => { return (node, context) => - (transformAssetUrl as Function)(node, context, mergedOptions) + (transformAssetUrl as Function)(node, context, options) } /** @@ -56,7 +78,7 @@ export const createAssetUrlTransformWithOptions = ( export const transformAssetUrl: NodeTransform = ( node, context, - options: NormlaizedAssetURLOptions = defaultAssetUrlOptions + options: AssetURLOptions = defaultAssetUrlOptions ) => { if (node.type === NodeTypes.ELEMENT) { const tags = options.tags || defaultAssetUrlOptions.tags @@ -69,16 +91,21 @@ export const transformAssetUrl: NodeTransform = ( attr.type !== NodeTypes.ATTRIBUTE || attr.name !== name || !attr.value || - !isRelativeUrl(attr.value.content) + (!options.includeAbsolute && !isRelativeUrl(attr.value.content)) ) { return } + const url = parseUrl(attr.value.content) + if (options.base) { // explicit base - directly rewrite the url into absolute url - // does not apply to url that starts with `@` since they are - // aliases - if (attr.value.content[0] !== '@') { + // does not apply to absolute urls or urls that start with `@` + // since they are aliases + if ( + attr.value.content[0] !== '@' && + isRelativeUrl(attr.value.content) + ) { // when packaged in the browser, path will be using the posix- // only version provided by rollup-plugin-node-builtins. attr.value.content = (path.posix || path).join( @@ -86,24 +113,25 @@ export const transformAssetUrl: NodeTransform = ( url.path + (url.hash || '') ) } - } 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 - } + return + } + + // 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 } }) }) diff --git a/packages/compiler-sfc/src/templateTransformSrcset.ts b/packages/compiler-sfc/src/templateTransformSrcset.ts index 06b16ffd67c..aa7a400870c 100644 --- a/packages/compiler-sfc/src/templateTransformSrcset.ts +++ b/packages/compiler-sfc/src/templateTransformSrcset.ts @@ -1,3 +1,4 @@ +import path from 'path' import { createCompoundExpression, createSimpleExpression, @@ -5,7 +6,11 @@ import { NodeTypes, SimpleExpressionNode } from '@vue/compiler-core' -import { isRelativeUrl, parseUrl } from './templateUtils' +import { isRelativeUrl, parseUrl, isExternalUrl } from './templateUtils' +import { + AssetURLOptions, + defaultAssetUrlOptions +} from './templateTransformAssetUrl' const srcsetTags = ['img', 'source'] @@ -17,13 +22,23 @@ interface ImageCandidate { // http://w3c.github.io/html/semantics-embedded-content.html#ref-for-image-candidate-string-5 const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g -export const transformSrcset: NodeTransform = (node, context) => { +export const createSrcsetTransformWithOptions = ( + options: Required +): NodeTransform => { + return (node, context) => + (transformSrcset as Function)(node, context, options) +} + +export const transformSrcset: NodeTransform = ( + node, + context, + options: Required = defaultAssetUrlOptions +) => { if (node.type === NodeTypes.ELEMENT) { if (srcsetTags.includes(node.tag) && node.props.length) { node.props.forEach((attr, index) => { if (attr.name === 'srcset' && attr.type === NodeTypes.ATTRIBUTE) { if (!attr.value) return - // same logic as in transform-require.js const value = attr.value.content const imageCandidates: ImageCandidate[] = value.split(',').map(s => { @@ -37,11 +52,34 @@ export const transformSrcset: NodeTransform = (node, context) => { }) // When srcset does not contain any relative URLs, skip transforming - if (!imageCandidates.some(({ url }) => isRelativeUrl(url))) return + if ( + !options.includeAbsolute && + !imageCandidates.some(({ url }) => isRelativeUrl(url)) + ) { + return + } + + if (options.base) { + const base = options.base + const set: string[] = [] + imageCandidates.forEach(({ url, descriptor }, index) => { + descriptor = descriptor ? ` ${descriptor}` : `` + if (isRelativeUrl(url)) { + set.push((path.posix || path).join(base, url) + descriptor) + } else { + set.push(url + descriptor) + } + }) + attr.value.content = set.join(', ') + return + } const compoundExpression = createCompoundExpression([], attr.loc) imageCandidates.forEach(({ url, descriptor }, index) => { - if (isRelativeUrl(url)) { + if ( + !isExternalUrl(url) && + (options.includeAbsolute || isRelativeUrl(url)) + ) { const { path } = parseUrl(url) let exp: SimpleExpressionNode if (path) { diff --git a/packages/compiler-sfc/src/templateUtils.ts b/packages/compiler-sfc/src/templateUtils.ts index 70a5b26fa60..40b40199089 100644 --- a/packages/compiler-sfc/src/templateUtils.ts +++ b/packages/compiler-sfc/src/templateUtils.ts @@ -6,6 +6,11 @@ export function isRelativeUrl(url: string): boolean { return firstChar === '.' || firstChar === '~' || firstChar === '@' } +const externalRE = /^https?:\/\// +export function isExternalUrl(url: string): boolean { + return externalRE.test(url) +} + /** * Parses string url into URL object. */