diff --git a/.gitignore b/.gitignore index 467138d89..4e5dae3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ typings/ codealike.json .node -.must.config.js +# docs +.vitepress diff --git a/.prettierrc b/.prettierrc index 9153397d3..dc245b2cb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "printWidth": 100, + "printWidth": 120, "tabWidth": 2, "semi": true, "jsxSingleQuote": false, diff --git a/TODOS.md b/TODOS.md index 95fda2cdf..c9b71c9e8 100644 --- a/TODOS.md +++ b/TODOS.md @@ -12,3 +12,7 @@ 7. github workflows lodaes replace + +IDE 的引擎设计 +工作区的虚拟文件设计 +窗口、视图、编辑器、边栏、面板的设计 diff --git a/docs/api-examples.md b/docs/api-examples.md new file mode 100644 index 000000000..6bd8bb5c1 --- /dev/null +++ b/docs/api-examples.md @@ -0,0 +1,49 @@ +--- +outline: deep +--- + +# Runtime API Examples + +This page demonstrates usage of some of the runtime APIs provided by VitePress. + +The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: + +```md + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+``` + + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+ +## More + +Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md new file mode 100644 index 000000000..3b756a987 --- /dev/null +++ b/docs/examples/hello-world.md @@ -0,0 +1,8 @@ +# 你好,世界 + +step1: 启动引擎,新建一个项目,新建一个低代码文件(.lc) +step2: 打开画布 +step3: 拖拽组件 +step4: 点击保存 (cmd + s) +step5: 点击预览 (cmd + p) +step6: 点击关闭,重新打开这个页面,内容不变 diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..d629239e7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,22 @@ +--- +layout: home + +hero: + name: 'LowCodeEngine' + text: '基于 LowCodeEngine 快速打造高生产力的低代码研发平台' + actions: + - theme: brand + text: 介绍 + link: /guide/introduction + - theme: alt + text: 快速开始 + link: /guide/quick-start + +features: + - title: 标准化协议 + details: 底层协议栈定义的是标准,标准的统一让上层产物的互通成为可能 + - title: 最小内核 + details: 精心打造低代码领域的编排、入料、出码、渲染模块 + - title: 最强生态 + details: 配套生态开箱即用,打造企业级低代码技术体系 +--- diff --git a/docs/markdown-examples.md b/docs/markdown-examples.md new file mode 100644 index 000000000..f9258a550 --- /dev/null +++ b/docs/markdown-examples.md @@ -0,0 +1,85 @@ +# Markdown Extension Examples + +This page demonstrates some of the built-in markdown extensions provided by VitePress. + +## Syntax Highlighting + +VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: + +**Input** + +````md +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` +```` + +**Output** + +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` + +## Custom Containers + +**Input** + +```md +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: +``` + +**Output** + +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: + +## More + +Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..00c356d09 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alilc/lowcode-engine-docs", + "version": "2.0.0-alpha.0", + "description": "低代码引擎版本化文档", + "keywords": [ + "lowcode", + "engine", + "docs" + ], + "scripts": { + "docs:dev": "vitepress dev .", + "docs:build": "vitepress build .", + "docs:preview": "vitepress preview ." + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main" + }, + "license": "MIT", + "devDependencies": { + "vitepress": "^1.3.1" + } +} diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 000000000..9db6d0d06 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/engine-core/src/command/commandRegistry.ts b/packages/engine-core/src/command/commandRegistry.ts index abf01e129..5cefb00c8 100644 --- a/packages/engine-core/src/command/commandRegistry.ts +++ b/packages/engine-core/src/command/commandRegistry.ts @@ -1,12 +1,12 @@ import { - type Event, - type EventDisposable, - type EventListener, - Emitter, + Events, LinkedList, TypeConstraint, validateConstraints, Iterable, + IDisposable, + Disposable, + toDisposable, } from '@alilc/lowcode-shared'; import { ICommand, ICommandHandler } from './command'; import { Extensions, Registry } from '../extension/registry'; @@ -15,27 +15,24 @@ import { ICommandService } from './commandService'; export type ICommandsMap = Map; export interface ICommandRegistry { - onDidRegisterCommand: Event; + onDidRegisterCommand: Events.Event; - registerCommand(id: string, command: ICommandHandler): EventDisposable; - registerCommand(command: ICommand): EventDisposable; + registerCommand(id: string, command: ICommandHandler): IDisposable; + registerCommand(command: ICommand): IDisposable; - registerCommandAlias(oldId: string, newId: string): EventDisposable; + registerCommandAlias(oldId: string, newId: string): IDisposable; getCommand(id: string): ICommand | undefined; getCommands(): ICommandsMap; } -class CommandsRegistryImpl implements ICommandRegistry { +class CommandsRegistryImpl extends Disposable implements ICommandRegistry { private readonly _commands = new Map>(); - private readonly _didRegisterCommandEmitter = new Emitter(); + private readonly _onDidRegisterCommand = this._addDispose(new Events.Emitter()); + onDidRegisterCommand = this._onDidRegisterCommand.event; - onDidRegisterCommand(fn: EventListener) { - return this._didRegisterCommandEmitter.on(fn); - } - - registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable { + registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): IDisposable { if (!idOrCommand) { throw new Error(`invalid command`); } @@ -71,21 +68,21 @@ class CommandsRegistryImpl implements ICommandRegistry { const removeFn = commands.unshift(idOrCommand); - const ret = () => { + const ret = toDisposable(() => { removeFn(); const command = this._commands.get(id); if (command?.isEmpty()) { this._commands.delete(id); } - }; + }); // tell the world about this command - this._didRegisterCommandEmitter.emit(id); + this._onDidRegisterCommand.notify(id); return ret; } - registerCommandAlias(oldId: string, newId: string): EventDisposable { + registerCommandAlias(oldId: string, newId: string): IDisposable { return this.registerCommand(oldId, (accessor, ...args) => accessor.get(ICommandService).executeCommand(newId, ...args), ); diff --git a/packages/engine-core/src/common/charCode.ts b/packages/engine-core/src/common/charCode.ts new file mode 100644 index 000000000..bbe299c24 --- /dev/null +++ b/packages/engine-core/src/common/charCode.ts @@ -0,0 +1,228 @@ +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, +} diff --git a/packages/engine-core/src/common/glob.ts b/packages/engine-core/src/common/glob.ts new file mode 100644 index 000000000..96241b89b --- /dev/null +++ b/packages/engine-core/src/common/glob.ts @@ -0,0 +1,23 @@ +export interface IRelativePattern { + /** + * A base file path to which this pattern will be matched against relatively. + */ + readonly base: string; + + /** + * A file glob pattern like `*.{ts,js}` that will be matched on file paths + * relative to the base path. + * + * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`, + * the file glob pattern will match on `index.js`. + */ + readonly pattern: string; +} + +export interface IExpression { + [pattern: string]: boolean | SiblingClause; +} + +interface SiblingClause { + when: string; +} diff --git a/packages/engine-core/src/common/index.ts b/packages/engine-core/src/common/index.ts new file mode 100644 index 000000000..292fa1e36 --- /dev/null +++ b/packages/engine-core/src/common/index.ts @@ -0,0 +1,11 @@ +import * as Schemas from './schemas'; + +export { Schemas }; + +export * from './charCode'; +export * from './glob'; +export * from './keyCodes'; +export * from './path'; +export * from './strings'; +export * from './ternarySearchTree'; +export * from './uri'; diff --git a/packages/shared/src/common/keyCodes.ts b/packages/engine-core/src/common/keyCodes.ts similarity index 95% rename from packages/shared/src/common/keyCodes.ts rename to packages/engine-core/src/common/keyCodes.ts index 8436a2e6b..0e678761a 100644 --- a/packages/shared/src/common/keyCodes.ts +++ b/packages/engine-core/src/common/keyCodes.ts @@ -1243,50 +1243,21 @@ for (let i = 0; i <= KeyCode.MAX_VALUE; i++) { IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter; })(); -export namespace KeyCodeUtils { - export function toString(keyCode: KeyCode): string { - return uiMap.keyCodeToStr(keyCode); - } - export function fromString(key: string): KeyCode { - return uiMap.strToKeyCode(key); - } - - export function toUserSettingsUS(keyCode: KeyCode): string { - return userSettingsUSMap.keyCodeToStr(keyCode); - } - export function toUserSettingsGeneral(keyCode: KeyCode): string { - return userSettingsGeneralMap.keyCodeToStr(keyCode); - } - export function fromUserSettings(key: string): KeyCode { - return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); - } - - export function toElectronAccelerator(keyCode: KeyCode): string | null { - if (keyCode >= KeyCode.Numpad0 && keyCode <= KeyCode.NumpadDivide) { - // [Electron Accelerators] Electron is able to parse numpad keys, but unfortunately it - // renders them just as regular keys in menus. For example, num0 is rendered as "0", - // numdiv is rendered as "/", numsub is rendered as "-". - // - // This can lead to incredible confusion, as it makes numpad based keybindings indistinguishable - // from keybindings based on regular keys. - // - // We therefore need to fall back to custom rendering for numpad keys. - return null; - } - - switch (keyCode) { - case KeyCode.UpArrow: - return 'Up'; - case KeyCode.DownArrow: - return 'Down'; - case KeyCode.LeftArrow: - return 'Left'; - case KeyCode.RightArrow: - return 'Right'; - } +export function keyCodeToString(keyCode: KeyCode): string { + return uiMap.keyCodeToStr(keyCode); +} +export function keyCodeFromString(key: string): KeyCode { + return uiMap.strToKeyCode(key); +} - return uiMap.keyCodeToStr(keyCode); - } +export function keyCodeToUserSettingsUS(keyCode: KeyCode): string { + return userSettingsUSMap.keyCodeToStr(keyCode); +} +export function keyCodeToUserSettingsGeneral(keyCode: KeyCode): string { + return userSettingsGeneralMap.keyCodeToStr(keyCode); +} +export function keyCodeFromUserSettings(key: string): KeyCode { + return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); } export const enum KeyMod { diff --git a/packages/engine-core/src/common/path.ts b/packages/engine-core/src/common/path.ts new file mode 100644 index 000000000..b146770f1 --- /dev/null +++ b/packages/engine-core/src/common/path.ts @@ -0,0 +1,279 @@ +import { CharCode } from './charCode'; + +export function normalize(path: string): string { + if (path.length === 0) { + return '.'; + } + + const isAbsolute = path.charCodeAt(0) === CharCode.Slash; + const trailingSeparator = path.charCodeAt(path.length - 1) === CharCode.Slash; + + // Normalize the path + path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator); + + if (path.length === 0) { + if (isAbsolute) { + return '/'; + } + return trailingSeparator ? './' : '.'; + } + if (trailingSeparator) { + path += '/'; + } + + return isAbsolute ? `/${path}` : path; +} + +export function join(...paths: string[]): string { + if (paths.length === 0) { + return '.'; + } + let joined; + for (let i = 0; i < paths.length; ++i) { + const arg = paths[i]; + if (arg.length > 0) { + if (joined === undefined) { + joined = arg; + } else { + joined += `/${arg}`; + } + } + } + if (joined === undefined) { + return '.'; + } + return normalize(joined); +} + +export function dirname(path: string): string { + if (path.length === 0) { + return '.'; + } + const hasRoot = path.charCodeAt(0) === CharCode.Slash; + let end = -1; + let matchedSlash = true; + for (let i = path.length - 1; i >= 1; --i) { + if (path.charCodeAt(i) === CharCode.Slash) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) { + return hasRoot ? '/' : '.'; + } + if (hasRoot && end === 1) { + return '//'; + } + return path.slice(0, end); +} + +export function basename(path: string, suffix?: string): string { + let start = 0; + let end = -1; + let matchedSlash = true; + let i; + + if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { + if (suffix === path) { + return ''; + } + let extIdx = suffix.length - 1; + let firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CharCode.Slash) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === suffix.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) { + end = firstNonSlashEnd; + } else if (end === -1) { + end = path.length; + } + return path.slice(start, end); + } + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === CharCode.Slash) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) { + return ''; + } + return path.slice(start, end); +} + +export function extname(path: string): string { + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CharCode.Slash) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CharCode.Period) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) { + startDot = i; + } else if (preDotState !== 1) { + preDotState = 1; + } + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ''; + } + return path.slice(startDot, end); +} + +function isPosixPathSeparator(code: number | undefined) { + return code === CharCode.Slash; +} + +// Resolves . and .. elements in a path with directory names +function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code?: number) => boolean, +) { + let res = ''; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code = 0; + for (let i = 0; i <= path.length; ++i) { + if (i < path.length) { + code = path.charCodeAt(i); + } else if (isPathSeparator(code)) { + break; + } else { + code = CharCode.Slash; + } + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CharCode.Period || + res.charCodeAt(res.length - 2) !== CharCode.Period + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ''; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length !== 0) { + res = ''; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + res += res.length > 0 ? `${separator}..` : '..'; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) { + res += `${separator}${path.slice(lastSlash + 1, i)}`; + } else { + res = path.slice(lastSlash + 1, i); + } + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CharCode.Period && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} diff --git a/packages/engine-core/src/common/schemas.ts b/packages/engine-core/src/common/schemas.ts new file mode 100644 index 000000000..67fedca75 --- /dev/null +++ b/packages/engine-core/src/common/schemas.ts @@ -0,0 +1,7 @@ +export const http = 'http'; + +export const https = 'https'; + +export const file = 'file'; + +export const untitled = 'untitled'; diff --git a/packages/engine-core/src/common/strings.ts b/packages/engine-core/src/common/strings.ts new file mode 100644 index 000000000..4dab4afb4 --- /dev/null +++ b/packages/engine-core/src/common/strings.ts @@ -0,0 +1,88 @@ +import { CharCode } from './charCode'; + +export function compareSubstring( + a: string, + b: string, + aStart: number = 0, + aEnd: number = a.length, + bStart: number = 0, + bEnd: number = b.length, +): number { + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + const codeA = a.charCodeAt(aStart); + const codeB = b.charCodeAt(bStart); + if (codeA < codeB) { + return -1; + } else if (codeA > codeB) { + return 1; + } + } + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + return 0; +} + +export function compareIgnoreCase(a: string, b: string): number { + return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length); +} + +export function compareSubstringIgnoreCase( + a: string, + b: string, + aStart: number = 0, + aEnd: number = a.length, + bStart: number = 0, + bEnd: number = b.length, +): number { + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + let codeA = a.charCodeAt(aStart); + let codeB = b.charCodeAt(bStart); + + if (codeA === codeB) { + // equal + continue; + } + + if (codeA >= 128 || codeB >= 128) { + // not ASCII letters -> fallback to lower-casing strings + return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd); + } + + // mapper lower-case ascii letter onto upper-case varinats + // [97-122] (lower ascii) --> [65-90] (upper ascii) + if (isLowerAsciiLetter(codeA)) { + codeA -= 32; + } + if (isLowerAsciiLetter(codeB)) { + codeB -= 32; + } + + // compare both code points + const diff = codeA - codeB; + if (diff === 0) { + continue; + } + + return diff; + } + + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + + return 0; +} + +export function isLowerAsciiLetter(code: number): boolean { + return code >= CharCode.a && code <= CharCode.z; +} diff --git a/packages/engine-core/src/common/ternarySearchTree.ts b/packages/engine-core/src/common/ternarySearchTree.ts new file mode 100644 index 000000000..9042cbb5d --- /dev/null +++ b/packages/engine-core/src/common/ternarySearchTree.ts @@ -0,0 +1,297 @@ +import { CharCode } from './charCode'; +import { compareSubstring, compareSubstringIgnoreCase } from './strings'; + +export interface IKeyIterator { + reset(key: K): this; + next(): this; + + hasNext(): boolean; + compare(a: string): number; + value(): string; +} + +export class PathIterator implements IKeyIterator { + private _value!: string; + private _valueLen!: number; + private _from!: number; + private _to!: number; + + constructor(private readonly _caseSensitive: boolean = true) {} + + reset(key: string): this { + this._from = 0; + this._to = 0; + this._value = key; + this._valueLen = key.length; + + for (let pos = key.length - 1; pos >= 0; pos--, this._valueLen--) { + const ch = this._value.charCodeAt(pos); + if (!(ch === CharCode.Slash)) { + break; + } + } + + return this.next(); + } + + next(): this { + this._from = this._to; + + let justSeps = true; + for (; this._to < this._valueLen; this._to++) { + const ch = this._value.charCodeAt(this._to); + if (ch === CharCode.Slash) { + if (justSeps) { + this._from++; + } else { + break; + } + } else { + justSeps = false; + } + } + + return this; + } + + hasNext(): boolean { + return this._to < this._valueLen; + } + + compare(a: string): number { + return this._caseSensitive + ? compareSubstring(a, this._value, 0, a.length, this._from, this._to) + : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to); + } + + value(): string { + return this._value.substring(this._from, this._to); + } +} + +class TernarySearchTreeNode { + height: number = 1; + segment!: string; + value: V | undefined; + key: K | undefined; + left: TernarySearchTreeNode | undefined; + mid: TernarySearchTreeNode | undefined; + right: TernarySearchTreeNode | undefined; + + isEmpty(): boolean { + return !this.left && !this.mid && !this.right && !this.value; + } + + rotateLeft() { + const tmp = this.right!; + this.right = tmp.left; + tmp.left = this; + this.updateHeight(); + tmp.updateHeight(); + return tmp; + } + + rotateRight() { + const tmp = this.left!; + this.left = tmp.right; + tmp.right = this; + this.updateHeight(); + tmp.updateHeight(); + return tmp; + } + + updateHeight() { + this.height = 1 + Math.max(this.heightLeft, this.heightRight); + } + + balanceFactor() { + return this.heightRight - this.heightLeft; + } + + get heightLeft() { + return this.left?.height ?? 0; + } + + get heightRight() { + return this.right?.height ?? 0; + } +} + +const enum Dir { + Left = -1, + Mid = 0, + Right = 1, +} + +/** + * TST的应用场景包括但不限于搜索引擎、代码检查、自动补全等功能,尤其适用于大量字符串查询的场景 。 + * 例如,在实现搜索框智能提示功能时,TST可以高效地进行前缀匹配,快速给出用户可能的输入选项 。 + */ +export class TernarySearchTree { + static forPaths(ignorePathCasing = false): TernarySearchTree { + return new TernarySearchTree(new PathIterator(ignorePathCasing)); + } + + private _iter: IKeyIterator; + + private _root: TernarySearchTreeNode | undefined; + + constructor(segments: IKeyIterator) { + this._iter = segments; + } + + clear() { + this._root = undefined; + } + + set(key: K, element: V): V | undefined { + const iter = this._iter.reset(key); + + if (!this._root) { + this._root = new TernarySearchTreeNode(); + this._root.segment = iter.value(); + } + + const stack: [Dir, TernarySearchTreeNode][] = []; + + let node: TernarySearchTreeNode = this._root; + while (true) { + const val = iter.compare(node.segment); + + if (val > 0) { + // left + if (!node.left) { + node.left = new TernarySearchTreeNode(); + node.left.segment = iter.value(); + } + stack.push([Dir.Left, node]); + node = node.left; + } else if (val < 0) { + // right + if (!node.right) { + node.right = new TernarySearchTreeNode(); + node.right.segment = iter.value(); + } + stack.push([Dir.Right, node]); + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + if (!node.mid) { + node.mid = new TernarySearchTreeNode(); + node.mid.segment = iter.value(); + } + stack.push([Dir.Mid, node]); + node = node.mid; + } else { + break; + } + } + + // set value + const oldElement = node.value; + node.value = element; + node.key = key; + + // balance + for (let i = stack.length - 1; i >= 0; i--) { + const node = stack[i][1]; + + node.updateHeight(); + const bf = node.balanceFactor(); + + if (bf < -1 || bf > 1) { + // needs rotate + const d1 = stack[i][0]; + const d2 = stack[i + 1][0]; + + if (d1 === Dir.Right && d2 === Dir.Right) { + //right, right -> rotate left + stack[i][1] = node.rotateLeft(); + } else if (d1 === Dir.Left && d2 === Dir.Left) { + // left, left -> rotate right + stack[i][1] = node.rotateRight(); + } else if (d1 === Dir.Right && d2 === Dir.Left) { + // right, left -> double rotate right, left + node.right = stack[i + 1][1] = stack[i + 1][1].rotateRight(); + stack[i][1] = node.rotateLeft(); + } else if (d1 === Dir.Left && d2 === Dir.Right) { + // left, right -> double rotate left, right + node.left = stack[i + 1][1] = stack[i + 1][1].rotateLeft(); + stack[i][1] = node.rotateRight(); + } else { + throw new Error(); + } + + // patch path to parent + if (i > 0) { + switch (stack[i - 1][0]) { + case Dir.Left: + stack[i - 1][1].left = stack[i][1]; + break; + case Dir.Right: + stack[i - 1][1].right = stack[i][1]; + break; + case Dir.Mid: + stack[i - 1][1].mid = stack[i][1]; + break; + } + } else { + this._root = stack[0][1]; + } + } + } + + return oldElement; + } + + get(key: K): V | undefined { + return this._getNode(key)?.value; + } + + private _getNode(key: K) { + const iter = this._iter.reset(key); + let node = this._root; + while (node) { + const val = iter.compare(node.segment); + if (val > 0) { + // left + node = node.left; + } else if (val < 0) { + // right + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + node = node.mid; + } else { + break; + } + } + return node; + } + + findSubstr(key: K): V | undefined { + const iter = this._iter.reset(key); + + let node = this._root; + let candidate: V | undefined = undefined; + while (node) { + const val = iter.compare(node.segment); + if (val > 0) { + node = node.left; + } else if (val < 0) { + node = node.right; + } else if (iter.hasNext()) { + iter.next(); + + candidate = node.value || candidate; + node = node.mid; + } else { + break; + } + } + + return node?.value ?? candidate; + } +} diff --git a/packages/engine-core/src/common/uri.ts b/packages/engine-core/src/common/uri.ts index 83f1ca4b3..a09aecabe 100644 --- a/packages/engine-core/src/common/uri.ts +++ b/packages/engine-core/src/common/uri.ts @@ -1,11 +1,133 @@ +import * as Schemas from './schemas'; +import { join } from './path'; + export interface UriComponents { - path: string; + scheme?: string; + authority?: string; + path?: string; } +const EMPTY = ''; +const SLASH = '/'; +const URI_REGEXP = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + +/** + * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component parts + * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * ```txt + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + * ``` + */ export class URI implements UriComponents { + readonly scheme: string; + readonly authority: string; readonly path: string; - constructor(path: string) { - this.path = path; + constructor(scheme?: string, authority?: string, path?: string); + constructor(data?: UriComponents); + constructor(data?: UriComponents | string, authority?: string, path?: string) { + if (typeof data === 'object') { + this.scheme = data.scheme || EMPTY; + this.authority = data.authority || EMPTY; + this.path = data.path || EMPTY; + } else { + this.scheme = data || 'file'; + this.authority = authority || EMPTY; + this.path = path || EMPTY; + } + } + + with(change: { scheme?: string; path?: string }): URI { + let { scheme, path } = change; + + if (scheme === undefined) { + scheme = this.scheme; + } else if (scheme === null) { + scheme = EMPTY; + } + + if (path === undefined) { + path = this.path; + } else if (path === null) { + path = EMPTY; + } + + return new URI({ scheme, path }); + } + + toString(): string { + let res = ''; + const { scheme, authority, path } = this; + if (scheme) { + res += scheme; + res += ':'; + } + if (authority || scheme === Schemas.file) { + res += SLASH; + res += SLASH; + } + if (authority) { + res += authority.toLowerCase(); + } + if (path) { + res += path; + } + + return res; + } + + static isUri(thing: any): thing is URI { + if (thing instanceof URI) return true; + if (!thing) return false; + + return typeof thing.path === 'string' && typeof thing.authority === 'string' && typeof thing.scheme === 'string'; + } + + static joinPath(uri: URI, ...pathSegments: string[]) { + if (!uri.path) { + throw new URIError(`cannot call joinPath on URI without path`); + } + + return uri.with({ path: join(uri.path, ...pathSegments) }); + } + + /** + * Creates new URI from uri components. + * + * Unless `strict` is `true` the scheme is defaults to be `file`. This function performs + * validation and should be used for untrusted uri components retrieved from storage, + * user input, command arguments etc + */ + static from(components: UriComponents): URI { + return new URI(components); + } + + /** + * Creates a new URI from a string, e.g. `/some/path` + * + * @param value A string which represents an URI (see `URI#toString`). + */ + static parse(value: string): URI { + const match = URI_REGEXP.exec(value); + if (!match) { + return new URI(); + } + return new URI(match[2] || EMPTY, match[4] || EMPTY, match[5] || EMPTY); + } +} + +class URIError extends Error { + constructor(message: string) { + super(`[UriError]: ${message}`); + this.name = 'URIError'; } } diff --git a/packages/engine-core/src/configuration/configurationRegistry.ts b/packages/engine-core/src/configuration/configurationRegistry.ts index b077ff551..7570f5cb6 100644 --- a/packages/engine-core/src/configuration/configurationRegistry.ts +++ b/packages/engine-core/src/configuration/configurationRegistry.ts @@ -1,11 +1,11 @@ import { - type Event, - Emitter, + Events, type StringDictionary, type JSONSchemaType, jsonTypes, IJSONSchema, types, + Disposable, } from '@alilc/lowcode-shared'; import { isUndefined, isObject } from 'lodash-es'; import { Extensions, Registry } from '../extension/registry'; @@ -44,7 +44,7 @@ export interface IConfigurationRegistry { * Event that fires whenever a configuration has been * registered. */ - readonly onDidUpdateConfiguration: Event<{ + readonly onDidUpdateConfiguration: Events.Event<{ properties: ReadonlySet; defaultsOverrides?: boolean; }>; @@ -133,7 +133,15 @@ export const allSettings: { patternProperties: StringDictionary; } = { properties: {}, patternProperties: {} }; -export class ConfigurationRegistryImpl implements IConfigurationRegistry { +export class ConfigurationRegistryImpl extends Disposable implements IConfigurationRegistry { + private _onDidUpdateConfiguration = this._addDispose( + new Events.Emitter<{ + properties: ReadonlySet; + defaultsOverrides?: boolean; + }>(), + ); + onDidUpdateConfiguration = this._onDidUpdateConfiguration.event; + private registeredConfigurationDefaults: IConfigurationDefaults[] = []; private readonly configurationDefaultsOverrides: Map< string, @@ -147,12 +155,9 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { private readonly excludedConfigurationProperties: StringDictionary; private overrideIdentifiers = new Set(); - private propertiesChangeEmitter = new Emitter<{ - properties: ReadonlySet; - defaultsOverrides?: boolean; - }>(); - constructor() { + super(); + this.configurationDefaultsOverrides = new Map(); this.configurationProperties = {}; this.excludedConfigurationProperties = {}; @@ -169,7 +174,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { const properties = new Set(); this.doRegisterConfigurations(configurations, validate, properties); - this.propertiesChangeEmitter.emit({ properties }); + this._onDidUpdateConfiguration.notify({ properties }); return properties; } @@ -278,7 +283,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { deregisterConfigurations(configurations: IConfigurationNode[]): void { const properties = new Set(); this.doDeregisterConfigurations(configurations, properties); - this.propertiesChangeEmitter.emit({ properties }); + this._onDidUpdateConfiguration.notify({ properties }); } private doDeregisterConfigurations( @@ -305,7 +310,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { const properties = new Set(); this.doRegisterDefaultConfigurations(configurationDefaults, properties); - this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true }); + this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true }); } private doRegisterDefaultConfigurations( @@ -476,7 +481,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { const properties = new Set(); this.doDeregisterDefaultConfigurations(defaultConfigurations, properties); - this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true }); + this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true }); } private doDeregisterDefaultConfigurations( @@ -608,15 +613,6 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry { return configurationDefaultsOverrides; } - onDidUpdateConfiguration( - fn: (change: { - properties: ReadonlySet; - defaultsOverrides?: boolean | undefined; - }) => void, - ) { - return this.propertiesChangeEmitter.on(fn); - } - private registerJSONConfiguration(configuration: IConfigurationNode) { const register = (configuration: IConfigurationNode) => { const properties = configuration.properties; diff --git a/packages/engine-core/src/configuration/configurationService.ts b/packages/engine-core/src/configuration/configurationService.ts index c257c1736..dd22efdba 100644 --- a/packages/engine-core/src/configuration/configurationService.ts +++ b/packages/engine-core/src/configuration/configurationService.ts @@ -1,4 +1,4 @@ -import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared'; +import { createDecorator, Disposable, Events } from '@alilc/lowcode-shared'; import { Configuration, DefaultConfiguration, @@ -58,19 +58,24 @@ export interface IConfigurationService { memory?: string[]; }; - onDidChangeConfiguration: Event; + onDidChangeConfiguration: Events.Event; } export const IConfigurationService = createDecorator('configurationService'); -export class ConfigurationService implements IConfigurationService { +export class ConfigurationService extends Disposable implements IConfigurationService { private configuration: Configuration; private readonly defaultConfiguration: DefaultConfiguration; private readonly userConfiguration: UserConfiguration; - private readonly didChangeEmitter = new Emitter(); + private readonly _onDidChangeConfiguration = this._addDispose( + new Events.Emitter(), + ); + onDidChangeConfiguration = this._onDidChangeConfiguration.event; constructor() { + super(); + this.defaultConfiguration = new DefaultConfiguration(); this.userConfiguration = new UserConfiguration({}); this.configuration = new Configuration( @@ -172,11 +177,7 @@ export class ConfigurationService implements IConfigurationService { { data: previous }, this.configuration, ); - this.didChangeEmitter.emit(event); - } - - onDidChangeConfiguration(listener: EventListener) { - return this.didChangeEmitter.on(listener); + this._onDidChangeConfiguration.notify(event); } } diff --git a/packages/engine-core/src/configuration/configurations.ts b/packages/engine-core/src/configuration/configurations.ts index 42f40dafe..4b8c541e3 100644 --- a/packages/engine-core/src/configuration/configurations.ts +++ b/packages/engine-core/src/configuration/configurations.ts @@ -1,4 +1,4 @@ -import { type StringDictionary, Emitter, type EventListener } from '@alilc/lowcode-shared'; +import { type StringDictionary, Disposable, Events } from '@alilc/lowcode-shared'; import { ConfigurationModel, type IConfigurationModel, @@ -8,7 +8,6 @@ import { import { ConfigurationRegistry, type IConfigurationPropertySchema, - type IConfigurationRegistry, type IRegisteredConfigurationPropertySchema, } from './configurationRegistry'; import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es'; @@ -23,11 +22,14 @@ export interface IConfigurationOverrides { overrideIdentifier?: string | null; } -export class DefaultConfiguration { - private emitter = new Emitter<{ - defaults: ConfigurationModel; - properties: string[]; - }>(); +export class DefaultConfiguration extends Disposable { + private _onDidChangeConfiguration = this._addDispose( + new Events.Emitter<{ + defaults: ConfigurationModel; + properties: string[]; + }>(), + ); + onDidChangeConfiguration = this._onDidChangeConfiguration.event; private _configurationModel = ConfigurationModel.createEmptyModel(); @@ -49,15 +51,9 @@ export class DefaultConfiguration { return this.configurationModel; } - onDidChangeConfiguration( - listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>, - ) { - return this.emitter.on(listener); - } - private onDidUpdateConfiguration(properties: string[]): void { this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties()); - this.emitter.emit({ defaults: this.configurationModel, properties }); + this._onDidChangeConfiguration.notify({ defaults: this.configurationModel, properties }); } private resetConfigurationModel(): void { @@ -238,7 +234,7 @@ export class UserConfiguration { async loadConfiguration(): Promise { try { - // const content = await this.fileService.readFile(this.userSettingsResource); + // 可能远程请求或者读取对应配置 this.parser.parse({}, this.parseOptions); return this.parser.configurationModel; } catch (e) { diff --git a/packages/engine-core/src/file/file.ts b/packages/engine-core/src/file/file.ts new file mode 100644 index 000000000..2876624f4 --- /dev/null +++ b/packages/engine-core/src/file/file.ts @@ -0,0 +1,243 @@ +import { type IDisposable, Events } from '@alilc/lowcode-shared'; +import { URI, IRelativePattern } from '../common'; + +export enum FileType { + /** + * File is unknown (neither file, directory). + */ + Unknown = 0, + + /** + * File is a normal file. + */ + File = 1, + + /** + * File is a directory. + */ + Directory = 2, +} + +export enum FilePermission { + None = 0, + + /** + * File is readonly. Components like editors should not + * offer to edit the contents. + */ + Readable = 1, + + Writable = 2, +} + +export interface IStat { + /** + * The file type. + */ + readonly type: FileType; + + /** + * The last modification date represented as millis from unix epoch. + */ + readonly mtime: number; + + /** + * The creation date represented as millis from unix epoch. + */ + readonly ctime: number; + + /** + * The file permissions. + */ + readonly permission: FilePermission; +} + +export interface IBaseFileStat { + /** + * The unified resource identifier of this file or folder. + */ + readonly resource: URI; + + /** + * The name which is the last segment + * of the {{path}}. + */ + readonly name: string; + + /** + * The last modification date represented as millis from unix epoch. + * + * The value may or may not be resolved as + * it is optional. + */ + readonly mtime: number; + + /** + * The creation date represented as millis from unix epoch. + */ + readonly ctime: number; + + readonly permission: FilePermission; +} + +/** + * A file resource with meta information and resolved children if any. + */ +export interface IFileStat extends IBaseFileStat { + /** + * The resource is a file. + */ + readonly isFile: boolean; + + /** + * The resource is a directory. + */ + readonly isDirectory: boolean; +} + +export interface IFileOverwriteOptions { + /** + * Set to `true` to overwrite a file if it exists. Will + * throw an error otherwise if the file does exist. + */ + readonly overwrite?: boolean; +} + +export interface IFileCreateOptions { + /** + * Set to `true` to create parent directory when it does not exist. Will + * throw an error otherwise if the file does not exist. + */ + readonly recursive?: boolean; +} + +export interface IFileDeleteOptions { + /** + * Set to `true` to recursively delete any children of the file. This + * only applies to folders and can lead to an error unless provided + * if the folder is not empty. + */ + readonly recursive?: boolean; +} + +export interface IFileWriteOptions extends IFileCreateOptions, IFileOverwriteOptions {} + +/** + * Identifies a single change in a file. + */ +export interface IFileChange { + /** + * The type of change that occurred to the file. + */ + type: FileChangeType; + + /** + * The unified resource identifier of the file that changed. + */ + readonly resource: URI; +} + +/** + * Possible changes that can occur to a file. + */ +export const enum FileChangeType { + UPDATED = 1 << 1, + ADDED = 1 << 2, + DELETED = 1 << 3, +} + +export interface IWatchOptions { + /** + * Set to `true` to watch for changes recursively in a folder + * and all of its children. + */ + recursive: boolean; + + /** + * A set of glob patterns or paths to exclude from watching. + * Paths can be relative or absolute and when relative are + * resolved against the watched folder. Glob patterns are + * always matched relative to the watched folder. + */ + excludes: string[]; + + /** + * An optional set of glob patterns or paths to include for + * watching. If not provided, all paths are considered for + * events. + * Paths can be relative or absolute and when relative are + * resolved against the watched folder. Glob patterns are + * always matched relative to the watched folder. + */ + includes?: Array; + + /** + * If provided, allows to filter the events that the watcher should consider + * for emitting. If not provided, all events are emitted. + * + * For example, to emit added and updated events, set to: + * `FileChangeType.ADDED | FileChangeType.UPDATED`. + */ + filter?: number; +} + +export interface IFileSystemWatcher extends IDisposable { + /** + * An event which fires on file/folder change only for changes + * that correlate to the watch request with matching correlation + * identifier. + */ + readonly onDidChange: Events.Event; +} + +export class FileChangesEvent {} + +export enum FsContants { + F_OK = 1, + R_OK = 1 << 1, + W_OK = 1 << 2, +} + +export interface IFileSystemProvider { + watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher; + + chmod(resource: URI, mode: number): Promise; + access(resource: URI, mode?: number): Promise; + stat(resource: URI): Promise; + mkdir(resource: URI, opts: IFileWriteOptions): Promise; + readdir(resource: URI): Promise<[string, FileType][]>; + delete(resource: URI, opts: IFileDeleteOptions): Promise; + + rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise; + // copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise; + + readFile(resource: URI): Promise; + writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise; +} + +export enum FileSystemErrorCode { + FileExists = 'EntryExists', + FileNotFound = 'EntryNotFound', + FileNotADirectory = 'EntryNotADirectory', + FileIsADirectory = 'EntryIsADirectory', + FileTooLarge = 'EntryTooLarge', + FileNotReadable = 'EntryNotReadable', + FileNotWritable = 'EntryNotWritable', + Unavailable = 'Unavailable', + Unknown = 'Unknown', +} + +export class FileSystemError extends Error { + static create(error: Error | string, code: FileSystemErrorCode): FileSystemError { + const providerError = new FileSystemError(error.toString(), code); + return providerError; + } + + private constructor( + message: string, + readonly code: FileSystemErrorCode, + ) { + super(message); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + } +} diff --git a/packages/engine-core/src/file/fileService.ts b/packages/engine-core/src/file/fileService.ts new file mode 100644 index 000000000..b4687d972 --- /dev/null +++ b/packages/engine-core/src/file/fileService.ts @@ -0,0 +1,62 @@ +import { createDecorator, Disposable, IDisposable, toDisposable } from '@alilc/lowcode-shared'; +import { type IFileSystemProvider } from './file'; +import { URI } from '../common'; + +export interface IFileService { + /** + * Registers a file system provider for a certain scheme. + */ + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable; + + /** + * Returns a file system provider for a certain scheme. + */ + getProvider(scheme: string): IFileSystemProvider | undefined; + + /** + * Checks if the file service has a registered provider for the + * provided resource. + * + * Note: this does NOT account for contributed providers from + * extensions that have not been activated yet. To include those, + * consider to call `await fileService.canHandleResource(resource)`. + */ + hasProvider(resource: URI): boolean; + + withProvider(resource: URI): IFileSystemProvider | undefined; + + /** + * Frees up any resources occupied by this service. + */ + dispose(): void; +} + +export const IFileService = createDecorator('fileService'); + +export class FileService extends Disposable implements IFileService { + private readonly provider = new Map(); + + constructor() { + super(); + } + + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { + this.provider.set(scheme, provider); + + return toDisposable(() => { + this.provider.delete(scheme); + }); + } + + getProvider(scheme: string): IFileSystemProvider | undefined { + return this.provider.get(scheme); + } + + hasProvider(resource: URI): boolean { + return this.provider.has(resource.scheme); + } + + withProvider(resource: URI): IFileSystemProvider | undefined { + return this.provider.get(resource.scheme); + } +} diff --git a/packages/engine-core/src/file/inMemoryFileSystemProvider.ts b/packages/engine-core/src/file/inMemoryFileSystemProvider.ts new file mode 100644 index 000000000..b06e25b33 --- /dev/null +++ b/packages/engine-core/src/file/inMemoryFileSystemProvider.ts @@ -0,0 +1,346 @@ +import { Events } from '@alilc/lowcode-shared'; +import { + FileType, + IFileSystemProvider, + type IStat, + type IFileChange, + type IFileDeleteOptions, + type IFileSystemWatcher, + type IWatchOptions, + type IFileWriteOptions, + type IFileOverwriteOptions, + FileSystemErrorCode, + FileSystemError, + IFileStat, + FilePermission, + FileChangeType, + FsContants, +} from './file'; +import { URI, basename, dirname } from '../common'; + +class File implements IStat { + readonly type: FileType.File; + readonly ctime: number; + mtime: number; + + name: string; + data: string; + permission = FilePermission.Writable; + + constructor(name: string) { + this.type = FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.name = name; + } +} + +class Directory implements IStat { + readonly type: FileType.Directory; + readonly ctime: number; + mtime: number; + + name: string; + permission = FilePermission.Writable; + readonly entries: Map; + + constructor(name: string) { + this.type = FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.name = name; + this.entries = new Map(); + } + + get size() { + return this.entries.size; + } +} + +type Entry = File | Directory; + +export class InMemoryFileSystemProvider implements IFileSystemProvider { + private readonly _onDidChangeFile = new Events.Emitter(); + onDidChangeFile = this._onDidChangeFile.event; + + private _bufferedChanges: IFileChange[] = []; + private _fireSoonHandle: number | undefined; + + private readonly _root = new Directory(''); + + async chmod(resource: URI, mode: number): Promise { + const entry = this._lookup(resource.path, false); + + if (FilePermission.Writable < mode) { + throw FileSystemError.create('Unsupported mode', FileSystemErrorCode.Unavailable); + } + + entry.permission = mode; + } + + async access(resource: URI, mode: number = FsContants.F_OK): Promise { + const entry = this._lookup(resource.path, false); + + if (mode === FsContants.F_OK) return; + if (mode === FsContants.R_OK && entry.permission >= FilePermission.Readable) { + return; + } + if (mode === FsContants.W_OK && entry.permission >= FilePermission.Writable) { + return; + } + } + + async stat(resource: URI): Promise { + const file = this._lookup(resource.path, false); + + return { + resource, + name: file.name, + ctime: file.ctime, + mtime: file.mtime, + permission: file.permission, + isDirectory: file.type === FileType.Directory, + isFile: file.type === FileType.File, + }; + } + + watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher { + return { resource, opts } as any as IFileSystemWatcher; + } + + async mkdir(resource: URI, opts: IFileWriteOptions): Promise { + const base = basename(resource.path); + const dir = dirname(resource.path); + const parent = this._lookupAsDirectory(dir, true); + + if (parent) { + if (!opts.overwrite && parent.entries.has(base)) { + throw FileSystemError.create('directory exists', FileSystemErrorCode.FileExists); + } + + const entry = new Directory(base); + entry.mtime = Date.now(); + parent.entries.set(entry.name, entry); + this._fireSoon( + { resource: resource.with({ path: dir }), type: FileChangeType.UPDATED }, + { resource, type: FileChangeType.ADDED }, + ); + } else { + if (!opts.recursive) { + throw FileSystemError.create('parent directory not found', FileSystemErrorCode.FileNotFound); + } + + this._mkdirRecursive(resource); + } + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const dir = this._lookupAsDirectory(resource.path, false); + + if (dir.permission < FilePermission.Readable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotReadable); + } + + return [...dir.entries.entries()].map(([name, entry]) => [name, entry.type]); + } + + async readFile(resource: URI): Promise { + const file = this._lookupAsFile(resource.path, false); + + if (file.permission < FilePermission.Readable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotReadable); + } + + return file.data; + } + + async writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise { + const base = basename(resource.path); + const dir = dirname(resource.path); + const dirUri = resource.with({ path: dir }); + let parent = this._lookupAsDirectory(dir, true); + + if (!parent) { + if (!opts.recursive) { + throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound); + } + parent = await this._mkdirRecursive(dirUri); + } + + let entry = parent.entries.get(base); + if (entry instanceof Directory) { + throw FileSystemError.create('file is directory', FileSystemErrorCode.FileIsADirectory); + } + if (entry && entry.permission < FilePermission.Writable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); + } + if (entry && !opts.overwrite) { + throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists); + } + + if (!entry) { + entry = new File(base); + parent.entries.set(base, entry); + this._fireSoon({ resource, type: FileChangeType.ADDED }); + } + + entry.mtime = Date.now(); + entry.data = content; + this._fireSoon({ resource, type: FileChangeType.UPDATED }); + } + + async delete(resource: URI, opts: IFileDeleteOptions): Promise { + const dir = dirname(resource.path); + const base = basename(resource.path); + const parent = this._lookupAsDirectory(dir, false); + + if (parent.permission < FilePermission.Writable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); + } + + if (parent.entries.has(base)) { + const entry = parent.entries.get(base)!; + + if (entry.permission < FilePermission.Writable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); + } + + if (entry instanceof Directory) { + if (opts.recursive) { + parent.entries.delete(base); + parent.mtime = Date.now(); + } else { + throw FileSystemError.create('file is directory', FileSystemErrorCode.FileIsADirectory); + } + } else { + parent.entries.delete(base); + parent.mtime = Date.now(); + } + + this._fireSoon( + { resource, type: FileChangeType.DELETED }, + { resource: resource.with({ path: dir }), type: FileChangeType.UPDATED }, + ); + } + } + + async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { + if (from.path === to.path) return; + + const entry = this._lookup(from.path, false); + + if (entry.permission < FilePermission.Writable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); + } + if (!opts.overwrite) { + throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists); + } + + const oldParent = this._lookupAsDirectory(dirname(from.path), false); + + const newParent = this._lookupAsDirectory(dirname(to.path), false); + const newName = basename(to.path); + + oldParent.entries.delete(entry.name); + entry.name = newName; + newParent.entries.set(newName, entry); + + this._fireSoon({ resource: to, type: FileChangeType.ADDED }, { resource: from, type: FileChangeType.DELETED }); + } + + private async _mkdirRecursive(target: URI): Promise { + const dir = dirname(target.path); + const dirUri = target.with({ path: dir }); + let parent = this._lookupAsDirectory(dir, false); + + if (!parent) { + parent = await this._mkdirRecursive(dirUri); + } + + if (parent.permission < FilePermission.Writable) { + throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); + } + + const directory = new Directory(basename(target.path)); + directory.mtime = Date.now(); + parent.entries.set(directory.name, directory); + this._fireSoon( + { + resource: dirUri, + type: FileChangeType.UPDATED, + }, + { + resource: target, + type: FileChangeType.ADDED, + }, + ); + + return directory; + } + + // --- lookup + + private _lookup(target: string, silent: false): Entry; + private _lookup(target: string, silent: boolean): Entry | undefined; + private _lookup(target: string, silent: boolean): Entry | undefined { + const parts = target.split('/'); + + let entry: Entry = this._root; + for (const part of parts) { + if (!part) { + continue; + } + let child: Entry | undefined; + if (entry.type === FileType.Directory) { + child = entry.entries.get(part); + } + if (!child) { + if (!silent) { + throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound); + } else { + return undefined; + } + } + entry = child; + } + return entry; + } + + private _lookupAsDirectory(target: string, silent: false): Directory; + private _lookupAsDirectory(target: string, silent: boolean): Directory | undefined; + private _lookupAsDirectory(target: string, silent: boolean): Directory | undefined { + const entry = this._lookup(target, silent); + + if (entry instanceof Directory) { + return entry; + } + if (!silent) { + throw FileSystemError.create('directory not found', FileSystemErrorCode.FileNotFound); + } + } + + private _lookupAsFile(target: string, silent: false): File; + private _lookupAsFile(target: string, silent: boolean): File | undefined; + private _lookupAsFile(target: string, silent: boolean): File | undefined { + const entry = this._lookup(target, silent); + if (entry instanceof File) { + return entry; + } + if (!silent) { + throw FileSystemError.create('file is a directory', FileSystemErrorCode.FileIsADirectory); + } + } + + private _fireSoon(...changes: IFileChange[]): void { + this._bufferedChanges.push(...changes); + + if (this._fireSoonHandle) { + clearTimeout(this._fireSoonHandle); + } + + this._fireSoonHandle = window.setTimeout(() => { + this._onDidChangeFile.notify(this._bufferedChanges); + this._bufferedChanges.length = 0; + }, 5); + } +} diff --git a/packages/engine-core/src/file/index.ts b/packages/engine-core/src/file/index.ts new file mode 100644 index 000000000..b1a8f44c0 --- /dev/null +++ b/packages/engine-core/src/file/index.ts @@ -0,0 +1,3 @@ +export * from './file'; +export * from './fileService'; +export * from './inMemoryFileSystemProvider'; diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 44a4836a4..3f925d305 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -2,3 +2,8 @@ export * from './configuration'; export * from './extension/extension'; export * from './resource'; export * from './command'; +export * from './workspace'; +export * from './common'; + +// test +export * from './main'; diff --git a/packages/engine-core/src/keybinding/keybindings.ts b/packages/engine-core/src/keybinding/keybindings.ts index 36fb98daf..6c521f87d 100644 --- a/packages/engine-core/src/keybinding/keybindings.ts +++ b/packages/engine-core/src/keybinding/keybindings.ts @@ -1,4 +1,5 @@ -import { illegalArgument, KeyCode, OperatingSystem, ScanCode } from '@alilc/lowcode-shared'; +import { illegalArgument, OperatingSystem } from '@alilc/lowcode-shared'; +import { KeyCode, ScanCode } from '../common/keyCodes'; /** * Binary encoding strategy: diff --git a/packages/engine-core/src/main.ts b/packages/engine-core/src/main.ts index a070eb846..da9bbd813 100644 --- a/packages/engine-core/src/main.ts +++ b/packages/engine-core/src/main.ts @@ -1,27 +1,70 @@ -import { InstantiationService } from '@alilc/lowcode-shared'; -import { IWorkbenchService } from './workbench'; +import { InstantiationService, BeanContainer, CtorDescriptor } from '@alilc/lowcode-shared'; import { ConfigurationService, IConfigurationService } from './configuration'; +import { IWorkspaceService, WorkspaceService, toWorkspaceIdentifier } from './workspace'; +import { IWindowService, WindowService } from './window'; +import { IFileService, FileService, InMemoryFileSystemProvider } from './file'; +import { URI } from './common/uri'; +import * as Schemas from './common/schemas'; class TestMainApplication { + instantiationService: InstantiationService; + constructor() { console.log('main application'); } async main() { - const workbench = instantiationService.get(IWorkbenchService); + await this._initServices(); - await configurationService.initialize(); - workbench.initialize(); + const [workspaceService, windowService, fileService] = this.instantiationService.invokeFunction((accessor) => [ + accessor.get(IWorkspaceService), + accessor.get(IWindowService), + accessor.get(IFileService), + ]); + + fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider()); + + try { + const uri = URI.from({ path: '/Desktop' }); + + await workspaceService.enterWorkspace(toWorkspaceIdentifier(uri.path)); + + const fileUri = URI.joinPath(uri, 'test.lc'); + + await windowService.open({ + urisToOpen: [{ fileUri }], + openOnlyIfExists: false, + }); + } catch (e) { + console.log('error', e); + } } - createServices() { - const instantiationService = new InstantiationService(); + private _createServices(): [IConfigurationService, IWorkspaceService] { + const container = new BeanContainer(); const configurationService = new ConfigurationService(); - instantiationService.container.set(IConfigurationService, configurationService); + container.set(IConfigurationService, configurationService); + + const workspaceService = new WorkspaceService(); + container.set(IWorkspaceService, workspaceService); + + container.set(IFileService, new FileService()); + + container.set(IWindowService, new CtorDescriptor(WindowService)); + + this.instantiationService = new InstantiationService(container); + + return [configurationService, workspaceService]; } - initServices() {} + private async _initServices() { + const [configurationService, workspaceService] = this._createServices(); + + await configurationService.initialize(); + // init workspace + await workspaceService.initialize(); + } } export async function createLowCodeEngineApp() { diff --git a/packages/engine-core/src/window/index.ts b/packages/engine-core/src/window/index.ts new file mode 100644 index 000000000..aa55170b6 --- /dev/null +++ b/packages/engine-core/src/window/index.ts @@ -0,0 +1,2 @@ +export * from './window'; +export * from './windowService'; diff --git a/packages/engine-core/src/window/window.ts b/packages/engine-core/src/window/window.ts new file mode 100644 index 000000000..0e82f827b --- /dev/null +++ b/packages/engine-core/src/window/window.ts @@ -0,0 +1,79 @@ +import { type IDisposable, type Events } from '@alilc/lowcode-shared'; +import { URI } from '../common'; +import { IWorkspaceIdentifier } from '../workspace'; + +export const enum WindowMode { + Maximized, + Normal, + Fullscreen, + Custom, +} + +export interface IWindowState { + width?: number; + height?: number; + x?: number; + y?: number; + mode?: WindowMode; + zoomLevel?: number; + readonly display?: number; +} + +export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState { + return { + width: 1024, + height: 768, + mode, + }; +}; + +export interface IEditOptions {} + +export interface IPath { + /** + * The file path to open within the instance + */ + readonly fileUri: URI; + + /** + * A hint that the file exists. if true, the + * file exists, if false it does not. with + * `undefined` the state is unknown. + */ + readonly exists?: boolean; + + /** + * Optional editor options to apply in the file + */ + options?: T; +} + +export interface IWindowConfiguration { + fileToOpenOrCreate: IPath; + + workspace?: IWorkspaceIdentifier; +} + +export interface IEditWindow extends IDisposable { + readonly onWillLoad: Events.Event; + readonly onDidSignalReady: Events.Event; + readonly onDidDestroy: Events.Event; + readonly onDidClose: Events.Event; + + readonly id: number; + + readonly config: IWindowConfiguration | undefined; + + readonly lastFocusTime: number; + focus(): void; + + readonly isReady: boolean; + ready(): Promise; + + load(config: IWindowConfiguration, options?: { isReload?: boolean }): void; + reload(): void; + + close(): void; + + sendWhenReady(channel: string, ...args: any[]): void; +} diff --git a/packages/engine-core/src/window/windowImpl.ts b/packages/engine-core/src/window/windowImpl.ts new file mode 100644 index 000000000..6171e2b25 --- /dev/null +++ b/packages/engine-core/src/window/windowImpl.ts @@ -0,0 +1,104 @@ +import { Disposable, Events } from '@alilc/lowcode-shared'; +import { IWindowState, IEditWindow, IWindowConfiguration } from './window'; +import { IFileService } from '../file'; + +export interface IWindowCreationOptions { + readonly state: IWindowState; +} + +export class EditWindow extends Disposable implements IEditWindow { + private readonly _onWillLoad = this._addDispose(new Events.Emitter()); + onWillLoad = this._onWillLoad.event; + + private readonly _onDidSignalReady = this._addDispose(new Events.Emitter()); + onDidSignalReady = this._onDidSignalReady.event; + + private readonly _onDidClose = this._addDispose(new Events.Emitter()); + onDidClose = this._onDidClose.event; + + private readonly _onDidDestroy = this._addDispose(new Events.Emitter()); + onDidDestroy = this._onDidDestroy.event; + + private _id: number; + get id(): number { + return this._id; + } + + private _windowState: IWindowState; + + private readonly _whenReadyCallbacks: ((window: IEditWindow) => void)[] = []; + + private _readyState: boolean; + get isReady(): boolean { + return this._readyState; + } + + private _lastFocusTime; + get lastFocusTime(): number { + return this._lastFocusTime; + } + + private _config: IWindowConfiguration | undefined; + get config(): IWindowConfiguration | undefined { + return this._config; + } + + constructor( + options: IWindowCreationOptions, + @IFileService private readonly fileService: IFileService, + ) { + super(); + + this._windowState = options.state; + + this._id = 0; + + this._lastFocusTime = Date.now(); + } + + ready(): Promise { + return new Promise((resolve) => { + if (this.isReady) { + return resolve(this); + } + + // otherwise keep and call later when we are ready + this._whenReadyCallbacks.push(resolve); + }); + } + + private setReady(): void { + this._readyState = true; + + // inform all waiting promises that we are ready now + while (this._whenReadyCallbacks.length) { + this._whenReadyCallbacks.pop()!(this); + } + } + + load(config: IWindowConfiguration): void { + this._onWillLoad.notify(); + + this._config = config; + } + + reload(): void {} + + focus(): void {} + + close(): void {} + + sendWhenReady(channel: string, ...args: any[]): void { + if (this.isReady) { + this.send(channel, ...args); + } else { + this.ready().then(() => { + this.send(channel, ...args); + }); + } + } + + private send(channel: string, ...args: any[]): void { + // todo + } +} diff --git a/packages/engine-core/src/window/windowManagementService.ts b/packages/engine-core/src/window/windowManagementService.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/engine-core/src/window/windowService.ts b/packages/engine-core/src/window/windowService.ts new file mode 100644 index 000000000..e44515440 --- /dev/null +++ b/packages/engine-core/src/window/windowService.ts @@ -0,0 +1,133 @@ +import { createDecorator, Disposable, Events, IInstantiationService } from '@alilc/lowcode-shared'; +import { defaultWindowState, IEditWindow, IWindowConfiguration } from './window'; +import { Schemas, URI } from '../common'; +import { EditWindow } from './windowImpl'; +import { IFileService } from '../file'; + +export interface IOpenConfiguration { + readonly urisToOpen: IWindowOpenable[]; + + readonly forceNewWindow?: boolean; + + /** + * Specifies if the file should be only be opened + * if it exists. + */ + readonly openOnlyIfExists?: boolean; + + addMode?: boolean; +} + +export interface IWindowOpenable { + label?: string; + readonly fileUri: URI; +} + +export interface IWindowService { + readonly onDidOpenWindow: Events.Event; + // readonly onDidChangeFullScreen: Events.Event<{ window: IEditWindow; fullscreen: boolean }>; + // readonly onDidDestroyWindow: Events.Event; + + open(openConfig: IOpenConfiguration): Promise; + + // sendToFocused(channel: string, ...args: any[]): void; + // sendToOpeningWindow(channel: string, ...args: any[]): void; + // sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; + + getWindows(): IEditWindow[]; + getWindowCount(): number; + + getLastActiveWindow(): IEditWindow | undefined; + + getWindowById(windowId: number): IEditWindow | undefined; +} + +export const IWindowService = createDecorator('windowService'); + +export class WindowService extends Disposable implements IWindowService { + private _onDidOpenWindow = this._addDispose(new Events.Emitter()); + onDidOpenWindow = this._onDidOpenWindow.event; + + private readonly windows = new Map(); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileService private readonly fileService: IFileService, + ) { + super(); + } + + getWindows(): IEditWindow[] { + return [...this.windows.values()]; + } + + getWindowCount(): number { + return this.windows.size; + } + + getWindowById(windowId: number): IEditWindow | undefined { + return this.windows.get(windowId); + } + + getLastActiveWindow(): IEditWindow | undefined { + let lastFocusedWindow: IEditWindow | undefined = undefined; + let maxLastFocusTime = Number.MIN_VALUE; + + const windows = this.getWindows(); + for (const window of windows) { + if (window.lastFocusTime > maxLastFocusTime) { + maxLastFocusTime = window.lastFocusTime; + lastFocusedWindow = window; + } + } + + return lastFocusedWindow; + } + + async open(openConfig: IOpenConfiguration): Promise { + return this._doOpen(openConfig); + } + + private async _doOpen(openConfig: IOpenConfiguration): Promise { + const usedWindows: IEditWindow[] = []; + const { urisToOpen, openOnlyIfExists } = openConfig; + + for (const item of urisToOpen) { + const fs = this.fileService.getProvider(Schemas.file)!; + + let exists = false; + try { + await fs.access(item.fileUri); + exists = true; + } catch { + if (openOnlyIfExists) continue; + } + + const config: IWindowConfiguration = { + fileToOpenOrCreate: { + fileUri: item.fileUri, + exists, + options: {}, + }, + }; + const window = await this._openInEditWindow(config); + + usedWindows.push(window); + } + + return usedWindows; + } + + private async _openInEditWindow(config: IWindowConfiguration): Promise { + const newWindow = this.instantiationService.createInstance(EditWindow, defaultWindowState()); + + this.windows.set(newWindow.id, newWindow); + + // Indicate new window via event + this._onDidOpenWindow.notify(newWindow); + + newWindow.load(config); + + return newWindow; + } +} diff --git a/packages/engine-core/src/workbench/index.ts b/packages/engine-core/src/workbench/index.ts index 3e3ca64a5..c4b9cc69a 100644 --- a/packages/engine-core/src/workbench/index.ts +++ b/packages/engine-core/src/workbench/index.ts @@ -1 +1,2 @@ export * from './workbenchService'; +export * from './workbench'; diff --git a/packages/engine-core/src/workbench/widget/widgetRegistry.ts b/packages/engine-core/src/workbench/widget/widgetRegistry.ts index fb3c4490e..cb66a002f 100644 --- a/packages/engine-core/src/workbench/widget/widgetRegistry.ts +++ b/packages/engine-core/src/workbench/widget/widgetRegistry.ts @@ -1,9 +1,9 @@ -import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared'; +import { Disposable, Events } from '@alilc/lowcode-shared'; import { IWidget } from './widget'; import { Extensions, Registry } from '../../extension/registry'; export interface IWidgetRegistry { - onDidRegister: Event[]>; + onDidRegister: Events.Event[]>; registerWidget(widget: IWidget): string; @@ -12,13 +12,15 @@ export interface IWidgetRegistry { getWidgets(): IWidget[]; } -export class WidgetRegistryImpl implements IWidgetRegistry { +export class WidgetRegistryImpl extends Disposable implements IWidgetRegistry { private _widgets: Map> = new Map(); - private emitter = new Emitter[]>(); + private _onDidRegister = this._addDispose(new Events.Emitter[]>()); - onDidRegister(fn: EventListener[]>) { - return this.emitter.on(fn); + onDidRegister = this._onDidRegister.event; + + constructor() { + super(); } getWidgets(): IWidget[] { diff --git a/packages/engine-core/src/workbench/workbench.ts b/packages/engine-core/src/workbench/workbench.ts new file mode 100644 index 000000000..650581437 --- /dev/null +++ b/packages/engine-core/src/workbench/workbench.ts @@ -0,0 +1,5 @@ +export const enum WorkbenchState { + EMPTY = 1, + FOLDER, + WORKSPACE /* preset */, +} diff --git a/packages/engine-core/src/workspace/file/file.ts b/packages/engine-core/src/workspace/file/file.ts deleted file mode 100644 index d4cdc4954..000000000 --- a/packages/engine-core/src/workspace/file/file.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { URI } from '../../common/uri'; - -export enum FileType { - /** - * File is unknown (neither file, directory). - */ - Unknown = 0, - - /** - * File is a normal file. - */ - File = 1, - - /** - * File is a directory. - */ - Directory = 2, -} - -export interface IStat { - /** - * The file type. - */ - readonly type: FileType; - - /** - * The last modification date represented as millis from unix epoch. - */ - readonly mtime: number; - - /** - * The creation date represented as millis from unix epoch. - */ - readonly ctime: number; -} - -export interface IBaseFileStat { - /** - * The unified resource identifier of this file or folder. - */ - readonly resource: URI; - - /** - * The name which is the last segment - * of the {{path}}. - */ - readonly name: string; - - /** - * The size of the file. - * - * The value may or may not be resolved as - * it is optional. - */ - readonly size?: number; - - /** - * The last modification date represented as millis from unix epoch. - * - * The value may or may not be resolved as - * it is optional. - */ - readonly mtime?: number; - - /** - * The creation date represented as millis from unix epoch. - * - * The value may or may not be resolved as - * it is optional. - */ - readonly ctime?: number; - - /** - * A unique identifier that represents the - * current state of the file or directory. - * - * The value may or may not be resolved as - * it is optional. - */ - readonly etag?: string; - - /** - * File is readonly. Components like editors should not - * offer to edit the contents. - */ - readonly readonly?: boolean; - - /** - * File is locked. Components like editors should offer - * to edit the contents and ask the user upon saving to - * remove the lock. - */ - readonly locked?: boolean; -} - -/** - * A file resource with meta information and resolved children if any. - */ -export interface IFileStat extends IBaseFileStat { - /** - * The resource is a file. - */ - readonly isFile: boolean; - - /** - * The resource is a directory. - */ - readonly isDirectory: boolean; - - /** - * The children of the file stat or undefined if none. - */ - children: IFileStat[] | undefined; -} - -export interface IFileStatWithMetadata extends Required { - readonly children: IFileStatWithMetadata[]; -} - -export const enum FileOperation { - CREATE, - DELETE, - MOVE, - COPY, - WRITE, -} - -export interface IFileOperationEvent { - readonly resource: URI; - readonly operation: FileOperation; - - isOperation(operation: FileOperation.DELETE | FileOperation.WRITE): boolean; - isOperation( - operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, - ): this is IFileOperationEventWithMetadata; -} - -export interface IFileOperationEventWithMetadata extends IFileOperationEvent { - readonly target: IFileStatWithMetadata; -} diff --git a/packages/engine-core/src/workspace/file/fileManagement.ts b/packages/engine-core/src/workspace/file/fileManagement.ts deleted file mode 100644 index 1e8d8079c..000000000 --- a/packages/engine-core/src/workspace/file/fileManagement.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * URI -> file content - * URI -> file Stat - */ - -export interface IFileManagement {} diff --git a/packages/engine-core/src/workspace/file/fileService.ts b/packages/engine-core/src/workspace/file/fileService.ts deleted file mode 100644 index a3204aa48..000000000 --- a/packages/engine-core/src/workspace/file/fileService.ts +++ /dev/null @@ -1 +0,0 @@ -export interface IFileService {} diff --git a/packages/engine-core/src/workspace/folder.ts b/packages/engine-core/src/workspace/folder.ts deleted file mode 100644 index 68c2a57d4..000000000 --- a/packages/engine-core/src/workspace/folder.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { URI } from '../common/uri'; - -export interface IWorkspaceFolderData { - /** - * The associated URI for this workspace folder. - */ - readonly uri: URI; - - /** - * The name of this workspace folder. Defaults to - * the basename of its [uri-path](#Uri.path) - */ - readonly name: string; - - /** - * The ordinal number of this workspace folder. - */ - readonly index: number; -} - -export interface IWorkspaceFolder extends IWorkspaceFolderData { - /** - * Given workspace folder relative path, returns the resource with the absolute path. - */ - toResource: (relativePath: string) => URI; -} diff --git a/packages/engine-core/src/workspace/index.ts b/packages/engine-core/src/workspace/index.ts new file mode 100644 index 000000000..5aeee7399 --- /dev/null +++ b/packages/engine-core/src/workspace/index.ts @@ -0,0 +1,3 @@ +export * from './workspaceService'; +export * from './workspace'; +export * from './workspaceFolder'; diff --git a/packages/engine-core/src/workspace/window/window.ts b/packages/engine-core/src/workspace/window/window.ts deleted file mode 100644 index cebdd8e28..000000000 --- a/packages/engine-core/src/workspace/window/window.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type Event } from '@alilc/lowcode-shared'; -import { URI } from '../../common/uri'; - -export interface IEditWindow { - // readonly onWillLoad: Event; - readonly onDidSignalReady: Event; - readonly onDidDestroy: Event; - - readonly onDidClose: Event; - - readonly id: number; - - readonly config: IWindowConfiguration | undefined; - - readonly isReady: boolean; - ready(): Promise; - - load(config: IWindowConfiguration, options?: { isReload?: boolean }): void; - reload(): void; -} - -export interface IWindowConfiguration { - filesToOpenOrCreate?: IPath[]; -} - -export interface IPath { - /** - * Optional editor options to apply in the file - */ - readonly options?: T; - - /** - * The file path to open within the instance - */ - fileUri?: URI; - - /** - * Specifies if the file should be only be opened - * if it exists. - */ - readonly openOnlyIfExists?: boolean; -} - -export const enum WindowMode { - Maximized, - Normal, - Fullscreen, - Custom, -} - -export interface IWindowState { - width?: number; - height?: number; - x?: number; - y?: number; - mode?: WindowMode; - zoomLevel?: number; - readonly display?: number; -} - -export interface IOpenConfiguration { - readonly urisToOpen?: IWindowOpenable[]; - readonly preferNewWindow?: boolean; - readonly forceNewWindow?: boolean; - readonly forceNewTabbedWindow?: boolean; - readonly forceReuseWindow?: boolean; - readonly forceEmpty?: boolean; -} - -export interface IBaseWindowOpenable { - label?: string; -} - -export interface IFolderToOpen extends IBaseWindowOpenable { - readonly folderUri: URI; -} - -export interface IFileToOpen extends IBaseWindowOpenable { - readonly fileUri: URI; -} - -export type IWindowOpenable = IFolderToOpen | IFileToOpen; diff --git a/packages/engine-core/src/workspace/window/windowService.ts b/packages/engine-core/src/workspace/window/windowService.ts deleted file mode 100644 index 38b95ac28..000000000 --- a/packages/engine-core/src/workspace/window/windowService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type Event } from '@alilc/lowcode-shared'; -import { IEditWindow, IOpenConfiguration } from './window'; - -export interface IWindowService { - readonly onDidOpenWindow: Event; - readonly onDidSignalReadyWindow: Event; - readonly onDidChangeFullScreen: Event<{ window: IEditWindow; fullscreen: boolean }>; - readonly onDidDestroyWindow: Event; - - open(openConfig: IOpenConfiguration): Promise; - - sendToFocused(channel: string, ...args: any[]): void; - sendToOpeningWindow(channel: string, ...args: any[]): void; - sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; - - getWindows(): IEditWindow[]; - getWindowCount(): number; - - getFocusedWindow(): IEditWindow | undefined; - getLastActiveWindow(): IEditWindow | undefined; - - getWindowById(windowId: number): IEditWindow | undefined; -} - -export class WindowService implements IWindowService { - private readonly windows = new Map(); - - getWindows(): IEditWindow[] { - return [...this.windows.values()]; - } - - getWindowCount(): number { - return this.windows.size; - } - - getFocusedWindow(): IEditWindow | undefined { - return this.getWindows().find((w) => w.focused); - } - - getLastActiveWindow(): IEditWindow | undefined { - return this.getWindows().find((w) => w.lastActive); - } -} diff --git a/packages/engine-core/src/workspace/workspace.ts b/packages/engine-core/src/workspace/workspace.ts index d365ea2b7..2fe4a2759 100644 --- a/packages/engine-core/src/workspace/workspace.ts +++ b/packages/engine-core/src/workspace/workspace.ts @@ -1,31 +1,92 @@ -import { IWorkspaceFolder } from './folder'; - -/** - * workspace -> one or more folders -> virtual files - * file -> editWindow - * editorView -> component tree schema - * - * project = (one or muti folders -> files) + some configs - */ +import { URI, basename, TernarySearchTree } from '../common'; +import { IWorkspaceFolder, WorkspaceFolder } from './workspaceFolder'; + +export interface IWorkspaceIdentifier { + /** + * Every workspace (multi-root, single folder or empty) + * has a unique identifier. It is not possible to open + * a workspace with the same `id` in multiple windows + */ + readonly id: string; + /** + * Folder path as `URI`. + */ + readonly uri: URI; +} + +export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { + const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined; + + return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.uri); +} + +export function toWorkspaceIdentifier(pathOrWorkspace: IWorkspace | string): IWorkspaceIdentifier { + if (typeof pathOrWorkspace === 'string') { + return { + id: basename(pathOrWorkspace), + uri: URI.from({ path: pathOrWorkspace }), + }; + } + + const workspace = pathOrWorkspace; + + return { + id: workspace.id, + uri: workspace.uri, + }; +} + export interface IWorkspace { readonly id: string; + readonly uri: URI; /** * Folders in the workspace. */ - readonly folders: IWorkspaceFolder[]; + folders: IWorkspaceFolder[]; } export class Workspace implements IWorkspace { - private _folders: IWorkspaceFolder[] = []; + private _folders: WorkspaceFolder[]; + private _foldersMap: TernarySearchTree; - constructor(private _id: string) {} + constructor( + private _id: string, + private _uri: URI, + folders: WorkspaceFolder[], + private _ignorePathCasing = false, + ) { + this.folders = folders; + } get id() { return this._id; } + get uri() { + return this._uri; + } + get folders() { return this._folders; } + + set folders(folders: WorkspaceFolder[]) { + this._folders = folders; + this._updateFoldersMap(); + } + + getFolder(resource: URI): IWorkspaceFolder | null { + if (!resource) { + return null; + } + return this._foldersMap.findSubstr(resource.path) || null; + } + + private _updateFoldersMap(): void { + this._foldersMap = TernarySearchTree.forPaths(this._ignorePathCasing); + for (const folder of this.folders) { + this._foldersMap.set(folder.uri.path, folder); + } + } } diff --git a/packages/engine-core/src/workspace/workspaceFolder.ts b/packages/engine-core/src/workspace/workspaceFolder.ts new file mode 100644 index 000000000..9c544f728 --- /dev/null +++ b/packages/engine-core/src/workspace/workspaceFolder.ts @@ -0,0 +1,62 @@ +import { URI, basename } from '../common'; + +export interface IWorkspaceFolderData { + /** + * The associated URI for this workspace folder. + */ + readonly uri: URI; + + /** + * The name of this workspace folder. Defaults to + * the basename of its [uri-path](#Uri.path) + */ + readonly name: string; + + /** + * The ordinal number of this workspace folder. + */ + readonly index: number; +} + +export interface IWorkspaceFolder extends IWorkspaceFolderData { + /** + * Given workspace folder relative path, returns the resource with the absolute path. + */ + toResource: (relativePath: string) => URI; +} + +export class WorkspaceFolder implements IWorkspaceFolder { + readonly uri: URI; + readonly name: string; + readonly index: number; + + constructor(data: IWorkspaceFolderData) { + this.uri = data.uri; + this.name = data.name; + this.index = data.index; + } + + toResource(relativePath: string) { + return URI.joinPath(this.uri, relativePath); + } + + toJSON(): IWorkspaceFolderData { + return { uri: this.uri, name: this.name, index: this.index }; + } +} + +export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { + const candidate = thing as IWorkspaceFolder; + + return !!( + candidate && + typeof candidate === 'object' && + URI.isUri(candidate.uri) && + typeof candidate.name === 'string' && + typeof candidate.toResource === 'function' + ); +} + +export function toWorkspaceFolder(resource: URI): WorkspaceFolder { + return new WorkspaceFolder({ uri: resource, index: 0, name: basename(resource.path) }); +} diff --git a/packages/engine-core/src/workspace/workspaceService.ts b/packages/engine-core/src/workspace/workspaceService.ts index 13ea2cca9..8e9ceea80 100644 --- a/packages/engine-core/src/workspace/workspaceService.ts +++ b/packages/engine-core/src/workspace/workspaceService.ts @@ -1,7 +1,42 @@ -import { createDecorator } from '@alilc/lowcode-shared'; +import { createDecorator, Disposable } from '@alilc/lowcode-shared'; +import { Workspace, type IWorkspaceIdentifier, isWorkspaceIdentifier } from './workspace'; +import { toWorkspaceFolder, IWorkspaceFolder } from './workspaceFolder'; +import { URI } from '../common'; -export interface IWorkspaceService {} +export interface IWorkspaceService { + initialize(): Promise; + + enterWorkspace(identifier: IWorkspaceIdentifier): Promise; + + getWorkspace(): Workspace; + + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null; +} export const IWorkspaceService = createDecorator('workspaceService'); -export class WorkspaceService implements IWorkspaceService {} +export class WorkspaceService extends Disposable implements IWorkspaceService { + private _workspace: Workspace; + + constructor() { + super(); + } + + async initialize() {} + + async enterWorkspace(identifier: IWorkspaceIdentifier) { + if (!isWorkspaceIdentifier(identifier)) { + throw new Error('Invalid workspace identifier'); + } + + this._workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]); + } + + getWorkspace(): Workspace { + return this._workspace; + } + + getWorkspaceFolder(resource: URI) { + return this._workspace.getFolder(resource); + } +} diff --git a/packages/engine-core/tsconfig.json b/packages/engine-core/tsconfig.json index 039e0b4d1..9252ca84e 100644 --- a/packages/engine-core/tsconfig.json +++ b/packages/engine-core/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["src"] + "include": ["src", "src/common/uri.ts"] } diff --git a/packages/react-renderer/src/api/component.tsx b/packages/react-renderer/src/api/component.tsx index f6607bf75..7b1c9d979 100644 --- a/packages/react-renderer/src/api/component.tsx +++ b/packages/react-renderer/src/api/component.tsx @@ -1,12 +1,12 @@ import { type StringDictionary, type ComponentTree } from '@alilc/lowcode-shared'; import { CodeRuntime } from '@alilc/lowcode-renderer-core'; import { FunctionComponent, ComponentType } from 'react'; +import { reactiveStateFactory } from '../app/reactiveState'; import { type LowCodeComponentProps, createComponent as createSchemaComponent, type ComponentOptions as SchemaComponentOptions, - reactiveStateFactory, -} from '../runtime'; +} from '../runtime/createComponent'; import { type ComponentsAccessor } from '../app'; export interface ComponentOptions extends SchemaComponentOptions { diff --git a/packages/react-renderer/src/app/app.ts b/packages/react-renderer/src/app/app.ts index 8783df8a9..2a7692951 100644 --- a/packages/react-renderer/src/app/app.ts +++ b/packages/react-renderer/src/app/app.ts @@ -27,7 +27,7 @@ import { type ReactRendererBoostsApi, } from './boosts'; import { createAppView } from './components/view'; -import { ComponentOptions } from '../runtime'; +import { type ComponentOptions } from '../runtime/createComponent'; import type { Project, Package } from '@alilc/lowcode-shared'; import type { @@ -185,8 +185,6 @@ export class App extends Disposable implements IRendererApplication { await extensionHostService.registerPlugin(this.options.plugins ?? []); await packageManagementService.loadPackages(this.options.packages ?? []); - - lifeCycleService.setPhase(LifecyclePhase.Ready); } private async _createRouter() { diff --git a/packages/react-renderer/src/app/components/route.tsx b/packages/react-renderer/src/app/components/route.tsx index a4fed8b7c..494518cb3 100644 --- a/packages/react-renderer/src/app/components/route.tsx +++ b/packages/react-renderer/src/app/components/route.tsx @@ -4,7 +4,7 @@ import { useAppContext } from '../context'; import { OutletProps } from '../boosts'; import { useRouteLocation } from '../context'; import { createComponent } from '../../runtime/createComponent'; -import { reactiveStateFactory } from '../../runtime/reactiveState'; +import { reactiveStateFactory } from '../reactiveState'; export function RouteOutlet(props: OutletProps) { const app = useAppContext(); diff --git a/packages/react-renderer/src/app/components/view.tsx b/packages/react-renderer/src/app/components/view.tsx index ebfbef752..09b62b686 100644 --- a/packages/react-renderer/src/app/components/view.tsx +++ b/packages/react-renderer/src/app/components/view.tsx @@ -1,6 +1,7 @@ import { AppContext } from '../context'; import { type App } from '../app'; -import { getOrCreateComponent, reactiveStateFactory } from '../../runtime'; +import { reactiveStateFactory } from '../reactiveState'; +import { getOrCreateComponent } from '../../runtime/createComponent'; import { RouterView } from './routerView'; import { RouteOutlet } from './route'; import { type WrapperComponent, type Outlet } from '../boosts'; diff --git a/packages/react-renderer/src/runtime/reactiveState.ts b/packages/react-renderer/src/app/reactiveState.ts similarity index 100% rename from packages/react-renderer/src/runtime/reactiveState.ts rename to packages/react-renderer/src/app/reactiveState.ts diff --git a/packages/react-renderer/src/runtime/elements.tsx b/packages/react-renderer/src/runtime/elements.tsx index d8b56b344..2e8d00b27 100644 --- a/packages/react-renderer/src/runtime/elements.tsx +++ b/packages/react-renderer/src/runtime/elements.tsx @@ -20,7 +20,7 @@ import { type ReactNode, } from 'react'; import { ComponentsAccessor } from '../app'; -import { useReactiveStore } from './hooks/useReactiveStore'; +import { useReactiveStore } from './useReactiveStore'; import { getOrCreateComponent, type ComponentOptions } from './createComponent'; export type ReactComponent = ComponentType; diff --git a/packages/react-renderer/src/runtime/index.ts b/packages/react-renderer/src/runtime/index.ts deleted file mode 100644 index f4cfd142d..000000000 --- a/packages/react-renderer/src/runtime/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './createComponent'; -export * from './reactiveState'; -export * from './elements'; diff --git a/packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx b/packages/react-renderer/src/runtime/useReactiveStore.tsx similarity index 100% rename from packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx rename to packages/react-renderer/src/runtime/useReactiveStore.tsx diff --git a/packages/react-simulator-renderer/package.json b/packages/react-simulator-renderer/package.json index b31b4f6e3..d71aad8d8 100644 --- a/packages/react-simulator-renderer/package.json +++ b/packages/react-simulator-renderer/package.json @@ -15,11 +15,9 @@ "scripts": { "build:target": "vite build", "build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.mjs", - "test": "vitest", - "test:cov": "build-scripts test --config build.test.json --jest-coverage" + "test": "vitest" }, "dependencies": { - "@alilc/lowcode-designer": "workspace:*", "@alilc/lowcode-react-renderer": "workspace:*", "classnames": "^2.5.1", "react": "^18.2.0", diff --git a/packages/react-simulator-renderer/test/schema/basic.ts b/packages/react-simulator-renderer/test/schema/basic.ts deleted file mode 100644 index 5dffd7267..000000000 --- a/packages/react-simulator-renderer/test/schema/basic.ts +++ /dev/null @@ -1,81 +0,0 @@ -export default { - id: 'node_ockvuu8u911', - css: 'body{background-color:#f2f3f5}', - flows: [], - props: { - className: 'page_kvuu9hym', - pageStyle: { - backgroundColor: '#f2f3f5', - }, - containerStyle: {}, - templateVersion: '1.0.0', - }, - state: {}, - title: '', - methods: { - __initMethods__: { - type: 'JSExpression', - value: "function (exports, module) { \"use strict\";\n\nexports.__esModule = true;\nexports.func1 = func1;\nexports.helloPage = helloPage;\n\nfunction func1() {\n console.info('hello, this is a page function');\n}\n\nfunction helloPage() {\n // 你可以这么调用其他函数\n this.func1(); // 你可以这么调用组件的函数\n // this.$('textField_xxx').getValue();\n // 你可以这么使用「数据源面板」定义的「变量」\n // this.state.xxx\n // 你可以这么发送一个在「数据源面板」定义的「远程 API」\n // this.dataSourceMap['xxx'].load(data)\n // API 详见:https://go.alibaba-inc.com/help3/API\n} \n}", - }, - }, - children: [ - { - id: 'node_ockvuu8u915', - props: { - fieldId: 'div_kvuu9gl1', - behavior: 'NORMAL', - __style__: {}, - customClassName: '', - useFieldIdAsDomId: false, - }, - title: '', - children: [ - { - id: 'node_ockvuu8u916', - props: { - content: { - use: 'zh-CN', - type: 'JSExpression', - 'en-US': 'Tips content', - value: '"我是一个简单的测试页面"', - 'zh-CN': '我是一个简单的测试页面', - extType: 'i18n', - }, - fieldId: 'text_kvuu9gl2', - maxLine: 0, - behavior: 'NORMAL', - __style__: {}, - showTitle: false, - }, - title: '', - condition: true, - componentName: 'Text', - }, - ], - condition: true, - componentName: 'Div', - }, - ], - condition: true, - dataSource: { - list: [], - sync: true, - online: [], - offline: [], - globalConfig: { - fit: { - type: 'JSExpression', - value: "function main(){\n 'use strict';\n\nvar __compiledFunc__ = function fit(response) {\n var content = response.content !== undefined ? response.content : response;\n var error = {\n message: response.errorMsg || response.errors && response.errors[0] && response.errors[0].msg || response.content || '远程数据源请求出错,success is false'\n };\n var success = true;\n if (response.success !== undefined) {\n success = response.success;\n } else if (response.hasError !== undefined) {\n success = !response.hasError;\n }\n return {\n content: content,\n success: success,\n error: error\n };\n};\n return __compiledFunc__.apply(this, arguments);\n}", - extType: 'function', - }, - }, - }, - lifeCycles: { - constructor: { - type: 'JSExpression', - value: "function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}", - extType: 'function', - }, - }, - componentName: 'Page', -}; diff --git a/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap b/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap deleted file mode 100644 index 2f2d19f26..000000000 --- a/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Base should be render NotFoundComponent 1`] = ` -
-
-
- Text Component Not Found -
-
-
-`; - -exports[`Base should be render Text 1`] = ` -
-
-
- 我是一个简单的测试页面 -
-
-
-`; diff --git a/packages/react-simulator-renderer/test/src/renderer/demo.test.tsx b/packages/react-simulator-renderer/test/src/renderer/demo.test.tsx deleted file mode 100644 index b849678a3..000000000 --- a/packages/react-simulator-renderer/test/src/renderer/demo.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import renderer from 'react-test-renderer'; -import rendererContainer from '../../../src/renderer'; -import SimulatorRendererView from '../../../src/renderer-view'; -import { Text } from '../../utils/components'; - -describe('Base', () => { - const component = renderer.create( - - ); - - it('should be render NotFoundComponent', () => { - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should be render Text', () => { - // 更新 _componentsMap 值 - (rendererContainer as any)._componentsMap.Text = Text;// = host.designer.componentsMap; - // 更新 components 列表 - (rendererContainer as any).buildComponents(); - - expect(!!(rendererContainer.components as any).Text).toBeTruthy(); - - rendererContainer.rerender(); - - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); -}) \ No newline at end of file diff --git a/packages/react-simulator-renderer/test/utils/components.tsx b/packages/react-simulator-renderer/test/utils/components.tsx deleted file mode 100644 index 0de3d3cd0..000000000 --- a/packages/react-simulator-renderer/test/utils/components.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Text = ({ - __tag, - content, - ...props -}: any) => (
{content}
); - -export const Page = (props: any) => (
{props.children}
); \ No newline at end of file diff --git a/packages/react-simulator-renderer/test/utils/host.ts b/packages/react-simulator-renderer/test/utils/host.ts deleted file mode 100644 index f7ab34357..000000000 --- a/packages/react-simulator-renderer/test/utils/host.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Box, Breadcrumb, Form, Select, Input, Button, Table, Pagination, Dialog } from '@alifd/next'; -import defaultSchema from '../schema/basic'; -import { Page } from './components'; - -class Designer { - componentsMap = { - Box, - Breadcrumb, - 'Breadcrumb.Item': Breadcrumb.Item, - Form, - 'Form.Item': Form.Item, - Select, - Input, - Button, - 'Button.Group': Button.Group, - Table, - Pagination, - Dialog, - Page, - } -} - -class Host { - designer = new Designer(); - - connect = () => {} - - autorun = (fn: Function) => { - fn(); - } - - autoRender = true; - - componentsConsumer = { - consume() {} - } - - schema = defaultSchema; - - project = { - documents: [ - { - id: '1', - path: '/', - fileName: '', - export: () => { - return this.schema; - }, - getNode: () => {}, - } - ], - get: () => ({}), - } - - setInstance() {} - - designMode = 'design' - - get() {} - - injectionConsumer = { - consume() {} - } - - i18nConsumer = { - consume() {} - } - - /** 下列的函数或者方法是方便测试用 */ - mockSchema = (schema: any) => { - this.schema = schema; - }; -} - -if (!(window as any).LCSimulatorHost) { - (window as any).LCSimulatorHost = new Host(); -} - -export default (window as any).LCSimulatorHost; \ No newline at end of file diff --git a/packages/react-simulator-renderer/tsconfig.json b/packages/react-simulator-renderer/tsconfig.json index 9519ab847..b4e69ae1f 100644 --- a/packages/react-simulator-renderer/tsconfig.json +++ b/packages/react-simulator-renderer/tsconfig.json @@ -1,9 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "lib" - }, - "include": [ - "./src/", "../../index.ts" - ] + "outDir": "dist" + } } diff --git a/packages/renderer-core/src/code-runtime/codeRuntime.ts b/packages/renderer-core/src/code-runtime/codeRuntime.ts index 805b26506..62b2ed34f 100644 --- a/packages/renderer-core/src/code-runtime/codeRuntime.ts +++ b/packages/renderer-core/src/code-runtime/codeRuntime.ts @@ -11,14 +11,18 @@ import { } from '@alilc/lowcode-shared'; import { type ICodeScope, CodeScope } from './codeScope'; import { mapValue } from './value'; -import { evaluate } from './evaluate'; +import { defaultSandbox } from './sandbox'; + +export interface ISandbox { + eval(code: string, scope: any): any; +} export interface CodeRuntimeOptions { initScopeValue?: Partial; parentScope?: ICodeScope; - evalCodeFunction?: EvalCodeFunction; + sandbox?: ISandbox; } export interface ICodeRuntime extends IDisposable { @@ -37,22 +41,20 @@ export interface ICodeRuntime ext export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined; -export type EvalCodeFunction = (code: string, scope: any) => any; - export class CodeRuntime extends Disposable implements ICodeRuntime { private _codeScope: ICodeScope; - private _evalCodeFunction: EvalCodeFunction = evaluate; + private _sandbox: ISandbox = defaultSandbox; private _resolveHandlers: NodeResolverHandler[] = []; constructor(options: CodeRuntimeOptions = {}) { super(); - if (options.evalCodeFunction) this._evalCodeFunction = options.evalCodeFunction; + if (options.sandbox) this._sandbox = options.sandbox; this._codeScope = this._addDispose( options.parentScope ? options.parentScope.createChild(options.initScopeValue ?? {}) diff --git a/packages/renderer-core/src/code-runtime/evaluate.ts b/packages/renderer-core/src/code-runtime/evaluate.ts deleted file mode 100644 index 465b6a2b3..000000000 --- a/packages/renderer-core/src/code-runtime/evaluate.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type EvalCodeFunction } from './codeRuntime'; - -export const evaluate: EvalCodeFunction = (code: string, scope: any) => { - return new Function('scope', `"use strict";return (function(){return (${code})}).bind(scope)();`)( - scope, - ); -}; diff --git a/packages/renderer-core/src/code-runtime/sandbox.ts b/packages/renderer-core/src/code-runtime/sandbox.ts new file mode 100644 index 000000000..0e798cf2c --- /dev/null +++ b/packages/renderer-core/src/code-runtime/sandbox.ts @@ -0,0 +1,10 @@ +import { type ISandbox } from './codeRuntime'; + +export const defaultSandbox: ISandbox = { + eval(code, scope) { + return new Function( + 'scope', + `"use strict";return (function(){return (${code})}).bind(scope)();`, + )(scope); + }, +}; diff --git a/packages/renderer-core/src/component-tree-model/componentTreeModel.ts b/packages/renderer-core/src/component-tree-model/componentTreeModel.ts index 951fdd662..404766592 100644 --- a/packages/renderer-core/src/component-tree-model/componentTreeModel.ts +++ b/packages/renderer-core/src/component-tree-model/componentTreeModel.ts @@ -15,7 +15,7 @@ import { Disposable, } from '@alilc/lowcode-shared'; import { type ICodeRuntime } from '../code-runtime'; -import { IWidget, Widget } from '../widget'; +import { IWidget, Widget } from './widget'; export interface NormalizedComponentNode extends ComponentNode { loopArgs: [string, string]; diff --git a/packages/shared/src/common/errors.ts b/packages/shared/src/common/errors.ts deleted file mode 100644 index dc3256423..000000000 --- a/packages/shared/src/common/errors.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function illegalArgument(name?: string): Error { - if (name) { - return new Error(`Illegal argument: ${name}`); - } else { - return new Error('Illegal argument'); - } -} diff --git a/packages/shared/src/common/index.ts b/packages/shared/src/common/index.ts index b888c4471..a9f35912f 100644 --- a/packages/shared/src/common/index.ts +++ b/packages/shared/src/common/index.ts @@ -7,9 +7,6 @@ export * from './platform'; export * from './logger'; export * from './intl'; export * from './instantiation'; - -export * from './keyCodes'; -export * from './errors'; export * from './disposable'; export * from './linkedList'; diff --git a/packages/shared/src/common/instantiation/container.ts b/packages/shared/src/common/instantiation/container.ts index 544def430..ec7201f51 100644 --- a/packages/shared/src/common/instantiation/container.ts +++ b/packages/shared/src/common/instantiation/container.ts @@ -52,8 +52,6 @@ export function mapDepsToBeanId(beanId: BeanIdentifier, target: Constructor } } -export function getBeanDependecies( - target: Constructor, -): { id: BeanIdentifier; index: number }[] { +export function getBeanDependecies(target: Constructor): { beanId: BeanIdentifier; index: number }[] { return (target as any)[DEPENDENCIES] || []; } diff --git a/packages/shared/src/common/instantiation/instantiationService.ts b/packages/shared/src/common/instantiation/instantiationService.ts index 99692eeca..fe5d64a12 100644 --- a/packages/shared/src/common/instantiation/instantiationService.ts +++ b/packages/shared/src/common/instantiation/instantiationService.ts @@ -1,12 +1,6 @@ import { dispose, isDisposable } from '../disposable'; import { Graph, CyclicDependencyError } from '../graph'; -import { - type BeanIdentifier, - BeanContainer, - type Constructor, - getBeanDependecies, - CtorDescriptor, -} from './container'; +import { type BeanIdentifier, BeanContainer, type Constructor, getBeanDependecies, CtorDescriptor } from './container'; import { createDecorator } from './decorators'; export interface InstanceAccessor { @@ -16,10 +10,7 @@ export interface InstanceAccessor { export interface IInstantiationService { createInstance(Ctor: T, ...args: any[]): InstanceType; - invokeFunction( - fn: (accessor: InstanceAccessor, ...args: Args) => R, - ...args: Args - ): R; + invokeFunction(fn: (accessor: InstanceAccessor, ...args: Args) => R, ...args: Args): R; createChild(container: BeanContainer): IInstantiationService; @@ -77,10 +68,7 @@ export class InstantiationService implements IInstantiationService { /** * Calls a function with a service accessor. */ - invokeFunction( - fn: (accessor: InstanceAccessor, ...args: TS) => R, - ...args: TS - ): R { + invokeFunction(fn: (accessor: InstanceAccessor, ...args: TS) => R, ...args: TS): R { this._throwIfDisposed(); const accessor: InstanceAccessor = { @@ -105,9 +93,9 @@ export class InstantiationService implements IInstantiationService { const beanArgs = []; for (const dependency of beanDependencies) { - const instance = this._getOrCreateInstance(dependency.id); + const instance = this._getOrCreateInstance(dependency.beanId); if (!instance) { - throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.id}.`); + throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.beanId}.`); } beanArgs.push(instance); @@ -149,9 +137,7 @@ export class InstantiationService implements IInstantiationService { } private _createAndCacheServiceInstance(id: BeanIdentifier, desc: CtorDescriptor): T { - const graph = new Graph<{ id: BeanIdentifier; desc: CtorDescriptor }>((data) => - data.id.toString(), - ); + const graph = new Graph<{ id: BeanIdentifier; desc: CtorDescriptor }>((data) => data.id.toString()); let cycleCount = 0; const stack = [{ id, desc }]; @@ -174,16 +160,14 @@ export class InstantiationService implements IInstantiationService { // check all dependencies for existence and if they need to be created first for (const dependency of getBeanDependecies(item.desc.ctor)) { - const instanceOrDesc = this._container.get(dependency.id); + const instanceOrDesc = this._container.get(dependency.beanId); if (!instanceOrDesc) { - throw new Error( - `[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, - ); + throw new Error(`[createInstance] ${id} depends on ${dependency.beanId} which is NOT registered.`); } if (instanceOrDesc instanceof CtorDescriptor) { const d = { - id: dependency.id, + id: dependency.beanId, desc: instanceOrDesc, }; graph.insertEdge(item, d); @@ -210,11 +194,7 @@ export class InstantiationService implements IInstantiationService { const instanceOrDesc = this._container.get(data.id); if (instanceOrDesc instanceof CtorDescriptor) { // create instance and overwrite the service collections - const instance = this._createServiceInstanceWithOwner( - data.id, - data.desc.ctor, - data.desc.staticArguments, - ); + const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments); this._setCreatedServiceInstance(data.id, instance); } graph.removeNode(data); diff --git a/packages/shared/src/utils/invariant.ts b/packages/shared/src/utils/invariant.ts index 52a5bb911..fc7a03680 100644 --- a/packages/shared/src/utils/invariant.ts +++ b/packages/shared/src/utils/invariant.ts @@ -3,3 +3,11 @@ export function invariant(check: unknown, message: string, thing?: any): asserts throw new Error(`Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`); } } + +export function illegalArgument(name?: string): Error { + if (name) { + return new Error(`Illegal argument: ${name}`); + } else { + return new Error('Illegal argument'); + } +} diff --git a/scripts/build.js b/scripts/build.js index dc3e637ec..c2296af02 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -24,7 +24,7 @@ async function run() { }); if (buildTypes) { - await execa('pnpm', ['--filter', finalName[0], 'build:dts'], { + await execa('pnpm', ['--filter', manifest.name, 'build:dts'], { stdio: 'inherit', }); }