From 06cebfa2d742424ad8c20580b3b0581775879365 Mon Sep 17 00:00:00 2001 From: chilingling Date: Sat, 2 Dec 2023 02:31:37 -0800 Subject: [PATCH 01/37] feat(vue-dsl): add app generate code --- packages/vue-generator/README.md | 11 ++ .../src/generator/generateApp.js | 40 +++++ .../src/templates/vue-template/index.js | 98 ++++++++++++ .../vue-template/templateFiles/.gitignore | 13 ++ .../vue-template/templateFiles/README.md | 19 +++ .../vue-template/templateFiles/index.html | 13 ++ .../vue-template/templateFiles/packageJson.js | 31 ++++ .../vue-template/templateFiles/src/App.vue | 11 ++ .../templateFiles/src/http/axios.js | 139 ++++++++++++++++++ .../templateFiles/src/http/config.js | 15 ++ .../templateFiles/src/http/index.js | 27 ++++ .../templateFiles/src/lowcodeConfig/bridge.js | 13 ++ .../src/lowcodeConfig/dataSource.js | 102 +++++++++++++ .../src/lowcodeConfig/lowcode.js | 82 +++++++++++ .../vue-template/templateFiles/src/main.js | 20 +++ .../vue-template/templateFiles/vite.config.js | 23 +++ 16 files changed, 657 insertions(+) create mode 100644 packages/vue-generator/README.md create mode 100644 packages/vue-generator/src/generator/generateApp.js create mode 100644 packages/vue-generator/src/templates/vue-template/index.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/README.md create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/index.html create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/vite.config.js diff --git a/packages/vue-generator/README.md b/packages/vue-generator/README.md new file mode 100644 index 0000000000..0391c6c091 --- /dev/null +++ b/packages/vue-generator/README.md @@ -0,0 +1,11 @@ +# @opentiny/tiny-engine-dsl-vue + +> 将 schema 转换成具体的,可读性高,可维护的代码 + + +TODO: + +- [ ] 架构支持配置出码 +- [ ] 抽取通用底层能力,支持用户自定义插件,自定义出码结果 +- [ ] 官方提供更多内置出码方案 + diff --git a/packages/vue-generator/src/generator/generateApp.js b/packages/vue-generator/src/generator/generateApp.js new file mode 100644 index 0000000000..f927898304 --- /dev/null +++ b/packages/vue-generator/src/generator/generateApp.js @@ -0,0 +1,40 @@ +import { generateTemplate } from '../templates/vue-template' + +const templateMap = { + default: generateTemplate +} + +// function + +function generateI18n() {} + +function generateDataSource() {} + +function generatePageOrComponent() {} + +function generateRouter() {} + +function generateDependencies() {} + +/** + * 整体应用出码 + */ +export function generateApp(config, appSchema) { + // 预处理 app schema + + // 初始化模板 + const { staticTemplate } = config + + if (typeof staticTemplate === 'function') { + staticTemplate({}) + } + + // 国际化出码 + + // 数据源出码 + // 页面出码 + // 区块出码 + // utils 工具类出码 + // 路由出码 + // 依赖出码 +} diff --git a/packages/vue-generator/src/templates/vue-template/index.js b/packages/vue-generator/src/templates/vue-template/index.js new file mode 100644 index 0000000000..7d20896b00 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/index.js @@ -0,0 +1,98 @@ +import readmeFile from './templateFiles/README.md?raw' +import viteConfigFile from './templateFiles/vite.config.js?raw' +import getPackageJson from './templateFiles/packageJson' +import gitIgnoreFile from './templateFiles/.gitignore?raw' +import entryHTMLFile from './templateFiles/index.html?raw' +import mainJSFile from './templateFiles/src/main.js?raw' +import appVueFile from './templateFiles/src/App.vue?raw' +import bridgeFile from './templateFiles/src/lowcodeConfig/bridge.js?raw' +import dataSourceFile from './templateFiles/src/lowcodeConfig/dataSource.js?raw' +import lowcodeJSFile from './templateFiles/src/lowcodeConfig/lowcode.js?raw' +import axiosFile from './templateFiles/src/http/axios.js?raw' +import axiosConfigFile from './templateFiles/src/http/config.js?raw' +import httpEntryFile from './templateFiles/src/http/index.js?raw' + +const getTemplate = (context, str) => { + return str.replace(/(\$\$TinyEngine{(.*)}END\$)/g, function (match, p1, p2) { + if (!p2) { + return '' + } + const keyArr = p2.split('.') + const value = keyArr.reduce((preVal, key) => preVal?.[key] ?? '', context) + return value + }) +} + +/** + * get project template + * @returns + */ +export function generateTemplate(context) { + return [ + { + fileName: 'README.md', + paths: '.', + fileContent: getTemplate(context, readmeFile) + }, + { + fileName: 'vite.config.js', + paths: '.', + fileContent: getTemplate(context, viteConfigFile) + }, + { + fileName: 'package.json', + paths: '.', + fileContent: getPackageJson(context) + }, + { + fileName: '.gitignore', + paths: '.', + fileContent: getTemplate(context, gitIgnoreFile) + }, + { + fileName: 'index.html', + paths: '.', + fileContent: getTemplate(context, entryHTMLFile) + }, + { + fileName: 'main.js', + paths: './src', + fileContent: getTemplate(context, mainJSFile) + }, + { + fileName: 'App.vue', + paths: './src', + fileContent: getTemplate(context, appVueFile) + }, + { + fileName: 'bridge.js', + paths: './src/lowcodeConfig', + fileContent: bridgeFile + }, + { + fileName: 'dataSource.js', + paths: './src/lowcodeConfig', + fileContent: dataSourceFile + }, + { + fileName: 'lowcode.js', + paths: './src/lowcodeConfig', + fileContent: lowcodeJSFile + }, + { + fileName: 'axios.js', + paths: './src/http', + fileContent: axiosFile + }, + { + fileName: 'config.js', + paths: './src/http', + fileContent: axiosConfigFile + }, + { + fileName: 'index.js', + paths: './src/http', + fileContent: httpEntryFile + } + ] +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore b/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore new file mode 100644 index 0000000000..9961aac9d4 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/.gitignore @@ -0,0 +1,13 @@ +node_modules +dist/ + +# local env files +.env.local +.env.*.local + +# Editor directories and files +.vscode +.idea + +yarn.lock +package-lock.json \ No newline at end of file diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/README.md b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md new file mode 100644 index 0000000000..70e735d7b2 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md @@ -0,0 +1,19 @@ +## $TinyEngine{appName}END$ + +本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。 + +## 使用 + +安装依赖: + +```bash +npm install +``` + +本地启动项目: + +```bash +npm run dev +``` + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/index.html b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html new file mode 100644 index 0000000000..13b675be42 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html @@ -0,0 +1,13 @@ + + + + + + + ${appName} + + +
+ + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js new file mode 100644 index 0000000000..e0daa765c4 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js @@ -0,0 +1,31 @@ +// 这里 package.json 格式设置为 js,避免被识别成一个 package +export default (context) => { + const packageName = context?.appName || '@opentiny/tiny-engine-preview-vue' + + return { + name: packageName, + version: '1.0.0', + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview' + }, + main: 'dist/index.js', + module: 'dist/index.js', + dependencies: { + '@opentiny/tiny-engine-i18n-host': '^1.0.0', + '@opentiny/vue': '^3.10.0', + axios: '^0.21.1', + 'axios-mock-adapter': '^1.19.0', + vue: '^3.3.9', + 'vue-i18n': '^9.2.0-beta.3', + 'vue-router': '^4.2.5', + pinia: '^2.1.7' + }, + devDependencies: { + '@vitejs/plugin-vue': '^4.5.1', + '@vitejs/plugin-vue-jsx': '^3.1.0', + vite: '^4.3.7' + } + } +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue b/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue new file mode 100644 index 0000000000..72b6032dea --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js new file mode 100644 index 0000000000..4b2d6e4208 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/axios.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +export default (config) => { + const instance = axios.create(config) + const defaults = {} + let mock + + if (typeof MockAdapter.prototype.proxy === 'undefined') { + MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) { + let stream = this + const request = (proxy, any) => { + return (setting) => { + return new Promise((resolve) => { + config.responseType = 'json' + axios + .get(any ? proxy + setting.url + '.json' : proxy, config) + .then(({ data }) => { + /* eslint-disable no-useless-call */ + typeof handleData === 'function' && (data = handleData.call(null, data, setting)) + resolve([200, data]) + }) + .catch((error) => { + resolve([error.response.status, error.response.data]) + }) + }) + } + } + + if (url === '*' && proxy && typeof proxy === 'string') { + stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true)) + } else { + if (proxy && typeof proxy === 'string') { + stream = this.onAny(url).reply(request(proxy)) + } else if (typeof response === 'function') { + stream = this.onAny(url).reply(response) + } + } + + return stream + } + } + + return { + request(config) { + return instance(config) + }, + get(url, config) { + return instance.get(url, config) + }, + delete(url, config) { + return instance.delete(url, config) + }, + head(url, config) { + return instance.head(url, config) + }, + post(url, data, config) { + return instance.post(url, data, config) + }, + put(url, data, config) { + return instance.put(url, data, config) + }, + patch(url, data, config) { + return instance.patch(url, data, config) + }, + all(iterable) { + return axios.all(iterable) + }, + spread(callback) { + return axios.spread(callback) + }, + defaults(key, value) { + if (key && typeof key === 'string') { + if (typeof value === 'undefined') { + return instance.defaults[key] + } + instance.defaults[key] = value + defaults[key] = value + } else { + return instance.defaults + } + }, + defaultSettings() { + return defaults + }, + interceptors: { + request: { + use(fnHandle, fnError) { + return instance.interceptors.request.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.request.eject(id) + } + }, + response: { + use(fnHandle, fnError) { + return instance.interceptors.response.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.response.eject(id) + } + } + }, + mock(config) { + if (!mock) { + mock = new MockAdapter(instance) + } + + if (Array.isArray(config)) { + config.forEach((item) => { + mock.proxy(item) + }) + } + + return mock + }, + disableMock() { + mock && mock.restore() + mock = undefined + }, + isMock() { + return typeof mock !== 'undefined' + }, + CancelToken: axios.CancelToken, + isCancel: axios.isCancel + } +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js new file mode 100644 index 0000000000..cfa3714e17 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/config.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export default { + withCredentials: false +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js new file mode 100644 index 0000000000..b0a08546a6 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/http/index.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from './axios' +import config from './config' + +export default (dataHandler) => { + const http = axios(config) + + http.interceptors.response.use(dataHandler, (error) => { + const response = error.response + if (response.status === 403 && response.headers && response.headers['x-login-url']) { + // TODO 处理无权限时,重新登录再发送请求 + } + }) + + return http +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js new file mode 100644 index 0000000000..7a19e4a116 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/bridge.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export default () => {} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js new file mode 100644 index 0000000000..14d13a17e7 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/dataSource.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import useHttp from '../http' +import dataSources from './dataSource.json' + +const dataSourceMap = {} + +// 暂时使用 eval 解析 JSON 数据里的函数 +const createFn = (fnContent) => { + return (...args) => { + // eslint-disable-next-line no-eval + window.eval('var fn = ' + fnContent) + // eslint-disable-next-line no-undef + return fn.apply(this, args) + } +} + +const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res) => res + +const load = (http, options, dataSource, shouldFetch) => (params, customUrl) => { + // 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理 + if (!options) { + return globalDataHandle(dataSource.config.data) + } + + if (!shouldFetch()) { + return + } + + dataSource.status = 'loading' + + const { method, uri: url, params: defaultParams, timeout, headers } = options + const config = { method, url, headers, timeout } + + const data = params || defaultParams + + config.url = customUrl || config.url + + if (method.toLowerCase() === 'get') { + config.params = data + } else { + config.data = data + } + + return http.request(config) +} + +dataSources.list.forEach((config) => { + const http = useHttp(globalDataHandle) + const dataSource = { config } + + dataSourceMap[config.id] = dataSource + + const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true + const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (options) => options + + const dataHandler = (res) => { + const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res + dataSource.status = 'loaded' + dataSource.data = data + return data + } + + const errorHandler = (error) => { + config.errorHandler?.value && createFn(config.errorHandler.value)(error) + dataSource.status = 'error' + dataSource.error = error + } + + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + + if (import.meta.env.VITE_APP_MOCK === 'mock') { + http.mock([ + { + url: config.options?.uri, + response() { + return Promise.resolve([200, { data: config.data }]) + } + }, + { + url: '*', + proxy: '*' + } + ]) + } + + dataSource.status = 'init' + dataSource.load = load(http, config.options, dataSource, shouldFetch) +}) + +export default dataSourceMap diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js new file mode 100644 index 0000000000..03f152c26d --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { getCurrentInstance, nextTick, provide, inject } from 'vue' +import { useRouter, useRoute } from 'vue-router' +import { I18nInjectionKey } from 'vue-i18n' +import dataSourceMap from './dataSource' +import * as utils from '../utils' +import * as bridge from './bridge' + +export const lowcodeWrap = (props, context) => { + const global = {} + const instance = getCurrentInstance() + const router = useRouter() + const route = useRoute() + const { t, locale } = inject(I18nInjectionKey).global + const emit = context.emit + const ref = (ref) => instance.refs[ref] + + const setState = (newState, callback) => { + Object.assign(global.state, newState) + nextTick(() => callback.apply(global)) + } + + const getLocale = () => locale.value + const setLocale = (val) => { + locale.value = val + } + + const location = () => window.location + const history = () => window.history + + Object.defineProperties(global, { + props: { get: () => props }, + emit: { get: () => emit }, + setState: { get: () => setState }, + router: { get: () => router }, + route: { get: () => route }, + i18n: { get: () => t }, + getLocale: { get: () => getLocale }, + setLocale: { get: () => setLocale }, + location: { get: location }, + history: { get: history }, + utils: { get: () => utils }, + bridge: { get: () => bridge }, + dataSourceMap: { get: () => dataSourceMap }, + $: { get: () => ref } + }) + + const wrap = (fn) => { + if (typeof fn === 'function') { + return (...args) => fn.apply(global, args) + } + + Object.entries(fn).forEach(([name, value]) => { + Object.defineProperty(global, name, { + get: () => value + }) + }) + + fn.t = t + + return fn + } + + return wrap +} + +export default () => { + const i18n = inject(I18nInjectionKey) + provide(I18nInjectionKey, i18n) + return { t: i18n.global.t, lowcodeWrap } +} diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js new file mode 100644 index 0000000000..c4574461b3 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/main.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { createApp } from 'vue' +import router from './router' +import { createPinia } from 'pinia' +import App from './App.vue' + +const pinia = createPinia() + +createApp(App).use(pinia).use(router).mount('#app') diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/vite.config.js b/packages/vue-generator/src/templates/vue-template/templateFiles/vite.config.js new file mode 100644 index 0000000000..e1e57978b1 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import path from 'path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + plugins: [vue(), vueJsx()], + define: { + 'process.env': { ...process.env } + }, + build: { + minify: true, + commonjsOptions: { + transformMixedEsModules: true + }, + cssCodeSplit: false + } +}) From d16a07943256b7504047bfc58cbb275e4e8412ba Mon Sep 17 00:00:00 2001 From: chilingling Date: Thu, 21 Dec 2023 05:05:44 -0800 Subject: [PATCH 02/37] feat(vue-dsl): add app generate code --- .../src/generator/codeGenerator.js | 118 ++++++++++++++++ .../src/generator/generateApp.js | 128 ++++++++++++++---- packages/vue-generator/src/generator/index.js | 20 +-- .../src/plugins/genBlockPlugin.js | 57 ++++++++ .../src/plugins/genDataSourcePlugin.js | 52 +++++++ .../src/plugins/genDependenciesPlugin.js | 61 +++++++++ .../src/plugins/genI18nPlugin.js | 75 ++++++++++ .../src/plugins/genPagePlugin.js | 87 ++++++++++++ .../src/plugins/genRouterPlugin.js | 78 +++++++++++ .../src/plugins/genTemplatePlugin.js | 32 +++++ .../src/plugins/genUtilsPlugin.js | 94 +++++++++++++ packages/vue-generator/src/plugins/index.js | 8 ++ packages/vue-generator/src/templates/index.js | 5 + .../src/templates/vue-template/index.js | 6 + .../src/utils/generateImportStatement.js | 16 +++ .../vue-generator/src/utils/mergeOptions.js | 29 ++++ 16 files changed, 830 insertions(+), 36 deletions(-) create mode 100644 packages/vue-generator/src/generator/codeGenerator.js create mode 100644 packages/vue-generator/src/plugins/genBlockPlugin.js create mode 100644 packages/vue-generator/src/plugins/genDataSourcePlugin.js create mode 100644 packages/vue-generator/src/plugins/genDependenciesPlugin.js create mode 100644 packages/vue-generator/src/plugins/genI18nPlugin.js create mode 100644 packages/vue-generator/src/plugins/genPagePlugin.js create mode 100644 packages/vue-generator/src/plugins/genRouterPlugin.js create mode 100644 packages/vue-generator/src/plugins/genTemplatePlugin.js create mode 100644 packages/vue-generator/src/plugins/genUtilsPlugin.js create mode 100644 packages/vue-generator/src/plugins/index.js create mode 100644 packages/vue-generator/src/templates/index.js create mode 100644 packages/vue-generator/src/utils/generateImportStatement.js create mode 100644 packages/vue-generator/src/utils/mergeOptions.js diff --git a/packages/vue-generator/src/generator/codeGenerator.js b/packages/vue-generator/src/generator/codeGenerator.js new file mode 100644 index 0000000000..8d385cf748 --- /dev/null +++ b/packages/vue-generator/src/generator/codeGenerator.js @@ -0,0 +1,118 @@ +class CodeGenerator { + config = {} + genResult = [] + plugins = [] + genLogs = [] + schema = {} + parsedSchema = {} + context = {} + constructor(config) { + this.config = config + this.plugins = config.plugins + } + getContext() { + return { + config: this.config, + genResult: this.genResult, + plugins: this.plugins, + genLogs: this.genLogs, + schema: this.schema, + parsedSchema: this.parsedSchema, + ...this.context + } + } + getPluginsByHook(hookName) { + const res = [] + + for (const pluginItem of this.plugins) { + if (typeof pluginItem[hookName] === 'function') { + res.push(pluginItem[hookName]) + } + } + + return res + } + async generate(schema) { + const hooks = ['transformStart', 'parseConfig', 'parseSchema', 'transform', 'transformEnd'] + let err = null + + this.schema = schema + + try { + for (const hookItem of hooks) { + const plugins = this.getPluginsByHook(hookItem) + + await this[hookItem](plugins) + } + } catch (error) { + err = error + } finally { + const plugins = this.getPluginsByHook('transformEnd') + await this.transformEnd(plugins, err) + } + + return { + genResult: this.genResult, + genLogs: this.genLogs + } + } + async transformStart(plugins) { + for (const pluginItem of plugins) { + await pluginItem.apply(this, [this.config]) + } + } + async parseConfig(plugins) { + for (const pluginItem of plugins) { + const newConfig = await pluginItem.apply(this, [this.config]) + + if (newConfig) { + this.config = newConfig + } + } + } + + async parseSchema(plugins) { + for (const pluginItem of plugins) { + const parseResult = await pluginItem.apply(this, [this.schema]) + + if (!parseResult?.id || !parseResult?.result) { + continue + } + + this.parsedSchema[parseResult.id] = parseResult.result + } + } + async transform(plugins) { + for (const pluginItem of plugins) { + const transformRes = await pluginItem.apply(this, [this.parsedSchema]) + + if (!transformRes) { + return + } + + if (Array.isArray(transformRes)) { + this.genResult.push(...transformRes) + } else { + this.genResult.push(transformRes) + } + } + } + async transformEnd(plugins, err) { + for (const pluginItem of plugins) { + await pluginItem.apply(this, [err]) + } + } + replaceGenResult(resultItem) { + const { path, fileName } = resultItem + + const index = this.genResult.findIndex((item) => item.path === path && item.fileName === fileName) + + if (index === -1) { + return + } + + this.genResult.splice(index, 1, resultItem) + } +} + +export default CodeGenerator diff --git a/packages/vue-generator/src/generator/generateApp.js b/packages/vue-generator/src/generator/generateApp.js index f927898304..042770fcc0 100644 --- a/packages/vue-generator/src/generator/generateApp.js +++ b/packages/vue-generator/src/generator/generateApp.js @@ -1,40 +1,116 @@ -import { generateTemplate } from '../templates/vue-template' +import { generateTemplate as genDefaultStaticTemplate } from '../templates/vue-template' +import { + genBlockPlugin, + genDataSourcePlugin, + genDependenciesPlugin, + genI18nPlugin, + genPagePlugin, + genRouterPlugin, + genTemplatePlugin, + genUtilsPlugin +} from '../plugins' +import CodeGenerator from './codeGenerator' -const templateMap = { - default: generateTemplate +const inputMock = { + // 应用相关配置信息 + //config: {}, + // 应用相关的 meta 信息 + appMeta: {}, + // 页面区块信息 + componentsTree: [], + blockList: [], + // 数据源信息 + dataSource: [], + // i18n 信息 + i18n: {}, + // utils 信息 + utils: [], + // 全局状态 + globalState: [] } -// function +// TODO 解析整个应用用到的区块 +// 1. 解析页面中用到的区块 +// 2. 解析区块中用到的区块 -function generateI18n() {} - -function generateDataSource() {} +const transformedSchema = { + // 整体应用 meta 信息 + appMeta: { + name: 'test' + }, + // 需要生成的页面 + pageCode: [ + { + // 类型是页面 + // type: 'PAGE', + // 类型是区块 + // type: 'BLOCK', + // 页面 meta 信息 + meta: {}, + // schema 信息,如果是 文件夹,则不需要 + schema: {} + // ... + } + ], + dataSource: {}, + i18n: {}, + routes: {}, + utils: {}, + globalState: [ + { + actions: {}, + getters: {}, + id: '', + state: {} + } + ] +} -function generatePageOrComponent() {} +// 预处理输入的 schema,转换为标准的格式 +function transformSchema(appSchema) { + const { appMeta, pageCode, dataSource, i18n, utils, globalState } = appSchema -function generateRouter() {} + const routes = pageCode.map(({ meta: { isHome, router }, fileName }) => ({ + fileName, + isHome, + path: router.startsWith('/') ? router : `/${router}` + })) -function generateDependencies() {} + const hasRoot = routes.some(({ path }) => path === '/') -/** - * 整体应用出码 - */ -export function generateApp(config, appSchema) { - // 预处理 app schema + if (!hasRoot && routes.length) { + const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path } - // 初始化模板 - const { staticTemplate } = config + routes.unshift({ path: '/', redirect: homePath }) + } - if (typeof staticTemplate === 'function') { - staticTemplate({}) + return { + appMeta, + pageCode, + dataSource, + i18n, + utils, + globalState, + routes } +} - // 国际化出码 +/** + * 整体应用出码 + */ +export async function generateApp(appSchema) { + const codeGenInstance = new CodeGenerator({ + plugins: [ + genBlockPlugin(), + genDataSourcePlugin(), + genDependenciesPlugin(), + genI18nPlugin(), + genPagePlugin(), + genRouterPlugin(), + genTemplatePlugin(), + genUtilsPlugin() + ] + }) - // 数据源出码 - // 页面出码 - // 区块出码 - // utils 工具类出码 - // 路由出码 - // 依赖出码 + return codeGenInstance.generate(appSchema) } diff --git a/packages/vue-generator/src/generator/index.js b/packages/vue-generator/src/generator/index.js index e852c70c08..68a33c499e 100644 --- a/packages/vue-generator/src/generator/index.js +++ b/packages/vue-generator/src/generator/index.js @@ -1,14 +1,14 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ import { generateCode, generateBlocksCode, generatePageCode } from './page' diff --git a/packages/vue-generator/src/plugins/genBlockPlugin.js b/packages/vue-generator/src/plugins/genBlockPlugin.js new file mode 100644 index 0000000000..38a548a99b --- /dev/null +++ b/packages/vue-generator/src/plugins/genBlockPlugin.js @@ -0,0 +1,57 @@ +import { mergeOptions } from '../utils/mergeOptions' +import { generatePageCode } from '../generator/page' + +const defaultOption = { + blockBasePath: './src/components' +} + +function genBlockPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { blockBasePath } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-block', + description: 'transform block schema to code', + parseSchema(schema) { + const { blockHistories } = schema + const blockSchema = blockHistories.map((block) => block?.content).filter((schema) => typeof schema === 'object') + + return { + id: 'blocks', + result: blockSchema + } + }, + transform(transformedSchema) { + const { blocks } = transformedSchema + + const resBlocks = [] + + for (const block of blocks) { + const res = generatePageCode({ + pageInfo: { schema: block, name: block.componentName }, + componentsMap: this.schema.componentsMap + }) + + const { errors, ...restInfo } = res[0] + + if (errors?.length > 0) { + this.genLogs.push(...errors) + continue + } + + const { panelName, panelValue } = restInfo + + resBlocks.push({ + fileName: panelName, + path: blockBasePath, + fileContent: panelValue + }) + } + + return resBlocks + } + } +} + +export default genBlockPlugin diff --git a/packages/vue-generator/src/plugins/genDataSourcePlugin.js b/packages/vue-generator/src/plugins/genDataSourcePlugin.js new file mode 100644 index 0000000000..84d02e4439 --- /dev/null +++ b/packages/vue-generator/src/plugins/genDataSourcePlugin.js @@ -0,0 +1,52 @@ +import { mergeOptions } from '../utils/mergeOptions' + +const defaultOption = { + fileName: 'dataSource.json', + path: './src' +} + +function genDataSourcePlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, fileName } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-datasource', + description: 'transform schema to dataSource plugin', + parseSchema(schema) { + return { + id: 'dataSource', + result: schema?.dataSource || {} + } + }, + transform(transformedSchema) { + const dataSource = transformedSchema.dataSource + + const { dataHandler, errorHandler, willFetch, list } = dataSource || {} + + const data = { + list: list.map(({ id, name, data }) => ({ id, name, ...data })) + } + + if (dataHandler) { + data.dataHandler = dataHandler + } + + if (errorHandler) { + data.errorHandler = errorHandler + } + + if (willFetch) { + data.willFetch = willFetch + } + + return { + fileName, + path, + fileContent: JSON.stringify(data) + } + } + } +} + +export default genDataSourcePlugin diff --git a/packages/vue-generator/src/plugins/genDependenciesPlugin.js b/packages/vue-generator/src/plugins/genDependenciesPlugin.js new file mode 100644 index 0000000000..e9ef5f7791 --- /dev/null +++ b/packages/vue-generator/src/plugins/genDependenciesPlugin.js @@ -0,0 +1,61 @@ +import { mergeOptions } from '../utils/mergeOptions' + +const defaultOption = { + fileName: 'package.json', + path: '.' +} + +function genDependenciesPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, fileName } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-dependencies', + description: 'transform dependencies to package.json', + parseSchema(schema) { + const { utils } = schema + + const utilsDependencies = {} + + for (const { + type, + content: { package: packageName, version } + } of utils) { + if (type !== 'npm') { + continue + } + + utilsDependencies[packageName] = version || 'latest' + } + + return { + id: 'dependencies', + result: utilsDependencies + } + }, + transform(transformedSchema) { + const { dependencies } = transformedSchema + const originPackageItem = this.genResult.find((item) => item.fileName === fileName && item.path === path) + + if (!originPackageItem) { + return { + fileName, + path, + fileContent: JSON.stringify({ dependencies }) + } + } + + let originPackageJSON = JSON.parse(originPackageItem.fileContent) + + originPackageJSON.dependencies = { + ...originPackageJSON.dependencies, + ...dependencies + } + + this.replaceGenResult({ fileName, path, fileContent: JSON.stringify(originPackageJSON) }) + } + } +} + +export default genDependenciesPlugin diff --git a/packages/vue-generator/src/plugins/genI18nPlugin.js b/packages/vue-generator/src/plugins/genI18nPlugin.js new file mode 100644 index 0000000000..0d6e46cee5 --- /dev/null +++ b/packages/vue-generator/src/plugins/genI18nPlugin.js @@ -0,0 +1,75 @@ +import { mergeOptions } from '../utils/mergeOptions' +import { generateImportStatement } from '../utils/generateImportStatement' + +const defaultOption = { + localeFileName: 'locale.js', + entryFileName: 'index.js', + path: './src/i18n' +} + +function genI18nPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, localeFileName, entryFileName } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-i18n', + description: 'transform i18n schema to i18n code plugin', + parseSchema(schema) { + return { + id: 'i18n', + result: schema?.i18n || [] + } + }, + transform(transformedSchema) { + const { i18n } = transformedSchema || {} + + const res = [] + + // 生成国际化词条文件 + for (const [key, value] of Object.entries(i18n)) { + res.push({ + fileName: `${key}.json`, + path, + fileContent: JSON.stringify(value, null, 2) + }) + } + + const langs = Object.keys(i18n) + const importStatements = langs.map((lang) => + generateImportStatement({ moduleName: `./${lang}.json`, exportName: lang }) + ) + + // 生成 locale.js + res.push({ + fileName: localeFileName, + path, + fileContent: ` + ${importStatements.join('\n')} + + export default { ${langs.join(',')} } + ` + }) + + // 生成 index.js 入口文件 + res.push({ + fileName: entryFileName, + path, + fileContent: ` + import i18n from '@opentiny/tiny-engine-i18n-host' + import lowcode from '../lowcode' + import locale from './${localeFileName}' + + i18n.lowcode = lowcode + ${langs.map((langItem) => `i18n.global.mergeLocaleMessage('${langItem}', locale.${langItem})`).join('\n')} + + export default i18n + ` + }) + + return res + } + } +} + +export default genI18nPlugin diff --git a/packages/vue-generator/src/plugins/genPagePlugin.js b/packages/vue-generator/src/plugins/genPagePlugin.js new file mode 100644 index 0000000000..40193f68c0 --- /dev/null +++ b/packages/vue-generator/src/plugins/genPagePlugin.js @@ -0,0 +1,87 @@ +import { mergeOptions } from '../utils/mergeOptions' +import { generatePageCode } from '../generator/page' + +const defaultOption = { + pageBasePath: './src/views' +} + +function genPagePlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { pageBasePath } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-page', + description: 'transform page schema to code', + parseSchema(schema) { + const { componentsTree } = schema + const pagesMap = {} + const resPageTree = [] + + for (const componentItem of componentsTree) { + pagesMap[componentItem.id] = componentItem + } + + for (const componentItem of componentsTree) { + if (componentItem.componentName === 'Folder') { + continue + } + + const newComponentItem = { + ...componentItem + } + let path = pageBasePath + let curParentId = componentItem.meta.parentId + let depth = 0 + + while (curParentId !== '0' && depth < 1000) { + const preFolder = pagesMap[curParentId] + + path += `/${preFolder.folderName}` + curParentId = preFolder.parentId + depth++ + } + + newComponentItem.path = path + + resPageTree.push(newComponentItem) + } + + return { + id: 'pages', + result: resPageTree + } + }, + transform(transformedSchema) { + const { pages } = transformedSchema + + const resPage = [] + + for (const page of pages) { + const res = generatePageCode({ + pageInfo: { schema: page, name: page.componentName }, + componentsMap: this.schema.componentsMap + }) + + const { errors, ...restInfo } = res[0] + + if (errors?.length > 0) { + this.genLogs.push(...errors) + continue + } + + const { panelName, panelValue } = restInfo + + resPage.push({ + fileName: panelName, + path: page.path, + fileContent: panelValue + }) + } + + return resPage + } + } +} + +export default genPagePlugin diff --git a/packages/vue-generator/src/plugins/genRouterPlugin.js b/packages/vue-generator/src/plugins/genRouterPlugin.js new file mode 100644 index 0000000000..918d0207eb --- /dev/null +++ b/packages/vue-generator/src/plugins/genRouterPlugin.js @@ -0,0 +1,78 @@ +import { mergeOptions } from '../utils/mergeOptions' + +const defaultOption = { + fileName: 'index.js', + path: './src/router' +} + +function genRouterPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, fileName } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-router', + description: 'transform router schema to router code plugin', + parseSchema(schema) { + const { pageCode } = schema + + const routes = pageCode.map(({ meta: { isHome, router }, fileName }) => ({ + fileName, + isHome, + path: router.startsWith('/') ? router : `/${router}` + })) + + const hasRoot = routes.some(({ path }) => path === '/') + + if (!hasRoot && routes.length) { + const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path } + + routes.unshift({ path: '/', redirect: homePath }) + } + + return { + id: 'routes', + result: routes + } + }, + transform(transformedSchema) { + const { routes: routesList } = transformedSchema || {} + + // TODO: 支持 hash 模式、history 模式 + const importSnippet = "import { createRouter, createWebHashHistory } from 'vue-router'" + const exportSnippet = ` + export default createRouter({ + history: createWebHashHistory(), + routes + }) + ` + const routes = routesList.map(({ fileName, path, redirect, filePath }) => { + const routeItem = { + path + } + + if (redirect) { + routeItem.redirect = redirect + } + + if (fileName) { + routeItem.component = `() => import('${filePath}')` + } + + return JSON.stringify(routeItem) + }) + + const routeSnippets = `const routes = [${routes.join(',')}]` + + const res = { + fileName, + path, + fileContent: `${importSnippet}\n ${routeSnippets} \n ${exportSnippet}` + } + + return res + } + } +} + +export default genRouterPlugin diff --git a/packages/vue-generator/src/plugins/genTemplatePlugin.js b/packages/vue-generator/src/plugins/genTemplatePlugin.js new file mode 100644 index 0000000000..6331ac17e2 --- /dev/null +++ b/packages/vue-generator/src/plugins/genTemplatePlugin.js @@ -0,0 +1,32 @@ +import { mergeOptions } from '../utils/mergeOptions' +import { templateMap } from '../templates' + +const defaultOption = {} + +function genTemplatePlugin(options = {}) { + // 保留,用作拓展配置用途 + const realOptions = mergeOptions(defaultOption, options) + + return { + name: 'tinyengine-plugin-generatecode-template', + description: 'generate template code', + transform() { + const meta = this.schema.appMeta + const { template } = meta + + if (!template) { + return + } + + if (typeof template === 'function') { + return template(meta) + } + + if (templateMap[template]) { + return templateMap[template](meta) + } + } + } +} + +export default genTemplatePlugin diff --git a/packages/vue-generator/src/plugins/genUtilsPlugin.js b/packages/vue-generator/src/plugins/genUtilsPlugin.js new file mode 100644 index 0000000000..ad92fc4d0a --- /dev/null +++ b/packages/vue-generator/src/plugins/genUtilsPlugin.js @@ -0,0 +1,94 @@ +import { mergeOptions } from '../utils/mergeOptions' +import { generateImportStatement } from '../utils/generateImportStatement' + +const defaultOption = { + fileName: 'utils.js', + path: './src' +} + +function genUtilsPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, fileName } = realOptions + + const handleNpmUtils = (utilsConfig) => { + const { content } = utilsConfig + const { package: packageName, exportName, destructuring, subName } = content + + const statement = generateImportStatement({ moduleName: packageName, exportName, alias: subName, destructuring }) + let realExportName = exportName + + if (subName) { + realExportName = subName + } + + return { + res: statement, + exportName: realExportName + } + } + + const handleFunctionUtils = (utilsConfig) => { + const { content, name } = utilsConfig + + return { + res: `const ${name} = ${content.value}`, + exportName: name + } + } + + return { + name: 'tinyengine-plugin-generatecode-utils', + description: 'transform utils schema to utils code', + parseSchema(schema) { + const { utils } = schema + + return { + id: 'utils', + result: utils || [] + } + }, + transform(transformedSchema) { + const { utils } = transformedSchema + + if (!Array.isArray(utils)) { + return + } + + const importStatements = [] + const variableStatements = [] + const exportVariables = [] + + const utilsHandlerMap = { + npm: handleNpmUtils, + function: handleFunctionUtils + } + + for (const utilItem of utils) { + const { res, exportName } = utilsHandlerMap[utilItem.type](utilItem) + + if (utilItem.type === 'function') { + variableStatements.push(res) + } else { + importStatements.push(res) + } + + exportVariables.push(exportName) + } + + const fileContent = ` + ${importStatements.join('\n')}\n + ${variableStatements.join('\n')}\n + export { ${exportVariables.join(',')} } + ` + + return { + fileName, + path, + fileContent + } + } + } +} + +export default genUtilsPlugin diff --git a/packages/vue-generator/src/plugins/index.js b/packages/vue-generator/src/plugins/index.js new file mode 100644 index 0000000000..de2a8eefdd --- /dev/null +++ b/packages/vue-generator/src/plugins/index.js @@ -0,0 +1,8 @@ +export { default as genDataSourcePlugin } from './genDataSourcePlugin' +export { default as genBlockPlugin } from './genBlockPlugin' +export { default as genDependenciesPlugin } from './genDependenciesPlugin' +export { default as genPagePlugin } from './genPagePlugin' +export { default as genRouterPlugin } from './genRouterPlugin' +export { default as genUtilsPlugin } from './genUtilsPlugin' +export { default as genI18nPlugin } from './genI18nPlugin' +export { default as genTemplatePlugin } from './genTemplatePlugin' diff --git a/packages/vue-generator/src/templates/index.js b/packages/vue-generator/src/templates/index.js new file mode 100644 index 0000000000..7450fb42c9 --- /dev/null +++ b/packages/vue-generator/src/templates/index.js @@ -0,0 +1,5 @@ +import { generateTemplate as genDefaultStaticTemplate } from './templates/vue-template' + +export const templateMap = { + default: genDefaultStaticTemplate +} diff --git a/packages/vue-generator/src/templates/vue-template/index.js b/packages/vue-generator/src/templates/vue-template/index.js index 7d20896b00..ca8f561977 100644 --- a/packages/vue-generator/src/templates/vue-template/index.js +++ b/packages/vue-generator/src/templates/vue-template/index.js @@ -12,6 +12,12 @@ import axiosFile from './templateFiles/src/http/axios.js?raw' import axiosConfigFile from './templateFiles/src/http/config.js?raw' import httpEntryFile from './templateFiles/src/http/index.js?raw' +/** + * 模板写入动态内容 + * @param {*} context + * @param {*} str + * @returns + */ const getTemplate = (context, str) => { return str.replace(/(\$\$TinyEngine{(.*)}END\$)/g, function (match, p1, p2) { if (!p2) { diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js new file mode 100644 index 0000000000..5a9588b014 --- /dev/null +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -0,0 +1,16 @@ +// TODO: 支持4种 import 的形式 +export function generateImportStatement(config) { + const { moduleName, exportName, alias, destructuring } = config + + let statementName = `${exportName}` + + if (alias && alias !== exportName) { + statementName = `${exportName} as ${alias}` + } + + if (destructuring) { + statementName = `{ ${statementName} }` + } + + return `import ${statementName} from ${moduleName}` +} diff --git a/packages/vue-generator/src/utils/mergeOptions.js b/packages/vue-generator/src/utils/mergeOptions.js new file mode 100644 index 0000000000..2f83e42da3 --- /dev/null +++ b/packages/vue-generator/src/utils/mergeOptions.js @@ -0,0 +1,29 @@ +function isObject(target) { + return Object.prototype.toString.call(target) === '[object Object]' +} + +export const mergeOptions = (originOptions, newOptions) => { + if (!isObject(originOptions) || !isObject(newOptions)) { + return originOptions + } + + const res = {} + + for (const [key, value] of Object.entries(originOptions)) { + if (!Object.prototype.hasOwnProperty.call(newOptions, key)) { + res[key] = value + } + + if (isObject(value) && isObject(newOptions[key])) { + res[key] = mergeOptions(value, newOptions[key]) + } + } + + for (const [key, value] of Object.entries(newOptions)) { + if (!Object.prototype.hasOwnProperty.call(res, key)) { + res[key] = value + } + } + + return res +} From 7153387665e9dccf3ef0bc0e983c23dfc6dfb664 Mon Sep 17 00:00:00 2001 From: chilingling Date: Mon, 25 Dec 2023 22:41:50 -0800 Subject: [PATCH 03/37] feat(vue-dsl): add generate global store code --- .../src/plugins/genGlobalState.js | 81 +++++++++++++++++++ .../src/lowcodeConfig/lowcode.js | 6 +- .../templateFiles/src/lowcodeConfig/store.js | 13 +++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/vue-generator/src/plugins/genGlobalState.js create mode 100644 packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js diff --git a/packages/vue-generator/src/plugins/genGlobalState.js b/packages/vue-generator/src/plugins/genGlobalState.js new file mode 100644 index 0000000000..586121317a --- /dev/null +++ b/packages/vue-generator/src/plugins/genGlobalState.js @@ -0,0 +1,81 @@ +import { mergeOptions } from '../utils/mergeOptions' + +const defaultOption = { + fileName: '', + path: './src/stores' +} + +function genDependenciesPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path } = realOptions + + return { + name: 'tinyengine-plugin-generatecode-globalState', + description: 'transform schema to globalState', + parseSchema(schema) { + let { global_state } = schema + + if (!Array.isArray(global_state)) { + global_state = [] + } + + return { + id: 'globalState', + result: global_state + } + }, + transform(transformedSchema) { + const { globalState } = transformedSchema + + const res = [] + const ids = [] + + for (const stateItem of globalState) { + let importStatement = "import { defineStore } from 'pinia'" + const { id, state, getters, actions } = stateItem + + ids.push(id) + + const stateExpression = `() => ({ ${Object.entries(state) + .map((item) => item.join(':')) + .join(',')} })` + + const getterExpression = Object.entries(getters) + .filter((item) => item.value?.type === 'JSFunction') + .map(([key, value]) => `${key}: ${value.value}`) + .join(',') + + const actionExpressions = Object.entries(actions) + .filter((item) => item.value?.type === 'JSFunction') + .map(([key, value]) => `${key}: ${value.value}`) + .join(',') + + const storeFiles = ` + ${importStatement} + export const ${id} = defineStore({ + id: ${id}, + state: ${stateExpression}, + getters: { ${getterExpression} }, + actions: { ${actionExpressions} } + }) + ` + res.push({ + fileName: `${id}.js`, + path, + fileContent: storeFiles + }) + } + + res.push({ + fileName: 'index.js', + path, + fileContent: ids.map((item) => `export { ${item} } from './${item}'`).join('\n') + }) + + return res + } + } +} + +export default genDependenciesPlugin diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js index 03f152c26d..29da8186b5 100644 --- a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/lowcode.js @@ -16,6 +16,7 @@ import { I18nInjectionKey } from 'vue-i18n' import dataSourceMap from './dataSource' import * as utils from '../utils' import * as bridge from './bridge' +import { useStores } from './store' export const lowcodeWrap = (props, context) => { const global = {} @@ -78,5 +79,8 @@ export const lowcodeWrap = (props, context) => { export default () => { const i18n = inject(I18nInjectionKey) provide(I18nInjectionKey, i18n) - return { t: i18n.global.t, lowcodeWrap } + + const stores = useStores() + + return { t: i18n.global.t, lowcodeWrap, stores } } diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js new file mode 100644 index 0000000000..f7f39c7a84 --- /dev/null +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/src/lowcodeConfig/store.js @@ -0,0 +1,13 @@ +import * as useDefinedStores from '@/stores' + +const useStores = () => { + const stores = {} + + Object.values({ ...useDefinedStores }).forEach((store) => { + stores[store.$id] = store() + }) + + return stores +} + +export { useStores } From 1d96605c901c5dda95a910a201ba0ded416c97f9 Mon Sep 17 00:00:00 2001 From: chilingling Date: Tue, 23 Jan 2024 04:46:49 -0800 Subject: [PATCH 04/37] feat(vue-dsl): delete parse config and parse schema hook --- .../src/generator/codeGenerator.js | 100 ++++++++++++------ .../src/generator/generateApp.js | 5 +- .../src/plugins/genBlockPlugin.js | 12 +-- .../src/plugins/genDataSourcePlugin.js | 10 +- .../src/plugins/genDependenciesPlugin.js | 12 +-- .../src/plugins/genGlobalState.js | 11 +- .../src/plugins/genI18nPlugin.js | 12 +-- .../src/plugins/genPagePlugin.js | 12 +-- .../src/plugins/genRouterPlugin.js | 10 +- .../src/plugins/genUtilsPlugin.js | 18 ++-- .../src/templates/vue-template/index.js | 12 +++ 11 files changed, 124 insertions(+), 90 deletions(-) diff --git a/packages/vue-generator/src/generator/codeGenerator.js b/packages/vue-generator/src/generator/codeGenerator.js index 8d385cf748..8349952104 100644 --- a/packages/vue-generator/src/generator/codeGenerator.js +++ b/packages/vue-generator/src/generator/codeGenerator.js @@ -4,23 +4,50 @@ class CodeGenerator { plugins = [] genLogs = [] schema = {} - parsedSchema = {} context = {} + // 是否允许插件报错 + tolerateError = true constructor(config) { this.config = config this.plugins = config.plugins + this.context = { + ...this.context, + ...(this.config.context || {}) + } + + if (typeof config.tolerateError === 'boolean') { + this.tolerateError = config.tolerateError + } } getContext() { return { config: this.config, genResult: this.genResult, - plugins: this.plugins, genLogs: this.genLogs, - schema: this.schema, - parsedSchema: this.parsedSchema, ...this.context } } + /** + * 写入 log + * @param {*} log + */ + addGenLogs(log) { + this.genLogs.push(log) + } + /** + * 覆写 config + * @param {*} newConfig + */ + overrideConfig(newConfig) { + this.config = newConfig + } + /** + * 覆写 schema + * @param {*} newSchema + */ + overrideSchema(newSchema) { + this.schema = newSchema + } getPluginsByHook(hookName) { const res = [] @@ -33,19 +60,30 @@ class CodeGenerator { return res } async generate(schema) { - const hooks = ['transformStart', 'parseConfig', 'parseSchema', 'transform', 'transformEnd'] - let err = null + const hooks = ['transformStart', 'transform', 'transformEnd'] + + this.schema = this.parseSchema(schema) - this.schema = schema + let err = [] + let curHookName = '' try { for (const hookItem of hooks) { + curHookName = hookItem const plugins = this.getPluginsByHook(hookItem) await this[hookItem](plugins) } } catch (error) { - err = error + err.push(error) + + if (!this.tolerateError) { + throw new Error( + `[codeGenerator][generate] get error when running hook: ${curHookName}. error message: ${JSON.stringify( + error + )}` + ) + } } finally { const plugins = this.getPluginsByHook('transformEnd') await this.transformEnd(plugins, err) @@ -56,35 +94,18 @@ class CodeGenerator { genLogs: this.genLogs } } + /** + * 转换开始的钩子,在正式开始转换前,用户可以做一些预处理的动作 + * @param {*} plugins + */ async transformStart(plugins) { for (const pluginItem of plugins) { - await pluginItem.apply(this, [this.config]) - } - } - async parseConfig(plugins) { - for (const pluginItem of plugins) { - const newConfig = await pluginItem.apply(this, [this.config]) - - if (newConfig) { - this.config = newConfig - } - } - } - - async parseSchema(plugins) { - for (const pluginItem of plugins) { - const parseResult = await pluginItem.apply(this, [this.schema]) - - if (!parseResult?.id || !parseResult?.result) { - continue - } - - this.parsedSchema[parseResult.id] = parseResult.result + await pluginItem(this.config, this.getContext()) } } async transform(plugins) { for (const pluginItem of plugins) { - const transformRes = await pluginItem.apply(this, [this.parsedSchema]) + const transformRes = await pluginItem(this.schema, this.getContext()) if (!transformRes) { return @@ -99,7 +120,22 @@ class CodeGenerator { } async transformEnd(plugins, err) { for (const pluginItem of plugins) { - await pluginItem.apply(this, [err]) + await pluginItem(err) + } + } + parseSchema(schema) { + if (!schema) { + throw new Error( + '[codeGenerator][generate] parseSchema error, schema is not valid, should be json object or json string.' + ) + } + + try { + return typeof schema === 'string' ? JSON.parse(schema) : schema + } catch (error) { + throw new Error( + '[codeGenerator][generate] parseSchema error, schema is not valid, please check the input params.' + ) } } replaceGenResult(resultItem) { diff --git a/packages/vue-generator/src/generator/generateApp.js b/packages/vue-generator/src/generator/generateApp.js index 042770fcc0..b78d28f352 100644 --- a/packages/vue-generator/src/generator/generateApp.js +++ b/packages/vue-generator/src/generator/generateApp.js @@ -98,7 +98,7 @@ function transformSchema(appSchema) { /** * 整体应用出码 */ -export async function generateApp(appSchema) { +export async function generateApp(appSchema, context = {}) { const codeGenInstance = new CodeGenerator({ plugins: [ genBlockPlugin(), @@ -109,7 +109,8 @@ export async function generateApp(appSchema) { genRouterPlugin(), genTemplatePlugin(), genUtilsPlugin() - ] + ], + context: context || {} }) return codeGenInstance.generate(appSchema) diff --git a/packages/vue-generator/src/plugins/genBlockPlugin.js b/packages/vue-generator/src/plugins/genBlockPlugin.js index 38a548a99b..bcdae4e88f 100644 --- a/packages/vue-generator/src/plugins/genBlockPlugin.js +++ b/packages/vue-generator/src/plugins/genBlockPlugin.js @@ -17,13 +17,10 @@ function genBlockPlugin(options = {}) { const { blockHistories } = schema const blockSchema = blockHistories.map((block) => block?.content).filter((schema) => typeof schema === 'object') - return { - id: 'blocks', - result: blockSchema - } + return blockSchema }, - transform(transformedSchema) { - const { blocks } = transformedSchema + transform(schema) { + const blocks = this.parseSchema(schema) const resBlocks = [] @@ -40,9 +37,10 @@ function genBlockPlugin(options = {}) { continue } - const { panelName, panelValue } = restInfo + const { panelName, panelValue, panelType } = restInfo resBlocks.push({ + fileType: panelType, fileName: panelName, path: blockBasePath, fileContent: panelValue diff --git a/packages/vue-generator/src/plugins/genDataSourcePlugin.js b/packages/vue-generator/src/plugins/genDataSourcePlugin.js index 84d02e4439..fb4c40b006 100644 --- a/packages/vue-generator/src/plugins/genDataSourcePlugin.js +++ b/packages/vue-generator/src/plugins/genDataSourcePlugin.js @@ -14,13 +14,10 @@ function genDataSourcePlugin(options = {}) { name: 'tinyengine-plugin-generatecode-datasource', description: 'transform schema to dataSource plugin', parseSchema(schema) { - return { - id: 'dataSource', - result: schema?.dataSource || {} - } + return schema?.dataSource || {} }, - transform(transformedSchema) { - const dataSource = transformedSchema.dataSource + transform(schema) { + const dataSource = this.parseSchema(schema) const { dataHandler, errorHandler, willFetch, list } = dataSource || {} @@ -41,6 +38,7 @@ function genDataSourcePlugin(options = {}) { } return { + fileType: 'json', fileName, path, fileContent: JSON.stringify(data) diff --git a/packages/vue-generator/src/plugins/genDependenciesPlugin.js b/packages/vue-generator/src/plugins/genDependenciesPlugin.js index e9ef5f7791..bda32221b8 100644 --- a/packages/vue-generator/src/plugins/genDependenciesPlugin.js +++ b/packages/vue-generator/src/plugins/genDependenciesPlugin.js @@ -29,13 +29,11 @@ function genDependenciesPlugin(options = {}) { utilsDependencies[packageName] = version || 'latest' } - return { - id: 'dependencies', - result: utilsDependencies - } + // TODO, 这里缺组件依赖分析 + return utilsDependencies }, - transform(transformedSchema) { - const { dependencies } = transformedSchema + transform(schema) { + const { dependencies } = this.parseSchema(schema) const originPackageItem = this.genResult.find((item) => item.fileName === fileName && item.path === path) if (!originPackageItem) { @@ -53,7 +51,7 @@ function genDependenciesPlugin(options = {}) { ...dependencies } - this.replaceGenResult({ fileName, path, fileContent: JSON.stringify(originPackageJSON) }) + this.replaceGenResult({ fileType: 'json', fileName, path, fileContent: JSON.stringify(originPackageJSON) }) } } } diff --git a/packages/vue-generator/src/plugins/genGlobalState.js b/packages/vue-generator/src/plugins/genGlobalState.js index 586121317a..8146f9c53f 100644 --- a/packages/vue-generator/src/plugins/genGlobalState.js +++ b/packages/vue-generator/src/plugins/genGlobalState.js @@ -20,13 +20,10 @@ function genDependenciesPlugin(options = {}) { global_state = [] } - return { - id: 'globalState', - result: global_state - } + return global_state }, - transform(transformedSchema) { - const { globalState } = transformedSchema + transform(schema) { + const globalState = this.parseSchema(schema) const res = [] const ids = [] @@ -61,6 +58,7 @@ function genDependenciesPlugin(options = {}) { }) ` res.push({ + fileType: 'js', fileName: `${id}.js`, path, fileContent: storeFiles @@ -68,6 +66,7 @@ function genDependenciesPlugin(options = {}) { } res.push({ + fileType: 'js', fileName: 'index.js', path, fileContent: ids.map((item) => `export { ${item} } from './${item}'`).join('\n') diff --git a/packages/vue-generator/src/plugins/genI18nPlugin.js b/packages/vue-generator/src/plugins/genI18nPlugin.js index 0d6e46cee5..6d25c3241d 100644 --- a/packages/vue-generator/src/plugins/genI18nPlugin.js +++ b/packages/vue-generator/src/plugins/genI18nPlugin.js @@ -15,20 +15,15 @@ function genI18nPlugin(options = {}) { return { name: 'tinyengine-plugin-generatecode-i18n', description: 'transform i18n schema to i18n code plugin', - parseSchema(schema) { - return { - id: 'i18n', - result: schema?.i18n || [] - } - }, - transform(transformedSchema) { - const { i18n } = transformedSchema || {} + transform(schema) { + const i18n = schema?.i18n || [] const res = [] // 生成国际化词条文件 for (const [key, value] of Object.entries(i18n)) { res.push({ + fileType: 'json', fileName: `${key}.json`, path, fileContent: JSON.stringify(value, null, 2) @@ -42,6 +37,7 @@ function genI18nPlugin(options = {}) { // 生成 locale.js res.push({ + fileType: 'json', fileName: localeFileName, path, fileContent: ` diff --git a/packages/vue-generator/src/plugins/genPagePlugin.js b/packages/vue-generator/src/plugins/genPagePlugin.js index 40193f68c0..ed4350256f 100644 --- a/packages/vue-generator/src/plugins/genPagePlugin.js +++ b/packages/vue-generator/src/plugins/genPagePlugin.js @@ -47,13 +47,10 @@ function genPagePlugin(options = {}) { resPageTree.push(newComponentItem) } - return { - id: 'pages', - result: resPageTree - } + return resPageTree }, - transform(transformedSchema) { - const { pages } = transformedSchema + transform(schema) { + const pages = this.parseSchema(schema) const resPage = [] @@ -70,9 +67,10 @@ function genPagePlugin(options = {}) { continue } - const { panelName, panelValue } = restInfo + const { panelName, panelValue, panelType } = restInfo resPage.push({ + fileType: panelType, fileName: panelName, path: page.path, fileContent: panelValue diff --git a/packages/vue-generator/src/plugins/genRouterPlugin.js b/packages/vue-generator/src/plugins/genRouterPlugin.js index 918d0207eb..71a41d861f 100644 --- a/packages/vue-generator/src/plugins/genRouterPlugin.js +++ b/packages/vue-generator/src/plugins/genRouterPlugin.js @@ -30,13 +30,10 @@ function genRouterPlugin(options = {}) { routes.unshift({ path: '/', redirect: homePath }) } - return { - id: 'routes', - result: routes - } + return routes }, - transform(transformedSchema) { - const { routes: routesList } = transformedSchema || {} + transform(schema) { + const { routes: routesList } = this.parseSchema(schema) // TODO: 支持 hash 模式、history 模式 const importSnippet = "import { createRouter, createWebHashHistory } from 'vue-router'" @@ -65,6 +62,7 @@ function genRouterPlugin(options = {}) { const routeSnippets = `const routes = [${routes.join(',')}]` const res = { + fileType: 'js', fileName, path, fileContent: `${importSnippet}\n ${routeSnippets} \n ${exportSnippet}` diff --git a/packages/vue-generator/src/plugins/genUtilsPlugin.js b/packages/vue-generator/src/plugins/genUtilsPlugin.js index ad92fc4d0a..9d981d017c 100644 --- a/packages/vue-generator/src/plugins/genUtilsPlugin.js +++ b/packages/vue-generator/src/plugins/genUtilsPlugin.js @@ -40,17 +40,16 @@ function genUtilsPlugin(options = {}) { return { name: 'tinyengine-plugin-generatecode-utils', description: 'transform utils schema to utils code', - parseSchema(schema) { + // parseSchema(schema) { + // const { utils } = schema + // return { + // id: 'utils', + // result: utils || [] + // } + // }, + transform(schema) { const { utils } = schema - return { - id: 'utils', - result: utils || [] - } - }, - transform(transformedSchema) { - const { utils } = transformedSchema - if (!Array.isArray(utils)) { return } @@ -83,6 +82,7 @@ function genUtilsPlugin(options = {}) { ` return { + fileType: 'js', fileName, path, fileContent diff --git a/packages/vue-generator/src/templates/vue-template/index.js b/packages/vue-generator/src/templates/vue-template/index.js index ca8f561977..ecbf9ac0cc 100644 --- a/packages/vue-generator/src/templates/vue-template/index.js +++ b/packages/vue-generator/src/templates/vue-template/index.js @@ -36,16 +36,19 @@ const getTemplate = (context, str) => { export function generateTemplate(context) { return [ { + fileType: 'md', fileName: 'README.md', paths: '.', fileContent: getTemplate(context, readmeFile) }, { + fileType: 'js', fileName: 'vite.config.js', paths: '.', fileContent: getTemplate(context, viteConfigFile) }, { + fileType: 'json', fileName: 'package.json', paths: '.', fileContent: getPackageJson(context) @@ -56,46 +59,55 @@ export function generateTemplate(context) { fileContent: getTemplate(context, gitIgnoreFile) }, { + fileType: 'html', fileName: 'index.html', paths: '.', fileContent: getTemplate(context, entryHTMLFile) }, { + fileType: 'js', fileName: 'main.js', paths: './src', fileContent: getTemplate(context, mainJSFile) }, { + fileType: 'vue', fileName: 'App.vue', paths: './src', fileContent: getTemplate(context, appVueFile) }, { + fileType: 'js', fileName: 'bridge.js', paths: './src/lowcodeConfig', fileContent: bridgeFile }, { + fileType: 'js', fileName: 'dataSource.js', paths: './src/lowcodeConfig', fileContent: dataSourceFile }, { + fileType: 'js', fileName: 'lowcode.js', paths: './src/lowcodeConfig', fileContent: lowcodeJSFile }, { + fileType: 'js', fileName: 'axios.js', paths: './src/http', fileContent: axiosFile }, { + fileType: 'js', fileName: 'config.js', paths: './src/http', fileContent: axiosConfigFile }, { + fileType: 'js', fileName: 'index.js', paths: './src/http', fileContent: httpEntryFile From 3ee82bd7ad61bd5b1e1854b6ea6c60b25cfd5e22 Mon Sep 17 00:00:00 2001 From: chilingling Date: Sat, 9 Mar 2024 02:36:01 -0800 Subject: [PATCH 05/37] feat(tempalte): refra generateTemplate logic --- packages/vue-generator/package.json | 6 +- packages/vue-generator/src/generator/page.js | 1 + .../vue-generator/src/generator/vue/index.js | 0 .../vue/template/generateAttribute.js | 307 ++++++++++++++++++ .../vue/template/generateChildren.js | 4 + .../generator/vue/template/generateScript.js | 9 + .../generator/vue/template/generateStyle.js | 11 + .../src/generator/vue/template/generateTag.js | 49 +++ .../vue/template/generateTemplate.js | 53 +++ .../vue-generator/src/parser/state-type.js | 23 +- packages/vue-generator/src/utils/index.js | 3 +- .../test/testcases/full/index.js | 2 +- .../test/unit/template/test/generate.test.js | 41 +++ .../template/test/generateAttribute.test.js | 20 ++ packages/vue-generator/vite.config.js | 7 +- 15 files changed, 520 insertions(+), 16 deletions(-) create mode 100644 packages/vue-generator/src/generator/vue/index.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateAttribute.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateChildren.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateScript.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateStyle.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateTag.js create mode 100644 packages/vue-generator/src/generator/vue/template/generateTemplate.js create mode 100644 packages/vue-generator/test/unit/template/test/generate.test.js create mode 100644 packages/vue-generator/test/unit/template/test/generateAttribute.test.js diff --git a/packages/vue-generator/package.json b/packages/vue-generator/package.json index d3ce22c520..58a65c2da6 100644 --- a/packages/vue-generator/package.json +++ b/packages/vue-generator/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "main": "dist/tiny-engine-dsl-vue.cjs.js", + "main": "dist/tiny-engine-dsl-vue.js", "files": [ "dist" ], @@ -12,6 +12,7 @@ "build": "vite build", "test": "npx nyc@latest --reporter=lcov node test/test_generator.js", "test:latest": "npm run build && node test/testcases/full/index.js", + "test:unit": "vitest", "publish:npm": "npm publish --verbose" }, "repository": { @@ -40,7 +41,8 @@ "eslint-plugin-vue": "^8.6.0", "fs-extra": "^10.0.1", "prettier": "^2.6.1", - "vite": "^2.8.6", + "vite": "^4.3.7", + "vitest": "^0.34.6", "winston": "^3.10.0" } } diff --git a/packages/vue-generator/src/generator/page.js b/packages/vue-generator/src/generator/page.js index 099f0d67bc..6ba9f6fd2b 100644 --- a/packages/vue-generator/src/generator/page.js +++ b/packages/vue-generator/src/generator/page.js @@ -94,6 +94,7 @@ const handleLiteralBinding = ({ key, item, attrsArr, description, state }) => { // string 直接静态绑定 if (typeof item === 'string') return attrsArr.push(`${key}="${item.replace(/"/g, '"')}"`) + // TODO: 拿到这里的场景 case? if (item?.componentName === BUILTIN_COMPONENT_NAME.ICON) { const iconName = handleIconInProps(description, item) diff --git a/packages/vue-generator/src/generator/vue/index.js b/packages/vue-generator/src/generator/vue/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vue-generator/src/generator/vue/template/generateAttribute.js b/packages/vue-generator/src/generator/vue/template/generateAttribute.js new file mode 100644 index 0000000000..83ed49f786 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateAttribute.js @@ -0,0 +1,307 @@ +import { BUILTIN_COMPONENT_NAME, JS_EXPRESSION, JS_FUNCTION, JS_I18N, JS_RESOURCE, JS_SLOT } from '@/constant' +import { isOn, toEventKey, thisBindRe, randomString } from '@/utils' +import { strategy } from '@/parser/state-type' +import { unwrapExpression } from '@/parser/state' + +export const generateTemplateCondition = (condition) => { + if (typeof condition === 'boolean') { + return `v-if=${condition}` + } + + if (!condition?.type) { + return '' + } + + if (condition?.kind === 'else') { + return 'v-else' + } + + const conditionValue = condition?.value?.replace(thisBindRe, '') + + return `v-${condition?.kind || 'if'}=${conditionValue}` +} + +export const generateLoopTemplate = (loop, loopArgs) => { + // 没有设置循环,返回空字符 + if (!loop) { + return '' + } + + const source = (loop?.value || '').replace(thisBindRe, '') + const iterVar = [...loopArgs] + + return `v-for="(${iterVar.join(',')}) in ${source}"` +} + +const handleEventBinding = (key, item) => { + const eventKey = toEventKey(key) + let eventBinding = '' + + // vue 事件绑定,仅支持:内联事件处理器 or 方法事件处理器(绑定方法名或对某个方法的调用) + if (item?.type === JS_EXPRESSION) { + const eventHandler = item.value.replace(thisBindRe, '') + + // Vue Template 中,为事件处理函数传递额外的参数时,需要使用内联箭头函数 + if (item.params?.length) { + const extendParams = item.params.join(',') + eventBinding = `@${eventKey}="(...eventArgs) => ${eventHandler}(eventArgs, ${extendParams})"` + } else { + eventBinding = `@${eventKey}="${eventHandler}"` + } + } + + return eventBinding +} + +export const handleAttributeKey = (key) => { + const specialKey = { + className: 'class' + } + + if (specialKey[key]) { + return specialKey + } + + return key +} + +export const genSlotBinding = (props = {}) => { + const { slot } = props + + if (!slot) { + return '' + } + + if (typeof slot === 'string') { + return `#${slot}` + } + + const { name, params } = slot + + let paramsValue = '' + + if (Array.isArray(params)) { + paramsValue = `={ ${params.join(',')} }` + } else if (typeof params === 'string') { + paramsValue = `="${params}"` + } + + return `#${name}${paramsValue}` +} + +export const handlePrimitiveBinding = (key, value) => { + const varBinding = ['boolean', 'number'].includes(typeof value) ? ':' : '' + + return `${varBinding}${key}=${value}` +} + +const specialTypes = [JS_FUNCTION, JS_RESOURCE, JS_SLOT] + +export const checkHasSpecialType = (obj) => { + if (!obj || typeof obj !== 'object') { + return false + } + + for (const item of Object.values(obj)) { + if (typeof item !== 'object') { + continue + } + + if (specialTypes.includes(item?.type) || checkHasSpecialType(item)) { + return true + } + } + + return false +} + +export const mergeDescription = (oldDesc, newDesc) => { + oldDesc.hasJSX = oldDesc.hasJSX || newDesc.hasJSX + oldDesc.jsResource.utils = oldDesc.jsResource.utils || newDesc.jsResource.utils + oldDesc.jsResource.bridge = oldDesc.jsResource.bridge || newDesc.jsResource.bridge +} + +const transformSpecialType = (obj) => { + if (!obj || typeof obj !== 'object') { + return + } + + let res = {} + let description = { + hasJSX: false, + jsResource: { utils: false, bridge: false } + } + + if (Array.isArray(obj)) { + res = [] + } + + for (const [key, value] of Object.entries(obj)) { + if (typeof value !== 'object') { + res[key] = value + continue + } + + if (specialTypes?.includes(value?.type)) { + res[key] = strategy[value.type](value, description) + + continue + } + + const { res: tempRes, description: desc = {} } = transformSpecialType(value) || {} + + if (tempRes) { + res[key] = tempRes + } + + mergeDescription(description, desc) + } + + return { + res, + description + } +} + +export const handleObjectBinding = (key, value) => { + let shouldBindToState = false + + if (!value || typeof value !== 'object') { + return { + shouldBindToState, + resultStr: '' + } + } + + const hasSpecialType = checkHasSpecialType(value) + + const { res = '', description = {} } = transformSpecialType(value) || {} + + return { + hasSpecialType, + res, + description + } +} + +const handleJSExpressionBinding = (key, value) => { + // 支持带参数的 v-model + if (value.model) { + const modelArgs = value.model?.prop ? `:${value.model.prop}` : '' + + return `v-model${modelArgs}="${value.value.replace(thisBindRe, '')}"` + } + + // expression 使用 v-bind 绑定 + return `:${key}="${value.value.replace(thisBindRe, '')}"` +} + +const handleBindI18n = (key, value) => { + const tArguments = [`'${value.key}'`] + // TODO: 拿到场景用例 + const i18nParams = JSON.stringify(value.params) + + i18nParams && tArguments.push(i18nParams) + + return `:${key}="t(${tArguments.join(',')})"` +} + +export const generateAttribute = (schema) => { + const { condition, loop, loopArgs, props, componentName } = schema + + const finalRes = { + description: { + hasJSX: false, + jsResource: { utils: false, bridge: false } + }, + stateVariable: {} + } + + let resultArr = [] + + // 处理 v-if 绑定 + const conditionStr = generateTemplateCondition(condition) + + resultArr.push(conditionStr) + + // 处理 v-for 绑定 + + const loopStr = generateLoopTemplate(loop, loopArgs) + + resultArr.push(loopStr) + + const slotBindingStr = genSlotBinding(props) + + resultArr.push(slotBindingStr) + + // 处理 ComponentName 为 template 的场景,不应该再有其他属性 + if (componentName === BUILTIN_COMPONENT_NAME.TEMPLATE) { + return resultArr.join(' ') + } + + Object.entries(props).forEach(([key, value]) => { + if (key === 'slot') { + return + } + + if (isOn(key)) { + const eventBindStr = handleEventBinding(key, value) + + resultArr.push(eventBindStr) + + return + } + + // 处理特殊的 key,比如 className -> class + const actualKey = handleAttributeKey(key) + + // 基本类型值绑定 + if (typeof value !== 'object') { + const primitiveStr = handlePrimitiveBinding(actualKey, value) + + resultArr.push(primitiveStr) + + return + } + + // 处理 expression 类型值绑定 + if (value?.type === JS_EXPRESSION) { + const expressionStr = handleJSExpressionBinding(actualKey, value) + + resultArr.push(expressionStr) + + return + } + + // 处理 i18n 绑定 + if (value?.type === JS_I18N) { + resultArr.push(handleBindI18n(actualKey, value)) + + return + } + + // 处理 object 绑定 + const { res, hasSpecialType, description } = handleObjectBinding(actualKey, value) + + // 有特殊类型说明不能直接拼接到 template attribute 中 + if (hasSpecialType) { + const stateValueKey = `${key}${randomString()}` + resultArr.push(`:${key}=state.${stateValueKey}`) + + finalRes.stateVariable[stateValueKey] = res + } else { + const unWrapValue = unwrapExpression(JSON.stringify(res)) + .replace(/props\./g, '') + .replace(/"/g, '"') + + resultArr.push(`:${key}=${unWrapValue}`) + } + + mergeDescription(finalRes.description, description) + }) + + return { + ...finalRes, + resultStr: resultArr.join(' ') + } +} diff --git a/packages/vue-generator/src/generator/vue/template/generateChildren.js b/packages/vue-generator/src/generator/vue/template/generateChildren.js new file mode 100644 index 0000000000..a60ac008c0 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateChildren.js @@ -0,0 +1,4 @@ +export const generateChildren = () => { + // generate empty children + // generate tag name children +} diff --git a/packages/vue-generator/src/generator/vue/template/generateScript.js b/packages/vue-generator/src/generator/vue/template/generateScript.js new file mode 100644 index 0000000000..9a10f04af1 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateScript.js @@ -0,0 +1,9 @@ +export const generateSetupScript = () => { + // generate import statement + // props 声明 + // emits 声明 + // resource 工具类绑定 + // reactive State 页面变量绑定声明 + // js 方法声明绑定 + // 生命周期绑定 +} diff --git a/packages/vue-generator/src/generator/vue/template/generateStyle.js b/packages/vue-generator/src/generator/vue/template/generateStyle.js new file mode 100644 index 0000000000..e963c41b44 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateStyle.js @@ -0,0 +1,11 @@ +export const generateStyleTag = (schema) => { + const { cssLang, css } = schema + + let langDesc = '' + + if (cssLang) { + langDesc = `lang=${langDesc}` + } + + return `` +} diff --git a/packages/vue-generator/src/generator/vue/template/generateTag.js b/packages/vue-generator/src/generator/vue/template/generateTag.js new file mode 100644 index 0000000000..80d3df7eae --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateTag.js @@ -0,0 +1,49 @@ +import { hyphenate } from '@vue/shared' + +const HTML_DEFAULT_VOID_ELEMENTS = [ + 'img', + 'input', + 'br', + 'hr', + 'link', + 'area', + 'base', + 'col', + 'embed', + 'meta', + 'source', + 'track', + 'wbr' +] + +export const generateTag = (tagName, config = {}) => { + const { isVoidElement, isStartTag = true, attribute, useHyphenate = true } = config + + if (typeof tagName !== 'string' || !tagName) { + return '' + } + + const isVoidEle = + isVoidElement || (typeof isVoidElement !== 'boolean' && HTML_DEFAULT_VOID_ELEMENTS.includes(renderTagName)) + + // 自闭合标签生成闭合标签时,返回空字符串 + if (!isStartTag && isVoidEle) { + return '' + } + + let renderTagName = tagName + + if (useHyphenate) { + renderTagName = hyphenate(tagName) + } + + if (isVoidEle) { + return `<${renderTagName} />` + } + + if (isStartTag) { + return `<${renderTagName} ${attribute || ''}>` + } + + return `` +} diff --git a/packages/vue-generator/src/generator/vue/template/generateTemplate.js b/packages/vue-generator/src/generator/vue/template/generateTemplate.js new file mode 100644 index 0000000000..98f23783e9 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/template/generateTemplate.js @@ -0,0 +1,53 @@ +import { generateTag } from './generateTag' +import { generateAttribute, mergeDescription } from './generateAttribute' + +const recursiveGenTemplate = (children) => { + const effect = { + description: { + hasJSX: false, + jsResource: { utils: false, bridge: false } + }, + stateVariable: {} + } + + const schemaChildren = children || [] + + const resArr = schemaChildren.map((schemaItem) => { + const { componentName, children } = schemaItem + const { description, stateVariable, resultStr } = generateAttribute(schemaItem) + + mergeDescription(effect.description, description) + effect.stateVariable = { + ...effect.stateVariable, + ...(stateVariable || {}) + } + + const startTag = generateTag(componentName, { attribute: resultStr }) + const endTag = generateTag(componentName, { isStartTag: false }) + const { description: childDesc, stateVariable: childStateVar, resStr } = recursiveGenTemplate(children) + + mergeDescription(effect.description, childDesc) + + effect.stateVariable = { + ...effect.stateVariable, + ...(childStateVar || {}) + } + + return `${startTag}${resStr}${endTag}` + }) + + return { + ...effect, + resStr: resArr.join('') + } +} + +export const genTemplate = (schema) => { + const { description, stateVariable, resStr } = recursiveGenTemplate(schema.children) + + return { + description, + stateVariable, + resStr: `` + } +} diff --git a/packages/vue-generator/src/parser/state-type.js b/packages/vue-generator/src/parser/state-type.js index c78645d699..744ee723bc 100644 --- a/packages/vue-generator/src/parser/state-type.js +++ b/packages/vue-generator/src/parser/state-type.js @@ -1,14 +1,14 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ import { UNWRAP_QUOTES, JS_EXPRESSION, JS_FUNCTION, JS_I18N, JS_RESOURCE, JS_SLOT } from '../constant' import { getFunctionInfo, hasAccessor, addAccessorRecord } from '../utils' @@ -17,7 +17,7 @@ import { generateJSXTemplate } from './jsx-slot' const { start, end } = UNWRAP_QUOTES -const strategy = { +export const strategy = { [JS_EXPRESSION]: ({ value, computed }) => { if (computed) { return `${start}vue.computed(${value.replace(/this\./g, '')})${end}` @@ -72,6 +72,7 @@ const transformType = (current, prop, description) => { current[prop] = strategy[type](current[prop], description) } + // TODO: 这个是什么场景? if (hasAccessor(accessor)) { current[prop] = defaultValue diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index d9e7b1bd03..e355406441 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -40,7 +40,7 @@ const safeRandom = () => { return mathConstructor.random } -const randomString = (length = 4, chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => { +export const randomString = (length = 4, chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => { let result = '' for (let i = length; i > 0; --i) { result += chars[Math.floor(safeRandom() * chars.length)] @@ -78,6 +78,7 @@ const prettierOpts = { const onRE = /^on([A-Z]\w*)/ const onUpdateRE = /^on(Update:\w+)/ +export const thisBindRe = /this\.(props\.)?/g const isOn = (key) => onRE.test(key) const isOnUpdate = (key) => onUpdateRE.test(key) diff --git a/packages/vue-generator/test/testcases/full/index.js b/packages/vue-generator/test/testcases/full/index.js index 7f9d2b8ea0..47d225f6ae 100644 --- a/packages/vue-generator/test/testcases/full/index.js +++ b/packages/vue-generator/test/testcases/full/index.js @@ -14,7 +14,7 @@ const path = require('path') const fs = require('fs-extra') const prettier = require('prettier') const { execSync } = require('child_process') -const { generateCode } = require('../../../dist/tiny-engine-dsl-vue.cjs') +const { generateCode } = require('../../../dist/tiny-engine-dsl-vue.js') const { logger } = require('../../utils/logger') const getPageData = (testCaseFile) => { diff --git a/packages/vue-generator/test/unit/template/test/generate.test.js b/packages/vue-generator/test/unit/template/test/generate.test.js new file mode 100644 index 0000000000..8c9f2f7466 --- /dev/null +++ b/packages/vue-generator/test/unit/template/test/generate.test.js @@ -0,0 +1,41 @@ +import { expect, test } from 'vitest' +import { generateTag } from '@/generator/vue/template/generateTag' + +test('should validate tagName', () => { + expect(generateTag('')).toBe(undefined) +}) + +test('should generate start tag correctly', () => { + expect(generateTag('div', { isStartTag: true })).toBe('
') +}) + +test('should generate close tag correctly', () => { + expect(generateTag('div', { isStartTag: false })).toBe('
') +}) + +test('void element should generate self close tag', () => { + expect(generateTag('img')).toBe('') + expect(generateTag('input')).toBe('') + expect(generateTag('br')).toBe('
') + expect(generateTag('hr')).toBe('
') + expect(generateTag('link')).toBe('') + expect(generateTag('area')).toBe('') + expect(generateTag('base')).toBe('') + expect(generateTag('col')).toBe('') + expect(generateTag('embed')).toBe('') + expect(generateTag('meta')).toBe('') + expect(generateTag('source')).toBe('') + expect(generateTag('track')).toBe('') + expect(generateTag('wbr')).toBe('') + // should respect config + expect(generateTag('div', { isVoidElement: true })).toBe('
') +}) + +test('should default transform to hyphenate style', () => { + expect(generateTag('TinyFormItem', { isStartTag: true })).toBe('') +}) + +test('should generate attribute', () => { + const attribute = ':class=["test"] v-model="state.formItem" @click="handleClick"' + expect(generateTag('TinyFormItem', { isStartTag: true, attribute })).toBe(``) +}) diff --git a/packages/vue-generator/test/unit/template/test/generateAttribute.test.js b/packages/vue-generator/test/unit/template/test/generateAttribute.test.js new file mode 100644 index 0000000000..3b3b4991b6 --- /dev/null +++ b/packages/vue-generator/test/unit/template/test/generateAttribute.test.js @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest' +import { generateAttribute } from '@/generator/vue/template/generateAttribute' + +// 处理 number、boolean 绑定 + +test('should validate tagName', () => { + // expect(generateTag('')).toBe(undefined) +}) + +// 处理 v-if 绑定 +// 处理 v-for 绑定 +// 处理 className、style 绑定 +// 处理 state 绑定 +// 处理 props 绑定 +// 处理 script 中 export 的变量绑定 +// 处理 @xxx 事件绑定 +// 处理 v-model 绑定 +// 处理 i18n 绑定 +// 处理 jsExpression 绑定 +// 处理 slot attribute #data={ xxx } 作用域插槽的场景 diff --git a/packages/vue-generator/vite.config.js b/packages/vue-generator/vite.config.js index 96ac13f6a5..933f1c6af3 100644 --- a/packages/vue-generator/vite.config.js +++ b/packages/vue-generator/vite.config.js @@ -15,10 +15,15 @@ import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, build: { lib: { entry: path.resolve(__dirname, './src/index.js'), - formats: ['cjs'] + formats: ['cjs', 'es'] }, sourcemap: true } From 69b7ccccd0c84224eac3a418c10c1e6578682260 Mon Sep 17 00:00:00 2001 From: chilingling Date: Mon, 11 Mar 2024 06:27:16 -0700 Subject: [PATCH 06/37] feat(vue-generator): add generate script frame code --- .../{template => sfc}/generateAttribute.js | 0 .../vue/{template => sfc}/generateChildren.js | 0 .../src/generator/vue/sfc/generateScript.js | 34 +++++++++++++ .../vue/{template => sfc}/generateStyle.js | 0 .../vue/{template => sfc}/generateTag.js | 0 .../vue/{template => sfc}/generateTemplate.js | 0 .../src/generator/vue/sfc/index.js | 36 +++++++++++++ .../src/generator/vue/sfc/parseImport.js | 51 +++++++++++++++++++ .../generator/vue/template/generateScript.js | 9 ---- .../src/utils/generateImportStatement.js | 30 +++++++++++ 10 files changed, 151 insertions(+), 9 deletions(-) rename packages/vue-generator/src/generator/vue/{template => sfc}/generateAttribute.js (100%) rename packages/vue-generator/src/generator/vue/{template => sfc}/generateChildren.js (100%) create mode 100644 packages/vue-generator/src/generator/vue/sfc/generateScript.js rename packages/vue-generator/src/generator/vue/{template => sfc}/generateStyle.js (100%) rename packages/vue-generator/src/generator/vue/{template => sfc}/generateTag.js (100%) rename packages/vue-generator/src/generator/vue/{template => sfc}/generateTemplate.js (100%) create mode 100644 packages/vue-generator/src/generator/vue/sfc/index.js create mode 100644 packages/vue-generator/src/generator/vue/sfc/parseImport.js delete mode 100644 packages/vue-generator/src/generator/vue/template/generateScript.js diff --git a/packages/vue-generator/src/generator/vue/template/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js similarity index 100% rename from packages/vue-generator/src/generator/vue/template/generateAttribute.js rename to packages/vue-generator/src/generator/vue/sfc/generateAttribute.js diff --git a/packages/vue-generator/src/generator/vue/template/generateChildren.js b/packages/vue-generator/src/generator/vue/sfc/generateChildren.js similarity index 100% rename from packages/vue-generator/src/generator/vue/template/generateChildren.js rename to packages/vue-generator/src/generator/vue/sfc/generateChildren.js diff --git a/packages/vue-generator/src/generator/vue/sfc/generateScript.js b/packages/vue-generator/src/generator/vue/sfc/generateScript.js new file mode 100644 index 0000000000..332ef4182d --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/generateScript.js @@ -0,0 +1,34 @@ +import { genCompImport } from './parseImport' + +const generateImports = (schema, config = {}) => { + const { defaultImports = [], componentsMap = [] } = config + // 组件 import + const compImportStr = genCompImport(schema, componentsMap) + + return `${[...defaultImports, ...compImportStr].join('\n')}` +} + +export const generateSetupScript = (schema, config) => { + // generate import statement + // props 声明 + // emits 声明 + // resource 工具类绑定 + // reactive State 页面变量绑定声明 + // js 方法声明绑定 + // 生命周期绑定 + + const lang = '' + const scriptStart = `' + const defaultImports = [ + 'import * as vue from "vue"', + 'import { defineProps, defineEmits } from "vue"', + 'import { I18nInjectionKey } from "vue-i18n"' + ] + const compImportStr = generateImports(schema) + const importStr = `${defaultImports.join('\n')}\n${compImportStr}` + + + + +} diff --git a/packages/vue-generator/src/generator/vue/template/generateStyle.js b/packages/vue-generator/src/generator/vue/sfc/generateStyle.js similarity index 100% rename from packages/vue-generator/src/generator/vue/template/generateStyle.js rename to packages/vue-generator/src/generator/vue/sfc/generateStyle.js diff --git a/packages/vue-generator/src/generator/vue/template/generateTag.js b/packages/vue-generator/src/generator/vue/sfc/generateTag.js similarity index 100% rename from packages/vue-generator/src/generator/vue/template/generateTag.js rename to packages/vue-generator/src/generator/vue/sfc/generateTag.js diff --git a/packages/vue-generator/src/generator/vue/template/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js similarity index 100% rename from packages/vue-generator/src/generator/vue/template/generateTemplate.js rename to packages/vue-generator/src/generator/vue/sfc/generateTemplate.js diff --git a/packages/vue-generator/src/generator/vue/sfc/index.js b/packages/vue-generator/src/generator/vue/sfc/index.js new file mode 100644 index 0000000000..3a78f74425 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/index.js @@ -0,0 +1,36 @@ +const handleComponentHook = () => {} + +const handleAttributeHook = () => {} + +const handleChildrenHook = () => {} + +const injectStatement = () => {} + +const injectMethod = () => {} + +const injectState = () => {} + +const injectImport = () => {} + +const setScriptConfig = () => {} + +const setCssConfig = () => {} + +const injectCss = () => {} + +const generateSFCFile = (schema, componentsMap, config) => { + // 前置动作 + // 解析 import + // 解析 state + // 解析 method + // + + // config + const scriptConfig = {} + + const styleConfig = {} + + const globalHooks = {} +} + +export default generateSFCFile diff --git a/packages/vue-generator/src/generator/vue/sfc/parseImport.js b/packages/vue-generator/src/generator/vue/sfc/parseImport.js new file mode 100644 index 0000000000..e147bc94aa --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/parseImport.js @@ -0,0 +1,51 @@ +import { BUILTIN_COMPONENT_NAME } from '@/constant' +import { generateImportByPkgName } from '@/utils/generateImportStatement' + +export const parseImport = (children) => { + let components = [] + let blocks = [] + + for (const item of children || []) { + if (item?.componentType === BUILTIN_COMPONENT_NAME.BLOCK) { + blocks.push(item?.componentName) + } else { + components.push(item?.componentName) + } + + if (Array.isArray(item?.children) && item.children.length > 0) { + const { components: childComp, blocks: childBlocks } = parseImport(item.children) + + components = components.concat(childComp) + blocks = blocks.concat(childBlocks) + } + } + + return { + components: [...new Set(components)], + blocks: [...new Set(blocks)] + } +} + +export const genCompImport = (schema, componentsMap, config = {}) => { + const { components, blocks } = parseImport(schema.children) + const pkgMap = {} + const { blockRelativePath = '../components/', blockSuffix = '.vue' } = config + + const importComps = componentsMap.filter(({ componentName }) => components.includes(componentName)) + + importComps.forEach((item) => { + pkgMap[item.package] = pkgMap[item.package] || [] + + pkgMap[item.package].push(item) + }) + + const batchImportStatements = Object.entries(pkgMap).map(([key, value]) => { + return generateImportByPkgName({ pkgName: key, imports: value }) + }) + + const blockImportStatement = blocks.map((name) => { + return `import ${name} from ${blockRelativePath}/${name}${blockSuffix}` + }) + + return `${batchImportStatements.join('\n')}\n${blockImportStatement.join('\n')}` +} diff --git a/packages/vue-generator/src/generator/vue/template/generateScript.js b/packages/vue-generator/src/generator/vue/template/generateScript.js deleted file mode 100644 index 9a10f04af1..0000000000 --- a/packages/vue-generator/src/generator/vue/template/generateScript.js +++ /dev/null @@ -1,9 +0,0 @@ -export const generateSetupScript = () => { - // generate import statement - // props 声明 - // emits 声明 - // resource 工具类绑定 - // reactive State 页面变量绑定声明 - // js 方法声明绑定 - // 生命周期绑定 -} diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js index 5a9588b014..aa8b470bcf 100644 --- a/packages/vue-generator/src/utils/generateImportStatement.js +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -14,3 +14,33 @@ export function generateImportStatement(config) { return `import ${statementName} from ${moduleName}` } + +export function generateImportByPkgName(config) { + const { pkgName, imports } = config + + const importStatements = imports + .filter(({ destructuring }) => destructuring) + .map(({ componentName, exportName }) => { + if (componentName === exportName) { + return componentName + } + + return `${exportName} as ${componentName}` + }) + + // 默认导出如果存在,应该只有一个 + let defaultImports = imports.find(({ destructuring }) => !destructuring) + let defaultImportStatement = '' + + if (defaultImports) { + const { componentName, exportName } = defaultImports + + if (exportName && exportName !== componentName) { + defaultImportStatement = `${exportName} as ${componentName},` + } else { + defaultImportStatement = `${exportName},` + } + } + + return `import ${defaultImportStatement} { ${importStatements.join(',')} } from ${pkgName}` +} From ca4f8dd0a68cf6f5dfe59dc74a081fd3cab02ccd Mon Sep 17 00:00:00 2001 From: chilingling Date: Tue, 12 Mar 2024 06:53:21 -0700 Subject: [PATCH 07/37] feat(vue-generator): add hook flow for sfc file generate --- packages/vue-generator/src/constant/index.js | 16 ++ .../src/generator/vue/sfc/genSetupSFC.js | 178 ++++++++++++++++++ .../generator/vue/sfc/generateAttribute.js | 68 +++++++ .../src/generator/vue/sfc/generateScript.js | 16 +- .../src/generator/vue/sfc/generateStyle.js | 14 +- .../src/generator/vue/sfc/generateTemplate.js | 137 ++++++++++++++ .../src/generator/vue/sfc/index.js | 168 +++++++++++++++-- .../src/generator/vue/sfc/parseImport.js | 31 +++ .../vue-generator/src/pre-processor/index.js | 20 +- packages/vue-generator/src/utils/index.js | 2 +- 10 files changed, 615 insertions(+), 35 deletions(-) create mode 100644 packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js diff --git a/packages/vue-generator/src/constant/index.js b/packages/vue-generator/src/constant/index.js index e0d66e209f..239e247eab 100644 --- a/packages/vue-generator/src/constant/index.js +++ b/packages/vue-generator/src/constant/index.js @@ -2211,6 +2211,22 @@ const BUILTIN_COMPONENT_NAME = { ICON: 'Icon' } +export const BUILTIN_COMPONENT_NAME_MAP = { + Text: 'span', + Collection: 'div', + Block: 'div' +} + +export const INSERT_POSITION = { + AFTER_IMPORT: 'AFTER_IMPORT', + BEFORE_PROPS: 'BEFORE_PROPS', + AFTER_PROPS: 'AFTER_PROPS', + BEFORE_STATE: 'BEFORE_STATE', + AFTER_STATE: 'AFTER_STATE', + BEFORE_METHODS: 'BEFORE_METHODS', + AFTER_METHODS: 'AFTER_METHODS' +} + /** * 图标组件名,统一前缀为 TinyIcon,与从组件库引入的方法名 iconXxx 区分开 */ diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js new file mode 100644 index 0000000000..951c160360 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -0,0 +1,178 @@ +import { INSERT_POSITION } from '@/constant' +import { getImportMap } from './parseImport' +import { genTemplateByHook, handleComponentNameHook, handleTinyGrid, handleTinyIcon } from './generateTemplate' +import { generateStyleTag } from './generateStyle' + +const parseConfig = (config = {}) => { + const { + blockRelativePath = '../components/', + blockSuffix = '.vue', + scriptConfig = {}, + styleConfig = {} + } = config || {} + const res = { + ...config, + blockRelativePath, + blockSuffix, + scriptConfig, + styleConfig + } + + return res +} + +const defaultScriptConfig = { + lang: '', + setup: true +} + +const defaultStyleConfig = { + scoped: true, + lang: '' +} + +const generateSFCFile = (schema, componentsMap, config = {}) => { + const parsedConfig = parseConfig(config) + const { blockRelativePath, blockSuffix, scriptConfig: initScriptConfig, styleConfig: initStyleConfig } = parsedConfig + // 前置动作,对 Schema 进行解析初始化相关配置与变量 + + // 解析 import + const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix }) + + // 解析 state + const state = schema.state || {} + + // 解析 method + const methods = schema.methods || {} + + // 其他表达式语句 + const statements = { + [INSERT_POSITION.AFTER_IMPORT]: [], + [INSERT_POSITION.BEFORE_PROPS]: [], + [INSERT_POSITION.AFTER_PROPS]: [], + [INSERT_POSITION.BEFORE_STATE]: [], + [INSERT_POSITION.AFTER_STATE]: [], + [INSERT_POSITION.BEFORE_METHODS]: [], + [INSERT_POSITION.AFTER_METHODS]: [] + } + + // config + let scriptConfig = { + ...defaultScriptConfig, + ...initScriptConfig + } + + let styleConfig = { + ...defaultStyleConfig, + ...initStyleConfig + } + + const globalHooks = { + addStatement: (newStatement) => { + if (!newStatement?.value) { + return false + } + + ;(statements[newStatement?.position] || statements[INSERT_POSITION.AFTER_METHODS]).push(newStatement?.value) + + return true + }, + addMethod: (key, value) => { + if (methods[key]) { + return false + } + + methods[key] = value + + return true + }, + addState: (key, value) => { + if (state[key]) { + return false + } + + state[key] = value + + return true + }, + addImport: (fromPath, config) => { + const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath] + + if (dependenciesMap) { + // 默认导出 + if (!config.destructuring && dependenciesMap.find(({ destructuring }) => !destructuring)) { + return false + } + + dependenciesMap.push(config) + + return true + } + + pkgMap[fromPath] = [config] + + return true + }, + setScriptConfig: (newConfig) => { + if (!newConfig || typeof newConfig !== 'object') { + return + } + + scriptConfig = { + ...scriptConfig, + ...newConfig + } + }, + getScriptConfig: () => scriptConfig, + setStyleConfig: (newConfig = {}) => { + if (!newConfig || typeof newConfig !== 'object') { + return + } + + styleConfig = { + ...styleConfig, + ...newConfig + } + }, + getStyleConfig: () => styleConfig, + addCss: (css) => { + schema.css = `${schema.css}\n${css}` + } + } + + // TODO: 支持页面级别的 dataSource、utils + + // 解析 template + const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) + + // 生成 script + const scriptStr = '' + + // 生成 style + const styleStr = generateStyleTag(schema, styleConfig) + + return `${templateStr}\n${scriptStr}\n${styleStr}` +} + +export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => { + // const hooks = config.hooks + const { componentName = [], attribute = [], children = [] } = config.hooks || {} + const defaultComponentHooks = [handleComponentNameHook, handleTinyIcon] + + const defaultAttributeHook = [handleTinyGrid] + + const defaultChildrenHook = [] + + const newConfig = { + ...config, + hooks: { + componentName: [...componentName, ...defaultComponentHooks], + attribute: [...attribute, ...defaultAttributeHook], + children: [...children, ...defaultChildrenHook] + } + } + + return generateSFCFile(schema, componentsMap, newConfig) +} + +export default generateSFCFile diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 83ed49f786..f8e7979884 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -305,3 +305,71 @@ export const generateAttribute = (schema) => { resultStr: resultArr.join(' ') } } + +export const handleConditionAttrHook = (schemaData) => { + const { resArr, schema } = schemaData + const { condition } = schema + + if (typeof condition === 'boolean') { + resArr.unshift(`v-if=${condition}`) + return + } + + if (!condition?.type) { + return + } + + if (condition?.kind === 'else') { + resArr.unshift('v-else') + } + + const conditionValue = condition?.value?.replace(thisBindRe, '') + + resArr.unshift(`v-${condition?.kind || 'if'}=${conditionValue}`) +} + +export const handleLoopAttrHook = (schemaData = {}) => { + const { resArr, schema } = schemaData + const { loop, loopArgs } = schema || {} + + if (!loop) { + return + } + + const source = (loop?.value || '').replace(thisBindRe, '') + const iterVar = [...loopArgs] + + resArr.push(`v-for="(${iterVar.join(',')}) in ${source}"`) +} + +export const handleEventAttrHook = (schemaData) => { + const { resArr, props } = schemaData + + const eventBindArr = Object.entries(props) + .filter(([key]) => isOn(key)) + .map(([key, value]) => handleEventBinding(key, value)) + + resArr.push(...eventBindArr) +} + +// 处理基本类似的 attribute,如 string、boolean +export const handlePrimitiveAttributeHook = (schemaData) => { + const { resArr, props } = schemaData + + for (const [key, value] of Object.entries(props)) { + const valueType = typeof value + const renderKey = handleAttributeKey(key) + + if (valueType === 'string') { + resArr.push(`${renderKey}=${value}`) + + delete props[key] + } + + if (['boolean', 'number'].includes(valueType)) { + resArr.push(`:${renderKey}=${value}`) + + delete props[key] + } + } +} diff --git a/packages/vue-generator/src/generator/vue/sfc/generateScript.js b/packages/vue-generator/src/generator/vue/sfc/generateScript.js index 332ef4182d..7d133a4cc8 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateScript.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateScript.js @@ -27,8 +27,22 @@ export const generateSetupScript = (schema, config) => { ] const compImportStr = generateImports(schema) const importStr = `${defaultImports.join('\n')}\n${compImportStr}` +} - +export const genScriptByHook = (schema, globalHooks, config) => { + // 变量、方法、生命周期可能会相互影响,对 script lang 也可能有影响(产生 jsx),先解析,再生成 + // parseState() + // parseMethod() + // parseLifeCycles() + // generateImports + // generate props declaration + // generate emit declaration + // generate reactive statement + // generate method statement + // generate lifecycle statement + // generate setup statement + // generate extra statement + return `` } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateStyle.js b/packages/vue-generator/src/generator/vue/sfc/generateStyle.js index e963c41b44..53fe284f01 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateStyle.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateStyle.js @@ -1,11 +1,17 @@ -export const generateStyleTag = (schema) => { - const { cssLang, css } = schema +export const generateStyleTag = (schema, config = {}) => { + const { css } = schema + const { scoped = true, lang = '' } = config let langDesc = '' + let scopedStr = '' - if (cssLang) { + if (scoped) { + scopedStr = 'scoped' + } + + if (lang) { langDesc = `lang=${langDesc}` } - return `` + return `` } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js index 98f23783e9..4a5c1fdb76 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -1,3 +1,4 @@ +import { BUILTIN_COMPONENT_NAME, BUILTIN_COMPONENT_NAME_MAP, TINY_ICON, INSERT_POSITION } from '@/constant' import { generateTag } from './generateTag' import { generateAttribute, mergeDescription } from './generateAttribute' @@ -51,3 +52,139 @@ export const genTemplate = (schema) => { resStr: `` } } + +export const handleComponentNameHook = (nameObj) => { + const { componentName, schema } = nameObj + + // 内置 component + if (!BUILTIN_COMPONENT_NAME_MAP[componentName]) { + return + } + + if (componentName === BUILTIN_COMPONENT_NAME.TEXT && schema.props.text) { + schema.children = [schema.props.text] + delete schema.props.text + } + + nameObj.componentName = BUILTIN_COMPONENT_NAME_MAP[componentName] +} + +export const handleTinyIcon = (nameObj, globalHooks) => { + if (BUILTIN_COMPONENT_NAME.ICON !== nameObj.componentName) { + return + } + + const name = nameObj.schema.props.name + const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` + + globalHooks.addImport('@opentiny/vue-icon', { + componentName: iconName, + exportName: name, + package: '@opentiny/vue-icon', + version: '^3.10.0', + destructuring: true + }) + + // tiny icon 需要调用 + globalHooks.addStatement(INSERT_POSITION.BEFORE_PROPS, `const ${iconName} = ${name}()`) + + nameObj.componentName = iconName + delete nameObj.schema.props.name +} + +export const handleTinyGrid = (schemaData) => { + const { componentName, props } = schemaData.schema + + // 同时存在 data 和 fetchData 的时候,删除 data + if (componentName === 'TinyGrid' && props?.data && props?.fetchData) { + delete props.data + } +} + +// 处理 templateName +// - 内置 templateName 映射 +// - 自定义 templateName 处理 +// 处理 attribute +// - 常见 attribute 处理(string、number、boolean) +// - 特殊 attribute 处理 +// - js expression 、js function、 js slot、i18n、js resource、 +// - 自定义 attribute 处理(可以转化成 children、add import、add script statement、) +// 处理 children +// 自定义处理 children 方法() + +// 处理 attribute +// 处理 children + +// TODO: 支持物料中自定义出码关联片段 + +// hooks 如何注入? +// 如何判断 hooks 已处理?、未处理? +// 如何判断该调用哪一个 hooks ? +const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => { + const schemaChildren = schemaWithRes.children || [] + const { hooks = {} } = config + // 自定义 hooks + const { componentName: componentNameHooks, attribute: attributeHooks, children: childrenHooks } = hooks + + const resArr = schemaChildren.map((schemaItem) => { + if (typeof schemaItem !== 'object' || !schemaItem) { + return schemaItem || '' + } + + const { componentName, props } = schemaItem + const parsedComponentName = { + componentName, + voidElement: false, + schema: schemaItem + } + + for (const hookItem of componentNameHooks) { + hookItem(parsedComponentName, globalHooks) + } + + const parsedAttribute = { + resArr: [], + props: structuredClone(props), + schema: schemaItem + } + + for (const hookItem of attributeHooks) { + hookItem(parsedAttribute, globalHooks) + } + + const parsedChildren = { + resArr: [], + schema: schemaItem + } + + for (const hookItem of [...childrenHooks, recursiveGenTemplateByHook]) { + hookItem(parsedChildren, globalHooks, config) + } + + const startTag = generateTag(parsedComponentName.componentName, { + attribute: parsedAttribute.join(' '), + isVoidElement: parsedComponentName.voidElement + }) + + let endTag = '' + + if (!parsedComponentName.voidElement) { + endTag = generateTag(parsedComponentName.componentName, { isStartTag: false }) + } + + return `${startTag}${parsedChildren.join('')}${endTag}` + }) + + schemaWithRes.resArr = schemaWithRes.resArr.concat(resArr) +} + +export const genTemplateByHook = (schema, globalHooks, config) => { + const parsedSchema = { + resArr: [], + schema: structuredClone(schema) + } + + recursiveGenTemplateByHook(parsedSchema, globalHooks, config) + + return `` +} diff --git a/packages/vue-generator/src/generator/vue/sfc/index.js b/packages/vue-generator/src/generator/vue/sfc/index.js index 3a78f74425..b88b592d74 100644 --- a/packages/vue-generator/src/generator/vue/sfc/index.js +++ b/packages/vue-generator/src/generator/vue/sfc/index.js @@ -1,36 +1,166 @@ -const handleComponentHook = () => {} +import { getImportMap } from './parseImport' +import { genTemplateByHook } from './generateTemplate' +import { generateStyleTag } from './generateStyle' -const handleAttributeHook = () => {} +const parseConfig = (config = {}) => { + const { + blockRelativePath = '../components/', + blockSuffix = '.vue', + scriptConfig = {}, + styleConfig = {} + } = config || {} + const res = { + ...config, + blockRelativePath, + blockSuffix, + scriptConfig, + styleConfig + } -const handleChildrenHook = () => {} + return res +} -const injectStatement = () => {} +const defaultScriptConfig = { + lang: '', + setup: true +} -const injectMethod = () => {} +const defaultStyleConfig = { + scoped: true, + lang: '' +} -const injectState = () => {} +const generateSFCFile = (schema, componentsMap, config = {}) => { + const parsedConfig = parseConfig(config) + const { blockRelativePath, blockSuffix, scriptConfig: initScriptConfig, styleConfig: initStyleConfig } = parsedConfig + // 前置动作,对 Schema 进行解析初始化相关配置与变量 -const injectImport = () => {} + // 解析 import + const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix }) -const setScriptConfig = () => {} + // 解析 state + const state = schema.state || {} -const setCssConfig = () => {} + // 解析 method + const methods = schema.methods || {} -const injectCss = () => {} + const POSITION = { + AFTER_IMPORT: 'AFTER_IMPORT', + BEFORE_PROPS: 'BEFORE_PROPS', + AFTER_PROPS: 'AFTER_PROPS', + BEFORE_STATE: 'BEFORE_STATE', + AFTER_STATE: 'AFTER_STATE', + BEFORE_METHODS: 'BEFORE_METHODS', + AFTER_METHODS: 'AFTER_METHODS' + } -const generateSFCFile = (schema, componentsMap, config) => { - // 前置动作 - // 解析 import - // 解析 state - // 解析 method - // + // 其他表达式语句 + const statements = { + [POSITION.AFTER_IMPORT]: [], + [POSITION.BEFORE_PROPS]: [], + [POSITION.AFTER_PROPS]: [], + [POSITION.BEFORE_STATE]: [], + [POSITION.AFTER_STATE]: [], + [POSITION.BEFORE_METHODS]: [], + [POSITION.AFTER_METHODS]: [] + } // config - const scriptConfig = {} + let scriptConfig = { + ...defaultScriptConfig, + ...initScriptConfig + } + + let styleConfig = { + ...defaultStyleConfig, + ...initStyleConfig + } + + const globalHooks = { + addStatement: (newStatement) => { + if (!newStatement?.value) { + return false + } + + ;(statements[newStatement?.position] || statements[POSITION.AFTER_METHODS]).push(newStatement?.value) + + return true + }, + addMethod: (key, value) => { + if (methods[key]) { + return false + } + + methods[key] = value + + return true + }, + addState: (key, value) => { + if (state[key]) { + return false + } + + state[key] = value + + return true + }, + addImport: (fromPath, config) => { + const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath] + + if (dependenciesMap) { + // 默认导出 + if (!config.destructuring && dependenciesMap.find(({ destructuring }) => !destructuring)) { + return false + } + + dependenciesMap.push(config) + + return true + } + + pkgMap[fromPath] = [config] + + return true + }, + setScriptConfig: (newConfig) => { + if (!newConfig || typeof newConfig !== 'object') { + return + } + + scriptConfig = { + ...scriptConfig, + ...newConfig + } + }, + getScriptConfig: () => scriptConfig, + setStyleConfig: (newConfig = {}) => { + if (!newConfig || typeof newConfig !== 'object') { + return + } + + styleConfig = { + ...styleConfig, + ...newConfig + } + }, + getStyleConfig: () => styleConfig, + addCss: (css) => { + schema.css = `${schema.css}\n${css}` + } + } + + // TODO: 支持页面级别的 dataSource、utils + + // 解析 template + const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) + + // 生成 script + const scriptStr = '' - const styleConfig = {} + // 生成 style + const styleStr = generateStyleTag(schema, styleConfig) - const globalHooks = {} + return `${templateStr}\n${scriptStr}\n${styleStr}` } export default generateSFCFile diff --git a/packages/vue-generator/src/generator/vue/sfc/parseImport.js b/packages/vue-generator/src/generator/vue/sfc/parseImport.js index e147bc94aa..35ece3d48e 100644 --- a/packages/vue-generator/src/generator/vue/sfc/parseImport.js +++ b/packages/vue-generator/src/generator/vue/sfc/parseImport.js @@ -26,6 +26,37 @@ export const parseImport = (children) => { } } +export const getImportMap = (schema, componentsMap, config) => { + const { components, blocks } = parseImport(schema.children) + const pkgMap = {} + const importComps = componentsMap.filter(({ componentName }) => components.includes(componentName)) + + importComps.forEach((item) => { + pkgMap[item.package] = pkgMap[item.package] || [] + + pkgMap[item.package].push(item) + }) + + const { blockRelativePath = '../components/', blockSuffix = '.vue' } = config + const blockPkgMap = {} + + blocks.map((name) => { + const source = `${blockRelativePath}/${name}${blockSuffix}` + + blockPkgMap[source] = { + componentName: name, + exportName: name, + destructuring: false, + package: source + } + }) + + return { + pkgMap, + blockPkgMap + } +} + export const genCompImport = (schema, componentsMap, config = {}) => { const { components, blocks } = parseImport(schema.children) const pkgMap = {} diff --git a/packages/vue-generator/src/pre-processor/index.js b/packages/vue-generator/src/pre-processor/index.js index 331910ba89..b19e705641 100644 --- a/packages/vue-generator/src/pre-processor/index.js +++ b/packages/vue-generator/src/pre-processor/index.js @@ -1,14 +1,14 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ import { BUILTIN_COMPONENT_NAME, TINY_ICON } from '../constant' diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index e355406441..0d7a9c06d0 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -146,7 +146,7 @@ export { getTypeOfSchema, getFunctionInfo, safeRandom, - randomString, + // randomString, avoidDuplicateString, lowerFirst, toPascalCase, From a074cc1bd84846a43e071c9ff0e18fa1358c58ac Mon Sep 17 00:00:00 2001 From: chilingling Date: Wed, 13 Mar 2024 05:46:53 -0700 Subject: [PATCH 08/37] feat(vue-generator): support generate sfc script --- packages/vue-generator/src/constant/index.js | 2 + .../src/generator/vue/sfc/genSetupSFC.js | 112 ++- .../generator/vue/sfc/generateAttribute.js | 231 +++++- .../src/generator/vue/sfc/generateChildren.js | 4 - .../src/generator/vue/sfc/generateScript.js | 286 ++++++- .../src/generator/vue/sfc/generateTag.js | 4 +- .../src/generator/vue/sfc/generateTemplate.js | 48 +- .../src/generator/vue/sfc/index.js | 167 +--- packages/vue-generator/src/utils/index.js | 7 +- .../test/testcases/sfc/case01/cast01.test.js | 11 + .../testcases/sfc/case01/componentsMap.json | 28 + .../test/testcases/sfc/case01/expected.vue | 472 ++++++++++++ .../test/testcases/sfc/case01/schema.json | 715 ++++++++++++++++++ .../vue-generator/test/testcases/sfc/index.js | 0 .../test/unit/template/test/generate.test.js | 4 +- .../template/test/generateAttribute.test.js | 2 +- 16 files changed, 1826 insertions(+), 267 deletions(-) delete mode 100644 packages/vue-generator/src/generator/vue/sfc/generateChildren.js create mode 100644 packages/vue-generator/test/testcases/sfc/case01/cast01.test.js create mode 100644 packages/vue-generator/test/testcases/sfc/case01/componentsMap.json create mode 100644 packages/vue-generator/test/testcases/sfc/case01/expected.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case01/schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/index.js diff --git a/packages/vue-generator/src/constant/index.js b/packages/vue-generator/src/constant/index.js index 239e247eab..45ce2238c3 100644 --- a/packages/vue-generator/src/constant/index.js +++ b/packages/vue-generator/src/constant/index.js @@ -2240,6 +2240,8 @@ const UNWRAP_QUOTES = { end: '#QUOTES_END#' } +export const SPECIAL_UTILS_TYPE = ['utils', 'bridge'] + /** * 协议中的类型 */ diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js index 951c160360..a7c096bf0c 100644 --- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -1,7 +1,33 @@ -import { INSERT_POSITION } from '@/constant' import { getImportMap } from './parseImport' import { genTemplateByHook, handleComponentNameHook, handleTinyGrid, handleTinyIcon } from './generateTemplate' import { generateStyleTag } from './generateStyle' +import { + handleConditionAttrHook, + handleLoopAttrHook, + handleSlotBindAttrHook, + handleAttrKeyHook, + handlePrimitiveAttributeHook, + handleExpressionAttrHook, + handleI18nAttrHook, + handleObjBindAttrHook, + handleEventAttrHook +} from './generateAttribute' +import { + GEN_SCRIPT_HOOKS, + genScriptByHook, + parsePropsHook, + parseReactiveStateHook, + addDefaultVueImport, + addDefaultVueI18nImport, + handleProvideStatesContextHook, + handleContextInjectHook, + defaultGenImportHook, + defaultGenPropsHook, + defaultGenEmitsHook, + defaultGenStateHook, + defaultGenMethodHook, + defaultGenLifecycleHook +} from './generateScript' const parseConfig = (config = {}) => { const { @@ -40,21 +66,13 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix }) // 解析 state - const state = schema.state || {} + let state = schema.state || {} // 解析 method const methods = schema.methods || {} // 其他表达式语句 - const statements = { - [INSERT_POSITION.AFTER_IMPORT]: [], - [INSERT_POSITION.BEFORE_PROPS]: [], - [INSERT_POSITION.AFTER_PROPS]: [], - [INSERT_POSITION.BEFORE_STATE]: [], - [INSERT_POSITION.AFTER_STATE]: [], - [INSERT_POSITION.BEFORE_METHODS]: [], - [INSERT_POSITION.AFTER_METHODS]: [] - } + const statements = {} // config let scriptConfig = { @@ -73,11 +91,18 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { return false } - ;(statements[newStatement?.position] || statements[INSERT_POSITION.AFTER_METHODS]).push(newStatement?.value) + const key = newStatement.key || newStatement.value + + if (statements[key]) { + return false + } + + statements[key] = newStatement return true }, - addMethod: (key, value) => { + getStatements: () => statements, + addMethods: (key, value) => { if (methods[key]) { return false } @@ -86,6 +111,7 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { return true }, + getMethods: () => methods, addState: (key, value) => { if (state[key]) { return false @@ -95,6 +121,10 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { return true }, + getState: () => state, + setState: (newState) => { + state = newState + }, addImport: (fromPath, config) => { const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath] @@ -104,6 +134,18 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { return false } + const hasExists = dependenciesMap.find(({ destructuring, exportName, componentName }) => { + return ( + destructuring === config.destructuring && + exportName === config.exportName && + componentName === config.componentName + ) + }) + + if (hasExists) { + return false + } + dependenciesMap.push(config) return true @@ -146,7 +188,7 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) // 生成 script - const scriptStr = '' + const scriptStr = genScriptByHook(schema, globalHooks, parsedConfig) // 生成 style const styleStr = generateStyleTag(schema, styleConfig) @@ -155,20 +197,54 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { } export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => { - // const hooks = config.hooks - const { componentName = [], attribute = [], children = [] } = config.hooks || {} + const { componentName = [], attribute = [], children = [], genScript = {}, parseScript = [] } = config.hooks || {} const defaultComponentHooks = [handleComponentNameHook, handleTinyIcon] - const defaultAttributeHook = [handleTinyGrid] + const defaultAttributeHook = [ + handleTinyGrid, + handleConditionAttrHook, + handleLoopAttrHook, + handleSlotBindAttrHook, + handleAttrKeyHook, + handlePrimitiveAttributeHook, + handleExpressionAttrHook, + handleI18nAttrHook, + handleObjBindAttrHook, + handleEventAttrHook + ] const defaultChildrenHook = [] + const defaultParseScriptHook = [ + addDefaultVueImport, + addDefaultVueI18nImport, + parsePropsHook, + parseReactiveStateHook, + handleProvideStatesContextHook, + handleContextInjectHook + ] + + const { GEN_IMPORT, GEN_PROPS, GEN_EMIT, GEN_STATE, GEN_METHOD, GEN_LIFECYCLE } = GEN_SCRIPT_HOOKS + const defaultGenScriptHooks = { + [GEN_IMPORT]: defaultGenImportHook, + [GEN_PROPS]: defaultGenPropsHook, + [GEN_EMIT]: defaultGenEmitsHook, + [GEN_STATE]: defaultGenStateHook, + [GEN_METHOD]: defaultGenMethodHook, + [GEN_LIFECYCLE]: defaultGenLifecycleHook + } + const newConfig = { ...config, hooks: { componentName: [...componentName, ...defaultComponentHooks], attribute: [...attribute, ...defaultAttributeHook], - children: [...children, ...defaultChildrenHook] + children: [...children, ...defaultChildrenHook], + parseScript: [...parseScript, ...defaultParseScriptHook], + genScript: { + ...defaultGenScriptHooks, + ...genScript + } } } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index f8e7979884..73105089ba 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -1,7 +1,17 @@ -import { BUILTIN_COMPONENT_NAME, JS_EXPRESSION, JS_FUNCTION, JS_I18N, JS_RESOURCE, JS_SLOT } from '@/constant' -import { isOn, toEventKey, thisBindRe, randomString } from '@/utils' +import { + BUILTIN_COMPONENT_NAME, + JS_EXPRESSION, + JS_FUNCTION, + JS_I18N, + JS_RESOURCE, + JS_SLOT, + SPECIAL_UTILS_TYPE, + INSERT_POSITION +} from '@/constant' +import { isOn, toEventKey, thisBindRe, randomString, getFunctionInfo, hasAccessor } from '@/utils' import { strategy } from '@/parser/state-type' import { unwrapExpression } from '@/parser/state' +import { recursiveGenTemplateByHook } from './generateTemplate' export const generateTemplateCondition = (condition) => { if (typeof condition === 'boolean') { @@ -352,22 +362,233 @@ export const handleEventAttrHook = (schemaData) => { resArr.push(...eventBindArr) } +export const handleSlotBindAttrHook = (schemaData) => { + const { resArr, props } = schemaData + + const slot = props?.slot + + if (!slot) { + return + } + + if (typeof slot === 'string') { + resArr.push(`#${slot}`) + + delete props.slot + } + + const { name, params } = slot + + let paramsValue = '' + + if (Array.isArray(params)) { + paramsValue = `={ ${params.join(',')} }` + } else if (typeof params === 'string') { + paramsValue = `="${params}"` + } + + resArr.push(`#${name}${paramsValue}`) + + delete props.slot +} + +export const handleAttrKeyHook = (schemaData) => { + const { props } = schemaData + const specialKey = { + className: 'class' + } + + Object.keys(props || {}).forEach((key) => { + if (specialKey[key]) { + props[specialKey[key]] = props[key] + + delete props[key] + } + }) +} + +export const handleExpressionAttrHook = (schemaData) => { + const { resArr, props } = schemaData + + Object.entries(props).forEach(([key, value]) => { + if (value?.type === JS_EXPRESSION && !isOn(key)) { + resArr.push(handleJSExpressionBinding(key, value)) + + delete props[key] + } + }) +} + +export const handleI18nAttrHook = (schemaData) => { + const { resArr, props } = schemaData + + Object.entries(props).forEach(([key, value]) => { + if (value?.type === JS_I18N) { + resArr.push(handleBindI18n(key, value)) + } + }) +} + +const specialTypeHandler = { + [JS_EXPRESSION]: ({ value, computed }) => { + if (computed) { + return { + value: `vue.computed(${value.replace(/this\./g, '')})` + } + } + + return { + value: value.replace(/this\./g, '') + } + }, + [JS_FUNCTION]: ({ value }) => { + const { type, params, body } = getFunctionInfo(value) + const inlineFunc = `${type} (${params.join(',')}) => { ${body.replace(/this\./g, '')} }` + + return { + value: inlineFunc + } + }, + [JS_I18N]: ({ key }) => { + return { + value: `t("${key}")` + } + }, + [JS_RESOURCE]: ({ value }, globalHooks) => { + const resourceType = value.split('.')[1] + + if (SPECIAL_UTILS_TYPE.includes(resourceType)) { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_PROPS, + value: `const { ${resourceType} } = wrap(function() { return this })()`, + key: resourceType + }) + } + + return { + value: value.replace(/this\./g, '') + } + }, + [JS_SLOT]: ({ value = [], params = ['row'] }, globalHooks, config) => { + globalHooks.setScriptConfig({ lang: 'jsx' }) + + const structData = { + resArr: [], + schema: { children: value } + } + + // TODO: 需要验证 template 的生成有无问题 + recursiveGenTemplateByHook(structData, globalHooks, config) + + // TODO: 这里不通用,需要设计通用的做法,或者独立成 grid 的 hook + + return { + value: `({${params.join(',')}}, h) => ${structData.resArr.join('')}` + } + } +} + +export const transformObjType = (obj, globalHooks, config) => { + if (!obj || typeof obj !== 'object') { + return + } + + let res = {} + let shouldBindToState = false + + if (Array.isArray(obj)) { + res = [] + } + + for (const [key, value] of Object.entries(obj)) { + if (typeof value !== 'object') { + res[key] = value + + continue + } + + if (specialTypeHandler[value?.type]) { + res[key] = specialTypeHandler[value.type](value, globalHooks, config)?.value || '' + + if (specialTypes.includes(value.type)) { + shouldBindToState = true + } + + continue + } + + if (hasAccessor(value?.accessor)) { + res[key] = value.defaultValue + + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_METHODS, + value: value.accessor.getter?.value || value.accessor.setter?.value + }) + } + + const { res: tempRes, shouldBindToState: tempShouldBindToState } = transformObjType(value, globalHooks, config) + + res[key] = tempRes + + if (tempShouldBindToState) { + shouldBindToState = true + } + } + + return { + shouldBindToState, + res + } +} + +export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { + const { resArr, props } = schemaData + + Object.entries(props).forEach(([key, value]) => { + if (!value || typeof value !== 'object') { + return + } + + if ([JS_EXPRESSION, JS_I18N].includes(value?.type)) { + return + } + + // TODO: 处理 accessor 协议 + const { res, shouldBindToState } = transformObjType(value, globalHooks, config) + + if (shouldBindToState) { + let stateKey = key + let addSuccess = globalHooks.addState(key, res) + + while (!addSuccess) { + stateKey = `${key}${randomString()}` + addSuccess = globalHooks.addState(key, res) + } + + resArr.push(`:${key}=state.${stateKey}`) + } else { + resArr.push(`:${key}=${res}`) + } + + delete props[key] + }) +} + // 处理基本类似的 attribute,如 string、boolean export const handlePrimitiveAttributeHook = (schemaData) => { const { resArr, props } = schemaData for (const [key, value] of Object.entries(props)) { const valueType = typeof value - const renderKey = handleAttributeKey(key) if (valueType === 'string') { - resArr.push(`${renderKey}=${value}`) + resArr.push(`${key}=${value}`) delete props[key] } if (['boolean', 'number'].includes(valueType)) { - resArr.push(`:${renderKey}=${value}`) + resArr.push(`:${key}=${value}`) delete props[key] } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateChildren.js b/packages/vue-generator/src/generator/vue/sfc/generateChildren.js deleted file mode 100644 index a60ac008c0..0000000000 --- a/packages/vue-generator/src/generator/vue/sfc/generateChildren.js +++ /dev/null @@ -1,4 +0,0 @@ -export const generateChildren = () => { - // generate empty children - // generate tag name children -} diff --git a/packages/vue-generator/src/generator/vue/sfc/generateScript.js b/packages/vue-generator/src/generator/vue/sfc/generateScript.js index 7d133a4cc8..4ca7255004 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateScript.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateScript.js @@ -1,48 +1,256 @@ -import { genCompImport } from './parseImport' +import { capitalize } from '@vue/shared' +import { toEventKey, hasAccessor } from '@/utils' +import { generateImportByPkgName } from '@/utils/generateImportStatement' +import { INSERT_POSITION } from '@/constant' +// import { genCompImport } from './parseImport' +import { transformObjType } from './generateAttribute' -const generateImports = (schema, config = {}) => { - const { defaultImports = [], componentsMap = [] } = config - // 组件 import - const compImportStr = genCompImport(schema, componentsMap) +// const generateImports = (schema, config = {}) => { +// const { defaultImports = [], componentsMap = [] } = config +// // 组件 import +// const compImportStr = genCompImport(schema, componentsMap) - return `${[...defaultImports, ...compImportStr].join('\n')}` +// return `${[...defaultImports, ...compImportStr].join('\n')}` +// } + +// export const generateSetupScript = (schema, config) => { +// // generate import statement +// // props 声明 +// // emits 声明 +// // resource 工具类绑定 +// // reactive State 页面变量绑定声明 +// // js 方法声明绑定 +// // 生命周期绑定 + +// const lang = '' +// const scriptStart = `' +// const defaultImports = [ +// 'import * as vue from "vue"', +// 'import { defineProps, defineEmits } from "vue"', +// 'import { I18nInjectionKey } from "vue-i18n"' +// ] +// const compImportStr = generateImports(schema) +// const importStr = `${defaultImports.join('\n')}\n${compImportStr}` +// } + +export const defaultGenImportHook = (dependenciesMap = {}) => { + return Object.entries(dependenciesMap) + .map(([key, value]) => { + return generateImportByPkgName({ pkgName: key, imports: value }) || '' + }) + .join('\n') +} + +export const defaultGenPropsHook = (schema) => { + const propsArr = [] + const properties = schema?.schema?.properties || [] + + properties.forEach(({ content = [] }) => { + content.forEach(({ property, type, defaultValue }) => { + let propType = capitalize(type) + let propValue = defaultValue + + if (propType === 'String') { + propValue = JSON.stringify(defaultValue) + } else if (['Array', 'Object'].includes(propType)) { + propValue = `() => (${JSON.stringify(defaultValue)})` + } else if (propType === 'Function') { + propValue = defaultValue.value + } + + propsArr.push(`${property}: { type: ${propType}, default: ${propValue} }`) + }) + }) + + return `const props = defineProps({ ${propsArr.join(',')} })\n` +} + +export const defaultGenEmitsHook = (schema) => { + const emitArr = schema?.schema?.events || {} + const renderArr = Object.keys(emitArr).map(toEventKey) + + return `const emit = defineEmits(${JSON.stringify(renderArr)})` +} + +export const defaultGenStateHook = (schema, globalHooks) => { + const reactiveStatement = `const state = vue.reactive(${JSON.stringify(globalHooks.getState() || {}, null, 2)})` + + return reactiveStatement +} + +export const defaultGenMethodHook = (schema, globalHooks) => { + const methods = globalHooks.getMethods() || {} + + // TODO: 判断 methods 中是否有 jsx + const methodsArr = Object.entries(methods).map(([key, item]) => `const ${key} = wrap(${item.value})`) + const methodsName = Object.keys(methods) + + return `${methodsArr.join('\n')}\nwrap({ ${methodsName.join(',')} })` +} + +export const defaultGenLifecycleHook = (schema) => { + const { setup: setupFunc, ...restLifeCycle } = schema?.lifeCycles || {} + + let setupRes = '' + + if (setupFunc) { + const setupStatement = `const setup = wrap(${setupFunc.value})` + const setupExecution = 'setup({ props, context: { emit }, state, ...vue })' + + setupRes = `${setupStatement}\n${setupExecution}` + } + + const restLifeCycleRes = Object.entries(restLifeCycle).map(([key, item]) => `vue.${key}(wrap(${item.value}))`) + + return `${setupRes}${restLifeCycleRes.join('\n')}` } -export const generateSetupScript = (schema, config) => { - // generate import statement - // props 声明 - // emits 声明 - // resource 工具类绑定 - // reactive State 页面变量绑定声明 - // js 方法声明绑定 - // 生命周期绑定 +export const parsePropsHook = (schema, globalHooks) => { + const properties = schema?.schema?.properties || [] - const lang = '' - const scriptStart = `' - const defaultImports = [ - 'import * as vue from "vue"', - 'import { defineProps, defineEmits } from "vue"', - 'import { I18nInjectionKey } from "vue-i18n"' - ] - const compImportStr = generateImports(schema) - const importStr = `${defaultImports.join('\n')}\n${compImportStr}` + properties.forEach(({ content = [] }) => { + content.forEach(({ accessor } = {}) => { + if (hasAccessor(accessor)) { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_METHODS, + value: accessor.getter?.value || accessor.setter?.value + }) + } + }) + }) +} + +export const parseReactiveStateHook = (schema, globalHooks, config) => { + const { res } = transformObjType(globalHooks.getState() || {}, globalHooks, config) + + globalHooks.setState(res || {}) +} + +export const handleProvideStatesContextHook = (schema, globalHooks) => { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_STATE, + value: `wrap({ state })` + }) +} + +export const handleContextInjectHook = (schema, globalHooks) => { + const injectLowcode = 'const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()' + const injectLowcodeWrap = 'const wrap = lowcodeWrap(props, { emit })' + const wrapStoresStatement = `wrap({ stores })` + + globalHooks.addStatement({ + key: 'tiny-engine-inject-statement', + position: INSERT_POSITION.AFTER_PROPS, + value: `${injectLowcode}\n${injectLowcodeWrap}\n${wrapStoresStatement}` + }) +} + +export const addDefaultVueImport = (schema, globalHooks) => { + globalHooks.addImport('vue', { + destructuring: false, + exportName: '*', + componentName: 'vue' + }) + + globalHooks.addImport('vue', { + destructuring: true, + exportName: 'defineProps', + componentName: 'defineProps' + }) + + globalHooks.addImport('vue', { + destructuring: true, + exportName: 'defineEmits', + componentName: 'defineEmits' + }) +} + +export const addDefaultVueI18nImport = (schema, globalHooks) => { + globalHooks.addImport('vue-i18n', { + destructuring: true, + exportName: 'I18nInjectionKey', + componentName: 'I18nInjectionKey' + }) +} + +export const GEN_SCRIPT_HOOKS = { + GEN_IMPORT: 'GEN_IMPORT', + GEN_PROPS: 'GEN_PROPS', + GEN_EMIT: 'GEN_EMIT', + GEN_STATE: 'GEN_STATE', + GEN_METHOD: 'GEN_METHOD', + GEN_LIFECYCLE: 'GEN_LIFECYCLE' } export const genScriptByHook = (schema, globalHooks, config) => { - // 变量、方法、生命周期可能会相互影响,对 script lang 也可能有影响(产生 jsx),先解析,再生成 - // parseState() - // parseMethod() - // parseLifeCycles() - - // generateImports - // generate props declaration - // generate emit declaration - // generate reactive statement - // generate method statement - // generate lifecycle statement - // generate setup statement - // generate extra statement - - return `` + const hooks = config.hooks || {} + const { parseScript = [], genScript = {} } = hooks + + for (const parseHook of parseScript) { + parseHook(schema, globalHooks, config) + } + + const { AFTER_IMPORT, BEFORE_PROPS, AFTER_PROPS, BEFORE_STATE, AFTER_STATE, BEFORE_METHODS, AFTER_METHODS } = + INSERT_POSITION + + const statementGroupByPosition = { + [AFTER_IMPORT]: [], + [BEFORE_PROPS]: [], + [AFTER_PROPS]: [], + [BEFORE_STATE]: [], + [AFTER_STATE]: [], + [BEFORE_METHODS]: [], + [AFTER_METHODS]: [] + } + + const statements = globalHooks.getStatements() || {} + + Object.values(statements).forEach((statement) => { + if (statementGroupByPosition[statement.position]) { + statementGroupByPosition[statement.position].push(statement?.value) + + return + } + + statementGroupByPosition[AFTER_METHODS].push(statement?.value) + }) + + // TODO: statement generate + const importStr = genScript[GEN_SCRIPT_HOOKS.GEN_IMPORT]?.() || '' + const propsStr = genScript[GEN_SCRIPT_HOOKS.GEN_PROPS]?.() || '' + const emitStr = genScript[GEN_SCRIPT_HOOKS.GEN_EMIT]?.() || '' + const stateStr = genScript[GEN_SCRIPT_HOOKS.GEN_STATE]?.() || '' + const methodStr = genScript[GEN_SCRIPT_HOOKS.GEN_METHOD]?.() || '' + const lifeCycleStr = genScript[GEN_SCRIPT_HOOKS.GEN_LIFECYCLE]?.() || '' + + const scriptConfig = globalHooks.getScriptConfig() + + let scriptTag = ' +` } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTag.js b/packages/vue-generator/src/generator/vue/sfc/generateTag.js index 80d3df7eae..10973c9bf2 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTag.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTag.js @@ -23,6 +23,8 @@ export const generateTag = (tagName, config = {}) => { return '' } + let renderTagName = tagName + const isVoidEle = isVoidElement || (typeof isVoidElement !== 'boolean' && HTML_DEFAULT_VOID_ELEMENTS.includes(renderTagName)) @@ -31,8 +33,6 @@ export const generateTag = (tagName, config = {}) => { return '' } - let renderTagName = tagName - if (useHyphenate) { renderTagName = hyphenate(tagName) } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js index 4a5c1fdb76..4c1cd8c089 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -77,7 +77,7 @@ export const handleTinyIcon = (nameObj, globalHooks) => { const name = nameObj.schema.props.name const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` - globalHooks.addImport('@opentiny/vue-icon', { + const success = globalHooks.addImport('@opentiny/vue-icon', { componentName: iconName, exportName: name, package: '@opentiny/vue-icon', @@ -86,7 +86,13 @@ export const handleTinyIcon = (nameObj, globalHooks) => { }) // tiny icon 需要调用 - globalHooks.addStatement(INSERT_POSITION.BEFORE_PROPS, `const ${iconName} = ${name}()`) + if (success) { + globalHooks.addStatement({ + position: INSERT_POSITION.BEFORE_PROPS, + value: `const ${iconName} = ${name}()`, + key: iconName + }) + } nameObj.componentName = iconName delete nameObj.schema.props.name @@ -101,31 +107,21 @@ export const handleTinyGrid = (schemaData) => { } } -// 处理 templateName -// - 内置 templateName 映射 -// - 自定义 templateName 处理 -// 处理 attribute -// - 常见 attribute 处理(string、number、boolean) -// - 特殊 attribute 处理 -// - js expression 、js function、 js slot、i18n、js resource、 -// - 自定义 attribute 处理(可以转化成 children、add import、add script statement、) -// 处理 children -// 自定义处理 children 方法() - -// 处理 attribute -// 处理 children - // TODO: 支持物料中自定义出码关联片段 -// hooks 如何注入? -// 如何判断 hooks 已处理?、未处理? -// 如何判断该调用哪一个 hooks ? -const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => { - const schemaChildren = schemaWithRes.children || [] +export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => { + const schemaChildren = schemaWithRes?.schema?.children || [] + console.log('schemaChildren', schemaChildren) const { hooks = {} } = config // 自定义 hooks const { componentName: componentNameHooks, attribute: attributeHooks, children: childrenHooks } = hooks + if (!Array.isArray(schemaChildren)) { + schemaWithRes.resArr.push(schemaChildren || '') + + return + } + const resArr = schemaChildren.map((schemaItem) => { if (typeof schemaItem !== 'object' || !schemaItem) { return schemaItem || '' @@ -139,17 +135,17 @@ const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => } for (const hookItem of componentNameHooks) { - hookItem(parsedComponentName, globalHooks) + hookItem(parsedComponentName, globalHooks, config) } const parsedAttribute = { resArr: [], - props: structuredClone(props), + props: structuredClone(props || {}), schema: schemaItem } for (const hookItem of attributeHooks) { - hookItem(parsedAttribute, globalHooks) + hookItem(parsedAttribute, globalHooks, config) } const parsedChildren = { @@ -162,7 +158,7 @@ const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => } const startTag = generateTag(parsedComponentName.componentName, { - attribute: parsedAttribute.join(' '), + attribute: parsedAttribute.resArr.join(' '), isVoidElement: parsedComponentName.voidElement }) @@ -172,7 +168,7 @@ const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => endTag = generateTag(parsedComponentName.componentName, { isStartTag: false }) } - return `${startTag}${parsedChildren.join('')}${endTag}` + return `${startTag}${parsedChildren.resArr.join('')}${endTag}` }) schemaWithRes.resArr = schemaWithRes.resArr.concat(resArr) diff --git a/packages/vue-generator/src/generator/vue/sfc/index.js b/packages/vue-generator/src/generator/vue/sfc/index.js index b88b592d74..27a612c087 100644 --- a/packages/vue-generator/src/generator/vue/sfc/index.js +++ b/packages/vue-generator/src/generator/vue/sfc/index.js @@ -1,166 +1 @@ -import { getImportMap } from './parseImport' -import { genTemplateByHook } from './generateTemplate' -import { generateStyleTag } from './generateStyle' - -const parseConfig = (config = {}) => { - const { - blockRelativePath = '../components/', - blockSuffix = '.vue', - scriptConfig = {}, - styleConfig = {} - } = config || {} - const res = { - ...config, - blockRelativePath, - blockSuffix, - scriptConfig, - styleConfig - } - - return res -} - -const defaultScriptConfig = { - lang: '', - setup: true -} - -const defaultStyleConfig = { - scoped: true, - lang: '' -} - -const generateSFCFile = (schema, componentsMap, config = {}) => { - const parsedConfig = parseConfig(config) - const { blockRelativePath, blockSuffix, scriptConfig: initScriptConfig, styleConfig: initStyleConfig } = parsedConfig - // 前置动作,对 Schema 进行解析初始化相关配置与变量 - - // 解析 import - const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix }) - - // 解析 state - const state = schema.state || {} - - // 解析 method - const methods = schema.methods || {} - - const POSITION = { - AFTER_IMPORT: 'AFTER_IMPORT', - BEFORE_PROPS: 'BEFORE_PROPS', - AFTER_PROPS: 'AFTER_PROPS', - BEFORE_STATE: 'BEFORE_STATE', - AFTER_STATE: 'AFTER_STATE', - BEFORE_METHODS: 'BEFORE_METHODS', - AFTER_METHODS: 'AFTER_METHODS' - } - - // 其他表达式语句 - const statements = { - [POSITION.AFTER_IMPORT]: [], - [POSITION.BEFORE_PROPS]: [], - [POSITION.AFTER_PROPS]: [], - [POSITION.BEFORE_STATE]: [], - [POSITION.AFTER_STATE]: [], - [POSITION.BEFORE_METHODS]: [], - [POSITION.AFTER_METHODS]: [] - } - - // config - let scriptConfig = { - ...defaultScriptConfig, - ...initScriptConfig - } - - let styleConfig = { - ...defaultStyleConfig, - ...initStyleConfig - } - - const globalHooks = { - addStatement: (newStatement) => { - if (!newStatement?.value) { - return false - } - - ;(statements[newStatement?.position] || statements[POSITION.AFTER_METHODS]).push(newStatement?.value) - - return true - }, - addMethod: (key, value) => { - if (methods[key]) { - return false - } - - methods[key] = value - - return true - }, - addState: (key, value) => { - if (state[key]) { - return false - } - - state[key] = value - - return true - }, - addImport: (fromPath, config) => { - const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath] - - if (dependenciesMap) { - // 默认导出 - if (!config.destructuring && dependenciesMap.find(({ destructuring }) => !destructuring)) { - return false - } - - dependenciesMap.push(config) - - return true - } - - pkgMap[fromPath] = [config] - - return true - }, - setScriptConfig: (newConfig) => { - if (!newConfig || typeof newConfig !== 'object') { - return - } - - scriptConfig = { - ...scriptConfig, - ...newConfig - } - }, - getScriptConfig: () => scriptConfig, - setStyleConfig: (newConfig = {}) => { - if (!newConfig || typeof newConfig !== 'object') { - return - } - - styleConfig = { - ...styleConfig, - ...newConfig - } - }, - getStyleConfig: () => styleConfig, - addCss: (css) => { - schema.css = `${schema.css}\n${css}` - } - } - - // TODO: 支持页面级别的 dataSource、utils - - // 解析 template - const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) - - // 生成 script - const scriptStr = '' - - // 生成 style - const styleStr = generateStyleTag(schema, styleConfig) - - return `${templateStr}\n${scriptStr}\n${styleStr}` -} - -export default generateSFCFile +export { default as generateSFCFile, genSFCWithDefaultPlugin } from './genSetupSFC' diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index 0d7a9c06d0..6289eb57fb 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -93,9 +93,9 @@ const toEventKey = (str) => { return hyphenate(strRemovedPrefix) } -const isGetter = (accessor) => accessor?.getter?.type === JS_FUNCTION -const isSetter = (accessor) => accessor?.setter?.type === JS_FUNCTION -const hasAccessor = (accessor) => isGetter(accessor) || isSetter(accessor) +export const isGetter = (accessor) => accessor?.getter?.type === JS_FUNCTION +export const isSetter = (accessor) => accessor?.setter?.type === JS_FUNCTION +export const hasAccessor = (accessor) => isGetter(accessor) || isSetter(accessor) const addAccessorRecord = (accessor, record) => { if (isGetter(accessor)) { @@ -153,7 +153,6 @@ export { prettierOpts, isOn, toEventKey, - hasAccessor, addAccessorRecord, addIconRecord, handleIconInProps diff --git a/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js b/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js new file mode 100644 index 0000000000..d7905ec863 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js @@ -0,0 +1,11 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './schema.json' +import componentsMap from './componentsMap.json' +import expectedRes from './expected.vue?raw' + +console.log('case01', typeof expectedRes) + +test('should validate tagName', () => { + expect(genSFCWithDefaultPlugin(schema, componentsMap)).toBe('') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json new file mode 100644 index 0000000000..41c01dd1f9 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json @@ -0,0 +1,28 @@ +[ + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "Img", + "exportName": "", + "package": "", + "version": "1.0.0", + "destructuring": true + }, + { + "componentName": "FormTable", + "main": "./views" + }, + { + "componentName": "ImageTitle", + "main": "./components" + }, + { + "componentName": "CrmQuoteListGridStatus", + "main": "./views/crm/quote-list" + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected.vue b/packages/vue-generator/test/testcases/sfc/case01/expected.vue new file mode 100644 index 0000000000..04a4e7c925 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/expected.vue @@ -0,0 +1,472 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case01/schema.json b/packages/vue-generator/test/testcases/sfc/case01/schema.json new file mode 100644 index 0000000000..bda02c6445 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/schema.json @@ -0,0 +1,715 @@ +{ + "version": "1.1", + "componentName": "Page", + "fileName": "FormTable", + "css": ".overflow-container .card {\n padding-bottom: 8px;\n}\n.main-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n margin: 20px 20px 9px 20px;\n}\n.card {\n padding: 20px 20px;\n background-color: #ffffff;\n box-shadow: 0 2px 10px 0 rgb(0 0 0 / 6%);\n border-radius: 2px;\n}\n.manage-list {\n margin-bottom: 60px !important;\n} .crm-title-wrapper{\n display: flex;\n justify-content: start;\n align-items: center;\n margin-bottom: 20px;\n gap: 20px;\n}\n .crm-import-button:not(:last-child) {\n margin-right: 10px;\n}", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "style": "background: url(\"**/public/logo.png\");", + "className": "page-header", + "text": "标题区" + } + }, + { + "componentName": "Text", + "props": { + "style": "background: url('**/public/background.png');", + "text": "副标题区" + } + }, + { + "componentName": "Template", + "props": { + "text": "空插槽,出码会跳过此节点" + }, + "children": [] + }, + { + "componentName": "ImageTitle", + "fileName": "ImageTitle", + "props": { + "className": { + "type": "JSExpression", + "value": "['basic-info', {'form-fixed-layout': this.props.isFixed}, {'form-auto-layout': this.props.isAuto}]" + }, + "text": "配置报价", + "hasSplitLine": false, + "onClickLogo": { + "type": "JSExpression", + "value": "this.handleReset", + "params": ["state.flag"] + } + } + }, + { + "componentName": "TinyForm", + "props": { + "inline": true, + "style": { + "margin": "12px" + }, + "className": "form" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": { + "type": "i18n", + "key": "company.name" + } + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "disabled": false, + "value": { + "type": "JSExpression", + "value": "state.companyName" + } + } + } + ] + }, + { + "componentName": "TinyFormItem", + "condition": { + "type": "JSExpression", + "value": "state.cityOptions.length" + }, + "children": [ + { + "componentName": "Template", + "props": { + "slot": "label" + }, + "children": "城市" + }, + { + "componentName": "TinySelect", + "props": { + "value": { + "type": "JSExpression", + "value": "state.companyCity" + }, + "options": [ + { + "label": { + "type": "i18n", + "key": "city.foochow", + "zh_CN": "福州", + "en_US": "Foochow" + }, + "value": 0 + }, + { + "label": "深'i'圳", + "value": 1 + }, + { + "label": "中山", + "value": 2 + }, + { + "label": "龙岩", + "value": 3 + }, + { + "label": "韶关", + "value": 4 + }, + { + "label": "黄冈", + "value": 5 + }, + { + "label": "赤壁", + "value": 6 + }, + { + "label": "厦门", + "value": 7 + } + ] + } + } + ] + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "className": "form-footer", + "text": "表单提交区" + } + }, + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "icon": { + "componentName": "Icon", + "props": { + "name": "IconSearch" + } + }, + "onClick": { + "type": "JSExpression", + "value": "this.handleSearch" + } + }, + "children": "搜索" + }, + { + "componentName": "TinyButton", + "props": { + "onClick": { + "type": "JSExpression", + "value": "this.handleReset" + } + }, + "children": { + "type": "i18n", + "key": "operation.reset" + } + } + ] + } + ] + }, + { + "componentName": "Collection", + "props": { + "dataSource": "a5f6ef4f" + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "columns": { + "type": "JSExpression", + "value": "state.columns" + }, + "data": { + "type": "JSExpression", + "value": "state.tableData" + }, + "fetchData": { + "type": "JSExpression", + "value": "{ api: getTableData }" + } + } + } + ] + }, + { + "componentName": "Collection", + "props": { + "dataSource": "a5f6ef4f" + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "columns": [ + { "type": "index", "width": 60, "title": "" }, + { "type": "selection", "width": 60 }, + { + "field": "employees", + "title": "员工数", + "slots": { + "default": { + "type": "JSSlot", + "params": ["row", "rowIndex"], + "value": [{ "componentName": "TinyInput", "props": {}, "id": "49e232ce" }] + } + } + }, + { "field": "city", "title": "城市" }, + { + "title": "产品", + "slots": { + "default": { + "type": "JSSlot", + "params": ["row"], + "value": [ + { + "componentName": "div", + "id": "592fbc05", + "children": [{ "componentName": "TinySwitch", "props": { "modelValue": "" }, "id": "46a60c6f" }] + } + ] + } + } + }, + { + "title": "操作", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "TinyButton", + "props": { + "text": "删除", + "icon": { + "componentName": "Icon", + "props": { + "name": "IconDel" + } + }, + "onClick": { + "type": "JSExpression", + "value": "this.emit", + "params": ["row"] + } + } + } + ] + } + } + } + ], + "data": { + "type": "JSExpression", + "value": "state.tableData" + }, + "fetchData": { + "type": "JSExpression", + "value": "{ api: getTableData }" + } + } + } + ] + }, + { + "componentName": "div", + "props": { + "style": { + "width": { + "type": "JSExpression", + "value": "this.props.quotePopWidth" + } + } + }, + "children": "循环渲染:" + }, + { + "componentName": "Icon", + "condition": false, + "props": { + "name": "TinyIconHelpCircle" + } + }, + { + "children": [ + { + "componentName": "TinyButton", + "loop": { + "type": "JSExpression", + "value": "state.buttons" + }, + "loopArgs": ["item", "index"], + "props": { + "key": { + "type": "JSExpression", + "value": "item.text" + }, + "type": { + "type": "JSExpression", + "value": "item.type" + }, + "text": { + "type": "JSExpression", + "value": "index + item.text" + } + } + } + ] + }, + { + "componentName": "br" + }, + { + "children": [ + { + "componentName": "TinyButton", + "loop": [ + { + "type": "primary", + "text": "字面量" + }, + { + "type": "success", + "text": "字面量" + }, + { + "type": "danger", + "text": "危险操作" + } + ], + "loopArgs": ["item"], + "props": { + "key": { + "type": "JSExpression", + "value": "item.text" + }, + "type": { + "type": "JSExpression", + "value": "item.type" + }, + "text": { + "type": "JSExpression", + "value": "item.text" + } + } + } + ] + } + ], + "state": { + "IconPlusSquare": { + "type": "JSResource", + "value": "this.utils.IconPlusSquare()" + }, + "theme": "{ \"id\": 22, \"name\": \"@cloud/tinybuilder-theme-dark\", \"description\": \"黑暗主题\" }", + "companyName": "", + "companyOptions": null, + "companyCity": "", + "cityOptions": [ + { + "label": "福州", + "value": 0 + }, + { + "label": "深圳", + "value": 1 + }, + { + "label": "中山", + "value": 2 + }, + { + "label": "龙岩", + "value": 3 + }, + { + "label": "韶关", + "value": 4 + }, + { + "label": "黄冈", + "value": 5 + }, + { + "label": "赤壁", + "value": 6 + }, + { + "label": "厦门", + "value": 7 + } + ], + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true, + "activeMethod": { + "type": "JSFunction", + "value": "function() { return this.props.isEdit }" + } + }, + "columns": [ + { + "type": { + "type": "JSExpression", + "value": "this.props.isEdit ? 'selection' : 'index'" + }, + "width": "60", + "title": { + "type": "JSExpression", + "value": "this.props.isEdit ? '' : '序号'" + } + }, + { + "field": "status", + "title": "状态", + "filter": { + "layout": "input,enum,default,extends,base", + "inputFilter": { + "component": { + "type": "JSResource", + "value": "this.utils.Numeric" + }, + "attrs": { "format": "yyyy/MM/dd hh:mm:ss" }, + "relation": "A", + "relations": [ + { + "label": "小于", + "value": "A", + "method": { + "type": "JSFunction", + "value": "function({ value, input }) { return value < input }" + } + }, + { "label": "等于", "value": "equals" }, + { "label": "大于", "value": "greaterThan" } + ] + }, + "extends": [ + { + "label": "我要过滤大于800的数", + "method": { + "type": "JSFunction", + "value": "function({ value }) { return value > 800 }" + } + }, + { + "label": "我要过滤全部的数", + "method": { + "type": "JSFunction", + "value": "function() { return true }" + } + } + ] + }, + "slots": { + "default": { + "type": "JSSlot", + "params": ["row"], + "value": [ + { + "componentName": "div", + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconEdit" + } + }, + { + "componentName": "Block", + "fileName": "CrmQuoteListGridStatus", + "condition": { + "type": "JSExpression", + "value": "this.props.isEdit" + }, + "props": { + "isEdit": { + "type": "JSExpression", + "value": "this.props.isEdit" + }, + "status": { + "type": "JSExpression", + "value": "row.status" + } + } + } + ] + } + ] + } + } + }, + { + "type": "index", + "width": 60 + }, + { + "type": "selection", + "width": 60 + }, + { + "field": "name", + "title": "公司名称" + }, + { + "field": "employees", + "title": "员工数" + }, + { + "field": "city", + "title": "城市" + }, + { + "title": "操作", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "component": "div", + "props": { + "style": "color: rgb(94,124, 224);cursor:pointer;", + "text": { + "type": "i18n", + "key": "operation.delete" + }, + "prop1": { + "a": 123 + }, + "visible": true, + "onClick": { + "type": "JSExpression", + "value": "this.emit" + } + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "value": { + "type": "JSExpression", + "value": "row.giveamount", + "model": { + "prop": "" + } + } + } + }, + { + "component": "span", + "condition": { + "type": "JSExpression", + "value": "state.cityOptions.length" + }, + "children": { + "type": "i18n", + "key": "operation.hello" + } + }, + { + "componentName": "Icon", + "props": { + "name": "TinyIconHelpCircle", + "style": "margin-left: 6px; cursor: pointer;vertical-align: top;" + } + } + ] + } + ] + } + } + } + ], + "tableData": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "boole": true + }, + { + "id": "3", + "name": "RFV有限责任公司", + "city": "中山", + "employees": 1300, + "boole": false + }, + { + "id": "4", + "name": "TGB科技有限公司", + "city": "龙岩", + "employees": 360, + "boole": true + }, + { + "id": "5", + "name": "YHN科技有限公司", + "city": "韶关", + "employees": 810, + "boole": true + }, + { + "id": "6", + "name": "WSX科技有限公司", + "city": "黄冈", + "employees": 800, + "boole": true + }, + { + "id": "7", + "name": "KBG物业有限公司", + "city": "赤壁", + "employees": 400, + "boole": false + }, + { + "id": "8", + "name": "深圳市福德宝网络技术有限公司", + "boole": true, + "city": "厦门", + "employees": 540 + } + ], + "status": { + "type": "JSExpression", + "value": "this.statusData", + "computed": true + }, + "buttons": [ + { + "type": "primary", + "text": "主要操作" + }, + { + "type": "success", + "text": "成功操作" + }, + { + "type": "danger", + "text": { + "type": "i18n", + "key": "operation.danger" + } + } + ] + }, + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function({ props, watch, onMounted }) {\r\n onMounted(() => {\r\n this.getTableDta()\r\n })\r\n watch(\r\n () => props.load,\r\n (load) => {\r\n if (load.isLoad) {\r\n this.getTableDta()\r\n }\r\n },\r\n {\r\n deep: true\r\n }\r\n )\r\n}" + }, + "onBeforeMount": { + "type": "JSFunction", + "value": "function() { return '生命周期:onBeforeMount'; }" + }, + "onMounted": { + "type": "JSFunction", + "value": "function onMounted() { return '生命周期:onMounted'; }" + } + }, + "methods": { + "getTableData": { + "type": "JSFunction", + "value": "function getData({ page, filterArgs }) {\n const { curPage, pageSize } = page;\n const offset = (curPage - 1) * pageSize;\n\n return new Promise((resolve) => {\n setTimeout(() => {\n const { tableData } = this.state;\n let result = [...tableData];\n\n if (filterArgs) {\n result = result.filter((item) => item.city === filterArgs);\n }\n\n const total = result.length;\n result = result.slice(offset, offset + pageSize);\n\n resolve({ result, page: { total } });\n }, 500);\n });\n}" + }, + "handleSearch": { + "type": "JSFunction", + "value": "function(e) { return ['搜索:', this.i18n('operation.search'), e]; }" + }, + "handleReset": { + "type": "JSFunction", + "value": "function handleReset(e) { return ['重置:', e]; }" + }, + "statusData": { + "type": "JSFunction", + "value": "function () {\r\n return [\r\n { name: this.i18n('quotes.common.configure_basic_information'), status: 'ready' },\r\n { name: this.i18n('quotes.quote_list.quote'), status: 'wait' },\r\n { name: this.i18n('quotes.common.complete_configuration_quote'), status: 'wait' }\r\n ]\r\n}" + } + } +} diff --git a/packages/vue-generator/test/testcases/sfc/index.js b/packages/vue-generator/test/testcases/sfc/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vue-generator/test/unit/template/test/generate.test.js b/packages/vue-generator/test/unit/template/test/generate.test.js index 8c9f2f7466..652647b2e9 100644 --- a/packages/vue-generator/test/unit/template/test/generate.test.js +++ b/packages/vue-generator/test/unit/template/test/generate.test.js @@ -1,8 +1,8 @@ import { expect, test } from 'vitest' -import { generateTag } from '@/generator/vue/template/generateTag' +import { generateTag } from '@/generator/vue/sfc/generateTag' test('should validate tagName', () => { - expect(generateTag('')).toBe(undefined) + expect(generateTag('')).toBe('') }) test('should generate start tag correctly', () => { diff --git a/packages/vue-generator/test/unit/template/test/generateAttribute.test.js b/packages/vue-generator/test/unit/template/test/generateAttribute.test.js index 3b3b4991b6..bee13d9042 100644 --- a/packages/vue-generator/test/unit/template/test/generateAttribute.test.js +++ b/packages/vue-generator/test/unit/template/test/generateAttribute.test.js @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { generateAttribute } from '@/generator/vue/template/generateAttribute' +// import { generateAttribute } from '@/generator/vue/template/generateAttribute' // 处理 number、boolean 绑定 From 0e5b177ac61b05b5125aa2e62e2cb0876ef5b7db Mon Sep 17 00:00:00 2001 From: chilingling Date: Thu, 14 Mar 2024 06:27:45 -0700 Subject: [PATCH 09/37] feat(vue-generator): support jsx generate --- .../src/generator/vue/sfc/genSetupSFC.js | 27 ++- .../generator/vue/sfc/generateAttribute.js | 217 ++++++++++++++---- .../src/generator/vue/sfc/generateScript.js | 56 ++--- .../src/generator/vue/sfc/generateTemplate.js | 106 ++++++--- .../src/generator/vue/sfc/parseImport.js | 14 +- .../src/utils/generateImportStatement.js | 12 +- packages/vue-generator/src/utils/index.js | 3 +- 7 files changed, 297 insertions(+), 138 deletions(-) diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js index a7c096bf0c..7133984d96 100644 --- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -1,5 +1,12 @@ import { getImportMap } from './parseImport' -import { genTemplateByHook, handleComponentNameHook, handleTinyGrid, handleTinyIcon } from './generateTemplate' +import { + genTemplateByHook, + handleComponentNameHook, + handleTinyGrid, + handleTinyIcon, + handleExpressionChildren, + validEmptyTemplateHook +} from './generateTemplate' import { generateStyleTag } from './generateStyle' import { handleConditionAttrHook, @@ -10,7 +17,8 @@ import { handleExpressionAttrHook, handleI18nAttrHook, handleObjBindAttrHook, - handleEventAttrHook + handleEventAttrHook, + handleTinyIconPropsHook } from './generateAttribute' import { GEN_SCRIPT_HOOKS, @@ -155,6 +163,7 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { return true }, + getImport: () => ({ ...pkgMap, ...blockPkgMap }), setScriptConfig: (newConfig) => { if (!newConfig || typeof newConfig !== 'object') { return @@ -197,7 +206,14 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { } export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => { - const { componentName = [], attribute = [], children = [], genScript = {}, parseScript = [] } = config.hooks || {} + const { + componentName = [], + attribute = [], + children = [], + genScript = {}, + parseScript = [], + templateItemValidate = [] + } = config.hooks || {} const defaultComponentHooks = [handleComponentNameHook, handleTinyIcon] const defaultAttributeHook = [ @@ -209,11 +225,13 @@ export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => { handlePrimitiveAttributeHook, handleExpressionAttrHook, handleI18nAttrHook, + handleTinyIconPropsHook, handleObjBindAttrHook, handleEventAttrHook ] - const defaultChildrenHook = [] + const defaultChildrenHook = [handleExpressionChildren] + const defaultTemplateItemValidateHook = [validEmptyTemplateHook] const defaultParseScriptHook = [ addDefaultVueImport, @@ -237,6 +255,7 @@ export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}) => { const newConfig = { ...config, hooks: { + templateItemValidate: [...templateItemValidate, ...defaultTemplateItemValidateHook], componentName: [...componentName, ...defaultComponentHooks], attribute: [...attribute, ...defaultAttributeHook], children: [...children, ...defaultChildrenHook], diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 73105089ba..04eec8699d 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -6,7 +6,8 @@ import { JS_RESOURCE, JS_SLOT, SPECIAL_UTILS_TYPE, - INSERT_POSITION + INSERT_POSITION, + TINY_ICON } from '@/constant' import { isOn, toEventKey, thisBindRe, randomString, getFunctionInfo, hasAccessor } from '@/utils' import { strategy } from '@/parser/state-type' @@ -43,21 +44,28 @@ export const generateLoopTemplate = (loop, loopArgs) => { return `v-for="(${iterVar.join(',')}) in ${source}"` } -const handleEventBinding = (key, item) => { +const handleEventBinding = (key, item, isJSX) => { const eventKey = toEventKey(key) let eventBinding = '' // vue 事件绑定,仅支持:内联事件处理器 or 方法事件处理器(绑定方法名或对某个方法的调用) if (item?.type === JS_EXPRESSION) { - const eventHandler = item.value.replace(thisBindRe, '') + let eventHandler = item.value.replace(thisBindRe, '') + let renderKey = isJSX ? `${key}` : `@${eventKey}` // Vue Template 中,为事件处理函数传递额外的参数时,需要使用内联箭头函数 if (item.params?.length) { const extendParams = item.params.join(',') - eventBinding = `@${eventKey}="(...eventArgs) => ${eventHandler}(eventArgs, ${extendParams})"` + eventHandler = `(...eventArgs) => ${eventHandler}(eventArgs, ${extendParams})` + } + + if (isJSX) { + eventHandler = `{${eventHandler}}` } else { - eventBinding = `@${eventKey}="${eventHandler}"` + eventHandler = `"${eventHandler}"` } + + eventBinding = `${renderKey}=${eventHandler}` } return eventBinding @@ -194,25 +202,35 @@ export const handleObjectBinding = (key, value) => { } } -const handleJSExpressionBinding = (key, value) => { +const handleJSExpressionBinding = (key, value, isJSX) => { + const expressValue = value.value.replace(thisBindRe, '') + + if (isJSX) { + return `${key}={${expressValue}}` + } + // 支持带参数的 v-model if (value.model) { const modelArgs = value.model?.prop ? `:${value.model.prop}` : '' - return `v-model${modelArgs}="${value.value.replace(thisBindRe, '')}"` + return `v-model${modelArgs}="${expressValue}"` } // expression 使用 v-bind 绑定 - return `:${key}="${value.value.replace(thisBindRe, '')}"` + return `:${key}="${expressValue}"` } -const handleBindI18n = (key, value) => { +const handleBindI18n = (key, value, isJSX) => { const tArguments = [`'${value.key}'`] // TODO: 拿到场景用例 const i18nParams = JSON.stringify(value.params) i18nParams && tArguments.push(i18nParams) + if (isJSX) { + return `${key}={t(${tArguments.join(',')})}` + } + return `:${key}="t(${tArguments.join(',')})"` } @@ -316,54 +334,114 @@ export const generateAttribute = (schema) => { } } -export const handleConditionAttrHook = (schemaData) => { - const { resArr, schema } = schemaData - const { condition } = schema +const handleJSXConditionBind = (schemaData, globalHooks, config) => { + const { prefix, suffix, schema: { condition } = {} } = schemaData + const isJSX = config.isJSX + + if (!isJSX) { + return + } + + if (typeof condition !== 'boolean' && !condition?.type) { + return + } + + if (prefix[0] !== '{') { + prefix.unshift('{') + } + + if (suffix.at(-1) !== '}') { + suffix.push('}') + } if (typeof condition === 'boolean') { - resArr.unshift(`v-if=${condition}`) + prefix.push(`${condition} && `) + return } - if (!condition?.type) { + const conditionValue = condition?.value?.replace(thisBindRe, '') + + prefix.push(`${conditionValue} &&`) +} + +export const handleConditionAttrHook = (schemaData, globalHooks, config) => { + const { attributes, schema: { condition } = {} } = schemaData + const isJSX = config.isJSX + + if (isJSX) { + handleJSXConditionBind(schemaData, globalHooks, config) + return } - if (condition?.kind === 'else') { - resArr.unshift('v-else') + if (typeof condition === 'boolean') { + attributes.unshift(`v-if=${condition}`) + return + } + + if (!condition?.type) { + return } const conditionValue = condition?.value?.replace(thisBindRe, '') - resArr.unshift(`v-${condition?.kind || 'if'}=${conditionValue}`) + if (condition?.kind === 'else') { + attributes.unshift('v-else') + } + + attributes.unshift(`v-${condition?.kind || 'if'}=${conditionValue}`) } -export const handleLoopAttrHook = (schemaData = {}) => { - const { resArr, schema } = schemaData - const { loop, loopArgs } = schema || {} +export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { + const { prefix, suffix, attributes, schema: { loop, loopArgs } = {} } = schemaData + const isJSX = config.isJSX if (!loop) { return } - const source = (loop?.value || '').replace(thisBindRe, '') + let source = '' + + if (loop?.value && loop?.type) { + source = loop.value.replace(thisBindRe, '') + } else { + source = JSON.stringify(loop) + } + const iterVar = [...loopArgs] - resArr.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + if (!isJSX) { + attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + + return + } + + prefix.push(`${source}.map((${iterVar.join(',')}) => `) + suffix.unshift(`)`) + + if (prefix[0] !== '{') { + prefix.unshift['{'] + } + + if (suffix.at(-1) !== '}') { + suffix.push('}') + } } -export const handleEventAttrHook = (schemaData) => { - const { resArr, props } = schemaData +export const handleEventAttrHook = (schemaData, globalHooks, config) => { + const { attributes, schema: { props = {} } = {} } = schemaData || {} + const isJSX = config.isJSX const eventBindArr = Object.entries(props) .filter(([key]) => isOn(key)) - .map(([key, value]) => handleEventBinding(key, value)) + .map(([key, value]) => handleEventBinding(key, value, isJSX)) - resArr.push(...eventBindArr) + attributes.push(...eventBindArr) } export const handleSlotBindAttrHook = (schemaData) => { - const { resArr, props } = schemaData + const { attributes, schema: { props = {} } = {} } = schemaData || {} const slot = props?.slot @@ -372,9 +450,11 @@ export const handleSlotBindAttrHook = (schemaData) => { } if (typeof slot === 'string') { - resArr.push(`#${slot}`) + attributes.push(`#${slot}`) delete props.slot + + return } const { name, params } = slot @@ -387,13 +467,13 @@ export const handleSlotBindAttrHook = (schemaData) => { paramsValue = `="${params}"` } - resArr.push(`#${name}${paramsValue}`) + attributes.push(`#${name}${paramsValue}`) delete props.slot } export const handleAttrKeyHook = (schemaData) => { - const { props } = schemaData + const { schema: { props = {} } = {} } = schemaData const specialKey = { className: 'class' } @@ -407,24 +487,57 @@ export const handleAttrKeyHook = (schemaData) => { }) } -export const handleExpressionAttrHook = (schemaData) => { - const { resArr, props } = schemaData +export const handleExpressionAttrHook = (schemaData, globalHooks, config) => { + const { attributes, schema: { props = {} } = {} } = schemaData || {} + const isJSX = config.isJSX Object.entries(props).forEach(([key, value]) => { if (value?.type === JS_EXPRESSION && !isOn(key)) { - resArr.push(handleJSExpressionBinding(key, value)) + attributes.push(handleJSExpressionBinding(key, value, isJSX)) delete props[key] } }) } -export const handleI18nAttrHook = (schemaData) => { - const { resArr, props } = schemaData +export const handleI18nAttrHook = (schemaData, globalHooks, config) => { + const { attributes, schema: { props = {} } = {} } = schemaData || {} + const isJSX = config.isJSX Object.entries(props).forEach(([key, value]) => { if (value?.type === JS_I18N) { - resArr.push(handleBindI18n(key, value)) + attributes.push(handleBindI18n(key, value, isJSX)) + } + }) +} + +export const handleTinyIconPropsHook = (schemaData, globalHooks, config) => { + const { attributes, schema: { props = {} } = {} } = schemaData || {} + const isJSX = config.isJSX + + Object.entries(props).forEach(([key, value]) => { + if (value?.componentName === 'Icon' && value?.props?.name) { + const name = value.props.name + const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` + const success = globalHooks.addImport('@opentiny/vue-icon', { + componentName: name, + exportName: name, + package: '@opentiny/vue-icon', + version: '^3.10.0', + destructuring: true + }) + + if (success) { + globalHooks.addStatement({ + position: INSERT_POSITION.BEFORE_PROPS, + value: `const ${iconName} = ${name}()`, + key: iconName + }) + } + + attributes.push(isJSX ? `icon={${iconName}}` : `:icon="${iconName}"`) + + delete props[key] } }) } @@ -473,17 +586,17 @@ const specialTypeHandler = { globalHooks.setScriptConfig({ lang: 'jsx' }) const structData = { - resArr: [], + children: [], schema: { children: value } } // TODO: 需要验证 template 的生成有无问题 - recursiveGenTemplateByHook(structData, globalHooks, config) + recursiveGenTemplateByHook(structData, globalHooks, { ...config, isJSX: true }) // TODO: 这里不通用,需要设计通用的做法,或者独立成 grid 的 hook return { - value: `({${params.join(',')}}, h) => ${structData.resArr.join('')}` + value: `({${params.join(',')}}, h) => ${structData.children.join('')}` } } } @@ -526,7 +639,8 @@ export const transformObjType = (obj, globalHooks, config) => { }) } - const { res: tempRes, shouldBindToState: tempShouldBindToState } = transformObjType(value, globalHooks, config) + const { res: tempRes, shouldBindToState: tempShouldBindToState } = + transformObjType(value, globalHooks, config) || {} res[key] = tempRes @@ -542,7 +656,9 @@ export const transformObjType = (obj, globalHooks, config) => { } export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { - const { resArr, props } = schemaData + const { attributes, schema: { props = {} } = {} } = schemaData || {} + + const isJSX = config.isJSX Object.entries(props).forEach(([key, value]) => { if (!value || typeof value !== 'object') { @@ -553,21 +669,20 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { return } - // TODO: 处理 accessor 协议 const { res, shouldBindToState } = transformObjType(value, globalHooks, config) - if (shouldBindToState) { + if (shouldBindToState && !isJSX) { let stateKey = key let addSuccess = globalHooks.addState(key, res) while (!addSuccess) { stateKey = `${key}${randomString()}` - addSuccess = globalHooks.addState(key, res) + addSuccess = globalHooks.addState(stateKey, res) } - resArr.push(`:${key}=state.${stateKey}`) + attributes.push(`:${key}="state.${stateKey}"`) } else { - resArr.push(`:${key}=${res}`) + attributes.push(isJSX ? `${key}={${JSON.stringify(res)}}` : `:${key}="${JSON.stringify(res)}"`) } delete props[key] @@ -575,20 +690,22 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { } // 处理基本类似的 attribute,如 string、boolean -export const handlePrimitiveAttributeHook = (schemaData) => { - const { resArr, props } = schemaData +export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) => { + const { attributes } = schemaData + const props = schemaData.schema?.props || {} + const isJSX = config.isJSX for (const [key, value] of Object.entries(props)) { const valueType = typeof value if (valueType === 'string') { - resArr.push(`${key}=${value}`) + attributes.push(`${key}="${value}"`) delete props[key] } if (['boolean', 'number'].includes(valueType)) { - resArr.push(`:${key}=${value}`) + attributes.push(isJSX ? `${key}={${value}}` : `:${key}="${value}"`) delete props[key] } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateScript.js b/packages/vue-generator/src/generator/vue/sfc/generateScript.js index 4ca7255004..769a361e85 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateScript.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateScript.js @@ -2,39 +2,11 @@ import { capitalize } from '@vue/shared' import { toEventKey, hasAccessor } from '@/utils' import { generateImportByPkgName } from '@/utils/generateImportStatement' import { INSERT_POSITION } from '@/constant' -// import { genCompImport } from './parseImport' import { transformObjType } from './generateAttribute' -// const generateImports = (schema, config = {}) => { -// const { defaultImports = [], componentsMap = [] } = config -// // 组件 import -// const compImportStr = genCompImport(schema, componentsMap) - -// return `${[...defaultImports, ...compImportStr].join('\n')}` -// } - -// export const generateSetupScript = (schema, config) => { -// // generate import statement -// // props 声明 -// // emits 声明 -// // resource 工具类绑定 -// // reactive State 页面变量绑定声明 -// // js 方法声明绑定 -// // 生命周期绑定 - -// const lang = '' -// const scriptStart = `' -// const defaultImports = [ -// 'import * as vue from "vue"', -// 'import { defineProps, defineEmits } from "vue"', -// 'import { I18nInjectionKey } from "vue-i18n"' -// ] -// const compImportStr = generateImports(schema) -// const importStr = `${defaultImports.join('\n')}\n${compImportStr}` -// } - -export const defaultGenImportHook = (dependenciesMap = {}) => { +export const defaultGenImportHook = (schema, globalHooks) => { + const dependenciesMap = globalHooks.getImport() || {} + return Object.entries(dependenciesMap) .map(([key, value]) => { return generateImportByPkgName({ pkgName: key, imports: value }) || '' @@ -103,7 +75,7 @@ export const defaultGenLifecycleHook = (schema) => { const restLifeCycleRes = Object.entries(restLifeCycle).map(([key, item]) => `vue.${key}(wrap(${item.value}))`) - return `${setupRes}${restLifeCycleRes.join('\n')}` + return `${setupRes}\n${restLifeCycleRes.join('\n')}` } export const parsePropsHook = (schema, globalHooks) => { @@ -216,13 +188,12 @@ export const genScriptByHook = (schema, globalHooks, config) => { statementGroupByPosition[AFTER_METHODS].push(statement?.value) }) - // TODO: statement generate - const importStr = genScript[GEN_SCRIPT_HOOKS.GEN_IMPORT]?.() || '' - const propsStr = genScript[GEN_SCRIPT_HOOKS.GEN_PROPS]?.() || '' - const emitStr = genScript[GEN_SCRIPT_HOOKS.GEN_EMIT]?.() || '' - const stateStr = genScript[GEN_SCRIPT_HOOKS.GEN_STATE]?.() || '' - const methodStr = genScript[GEN_SCRIPT_HOOKS.GEN_METHOD]?.() || '' - const lifeCycleStr = genScript[GEN_SCRIPT_HOOKS.GEN_LIFECYCLE]?.() || '' + const importStr = genScript[GEN_SCRIPT_HOOKS.GEN_IMPORT]?.(schema, globalHooks, config) || '' + const propsStr = genScript[GEN_SCRIPT_HOOKS.GEN_PROPS]?.(schema, globalHooks, config) || '' + const emitStr = genScript[GEN_SCRIPT_HOOKS.GEN_EMIT]?.(schema, globalHooks, config) || '' + const stateStr = genScript[GEN_SCRIPT_HOOKS.GEN_STATE]?.(schema, globalHooks, config) || '' + const methodStr = genScript[GEN_SCRIPT_HOOKS.GEN_METHOD]?.(schema, globalHooks, config) || '' + const lifeCycleStr = genScript[GEN_SCRIPT_HOOKS.GEN_LIFECYCLE]?.(schema, globalHooks, config) || '' const scriptConfig = globalHooks.getScriptConfig() @@ -233,9 +204,11 @@ export const genScriptByHook = (schema, globalHooks, config) => { } if (scriptConfig.lang) { - scriptTag = `${scriptTag} lang=${scriptConfig.lang}` + scriptTag = `${scriptTag} lang="${scriptConfig.lang}"` } + scriptTag = `${scriptTag}>` + return ` ${scriptTag} ${importStr} @@ -251,6 +224,5 @@ ${statementGroupByPosition[BEFORE_METHODS].join('\n')} ${methodStr} ${statementGroupByPosition[AFTER_METHODS].join('\n')} ${lifeCycleStr} - -` +` } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js index 4c1cd8c089..d3f9a7859b 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -1,6 +1,14 @@ -import { BUILTIN_COMPONENT_NAME, BUILTIN_COMPONENT_NAME_MAP, TINY_ICON, INSERT_POSITION } from '@/constant' +import { + BUILTIN_COMPONENT_NAME, + BUILTIN_COMPONENT_NAME_MAP, + TINY_ICON, + INSERT_POSITION, + JS_EXPRESSION, + JS_I18N +} from '@/constant' import { generateTag } from './generateTag' import { generateAttribute, mergeDescription } from './generateAttribute' +import { thisBindRe } from '@/utils' const recursiveGenTemplate = (children) => { const effect = { @@ -78,7 +86,7 @@ export const handleTinyIcon = (nameObj, globalHooks) => { const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` const success = globalHooks.addImport('@opentiny/vue-icon', { - componentName: iconName, + componentName: name, exportName: name, package: '@opentiny/vue-icon', version: '^3.10.0', @@ -107,80 +115,112 @@ export const handleTinyGrid = (schemaData) => { } } +export const handleExpressionChildren = (schemaData = {}) => { + const { children, schema } = schemaData + const type = schema?.children?.type + + if (type === JS_EXPRESSION) { + children.push(`{{ ${schema.children?.value.replace(thisBindRe, '') || ''} }}`) + + delete schema.children + return + } + + if (type === JS_I18N && schema.children?.key) { + children.push(`{{ t('${schema.children.key}') }}`) + + delete schema.children + return + } +} + +export const validEmptyTemplateHook = (schema = {}) => { + if (schema.componentName === BUILTIN_COMPONENT_NAME.TEMPLATE && !schema.children?.length) { + return false + } + + return true +} + // TODO: 支持物料中自定义出码关联片段 export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => { const schemaChildren = schemaWithRes?.schema?.children || [] - console.log('schemaChildren', schemaChildren) const { hooks = {} } = config // 自定义 hooks - const { componentName: componentNameHooks, attribute: attributeHooks, children: childrenHooks } = hooks + const { + componentName: componentNameHooks, + attribute: attributeHooks, + children: childrenHooks, + templateItemValidate + } = hooks if (!Array.isArray(schemaChildren)) { - schemaWithRes.resArr.push(schemaChildren || '') + schemaWithRes.children.push(schemaChildren || '') return } const resArr = schemaChildren.map((schemaItem) => { + for (const validateItem of templateItemValidate) { + if (!validateItem(schemaItem, globalHooks, config)) { + return '' + } + } + if (typeof schemaItem !== 'object' || !schemaItem) { return schemaItem || '' } - const { componentName, props } = schemaItem - const parsedComponentName = { - componentName, + const { componentName } = schemaItem + + const optionData = { + schema: schemaItem, voidElement: false, - schema: schemaItem + componentName, + prefix: [], + attributes: [], + children: [], + suffix: [] } for (const hookItem of componentNameHooks) { - hookItem(parsedComponentName, globalHooks, config) - } - - const parsedAttribute = { - resArr: [], - props: structuredClone(props || {}), - schema: schemaItem + hookItem(optionData, globalHooks, config) } for (const hookItem of attributeHooks) { - hookItem(parsedAttribute, globalHooks, config) - } - - const parsedChildren = { - resArr: [], - schema: schemaItem + hookItem(optionData, globalHooks, config) } for (const hookItem of [...childrenHooks, recursiveGenTemplateByHook]) { - hookItem(parsedChildren, globalHooks, config) + hookItem(optionData, globalHooks, config) } - const startTag = generateTag(parsedComponentName.componentName, { - attribute: parsedAttribute.resArr.join(' '), - isVoidElement: parsedComponentName.voidElement + const startTag = generateTag(optionData.componentName, { + attribute: optionData.attributes.join(' '), + isVoidElement: optionData.voidElement }) let endTag = '' - if (!parsedComponentName.voidElement) { - endTag = generateTag(parsedComponentName.componentName, { isStartTag: false }) + if (!optionData.voidElement) { + endTag = generateTag(optionData.componentName, { isStartTag: false }) } - return `${startTag}${parsedChildren.resArr.join('')}${endTag}` + return ` +${optionData.prefix.join('')}${startTag}${optionData.children.join('')}${endTag}${optionData.suffix.join('')}` }) - schemaWithRes.resArr = schemaWithRes.resArr.concat(resArr) + schemaWithRes.children = schemaWithRes.children.concat(resArr) } export const genTemplateByHook = (schema, globalHooks, config) => { const parsedSchema = { - resArr: [], - schema: structuredClone(schema) + children: [], + schema: structuredClone({ children: [{ ...schema, componentName: 'div' }] }) } recursiveGenTemplateByHook(parsedSchema, globalHooks, config) - return `` + return `` } diff --git a/packages/vue-generator/src/generator/vue/sfc/parseImport.js b/packages/vue-generator/src/generator/vue/sfc/parseImport.js index 35ece3d48e..e3208c8e27 100644 --- a/packages/vue-generator/src/generator/vue/sfc/parseImport.js +++ b/packages/vue-generator/src/generator/vue/sfc/parseImport.js @@ -32,9 +32,14 @@ export const getImportMap = (schema, componentsMap, config) => { const importComps = componentsMap.filter(({ componentName }) => components.includes(componentName)) importComps.forEach((item) => { - pkgMap[item.package] = pkgMap[item.package] || [] + const key = item.package || item.main + if (!key) { + return + } - pkgMap[item.package].push(item) + pkgMap[key] = pkgMap[key] || [] + + pkgMap[key].push(item) }) const { blockRelativePath = '../components/', blockSuffix = '.vue' } = config @@ -43,12 +48,13 @@ export const getImportMap = (schema, componentsMap, config) => { blocks.map((name) => { const source = `${blockRelativePath}/${name}${blockSuffix}` - blockPkgMap[source] = { + blockPkgMap[source] = blockPkgMap[source] || [] + blockPkgMap[source].push({ componentName: name, exportName: name, destructuring: false, package: source - } + }) }) return { diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js index aa8b470bcf..9c5b7caa10 100644 --- a/packages/vue-generator/src/utils/generateImportStatement.js +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -36,11 +36,17 @@ export function generateImportByPkgName(config) { const { componentName, exportName } = defaultImports if (exportName && exportName !== componentName) { - defaultImportStatement = `${exportName} as ${componentName},` + defaultImportStatement = `${exportName} as ${componentName}` } else { - defaultImportStatement = `${exportName},` + defaultImportStatement = `${exportName || componentName || ''}` } } - return `import ${defaultImportStatement} { ${importStatements.join(',')} } from ${pkgName}` + if (!importStatements.length) { + return `import ${defaultImportStatement} from "${pkgName}"` + } + + const comma = defaultImportStatement ? ',' : '' + + return `import ${defaultImportStatement}${comma} { ${importStatements.join(',')} } from "${pkgName}"` } diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index 6289eb57fb..f3127e00f1 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -37,7 +37,7 @@ const getFunctionInfo = (fnStr) => { const safeRandom = () => { const mathConstructor = Math - return mathConstructor.random + return mathConstructor.random() } export const randomString = (length = 4, chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => { @@ -146,7 +146,6 @@ export { getTypeOfSchema, getFunctionInfo, safeRandom, - // randomString, avoidDuplicateString, lowerFirst, toPascalCase, From 9731d1342e1befca755727ce9896b691b056535f Mon Sep 17 00:00:00 2001 From: chilingling Date: Sat, 16 Mar 2024 03:40:27 -0700 Subject: [PATCH 10/37] fix(vue-generator): fix double quotes issue --- packages/vue-generator/src/constant/index.js | 2 + .../src/generator/generateApp.js | 2 +- packages/vue-generator/src/generator/index.js | 5 +- .../vue-generator/src/generator/vue/index.js | 0 .../src/generator/vue/sfc/README.md | 2 + .../src/generator/vue/sfc/genSetupSFC.js | 13 +- .../generator/vue/sfc/generateAttribute.js | 332 +++--------------- .../src/generator/vue/sfc/generateScript.js | 48 ++- .../src/generator/vue/sfc/generateTag.js | 2 +- .../src/generator/vue/sfc/generateTemplate.js | 83 +---- packages/vue-generator/src/index.js | 24 +- .../src/plugins/genBlockPlugin.js | 22 +- .../src/plugins/genPagePlugin.js | 22 +- .../src/utils/generateImportStatement.js | 10 +- packages/vue-generator/src/utils/index.js | 2 + .../test/testcases/sfc/case01/ImageTitle.vue | 73 ++++ .../testcases/sfc/case01/ImageTitleRes.vue | 59 ++++ .../testcases/sfc/case01/blocks.schema.json | 207 +++++++++++ .../test/testcases/sfc/case01/cast01.test.js | 15 +- .../test/testcases/sfc/case01/res.vue | 147 ++++++++ .../testcases/sfc/case02/PropAccessor.vue | 54 +++ .../testcases/sfc/case02/PropAccessorRes.vue | 50 +++ .../test/testcases/sfc/case02/Res.vue | 40 +++ .../testcases/sfc/case02/UsePropAccessor.vue | 34 ++ .../testcases/sfc/case02/blocks.schema.json | 150 ++++++++ .../test/testcases/sfc/case02/case02.test.js | 24 ++ .../testcases/sfc/case02/components-map.json | 24 ++ .../testcases/sfc/case02/page.schema.json | 46 +++ .../testcases/sfc/case03/StateAccessor.vue | 50 +++ .../testcases/sfc/case03/StateAccessorRes.vue | 48 +++ .../testcases/sfc/case03/UseStateAccessor.vue | 35 ++ .../sfc/case03/UseStateAccessorRes.vue | 40 +++ .../testcases/sfc/case03/blocks.schema.json | 158 +++++++++ .../test/testcases/sfc/case03/case03.test.js | 24 ++ .../testcases/sfc/case03/components-map.json | 31 ++ .../testcases/sfc/case03/page.schema.json | 48 +++ 36 files changed, 1513 insertions(+), 413 deletions(-) delete mode 100644 packages/vue-generator/src/generator/vue/index.js create mode 100644 packages/vue-generator/src/generator/vue/sfc/README.md create mode 100644 packages/vue-generator/test/testcases/sfc/case01/ImageTitle.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case01/ImageTitleRes.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/case01/res.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case02/Res.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/case02/case02.test.js create mode 100644 packages/vue-generator/test/testcases/sfc/case02/components-map.json create mode 100644 packages/vue-generator/test/testcases/sfc/case02/page.schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue create mode 100644 packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json create mode 100644 packages/vue-generator/test/testcases/sfc/case03/case03.test.js create mode 100644 packages/vue-generator/test/testcases/sfc/case03/components-map.json create mode 100644 packages/vue-generator/test/testcases/sfc/case03/page.schema.json diff --git a/packages/vue-generator/src/constant/index.js b/packages/vue-generator/src/constant/index.js index 45ce2238c3..c711f2d59f 100644 --- a/packages/vue-generator/src/constant/index.js +++ b/packages/vue-generator/src/constant/index.js @@ -2221,6 +2221,8 @@ export const INSERT_POSITION = { AFTER_IMPORT: 'AFTER_IMPORT', BEFORE_PROPS: 'BEFORE_PROPS', AFTER_PROPS: 'AFTER_PROPS', + BEFORE_EMIT: 'BEFORE_EMIT', + AFTER_EMIT: 'AFTER_EMIT', BEFORE_STATE: 'BEFORE_STATE', AFTER_STATE: 'AFTER_STATE', BEFORE_METHODS: 'BEFORE_METHODS', diff --git a/packages/vue-generator/src/generator/generateApp.js b/packages/vue-generator/src/generator/generateApp.js index b78d28f352..454457359a 100644 --- a/packages/vue-generator/src/generator/generateApp.js +++ b/packages/vue-generator/src/generator/generateApp.js @@ -13,7 +13,7 @@ import CodeGenerator from './codeGenerator' const inputMock = { // 应用相关配置信息 - //config: {}, + // config: {}, // 应用相关的 meta 信息 appMeta: {}, // 页面区块信息 diff --git a/packages/vue-generator/src/generator/index.js b/packages/vue-generator/src/generator/index.js index 68a33c499e..9f45e01577 100644 --- a/packages/vue-generator/src/generator/index.js +++ b/packages/vue-generator/src/generator/index.js @@ -10,6 +10,5 @@ * */ -import { generateCode, generateBlocksCode, generatePageCode } from './page' - -export { generateCode, generateBlocksCode, generatePageCode } +export { generateCode, generateBlocksCode, generatePageCode } from './page' +export { genSFCWithDefaultPlugin } from './vue/sfc' diff --git a/packages/vue-generator/src/generator/vue/index.js b/packages/vue-generator/src/generator/vue/index.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/vue-generator/src/generator/vue/sfc/README.md b/packages/vue-generator/src/generator/vue/sfc/README.md new file mode 100644 index 0000000000..69df6b7cc8 --- /dev/null +++ b/packages/vue-generator/src/generator/vue/sfc/README.md @@ -0,0 +1,2 @@ +# vue sfc code generator + diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js index 7133984d96..4f0d21012c 100644 --- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -74,7 +74,8 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { const { pkgMap, blockPkgMap } = getImportMap(schema, componentsMap, { blockRelativePath, blockSuffix }) // 解析 state - let state = schema.state || {} + // let state = schema.state || {} + let stateRes = {} // 解析 method const methods = schema.methods || {} @@ -121,17 +122,17 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { }, getMethods: () => methods, addState: (key, value) => { - if (state[key]) { + if (schema.state[key] || stateRes[key]) { return false } - state[key] = value + stateRes[key] = value return true }, - getState: () => state, - setState: (newState) => { - state = newState + getState: () => stateRes, + setState: () => { + // state = newState }, addImport: (fromPath, config) => { const dependenciesMap = pkgMap[fromPath] || blockPkgMap[fromPath] diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 04eec8699d..8292d41ef4 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -1,5 +1,4 @@ import { - BUILTIN_COMPONENT_NAME, JS_EXPRESSION, JS_FUNCTION, JS_I18N, @@ -9,48 +8,26 @@ import { INSERT_POSITION, TINY_ICON } from '@/constant' -import { isOn, toEventKey, thisBindRe, randomString, getFunctionInfo, hasAccessor } from '@/utils' -import { strategy } from '@/parser/state-type' -import { unwrapExpression } from '@/parser/state' +import { + isOn, + toEventKey, + thisPropsBindRe, + randomString, + getFunctionInfo, + hasAccessor, + thisRegexp, + isGetter, + isSetter +} from '@/utils' import { recursiveGenTemplateByHook } from './generateTemplate' -export const generateTemplateCondition = (condition) => { - if (typeof condition === 'boolean') { - return `v-if=${condition}` - } - - if (!condition?.type) { - return '' - } - - if (condition?.kind === 'else') { - return 'v-else' - } - - const conditionValue = condition?.value?.replace(thisBindRe, '') - - return `v-${condition?.kind || 'if'}=${conditionValue}` -} - -export const generateLoopTemplate = (loop, loopArgs) => { - // 没有设置循环,返回空字符 - if (!loop) { - return '' - } - - const source = (loop?.value || '').replace(thisBindRe, '') - const iterVar = [...loopArgs] - - return `v-for="(${iterVar.join(',')}) in ${source}"` -} - const handleEventBinding = (key, item, isJSX) => { const eventKey = toEventKey(key) let eventBinding = '' // vue 事件绑定,仅支持:内联事件处理器 or 方法事件处理器(绑定方法名或对某个方法的调用) if (item?.type === JS_EXPRESSION) { - let eventHandler = item.value.replace(thisBindRe, '') + let eventHandler = item.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') let renderKey = isJSX ? `${key}` : `@${eventKey}` // Vue Template 中,为事件处理函数传递额外的参数时,需要使用内联箭头函数 @@ -71,48 +48,6 @@ const handleEventBinding = (key, item, isJSX) => { return eventBinding } -export const handleAttributeKey = (key) => { - const specialKey = { - className: 'class' - } - - if (specialKey[key]) { - return specialKey - } - - return key -} - -export const genSlotBinding = (props = {}) => { - const { slot } = props - - if (!slot) { - return '' - } - - if (typeof slot === 'string') { - return `#${slot}` - } - - const { name, params } = slot - - let paramsValue = '' - - if (Array.isArray(params)) { - paramsValue = `={ ${params.join(',')} }` - } else if (typeof params === 'string') { - paramsValue = `="${params}"` - } - - return `#${name}${paramsValue}` -} - -export const handlePrimitiveBinding = (key, value) => { - const varBinding = ['boolean', 'number'].includes(typeof value) ? ':' : '' - - return `${varBinding}${key}=${value}` -} - const specialTypes = [JS_FUNCTION, JS_RESOURCE, JS_SLOT] export const checkHasSpecialType = (obj) => { @@ -133,77 +68,8 @@ export const checkHasSpecialType = (obj) => { return false } -export const mergeDescription = (oldDesc, newDesc) => { - oldDesc.hasJSX = oldDesc.hasJSX || newDesc.hasJSX - oldDesc.jsResource.utils = oldDesc.jsResource.utils || newDesc.jsResource.utils - oldDesc.jsResource.bridge = oldDesc.jsResource.bridge || newDesc.jsResource.bridge -} - -const transformSpecialType = (obj) => { - if (!obj || typeof obj !== 'object') { - return - } - - let res = {} - let description = { - hasJSX: false, - jsResource: { utils: false, bridge: false } - } - - if (Array.isArray(obj)) { - res = [] - } - - for (const [key, value] of Object.entries(obj)) { - if (typeof value !== 'object') { - res[key] = value - continue - } - - if (specialTypes?.includes(value?.type)) { - res[key] = strategy[value.type](value, description) - - continue - } - - const { res: tempRes, description: desc = {} } = transformSpecialType(value) || {} - - if (tempRes) { - res[key] = tempRes - } - - mergeDescription(description, desc) - } - - return { - res, - description - } -} - -export const handleObjectBinding = (key, value) => { - let shouldBindToState = false - - if (!value || typeof value !== 'object') { - return { - shouldBindToState, - resultStr: '' - } - } - - const hasSpecialType = checkHasSpecialType(value) - - const { res = '', description = {} } = transformSpecialType(value) || {} - - return { - hasSpecialType, - res, - description - } -} - const handleJSExpressionBinding = (key, value, isJSX) => { - const expressValue = value.value.replace(thisBindRe, '') + const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') if (isJSX) { return `${key}={${expressValue}}` @@ -234,106 +100,6 @@ const handleBindI18n = (key, value, isJSX) => { return `:${key}="t(${tArguments.join(',')})"` } -export const generateAttribute = (schema) => { - const { condition, loop, loopArgs, props, componentName } = schema - - const finalRes = { - description: { - hasJSX: false, - jsResource: { utils: false, bridge: false } - }, - stateVariable: {} - } - - let resultArr = [] - - // 处理 v-if 绑定 - const conditionStr = generateTemplateCondition(condition) - - resultArr.push(conditionStr) - - // 处理 v-for 绑定 - - const loopStr = generateLoopTemplate(loop, loopArgs) - - resultArr.push(loopStr) - - const slotBindingStr = genSlotBinding(props) - - resultArr.push(slotBindingStr) - - // 处理 ComponentName 为 template 的场景,不应该再有其他属性 - if (componentName === BUILTIN_COMPONENT_NAME.TEMPLATE) { - return resultArr.join(' ') - } - - Object.entries(props).forEach(([key, value]) => { - if (key === 'slot') { - return - } - - if (isOn(key)) { - const eventBindStr = handleEventBinding(key, value) - - resultArr.push(eventBindStr) - - return - } - - // 处理特殊的 key,比如 className -> class - const actualKey = handleAttributeKey(key) - - // 基本类型值绑定 - if (typeof value !== 'object') { - const primitiveStr = handlePrimitiveBinding(actualKey, value) - - resultArr.push(primitiveStr) - - return - } - - // 处理 expression 类型值绑定 - if (value?.type === JS_EXPRESSION) { - const expressionStr = handleJSExpressionBinding(actualKey, value) - - resultArr.push(expressionStr) - - return - } - - // 处理 i18n 绑定 - if (value?.type === JS_I18N) { - resultArr.push(handleBindI18n(actualKey, value)) - - return - } - - // 处理 object 绑定 - const { res, hasSpecialType, description } = handleObjectBinding(actualKey, value) - - // 有特殊类型说明不能直接拼接到 template attribute 中 - if (hasSpecialType) { - const stateValueKey = `${key}${randomString()}` - resultArr.push(`:${key}=state.${stateValueKey}`) - - finalRes.stateVariable[stateValueKey] = res - } else { - const unWrapValue = unwrapExpression(JSON.stringify(res)) - .replace(/props\./g, '') - .replace(/"/g, '"') - - resultArr.push(`:${key}=${unWrapValue}`) - } - - mergeDescription(finalRes.description, description) - }) - - return { - ...finalRes, - resultStr: resultArr.join(' ') - } -} - const handleJSXConditionBind = (schemaData, globalHooks, config) => { const { prefix, suffix, schema: { condition } = {} } = schemaData const isJSX = config.isJSX @@ -360,7 +126,7 @@ const handleJSXConditionBind = (schemaData, globalHooks, config) => { return } - const conditionValue = condition?.value?.replace(thisBindRe, '') + const conditionValue = condition?.value?.replace(thisRegexp, '') prefix.push(`${conditionValue} &&`) } @@ -384,7 +150,7 @@ export const handleConditionAttrHook = (schemaData, globalHooks, config) => { return } - const conditionValue = condition?.value?.replace(thisBindRe, '') + const conditionValue = condition?.value?.replace(isJSX ? thisRegexp : thisPropsBindRe, '') if (condition?.kind === 'else') { attributes.unshift('v-else') @@ -404,9 +170,9 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { let source = '' if (loop?.value && loop?.type) { - source = loop.value.replace(thisBindRe, '') + source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') } else { - source = JSON.stringify(loop) + source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'") } const iterVar = [...loopArgs] @@ -519,9 +285,10 @@ export const handleTinyIconPropsHook = (schemaData, globalHooks, config) => { if (value?.componentName === 'Icon' && value?.props?.name) { const name = value.props.name const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` + const exportName = name.replace(TINY_ICON, 'icon') const success = globalHooks.addImport('@opentiny/vue-icon', { - componentName: name, - exportName: name, + componentName: exportName, + exportName: exportName, package: '@opentiny/vue-icon', version: '^3.10.0', destructuring: true @@ -530,7 +297,7 @@ export const handleTinyIconPropsHook = (schemaData, globalHooks, config) => { if (success) { globalHooks.addStatement({ position: INSERT_POSITION.BEFORE_PROPS, - value: `const ${iconName} = ${name}()`, + value: `const ${iconName} = ${exportName}()`, key: iconName }) } @@ -579,7 +346,7 @@ const specialTypeHandler = { } return { - value: value.replace(/this\./g, '') + value: `${value.replace(/this\./g, '')}` } }, [JS_SLOT]: ({ value = [], params = ['row'] }, globalHooks, config) => { @@ -603,25 +370,31 @@ const specialTypeHandler = { export const transformObjType = (obj, globalHooks, config) => { if (!obj || typeof obj !== 'object') { - return + return obj } - let res = {} + let resStr = [] let shouldBindToState = false - - if (Array.isArray(obj)) { - res = [] - } + let shouldRenderKey = !Array.isArray(obj) for (const [key, value] of Object.entries(obj)) { + let renderKey = shouldRenderKey ? `${key}: ` : '' + + if (typeof value === 'string') { + resStr.push(`${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`) + + continue + } + if (typeof value !== 'object') { - res[key] = value + resStr.push(`${renderKey}${value}`) continue } if (specialTypeHandler[value?.type]) { - res[key] = specialTypeHandler[value.type](value, globalHooks, config)?.value || '' + const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || '' + resStr.push(`${renderKey}${specialVal}`) if (specialTypes.includes(value.type)) { shouldBindToState = true @@ -631,18 +404,29 @@ export const transformObjType = (obj, globalHooks, config) => { } if (hasAccessor(value?.accessor)) { - res[key] = value.defaultValue + resStr.push(`${renderKey}${value.defaultValue || "''"}`) - globalHooks.addStatement({ - position: INSERT_POSITION.AFTER_METHODS, - value: value.accessor.getter?.value || value.accessor.setter?.value - }) + if (isSetter(value?.accessor)) { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_METHODS, + value: `vue.watchEffect(wrap(${value.accessor.setter?.value ?? ''}))` + }) + } + + if (isGetter(value?.accessor)) { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_METHODS, + value: `vue.watchEffect(wrap(${value.accessor.getter?.value ?? ''}))` + }) + } + + continue } const { res: tempRes, shouldBindToState: tempShouldBindToState } = transformObjType(value, globalHooks, config) || {} - res[key] = tempRes + resStr.push(`${renderKey}${tempRes}`) if (tempShouldBindToState) { shouldBindToState = true @@ -651,7 +435,7 @@ export const transformObjType = (obj, globalHooks, config) => { return { shouldBindToState, - res + res: Array.isArray(obj) ? `[${resStr.join(',')}]` : `{ ${resStr.join(',')} }` } } @@ -673,16 +457,16 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { if (shouldBindToState && !isJSX) { let stateKey = key - let addSuccess = globalHooks.addState(key, res) + let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`) while (!addSuccess) { stateKey = `${key}${randomString()}` - addSuccess = globalHooks.addState(stateKey, res) + addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`) } attributes.push(`:${key}="state.${stateKey}"`) } else { - attributes.push(isJSX ? `${key}={${JSON.stringify(res)}}` : `:${key}="${JSON.stringify(res)}"`) + attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`) } delete props[key] @@ -699,7 +483,7 @@ export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) => const valueType = typeof value if (valueType === 'string') { - attributes.push(`${key}="${value}"`) + attributes.push(`${key}="${value.replaceAll(/"/g, "'")}"`) delete props[key] } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateScript.js b/packages/vue-generator/src/generator/vue/sfc/generateScript.js index 769a361e85..4d334df752 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateScript.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateScript.js @@ -1,5 +1,5 @@ import { capitalize } from '@vue/shared' -import { toEventKey, hasAccessor } from '@/utils' +import { toEventKey, isGetter, isSetter } from '@/utils' import { generateImportByPkgName } from '@/utils/generateImportStatement' import { INSERT_POSITION } from '@/constant' import { transformObjType } from './generateAttribute' @@ -46,7 +46,7 @@ export const defaultGenEmitsHook = (schema) => { } export const defaultGenStateHook = (schema, globalHooks) => { - const reactiveStatement = `const state = vue.reactive(${JSON.stringify(globalHooks.getState() || {}, null, 2)})` + const reactiveStatement = `const state = vue.reactive({${Object.values(globalHooks.getState()).join(',')}})` return reactiveStatement } @@ -56,9 +56,10 @@ export const defaultGenMethodHook = (schema, globalHooks) => { // TODO: 判断 methods 中是否有 jsx const methodsArr = Object.entries(methods).map(([key, item]) => `const ${key} = wrap(${item.value})`) - const methodsName = Object.keys(methods) + const methodsNames = Object.keys(methods) + const wrapMethods = methodsNames.length ? `wrap({ ${methodsNames.join(',')} })` : '' - return `${methodsArr.join('\n')}\nwrap({ ${methodsName.join(',')} })` + return `${methodsArr.join('\n')}\n\n${wrapMethods}` } export const defaultGenLifecycleHook = (schema) => { @@ -83,10 +84,17 @@ export const parsePropsHook = (schema, globalHooks) => { properties.forEach(({ content = [] }) => { content.forEach(({ accessor } = {}) => { - if (hasAccessor(accessor)) { + if (isGetter(accessor)) { globalHooks.addStatement({ position: INSERT_POSITION.AFTER_METHODS, - value: accessor.getter?.value || accessor.setter?.value + value: `vue.watchEffect(wrap(${accessor.getter?.value ?? ''}))` + }) + } + + if (isSetter(accessor)) { + globalHooks.addStatement({ + position: INSERT_POSITION.AFTER_METHODS, + value: `vue.watchEffect(wrap(${accessor.setter?.value ?? ''}))` }) } }) @@ -94,9 +102,9 @@ export const parsePropsHook = (schema, globalHooks) => { } export const parseReactiveStateHook = (schema, globalHooks, config) => { - const { res } = transformObjType(globalHooks.getState() || {}, globalHooks, config) + const { res } = transformObjType(schema.state, globalHooks, config) - globalHooks.setState(res || {}) + globalHooks.addState('$$innerState', `${res.slice(1, -1)}`) } export const handleProvideStatesContextHook = (schema, globalHooks) => { @@ -113,7 +121,7 @@ export const handleContextInjectHook = (schema, globalHooks) => { globalHooks.addStatement({ key: 'tiny-engine-inject-statement', - position: INSERT_POSITION.AFTER_PROPS, + position: INSERT_POSITION.AFTER_EMIT, value: `${injectLowcode}\n${injectLowcodeWrap}\n${wrapStoresStatement}` }) } @@ -163,13 +171,24 @@ export const genScriptByHook = (schema, globalHooks, config) => { parseHook(schema, globalHooks, config) } - const { AFTER_IMPORT, BEFORE_PROPS, AFTER_PROPS, BEFORE_STATE, AFTER_STATE, BEFORE_METHODS, AFTER_METHODS } = - INSERT_POSITION + const { + AFTER_IMPORT, + BEFORE_PROPS, + AFTER_PROPS, + BEFORE_STATE, + AFTER_STATE, + BEFORE_METHODS, + AFTER_METHODS, + BEFORE_EMIT, + AFTER_EMIT + } = INSERT_POSITION const statementGroupByPosition = { [AFTER_IMPORT]: [], [BEFORE_PROPS]: [], [AFTER_PROPS]: [], + [BEFORE_EMIT]: [], + [AFTER_EMIT]: [], [BEFORE_STATE]: [], [AFTER_STATE]: [], [BEFORE_METHODS]: [], @@ -212,17 +231,24 @@ export const genScriptByHook = (schema, globalHooks, config) => { return ` ${scriptTag} ${importStr} + ${statementGroupByPosition[AFTER_IMPORT].join('\n')} ${statementGroupByPosition[BEFORE_PROPS].join('\n')} ${propsStr} ${statementGroupByPosition[AFTER_PROPS].join('\n')} + +${statementGroupByPosition[BEFORE_EMIT].join('\n')} ${emitStr} +${statementGroupByPosition[AFTER_EMIT].join('\n')} + ${statementGroupByPosition[BEFORE_STATE].join('\n')} ${stateStr} ${statementGroupByPosition[AFTER_STATE].join('\n')} + ${statementGroupByPosition[BEFORE_METHODS].join('\n')} ${methodStr} ${statementGroupByPosition[AFTER_METHODS].join('\n')} + ${lifeCycleStr} ` } diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTag.js b/packages/vue-generator/src/generator/vue/sfc/generateTag.js index 10973c9bf2..001653af96 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTag.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTag.js @@ -17,7 +17,7 @@ const HTML_DEFAULT_VOID_ELEMENTS = [ ] export const generateTag = (tagName, config = {}) => { - const { isVoidElement, isStartTag = true, attribute, useHyphenate = true } = config + const { isVoidElement, isStartTag = true, attribute, isJSX = false, useHyphenate = !isJSX } = config if (typeof tagName !== 'string' || !tagName) { return '' diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js index d3f9a7859b..3418e6d262 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -7,59 +7,7 @@ import { JS_I18N } from '@/constant' import { generateTag } from './generateTag' -import { generateAttribute, mergeDescription } from './generateAttribute' -import { thisBindRe } from '@/utils' - -const recursiveGenTemplate = (children) => { - const effect = { - description: { - hasJSX: false, - jsResource: { utils: false, bridge: false } - }, - stateVariable: {} - } - - const schemaChildren = children || [] - - const resArr = schemaChildren.map((schemaItem) => { - const { componentName, children } = schemaItem - const { description, stateVariable, resultStr } = generateAttribute(schemaItem) - - mergeDescription(effect.description, description) - effect.stateVariable = { - ...effect.stateVariable, - ...(stateVariable || {}) - } - - const startTag = generateTag(componentName, { attribute: resultStr }) - const endTag = generateTag(componentName, { isStartTag: false }) - const { description: childDesc, stateVariable: childStateVar, resStr } = recursiveGenTemplate(children) - - mergeDescription(effect.description, childDesc) - - effect.stateVariable = { - ...effect.stateVariable, - ...(childStateVar || {}) - } - - return `${startTag}${resStr}${endTag}` - }) - - return { - ...effect, - resStr: resArr.join('') - } -} - -export const genTemplate = (schema) => { - const { description, stateVariable, resStr } = recursiveGenTemplate(schema.children) - - return { - description, - stateVariable, - resStr: `` - } -} +import { thisPropsBindRe, thisRegexp } from '@/utils' export const handleComponentNameHook = (nameObj) => { const { componentName, schema } = nameObj @@ -84,10 +32,11 @@ export const handleTinyIcon = (nameObj, globalHooks) => { const name = nameObj.schema.props.name const iconName = name.startsWith(TINY_ICON) ? name : `Tiny${name}` + const exportName = name.replace(TINY_ICON, 'icon') const success = globalHooks.addImport('@opentiny/vue-icon', { - componentName: name, - exportName: name, + componentName: exportName, + exportName: exportName, package: '@opentiny/vue-icon', version: '^3.10.0', destructuring: true @@ -97,7 +46,7 @@ export const handleTinyIcon = (nameObj, globalHooks) => { if (success) { globalHooks.addStatement({ position: INSERT_POSITION.BEFORE_PROPS, - value: `const ${iconName} = ${name}()`, + value: `const ${iconName} = ${exportName}()`, key: iconName }) } @@ -115,19 +64,24 @@ export const handleTinyGrid = (schemaData) => { } } -export const handleExpressionChildren = (schemaData = {}) => { +export const handleExpressionChildren = (schemaData = {}, globalHooks, config) => { const { children, schema } = schemaData const type = schema?.children?.type + const isJSX = config.isJSX + const prefix = isJSX ? '{' : '{{' + const suffix = isJSX ? '}' : '}}' if (type === JS_EXPRESSION) { - children.push(`{{ ${schema.children?.value.replace(thisBindRe, '') || ''} }}`) + children.push( + `${prefix} ${schema.children?.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') || ''} ${suffix}` + ) delete schema.children return } if (type === JS_I18N && schema.children?.key) { - children.push(`{{ t('${schema.children.key}') }}`) + children.push(`${prefix} t('${schema.children.key}') ${suffix}`) delete schema.children return @@ -146,7 +100,7 @@ export const validEmptyTemplateHook = (schema = {}) => { export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = {}) => { const schemaChildren = schemaWithRes?.schema?.children || [] - const { hooks = {} } = config + const { hooks = {}, isJSX } = config // 自定义 hooks const { componentName: componentNameHooks, @@ -172,12 +126,12 @@ export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = return schemaItem || '' } - const { componentName } = schemaItem + const { componentName, component } = schemaItem const optionData = { schema: schemaItem, voidElement: false, - componentName, + componentName: componentName ?? component ?? '', prefix: [], attributes: [], children: [], @@ -198,13 +152,14 @@ export const recursiveGenTemplateByHook = (schemaWithRes, globalHooks, config = const startTag = generateTag(optionData.componentName, { attribute: optionData.attributes.join(' '), - isVoidElement: optionData.voidElement + isVoidElement: optionData.voidElement, + isJSX }) let endTag = '' if (!optionData.voidElement) { - endTag = generateTag(optionData.componentName, { isStartTag: false }) + endTag = generateTag(optionData.componentName, { isStartTag: false, isJSX }) } return ` diff --git a/packages/vue-generator/src/index.js b/packages/vue-generator/src/index.js index 4da0c2d492..3153feaaec 100644 --- a/packages/vue-generator/src/index.js +++ b/packages/vue-generator/src/index.js @@ -1,15 +1,13 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ -import { generateCode, generateBlocksCode, generatePageCode } from './generator' - -export { generateCode, generateBlocksCode, generatePageCode } +export { generateCode, generateBlocksCode, generatePageCode } from './generator' diff --git a/packages/vue-generator/src/plugins/genBlockPlugin.js b/packages/vue-generator/src/plugins/genBlockPlugin.js index bcdae4e88f..c9a8072bf7 100644 --- a/packages/vue-generator/src/plugins/genBlockPlugin.js +++ b/packages/vue-generator/src/plugins/genBlockPlugin.js @@ -1,5 +1,5 @@ import { mergeOptions } from '../utils/mergeOptions' -import { generatePageCode } from '../generator/page' +import { genSFCWithDefaultPlugin } from '../generator' const defaultOption = { blockBasePath: './src/components' @@ -25,25 +25,13 @@ function genBlockPlugin(options = {}) { const resBlocks = [] for (const block of blocks) { - const res = generatePageCode({ - pageInfo: { schema: block, name: block.componentName }, - componentsMap: this.schema.componentsMap - }) - - const { errors, ...restInfo } = res[0] - - if (errors?.length > 0) { - this.genLogs.push(...errors) - continue - } - - const { panelName, panelValue, panelType } = restInfo + const res = genSFCWithDefaultPlugin(block, this.schema.componentsMap, { blockRelativePath: './' }) resBlocks.push({ - fileType: panelType, - fileName: panelName, + fileType: 'vue', + fileName: `${block.componentName}.vue`, path: blockBasePath, - fileContent: panelValue + fileContent: res }) } diff --git a/packages/vue-generator/src/plugins/genPagePlugin.js b/packages/vue-generator/src/plugins/genPagePlugin.js index ed4350256f..96c568cb6f 100644 --- a/packages/vue-generator/src/plugins/genPagePlugin.js +++ b/packages/vue-generator/src/plugins/genPagePlugin.js @@ -1,5 +1,5 @@ import { mergeOptions } from '../utils/mergeOptions' -import { generatePageCode } from '../generator/page' +import { genSFCWithDefaultPlugin } from '../generator' const defaultOption = { pageBasePath: './src/views' @@ -55,25 +55,13 @@ function genPagePlugin(options = {}) { const resPage = [] for (const page of pages) { - const res = generatePageCode({ - pageInfo: { schema: page, name: page.componentName }, - componentsMap: this.schema.componentsMap - }) - - const { errors, ...restInfo } = res[0] - - if (errors?.length > 0) { - this.genLogs.push(...errors) - continue - } - - const { panelName, panelValue, panelType } = restInfo + const res = genSFCWithDefaultPlugin(page, this.schema.componentsMap) resPage.push({ - fileType: panelType, - fileName: panelName, + fileType: 'vue', + fileName: `${page.componentName}.vue`, path: page.path, - fileContent: panelValue + fileContent: res }) } diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js index 9c5b7caa10..70532aeafb 100644 --- a/packages/vue-generator/src/utils/generateImportStatement.js +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -40,13 +40,13 @@ export function generateImportByPkgName(config) { } else { defaultImportStatement = `${exportName || componentName || ''}` } - } - if (!importStatements.length) { - return `import ${defaultImportStatement} from "${pkgName}"` + defaultImportStatement = `import ${defaultImportStatement} from "${pkgName}"\n` } - const comma = defaultImportStatement ? ',' : '' + if (!importStatements.length && defaultImportStatement) { + return defaultImportStatement + } - return `import ${defaultImportStatement}${comma} { ${importStatements.join(',')} } from "${pkgName}"` + return `${defaultImportStatement}import { ${importStatements.join(',')} } from "${pkgName}"` } diff --git a/packages/vue-generator/src/utils/index.js b/packages/vue-generator/src/utils/index.js index f3127e00f1..0266d85f19 100644 --- a/packages/vue-generator/src/utils/index.js +++ b/packages/vue-generator/src/utils/index.js @@ -79,6 +79,8 @@ const prettierOpts = { const onRE = /^on([A-Z]\w*)/ const onUpdateRE = /^on(Update:\w+)/ export const thisBindRe = /this\.(props\.)?/g +export const thisPropsBindRe = /this\.(props\.)?/g +export const thisRegexp = /this\./g const isOn = (key) => onRE.test(key) const isOnUpdate = (key) => onUpdateRE.test(key) diff --git a/packages/vue-generator/test/testcases/sfc/case01/ImageTitle.vue b/packages/vue-generator/test/testcases/sfc/case01/ImageTitle.vue new file mode 100644 index 0000000000..62550d8fcb --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/ImageTitle.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case01/ImageTitleRes.vue b/packages/vue-generator/test/testcases/sfc/case01/ImageTitleRes.vue new file mode 100644 index 0000000000..72eb5217fb --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/ImageTitleRes.vue @@ -0,0 +1,59 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json new file mode 100644 index 0000000000..f6824ce7ab --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/blocks.schema.json @@ -0,0 +1,207 @@ +{ + "componentName": "Block", + "fileName": "ImageTitle", + "css": ".image-title {\n margin-right: 15px;\ndisplay: flex;\n align-items: center; \n}\n.crm-title {\n margin-left: 8px;\n font-family: PingFangSC-Regular; \nfont-size: 22px; \ncolor: #333333; \nline-height: 30px; \n}\n.split {\r\n align-self: center;\r\n width: 1px;\r\n height: 20px;\r\n background-color: #dddee4;\r\n margin-left: 20px;\r\n}\r\n", + "props": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "div", + "id": "ImageTitleizk3", + "props": { + "className": "image-title", + "onClick": { + "type": "JSExpression", + "value": "this.handleClick" + } + }, + "children": [ + { + "componentName": "img", + "id": "imageizk3", + "props": { + "src": { + "type": "JSExpression", + "value": "this.props.src" + } + } + }, + { + "componentName": "span", + "id": "spanizk3", + "props": { + "className": "crm-title" + }, + "children": { + "type": "JSExpression", + "value": "this.props.text" + } + }, + { + "componentName": "span", + "id": "spanizk4", + "condition": { + "type": "JSExpression", + "value": "this.props.hasSplitLine" + }, + "props": { + "className": "split" + } + } + ] + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "handleClick", + "type": "Function", + "defaultValue": { + "type": "Function", + "value": "function handleClick(event) { return event }" + }, + "label": { + "text": { + "zh_CN": "点击Image触发事件" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaCodeEditor", + "props": {} + } + }, + { + "property": "options", + "type": "Array", + "defaultValue": [], + "label": { + "text": { + "zh_CN": "选项" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaCodeEditor", + "props": { + "modelValue": [] + } + } + }, + { + "property": "src", + "type": "string", + "defaultValue": "https://res-static.hc-cdn.cn/cloudbu-site/china/zh-cn/TinyLowCode/crm/img/bussiness/businessmanage.svg", + "label": { + "text": { + "zh_CN": "图片地址" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": { + "modelValue": "https://res-static.hc-cdn.cn/cloudbu-site/china/zh-cn/TinyLowCode/crm/img/bussiness/businessmanage.svg" + } + } + }, + { + "property": "text", + "type": "String", + "defaultValue": "商务管理", + "label": { + "text": { + "zh_CN": "标题文本" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": { + "modelValue": "商务管理" + } + } + }, + { + "property": "hasSplitLine", + "type": "Boolean", + "defaultValue": true, + "label": { + "text": { + "zh_CN": "是否添加分割线" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaSwitch", + "props": { + "modelValue": true + } + } + } + ] + } + ], + "events": { + "onClickLogo": { + "label": { + "zh_CN": "点击事件" + }, + "description": { + "zh_CN": "通常用于配置处理点击跳转" + } + } + } + }, + "state": { + "activeMethod": { + "type": "JSFunction", + "value": "function() {\n return this.props.isEdit;\r\n}" + } + }, + "methods": { + "handleClick": { + "type": "JSFunction", + "value": "function() { this.emit('click-logo') }" + } + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js b/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js index d7905ec863..778a8dd50d 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js +++ b/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js @@ -1,11 +1,24 @@ +import fs from 'fs' +import path from 'path' import { expect, test } from 'vitest' import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' import schema from './schema.json' +import blockSchema from './blocks.schema.json' import componentsMap from './componentsMap.json' import expectedRes from './expected.vue?raw' console.log('case01', typeof expectedRes) test('should validate tagName', () => { - expect(genSFCWithDefaultPlugin(schema, componentsMap)).toBe('') + const res = genSFCWithDefaultPlugin(schema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'res.vue'), res) + expect(res).toBe('') +}) + +test('should generate block component correct', () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'ImageTitleRes.vue'), res) + expect(res).toBe('') }) diff --git a/packages/vue-generator/test/testcases/sfc/case01/res.vue b/packages/vue-generator/test/testcases/sfc/case01/res.vue new file mode 100644 index 0000000000..72915caa2c --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/res.vue @@ -0,0 +1,147 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue new file mode 100644 index 0000000000..aa41a95753 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue new file mode 100644 index 0000000000..f4cdab154f --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case02/Res.vue b/packages/vue-generator/test/testcases/sfc/case02/Res.vue new file mode 100644 index 0000000000..4fcabf8c55 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/Res.vue @@ -0,0 +1,40 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue new file mode 100644 index 0000000000..8a2b8b8356 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json new file mode 100644 index 0000000000..517e48a73b --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/blocks.schema.json @@ -0,0 +1,150 @@ +{ + "componentName": "Block", + "fileName": "PropAccessor", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "姓" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入姓氏", + "modelValue": { + "type": "JSExpression", + "value": "this.state.lastName", + "model": true + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入名字", + "modelValue": { + "type": "JSExpression", + "value": "this.state.firstName", + "model": true + } + }, + "id": "f029ce23" + } + ], + "id": "3751a68b" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "提交", + "type": "primary" + }, + "id": "d2b35138" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "style": "margin-left: 10px" + }, + "id": "89f92a92" + } + ], + "id": "2e93998e" + } + ], + "id": "88ecfcff" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "name", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "全名" + } + }, + "cols": 12, + "rules": [], + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.emit('update:name', `${this.state.firstName} ${this.state.lastName}`)\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [firstName, lastName] = this.props.name.split(' ')\r\n this.state.firstName = firstName\r\n this.state.lastName = lastName\r\n}" + } + }, + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + } + ] + } + ], + "events": { + "onUpdate:name": { + "label": { + "zh_CN": "双向绑定的name变化时触发" + }, + "description": { + "zh_CN": "" + } + } + }, + "slots": {} + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case02/case02.test.js b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js new file mode 100644 index 0000000000..2fd7366e5e --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js @@ -0,0 +1,24 @@ +import fs from 'fs' +import path from 'path' +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import componentsMap from './components-map.json' +import expectedRes from './UsePropAccessor.vue?raw' +import blockSchema from './blocks.schema.json' + +console.log('case02', typeof expectedRes) + +test('should validate tagName', () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'Res.vue'), res) + expect(res).toBe('') +}) + +test('should generate prop accessor correctly', () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'PropAccessorRes.vue'), res) + expect(res).toBe('') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case02/components-map.json b/packages/vue-generator/test/testcases/sfc/case02/components-map.json new file mode 100644 index 0000000000..21f1077221 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/components-map.json @@ -0,0 +1,24 @@ +[ + { + "componentName": "UsePropAccessor", + "main": "./views" + }, + { + "componentName": "PropAccessor", + "main": "./components" + }, + { + "componentName": "TinyForm", + "exportName": "Form", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyFormItem", + "exportName": "FormItem", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case02/page.schema.json b/packages/vue-generator/test/testcases/sfc/case02/page.schema.json new file mode 100644 index 0000000000..4f60b89ae6 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case02/page.schema.json @@ -0,0 +1,46 @@ +{ + "componentName": "Page", + "fileName": "UsePropAccessor", + "css": "", + "props": {}, + "state": { + "fullName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentType": "Block", + "componentName": "PropAccessor", + "props": { + "name": { + "type": "JSExpression", + "value": "this.state.fullName", + "model": { + "prop": "name" + } + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + } + ], + "id": "88ecfcff" + } + ] +} diff --git a/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue new file mode 100644 index 0000000000..baaeb808c8 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue new file mode 100644 index 0000000000..9090a62c5c --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue @@ -0,0 +1,48 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue new file mode 100644 index 0000000000..e2c6f567b1 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue new file mode 100644 index 0000000000..ac76db4eaf --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue @@ -0,0 +1,40 @@ + + + + \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json b/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json new file mode 100644 index 0000000000..8b0c98f28c --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/blocks.schema.json @@ -0,0 +1,158 @@ +{ + "componentName": "Block", + "fileName": "StateAccessor", + "css": "", + "props": {}, + "state": { + "fullName": { + "defaultValue": "", + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.state.fullName = `${this.props.firstName} ${this.props.lastName}`\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [firstName, lastName] = this.state.fullName.split(' ')\r\n this.emit('update:firstName', firstName)\r\n this.emit('update:lastName', lastName)\r\n}" + } + } + } + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入全名", + "modelValue": { + "type": "JSExpression", + "value": "this.state.fullName", + "model": true + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + }, + { + "componentName": "TinyFormItem", + "props": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "提交", + "type": "primary" + }, + "id": "d2b35138" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "style": "margin-left: 10px" + }, + "id": "89f92a92" + } + ], + "id": "2e93998e" + } + ], + "id": "88ecfcff" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "firstName", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "姓氏" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + }, + { + "property": "lastName", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "名字" + } + }, + "cols": 12, + "rules": [], + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": {} + } + } + ] + } + ], + "events": { + "onUpdate:firstName": { + "label": { + "zh_CN": "firstName变化时触发" + }, + "description": { + "zh_CN": "" + } + }, + "onUpdate:lastName": { + "label": { + "zh_CN": "lastName变化时触发" + }, + "description": { + "zh_CN": "" + } + } + }, + "slots": {} + } +} diff --git a/packages/vue-generator/test/testcases/sfc/case03/case03.test.js b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js new file mode 100644 index 0000000000..d0bfe16bf3 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js @@ -0,0 +1,24 @@ +import fs from 'fs' +import path from 'path' +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './page.schema.json' +import blockSchema from './blocks.schema.json' +import componentsMap from './components-map.json' +// import expectedRes from './expected.vue?raw' + +// console.log('case01', typeof expectedRes) + +test('should generate useStateAccessorCorrect', () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'UseStateAccessorRes.vue'), res) + expect(res).toBe('') +}) + +test('should generate block state accessor correct', () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + + fs.writeFileSync(path.resolve(__dirname, 'StateAccessorRes.vue'), res) + expect(res).toBe('') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case03/components-map.json b/packages/vue-generator/test/testcases/sfc/case03/components-map.json new file mode 100644 index 0000000000..bb63ef2ace --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/components-map.json @@ -0,0 +1,31 @@ +[ + { + "componentName": "UseStateAccessor", + "main": "./views" + }, + { + "componentName": "StateAccessor", + "main": "./components" + }, + { + "componentName": "TinyForm", + "exportName": "Form", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyFormItem", + "exportName": "FormItem", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/case03/page.schema.json b/packages/vue-generator/test/testcases/sfc/case03/page.schema.json new file mode 100644 index 0000000000..6b1ff8dfcb --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case03/page.schema.json @@ -0,0 +1,48 @@ +{ + "componentName": "Page", + "fileName": "UseStateAccessor", + "css": "", + "props": {}, + "state": { + "firstName": "", + "lastName": "" + }, + "methods": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "全名" + }, + "children": [ + { + "componentType": "Block", + "componentName": "StateAccessor", + "props": { + "firstName": { + "type": "JSExpression", + "value": "this.state.firstName" + }, + "lastName": { + "type": "JSExpression", + "value": "this.state.lastName" + } + }, + "id": "acdd0030" + } + ], + "id": "6140da6b" + } + ], + "id": "88ecfcff" + } + ] +} From e1054633f71277af538bf67546ffdda5e7ee73e8 Mon Sep 17 00:00:00 2001 From: chilingling Date: Mon, 18 Mar 2024 06:10:18 -0700 Subject: [PATCH 11/37] feat(vue-generator): handle app generate code --- packages/utils/package.json | 2 +- packages/vue-generator/.eslintrc.cjs | 1 + packages/vue-generator/.gitignore | 2 +- packages/vue-generator/README.md | 60 + packages/vue-generator/package.json | 2 +- .../src/generator/codeGenerator.js | 124 +- .../src/generator/generateApp.js | 234 +- packages/vue-generator/src/generator/index.js | 2 + .../src/generator/vue/sfc/genSetupSFC.js | 2 - .../generator/vue/sfc/generateAttribute.js | 2 +- packages/vue-generator/src/index.js | 2 +- .../src/plugins/formatCodePlugin.js | 56 + .../src/plugins/genBlockPlugin.js | 26 +- .../src/plugins/genDataSourcePlugin.js | 14 +- .../src/plugins/genDependenciesPlugin.js | 59 +- .../src/plugins/genGlobalState.js | 17 +- .../src/plugins/genI18nPlugin.js | 27 +- .../src/plugins/genPagePlugin.js | 55 +- .../src/plugins/genRouterPlugin.js | 74 +- .../src/plugins/genTemplatePlugin.js | 20 +- .../src/plugins/genUtilsPlugin.js | 24 +- packages/vue-generator/src/plugins/index.js | 2 + .../src/plugins/parseSchemaPlugin.js | 50 + packages/vue-generator/src/templates/index.js | 2 +- .../src/templates/vue-template/index.js | 48 +- .../vue-template/templateFiles/README.md | 2 +- .../vue-template/templateFiles/index.html | 2 +- .../vue-template/templateFiles/packageJson.js | 8 +- .../src/utils/generateImportStatement.js | 2 +- .../testcases/generator/generateApp.test.js | 22 + .../test/testcases/generator/mockData.js | 2103 +++++++++++++++++ packages/vue-generator/vite.config.js | 4 + 32 files changed, 2708 insertions(+), 342 deletions(-) create mode 100644 packages/vue-generator/src/plugins/formatCodePlugin.js create mode 100644 packages/vue-generator/src/plugins/parseSchemaPlugin.js create mode 100644 packages/vue-generator/test/testcases/generator/generateApp.test.js create mode 100644 packages/vue-generator/test/testcases/generator/mockData.js diff --git a/packages/utils/package.json b/packages/utils/package.json index de16c9b6e7..1e19faed8d 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -28,7 +28,7 @@ "homepage": "https://opentiny.design/tiny-engine", "devDependencies": { "vite": "^4.3.7", - "vitest": "^0.34.6" + "vitest": "^1.4.0" }, "dependencies": {}, "peerDependencies": { diff --git a/packages/vue-generator/.eslintrc.cjs b/packages/vue-generator/.eslintrc.cjs index fc0c9513b6..2837ea9cc0 100644 --- a/packages/vue-generator/.eslintrc.cjs +++ b/packages/vue-generator/.eslintrc.cjs @@ -11,6 +11,7 @@ module.exports = { node: true }, parserOptions: { + ecmaVersion: 'latest', ecmaFeatures: { jsx: true } diff --git a/packages/vue-generator/.gitignore b/packages/vue-generator/.gitignore index d180ec5e28..955710439d 100644 --- a/packages/vue-generator/.gitignore +++ b/packages/vue-generator/.gitignore @@ -1 +1 @@ -test/testcases/full/**/result/* +test/**/result/* diff --git a/packages/vue-generator/README.md b/packages/vue-generator/README.md index 0391c6c091..fc4023c5f2 100644 --- a/packages/vue-generator/README.md +++ b/packages/vue-generator/README.md @@ -9,3 +9,63 @@ TODO: - [ ] 抽取通用底层能力,支持用户自定义插件,自定义出码结果 - [ ] 官方提供更多内置出码方案 +## 安装 + +```bash +npm install @opentiny/tiny-engine-dsl-vue +``` + +## 使用 + +### 使用官方默认配置出码 + +```javascript +import { generateApp } from '@opentiny/tiny-engine-dsl-vue' + +const instance = generateApp() + +const res = await instance.generate(appSchema) +``` + +### 传入配置 + +```javascript +import { generateApp } from '@opentiny/tiny-engine-dsl-vue' + +const instance = generateApp({ + pluginConfig: { + formatCode: { + singleQuote: false, + printWidth: 180, + semi: true + } + } +}) + +const res = await instance.generate(appSchema) +``` + +### 使用自定义插件替换官方插件 + +```javascript +import { generateApp } from '@opentiny/tiny-engine-dsl-vue' + +const customDataSourcePlugin = () => { + return { + name: '', + description: '', + run: () { + // ... 自定义出码逻辑 + } + } +} + +const instance = generateApp({ + customPlugins: { + dataSource: customDataSourcePlugin() + } +}) + +const res = await instance.generate(appSchema) +``` + diff --git a/packages/vue-generator/package.json b/packages/vue-generator/package.json index 58a65c2da6..47157fec0f 100644 --- a/packages/vue-generator/package.json +++ b/packages/vue-generator/package.json @@ -42,7 +42,7 @@ "fs-extra": "^10.0.1", "prettier": "^2.6.1", "vite": "^4.3.7", - "vitest": "^0.34.6", + "vitest": "^1.4.0", "winston": "^3.10.0" } } diff --git a/packages/vue-generator/src/generator/codeGenerator.js b/packages/vue-generator/src/generator/codeGenerator.js index 8349952104..16f42c444b 100644 --- a/packages/vue-generator/src/generator/codeGenerator.js +++ b/packages/vue-generator/src/generator/codeGenerator.js @@ -7,6 +7,13 @@ class CodeGenerator { context = {} // 是否允许插件报错 tolerateError = true + error = [] + contextApi = { + addLog: this.addLog.bind(this), + addFile: this.addFile.bind(this), + getFile: this.getFile.bind(this), + replaceFile: this.replaceFile.bind(this) + } constructor(config) { this.config = config this.plugins = config.plugins @@ -24,58 +31,21 @@ class CodeGenerator { config: this.config, genResult: this.genResult, genLogs: this.genLogs, + error: this.error, ...this.context } } - /** - * 写入 log - * @param {*} log - */ - addGenLogs(log) { - this.genLogs.push(log) - } - /** - * 覆写 config - * @param {*} newConfig - */ - overrideConfig(newConfig) { - this.config = newConfig - } - /** - * 覆写 schema - * @param {*} newSchema - */ - overrideSchema(newSchema) { - this.schema = newSchema - } - getPluginsByHook(hookName) { - const res = [] - - for (const pluginItem of this.plugins) { - if (typeof pluginItem[hookName] === 'function') { - res.push(pluginItem[hookName]) - } - } - - return res - } async generate(schema) { - const hooks = ['transformStart', 'transform', 'transformEnd'] - this.schema = this.parseSchema(schema) + this.error = [] - let err = [] let curHookName = '' try { - for (const hookItem of hooks) { - curHookName = hookItem - const plugins = this.getPluginsByHook(hookItem) - - await this[hookItem](plugins) - } + await this.transformStart() + await this.transform() } catch (error) { - err.push(error) + this.error.push(error) if (!this.tolerateError) { throw new Error( @@ -85,8 +55,7 @@ class CodeGenerator { ) } } finally { - const plugins = this.getPluginsByHook('transformEnd') - await this.transformEnd(plugins, err) + await this.transformEnd() } return { @@ -98,17 +67,25 @@ class CodeGenerator { * 转换开始的钩子,在正式开始转换前,用户可以做一些预处理的动作 * @param {*} plugins */ - async transformStart(plugins) { - for (const pluginItem of plugins) { - await pluginItem(this.config, this.getContext()) + async transformStart() { + for (const pluginItem of this.plugins.transformStart) { + if (typeof pluginItem.run !== 'function') { + continue + } + + await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()]) } } - async transform(plugins) { - for (const pluginItem of plugins) { - const transformRes = await pluginItem(this.schema, this.getContext()) + async transform() { + for (const pluginItem of this.plugins.transform) { + if (typeof pluginItem.run !== 'function') { + continue + } + + const transformRes = await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()]) if (!transformRes) { - return + continue } if (Array.isArray(transformRes)) { @@ -118,9 +95,13 @@ class CodeGenerator { } } } - async transformEnd(plugins, err) { - for (const pluginItem of plugins) { - await pluginItem(err) + async transformEnd() { + for (const pluginItem of this.plugins.transformEnd) { + if (typeof pluginItem.run !== 'function') { + continue + } + + await pluginItem.run.apply(this.contextApi, [this.schema, this.getContext()]) } } parseSchema(schema) { @@ -138,16 +119,47 @@ class CodeGenerator { ) } } - replaceGenResult(resultItem) { + /** + * 写入 log + * @param {*} log + */ + addLog(log) { + this.genLogs.push(log) + } + getFile(path, fileName) { + return this.genResult.find((item) => item.path === path && item.fileName === fileName) + } + addFile(file, override) { + const { path, fileName } = file + + const isExist = this.getFile(path, fileName) + + if (isExist && !override) { + return false + } + + if (isExist) { + this.replaceFile(file) + + return true + } + + this.genResult.push(file) + + return true + } + replaceFile(resultItem) { const { path, fileName } = resultItem const index = this.genResult.findIndex((item) => item.path === path && item.fileName === fileName) if (index === -1) { - return + return false } this.genResult.splice(index, 1, resultItem) + + return true } } diff --git a/packages/vue-generator/src/generator/generateApp.js b/packages/vue-generator/src/generator/generateApp.js index 454457359a..fd5e845a4b 100644 --- a/packages/vue-generator/src/generator/generateApp.js +++ b/packages/vue-generator/src/generator/generateApp.js @@ -1,4 +1,3 @@ -import { generateTemplate as genDefaultStaticTemplate } from '../templates/vue-template' import { genBlockPlugin, genDataSourcePlugin, @@ -7,111 +6,170 @@ import { genPagePlugin, genRouterPlugin, genTemplatePlugin, - genUtilsPlugin + genUtilsPlugin, + formatCodePlugin, + parseSchemaPlugin } from '../plugins' import CodeGenerator from './codeGenerator' -const inputMock = { - // 应用相关配置信息 - // config: {}, - // 应用相关的 meta 信息 - appMeta: {}, - // 页面区块信息 - componentsTree: [], - blockList: [], - // 数据源信息 - dataSource: [], - // i18n 信息 - i18n: {}, - // utils 信息 - utils: [], - // 全局状态 - globalState: [] -} +/** + * @typedef {Object} FuncType + * @property {String} type + * @property {String} value + */ -// TODO 解析整个应用用到的区块 -// 1. 解析页面中用到的区块 -// 2. 解析区块中用到的区块 +/** + * @typedef {Object} DataSource + * @property {Array.<{ id: Number, name: String, data: Object }>} list + * @property {FuncType} [dataHandler] + * @property {FuncType} [errorHandler] + * @property {FuncType} [willFetch] + */ -const transformedSchema = { - // 整体应用 meta 信息 - appMeta: { - name: 'test' - }, - // 需要生成的页面 - pageCode: [ - { - // 类型是页面 - // type: 'PAGE', - // 类型是区块 - // type: 'BLOCK', - // 页面 meta 信息 - meta: {}, - // schema 信息,如果是 文件夹,则不需要 - schema: {} - // ... - } - ], - dataSource: {}, - i18n: {}, - routes: {}, - utils: {}, - globalState: [ - { - actions: {}, - getters: {}, - id: '', - state: {} - } - ] -} +/** + * @typedef {Object} GlobalStateItem + * @property {String} id + * @property {Object} state + * @property {Object.>} actions + * @property {Object.>} getters + */ -// 预处理输入的 schema,转换为标准的格式 -function transformSchema(appSchema) { - const { appMeta, pageCode, dataSource, i18n, utils, globalState } = appSchema +/** + * @typedef {Object} SchemaChildrenItem + * @property {SchemaChildrenItem} children + * @property {String} componentName + * @property {String} id + * @property {Object.} props + */ - const routes = pageCode.map(({ meta: { isHome, router }, fileName }) => ({ - fileName, - isHome, - path: router.startsWith('/') ? router : `/${router}` - })) +/** + * @typedef {Object} PageOrBlockSchema + * @property {String} componentName + * @property {String} css + * @property {String} fileName + * @property {Object.>} lifeCycles + * @property {Object.} methods + * @property {Object} props + * @property {Array>} state + * @property {{ id: Number, isHome: Boolean, parentId: String, rootElement: String, router: String }} meta + * @property {Array.} children + * @property {{ properties: Array>, events: Object. }} [schema] + */ - const hasRoot = routes.some(({ path }) => path === '/') +/** + * @typedef {Object} FolderItem + * @property {String} componentName + * @property {Number} depth + * @property {String} folderName + * @property {Number} id + * @property {String} parentId + * @property {String} router + */ - if (!hasRoot && routes.length) { - const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path } +/** + * @typedef {Object} ComponentMapItem + * @property {String} componentName + * @property {Boolean} destructuring + * @property {String} [exportName] + * @property {String} [package] + * @property {String} [main] + * @property {String} version + */ - routes.unshift({ path: '/', redirect: homePath }) +/** + * @typedef {Object} MetaInfo 应用APP 元信息 + * @property {String} name + * @property {String} description + */ + +/** + * @typedef {Object} AppSchema + * @property {{en_US: Object., zh_CN: Object.}} i18n 国际化数据 + * @property {Array} bridge 桥接源 + * @property {Array.<{ name: String, type: 'npm' | 'function', content: Object }>} utils 工具类 + * @property {DataSource} dataSource 数据源 + * @property {Array} globalState 全局状态 + * @property {Array.} pageSchema 页面 schema + * @property {Array.} blockSchema 区块 schema + * @property {Array.} componentsMap 物料 package 信息 + * @property {MetaInfo} meta 应用元信息 + */ + +/** + * @typedef PluginConfig + * @property {Object.} [template] + * @property {Object.} [block] + * @property {Object.} [page] + * @property {Object.} [dataSource] + * @property {Object.} [dependencies] + * @property {Object.} [i18n] + * @property {Object.} [router] + * @property {Object.} [utils] + */ + +/** + * @typedef {Object} GenerateAppOptions + * @property {Object.} customPlugins + * @property {Object.} customContext + * @property {PluginConfig} pluginConfig + * @property {Boolean} tolerateError + */ + +/** + * 整体应用出码 + * @param {GenerateAppOptions} config + * @param {Object.} context + * @returns {Promise} + */ +export function generateApp(config = {}) { + const defaultPlugins = { + template: new genTemplatePlugin(config.pluginConfig?.template || {}), + block: new genBlockPlugin(config.pluginConfig?.block || {}), + page: new genPagePlugin(config.pluginConfig?.page || {}), + dataSource: new genDataSourcePlugin(config.pluginConfig?.dataSource || {}), + dependencies: new genDependenciesPlugin(config.pluginConfig?.dependencies || {}), + i18n: new genI18nPlugin(config.pluginConfig?.i18n || {}), + router: new genRouterPlugin(config.pluginConfig?.router || {}), + utils: new genUtilsPlugin(config.pluginConfig?.utils || {}), + formatCode: new formatCodePlugin(config.pluginConfig?.formatCode || {}), + parseSchema: new parseSchemaPlugin(config.pluginConfig?.parseSchema || {}) } - return { - appMeta, - pageCode, + const { customPlugins = {} } = config + const { + template, + block, + page, dataSource, + dependencies, i18n, + router, utils, - globalState, - routes + formatCode, + parseSchema, + transformStart = [], + transform = [], + transformEnd = [] + } = customPlugins + const mergeWithDefaultPlugin = { + template: template || defaultPlugins.template, + block: block || defaultPlugins.block, + page: page || defaultPlugins.page, + dataSource: dataSource || defaultPlugins.dataSource, + dependencies: dependencies || defaultPlugins.dependencies, + i18n: i18n || defaultPlugins.i18n, + router: router || defaultPlugins.router, + utils: utils || defaultPlugins.utils } -} -/** - * 整体应用出码 - */ -export async function generateApp(appSchema, context = {}) { const codeGenInstance = new CodeGenerator({ - plugins: [ - genBlockPlugin(), - genDataSourcePlugin(), - genDependenciesPlugin(), - genI18nPlugin(), - genPagePlugin(), - genRouterPlugin(), - genTemplatePlugin(), - genUtilsPlugin() - ], - context: context || {} + plugins: { + transformStart: [parseSchema || defaultPlugins.parseSchema, ...transformStart], + transform: [...Object.values(mergeWithDefaultPlugin), ...transform], + transformEnd: [formatCode || defaultPlugins.formatCode, ...transformEnd] + }, + context: config?.customContext || {} }) - return codeGenInstance.generate(appSchema) + return codeGenInstance } diff --git a/packages/vue-generator/src/generator/index.js b/packages/vue-generator/src/generator/index.js index 9f45e01577..d0093f9ef7 100644 --- a/packages/vue-generator/src/generator/index.js +++ b/packages/vue-generator/src/generator/index.js @@ -12,3 +12,5 @@ export { generateCode, generateBlocksCode, generatePageCode } from './page' export { genSFCWithDefaultPlugin } from './vue/sfc' +export { generateApp } from './generateApp' +export { default as CodeGenerator } from './codeGenerator' diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js index 4f0d21012c..51c09dbbd8 100644 --- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -192,8 +192,6 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { } } - // TODO: 支持页面级别的 dataSource、utils - // 解析 template const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 8292d41ef4..ac2b5e1803 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -160,7 +160,7 @@ export const handleConditionAttrHook = (schemaData, globalHooks, config) => { } export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { - const { prefix, suffix, attributes, schema: { loop, loopArgs } = {} } = schemaData + const { prefix, suffix, attributes, schema: { loop, loopArgs = [] } = {} } = schemaData const isJSX = config.isJSX if (!loop) { diff --git a/packages/vue-generator/src/index.js b/packages/vue-generator/src/index.js index 3153feaaec..a648a40220 100644 --- a/packages/vue-generator/src/index.js +++ b/packages/vue-generator/src/index.js @@ -10,4 +10,4 @@ * */ -export { generateCode, generateBlocksCode, generatePageCode } from './generator' +export { generateCode, generateBlocksCode, generatePageCode, generateApp, CodeGenerator } from './generator' diff --git a/packages/vue-generator/src/plugins/formatCodePlugin.js b/packages/vue-generator/src/plugins/formatCodePlugin.js new file mode 100644 index 0000000000..2011994491 --- /dev/null +++ b/packages/vue-generator/src/plugins/formatCodePlugin.js @@ -0,0 +1,56 @@ +import prettier from 'prettier' +import parserHtml from 'prettier/parser-html' +import parseCss from 'prettier/parser-postcss' +import parserBabel from 'prettier/parser-babel' +import { mergeOptions } from '../utils/mergeOptions' + +function formatCode(options = {}) { + const defaultOption = { + singleQuote: true, + printWidth: 120, + semi: false, + trailingComma: 'none' + } + + const parserMap = { + json: 'json-stringify', + js: 'babel', + jsx: 'babel', + css: 'css', + less: 'less', + html: 'html', + vue: 'vue' + } + + const mergedOption = mergeOptions(defaultOption, options) + + return { + name: 'tinyEngine-generateCode-plugin-format-code', + description: 'transform block schema to code', + /** + * 格式化出码 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema, context) { + context.genResult.forEach((item) => { + const { fileContent, fileName } = item + const parser = parserMap[fileName.split('.').at(-1)] + + if (!parser) { + return + } + + const formattedCode = prettier.format(fileContent, { + parser, + plugins: [parserBabel, parseCss, parserHtml, ...(mergedOption.customPlugin || [])], + ...mergedOption + }) + + this.replaceFile({ ...item, fileContent: formattedCode }) + }) + } + } +} + +export default formatCode diff --git a/packages/vue-generator/src/plugins/genBlockPlugin.js b/packages/vue-generator/src/plugins/genBlockPlugin.js index c9a8072bf7..2f90d73feb 100644 --- a/packages/vue-generator/src/plugins/genBlockPlugin.js +++ b/packages/vue-generator/src/plugins/genBlockPlugin.js @@ -8,24 +8,28 @@ const defaultOption = { function genBlockPlugin(options = {}) { const realOptions = mergeOptions(defaultOption, options) - const { blockBasePath } = realOptions + const { blockBasePath, sfcConfig = {} } = realOptions return { - name: 'tinyengine-plugin-generatecode-block', + name: 'tinyEngine-generateCode-plugin-block', description: 'transform block schema to code', - parseSchema(schema) { - const { blockHistories } = schema - const blockSchema = blockHistories.map((block) => block?.content).filter((schema) => typeof schema === 'object') - - return blockSchema - }, - transform(schema) { - const blocks = this.parseSchema(schema) + /** + * 将区块 schema 转换成高代码 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { + const blocks = schema?.blockSchema || [] + const componentsMap = schema?.componentsMap + + if (blocks && !Array.isArray(blocks)) { + throw new Error(`[codeGenerate][plugins] blockSchema should be array, but actually receive ${typeof blocks}`) + } const resBlocks = [] for (const block of blocks) { - const res = genSFCWithDefaultPlugin(block, this.schema.componentsMap, { blockRelativePath: './' }) + const res = genSFCWithDefaultPlugin(block, componentsMap, { blockRelativePath: './', ...sfcConfig }) resBlocks.push({ fileType: 'vue', diff --git a/packages/vue-generator/src/plugins/genDataSourcePlugin.js b/packages/vue-generator/src/plugins/genDataSourcePlugin.js index fb4c40b006..67af90a2c0 100644 --- a/packages/vue-generator/src/plugins/genDataSourcePlugin.js +++ b/packages/vue-generator/src/plugins/genDataSourcePlugin.js @@ -11,13 +11,15 @@ function genDataSourcePlugin(options = {}) { const { path, fileName } = realOptions return { - name: 'tinyengine-plugin-generatecode-datasource', + name: 'tinyEngine-generateCode-plugin-dataSource', description: 'transform schema to dataSource plugin', - parseSchema(schema) { - return schema?.dataSource || {} - }, - transform(schema) { - const dataSource = this.parseSchema(schema) + /** + * 转换 dataSource + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { + const dataSource = schema?.dataSource || {} const { dataHandler, errorHandler, willFetch, list } = dataSource || {} diff --git a/packages/vue-generator/src/plugins/genDependenciesPlugin.js b/packages/vue-generator/src/plugins/genDependenciesPlugin.js index bda32221b8..b3d13794ee 100644 --- a/packages/vue-generator/src/plugins/genDependenciesPlugin.js +++ b/packages/vue-generator/src/plugins/genDependenciesPlugin.js @@ -5,36 +5,47 @@ const defaultOption = { path: '.' } +const parseSchema = (schema) => { + const { utils = [], componentsMap = [] } = schema + + const resDeps = {} + + for (const { + type, + content: { package: packageName, version } + } of utils) { + if (type !== 'npm' || resDeps[packageName]) { + continue + } + + resDeps[packageName] = version || 'latest' + } + + for (const { package: packageName, version } of componentsMap) { + if (packageName && !resDeps[packageName]) { + resDeps[packageName] = version || 'latest' + } + } + + return resDeps +} + function genDependenciesPlugin(options = {}) { const realOptions = mergeOptions(defaultOption, options) const { path, fileName } = realOptions return { - name: 'tinyengine-plugin-generatecode-dependencies', + name: 'tinyEngine-generateCode-plugin-dependencies', description: 'transform dependencies to package.json', - parseSchema(schema) { - const { utils } = schema - - const utilsDependencies = {} - - for (const { - type, - content: { package: packageName, version } - } of utils) { - if (type !== 'npm') { - continue - } - - utilsDependencies[packageName] = version || 'latest' - } - - // TODO, 这里缺组件依赖分析 - return utilsDependencies - }, - transform(schema) { - const { dependencies } = this.parseSchema(schema) - const originPackageItem = this.genResult.find((item) => item.fileName === fileName && item.path === path) + /** + * 分析依赖,写入 package.json + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { + const dependencies = parseSchema(schema) + const originPackageItem = this.getFile(path, fileName) if (!originPackageItem) { return { @@ -51,7 +62,7 @@ function genDependenciesPlugin(options = {}) { ...dependencies } - this.replaceGenResult({ fileType: 'json', fileName, path, fileContent: JSON.stringify(originPackageJSON) }) + this.addFile({ fileType: 'json', fileName, path, fileContent: JSON.stringify(originPackageJSON) }, true) } } } diff --git a/packages/vue-generator/src/plugins/genGlobalState.js b/packages/vue-generator/src/plugins/genGlobalState.js index 8146f9c53f..1234e44a64 100644 --- a/packages/vue-generator/src/plugins/genGlobalState.js +++ b/packages/vue-generator/src/plugins/genGlobalState.js @@ -11,18 +11,23 @@ function genDependenciesPlugin(options = {}) { const { path } = realOptions return { - name: 'tinyengine-plugin-generatecode-globalState', + name: 'tinyEngine-generateCode-plugin-globalState', description: 'transform schema to globalState', parseSchema(schema) { - let { global_state } = schema + let globalState = schema?.globalState - if (!Array.isArray(global_state)) { - global_state = [] + if (!Array.isArray(globalState)) { + globalState = [] } - return global_state + return globalState }, - transform(schema) { + /** + * 转换 globalState + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { const globalState = this.parseSchema(schema) const res = [] diff --git a/packages/vue-generator/src/plugins/genI18nPlugin.js b/packages/vue-generator/src/plugins/genI18nPlugin.js index 6d25c3241d..d346d041b6 100644 --- a/packages/vue-generator/src/plugins/genI18nPlugin.js +++ b/packages/vue-generator/src/plugins/genI18nPlugin.js @@ -13,9 +13,14 @@ function genI18nPlugin(options = {}) { const { path, localeFileName, entryFileName } = realOptions return { - name: 'tinyengine-plugin-generatecode-i18n', + name: 'tinyEngine-generateCode-plugin-i18n', description: 'transform i18n schema to i18n code plugin', - transform(schema) { + /** + * 将国际化 schema 转换成 i18n 高代码 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { const i18n = schema?.i18n || [] const res = [] @@ -41,10 +46,9 @@ function genI18nPlugin(options = {}) { fileName: localeFileName, path, fileContent: ` - ${importStatements.join('\n')} +${importStatements.join('\n')} - export default { ${langs.join(',')} } - ` +export default { ${langs.join(',')} }` }) // 生成 index.js 入口文件 @@ -52,15 +56,14 @@ function genI18nPlugin(options = {}) { fileName: entryFileName, path, fileContent: ` - import i18n from '@opentiny/tiny-engine-i18n-host' - import lowcode from '../lowcode' - import locale from './${localeFileName}' +import i18n from '@opentiny/tiny-engine-i18n-host' +import lowcode from '../lowcode' +import locale from './${localeFileName}' - i18n.lowcode = lowcode - ${langs.map((langItem) => `i18n.global.mergeLocaleMessage('${langItem}', locale.${langItem})`).join('\n')} +i18n.lowcode = lowcode +${langs.map((langItem) => `i18n.global.mergeLocaleMessage('${langItem}', locale.${langItem})`).join('\n')} - export default i18n - ` +export default i18n` }) return res diff --git a/packages/vue-generator/src/plugins/genPagePlugin.js b/packages/vue-generator/src/plugins/genPagePlugin.js index 96c568cb6f..6cb6ed07b6 100644 --- a/packages/vue-generator/src/plugins/genPagePlugin.js +++ b/packages/vue-generator/src/plugins/genPagePlugin.js @@ -8,59 +8,28 @@ const defaultOption = { function genPagePlugin(options = {}) { const realOptions = mergeOptions(defaultOption, options) - const { pageBasePath } = realOptions + const { pageBasePath, sfcConfig = {} } = realOptions return { - name: 'tinyengine-plugin-generatecode-page', + name: 'tinyEngine-generateCode-plugin-page', description: 'transform page schema to code', - parseSchema(schema) { - const { componentsTree } = schema - const pagesMap = {} - const resPageTree = [] - - for (const componentItem of componentsTree) { - pagesMap[componentItem.id] = componentItem - } - - for (const componentItem of componentsTree) { - if (componentItem.componentName === 'Folder') { - continue - } - - const newComponentItem = { - ...componentItem - } - let path = pageBasePath - let curParentId = componentItem.meta.parentId - let depth = 0 - - while (curParentId !== '0' && depth < 1000) { - const preFolder = pagesMap[curParentId] - - path += `/${preFolder.folderName}` - curParentId = preFolder.parentId - depth++ - } - - newComponentItem.path = path - - resPageTree.push(newComponentItem) - } - - return resPageTree - }, - transform(schema) { - const pages = this.parseSchema(schema) + /** + * 将页面 schema 转换成高代码 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { + const pages = schema.pageSchema const resPage = [] for (const page of pages) { - const res = genSFCWithDefaultPlugin(page, this.schema.componentsMap) + const res = genSFCWithDefaultPlugin(page, schema.componentsMap, sfcConfig) resPage.push({ fileType: 'vue', - fileName: `${page.componentName}.vue`, - path: page.path, + fileName: `${page.fileName}.vue`, + path: `${pageBasePath}/${page.path || ''}`, fileContent: res }) } diff --git a/packages/vue-generator/src/plugins/genRouterPlugin.js b/packages/vue-generator/src/plugins/genRouterPlugin.js index 71a41d861f..6c826c29eb 100644 --- a/packages/vue-generator/src/plugins/genRouterPlugin.js +++ b/packages/vue-generator/src/plugins/genRouterPlugin.js @@ -5,58 +5,66 @@ const defaultOption = { path: './src/router' } -function genRouterPlugin(options = {}) { - const realOptions = mergeOptions(defaultOption, options) +const parseSchema = (schema) => { + const { pageSchema } = schema - const { path, fileName } = realOptions + const routes = pageSchema.map(({ meta: { isHome, router }, fileName, path }) => ({ + filePath: `@/views${path ? `/${path}` : ''}/${fileName}.vue`, + fileName, + isHome, + path: router.startsWith('/') ? router : `/${router}` + })) - return { - name: 'tinyengine-plugin-generatecode-router', - description: 'transform router schema to router code plugin', - parseSchema(schema) { - const { pageCode } = schema + const hasRoot = routes.some(({ path }) => path === '/') - const routes = pageCode.map(({ meta: { isHome, router }, fileName }) => ({ - fileName, - isHome, - path: router.startsWith('/') ? router : `/${router}` - })) + if (!hasRoot && routes.length) { + const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path } - const hasRoot = routes.some(({ path }) => path === '/') + routes.unshift({ path: '/', redirect: homePath }) + } - if (!hasRoot && routes.length) { - const { path: homePath } = routes.find(({ isHome }) => isHome) || { path: routes[0].path } + return routes +} - routes.unshift({ path: '/', redirect: homePath }) - } +function genRouterPlugin(options = {}) { + const realOptions = mergeOptions(defaultOption, options) + + const { path, fileName } = realOptions - return routes - }, - transform(schema) { - const { routes: routesList } = this.parseSchema(schema) + return { + name: 'tinyEngine-generateCode-plugin-router', + description: 'transform router schema to router code plugin', + /** + * 根据页面生成路由配置 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { + const routesList = parseSchema(schema) // TODO: 支持 hash 模式、history 模式 const importSnippet = "import { createRouter, createWebHashHistory } from 'vue-router'" const exportSnippet = ` - export default createRouter({ - history: createWebHashHistory(), - routes - }) - ` +export default createRouter({ + history: createWebHashHistory(), + routes +})` const routes = routesList.map(({ fileName, path, redirect, filePath }) => { - const routeItem = { - path - } + let pathAttr = `path: '${path}'` + let redirectAttr = '' + let componentAttr = '' if (redirect) { - routeItem.redirect = redirect + redirectAttr = `redirect: '${redirect}'` } if (fileName) { - routeItem.component = `() => import('${filePath}')` + componentAttr = `component: () => import('${filePath}')` } - return JSON.stringify(routeItem) + const res = [pathAttr, redirectAttr, componentAttr].filter((item) => Boolean(item)).join(',') + + return `{${res}}` }) const routeSnippets = `const routes = [${routes.join(',')}]` diff --git a/packages/vue-generator/src/plugins/genTemplatePlugin.js b/packages/vue-generator/src/plugins/genTemplatePlugin.js index 6331ac17e2..97dfafc568 100644 --- a/packages/vue-generator/src/plugins/genTemplatePlugin.js +++ b/packages/vue-generator/src/plugins/genTemplatePlugin.js @@ -1,29 +1,23 @@ -import { mergeOptions } from '../utils/mergeOptions' import { templateMap } from '../templates' -const defaultOption = {} - -function genTemplatePlugin(options = {}) { - // 保留,用作拓展配置用途 - const realOptions = mergeOptions(defaultOption, options) - +function genTemplatePlugin() { return { - name: 'tinyengine-plugin-generatecode-template', + name: 'tinyEngine-generateCode-plugin-template', description: 'generate template code', - transform() { - const meta = this.schema.appMeta - const { template } = meta + run(schema, context) { + const template = context?.template || 'default' if (!template) { return } if (typeof template === 'function') { - return template(meta) + context.genResult.push(...(template(schema) || [])) + return } if (templateMap[template]) { - return templateMap[template](meta) + context.genResult.push(...templateMap[template](schema)) } } } diff --git a/packages/vue-generator/src/plugins/genUtilsPlugin.js b/packages/vue-generator/src/plugins/genUtilsPlugin.js index 9d981d017c..709d4caa2e 100644 --- a/packages/vue-generator/src/plugins/genUtilsPlugin.js +++ b/packages/vue-generator/src/plugins/genUtilsPlugin.js @@ -38,16 +38,14 @@ function genUtilsPlugin(options = {}) { } return { - name: 'tinyengine-plugin-generatecode-utils', + name: 'tinyEngine-generateCode-plugin-utils', description: 'transform utils schema to utils code', - // parseSchema(schema) { - // const { utils } = schema - // return { - // id: 'utils', - // result: utils || [] - // } - // }, - transform(schema) { + /** + * 生成 utils 源码 + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + run(schema) { const { utils } = schema if (!Array.isArray(utils)) { @@ -76,10 +74,10 @@ function genUtilsPlugin(options = {}) { } const fileContent = ` - ${importStatements.join('\n')}\n - ${variableStatements.join('\n')}\n - export { ${exportVariables.join(',')} } - ` +${importStatements.join('\n')} +${variableStatements.join('\n')} +export { ${exportVariables.join(',')} } +` return { fileType: 'js', diff --git a/packages/vue-generator/src/plugins/index.js b/packages/vue-generator/src/plugins/index.js index de2a8eefdd..649f93a68e 100644 --- a/packages/vue-generator/src/plugins/index.js +++ b/packages/vue-generator/src/plugins/index.js @@ -6,3 +6,5 @@ export { default as genRouterPlugin } from './genRouterPlugin' export { default as genUtilsPlugin } from './genUtilsPlugin' export { default as genI18nPlugin } from './genI18nPlugin' export { default as genTemplatePlugin } from './genTemplatePlugin' +export { default as formatCodePlugin } from './formatCodePlugin' +export { default as parseSchemaPlugin } from './parseSchemaPlugin' diff --git a/packages/vue-generator/src/plugins/parseSchemaPlugin.js b/packages/vue-generator/src/plugins/parseSchemaPlugin.js new file mode 100644 index 0000000000..5d56fdaa7b --- /dev/null +++ b/packages/vue-generator/src/plugins/parseSchemaPlugin.js @@ -0,0 +1,50 @@ +function parseSchema() { + return { + name: 'tinyEngine-generateCode-plugin-parse-schema', + description: 'parse schema, preprocess schema', + + /** + * 解析schema,预处理 schema + * @param {import('../generator/generateApp').AppSchema} schema + * @returns + */ + parse(schema) { + const { pageSchema } = schema + const pagesMap = {} + const resPageTree = [] + + for (const componentItem of pageSchema) { + pagesMap[componentItem.id] = componentItem + } + + for (const componentItem of pageSchema) { + if (componentItem.componentName === 'Folder') { + continue + } + + const newComponentItem = { + ...componentItem + } + let path = '' + let curParentId = componentItem.meta.parentId + let depth = 0 + + while (curParentId !== '0' && depth < 1000) { + const preFolder = pagesMap[curParentId] + + path = `${preFolder.folderName}/${path}` + curParentId = preFolder.parentId + depth++ + } + + newComponentItem.path = path + + resPageTree.push(newComponentItem) + } + + schema.pageSchema = resPageTree + } + } +} + +export default parseSchema diff --git a/packages/vue-generator/src/templates/index.js b/packages/vue-generator/src/templates/index.js index 7450fb42c9..1a7cfd5527 100644 --- a/packages/vue-generator/src/templates/index.js +++ b/packages/vue-generator/src/templates/index.js @@ -1,4 +1,4 @@ -import { generateTemplate as genDefaultStaticTemplate } from './templates/vue-template' +import { generateTemplate as genDefaultStaticTemplate } from './vue-template' export const templateMap = { default: genDefaultStaticTemplate diff --git a/packages/vue-generator/src/templates/vue-template/index.js b/packages/vue-generator/src/templates/vue-template/index.js index ecbf9ac0cc..f6bc698840 100644 --- a/packages/vue-generator/src/templates/vue-template/index.js +++ b/packages/vue-generator/src/templates/vue-template/index.js @@ -18,13 +18,15 @@ import httpEntryFile from './templateFiles/src/http/index.js?raw' * @param {*} str * @returns */ -const getTemplate = (context, str) => { +const getTemplate = (schema, str) => { return str.replace(/(\$\$TinyEngine{(.*)}END\$)/g, function (match, p1, p2) { if (!p2) { return '' } + const keyArr = p2.split('.') - const value = keyArr.reduce((preVal, key) => preVal?.[key] ?? '', context) + const value = keyArr.reduce((preVal, key) => preVal?.[key] ?? '', schema) + return value }) } @@ -33,83 +35,83 @@ const getTemplate = (context, str) => { * get project template * @returns */ -export function generateTemplate(context) { +export function generateTemplate(schema) { return [ { fileType: 'md', fileName: 'README.md', - paths: '.', - fileContent: getTemplate(context, readmeFile) + path: '.', + fileContent: getTemplate(schema, readmeFile) }, { fileType: 'js', fileName: 'vite.config.js', - paths: '.', - fileContent: getTemplate(context, viteConfigFile) + path: '.', + fileContent: getTemplate(schema, viteConfigFile) }, { fileType: 'json', fileName: 'package.json', - paths: '.', - fileContent: getPackageJson(context) + path: '.', + fileContent: getPackageJson(schema) }, { fileName: '.gitignore', - paths: '.', - fileContent: getTemplate(context, gitIgnoreFile) + path: '.', + fileContent: getTemplate(schema, gitIgnoreFile) }, { fileType: 'html', fileName: 'index.html', - paths: '.', - fileContent: getTemplate(context, entryHTMLFile) + path: '.', + fileContent: getTemplate(schema, entryHTMLFile) }, { fileType: 'js', fileName: 'main.js', - paths: './src', - fileContent: getTemplate(context, mainJSFile) + path: './src', + fileContent: getTemplate(schema, mainJSFile) }, { fileType: 'vue', fileName: 'App.vue', - paths: './src', - fileContent: getTemplate(context, appVueFile) + path: './src', + fileContent: getTemplate(schema, appVueFile) }, { fileType: 'js', fileName: 'bridge.js', - paths: './src/lowcodeConfig', + path: './src/lowcodeConfig', fileContent: bridgeFile }, { fileType: 'js', fileName: 'dataSource.js', - paths: './src/lowcodeConfig', + path: './src/lowcodeConfig', fileContent: dataSourceFile }, { fileType: 'js', fileName: 'lowcode.js', - paths: './src/lowcodeConfig', + path: './src/lowcodeConfig', fileContent: lowcodeJSFile }, { fileType: 'js', fileName: 'axios.js', - paths: './src/http', + path: './src/http', fileContent: axiosFile }, { fileType: 'js', fileName: 'config.js', - paths: './src/http', + path: './src/http', fileContent: axiosConfigFile }, { fileType: 'js', fileName: 'index.js', - paths: './src/http', + path: './src/http', fileContent: httpEntryFile } ] diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/README.md b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md index 70e735d7b2..c653b40df9 100644 --- a/packages/vue-generator/src/templates/vue-template/templateFiles/README.md +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/README.md @@ -1,4 +1,4 @@ -## $TinyEngine{appName}END$ +## $$TinyEngine{meta.name}END$ 本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。 diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/index.html b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html index 13b675be42..26aaf6d1d9 100644 --- a/packages/vue-generator/src/templates/vue-template/templateFiles/index.html +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/index.html @@ -4,7 +4,7 @@ - ${appName} + $$TinyEngine{meta.name}END$
diff --git a/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js index e0daa765c4..aa01d43759 100644 --- a/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js +++ b/packages/vue-generator/src/templates/vue-template/templateFiles/packageJson.js @@ -1,8 +1,8 @@ // 这里 package.json 格式设置为 js,避免被识别成一个 package -export default (context) => { - const packageName = context?.appName || '@opentiny/tiny-engine-preview-vue' +export default (schema) => { + const packageName = schema?.meta?.name || '@opentiny/tiny-engine-preview-vue' - return { + const res = { name: packageName, version: '1.0.0', scripts: { @@ -28,4 +28,6 @@ export default (context) => { vite: '^4.3.7' } } + + return JSON.stringify(res) } diff --git a/packages/vue-generator/src/utils/generateImportStatement.js b/packages/vue-generator/src/utils/generateImportStatement.js index 70532aeafb..2357ed2f98 100644 --- a/packages/vue-generator/src/utils/generateImportStatement.js +++ b/packages/vue-generator/src/utils/generateImportStatement.js @@ -12,7 +12,7 @@ export function generateImportStatement(config) { statementName = `{ ${statementName} }` } - return `import ${statementName} from ${moduleName}` + return `import ${statementName} from '${moduleName}'` } export function generateImportByPkgName(config) { diff --git a/packages/vue-generator/test/testcases/generator/generateApp.test.js b/packages/vue-generator/test/testcases/generator/generateApp.test.js new file mode 100644 index 0000000000..66d054ac80 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/generateApp.test.js @@ -0,0 +1,22 @@ +import { expect, test, describe } from 'vitest' +import path from 'path' +import fs from 'fs' +import { generateApp } from '@/generator/generateApp' +import { appSchemaDemo01 } from './mockData' +// import { describe } from 'node:test' + +describe('generate whole application', () => { + test('should not throw error', async () => { + const instance = generateApp() + + const res = await instance.generate(appSchemaDemo01) + const { genResult } = res + + genResult.forEach(({ fileName, path: filePath, fileContent }) => { + fs.mkdirSync(path.resolve(__dirname, `./result/${filePath}`), { recursive: true }) + fs.writeFileSync(path.resolve(__dirname, `./result/${filePath}/${fileName}`), fileContent) + }) + + expect(true).toBe(true) + }) +}) diff --git a/packages/vue-generator/test/testcases/generator/mockData.js b/packages/vue-generator/test/testcases/generator/mockData.js new file mode 100644 index 0000000000..ef1153e198 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/mockData.js @@ -0,0 +1,2103 @@ +export const appSchemaDemo01 = { + meta: { + name: 'portal-app', + tenant: 1, + git_group: '', + project_name: '', + description: 'demo应用', + branch: 'develop', + is_demo: null, + global_state: [], + appId: '918', + creator: '', + gmt_create: '2022-06-08 03:19:01', + gmt_modified: '2023-08-23 10:22:28' + }, + dataSource: { + list: [ + { + id: 132, + name: 'getAllComponent', + data: { + data: [], + type: 'array' + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-28T06:26:26.000Z', + updated_at: '2022-06-28T07:02:30.000Z' + }, + { + id: 133, + name: 'getAllList', + data: { + columns: [ + { + name: 'test', + title: '测试', + field: 'test', + type: 'string', + format: {} + }, + { + name: 'test1', + title: '测试1', + field: 'test1', + type: 'string', + format: {} + } + ], + type: 'array', + data: [ + { + test: 'test1', + test1: 'test1', + _id: '341efc48' + }, + { + test: 'test2', + test1: 'test1', + _id: 'b86b516c' + }, + { + test: 'test3', + test1: 'test1', + _id: 'f680cd78' + } + ], + options: { + uri: '', + method: 'GET' + }, + dataHandler: { + type: 'JSFunction', + value: 'function dataHandler(data) { \n return data \n}' + }, + willFetch: { + type: 'JSFunction', + value: 'function willFetch(option) {\n return option \n}' + }, + shouldFetch: { + type: 'JSFunction', + value: 'function shouldFetch(option) {\n return true \n}' + }, + errorHandler: { + type: 'JSFunction', + value: 'function errorHandler(err) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-28T07:32:16.000Z', + updated_at: '2023-01-19T03:29:11.000Z' + }, + { + id: 135, + name: 'getAllMaterialList', + data: { + columns: [ + { + name: 'id', + title: 'id', + field: 'id', + type: 'string', + format: {} + }, + { + name: 'name', + title: 'name', + field: 'name', + type: 'string', + format: {} + }, + { + name: 'framework', + title: 'framework', + field: 'framework', + type: 'string', + format: { + required: true + } + }, + { + name: 'components', + title: 'components', + field: 'components', + type: 'string', + format: {} + }, + { + name: 'content', + title: 'content', + field: 'content', + type: 'string', + format: {} + }, + { + name: 'url', + title: 'url', + field: 'url', + type: 'string', + format: {} + }, + { + name: 'published_at', + title: 'published_at', + field: 'published_at', + type: 'string', + format: {} + }, + { + name: 'created_at', + title: 'created_at', + field: 'created_at', + type: 'string', + format: {} + }, + { + name: 'updated_at', + title: 'updated_at', + field: 'updated_at', + type: 'string', + format: {} + }, + { + name: 'published', + title: 'published', + field: 'published', + type: 'string', + format: {} + }, + { + name: 'last_build_info', + title: 'last_build_info', + field: 'last_build_info', + type: 'string', + format: {} + }, + { + name: 'tenant', + title: 'tenant', + field: 'tenant', + type: 'string', + format: {} + }, + { + name: 'version', + title: 'version', + field: 'version', + type: 'string', + format: {} + }, + { + name: 'description', + title: 'description', + field: 'description', + type: 'string', + format: {} + } + ], + type: 'array', + data: [ + { + id: 'f37123ec', + url: '', + name: 'ng-material', + tenant: '', + content: '', + version: '1.0.0', + framework: 'Angular', + published: '', + components: '', + created_at: '2021-11-02T11:32:22.000Z', + updated_at: '2021-11-02T11:32:22.000Z', + description: 'angular组件库物料', + published_at: '2021-11-02T11:32:22.000Z', + last_build_info: '', + _id: '2a23e653' + }, + { + id: 'f37123ec', + url: '', + name: 'ng-material', + tenant: '', + content: '', + version: '1.0.0', + framework: 'Angular', + published: '', + components: '', + created_at: '2021-11-02T11:32:22.000Z', + updated_at: '2021-11-02T11:32:22.000Z', + description: 'angular组件库物料', + published_at: '2021-11-02T11:32:22.000Z', + last_build_info: '', + _id: '06b253be' + }, + { + id: 'f37123ec', + url: '', + name: 'ng-material', + tenant: '', + content: '', + version: '1.0.0', + framework: 'Angular', + published: '', + components: '', + created_at: '2021-11-02T11:32:22.000Z', + updated_at: '2021-11-02T11:32:22.000Z', + description: 'angular组件库物料', + published_at: '2021-11-02T11:32:22.000Z', + last_build_info: '', + _id: 'c55a41ed' + }, + { + id: 'f37123ec', + url: '', + name: 'ng-material', + tenant: '', + content: '', + version: '1.0.0', + framework: 'Angular', + published: '', + components: '', + created_at: '2021-11-02T11:32:22.000Z', + updated_at: '2021-11-02T11:32:22.000Z', + description: 'angular组件库物料', + published_at: '2021-11-02T11:32:22.000Z', + last_build_info: '', + _id: 'f37123ec' + }, + { + id: '7a63c1a2', + url: '', + name: 'tiny-vue', + tenant: '', + content: 'Tiny Vue物料', + version: '1.0.0', + framework: 'Vue', + published: '', + components: '', + created_at: '', + updated_at: '', + description: 'Tiny Vue物料', + published_at: '', + last_build_info: '', + _id: '7a63c1a2' + } + ], + options: { + uri: '', + method: 'GET' + }, + willFetch: { + type: 'JSFunction', + value: 'function willFetch(option) {\n return option \n}' + }, + dataHandler: { + type: 'JSFunction', + value: 'function dataHandler(data) { \n return data \n}' + }, + shouldFetch: { + type: 'JSFunction', + value: 'function shouldFetch(option) {\n return true \n}' + }, + errorHandler: { + type: 'JSFunction', + value: 'function errorHandler(err) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-29T00:57:50.000Z', + updated_at: '2023-05-15T02:37:12.000Z' + }, + { + id: 139, + name: 'treedata', + data: { + data: [ + { + label: 'level111', + value: '111', + id: 'f6609643', + pid: '', + _RID: 'row_4' + }, + { + label: 'level1-son', + value: '111-1', + id: 'af1f937f', + pid: 'f6609643', + _RID: 'row_5' + }, + { + label: 'level222', + value: '222', + id: '28e3709c', + pid: '', + _RID: 'row_6' + }, + { + label: 'level2-son', + value: '222-1', + id: '6b571bef', + pid: '28e3709c', + _RID: 'row_5' + }, + { + id: '6317c2cc', + pid: 'fdfa', + label: 'fsdfaa', + value: 'fsadf', + _RID: 'row_6' + }, + { + id: '9cce369f', + pid: 'test', + label: 'test1', + value: '001' + } + ], + type: 'tree' + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-06-30T06:13:57.000Z', + updated_at: '2022-07-29T03:14:55.000Z' + }, + { + id: 150, + name: 'componentList', + data: { + data: [ + { + _RID: 'row_1', + name: '表单', + isSelected: 'true', + description: '由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据' + }, + { + name: '按钮', + isSelected: 'false', + description: '常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型' + }, + { + id: '490f8a00', + _RID: 'row_3', + name: '表单项', + framework: '', + materials: '', + description: 'Form 组件下的 FormItem 配置' + }, + { + id: 'c259b8b3', + _RID: 'row_4', + name: '开关', + framework: '', + materials: '', + description: '关闭或打开' + }, + { + id: '083ed9c7', + _RID: 'row_5', + name: '互斥按钮组', + framework: '', + materials: '', + description: '以按钮组的方式出现,常用于多项类似操作' + }, + { + id: '09136cea', + _RID: 'row_6', + name: '提示框', + framework: '', + materials: '', + description: 'Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画' + }, + { + id: 'a63b57d5', + _RID: 'row_7', + name: '文字提示框', + framework: '', + materials: '', + description: + '动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信' + }, + { + id: 'a0f6e8a3', + _RID: 'row_8', + name: '树', + framework: '', + materials: '', + description: + '可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单' + }, + { + id: 'd1aa18fc', + _RID: 'row_9', + name: '分页', + framework: '', + materials: '', + description: '当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件' + }, + { + id: 'ca49cc52', + _RID: 'row_10', + name: '表格', + framework: '', + materials: '', + description: '提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等' + }, + { + id: '4e20ecc9', + name: '搜索框', + framework: '', + materials: '', + description: '指定条件对象进行搜索数据' + }, + { + id: '6b093ee5', + name: '折叠面板', + framework: '', + materials: '', + description: '内容区可指定动态页面或自定义 html 等,支持展开收起操作' + }, + { + id: '0a09abc0', + name: '对话框', + framework: '', + materials: '', + description: '模态对话框,在浮层中显示,引导用户进行相关操作' + }, + { + id: 'f814b901', + name: '标签页签项', + framework: '', + materials: '', + description: 'tab页签' + }, + { + id: 'c5ae797c', + name: '单选', + framework: '', + materials: '', + description: '用于配置不同场景的选项,在一组备选项中进行单选' + }, + { + id: '33d0c590', + _RID: 'row_13', + name: '弹出编辑', + framework: '', + materials: '', + description: + '该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件' + }, + { + id: '16711dfa', + _RID: 'row_14', + name: '下拉框', + framework: '', + materials: '', + description: 'Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件' + }, + { + id: 'a9fd190a', + _RID: 'row_15', + name: '折叠面板项', + framework: '', + materials: '', + description: '内容区可指定动态页面或自定义 html 等,支持展开收起操作' + }, + { + id: 'a7dfa9ec', + _RID: 'row_16', + name: '复选框', + framework: '', + materials: '', + description: '用于配置不同场景的选项,提供用户可在一组选项中进行多选' + }, + { + id: 'd4bb8330', + name: '输入框', + framework: '', + materials: '', + description: '通过鼠标或键盘输入字符' + }, + { + id: 'ced3dc83', + name: '时间线', + framework: '', + materials: '', + description: '时间线' + } + ], + type: 'array', + columns: [ + { + name: 'name', + type: 'string', + field: 'name', + title: 'name', + format: { + max: 0, + min: 0, + dateTime: false, + required: false, + stringType: '' + } + }, + { + name: 'description', + type: 'string', + field: 'description', + title: 'description', + format: { + max: 0, + min: 0, + dateTime: false, + required: false, + stringType: '' + } + }, + { + name: 'isSelected', + type: 'string', + field: 'isSelected', + title: 'isSelected', + format: { + max: 0, + min: 0, + dateTime: false, + required: false, + stringType: '' + } + } + ], + options: { + uri: 'http://localhost:9090/assets/json/bundle.json', + method: 'GET' + }, + willFetch: { + type: 'JSFunction', + value: 'function willFetch(option) {\n return option \n}' + }, + dataHandler: { + type: 'JSFunction', + value: 'function dataHandler(data) { \n return data \n}' + }, + shouldFetch: { + type: 'JSFunction', + value: 'function shouldFetch(option) {\n return true \n}' + }, + errorHandler: { + type: 'JSFunction', + value: 'function errorHandler(err) {}' + } + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-07-04T02:20:07.000Z', + updated_at: '2022-07-04T06:25:29.000Z' + }, + { + id: 151, + name: 'selectedComponents', + data: { + columns: [ + { + name: 'name', + title: 'name', + field: 'name', + type: 'string', + format: { + required: false, + stringType: '', + min: 0, + max: 0, + dateTime: false + } + }, + { + name: 'description', + title: 'description', + field: 'description', + type: 'string', + format: { + required: false, + stringType: '', + min: 0, + max: 0, + dateTime: false + } + }, + { + name: 'isSelected', + title: 'isSelected', + field: 'isSelected', + type: 'string', + format: { + required: false, + stringType: '', + min: 0, + max: 0, + dateTime: false + } + } + ], + type: 'array', + data: [ + { + name: '标签页', + description: '分隔内容上有关联但属于不同类别的数据集合', + isSelected: 'true', + _RID: 'row_2' + }, + { + name: '布局列', + description: '列配置信息', + isSelected: 'true', + id: '76a7080a', + _RID: 'row_4' + }, + { + name: '日期选择器', + description: '用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式', + isSelected: 'true', + id: '76b20d73', + _RID: 'row_1' + }, + { + name: '走马灯', + description: '常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现', + isSelected: 'true', + id: '4c884c3d' + } + ] + }, + tpl: null, + app: '918', + desc: null, + created_at: '2022-07-04T03:04:05.000Z', + updated_at: '2022-07-04T03:43:40.000Z' + } + ], + dataHandler: { + type: 'JSFunction', + value: 'function dataHanlder(res){\n return res;\n}' + } + }, + i18n: { + zh_CN: { + 'lowcode.cca8d0ea': '应用', + 'lowcode.c257d5e8': '查询', + 'lowcode.61c8ac8c': '地方', + 'lowcode.f53187a0': '测试', + 'lowcode.97ad00dd': '创建物料资产包', + 'lowcode.61dcef52': 'terterere', + 'lowcode.45f4c42a': 'gdfgdf', + 'lowcode.c6f5a652': 'fsdaf', + 'lowcode.34923432': 'fdsafdsa', + 'lowcode.48521e45': 'fdsfds', + 'lowcode.6534943e': 'fdsafds', + 'lowcode.44252642': 'fdsafds', + 'lowcode.2a743651': 'sda', + 'lowcode.24315357': 'fdsafds', + 'lowcode.44621691': 'fdsafsd', + 'lowcode.65636226': 'fdsaf', + 'lowcode.6426a4e2': 'sd', + 'lowcode.e41c6636': 'aa', + 'lowcode.51c23164': 'aa', + 'lowcode.17245b46': 'aa', + 'lowcode.4573143c': 'aa', + 'lowcode.56432442': 'aa', + 'lowcode.33566643': 'aa', + 'lowcode.565128f3': 'aa', + 'lowcode.56643835': 'aa', + 'lowcode.33311134': 'aa', + 'lowcode.44326643': 'aa', + 'lowcode.36223242': 'aa' + }, + en_US: { + 'lowcode.cca8d0ea': 'app', + 'lowcode.c257d5e8': 'search', + 'lowcode.61c8ac8c': 'dsdsa', + 'lowcode.f53187a0': 'test', + 'lowcode.97ad00dd': 'createMaterial', + 'lowcode.61dcef52': 'sadasda', + 'lowcode.45f4c42a': 'gfdgfd', + 'lowcode.c6f5a652': 'fsdafds', + 'lowcode.34923432': 'fdsafds', + 'lowcode.6534943e': 'fdsafdsa', + 'lowcode.44252642': 'aaaa', + 'lowcode.2a743651': 'fdsaf', + 'lowcode.24315357': 'fsdafds', + 'lowcode.44621691': 'sd', + 'lowcode.65636226': 'fdsfsd', + 'lowcode.6426a4e2': 'fdsafsd', + 'lowcode.e41c6636': 'aa', + 'lowcode.51c23164': 'aa', + 'lowcode.17245b46': 'aa', + 'lowcode.4573143c': 'a', + 'lowcode.56432442': 'aa', + 'lowcode.33566643': 'aa', + 'lowcode.565128f3': 'aa', + 'lowcode.56643835': 'aa', + 'lowcode.33311134': 'aa', + 'lowcode.44326643': 'aa', + 'lowcode.36223242': 'aa' + } + }, + pageSchema: [ + { + state: { + dataDisk: [1, 2, 3] + }, + methods: {}, + componentName: 'Page', + css: 'body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}', + props: {}, + children: [ + { + componentName: 'div', + props: { + style: 'padding-bottom: 10px; padding-top: 10px;' + }, + id: '2b2cabf0', + children: [ + { + componentName: 'TinyTimeLine', + props: { + active: '2', + data: [ + { + name: '基础配置' + }, + { + name: '网络配置' + }, + { + name: '高级配置' + }, + { + name: '确认配置' + } + ], + horizontal: true, + style: 'border-radius: 0px;' + }, + id: 'dd764b17' + } + ] + }, + { + componentName: 'div', + props: { + style: + 'border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;' + }, + id: '30c94cc8', + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '计费模式' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '包年/包月', + value: '1' + }, + { + text: '按需计费', + value: '2' + } + ], + modelValue: '1' + }, + id: 'a8d84361' + } + ], + id: '9f39f3e7' + }, + { + componentName: 'TinyFormItem', + props: { + label: '区域' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '乌兰察布二零一', + value: '1' + } + ], + modelValue: '1', + style: 'border-radius: 0px; margin-right: 10px;' + }, + id: 'c97ccd99' + }, + { + componentName: 'Text', + props: { + text: '温馨提示:页面左上角切换区域', + style: 'background-color: [object Event]; color: #8a8e99; font-size: 12px;' + }, + id: '20923497' + }, + { + componentName: 'Text', + props: { + text: '不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度', + style: 'display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;' + }, + id: '54780a26' + } + ], + id: '4966384d' + }, + { + componentName: 'TinyFormItem', + props: { + label: '可用区', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '可用区1', + value: '1' + }, + { + text: '可用区2', + value: '2' + }, + { + text: '可用区3', + value: '3' + } + ], + modelValue: '1' + }, + id: '6184481b' + } + ], + id: '690837bf' + } + ], + id: 'b6a425d4' + } + ] + }, + { + componentName: 'div', + props: { + style: + 'border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: 'CPU架构' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: 'x86计算', + value: '1' + }, + { + text: '鲲鹏计算', + value: '2' + } + ], + modelValue: '1' + }, + id: '7d33ced7' + } + ], + id: '05ed5a79' + }, + { + componentName: 'TinyFormItem', + props: { + label: '区域' + }, + children: [ + { + componentName: 'div', + props: { + style: 'display: flex; justify-content: flex-start; align-items: center;' + }, + id: '606edf78', + children: [ + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center; margin-right: 10px;' + }, + id: 'f3f98246', + children: [ + { + componentName: 'Text', + props: { + text: 'vCPUs', + style: 'width: 80px;' + }, + id: 'c287437e' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ] + }, + id: '4c43286b' + } + ] + }, + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center; margin-right: 10px;' + }, + children: [ + { + componentName: 'Text', + props: { + text: '内存', + style: 'width: 80px; border-radius: 0px;' + }, + id: '38b8fa1f' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ] + }, + id: 'cd33328e' + } + ], + id: '2b2c678f' + }, + { + componentName: 'div', + props: { + style: 'display: flex; align-items: center;' + }, + children: [ + { + componentName: 'Text', + props: { + text: '规格名称', + style: 'width: 80px;' + }, + id: 'd3eb6352' + }, + { + componentName: 'TinySearch', + props: { + modelValue: '', + placeholder: '输入关键词' + }, + id: '21cb9282' + } + ], + id: 'b8e0f35c' + } + ] + }, + { + componentName: 'div', + props: { + style: 'border-radius: 0px;' + }, + id: '5000c83e', + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '通用计算型', + value: '1' + }, + { + text: '通用计算增强型', + value: '2' + }, + { + text: '内存优化型', + value: '3' + }, + { + text: '内存优化型', + value: '4' + }, + { + text: '磁盘增强型', + value: '5' + }, + { + text: '超高I/O型', + value: '6' + }, + { + text: 'GPU加速型', + value: '7' + } + ], + modelValue: '1', + style: 'border-radius: 0px; margin-top: 12px;' + }, + id: 'b8724703' + }, + { + componentName: 'TinyGrid', + props: { + editConfig: { + trigger: 'click', + mode: 'cell', + showStatus: true + }, + columns: [ + { + type: 'radio', + width: 60 + }, + { + field: 'employees', + title: '规格名称' + }, + { + field: 'created_date', + title: 'vCPUs | 内存(GiB)', + sortable: true + }, + { + field: 'city', + title: 'CPU', + sortable: true + }, + { + title: '基准 / 最大带宽\t', + sortable: true + }, + { + title: '内网收发包', + sortable: true + } + ], + data: [ + { + id: '1', + name: 'GFD科技有限公司', + city: '福州', + employees: 800, + created_date: '2014-04-30 00:56:00', + boole: false + }, + { + id: '2', + name: 'WWW科技有限公司', + city: '深圳', + employees: 300, + created_date: '2016-07-08 12:36:22', + boole: true + } + ], + style: 'margin-top: 12px; border-radius: 0px;', + 'auto-resize': true + }, + id: '77701c25' + }, + { + componentName: 'div', + props: { + style: 'margin-top: 12px; border-radius: 0px;' + }, + id: '3339838b', + children: [ + { + componentName: 'Text', + props: { + text: '当前规格', + style: 'width: 150px; display: inline-block;' + }, + id: '203b012b' + }, + { + componentName: 'Text', + props: { + text: '通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB', + style: 'font-weight: 700;' + }, + id: '87723f52' + } + ] + } + ] + } + ], + id: '657fb2fc' + } + ], + id: 'd19b15cf' + } + ], + id: '9991228b' + }, + { + componentName: 'div', + props: { + style: + 'border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '镜像', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyButtonGroup', + props: { + data: [ + { + text: '公共镜像', + value: '1' + }, + { + text: '私有镜像', + value: '2' + }, + { + text: '共享镜像', + value: '3' + } + ], + modelValue: '1' + }, + id: '922b14cb' + }, + { + componentName: 'div', + props: { + style: 'display: flex; margin-top: 12px; border-radius: 0px;' + }, + id: '6b679524', + children: [ + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 170px; margin-right: 10px;' + }, + id: '4851fff7' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 340px;' + }, + id: 'a7183eb7' + } + ] + }, + { + componentName: 'div', + props: { + style: 'margin-top: 12px;' + }, + id: '57aee314', + children: [ + { + componentName: 'Text', + props: { + text: '请注意操作系统的语言类型。', + style: 'color: #e37d29;' + }, + id: '56d36c27' + } + ] + } + ], + id: 'e3b02436' + } + ], + id: '59aebf2b' + } + ], + id: '87ff7b99' + }, + { + componentName: 'div', + props: { + style: + 'border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '系统盘', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'div', + props: { + style: 'display: flex;' + }, + id: 'cddba5b8', + children: [ + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 200px; margin-right: 10px;' + }, + id: 'a97fbe15' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '1cde4c0f' + }, + { + componentName: 'Text', + props: { + text: 'GiB \nIOPS上限240,IOPS突发上限5,000', + style: 'color: #575d6c; font-size: 12px;' + }, + id: '2815d82d' + } + ] + } + ], + id: '50239a3a' + } + ], + id: 'e8582986' + }, + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: '数据盘', + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'div', + props: { + style: 'margin-top: 12px; display: flex;' + }, + id: '728c9825', + children: [ + { + componentName: 'Icon', + props: { + style: 'margin-right: 10px; width: 16px; height: 16px;', + name: 'IconPanelMini' + }, + id: 'fded6930' + }, + { + componentName: 'TinySelect', + props: { + modelValue: '', + placeholder: '请选择', + options: [ + { + value: '1', + label: '黄金糕' + }, + { + value: '2', + label: '双皮奶' + } + ], + style: 'width: 200px; margin-right: 10px;' + }, + id: '62734e3f' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '667c7926' + }, + { + componentName: 'Text', + props: { + text: 'GiB \nIOPS上限600,IOPS突发上限5,000', + style: 'color: #575d6c; font-size: 12px; margin-right: 10px;' + }, + id: 'e7bc36d6' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px;' + }, + id: '1bd56dc0' + } + ], + loop: { + type: 'JSExpression', + value: 'this.state.dataDisk' + } + }, + { + componentName: 'div', + props: { + style: 'display: flex; margin-top: 12px; border-radius: 0px;' + }, + children: [ + { + componentName: 'Icon', + props: { + name: 'IconPlus', + style: 'width: 16px; height: 16px; margin-right: 10px;' + }, + id: '65c89f2b' + }, + { + componentName: 'Text', + props: { + text: '增加一块数据盘', + style: 'font-size: 12px; border-radius: 0px; margin-right: 10px;' + }, + id: 'cb344071' + }, + { + componentName: 'Text', + props: { + text: '您还可以挂载 21 块磁盘(云硬盘)', + style: 'color: #8a8e99; font-size: 12px;' + }, + id: '80eea996' + } + ], + id: 'e9e530ab' + } + ], + id: '078e03ef' + } + ], + id: 'ccef886e' + } + ], + id: '0fb7bd74' + }, + { + componentName: 'div', + props: { + style: + 'border-width: 1px; border-style: solid; border-color: #ffffff; padding-top: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; position: fixed; inset: auto 0% 0% 0%; height: 80px; line-height: 80px; border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyForm', + props: { + labelWidth: '80px', + labelPosition: 'top', + inline: false, + 'label-position': 'left ', + 'label-width': '150px', + style: 'border-radius: 0px;' + }, + children: [], + id: '21ed4475' + }, + { + componentName: 'TinyRow', + props: { + style: 'border-radius: 0px; height: 100%;' + }, + children: [ + { + componentName: 'TinyCol', + props: { + span: '8' + }, + id: 'b9d051a5', + children: [ + { + componentName: 'TinyRow', + props: { + style: 'border-radius: 0px;' + }, + children: [ + { + componentName: 'TinyCol', + props: { + span: '5', + style: 'display: flex;' + }, + id: '02352776', + children: [ + { + componentName: 'Text', + props: { + text: '购买量', + style: 'margin-right: 10px;' + }, + id: '0cd9ed5c' + }, + { + componentName: 'TinyInput', + props: { + placeholder: '请输入', + modelValue: '', + style: 'width: 120px; margin-right: 10px;' + }, + id: '2f9cf442' + }, + { + componentName: 'Text', + props: { + text: '台' + }, + id: 'facd4481' + } + ] + }, + { + componentName: 'TinyCol', + props: { + span: '7' + }, + id: '82b6c659', + children: [ + { + componentName: 'div', + props: {}, + id: '9cd65874', + children: [ + { + componentName: 'Text', + props: { + text: '配置费用', + style: 'font-size: 12px;' + }, + id: 'b5a0a0da' + }, + { + componentName: 'Text', + props: { + text: '¥1.5776', + style: 'padding-left: 10px; padding-right: 10px; color: #de504e;' + }, + id: 'd9464214' + }, + { + componentName: 'Text', + props: { + text: '/小时', + style: 'font-size: 12px;' + }, + id: 'af7cc5e6' + } + ] + }, + { + componentName: 'div', + props: {}, + id: '89063830', + children: [ + { + componentName: 'Text', + props: { + text: '参考价格,具体扣费请以账单为准。', + style: 'font-size: 12px; border-radius: 0px;' + }, + id: 'd8995fbc' + }, + { + componentName: 'Text', + props: { + text: '了解计费详情', + style: 'font-size: 12px; color: #344899;' + }, + id: 'b383c3e2' + } + ] + } + ] + } + ], + id: '94fc0e43' + } + ] + }, + { + componentName: 'TinyCol', + props: { + span: '4', + style: + 'display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;' + }, + id: '10b73009', + children: [ + { + componentName: 'TinyButton', + props: { + text: '下一步: 网络配置', + type: 'danger', + style: 'max-width: unset;' + }, + id: '0b584011' + } + ] + } + ], + id: 'd414a473' + } + ], + id: 'e8ec029b' + } + ], + fileName: 'createVm', + meta: { + id: 1977, + parentId: '0', + group: 'staticPages', + occupier: { + id: 86, + username: '开发者', + email: 'developer@lowcode.com', + provider: null, + password: null, + resetPasswordToken: 'developer', + confirmationToken: 'dfb2c162-351f-4f44-ad5f-899831311129', + confirmed: true, + blocked: null, + role: null, + created_by: null, + updated_by: null, + created_at: '2022-05-27T16:50:44.000Z', + updated_at: '2022-05-27T16:50:44.000Z', + block: null, + is_admin: true, + is_public: null + }, + isHome: false, + router: 'createVm', + rootElement: 'div', + creator: '', + gmt_create: '2022-07-21 03:08:20', + gmt_modified: '2022-07-21 05:18:26' + } + } + ], + componentsMap: [ + { + componentName: 'TinyCarouselItem', + package: '@opentiny/vue', + exportName: 'CarouselItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCheckboxButton', + package: '@opentiny/vue', + exportName: 'CheckboxButton', + destructuring: true, + version: '0.1.17' + }, + { + componentName: 'TinyTree', + package: '@opentiny/vue', + exportName: 'Tree', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPopover', + package: '@opentiny/vue', + exportName: 'Popover', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTooltip', + package: '@opentiny/vue', + exportName: 'Tooltip', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinyCol', + package: '@opentiny/vue', + exportName: 'Col', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdownItem', + package: '@opentiny/vue', + exportName: 'DropdownItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPager', + package: '@opentiny/vue', + exportName: 'Pager', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPlusAccessdeclined', + package: '@opentiny/vue', + exportName: 'AccessDeclined', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusFrozenPage', + package: '@opentiny/vuee', + exportName: 'FrozenPage', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusNonSupportRegion', + package: '@opentiny/vue', + exportName: 'NonSupportRegion', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinyPlusBeta', + package: '@opentiny/vue', + exportName: 'Beta', + destructuring: true, + version: '3.4.1' + }, + { + componentName: 'TinySearch', + package: '@opentiny/vue', + exportName: 'Search', + destructuring: true, + version: '0.1.13' + }, + { + componentName: 'TinyRow', + package: '@opentiny/vue', + exportName: 'Row', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyFormItem', + package: '@opentiny/vue', + exportName: 'FormItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyAlert', + package: '@opentiny/vue', + exportName: 'Alert', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinyInput', + package: '@opentiny/vue', + exportName: 'Input', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTabs', + package: '@opentiny/vue', + exportName: 'Tabs', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdownMenu', + package: '@opentiny/vue', + exportName: 'DropdownMenu', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDialogBox', + package: '@opentiny/vue', + exportName: 'DialogBox', + destructuring: true, + version: '3.2.0' + }, + { + componentName: 'TinySwitch', + package: '@opentiny/vue', + exportName: 'Switch', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTimeLine', + package: '@opentiny/vue', + exportName: 'TimeLine', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyTabItem', + package: '@opentiny/vue', + exportName: 'TabItem', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyRadio', + package: '@opentiny/vue', + exportName: 'Radio', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyForm', + package: '@opentiny/vue', + exportName: 'Form', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyGrid', + package: '@opentiny/vue', + exportName: 'Grid', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyNumeric', + package: '@opentiny/vue', + exportName: 'Numeric', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCheckboxGroup', + package: '@opentiny/vue', + exportName: 'CheckboxGroup', + destructuring: true, + version: '0.1.17' + }, + { + componentName: 'TinySelect', + package: '@opentiny/vue', + exportName: 'Select', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyButtonGroup', + package: '@opentiny/vue', + exportName: 'ButtonGroup', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyCarousel', + package: '@opentiny/vue', + exportName: 'Carousel', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyPopeditor', + package: '@opentiny/vue', + exportName: 'Popeditor', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDatePicker', + package: '@opentiny/vue', + exportName: 'DatePicker', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'TinyDropdown', + package: '@opentiny/vue', + exportName: 'Dropdown', + destructuring: true, + version: '0.1.20' + }, + { + componentName: 'TinyChartHistogram', + package: '@opentiny/vue', + exportName: 'ChartHistogram', + destructuring: true, + version: '0.1.16' + }, + { + componentName: 'PortalHome', + main: 'common/components/home', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PreviewBlock1', + main: 'preview', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalHeader', + main: 'common', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalBlock', + main: 'portal', + destructuring: false, + version: '1.0.0' + }, + { + componentName: 'PortalPermissionBlock', + main: '', + destructuring: false, + version: '1.0.0' + } + ], + bridge: [], + utils: [ + { + name: 'axios', + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: 'axios', + destructuring: false, + exportName: 'axios' + } + }, + { + name: 'Button', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Button', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'Menu', + type: 'npm', + content: { + type: 'JSFunction', + value: '', + package: '@opentiny/vue', + exportName: 'NavMenu', + destructuring: true + } + }, + { + name: 'Modal ', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Modal ', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'npm', + type: 'function', + content: { + type: 'JSFunction', + value: "''" + } + }, + { + name: 'Pager', + type: 'npm', + content: { + package: '@opentiny/vue', + version: '', + exportName: 'Pager', + subName: '', + destructuring: true, + main: '' + } + }, + { + name: 'test', + type: 'function', + content: { + type: 'JSFunction', + value: "function test() {\r\n return 'test'\r\n}" + } + }, + { + name: 'util', + type: 'function', + content: { + type: 'JSFunction', + value: 'function util () {\r\n console.log(321)\r\n}' + } + } + ], + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'app' + }, + constants: '', + css: '', + version: '', + globalState: [] +} diff --git a/packages/vue-generator/vite.config.js b/packages/vue-generator/vite.config.js index 933f1c6af3..bd698103a8 100644 --- a/packages/vue-generator/vite.config.js +++ b/packages/vue-generator/vite.config.js @@ -15,6 +15,10 @@ import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ + test: { + exclude: ['**/result/**'], + watchExclude: ['**/result/**'] + }, resolve: { alias: { '@': path.resolve(__dirname, './src') From 1762a2250174016f30e91670c2e1d9375d4b4c06 Mon Sep 17 00:00:00 2001 From: chilingling Date: Wed, 20 Mar 2024 04:20:53 -0700 Subject: [PATCH 12/37] feat(toolbar-generate-vue): toolbar-generate-vue use new codegen function --- .../get/app-center/v1/apps/schema/918.json | 2 +- packages/toolbars/generate-vue/package.json | 1 + packages/toolbars/generate-vue/src/Main.vue | 84 ++++- packages/vue-generator/package.json | 1 + .../src/generator/generateApp.js | 30 +- .../src/generator/vue/sfc/genSetupSFC.js | 1 - .../src/plugins/genDataSourcePlugin.js | 2 +- .../src/plugins/genDependenciesPlugin.js | 6 + .../src/plugins/genGlobalState.js | 21 +- .../src/plugins/genI18nPlugin.js | 2 +- packages/vue-generator/src/plugins/index.js | 1 + .../src/templates/vue-template/index.js | 7 + .../vue-template/templateFiles/packageJson.js | 1 + .../testcases/generator/generateApp.test.js | 5 +- .../test/testcases/generator/mockData.js | 356 ++++++++++-------- 15 files changed, 312 insertions(+), 208 deletions(-) diff --git a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json index 28a861d55d..534fbfcd1d 100644 --- a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json +++ b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json @@ -1787,7 +1787,7 @@ }, { "componentName": "TinyPlusFrozenPage", - "package": "@opentiny/vuee", + "package": "@opentiny/vue", "exportName": "FrozenPage", "destructuring": true, "version": "3.4.1" diff --git a/packages/toolbars/generate-vue/package.json b/packages/toolbars/generate-vue/package.json index 070f99be65..23c53ddcbf 100644 --- a/packages/toolbars/generate-vue/package.json +++ b/packages/toolbars/generate-vue/package.json @@ -26,6 +26,7 @@ "dependencies": { "@opentiny/tiny-engine-canvas": "workspace:*", "@opentiny/tiny-engine-controller": "workspace:*", + "@opentiny/tiny-engine-dsl-vue": "workspace:*", "@opentiny/tiny-engine-http": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", "prettier": "2.7.1" diff --git a/packages/toolbars/generate-vue/src/Main.vue b/packages/toolbars/generate-vue/src/Main.vue index 2884e909fd..11bf284676 100644 --- a/packages/toolbars/generate-vue/src/Main.vue +++ b/packages/toolbars/generate-vue/src/Main.vue @@ -23,10 +23,18 @@ - \ No newline at end of file + diff --git a/packages/vue-generator/test/testcases/sfc/case01/case01.test.js b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js new file mode 100644 index 0000000000..a8a7aadcc5 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js @@ -0,0 +1,30 @@ +import fs from 'fs' +import path from 'path' +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import schema from './schema.json' +import blockSchema from './blocks.schema.json' +import componentsMap from './componentsMap.json' +import { formatCode } from '@/utils/formatCode' + +// import expectedRes from './expected.vue?raw' + +// console.log('case01', typeof expectedRes) + +test('should validate tagName', async () => { + const res = genSFCWithDefaultPlugin(schema, componentsMap) + const formattedCode = formatCode(res, 'vue') + + fs.writeFileSync(path.resolve(__dirname, 'res.vue'), formattedCode) + await expect(formattedCode).toMatchFileSnapshot('./expected.vue') + // expect(res).toBe('') +}) + +test('should generate block component correct', async () => { + const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + const formattedCode = formatCode(res, 'vue') + fs.writeFileSync(path.resolve(__dirname, 'ImageTitleRes.vue'), formattedCode) + await expect(formattedCode).toMatchFileSnapshot('./ImageTitle.vue') + + // expect(res).toBe('') +}) diff --git a/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js b/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js deleted file mode 100644 index 778a8dd50d..0000000000 --- a/packages/vue-generator/test/testcases/sfc/case01/cast01.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { expect, test } from 'vitest' -import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' -import schema from './schema.json' -import blockSchema from './blocks.schema.json' -import componentsMap from './componentsMap.json' -import expectedRes from './expected.vue?raw' - -console.log('case01', typeof expectedRes) - -test('should validate tagName', () => { - const res = genSFCWithDefaultPlugin(schema, componentsMap) - - fs.writeFileSync(path.resolve(__dirname, 'res.vue'), res) - expect(res).toBe('') -}) - -test('should generate block component correct', () => { - const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) - - fs.writeFileSync(path.resolve(__dirname, 'ImageTitleRes.vue'), res) - expect(res).toBe('') -}) diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected.vue b/packages/vue-generator/test/testcases/sfc/case01/expected.vue index 04a4e7c925..5d042760c7 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/expected.vue +++ b/packages/vue-generator/test/testcases/sfc/case01/expected.vue @@ -3,9 +3,9 @@ 标题区 副标题区 @@ -79,22 +79,19 @@ import { Select as TinySelect, Switch as TinySwitch } from '@opentiny/vue' -import { iconDel, iconEdit, iconHelpCircle, iconSearch } from '@opentiny/vue-icon' +import { IconSearch, IconDel, iconHelpCircle, IconEdit } from '@opentiny/vue-icon' import ImageTitle from '../components/ImageTitle.vue' import CrmQuoteListGridStatus from './crm/quote-list/CrmQuoteListGridStatus.vue' +const TinyIconSearch = IconSearch() +const TinyIconDel = IconDel() +const TinyIconHelpCircle = iconHelpCircle() +const TinyIconEdit = IconEdit() const props = defineProps({}) const emit = defineEmits([]) const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode() -const wrap = lowcodeWrap(props, { emit }, t) - -const [TinyIconSearch, TinyIconDel, TinyIconEdit, TinyIconHelpCircle] = [ - iconSearch(), - iconDel(), - iconEdit(), - iconHelpCircle() -] +const wrap = lowcodeWrap(props, { emit }) const { utils } = wrap(function () { return this diff --git a/packages/vue-generator/test/testcases/sfc/case01/res.vue b/packages/vue-generator/test/testcases/sfc/case01/res.vue index 72915caa2c..a4d6adc798 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/res.vue +++ b/packages/vue-generator/test/testcases/sfc/case01/res.vue @@ -1,104 +1,268 @@ + +
+ - \ No newline at end of file +} + From 81dc66717bdf1f97d0dc385f94aa2a5f600253ba Mon Sep 17 00:00:00 2001 From: chilingling Date: Sun, 7 Apr 2024 18:35:16 -0700 Subject: [PATCH 19/37] fix(vue-generator): add unit test --- .../src/generator/vue/sfc/genSetupSFC.js | 4 +- .../generator/vue/sfc/generateAttribute.js | 14 +- .../src/generator/vue/sfc/generateTemplate.js | 65 ++- .../sfc/case01/{res.vue => FormTable.vue} | 32 +- .../test/testcases/sfc/case01/ImageTitle.vue | 10 +- .../testcases/sfc/case01/ImageTitleRes.vue | 70 --- .../test/testcases/sfc/case01/case01.test.js | 33 +- .../testcases/sfc/case01/componentsMap.json | 42 ++ .../test/testcases/sfc/case01/expected.vue | 469 ------------------ .../test/testcases/sfc/case01/schema.json | 5 +- 10 files changed, 154 insertions(+), 590 deletions(-) rename packages/vue-generator/test/testcases/sfc/case01/{res.vue => FormTable.vue} (94%) delete mode 100644 packages/vue-generator/test/testcases/sfc/case01/ImageTitleRes.vue delete mode 100644 packages/vue-generator/test/testcases/sfc/case01/expected.vue diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js index 6f4d97d78d..7818df3e32 100644 --- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js +++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js @@ -195,10 +195,10 @@ const generateSFCFile = (schema, componentsMap, config = {}) => { } // 解析 template - const templateStr = genTemplateByHook(schema, globalHooks, parsedConfig) + const templateStr = genTemplateByHook(schema, globalHooks, { ...parsedConfig, componentsMap }) // 生成 script - const scriptStr = genScriptByHook(schema, globalHooks, parsedConfig) + const scriptStr = genScriptByHook(schema, globalHooks, { ...parsedConfig, componentsMap }) // 生成 style const styleStr = generateStyleTag(schema, styleConfig) diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index ac2b5e1803..da4932ab9e 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -20,6 +20,7 @@ import { isSetter } from '@/utils' import { recursiveGenTemplateByHook } from './generateTemplate' +import { getImportMap } from './parseImport' const handleEventBinding = (key, item, isJSX) => { const eventKey = toEventKey(key) @@ -339,7 +340,7 @@ const specialTypeHandler = { if (SPECIAL_UTILS_TYPE.includes(resourceType)) { globalHooks.addStatement({ - position: INSERT_POSITION.AFTER_PROPS, + position: INSERT_POSITION.BEFORE_STATE, value: `const { ${resourceType} } = wrap(function() { return this })()`, key: resourceType }) @@ -357,11 +358,18 @@ const specialTypeHandler = { schema: { children: value } } + const { pkgMap = {}, blockPkgMap = {} } = getImportMap(structData.schema, config.componentsMap, config) + + Object.entries({ ...pkgMap, ...blockPkgMap }).forEach(([key, value]) => { + value.forEach((valueItem) => { + globalHooks.addImport(key, valueItem) + }) + }) + // TODO: 需要验证 template 的生成有无问题 recursiveGenTemplateByHook(structData, globalHooks, { ...config, isJSX: true }) // TODO: 这里不通用,需要设计通用的做法,或者独立成 grid 的 hook - return { value: `({${params.join(',')}}, h) => ${structData.children.join('')}` } @@ -386,7 +394,7 @@ export const transformObjType = (obj, globalHooks, config) => { continue } - if (typeof value !== 'object') { + if (typeof value !== 'object' || value === null) { resStr.push(`${renderKey}${value}`) continue diff --git a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js index 76c4f2346c..f47371b7a3 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateTemplate.js @@ -59,7 +59,40 @@ export const handleTinyIcon = (nameObj, globalHooks) => { delete nameObj.schema.props.name } -export const handleTinyGrid = (schemaData, globalHooks) => { +const handleTinyGridSlots = (value, globalHooks, config) => { + if (!Array.isArray(value)) { + return + } + + value.forEach((slotItem) => { + const name = slotItem.componentName + + if (!name) { + return + } + + if (slotItem.componentType === 'Block') { + const importPath = `${config.blockRelativePath}${name}${config.blockSuffix}` + + globalHooks.addImport(importPath, { + exportName: name, + componentName: name, + package: importPath + }) + } else if (name?.startsWith?.('Tiny')) { + globalHooks.addImport('@opentiny/vue', { + destructuring: true, + exportName: name.slice(4), + componentName: name, + package: '@opentiny/vue' + }) + } + + handleTinyGridSlots(slotItem.children, globalHooks, config) + }) +} + +export const handleTinyGrid = (schemaData, globalHooks, config) => { const { componentName, props } = schemaData.schema // 同时存在 data 和 fetchData 的时候,删除 data @@ -74,22 +107,24 @@ export const handleTinyGrid = (schemaData, globalHooks) => { // 处理 TinyGrid 组件 editor 插槽组件使用 opentiny/vue 组件的场景,需要在 import 中添加对应Tiny组件的引入 props.columns.forEach((item) => { - if (!item.editor?.component?.startsWith?.('Tiny')) { - return + if (item.editor?.component?.startsWith?.('Tiny')) { + const name = item.editor?.component + + globalHooks.addImport('@opentiny/vue', { + destructuring: true, + exportName: name.slice(4), + componentName: name, + package: '@opentiny/vue' + }) + + item.editor.component = { + type: 'JSExpression', + value: name + } } - const name = item.editor?.component - - globalHooks.addImport('@opentiny/vue', { - destructuring: true, - exportName: name.slice(4), - componentName: name, - package: '@opentiny/vue' - }) - - item.editor.component = { - type: 'JSExpression', - value: name + if (typeof item.slots === 'object') { + Object.values(item.slots).forEach((slotItem) => handleTinyGridSlots(slotItem?.value, globalHooks, config)) } }) } diff --git a/packages/vue-generator/test/testcases/sfc/case01/res.vue b/packages/vue-generator/test/testcases/sfc/case01/FormTable.vue similarity index 94% rename from packages/vue-generator/test/testcases/sfc/case01/res.vue rename to packages/vue-generator/test/testcases/sfc/case01/FormTable.vue index a4d6adc798..e262db8ce5 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/res.vue +++ b/packages/vue-generator/test/testcases/sfc/case01/FormTable.vue @@ -38,7 +38,7 @@
- +
循环渲染:
@@ -65,13 +65,22 @@ - diff --git a/packages/vue-generator/test/testcases/sfc/case01/case01.test.js b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js index a8a7aadcc5..5ae9320663 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/case01.test.js +++ b/packages/vue-generator/test/testcases/sfc/case01/case01.test.js @@ -1,30 +1,41 @@ -import fs from 'fs' -import path from 'path' -import { expect, test } from 'vitest' +import { expect, test, beforeEach, afterEach, vi } from 'vitest' import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' import schema from './schema.json' import blockSchema from './blocks.schema.json' import componentsMap from './componentsMap.json' import { formatCode } from '@/utils/formatCode' -// import expectedRes from './expected.vue?raw' +let count = 0 +const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] -// console.log('case01', typeof expectedRes) +beforeEach(() => { + // 伪随机数,保证每次快照都一致 + vi.spyOn(global.Math, 'random').mockImplementation(() => { + const res = mockValue[count] + + count++ + if (count > 10) { + count = 0 + } + + return res + }) +}) + +afterEach(() => { + vi.spyOn(global.Math, 'random').mockRestore() +}) test('should validate tagName', async () => { const res = genSFCWithDefaultPlugin(schema, componentsMap) const formattedCode = formatCode(res, 'vue') - fs.writeFileSync(path.resolve(__dirname, 'res.vue'), formattedCode) - await expect(formattedCode).toMatchFileSnapshot('./expected.vue') - // expect(res).toBe('') + await expect(formattedCode).toMatchFileSnapshot('./FormTable.vue') }) test('should generate block component correct', async () => { const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) const formattedCode = formatCode(res, 'vue') - fs.writeFileSync(path.resolve(__dirname, 'ImageTitleRes.vue'), formattedCode) - await expect(formattedCode).toMatchFileSnapshot('./ImageTitle.vue') - // expect(res).toBe('') + await expect(formattedCode).toMatchFileSnapshot('./ImageTitle.vue') }) diff --git a/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json index 41c01dd1f9..2f62c77fbf 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json +++ b/packages/vue-generator/test/testcases/sfc/case01/componentsMap.json @@ -6,6 +6,48 @@ "version": "^3.10.0", "destructuring": true }, + { + "componentName": "TinyForm", + "exportName": "Form", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyFormItem", + "exportName": "FormItem", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyGrid", + "exportName": "Grid", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinySelect", + "exportName": "Select", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinySwitch", + "exportName": "Switch", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, { "componentName": "Img", "exportName": "", diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected.vue b/packages/vue-generator/test/testcases/sfc/case01/expected.vue deleted file mode 100644 index 5d042760c7..0000000000 --- a/packages/vue-generator/test/testcases/sfc/case01/expected.vue +++ /dev/null @@ -1,469 +0,0 @@ - - - - - diff --git a/packages/vue-generator/test/testcases/sfc/case01/schema.json b/packages/vue-generator/test/testcases/sfc/case01/schema.json index bda02c6445..a0d384e9de 100644 --- a/packages/vue-generator/test/testcases/sfc/case01/schema.json +++ b/packages/vue-generator/test/testcases/sfc/case01/schema.json @@ -30,6 +30,7 @@ { "componentName": "ImageTitle", "fileName": "ImageTitle", + "componentType": "Block", "props": { "className": { "type": "JSExpression", @@ -495,8 +496,8 @@ } }, { - "componentName": "Block", - "fileName": "CrmQuoteListGridStatus", + "componentName": "CrmQuoteListGridStatus", + "componentType": "Block", "condition": { "type": "JSExpression", "value": "this.props.isEdit" From fa3674ef57852f19846466698dae732fa8f0c112 Mon Sep 17 00:00:00 2001 From: chilingling Date: Sun, 7 Apr 2024 19:55:14 -0700 Subject: [PATCH 20/37] feat(vue-generator): add sfc generator unit test --- .../testcases/sfc/case02/PropAccessor.vue | 32 ++++----- .../testcases/sfc/case02/PropAccessorRes.vue | 50 -------------- .../test/testcases/sfc/case02/Res.vue | 40 ------------ .../testcases/sfc/case02/UsePropAccessor.vue | 24 +++---- .../test/testcases/sfc/case02/case02.test.js | 20 +++--- .../testcases/sfc/case02/components-map.json | 14 ++++ .../testcases/sfc/case03/StateAccessor.vue | 37 +++++------ .../testcases/sfc/case03/StateAccessorRes.vue | 65 +++++++++---------- .../testcases/sfc/case03/UseStateAccessor.vue | 24 +++---- .../sfc/case03/UseStateAccessorRes.vue | 43 +++++------- .../test/testcases/sfc/case03/case03.test.js | 18 ++--- .../testcases/sfc/case03/components-map.json | 7 ++ .../vue-generator/test/testcases/sfc/index.js | 0 13 files changed, 125 insertions(+), 249 deletions(-) delete mode 100644 packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue delete mode 100644 packages/vue-generator/test/testcases/sfc/case02/Res.vue delete mode 100644 packages/vue-generator/test/testcases/sfc/index.js diff --git a/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue index aa41a95753..6a938b7d99 100644 --- a/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue +++ b/packages/vue-generator/test/testcases/sfc/case02/PropAccessor.vue @@ -2,40 +2,33 @@
- - + - - + - - - +
- diff --git a/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue deleted file mode 100644 index f4cdab154f..0000000000 --- a/packages/vue-generator/test/testcases/sfc/case02/PropAccessorRes.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case02/Res.vue b/packages/vue-generator/test/testcases/sfc/case02/Res.vue deleted file mode 100644 index 4fcabf8c55..0000000000 --- a/packages/vue-generator/test/testcases/sfc/case02/Res.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue index 8a2b8b8356..69bfc22bc8 100644 --- a/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue +++ b/packages/vue-generator/test/testcases/sfc/case02/UsePropAccessor.vue @@ -1,34 +1,26 @@ - diff --git a/packages/vue-generator/test/testcases/sfc/case02/case02.test.js b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js index 2fd7366e5e..35cf3b87e4 100644 --- a/packages/vue-generator/test/testcases/sfc/case02/case02.test.js +++ b/packages/vue-generator/test/testcases/sfc/case02/case02.test.js @@ -1,24 +1,22 @@ -import fs from 'fs' -import path from 'path' import { expect, test } from 'vitest' import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' import schema from './page.schema.json' import componentsMap from './components-map.json' -import expectedRes from './UsePropAccessor.vue?raw' import blockSchema from './blocks.schema.json' +import { formatCode } from '@/utils/formatCode' -console.log('case02', typeof expectedRes) - -test('should validate tagName', () => { +test('should generate use prop accessor correctly', async () => { const res = genSFCWithDefaultPlugin(schema, componentsMap) - fs.writeFileSync(path.resolve(__dirname, 'Res.vue'), res) - expect(res).toBe('') + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./UsePropAccessor.vue') }) -test('should generate prop accessor correctly', () => { +test('should generate prop accessor correctly', async () => { const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) - fs.writeFileSync(path.resolve(__dirname, 'PropAccessorRes.vue'), res) - expect(res).toBe('') + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./PropAccessor.vue') }) diff --git a/packages/vue-generator/test/testcases/sfc/case02/components-map.json b/packages/vue-generator/test/testcases/sfc/case02/components-map.json index 21f1077221..631735610b 100644 --- a/packages/vue-generator/test/testcases/sfc/case02/components-map.json +++ b/packages/vue-generator/test/testcases/sfc/case02/components-map.json @@ -7,6 +7,20 @@ "componentName": "PropAccessor", "main": "./components" }, + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + }, { "componentName": "TinyForm", "exportName": "Form", diff --git a/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue index baaeb808c8..1ecafa5fa9 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue +++ b/packages/vue-generator/test/testcases/sfc/case03/StateAccessor.vue @@ -2,42 +2,31 @@
- - + - - - +
- diff --git a/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue index 9090a62c5c..1ecafa5fa9 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue +++ b/packages/vue-generator/test/testcases/sfc/case03/StateAccessorRes.vue @@ -1,48 +1,43 @@ +
+ + + + + + +
+ - \ No newline at end of file + diff --git a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue index e2c6f567b1..82302754ad 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue +++ b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessor.vue @@ -2,34 +2,26 @@
- - - +
- diff --git a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue index ac76db4eaf..82302754ad 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue +++ b/packages/vue-generator/test/testcases/sfc/case03/UseStateAccessorRes.vue @@ -1,40 +1,27 @@ +
+ + + +
+ - \ No newline at end of file + diff --git a/packages/vue-generator/test/testcases/sfc/case03/case03.test.js b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js index d0bfe16bf3..f1cc46aa9d 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/case03.test.js +++ b/packages/vue-generator/test/testcases/sfc/case03/case03.test.js @@ -1,24 +1,20 @@ -import fs from 'fs' -import path from 'path' import { expect, test } from 'vitest' import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' import schema from './page.schema.json' import blockSchema from './blocks.schema.json' import componentsMap from './components-map.json' -// import expectedRes from './expected.vue?raw' +import { formatCode } from '@/utils/formatCode' -// console.log('case01', typeof expectedRes) - -test('should generate useStateAccessorCorrect', () => { +test('should generate useStateAccessor Correct', async () => { const res = genSFCWithDefaultPlugin(schema, componentsMap) + const formattedCode = formatCode(res, 'vue') - fs.writeFileSync(path.resolve(__dirname, 'UseStateAccessorRes.vue'), res) - expect(res).toBe('') + await expect(formattedCode).toMatchFileSnapshot('./UseStateAccessor.vue') }) -test('should generate block state accessor correct', () => { +test('should generate block state accessor correct', async () => { const res = genSFCWithDefaultPlugin(blockSchema, componentsMap) + const formattedCode = formatCode(res, 'vue') - fs.writeFileSync(path.resolve(__dirname, 'StateAccessorRes.vue'), res) - expect(res).toBe('') + await expect(formattedCode).toMatchFileSnapshot('./StateAccessor.vue') }) diff --git a/packages/vue-generator/test/testcases/sfc/case03/components-map.json b/packages/vue-generator/test/testcases/sfc/case03/components-map.json index bb63ef2ace..0512382b53 100644 --- a/packages/vue-generator/test/testcases/sfc/case03/components-map.json +++ b/packages/vue-generator/test/testcases/sfc/case03/components-map.json @@ -27,5 +27,12 @@ "package": "@opentiny/vue", "version": "^3.10.0", "destructuring": true + }, + { + "componentName": "TinyInput", + "exportName": "Input", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true } ] diff --git a/packages/vue-generator/test/testcases/sfc/index.js b/packages/vue-generator/test/testcases/sfc/index.js deleted file mode 100644 index e69de29bb2..0000000000 From d72a0108eb04378c48d9b6d7c607926325dfbd71 Mon Sep 17 00:00:00 2001 From: chilingling Date: Mon, 8 Apr 2024 01:53:52 -0700 Subject: [PATCH 21/37] feat(vue-generator[docs]): add contributing docs --- packages/vue-generator/CONTRIBUTING.md | 45 +++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/vue-generator/CONTRIBUTING.md b/packages/vue-generator/CONTRIBUTING.md index e3712f5af8..ba0adf6da7 100644 --- a/packages/vue-generator/CONTRIBUTING.md +++ b/packages/vue-generator/CONTRIBUTING.md @@ -1,2 +1,45 @@ -# 如何参与TinyEngine 出码能力共建 +# 如何参与 TinyEngine 出码能力共建 +> 你好,很高兴你有兴趣参与 TinyEngine 出码能力的共建,增强出码能力。在参与贡献之前,请阅读以下的贡献指南。 + +## 提交 issue + +请遵循 [issue 提交指引](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.zh-CN.md#%E6%8F%90%E4%BA%A4-issue) + +## 提交 Pull Request + +请遵循 [PR 提交指引](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.zh-CN.md#%E6%8F%90%E4%BA%A4-issue) + +## 出码能力共建 + +1. 基于 develop 分支,创建新分支,如果是提交新 feature,则分支名为 feat/xxx 格式,如果是 bugfix,则分支名为 fix/xxx 格式。 +2. 执行 pnpm install 安装依赖。 +3. 在终端打开 `vue-generator` 目录,`cd packages/vue-generator`。 +4. 在 `vue-generator/src` 目录下新增您的 feature 或者是修复 bug。 +5. 在 `vue-generator/test` 目录下增加测试用例。 +6. 在 `packages/vue-generator` 目录下, 终端执行 `pnpm run test:unit` 确保所有用例通过。 +7. 在 Github 上发起 PR并通知 Opentiny 官方。 + +## 自测试指引 + +### 测试使用的 library + +我们使用 [vitest](https://vitest.dev/),所以需要你同时遵守 vitest 相关的约定。 +比如:测试文件以 `.test.js` 为后缀 + +### 执行单个用例文件 + +假如我们有测试文件 `testCaseName.test.js`,如果我们只想执行该测试文件,则可以: + +```bash +pnpm test:unit testCaseName +``` + +### 使用 vscode debugger 调试能力调试测试用例。 + +1. 新建 vscode JavaScript Debug Terminal(JavaScript 调试终端) +2. 终端打开 vue-generator 目录,`cd packages/vue-generator` +3. 对需要调试的位置打上断点(VSCode 文件行数旁边) +4. 执行 `pnpm test:unit testCaseName` + +### 更多测试指引,可参考 [vitest](https://vitest.dev/) 指引 From 638c8cb1d6d07f219da697b7ce5e138c2b572f69 Mon Sep 17 00:00:00 2001 From: chilingling Date: Tue, 9 Apr 2024 00:15:24 -0700 Subject: [PATCH 22/37] feat(vue-generator): add test coverage script and app generate test case --- packages/vue-generator/.eslintrc.cjs | 4 +- packages/vue-generator/.gitignore | 2 + packages/vue-generator/package.json | 3 + .../src/generator/codeGenerator.js | 11 + .../src/plugins/parseSchemaPlugin.js | 2 +- .../src/utils/parseRequiredBlocks.js | 6 +- .../generator/expected/appdemo01/.gitignore | 13 + .../generator/expected/appdemo01/README.md | 19 + .../generator/expected/appdemo01/index.html | 13 + .../generator/expected/appdemo01/package.json | 27 + .../generator/expected/appdemo01/src/App.vue | 11 + .../expected/appdemo01/src/http/axios.js | 139 ++++ .../expected/appdemo01/src/http/config.js | 15 + .../expected/appdemo01/src/http/index.js | 27 + .../expected/appdemo01/src/i18n/en_US.json | 25 + .../expected/appdemo01/src/i18n/index.js | 9 + .../expected/appdemo01/src/i18n/locale.js | 4 + .../expected/appdemo01/src/i18n/zh_CN.json | 26 + .../appdemo01/src/lowcodeConfig/bridge.js | 13 + .../appdemo01/src/lowcodeConfig/dataSource.js | 102 +++ .../src/lowcodeConfig/dataSource.json | 632 ++++++++++++++++++ .../appdemo01/src/lowcodeConfig/lowcode.js | 86 +++ .../appdemo01/src/lowcodeConfig/store.js | 13 + .../generator/expected/appdemo01/src/main.js | 20 + .../expected/appdemo01/src/router/index.js | 11 + .../expected/appdemo01/src/stores/index.js | 0 .../generator/expected/appdemo01/src/utils.js | 13 + .../expected/appdemo01/src/views/DemoPage.vue | 25 + .../expected/appdemo01/src/views/createVm.vue | 408 +++++++++++ .../expected/appdemo01/vite.config.js | 23 + .../testcases/generator/generateApp.test.js | 20 +- .../test/unit/parseRequiredBlocks.test.js | 46 ++ .../template/test/generateAttribute.test.js | 20 - .../vue-generator/test/utils/logDiffResult.js | 32 + .../vue-generator/test/utils/logger/index.js | 20 +- pnpm-workspace.yaml | 2 + 36 files changed, 1808 insertions(+), 34 deletions(-) create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/stores/index.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue create mode 100644 packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js create mode 100644 packages/vue-generator/test/unit/parseRequiredBlocks.test.js delete mode 100644 packages/vue-generator/test/unit/template/test/generateAttribute.test.js create mode 100644 packages/vue-generator/test/utils/logDiffResult.js diff --git a/packages/vue-generator/.eslintrc.cjs b/packages/vue-generator/.eslintrc.cjs index 2837ea9cc0..951f20b0eb 100644 --- a/packages/vue-generator/.eslintrc.cjs +++ b/packages/vue-generator/.eslintrc.cjs @@ -15,5 +15,7 @@ module.exports = { ecmaFeatures: { jsx: true } - } + }, + // 忽略 expected 中的内容 + ignorePatterns: ['**/**/expected/*'] } diff --git a/packages/vue-generator/.gitignore b/packages/vue-generator/.gitignore index 955710439d..d500b8dfd5 100644 --- a/packages/vue-generator/.gitignore +++ b/packages/vue-generator/.gitignore @@ -1 +1,3 @@ test/**/result/* + +coverage \ No newline at end of file diff --git a/packages/vue-generator/package.json b/packages/vue-generator/package.json index 20956afbd2..5bd725a722 100644 --- a/packages/vue-generator/package.json +++ b/packages/vue-generator/package.json @@ -14,6 +14,7 @@ "test": "npx nyc@latest --reporter=lcov node test/test_generator.js", "test:latest": "npm run build && node test/testcases/full/index.js", "test:unit": "vitest", + "coverage": "vitest run --coverage", "publish:npm": "npm publish --verbose" }, "repository": { @@ -37,7 +38,9 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.1.1", + "@vitest/coverage-v8": "^1.4.0", "@vue/eslint-config-prettier": "^7.0.0", + "dir-compare": "^4.2.0", "eslint": "^8.12.0", "eslint-plugin-vue": "^8.6.0", "fs-extra": "^10.0.1", diff --git a/packages/vue-generator/src/generator/codeGenerator.js b/packages/vue-generator/src/generator/codeGenerator.js index cf02c5638b..b2cd8c5a66 100644 --- a/packages/vue-generator/src/generator/codeGenerator.js +++ b/packages/vue-generator/src/generator/codeGenerator.js @@ -151,6 +151,17 @@ class CodeGenerator { return true } + deleteFile(file) { + const { path, fileName } = file + const index = this.genResult.findIndex((item) => item.path === path && item.fileName === fileName) + + if (index !== -1) { + this.genResult.splice(index, 1) + return true + } + + return false + } replaceFile(resultItem) { const { path, fileName } = resultItem diff --git a/packages/vue-generator/src/plugins/parseSchemaPlugin.js b/packages/vue-generator/src/plugins/parseSchemaPlugin.js index 5d56fdaa7b..022e469b8c 100644 --- a/packages/vue-generator/src/plugins/parseSchemaPlugin.js +++ b/packages/vue-generator/src/plugins/parseSchemaPlugin.js @@ -8,7 +8,7 @@ function parseSchema() { * @param {import('../generator/generateApp').AppSchema} schema * @returns */ - parse(schema) { + run(schema) { const { pageSchema } = schema const pagesMap = {} const resPageTree = [] diff --git a/packages/vue-generator/src/utils/parseRequiredBlocks.js b/packages/vue-generator/src/utils/parseRequiredBlocks.js index 17d175dddb..5819a35360 100644 --- a/packages/vue-generator/src/utils/parseRequiredBlocks.js +++ b/packages/vue-generator/src/utils/parseRequiredBlocks.js @@ -1,7 +1,11 @@ export const parseRequiredBlocks = (schema) => { const res = [] - for (const item of schema?.children || []) { + if (!Array.isArray(schema?.children)) { + return res + } + + for (const item of schema.children) { if (item.componentType === 'Block') { res.push(item.componentName) } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore b/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore new file mode 100644 index 0000000000..9961aac9d4 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/.gitignore @@ -0,0 +1,13 @@ +node_modules +dist/ + +# local env files +.env.local +.env.*.local + +# Editor directories and files +.vscode +.idea + +yarn.lock +package-lock.json \ No newline at end of file diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md b/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md new file mode 100644 index 0000000000..275a5e79cd --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/README.md @@ -0,0 +1,19 @@ +## portal-app + +本工程是使用 TinyEngine 低代码引擎搭建之后得到的出码工程。 + +## 使用 + +安装依赖: + +```bash +npm install +``` + +本地启动项目: + +```bash +npm run dev +``` + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html b/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html new file mode 100644 index 0000000000..2f10f362d1 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/index.html @@ -0,0 +1,13 @@ + + + + + + + portal-app + + +
+ + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json new file mode 100644 index 0000000000..c41ca77f78 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/package.json @@ -0,0 +1,27 @@ +{ + "name": "portal-app", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "main": "dist/index.js", + "module": "dist/index.js", + "dependencies": { + "@opentiny/tiny-engine-i18n-host": "^1.0.0", + "@opentiny/vue": "latest", + "@opentiny/vue-icon": "latest", + "axios": "latest", + "axios-mock-adapter": "^1.19.0", + "vue": "^3.3.9", + "vue-i18n": "^9.2.0-beta.3", + "vue-router": "^4.2.5", + "pinia": "^2.1.7" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.1", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "vite": "^4.3.7" + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue new file mode 100644 index 0000000000..72b6032dea --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js new file mode 100644 index 0000000000..4b2d6e4208 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/axios.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +export default (config) => { + const instance = axios.create(config) + const defaults = {} + let mock + + if (typeof MockAdapter.prototype.proxy === 'undefined') { + MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) { + let stream = this + const request = (proxy, any) => { + return (setting) => { + return new Promise((resolve) => { + config.responseType = 'json' + axios + .get(any ? proxy + setting.url + '.json' : proxy, config) + .then(({ data }) => { + /* eslint-disable no-useless-call */ + typeof handleData === 'function' && (data = handleData.call(null, data, setting)) + resolve([200, data]) + }) + .catch((error) => { + resolve([error.response.status, error.response.data]) + }) + }) + } + } + + if (url === '*' && proxy && typeof proxy === 'string') { + stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true)) + } else { + if (proxy && typeof proxy === 'string') { + stream = this.onAny(url).reply(request(proxy)) + } else if (typeof response === 'function') { + stream = this.onAny(url).reply(response) + } + } + + return stream + } + } + + return { + request(config) { + return instance(config) + }, + get(url, config) { + return instance.get(url, config) + }, + delete(url, config) { + return instance.delete(url, config) + }, + head(url, config) { + return instance.head(url, config) + }, + post(url, data, config) { + return instance.post(url, data, config) + }, + put(url, data, config) { + return instance.put(url, data, config) + }, + patch(url, data, config) { + return instance.patch(url, data, config) + }, + all(iterable) { + return axios.all(iterable) + }, + spread(callback) { + return axios.spread(callback) + }, + defaults(key, value) { + if (key && typeof key === 'string') { + if (typeof value === 'undefined') { + return instance.defaults[key] + } + instance.defaults[key] = value + defaults[key] = value + } else { + return instance.defaults + } + }, + defaultSettings() { + return defaults + }, + interceptors: { + request: { + use(fnHandle, fnError) { + return instance.interceptors.request.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.request.eject(id) + } + }, + response: { + use(fnHandle, fnError) { + return instance.interceptors.response.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.response.eject(id) + } + } + }, + mock(config) { + if (!mock) { + mock = new MockAdapter(instance) + } + + if (Array.isArray(config)) { + config.forEach((item) => { + mock.proxy(item) + }) + } + + return mock + }, + disableMock() { + mock && mock.restore() + mock = undefined + }, + isMock() { + return typeof mock !== 'undefined' + }, + CancelToken: axios.CancelToken, + isCancel: axios.isCancel + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js new file mode 100644 index 0000000000..cfa3714e17 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/config.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export default { + withCredentials: false +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js new file mode 100644 index 0000000000..b0a08546a6 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/http/index.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from './axios' +import config from './config' + +export default (dataHandler) => { + const http = axios(config) + + http.interceptors.response.use(dataHandler, (error) => { + const response = error.response + if (response.status === 403 && response.headers && response.headers['x-login-url']) { + // TODO 处理无权限时,重新登录再发送请求 + } + }) + + return http +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json new file mode 100644 index 0000000000..be5c684e5e --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/en_US.json @@ -0,0 +1,25 @@ +{ + "lowcode.c257d5e8": "search", + "lowcode.61c8ac8c": "dsdsa", + "lowcode.f53187a0": "test", + "lowcode.97ad00dd": "createMaterial", + "lowcode.61dcef52": "sadasda", + "lowcode.45f4c42a": "gfdgfd", + "lowcode.c6f5a652": "fsdafds", + "lowcode.34923432": "fdsafds", + "lowcode.6534943e": "fdsafdsa", + "lowcode.44252642": "aaaa", + "lowcode.2a743651": "fdsaf", + "lowcode.24315357": "fsdafds", + "lowcode.44621691": "sd", + "lowcode.65636226": "fdsfsd", + "lowcode.6426a4e2": "fdsafsd", + "lowcode.e41c6636": "aa", + "lowcode.51c23164": "aa", + "lowcode.17245b46": "aa", + "lowcode.4573143c": "a", + "lowcode.56432442": "aa", + "lowcode.33566643": "aa", + "lowcode.565128f3": "aa", + "lowcode.56643835": "aa" +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js new file mode 100644 index 0000000000..f6c510b279 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/index.js @@ -0,0 +1,9 @@ +import i18n from '@opentiny/tiny-engine-i18n-host' +import lowcode from '../lowcodeConfig/lowcode' +import locale from './locale.js' + +i18n.lowcode = lowcode +i18n.global.mergeLocaleMessage('en_US', locale.en_US) +i18n.global.mergeLocaleMessage('zh_CN', locale.zh_CN) + +export default i18n diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js new file mode 100644 index 0000000000..75308fc752 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/locale.js @@ -0,0 +1,4 @@ +import en_US from './en_US.json' +import zh_CN from './zh_CN.json' + +export default { en_US, zh_CN } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json new file mode 100644 index 0000000000..59357fdfcc --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/i18n/zh_CN.json @@ -0,0 +1,26 @@ +{ + "lowcode.c257d5e8": "查询", + "lowcode.61c8ac8c": "地方", + "lowcode.f53187a0": "测试", + "lowcode.97ad00dd": "创建物料资产包", + "lowcode.61dcef52": "terterere", + "lowcode.45f4c42a": "gdfgdf", + "lowcode.c6f5a652": "fsdaf", + "lowcode.34923432": "fdsafdsa", + "lowcode.48521e45": "fdsfds", + "lowcode.6534943e": "fdsafds", + "lowcode.44252642": "fdsafds", + "lowcode.2a743651": "sda", + "lowcode.24315357": "fdsafds", + "lowcode.44621691": "fdsafsd", + "lowcode.65636226": "fdsaf", + "lowcode.6426a4e2": "sd", + "lowcode.e41c6636": "aa", + "lowcode.51c23164": "aa", + "lowcode.17245b46": "aa", + "lowcode.4573143c": "aa", + "lowcode.56432442": "aa", + "lowcode.33566643": "aa", + "lowcode.565128f3": "aa", + "lowcode.56643835": "aa" +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js new file mode 100644 index 0000000000..7a19e4a116 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/bridge.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export default () => {} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js new file mode 100644 index 0000000000..14d13a17e7 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import useHttp from '../http' +import dataSources from './dataSource.json' + +const dataSourceMap = {} + +// 暂时使用 eval 解析 JSON 数据里的函数 +const createFn = (fnContent) => { + return (...args) => { + // eslint-disable-next-line no-eval + window.eval('var fn = ' + fnContent) + // eslint-disable-next-line no-undef + return fn.apply(this, args) + } +} + +const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res) => res + +const load = (http, options, dataSource, shouldFetch) => (params, customUrl) => { + // 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理 + if (!options) { + return globalDataHandle(dataSource.config.data) + } + + if (!shouldFetch()) { + return + } + + dataSource.status = 'loading' + + const { method, uri: url, params: defaultParams, timeout, headers } = options + const config = { method, url, headers, timeout } + + const data = params || defaultParams + + config.url = customUrl || config.url + + if (method.toLowerCase() === 'get') { + config.params = data + } else { + config.data = data + } + + return http.request(config) +} + +dataSources.list.forEach((config) => { + const http = useHttp(globalDataHandle) + const dataSource = { config } + + dataSourceMap[config.id] = dataSource + + const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true + const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (options) => options + + const dataHandler = (res) => { + const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res + dataSource.status = 'loaded' + dataSource.data = data + return data + } + + const errorHandler = (error) => { + config.errorHandler?.value && createFn(config.errorHandler.value)(error) + dataSource.status = 'error' + dataSource.error = error + } + + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + + if (import.meta.env.VITE_APP_MOCK === 'mock') { + http.mock([ + { + url: config.options?.uri, + response() { + return Promise.resolve([200, { data: config.data }]) + } + }, + { + url: '*', + proxy: '*' + } + ]) + } + + dataSource.status = 'init' + dataSource.load = load(http, config.options, dataSource, shouldFetch) +}) + +export default dataSourceMap diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json new file mode 100644 index 0000000000..73ff9cb058 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/dataSource.json @@ -0,0 +1,632 @@ +{ + "list": [ + { + "id": 132, + "name": "getAllComponent", + "data": [], + "type": "array" + }, + { + "id": 133, + "name": "getAllList", + "columns": [ + { + "name": "test", + "title": "测试", + "field": "test", + "type": "string", + "format": {} + }, + { + "name": "test1", + "title": "测试1", + "field": "test1", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "test": "test1", + "test1": "test1", + "_id": "341efc48" + }, + { + "test": "test2", + "test1": "test1", + "_id": "b86b516c" + }, + { + "test": "test3", + "test1": "test1", + "_id": "f680cd78" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 135, + "name": "getAllMaterialList", + "columns": [ + { + "name": "id", + "title": "id", + "field": "id", + "type": "string", + "format": {} + }, + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": {} + }, + { + "name": "framework", + "title": "framework", + "field": "framework", + "type": "string", + "format": { + "required": true + } + }, + { + "name": "components", + "title": "components", + "field": "components", + "type": "string", + "format": {} + }, + { + "name": "content", + "title": "content", + "field": "content", + "type": "string", + "format": {} + }, + { + "name": "url", + "title": "url", + "field": "url", + "type": "string", + "format": {} + }, + { + "name": "published_at", + "title": "published_at", + "field": "published_at", + "type": "string", + "format": {} + }, + { + "name": "created_at", + "title": "created_at", + "field": "created_at", + "type": "string", + "format": {} + }, + { + "name": "updated_at", + "title": "updated_at", + "field": "updated_at", + "type": "string", + "format": {} + }, + { + "name": "published", + "title": "published", + "field": "published", + "type": "string", + "format": {} + }, + { + "name": "last_build_info", + "title": "last_build_info", + "field": "last_build_info", + "type": "string", + "format": {} + }, + { + "name": "tenant", + "title": "tenant", + "field": "tenant", + "type": "string", + "format": {} + }, + { + "name": "version", + "title": "version", + "field": "version", + "type": "string", + "format": {} + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "2a23e653" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "06b253be" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "c55a41ed" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "f37123ec" + }, + { + "id": "7a63c1a2", + "url": "", + "name": "tiny-vue", + "tenant": "", + "content": "Tiny Vue物料", + "version": "1.0.0", + "framework": "Vue", + "published": "", + "components": "", + "created_at": "", + "updated_at": "", + "description": "Tiny Vue物料", + "published_at": "", + "last_build_info": "", + "_id": "7a63c1a2" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 139, + "name": "treedata", + "data": [ + { + "label": "level111", + "value": "111", + "id": "f6609643", + "pid": "", + "_RID": "row_4" + }, + { + "label": "level1-son", + "value": "111-1", + "id": "af1f937f", + "pid": "f6609643", + "_RID": "row_5" + }, + { + "label": "level222", + "value": "222", + "id": "28e3709c", + "pid": "", + "_RID": "row_6" + }, + { + "label": "level2-son", + "value": "222-1", + "id": "6b571bef", + "pid": "28e3709c", + "_RID": "row_5" + }, + { + "id": "6317c2cc", + "pid": "fdfa", + "label": "fsdfaa", + "value": "fsadf", + "_RID": "row_6" + }, + { + "id": "9cce369f", + "pid": "test", + "label": "test1", + "value": "001" + } + ], + "type": "tree" + }, + { + "id": 150, + "name": "componentList", + "data": [ + { + "_RID": "row_1", + "name": "表单", + "isSelected": "true", + "description": "由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据" + }, + { + "name": "按钮", + "isSelected": "false", + "description": "常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型" + }, + { + "id": "490f8a00", + "_RID": "row_3", + "name": "表单项", + "framework": "", + "materials": "", + "description": "Form 组件下的 FormItem 配置" + }, + { + "id": "c259b8b3", + "_RID": "row_4", + "name": "开关", + "framework": "", + "materials": "", + "description": "关闭或打开" + }, + { + "id": "083ed9c7", + "_RID": "row_5", + "name": "互斥按钮组", + "framework": "", + "materials": "", + "description": "以按钮组的方式出现,常用于多项类似操作" + }, + { + "id": "09136cea", + "_RID": "row_6", + "name": "提示框", + "framework": "", + "materials": "", + "description": "Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画" + }, + { + "id": "a63b57d5", + "_RID": "row_7", + "name": "文字提示框", + "framework": "", + "materials": "", + "description": "动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信" + }, + { + "id": "a0f6e8a3", + "_RID": "row_8", + "name": "树", + "framework": "", + "materials": "", + "description": "可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单" + }, + { + "id": "d1aa18fc", + "_RID": "row_9", + "name": "分页", + "framework": "", + "materials": "", + "description": "当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件" + }, + { + "id": "ca49cc52", + "_RID": "row_10", + "name": "表格", + "framework": "", + "materials": "", + "description": "提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等" + }, + { + "id": "4e20ecc9", + "name": "搜索框", + "framework": "", + "materials": "", + "description": "指定条件对象进行搜索数据" + }, + { + "id": "6b093ee5", + "name": "折叠面板", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "0a09abc0", + "name": "对话框", + "framework": "", + "materials": "", + "description": "模态对话框,在浮层中显示,引导用户进行相关操作" + }, + { + "id": "f814b901", + "name": "标签页签项", + "framework": "", + "materials": "", + "description": "tab页签" + }, + { + "id": "c5ae797c", + "name": "单选", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,在一组备选项中进行单选" + }, + { + "id": "33d0c590", + "_RID": "row_13", + "name": "弹出编辑", + "framework": "", + "materials": "", + "description": "该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件" + }, + { + "id": "16711dfa", + "_RID": "row_14", + "name": "下拉框", + "framework": "", + "materials": "", + "description": "Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件" + }, + { + "id": "a9fd190a", + "_RID": "row_15", + "name": "折叠面板项", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "a7dfa9ec", + "_RID": "row_16", + "name": "复选框", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,提供用户可在一组选项中进行多选" + }, + { + "id": "d4bb8330", + "name": "输入框", + "framework": "", + "materials": "", + "description": "通过鼠标或键盘输入字符" + }, + { + "id": "ced3dc83", + "name": "时间线", + "framework": "", + "materials": "", + "description": "时间线" + } + ], + "type": "array", + "columns": [ + { + "name": "name", + "type": "string", + "field": "name", + "title": "name", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "description", + "type": "string", + "field": "description", + "title": "description", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "isSelected", + "type": "string", + "field": "isSelected", + "title": "isSelected", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + } + ], + "options": { + "uri": "http://localhost:9090/assets/json/bundle.json", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + { + "id": 151, + "name": "selectedComponents", + "columns": [ + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "isSelected", + "title": "isSelected", + "field": "isSelected", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + } + ], + "type": "array", + "data": [ + { + "name": "标签页", + "description": "分隔内容上有关联但属于不同类别的数据集合", + "isSelected": "true", + "_RID": "row_2" + }, + { + "name": "布局列", + "description": "列配置信息", + "isSelected": "true", + "id": "76a7080a", + "_RID": "row_4" + }, + { + "name": "日期选择器", + "description": "用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式", + "isSelected": "true", + "id": "76b20d73", + "_RID": "row_1" + }, + { + "name": "走马灯", + "description": "常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现", + "isSelected": "true", + "id": "4c884c3d" + } + ] + } + ], + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js new file mode 100644 index 0000000000..29da8186b5 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/lowcode.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { getCurrentInstance, nextTick, provide, inject } from 'vue' +import { useRouter, useRoute } from 'vue-router' +import { I18nInjectionKey } from 'vue-i18n' +import dataSourceMap from './dataSource' +import * as utils from '../utils' +import * as bridge from './bridge' +import { useStores } from './store' + +export const lowcodeWrap = (props, context) => { + const global = {} + const instance = getCurrentInstance() + const router = useRouter() + const route = useRoute() + const { t, locale } = inject(I18nInjectionKey).global + const emit = context.emit + const ref = (ref) => instance.refs[ref] + + const setState = (newState, callback) => { + Object.assign(global.state, newState) + nextTick(() => callback.apply(global)) + } + + const getLocale = () => locale.value + const setLocale = (val) => { + locale.value = val + } + + const location = () => window.location + const history = () => window.history + + Object.defineProperties(global, { + props: { get: () => props }, + emit: { get: () => emit }, + setState: { get: () => setState }, + router: { get: () => router }, + route: { get: () => route }, + i18n: { get: () => t }, + getLocale: { get: () => getLocale }, + setLocale: { get: () => setLocale }, + location: { get: location }, + history: { get: history }, + utils: { get: () => utils }, + bridge: { get: () => bridge }, + dataSourceMap: { get: () => dataSourceMap }, + $: { get: () => ref } + }) + + const wrap = (fn) => { + if (typeof fn === 'function') { + return (...args) => fn.apply(global, args) + } + + Object.entries(fn).forEach(([name, value]) => { + Object.defineProperty(global, name, { + get: () => value + }) + }) + + fn.t = t + + return fn + } + + return wrap +} + +export default () => { + const i18n = inject(I18nInjectionKey) + provide(I18nInjectionKey, i18n) + + const stores = useStores() + + return { t: i18n.global.t, lowcodeWrap, stores } +} diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js new file mode 100644 index 0000000000..f7f39c7a84 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/lowcodeConfig/store.js @@ -0,0 +1,13 @@ +import * as useDefinedStores from '@/stores' + +const useStores = () => { + const stores = {} + + Object.values({ ...useDefinedStores }).forEach((store) => { + stores[store.$id] = store() + }) + + return stores +} + +export { useStores } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js new file mode 100644 index 0000000000..c4574461b3 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/main.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { createApp } from 'vue' +import router from './router' +import { createPinia } from 'pinia' +import App from './App.vue' + +const pinia = createPinia() + +createApp(App).use(pinia).use(router).mount('#app') diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js new file mode 100644 index 0000000000..7cd5ff8e89 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/router/index.js @@ -0,0 +1,11 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +const routes = [ + { path: '/', redirect: '/demopage' }, + { path: '/demopage', component: () => import('@/views/DemoPage.vue') }, + { path: '/createVm', component: () => import('@/views/createVm.vue') } +] + +export default createRouter({ + history: createWebHashHistory(), + routes +}) diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/stores/index.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/stores/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js new file mode 100644 index 0000000000..42009b621d --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/utils.js @@ -0,0 +1,13 @@ +import axios from 'axios' +import { Button } from '@opentiny/vue' +import { NavMenu } from '@opentiny/vue' +import { Modal } from '@opentiny/vue' +import { Pager } from '@opentiny/vue' +const npm = '' +const test = function test() { + return 'test' +} +const util = function util() { + console.log(321) +} +export { axios, Button, NavMenu, Modal, npm, Pager, test, util } diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue new file mode 100644 index 0000000000..a5dc80472e --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/DemoPage.vue @@ -0,0 +1,25 @@ + + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue new file mode 100644 index 0000000000..c3dfa7f81e --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/src/views/createVm.vue @@ -0,0 +1,408 @@ + + + + diff --git a/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js b/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js new file mode 100644 index 0000000000..e1e57978b1 --- /dev/null +++ b/packages/vue-generator/test/testcases/generator/expected/appdemo01/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import path from 'path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + plugins: [vue(), vueJsx()], + define: { + 'process.env': { ...process.env } + }, + build: { + minify: true, + commonjsOptions: { + transformMixedEsModules: true + }, + cssCodeSplit: false + } +}) diff --git a/packages/vue-generator/test/testcases/generator/generateApp.test.js b/packages/vue-generator/test/testcases/generator/generateApp.test.js index ced45d93f2..5966c1701a 100644 --- a/packages/vue-generator/test/testcases/generator/generateApp.test.js +++ b/packages/vue-generator/test/testcases/generator/generateApp.test.js @@ -1,8 +1,10 @@ import { expect, test, describe } from 'vitest' import path from 'path' import fs from 'fs' +import dirCompare from 'dir-compare' import { generateApp } from '@/generator/generateApp' import { appSchemaDemo01 } from './mockData' +import { logDiffResult } from '../../utils/logDiffResult' describe('generate whole application', () => { test('should not throw error', async () => { @@ -11,11 +13,27 @@ describe('generate whole application', () => { const res = await instance.generate(appSchemaDemo01) const { genResult } = res + // 写入文件 genResult.forEach(({ fileName, path: filePath, fileContent }) => { fs.mkdirSync(path.resolve(__dirname, `./result/appdemo01/${filePath}`), { recursive: true }) fs.writeFileSync(path.resolve(__dirname, `./result/appdemo01/${filePath}/${fileName}`), fileContent) }) - expect(true).toBe(true) + const compareOptions = { + compareContent: true, + ignoreLineEnding: true, + ignoreAllWhiteSpaces: true, + ignoreEmptyLines: true + } + + const path1 = path.resolve(__dirname, './expected/appdemo01') + const path2 = path.resolve(__dirname, './result/appdemo01') + + // 对比文件差异 + const diffResult = dirCompare.compareSync(path1, path2, compareOptions) + + logDiffResult(diffResult) + + expect(diffResult.same).toBe(true) }) }) diff --git a/packages/vue-generator/test/unit/parseRequiredBlocks.test.js b/packages/vue-generator/test/unit/parseRequiredBlocks.test.js new file mode 100644 index 0000000000..ea5e3c258b --- /dev/null +++ b/packages/vue-generator/test/unit/parseRequiredBlocks.test.js @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest' +import { parseRequiredBlocks } from '@/utils/parseRequiredBlocks' + +test('should return empty array when children is no array', () => { + expect(parseRequiredBlocks()).toStrictEqual([]) + expect(parseRequiredBlocks({})).toStrictEqual([]) + expect(parseRequiredBlocks({ children: null })).toStrictEqual([]) +}) + +test('should recursive parse children', () => { + const mockData = { + children: [ + { + componentType: 'Block', + componentName: 'Header', + children: [ + { + componentType: 'Block', + componentName: 'MenuList' + } + ] + }, + { + componentName: 'div', + children: [ + { + componentName: 'div', + children: [ + { + componentName: 'div', + children: [ + { + componentType: 'Block', + componentName: 'Container' + } + ] + } + ] + } + ] + } + ] + } + + expect(parseRequiredBlocks(mockData)).toStrictEqual(['Header', 'MenuList', 'Container']) +}) diff --git a/packages/vue-generator/test/unit/template/test/generateAttribute.test.js b/packages/vue-generator/test/unit/template/test/generateAttribute.test.js deleted file mode 100644 index bee13d9042..0000000000 --- a/packages/vue-generator/test/unit/template/test/generateAttribute.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { expect, test } from 'vitest' -// import { generateAttribute } from '@/generator/vue/template/generateAttribute' - -// 处理 number、boolean 绑定 - -test('should validate tagName', () => { - // expect(generateTag('')).toBe(undefined) -}) - -// 处理 v-if 绑定 -// 处理 v-for 绑定 -// 处理 className、style 绑定 -// 处理 state 绑定 -// 处理 props 绑定 -// 处理 script 中 export 的变量绑定 -// 处理 @xxx 事件绑定 -// 处理 v-model 绑定 -// 处理 i18n 绑定 -// 处理 jsExpression 绑定 -// 处理 slot attribute #data={ xxx } 作用域插槽的场景 diff --git a/packages/vue-generator/test/utils/logDiffResult.js b/packages/vue-generator/test/utils/logDiffResult.js new file mode 100644 index 0000000000..a2fe8ef850 --- /dev/null +++ b/packages/vue-generator/test/utils/logDiffResult.js @@ -0,0 +1,32 @@ +export const logDiffResult = (result) => { + console.log( + 'Statistics - equal entries: %s, distinct entries: %s, left only entries: %s, right only entries: %s, differences: %s', + result.equal, + result.distinct, + result.left, + result.right, + result.differences + ) + + result.diffSet.forEach((dif) => { + // diff 结果相等 + if (dif.state === 'equal') { + return + } + + // diff 结果不相等 + if (dif.state === 'distinct') { + console.log(`Difference on ${dif.relativePath} ${dif.name1} please compare two files detail.`) + return + } + + // diff 结果多余文件 + if (dif.state === 'right') { + console.log(`unexpected extra file ${dif.path} ${dif.name2}.`) + + return + } + // diff 结果缺失文件 + console.log(`expect file: ${dif.path} ${dif.name1}, but result is missing.`) + }) +} diff --git a/packages/vue-generator/test/utils/logger/index.js b/packages/vue-generator/test/utils/logger/index.js index e73441da7a..ebbfbb0f90 100644 --- a/packages/vue-generator/test/utils/logger/index.js +++ b/packages/vue-generator/test/utils/logger/index.js @@ -1,14 +1,14 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ const { createLogger, format, transports } = require('winston') diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1f91739866..78b16e7e25 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ packages: - 'packages/**' - 'mockServer' + # 忽略测试文件夹中的 project + - '!packages/**/test/**' From 8ea749f4b578453d1a6732002f4ee030e895d055 Mon Sep 17 00:00:00 2001 From: chilingling Date: Tue, 16 Apr 2024 23:31:48 -0700 Subject: [PATCH 23/37] fix(generate-vue): optimize desc and file list --- .../generate-vue/src/FileSelector.vue | 25 +++++++++++++++---- packages/toolbars/generate-vue/src/Main.vue | 4 +-- .../src/templates/vue-template/index.js | 2 +- ...vite.config.js => vite.config.js.template} | 0 4 files changed, 23 insertions(+), 8 deletions(-) rename packages/vue-generator/src/templates/vue-template/templateFiles/{vite.config.js => vite.config.js.template} (100%) diff --git a/packages/toolbars/generate-vue/src/FileSelector.vue b/packages/toolbars/generate-vue/src/FileSelector.vue index 2ccc9c5768..94099cb13e 100644 --- a/packages/toolbars/generate-vue/src/FileSelector.vue +++ b/packages/toolbars/generate-vue/src/FileSelector.vue @@ -54,15 +54,30 @@ export default { emits: ['cancel', 'confirm'], setup(props, { emit }) { const getTableTreeData = (data) => { - const dataMap = {} + const res = [] data.forEach((item) => { - if (!dataMap[item.fileType]) { - dataMap[item.fileType] = { fileType: item.fileType, children: [] } + const folder = item.filePath.split('/').slice(0, -1) + + if (!folder.length) { + res.push(item) + return } - dataMap[item.fileType].children.push(item) + + const parentFolder = folder.reduce((parent, curPath) => { + let curItem = parent.find((parItem) => parItem.path === curPath) + + if (!curItem) { + curItem = { path: curPath, filePath: curPath, children: [] } + parent.push(curItem) + } + + return curItem.children + }, res) + + parentFolder.push(item) }) - return Object.values(dataMap) + return res } const tableData = computed(() => getTableTreeData(props.data)) diff --git a/packages/toolbars/generate-vue/src/Main.vue b/packages/toolbars/generate-vue/src/Main.vue index 0b0dcea03c..54d25cf2f1 100644 --- a/packages/toolbars/generate-vue/src/Main.vue +++ b/packages/toolbars/generate-vue/src/Main.vue @@ -4,7 +4,7 @@ :open-delay="1000" popper-class="toolbar-right-popover" append-to-body - content="生成当前页面/区块的Vue代码到本地文件" + content="生成当前应用代码到本地文件" >