mirror of
https://gitee.com/newgateway/vtj.git
synced 2026-05-13 00:16:21 +08:00
feat: ✨ parser style
This commit is contained in:
@@ -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:~"
|
||||
|
||||
@@ -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<void>) {
|
||||
};
|
||||
await walking(node);
|
||||
}
|
||||
|
||||
function parseDeps(
|
||||
imports: ImportStatement[] = [],
|
||||
dependencies: Dependencie[] = []
|
||||
) {
|
||||
const libs: Record<string, string> = {};
|
||||
const depsMap = dependencies.reduce(
|
||||
(prev, current) => {
|
||||
prev[current.package] = current.library;
|
||||
return prev;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
for (const { from, imports: names } of imports) {
|
||||
names.forEach((name) => {
|
||||
const library = depsMap[from];
|
||||
if (library) {
|
||||
libs[name] = library;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
libs
|
||||
};
|
||||
}
|
||||
|
||||
33
packages/parser/src/vue/style.ts
Normal file
33
packages/parser/src/vue/style.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import postcss from 'postcss';
|
||||
|
||||
export type CSSRules = Record<string, Record<string, string>>;
|
||||
|
||||
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<string, string> = {};
|
||||
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')
|
||||
};
|
||||
}
|
||||
@@ -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<string, Set<string>> = {};
|
||||
let __handlers: Record<string, JSFunction> = {};
|
||||
let __styles: CSSRules = {};
|
||||
|
||||
export function parseTemplate(id: string, name: string, content: string = '') {
|
||||
export interface ParseTemplateOptions {
|
||||
handlers?: Record<string, JSFunction>;
|
||||
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<AttributeNode | DirectiveNode>) {
|
||||
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<AttributeNode | DirectiveNode>) {
|
||||
return props;
|
||||
}
|
||||
|
||||
function getEvents(nodes: Array<AttributeNode | DirectiveNode>) {
|
||||
function getEvents(
|
||||
nodes: Array<AttributeNode | DirectiveNode>,
|
||||
handlers: Record<string, JSFunction> = {}
|
||||
) {
|
||||
const events: NodeEvents = {};
|
||||
for (const item of nodes) {
|
||||
// 动态绑定的属性
|
||||
@@ -97,11 +131,23 @@ function getEvents(nodes: Array<AttributeNode | DirectiveNode>) {
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
<template>
|
||||
<XPanel
|
||||
v-for="(item, index) in 3"
|
||||
header="标题"
|
||||
@click.stop="(...args: any[]) => click_13mntm28({ item, index }, args)">
|
||||
@click.stop="(...args: any[]) => click_13mxuu2q({ item, index }, args)">
|
||||
<div class="my-div div_193l8saav">
|
||||
<span> {{ item }}</span>
|
||||
</div></XPanel
|
||||
>
|
||||
<ElButton type="primary" @click="click_13mph5o7"> 按钮</ElButton>
|
||||
<ElButton type="primary" @click="click_33mxuu2q"> 按钮</ElButton>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
import { XPanel } from '@vtj/ui';
|
||||
import { ElButton } from 'element-plus';
|
||||
import { dateFormat } from '@vtj/utils';
|
||||
import { useProvider } from '@vtj/renderer';
|
||||
export default defineComponent({
|
||||
name: 'Bbb',
|
||||
components: { XPanel, ElButton },
|
||||
setup(props) {
|
||||
const provider = useProvider({ id: '13dbje0g', version: '1743564663400' });
|
||||
const provider = useProvider({ id: '13dbje0g', version: '1743578537999' });
|
||||
const state = reactive({});
|
||||
return { state, props, provider };
|
||||
return { state, props, provider, dateFormat };
|
||||
},
|
||||
methods: {
|
||||
click_13mntm28({ item, index }, args) {
|
||||
click_13mxuu2q({ item, index }, args) {
|
||||
return (() => {
|
||||
console.log('click panel!', item);
|
||||
}).apply(this, args);
|
||||
},
|
||||
click_13mph5o7(e) {
|
||||
click_33mxuu2q(e) {
|
||||
console.log('click button!', e);
|
||||
console.log(dateFormat(new Date(), 'YYYY-MM-DD'));
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.my-div {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.div_193l8saav {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user