From 36972c20b5c2451c8345361f9c015655afbfdd87 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 2 May 2020 14:49:28 -0400 Subject: [PATCH] feat(compiler-sfc): add transformAssetUrlsBase option --- .../templateTransformAssetUrl.spec.ts.snap | 13 +++ .../templateTransformAssetUrl.spec.ts | 27 ++++- packages/compiler-sfc/src/compileTemplate.ts | 37 +++++-- .../src/templateTransformAssetUrl.ts | 102 +++++++++++++----- packages/compiler-sfc/src/templateUtils.ts | 5 +- rollup.config.js | 2 +- 6 files changed, 145 insertions(+), 41 deletions(-) diff --git a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap index ddf31942..e997cd09 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap @@ -36,3 +36,16 @@ export function render(_ctx, _cache) { ], 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 */)) +}" +`; diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts index 40531d78..64a94c6a 100644 --- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts @@ -1,5 +1,8 @@ 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 { transformBind } from '../../compiler-core/src/transforms/vBind' @@ -46,4 +49,26 @@ describe('compiler sfc: transform asset url', () => { expect(result.code).toMatchSnapshot() }) + + test('with explicit base', () => { + const ast = baseParse( + `` + // -> /foo/bar.png + `` + // -> /foo/bar.png + `` + // -> bar.png (untouched) + `` // -> @theme/bar.png (untouched) + ) + transform(ast, { + nodeTransforms: [ + createAssetUrlTransformWithOptions({ + base: '/foo' + }), + transformElement + ], + directiveTransforms: { + bind: transformBind + } + }) + const { code } = generate(ast, { mode: 'module' }) + expect(code).toMatchSnapshot() + }) }) diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index 2c50c7f3..df0051b0 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -40,8 +40,23 @@ export interface SFCTemplateCompileOptions { compilerOptions?: CompilerOptions preprocessLang?: string 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 + /** + * 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. + */ + transformAssetUrlsBase?: string } function preprocess( @@ -129,18 +144,24 @@ function doCompileTemplate({ ssr = false, compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM, compilerOptions = {}, - transformAssetUrls + transformAssetUrls, + transformAssetUrlsBase }: SFCTemplateCompileOptions): SFCTemplateCompileResults { const errors: CompilerError[] = [] let nodeTransforms: NodeTransform[] = [] - if (isObject(transformAssetUrls)) { - nodeTransforms = [ - createAssetUrlTransformWithOptions(transformAssetUrls), - transformSrcset - ] - } else if (transformAssetUrls !== false) { - nodeTransforms = [transformAssetUrl, transformSrcset] + if (transformAssetUrls !== false) { + if (transformAssetUrlsBase || isObject(transformAssetUrls)) { + nodeTransforms = [ + createAssetUrlTransformWithOptions({ + base: transformAssetUrlsBase, + tags: isObject(transformAssetUrls) ? transformAssetUrls : undefined + }), + transformSrcset + ] + } else { + 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 b981bf67..44ebe36a 100644 --- a/packages/compiler-sfc/src/templateTransformAssetUrl.ts +++ b/packages/compiler-sfc/src/templateTransformAssetUrl.ts @@ -1,3 +1,4 @@ +import path from 'path' import { createSimpleExpression, ExpressionNode, @@ -12,54 +13,97 @@ export interface AssetURLOptions { [name: string]: string[] } -const defaultOptions: AssetURLOptions = { - video: ['src', 'poster'], - source: ['src'], - img: ['src'], - image: ['xlink:href', 'href'], - use: ['xlink:href', 'href'] +export interface NormlaizedAssetURLOptions { + base?: string | null + tags?: AssetURLOptions +} + +const defaultAssetUrlOptions: Required = { + base: null, + tags: { + video: ['src', 'poster'], + source: ['src'], + img: ['src'], + image: ['xlink:href', 'href'], + use: ['xlink:href', 'href'] + } } export const createAssetUrlTransformWithOptions = ( - options: AssetURLOptions + options: NormlaizedAssetURLOptions ): NodeTransform => { const mergedOptions = { - ...defaultOptions, + ...defaultAssetUrlOptions, ...options } return (node, context) => (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 = ( node, context, - options: AssetURLOptions = defaultOptions + options: NormlaizedAssetURLOptions = defaultAssetUrlOptions ) => { 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) { - const attributes = options[tag] - attributes.forEach(item => { + const attributes = tags[tag] + attributes.forEach(name => { node.props.forEach((attr, index) => { - if (attr.type !== NodeTypes.ATTRIBUTE) return - if (attr.name !== item) return - if (!attr.value) return - if (!isRelativeUrl(attr.value.content)) return + if ( + attr.type !== NodeTypes.ATTRIBUTE || + attr.name !== name || + !attr.value || + !isRelativeUrl(attr.value.content) + ) { + return + } const url = parseUrl(attr.value.content) - const exp = getImportsExpressionExp( - url.path, - url.hash, - attr.loc, - context - ) - node.props[index] = { - type: NodeTypes.DIRECTIVE, - name: 'bind', - arg: createSimpleExpression(item, true, attr.loc), - exp, - modifiers: [], - loc: attr.loc + 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] !== '@') { + // 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( + options.base, + 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 + } } }) }) diff --git a/packages/compiler-sfc/src/templateUtils.ts b/packages/compiler-sfc/src/templateUtils.ts index 1040571a..70a5b26f 100644 --- a/packages/compiler-sfc/src/templateUtils.ts +++ b/packages/compiler-sfc/src/templateUtils.ts @@ -6,8 +6,9 @@ export function isRelativeUrl(url: string): boolean { 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 { const firstChar = url.charAt(0) if (firstChar === '~') { diff --git a/rollup.config.js b/rollup.config.js index e282f777..ef8d7af1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -129,7 +129,7 @@ function createConfig(format, output, plugins = []) { [ ...Object.keys(pkg.dependencies || {}), ...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