提交 50ef845a 编写于 作者: fxy060608's avatar fxy060608

wip(mp): wxs

上级 f32c4923
import { parseFilterNames } from '../src/filter'
describe('filter', () => {
test(`basic`, () => {
expect(
parseFilterNames(
'wxs',
`<script src="./wx.wxs" module="swipe" lang="wxs"></script>`
)[0]
).toBe('swipe')
})
test(`self close`, () => {
expect(
parseFilterNames(
'wxs',
`<script src="./wx.wxs" module="swipe" lang="wxs"/>`
)[0]
).toBe('swipe')
})
test(`multi filter`, () => {
expect(
parseFilterNames(
'wxs',
`<script module="swipe1" lang="wxs">
module.exports = { message:'hello' }
</script>
<script src="./wx.wxs" module="swipe2" lang="wxs"/>
`
)
).toMatchObject(['swipe1', 'swipe2'])
})
test(`complex`, () => {
expect(
parseFilterNames(
'wxs',
`<template><view/></template>
<script module="swipe1" lang="wxs">
module.exports = { message:'hello' }
</script>
<script src="./wx.wxs" module="swipe2" lang="wxs"/>
<script src="./renderjs.js" module="swipe3" lang="renderjs"/>
<script setup>
const title = '123'
</script>
`
)
).toMatchObject(['swipe1', 'swipe2'])
})
})
......@@ -39,3 +39,23 @@ export function missingModuleName(type: 'wxs' | 'renderjs', code: string) {
${code}
</script>`
}
const moduleRE = /module=["'](.*?)["']/
export function parseFilterNames(lang: string, code: string) {
const names: string[] = []
const scriptTags = code.match(/<script\b[^>]*>/gm)
if (!scriptTags) {
return names
}
const langRE = new RegExp(`lang=["']${lang}["']`)
scriptTags.forEach((scriptTag) => {
if (langRE.test(scriptTag)) {
const matches = scriptTag.match(moduleRE)
if (matches) {
names.push(matches[1])
}
}
})
return names
}
......@@ -13,7 +13,7 @@ export * from './easycom'
export * from './constants'
export * from './preprocess'
export * from './postcss'
export * from './renderjs'
export * from './filter'
export * from './esbuild'
export { M } from './messages'
......
......@@ -3,12 +3,14 @@ export function formatMiniProgramEvent(
{
isCatch,
isCapture,
isComponent,
}: {
isCatch?: boolean
isCapture?: boolean
isComponent?: boolean
}
) {
if (eventName === 'click') {
if (!isComponent && eventName === 'click') {
eventName = 'tap'
}
let eventType = 'bind'
......
......@@ -11,7 +11,7 @@ export interface MiniProgramFilterOptions {
type GenFilterFn = (filter: MiniProgramFilterOptions) => string | void
const templateFilesCache = new Map<string, string>()
const templateFiltersCache = new Map<string, Set<MiniProgramFilterOptions>>()
const templateFiltersCache = new Map<string, MiniProgramFilterOptions[]>()
export function findMiniProgramTemplateFiles(genFilter?: GenFilterFn) {
const files: Record<string, string> = Object.create(null)
......@@ -20,7 +20,7 @@ export function findMiniProgramTemplateFiles(genFilter?: GenFilterFn) {
files[filename] = code
} else {
const filters = getMiniProgramTemplateFilters(filename)
if (filters.length) {
if (filters && filters.length) {
files[filename] =
filters.map((filter) => genFilter(filter)).join(LINEFEED) +
LINEFEED +
......@@ -42,7 +42,7 @@ export function addMiniProgramTemplateFile(filename: string, code: string) {
}
function getMiniProgramTemplateFilters(filename: string) {
return [...(templateFiltersCache.get(filename) || [])]
return templateFiltersCache.get(filename)
}
export function clearMiniProgramTemplateFilter(filename: string) {
......@@ -55,11 +55,13 @@ export function addMiniProgramTemplateFilter(
) {
const filters = templateFiltersCache.get(filename)
if (filters) {
filters.add(filter)
const filterIndex = filters.findIndex((f) => f.id === filter.id)
if (filterIndex > -1) {
filters.splice(filterIndex, 1, filter)
} else {
filters.push(filter)
}
} else {
templateFiltersCache.set(
filename,
new Set<MiniProgramFilterOptions>([filter])
)
templateFiltersCache.set(filename, [filter])
}
}
import { assert } from './testUtils'
describe('compiler: transform wxs', () => {
test('basic', () => {
assert(
`<view :data-threshold="threshold" :change:prop="swipe.showWatch" :prop="is_show" @touchstart="swipe.touchstart"/>`,
`<view data-threshold="{{a}}" change:prop="{{swipe.showWatch}}" prop="{{b}}" bindtouchstart="{{swipe.touchstart}}"/>`,
`(_ctx, _cache) => {
return { a: _ctx.threshold, b: _ctx.is_show }
}`,
{
filters: ['swipe'],
cacheHandlers: true,
}
)
})
test('cacheHandlers', () => {
assert(
`<view :data-threshold="threshold" :change:prop="swipe.showWatch" :prop="is_show" @touchstart="swipe.touchstart"/>`,
`<view data-threshold="{{a}}" change:prop="{{swipe.showWatch}}" prop="{{b}}" bindtouchstart="{{swipe.touchstart}}"/>`,
`(_ctx, _cache) => {
return { a: _ctx.threshold, b: _ctx.is_show }
}`,
{
filters: ['swipe'],
cacheHandlers: true,
}
)
})
})
import fs from 'fs'
import { baseParse } from '@vue/compiler-core'
import { isString, extend } from '@vue/shared'
import { parseFilterNames } from '@dcloudio/uni-cli-shared'
import { generate } from './codegen'
import { CompilerOptions } from './options'
import { DirectiveTransform, NodeTransform, transform } from './transform'
......@@ -44,6 +45,12 @@ export function baseCompile(template: string, options: CompilerOptions = {}) {
prefixIdentifiers,
skipTransformIdentifier: options.skipTransformIdentifier === true,
})
if (options.filename && !options.filters && options.miniProgram?.filter) {
options.filters = parseFilters(
options.miniProgram.filter.lang,
options.filename
)
}
const context = transform(
ast,
extend({}, options, {
......@@ -78,3 +85,11 @@ export function baseCompile(template: string, options: CompilerOptions = {}) {
return result
}
function parseFilters(lang: string, filename: string) {
filename = filename.split('?')[0]
if (fs.existsSync(filename)) {
return parseFilterNames(lang as any, fs.readFileSync(filename, 'utf8'))
}
return []
}
......@@ -46,6 +46,7 @@ export interface TransformOptions
extends SharedTransformCodegenOptions,
ErrorHandlingOptions {
scopeId?: string | null
filters?: string[]
cacheHandlers?: boolean
nodeTransforms?: NodeTransform[]
directiveTransforms?: Record<string, DirectiveTransform | undefined>
......@@ -91,6 +92,9 @@ export interface CodegenOptions extends SharedTransformCodegenOptions {
slot: {
fallback: boolean
}
filter?: {
lang: string
}
directive: string
emitFile?: (emittedFile: EmittedFile) => string
}
......
import { hyphenate } from '@vue/shared'
import { formatMiniProgramEvent } from '@dcloudio/uni-cli-shared'
import {
AttributeNode,
DirectiveNode,
ElementNode,
ElementTypes,
......@@ -245,7 +244,7 @@ function genElement(node: ElementNode, context: TemplateCodegenContext) {
genVFor(node.vFor, node, context)
}
if (props.length) {
genElementProps(props, context)
genElementProps(node, context)
}
if (isSelfClosing) {
......@@ -260,11 +259,11 @@ function genElement(node: ElementNode, context: TemplateCodegenContext) {
}
export function genElementProps(
props: Array<AttributeNode | DirectiveNode>,
node: ElementNode,
context: TemplateCodegenContext
) {
const { push } = context
props.forEach((prop) => {
node.props.forEach((prop) => {
if (prop.type === NodeTypes.ATTRIBUTE) {
const { value } = prop
if (value) {
......@@ -276,14 +275,18 @@ export function genElementProps(
const { name } = prop
push(` `)
if (name === 'on') {
genOn(prop, context)
genOn(prop, node, context)
} else {
genDirectiveNode(prop, context)
}
}
})
}
function genOn(prop: DirectiveNode, { push }: TemplateCodegenContext) {
function genOn(
prop: DirectiveNode,
node: ElementNode,
{ push }: TemplateCodegenContext
) {
const arg = (prop.arg as SimpleExpressionNode).content
const exp = (prop.exp as SimpleExpressionNode).content
const modifiers = prop.modifiers
......@@ -291,6 +294,7 @@ function genOn(prop: DirectiveNode, { push }: TemplateCodegenContext) {
`${formatMiniProgramEvent(arg, {
isCatch: modifiers.includes('stop') || modifiers.includes('prevent'),
isCapture: modifiers.includes('capture'),
isComponent: node.tagType === ElementTypes.COMPONENT,
})}="{{${exp}}}"`
)
}
......
......@@ -16,7 +16,6 @@ import {
RootNode,
ParentNode,
TemplateChildNode,
CREATE_COMMENT,
TO_DISPLAY_STRING,
CompilerError,
helperNameMap,
......@@ -156,7 +155,7 @@ export function traverseNode(
switch (node.type) {
case NodeTypes.COMMENT:
context.helper(CREATE_COMMENT)
// context.helper(CREATE_COMMENT)
break
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
......@@ -215,6 +214,7 @@ export function createTransformContext(
isTS = false,
inline = false,
scopeId = null,
filters = [],
bindingMetadata = EMPTY_OBJ,
cacheHandlers = false,
prefixIdentifiers = false,
......@@ -271,6 +271,7 @@ export function createTransformContext(
isTS,
inline,
scopeId,
filters,
bindingMetadata,
cacheHandlers,
prefixIdentifiers,
......
......@@ -185,7 +185,14 @@ export function processExpression(
const isScopeVarReference = context.identifiers[rawExp]
const isAllowedGlobal = isGloballyWhitelisted(rawExp)
const isLiteral = isLiteralWhitelisted(rawExp)
if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) {
const isFilter = context.filters.includes(rawExp)
if (
!asParams &&
!isScopeVarReference &&
!isAllowedGlobal &&
!isLiteral &&
!isFilter
) {
// const bindings exposed from setup can be skipped for patching but
// cannot be hoisted to module scope
if (bindingMetadata[node.content] === BindingTypes.SETUP_CONST) {
......@@ -231,6 +238,9 @@ export function processExpression(
const ids: QualifiedId[] = []
const parentStack: Node[] = []
const knownIds: Record<string, number> = Object.create(context.identifiers)
context.filters.forEach((name) => {
knownIds[name] = 1
})
walkIdentifiers(
ast,
......
......@@ -17,7 +17,7 @@ import {
import { walk, BaseNode } from 'estree-walker'
import { isUndefined, parseExpr } from '../ast'
import { genBabelExpr, genExpr } from '../codegen'
import { CodegenScope, CodegenVForScope } from '../options'
import { CodegenScope } from '../options'
import { isVForScope, isVIfScope, TransformContext } from '../transform'
export function rewriteSpreadElement(
......@@ -74,6 +74,13 @@ export function rewriteExpression(
return createSimpleExpression('undefined', false, node.loc)
}
// wxs 等表达式
if (context.filters?.length) {
if (isReferencedByIds(babelNode, context.filters)) {
return createSimpleExpression(genExpr(node), false, node.loc)
}
}
scope = findReferencedScope(babelNode, scope)
const id = scope.id.next()
scope.properties.push(objectProperty(identifier(id), babelNode!))
......@@ -96,7 +103,7 @@ function findReferencedScope(
if (isVIfScope(scope)) {
return scope
} else if (isVForScope(scope)) {
if (isReferencedScope(node, scope)) {
if (isReferencedByIds(node, scope.locals)) {
return scope
}
return findReferencedScope(node, scope.parent!)
......@@ -104,8 +111,7 @@ function findReferencedScope(
return scope
}
function isReferencedScope(node: Expression, scope: CodegenVForScope) {
const knownIds: string[] = scope.locals
function isReferencedByIds(node: Expression, knownIds: string[]) {
let referenced = false
walk(node as unknown as BaseNode, {
enter(node: BaseNode, parent: BaseNode) {
......
......@@ -107,7 +107,9 @@ export const transformOn: DirectiveTransform = (
!(isMemberExp && node.tagType === ElementTypes.COMPONENT) &&
// bail if the function references closure variables (v-for, v-slot)
// it must be passed fresh to avoid stale values.
!hasScopeRef(exp, context.identifiers)
!hasScopeRef(exp, context.identifiers) &&
// wxs event
!isFilterExpr(exp, context)
// If the expression is optimizable and is a member expression pointing
// to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus
......@@ -175,7 +177,24 @@ export const transformOn: DirectiveTransform = (
return ret
}
function isFilterExpr(value: ExpressionNode, context: TransformContext) {
if (context.filters.length && value.type === NodeTypes.COMPOUND_EXPRESSION) {
const firstChild = value.children[0] as ExpressionNode
if (
firstChild.type === NodeTypes.SIMPLE_EXPRESSION &&
context.filters.includes(firstChild.content)
) {
return true
}
}
return false
}
export function wrapperVOn(value: ExpressionNode, context: TransformContext) {
// wxs event
if (isFilterExpr(value, context)) {
return value
}
return createCompoundExpression([
`${context.helperString(V_ON)}(`,
value,
......
......@@ -38,8 +38,8 @@ export interface UniMiniProgramPluginOptions {
fallback: boolean
}
filter?: {
lang: string
extname: string
tag: string
generate: Parameters<typeof findMiniProgramTemplateFiles>[0]
}
}
......@@ -71,6 +71,7 @@ export function uniMiniProgramPlugin(
uni: uniOptions({
copyOptions,
miniProgram: {
filter: template.filter ? { lang: template.filter.lang } : undefined,
directive: template.directive,
emitFile,
slot: template.slot,
......@@ -96,10 +97,11 @@ export function uniMiniProgramPlugin(
const extname = template.filter.extname
const filterFiles = getFilterFiles(resolvedConfig, this.getModuleInfo)
Object.keys(filterFiles).forEach((filename) => {
const { code } = filterFiles[filename]
this.emitFile({
type: 'asset',
fileName: filename + extname,
source: filterFiles[filename],
source: code,
})
})
}
......
......@@ -20,7 +20,7 @@ export function getFilterFiles(
resolvedConfig: ResolvedConfig,
getModuleInfo: GetModuleInfo
) {
const filters: Record<string, string> = Object.create(null)
const filters: Record<string, MiniProgramFilterOptions> = Object.create(null)
const filtersCache = getFiltersCache(resolvedConfig)
if (!filtersCache.length) {
return filters
......@@ -37,7 +37,7 @@ export function getFilterFiles(
if (templateFilename !== filterFilename) {
// 外链
filter.src = filterFilename
filters[filterFilename] = filter.code
filters[filterFilename] = filter
}
}
filtersCache.forEach((filter) => {
......
......@@ -14,6 +14,9 @@ export function uniOptions({
slot: {
fallback: boolean
}
filter?: {
lang: string
}
directive: string
emitFile?: (emittedFile: EmittedFile) => string
}
......
......@@ -109,7 +109,7 @@ const options = {
template: {
filter: {
extname: '.wxs',
tag: 'wxs',
lang: 'wxs',
generate(filter) {
if (filter.src) {
return `<wxs src="/${filter.src}.wxs" module="${filter.name}"/>`;
......
......@@ -63,7 +63,7 @@ const options: UniMiniProgramPluginOptions = {
template: {
filter: {
extname: '.wxs',
tag: 'wxs',
lang: 'wxs',
generate(filter) {
if (filter.src) {
return `<wxs src="/${filter.src}.wxs" module="${filter.name}"/>`
......
......@@ -17,3 +17,21 @@ exports[`block normalizeBlockCode 2`] = `
<style></style>
"
`;
exports[`block parseBlockCode 1`] = `
"<template><view><template></template></view></template>
<script>
export default {}
</script>
<style></style>
"
`;
exports[`block parseBlockCode 2`] = `
"<template><view><template v-if=\\"a\\">a</template><template v-else>b</template></view></template>
<script>
export default {}
</script>
<style></style>
"
`;
......@@ -45,3 +45,49 @@ exports[`wxs normalizeWxsCode 2`] = `
</style>
"
`;
exports[`wxs parseWxsCode 1`] = `
"<template><view></view><view></view></template>
<script>
export default {}
</script>
<renderjs name=\\"echarts\\">
export default{
mounted(){
console.log('mounted')
}
}
</renderjs>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
"
`;
exports[`wxs parseWxsCode 2`] = `
"<template><view></view><view></view></template>
<script>
export default {}
</script>
<wxs name=\\"echarts\\">
export default{
mounted(){
console.log('mounted')
}
}
</wxs>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
"
`;
import { parseVue } from '../src/utils'
import { normalizeBlockCode } from '../src/configResolved/plugins/preVue'
import { parseBlockCode } from '../src/configResolved/plugins/preVue'
describe('block', () => {
test('normalizeBlockCode', () => {
test('parseBlockCode', () => {
const blockCode1 = `<template><view><block></block></view></template>
<script>
export default {}
......@@ -10,7 +10,7 @@ describe('block', () => {
<style></style>
`
expect(
normalizeBlockCode(parseVue(blockCode1, []), blockCode1)
parseBlockCode(parseVue(blockCode1, []), blockCode1)
).toMatchSnapshot()
const blockCode2 = `<template><view><block v-if="a">a</block><block v-else>b</block></view></template>
<script>
......@@ -19,7 +19,7 @@ describe('block', () => {
<style></style>
`
expect(
normalizeBlockCode(parseVue(blockCode2, []), blockCode2)
parseBlockCode(parseVue(blockCode2, []), blockCode2)
).toMatchSnapshot()
})
})
import { parseVue } from '../src/utils'
import { normalizeWxsCode } from '../src/configResolved/plugins/preVue'
import {
parseWxsCode,
parseWxsNodes,
} from '../src/configResolved/plugins/preVue'
describe('wxs', () => {
test('normalizeWxsCode', () => {
test('parseWxsCode', () => {
const renderjsCode = `<template><view></view><view></view></template>
<script>
export default {}
......@@ -24,7 +27,7 @@ describe('wxs', () => {
</style>
`
expect(
normalizeWxsCode(parseVue(renderjsCode, []), renderjsCode)
parseWxsCode(parseWxsNodes(parseVue(renderjsCode, [])), renderjsCode)
).toMatchSnapshot()
const wxsCode = `<template><view></view><view></view></template>
<script>
......@@ -46,6 +49,8 @@ describe('wxs', () => {
}
</style>
`
expect(normalizeWxsCode(parseVue(wxsCode, []), wxsCode)).toMatchSnapshot()
expect(
parseWxsCode(parseWxsNodes(parseVue(wxsCode, [])), wxsCode)
).toMatchSnapshot()
})
})
......@@ -11,7 +11,13 @@ import {
} from '@vue/compiler-core'
import { MagicString } from '@vue/compiler-sfc'
import { EXTNAME_VUE, parseVueRequest } from '@dcloudio/uni-cli-shared'
import {
clearMiniProgramTemplateFilter,
EXTNAME_VUE,
normalizeMiniProgramFilename,
parseVueRequest,
removeExt,
} from '@dcloudio/uni-cli-shared'
import { isElementNode, parseVue } from '../../utils'
const debugPreVue = debug('vite:uni:pre-vue')
......@@ -22,12 +28,10 @@ const WXS_LANG_RE = /lang=["|'](renderjs|wxs)["|']/
const WXS_ATTRS = ['wxs', 'renderjs']
const sourceToSFC = new Map<string, string>()
export function uniPreVuePlugin(): Plugin {
return {
name: 'vite:uni-pre-vue',
transform(code, id) {
async transform(code, id) {
const { filename, query } = parseVueRequest(id)
if (query.vue) {
return
......@@ -35,33 +39,42 @@ export function uniPreVuePlugin(): Plugin {
if (!EXTNAME_VUE.includes(path.extname(filename))) {
return
}
const sourceKey = code + filename
const cache = sourceToSFC.get(sourceKey)
if (cache) {
debugPreVue('cache', id)
return {
code: cache,
map: null,
}
}
// 清空当前页面已缓存的 filter 信息
clearMiniProgramTemplateFilter(
removeExt(normalizeMiniProgramFilename(id, process.env.UNI_INPUT_DIR))
)
const hasBlock = BLOCK_RE.test(code)
const hasWxs = WXS_LANG_RE.test(code)
if (!hasBlock && !hasWxs) {
return
}
debugPreVue(id)
const watchFiles: string[] = []
const errors: SyntaxError[] = []
const ast = parseVue(code, errors)
if (hasBlock) {
code = normalizeBlockCode(ast, code)
code = parseBlockCode(ast, code)
}
if (hasWxs) {
code = normalizeWxsCode(ast, code)
const wxsNodes = parseWxsNodes(ast)
code = parseWxsCode(wxsNodes, code)
// add watch
for (const wxsNode of wxsNodes) {
const srcProp = wxsNode.props.find(
(prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === 'src'
) as AttributeNode | undefined
if (srcProp && srcProp.value) {
const resolveId = await this.resolve(srcProp.value.content, id)
if (resolveId) {
watchFiles.push(resolveId.id)
}
}
}
}
// if (errors.length) {
// this.error(errors.join('\n'))
// }
sourceToSFC.set(sourceKey, code)
watchFiles.forEach((file) => this.addWatchFile(file))
return {
code, // 暂不提供sourcemap,意义不大
map: null,
......@@ -91,11 +104,11 @@ function traverseNode(
}
}
export function normalizeBlockCode(ast: RootNode, code: string) {
export function parseBlockCode(ast: RootNode, code: string) {
const blockNodes: ElementNode[] = []
traverseNode(ast, blockNodes)
if (blockNodes.length) {
return normalizeBlockNode(code, blockNodes)
return parseBlockNode(code, blockNodes)
}
return code
}
......@@ -103,7 +116,7 @@ export function normalizeBlockCode(ast: RootNode, code: string) {
const BLOCK_END_LEN = '</block>'.length
const BLOCK_START_LEN = '<block'.length
function normalizeBlockNode(code: string, blocks: ElementNode[]) {
function parseBlockNode(code: string, blocks: ElementNode[]) {
const magicString = new MagicString(code)
blocks.forEach(({ loc }) => {
const startOffset = loc.start.offset
......@@ -118,8 +131,8 @@ function normalizeBlockNode(code: string, blocks: ElementNode[]) {
return magicString.toString()
}
export function normalizeWxsCode(ast: RootNode, code: string) {
const wxsNodes = ast.children.filter(
export function parseWxsNodes(ast: RootNode) {
return ast.children.filter(
(node) =>
node.type === NodeTypes.ELEMENT &&
node.tag === 'script' &&
......@@ -130,9 +143,12 @@ export function normalizeWxsCode(ast: RootNode, code: string) {
prop.value &&
WXS_ATTRS.includes(prop.value.content)
)
)
) as ElementNode[]
}
export function parseWxsCode(wxsNodes: ElementNode[], code: string) {
if (wxsNodes.length) {
code = normalizeWxsNode(code, wxsNodes as ElementNode[])
code = parseWxsNode(code, wxsNodes)
}
return code
}
......@@ -140,7 +156,7 @@ export function normalizeWxsCode(ast: RootNode, code: string) {
const SCRIPT_END_LEN = '</script>'.length
const SCRIPT_START_LEN = '<script'.length
function normalizeWxsNode(code: string, nodes: ElementNode[]) {
function parseWxsNode(code: string, nodes: ElementNode[]) {
const magicString = new MagicString(code)
nodes.forEach(({ loc, props }) => {
const langAttr = props.find((prop) => prop.name === 'lang') as AttributeNode
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册