diff --git a/packages/parser/package.json b/packages/parser/package.json index 438ae6976..6a5edadac 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -34,7 +34,8 @@ "@vtj/coder": "workspace:~", "@vtj/core": "workspace:~", "@vue/compiler-dom": "~3.5.13", - "@vue/compiler-sfc": "~3.5.13" + "@vue/compiler-sfc": "~3.5.13", + "postcss": "~8.5.0" }, "devDependencies": { "@vtj/cli": "workspace:~" diff --git a/packages/parser/src/vue/index.ts b/packages/parser/src/vue/index.ts index 1e6daeb9c..1c996a308 100644 --- a/packages/parser/src/vue/index.ts +++ b/packages/parser/src/vue/index.ts @@ -1,22 +1,43 @@ -import { type BlockSchema, type NodeSchema, BlockModel } from '@vtj/core'; +import { + type BlockSchema, + type NodeSchema, + type Dependencie, + BlockModel +} from '@vtj/core'; import { tsFormatter } from '@vtj/coder'; import { parseSFC, isJSCode } from '../shared'; import { parseTemplate } from './template'; -import { parseScripts } from './scripts'; +import { parseScripts, type ImportStatement } from './scripts'; +import { parseStyle } from './style'; import { patchCode } from './utils'; export interface ParseVueOptions { id: string; name: string; source: string; + dependencies?: Dependencie[]; } export async function parseVue(options: ParseVueOptions) { - const { id, name, source } = options; + const { id, name, source, dependencies = [] } = options; const sfc = parseSFC(source); - const { state, watch, lifeCycles, computed, methods, props, emits, inject } = - parseScripts(sfc.script); - const { nodes, slots, context } = parseTemplate(id, name, sfc.template); + const { styles, css } = parseStyle(sfc.styles.join('\n')); + const { + state, + watch, + lifeCycles, + computed, + methods, + props, + emits, + inject, + handlers, + imports + } = parseScripts(sfc.script); + const { nodes, slots, context } = parseTemplate(id, name, sfc.template, { + handlers, + styles + }); const dsl: BlockSchema = { id, name, @@ -29,16 +50,19 @@ export async function parseVue(options: ParseVueOptions) { methods, slots, emits, - nodes + nodes, + css }; + const computedKeys = Object.keys(computed || {}); + const { libs } = parseDeps(imports, dependencies); await walkDsl(dsl, async (node: NodeSchema) => { await walkNode(node, async (content: any) => { if (isJSCode(content)) { const options = { context, - computed: [], - libs: {} + computed: computedKeys, + libs }; const code = await tsFormatter(content.value); content.value = patchCode(code, node.id as string, options); @@ -88,3 +112,28 @@ async function walkNode(node: NodeSchema, callback: (n: any) => Promise) { }; await walking(node); } + +function parseDeps( + imports: ImportStatement[] = [], + dependencies: Dependencie[] = [] +) { + const libs: Record = {}; + const depsMap = dependencies.reduce( + (prev, current) => { + prev[current.package] = current.library; + return prev; + }, + {} as Record + ); + for (const { from, imports: names } of imports) { + names.forEach((name) => { + const library = depsMap[from]; + if (library) { + libs[name] = library; + } + }); + } + return { + libs + }; +} diff --git a/packages/parser/src/vue/style.ts b/packages/parser/src/vue/style.ts new file mode 100644 index 000000000..f16e65976 --- /dev/null +++ b/packages/parser/src/vue/style.ts @@ -0,0 +1,33 @@ +import postcss from 'postcss'; + +export type CSSRules = Record>; + +export interface ParseStyleResult { + styles: CSSRules; + css: string; +} +export function parseStyle(content: string) { + const styles: CSSRules = {}; + const css: string[] = []; + const root = postcss.parse(content); + const classRegex = /^.[\w]+_[\w]{5,}/; + for (const rule of root.nodes) { + if (rule.type === 'rule') { + const style: Record = {}; + if (classRegex.test(rule.selector)) { + rule.nodes.forEach((decl) => { + if (decl.type === 'decl') { + style[decl.prop] = decl.value; + } + }); + styles[rule.selector] = style; + } else { + css.push(rule.toString()); + } + } + } + return { + styles, + css: css.join('\n') + }; +} diff --git a/packages/parser/src/vue/template.ts b/packages/parser/src/vue/template.ts index b95194960..d1f44e703 100644 --- a/packages/parser/src/vue/template.ts +++ b/packages/parser/src/vue/template.ts @@ -4,7 +4,8 @@ import { type NodeEvents, type JSExpression, type NodeDirective, - type BlockSlot + type BlockSlot, + type JSFunction } from '@vtj/core'; import { compileTemplate } from '@vue/compiler-sfc'; import { @@ -20,13 +21,29 @@ import { import { uid } from '@vtj/base'; import { isJSExpression, isNodeSchema } from '../shared'; import { getJSExpression, getJSFunction } from './utils'; +import type { CSSRules } from './style'; let __slots: BlockSlot[] = []; let __context: Record> = {}; +let __handlers: Record = {}; +let __styles: CSSRules = {}; -export function parseTemplate(id: string, name: string, content: string = '') { +export interface ParseTemplateOptions { + handlers?: Record; + styles?: CSSRules; +} + +export function parseTemplate( + id: string, + name: string, + content: string = '', + options?: ParseTemplateOptions +) { __slots = []; __context = {}; + __handlers = options?.handlers || {}; + __styles = options?.styles || {}; + const result = compileTemplate({ id, filename: name, @@ -64,7 +81,21 @@ function getProps(nodes: Array) { for (const item of nodes) { // 普通属性 if (item.type === NodeTypes.ATTRIBUTE) { - props[item.name] = item.value?.content || ''; + if (item.name === 'class') { + const classValue = item.value?.content || ''; + const classRegex = /[\w]+_[\w]{5,}/; + const selector = classValue.match(classRegex)?.[0] || ''; + const classes = classValue.split(' ').filter((n) => n !== selector); + const style = __styles[`.${selector}`]; + if (style) { + props.style = style; + } + if (classes.length) { + props.class = classes.join(' '); + } + } else { + props[item.name] = item.value?.content || ''; + } } // 动态绑定的属性 @@ -81,7 +112,10 @@ function getProps(nodes: Array) { return props; } -function getEvents(nodes: Array) { +function getEvents( + nodes: Array, + handlers: Record = {} +) { const events: NodeEvents = {}; for (const item of nodes) { // 动态绑定的属性 @@ -97,11 +131,23 @@ function getEvents(nodes: Array) { }, {} as Record ); - events[item.arg.content] = { - name: item.arg.content, - handler: getJSFunction(`(${item.exp?.loc.source})`), - modifiers - }; + const code = item.exp?.loc.source || ''; + const regex = new RegExp(`${item.arg.content}_\[\\w\]\{5\,\}`); + const name = code.match(regex)?.[0] || ''; + const handler = handlers[name]; + if (name && handler) { + events[item.arg.content] = { + name: item.arg.content, + handler, + modifiers + }; + } else { + events[item.arg.content] = { + name: item.arg.content, + handler: getJSFunction(`(${code})`), + modifiers + }; + } } } } @@ -232,7 +278,7 @@ function createNodeSchema( const dsl: NodeSchema = { name: node.tag, props: getProps(node.props), - events: getEvents(node.props), + events: getEvents(node.props, __handlers), directives: getDirectives(scope || node) }; dsl.id = getNodeId(dsl); diff --git a/packages/parser/tests/index.test.ts b/packages/parser/tests/index.test.ts index 257663302..44f7eacc4 100644 --- a/packages/parser/tests/index.test.ts +++ b/packages/parser/tests/index.test.ts @@ -2,11 +2,7 @@ import { expect, test } from 'vitest'; import { tsFormatter } from '@vtj/coder'; import { parseUniApp, parseVue } from '../src'; import { App } from './UniApp'; -import { template1 } from './template'; - -const dependencies = { - 'element-plus': ['ElInput', 'ElButton'] -}; +import { template1, dependencies } from './template'; // test('index', async () => { // const result = parseUniApp(App); @@ -19,7 +15,8 @@ test('template1', async () => { const result = await parseVue({ id: '235w0t1w', name: 'Bbb', - source: template1 + source: template1, + dependencies: dependencies as any }); console.log(JSON.stringify(result, null, 2)); diff --git a/packages/parser/tests/template.ts b/packages/parser/tests/template.ts index 2bc75df0e..8871d0b4a 100644 --- a/packages/parser/tests/template.ts +++ b/packages/parser/tests/template.ts @@ -1,42 +1,197 @@ +export const dependencies = [ + { + package: 'vue', + version: 'latest', + library: 'Vue', + urls: ['@vtj/materials/deps/vue/vue.global.prod.js'], + assetsLibrary: 'VueMaterial', + required: true, + official: true, + enabled: true, + platform: ['web', 'h5'] + }, + { + package: 'vue-router', + version: 'latest', + library: 'VueRouter', + urls: ['@vtj/materials/deps/vue-router/vue-router.global.prod.js'], + assetsLibrary: 'VueRouterMaterial', + required: true, + official: true, + enabled: true + }, + { + package: '@vtj/utils', + version: 'latest', + library: 'VtjUtils', + urls: ['@vtj/materials/deps/@vtj/utils/index.umd.js'], + required: true, + official: true, + enabled: true + }, + { + package: '@vtj/icons', + version: 'latest', + library: 'VtjIcons', + urls: [ + '@vtj/materials/deps/@vtj/icons/style.css', + '@vtj/materials/deps/@vtj/icons/index.umd.js' + ], + required: true, + official: true, + enabled: true, + platform: ['web', 'h5'] + }, + { + package: '@vueuse/core', + version: 'latest', + library: 'VueUse', + urls: [ + '@vtj/materials/deps/@vueuse/shared/index.iife.min.js', + '@vtj/materials/deps/@vueuse/core/index.iife.min.js' + ], + required: false, + official: true, + enabled: true, + platform: ['web', 'h5'] + }, + { + package: 'element-plus', + version: 'latest', + library: 'ElementPlus', + localeLibrary: 'ElementPlusLocaleZhCn', + urls: [ + '@vtj/materials/deps/element-plus/dark/css-vars.css', + '@vtj/materials/deps/element-plus/index.css', + '@vtj/materials/deps/element-plus/zh-cn.js', + '@vtj/materials/deps/element-plus/index.full.min.js' + ], + assetsUrl: '@vtj/materials/assets/element/index.umd.js', + assetsLibrary: 'ElementPlusMaterial', + required: false, + official: true, + enabled: true, + platform: 'web' + }, + { + package: '@vtj/ui', + version: 'latest', + library: 'VtjUI', + urls: [ + '@vtj/materials/deps/vxe-table/style.min.css', + '@vtj/materials/deps/@vtj/ui/style.css', + '@vtj/materials/deps/xe-utils/xe-utils.umd.min.js', + '@vtj/materials/deps/vxe-table/index.umd.min.js', + '@vtj/materials/deps/@vtj/ui/index.umd.js' + ], + assetsUrl: '@vtj/materials/assets/ui/index.umd.js', + assetsLibrary: 'VtjUIMaterial', + required: false, + official: true, + enabled: true, + platform: 'web' + }, + { + package: 'ant-design-vue', + version: 'latest', + library: 'antd', + urls: [ + '@vtj/materials/deps/ant-design-vue/reset.css', + '@vtj/materials/deps/ant-design-vue/dayjs/dayjs.min.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/customParseFormat.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekday.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/localeData.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekOfYear.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekYear.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/advancedFormat.js', + '@vtj/materials/deps/ant-design-vue/dayjs/plugin/quarterOfYear.js', + '@vtj/materials/deps/ant-design-vue/antd.min.js' + ], + assetsUrl: '@vtj/materials/assets/antdv/index.umd.js', + assetsLibrary: 'AntdvMaterial', + required: false, + official: true, + enabled: false, + platform: ['web'] + }, + { + package: '@vtj/charts', + version: 'latest', + library: 'VtjCharts', + urls: [ + '@vtj/materials/deps/echarts/echarts.min.js', + '@vtj/materials/deps/@vtj/charts/index.umd.js' + ], + assetsUrl: '@vtj/materials/assets/charts/index.umd.js', + assetsLibrary: 'VtjChartsMaterial', + required: false, + official: true, + enabled: true, + platform: ['web', 'h5'] + }, + { + package: 'mockjs', + version: 'latest', + library: 'Mock', + urls: ['@vtj/materials/deps/mockjs/mock-min.js'], + required: false, + official: true, + enabled: true + } +]; + export const template1 = ` - + `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ac3621cd..e1a4cac6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,6 +822,9 @@ importers: '@vue/compiler-sfc': specifier: ~3.5.13 version: 3.5.13 + postcss: + specifier: ~8.5.0 + version: 8.5.3 devDependencies: '@vtj/cli': specifier: workspace:~