提交 05995c97 编写于 作者: fxy060608's avatar fxy060608

wip(mp): usingComponents

上级 e858f31c
import path from 'path'
import { ResolvedId } from 'rollup'
import { transformVueComponentImports } from '../src/mp/transformImports'
const root = '/usr/xxx/projects/test/src'
const importer = '/usr/xxx/projects/test/src/pages/index/index.vue'
async function resolve(id: string, importer?: string) {
return {
id: importer ? path.resolve(path.dirname(importer), id) : id,
} as ResolvedId
}
function dynamicImport(name: string, source: string) {
return `const ${name} = ()=>import('${source}')`
}
describe('transformVueComponentImports', () => {
test(`basic`, async () => {
const source = `import test1 from "${root}/components/test1.vue";
const _sfc_main = {
components: {
test1
}
};
const __BINDING_COMPONENTS__ = '{"test1":{"name":"_component_test1","type":"unknown"}}';
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {};
}
import "${importer}?vue&type=style&index=0&lang.css";
import _export_sfc from "plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
`
const { code, usingComponents } = await transformVueComponentImports(
source,
importer,
{
root,
resolve,
dynamicImport,
}
)
expect(code).toContain(
`const test1 = ()=>import('${root}/components/test1.vue')`
)
expect(usingComponents).toMatchObject({ test1: '/components/test1' })
})
test(`easycom`, async () => {
const source = `import test1 from "../../components/test1.vue";
import MyComponentName from "../../components/test1.vue";
const _sfc_main = {
components: {
test1,
MyComponentName
}
};
const __BINDING_COMPONENTS__ = '{"test":{"name":"_easycom_test","type":"unknown"},"test1":{"name":"_component_test1","type":"unknown"},"MyComponentName":{"name":"_component_MyComponentName","type":"unknown"},"my-component-name":{"name":"_component_my_component_name","type":"unknown"}}';
import _easycom_test from "${root}/components/test/test.vue";
if (!Math) {
Math.max.call(Max, _easycom_test);
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {};
}
import "${root}/pages/index/index.vue?vue&type=style&index=0&lang.css";
import _export_sfc from "plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
`
const { code, usingComponents } = await transformVueComponentImports(
source,
importer,
{
root,
resolve,
dynamicImport,
}
)
expect(code).toContain(
`const _easycom_test = ()=>import('${root}/components/test/test.vue')`
)
expect(usingComponents).toMatchObject({
test: '/components/test/test',
test1: '/components/test1',
'my-component-name': '/components/test1',
})
})
test(`PascalCase`, async () => {
const source = `import test1 from "../../components/test1.vue";
import MyComponentName from "../../components/test1.vue";
const _sfc_main = {
components: {
test1,
MyComponentName
}
};
const __BINDING_COMPONENTS__ = '{"test1":{"name":"_component_test1","type":"unknown"},"MyComponentName":{"name":"_component_MyComponentName","type":"unknown"},"my-component-name":{"name":"_component_my_component_name","type":"unknown"}}';
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {};
}
import "${root}/pages/index/index.vue?vue&type=style&index=0&lang.css";
import _export_sfc from "plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
`
const { code, usingComponents } = await transformVueComponentImports(
source,
importer,
{
root,
resolve,
dynamicImport,
}
)
expect(code).toContain(
`const MyComponentName = ()=>import('${root}/components/test1.vue')`
)
expect(usingComponents).toMatchObject({
test1: '/components/test1',
'my-component-name': '/components/test1',
})
})
test(`setup`, async () => {
const source = `import { defineComponent as _defineComponent } from "vue";
const __BINDING_COMPONENTS__ = '{"test1":{"name":"test1","type":"setup"},"MyComponentName":{"name":"MyComponentName","type":"setup"},"my-component-name":{"name":"MyComponentName","type":"setup"}}';
if (!Math) {
Math.max.call(Max, test1, MyComponentName, MyComponentName);
}
import test1 from "../../components/test1.vue";
import MyComponentName from "../../components/test1.vue";
const _sfc_main = /* @__PURE__ */ _defineComponent({
setup(__props) {
return (_ctx, _cache) => {
return {};
};
}
});
import "${root}/pages/index/index.vue?vue&type=style&index=0&lang.css";
export default _sfc_main;
`
const { code, usingComponents } = await transformVueComponentImports(
source,
importer,
{
root,
resolve,
dynamicImport,
}
)
expect(code).toContain(
`const MyComponentName = ()=>import('${root}/components/test1.vue')`
)
expect(usingComponents).toMatchObject({
test1: '/components/test1',
'my-component-name': '/components/test1',
})
})
test(`setup with easycom`, async () => {
const source = `import { defineComponent as _defineComponent } from "vue";
const __BINDING_COMPONENTS__ = '{"test":{"name":"_easycom_test","type":"unknown"},"test1":{"name":"test1","type":"setup"},"MyComponentName":{"name":"MyComponentName","type":"setup"},"my-component-name":{"name":"MyComponentName","type":"setup"}}';
import _easycom_test from "${root}/components/test/test.vue";
if (!Math) {
Math.max.call(Max, _easycom_test, test1, MyComponentName, MyComponentName);
}
import test1 from "../../components/test1.vue";
import MyComponentName from "../../components/test1.vue";
const _sfc_main = /* @__PURE__ */ _defineComponent({
setup(__props) {
return (_ctx, _cache) => {
return {};
};
}
});
import "${root}/pages/index/index.vue?vue&type=style&index=0&lang.css";
export default _sfc_main;
`
const { code, usingComponents } = await transformVueComponentImports(
source,
importer,
{
root,
resolve,
dynamicImport,
}
)
expect(code).toContain(
`const _easycom_test = ()=>import('${root}/components/test/test.vue')`
)
expect(usingComponents).toMatchObject({
test: '/components/test/test',
test1: '/components/test1',
'my-component-name': '/components/test1',
})
})
})
......@@ -4,6 +4,7 @@ export const EXTNAME_VUE = ['.vue', '.nvue']
export const EXTNAME_VUE_RE = /\.(vue|nvue)$/
export const EXTNAME_JS_RE = /\.[jt]sx?$/
export const BINDING_COMPONENTS = '__BINDING_COMPONENTS__'
// APP 平台解析页面后缀的优先级
export const PAGE_EXTNAME_APP = ['.nvue', '.vue', '.tsx', '.jsx', '.js']
// 其他平台解析页面后缀的优先级
......
......@@ -17,7 +17,7 @@ export function initWebpackNVueEntry(pages: UniApp.PagesJsonPageOptions[]) {
if (!path) {
return
}
const subNVuePath = removeExt(path.split('?')[0])
const subNVuePath = removeExt(normalizePath(path.split('?')[0]))
process.UNI_NVUE_ENTRY[subNVuePath] = genWebpackBase64Code(
genNVueEntryCode(subNVuePath)
)
......
export * from './jsonFile'
export { AppJson } from './types'
export { parseMiniProgramPagesJson } from './pages'
export { parseMiniProgramProjectJson } from './project'
import { extend } from '@vue/shared'
import { ComponentJson, PageWindowOptions, UsingComponents } from './types'
import { normalizeNodeModules } from '../../utils'
export const jsonPagesCache = new Map<string, PageWindowOptions>()
export const jsonComponentsCache = new Map<string, ComponentJson>()
export const jsonUsingComponentsCache = new Map<string, UsingComponents>()
let appJsonCache: Record<string, any> = {}
const jsonFilesCache = new Map<string, string>()
const jsonPagesCache = new Map<string, PageWindowOptions>()
const jsonComponentsCache = new Map<string, ComponentJson>()
const jsonUsingComponentsCache = new Map<string, UsingComponents>()
export function addPageJson(filename: string, json: PageWindowOptions) {
export function normalizeJsonFilename(filename: string) {
return normalizeNodeModules(filename)
}
export function findChangedJsonFiles() {
const changedJsonFiles = new Map<string, string>()
function findChangedFile(name: string, json: Record<string, any>) {
const newJson = extend({}, json)
if (!newJson.usingComponents) {
newJson.usingComponents = {}
}
extend(newJson.usingComponents, jsonUsingComponentsCache.get(name))
const jsonStr = JSON.stringify(newJson, null, 2)
if (jsonFilesCache.get(name) !== jsonStr) {
changedJsonFiles.set(name, jsonStr)
jsonFilesCache.set(name, jsonStr)
}
}
function findChangedFiles(jsonsCache: Map<string, any>) {
for (const name of jsonsCache.keys()) {
findChangedFile(name, jsonsCache.get(name))
}
}
findChangedFile('app', appJsonCache)
findChangedFiles(jsonPagesCache)
findChangedFiles(jsonComponentsCache)
return changedJsonFiles
}
export function addMiniProgramAppJson(appJson: Record<string, any>) {
appJsonCache = appJson
}
export function addMiniProgramPageJson(
filename: string,
json: PageWindowOptions
) {
jsonPagesCache.set(filename, json)
}
export function addComponentJson(filename: string, json: ComponentJson) {
export function addMiniProgramComponentJson(
filename: string,
json: ComponentJson
) {
jsonComponentsCache.set(filename, json)
}
export function addUsingComponents(filename: string, json: UsingComponents) {
export function addMiniProgramUsingComponents(
filename: string,
json: UsingComponents
) {
jsonUsingComponentsCache.set(filename, json)
}
export * from './event'
export { findVueComponentImports } from './imports'
export { transformVueComponentImports } from './transformImports'
import { parse, ParserPlugin } from '@babel/parser'
import {
ImportDeclaration,
isIdentifier,
isImportDeclaration,
isObjectExpression,
isObjectProperty,
isStringLiteral,
isVariableDeclaration,
ObjectProperty,
Program,
Statement,
StringLiteral,
} from '@babel/types'
import { camelize, capitalize, hyphenate } from '@vue/shared'
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import { PluginContext } from 'rollup'
import { BINDING_COMPONENTS } from '../constants'
import { normalizeMiniProgramFilename, removeExt } from '../utils'
interface TransformVueComponentImportsOptions {
root: string
resolve: PluginContext['resolve']
dynamicImport: (name: string, source: string) => string
babelParserPlugins?: ParserPlugin[]
}
export async function transformVueComponentImports(
code: string,
importer: string,
{
root,
resolve,
dynamicImport,
babelParserPlugins,
}: TransformVueComponentImportsOptions
): Promise<{
code: string
usingComponents: Record<string, string>
}> {
if (!code.includes(BINDING_COMPONENTS)) {
return { code, usingComponents: {} }
}
const s = new MagicString(code)
const scriptAst = parse(code, {
plugins: [...(babelParserPlugins || [])],
sourceType: 'module',
}).program
const imports = findVueComponentImports(
scriptAst.body,
parseComponents(scriptAst, findBindingComponents(scriptAst.body))
)
const usingComponents: Record<string, string> = {}
for (let i = 0; i < imports.length; i++) {
const {
tag,
import: {
start,
end,
specifiers: [specifier],
source,
},
} = imports[i]
const resolveId = await resolve(source.value, importer)
if (resolveId) {
s.overwrite(
start!,
end!,
dynamicImport(specifier.local.name, resolveId.id) + ';'
)
const componentName = hyphenate(tag)
if (!usingComponents[componentName]) {
usingComponents[componentName] =
'/' + removeExt(normalizeMiniProgramFilename(resolveId.id, root))
}
}
}
return { code: s.toString(), usingComponents }
}
type BindingComponents = Record<
string,
{ tag: string; type: 'unknown' | 'setup' | 'self' }
>
/**
* 解析编译器生成的 bindingComponents
* @param ast
* @returns
*/
function findBindingComponents(ast: Statement[]): BindingComponents {
for (const node of ast) {
if (!isVariableDeclaration(node)) {
continue
}
const declarator = node.declarations[0]
if (
isIdentifier(declarator.id) &&
declarator.id.name === BINDING_COMPONENTS
) {
const bindingComponents = JSON.parse(
(declarator.init as StringLiteral).value
) as Record<string, { name: string; type: 'unknown' | 'setup' | 'self' }>
return Object.keys(bindingComponents).reduce<BindingComponents>(
(bindings, tag) => {
const binding = bindingComponents[tag]
bindings[binding.name] = {
tag,
type: binding.type,
}
return bindings
},
{}
)
}
}
return {}
}
/**
* 从 components 中查找定义的组件,修改 bindingComponents
* @param ast
* @param bindingComponents
*/
function parseComponents(ast: Program, bindingComponents: BindingComponents) {
;(walk as any)(ast, {
enter(child: Node) {
if (!isObjectExpression(child)) {
return
}
const componentsProp = child.properties.find(
(prop) =>
isObjectProperty(prop) &&
isIdentifier(prop.key) &&
prop.key.name === 'components'
) as ObjectProperty
if (!componentsProp) {
return
}
const componentsExpr = componentsProp.value
if (!isObjectExpression(componentsExpr)) {
return
}
componentsExpr.properties.forEach((prop) => {
if (!isObjectProperty(prop)) {
return
}
if (!isIdentifier(prop.key) && !isStringLiteral(prop.key)) {
return
}
if (!isIdentifier(prop.value)) {
return
}
const tag = isIdentifier(prop.key) ? prop.key.name : prop.key.value
const name = findBindingComponent(tag, bindingComponents)
if (name) {
bindingComponents[prop.value.name] = bindingComponents[name]
}
})
},
})
return bindingComponents
}
function findBindingComponent(
tag: string,
bindingComponents: BindingComponents
) {
return Object.keys(bindingComponents).find((name) => {
const componentTag = bindingComponents[name].tag
const camelName = camelize(componentTag)
const PascalName = capitalize(camelName)
return tag === componentTag || tag === camelName || tag === PascalName
})
}
function findVueComponentImports(
ast: Statement[],
bindingComponents: BindingComponents
) {
const imports: { tag: string; import: ImportDeclaration }[] = []
for (let i = 0; i < ast.length; i++) {
const node = ast[i]
if (!isImportDeclaration(node)) {
continue
}
if (node.specifiers.length !== 1) {
continue
}
const { name } = node.specifiers[0].local
if (!bindingComponents[name]) {
continue
}
imports.push({ tag: bindingComponents[name].tag, import: node })
}
return imports
}
......@@ -6,6 +6,7 @@ import { once } from '@dcloudio/uni-shared'
export { default as hash } from 'hash-sum'
import { PAGE_EXTNAME, PAGE_EXTNAME_APP } from './constants'
import { SFCTemplateCompileOptions } from '@vue/compiler-sfc'
export const isWindows = os.platform() === 'win32'
export function normalizePath(id: string): string {
return isWindows ? id.replace(/\\/g, '/') : id
......@@ -42,12 +43,8 @@ export function normalizePagePath(pagePath: string, platform: UniApp.PLATFORM) {
console.error(`${pagePath} not found`)
}
export function removeExt(str: string, ext?: string) {
if (ext) {
const reg = new RegExp(ext.replace(/\./, '\\.') + '$')
return normalizePath(str.replace(reg, ''))
}
return normalizePath(str.replace(/\.\w+$/g, ''))
export function removeExt(str: string) {
return str.split('?')[0].replace(/\.\w+$/g, '')
}
const NODE_MODULES_REGEX = /(\.\.\/)?node_modules/g
......@@ -61,3 +58,36 @@ export function normalizeNodeModules(str: string) {
}
return str
}
export function normalizeMiniProgramFilename(
filename: string,
inputDir?: string
) {
if (!inputDir) {
return normalizeNodeModules(filename)
}
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'],
},
}
}
import { BindingTypes, ElementNode, RootNode } from '@vue/compiler-core'
import { compileTemplate, 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'
function parseWithElementTransform(
template: string,
......@@ -24,12 +27,33 @@ function parseWithElementTransform(
}
describe('compiler: element transform', () => {
test(`transformAssetUrls`, () => {
const result = compileTemplate({
source: `<image src="/static/logo.png"/>`,
filename: 'foo.vue',
id: 'foo',
compiler: MPCompiler as unknown as TemplateCompiler,
compilerOptions: {
mode: 'module',
},
transformAssetUrls: {
includeAbsolute: true,
...(createUniVueTransformAssetUrls('/') as Record<string, any>),
},
})
expect(result.code).toBe(`import _imports_0 from '/static/logo.png'
export function render(_ctx, _cache) {
return { a: _imports_0 }
}`)
})
test('import + resolve component', () => {
const { root } = parseWithElementTransform(`<Foo/>`)
expect((root as CodegenRootNode).bindingComponents).toEqual({
Foo: { name: '_component_Foo', type: BindingComponentTypes.UNKNOWN },
})
// expect(code).toContain(`if (!Math) {Math.max.call(Max, _component_Foo)}`)
// expect(code).toContain(`if (!Math) { Math.max.call(Max, _component_Foo) }`)
})
test('import + resolve component multi', () => {
......@@ -48,7 +72,9 @@ describe('compiler: element transform', () => {
Example: { name: '$setup["Example"]', type: BindingComponentTypes.SETUP },
Test: { name: '_component_Test', type: BindingComponentTypes.SELF },
})
expect(code).toContain(`if (!Math) {Math.max.call(Max, $setup["Example"])}`)
expect(code).toContain(
`if (!Math) { Math.max.call(Max, $setup["Example"]) }`
)
})
test('resolve implcitly self-referencing component', () => {
......@@ -69,7 +95,9 @@ describe('compiler: element transform', () => {
expect((root as CodegenRootNode).bindingComponents).toEqual({
Example: { name: '$setup["Example"]', type: BindingComponentTypes.SETUP },
})
expect(code).toContain(`if (!Math) {Math.max.call(Max, $setup["Example"])}`)
expect(code).toContain(
`if (!Math) { Math.max.call(Max, $setup["Example"]) }`
)
})
test('resolve component from setup bindings (inline)', () => {
......@@ -83,7 +111,7 @@ describe('compiler: element transform', () => {
Example: { name: '_unref(Example)', type: BindingComponentTypes.SETUP },
})
expect(preamble).toContain(
`if (!Math) {Math.max.call(Max, _unref(Example))}`
`if (!Math) { Math.max.call(Max, _unref(Example)) }`
)
})
......@@ -97,7 +125,7 @@ describe('compiler: element transform', () => {
expect((root as CodegenRootNode).bindingComponents).toEqual({
Example: { name: 'Example', type: BindingComponentTypes.SETUP },
})
expect(preamble).toContain(`if (!Math) {Math.max.call(Max, Example)}`)
expect(preamble).toContain(`if (!Math) { Math.max.call(Max, Example) }`)
})
test('resolve namespaced component from setup bindings', () => {
......
import { assert } from './testUtils'
describe('compiler: transform v-show', () => {
test('basic', () => {
assert(
`<view v-show="show"/>`,
`<view hidden="{{!a}}"/>`,
`(_ctx, _cache) => {
return { a: _ctx.show }
}`
)
})
})
import { assert } from './testUtils'
describe('compiler: transform v-slot', () => {
test('default slot', () => {
assert(
`<template v-slot/>`,
`<block />`,
`(_ctx, _cache) => {
return {}
}`
)
})
test('named slot', () => {
assert(
`<template v-slot:header/><template v-slot:default/><template v-slot:footer/>`,
`<block slot="header"/><block slot="default"/><block slot="footer"/>`,
`(_ctx, _cache) => {
return {}
}`
)
})
// TODO 还未实现scoped slot
test('named slot', () => {
assert(
`<template v-slot:default="slotProps"><view>{{ slotProps.item }}</view></template>`,
`<block slot="default"><view>{{a}}</view></block>`,
`(_ctx, _cache) => {
return { a: _toDisplayString(_ctx.slotProps.item), b: slotProps }
}`
)
})
})
......@@ -16,7 +16,11 @@ import { addImportDeclaration, matchEasycom } from '@dcloudio/uni-cli-shared'
import { CodegenOptions, CodegenRootNode } from './options'
import { createObjectExpression } from './ast'
import { BindingComponentTypes, TransformContext } from './transform'
import {
BindingComponentTypes,
ImportItem,
TransformContext,
} from './transform'
interface CodegenContext extends CodegenOptions {
code: string
......@@ -148,28 +152,44 @@ function createCodegenContext(
function genComponentImports(
bindingComponents: TransformContext['bindingComponents'],
{ push }: CodegenContext
{ push, newline }: CodegenContext
) {
const tags = Object.keys(bindingComponents)
const importDeclarations: string[] = []
// 仅记录easycom和setup组件
const components: string[] = []
Object.keys(bindingComponents).forEach((tag) => {
tags.forEach((tag) => {
const { name, type } = bindingComponents[tag]
if (type === BindingComponentTypes.UNKNOWN) {
const source = matchEasycom(tag)
if (source) {
components.push(name)
addImportDeclaration(importDeclarations, name, source)
// 调整为easycom命名
const easycomName = name.replace('component', 'easycom')
bindingComponents[tag].name = easycomName
components.push(easycomName)
addImportDeclaration(importDeclarations, easycomName, source)
}
} else if (type === BindingComponentTypes.SETUP) {
components.push(name)
}
})
importDeclarations.forEach((str) => push(str))
if (components.length) {
push(`if (!Math) {`)
push(`Math.max.call(Max, ${components.map((name) => name).join(', ')})`)
push(`}`)
if (tags.length) {
push(
`const __BINDING_COMPONENTS__ = '` +
JSON.stringify(bindingComponents) +
`'`
)
newline()
importDeclarations.forEach((str) => push(str))
if (importDeclarations.length) {
newline()
}
if (components.length) {
push(`if (!Math) {`)
push(` Math.max.call(Max, ${components.map((name) => name).join(', ')}) `)
push(`}`)
newline()
}
}
}
......@@ -210,6 +230,11 @@ function genModulePreamble(
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
}
genComponentImports(bindingComponents, context)
newline()
if (!inline) {
......@@ -217,6 +242,21 @@ function genModulePreamble(
}
}
function genImports(
importsOptions: ImportItem[],
{ push, newline }: CodegenContext
) {
if (!importsOptions.length) {
return
}
importsOptions.forEach((imports) => {
push(`import `)
push(genExpr(imports.exp))
push(` from '${imports.path}'`)
newline()
})
}
type CodegenNode =
| SimpleExpressionNode
| CompoundExpressionNode
......
import { hyphenate } from '@vue/shared'
import { formatMiniProgramEvent } from '@dcloudio/uni-cli-shared'
import {
AttributeNode,
DirectiveNode,
ElementNode,
ElementTypes,
ExpressionNode,
findProp,
NodeTypes,
......@@ -95,7 +97,10 @@ const tagMap: Record<string, string> = {
}
export function genElement(node: ElementNode, context: TemplateCodegenContext) {
const { children, isSelfClosing, props } = node
const tag = tagMap[node.tag] || node.tag
let tag = tagMap[node.tag] || node.tag
if (node.tagType === ElementTypes.COMPONENT) {
tag = hyphenate(tag)
}
const { push } = context
push(`<${tag}`)
if (isIfElementNode(node)) {
......@@ -166,7 +171,19 @@ function genDirectiveNode(
prop: DirectiveNode,
{ push }: TemplateCodegenContext
) {
const arg = (prop.arg as SimpleExpressionNode).content
const exp = (prop.exp as SimpleExpressionNode).content
push(`${arg}="{{${exp}}}"`)
if (prop.name === 'slot') {
if (prop.arg) {
push(`slot="${(prop.arg as SimpleExpressionNode).content}"`)
}
} else if (prop.name === 'model') {
// TODO
} else if (prop.name === 'show') {
push(`hidden="{{!${(prop.exp as SimpleExpressionNode).content}}}"`)
} else if (prop.arg && prop.exp) {
const arg = (prop.arg as SimpleExpressionNode).content
const exp = (prop.exp as SimpleExpressionNode).content
push(`${arg}="{{${exp}}}"`)
} else {
throw new Error(`unknown directive` + JSON.stringify(prop))
}
}
......@@ -38,6 +38,11 @@ import {
TransformOptions,
} from './options'
export interface ImportItem {
exp: string | ExpressionNode
path: string
}
export type NodeTransform = (
node: RootNode | TemplateChildNode,
context: TransformContext
......@@ -73,6 +78,7 @@ export interface TransformContext
childIndex: number
helpers: Map<symbol, number>
components: Set<string>
imports: ImportItem[]
bindingComponents: Record<
string,
{ type: BindingComponentTypes; name: string }
......@@ -117,6 +123,7 @@ export function transform(root: RootNode, options: TransformOptions) {
// finalize meta information
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.imports = context.imports
root.cached = context.cached
return context
}
......@@ -278,6 +285,7 @@ export function createTransformContext(
childIndex: 0,
helpers: new Map(),
components: new Set(),
imports: [],
bindingComponents: Object.create(null),
cached: 0,
identifiers,
......
import { uniMiniProgramPlugin, UniMiniProgramPluginOptions } from './plugin'
import { uniComponentPlugin } from './plugins/component'
import { uniUsingComponentsPlugin } from './plugins/usingComponents'
import { uniMainJsPlugin } from './plugins/mainJs'
import { uniManifestJsonPlugin } from './plugins/manifestJson'
import { uniPagesJsonPlugin } from './plugins/pagesJson'
import { uniVirtualPlugin } from './plugins/virtual'
import { uniEntryPlugin } from './plugins/entry'
import { SFCScriptCompileOptions } from '@vue/compiler-sfc'
export { UniMiniProgramPluginOptions } from './plugin'
export default (options: UniMiniProgramPluginOptions) => {
return [
uniMainJsPlugin(options),
uniManifestJsonPlugin(options),
uniPagesJsonPlugin(options),
uniVirtualPlugin(options),
uniEntryPlugin(options),
uniMiniProgramPlugin(options),
uniComponentPlugin(),
(options: {
vueOptions?: { script?: Partial<SFCScriptCompileOptions> }
}) => {
return uniUsingComponentsPlugin(options.vueOptions?.script)
},
]
}
......@@ -14,7 +14,7 @@ import {
isUniPageUrl,
parseVirtualComponentPath,
parseVirtualPagePath,
} from '../plugins/virtual'
} from '../plugins/entry'
export function buildOptions(): UserConfig['build'] {
const inputDir = process.env.UNI_INPUT_DIR
......
import path from 'path'
import { Plugin } from 'vite'
import {
EXTNAME_VUE,
parseVueRequest,
findVueComponentImports,
} from '@dcloudio/uni-cli-shared'
import MagicString from 'magic-string'
import { virtualComponentPath } from './virtual'
export function uniComponentPlugin(): Plugin {
return {
name: 'vite:uni-mp-component',
async transform(code, id) {
const { filename, query } = parseVueRequest(id)
if (query.vue) {
return null
}
if (!EXTNAME_VUE.includes(path.extname(filename))) {
return null
}
const vueComponentImports = await findVueComponentImports(
code,
id,
this.resolve
)
if (!vueComponentImports.length) {
return null
}
const s = new MagicString(code)
const rewriteImports: string[] = []
vueComponentImports.forEach(({ n, ss, se, i }) => {
s.remove(ss, se)
rewriteImports.push(
`const ${i} = ()=>import('${virtualComponentPath(n!)}')`
)
})
s.prepend(rewriteImports.join(';') + ';\n')
return s.toString()
},
}
}
import path from 'path'
import { normalizePath } from '@dcloudio/uni-cli-shared'
import {
addMiniProgramComponentJson,
normalizeMiniProgramFilename,
normalizePath,
removeExt,
} from '@dcloudio/uni-cli-shared'
import { Plugin } from 'vite'
import { UniMiniProgramPluginOptions } from '../plugin'
......@@ -37,7 +42,7 @@ export function isUniComponentUrl(id: string) {
return id.startsWith(uniComponentPrefix)
}
export function uniVirtualPlugin({
export function uniEntryPlugin({
global,
}: UniMiniProgramPluginOptions): Plugin {
const inputDir = process.env.UNI_INPUT_DIR
......@@ -64,6 +69,10 @@ ${global}.createPage(Page)`,
path.resolve(inputDir, parseVirtualComponentPath(id))
)
this.addWatchFile(filepath)
addMiniProgramComponentJson(
removeExt(normalizeMiniProgramFilename(filepath, inputDir)),
{ component: true }
)
return {
code: `import Component from '${filepath}'
${global}.createComponent(Component)`,
......
import fs from 'fs'
import path from 'path'
import debug from 'debug'
import { Plugin } from 'vite'
import {
......@@ -7,17 +8,20 @@ import {
defineUniPagesJsonPlugin,
getLocaleFiles,
normalizePagePath,
normalizeNodeModules,
parseManifestJsonOnce,
parseMiniProgramPagesJson,
addMiniProgramPageJson,
addMiniProgramAppJson,
findChangedJsonFiles,
} from '@dcloudio/uni-cli-shared'
import { virtualPagePath } from './virtual'
import { virtualPagePath } from './entry'
import { UniMiniProgramPluginOptions } from '../plugin'
const debugPagesJson = debug('vite:uni:pages-json')
export function uniPagesJsonPlugin(
options: UniMiniProgramPluginOptions
): Plugin {
let appJson: AppJson
return defineUniPagesJsonPlugin((opts) => {
return {
name: 'vite:uni-mp-pages-json',
......@@ -32,21 +36,21 @@ export function uniPagesJsonPlugin(
this.addWatchFile(filepath)
})
const manifestJson = parseManifestJsonOnce(inputDir)
const res = parseMiniProgramPagesJson(code, process.env.UNI_PLATFORM, {
debug: !!manifestJson.debug,
darkmode:
options.app.darkmode &&
fs.existsSync(path.resolve(inputDir, 'theme.json')),
networkTimeout: manifestJson.networkTimeout,
subpackages: options.app.subpackages,
})
appJson = res.appJson
Object.keys(res.pageJsons).forEach((name) => {
this.emitFile({
fileName: normalizeNodeModules(name) + '.json',
type: 'asset',
source: JSON.stringify(res.pageJsons[name], null, 2),
})
const { appJson, pageJsons } = parseMiniProgramPagesJson(
code,
process.env.UNI_PLATFORM,
{
debug: !!manifestJson.debug,
darkmode:
options.app.darkmode &&
fs.existsSync(path.resolve(inputDir, 'theme.json')),
networkTimeout: manifestJson.networkTimeout,
subpackages: options.app.subpackages,
}
)
addMiniProgramAppJson(appJson)
Object.keys(pageJsons).forEach((name) => {
addMiniProgramPageJson(name, pageJsons[name])
})
return {
code: `import './manifest.json.js'\n` + importPagesCode(appJson),
......@@ -54,13 +58,14 @@ export function uniPagesJsonPlugin(
}
},
generateBundle() {
if (appJson) {
findChangedJsonFiles().forEach((value, key) => {
debugPagesJson('json.changed', key)
this.emitFile({
fileName: `app.json`,
type: 'asset',
source: JSON.stringify(appJson, null, 2),
fileName: key + '.json',
source: value,
})
}
})
},
}
})
......
import path from 'path'
import { Plugin } from 'vite'
import { SFCScriptCompileOptions } from '@vue/compiler-sfc'
import {
EXTNAME_VUE,
parseVueRequest,
transformVueComponentImports,
normalizeMiniProgramFilename,
addMiniProgramUsingComponents,
removeExt,
} from '@dcloudio/uni-cli-shared'
import { virtualComponentPath } from './entry'
export function uniUsingComponentsPlugin(
options: Partial<SFCScriptCompileOptions> = {}
): Plugin {
return {
name: 'vite:uni-mp-using-component',
async transform(source, id) {
const { filename, query } = parseVueRequest(id)
if (query.vue) {
return null
}
if (!EXTNAME_VUE.includes(path.extname(filename))) {
return null
}
const inputDir = process.env.UNI_INPUT_DIR
const { code, usingComponents } = await transformVueComponentImports(
source,
id,
{
root: inputDir,
resolve: this.resolve,
dynamicImport,
babelParserPlugins: options.babelParserPlugins,
}
)
addMiniProgramUsingComponents(
removeExt(normalizeMiniProgramFilename(id, inputDir)),
usingComponents
)
return {
code,
map: this.getCombinedSourcemap(),
}
},
}
}
function dynamicImport(name: string, value: string) {
return `const ${name} = ()=>import('${virtualComponentPath(value)}')`
}
......@@ -93,7 +93,8 @@ export default function uniPlugin(
const uniPlugins = initExtraPlugins(
process.env.UNI_CLI_CONTEXT || process.cwd(),
(process.env.UNI_PLATFORM as UniApp.PLATFORM) || 'h5'
(process.env.UNI_PLATFORM as UniApp.PLATFORM) || 'h5',
options
)
debugUni(uniPlugins)
......
......@@ -8,6 +8,7 @@ import type {
UniVitePlugin,
} from '@dcloudio/uni-cli-shared'
import { TemplateCompiler } from '@vue/compiler-sfc'
import { VitePluginUniResolvedOptions } from '..'
interface PluginConfig {
id: string
......@@ -68,20 +69,40 @@ export function initPluginUniOptions(UniVitePlugins: UniVitePlugin[]) {
}
}
export function initExtraPlugins(cliRoot: string, platform: UniApp.PLATFORM) {
return initPlugins(resolvePlugins(cliRoot, platform))
export function initExtraPlugins(
cliRoot: string,
platform: UniApp.PLATFORM,
options: VitePluginUniResolvedOptions
) {
return initPlugins(resolvePlugins(cliRoot, platform), options)
}
function initPlugin({ id, config: { main } }: PluginConfig): Plugin | void {
const plugin = require(path.join(id, main || '/lib/uni.plugin.js'))
return plugin.default || plugin
function initPlugin(
{ id, config: { main } }: PluginConfig,
options: VitePluginUniResolvedOptions
): Plugin | void {
let plugin = require(path.join(id, main || '/lib/uni.plugin.js'))
plugin = plugin.default || plugin
if (isFunction(plugin)) {
plugin = plugin(options)
}
return plugin
}
function initPlugins(plugins: PluginConfig[]): Plugin[] {
function initPlugins(
plugins: PluginConfig[],
options: VitePluginUniResolvedOptions
): Plugin[] {
return plugins
.map((plugin) => initPlugin(plugin))
.map((plugin) => initPlugin(plugin, options))
.flat()
.filter<Plugin>(Boolean as any)
.map((plugin) => {
if (isFunction(plugin)) {
return plugin(options)
}
return plugin
})
}
function resolvePlugins(cliRoot: string, platform: UniApp.PLATFORM) {
......
import { extend, hasOwn, isArray, isPlainObject } from '@vue/shared'
import { SFCTemplateCompileOptions, TemplateCompiler } from '@vue/compiler-sfc'
import { TemplateCompiler } from '@vue/compiler-sfc'
import { isCustomElement } from '@dcloudio/uni-shared'
import {
EXTNAME_VUE_RE,
UniVitePlugin,
uniPostcssScopedPlugin,
createUniVueTransformAssetUrls,
} from '@dcloudio/uni-cli-shared'
import { VitePluginUniResolvedOptions } from '..'
......@@ -12,29 +13,6 @@ import { transformMatchMedia } from './transforms/transformMatchMedia'
import { createTransformEvent } from './transforms/transformEvent'
// import { transformContext } from './transforms/transformContext'
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 function initPluginVueOptions(
options: VitePluginUniResolvedOptions,
UniVitePlugins: UniVitePlugin[],
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册