From 9e08c2a224eb4166fec7db0e776123f16bde6ee0 Mon Sep 17 00:00:00 2001
From: 1ncounter <1ncounter.100@gmail.com>
Date: Thu, 8 Aug 2024 17:39:16 +0800
Subject: [PATCH] feat: init docs by vitepress
---
.gitignore | 3 +-
.prettierrc | 2 +-
TODOS.md | 4 +
docs/api-examples.md | 49 +++
docs/api/index.md | 0
docs/examples/hello-world.md | 8 +
docs/guide/introduction.md | 0
docs/guide/quick-start.md | 0
docs/index.md | 22 ++
docs/markdown-examples.md | 85 +++++
docs/package.json | 23 ++
docs/public/logo.svg | 1 +
.../src/command/commandRegistry.ts | 35 +-
packages/engine-core/src/common/charCode.ts | 228 ++++++++++++
packages/engine-core/src/common/glob.ts | 23 ++
packages/engine-core/src/common/index.ts | 11 +
.../src/common/keyCodes.ts | 57 +--
packages/engine-core/src/common/path.ts | 279 ++++++++++++++
packages/engine-core/src/common/schemas.ts | 7 +
packages/engine-core/src/common/strings.ts | 88 +++++
.../src/common/ternarySearchTree.ts | 297 +++++++++++++++
packages/engine-core/src/common/uri.ts | 128 ++++++-
.../configuration/configurationRegistry.ts | 40 +-
.../src/configuration/configurationService.ts | 19 +-
.../src/configuration/configurations.ts | 26 +-
packages/engine-core/src/file/file.ts | 243 ++++++++++++
packages/engine-core/src/file/fileService.ts | 62 ++++
.../src/file/inMemoryFileSystemProvider.ts | 346 ++++++++++++++++++
packages/engine-core/src/file/index.ts | 3 +
packages/engine-core/src/index.ts | 5 +
.../engine-core/src/keybinding/keybindings.ts | 3 +-
packages/engine-core/src/main.ts | 61 ++-
packages/engine-core/src/window/index.ts | 2 +
packages/engine-core/src/window/window.ts | 79 ++++
packages/engine-core/src/window/windowImpl.ts | 104 ++++++
.../src/window/windowManagementService.ts | 0
.../engine-core/src/window/windowService.ts | 133 +++++++
packages/engine-core/src/workbench/index.ts | 1 +
.../src/workbench/widget/widgetRegistry.ts | 14 +-
.../engine-core/src/workbench/workbench.ts | 5 +
.../engine-core/src/workspace/file/file.ts | 140 -------
.../src/workspace/file/fileManagement.ts | 6 -
.../src/workspace/file/fileService.ts | 1 -
packages/engine-core/src/workspace/folder.ts | 26 --
packages/engine-core/src/workspace/index.ts | 3 +
.../src/workspace/window/window.ts | 82 -----
.../src/workspace/window/windowService.ts | 43 ---
.../engine-core/src/workspace/workspace.ts | 85 ++++-
.../src/workspace/workspaceFolder.ts | 62 ++++
.../src/workspace/workspaceService.ts | 41 ++-
packages/engine-core/tsconfig.json | 2 +-
packages/react-renderer/src/api/component.tsx | 4 +-
packages/react-renderer/src/app/app.ts | 4 +-
.../src/app/components/route.tsx | 2 +-
.../src/app/components/view.tsx | 3 +-
.../src/{runtime => app}/reactiveState.ts | 0
.../react-renderer/src/runtime/elements.tsx | 2 +-
packages/react-renderer/src/runtime/index.ts | 3 -
.../runtime/{hooks => }/useReactiveStore.tsx | 0
.../react-simulator-renderer/package.json | 4 +-
.../test/schema/basic.ts | 81 ----
.../renderer/__snapshots__/demo.test.tsx.snap | 42 ---
.../test/src/renderer/demo.test.tsx | 31 --
.../test/utils/components.tsx | 7 -
.../test/utils/host.ts | 79 ----
.../react-simulator-renderer/tsconfig.json | 7 +-
.../src/code-runtime/codeRuntime.ts | 14 +-
.../src/code-runtime/evaluate.ts | 7 -
.../renderer-core/src/code-runtime/sandbox.ts | 10 +
.../componentTreeModel.ts | 2 +-
packages/shared/src/common/errors.ts | 7 -
packages/shared/src/common/index.ts | 3 -
.../src/common/instantiation/container.ts | 4 +-
.../instantiation/instantiationService.ts | 40 +-
packages/shared/src/utils/invariant.ts | 8 +
scripts/build.js | 2 +-
76 files changed, 2593 insertions(+), 760 deletions(-)
create mode 100644 docs/api-examples.md
create mode 100644 docs/api/index.md
create mode 100644 docs/examples/hello-world.md
create mode 100644 docs/guide/introduction.md
create mode 100644 docs/guide/quick-start.md
create mode 100644 docs/index.md
create mode 100644 docs/markdown-examples.md
create mode 100644 docs/package.json
create mode 100644 docs/public/logo.svg
create mode 100644 packages/engine-core/src/common/charCode.ts
create mode 100644 packages/engine-core/src/common/glob.ts
create mode 100644 packages/engine-core/src/common/index.ts
rename packages/{shared => engine-core}/src/common/keyCodes.ts (95%)
create mode 100644 packages/engine-core/src/common/path.ts
create mode 100644 packages/engine-core/src/common/schemas.ts
create mode 100644 packages/engine-core/src/common/strings.ts
create mode 100644 packages/engine-core/src/common/ternarySearchTree.ts
create mode 100644 packages/engine-core/src/file/file.ts
create mode 100644 packages/engine-core/src/file/fileService.ts
create mode 100644 packages/engine-core/src/file/inMemoryFileSystemProvider.ts
create mode 100644 packages/engine-core/src/file/index.ts
create mode 100644 packages/engine-core/src/window/index.ts
create mode 100644 packages/engine-core/src/window/window.ts
create mode 100644 packages/engine-core/src/window/windowImpl.ts
create mode 100644 packages/engine-core/src/window/windowManagementService.ts
create mode 100644 packages/engine-core/src/window/windowService.ts
create mode 100644 packages/engine-core/src/workbench/workbench.ts
delete mode 100644 packages/engine-core/src/workspace/file/file.ts
delete mode 100644 packages/engine-core/src/workspace/file/fileManagement.ts
delete mode 100644 packages/engine-core/src/workspace/file/fileService.ts
delete mode 100644 packages/engine-core/src/workspace/folder.ts
create mode 100644 packages/engine-core/src/workspace/index.ts
delete mode 100644 packages/engine-core/src/workspace/window/window.ts
delete mode 100644 packages/engine-core/src/workspace/window/windowService.ts
create mode 100644 packages/engine-core/src/workspace/workspaceFolder.ts
rename packages/react-renderer/src/{runtime => app}/reactiveState.ts (100%)
delete mode 100644 packages/react-renderer/src/runtime/index.ts
rename packages/react-renderer/src/runtime/{hooks => }/useReactiveStore.tsx (100%)
delete mode 100644 packages/react-simulator-renderer/test/schema/basic.ts
delete mode 100644 packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap
delete mode 100644 packages/react-simulator-renderer/test/src/renderer/demo.test.tsx
delete mode 100644 packages/react-simulator-renderer/test/utils/components.tsx
delete mode 100644 packages/react-simulator-renderer/test/utils/host.ts
delete mode 100644 packages/renderer-core/src/code-runtime/evaluate.ts
create mode 100644 packages/renderer-core/src/code-runtime/sandbox.ts
delete mode 100644 packages/shared/src/common/errors.ts
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',
});
}