提交 447e4454 编写于 作者: fxy060608's avatar fxy060608

feat(mp): transform non-static asset (#3014)

上级 6d699150
......@@ -25,6 +25,7 @@
"@rollup/pluginutils": "^4.1.1",
"@vue/compiler-core": "3.2.22",
"@vue/compiler-dom": "3.2.22",
"@vue/compiler-sfc": "3.2.22",
"@vue/shared": "3.2.22",
"chalk": "^4.1.1",
"chokidar": "^3.5.2",
......@@ -58,7 +59,6 @@
"@types/mime": "^2.0.3",
"@types/module-alias": "^2.0.1",
"@types/stylus": "^0.48.36",
"@vue/compiler-sfc": "3.2.22",
"postcss": "^8.3.8"
}
}
......@@ -3,7 +3,6 @@ import os from 'os'
import path from 'path'
import { camelize, capitalize } from '@vue/shared'
export { default as hash } from 'hash-sum'
import type { SFCTemplateCompileOptions } from '@vue/compiler-sfc'
import { PAGE_EXTNAME, PAGE_EXTNAME_APP } from './constants'
import {
......@@ -77,26 +76,3 @@ export function normalizeMiniProgramFilename(
}
return normalizeNodeModules(path.relative(inputDir, filename))
}
export function createUniVueTransformAssetUrls(
base: string
): SFCTemplateCompileOptions['transformAssetUrls'] {
return {
base,
tags: {
audio: ['src'],
video: ['src', 'poster'],
img: ['src'],
image: ['src'],
'cover-image': ['src'],
// h5
'v-uni-audio': ['src'],
'v-uni-video': ['src', 'poster'],
'v-uni-image': ['src'],
'v-uni-cover-image': ['src'],
// nvue
'u-image': ['src'],
'u-video': ['src', 'poster'],
},
}
}
export * from './transforms'
export * from './utils'
export { isExternalUrl } from './transforms/templateUtils'
......@@ -8,6 +8,8 @@ export * from './transformPageHead'
export * from './transformComponent'
export * from './transformEvent'
export * from './transformTag'
export { createAssetUrlTransformWithOptions } from './templateTransformAssetUrl'
export { createSrcsetTransformWithOptions } from './templateTransformSrcset'
export {
ATTR_DATASET_EVENT_OPTS,
createTransformOn,
......
import path from 'path'
import {
ConstantTypes,
createSimpleExpression,
ExpressionNode,
NodeTransform,
NodeTypes,
SimpleExpressionNode,
SourceLocation,
TransformContext,
} from '@vue/compiler-core'
import {
isRelativeUrl,
parseUrl,
isExternalUrl,
isDataUrl,
} from './templateUtils'
import { isArray } from '@vue/shared'
export interface AssetURLTagConfig {
[name: string]: string[]
}
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
/**
* If true, also processes absolute urls.
*/
includeAbsolute?: boolean
tags?: AssetURLTagConfig
}
export const defaultAssetUrlOptions: Required<AssetURLOptions> = {
base: null,
includeAbsolute: false,
tags: {
video: ['src', 'poster'],
source: ['src'],
img: ['src'],
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href'],
},
}
export const normalizeOptions = (
options: AssetURLOptions | AssetURLTagConfig
): Required<AssetURLOptions> => {
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<AssetURLOptions>
): NodeTransform => {
return (node, context) =>
(transformAssetUrl as Function)(node, context, options)
}
/**
* 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 = defaultAssetUrlOptions
) => {
if (node.type === NodeTypes.ELEMENT) {
if (!node.props.length) {
return
}
const tags = options.tags || defaultAssetUrlOptions.tags
const attrs = tags[node.tag]
const wildCardAttrs = tags['*']
if (!attrs && !wildCardAttrs) {
return
}
// 策略:
// h5 平台保留原始策略
// 非 h5 平台
// - 绝对路径 static 资源不做转换
// - 相对路径 static 资源转换为绝对路径
// - 非 static 资源转换为 import
const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
node.props.forEach((attr, index) => {
if (
attr.type !== NodeTypes.ATTRIBUTE ||
!assetAttrs.includes(attr.name) ||
!attr.value ||
isExternalUrl(attr.value.content) ||
isDataUrl(attr.value.content) ||
attr.value.content[0] === '#'
) {
return
}
// fixed by xxxxxx 区分 static 资源
const isStaticAsset = attr.value.content.indexOf('/static/') > -1
// 绝对路径的静态资源不作处理
if (isStaticAsset && !isRelativeUrl(attr.value.content)) {
return
}
const url = parseUrl(attr.value.content)
if (options.base && attr.value.content[0] === '.' && isStaticAsset) {
// explicit base - directly rewrite relative urls into absolute url
// to avoid generating extra imports
// Allow for full hostnames provided in options.base
const base = parseUrl(options.base)
const protocol = base.protocol || ''
const host = base.host ? protocol + '//' + base.host : ''
const basePath = base.path || '/'
// when packaged in the browser, path will be using the posix-
// only version provided by rollup-plugin-node-builtins.
attr.value.content =
host +
(path.posix || path).join(basePath, url.path + (url.hash || ''))
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(attr.name, true, attr.loc),
exp,
modifiers: [],
loc: attr.loc,
}
})
}
}
function getImportsExpressionExp(
path: string | null,
hash: string | null,
loc: SourceLocation,
context: TransformContext
): ExpressionNode {
if (path) {
let name: string
let exp: SimpleExpressionNode
const existingIndex = context.imports.findIndex((i) => i.path === path)
if (existingIndex > -1) {
name = `_imports_${existingIndex}`
exp = context.imports[existingIndex].exp as SimpleExpressionNode
} else {
name = `_imports_${context.imports.length}`
exp = createSimpleExpression(name, false, loc, ConstantTypes.CAN_HOIST)
context.imports.push({ exp, path })
}
if (!hash) {
return exp
}
const hashExp = `${name} + '${hash}'`
const existingHoistIndex = context.hoists.findIndex((h) => {
return (
h &&
h.type === NodeTypes.SIMPLE_EXPRESSION &&
!h.isStatic &&
h.content === hashExp
)
})
if (existingHoistIndex > -1) {
return createSimpleExpression(
`_hoisted_${existingHoistIndex + 1}`,
false,
loc,
ConstantTypes.CAN_HOIST
)
}
return context.hoist(
createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_HOIST)
)
} else {
return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_HOIST)
}
}
import path from 'path'
import {
ConstantTypes,
createCompoundExpression,
createSimpleExpression,
NodeTransform,
NodeTypes,
SimpleExpressionNode,
} from '@vue/compiler-core'
import {
isRelativeUrl,
parseUrl,
isExternalUrl,
isDataUrl,
} from './templateUtils'
import {
AssetURLOptions,
defaultAssetUrlOptions,
} from './templateTransformAssetUrl'
const srcsetTags = ['img', 'source']
interface ImageCandidate {
url: string
descriptor: string
}
// http://w3c.github.io/html/semantics-embedded-content.html#ref-for-image-candidate-string-5
const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g
export const createSrcsetTransformWithOptions = (
options: Required<AssetURLOptions>
): NodeTransform => {
return (node, context) =>
(transformSrcset as Function)(node, context, options)
}
export const transformSrcset: NodeTransform = (
node,
context,
options: Required<AssetURLOptions> = 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
const value = attr.value.content
if (!value) return
const imageCandidates: ImageCandidate[] = value
.split(',')
.map((s) => {
// The attribute value arrives here with all whitespace, except
// normal spaces, represented by escape sequences
const [url, descriptor] = s
.replace(escapedSpaceCharacters, ' ')
.trim()
.split(' ', 2)
return { url, descriptor }
})
// data urls contains comma after the ecoding so we need to re-merge
// them
for (let i = 0; i < imageCandidates.length; i++) {
const { url } = imageCandidates[i]
if (isDataUrl(url)) {
imageCandidates[i + 1].url =
url + ',' + imageCandidates[i + 1].url
imageCandidates.splice(i, 1)
}
}
const hasQualifiedUrl = imageCandidates.some(({ url }) => {
return (
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))
)
})
// When srcset does not contain any qualified URLs, skip transforming
if (!hasQualifiedUrl) {
return
}
if (options.base) {
const base = options.base
const set: string[] = []
imageCandidates.forEach(({ url, descriptor }) => {
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 (
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))
) {
const { path } = parseUrl(url)
let exp: SimpleExpressionNode
if (path) {
const existingImportsIndex = context.imports.findIndex(
(i) => i.path === path
)
if (existingImportsIndex > -1) {
exp = createSimpleExpression(
`_imports_${existingImportsIndex}`,
false,
attr.loc,
ConstantTypes.CAN_HOIST
)
} else {
exp = createSimpleExpression(
`_imports_${context.imports.length}`,
false,
attr.loc,
ConstantTypes.CAN_HOIST
)
context.imports.push({ exp, path })
}
compoundExpression.children.push(exp)
}
} else {
const exp = createSimpleExpression(
`"${url}"`,
false,
attr.loc,
ConstantTypes.CAN_HOIST
)
compoundExpression.children.push(exp)
}
const isNotLast = imageCandidates.length - 1 > index
if (descriptor && isNotLast) {
compoundExpression.children.push(` + ' ${descriptor}, ' + `)
} else if (descriptor) {
compoundExpression.children.push(` + ' ${descriptor}'`)
} else if (isNotLast) {
compoundExpression.children.push(` + ', ' + `)
}
})
const hoisted = context.hoist(compoundExpression)
hoisted.constType = ConstantTypes.CAN_HOIST
node.props[index] = {
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: createSimpleExpression('srcset', true, attr.loc),
exp: hoisted,
modifiers: [],
loc: attr.loc,
}
}
})
}
}
}
import { UrlWithStringQuery, parse as uriParse } from 'url'
import { isString } from '@vue/shared'
export function isRelativeUrl(url: string): boolean {
const firstChar = url.charAt(0)
return firstChar === '.' || firstChar === '~' || firstChar === '@'
}
const externalRE = /^(https?:)?\/\//
export function isExternalUrl(url: string): boolean {
return externalRE.test(url)
}
const dataUrlRE = /^\s*data:/i
export function isDataUrl(url: string): boolean {
return dataUrlRE.test(url)
}
/**
* Parses string url into URL object.
*/
export function parseUrl(url: string): UrlWithStringQuery {
const firstChar = url.charAt(0)
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
return parseUriParts(url)
}
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
* @param urlString an url as a string
*/
function parseUriParts(urlString: string): UrlWithStringQuery {
// A TypeError is thrown if urlString is not a string
// @see https://nodejs.org/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost
return uriParse(isString(urlString) ? urlString : '', false, true)
}
......@@ -15,6 +15,8 @@ import {
TemplateChildNode,
TransformContext,
} from '@vue/compiler-core'
import { createAssetUrlTransformWithOptions } from './transforms/templateTransformAssetUrl'
import { createSrcsetTransformWithOptions } from './transforms/templateTransformSrcset'
export const VUE_REF = 'r'
export const VUE_REF_IN_FOR = 'r-i-f'
......@@ -96,3 +98,33 @@ export function createBindDirectiveNode(
) {
return createDirectiveNode('bind', name, value)
}
export function createUniVueTransformAssetUrls(base: string) {
return {
base,
includeAbsolute: true,
tags: {
audio: ['src'],
video: ['src', 'poster'],
img: ['src'],
image: ['src'],
'cover-image': ['src'],
// h5
'v-uni-audio': ['src'],
'v-uni-video': ['src', 'poster'],
'v-uni-image': ['src'],
'v-uni-cover-image': ['src'],
// nvue
'u-image': ['src'],
'u-video': ['src', 'poster'],
},
}
}
export function getBaseNodeTransforms(base: string) {
const transformAssetUrls = createUniVueTransformAssetUrls(base)
return [
createAssetUrlTransformWithOptions(transformAssetUrls),
createSrcsetTransformWithOptions(transformAssetUrls),
]
}
import { BindingTypes, ElementNode, RootNode } from '@vue/compiler-core'
import { compileTemplate, TemplateCompiler } from '@vue/compiler-sfc'
import {
compileTemplate,
SFCTemplateCompileOptions,
TemplateCompiler,
} from '@vue/compiler-sfc'
import { compile } from '../src'
import * as MPCompiler from '../src'
import { MPErrorCodes } from '../src/errors'
import { CodegenRootNode, CompilerOptions } from '../src/options'
import { BindingComponentTypes } from '../src/transform'
import { createUniVueTransformAssetUrls } from '@dcloudio/uni-cli-shared'
import { getBaseNodeTransforms } from '@dcloudio/uni-cli-shared'
function parseWithElementTransform(
template: string,
......@@ -33,27 +37,37 @@ function parseWithElementTransform(
describe('compiler: element transform', () => {
test(`transformAssetUrls`, () => {
const result = compileTemplate({
source: `<image src="/static/logo.png"/>`,
const options: SFCTemplateCompileOptions = {
filename: 'foo.vue',
id: 'foo',
compiler: MPCompiler as unknown as TemplateCompiler,
compilerOptions: {
mode: 'module',
generatorOpts: {
concise: true,
},
} as any,
transformAssetUrls: {
includeAbsolute: true,
...(createUniVueTransformAssetUrls('/') as Record<string, any>),
},
})
expect(result.code).toBe(`import _imports_0 from '/static/logo.png'
nodeTransforms: getBaseNodeTransforms('/'),
} as CompilerOptions,
transformAssetUrls: false,
} as SFCTemplateCompileOptions
expect(
compileTemplate({
...options,
source: `<image src="/static/logo.png"/>`,
}).code
).toBe(`
export function render(_ctx, _cache) {
return {}
}`)
expect(
compileTemplate({
...options,
source: `<image src="../static/logo.png"/>`,
}).code
).toBe(`
export function render(_ctx, _cache) {
return { a: _imports_0 }
return {}
}`)
})
......
......@@ -5,6 +5,8 @@ import {
UniVitePlugin,
uniPostcssScopedPlugin,
createUniVueTransformAssetUrls,
getBaseNodeTransforms,
isExternalUrl,
} from '@dcloudio/uni-cli-shared'
import { VitePluginUniResolvedOptions } from '..'
......@@ -36,10 +38,6 @@ export function initPluginVueOptions(
const templateOptions = vueOptions.template || (vueOptions.template = {})
templateOptions.transformAssetUrls = createUniVueTransformAssetUrls(
options.base
)
const compilerOptions =
templateOptions.compilerOptions || (templateOptions.compilerOptions = {})
......@@ -53,6 +51,7 @@ export function initPluginVueOptions(
directiveTransforms,
},
} = uniPluginOptions
if (compiler) {
templateOptions.compiler = compiler
}
......@@ -70,6 +69,18 @@ export function initPluginVueOptions(
if (!compilerOptions.nodeTransforms) {
compilerOptions.nodeTransforms = []
}
if (options.platform === 'h5') {
templateOptions.transformAssetUrls = createUniVueTransformAssetUrls(
isExternalUrl(options.base) ? options.base : ''
)
} else {
// 替换内置的 transformAssetUrls 逻辑
templateOptions.transformAssetUrls = {
tags: {},
}
compilerOptions.nodeTransforms.push(...getBaseNodeTransforms(options.base))
}
if (nodeTransforms) {
compilerOptions.nodeTransforms.push(...nodeTransforms)
}
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册