├── pnpm-workspace.yaml ├── .eslintignore ├── .npmrc ├── packages ├── data-source │ ├── src │ │ ├── index.ts │ │ ├── handlers │ │ │ ├── index.ts │ │ │ └── fetch.ts │ │ └── core │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ ├── engine.ts │ │ │ └── data-source.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ └── __tests__ │ │ ├── fetch.spec.ts │ │ ├── __snapshots__ │ │ └── fetch.spec.ts.snap │ │ └── data-source-engine.spec.ts ├── hooks │ ├── src │ │ ├── index.ts │ │ ├── current-node.ts │ │ └── renderer-context.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── package.json ├── utils │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── create-defer.ts │ │ ├── schema.ts │ │ ├── setup-environment.ts │ │ ├── string.ts │ │ ├── script.ts │ │ ├── build-utils.ts │ │ ├── misc.ts │ │ ├── check.ts │ │ ├── build-components.ts │ │ └── asset.ts │ ├── vite.config.ts │ └── package.json ├── vue-renderer │ ├── tsconfig.build.json │ ├── __tests__ │ │ ├── helpers │ │ │ ├── index.ts │ │ │ ├── mixed.ts │ │ │ ├── document.ts │ │ │ └── node.ts │ │ ├── vue-router.spec.tsx │ │ └── renderer-hoc.spec.tsx │ ├── tsconfig.json │ ├── src │ │ ├── utils │ │ │ ├── array.ts │ │ │ ├── types.ts │ │ │ ├── warn.ts │ │ │ ├── index.ts │ │ │ ├── i18n.ts │ │ │ ├── parse.ts │ │ │ └── scope.ts │ │ ├── core │ │ │ ├── index.ts │ │ │ ├── lifecycles │ │ │ │ ├── created.ts │ │ │ │ ├── beforeCreate.ts │ │ │ │ ├── init-provide.ts │ │ │ │ ├── init-data.ts │ │ │ │ ├── init-emits.ts │ │ │ │ ├── setup.ts │ │ │ │ ├── init-computed.ts │ │ │ │ ├── init-inject.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-watch.ts │ │ │ │ ├── vue-router.ts │ │ │ │ └── init-props.ts │ │ │ ├── leaf │ │ │ │ ├── live.ts │ │ │ │ └── hoc.ts │ │ │ └── base.ts │ │ ├── renderers │ │ │ ├── index.ts │ │ │ ├── page.ts │ │ │ ├── component.ts │ │ │ └── block.ts │ │ ├── index.ts │ │ ├── config.ts │ │ ├── data-source │ │ │ └── index.ts │ │ └── renderer.ts │ ├── vite.config.umd.ts │ ├── vite.config.ts │ └── package.json └── vue-simulator-renderer │ ├── src │ ├── utils │ │ ├── logger.ts │ │ ├── index.ts │ │ ├── deep-merge.ts │ │ ├── get-client-rects.ts │ │ ├── path.ts │ │ ├── navtive-selection.ts │ │ ├── check-node.ts │ │ ├── cursor.ts │ │ ├── closest-node.ts │ │ ├── comp-node.ts │ │ └── find-dom-nodes.ts │ ├── buildin-components │ │ ├── index.ts │ │ ├── leaf.ts │ │ ├── page.ts │ │ └── slot.ts │ ├── host.ts │ ├── index.ts │ ├── interface.ts │ ├── index.less │ ├── simulator-view.ts │ └── simulator.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── vite.config.umd.ts │ ├── vue.config.js │ └── package.json ├── .husky └── pre-commit ├── .editorconfig ├── .prettierrc ├── .vscode └── settings.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.cjs ├── .stylelintrc.js ├── LICENSE ├── package.json └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | es 2 | lib 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /packages/data-source/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './handlers'; 3 | -------------------------------------------------------------------------------- /packages/data-source/src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export { request as fetchRequest } from './fetch'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /packages/data-source/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface'; 2 | export * from './engine'; 3 | -------------------------------------------------------------------------------- /packages/hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './current-node'; 2 | export * from './renderer-context'; 3 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/data-source/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/vue-renderer/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mixed'; 2 | export * from './document'; 3 | export * from './node'; 4 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export function warn(...messages: string[]): void { 2 | return console.warn('[vue-simulator-renderer]:', ...messages); 3 | } 4 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "types": ["vite/client"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/data-source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src", "./__tests__"], 4 | "compilerOptions": { 5 | "types": ["vitest/globals"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src", "./__tests__"], 4 | "compilerOptions": { 5 | "types": ["vitest/globals"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/buildin-components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Leaf } from './leaf'; 2 | export { default as Slot } from './slot'; 3 | export { default as Page } from './page'; 4 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/host.ts: -------------------------------------------------------------------------------- 1 | import type { BuiltinSimulatorHost } from '@alilc/lowcode-designer'; 2 | 3 | export const host: BuiltinSimulatorHost = (window as any).LCSimulatorHost; 4 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/array.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from '@knxcloud/lowcode-utils'; 2 | 3 | export function ensureArray(val: T | T[] | undefined | null): T[] { 4 | return val ? (isArray(val) ? val : [val]) : []; 5 | } 6 | 7 | export type MaybeArray = T | T[]; 8 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { ExtractDefaultPropTypes, ExtractPropTypes } from 'vue'; 2 | 3 | export type ExtractPublicPropTypes = Omit< 4 | ExtractPropTypes, 5 | keyof ExtractDefaultPropTypes 6 | > & 7 | Partial>; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | 8 | [*.md] 9 | trim_trailing_whitespace = false 10 | 11 | [*.{ts,tsx,vue,less}] 12 | indent_size = 2 13 | 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export { useRenderer, useLeaf, useRootScope } from './use'; 3 | export type { SlotSchemaMap } from './use'; 4 | export { 5 | setupLowCodeRouteGuard, 6 | LOWCODE_ROUTE_META, 7 | type SetupLowCodeRouteGuardOptions, 8 | } from './lifecycles'; 9 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build-utils'; 2 | export * from './build-components'; 3 | export * from './setup-environment'; 4 | export * from './asset'; 5 | export * from './check'; 6 | export * from './create-defer'; 7 | export * from './string'; 8 | export * from './schema'; 9 | export * from './misc'; 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "proseWrap": "preserve", 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "endOfLine": "auto", 7 | "bracketSpacing": true, 8 | "stylelintIntegration": true, 9 | "eslintIntegration": true, 10 | "semi": true, 11 | "vueIndentScriptAndStyle": false, 12 | "printWidth": 90 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/warn.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | export function warn(message: string) { 4 | console.warn('[vue-renderer]: ' + message); 5 | } 6 | 7 | const cached: Record = {}; 8 | export function warnOnce(message: string) { 9 | if (!cached[message] && (cached[message] = true)) { 10 | warn(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.fixAll.stylelint": "explicit" 7 | }, 8 | "css.validate": false, 9 | "less.validate": false, 10 | "scss.validate": false, 11 | "prettier.enable": true 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import type { RendererComponent } from '../core'; 2 | import { PageRenderer } from './page'; 3 | import { BlockRenderer } from './block'; 4 | import { ComponentRenderer } from './component'; 5 | 6 | export const RENDERER_COMPS: Record = { 7 | PageRenderer, 8 | BlockRenderer, 9 | ComponentRenderer, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-client-rects'; 2 | export * from './comp-node'; 3 | export * from './check-node'; 4 | export * from './closest-node'; 5 | export * from './find-dom-nodes'; 6 | export * from './logger'; 7 | export * from './cursor'; 8 | export * from './navtive-selection'; 9 | export * from './deep-merge'; 10 | export * from './path'; 11 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/helpers/mixed.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPublicInstance } from 'vue'; 2 | import { RuntimeScope } from '../../src'; 3 | 4 | export function sleep(ms?: number) { 5 | return new Promise((resolve) => { 6 | setTimeout(resolve, ms); 7 | }); 8 | } 9 | 10 | export function $$(inst: ComponentPublicInstance): RuntimeScope { 11 | return inst['runtimeScope']; 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/created.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@knxcloud/lowcode-utils'; 2 | import { type RuntimeScope, type SchemaParser } from '../../utils'; 3 | 4 | export function created( 5 | parser: SchemaParser, 6 | schema: unknown, 7 | scope: RuntimeScope, 8 | ): void { 9 | const createdFn = parser.parseSchema(schema, false); 10 | isFunction(createdFn) && createdFn.call(scope); 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/beforeCreate.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@knxcloud/lowcode-utils'; 2 | import { type RuntimeScope, type SchemaParser } from '../../utils'; 3 | 4 | export function beforeCreate( 5 | parser: SchemaParser, 6 | schema: unknown, 7 | scope: RuntimeScope, 8 | ): void { 9 | const beforeCreateFn = parser.parseSchema(schema, false); 10 | isFunction(beforeCreateFn) && beforeCreateFn.call(scope); 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/deep-merge.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@knxcloud/lowcode-utils'; 2 | 3 | export function deepMerge(o1: TL, o2: TR): TL & TR { 4 | if (isObject(o1) && isObject(o2)) { 5 | const result = Object.assign({}, o1); 6 | Object.keys(o2).forEach((key) => { 7 | Reflect.set(result, key, deepMerge(o1[key], o2[key])); 8 | }); 9 | return result as TL & TR; 10 | } 11 | return (o2 ?? o1) as TL & TR; 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/src/create-defer.ts: -------------------------------------------------------------------------------- 1 | export interface Defer { 2 | resolve(value?: T | PromiseLike): void; 3 | reject(reason?: any): void; 4 | promise(): Promise; 5 | } 6 | 7 | export function createDefer(): Defer { 8 | const r: any = {}; 9 | const promise = new Promise((resolve, reject) => { 10 | r.resolve = resolve; 11 | r.reject = reject; 12 | }); 13 | 14 | r.promise = () => promise; 15 | 16 | return r; 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .temp 12 | .DS_Store 13 | es 14 | lib 15 | dist 16 | temp 17 | dist-ssr 18 | coverage 19 | 20 | /cypress/videos/ 21 | /cypress/screenshots/ 22 | 23 | # Editor directories and files 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/extensions.json 27 | .idea 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/get-client-rects.ts: -------------------------------------------------------------------------------- 1 | import { isElement } from '@knxcloud/lowcode-utils'; 2 | 3 | // a range for test TextNode clientRect 4 | const cycleRange = document.createRange(); 5 | 6 | export function getClientRects(node: Element | Text) { 7 | if (!node.parentNode) return []; 8 | 9 | if (isElement(node)) { 10 | return [node.getBoundingClientRect()]; 11 | } 12 | 13 | cycleRange.selectNode(node); 14 | return Array.from(cycleRange.getClientRects()); 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | export function parseFileNameToPath(fileName: string): string { 2 | const path = fileName.endsWith('/index.vue') 3 | ? fileName.slice(0, fileName.length - 10) 4 | : fileName.replace(/\.(\w*)$/, ''); 5 | 6 | return '/' + path.replace(/^\//, ''); 7 | } 8 | 9 | export function parseFileNameToCompName(fileName: string): string { 10 | const path = parseFileNameToPath(fileName); 11 | return path.replace(/[/-_][\w]/, (s) => s[1].toUpperCase()); 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { getI18n, type I18nMessages } from './i18n'; 2 | export { 3 | mergeScope, 4 | addToScope, 5 | AccessTypes, 6 | getAccessTarget, 7 | isRuntimeScope, 8 | isValidScope, 9 | type RuntimeScope, 10 | type BlockScope, 11 | } from './scope'; 12 | export { ensureArray, type MaybeArray } from './array'; 13 | export { SchemaParser, type SchemaParserOptions } from './parse'; 14 | export type { ExtractPublicPropTypes } from './types'; 15 | export { warn, warnOnce } from './warn'; 16 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/navtive-selection.ts: -------------------------------------------------------------------------------- 1 | let nativeSelectionEnabled = true; 2 | const preventSelection = (e: Event) => { 3 | if (nativeSelectionEnabled) { 4 | return null; 5 | } 6 | e.preventDefault(); 7 | e.stopPropagation(); 8 | return false; 9 | }; 10 | document.addEventListener('selectstart', preventSelection, true); 11 | document.addEventListener('dragstart', preventSelection, true); 12 | 13 | export function setNativeSelection(enableFlag: boolean) { 14 | nativeSelectionEnabled = enableFlag; 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import types from 'vite-plugin-lib-types'; 3 | 4 | import pkg from './package.json'; 5 | 6 | export default defineConfig({ 7 | plugins: [types()], 8 | build: { 9 | target: 'ES2018', 10 | sourcemap: true, 11 | minify: false, 12 | lib: { 13 | entry: 'src/index.ts', 14 | formats: ['cjs', 'es'], 15 | }, 16 | emptyOutDir: true, 17 | rollupOptions: { 18 | external: Object.keys(pkg.peerDependencies), 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export { default as config, type Config, type RendererModules } from './config'; 3 | export { 4 | VueRenderer as default, 5 | vueRendererProps, 6 | cleanCachedModules as cleanCacledModules, 7 | } from './renderer'; 8 | export type { VueRendererProps, I18nMessages, BlockScope } from './renderer'; 9 | export { mergeScope, SchemaParser } from './utils'; 10 | export type { 11 | RuntimeScope, 12 | SchemaParserOptions, 13 | ExtractPublicPropTypes, 14 | MaybeArray, 15 | } from './utils'; 16 | -------------------------------------------------------------------------------- /packages/hooks/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import types from 'vite-plugin-lib-types'; 3 | 4 | import pkg from './package.json'; 5 | 6 | export default defineConfig({ 7 | plugins: [types()], 8 | build: { 9 | target: 'ES2018', 10 | sourcemap: true, 11 | minify: false, 12 | lib: { 13 | entry: 'src/index.ts', 14 | formats: ['cjs', 'es'], 15 | }, 16 | emptyOutDir: true, 17 | rollupOptions: { 18 | external: [...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies)], 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/buildin-components/leaf.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, renderSlot } from 'vue'; 2 | 3 | const Leaf = defineComponent({ 4 | name: 'Leaf', 5 | render() { 6 | return renderSlot(this.$slots, 'default'); 7 | }, 8 | }); 9 | 10 | Object.assign(Leaf, { 11 | displayName: 'Leaf', 12 | componentMetadata: { 13 | componentName: 'Leaf', 14 | configure: { 15 | props: [ 16 | { 17 | name: 'children', 18 | setter: 'StringSetter', 19 | }, 20 | ], 21 | supports: false, 22 | }, 23 | }, 24 | }); 25 | 26 | export default Leaf; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": ["es2015", "dom"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "useDefineForClassFields": true, 9 | "jsx": "preserve", 10 | "noImplicitThis": true, 11 | "noImplicitAny": false, 12 | "strict": true, 13 | "isolatedModules": true, 14 | "target": "ESNext", 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": ["**/test", "**/lib", "**/es", "**/dist", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/buildin-components/page.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue'; 2 | 3 | const Page = defineComponent((props, { slots }) => { 4 | return () => h('div', { class: 'lc-page', ...props }, slots); 5 | }); 6 | 7 | Object.assign(Page, { 8 | displayName: 'Page', 9 | componentMetadata: { 10 | componentName: 'Page', 11 | configure: { 12 | supports: { 13 | style: true, 14 | className: true, 15 | }, 16 | component: { 17 | isContainer: true, 18 | disableBehaviors: '*', 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | export default Page; 25 | -------------------------------------------------------------------------------- /packages/utils/src/schema.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; 2 | import { IPublicEnumTransformStage } from '@alilc/lowcode-types/lib/shell/enum/transform-stage'; 3 | import { isFunction, isObject } from './check'; 4 | 5 | export function exportSchema(node: unknown): T { 6 | if (isObject(node)) { 7 | if (isFunction(node.export)) { 8 | return node.export(IPublicEnumTransformStage.Render); 9 | } else if (isFunction(node.exportSchema)) { 10 | return node.exportSchema(IPublicEnumTransformStage.Render); 11 | } 12 | } 13 | return null as unknown as T; 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/src/setup-environment.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicApiProject } from '@alilc/lowcode-types'; 2 | import { assetItem, AssetType } from './asset'; 3 | 4 | export function setupHostEnvironment( 5 | project: IPublicApiProject, 6 | vueRuntimeUrl: string, 7 | ): void { 8 | project.onSimulatorHostReady((host) => { 9 | host.set('environment', [ 10 | assetItem( 11 | AssetType.JSText, 12 | 'window.__is_simulator_env__=true;window.__VUE_DEVTOOLS_GLOBAL_HOOK__=window.parent.__VUE_DEVTOOLS_GLOBAL_HOOK__;', 13 | ), 14 | assetItem(AssetType.JSUrl, vueRuntimeUrl, undefined, 'vue'), 15 | ]); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-provide.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject } from '@knxcloud/lowcode-utils'; 2 | import { provide } from 'vue'; 3 | import { type RuntimeScope, type SchemaParser } from '../../utils'; 4 | 5 | export function initProvide( 6 | parser: SchemaParser, 7 | schema: unknown, 8 | scope: RuntimeScope, 9 | ): void { 10 | const provideOptions = parser.parseSchema(schema, scope); 11 | 12 | const provides = isFunction(provideOptions) ? provideOptions() : provideOptions; 13 | 14 | if (isObject(provides)) { 15 | Reflect.ownKeys(provides).forEach((key) => { 16 | const value = Reflect.get(provides, key); 17 | provide(key, value); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import IntlMessageFormat from 'intl-messageformat'; 2 | 3 | export type I18nMessages = { 4 | [locale: string]: Record; 5 | }; 6 | 7 | /** 8 | * 用于处理国际化字符串 9 | * @param key - 语料标识 10 | * @param values - 字符串模版变量 11 | * @param locale - 国际化标识,例如 zh-CN、en-US 12 | * @param messages - 国际化语言包 13 | */ 14 | export function getI18n( 15 | key: string, 16 | values = {}, 17 | locale = 'zh-CN', 18 | messages: I18nMessages = {}, 19 | ) { 20 | if (!messages || !messages[locale] || !messages[locale][key]) { 21 | return ''; 22 | } 23 | const formater = new IntlMessageFormat(messages[locale][key], locale); 24 | return formater.format(values); 25 | } 26 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/config.ts: -------------------------------------------------------------------------------- 1 | import type { RendererComponent } from './core'; 2 | import { RENDERER_COMPS } from './renderers'; 3 | 4 | export type RendererModules = Record; 5 | 6 | export class Config { 7 | private renderers: RendererModules = { ...RENDERER_COMPS }; 8 | private configProvider: any = null; 9 | 10 | setConfigProvider(comp: any) { 11 | this.configProvider = comp; 12 | } 13 | 14 | getConfigProvider() { 15 | return this.configProvider; 16 | } 17 | 18 | setRenderers(renderers: RendererModules) { 19 | this.renderers = renderers; 20 | } 21 | 22 | getRenderers() { 23 | return this.renderers; 24 | } 25 | } 26 | 27 | export default new Config(); 28 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-recommended', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | rules: { 13 | 'vue/prop-name-casing': 'off', 14 | 'vue/one-component-per-file': 'off', 15 | 'vue/multi-word-component-names': 'off', 16 | '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '_+' }], 17 | '@typescript-eslint/ban-ts-comment': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | '@typescript-eslint/no-non-null-assertion': 'off', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-data.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject } from '@knxcloud/lowcode-utils'; 2 | import { 3 | AccessTypes, 4 | addToScope, 5 | type RuntimeScope, 6 | type SchemaParser, 7 | } from '../../utils'; 8 | 9 | export function initData( 10 | parser: SchemaParser, 11 | schema: unknown, 12 | scope: RuntimeScope, 13 | ): void { 14 | const dataOptions = parser.parseSchema(schema, false); 15 | 16 | const dataResult = isFunction(dataOptions) 17 | ? dataOptions.call(scope) 18 | : isObject(dataOptions) 19 | ? dataOptions 20 | : null; 21 | if (!dataResult || Object.keys(dataResult).length === 0) return; 22 | 23 | addToScope(scope, AccessTypes.DATA, dataResult); 24 | } 25 | -------------------------------------------------------------------------------- /packages/vue-renderer/vite.config.umd.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | define: { 5 | __VUE_PROD_DEVTOOLS__: JSON.stringify('false'), 6 | 'process.env.NODE_ENV': JSON.stringify('production'), 7 | }, 8 | build: { 9 | target: 'ES2018', 10 | sourcemap: true, 11 | lib: { 12 | name: 'LCVueRenderer', 13 | entry: 'src/index.ts', 14 | fileName: () => 'vue-renderer.js', 15 | formats: ['umd'], 16 | }, 17 | emptyOutDir: false, 18 | rollupOptions: { 19 | external: ['vue'], 20 | output: { 21 | exports: 'named', 22 | globals: { 23 | vue: 'Vue', 24 | }, 25 | }, 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/leaf/live.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, mergeProps, renderSlot, toRaw } from 'vue'; 2 | import { isFragment, splitLeafProps } from '../use'; 3 | import { leafProps } from '../base'; 4 | 5 | export const Live = defineComponent({ 6 | inheritAttrs: false, 7 | props: leafProps, 8 | setup: (props, { attrs, slots }) => { 9 | return () => { 10 | const comp = toRaw(props.__comp); 11 | const vnodeProps = { ...props.__vnodeProps }; 12 | const compProps = splitLeafProps(attrs)[1]; 13 | if (isFragment(comp)) { 14 | return renderSlot(slots, 'default', attrs); 15 | } 16 | return comp ? h(comp, mergeProps(compProps, vnodeProps), slots) : null; 17 | }; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import types from 'vite-plugin-lib-types'; 3 | import pkg from './package.json'; 4 | 5 | export default defineConfig({ 6 | plugins: [types()], 7 | build: { 8 | sourcemap: true, 9 | minify: false, 10 | target: 'ES2018', 11 | lib: { 12 | entry: { 'vue-simulator-renderer': 'src/index.ts' }, 13 | formats: ['es'], 14 | }, 15 | rollupOptions: { 16 | external: [...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies)], 17 | output: { 18 | assetFileNames({ name }) { 19 | return name === 'style.css' ? 'vue-simulator-renderer.css' : name!; 20 | }, 21 | }, 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/check-node.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@knxcloud/lowcode-utils'; 2 | 3 | export function isCommentNode(el: Element | Text | Comment | Node): el is Comment { 4 | return el.nodeType === 8; 5 | } 6 | 7 | export function isTextNode(el: Element | Text | Comment | Node): el is Text { 8 | return el.nodeType === 3; 9 | } 10 | 11 | export function isDomNode(el: unknown): el is Element | Text { 12 | return ( 13 | isObject(el) && 14 | 'nodeType' in el && 15 | (el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.TEXT_NODE) 16 | ); 17 | } 18 | 19 | export function isEmptyNode(el: Element | Text | Comment | Node): boolean { 20 | return isCommentNode(el) || (isTextNode(el) && el.nodeValue === ''); 21 | } 22 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/renderers/page.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue'; 2 | import { useRenderer, rendererProps, useRootScope } from '../core'; 3 | 4 | const Page = defineComponent((props, { slots }) => { 5 | return () => h('div', { class: 'lc-page', style: { height: '100%' }, ...props }, slots); 6 | }); 7 | 8 | export const PageRenderer = defineComponent({ 9 | name: 'PageRenderer', 10 | props: rendererProps, 11 | __renderer__: true, 12 | setup(props, context) { 13 | const { scope, wrapRender } = useRootScope(props, context); 14 | const { renderComp, componentsRef, schemaRef } = useRenderer(props, scope); 15 | 16 | return wrapRender(() => { 17 | return renderComp(schemaRef.value, scope, componentsRef.value.Page || Page); 18 | }); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-emits.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isObject } from '@knxcloud/lowcode-utils'; 2 | import type { RuntimeScope, SchemaParser } from '../../utils'; 3 | 4 | export function initEmits( 5 | parser: SchemaParser, 6 | schema: unknown, 7 | scope: RuntimeScope, 8 | ): void { 9 | const emitsOptions = parser.parseSchema(schema, false); 10 | 11 | const dataResult = isArray(emitsOptions) 12 | ? emitsOptions.reduce((res, next) => ((res[next] = null), res), {}) 13 | : isObject(emitsOptions) 14 | ? emitsOptions 15 | : null; 16 | 17 | if (!dataResult || Object.keys(dataResult).length === 0) return; 18 | 19 | scope.$.emitsOptions = Object.create( 20 | scope.$.emitsOptions, 21 | Object.getOwnPropertyDescriptors(dataResult), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/vite.config.umd.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | define: { 5 | __VUE_PROD_DEVTOOLS__: JSON.stringify('false'), 6 | 'process.env.NODE_ENV': JSON.stringify('production'), 7 | }, 8 | build: { 9 | sourcemap: true, 10 | target: 'ES2018', 11 | lib: { 12 | name: 'LCVueSimulatorRenderer', 13 | entry: 'src/index.ts', 14 | fileName: () => 'vue-simulator-renderer.js', 15 | formats: ['umd'], 16 | }, 17 | emptyOutDir: false, 18 | rollupOptions: { 19 | external: ['vue'], 20 | output: { 21 | exports: 'named', 22 | globals: { 23 | vue: 'Vue', 24 | }, 25 | assetFileNames: 'vue-simulator-renderer.css', 26 | }, 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/data-source/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import types from 'vite-plugin-lib-types'; 3 | 4 | import pkg from './package.json'; 5 | 6 | export default defineConfig({ 7 | test: { 8 | globals: true, 9 | environment: 'jsdom', 10 | coverage: { 11 | provider: 'istanbul', 12 | reporter: ['text', 'html'], 13 | }, 14 | }, 15 | plugins: [ 16 | types({ 17 | tsconfigPath: './tsconfig.build.json', 18 | }), 19 | ], 20 | build: { 21 | target: 'ES2018', 22 | sourcemap: true, 23 | minify: false, 24 | lib: { 25 | entry: 'src/index.ts', 26 | formats: ['cjs', 'es'], 27 | }, 28 | emptyOutDir: true, 29 | rollupOptions: { 30 | external: [...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies)], 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-prettier', 5 | 'stylelint-config-rational-order', 6 | 'stylelint-config-recommended-less', 7 | ], 8 | rules: { 9 | 'at-rule-no-unknown': null, 10 | 'no-descending-specificity': null, 11 | 'color-no-invalid-hex': true, 12 | 'less/color-no-invalid-hex': true, 13 | 'selector-pseudo-element-no-unknown': [true, { ignorePseudoElements: ['v-deep'] }], 14 | 'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['deep'] }], 15 | 'no-descending-specificity': null, 16 | 'declaration-block-trailing-semicolon': null, 17 | 'font-family-no-missing-generic-family-keyword': null, 18 | }, 19 | overrides: [ 20 | { 21 | files: ['**/*.less'], 22 | customSyntax: 'postcss-less', 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | import simulator from './simulator'; 2 | import { warn } from './utils'; 3 | import './index.less'; 4 | 5 | const win = window as any; 6 | 7 | if (typeof win !== 'undefined') { 8 | win.SimulatorRenderer = simulator; 9 | } 10 | 11 | win.addEventListener('load', () => { 12 | if (!win.__VUE_HMR_RUNTIME__) { 13 | warn('检测到您正在使用 vue 运行时的生产环境版本'); 14 | warn('这将导致画布的部分功能异常,请使用非生产环境版本代替'); 15 | warn('https://unpkg.com/vue/dist/vue.runtime.global.js'); 16 | } 17 | }); 18 | 19 | win.addEventListener('beforeunload', () => { 20 | win.LCSimulatorHost = null; 21 | win.SimulatorRenderer = null; 22 | simulator.dispose(); 23 | }); 24 | 25 | export default simulator; 26 | export * from '@knxcloud/lowcode-vue-renderer'; 27 | export { 28 | config as vueRendererConfig, 29 | default as VueRenderer, 30 | } from '@knxcloud/lowcode-vue-renderer'; 31 | export * from './interface'; 32 | -------------------------------------------------------------------------------- /packages/vue-renderer/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import types from 'vite-plugin-lib-types'; 3 | import vueJsx from '@vitejs/plugin-vue-jsx'; 4 | import pkg from './package.json'; 5 | 6 | export default defineConfig({ 7 | test: { 8 | globals: true, 9 | environment: 'jsdom', 10 | coverage: { 11 | provider: 'istanbul', 12 | reporter: ['text', 'html'], 13 | }, 14 | }, 15 | plugins: [ 16 | types({ 17 | fileName: 'vue-renderer.d.ts', 18 | tsconfigPath: './tsconfig.build.json', 19 | }), 20 | vueJsx(), 21 | ], 22 | build: { 23 | target: 'ES2018', 24 | sourcemap: true, 25 | minify: false, 26 | lib: { 27 | entry: 'src/index.ts', 28 | fileName: () => 'vue-renderer.mjs', 29 | formats: ['es'], 30 | }, 31 | rollupOptions: { 32 | external: [ 33 | ...Object.keys(pkg.dependencies), 34 | ...Object.keys(pkg.peerDependencies), 35 | ].filter((item) => !item.includes('@alilc')), 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@knxcloud/lowcode-hooks", 3 | "version": "1.6.0", 4 | "main": "dist/lowcode-hooks.js", 5 | "module": "dist/lowcode-hooks.mjs", 6 | "typings": "dist/lowcode-hooks.d.ts", 7 | "keywords": [ 8 | "vue", 9 | "lowcode", 10 | "lowcode-engine" 11 | ], 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "vite build", 17 | "lint:type": "tsc -p ./tsconfig.json --noEmit", 18 | "prepack": "pnpm build" 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@alilc/lowcode-types": "^1.1.10", 23 | "vite": "^4.4.9", 24 | "vite-plugin-lib-types": "^2.0.4", 25 | "vue": "^3.3.4" 26 | }, 27 | "peerDependencies": { 28 | "@alilc/lowcode-types": "^1.0.0", 29 | "vue": "^3.0.0" 30 | }, 31 | "publishConfig": { 32 | "access": "public", 33 | "registry": "https://registry.npmjs.org/" 34 | }, 35 | "repository": { 36 | "type": "http", 37 | "url": "https://github.com/KNXCloud/lowcode-engine-vue/tree/main/packages/hooks" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/renderers/component.ts: -------------------------------------------------------------------------------- 1 | import { useRendererContext } from '@knxcloud/lowcode-hooks'; 2 | import { defineComponent, Fragment, getCurrentInstance, onMounted } from 'vue'; 3 | import { useRenderer, rendererProps, useRootScope } from '../core'; 4 | import { isFragment } from '../core/use'; 5 | 6 | export const ComponentRenderer = defineComponent({ 7 | name: 'ComponentRenderer', 8 | props: rendererProps, 9 | __renderer__: true, 10 | setup(props, context) { 11 | const { scope, wrapRender } = useRootScope(props, context); 12 | const { triggerCompGetCtx } = useRendererContext(); 13 | const { renderComp, schemaRef, componentsRef } = useRenderer(props, scope); 14 | 15 | const Component = componentsRef.value[schemaRef.value.componentName] || Fragment; 16 | const instance = getCurrentInstance(); 17 | 18 | if (isFragment(Component)) { 19 | onMounted(() => { 20 | instance?.proxy && triggerCompGetCtx(schemaRef.value, instance.proxy); 21 | }); 22 | } 23 | 24 | return wrapRender(() => renderComp(schemaRef.value, scope, Component)); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/utils/src/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将驼峰命名法的字符串转化为中划线连接的字符串 3 | * 4 | * @example 5 | * 6 | * const res = kebabCase('userName'); // user-name 7 | * 8 | * @param str - 被转换的字符串 9 | */ 10 | export function kebabCase(str: string): string { 11 | if (!str) return str; 12 | return str.replace(/[A-Z]/g, (c, i) => { 13 | const suf = i > 0 ? '-' : ''; 14 | return suf + c.toLocaleLowerCase(); 15 | }); 16 | } 17 | 18 | /** 19 | * 将中划线连接的字符串转化为小驼峰命名法 20 | * 21 | * @example 22 | * 23 | * const res = camelCase('user-name'); // userName 24 | * 25 | * @param str - 被转换的字符串 26 | */ 27 | export function camelCase(str: string): string { 28 | if (!str) return str; 29 | return str.replace(/-[a-zA-Z]/g, (c) => { 30 | return c.charAt(1).toLocaleUpperCase(); 31 | }); 32 | } 33 | 34 | /** 35 | * 将中划线连接的字符串转化为大驼峰命名法 36 | * 37 | * @example 38 | * 39 | * const res = pascalCase('user-name'); // UserName 40 | * 41 | * @param str - 被转换的字符串 42 | */ 43 | export function pascalCase(str: string): string { 44 | const res = camelCase(str); 45 | return res && res.charAt(0).toLocaleUpperCase() + res.slice(1); 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 KNX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@knxcloud/lowcode-utils", 3 | "main": "./dist/lowcode-utils.js", 4 | "module": "./dist/lowcode-utils.mjs", 5 | "typings": "./dist/lowcode-utils.d.ts", 6 | "version": "1.6.0", 7 | "keywords": [ 8 | "vue", 9 | "lowcode", 10 | "lowcode-engine" 11 | ], 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "vite build", 17 | "lint:type": "tsc -p ./tsconfig.json --noEmit", 18 | "prepack": "pnpm build" 19 | }, 20 | "devDependencies": { 21 | "@alilc/lowcode-types": "^1.1.10", 22 | "vite": "^4.4.9", 23 | "vite-plugin-lib-types": "^2.0.4", 24 | "vue": "^3.3.4" 25 | }, 26 | "peerDependencies": { 27 | "@alilc/lowcode-types": "^1.0.0", 28 | "vue": "^3.0.0" 29 | }, 30 | "peerDependenciesMeta": { 31 | "vue": { 32 | "optional": true 33 | } 34 | }, 35 | "publishConfig": { 36 | "access": "public", 37 | "registry": "https://registry.npmjs.org/" 38 | }, 39 | "repository": { 40 | "type": "http", 41 | "url": "https://github.com/KNXCloud/lowcode-engine-vue/tree/main/packages/utils" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/setup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isFunction, 3 | isNil, 4 | isObject, 5 | isPromise, 6 | toString, 7 | } from '@knxcloud/lowcode-utils'; 8 | import { toRaw } from 'vue'; 9 | import { 10 | AccessTypes, 11 | addToScope, 12 | warn, 13 | type RuntimeScope, 14 | type SchemaParser, 15 | } from '../../utils'; 16 | 17 | export function setup( 18 | parser: SchemaParser, 19 | schema: unknown, 20 | scope: RuntimeScope, 21 | [props, ctx]: [object, object], 22 | ): void | Promise { 23 | const setupFn = parser.parseSchema(schema, false); 24 | if (!isFunction(setupFn)) return; 25 | 26 | const setupResult = setupFn.apply(undefined, [props, ctx]); 27 | if (isPromise(setupResult)) { 28 | return setupResult.then((res) => handleResult(res, scope)); 29 | } else { 30 | handleResult(setupResult, scope); 31 | } 32 | } 33 | 34 | function handleResult(result: unknown, scope: RuntimeScope) { 35 | if (isNil(result)) return; 36 | if (!isObject(result)) { 37 | warn('不支持的 setup 返回值类型, type: ' + toString(result)); 38 | return; 39 | } 40 | addToScope(scope, AccessTypes.SETUP, toRaw(result)); 41 | } 42 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/renderers/block.ts: -------------------------------------------------------------------------------- 1 | import { useRendererContext } from '@knxcloud/lowcode-hooks'; 2 | import { defineComponent, Fragment, getCurrentInstance, onMounted } from 'vue'; 3 | import { useRenderer, rendererProps, useRootScope } from '../core'; 4 | import { isFragment } from '../core/use'; 5 | 6 | export const BlockRenderer = defineComponent({ 7 | name: 'BlockRenderer', 8 | props: rendererProps, 9 | __renderer__: true, 10 | setup(props, context) { 11 | const { scope, wrapRender } = useRootScope(props, context); 12 | const { triggerCompGetCtx } = useRendererContext(); 13 | const { renderComp, schemaRef, componentsRef } = useRenderer(props, scope); 14 | 15 | const Component = componentsRef.value[schemaRef.value.componentName] || Fragment; 16 | const instance = getCurrentInstance(); 17 | 18 | if (isFragment(Component)) { 19 | onMounted(() => { 20 | instance?.proxy && triggerCompGetCtx(schemaRef.value, instance.proxy); 21 | }); 22 | } 23 | 24 | return wrapRender(() => { 25 | return renderComp(schemaRef.value, scope, componentsRef.value.Block || Fragment); 26 | }); 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /packages/data-source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@knxcloud/lowcode-data-source", 3 | "version": "1.0.0", 4 | "main": "dist/lowcode-data-source.js", 5 | "module": "dist/lowcode-data-source.mjs", 6 | "typings": "dist/lowcode-data-source.d.ts", 7 | "keywords": [ 8 | "vue", 9 | "lowcode", 10 | "lowcode-engine" 11 | ], 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "vite build", 17 | "test": "vitest --run", 18 | "lint:type": "tsc -p ./tsconfig.json --noEmit", 19 | "prepack": "pnpm build" 20 | }, 21 | "dependencies": { 22 | "@knxcloud/lowcode-utils": "workspace:*" 23 | }, 24 | "devDependencies": { 25 | "@alilc/lowcode-types": "^1.1.10", 26 | "vite": "^4.4.9", 27 | "vite-plugin-lib-types": "^2.0.4", 28 | "vue": "^3.3.4" 29 | }, 30 | "peerDependencies": { 31 | "@alilc/lowcode-types": "^1.0.0", 32 | "vue": "^3.0.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public", 36 | "registry": "https://registry.npmjs.org/" 37 | }, 38 | "repository": { 39 | "type": "http", 40 | "url": "https://github.com/KNXCloud/lowcode-engine-vue/tree/main/packages/data-source" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/utils/src/script.ts: -------------------------------------------------------------------------------- 1 | import { createDefer } from './create-defer'; 2 | 3 | export function evaluate(script: string) { 4 | const scriptEl = document.createElement('script'); 5 | scriptEl.text = script; 6 | document.head.appendChild(scriptEl); 7 | document.head.removeChild(scriptEl); 8 | } 9 | 10 | export function load(url: string) { 11 | const node: any = document.createElement('script'); 12 | 13 | node.onload = onload; 14 | node.onerror = onload; 15 | 16 | const i = createDefer(); 17 | 18 | function onload(e: any) { 19 | node.onload = null; 20 | node.onerror = null; 21 | if (e.type === 'load') { 22 | i.resolve(); 23 | } else { 24 | i.reject(); 25 | } 26 | } 27 | 28 | node.src = url; 29 | node.async = false; 30 | 31 | document.head.appendChild(node); 32 | 33 | return i.promise(); 34 | } 35 | 36 | export function evaluateExpression(expr: string) { 37 | return new Function(expr)(); 38 | } 39 | 40 | export function newFunction(args: string, code: string) { 41 | try { 42 | return new Function(args, code); 43 | } catch (e) { 44 | console.warn('Caught error, Cant init func'); 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/buildin-components/slot.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, renderSlot } from 'vue'; 2 | 3 | const Slot = defineComponent({ 4 | render() { 5 | return renderSlot(this.$slots, 'default', this.$props, () => { 6 | return [h('div', { class: 'lc-container' })]; 7 | }); 8 | }, 9 | }); 10 | 11 | Object.assign(Slot, { 12 | displayName: 'Slot', 13 | componentMetadata: { 14 | componentName: 'Slot', 15 | configure: { 16 | props: [ 17 | { 18 | name: '___title', 19 | title: '插槽标题', 20 | setter: 'StringSetter', 21 | defaultValue: '插槽容器', 22 | }, 23 | { 24 | name: '___params', 25 | title: '插槽入参', 26 | setter: { 27 | componentName: 'ArraySetter', 28 | props: { 29 | itemSetter: { 30 | componentName: 'StringSetter', 31 | props: { 32 | placeholder: '参数名称', 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | ], 39 | component: { 40 | isContainer: true, 41 | disableBehaviors: '*', 42 | }, 43 | supports: false, 44 | }, 45 | }, 46 | }); 47 | 48 | export default Slot; 49 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-computed.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isPlainObject, noop } from '@knxcloud/lowcode-utils'; 2 | import { computed, type ComputedOptions } from 'vue'; 3 | import { 4 | AccessTypes, 5 | addToScope, 6 | type RuntimeScope, 7 | type SchemaParser, 8 | } from '../../utils'; 9 | 10 | export function initComputed( 11 | parser: SchemaParser, 12 | schema: unknown, 13 | scope: RuntimeScope, 14 | ): void { 15 | const options = parser.parseSchema(schema, false); 16 | if (!isPlainObject(options)) return; 17 | 18 | const computedValues: object = {}; 19 | for (const key in options) { 20 | const computedOptions = options[key] as ComputedOptions; 21 | const get = isFunction(computedOptions) 22 | ? computedOptions.bind(scope) 23 | : isFunction(computedOptions.get) 24 | ? computedOptions.get.bind(scope) 25 | : noop; 26 | const set = 27 | !isFunction(computedOptions) && isFunction(computedOptions.set) 28 | ? computedOptions.set.bind(scope) 29 | : noop; 30 | const computedValue = computed({ 31 | get, 32 | set, 33 | }); 34 | Object.defineProperty(computedValues, key, { 35 | enumerable: true, 36 | configurable: true, 37 | get: () => computedValue.value, 38 | set: (v) => (computedValue.value = v), 39 | }); 40 | } 41 | 42 | addToScope(scope, AccessTypes.CONTEXT, computedValues, true); 43 | } 44 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/cursor.ts: -------------------------------------------------------------------------------- 1 | class Cursor { 2 | private states = new Set(); 3 | 4 | setDragging(flag: boolean) { 5 | if (flag) { 6 | this.addState('dragging'); 7 | } else { 8 | this.removeState('dragging'); 9 | } 10 | } 11 | 12 | setXResizing(flag: boolean) { 13 | if (flag) { 14 | this.addState('x-resizing'); 15 | } else { 16 | this.removeState('x-resizing'); 17 | } 18 | } 19 | 20 | setYResizing(flag: boolean) { 21 | if (flag) { 22 | this.addState('y-resizing'); 23 | } else { 24 | this.removeState('y-resizing'); 25 | } 26 | } 27 | 28 | setCopy(flag: boolean) { 29 | if (flag) { 30 | this.addState('copy'); 31 | } else { 32 | this.removeState('copy'); 33 | } 34 | } 35 | 36 | isCopy() { 37 | return this.states.has('copy'); 38 | } 39 | 40 | release() { 41 | for (const state of this.states) { 42 | this.removeState(state); 43 | } 44 | } 45 | 46 | addState(state: string) { 47 | if (!this.states.has(state)) { 48 | this.states.add(state); 49 | document.documentElement.classList.add(`lc-cursor-${state}`); 50 | } 51 | } 52 | 53 | private removeState(state: string) { 54 | if (this.states.has(state)) { 55 | this.states.delete(state); 56 | document.documentElement.classList.remove(`lc-cursor-${state}`); 57 | } 58 | } 59 | } 60 | 61 | export const cursor = new Cursor(); 62 | -------------------------------------------------------------------------------- /packages/utils/src/build-utils.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeNpmInfo } from '@alilc/lowcode-types'; 2 | import { accessLibrary } from './build-components'; 3 | import { isJSFunction } from './check'; 4 | 5 | export interface UtilsNpmMetadata { 6 | name: string; 7 | type: 'npm'; 8 | content: IPublicTypeNpmInfo; 9 | } 10 | 11 | export interface UtilsFunctionMetadata { 12 | name: string; 13 | type: 'function'; 14 | content: IPublicTypeNpmInfo | CallableFunction; 15 | } 16 | 17 | export type UtilsMetadata = UtilsNpmMetadata | UtilsFunctionMetadata; 18 | 19 | export function buildUtils( 20 | libraryMap: Record, 21 | utilsMetadata: UtilsMetadata[], 22 | ): Record { 23 | return utilsMetadata 24 | .filter((meta) => meta && meta.name) 25 | .reduce( 26 | (utils, meta) => { 27 | const { name, content, type } = meta; 28 | if (type === 'npm') { 29 | const { package: pkg, exportName, destructuring } = content ?? {}; 30 | if (libraryMap[pkg]) { 31 | const library = accessLibrary(libraryMap[pkg]); 32 | if (library) { 33 | utils[name] = destructuring && exportName ? library[exportName] : library; 34 | } 35 | } 36 | } else if (type === 'function') { 37 | utils[name] = isJSFunction(content) 38 | ? new Function(`return ${content.value}`)() 39 | : meta.content; 40 | } 41 | return utils; 42 | }, 43 | {} as Record, 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/data-source/src/core/interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataHandler, 3 | ErrorHandler, 4 | WillFetch, 5 | RuntimeOptions, 6 | CustomRequestHandler, 7 | } from '@alilc/lowcode-types'; 8 | 9 | export type ReturnValueFunc = (...args: unknown[]) => T; 10 | // eslint-disable-next-line @typescript-eslint/ban-types 11 | export type MaybyFunc = ReturnValueFunc | T; 12 | 13 | export interface DataSourceOptions { 14 | list: DataSourceConfig[]; 15 | dataHandler?: DataHandler; 16 | } 17 | 18 | export interface DataSourceConfig { 19 | id: string; 20 | type?: string; 21 | isInit?: MaybyFunc; 22 | isSync?: MaybyFunc; 23 | requestHandler?: CustomRequestHandler; 24 | dataHandler?: DataHandler; 25 | errorHandler?: ErrorHandler; 26 | willFetch?: WillFetch; 27 | shouldFetch?: ReturnValueFunc; 28 | options: RuntimeOptions; 29 | [otherKey: string]: unknown; 30 | } 31 | 32 | export type DataSourceStatus = 'init' | 'loading' | 'loaded' | 'error'; 33 | 34 | export type DataSourceLoader = ( 35 | params?: Record, 36 | options?: Record, 37 | ) => Promise; 38 | 39 | export interface DataSource { 40 | data: T; 41 | error: unknown; 42 | loading: boolean; 43 | status: DataSourceStatus; 44 | isInit: boolean; 45 | isSync: boolean; 46 | load: DataSourceLoader; 47 | } 48 | 49 | export interface DataSourceContext> { 50 | state: TState; 51 | setState(state: TState): void; 52 | forceUpdate(): void; 53 | } 54 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const path = require('path'); 3 | const { defineConfig } = require('@vue/cli-service'); 4 | 5 | const resolve = (...p) => path.resolve(__dirname, ...p); 6 | 7 | module.exports = defineConfig({ 8 | productionSourceMap: false, 9 | transpileDependencies: false, 10 | css: { 11 | extract: true, 12 | }, 13 | configureWebpack: { 14 | devServer: { 15 | host: '127.0.0.1', 16 | port: 5559, 17 | headers: { 18 | 'Access-Control-Allow-Origin': '*', 19 | }, 20 | }, 21 | externals: { 22 | vue: 'Vue', 23 | }, 24 | optimization: { 25 | splitChunks: false, 26 | }, 27 | }, 28 | chainWebpack: (config) => { 29 | config.entryPoints 30 | .delete('app') 31 | .end() 32 | .entry('vue-simulator-renderer') 33 | .add('./src/index.ts'); 34 | 35 | config.output 36 | .chunkFilename('[name].js') 37 | .filename('[name].js') 38 | .library('LCVueSimulatorRenderer') 39 | .libraryTarget('umd'); 40 | 41 | config.plugin('extract-css').tap(([options]) => { 42 | return [ 43 | Object.assign(options, { 44 | filename: '[name].css', 45 | chunkFilename: '[name].css', 46 | }), 47 | ]; 48 | }); 49 | 50 | config.resolve.alias.merge({ 51 | '@knxcloud/lowcode-hooks': resolve('../hooks/src'), 52 | '@knxcloud/lowcode-utils': resolve('../utils/src'), 53 | '@knxcloud/lowcode-vue-renderer': resolve('../vue-renderer/src'), 54 | }); 55 | 56 | config.devServer.allowedHosts.add('all'); 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /packages/vue-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@knxcloud/lowcode-vue-renderer", 3 | "main": "./dist/vue-renderer.js", 4 | "module": "./dist/vue-renderer.mjs", 5 | "typings": "./dist/vue-renderer.d.ts", 6 | "version": "1.6.0", 7 | "keywords": [ 8 | "vue", 9 | "lowcode", 10 | "lowcode-engine" 11 | ], 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "vite build && vite build -c vite.config.umd.ts", 17 | "test": "vitest --run", 18 | "test:coverage": "pnpm run test --coverage", 19 | "lint:type": "tsc -p ./tsconfig.build.json --noEmit", 20 | "prepack": "pnpm test && pnpm build" 21 | }, 22 | "dependencies": { 23 | "@knxcloud/lowcode-data-source": "workspace:*", 24 | "@knxcloud/lowcode-hooks": "workspace:*", 25 | "@knxcloud/lowcode-utils": "workspace:*", 26 | "intl-messageformat": "^10.5.0", 27 | "vue-router": "^4.2.4" 28 | }, 29 | "devDependencies": { 30 | "@alilc/lowcode-types": "^1.1.10", 31 | "@vitejs/plugin-vue-jsx": "^3.0.2", 32 | "vite": "^4.4.9", 33 | "vite-plugin-lib-types": "^2.0.4", 34 | "vue": "^3.3.4" 35 | }, 36 | "peerDependencies": { 37 | "@alilc/lowcode-types": "^1.0.0", 38 | "vue": ">= 3.x < 4", 39 | "vue-router": ">= 4.x < 5" 40 | }, 41 | "peerDependenciesMeta": { 42 | "vue-router": { 43 | "optional": true 44 | }, 45 | "@alilc/lowcode-types": { 46 | "optional": true 47 | } 48 | }, 49 | "publishConfig": { 50 | "access": "public", 51 | "registry": "https://registry.npmjs.org/" 52 | }, 53 | "repository": { 54 | "type": "http", 55 | "url": "https://github.com/KNXCloud/lowcode-engine-vue/tree/main/packages/vue-renderer" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@knxcloud/lowcode-vue-simulator-renderer", 3 | "main": "./dist/vue-simulator-renderer.js", 4 | "module": "./dist/vue-simulator-renderer.mjs", 5 | "typings": "./dist/vue-simulator-renderer.d.ts", 6 | "version": "1.6.1", 7 | "keywords": [ 8 | "vue", 9 | "lowcode", 10 | "lowcode-engine" 11 | ], 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "start": "vue-cli-service serve", 17 | "build": "vite build && vite build -c vite.config.umd.ts", 18 | "lint:type": "tsc -p ./tsconfig.json --noEmit", 19 | "prepack": "pnpm build" 20 | }, 21 | "dependencies": { 22 | "@knxcloud/lowcode-hooks": "workspace:*", 23 | "@knxcloud/lowcode-utils": "workspace:*", 24 | "@knxcloud/lowcode-vue-renderer": "workspace:*" 25 | }, 26 | "devDependencies": { 27 | "@alilc/lowcode-designer": "^1.1.10", 28 | "@alilc/lowcode-types": "^1.1.10", 29 | "@rollup/pluginutils": "^5.0.4", 30 | "@vue/cli-plugin-babel": "^5.0.8", 31 | "@vue/cli-plugin-eslint": "^5.0.8", 32 | "@vue/cli-plugin-typescript": "^5.0.8", 33 | "@vue/cli-service": "^5.0.8", 34 | "less": "^4.2.0", 35 | "less-loader": "^11.1.3", 36 | "vite": "^4.4.9", 37 | "vite-plugin-lib-types": "^2.0.4", 38 | "vue": "^3.3.4", 39 | "vue-router": "^4.2.4" 40 | }, 41 | "peerDependencies": { 42 | "@alilc/lowcode-types": "^1.0.0", 43 | "vue": "^3.0.0", 44 | "vue-router": "^4.0.0" 45 | }, 46 | "publishConfig": { 47 | "access": "public", 48 | "registry": "https://registry.npmjs.org/" 49 | }, 50 | "repository": { 51 | "type": "http", 52 | "url": "https://github.com/KNXCloud/lowcode-engine-vue/tree/main/packages/vue-simulator-renderer" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/hooks/src/current-node.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GlobalEvent, 3 | IPublicModelNode, 4 | IPublicTypeDisposable, 5 | } from '@alilc/lowcode-types'; 6 | import type { InjectionKey } from 'vue'; 7 | import type { DesignMode } from './renderer-context'; 8 | import { inject } from 'vue'; 9 | 10 | export type IPublicTypePropChangeOptions = Omit< 11 | GlobalEvent.Node.Prop.ChangeOptions, 12 | 'node' 13 | >; 14 | 15 | export interface INode extends IPublicModelNode { 16 | onVisibleChange(func: (flag: boolean) => any): () => void; 17 | onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable; 18 | onChildrenChange( 19 | fn: (param?: { type: string; node: INode } | undefined) => void, 20 | ): IPublicTypeDisposable | undefined; 21 | } 22 | 23 | export interface EnvNode { 24 | mode: DesignMode; 25 | node: INode | null; 26 | isDesignerEnv: boolean; 27 | } 28 | 29 | export interface DesignerEnvNode extends EnvNode { 30 | mode: 'design'; 31 | node: INode; 32 | isDesignerEnv: true; 33 | } 34 | 35 | export interface LiveEnvNode extends EnvNode { 36 | mode: 'live'; 37 | node: null; 38 | isDesignerEnv: false; 39 | } 40 | 41 | export type CurrentNode = DesignerEnvNode | LiveEnvNode; 42 | 43 | export function getCurrentNodeKey(): InjectionKey { 44 | let key = (window as any).__currentNode; 45 | if (!key) { 46 | key = Symbol('__currentNode'); 47 | (window as any).__currentNode = key; 48 | } 49 | return key; 50 | } 51 | 52 | export function useCurrentNode(): CurrentNode { 53 | const key = getCurrentNodeKey(); 54 | return inject( 55 | key, 56 | () => { 57 | return { 58 | mode: 'live', 59 | node: null, 60 | isDesignerEnv: false, 61 | } as LiveEnvNode; 62 | }, 63 | true, 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "pnpm@8.7.0", 3 | "engines": { 4 | "node": "^16.0.0", 5 | "pnpm": "^8.0.0" 6 | }, 7 | "scripts": { 8 | "start": "pnpm -C packages/vue-simulator-renderer start", 9 | "build": "pnpm -r build", 10 | "lint": "pnpm run lint:code && pnpm run lint:type", 11 | "lint:code": "eslint ./packages/**/*.ts && stylelint ./packages/**/*.less", 12 | "lint:type": "pnpm -r lint:type", 13 | "prepare": "husky install" 14 | }, 15 | "devDependencies": { 16 | "@rushstack/eslint-patch": "^1.3.3", 17 | "@types/node": "^20.5.7", 18 | "@typescript-eslint/eslint-plugin": "^6.5.0", 19 | "@typescript-eslint/parser": "^6.5.0", 20 | "@vitest/coverage-istanbul": "^0.31.4", 21 | "@vue/eslint-config-prettier": "^8.0.0", 22 | "@vue/eslint-config-typescript": "^11.0.3", 23 | "@vue/test-utils": "^2.4.1", 24 | "eslint": "^8.48.0", 25 | "eslint-config-prettier": "^9.0.0", 26 | "eslint-plugin-prettier": "^5.0.0", 27 | "eslint-plugin-vue": "^9.17.0", 28 | "husky": "^8.0.3", 29 | "jsdom": "^22.1.0", 30 | "lint-staged": "^14.0.1", 31 | "postcss": "8.4.14", 32 | "postcss-less": "^6.0.0", 33 | "prettier": "^3.0.3", 34 | "stylelint": "^15.10.3", 35 | "stylelint-config-prettier": "^9.0.5", 36 | "stylelint-config-rational-order": "^0.1.2", 37 | "stylelint-config-recommended-less": "^1.0.4", 38 | "stylelint-config-standard": "^34.0.0", 39 | "stylelint-less": "^1.0.8", 40 | "typescript": "^5.2.2", 41 | "vitest": "^0.31.4", 42 | "vue": "^3.3.4" 43 | }, 44 | "lint-staged": { 45 | "*.{ts,js}": [ 46 | "prettier --write", 47 | "eslint --fix" 48 | ], 49 | "*.json": [ 50 | "prettier --write" 51 | ], 52 | "*.{less,css}": [ 53 | "stylelint --fix" 54 | ] 55 | }, 56 | "author": "KNXCloud", 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-inject.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isObject } from '@knxcloud/lowcode-utils'; 2 | import { inject, isRef, type Ref } from 'vue'; 3 | import { 4 | AccessTypes, 5 | addToScope, 6 | type RuntimeScope, 7 | type SchemaParser, 8 | } from '../../utils'; 9 | 10 | export function initInject( 11 | parser: SchemaParser, 12 | schema: unknown, 13 | scope: RuntimeScope, 14 | ): void { 15 | const injectOptions = parser.parseSchema(schema, false); 16 | 17 | let normalizedOptions: Record; 18 | 19 | if (isArray(injectOptions)) { 20 | normalizedOptions = injectOptions.reduce((res, next) => { 21 | return (res[next] = next), res; 22 | }, {}); 23 | } else if (isObject(injectOptions)) { 24 | normalizedOptions = injectOptions as Record; 25 | } else { 26 | return; 27 | } 28 | 29 | const injectedValues: Record = {}; 30 | 31 | for (const key in normalizedOptions) { 32 | const opt = normalizedOptions[key]; 33 | let injected: unknown; 34 | if (isObject(opt)) { 35 | const injectionKey = (opt.from || key) as string; 36 | if ('default' in opt) { 37 | injected = inject( 38 | injectionKey, 39 | opt.default, 40 | true /* treat default function as factory */, 41 | ); 42 | } else { 43 | injected = inject(injectionKey); 44 | } 45 | } else { 46 | injected = inject(opt as string); 47 | } 48 | 49 | if (isRef(injected)) { 50 | Object.defineProperty(injectedValues, key, { 51 | enumerable: true, 52 | configurable: true, 53 | get: () => (injected as Ref).value, 54 | set: (v) => ((injected as Ref).value = v), 55 | }); 56 | } else { 57 | injectedValues[key] = injected; 58 | } 59 | } 60 | 61 | addToScope(scope, AccessTypes.CONTEXT, injectedValues, true); 62 | } 63 | -------------------------------------------------------------------------------- /packages/hooks/src/renderer-context.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; 2 | import type { Component, ComponentPublicInstance, InjectionKey } from 'vue'; 3 | import type { INode } from './current-node'; 4 | import { inject, getCurrentInstance } from 'vue'; 5 | 6 | export type DesignMode = 'live' | 'design'; 7 | 8 | export interface RendererContext { 9 | readonly components: Record>; 10 | readonly designMode: DesignMode; 11 | readonly thisRequiredInJSE: boolean; 12 | getNode(id: string): INode | null; 13 | rerender(): void; 14 | wrapLeafComp(name: string, comp: C, leaf: L): L; 15 | triggerCompGetCtx(schema: IPublicTypeNodeSchema, val: ComponentPublicInstance): void; 16 | } 17 | 18 | export function getRendererContextKey(): InjectionKey { 19 | let key = (window as any).__rendererContext; 20 | if (!key) { 21 | key = Symbol('__rendererContext'); 22 | (window as any).__rendererContext = key; 23 | } 24 | return key; 25 | } 26 | 27 | export function useRendererContext(): RendererContext { 28 | const key = getRendererContextKey(); 29 | return inject( 30 | key, 31 | () => { 32 | const props = getCurrentInstance()?.props ?? {}; 33 | return { 34 | rerender: () => void 0, 35 | thisRequiredInJSE: true, 36 | components: getPropValue(props, 'components', {}), 37 | designMode: getPropValue(props, 'designMode', 'live'), 38 | getNode: getPropValue(props, 'getNode', () => null), 39 | wrapLeafComp: (_: string, __: T, leaf: L) => 40 | leaf, 41 | triggerCompGetCtx: getPropValue(props, 'triggerCompGetCtx', () => void 0), 42 | }; 43 | }, 44 | true, 45 | ); 46 | } 47 | 48 | function getPropValue( 49 | props: Record, 50 | key: string, 51 | defaultValue: T, 52 | ): T { 53 | return (props[key] || props[`__${key}`] || defaultValue) as T; 54 | } 55 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/helpers/document.ts: -------------------------------------------------------------------------------- 1 | import { INode } from '@knxcloud/lowcode-hooks'; 2 | import { 3 | IPublicTypeContainerSchema, 4 | IPublicModelDocumentModel, 5 | IPublicTypeComponentMetadata, 6 | } from '@alilc/lowcode-types'; 7 | import { get } from 'lodash'; 8 | import { createNode } from './node'; 9 | import { isArray } from '@knxcloud/lowcode-utils'; 10 | import { shallowRef } from 'vue'; 11 | 12 | export function createDocument( 13 | schema: IPublicTypeContainerSchema, 14 | metas: Record> = {} 15 | ): IPublicModelDocumentModel { 16 | const nodesMap: Record = {}; 17 | const schemaRef = shallowRef(schema); 18 | 19 | function createNodeById(id: string, path?: string): INode | null { 20 | const node = path ? get(schema, path) : schema; 21 | if (isArray(node)) { 22 | for (let idx = 0; idx < node.length; idx++) { 23 | const createdNode = createNodeById( 24 | id, 25 | [path, idx].filter((item) => item != null).join('.') 26 | ); 27 | if (createdNode) return createdNode; 28 | } 29 | } 30 | if (node.id === id) { 31 | return createNode(schemaRef, path, metas[id], nodesMap); 32 | } else if (node.children) { 33 | if (isArray(node.children)) { 34 | for (let idx = 0; idx < node.children.length; idx++) { 35 | const createdNode = createNodeById( 36 | id, 37 | [path, 'children', idx].filter((item) => item != null).join('.') 38 | ); 39 | if (createdNode) return createdNode; 40 | } 41 | } else { 42 | return createNodeById( 43 | id, 44 | [path, 'children'].filter((item) => item != null).join('.') 45 | ); 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | return { 52 | getNodeById(id: string): INode | null { 53 | return nodesMap[id] || (nodesMap[id] = createNodeById(id)); 54 | }, 55 | } as IPublicModelDocumentModel; 56 | } 57 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/index.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeContainerSchema } from '@alilc/lowcode-types'; 2 | import type { RuntimeScope, SchemaParser } from '../../utils'; 3 | import { initComputed } from './init-computed'; 4 | import { initProps } from './init-props'; 5 | import { initEmits } from './init-emits'; 6 | import { initData } from './init-data'; 7 | import { initWatch } from './init-watch'; 8 | import { initInject } from './init-inject'; 9 | import { initProvide } from './init-provide'; 10 | import { setup } from './setup'; 11 | import { created } from './created'; 12 | import { beforeCreate } from './beforeCreate'; 13 | 14 | const VUE_LOWCODE_LIFTCYCLES_MAP = { 15 | setup: setup, 16 | created: created, 17 | beforeCreate: beforeCreate, 18 | initInject: initInject, 19 | initProvide: initProvide, 20 | initEmits: initEmits, 21 | initProps: initProps, 22 | initData: initData, 23 | initWatch: initWatch, 24 | initComputed: initComputed, 25 | }; 26 | 27 | export type LowCodeHookMap = typeof VUE_LOWCODE_LIFTCYCLES_MAP; 28 | export type LowCodeHook = keyof LowCodeHookMap; 29 | 30 | export function createHookCaller( 31 | schema: IPublicTypeContainerSchema, 32 | scope: RuntimeScope, 33 | parser: SchemaParser, 34 | ) { 35 | function callHook(hook: 'setup', props: object, ctx: object): void | Promise; 36 | function callHook>(hook: T): void; 37 | function callHook( 38 | hook: T, 39 | param1?: object, 40 | param2?: object, 41 | ): void | Promise { 42 | const lifeCycles = schema.lifeCycles ?? {}; 43 | const lifeCycleSchema = lifeCycles[hook]; 44 | const hookFn = VUE_LOWCODE_LIFTCYCLES_MAP[hook]; 45 | if (lifeCycleSchema && hookFn) { 46 | return hookFn(parser, lifeCycleSchema, scope, [param1!, param2!]); 47 | } 48 | } 49 | 50 | return callHook; 51 | } 52 | 53 | export { 54 | setupLowCodeRouteGuard, 55 | type SetupLowCodeRouteGuardOptions, 56 | LOWCODE_ROUTE_META, 57 | } from './vue-router'; 58 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/closest-node.ts: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeNodeInstance as NodeInstance } from '@alilc/lowcode-types'; 2 | import type { ComponentInternalInstance } from 'vue'; 3 | import type { ComponentRecord } from '../interface'; 4 | import { 5 | getCompRootData, 6 | isVNodeHTMLElement, 7 | isCompRootHTMLElement, 8 | createComponentRecord, 9 | } from './comp-node'; 10 | 11 | export function getClosestNodeInstance( 12 | el: Element, 13 | specId: string | undefined, 14 | ): NodeInstance | null { 15 | if (!document.contains(el)) { 16 | return null; 17 | } 18 | return getClosestNodeInstanceByElement(el, specId); 19 | } 20 | 21 | export function getClosestNodeInstanceByElement( 22 | el: Element, 23 | specId: string | undefined, 24 | ): NodeInstance | null { 25 | while (el) { 26 | if (isVNodeHTMLElement(el)) { 27 | const component = el.__vueParentComponent; 28 | return getClosestNodeInstanceByComponent(component, specId); 29 | } 30 | if (isCompRootHTMLElement(el)) { 31 | const { nodeId, docId, instance } = getCompRootData(el); 32 | if (!specId || specId === nodeId) { 33 | return { 34 | docId, 35 | nodeId, 36 | instance: createComponentRecord(docId, nodeId, instance.$.uid), 37 | }; 38 | } 39 | } 40 | el = el.parentElement as Element; 41 | } 42 | 43 | return null; 44 | } 45 | 46 | export function getClosestNodeInstanceByComponent( 47 | instance: ComponentInternalInstance | null, 48 | specId: string | undefined, 49 | ): NodeInstance | null { 50 | while (instance) { 51 | const el = instance.vnode.el as Element; 52 | if (el && isCompRootHTMLElement(el)) { 53 | const { nodeId, docId, instance } = getCompRootData(el); 54 | if (!specId || specId === nodeId) { 55 | return { 56 | docId, 57 | nodeId, 58 | instance: createComponentRecord(docId, nodeId, instance.$.uid), 59 | }; 60 | } 61 | } 62 | instance = instance.parent; 63 | } 64 | return null; 65 | } 66 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/comp-node.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentInternalInstance, VNode } from 'vue'; 2 | import type { ComponentInstance, ComponentRecord } from '../interface'; 3 | import { isProxy } from 'vue'; 4 | import { isNil, isObject } from '@knxcloud/lowcode-utils'; 5 | 6 | const SYMBOL_VDID = Symbol('_LCDocId'); 7 | const SYMBOL_VNID = Symbol('_LCNodeId'); 8 | const SYMBOL_VInstance = Symbol('_LCVueInstance'); 9 | const SYMBOL_RECORD_FLAG = Symbol('_LCVueCompRecord'); 10 | 11 | export interface VNodeHTMLElement extends HTMLElement { 12 | __vnode: VNode; 13 | __vueParentComponent: ComponentInternalInstance; 14 | } 15 | 16 | export interface CompRootHTMLElement extends HTMLElement { 17 | [SYMBOL_VDID]: string; 18 | [SYMBOL_VNID]: string; 19 | [SYMBOL_VInstance]: ComponentInstance; 20 | } 21 | 22 | export interface CompRootData { 23 | docId: string; 24 | nodeId: string; 25 | instance: ComponentInstance; 26 | } 27 | 28 | export function isVNodeHTMLElement(el: unknown): el is VNodeHTMLElement { 29 | return isObject(el) && !isNil(el.__vueParentComponent); 30 | } 31 | 32 | export function isCompRootHTMLElement( 33 | el: Element | null | undefined, 34 | ): el is CompRootHTMLElement { 35 | return isObject(el) && SYMBOL_VDID in el; 36 | } 37 | 38 | export function isComponentRecord(el: unknown): el is ComponentRecord { 39 | return isObject(el) && SYMBOL_RECORD_FLAG in el; 40 | } 41 | 42 | export function isInternalInstance(el: unknown): el is ComponentInternalInstance { 43 | return isObject(el) && isProxy(el.proxy); 44 | } 45 | 46 | export function createComponentRecord(did: string, nid: string, cid: number) { 47 | return { 48 | did, 49 | nid, 50 | cid, 51 | [SYMBOL_RECORD_FLAG]: true, 52 | }; 53 | } 54 | 55 | export function getCompRootData(el: CompRootHTMLElement): CompRootData { 56 | return { 57 | docId: el[SYMBOL_VDID], 58 | nodeId: el[SYMBOL_VNID], 59 | instance: el[SYMBOL_VInstance], 60 | }; 61 | } 62 | 63 | export function setCompRootData(el: CompRootHTMLElement, data: CompRootData): void { 64 | el[SYMBOL_VDID] = data.docId; 65 | el[SYMBOL_VNID] = data.nodeId; 66 | el[SYMBOL_VInstance] = data.instance; 67 | } 68 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-watch.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isFunction, isObject, isString } from '@knxcloud/lowcode-utils'; 2 | import { warn, watch } from 'vue'; 3 | import { 4 | AccessTypes, 5 | getAccessTarget, 6 | type RuntimeScope, 7 | type SchemaParser, 8 | } from '../../utils'; 9 | 10 | function createPathGetter(ctx: any, path: string) { 11 | const segments = path.split('.'); 12 | return () => { 13 | let cur = ctx; 14 | for (let i = 0; i < segments.length && cur; i++) { 15 | cur = cur[segments[i]]; 16 | } 17 | return cur; 18 | }; 19 | } 20 | 21 | export function createWatcher( 22 | raw: unknown, 23 | ctx: Record, 24 | scope: RuntimeScope, 25 | key: string, 26 | ) { 27 | const getter = key.includes('.') ? createPathGetter(scope, key) : () => scope[key]; 28 | if (isString(raw)) { 29 | const handler = ctx[raw]; 30 | if (isFunction(handler)) { 31 | watch(getter, handler); 32 | } else { 33 | warn(`Invalid watch handler specified by key "${raw}"`, handler); 34 | } 35 | } else if (isFunction(raw)) { 36 | watch(getter, raw.bind(scope)); 37 | } else if (isObject(raw)) { 38 | if (isArray(raw)) { 39 | raw.forEach((r) => createWatcher(r, ctx, scope, key)); 40 | } else { 41 | const handler = isFunction(raw.handler) 42 | ? raw.handler.bind(scope) 43 | : isString(raw.handler) 44 | ? ctx[raw.handler] 45 | : null; 46 | if (isFunction(handler)) { 47 | watch(getter, handler, raw); 48 | } else { 49 | warn(`Invalid watch handler specified by key "${raw.handler}"`, handler); 50 | } 51 | } 52 | } else { 53 | warn(`Invalid watch option: "${key}"`, raw); 54 | } 55 | } 56 | 57 | export function initWatch( 58 | parser: SchemaParser, 59 | schema: unknown, 60 | scope: RuntimeScope, 61 | ): void { 62 | const watchConfigs = parser.parseSchema(schema, false); 63 | if (!watchConfigs || !isObject(watchConfigs) || Object.keys(watchConfigs).length === 0) 64 | return; 65 | 66 | const ctx = getAccessTarget(scope, AccessTypes.CONTEXT); 67 | 68 | for (const key in watchConfigs) { 69 | createWatcher(watchConfigs[key], ctx, scope, key); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/data-source/__tests__/fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequest } from '../src'; 2 | 3 | describe('fetch request', () => { 4 | const mockedFetch = vi.fn(); 5 | 6 | beforeEach(() => { 7 | vi.stubGlobal('fetch', mockedFetch); 8 | }); 9 | 10 | afterEach(() => { 11 | vi.unstubAllEnvs(); 12 | }); 13 | 14 | test('fetch success', async () => { 15 | mockedFetch.mockResolvedValueOnce({ 16 | json: () => Promise.resolve(1), 17 | status: 200, 18 | statusText: 'OK', 19 | }); 20 | const res = await fetchRequest({ 21 | uri: 'https://127.0.0.1/info.json', 22 | method: 'GET', 23 | }); 24 | expect(res.data).eq(1); 25 | expect(res.code).eq(200); 26 | }); 27 | 28 | test('fetch failure', async () => { 29 | mockedFetch.mockResolvedValueOnce({ 30 | json: () => Promise.resolve(1), 31 | status: 500, 32 | statusText: 'Server internal error', 33 | }); 34 | 35 | await expect(() => 36 | fetchRequest({ 37 | uri: 'https://127.0.0.1/info.json', 38 | method: 'GET', 39 | }) 40 | ).rejects.toThrowError('Server internal error'); 41 | }); 42 | 43 | test('with params', async () => { 44 | mockedFetch.mockResolvedValueOnce({ 45 | json: () => Promise.resolve(1), 46 | status: 200, 47 | statusText: 'OK', 48 | }); 49 | const res = await fetchRequest({ 50 | uri: 'https://127.0.0.1/info.json', 51 | method: 'GET', 52 | params: { a: 5, b: null, c: { d: 5 } }, 53 | }); 54 | expect(res.data).eq(1); 55 | expect(res.code).eq(200); 56 | expect(mockedFetch).toMatchSnapshot(); 57 | }); 58 | 59 | test('responseType', async () => { 60 | mockedFetch.mockResolvedValueOnce({ 61 | blob: () => Promise.resolve(new Blob()), 62 | status: 200, 63 | statusText: 'OK', 64 | }); 65 | const res = await fetchRequest({ 66 | uri: 'https://127.0.0.1/info.json', 67 | method: 'POST', 68 | headers: { 69 | 'content-type': 'json', 70 | }, 71 | params: { 72 | a: 5, 73 | }, 74 | responseType: 'blob', 75 | }); 76 | expect(res.code).eq(200); 77 | expect(res.data).toBeInstanceOf(Blob); 78 | expect(mockedFetch).toMatchSnapshot(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/data-source/src/core/engine.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandlersMap } from '@alilc/lowcode-types'; 2 | import { 3 | DataSourceOptions, 4 | DataSourceContext, 5 | DataSourceLoader, 6 | DataSource, 7 | } from './interface'; 8 | import { createDataSource } from './data-source'; 9 | 10 | export function createDataSourceEngine( 11 | config: DataSourceOptions, 12 | context: DataSourceContext, 13 | requestHandlersMap: RequestHandlersMap = {}, 14 | ) { 15 | const dataSource: Record = {}; 16 | const dataSourceMap: Record = {}; 17 | 18 | const { list, dataHandler } = config; 19 | 20 | for (const config of list) { 21 | const mergedConfig = { dataHandler, ...config }; 22 | const _dataSource = createDataSource(mergedConfig, context, requestHandlersMap); 23 | 24 | const func: DataSourceLoader = (params, otherOptions) => { 25 | const mergedOptions = { 26 | assignToScope: false, 27 | ...otherOptions, 28 | }; 29 | return _dataSource.load(params, mergedOptions); 30 | }; 31 | 32 | dataSource[config.id] = func; 33 | dataSourceMap[config.id] = _dataSource; 34 | } 35 | 36 | /** 37 | * 重新加载数据源 38 | * 39 | * - 若传入 id 则加载对应 id 的数据源 40 | * - 若不传入 id,则加载 isInit 为 true 的数据源 41 | */ 42 | const reloadDataSource = ( 43 | id?: string, 44 | params?: Record, 45 | otherOptions?: Record, 46 | ) => { 47 | if (id) { 48 | const dataSource = dataSourceMap[id]; 49 | if (!dataSource) { 50 | throw new Error('dataSource not found, id: ' + id); 51 | } 52 | return dataSource.load(params, otherOptions); 53 | } 54 | const syncItems: DataSource[] = []; 55 | const asyncItems: DataSource[] = []; 56 | 57 | Object.keys(dataSourceMap) 58 | .map((id) => dataSourceMap[id]) 59 | .filter((ds) => ds.isInit) 60 | .forEach((ds) => { 61 | ds.isSync ? syncItems.push(ds) : asyncItems.push(ds); 62 | }); 63 | 64 | const promises: Promise[] = [ 65 | ...asyncItems.map((ds) => ds.load()), 66 | syncItems.reduce( 67 | (res, next) => res.then(() => next.load()), 68 | Promise.resolve(null), 69 | ), 70 | ]; 71 | return Promise.all(promises); 72 | }; 73 | 74 | const needInit = () => 75 | Object.keys(dataSourceMap).some((id) => dataSourceMap[id].isInit); 76 | 77 | return { dataSource, dataSourceMap, reloadDataSource, shouldInit: needInit }; 78 | } 79 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router'; 2 | import type { Config, I18nMessages } from '@knxcloud/lowcode-vue-renderer'; 3 | import type { DesignMode } from '@knxcloud/lowcode-hooks'; 4 | import type { Component, ComponentPublicInstance, App } from 'vue'; 5 | import type { 6 | IPublicTypeSimulatorRenderer, 7 | IPublicModelNode as INode, 8 | IPublicModelDocumentModel as IDocumentModel, 9 | IPublicTypeNpmInfo as NpmInfo, 10 | IPublicTypeRootSchema as RootSchema, 11 | IPublicTypeComponentSchema as ComponentSchema, 12 | IPublicTypeNodeInstance as NodeInstance, 13 | } from '@alilc/lowcode-types'; 14 | 15 | export type MixedComponent = NpmInfo | Component | ComponentSchema; 16 | 17 | export type ComponentInstance = ComponentPublicInstance; 18 | 19 | export interface ComponentRecord { 20 | did: string; 21 | nid: string; 22 | cid: number; 23 | } 24 | 25 | export interface SimulatorViewLayout { 26 | Component?: Component; 27 | componentName?: string; 28 | props?: Record; 29 | } 30 | 31 | export interface DocumentInstance { 32 | readonly id: string; 33 | readonly key: string; 34 | readonly path: string; 35 | readonly scope: Record; 36 | readonly document: IDocumentModel; 37 | readonly instancesMap: Map; 38 | readonly schema: RootSchema; 39 | readonly messages: I18nMessages; 40 | readonly appHelper: Record; 41 | getComponentInstance(id: number): ComponentInstance | null; 42 | mountInstance( 43 | id: string, 44 | instance: ComponentInstance | HTMLElement, 45 | ): (() => void) | void; 46 | unmountInstance(id: string, instance: ComponentInstance): void; 47 | rerender(): void; 48 | getNode(id: string): INode | null; 49 | } 50 | 51 | export interface VueSimulatorRenderer 52 | extends IPublicTypeSimulatorRenderer { 53 | app: App; 54 | config: Config; 55 | router: Router; 56 | layout: SimulatorViewLayout; 57 | device: string; 58 | locale: string; 59 | designMode: DesignMode; 60 | libraryMap: Record; 61 | thisRequiredInJSE: boolean; 62 | autoRender: boolean; 63 | componentsMap: Record; 64 | disableCompMock: boolean | string[]; 65 | documentInstances: DocumentInstance[]; 66 | requestHandlersMap: Record; 67 | dispose(): void; 68 | getCurrentDocument(): DocumentInstance | null; 69 | getClosestNodeInstance( 70 | from: ComponentRecord | Element, 71 | nodeId?: string, 72 | ): NodeInstance | null; 73 | } 74 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/index.less: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | display: block; 4 | margin: 0; 5 | padding: 0; 6 | background: white; 7 | } 8 | 9 | html.engine-design-mode { 10 | padding-bottom: 0; 11 | } 12 | 13 | html.engine-cursor-move, 14 | html.engine-cursor-move * { 15 | cursor: grabbing !important; 16 | } 17 | 18 | html.engine-cursor-copy, 19 | html.engine-cursor-copy * { 20 | cursor: copy !important; 21 | } 22 | 23 | html.engine-cursor-ew-resize, 24 | html.engine-cursor-ew-resize * { 25 | cursor: ew-resize !important; 26 | } 27 | 28 | html.lc-cursor-dragging, 29 | html.lc-cursor-dragging * { 30 | cursor: move !important; 31 | } 32 | 33 | html.lc-cursor-x-resizing, 34 | html.lc-cursor-x-resizing * { 35 | cursor: col-resize; 36 | } 37 | 38 | html.lc-cursor-y-resizing, 39 | html.lc-cursor-y-resizing * { 40 | cursor: row-resize; 41 | } 42 | 43 | html.lc-cursor-copy, 44 | html.lc-cursor-copy * { 45 | cursor: copy !important; 46 | } 47 | 48 | ::-webkit-scrollbar { 49 | width: 5px; 50 | height: 5px; 51 | } 52 | 53 | ::-webkit-scrollbar-thumb { 54 | background-color: rgb(0 0 0 / 30%); 55 | border-radius: 5px; 56 | } 57 | 58 | .lc-container { 59 | height: 100%; 60 | 61 | &:empty { 62 | display: flex; 63 | align-items: center; 64 | min-width: 140px; 65 | height: 66px; 66 | max-height: 100%; 67 | overflow: hidden; 68 | color: #a7b1bd; 69 | text-align: center; 70 | background: #f2f3f5; 71 | outline: 1px dashed rgb(31 56 88 / 20%); 72 | outline-offset: -1px !important; 73 | 74 | &::before { 75 | z-index: 1; 76 | width: 100%; 77 | font-size: 14px; 78 | white-space: nowrap; 79 | content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC'; 80 | } 81 | } 82 | } 83 | 84 | .lc-container-placeholder { 85 | display: flex; 86 | align-items: center; 87 | justify-content: center; 88 | width: 100%; 89 | height: 100%; 90 | min-height: 60px; 91 | color: rgb(167 177 189); 92 | font-size: 14px; 93 | background-color: rgb(240 240 240); 94 | border: 1px dotted; 95 | 96 | &.lc-container-locked { 97 | background: #eccfcf; 98 | } 99 | } 100 | 101 | body.engine-document { 102 | &::after, 103 | &::before { 104 | display: table; 105 | content: ''; 106 | } 107 | 108 | &::after { 109 | clear: both; 110 | } 111 | } 112 | 113 | .engine-live-editing { 114 | outline: none; 115 | box-shadow: 0 0 0 2px rgb(102 188 92); 116 | cursor: text; 117 | user-select: text; 118 | } 119 | 120 | #app { 121 | height: 100vh; 122 | } 123 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/base.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RequestHandler, 3 | IPublicTypeNodeSchema as NodeSchema, 4 | IPublicTypeContainerSchema as ContainerSchema, 5 | } from '@alilc/lowcode-types'; 6 | import type { 7 | Component, 8 | ComponentPublicInstance, 9 | DefineComponent, 10 | ExtractPropTypes, 11 | PropType, 12 | } from 'vue'; 13 | import type { BlockScope, I18nMessages, SchemaParser } from '../utils'; 14 | import { INode } from '@knxcloud/lowcode-hooks'; 15 | 16 | export const rendererProps = { 17 | __scope: { 18 | type: Object as PropType, 19 | default: undefined, 20 | }, 21 | __schema: { 22 | type: Object as PropType, 23 | required: true, 24 | }, 25 | __appHelper: { 26 | type: Object as PropType>, 27 | default: () => ({}), 28 | }, 29 | __designMode: { 30 | type: String as PropType<'live' | 'design'>, 31 | default: 'live', 32 | }, 33 | __components: { 34 | type: Object as PropType>, 35 | required: true, 36 | }, 37 | __locale: { 38 | type: String, 39 | default: undefined, 40 | }, 41 | __messages: { 42 | type: Object as PropType, 43 | default: () => ({}), 44 | }, 45 | __getNode: { 46 | type: Function as PropType<(id: string) => INode | null>, 47 | required: true, 48 | }, 49 | __triggerCompGetCtx: { 50 | type: Function as PropType< 51 | (schema: NodeSchema, ref: ComponentPublicInstance) => void 52 | >, 53 | required: true, 54 | }, 55 | __thisRequiredInJSE: { 56 | type: Boolean, 57 | default: true, 58 | }, 59 | __requestHandlersMap: { 60 | type: Object as PropType>, 61 | default: () => ({}), 62 | }, 63 | __props: { 64 | type: Object, 65 | default: () => ({}), 66 | }, 67 | __parser: { 68 | type: Object as PropType, 69 | required: true, 70 | }, 71 | } as const; 72 | 73 | export type RendererProps = ExtractPropTypes; 74 | 75 | export const baseRendererPropKeys = Object.keys(rendererProps) as (keyof RendererProps)[]; 76 | 77 | export type RendererComponent = DefineComponent; 78 | 79 | export const leafProps = { 80 | __comp: null, 81 | __scope: null, 82 | __schema: { 83 | type: Object as PropType, 84 | default: () => ({}), 85 | }, 86 | __vnodeProps: { 87 | type: Object as PropType>, 88 | default: () => ({}), 89 | }, 90 | __isRootNode: Boolean, 91 | } as const; 92 | 93 | export type LeafProps = ExtractPropTypes; 94 | 95 | export const leafPropKeys = Object.keys(leafProps) as (keyof LeafProps)[]; 96 | 97 | export type LeafComponent = DefineComponent; 98 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/simulator-view.ts: -------------------------------------------------------------------------------- 1 | import { ref, Suspense, type PropType } from 'vue'; 2 | import type { DocumentInstance, VueSimulatorRenderer } from './interface'; 3 | import { defineComponent, h, renderSlot } from 'vue'; 4 | import LowCodeRenderer from '@knxcloud/lowcode-vue-renderer'; 5 | import { RouterView } from 'vue-router'; 6 | 7 | export const Layout = defineComponent({ 8 | props: { 9 | simulator: { 10 | type: Object as PropType, 11 | required: true, 12 | }, 13 | }, 14 | render() { 15 | const { simulator, $slots } = this; 16 | const { layout, getComponent } = simulator; 17 | if (layout) { 18 | const { Component, props = {}, componentName } = layout; 19 | if (Component) { 20 | return h(Component, { ...props, key: 'layout', simulator } as any, $slots); 21 | } 22 | const ComputedComponent = componentName && getComponent(componentName); 23 | if (ComputedComponent) { 24 | return h(ComputedComponent, { ...props, key: 'layout', simulator }, $slots); 25 | } 26 | } 27 | return renderSlot($slots, 'default'); 28 | }, 29 | }); 30 | 31 | export const SimulatorRendererView = defineComponent({ 32 | props: { 33 | simulator: { 34 | type: Object as PropType, 35 | required: true, 36 | }, 37 | }, 38 | render() { 39 | const { simulator } = this; 40 | return h(Layout, { simulator }, () => { 41 | return h(RouterView, null, { 42 | default: ({ Component }) => { 43 | return Component && h(Suspense, null, () => h(Component)); 44 | }, 45 | }); 46 | }); 47 | }, 48 | }); 49 | 50 | export const Renderer = defineComponent({ 51 | props: { 52 | simulator: { 53 | type: Object as PropType, 54 | required: true, 55 | }, 56 | documentInstance: { 57 | type: Object as PropType, 58 | required: true, 59 | }, 60 | }, 61 | setup: () => ({ renderer: ref() }), 62 | render() { 63 | const { documentInstance, simulator } = this; 64 | const { schema, scope, messages, appHelper, key } = documentInstance; 65 | const { designMode, device, locale, components, requestHandlersMap } = simulator; 66 | 67 | return h(LowCodeRenderer, { 68 | ref: 'renderer', 69 | key: key, 70 | scope: scope, 71 | schema: schema, 72 | locale: locale, 73 | device: device, 74 | messages: messages, 75 | appHelper: appHelper, 76 | components: components, 77 | designMode: designMode, 78 | requestHandlersMap: requestHandlersMap, 79 | disableCompMock: simulator.disableCompMock, 80 | thisRequiredInJSE: simulator.thisRequiredInJSE, 81 | getNode: (id) => documentInstance.getNode(id) as any, 82 | onCompGetCtx: (schema, ref) => documentInstance.mountInstance(schema.id!, ref), 83 | }); 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /packages/utils/src/misc.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isFunction, isString } from './check'; 2 | 3 | export const noop: (...args: any[]) => any = () => void 0; 4 | 5 | export function fromPairs>( 6 | entries: E, 7 | ): E extends Iterable 8 | ? T extends [infer K, infer V] 9 | ? K extends string 10 | ? Record 11 | : Record 12 | : Record 13 | : Record { 14 | const result: any = {}; 15 | for (const val of entries) { 16 | if (isArray(val) && val.length >= 2) { 17 | result[val[0]] = val[1]; 18 | } 19 | } 20 | return result; 21 | } 22 | 23 | export function debounce unknown>(fn: T, ms?: number): () => void { 24 | let timerId: any = null; 25 | 26 | if (!ms) { 27 | return function (this: unknown) { 28 | if (!timerId) { 29 | timerId = setTimeout(() => { 30 | timerId = null; 31 | fn.apply(this); 32 | }); 33 | } 34 | }; 35 | } else { 36 | return function (this: unknown) { 37 | if (timerId) { 38 | clearTimeout(timerId); 39 | } 40 | timerId = setTimeout(() => { 41 | timerId = null; 42 | fn.apply(this); 43 | }, ms); 44 | }; 45 | } 46 | } 47 | 48 | export const toString = (o: unknown) => Object.prototype.toString.call(o); 49 | 50 | export function sleep(ms?: number) { 51 | return new Promise((resolve) => { 52 | return setTimeout(resolve, ms); 53 | }); 54 | } 55 | 56 | export const createObjectSplitter = ( 57 | specialProps: string | string[] | ((prop: string) => boolean), 58 | ) => { 59 | const propsSet = new Set( 60 | isString(specialProps) 61 | ? specialProps.split(',') 62 | : isArray(specialProps) 63 | ? specialProps 64 | : [], 65 | ); 66 | 67 | const has = isFunction(specialProps) 68 | ? specialProps 69 | : (prop: string) => propsSet.has(prop); 70 | 71 | return (o: Record): [Record, Record, number] => { 72 | const keys = Object.keys(o); 73 | if (keys.every((k) => !has(k))) return [{}, o, 0]; 74 | 75 | let count = 0; 76 | const left: Record = {}; 77 | const right: Record = {}; 78 | 79 | for (const key of keys) { 80 | if (has(key)) { 81 | left[key] = o[key]; 82 | count++; 83 | } else { 84 | right[key] = o[key]; 85 | } 86 | } 87 | 88 | return [left, right, count]; 89 | }; 90 | }; 91 | 92 | export const cached = (fn: (param: string) => R): ((param: string) => R) => { 93 | const cacheStore: Record = {}; 94 | return function (this: unknown, param: string) { 95 | return param in cacheStore 96 | ? cacheStore[param] 97 | : (cacheStore[param] = fn.call(this, param)); 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/data-source/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataHandler, 3 | ErrorHandler, 4 | IPublicTypeJSExpression, 5 | IPublicTypeJSFunction, 6 | InterpretDataSource, 7 | RequestHandler, 8 | WillFetch, 9 | } from '@alilc/lowcode-types'; 10 | import { DataSourceConfig, createDataSourceEngine } from '@knxcloud/lowcode-data-source'; 11 | import { AccessTypes, RuntimeScope, SchemaParser, addToScope } from '../utils'; 12 | import { isJSExpression, isJSFunction } from '@knxcloud/lowcode-utils'; 13 | 14 | export function create( 15 | config: InterpretDataSource, 16 | scope: RuntimeScope, 17 | requestHandlerMaps?: Record, 18 | ) { 19 | const parser = scope.__parser; 20 | 21 | const dataHandler = parser.parseSchema(config.dataHandler, scope); 22 | const list = config.list.map( 23 | ({ 24 | isInit = false, 25 | isSync = false, 26 | requestHandler, 27 | dataHandler, 28 | errorHandler, 29 | willFetch, 30 | shouldFetch, 31 | options, 32 | ...otherConfig 33 | }): DataSourceConfig => ({ 34 | ...parser.parseSchema(otherConfig, scope), 35 | isInit: transformToJSFunction(isInit, parser, scope), 36 | isSync: transformToJSFunction(isSync, parser, scope), 37 | requestHandler: parser.parseSchema(requestHandler, scope), 38 | dataHandler: parser.parseSchema(dataHandler, scope), 39 | errorHandler: parser.parseSchema(errorHandler, scope), 40 | willFetch: parser.parseSchema(willFetch, scope), 41 | shouldFetch: parser.parseSchema<() => boolean>(shouldFetch, scope), 42 | options: () => parser.parseSchema(options, scope), 43 | }), 44 | ); 45 | return createDataSourceEngine( 46 | { list, dataHandler }, 47 | { 48 | state: scope, 49 | setState(state) { 50 | const needAddScope: Record = {}; 51 | for (const key in state) { 52 | if (key in scope) { 53 | scope[key] = state[key]; 54 | } else { 55 | needAddScope[key] = state[key]; 56 | } 57 | } 58 | if (Object.keys(needAddScope).length > 0) { 59 | addToScope(scope, AccessTypes.CONTEXT, needAddScope); 60 | scope.$forceUpdate(); 61 | } 62 | }, 63 | forceUpdate: () => scope.$forceUpdate(), 64 | }, 65 | requestHandlerMaps, 66 | ); 67 | } 68 | 69 | function transformToJSFunction( 70 | val: IPublicTypeJSFunction | T, 71 | parser: SchemaParser, 72 | scope: RuntimeScope, 73 | ): (() => T) | T; 74 | function transformToJSFunction( 75 | val: IPublicTypeJSExpression | T, 76 | parser: SchemaParser, 77 | scope: RuntimeScope, 78 | ): (() => T) | T; 79 | function transformToJSFunction( 80 | val: IPublicTypeJSExpression | IPublicTypeJSFunction | T, 81 | parser: SchemaParser, 82 | scope: RuntimeScope, 83 | ): (() => T) | T; 84 | function transformToJSFunction( 85 | val: IPublicTypeJSExpression | IPublicTypeJSFunction | T, 86 | parser: SchemaParser, 87 | scope: RuntimeScope, 88 | ): (() => T) | T { 89 | const res = isJSExpression(val) 90 | ? parser.parseSchema( 91 | { 92 | type: 'JSFunction', 93 | value: `function () { return ${val.value} }`, 94 | }, 95 | scope, 96 | ) 97 | : isJSFunction(val) 98 | ? parser.parseSchema(val, scope) 99 | : val; 100 | return res as (() => T) | T; 101 | } 102 | -------------------------------------------------------------------------------- /packages/utils/src/check.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IPublicTypeJSExpression, 3 | IPublicTypeJSFunction, 4 | IPublicTypeJSSlot, 5 | IPublicTypeI18nData, 6 | IPublicTypeNodeSchema, 7 | IPublicTypeSlotSchema, 8 | IPublicTypeComponentSchema, 9 | IPublicTypeContainerSchema, 10 | } from '@alilc/lowcode-types'; 11 | import { toString } from './misc'; 12 | 13 | export type ESModule = { 14 | __esModule: true; 15 | default: any; 16 | }; 17 | 18 | export function isNil(val: T | null | undefined): val is null | undefined { 19 | return val === null || val === undefined; 20 | } 21 | 22 | export function isUndefined(val: unknown): val is undefined { 23 | return val === undefined; 24 | } 25 | 26 | export function isString(val: unknown): val is string { 27 | return typeof val === 'string'; 28 | } 29 | 30 | export function isObject(val: unknown): val is Record { 31 | return !isNil(val) && typeof val === 'object'; 32 | } 33 | 34 | export function isBoolean(value: unknown): value is boolean { 35 | return typeof value === 'boolean'; 36 | } 37 | 38 | export function isArray(val: any): val is any[] { 39 | return Array.isArray(val); 40 | } 41 | 42 | export function isFunction(val: unknown): val is (...args: any[]) => any { 43 | return typeof val === 'function'; 44 | } 45 | 46 | export function isPromise(val: unknown): val is Promise { 47 | return isObject(val) && isFunction(val.then) && isFunction(val.catch); 48 | } 49 | 50 | export function isPlainObject(val: unknown): val is Record { 51 | return !isNil(val) && toString(val) === '[object Object]'; 52 | } 53 | 54 | export function isESModule(obj: T | { default: T }): obj is ESModule { 55 | return ( 56 | obj && 57 | (Reflect.get(obj, '__esModule') || Reflect.get(obj, Symbol.toStringTag) === 'Module') 58 | ); 59 | } 60 | 61 | export function isCSSUrl(url: string): boolean { 62 | return /\.css$/.test(url); 63 | } 64 | 65 | export function isElement(node: unknown): node is Element { 66 | return isObject(node) && node.nodeType === Node.ELEMENT_NODE; 67 | } 68 | 69 | export function isJSFunction(val: unknown): val is IPublicTypeJSFunction { 70 | return val 71 | ? isObject(val) && (val.type === 'JSFunction' || val.extType === 'function') 72 | : false; 73 | } 74 | 75 | export function isJSSlot(val: unknown): val is IPublicTypeJSSlot { 76 | return isObject(val) && val.type === 'JSSlot'; 77 | } 78 | 79 | export function isJSExpression(val: unknown): val is IPublicTypeJSExpression { 80 | return isObject(val) && val.type === 'JSExpression' && val.extType !== 'function'; 81 | } 82 | 83 | export function isI18nData(val: unknown): val is IPublicTypeI18nData { 84 | return isObject(val) && val.type === 'i18n'; 85 | } 86 | 87 | export function isDOMText(val: unknown): val is string { 88 | return isString(val); 89 | } 90 | 91 | export function isNodeSchema(data: any): data is IPublicTypeNodeSchema { 92 | return data && data.componentName; 93 | } 94 | 95 | export function isSlotSchema(data: any): data is IPublicTypeSlotSchema { 96 | return isNodeSchema(data) && data.componentName === 'Slot'; 97 | } 98 | 99 | export function isComponentSchema(val: unknown): val is IPublicTypeComponentSchema { 100 | return isObject(val) && val.componentName === 'Component'; 101 | } 102 | 103 | export function isContainerSchema(val: unknown): val is IPublicTypeContainerSchema { 104 | return ( 105 | isNodeSchema(val) && 106 | (val.componentName === 'Block' || 107 | val.componentName === 'Page' || 108 | val.componentName === 'Component') 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /packages/data-source/__tests__/__snapshots__/fetch.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`fetch request > responseType 1`] = ` 4 | [MockFunction spy] { 5 | "calls": [ 6 | [ 7 | "https://127.0.0.1/info.json", 8 | { 9 | "credentials": "include", 10 | "headers": { 11 | "Accept": "application/json", 12 | }, 13 | "method": "GET", 14 | }, 15 | ], 16 | [ 17 | "https://127.0.0.1/info.json", 18 | { 19 | "credentials": "include", 20 | "headers": { 21 | "Accept": "application/json", 22 | }, 23 | "method": "GET", 24 | }, 25 | ], 26 | [ 27 | "https://127.0.0.1/info.json?a=5&c=%7B%22d%22%3A5%7D", 28 | { 29 | "credentials": "include", 30 | "headers": { 31 | "Accept": "application/json", 32 | }, 33 | "method": "GET", 34 | }, 35 | ], 36 | [ 37 | "https://127.0.0.1/info.json", 38 | { 39 | "body": { 40 | "a": 5, 41 | }, 42 | "credentials": "include", 43 | "headers": { 44 | "Accept": "application/json", 45 | "content-type": "json", 46 | }, 47 | "method": "POST", 48 | }, 49 | ], 50 | ], 51 | "results": [ 52 | { 53 | "type": "return", 54 | "value": { 55 | "json": [Function], 56 | "status": 200, 57 | "statusText": "OK", 58 | }, 59 | }, 60 | { 61 | "type": "return", 62 | "value": { 63 | "json": [Function], 64 | "status": 500, 65 | "statusText": "Server internal error", 66 | }, 67 | }, 68 | { 69 | "type": "return", 70 | "value": { 71 | "json": [Function], 72 | "status": 200, 73 | "statusText": "OK", 74 | }, 75 | }, 76 | { 77 | "type": "return", 78 | "value": { 79 | "blob": [Function], 80 | "status": 200, 81 | "statusText": "OK", 82 | }, 83 | }, 84 | ], 85 | } 86 | `; 87 | 88 | exports[`fetch request > with params 1`] = ` 89 | [MockFunction spy] { 90 | "calls": [ 91 | [ 92 | "https://127.0.0.1/info.json", 93 | { 94 | "credentials": "include", 95 | "headers": { 96 | "Accept": "application/json", 97 | }, 98 | "method": "GET", 99 | }, 100 | ], 101 | [ 102 | "https://127.0.0.1/info.json", 103 | { 104 | "credentials": "include", 105 | "headers": { 106 | "Accept": "application/json", 107 | }, 108 | "method": "GET", 109 | }, 110 | ], 111 | [ 112 | "https://127.0.0.1/info.json?a=5&c=%7B%22d%22%3A5%7D", 113 | { 114 | "credentials": "include", 115 | "headers": { 116 | "Accept": "application/json", 117 | }, 118 | "method": "GET", 119 | }, 120 | ], 121 | ], 122 | "results": [ 123 | { 124 | "type": "return", 125 | "value": { 126 | "json": [Function], 127 | "status": 200, 128 | "statusText": "OK", 129 | }, 130 | }, 131 | { 132 | "type": "return", 133 | "value": { 134 | "json": [Function], 135 | "status": 500, 136 | "statusText": "Server internal error", 137 | }, 138 | }, 139 | { 140 | "type": "return", 141 | "value": { 142 | "json": [Function], 143 | "status": 200, 144 | "statusText": "OK", 145 | }, 146 | }, 147 | ], 148 | } 149 | `; 150 | -------------------------------------------------------------------------------- /packages/utils/src/build-components.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IPublicTypeNpmInfo, 3 | IPublicTypeComponentSchema, 4 | } from '@alilc/lowcode-types'; 5 | import { Component, DefineComponent, defineComponent, h } from 'vue'; 6 | import { isComponentSchema, isESModule, isFunction, isObject } from './check'; 7 | import { cached } from './misc'; 8 | 9 | function isVueComponent(val: unknown): val is Component | DefineComponent { 10 | if (isFunction(val)) return true; 11 | if (isObject(val) && ('render' in val || 'setup' in val || 'template' in val)) { 12 | return true; 13 | } 14 | return false; 15 | } 16 | 17 | const generateHtmlComp = cached((library: string) => { 18 | if (/^[a-z-]+$/.test(library)) { 19 | return defineComponent((_, { attrs, slots }) => { 20 | return () => h(library, attrs, slots); 21 | }); 22 | } 23 | }); 24 | 25 | export function accessLibrary(library: string | Record) { 26 | if (typeof library !== 'string') { 27 | return library; 28 | } 29 | 30 | return (window as any)[library] || generateHtmlComp(library); 31 | } 32 | 33 | export function getSubComponent(library: any, paths: string[]) { 34 | const l = paths.length; 35 | if (l < 1 || !library) { 36 | return library; 37 | } 38 | let i = 0; 39 | let component: any; 40 | while (i < l) { 41 | const key = paths[i]!; 42 | let ex: any; 43 | try { 44 | component = library[key]; 45 | } catch (e) { 46 | ex = e; 47 | component = null; 48 | } 49 | if (i === 0 && component == null && key === 'default') { 50 | if (ex) { 51 | return l === 1 ? library : null; 52 | } 53 | component = library; 54 | } else if (component == null) { 55 | return null; 56 | } 57 | library = component; 58 | i++; 59 | } 60 | return component; 61 | } 62 | 63 | export function findComponent( 64 | libraryMap: Record, 65 | componentName: string, 66 | npm?: IPublicTypeNpmInfo, 67 | ) { 68 | if (!npm) { 69 | return accessLibrary(componentName); 70 | } 71 | const exportName = npm.exportName || npm.componentName || componentName; 72 | const libraryName = libraryMap[npm.package] || exportName; 73 | const library = accessLibrary(libraryName); 74 | const paths = npm.exportName && npm.subName ? npm.subName.split('.') : []; 75 | if (npm.destructuring) { 76 | paths.unshift(exportName); 77 | } else if (isESModule(library)) { 78 | paths.unshift('default'); 79 | } 80 | return getSubComponent(library, paths); 81 | } 82 | 83 | export function buildComponents( 84 | libraryMap: Record, 85 | componentsMap: Record< 86 | string, 87 | IPublicTypeNpmInfo | IPublicTypeComponentSchema | unknown 88 | >, 89 | createComponent?: (schema: IPublicTypeComponentSchema) => any, 90 | ) { 91 | const components: any = {}; 92 | Object.keys(componentsMap).forEach((componentName) => { 93 | let component = componentsMap[componentName]; 94 | if (isComponentSchema(component)) { 95 | if (createComponent) { 96 | components[componentName] = createComponent( 97 | component as IPublicTypeComponentSchema, 98 | ); 99 | } 100 | } else if (isVueComponent(component)) { 101 | components[componentName] = component; 102 | } else { 103 | component = findComponent( 104 | libraryMap, 105 | componentName, 106 | component as IPublicTypeNpmInfo, 107 | ); 108 | if (component) { 109 | components[componentName] = component; 110 | } 111 | } 112 | }); 113 | return components; 114 | } 115 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/utils/find-dom-nodes.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentInternalInstance, VNode } from 'vue'; 2 | import type { ComponentInstance } from '../interface'; 3 | import { isVNode } from 'vue'; 4 | import { isVNodeHTMLElement } from './comp-node'; 5 | import { isDomNode, isEmptyNode } from './check-node'; 6 | import { getClientRects } from './get-client-rects'; 7 | import { isArray } from '@knxcloud/lowcode-utils'; 8 | 9 | export function findDOMNodes(instance: ComponentInstance) { 10 | const els: (Element | Text)[] = []; 11 | 12 | const el: Element | Text = instance.$el; 13 | 14 | if (isEmptyNode(el)) { 15 | const internalInstance = instance.$; 16 | appendSiblingElement(els, internalInstance, el, (node) => { 17 | return node.previousSibling; 18 | }); 19 | appendDescendantComponent(els, internalInstance); 20 | appendSiblingElement(els, internalInstance, el, (node) => { 21 | return node.nextSibling; 22 | }); 23 | } else { 24 | els.push(el); 25 | } 26 | 27 | return els; 28 | } 29 | 30 | function appendSiblingElement( 31 | target: (Element | Text)[], 32 | instance: ComponentInternalInstance, 33 | el: Element | Text, 34 | next: (el: Node) => Node | null, 35 | ) { 36 | let nextNode = next(el); 37 | while (nextNode) { 38 | if (isEmptyNode(nextNode)) { 39 | nextNode = next(nextNode); 40 | continue; 41 | } 42 | if (isVNodeHTMLElement(nextNode)) { 43 | const childInstance = nextNode.__vueParentComponent; 44 | if (isChildInstance(instance, childInstance)) { 45 | target.unshift(nextNode); 46 | nextNode = next(nextNode); 47 | continue; 48 | } 49 | } 50 | break; 51 | } 52 | } 53 | 54 | function appendDescendantComponent( 55 | target: (Element | Text)[], 56 | instance: ComponentInternalInstance, 57 | ): boolean { 58 | const subNode = instance.subTree; 59 | const current = subNode.el as Element | Text; 60 | if (isValidElement(current)) { 61 | target.push(current); 62 | return true; 63 | } 64 | if (isArray(subNode.children) && subNode.children.length > 0) { 65 | return appendDescendantChildren(target, subNode.children as VNode[]); 66 | } else if (subNode.component) { 67 | return appendDescendantComponent(target, subNode.component); 68 | } 69 | return false; 70 | } 71 | 72 | function appendDescendantChildren( 73 | target: (Element | Text)[], 74 | children: VNode[], 75 | ): boolean { 76 | const validElements = children.map(({ el }) => el).filter(isValidElement); 77 | if (validElements.length > 0) { 78 | target.push(...validElements); 79 | return true; 80 | } else { 81 | return ( 82 | children.length > 0 && 83 | children.some((item) => { 84 | if (isArray(item.children) && item.children.length > 0) { 85 | return appendDescendantChildren( 86 | target, 87 | item.children.filter((child): child is VNode => 88 | isVNode(child), 89 | ), 90 | ); 91 | } else if (item.component) { 92 | return appendDescendantComponent(target, item.component); 93 | } 94 | return false; 95 | }) 96 | ); 97 | } 98 | } 99 | 100 | function isValidElement(el: unknown): el is Element | Text { 101 | if (el && isDomNode(el) && !isEmptyNode(el)) { 102 | const rect = getClientRects(el); 103 | return rect.some((item) => item.width || item.height); 104 | } 105 | return false; 106 | } 107 | 108 | function isChildInstance( 109 | target: ComponentInternalInstance, 110 | source: ComponentInternalInstance | null, 111 | ): boolean { 112 | if (source == null) return false; 113 | if (target.uid > source.uid) return false; 114 | if (target.uid === source.uid) return true; 115 | return isChildInstance(target, source.parent); 116 | } 117 | -------------------------------------------------------------------------------- /packages/data-source/src/core/data-source.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler, RequestHandlersMap } from '@alilc/lowcode-types'; 2 | import { 3 | DataSource, 4 | DataSourceConfig, 5 | DataSourceContext, 6 | DataSourceLoader, 7 | DataSourceStatus, 8 | MaybyFunc, 9 | } from './interface'; 10 | import { computed, reactive, ref, shallowRef } from 'vue'; 11 | import { isFunction, isPlainObject, isUndefined } from '@knxcloud/lowcode-utils'; 12 | import { fetchRequest } from '../handlers'; 13 | 14 | export function createDataSource( 15 | config: DataSourceConfig, 16 | { state, setState }: DataSourceContext, 17 | requestHandlersMap: RequestHandlersMap, 18 | ): DataSource { 19 | const data = shallowRef(); 20 | const error = shallowRef(); 21 | const status = ref('init'); 22 | const loading = computed(() => status.value === 'loading'); 23 | const isInit = computed(() => 24 | config.isInit ? exec(config.isInit, state) : false, 25 | ); 26 | const isSync = computed(() => 27 | config.isSync ? exec(config.isSync, state) : false, 28 | ); 29 | 30 | const { 31 | willFetch = same, 32 | shouldFetch = alwaysTrue, 33 | dataHandler = (res: unknown) => res && Reflect.get(res, 'data'), 34 | errorHandler = alwaysThrow, 35 | } = config; 36 | 37 | const load: DataSourceLoader = async (inputParams, otherOptions = {}) => { 38 | try { 39 | const { type, options, id } = config; 40 | const request = getRequestHandler(config, requestHandlersMap); 41 | if (!request) { 42 | throw new Error('unsupport fetch type: ' + type); 43 | } 44 | 45 | if (!shouldFetch()) { 46 | throw new Error(`the ${id} request should not fetch, please check the condition`); 47 | } 48 | 49 | const { inputHeaders = {}, assignToScope = true, ...inputOptions } = otherOptions; 50 | 51 | status.value = 'loading'; 52 | const { params, headers, ...restOptions } = exec(options, state); 53 | const parsedOptions = await willFetch({ 54 | ...restOptions, 55 | ...inputOptions, 56 | params: 57 | isPlainObject(params) && isPlainObject(inputParams) 58 | ? { 59 | ...params, 60 | ...inputParams, 61 | } 62 | : inputParams ?? params, 63 | headers: { 64 | ...(isPlainObject(headers) ? headers : {}), 65 | ...(isPlainObject(inputHeaders) ? inputHeaders : {}), 66 | }, 67 | }); 68 | const res = await request(parsedOptions, { state, setState }); 69 | const _data = (data.value = dataHandler(res as any)); 70 | if (!isUndefined(_data) && assignToScope) { 71 | setState({ [id]: _data }); 72 | } 73 | status.value = 'loading'; 74 | return _data; 75 | } catch (err) { 76 | status.value = 'error'; 77 | error.value = err; 78 | errorHandler(err); 79 | } 80 | }; 81 | 82 | return reactive({ 83 | data, 84 | error, 85 | loading, 86 | status, 87 | isInit, 88 | isSync, 89 | load, 90 | }); 91 | } 92 | 93 | const same = (v: T) => v; 94 | const alwaysTrue = () => true; 95 | const alwaysThrow = (e: unknown) => { 96 | throw e; 97 | }; 98 | 99 | function exec(val: () => T, state?: unknown): T; 100 | function exec(val: MaybyFunc, state?: unknown): T; 101 | function exec(val: MaybyFunc, state?: unknown): T { 102 | if (isFunction(val)) { 103 | return val.call(state, state); 104 | } else if (isPlainObject(val)) { 105 | return Object.keys(val).reduce((res, next) => { 106 | Reflect.set(res!, next, exec(val[next], state)); 107 | return res; 108 | }, {} as T); 109 | } 110 | return val as T; 111 | } 112 | 113 | function getRequestHandler( 114 | config: DataSourceConfig, 115 | requestHandlersMap: RequestHandlersMap, 116 | ): RequestHandler | null { 117 | const { type, requestHandler } = config; 118 | if (type) { 119 | if (type === 'custom' && requestHandler) { 120 | return requestHandler; 121 | } else { 122 | return type === 'fetch' 123 | ? requestHandlersMap[type] ?? fetchRequest 124 | : requestHandlersMap[type] ?? null; 125 | } 126 | } 127 | return fetchRequest; 128 | } 129 | -------------------------------------------------------------------------------- /packages/data-source/__tests__/data-source-engine.spec.ts: -------------------------------------------------------------------------------- 1 | import { noop } from '@knxcloud/lowcode-utils'; 2 | import { createDataSourceEngine } from '../src'; 3 | 4 | describe('data source engine', async () => { 5 | const mockedFetch = vi.fn(); 6 | 7 | beforeEach(() => { 8 | vi.stubGlobal('fetch', mockedFetch); 9 | }); 10 | 11 | afterEach(() => { 12 | vi.unstubAllEnvs(); 13 | }); 14 | 15 | test('nomal request', async () => { 16 | mockedFetch.mockResolvedValueOnce({ 17 | json: () => Promise.resolve(1), 18 | status: 200, 19 | statusText: 'OK', 20 | }); 21 | const state: Record = {}; 22 | const engine = createDataSourceEngine( 23 | { 24 | list: [ 25 | { 26 | id: 'info', 27 | options: () => ({ 28 | uri: 'http://127.0.0.1/info.json', 29 | method: 'POST', 30 | }), 31 | }, 32 | ], 33 | }, 34 | { 35 | state: state, 36 | setState(newState) { 37 | Object.assign(state, newState); 38 | }, 39 | forceUpdate: noop, 40 | } 41 | ); 42 | const res = await engine.dataSourceMap.info.load(); 43 | expect(res).eq(engine.dataSourceMap.info.data).eq(state.info).eq(1); 44 | 45 | expect(mockedFetch).toHaveBeenCalledWith('http://127.0.0.1/info.json', { 46 | body: '{}', 47 | credentials: 'include', 48 | headers: { 49 | Accept: 'application/json', 50 | }, 51 | method: 'POST', 52 | }); 53 | }); 54 | 55 | test('error request', async () => { 56 | mockedFetch.mockResolvedValueOnce({ 57 | json: () => Promise.resolve(1), 58 | status: 500, 59 | statusText: 'Server Internal Error', 60 | }); 61 | const state: Record = {}; 62 | const engine = createDataSourceEngine( 63 | { 64 | list: [ 65 | { 66 | id: 'info', 67 | options: () => ({ 68 | uri: 'http://127.0.0.1/info.json', 69 | method: 'GET', 70 | }), 71 | }, 72 | ], 73 | }, 74 | { 75 | state: state, 76 | setState(newState) { 77 | Object.assign(state, newState); 78 | }, 79 | forceUpdate: noop, 80 | } 81 | ); 82 | expect(() => engine.dataSourceMap.info.load()).rejects.toThrowError( 83 | 'Server Internal Error' 84 | ); 85 | expect(engine.dataSourceMap.info.error).toBeUndefined(); 86 | }); 87 | 88 | test('should fetch', async () => { 89 | mockedFetch.mockResolvedValueOnce({ 90 | json: () => Promise.resolve(1), 91 | status: 200, 92 | statusText: 'OK', 93 | }); 94 | const state: Record = {}; 95 | const engine = createDataSourceEngine( 96 | { 97 | list: [ 98 | { 99 | id: 'info', 100 | options: () => ({ 101 | uri: 'http://127.0.0.1/info.json', 102 | method: 'GET', 103 | }), 104 | shouldFetch: () => false, 105 | }, 106 | ], 107 | }, 108 | { 109 | state: state, 110 | setState(newState) { 111 | Object.assign(state, newState); 112 | }, 113 | forceUpdate: noop, 114 | } 115 | ); 116 | 117 | expect(() => engine.dataSourceMap.info.load()).rejects.toThrowError(); 118 | }); 119 | 120 | test('will fetch', async () => { 121 | mockedFetch.mockResolvedValueOnce({ 122 | json: () => Promise.resolve(1), 123 | status: 200, 124 | statusText: 'OK', 125 | }); 126 | const state: Record = {}; 127 | const engine = createDataSourceEngine( 128 | { 129 | list: [ 130 | { 131 | id: 'info', 132 | options: () => ({ 133 | uri: 'http://127.0.0.1/info.json', 134 | method: 'GET', 135 | }), 136 | willFetch(options) { 137 | return { 138 | ...options, 139 | headers: { 140 | ...options.headers, 141 | testHeader: 'testHeaderValue', 142 | }, 143 | }; 144 | }, 145 | }, 146 | ], 147 | }, 148 | { 149 | state: state, 150 | setState(newState) { 151 | Object.assign(state, newState); 152 | }, 153 | forceUpdate: noop, 154 | } 155 | ); 156 | const res = await engine.dataSourceMap.info.load(); 157 | expect(res).eq(1); 158 | 159 | expect(mockedFetch).toHaveBeenCalledWith('http://127.0.0.1/info.json', { 160 | credentials: 'include', 161 | headers: { 162 | Accept: 'application/json', 163 | testHeader: 'testHeaderValue', 164 | }, 165 | method: 'GET', 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lowcode-engine-vue 2 | 3 | Lowcode Engine Vue 渲染器及适配器实现,点击查看[在线演示](https://knxcloud.github.io/lowcode-engine-demo/) 4 | 5 | > PS: 该项目仅包含画布实现,不能直接运行,如果需要本地查看效果请访问 [DEMO](https://github.com/KNXCloud/lowcode-engine-demo) 仓库 6 | 7 | ## 如何自定义组件 8 | 9 | 我们提供了 `npm init @knxcloud/lowcode@latest` 命令用于初始化一个基础的低代码组件项目,该项目基于 `vue-cli` 构建。项目启动后,会生成一个 `/assets.json` 文件,该文件可直接作为低代码物料的导入入口,部分代码示例如下: 10 | 11 | ```ts 12 | const editorInit = (ctx: ILowCodePluginContext) => { 13 | return { 14 | name: 'editor-init', 15 | async init() { 16 | const { material, project } = ctx; 17 | const assets = await fetch('http://127.0.0.1:9000/assets.json').then((res) => 18 | res.json(), 19 | ); 20 | material.setAssets(assets); 21 | }, 22 | }; 23 | }; 24 | ``` 25 | 26 | ## 和 React 渲染器使用区别 27 | 28 | 使用变量时: 29 | 30 | - `this.props.xxx` -> `this.xxx` 31 | - `this.state.xxx` -> `this.xxx` 32 | 33 | 若直接使用 react 代码编辑器编辑代码,渲染器已做适配: 34 | 35 | - state 内容会自动转化为 vue data 36 | - lifecycle 自动适配为 vue lifecycle 37 | - `componentDidMount` -> `onMounted` 38 | - `componentDidCatch` -> `onErrorCaptured` 39 | - `shouldComponentUpdate` -> `onBeforeUpdate` 40 | - `componentWillUnmount` -> `onBeforeUnmount` 41 | - 其余方法自动转化为 vue methods 42 | 43 | appHelper 暴露给 `this` 的属性都会加上 `$` 前缀,区别于其他属性,如 44 | 45 | - `utils` -> `this.$utils` 46 | - `constants` -> `this.$constants` 47 | 48 | ## Vue 代码编辑器 49 | 50 | 现已支持 [Vue 代码编辑器 @knxcloud/lowcode-plugin-vue-code-editor](https://github.com/KNXCloud/lowcode-engine-plugins/tree/main/packages/plugin-vue-code-editor),支持情况如下 51 | 52 | - [x] ESModule 53 | - [x] import (assets 加载的包,可以使用 `import` 语法导入) 54 | - [x] export default (必须导出一个组件) 55 | - [ ] export 56 | - [x] data 57 | - [x] props 58 | - [x] emits 59 | - [x] computed 60 | - [x] watch 61 | - [x] provide 62 | - [x] inject 63 | - [x] setup 64 | - [x] async setup 65 | - [x] return void 66 | - [x] return object 67 | - [ ] ~~return function~~ 68 | - [x] beforeCreate 69 | - [x] created 70 | - [x] beforeMount 71 | - [x] mounted 72 | - [x] beforeUpdate 73 | - [x] updated 74 | - [x] beforeUnmount 75 | - [x] unmounted 76 | - [ ] activated 77 | - [ ] deactivated 78 | - [x] errorCaptured 79 | - [x] renderTracked 80 | - [x] renderTriggered 81 | - [x] beforeRouteEnter 82 | - [x] beforeRouteUpdate 83 | - [x] beforeRouteLeave 84 | 85 | 对于 v-model 的适配: 86 | 87 | 在 assets 中使用 name 为 `v-model` 或 `v-model:xxx` 的属性会被作为双向绑定特性编译,编译的逻辑为 88 | 89 | ``` 90 | v-model -> modelValue prop + onUpdate:modelValue event 91 | v-model:xxx -> xxx prop + onUpdate:xxx event 92 | ``` 93 | 94 | ### VueRouter 95 | 96 | 若使用了 `beforeRouteEnter`、`beforeRouteUpdate`、`beforeRouteLeave` 钩子,则渲染器在使用时,必须作为 VueRouter 页面使用,使用示例 97 | 98 | ```ts 99 | // router.ts 100 | import { createRouter, createWebHistory } from 'vue-router' 101 | import VueRenderer, { 102 | LOWCODE_ROUTE_META, 103 | setupLowCodeRouteGuard, 104 | } from '@knxcloud/lowcode-vue-renderer' 105 | 106 | const schema = {} // 低代码设计器导出的页面 schema 107 | const components = {} // 组件映射关系对象 108 | 109 | const router = createRouter({ 110 | history: createWebHistory('/'), 111 | routes: [ 112 | { 113 | name: 'lowcode-page' 114 | path: '/lowcode-page-path', 115 | component: VueRenderer, 116 | meta: { 117 | [LOWCODE_ROUTE_META]: schema, 118 | }, 119 | props: { 120 | schema: schema, 121 | components: components, 122 | } 123 | } 124 | ] 125 | }) 126 | 127 | setupLowCodeRouteGuard(router) 128 | 129 | export default router; 130 | ``` 131 | 132 | ### async setup & init dataSource 133 | 134 | 若使用了 `async setup` 或者 `init dataSource`,则需要在渲染器组件外部包裹 `Suspense` 组件,使用方式参考 [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) 135 | 136 | ## 画布使用示例 137 | 138 | ```ts 139 | import { init, project } from '@alilc/lowcode-engine'; 140 | import { setupHostEnvironment } from '@knxcloud/lowcode-utils'; 141 | 142 | setupHostEnvironment(project, 'https://unpkg.com/vue@3.2.47/dist/vue.runtime.global.js'); 143 | 144 | init(document.getElementById('lce'), { 145 | // ... 146 | simulatorUrl: [ 147 | 'https://unpkg.com/@knxcloud/lowcode-vue-simulator-renderer/dist/vue-simulator-renderer.js', 148 | 'https://unpkg.com/@knxcloud/lowcode-vue-simulator-renderer/dist/vue-simulator-renderer.css', 149 | ], 150 | }); 151 | ``` 152 | 153 | > 当不指定版本号时,默认使用最新版,推荐在 cdn 链接上添加适配器具体版本号 154 | 155 | ## 本地调试 156 | 157 | ```bash 158 | git clone git@github.com:KNXCloud/lowcode-engine-vue.git 159 | cd lowcode-engine-vue 160 | pnpm install && pnpm -r build 161 | pnpm start 162 | ``` 163 | 164 | 项目启动后,提供了 umd 文件,可以结合 [DEMO](https://github.com/KNXCloud/lowcode-engine-demo) 项目做调试,文件代理推荐[XSwitch](https://chrome.google.com/webstore/detail/xswitch/idkjhjggpffolpidfkikidcokdkdaogg?hl=en-US), 规则参考: 165 | 166 | ```JSON 167 | { 168 | "proxy": [ 169 | [ 170 | "(?:.*)unpkg.com/@knxcloud/lowcode-vue-simulator-renderer(?:.*)/dist/(.*)", 171 | "http://localhost:5559/$1" 172 | ], 173 | ] 174 | } 175 | ``` 176 | 177 | ## 技术交流 178 | 179 | 微信搜索: cjf395782896,加好友&备注:低代码引擎,申请入群 180 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/vue-router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isFunction, 3 | isObject, 4 | isContainerSchema, 5 | isESModule, 6 | isPromise, 7 | sleep, 8 | } from '@knxcloud/lowcode-utils'; 9 | import type { ComponentPublicInstance } from 'vue'; 10 | import type { 11 | Router, 12 | RouteComponent, 13 | NavigationGuardWithThis, 14 | RouteRecordNormalized, 15 | } from 'vue-router'; 16 | import { SchemaParser, type SchemaParserOptions } from '../../utils'; 17 | 18 | const ADDED_SYMBOL = Symbol(); 19 | 20 | const markAdded = (target: any) => void (target[ADDED_SYMBOL] = true); 21 | 22 | const isAdded = (o: object) => ADDED_SYMBOL in o; 23 | 24 | const isLazyComponent = (o: T | (() => Promise)): o is () => Promise => { 25 | return isFunction(o); 26 | }; 27 | 28 | const routeLifeCycles = ['beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']; 29 | 30 | export interface SetupLowCodeRouteGuardOptions extends SchemaParserOptions { 31 | /** 32 | * @default 'runtimeScope' 33 | */ 34 | scopePath?: string; 35 | /** 36 | * 等待异步 setup 以及 init dataSource 的超时时间,默认为 1 分钟 37 | * @default 60000 ms 38 | */ 39 | timeout?: number; 40 | } 41 | 42 | export const LOWCODE_ROUTE_META = Symbol('LOWCODE_ROUTE_META'); 43 | 44 | function createPathGetter(path: string) { 45 | const segments = path.split('.'); 46 | return (ctx: any) => { 47 | let cur = ctx; 48 | for (let i = 0; i < segments.length && cur; i++) { 49 | cur = cur[segments[i]]; 50 | } 51 | return cur; 52 | }; 53 | } 54 | 55 | export function setupLowCodeRouteGuard( 56 | router: Router, 57 | options?: SetupLowCodeRouteGuardOptions, 58 | ) { 59 | if (isAdded(router)) return; 60 | markAdded(router); 61 | 62 | const timeout = options?.timeout ?? 60000; 63 | const parser = new SchemaParser(options); 64 | const get = createPathGetter(options?.scopePath ?? 'runtimeScope'); 65 | 66 | function wrapRouteComponentGuard( 67 | route: RouteRecordNormalized, 68 | component: RouteComponent, 69 | schema: unknown, 70 | parser: SchemaParser, 71 | ): RouteComponent { 72 | if (!isObject(schema) || !isObject(schema.lifeCycles)) return component; 73 | 74 | const lifeCycles: Record> = {}; 75 | 76 | for (const name of routeLifeCycles) { 77 | const guardSchema = schema.lifeCycles[name]; 78 | const guardFn = parser.parseSchema(guardSchema, false); 79 | if (isFunction(guardFn)) { 80 | lifeCycles[name] = wrapGuardFn(route, guardFn); 81 | } 82 | } 83 | 84 | return Object.keys(lifeCycles).length > 0 85 | ? Object.create(component, Object.getOwnPropertyDescriptors(lifeCycles)) 86 | : component; 87 | } 88 | 89 | function wrapGuardFn( 90 | route: RouteRecordNormalized, 91 | guardFn: (...args: unknown[]) => unknown, 92 | ): NavigationGuardWithThis { 93 | if (guardFn.length < 3) { 94 | return function (from, to) { 95 | const scope = get(this); 96 | return handleRes(guardFn.call(scope, from, to)); 97 | } as NavigationGuardWithThis; 98 | } else { 99 | return function (from, to, next) { 100 | const scope = get(this); 101 | return handleRes(guardFn.call(scope, from, to, next)); 102 | } as NavigationGuardWithThis; 103 | } 104 | } 105 | 106 | const handleRes = (result: unknown): unknown => { 107 | return isFunction(result) 108 | ? async (vm: ComponentPublicInstance) => { 109 | let scope: unknown; 110 | const now = Date.now(); 111 | while (!(scope = get(vm))) { 112 | if (Date.now() - now >= timeout) { 113 | throw new Error('lowcode guard wait timeout'); 114 | } 115 | await sleep(); 116 | } 117 | return result(scope); 118 | } 119 | : isPromise(result) 120 | ? result.then(handleRes) 121 | : result; 122 | }; 123 | 124 | return router.beforeEach((to, _, next) => { 125 | if (to.matched.every((route) => isAdded(route))) { 126 | return next(); 127 | } 128 | Promise.all( 129 | to.matched.map(async (route) => { 130 | if (isAdded(route)) return; 131 | 132 | const components = route.components ?? {}; 133 | const defaultView = components.default; 134 | const schema = route.meta[LOWCODE_ROUTE_META]; 135 | if (defaultView && isContainerSchema(schema)) { 136 | let addedView: RouteComponent; 137 | if (isLazyComponent(defaultView)) { 138 | addedView = await defaultView(); 139 | if (isESModule(addedView)) { 140 | addedView = addedView.default; 141 | } 142 | } else { 143 | addedView = defaultView; 144 | } 145 | components.default = wrapRouteComponentGuard( 146 | route, 147 | addedView, 148 | schema, 149 | parser.initModule(schema), 150 | ); 151 | } 152 | 153 | markAdded(route); 154 | }), 155 | ).then(() => next()); 156 | }); 157 | } 158 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/lifecycles/init-props.ts: -------------------------------------------------------------------------------- 1 | import { 2 | camelCase, 3 | isArray, 4 | isFunction, 5 | isObject, 6 | isString, 7 | } from '@knxcloud/lowcode-utils'; 8 | import { ComponentInternalInstance, Prop, PropType, withCtx } from 'vue'; 9 | import { 10 | warn, 11 | type RuntimeScope, 12 | type SchemaParser, 13 | addToScope, 14 | AccessTypes, 15 | } from '../../utils'; 16 | 17 | function getType(ctor: Prop): string { 18 | const match = ctor && ctor.toString().match(/^\s*function (\w+)/); 19 | return match ? match[1] : ctor === null ? 'null' : ''; 20 | } 21 | 22 | function isSameType(a: Prop, b: Prop): boolean { 23 | return getType(a) === getType(b); 24 | } 25 | 26 | function getTypeIndex( 27 | type: Prop, 28 | expectedTypes: PropType | void | null | true, 29 | ): number { 30 | if (isArray(expectedTypes)) { 31 | return expectedTypes.findIndex((t) => isSameType(t, type)); 32 | } else if (isFunction(expectedTypes)) { 33 | return isSameType(expectedTypes, type) ? 0 : -1; 34 | } 35 | return -1; 36 | } 37 | 38 | export function initProps( 39 | parser: SchemaParser, 40 | schema: unknown, 41 | scope: RuntimeScope, 42 | ): void { 43 | const propsConfig = parser.parseSchema(schema, false); 44 | if ( 45 | !propsConfig || 46 | (!isObject(propsConfig) && !isArray(propsConfig)) || 47 | (isObject(propsConfig) && Object.keys(propsConfig).length === 0) || 48 | (isArray(propsConfig) && propsConfig.length === 0) 49 | ) 50 | return; 51 | 52 | const instance = scope.$; 53 | 54 | const { 55 | propsOptions: [rawPropsOptions, rawNeedCastKeys], 56 | } = instance; 57 | 58 | const propsOptions: Record = {}; 59 | const needCastKeys: string[] = []; 60 | 61 | for (const key in propsConfig) { 62 | const opt = propsConfig[key]; 63 | let normalizedKey: string; 64 | let prop: Record; 65 | 66 | if (isString(opt)) { 67 | normalizedKey = camelCase(opt); 68 | prop = {}; 69 | } else { 70 | normalizedKey = camelCase(key); 71 | prop = 72 | isArray(opt) || isFunction(opt) ? { type: opt } : (opt as Record); 73 | } 74 | 75 | if (rawPropsOptions[normalizedKey]) { 76 | warn('prop ' + normalizedKey + '声明重复'); 77 | continue; 78 | } 79 | 80 | const booleanIndex = getTypeIndex(Boolean, prop.type); 81 | const stringIndex = getTypeIndex(String, prop.type); 82 | 83 | propsOptions[normalizedKey] = { 84 | 0: booleanIndex > -1, 85 | 1: stringIndex < 0 || booleanIndex < stringIndex, 86 | ...prop, 87 | }; 88 | 89 | if (booleanIndex > -1 || 'default' in prop) { 90 | needCastKeys.push(normalizedKey); 91 | } 92 | } 93 | 94 | if (Object.keys(propsOptions).length > 0) { 95 | instance.propsOptions = [ 96 | { ...rawPropsOptions, ...propsOptions }, 97 | [...rawNeedCastKeys, ...needCastKeys], 98 | ]; 99 | 100 | const { props, attrs } = instance; 101 | const propValues = Object.keys(propsOptions).reduce( 102 | (res, key) => { 103 | res[key] = resolvePropValue( 104 | propsOptions, 105 | { ...props, ...res }, 106 | key, 107 | attrs[key], 108 | instance, 109 | needCastKeys.includes(key), 110 | ); 111 | return res; 112 | }, 113 | {} as Record, 114 | ); 115 | 116 | if (Object.keys(propValues).length > 0) { 117 | addToScope(scope, AccessTypes.PROPS, propValues, false, false); 118 | } 119 | } 120 | } 121 | 122 | function resolvePropValue( 123 | options: object, 124 | props: Record, 125 | key: string, 126 | value: unknown, 127 | instance: ComponentInternalInstance, 128 | isAbsent: boolean, 129 | ) { 130 | const opt = options[key]; 131 | if (opt != null) { 132 | const hasDefault = Reflect.has(opt, 'default'); 133 | if (hasDefault && value === undefined) { 134 | const defaultValue = opt.default; 135 | if (opt.type !== Function && !opt.skipFactory && isFunction(defaultValue)) { 136 | const { propsDefaults } = instance; 137 | if (key in propsDefaults) { 138 | value = propsDefaults[key]; 139 | } else { 140 | value = propsDefaults[key] = withCtx( 141 | () => defaultValue.call(null, props), 142 | instance, 143 | )(); 144 | } 145 | } else { 146 | value = defaultValue; 147 | } 148 | } 149 | // boolean casting 150 | if (opt[0]) { 151 | if (isAbsent && !hasDefault) { 152 | value = false; 153 | } else if (opt[1] && (value === '' || value === hyphenate(key))) { 154 | value = true; 155 | } 156 | } 157 | } 158 | return value; 159 | } 160 | 161 | const cacheStringFunction = string>(fn: T): T => { 162 | const cache: Record = Object.create(null); 163 | return ((str: string) => { 164 | const hit = cache[str]; 165 | return hit || (cache[str] = fn(str)); 166 | }) as T; 167 | }; 168 | 169 | const hyphenateRE = /\B([A-Z])/g; 170 | 171 | const hyphenate = cacheStringFunction((str: string) => 172 | str.replace(hyphenateRE, '-$1').toLowerCase(), 173 | ); 174 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/core/leaf/hoc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | shallowRef, 4 | mergeProps, 5 | type InjectionKey, 6 | inject, 7 | provide, 8 | watch, 9 | Fragment, 10 | toRaw, 11 | } from 'vue'; 12 | 13 | import { onUnmounted, defineComponent } from 'vue'; 14 | import { leafProps } from '../base'; 15 | import { 16 | buildSchema, 17 | isFragment, 18 | splitLeafProps, 19 | useLeaf, 20 | type SlotSchemaMap, 21 | } from '../use'; 22 | import { useRendererContext } from '@knxcloud/lowcode-hooks'; 23 | import type { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; 24 | import { debounce, exportSchema, isJSSlot } from '@knxcloud/lowcode-utils'; 25 | 26 | const HOC_NODE_KEY: InjectionKey<{ rerenderSlots: () => void }> = Symbol('hocNode'); 27 | const useHocNode = (rerenderSlots: () => void) => { 28 | const { rerender } = useRendererContext(); 29 | const parentNode = inject(HOC_NODE_KEY, null); 30 | 31 | const debouncedRerender = debounce(rerenderSlots); 32 | 33 | provide(HOC_NODE_KEY, { 34 | rerenderSlots: debouncedRerender, 35 | }); 36 | 37 | if (!parentNode) { 38 | return { 39 | rerender: debouncedRerender, 40 | rerenderRoot: rerender, 41 | rerenderParent: rerender, 42 | }; 43 | } else { 44 | return { 45 | rerender: debouncedRerender, 46 | rerenderRoot: rerender, 47 | rerenderParent: parentNode.rerenderSlots, 48 | }; 49 | } 50 | }; 51 | 52 | export const Hoc = defineComponent({ 53 | name: 'Hoc', 54 | inheritAttrs: false, 55 | props: leafProps, 56 | setup(props, { slots, attrs }) { 57 | const showNode = shallowRef(true); 58 | const nodeSchema = shallowRef(props.__schema); 59 | const slotSchema = shallowRef(); 60 | 61 | const updateSchema = (newSchema: IPublicTypeNodeSchema) => { 62 | nodeSchema.value = newSchema; 63 | slotSchema.value = buildSchema(newSchema, node).slots; 64 | }; 65 | const { rerender, rerenderRoot, rerenderParent } = useHocNode(() => { 66 | const newSchema = node ? exportSchema(node) : null; 67 | newSchema && updateSchema(newSchema); 68 | }); 69 | 70 | const listenRecord: Record void> = {}; 71 | onUnmounted(() => 72 | Object.keys(listenRecord).forEach((k) => { 73 | listenRecord[k](); 74 | delete listenRecord[k]; 75 | }), 76 | ); 77 | 78 | const { locked, node, buildSlots, getNode, isRootNode } = useLeaf( 79 | props, 80 | (schema, show) => { 81 | const id = schema.id; 82 | if (id) { 83 | if (show && listenRecord[id]) { 84 | listenRecord[id](); 85 | delete listenRecord[id]; 86 | } else if (!show && !listenRecord[id]) { 87 | const childNode = getNode(id); 88 | if (childNode) { 89 | const cancelVisibleChange = childNode.onVisibleChange(() => rerender()); 90 | const cancelPropsChange = childNode.onPropChange(() => rerender()); 91 | listenRecord[id] = () => { 92 | cancelVisibleChange(); 93 | cancelPropsChange(); 94 | }; 95 | } 96 | } 97 | } 98 | }, 99 | ); 100 | 101 | if (node) { 102 | const cancel = node.onChildrenChange(() => { 103 | // 默认插槽内容变更,无法确定变更的层级,所以只能全部更新 104 | rerenderRoot(); 105 | }); 106 | cancel && onUnmounted(cancel); 107 | onUnmounted( 108 | node.onPropChange((info) => { 109 | const { key, prop, newValue, oldValue } = info; 110 | const isRootProp = prop.path.length === 1; 111 | if (isRootProp) { 112 | if (key === '___isLocked___') { 113 | locked.value = newValue; 114 | } else if (isJSSlot(newValue) || isJSSlot(oldValue)) { 115 | // 插槽内容变更,无法确定变更的层级,所以只能全部更新 116 | rerenderRoot(); 117 | } else { 118 | // 普通属性更新,通知父级重新渲染 119 | rerenderParent(); 120 | } 121 | } else { 122 | // 普通属性更新,通知父级重新渲染 123 | rerenderParent(); 124 | } 125 | }), 126 | ); 127 | onUnmounted( 128 | node.onVisibleChange((visible: boolean) => { 129 | isRootNode 130 | ? // 当前节点为根节点(Page),直接隐藏 131 | (showNode.value = visible) 132 | : // 当前节点显示隐藏发生改变,通知父级组件重新渲染子组件 133 | rerenderParent(); 134 | }), 135 | ); 136 | updateSchema(exportSchema(node)); 137 | } 138 | 139 | watch( 140 | () => props.__schema, 141 | (newSchema) => updateSchema(newSchema), 142 | ); 143 | 144 | return () => { 145 | const comp = toRaw(props.__comp); 146 | const scope = toRaw(props.__scope); 147 | const vnodeProps = { ...props.__vnodeProps }; 148 | const compProps = splitLeafProps(attrs)[1]; 149 | if (isRootNode && !showNode.value) return null; 150 | 151 | const builtSlots = slotSchema.value 152 | ? buildSlots(slotSchema.value, scope, node) 153 | : slots; 154 | 155 | return comp 156 | ? isFragment(comp) 157 | ? h(Fragment, builtSlots.default?.()) 158 | : h(comp, mergeProps(compProps, vnodeProps), builtSlots) 159 | : h('div', 'component not found'); 160 | }; 161 | }, 162 | }); 163 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/helpers/node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type { 3 | IPublicEnumTransformStage, 4 | IPublicTypeComponentMetadata, 5 | IPublicTypeContainerSchema, 6 | IPublicTypeNodeSchema, 7 | IPublicTypeSlotSchema, 8 | } from '@alilc/lowcode-types'; 9 | import * as uuid from 'uuid'; 10 | import { INode } from '@knxcloud/lowcode-hooks'; 11 | import { set, get, cloneDeep } from 'lodash'; 12 | import { isArray, isNodeSchema, isString } from '@knxcloud/lowcode-utils'; 13 | import { Ref, shallowRef } from 'vue'; 14 | 15 | const internalPropsRegexp = /^__(\w+)__$/; 16 | 17 | export function createNode( 18 | rootSchema: Ref, 19 | schemaPath: string | undefined, 20 | meta: Partial = {}, 21 | nodeMaps: Record = {} 22 | ): INode { 23 | const visibleChange: CallableFunction[] = []; 24 | const propChangeCbs: CallableFunction[] = []; 25 | const childrenChangeCbs: CallableFunction[] = []; 26 | return { 27 | get id() { 28 | return this.schema.id; 29 | }, 30 | get schema() { 31 | return schemaPath == null ? rootSchema.value : get(rootSchema.value, schemaPath); 32 | }, 33 | get isContainerNode() { 34 | return Array.isArray(meta.configure) 35 | ? false 36 | : !!meta.configure?.component?.isContainer; 37 | }, 38 | getProp(path: string | number, createIfNode?: boolean) { 39 | const value = get(this.schema.props ?? {}, path); 40 | if (value.type === 'JSSlot') { 41 | let slotNode: INode; 42 | if (!value.__cached_id || !(slotNode = nodeMaps[value.__cached_id]!)) { 43 | const slotNodeId = (value.__cached_id = uuid.v4()); 44 | slotNode = nodeMaps[slotNodeId] = createNode( 45 | shallowRef({ 46 | componentName: 'Slot', 47 | params: value.params ?? [], 48 | children: value.value, 49 | } as const), 50 | undefined, 51 | { 52 | configure: { 53 | component: { 54 | isContainer: true, 55 | }, 56 | }, 57 | } 58 | ); 59 | } 60 | return slotNode; 61 | } 62 | return null; 63 | }, 64 | exportSchema(stage: IPublicEnumTransformStage, options?: any) { 65 | return this.schema; 66 | }, 67 | replaceChild(node, data) { 68 | const { children, ...restSchema } = this.schema; 69 | if (isArray(children)) { 70 | const idx = children.findIndex((item) => { 71 | return isNodeSchema(item) && item.id === node.id; 72 | }); 73 | if (idx >= 0) { 74 | const newChildren = children.slice(); 75 | newChildren.splice(idx, 1, data); 76 | set(rootSchema.value, schemaPath, { 77 | ...restSchema, 78 | children: newChildren, 79 | }); 80 | } else { 81 | set(rootSchema.value, schemaPath, { 82 | ...restSchema, 83 | children: [...children, data], 84 | }); 85 | } 86 | } else if (isNodeSchema(children)) { 87 | if (children.id === node.id) { 88 | set(rootSchema.value, schemaPath, { ...restSchema, children: data }); 89 | } else { 90 | set(rootSchema.value, schemaPath, { 91 | ...restSchema, 92 | children: [{ ...children, id: uuid.v4(), data }], 93 | }); 94 | } 95 | } else { 96 | set(rootSchema.value, schemaPath, { 97 | ...restSchema, 98 | children: [children, data], 99 | }); 100 | } 101 | rootSchema.value = cloneDeep(rootSchema.value); 102 | childrenChangeCbs.forEach((cb) => cb()); 103 | }, 104 | setPropValue(path, value) { 105 | const schema = this.schema; 106 | const internalPropMatched = isString(path) ? internalPropsRegexp.exec(path) : null; 107 | let oldValue: any; 108 | if (internalPropMatched) { 109 | const internalPropName = internalPropMatched[1]; 110 | oldValue = schema[internalPropName]; 111 | set(schema, internalPropName, value); 112 | set(rootSchema.value, schemaPath, { ...schema }); 113 | } else { 114 | const props = cloneDeep(schema.props ?? {}); 115 | oldValue = get(props, path); 116 | set(props, path, value); 117 | set(rootSchema.value, schemaPath, { ...schema, props }); 118 | } 119 | const parts = path.toString().split('.'); 120 | const info = { 121 | key: parts[parts.length - 1], 122 | prop: { path: parts }, 123 | newValue: value, 124 | oldValue: oldValue, 125 | }; 126 | rootSchema.value = cloneDeep(rootSchema.value); 127 | propChangeCbs.forEach((cb) => cb(info)); 128 | }, 129 | onPropChange(func) { 130 | propChangeCbs.push(func); 131 | return () => { 132 | const idx = propChangeCbs.indexOf(func); 133 | idx >= 0 && propChangeCbs.splice(idx, 1); 134 | }; 135 | }, 136 | onVisibleChange(func) { 137 | visibleChange.push(func); 138 | return () => { 139 | const idx = visibleChange.indexOf(func); 140 | idx >= 0 && visibleChange.splice(idx, 1); 141 | }; 142 | }, 143 | onChildrenChange(func) { 144 | childrenChangeCbs.push(func); 145 | return () => { 146 | const idx = childrenChangeCbs.indexOf(func); 147 | idx >= 0 && childrenChangeCbs.splice(idx, 1); 148 | }; 149 | }, 150 | } as INode; 151 | } 152 | -------------------------------------------------------------------------------- /packages/data-source/src/handlers/fetch.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeOptionsConfig } from '@alilc/lowcode-types'; 2 | import { isPlainObject, isString, toString } from '@knxcloud/lowcode-utils'; 3 | 4 | function isFormData(o: unknown): o is FormData { 5 | return toString(o) === '[object FormData]'; 6 | } 7 | 8 | function serializeParams(obj: Record) { 9 | const result: string[] = []; 10 | const applyItem = (key: string, val: unknown) => { 11 | if (val === null || val === undefined || val === '') { 12 | return; 13 | } 14 | if (typeof val === 'object') { 15 | result.push(`${key}=${encodeURIComponent(JSON.stringify(val))}`); 16 | } else { 17 | result.push(`${key}=${encodeURIComponent(String(val))}`); 18 | } 19 | }; 20 | if (isFormData(obj)) { 21 | obj.forEach((val, key) => applyItem(key, val)); 22 | } else { 23 | Object.keys(obj).forEach((key) => applyItem(key, obj[key])); 24 | } 25 | return result.join('&'); 26 | } 27 | 28 | function buildUrl(dataAPI: string, params?: Record) { 29 | if (!params) return dataAPI; 30 | const paramStr = serializeParams(params); 31 | if (paramStr) { 32 | return dataAPI.indexOf('?') > 0 ? `${dataAPI}&${paramStr}` : `${dataAPI}?${paramStr}`; 33 | } 34 | return dataAPI; 35 | } 36 | 37 | function find(o: Record, k: string): [string, string] | [] { 38 | for (const key in o) { 39 | if (key.toLowerCase() === k) { 40 | return [o[key], key]; 41 | } 42 | } 43 | return []; 44 | } 45 | 46 | function isValidResponseType(type: unknown): type is ResponseType { 47 | return ( 48 | isString(type) && ['arrayBuffer', 'blob', 'formData', 'json', 'text'].includes(type) 49 | ); 50 | } 51 | 52 | function createFormData(data: Record): FormData { 53 | const formData = new FormData(); 54 | for (const key in data) { 55 | const value = data[key]; 56 | if (value instanceof Blob) { 57 | formData.append(key, value); 58 | } else { 59 | formData.append(key, String(value)); 60 | } 61 | } 62 | return formData; 63 | } 64 | 65 | const bodyParseStrategies: Record) => BodyInit> = { 66 | 'application/json': (data) => JSON.stringify(data), 67 | 'multipart/form-data': (data) => (isPlainObject(data) ? createFormData(data) : data), 68 | 'application/x-www-form-urlencoded': (data) => serializeParams(data), 69 | }; 70 | 71 | function parseRequestBody(contentType: string, data: Record): BodyInit { 72 | const parser = Object.keys(bodyParseStrategies).find((key) => 73 | contentType.includes(key), 74 | ); 75 | return parser ? bodyParseStrategies[parser](data) : (data as unknown as BodyInit); 76 | } 77 | 78 | export type ResponseType = 'blob' | 'arrayBuffer' | 'formData' | 'text' | 'json'; 79 | 80 | export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS'; 81 | 82 | export type RequestParams = Record; 83 | 84 | export class RequestError extends Error { 85 | constructor( 86 | message: string, 87 | public code: number, 88 | public data?: T, 89 | ) { 90 | super(message); 91 | } 92 | } 93 | 94 | export class Response { 95 | constructor( 96 | public code: number, 97 | public data: T, 98 | ) {} 99 | } 100 | 101 | export async function request(options: RuntimeOptionsConfig): Promise { 102 | const { 103 | uri, 104 | method, 105 | timeout, 106 | params = {}, 107 | headers = {}, 108 | isCors, 109 | responseType = 'json', 110 | ...restOptions 111 | } = options; 112 | 113 | let url: string; 114 | const requestHeaders: Record = { 115 | Accept: 'application/json', 116 | ...headers, 117 | }; 118 | 119 | const fetchOptions: RequestInit = { 120 | method, 121 | headers: requestHeaders, 122 | credentials: 'include', 123 | ...restOptions, 124 | }; 125 | 126 | isCors && (fetchOptions.mode = 'cors'); 127 | 128 | if (method === 'GET' || method === 'DELETE' || method === 'OPTIONS') { 129 | url = buildUrl(uri, params); 130 | } else { 131 | url = uri; 132 | const [contentType, key] = find(requestHeaders, 'content-type'); 133 | fetchOptions.body = parseRequestBody(contentType ?? 'application/json', params); 134 | 135 | if (contentType === 'multipart/form-data') { 136 | key && delete requestHeaders[key]; 137 | } 138 | } 139 | 140 | if (timeout) { 141 | const controller = new AbortController(); 142 | fetchOptions.signal = controller.signal; 143 | setTimeout(() => controller.abort(), timeout); 144 | } 145 | 146 | const res = await fetch(url, fetchOptions); 147 | const code = res.status; 148 | 149 | if (code >= 200 && code < 300) { 150 | if (code === 204) { 151 | if (method === 'DELETE') { 152 | return new Response(code, null); 153 | } else { 154 | throw new RequestError(res.statusText, code); 155 | } 156 | } else { 157 | if (!isValidResponseType(responseType)) { 158 | throw new RequestError(`invalid response type: ${responseType}`, -1); 159 | } 160 | return new Response(code, await res[responseType]()); 161 | } 162 | } else if (code >= 400) { 163 | try { 164 | const data = await res.json(); 165 | throw new RequestError(res.statusText, code, data); 166 | } catch { 167 | throw new RequestError(res.statusText, code); 168 | } 169 | } 170 | throw new RequestError(res.statusText, code); 171 | } 172 | 173 | export function createFetchRequest(defaultOptions: Partial) { 174 | return (options: RuntimeOptionsConfig) => request({ ...defaultOptions, ...options }); 175 | } 176 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/parse.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IPublicTypeI18nData as I18nData, 3 | IPublicTypeJSFunction as JSFunction, 4 | IPublicTypeJSExpression as JSExpression, 5 | IPublicTypeContainerSchema, 6 | } from '@alilc/lowcode-types'; 7 | import type { BlockScope, RuntimeScope } from './scope'; 8 | import { 9 | isArray, 10 | isFunction, 11 | isI18nData, 12 | isJSExpression, 13 | isJSFunction, 14 | isPlainObject, 15 | isString, 16 | } from '@knxcloud/lowcode-utils'; 17 | import { ensureArray } from './array'; 18 | 19 | export const EXPRESSION_TYPE = { 20 | JSEXPRESSION: 'JSExpression', 21 | JSFUNCTION: 'JSFunction', 22 | JSSLOT: 'JSSlot', 23 | JSBLOCK: 'JSBlock', 24 | I18N: 'i18n', 25 | } as const; 26 | 27 | export interface SchemaParserOptions { 28 | thisRequired?: boolean; 29 | } 30 | 31 | export class SchemaParser { 32 | static cacheModules: Record = {}; 33 | static cleanCachedModules() { 34 | this.cacheModules = {}; 35 | } 36 | private createFunction: (code: string) => CallableFunction; 37 | private exports = {}; 38 | 39 | constructor(options?: SchemaParserOptions) { 40 | this.createFunction = 41 | options && !options.thisRequired 42 | ? (code) => 43 | new Function( 44 | '__exports__', 45 | '__scope__', 46 | `with(__exports__) { with(__scope__) { ${code} } }`, 47 | ) 48 | : (code) => new Function('__exports__', `with(__exports__) { ${code} }`); 49 | } 50 | 51 | initModule(schema: IPublicTypeContainerSchema) { 52 | const initModuleSchema = schema?.lifeCycles?.initModule; 53 | const res = initModuleSchema 54 | ? this.parseSchema(initModuleSchema, false) 55 | : initModuleSchema; 56 | this.exports = isFunction(res) ? res(SchemaParser.cacheModules, window) : {}; 57 | return this; 58 | } 59 | 60 | parseSlotScope(args: unknown[], params: string[]): BlockScope { 61 | const slotParams: BlockScope = {}; 62 | ensureArray(params).forEach((item, idx) => { 63 | slotParams[item] = args[idx]; 64 | }); 65 | return slotParams; 66 | } 67 | parseI18n(i18nInfo: I18nData, scope?: RuntimeScope | boolean) { 68 | return this.parseExpression( 69 | { 70 | type: EXPRESSION_TYPE.JSEXPRESSION, 71 | value: `this.$t(${JSON.stringify(i18nInfo.key)})`, 72 | }, 73 | scope, 74 | ) as string | undefined; 75 | } 76 | 77 | parseSchema(schema: I18nData, scope?: RuntimeScope | boolean): string | undefined; 78 | parseSchema(schema: JSFunction, scope?: RuntimeScope | boolean): CallableFunction; 79 | parseSchema(schema: JSExpression, scope?: RuntimeScope | boolean): unknown; 80 | parseSchema( 81 | schema: T, 82 | scope: RuntimeScope | boolean, 83 | ): { 84 | [K in keyof T]: T[K] extends I18nData 85 | ? string 86 | : T[K] extends JSFunction 87 | ? CallableFunction 88 | : T[K] extends JSExpression 89 | ? unknown 90 | : T[K] extends JSExpression | JSFunction 91 | ? CallableFunction | unknown 92 | : T[K]; 93 | }; 94 | parseSchema(schema: unknown, scope?: RuntimeScope | boolean): T; 95 | parseSchema(schema: unknown, scope?: RuntimeScope | boolean): unknown { 96 | if (isJSExpression(schema) || isJSFunction(schema)) { 97 | return this.parseExpression(schema, scope); 98 | } else if (isI18nData(schema)) { 99 | return this.parseI18n(schema, scope); 100 | } else if (isString(schema)) { 101 | return schema.trim(); 102 | } else if (isArray(schema)) { 103 | return schema.map((item) => this.parseSchema(item, scope)); 104 | } else if (isFunction(schema)) { 105 | return schema.bind(scope); 106 | } else if (isPlainObject(schema)) { 107 | if (!schema) return schema; 108 | const res: Record = {}; 109 | Object.keys(schema).forEach((key) => { 110 | if (key.startsWith('__')) return; 111 | res[key] = this.parseSchema(schema[key], scope); 112 | }); 113 | return res; 114 | } 115 | return schema; 116 | } 117 | 118 | parseOnlyJsValue(schema: unknown): T; 119 | parseOnlyJsValue(schema: unknown): unknown; 120 | parseOnlyJsValue(schema: unknown): unknown { 121 | if (isJSExpression(schema) || isI18nData(schema)) { 122 | return undefined; 123 | } else if (isJSFunction(schema)) { 124 | return this.parseExpression(schema, false); 125 | } else if (isArray(schema)) { 126 | return schema.map((item) => this.parseOnlyJsValue(item)); 127 | } else if (isPlainObject(schema)) { 128 | return Object.keys(schema).reduce( 129 | (res, key) => { 130 | if (key.startsWith('__')) return res; 131 | res[key] = this.parseOnlyJsValue(schema[key]); 132 | return res; 133 | }, 134 | {} as Record, 135 | ); 136 | } 137 | return schema; 138 | } 139 | 140 | parseExpression(str: JSFunction, scope?: RuntimeScope | boolean): CallableFunction; 141 | parseExpression(str: JSExpression, scope?: RuntimeScope | boolean): unknown; 142 | parseExpression( 143 | str: JSExpression | JSFunction, 144 | scope?: RuntimeScope | boolean, 145 | ): CallableFunction | unknown; 146 | parseExpression( 147 | str: JSExpression | JSFunction, 148 | scope?: RuntimeScope | boolean, 149 | ): CallableFunction | unknown { 150 | try { 151 | const contextArr = ['"use strict";']; 152 | let tarStr: string; 153 | 154 | tarStr = (str.value || '').trim(); 155 | 156 | if (scope !== false && !tarStr.match(/^\([^)]*\)\s*=>/)) { 157 | tarStr = tarStr.replace(/this(\W|$)/g, (_a: string, b: string) => `__self${b}`); 158 | contextArr.push('var __self = arguments[1];'); 159 | } 160 | contextArr.push('return '); 161 | tarStr = contextArr.join('\n') + tarStr; 162 | const fn = this.createFunction(tarStr); 163 | return fn(this.exports, scope || {}); 164 | } catch (err) { 165 | console.warn('parseExpression.error', err, str, self); 166 | return undefined; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/utils/scope.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, proxyRefs, type ComponentPublicInstance } from 'vue'; 2 | import type { MaybeArray } from './array'; 3 | import { isProxy, reactive } from 'vue'; 4 | import { isBoolean, isObject, isUndefined } from '@knxcloud/lowcode-utils'; 5 | import { warn } from './warn'; 6 | import { SchemaParser } from './parse'; 7 | import { DataSource } from '@knxcloud/lowcode-data-source'; 8 | 9 | export interface BlockScope { 10 | [x: string | symbol]: unknown; 11 | } 12 | 13 | declare module 'vue' { 14 | export interface ComponentInternalInstance { 15 | ctx: Record; 16 | setupState: Record; 17 | emitsOptions: Record unknown) | null>; 18 | propsOptions: [Record, string[]]; 19 | accessCache: Record; 20 | propsDefaults: Record; 21 | } 22 | } 23 | 24 | export interface RuntimeScope extends BlockScope, ComponentPublicInstance { 25 | i18n(key: string, values: any): string; 26 | currentLocale: string; 27 | dataSourceMap: Record; 28 | reloadDataSource(): Promise; 29 | __parser: SchemaParser; 30 | __thisRequired: boolean; 31 | __loopScope?: boolean; 32 | __loopRefIndex?: number; 33 | __loopRefOffset?: number; 34 | } 35 | 36 | export const enum AccessTypes { 37 | OTHER, 38 | SETUP, 39 | DATA, 40 | PROPS, 41 | CONTEXT, 42 | } 43 | 44 | export function getAccessTarget( 45 | scope: RuntimeScope, 46 | accessType: AccessTypes, 47 | ): Record { 48 | switch (accessType) { 49 | case AccessTypes.SETUP: 50 | return scope.$.setupState.__lcSetup 51 | ? scope.$.setupState 52 | : (scope.$.setupState = proxyRefs( 53 | Object.create(null, { 54 | __lcSetup: { 55 | get: () => true, 56 | enumerable: false, 57 | configurable: false, 58 | }, 59 | }), 60 | )); 61 | case AccessTypes.DATA: 62 | return isReactive(scope.$.data) ? scope.$.data : (scope.$.data = reactive({})); 63 | case AccessTypes.PROPS: 64 | return scope.$.props; 65 | default: 66 | return scope.$.ctx; 67 | } 68 | } 69 | 70 | export function addToScope( 71 | scope: RuntimeScope, 72 | accessType: AccessTypes, 73 | source: object, 74 | useDefineProperty?: boolean, 75 | buildPropsOptions = true, 76 | ): void { 77 | const instance = scope.$; 78 | const target = getAccessTarget(scope, accessType); 79 | if (useDefineProperty) { 80 | const descriptors = Object.getOwnPropertyDescriptors(source); 81 | for (const key in descriptors) { 82 | if (key in target) { 83 | warn('重复定义 key: ' + key); 84 | continue; 85 | } 86 | Object.defineProperty(target, key, descriptors[key]); 87 | instance.accessCache[key] = accessType; 88 | } 89 | } else { 90 | for (const key in source) { 91 | if (key in target) { 92 | warn('重复定义 key: ' + key); 93 | continue; 94 | } 95 | target[key] = Reflect.get(source, key); 96 | instance.accessCache[key] = accessType; 97 | } 98 | } 99 | if ( 100 | accessType === AccessTypes.PROPS && 101 | Object.keys(source).length > 0 && 102 | buildPropsOptions 103 | ) { 104 | const { 105 | propsOptions: [rawPropsOptions, rawNeedCastKeys], 106 | } = instance; 107 | const propsOptions: Record = {}; 108 | const needCastKeys: string[] = []; 109 | for (const key in source) { 110 | if (rawPropsOptions[key]) continue; 111 | 112 | const val = Reflect.get(source, key); 113 | if (isBoolean(val)) { 114 | propsOptions[key] = { 115 | // 不传入值时默认为 val 116 | 0: true, 117 | // passVal === '' || passVal === key 时需要转化为 true 118 | 1: true, 119 | type: Boolean, 120 | default: val, 121 | }; 122 | needCastKeys.push(key); 123 | } else if (!isUndefined(val)) { 124 | propsOptions[key] = { 125 | // 不传入值时默认为 val 126 | 0: true, 127 | 1: false, 128 | type: null, 129 | default: val, 130 | }; 131 | needCastKeys.push(key); 132 | } else { 133 | propsOptions[key] = { 134 | 0: false, 135 | 1: false, 136 | type: null, 137 | }; 138 | } 139 | } 140 | 141 | if (Object.keys(propsOptions).length > 0) { 142 | instance.propsOptions = [ 143 | { ...rawPropsOptions, ...propsOptions }, 144 | [...rawNeedCastKeys, ...needCastKeys], 145 | ]; 146 | } 147 | } 148 | } 149 | 150 | export function isRuntimeScope(scope: object): scope is RuntimeScope { 151 | return '$' in scope; 152 | } 153 | 154 | export function isValidScope(scope: unknown): scope is BlockScope | RuntimeScope { 155 | // 为 null、undefined,或者不是对象 156 | if (!scope || !isObject(scope)) return false; 157 | 158 | // runtime scope 159 | if (isRuntimeScope(scope)) return true; 160 | 161 | // scope 属性不为空 162 | if (Object.keys(scope).length > 0) return true; 163 | return false; 164 | } 165 | 166 | export function mergeScope( 167 | scope: RuntimeScope, 168 | ...blockScope: MaybeArray[] 169 | ): RuntimeScope; 170 | export function mergeScope( 171 | ...blockScope: MaybeArray[] 172 | ): BlockScope; 173 | export function mergeScope( 174 | ...scopes: MaybeArray[] 175 | ): RuntimeScope | BlockScope { 176 | const normalizedScope: (RuntimeScope | BlockScope)[] = []; 177 | scopes.flat().forEach((scope) => { 178 | isValidScope(scope) && normalizedScope.push(scope); 179 | }); 180 | 181 | if (normalizedScope.length <= 1) return normalizedScope[0]; 182 | 183 | const [rootScope, ...resScopes] = normalizedScope; 184 | return resScopes.reduce((result, scope) => { 185 | if (isRuntimeScope(scope)) { 186 | if (!isRuntimeScope(result)) { 187 | const temp = result; 188 | result = scope; 189 | scope = temp; 190 | } else { 191 | return scope; 192 | } 193 | } 194 | 195 | const descriptors = Object.getOwnPropertyDescriptors(scope); 196 | result = Object.create(result, descriptors); 197 | return isProxy(scope) ? reactive(result) : result; 198 | }, rootScope); 199 | } 200 | -------------------------------------------------------------------------------- /packages/utils/src/asset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * copy from https://github.com/alibaba/lowcode-engine/blob/main/packages/utils/src/asset.ts 3 | */ 4 | import type { Asset, AssetBundle, AssetItem, AssetList } from '@alilc/lowcode-types'; 5 | import { AssetLevel, AssetLevels, AssetType } from '@alilc/lowcode-types/es/assets'; 6 | import { isArray, isCSSUrl } from './check'; 7 | import { createDefer } from './create-defer'; 8 | import { evaluate, load } from './script'; 9 | 10 | export { AssetLevel, AssetLevels, AssetType }; 11 | 12 | export function isAssetItem(obj: any): obj is AssetItem { 13 | return obj && !!obj.type; 14 | } 15 | 16 | export function isAssetBundle(obj: any): obj is AssetBundle { 17 | return obj && obj.type === AssetType.Bundle; 18 | } 19 | 20 | export function assetItem( 21 | type: AssetType, 22 | content?: string | null, 23 | level?: AssetLevel, 24 | id?: string, 25 | ): AssetItem | null { 26 | return content ? { type, content, level, id } : null; 27 | } 28 | 29 | function parseAssetList( 30 | scripts: any, 31 | styles: any, 32 | assets: AssetList, 33 | level?: AssetLevel, 34 | ) { 35 | for (const asset of assets) { 36 | parseAsset(scripts, styles, asset, level); 37 | } 38 | } 39 | 40 | function parseAsset( 41 | scripts: any, 42 | styles: any, 43 | asset: Asset | undefined | null, 44 | level?: AssetLevel, 45 | ) { 46 | if (!asset) { 47 | return; 48 | } 49 | if (isArray(asset)) { 50 | return parseAssetList(scripts, styles, asset, level); 51 | } 52 | 53 | if (isAssetBundle(asset)) { 54 | if (asset.assets) { 55 | if (isArray(asset.assets)) { 56 | parseAssetList(scripts, styles, asset.assets, asset.level || level); 57 | } else { 58 | parseAsset(scripts, styles, asset.assets, asset.level || level); 59 | } 60 | return; 61 | } 62 | return; 63 | } 64 | 65 | if (!isAssetItem(asset)) { 66 | asset = assetItem( 67 | isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, 68 | asset, 69 | level, 70 | )!; 71 | } 72 | 73 | let lv = asset.level || level; 74 | 75 | if (!lv || AssetLevel[lv] == null) { 76 | lv = AssetLevel.App; 77 | } 78 | 79 | asset.level = lv; 80 | if (asset.type === AssetType.CSSUrl || asset.type == AssetType.CSSText) { 81 | styles[lv].push(asset); 82 | } else { 83 | scripts[lv].push(asset); 84 | } 85 | } 86 | 87 | export class StylePoint { 88 | private lastContent: string | undefined; 89 | 90 | private lastUrl: string | undefined; 91 | 92 | private placeholder: Node; 93 | 94 | constructor( 95 | public readonly level: number, 96 | public readonly id?: string, 97 | ) { 98 | let placeholder: Node | null = null; 99 | if (id) { 100 | placeholder = document.head.querySelector(`style[data-id="${id}"]`); 101 | } 102 | if (!placeholder) { 103 | placeholder = document.createTextNode(''); 104 | const meta = document.head.querySelector(`meta[level="${level}"]`); 105 | if (meta) { 106 | document.head.insertBefore(placeholder, meta); 107 | } else { 108 | document.head.appendChild(placeholder); 109 | } 110 | } 111 | this.placeholder = placeholder; 112 | } 113 | 114 | applyText(content: string) { 115 | if (this.lastContent === content) { 116 | return; 117 | } 118 | this.lastContent = content; 119 | this.lastUrl = undefined; 120 | const element = document.createElement('style'); 121 | element.setAttribute('type', 'text/css'); 122 | if (this.id) { 123 | element.setAttribute('data-id', this.id); 124 | } 125 | element.appendChild(document.createTextNode(content)); 126 | document.head.insertBefore( 127 | element, 128 | this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null, 129 | ); 130 | document.head.removeChild(this.placeholder); 131 | this.placeholder = element; 132 | } 133 | 134 | applyUrl(url: string) { 135 | if (this.lastUrl === url) { 136 | return; 137 | } 138 | this.lastContent = undefined; 139 | this.lastUrl = url; 140 | const element = document.createElement('link'); 141 | element.onload = onload; 142 | element.onerror = onload; 143 | 144 | const i = createDefer(); 145 | function onload(e: any) { 146 | element.onload = null; 147 | element.onerror = null; 148 | if (e.type === 'load') { 149 | i.resolve(); 150 | } else { 151 | i.reject(); 152 | } 153 | } 154 | 155 | element.href = url; 156 | element.rel = 'stylesheet'; 157 | if (this.id) { 158 | element.setAttribute('data-id', this.id); 159 | } 160 | document.head.insertBefore( 161 | element, 162 | this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null, 163 | ); 164 | document.head.removeChild(this.placeholder); 165 | this.placeholder = element; 166 | return i.promise(); 167 | } 168 | } 169 | 170 | export class AssetLoader { 171 | async load(asset: Asset) { 172 | const styles: any = {}; 173 | const scripts: any = {}; 174 | AssetLevels.forEach((lv) => { 175 | styles[lv] = []; 176 | scripts[lv] = []; 177 | }); 178 | parseAsset(scripts, styles, asset); 179 | const styleQueue: AssetItem[] = styles[AssetLevel.Environment].concat( 180 | styles[AssetLevel.Library], 181 | styles[AssetLevel.Theme], 182 | styles[AssetLevel.Runtime], 183 | styles[AssetLevel.App], 184 | ); 185 | const scriptQueue: AssetItem[] = scripts[AssetLevel.Environment].concat( 186 | scripts[AssetLevel.Library], 187 | scripts[AssetLevel.Theme], 188 | scripts[AssetLevel.Runtime], 189 | scripts[AssetLevel.App], 190 | ); 191 | await Promise.all( 192 | styleQueue.map(({ content, level, type, id }) => 193 | this.loadStyle(content, level!, type === AssetType.CSSUrl, id), 194 | ), 195 | ); 196 | await Promise.all( 197 | scriptQueue.map(({ content, type }) => 198 | this.loadScript(content, type === AssetType.JSUrl), 199 | ), 200 | ); 201 | } 202 | 203 | private stylePoints = new Map(); 204 | 205 | private loadStyle( 206 | content: string | undefined | null, 207 | level: AssetLevel, 208 | isUrl?: boolean, 209 | id?: string, 210 | ) { 211 | if (!content) { 212 | return; 213 | } 214 | let point: StylePoint | undefined; 215 | if (id) { 216 | point = this.stylePoints.get(id); 217 | if (!point) { 218 | point = new StylePoint(level, id); 219 | this.stylePoints.set(id, point); 220 | } 221 | } else { 222 | point = new StylePoint(level); 223 | } 224 | return isUrl ? point.applyUrl(content) : point.applyText(content); 225 | } 226 | 227 | private loadScript(content: string | undefined | null, isUrl?: boolean) { 228 | if (!content) { 229 | return; 230 | } 231 | return isUrl ? load(content) : evaluate(content); 232 | } 233 | 234 | async loadAsyncLibrary(asyncLibraryMap: Record) { 235 | const promiseList: any[] = []; 236 | const libraryKeyList: any[] = []; 237 | for (const key in asyncLibraryMap) { 238 | if (asyncLibraryMap[key].async) { 239 | promiseList.push(window[asyncLibraryMap[key].library]); 240 | libraryKeyList.push(asyncLibraryMap[key].library); 241 | } 242 | } 243 | await Promise.all(promiseList).then((mods) => { 244 | if (mods.length > 0) { 245 | mods.map((item, index) => { 246 | window[libraryKeyList[index]] = item; 247 | return item; 248 | }); 249 | } 250 | }); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /packages/vue-renderer/src/renderer.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IPublicTypeNodeSchema as NodeSchema, 3 | IPublicTypeContainerSchema as ContainerSchema, 4 | } from '@alilc/lowcode-types'; 5 | import { getRendererContextKey, type DesignMode, INode } from '@knxcloud/lowcode-hooks'; 6 | import { 7 | type PropType, 8 | type Component, 9 | type ComponentPublicInstance, 10 | h, 11 | reactive, 12 | provide, 13 | computed, 14 | defineComponent, 15 | shallowRef, 16 | watch, 17 | triggerRef, 18 | ref, 19 | watchEffect, 20 | } from 'vue'; 21 | import { 22 | type I18nMessages, 23 | type BlockScope, 24 | type ExtractPublicPropTypes, 25 | SchemaParser, 26 | type RuntimeScope, 27 | } from './utils'; 28 | import config from './config'; 29 | import { RENDERER_COMPS } from './renderers'; 30 | import { 31 | createObjectSplitter, 32 | debounce, 33 | exportSchema, 34 | isBoolean, 35 | } from '@knxcloud/lowcode-utils'; 36 | 37 | const vueRendererProps = { 38 | scope: Object as PropType, 39 | schema: { 40 | type: Object as PropType, 41 | required: true, 42 | }, 43 | passProps: Object as PropType>, 44 | components: { 45 | type: Object as PropType>, 46 | required: true, 47 | }, 48 | /** 设计模式,可选值:live、design */ 49 | designMode: { 50 | type: String as PropType, 51 | default: 'live', 52 | }, 53 | /** 设备信息 */ 54 | device: String, 55 | /** 语言 */ 56 | locale: String, 57 | messages: { 58 | type: Object as PropType, 59 | default: () => ({}), 60 | }, 61 | getNode: Function as PropType<(id: string) => INode | null>, 62 | /** 组件获取 ref 时触发的钩子 */ 63 | onCompGetCtx: Function as PropType< 64 | (schema: NodeSchema, ref: ComponentPublicInstance) => void 65 | >, 66 | thisRequiredInJSE: { 67 | type: Boolean, 68 | default: true, 69 | }, 70 | disableCompMock: { 71 | type: [Array, Boolean] as PropType, 72 | default: false, 73 | }, 74 | appHelper: Object, 75 | requestHandlersMap: Object, 76 | } as const; 77 | 78 | type VueRendererProps = ExtractPublicPropTypes; 79 | 80 | const splitOptions = createObjectSplitter( 81 | (prop) => !prop.match(/^[a-z]+([A-Z][a-z]+)*$/), 82 | ); 83 | 84 | const isAsyncComp = (comp: any) => { 85 | return comp && comp.name === 'AsyncComponentWrapper'; 86 | }; 87 | 88 | const VueRenderer = defineComponent({ 89 | props: vueRendererProps, 90 | setup(props, { slots, expose }) { 91 | const parser = new SchemaParser({ 92 | thisRequired: props.thisRequiredInJSE, 93 | }).initModule(props.schema); 94 | 95 | const triggerCompGetCtx = (schema: NodeSchema, val: ComponentPublicInstance) => { 96 | val && props.onCompGetCtx?.(schema, val); 97 | }; 98 | const getNode = (id: string) => props.getNode?.(id) ?? null; 99 | 100 | const schemaRef = shallowRef(props.schema); 101 | watch( 102 | () => props.schema, 103 | () => (schemaRef.value = props.schema), 104 | ); 105 | 106 | let needWrapComp: (name: string) => boolean = () => true; 107 | 108 | watchEffect(() => { 109 | const disableCompMock = props.disableCompMock; 110 | if (isBoolean(disableCompMock)) { 111 | needWrapComp = disableCompMock ? () => false : () => true; 112 | } else if (disableCompMock) { 113 | needWrapComp = (name) => !disableCompMock.includes(name); 114 | } 115 | }); 116 | 117 | const wrapCached: Map> = new Map(); 118 | 119 | const rendererContext = reactive({ 120 | designMode: computed(() => props.designMode), 121 | components: computed(() => ({ 122 | ...config.getRenderers(), 123 | ...props.components, 124 | })), 125 | thisRequiredInJSE: computed(() => props.thisRequiredInJSE), 126 | getNode: (id: string) => (props.getNode?.(id) as any) ?? null, 127 | triggerCompGetCtx: (schema: NodeSchema, inst: ComponentPublicInstance) => { 128 | props.onCompGetCtx?.(schema, inst); 129 | }, 130 | rerender: debounce(() => { 131 | const id = props.schema.id; 132 | const node = id && getNode(id); 133 | if (node) { 134 | const newSchema = exportSchema(node); 135 | if (newSchema) { 136 | schemaRef.value = newSchema; 137 | } 138 | } 139 | triggerRef(schemaRef); 140 | }), 141 | wrapLeafComp: ( 142 | name: string, 143 | comp: T, 144 | leaf: L, 145 | ): L => { 146 | let record = wrapCached.get(leaf); 147 | if (record) { 148 | if (record.has(comp)) { 149 | return record.get(comp); 150 | } 151 | } else { 152 | record = new Map(); 153 | wrapCached.set(leaf, record); 154 | } 155 | 156 | if (needWrapComp(name) && !isAsyncComp(comp)) { 157 | const [privateOptions, _, privateOptionsCount] = splitOptions(comp as any); 158 | if (privateOptionsCount) { 159 | leaf = Object.create(leaf, Object.getOwnPropertyDescriptors(privateOptions)); 160 | } 161 | } 162 | record.set(comp, leaf); 163 | return leaf; 164 | }, 165 | }); 166 | 167 | provide(getRendererContextKey(), rendererContext); 168 | 169 | const runtimeScope = ref(); 170 | 171 | expose({ runtimeScope }); 172 | 173 | const renderContent = () => { 174 | const { components } = rendererContext; 175 | const { 176 | scope, 177 | locale, 178 | messages, 179 | designMode, 180 | thisRequiredInJSE, 181 | requestHandlersMap, 182 | passProps, 183 | appHelper, 184 | } = props; 185 | const { value: schema } = schemaRef; 186 | 187 | if (!schema) return null; 188 | 189 | const { componentName } = schema; 190 | let Comp = components[componentName] || components[`${componentName}Renderer`]; 191 | if (Comp && !(Comp as any).__renderer__) { 192 | Comp = RENDERER_COMPS[`${componentName}Renderer`]; 193 | } 194 | 195 | return Comp 196 | ? h( 197 | Comp, 198 | { 199 | key: schema.__ctx 200 | ? `${schema.__ctx.lceKey}_${schema.__ctx.idx || '0'}` 201 | : schema.id, 202 | ...passProps, 203 | ...parser.parseOnlyJsValue(schema.props), 204 | ref: runtimeScope, 205 | __parser: parser, 206 | __scope: scope, 207 | __schema: schema, 208 | __locale: locale, 209 | __messages: messages, 210 | __appHelper: appHelper, 211 | __components: components, 212 | __designMode: designMode, 213 | __thisRequiredInJSE: thisRequiredInJSE, 214 | __requestHandlersMap: requestHandlersMap, 215 | __getNode: getNode, 216 | __triggerCompGetCtx: triggerCompGetCtx, 217 | } as any, 218 | slots, 219 | ) 220 | : null; 221 | }; 222 | 223 | return () => { 224 | const { device, locale } = props; 225 | const configProvider = config.getConfigProvider(); 226 | return configProvider 227 | ? h(configProvider, { device, locale }, { default: renderContent }) 228 | : renderContent(); 229 | }; 230 | }, 231 | }); 232 | 233 | export const cleanCachedModules = () => { 234 | SchemaParser.cleanCachedModules(); 235 | }; 236 | 237 | export { VueRenderer, vueRendererProps }; 238 | export type { VueRendererProps, I18nMessages, BlockScope }; 239 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/vue-router.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, createApp } from 'vue'; 2 | import { createRouter, createMemoryHistory, RouterView } from 'vue-router'; 3 | import { VueRenderer } from '../src/renderer'; 4 | import { LOWCODE_ROUTE_META, setupLowCodeRouteGuard } from '../src'; 5 | import { IPublicTypePageSchema } from '@alilc/lowcode-types'; 6 | import { sleep } from '@knxcloud/lowcode-utils'; 7 | import { flushPromises } from '@vue/test-utils'; 8 | 9 | describe('vue-router lifecycles', () => { 10 | test('beforeRouteEnter', async () => { 11 | const schema: IPublicTypePageSchema = { 12 | fileName: '/', 13 | componentName: 'Page', 14 | lifeCycles: { 15 | beforeRouteEnter: { 16 | type: 'JSFunction', 17 | value: `async function (to) { 18 | to.meta.testValue = 5; 19 | await new Promise((resolve) => { 20 | setTimeout(resolve, 300); 21 | }); 22 | return true; 23 | }`, 24 | }, 25 | }, 26 | children: 'rendered', 27 | }; 28 | 29 | const app = createApp(RouterView); 30 | const router = createRouter({ 31 | history: createMemoryHistory('/'), 32 | routes: [ 33 | { 34 | path: '/', 35 | component: VueRenderer, 36 | props: { 37 | schema, 38 | components: {}, 39 | }, 40 | meta: { 41 | [LOWCODE_ROUTE_META]: schema, 42 | }, 43 | }, 44 | ], 45 | }); 46 | setupLowCodeRouteGuard(router); 47 | app.use(router); 48 | const el = document.createElement('div'); 49 | const inst = app.mount(el); 50 | 51 | expect(inst.$el.innerHTML).toBeUndefined(); 52 | 53 | await flushPromises(); 54 | 55 | expect(inst.$el.innerHTML).toBeUndefined(); 56 | 57 | await sleep(500); 58 | 59 | expect(inst.$el.innerHTML).eq('rendered'); 60 | }); 61 | 62 | test('beforeRouteUpdate', async () => { 63 | const schema: IPublicTypePageSchema = { 64 | fileName: '/', 65 | componentName: 'Page', 66 | lifeCycles: { 67 | beforeRouteUpdate: { 68 | type: 'JSFunction', 69 | value: `function (to) { 70 | return to.query.name === 'Tom' 71 | ? { path: to.path, query: { name: 'Sandy' } } 72 | : true; 73 | }`, 74 | }, 75 | }, 76 | children: { 77 | type: 'JSExpression', 78 | value: `this.$route.query.name || 'rendered'`, 79 | }, 80 | }; 81 | 82 | const app = createApp(RouterView); 83 | const router = createRouter({ 84 | history: createMemoryHistory('/'), 85 | routes: [ 86 | { 87 | path: '/', 88 | component: VueRenderer, 89 | props: { 90 | schema: schema, 91 | components: {}, 92 | }, 93 | meta: { 94 | [LOWCODE_ROUTE_META]: schema, 95 | }, 96 | }, 97 | ], 98 | }); 99 | 100 | setupLowCodeRouteGuard(router); 101 | app.use(router); 102 | const el = document.createElement('div'); 103 | const inst = app.mount(el); 104 | 105 | await flushPromises(); 106 | 107 | expect(inst.$el.innerHTML).eq('rendered'); 108 | 109 | await router.push({ 110 | path: '/', 111 | query: { 112 | name: 'Tom', 113 | }, 114 | }); 115 | 116 | expect(inst.$el.innerHTML).eq('Sandy'); 117 | }); 118 | 119 | test('beforeRouteLeave', async () => { 120 | const schema: IPublicTypePageSchema = { 121 | fileName: '/', 122 | componentName: 'Page', 123 | children: 'rendered', 124 | lifeCycles: { 125 | beforeRouteLeave: { 126 | type: 'JSExpression', 127 | value: `() => false`, 128 | }, 129 | }, 130 | }; 131 | 132 | const schema2: IPublicTypePageSchema = { 133 | fileName: '/two', 134 | componentName: 'Page', 135 | children: 'page two', 136 | }; 137 | 138 | const app = createApp(RouterView); 139 | const router = createRouter({ 140 | history: createMemoryHistory('/'), 141 | routes: [ 142 | { 143 | path: '/', 144 | component: VueRenderer, 145 | props: { 146 | schema: schema, 147 | components: {}, 148 | }, 149 | meta: { 150 | [LOWCODE_ROUTE_META]: schema, 151 | }, 152 | }, 153 | { 154 | path: '/two', 155 | component: VueRenderer, 156 | props: { 157 | schema: schema2, 158 | components: {}, 159 | }, 160 | meta: { 161 | [LOWCODE_ROUTE_META]: schema2, 162 | }, 163 | }, 164 | ], 165 | }); 166 | 167 | setupLowCodeRouteGuard(router); 168 | app.use(router); 169 | const el = document.createElement('div'); 170 | const inst = app.mount(el); 171 | 172 | expect(inst.$el.innerHTML).toBeUndefined(); 173 | 174 | await flushPromises(); 175 | 176 | expect(inst.$el.innerHTML).eq('rendered'); 177 | 178 | await router.push({ path: '/two' }); 179 | 180 | expect(inst.$el.innerHTML).eq('rendered'); 181 | }); 182 | }); 183 | 184 | describe('vue-router async components', async () => { 185 | test('async setup', async () => { 186 | const schema: IPublicTypePageSchema = { 187 | fileName: '/', 188 | componentName: 'Page', 189 | state: { 190 | name: 'Sandy', 191 | }, 192 | lifeCycles: { 193 | setup: { 194 | type: 'JSFunction', 195 | value: `async function() { 196 | await new Promise(resolve => { 197 | setTimeout(resolve, 300); 198 | }); 199 | }`, 200 | }, 201 | beforeRouteEnter: { 202 | type: 'JSFunction', 203 | value: `function () { 204 | return (vm) => void (vm.name = 'Tom'); 205 | }`, 206 | }, 207 | }, 208 | children: { 209 | type: 'JSExpression', 210 | value: 'this.name', 211 | }, 212 | }; 213 | 214 | const app = createApp(() => ( 215 | {({ Component }) => {Component}} 216 | )); 217 | const router = createRouter({ 218 | history: createMemoryHistory('/'), 219 | routes: [ 220 | { 221 | path: '/', 222 | component: VueRenderer, 223 | props: { 224 | schema, 225 | components: {}, 226 | }, 227 | meta: { 228 | [LOWCODE_ROUTE_META]: schema, 229 | }, 230 | }, 231 | ], 232 | }); 233 | setupLowCodeRouteGuard(router); 234 | app.use(router); 235 | const el = document.createElement('div'); 236 | app.mount(el); 237 | 238 | await flushPromises(); 239 | 240 | expect(el.querySelector('.lc-page')).toBeNull(); 241 | 242 | await sleep(300); 243 | 244 | expect(el.querySelector('.lc-page')).toBeDefined(); 245 | expect(el.querySelector('.lc-page')!.innerHTML).eq('Tom'); 246 | }); 247 | 248 | test('async component', async () => { 249 | const schema: IPublicTypePageSchema = { 250 | fileName: '/', 251 | componentName: 'Page', 252 | state: { 253 | name: 'Sandy', 254 | }, 255 | lifeCycles: { 256 | beforeRouteEnter: { 257 | type: 'JSFunction', 258 | value: `function () { 259 | return (vm) => void (vm.name = 'Tom'); 260 | }`, 261 | }, 262 | }, 263 | children: { 264 | type: 'JSExpression', 265 | value: 'this.name', 266 | }, 267 | }; 268 | 269 | const app = createApp(RouterView); 270 | const router = createRouter({ 271 | history: createMemoryHistory('/'), 272 | routes: [ 273 | { 274 | path: '/', 275 | component: async () => { 276 | await sleep(200); 277 | return VueRenderer; 278 | }, 279 | props: { 280 | schema, 281 | components: {}, 282 | }, 283 | meta: { 284 | [LOWCODE_ROUTE_META]: schema, 285 | }, 286 | }, 287 | ], 288 | }); 289 | setupLowCodeRouteGuard(router); 290 | app.use(router); 291 | const el = document.createElement('div'); 292 | app.mount(el); 293 | 294 | await flushPromises(); 295 | 296 | expect(el.querySelector('.lc-page')).toBeNull(); 297 | 298 | await sleep(300); 299 | 300 | expect(el.querySelector('.lc-page')).toBeDefined(); 301 | expect(el.querySelector('.lc-page')!.innerHTML).eq('Tom'); 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /packages/vue-renderer/__tests__/renderer-hoc.spec.tsx: -------------------------------------------------------------------------------- 1 | import type { IPublicTypeContainerSchema } from '@alilc/lowcode-types'; 2 | import type { INode } from '@knxcloud/lowcode-hooks'; 3 | import { mount } from '@vue/test-utils'; 4 | import { defineComponent, renderSlot } from 'vue'; 5 | import VueRenderer from '../src'; 6 | import { flushPromises } from '@vue/test-utils'; 7 | import { createDocument, sleep } from './helpers'; 8 | 9 | describe('test for props update', () => { 10 | const components = { 11 | TButton: defineComponent({ 12 | name: 'TButton', 13 | props: { 14 | type: { 15 | type: String, 16 | default: 'primary', 17 | }, 18 | }, 19 | emits: ['click'], 20 | render() { 21 | const vnode = renderSlot(this.$slots, 'default', this.$props); 22 | return ( 23 | 26 | ); 27 | }, 28 | }), 29 | Container: defineComponent({ 30 | render() { 31 | const vnode = renderSlot(this.$slots, 'default'); 32 | return
{vnode}
; 33 | }, 34 | }), 35 | }; 36 | 37 | test('container placeholder', async () => { 38 | const meta = { 39 | configure: { 40 | component: { 41 | isContainer: true, 42 | }, 43 | }, 44 | }; 45 | const doc = createDocument( 46 | { 47 | id: '0', 48 | fileName: '/', 49 | componentName: 'Page', 50 | }, 51 | { 0: meta } 52 | ); 53 | const inst = mount(VueRenderer, { 54 | props: { 55 | key: 0, 56 | components, 57 | designMode: 'design', 58 | schema: doc.getNodeById('0')!.schema as any, 59 | getNode: (id: string) => doc.getNodeById(id) as INode, 60 | }, 61 | }); 62 | expect(inst.find('.lc-container-placeholder').exists()).toBeTruthy(); 63 | meta.configure.component.isContainer = false; 64 | inst.vm.$forceUpdate(); 65 | await flushPromises(); 66 | expect(inst.find('.lc-container-placeholder').exists()).toBeFalsy(); 67 | }); 68 | 69 | test('normal prop update', async () => { 70 | const doc = createDocument({ 71 | id: '0', 72 | fileName: '/', 73 | componentName: 'Page', 74 | children: [ 75 | { 76 | id: '1', 77 | componentName: 'TButton', 78 | props: { 79 | type: 'warning', 80 | }, 81 | }, 82 | ], 83 | }); 84 | 85 | const inst = mount(VueRenderer, { 86 | props: { 87 | key: 0, 88 | components, 89 | designMode: 'design', 90 | schema: doc.getNodeById('0')!.schema as IPublicTypeContainerSchema, 91 | getNode: (id: string) => doc.getNodeById(id) as INode, 92 | }, 93 | }); 94 | expect(inst.html()).contain('t-warning'); 95 | doc.getNodeById('1')!.setPropValue('type', 'error'); 96 | await sleep(); 97 | expect(inst.html()).contain('t-error'); 98 | }); 99 | 100 | test('condition prop update', async () => { 101 | const doc = createDocument({ 102 | id: '0', 103 | fileName: '/', 104 | componentName: 'Page', 105 | children: [ 106 | { 107 | id: '1', 108 | componentName: 'TButton', 109 | props: { 110 | type: 'warning', 111 | }, 112 | }, 113 | ], 114 | }); 115 | 116 | const inst = mount(VueRenderer, { 117 | props: { 118 | key: 0, 119 | components, 120 | designMode: 'design', 121 | schema: doc.getNodeById('0')!.schema as IPublicTypeContainerSchema, 122 | getNode: (id: string) => doc.getNodeById(id) as INode, 123 | }, 124 | }); 125 | expect(inst.html()).contain('t-warning'); 126 | doc.getNodeById('1')!.setPropValue('__condition__', false); 127 | await sleep(); 128 | expect(inst.find('button').exists()).toBeFalsy(); 129 | 130 | doc.getNodeById('1')!.setPropValue('__condition__', true); 131 | await sleep(); 132 | expect(inst.find('button').exists()).toBeTruthy(); 133 | }); 134 | }); 135 | 136 | describe('test for slot update', () => { 137 | const components = { 138 | TButton: defineComponent({ 139 | name: 'TButton', 140 | props: { 141 | type: { 142 | type: String, 143 | default: 'primary', 144 | }, 145 | }, 146 | emits: ['click'], 147 | render() { 148 | const { $props, $slots } = this; 149 | return ( 150 | 156 | ); 157 | }, 158 | }), 159 | Container: defineComponent({ 160 | name: 'Container', 161 | render() { 162 | const vnode = renderSlot(this.$slots, 'default'); 163 | return
{vnode}
; 164 | }, 165 | }), 166 | }; 167 | 168 | test('children prop change', async () => { 169 | const doc = createDocument({ 170 | id: '0', 171 | fileName: '/', 172 | componentName: 'Page', 173 | children: [ 174 | { 175 | id: '1', 176 | componentName: 'Container', 177 | props: { 178 | children: 'first', 179 | }, 180 | }, 181 | ], 182 | }); 183 | 184 | const inst = mount(VueRenderer, { 185 | props: { 186 | key: 0, 187 | components, 188 | designMode: 'design', 189 | schema: doc.getNodeById('0')!.schema as IPublicTypeContainerSchema, 190 | getNode: (id: string) => doc.getNodeById(id) as INode, 191 | }, 192 | }); 193 | 194 | expect(inst.html()).contain('first'); 195 | 196 | doc.getNodeById('1')!.setPropValue('children', 'replaced1'); 197 | await sleep(); 198 | expect(inst.html()).contain('replaced1'); 199 | 200 | doc.getNodeById('1')!.setPropValue('children', { 201 | type: 'JSSlot', 202 | value: { 203 | id: '3', 204 | componentName: 'TButton', 205 | props: { 206 | type: 'warning', 207 | }, 208 | children: 'replaced2', 209 | }, 210 | }); 211 | await sleep(); 212 | expect(inst.html()).contain('replaced'); 213 | expect(inst.html()).contain('t-warning'); 214 | }); 215 | 216 | test('children field change', async () => { 217 | const doc = createDocument({ 218 | id: '0', 219 | fileName: '/', 220 | componentName: 'Page', 221 | children: [ 222 | { 223 | id: '1', 224 | componentName: 'Container', 225 | children: [ 226 | { 227 | id: '2', 228 | componentName: 'TButton', 229 | children: 'first', 230 | }, 231 | ], 232 | }, 233 | ], 234 | }); 235 | 236 | const inst = mount(VueRenderer, { 237 | props: { 238 | key: 0, 239 | components, 240 | designMode: 'design', 241 | schema: doc.getNodeById('0')!.schema as IPublicTypeContainerSchema, 242 | getNode: (id: string) => doc.getNodeById(id) as INode, 243 | }, 244 | }); 245 | 246 | expect(inst.html()).contain('first'); 247 | doc.getNodeById('1')!.replaceChild(doc.getNodeById('2')!, { 248 | id: '3', 249 | componentName: 'TButton', 250 | children: 'replaced', 251 | }); 252 | await sleep(); 253 | expect(inst.html()).contain('replaced'); 254 | }); 255 | 256 | test('jsslot prop change', async () => { 257 | const doc = createDocument({ 258 | id: '0', 259 | fileName: '/', 260 | componentName: 'Page', 261 | children: [ 262 | { 263 | id: '1', 264 | componentName: 'TButton', 265 | props: { 266 | children: 'first', 267 | icon: { 268 | type: 'JSSlot', 269 | value: 'icon-content', 270 | }, 271 | }, 272 | }, 273 | ], 274 | }); 275 | 276 | const inst = mount(VueRenderer, { 277 | props: { 278 | key: 0, 279 | components, 280 | designMode: 'design', 281 | schema: doc.getNodeById('0')!.schema as IPublicTypeContainerSchema, 282 | getNode: (id: string) => doc.getNodeById(id) as INode, 283 | }, 284 | }); 285 | 286 | expect(inst.find('.t-button__icon').text()).contain('icon-content'); 287 | expect(inst.find('.t-button__content').text()).contain('first'); 288 | 289 | doc.getNodeById('1')!.setPropValue('icon', { 290 | type: 'JSSlot', 291 | value: 'icon-replaced', 292 | }); 293 | 294 | await sleep(); 295 | 296 | expect(inst.find('.t-button__icon').text()).contain('icon-replaced'); 297 | 298 | doc.getNodeById('1')!.setPropValue('icon', { 299 | type: 'JSSlot', 300 | value: { 301 | id: '3', 302 | componentName: 'Container', 303 | children: 'icon-replaced2', 304 | }, 305 | }); 306 | await sleep(); 307 | 308 | expect(inst.find('.t-container').text()).contain('icon-replaced2'); 309 | }); 310 | }); 311 | -------------------------------------------------------------------------------- /packages/vue-simulator-renderer/src/simulator.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IPublicTypeContainerSchema, 3 | IPublicModelDocumentModel, 4 | } from '@alilc/lowcode-types'; 5 | import { 6 | type Ref, 7 | createApp, 8 | ref, 9 | shallowRef, 10 | reactive, 11 | computed, 12 | markRaw, 13 | onUnmounted, 14 | shallowReactive, 15 | } from 'vue'; 16 | import * as VueRouter from 'vue-router'; 17 | import type { 18 | ComponentInstance, 19 | ComponentRecord, 20 | DocumentInstance, 21 | MixedComponent, 22 | SimulatorViewLayout, 23 | VueSimulatorRenderer, 24 | } from './interface'; 25 | import { 26 | config, 27 | LOWCODE_ROUTE_META, 28 | SchemaParser, 29 | setupLowCodeRouteGuard, 30 | } from '@knxcloud/lowcode-vue-renderer'; 31 | import { 32 | AssetLoader, 33 | buildUtils, 34 | buildComponents, 35 | getSubComponent, 36 | exportSchema, 37 | isArray, 38 | } from '@knxcloud/lowcode-utils'; 39 | import { Renderer, SimulatorRendererView } from './simulator-view'; 40 | import { Slot, Leaf, Page } from './buildin-components'; 41 | import { host } from './host'; 42 | import { 43 | cursor, 44 | deepMerge, 45 | findDOMNodes, 46 | getClientRects, 47 | getCompRootData, 48 | setCompRootData, 49 | getClosestNodeInstance, 50 | isComponentRecord, 51 | getClosestNodeInstanceByComponent, 52 | setNativeSelection, 53 | createComponentRecord, 54 | parseFileNameToPath, 55 | isVNodeHTMLElement, 56 | CompRootHTMLElement, 57 | } from './utils'; 58 | 59 | Object.assign(window, { VueRouter }); 60 | 61 | const loader = new AssetLoader(); 62 | 63 | const builtinComponents = { Slot, Leaf, Page }; 64 | 65 | export interface ProjectContext { 66 | i18n: Record; 67 | appHelper: { 68 | utils?: Record; 69 | constants?: Record; 70 | [x: string]: unknown; 71 | }; 72 | suspense: boolean; 73 | } 74 | 75 | function createDocumentInstance( 76 | document: IPublicModelDocumentModel, 77 | context: ProjectContext, 78 | ): DocumentInstance { 79 | /** 记录单个节点的组件实例列表 */ 80 | const instancesMap = new Map(); 81 | /** 记录 vue 组件实例和组件 uid 的映射关系 */ 82 | const vueInstanceMap = new Map(); 83 | 84 | const timestamp = ref(Date.now()); 85 | 86 | const schema = computed(() => { 87 | void timestamp.value; 88 | return ( 89 | exportSchema(document) ?? { 90 | fileName: '/', 91 | componentName: 'Page', 92 | } 93 | ); 94 | }); 95 | 96 | const checkInstanceMounted = (instance: ComponentInstance | HTMLElement): boolean => { 97 | return '$' in instance ? instance.$.isMounted : !!instance; 98 | }; 99 | 100 | const setHostInstance = ( 101 | docId: string, 102 | nodeId: string, 103 | instances: ComponentInstance[] | null, 104 | ) => { 105 | const instanceRecords = !instances 106 | ? null 107 | : instances.map((inst) => createComponentRecord(docId, nodeId, inst.$.uid)); 108 | host.setInstance(docId, nodeId, instanceRecords); 109 | }; 110 | 111 | const getComponentInstance = (id: number) => { 112 | return vueInstanceMap.get(id); 113 | }; 114 | 115 | const mountInstance = (id: string, instanceOrEl: ComponentInstance | HTMLElement) => { 116 | const docId = document.id; 117 | if (instanceOrEl == null) { 118 | let instances = instancesMap.get(id); 119 | if (instances) { 120 | instances = instances.filter(checkInstanceMounted); 121 | if (instances.length > 0) { 122 | instancesMap.set(id, instances); 123 | setHostInstance(docId, id, instances); 124 | } else { 125 | instancesMap.delete(id); 126 | setHostInstance(docId, id, null); 127 | } 128 | } 129 | return; 130 | } 131 | 132 | let el: CompRootHTMLElement; 133 | let instance: ComponentInstance; 134 | 135 | if ('$' in instanceOrEl) { 136 | instance = instanceOrEl; 137 | el = instance.$el; 138 | } else if (isVNodeHTMLElement(instanceOrEl)) { 139 | instance = instanceOrEl.__vueParentComponent.proxy!; 140 | // @ts-expect-error 141 | el = instanceOrEl; 142 | } else { 143 | return; 144 | } 145 | 146 | const origId = getCompRootData(el).nodeId; 147 | if (origId && origId !== id) { 148 | // 另外一个节点的 instance 在此被复用了,需要从原来地方卸载 149 | unmountInstance(origId, instance); 150 | } 151 | 152 | onUnmounted(() => unmountInstance(id, instance), instance.$); 153 | 154 | setCompRootData(el, { 155 | nodeId: id, 156 | docId: docId, 157 | instance: instance, 158 | }); 159 | let instances = instancesMap.get(id); 160 | if (instances) { 161 | const l = instances.length; 162 | instances = instances.filter(checkInstanceMounted); 163 | let updated = instances.length !== l; 164 | if (!instances.includes(instance)) { 165 | instances.push(instance); 166 | updated = true; 167 | } 168 | if (!updated) return; 169 | } else { 170 | instances = [instance]; 171 | } 172 | vueInstanceMap.set(instance.$.uid, instance); 173 | instancesMap.set(id, instances); 174 | setHostInstance(docId, id, instances); 175 | }; 176 | 177 | const unmountInstance = (id: string, instance: ComponentInstance) => { 178 | const instances = instancesMap.get(id); 179 | if (instances) { 180 | const i = instances.indexOf(instance); 181 | if (i > -1) { 182 | const [instance] = instances.splice(i, 1); 183 | vueInstanceMap.delete(instance.$.uid); 184 | setHostInstance(document.id, id, instances); 185 | } 186 | } 187 | }; 188 | 189 | const getNode: DocumentInstance['getNode'] = (id) => { 190 | // @ts-expect-error getNodeById 不存在,使用 getNode 代替,这里的 ts 类型声明不正确 191 | return id ? document.getNode(id) : null; 192 | }; 193 | 194 | return reactive({ 195 | id: computed(() => document.id), 196 | path: computed(() => parseFileNameToPath(schema.value.fileName ?? '')), 197 | get key() { 198 | return `${document.id}:${timestamp.value}`; 199 | }, 200 | scope: computed(() => ({})), 201 | schema: schema, 202 | appHelper: computed(() => { 203 | const _schema = schema.value; 204 | 205 | const { 206 | utils: utilsInContext, 207 | constants: constantsInContext, 208 | ...otherHelpers 209 | } = context.appHelper; 210 | 211 | return { 212 | utils: { 213 | ...utilsInContext, 214 | ...buildUtils(host.libraryMap, Reflect.get(_schema, 'utils') ?? []), 215 | }, 216 | constants: { 217 | ...constantsInContext, 218 | ...Reflect.get(_schema, 'constants'), 219 | }, 220 | ...otherHelpers, 221 | }; 222 | }), 223 | document: computed(() => document), 224 | messages: computed(() => deepMerge(context.i18n, Reflect.get(schema.value, 'i18n'))), 225 | instancesMap: computed(() => instancesMap), 226 | getNode, 227 | mountInstance, 228 | unmountInstance, 229 | getComponentInstance, 230 | rerender: () => { 231 | const now = Date.now(); 232 | if (context.suspense) { 233 | Object.assign(timestamp, { 234 | _value: now, 235 | _rawValue: now, 236 | }); 237 | } else { 238 | timestamp.value = now; 239 | } 240 | SchemaParser.cleanCachedModules(); 241 | }, 242 | }) as DocumentInstance; 243 | } 244 | 245 | function createSimulatorRenderer() { 246 | const layout: Ref = shallowRef({}); 247 | const device: Ref = shallowRef('default'); 248 | const locale: Ref = shallowRef(); 249 | const autoRender = shallowRef(host.autoRender); 250 | const designMode: Ref = shallowRef('design'); 251 | const libraryMap: Ref> = shallowRef({}); 252 | const components: Ref> = shallowRef({}); 253 | const componentsMap: Ref> = shallowRef({}); 254 | const disableCompMock: Ref = shallowRef(true); 255 | const requestHandlersMap: Ref> = shallowRef({}); 256 | const documentInstances: Ref = shallowRef([]); 257 | const thisRequiredInJSE: Ref = shallowRef(false); 258 | 259 | const context: ProjectContext = shallowReactive({ 260 | i18n: {}, 261 | appHelper: { 262 | utils: {}, 263 | constants: {}, 264 | }, 265 | suspense: false, 266 | }); 267 | 268 | const disposeFunctions: Array<() => void> = []; 269 | 270 | const documentInstanceMap = new Map(); 271 | 272 | function _buildComponents() { 273 | components.value = { 274 | ...builtinComponents, 275 | ...buildComponents(libraryMap.value, componentsMap.value), 276 | }; 277 | } 278 | 279 | const simulator = reactive({ 280 | config: markRaw(config), 281 | layout, 282 | device, 283 | locale, 284 | designMode, 285 | libraryMap, 286 | components, 287 | autoRender, 288 | componentsMap, 289 | disableCompMock, 290 | documentInstances, 291 | requestHandlersMap, 292 | thisRequiredInJSE, 293 | isSimulatorRenderer: true, 294 | }) as VueSimulatorRenderer; 295 | 296 | simulator.app = markRaw(createApp(SimulatorRendererView, { simulator })); 297 | simulator.router = markRaw( 298 | VueRouter.createRouter({ 299 | history: VueRouter.createMemoryHistory('/'), 300 | routes: [], 301 | }), 302 | ); 303 | 304 | simulator.getComponent = (componentName) => { 305 | const paths = componentName.split('.'); 306 | const subs: string[] = []; 307 | while (paths.length > 0) { 308 | const component = components.value[componentName]; 309 | if (component) { 310 | return getSubComponent(component, subs); 311 | } 312 | const sub = paths.pop(); 313 | if (!sub) break; 314 | subs.unshift(sub); 315 | componentName = paths.join('.'); 316 | } 317 | return null!; 318 | }; 319 | 320 | simulator.getClosestNodeInstance = (el, specId) => { 321 | if (isComponentRecord(el)) { 322 | const { cid, did } = el; 323 | const documentInstance = documentInstanceMap.get(did); 324 | const instance = documentInstance?.getComponentInstance(cid) ?? null; 325 | return instance && getClosestNodeInstanceByComponent(instance.$, specId); 326 | } 327 | return getClosestNodeInstance(el, specId); 328 | }; 329 | 330 | simulator.findDOMNodes = (instance: ComponentRecord) => { 331 | if (instance) { 332 | const { did, cid } = instance; 333 | const documentInstance = documentInstanceMap.get(did); 334 | const compInst = documentInstance?.getComponentInstance(cid); 335 | return compInst ? findDOMNodes(compInst) : null; 336 | } 337 | return null; 338 | }; 339 | simulator.getComponent = (componentName) => components.value[componentName]; 340 | 341 | simulator.getClientRects = (element) => getClientRects(element); 342 | simulator.setNativeSelection = (enable) => setNativeSelection(enable); 343 | simulator.setDraggingState = (state) => cursor.setDragging(state); 344 | simulator.setCopyState = (state) => cursor.setCopy(state); 345 | simulator.clearState = () => cursor.release(); 346 | simulator.rerender = () => documentInstances.value.forEach((doc) => doc.rerender()); 347 | simulator.dispose = () => { 348 | simulator.app.unmount(); 349 | disposeFunctions.forEach((fn) => fn()); 350 | }; 351 | simulator.getCurrentDocument = () => { 352 | const crr = host.project.currentDocument; 353 | const docs = documentInstances.value; 354 | return crr ? docs.find((doc) => doc.id === crr.id) ?? null : null; 355 | }; 356 | simulator.load = (assets) => loader.load(assets); 357 | simulator.loadAsyncLibrary = async (asyncLibraryMap) => { 358 | await loader.loadAsyncLibrary(asyncLibraryMap); 359 | _buildComponents(); 360 | }; 361 | 362 | let running = false; 363 | simulator.run = () => { 364 | if (running) return; 365 | running = true; 366 | const containerId = 'app'; 367 | let container = document.getElementById(containerId); 368 | if (!container) { 369 | container = document.createElement('div'); 370 | document.body.appendChild(container); 371 | container.id = containerId; 372 | } 373 | document.documentElement.classList.add('engine-page'); 374 | document.body.classList.add('engine-document'); 375 | simulator.app.use(simulator.router).mount(container); 376 | host.project.setRendererReady(simulator); 377 | }; 378 | 379 | disposeFunctions.push( 380 | host.connect(simulator, () => { 381 | const config = host.project.get('config') || {}; 382 | 383 | // sync layout config 384 | layout.value = config.layout ?? {}; 385 | // sync disableCompMock 386 | disableCompMock.value = isArray(config.disableCompMock) 387 | ? config.disableCompMock 388 | : Boolean(config.disableCompMock); 389 | 390 | // todo: split with others, not all should recompute 391 | if ( 392 | libraryMap.value !== host.libraryMap || 393 | componentsMap.value !== host.designer.componentsMap 394 | ) { 395 | libraryMap.value = host.libraryMap || {}; 396 | componentsMap.value = host.designer.componentsMap; 397 | _buildComponents(); 398 | } 399 | 400 | locale.value = host.locale; 401 | 402 | // sync device 403 | device.value = host.device; 404 | 405 | // sync designMode 406 | designMode.value = host.designMode; 407 | 408 | // sync requestHandlersMap 409 | requestHandlersMap.value = host.requestHandlersMap ?? {}; 410 | 411 | thisRequiredInJSE.value = host.thisRequiredInJSE ?? false; 412 | 413 | documentInstances.value.forEach((doc) => doc.rerender()); 414 | 415 | setupLowCodeRouteGuard(simulator.router, { 416 | thisRequired: thisRequiredInJSE.value, 417 | scopePath: 'renderer.runtimeScope', 418 | }); 419 | }), 420 | ); 421 | 422 | disposeFunctions.push( 423 | host.autorun(async () => { 424 | const { router } = simulator; 425 | documentInstances.value = host.project.documents.map((doc) => { 426 | let documentInstance = documentInstanceMap.get(doc.id); 427 | if (!documentInstance) { 428 | // TODO: 类型不兼容 IDocumentModel to DocumentModel,暂时用类型强转处理 429 | documentInstance = createDocumentInstance(doc as any, context); 430 | documentInstanceMap.set(doc.id, documentInstance); 431 | } else if (router.hasRoute(documentInstance.id)) { 432 | router.removeRoute(documentInstance.id); 433 | } 434 | router.addRoute({ 435 | name: documentInstance.id, 436 | path: documentInstance.path, 437 | meta: { 438 | [LOWCODE_ROUTE_META]: documentInstance.schema, 439 | }, 440 | component: Renderer, 441 | props: ((doc, sim) => () => ({ 442 | simulator: sim, 443 | documentInstance: doc, 444 | }))(documentInstance, simulator), 445 | }); 446 | return documentInstance; 447 | }); 448 | router.getRoutes().forEach((route) => { 449 | const id = route.name as string; 450 | const hasDoc = documentInstances.value.some((doc) => doc.id === id); 451 | if (!hasDoc) { 452 | router.removeRoute(id); 453 | documentInstanceMap.delete(id); 454 | } 455 | }); 456 | const inst = simulator.getCurrentDocument(); 457 | if (inst) { 458 | try { 459 | context.suspense = true; 460 | await router.replace({ name: inst.id, force: true }); 461 | } finally { 462 | context.suspense = false; 463 | } 464 | } 465 | }), 466 | ); 467 | 468 | host.componentsConsumer.consume(async (componentsAsset) => { 469 | if (componentsAsset) { 470 | await loader.load(componentsAsset); 471 | _buildComponents(); 472 | } 473 | }); 474 | 475 | host.injectionConsumer.consume((data) => { 476 | if (data.appHelper) { 477 | const { utils, constants, ...others } = data.appHelper; 478 | Object.assign(context.appHelper, { 479 | utils: isArray(utils) ? buildUtils(host.libraryMap, utils) : utils ?? {}, 480 | constants: constants ?? {}, 481 | ...others, 482 | }); 483 | } 484 | context.i18n = data.i18n ?? {}; 485 | }); 486 | 487 | return simulator; 488 | } 489 | 490 | export default createSimulatorRenderer(); 491 | --------------------------------------------------------------------------------