diff --git a/lerna.json b/lerna.json index 97d148135..908253989 100644 --- a/lerna.json +++ b/lerna.json @@ -29,6 +29,7 @@ "**/*.md", "**/test/**" ], + "message": "chore(release): publish %v", "conventionalCommits": true } } diff --git a/package.json b/package.json index 218a64261..01dfccebb 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "clean": "rm -rf ./packages/*/lib ./packages/*/es ./packages/*/dist ./packages/*/build", "lint": "eslint --ext .ts,.tsx,.js,.jsx ./ --quiet", "lint:fix": "eslint --ext .ts,.tsx,.js,.jsx ./ --quiet --fix", - "pub": "lerna publish --force-publish --cd-version patch --message \"chore(release): publish %v\"", - "pub:prepatch": "lerna publish --force-publish --cd-version prepatch --npm-tag beta --preid beta --message \"chore(release): publish %v\"", - "pub:prerelease": "lerna publish --force-publish --cd-version prerelease --npm-tag beta --preid beta --message \"chore(release): publish %v\"", + "pub": "lerna publish --force-publish --cd-version patch", + "pub:prepatch": "lerna publish --force-publish --cd-version prepatch --npm-tag beta --preid beta", + "pub:prerelease": "lerna publish --force-publish --cd-version prerelease --npm-tag beta --preid beta", "setup": "./scripts/setup.sh", "start": "./scripts/start.sh", "start:server": "./scripts/start-server.sh", diff --git a/packages/designer/.eslintrc.js b/packages/designer/.eslintrc.js index cf6223647..fd197efc8 100644 --- a/packages/designer/.eslintrc.js +++ b/packages/designer/.eslintrc.js @@ -14,6 +14,8 @@ module.exports = { 'no-useless-constructor': 1, 'no-empty-function': 1, '@typescript-eslint/member-ordering': 0, - 'lines-between-class-members': 0 + 'lines-between-class-members': 0, + 'no-await-in-loop': 0, + 'no-plusplus': 0, } } \ No newline at end of file diff --git a/packages/designer/package.json b/packages/designer/package.json index ab701d90a..f0b0dfad7 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -22,7 +22,8 @@ "enzyme-adapter-react-16": "^1.15.5", "event": "^1.0.0", "react": "^16", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "zen-logger": "^1.1.0" }, "devDependencies": { "@ali/lowcode-test-mate": "^1.0.1", diff --git a/packages/designer/src/index.ts b/packages/designer/src/index.ts index 608af6f30..566f0fcd1 100644 --- a/packages/designer/src/index.ts +++ b/packages/designer/src/index.ts @@ -4,3 +4,4 @@ export * from './designer'; export * from './document'; export * from './project'; export * from './builtin-simulator'; +export * from './plugin'; diff --git a/packages/designer/src/plugin/index.ts b/packages/designer/src/plugin/index.ts new file mode 100644 index 000000000..4d1437e9e --- /dev/null +++ b/packages/designer/src/plugin/index.ts @@ -0,0 +1,3 @@ +export * from './plugin-context'; +export * from './plugin-manager'; +export * from './plugin'; diff --git a/packages/designer/src/plugin/plugin-context.ts b/packages/designer/src/plugin/plugin-context.ts new file mode 100644 index 000000000..980ba2a68 --- /dev/null +++ b/packages/designer/src/plugin/plugin-context.ts @@ -0,0 +1,62 @@ +import { Editor, Hotkey, hotkey } from '@ali/lowcode-editor-core'; +import { Skeleton } from '@ali/lowcode-editor-skeleton'; +import { ComponentAction, ILowCodePluginConfig } from '@ali/lowcode-types'; +import { getLogger, Logger } from '../utils'; +import { + registerMetadataTransducer, + addBuiltinComponentAction, + removeBuiltinComponentAction, + MetadataTransducer, +} from '../component-meta'; +import { Designer } from '../designer'; + +export interface IDesignerHelper { + registerMetadataTransducer: (transducer: MetadataTransducer, level = 100, id?: string) => void; + addBuiltinComponentAction: (action: ComponentAction) => void; + removeBuiltinComponentAction: (actionName: string) => void; +} + +export interface ILowCodePluginContext { + skeleton: Skeleton; + designer: Designer; + editor: Editor; + hotkey: Hotkey; + logger: Logger; + plugins: LowCodePluginManager; + designerHelper: IDesignerHelper; + /** + 其他暂不增加,按需增加 + */ +} + +export default class PluginContext implements ILowCodePluginContext { + editor: Editor; + skeleton: Skeleton; + designer: Designer; + hotkey: Hotkey; + logger: Logger; + plugins: LowCodePluginManager; + designerHelper: IDesignerHelper; + + constructor(editor: Editor, plugins: LowCodePluginManager) { + this.editor = editor; + this.designer = editor.get('designer'); + this.skeleton = editor.get('skeleton'); + this.hotkey = hotkey; + this.plugins = plugins; + this.designerHelper = this.createDesignerHelper(); + } + + private createDesignerHelper(): () => IDesignerHelper { + return { + registerMetadataTransducer, + addBuiltinComponentAction, + removeBuiltinComponentAction, + }; + } + + setLogger(config: ILowCodePluginConfig): (config: ILowCodePluginConfig) => void { + this.logger = getLogger({ level: 'log', bizName: `designer:plugin:${config.name}` }); + } +} + diff --git a/packages/designer/src/plugin/plugin-manager.ts b/packages/designer/src/plugin/plugin-manager.ts new file mode 100644 index 000000000..c031e5db6 --- /dev/null +++ b/packages/designer/src/plugin/plugin-manager.ts @@ -0,0 +1,113 @@ +import { Editor } from '@ali/lowcode-editor-core'; +import { CompositeObject, ILowCodePlugin, ILowCodePluginConfig, ILowCodePluginManager } from '@ali/lowcode-types'; +import { LowCodePlugin } from './plugin'; +import LowCodePluginContext from './plugin-context'; +import { getLogger, invariant } from '../utils'; +import sequencify from './sequencify'; + +const logger = getLogger({ level: 'warn', bizName: 'designer:pluginManager' }); + +export class LowCodePluginManager implements ILowCodePluginManager { + private plugins: ILowCodePlugin[] = []; + + private pluginsMap: Map = new Map(); + + private editor: Editor; + + constructor(editor: Editor) { + this.editor = editor; + } + + private _getLowCodePluginContext() { + return new LowCodePluginContext(this.editor, this); + } + + register( + pluginConfig: (ctx: ILowCodePluginContext, options: CompositeObject) => ILowCodePluginConfig, + options: CompositeObject, + ): void { + const ctx = this._getLowCodePluginContext(); + const config = pluginConfig(ctx, options); + invariant(config.name, `${config.name} required`, config); + ctx.setLogger(config); + invariant(!this.pluginsMap.has(config.name), `${config.name} already exists`, this.pluginsMap.get(config.name)); + const plugin = new LowCodePlugin(this, config, options); + this.plugins.push(plugin); + this.pluginsMap.set(plugin.name, plugin); + logger.log('plugin registered with config:', config, ', options:', options); + } + + get(pluginName: string): ILowCodePlugin { + return this.pluginsMap.get(pluginName); + } + + getAll(): ILowCodePlugin[] { + return this.plugins; + } + + has(pluginName: string): boolean { + return this.pluginsMap.has(pluginName); + } + + async delete(pluginName: string): boolean { + const idx = this.plugins.findIndex(plugin => plugin.name === pluginName); + if (idx < -1) return; + const plugin = this.plugins[idx]; + await plugin.destroy(); + + this.plugins.splice(idx, 1); + return this.pluginsMap.delete(pluginName); + } + + async init() { + const pluginNames = []; + const pluginObj = {}; + this.plugins.forEach(plugin => { + pluginNames.push(plugin.name); + pluginObj[plugin.name] = plugin; + }); + const { missingTasks, sequence } = sequencify(pluginObj, pluginNames); + invariant(!missingTasks.length, 'plugin dependency missing', missingTasks); + logger.log('load plugin sequence:', sequence); + + for (const pluginName of sequence) { + await this.pluginsMap.get(pluginName).init(); + } + } + + async destroy() { + for (const plugin of this.plugins) { + await plugin.destroy(); + } + } + + get size() { + return this.pluginsMap.size; + } + + toProxy() { + return new Proxy(this, { + get(target, prop, receiver) { + if (target.pluginsMap.has(prop)) { + // 禁用态的插件,直接返回 undefined + if (target.pluginsMap.get(prop).disabled) { + return undefined; + } + return target.pluginsMap.get(prop)?.toProxy(); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + + setDisabled(pluginName: string, flag = true) { + logger.warn(`plugin:${pluginName} has been set disable:${flag}`); + this.pluginsMap.get(pluginName)?.setDisabled(flag); + } + + async dispose() { + await this.destroy(); + this.plugins = []; + this.pluginsMap.clear(); + } +} diff --git a/packages/designer/src/plugin/plugin.ts b/packages/designer/src/plugin/plugin.ts new file mode 100644 index 000000000..07615ade2 --- /dev/null +++ b/packages/designer/src/plugin/plugin.ts @@ -0,0 +1,91 @@ +import { + ILowCodePlugin, + ILowCodePluginConfig, + ILowCodePluginManager, + CompositeObject, +} from '@ali/lowcode-types'; +import { EventEmitter } from 'events'; +import { getLogger, Logger, invariant } from '../utils'; + +export class LowCodePlugin implements ILowCodePlugin { + config: ILowCodePluginConfig; + + logger: Logger; + + private manager: ILowCodePluginManager; + + private options?: CompositeObject; + + private emiter: EventEmitter; + + private _inited: boolean; + + /** + * 标识插件状态,是否被 disabled + */ + private _disabled: boolean; + + constructor( + manager: ILowCodePluginManager, + config: ILowCodePluginConfig = {}, + options: CompositeObject = {}, + ) { + this.manager = manager; + this.config = config; + this.options = options; + this.emiter = new EventEmitter(); + this.logger = getLogger({ level: 'log', bizName: `designer:plugin:${config.name}` }); + } + + get name() { + return this.config.name; + } + + get dep() { + return this.config.dep || []; + } + + get disabled() { + return this._disabled; + } + + on(...args) { + return this.emiter.on(...args); + } + + emit(...args) { + return this.emiter.emit(...args); + } + + async init() { + this.logger.log('method init called'); + await this.config.init?.call(); + this._inited = true; + } + + async destroy() { + this.logger.log('method destroy called'); + await this.config.destroy?.call(); + } + + private setDisabled(flag = true) { + this._disabled = flag; + } + + toProxy() { + invariant(this._inited, 'Could not call toProxy before init'); + const exports = this.config.exports?.(); + return new Proxy(this, { + get(target, prop, receiver) { + if (hasOwnProperty.call(exports, prop)) { + return exports[prop]; + } + return Reflect.get(target, prop, receiver); + }, + }); + } + + dispose() { + return this.manager.delete(this.name); + } +} diff --git a/packages/designer/src/plugin/sequencify.ts b/packages/designer/src/plugin/sequencify.ts new file mode 100644 index 000000000..a312df207 --- /dev/null +++ b/packages/designer/src/plugin/sequencify.ts @@ -0,0 +1,40 @@ +function sequence(tasks, names, results, missing, recursive, nest) { + names.forEach((name) => { + if (results.indexOf(name) !== -1) { + return; // de-dup results + } + const node = tasks[name]; + if (!node) { + missing.push(name); + } else if (nest.indexOf(name) > -1) { + nest.push(name); + recursive.push(nest.slice(0)); + nest.pop(name); + } else if (node.dep.length) { + nest.push(name); + sequence(tasks, node.dep, results, missing, recursive, nest); // recurse + nest.pop(name); + } + results.push(name); + }); +} + +// tasks: object with keys as task names +// names: array of task names +export default function (tasks, names) { + let results = []; // the final sequence + const missing = []; // missing tasks + const recursive = []; // recursive task dependencies + + sequence(tasks, names, results, missing, recursive, []); + + if (missing.length || recursive.length) { + results = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion + } + + return { + sequence: results, + missingTasks: missing, + recursiveDependencies: recursive, + }; +} diff --git a/packages/designer/src/utils/index.ts b/packages/designer/src/utils/index.ts new file mode 100644 index 000000000..ab4a166b5 --- /dev/null +++ b/packages/designer/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './invariant'; +export * from './slot'; +export * from './tree'; +export * from './logger'; diff --git a/packages/designer/src/utils/invariant.ts b/packages/designer/src/utils/invariant.ts new file mode 100644 index 000000000..d5afc5276 --- /dev/null +++ b/packages/designer/src/utils/invariant.ts @@ -0,0 +1,5 @@ +export function invariant(check: any, message: string, thing?: any) { + if (!check) { + throw new Error('[designer] Invariant failed: ' + message + (thing ? ` in '${thing}'` : '')); + } +} diff --git a/packages/designer/src/utils/logger.ts b/packages/designer/src/utils/logger.ts new file mode 100644 index 000000000..21ff230a6 --- /dev/null +++ b/packages/designer/src/utils/logger.ts @@ -0,0 +1,7 @@ +import Logger, { Level } from 'zen-logger'; + +export { Logger }; + +export function getLogger(config: { level: Level, bizName: string }): Logger { + return new Logger(config); +} diff --git a/packages/editor-preset-vision/src/editor.ts b/packages/editor-preset-vision/src/editor.ts index 9f17555c2..62b179095 100644 --- a/packages/editor-preset-vision/src/editor.ts +++ b/packages/editor-preset-vision/src/editor.ts @@ -1,7 +1,7 @@ import { isJSBlock, isJSExpression, isJSSlot } from '@ali/lowcode-types'; import { isPlainObject, hasOwnProperty, cloneDeep, isI18NObject, isUseI18NSetter, convertToI18NObject, isString } from '@ali/lowcode-utils'; import { globalContext, Editor } from '@ali/lowcode-editor-core'; -import { Designer, LiveEditing, TransformStage, Node, getConvertedExtraKey } from '@ali/lowcode-designer'; +import { Designer, LiveEditing, TransformStage, Node, getConvertedExtraKey, LowCodePluginManager } from '@ali/lowcode-designer'; import Outline, { OutlineBackupPane, getTreeMaster } from '@ali/lowcode-plugin-outline-pane'; import bus from './bus'; import { VE_EVENTS } from './base/const'; @@ -35,6 +35,9 @@ export const designer = new Designer({ editor }); editor.set(Designer, designer); editor.set('designer', designer); +export const plugins = (new LowCodePluginManager(editor)).toProxy(); +editor.set('plugins', plugins); + designer.project.onCurrentDocumentChange((doc) => { bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY); editor.set('currentDocument', doc); diff --git a/packages/editor-preset-vision/src/index.ts b/packages/editor-preset-vision/src/index.ts index 52158b008..105fd2875 100644 --- a/packages/editor-preset-vision/src/index.ts +++ b/packages/editor-preset-vision/src/index.ts @@ -9,12 +9,13 @@ import { registerMetadataTransducer, addBuiltinComponentAction, removeBuiltinComponentAction, - modifyBuiltinComponentAction, + ILowCodePluginContext, + // modifyBuiltinComponentAction, } from '@ali/lowcode-designer'; import { createElement } from 'react'; import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const'; import Bus from './bus'; -import { skeleton, designer, editor } from './editor'; +import { skeleton, designer, editor, plugins } from './editor'; import { Workbench } from '@ali/lowcode-editor-skeleton'; import Panes from './panes'; import Exchange from './exchange'; @@ -37,13 +38,14 @@ import '@ali/lowcode-editor-setters'; import './vision.less'; -function init(container?: Element) { +async function init(container?: Element) { if (!container) { container = document.createElement('div'); document.body.appendChild(container); } container.id = 'engine'; + await plugins.init(); render( createElement(Workbench, { skeleton, @@ -125,6 +127,7 @@ const VisualEngine = { logger, Symbols, registerMetadataTransducer, + plugins, // Flags, }; @@ -177,12 +180,13 @@ export { logger, Symbols, registerMetadataTransducer, + plugins, }; -const version = '6.0.0 (LowcodeEngine 0.9.32)'; +const version = '1.0.28'; console.log( - `%c VisionEngine %c v${version} `, + `%c AliLowCodeEngine %c v${version} `, 'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060; font-weight: bold;', 'padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e; font-weight: bold;', ); diff --git a/packages/editor-skeleton/src/transducers/parse-props.ts b/packages/editor-skeleton/src/transducers/parse-props.ts index 53e899123..c413c9eff 100644 --- a/packages/editor-skeleton/src/transducers/parse-props.ts +++ b/packages/editor-skeleton/src/transducers/parse-props.ts @@ -69,7 +69,7 @@ function propTypeToSetter(propType: PropType): SetterType { const componentName = dataSource.length >= 4 ? 'SelectSetter' : 'RadioGroupSetter'; return { componentName, - props: { dataSource }, + props: { dataSource, options: dataSource }, isRequired, initialValue: dataSource[0] ? dataSource[0].value : null, }; @@ -148,6 +148,8 @@ function propTypeToSetter(propType: PropType): SetterType { }, isRequired, }; + default: + // do nothing } return { componentName: 'MixedSetter', diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 64db05263..927879331 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -17,3 +17,4 @@ export * from './node'; export * from './transform-stage'; export * from './code-intermediate'; export * from './code-result'; +export * from './plugin'; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts new file mode 100644 index 000000000..ed4a20f89 --- /dev/null +++ b/packages/types/src/plugin.ts @@ -0,0 +1,54 @@ +import { CompositeObject } from '@ali/lowcode-types'; +import Logger from 'zen-logger'; +import { Skeleton } from '@ali/lowcode-editor-skeleton'; +import { Editor, Hotkey } from '@ali/lowcode-editor-core'; + +export interface ILowCodePluginConfig { + manager: ILowCodePluginManager; + name: string; + dep: string[]; // 依赖插件名 + init(): void; + destroy(): void; + exports(): CompositeObject; +} + +export interface ILowCodePlugin { + name: string; + dep: string[]; + disabled: boolean; + config: ILowCodePluginConfig; + logger: Logger; + emit(): void; + on(): void; + init(): void; + destroy(): void; + toProxy(): any; + setDisabled(flag: boolean): void; +} + +export interface ILowCodePluginContext { + skeleton: Skeleton; + editor: Editor; + plugins: ILowCodePluginManager; + hotkey: Hotkey; + logger: Logger; + /** + 其他暂不增加,按需增加 + */ +} + +export interface ILowCodePluginManager { + register( + pluginConfig: (ctx: ILowCodePluginContext, options: CompositeObject) => ILowCodePluginConfig, + options: CompositeObject, + ): void; + get(pluginName: string): ILowCodePlugin; + getAll(): ILowCodePlugin[]; + has(pluginName: string): boolean; + delete(pluginName: string): boolean; + setDisabled(pluginName: string, flag: boolean): void; + dispose(): void; + /** + 后续可以补充插件操作,比如 disable / enable 之类的 + */ +}