mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-12 11:20:11 +00:00
refactor: diService、eventManager
This commit is contained in:
parent
9d0f178a06
commit
006b9b615e
@ -9,7 +9,7 @@ import {
|
||||
Iterable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { ICommand, ICommandHandler } from './command';
|
||||
import { Extensions, Registry } from '../common/registry';
|
||||
import { Extensions, Registry } from '../extension/registry';
|
||||
import { ICommandService } from './commandService';
|
||||
|
||||
export type ICommandsMap = Map<string, ICommand>;
|
||||
@ -26,7 +26,7 @@ export interface ICommandRegistry {
|
||||
getCommands(): ICommandsMap;
|
||||
}
|
||||
|
||||
class CommandsRegistry implements ICommandRegistry {
|
||||
class CommandsRegistryImpl implements ICommandRegistry {
|
||||
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
||||
|
||||
private readonly _didRegisterCommandEmitter = new Emitter<string>();
|
||||
@ -111,6 +111,6 @@ class CommandsRegistry implements ICommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
const commandsRegistry = new CommandsRegistry();
|
||||
export const CommandsRegistry = new CommandsRegistryImpl();
|
||||
|
||||
Registry.add(Extensions.Command, commandsRegistry);
|
||||
Registry.add(Extensions.Command, CommandsRegistry);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared';
|
||||
import { Registry, Extensions } from '../common/registry';
|
||||
import { ICommandRegistry } from './commandRegistry';
|
||||
import { createDecorator, IInstantiationService } from '@alilc/lowcode-shared';
|
||||
import { CommandsRegistry } from './commandRegistry';
|
||||
|
||||
export interface ICommandService {
|
||||
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
||||
@ -8,7 +7,6 @@ export interface ICommandService {
|
||||
|
||||
export const ICommandService = createDecorator<ICommandService>('commandService');
|
||||
|
||||
@Provide(ICommandService)
|
||||
export class CommandService implements ICommandService {
|
||||
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
|
||||
|
||||
@ -17,7 +15,7 @@ export class CommandService implements ICommandService {
|
||||
}
|
||||
|
||||
private tryExecuteCommand(id: string, args: any[]): Promise<any> {
|
||||
const command = Registry.as<ICommandRegistry>(Extensions.Command).getCommand(id);
|
||||
const command = CommandsRegistry.getCommand(id);
|
||||
if (!command) {
|
||||
return Promise.reject(new Error(`command '${id}' not found`));
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
types,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { isUndefined, isObject } from 'lodash-es';
|
||||
import { Extensions, Registry } from '../common/registry';
|
||||
import { Extensions, Registry } from '../extension/registry';
|
||||
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
|
||||
|
||||
export interface IConfigurationRegistry {
|
||||
@ -133,9 +133,9 @@ export const allSettings: {
|
||||
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
||||
} = { properties: {}, patternProperties: {} };
|
||||
|
||||
export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
||||
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
||||
private configurationDefaultsOverrides: Map<
|
||||
private readonly configurationDefaultsOverrides: Map<
|
||||
string,
|
||||
{
|
||||
configurationDefaultOverrides: IConfigurationDefaultOverride[];
|
||||
@ -143,8 +143,8 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
}
|
||||
>;
|
||||
|
||||
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private readonly configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private readonly excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private overrideIdentifiers = new Set<string>();
|
||||
|
||||
private propertiesChangeEmitter = new Emitter<{
|
||||
@ -645,4 +645,6 @@ function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean {
|
||||
return a.id === b.id && a.version === b.version;
|
||||
}
|
||||
|
||||
Registry.add(Extensions.Configuration, new ConfigurationRegistry());
|
||||
export const ConfigurationRegistry = new ConfigurationRegistryImpl();
|
||||
|
||||
Registry.add(Extensions.Configuration, ConfigurationRegistry);
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import {
|
||||
createDecorator,
|
||||
Emitter,
|
||||
Provide,
|
||||
type Event,
|
||||
type EventListener,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared';
|
||||
import {
|
||||
Configuration,
|
||||
DefaultConfiguration,
|
||||
@ -69,7 +63,6 @@ export interface IConfigurationService {
|
||||
|
||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||
|
||||
@Provide(IConfigurationService)
|
||||
export class ConfigurationService implements IConfigurationService {
|
||||
private configuration: Configuration;
|
||||
private readonly defaultConfiguration: DefaultConfiguration;
|
||||
|
||||
@ -6,11 +6,11 @@ import {
|
||||
type IOverrides,
|
||||
} from './configurationModel';
|
||||
import {
|
||||
ConfigurationRegistry,
|
||||
type IConfigurationPropertySchema,
|
||||
type IConfigurationRegistry,
|
||||
type IRegisteredConfigurationPropertySchema,
|
||||
} from './configurationRegistry';
|
||||
import { Registry, Extensions } from '../common/registry';
|
||||
import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es';
|
||||
import {
|
||||
IInspectValue,
|
||||
@ -37,8 +37,8 @@ export class DefaultConfiguration {
|
||||
|
||||
initialize(): ConfigurationModel {
|
||||
this.resetConfigurationModel();
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(
|
||||
({ properties }) => this.onDidUpdateConfiguration([...properties]),
|
||||
ConfigurationRegistry.onDidUpdateConfiguration(({ properties }) =>
|
||||
this.onDidUpdateConfiguration([...properties]),
|
||||
);
|
||||
|
||||
return this.configurationModel;
|
||||
@ -56,19 +56,14 @@ export class DefaultConfiguration {
|
||||
}
|
||||
|
||||
private onDidUpdateConfiguration(properties: string[]): void {
|
||||
this.updateConfigurationModel(
|
||||
properties,
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(),
|
||||
);
|
||||
this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties());
|
||||
this.emitter.emit({ defaults: this.configurationModel, properties });
|
||||
}
|
||||
|
||||
private resetConfigurationModel(): void {
|
||||
this._configurationModel = ConfigurationModel.createEmptyModel();
|
||||
|
||||
const properties = Registry.as<IConfigurationRegistry>(
|
||||
Extensions.Configuration,
|
||||
).getConfigurationProperties();
|
||||
const properties = ConfigurationRegistry.getConfigurationProperties();
|
||||
|
||||
this.updateConfigurationModel(Object.keys(properties), properties);
|
||||
}
|
||||
@ -156,9 +151,7 @@ class ConfigurationModelParser {
|
||||
raw: any,
|
||||
options?: ConfigurationParseOptions,
|
||||
): IConfigurationModel & { hasExcludedProperties?: boolean } {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(
|
||||
Extensions.Configuration,
|
||||
).getConfigurationProperties();
|
||||
const configurationProperties = ConfigurationRegistry.getConfigurationProperties();
|
||||
const filtered = this.filter(raw, configurationProperties, true, options);
|
||||
|
||||
raw = filtered.raw;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { type IConfigurationRegistry, type IConfigurationNode } from '../configuration';
|
||||
import { Registry, Extensions } from '../common/registry';
|
||||
import { ConfigurationRegistry, type IConfigurationNode } from '../configuration';
|
||||
import { type ExtensionInitializer, type IExtensionInstance } from './extension';
|
||||
import { invariant } from '@alilc/lowcode-shared';
|
||||
|
||||
@ -19,9 +18,8 @@ export class ExtensionHost {
|
||||
initializer: ExtensionInitializer,
|
||||
preferenceConfigurations: IConfigurationNode[],
|
||||
) {
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
this.configurationProperties =
|
||||
configurationRegistry.registerConfigurations(preferenceConfigurations);
|
||||
ConfigurationRegistry.registerConfigurations(preferenceConfigurations);
|
||||
|
||||
this.instance = initializer({});
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export class ExtensionManagement {
|
||||
): Promise<void> {
|
||||
if (!this.validateExtension(extension, override)) return;
|
||||
|
||||
const metadata = extension.meta ?? {};
|
||||
const metadata = extension.metadata ?? {};
|
||||
const host = new ExtensionHost(
|
||||
extension.name,
|
||||
extension,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||
import { createDecorator } from '@alilc/lowcode-shared';
|
||||
import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement';
|
||||
import { type IFunctionExtension } from './extension';
|
||||
import { ExtensionHost } from './extensionHost';
|
||||
@ -15,7 +15,6 @@ export interface IExtensionService {
|
||||
|
||||
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
||||
|
||||
@Provide(IExtensionService)
|
||||
export class ExtensionService implements IExtensionService {
|
||||
private extensionManagement = new ExtensionManagement();
|
||||
|
||||
|
||||
@ -41,5 +41,6 @@ export const Registry: IRegistry = new RegistryImpl();
|
||||
export const Extensions = {
|
||||
Configuration: 'base.contributions.configuration',
|
||||
Command: 'base.contributions.command',
|
||||
Keybinding: 'base.contributions.keybinding',
|
||||
Widget: 'base.contributions.widget',
|
||||
};
|
||||
@ -4,5 +4,9 @@ export * from './resource';
|
||||
export * from './command';
|
||||
|
||||
// test
|
||||
export * from './common/registry';
|
||||
export * from './extension/registry';
|
||||
export * from './main';
|
||||
export * from './keybinding/keybindingRegistry';
|
||||
export * from './keybinding/keybindingParser';
|
||||
export * from './keybinding/keybindingResolver';
|
||||
export * from './keybinding/keybindings';
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* A keybinding is a sequence of chords.
|
||||
*/
|
||||
export class Keybinding {
|
||||
public readonly chords: Chord[];
|
||||
|
||||
constructor(chords: Chord[]) {
|
||||
if (chords.length === 0) {
|
||||
throw illegalArgument(`chords`);
|
||||
}
|
||||
this.chords = chords;
|
||||
}
|
||||
|
||||
public getHashCode(): string {
|
||||
let result = '';
|
||||
for (let i = 0, len = this.chords.length; i < len; i++) {
|
||||
if (i !== 0) {
|
||||
result += ';';
|
||||
}
|
||||
result += this.chords[i].getHashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public equals(other: Keybinding | null): boolean {
|
||||
if (other === null) {
|
||||
return false;
|
||||
}
|
||||
if (this.chords.length !== other.chords.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this.chords.length; i++) {
|
||||
if (!this.chords[i].equals(other.chords[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
99
packages/engine-core/src/keybinding/keybindingParser.ts
Normal file
99
packages/engine-core/src/keybinding/keybindingParser.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { KeyCodeUtils, ScanCodeUtils } from '@alilc/lowcode-shared';
|
||||
import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from './keybindings';
|
||||
|
||||
export class KeybindingParser {
|
||||
private static _readModifiers(input: string) {
|
||||
input = input.toLowerCase().trim();
|
||||
|
||||
let ctrl = false;
|
||||
let shift = false;
|
||||
let alt = false;
|
||||
let meta = false;
|
||||
|
||||
let matchedModifier: boolean;
|
||||
|
||||
do {
|
||||
matchedModifier = false;
|
||||
if (/^ctrl(\+|-)/.test(input)) {
|
||||
ctrl = true;
|
||||
input = input.slice('ctrl-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^shift(\+|-)/.test(input)) {
|
||||
shift = true;
|
||||
input = input.slice('shift-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^alt(\+|-)/.test(input)) {
|
||||
alt = true;
|
||||
input = input.slice('alt-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^meta(\+|-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.slice('meta-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^win(\+|-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.slice('win-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^cmd(\+|-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.slice('cmd-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
} while (matchedModifier);
|
||||
|
||||
let key: string;
|
||||
|
||||
const firstSpaceIdx = input.indexOf(' ');
|
||||
if (firstSpaceIdx > 0) {
|
||||
key = input.substring(0, firstSpaceIdx);
|
||||
input = input.substring(firstSpaceIdx);
|
||||
} else {
|
||||
key = input;
|
||||
input = '';
|
||||
}
|
||||
|
||||
return {
|
||||
remains: input,
|
||||
ctrl,
|
||||
shift,
|
||||
alt,
|
||||
meta,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
private static parseChord(input: string): [Chord, string] {
|
||||
const mods = this._readModifiers(input);
|
||||
const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/);
|
||||
if (scanCodeMatch) {
|
||||
const strScanCode = scanCodeMatch[1];
|
||||
const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode);
|
||||
return [
|
||||
new ScanCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode),
|
||||
mods.remains,
|
||||
];
|
||||
}
|
||||
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
|
||||
return [new KeyCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
|
||||
}
|
||||
|
||||
static parseKeybinding(input: string): Keybinding | null {
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chords: Chord[] = [];
|
||||
let chord: Chord;
|
||||
|
||||
while (input.length > 0) {
|
||||
[chord, input] = this.parseChord(input);
|
||||
chords.push(chord);
|
||||
}
|
||||
return chords.length > 0 ? new Keybinding(chords) : null;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,8 @@
|
||||
import { OperatingSystem, OS } from '@alilc/lowcode-shared';
|
||||
import { ICommandHandler, ICommandMetadata, CommandsRegistry } from '../command';
|
||||
import { Keybinding } from './keybindings';
|
||||
import { Extensions, Registry } from '../extension/registry';
|
||||
|
||||
export interface IKeybindingItem {
|
||||
keybinding: Keybinding | null;
|
||||
command: string | null;
|
||||
@ -24,3 +29,81 @@ export interface IKeybindings {
|
||||
secondary?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IKeybindingRule extends IKeybindings {
|
||||
id: string;
|
||||
weight: number;
|
||||
args?: any;
|
||||
}
|
||||
|
||||
export interface ICommandAndKeybindingRule extends IKeybindingRule {
|
||||
handler: ICommandHandler;
|
||||
metadata?: ICommandMetadata | null;
|
||||
}
|
||||
|
||||
export interface IExtensionKeybindingRule {
|
||||
keybinding: Keybinding | null;
|
||||
id: string;
|
||||
args?: any;
|
||||
weight: number;
|
||||
extensionId?: string;
|
||||
isBuiltinExtension?: boolean;
|
||||
}
|
||||
|
||||
export const enum KeybindingWeight {
|
||||
EditorCore = 0,
|
||||
EditorContrib = 100,
|
||||
WorkbenchContrib = 200,
|
||||
BuiltinExtension = 300,
|
||||
ExternalExtension = 400,
|
||||
}
|
||||
|
||||
export interface IKeybindingsRegistry {
|
||||
registerKeybindingRule(rule: IKeybindingRule): void;
|
||||
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void;
|
||||
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void;
|
||||
getDefaultKeybindings(): IKeybindingItem[];
|
||||
}
|
||||
|
||||
export class KeybindingsRegistryImpl implements IKeybindingsRegistry {
|
||||
/**
|
||||
* Take current platform into account and reduce to primary & secondary.
|
||||
*/
|
||||
private static bindToCurrentPlatform(kb: IKeybindings): {
|
||||
primary?: number;
|
||||
secondary?: number[];
|
||||
} {
|
||||
if (OS === OperatingSystem.Windows) {
|
||||
if (kb && kb.win) {
|
||||
return kb.win;
|
||||
}
|
||||
} else if (OS === OperatingSystem.Macintosh) {
|
||||
if (kb && kb.mac) {
|
||||
return kb.mac;
|
||||
}
|
||||
} else {
|
||||
if (kb && kb.linux) {
|
||||
return kb.linux;
|
||||
}
|
||||
}
|
||||
|
||||
return kb;
|
||||
}
|
||||
|
||||
registerKeybindingRule(rule: IKeybindingRule): void {
|
||||
const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule);
|
||||
}
|
||||
|
||||
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void {
|
||||
this.registerKeybindingRule(desc);
|
||||
CommandsRegistry.registerCommand(desc);
|
||||
}
|
||||
|
||||
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {}
|
||||
|
||||
getDefaultKeybindings(): IKeybindingItem[] {}
|
||||
}
|
||||
|
||||
export const KeybindingsRegistry = new KeybindingsRegistryImpl();
|
||||
|
||||
Registry.add(Extensions.Keybinding, KeybindingsRegistry);
|
||||
|
||||
79
packages/engine-core/src/keybinding/keybindingResolver.ts
Normal file
79
packages/engine-core/src/keybinding/keybindingResolver.ts
Normal file
@ -0,0 +1,79 @@
|
||||
export const enum ResultKind {
|
||||
/** No keybinding found this sequence of chords */
|
||||
NoMatchingKb,
|
||||
|
||||
/** There're several keybindings that have the given sequence of chords as a prefix */
|
||||
MoreChordsNeeded,
|
||||
|
||||
/** A single keybinding found to be dispatched/invoked */
|
||||
KbFound,
|
||||
}
|
||||
|
||||
export type ResolutionResult =
|
||||
| { kind: ResultKind.NoMatchingKb }
|
||||
| { kind: ResultKind.MoreChordsNeeded }
|
||||
| { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean };
|
||||
|
||||
// util definitions to make working with the above types easier within this module:
|
||||
|
||||
export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb };
|
||||
const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded };
|
||||
function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult {
|
||||
return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export class ResolvedKeybindingItem {
|
||||
_resolvedKeybindingItemBrand: void = undefined;
|
||||
|
||||
public readonly resolvedKeybinding: ResolvedKeybinding | undefined;
|
||||
public readonly chords: string[];
|
||||
public readonly bubble: boolean;
|
||||
public readonly command: string | null;
|
||||
public readonly commandArgs: any;
|
||||
public readonly when: ContextKeyExpression | undefined;
|
||||
public readonly isDefault: boolean;
|
||||
public readonly extensionId: string | null;
|
||||
public readonly isBuiltinExtension: boolean;
|
||||
|
||||
constructor(
|
||||
resolvedKeybinding: ResolvedKeybinding | undefined,
|
||||
command: string | null,
|
||||
commandArgs: any,
|
||||
when: ContextKeyExpression | undefined,
|
||||
isDefault: boolean,
|
||||
extensionId: string | null,
|
||||
isBuiltinExtension: boolean,
|
||||
) {
|
||||
this.resolvedKeybinding = resolvedKeybinding;
|
||||
this.chords = resolvedKeybinding
|
||||
? toEmptyArrayIfContainsNull(resolvedKeybinding.getDispatchChords())
|
||||
: [];
|
||||
if (resolvedKeybinding && this.chords.length === 0) {
|
||||
// handle possible single modifier chord keybindings
|
||||
this.chords = toEmptyArrayIfContainsNull(
|
||||
resolvedKeybinding.getSingleModifierDispatchChords(),
|
||||
);
|
||||
}
|
||||
this.bubble = command ? command.charCodeAt(0) === CharCode.Caret : false;
|
||||
this.command = this.bubble ? command!.substr(1) : command;
|
||||
this.commandArgs = commandArgs;
|
||||
this.when = when;
|
||||
this.isDefault = isDefault;
|
||||
this.extensionId = extensionId;
|
||||
this.isBuiltinExtension = isBuiltinExtension;
|
||||
}
|
||||
}
|
||||
|
||||
export function toEmptyArrayIfContainsNull<T>(arr: (T | null)[]): T[] {
|
||||
const result: T[] = [];
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
const element = arr[i];
|
||||
if (!element) {
|
||||
return [];
|
||||
}
|
||||
result.push(element);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
100
packages/engine-core/src/keybinding/keybindingService.ts
Normal file
100
packages/engine-core/src/keybinding/keybindingService.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { createDecorator, type IJSONSchema, KeyCode, type Event } from '@alilc/lowcode-shared';
|
||||
import { Keybinding, ResolvedKeybinding } from './keybindings';
|
||||
import { ResolutionResult, ResolvedKeybindingItem } from './keybindingResolver';
|
||||
|
||||
export interface IUserFriendlyKeybinding {
|
||||
key: string;
|
||||
command: string;
|
||||
args?: any;
|
||||
when?: string;
|
||||
}
|
||||
|
||||
export interface IKeyboardEvent {
|
||||
readonly _standardKeyboardEventBrand: true;
|
||||
|
||||
readonly ctrlKey: boolean;
|
||||
readonly shiftKey: boolean;
|
||||
readonly altKey: boolean;
|
||||
readonly metaKey: boolean;
|
||||
readonly altGraphKey: boolean;
|
||||
readonly keyCode: KeyCode;
|
||||
readonly code: string;
|
||||
}
|
||||
|
||||
export interface KeybindingsSchemaContribution {
|
||||
readonly onDidChange?: Event<void>;
|
||||
|
||||
getSchemaAdditions(): IJSONSchema[];
|
||||
}
|
||||
|
||||
export interface IKeybindingService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly inChordMode: boolean;
|
||||
|
||||
onDidUpdateKeybindings: Event<void>;
|
||||
|
||||
/**
|
||||
* Returns none, one or many (depending on keyboard layout)!
|
||||
*/
|
||||
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
|
||||
|
||||
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
|
||||
|
||||
resolveUserBinding(userBinding: string): ResolvedKeybinding[];
|
||||
|
||||
/**
|
||||
* Resolve and dispatch `keyboardEvent` and invoke the command.
|
||||
*/
|
||||
dispatchEvent(e: IKeyboardEvent, target: any): boolean;
|
||||
|
||||
/**
|
||||
* Resolve and dispatch `keyboardEvent`, but do not invoke the command or change inner state.
|
||||
*/
|
||||
softDispatch(keyboardEvent: IKeyboardEvent, target: any): ResolutionResult;
|
||||
|
||||
/**
|
||||
* Enable hold mode for this command. This is only possible if the command is current being dispatched, meaning
|
||||
* we are after its keydown and before is keyup event.
|
||||
*
|
||||
* @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled.
|
||||
*/
|
||||
enableKeybindingHoldMode(commandId: string): Promise<void> | undefined;
|
||||
|
||||
dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void;
|
||||
|
||||
/**
|
||||
* Look up keybindings for a command.
|
||||
* Use `lookupKeybinding` if you are interested in the preferred keybinding.
|
||||
*/
|
||||
lookupKeybindings(commandId: string): ResolvedKeybinding[];
|
||||
|
||||
/**
|
||||
* Look up the preferred (last defined) keybinding for a command.
|
||||
* @returns The preferred keybinding or null if the command is not bound.
|
||||
*/
|
||||
lookupKeybinding(commandId: string, context?: any): ResolvedKeybinding | undefined;
|
||||
|
||||
getDefaultKeybindingsContent(): string;
|
||||
|
||||
getDefaultKeybindings(): readonly ResolvedKeybindingItem[];
|
||||
|
||||
getKeybindings(): readonly ResolvedKeybindingItem[];
|
||||
|
||||
customKeybindingsCount(): number;
|
||||
|
||||
/**
|
||||
* Will the given key event produce a character that's rendered on screen, e.g. in a
|
||||
* text box. *Note* that the results of this function can be incorrect.
|
||||
*/
|
||||
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
|
||||
|
||||
registerSchemaContribution(contribution: KeybindingsSchemaContribution): void;
|
||||
|
||||
toggleLogging(): boolean;
|
||||
|
||||
_dumpDebugInfo(): string;
|
||||
_dumpDebugInfoJSON(): string;
|
||||
}
|
||||
|
||||
export const IKeybindingService = createDecorator<IKeybindingService>('keybindingService');
|
||||
280
packages/engine-core/src/keybinding/keybindings.ts
Normal file
280
packages/engine-core/src/keybinding/keybindings.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { illegalArgument, KeyCode, OperatingSystem, ScanCode } from '@alilc/lowcode-shared';
|
||||
|
||||
/**
|
||||
* Binary encoding strategy:
|
||||
* ```
|
||||
* 1111 11
|
||||
* 5432 1098 7654 3210
|
||||
* ---- CSAW KKKK KKKK
|
||||
* C = bit 11 = ctrlCmd flag
|
||||
* S = bit 10 = shift flag
|
||||
* A = bit 9 = alt flag
|
||||
* W = bit 8 = winCtrl flag
|
||||
* K = bits 0-7 = key code
|
||||
* ```
|
||||
*/
|
||||
const enum BinaryKeybindingsMask {
|
||||
CtrlCmd = (1 << 11) >>> 0,
|
||||
Shift = (1 << 10) >>> 0,
|
||||
Alt = (1 << 9) >>> 0,
|
||||
WinCtrl = (1 << 8) >>> 0,
|
||||
KeyCode = 0x000000ff,
|
||||
}
|
||||
|
||||
export function decodeKeybinding(
|
||||
keybinding: number | number[],
|
||||
OS: OperatingSystem,
|
||||
): Keybinding | null {
|
||||
if (typeof keybinding === 'number') {
|
||||
if (keybinding === 0) {
|
||||
return null;
|
||||
}
|
||||
const firstChord = (keybinding & 0x0000ffff) >>> 0;
|
||||
const secondChord = (keybinding & 0xffff0000) >>> 16;
|
||||
if (secondChord !== 0) {
|
||||
return new Keybinding([
|
||||
createSimpleKeybinding(firstChord, OS),
|
||||
createSimpleKeybinding(secondChord, OS),
|
||||
]);
|
||||
}
|
||||
return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
|
||||
} else {
|
||||
const chords = [];
|
||||
for (let i = 0; i < keybinding.length; i++) {
|
||||
chords.push(createSimpleKeybinding(keybinding[i], OS));
|
||||
}
|
||||
return new Keybinding(chords);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): KeyCodeChord {
|
||||
const ctrlCmd = keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false;
|
||||
const winCtrl = keybinding & BinaryKeybindingsMask.WinCtrl ? true : false;
|
||||
|
||||
const ctrlKey = OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd;
|
||||
const shiftKey = keybinding & BinaryKeybindingsMask.Shift ? true : false;
|
||||
const altKey = keybinding & BinaryKeybindingsMask.Alt ? true : false;
|
||||
const metaKey = OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl;
|
||||
const keyCode = keybinding & BinaryKeybindingsMask.KeyCode;
|
||||
|
||||
return new KeyCodeChord(ctrlKey, shiftKey, altKey, metaKey, keyCode);
|
||||
}
|
||||
|
||||
export interface Modifiers {
|
||||
readonly ctrlKey: boolean;
|
||||
readonly shiftKey: boolean;
|
||||
readonly altKey: boolean;
|
||||
readonly metaKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a chord which uses the `keyCode` field of keyboard events.
|
||||
* A chord is a combination of keys pressed simultaneously.
|
||||
*/
|
||||
export class KeyCodeChord implements Modifiers {
|
||||
constructor(
|
||||
public readonly ctrlKey: boolean,
|
||||
public readonly shiftKey: boolean,
|
||||
public readonly altKey: boolean,
|
||||
public readonly metaKey: boolean,
|
||||
public readonly keyCode: KeyCode,
|
||||
) {}
|
||||
|
||||
equals(other: Chord): boolean {
|
||||
return (
|
||||
other instanceof KeyCodeChord &&
|
||||
this.ctrlKey === other.ctrlKey &&
|
||||
this.shiftKey === other.shiftKey &&
|
||||
this.altKey === other.altKey &&
|
||||
this.metaKey === other.metaKey &&
|
||||
this.keyCode === other.keyCode
|
||||
);
|
||||
}
|
||||
|
||||
getHashCode(): string {
|
||||
const ctrl = this.ctrlKey ? '1' : '0';
|
||||
const shift = this.shiftKey ? '1' : '0';
|
||||
const alt = this.altKey ? '1' : '0';
|
||||
const meta = this.metaKey ? '1' : '0';
|
||||
return `K${ctrl}${shift}${alt}${meta}${this.keyCode}`;
|
||||
}
|
||||
|
||||
isModifierKey(): boolean {
|
||||
return (
|
||||
this.keyCode === KeyCode.Unknown ||
|
||||
this.keyCode === KeyCode.Ctrl ||
|
||||
this.keyCode === KeyCode.Meta ||
|
||||
this.keyCode === KeyCode.Alt ||
|
||||
this.keyCode === KeyCode.Shift
|
||||
);
|
||||
}
|
||||
|
||||
toKeybinding(): Keybinding {
|
||||
return new Keybinding([this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
|
||||
*/
|
||||
isDuplicateModifierCase(): boolean {
|
||||
return (
|
||||
(this.ctrlKey && this.keyCode === KeyCode.Ctrl) ||
|
||||
(this.shiftKey && this.keyCode === KeyCode.Shift) ||
|
||||
(this.altKey && this.keyCode === KeyCode.Alt) ||
|
||||
(this.metaKey && this.keyCode === KeyCode.Meta)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a chord which uses the `code` field of keyboard events.
|
||||
* A chord is a combination of keys pressed simultaneously.
|
||||
*/
|
||||
export class ScanCodeChord implements Modifiers {
|
||||
constructor(
|
||||
public readonly ctrlKey: boolean,
|
||||
public readonly shiftKey: boolean,
|
||||
public readonly altKey: boolean,
|
||||
public readonly metaKey: boolean,
|
||||
public readonly scanCode: ScanCode,
|
||||
) {}
|
||||
|
||||
equals(other: Chord): boolean {
|
||||
return (
|
||||
other instanceof ScanCodeChord &&
|
||||
this.ctrlKey === other.ctrlKey &&
|
||||
this.shiftKey === other.shiftKey &&
|
||||
this.altKey === other.altKey &&
|
||||
this.metaKey === other.metaKey &&
|
||||
this.scanCode === other.scanCode
|
||||
);
|
||||
}
|
||||
|
||||
getHashCode(): string {
|
||||
const ctrl = this.ctrlKey ? '1' : '0';
|
||||
const shift = this.shiftKey ? '1' : '0';
|
||||
const alt = this.altKey ? '1' : '0';
|
||||
const meta = this.metaKey ? '1' : '0';
|
||||
return `S${ctrl}${shift}${alt}${meta}${this.scanCode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
|
||||
*/
|
||||
isDuplicateModifierCase(): boolean {
|
||||
return (
|
||||
(this.ctrlKey &&
|
||||
(this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight)) ||
|
||||
(this.shiftKey &&
|
||||
(this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight)) ||
|
||||
(this.altKey &&
|
||||
(this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight)) ||
|
||||
(this.metaKey &&
|
||||
(this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type Chord = KeyCodeChord | ScanCodeChord;
|
||||
|
||||
/**
|
||||
* A keybinding is a sequence of chords.
|
||||
*/
|
||||
export class Keybinding {
|
||||
readonly chords: Chord[];
|
||||
|
||||
constructor(chords: Chord[]) {
|
||||
if (chords.length === 0) {
|
||||
throw illegalArgument(`chords`);
|
||||
}
|
||||
this.chords = chords;
|
||||
}
|
||||
|
||||
getHashCode(): string {
|
||||
let result = '';
|
||||
for (let i = 0, len = this.chords.length; i < len; i++) {
|
||||
if (i !== 0) {
|
||||
result += ';';
|
||||
}
|
||||
result += this.chords[i].getHashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
equals(other: Keybinding | null): boolean {
|
||||
if (other === null) {
|
||||
return false;
|
||||
}
|
||||
if (this.chords.length !== other.chords.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this.chords.length; i++) {
|
||||
if (!this.chords[i].equals(other.chords[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResolvedChord {
|
||||
constructor(
|
||||
public readonly ctrlKey: boolean,
|
||||
public readonly shiftKey: boolean,
|
||||
public readonly altKey: boolean,
|
||||
public readonly metaKey: boolean,
|
||||
public readonly keyLabel: string | null,
|
||||
public readonly keyAriaLabel: string | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
export type SingleModifierChord = 'ctrl' | 'shift' | 'alt' | 'meta';
|
||||
|
||||
/**
|
||||
* A resolved keybinding. Consists of one or multiple chords.
|
||||
*/
|
||||
export abstract class ResolvedKeybinding {
|
||||
/**
|
||||
* This prints the binding in a format suitable for displaying in the UI.
|
||||
*/
|
||||
public abstract getLabel(): string | null;
|
||||
/**
|
||||
* This prints the binding in a format suitable for ARIA.
|
||||
*/
|
||||
public abstract getAriaLabel(): string | null;
|
||||
/**
|
||||
* This prints the binding in a format suitable for electron's accelerators.
|
||||
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
|
||||
*/
|
||||
public abstract getElectronAccelerator(): string | null;
|
||||
/**
|
||||
* This prints the binding in a format suitable for user settings.
|
||||
*/
|
||||
public abstract getUserSettingsLabel(): string | null;
|
||||
/**
|
||||
* Is the user settings label reflecting the label?
|
||||
*/
|
||||
public abstract isWYSIWYG(): boolean;
|
||||
/**
|
||||
* Does the keybinding consist of more than one chord?
|
||||
*/
|
||||
public abstract hasMultipleChords(): boolean;
|
||||
/**
|
||||
* Returns the chords that comprise of the keybinding.
|
||||
*/
|
||||
public abstract getChords(): ResolvedChord[];
|
||||
/**
|
||||
* Returns the chords as strings useful for dispatching.
|
||||
* Returns null for modifier only chords.
|
||||
* @example keybinding "Shift" -> null
|
||||
* @example keybinding ("D" with shift == true) -> "shift+D"
|
||||
*/
|
||||
public abstract getDispatchChords(): (string | null)[];
|
||||
/**
|
||||
* Returns the modifier only chords as strings useful for dispatching.
|
||||
* Returns null for chords that contain more than one modifier or a regular key.
|
||||
* @example keybinding "Shift" -> "shift"
|
||||
* @example keybinding ("D" with shift == true") -> null
|
||||
*/
|
||||
public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[];
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
createDecorator,
|
||||
Provide,
|
||||
type Package,
|
||||
type Reference,
|
||||
mapPackageToUniqueId,
|
||||
@ -22,7 +21,6 @@ export interface IResourceService {
|
||||
|
||||
export const IResourceService = createDecorator<IResourceService>('resourceService');
|
||||
|
||||
@Provide(IResourceService)
|
||||
export class ResourceService implements IResourceService {
|
||||
private resourceModel = new ResourceModel();
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Extensions, Registry } from '../../extension/extension';
|
||||
import { IWidgetRegistry } from '../widget/widgetRegistry';
|
||||
import { WidgetRegistry } from '../widget/widgetRegistry';
|
||||
|
||||
export const enum LayoutParts {
|
||||
TopBar = 1,
|
||||
@ -21,7 +20,7 @@ export interface ILayout {
|
||||
|
||||
export class Layout<View> implements ILayout {
|
||||
constructor(public mainContainer: HTMLElement) {
|
||||
Registry.as<IWidgetRegistry<View>>(Extensions.Widget).onDidRegister(() => {});
|
||||
WidgetRegistry.onDidRegister(() => {});
|
||||
}
|
||||
|
||||
registerPart(part: LayoutParts): void {}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared';
|
||||
import { IWidget } from './widget';
|
||||
import { Extensions, Registry } from '../../extension/extension';
|
||||
import { Extensions, Registry } from '../../extension/registry';
|
||||
|
||||
export interface IWidgetRegistry<View> {
|
||||
onDidRegister: Event<IWidget<View>[]>;
|
||||
@ -12,7 +12,7 @@ export interface IWidgetRegistry<View> {
|
||||
getWidgets(): IWidget<View>[];
|
||||
}
|
||||
|
||||
export class WidgetRegistry<View> implements IWidgetRegistry<View> {
|
||||
export class WidgetRegistryImpl<View> implements IWidgetRegistry<View> {
|
||||
private _widgets: Map<string, IWidget<View>> = new Map();
|
||||
|
||||
private emitter = new Emitter<IWidget<View>[]>();
|
||||
@ -34,4 +34,6 @@ export class WidgetRegistry<View> implements IWidgetRegistry<View> {
|
||||
}
|
||||
}
|
||||
|
||||
Registry.add(Extensions.Widget, new WidgetRegistry<any>());
|
||||
export const WidgetRegistry = new WidgetRegistryImpl<any>();
|
||||
|
||||
Registry.add(Extensions.Widget, WidgetRegistry);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||
import { createDecorator } from '@alilc/lowcode-shared';
|
||||
|
||||
export interface IWorkbenchService {
|
||||
initialize(): void;
|
||||
@ -6,7 +6,6 @@ export interface IWorkbenchService {
|
||||
|
||||
export const IWorkbenchService = createDecorator<IWorkbenchService>('workbenchService');
|
||||
|
||||
@Provide(IWorkbenchService)
|
||||
export class WorkbenchService implements IWorkbenchService {
|
||||
initialize(): void {
|
||||
console.log('workbench service');
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||
import { createDecorator } from '@alilc/lowcode-shared';
|
||||
|
||||
export interface IWorkspaceService {}
|
||||
|
||||
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
||||
|
||||
@Provide(IWorkspaceService)
|
||||
export class WorkspaceService implements IWorkspaceService {}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type Event, type EventListener, createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||
import { type Event, type EventListener, createDecorator } from '@alilc/lowcode-shared';
|
||||
|
||||
export interface ITheme {
|
||||
type: string;
|
||||
@ -14,7 +14,6 @@ export interface IThemeService {
|
||||
|
||||
export const IThemeService = createDecorator<IThemeService>('themeService');
|
||||
|
||||
@Provide(IThemeService)
|
||||
export class ThemeService implements IThemeService {
|
||||
private activeTheme: ITheme;
|
||||
|
||||
|
||||
4
packages/global.d.ts
vendored
4
packages/global.d.ts
vendored
@ -0,0 +1,4 @@
|
||||
// Global compile-time constants
|
||||
declare var __DEV__: boolean;
|
||||
declare var __VERSION__: string;
|
||||
declare var __COMPAT__: boolean;
|
||||
@ -1,9 +1,10 @@
|
||||
import {
|
||||
IBoostsService,
|
||||
IBoostsManager,
|
||||
IComponentTreeModelService,
|
||||
ILifeCycleService,
|
||||
IPackageManagementService,
|
||||
ISchemaService,
|
||||
IExtensionHostService,
|
||||
} from '@alilc/lowcode-renderer-core';
|
||||
import { InstanceAccessor } from '@alilc/lowcode-shared';
|
||||
import { createContext, useContext } from 'react';
|
||||
@ -16,7 +17,7 @@ export interface IRendererContext {
|
||||
|
||||
readonly packageManager: IPackageManagementService;
|
||||
|
||||
readonly boostsManager: IBoostsService;
|
||||
readonly boostsManager: IBoostsManager;
|
||||
|
||||
readonly componentTreeModel: IComponentTreeModelService;
|
||||
|
||||
@ -27,7 +28,7 @@ export const getRenderInstancesByAccessor = (accessor: InstanceAccessor) => {
|
||||
return {
|
||||
schema: accessor.get(ISchemaService),
|
||||
packageManager: accessor.get(IPackageManagementService),
|
||||
boostsManager: accessor.get(IBoostsService),
|
||||
boostsManager: accessor.get(IExtensionHostService).boostsManager,
|
||||
componentTreeModel: accessor.get(IComponentTreeModelService),
|
||||
lifeCycle: accessor.get(ILifeCycleService),
|
||||
};
|
||||
|
||||
@ -3,8 +3,7 @@ import {
|
||||
type AnyFunction,
|
||||
type StringDictionary,
|
||||
specTypes,
|
||||
computed,
|
||||
watch,
|
||||
Signals,
|
||||
invariant,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { useRef } from 'react';
|
||||
@ -60,10 +59,10 @@ function createReactiveStore<Snapshot = StringDictionary>(
|
||||
};
|
||||
|
||||
if (getter) {
|
||||
const computedValue = computed<any>(() => getter(target));
|
||||
const computedValue = Signals.computed<any>(() => getter(target));
|
||||
|
||||
cleanups.push(
|
||||
watch(
|
||||
Signals.watch(
|
||||
computedValue,
|
||||
(newValue) => {
|
||||
Promise.resolve().then(() => {
|
||||
@ -76,8 +75,8 @@ function createReactiveStore<Snapshot = StringDictionary>(
|
||||
);
|
||||
} else if (valueGetter) {
|
||||
const initValue = mapValue(target, filter, (node: any, paths) => {
|
||||
const computedValue = computed(() => valueGetter(node));
|
||||
const unwatch = watch(computedValue, (newValue) => {
|
||||
const computedValue = Signals.computed(() => valueGetter(node));
|
||||
const unwatch = Signals.watch(computedValue, (newValue) => {
|
||||
waitPathToSetValueMap.set(paths, newValue);
|
||||
|
||||
if (!isFlushPending && !isFlushing) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { signal, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared';
|
||||
import { Signals, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared';
|
||||
import { isPlainObject } from 'lodash-es';
|
||||
|
||||
export function reactiveStateFactory(initState: StringDictionary): InstanceStateApi {
|
||||
const proxyState = signal(initState);
|
||||
const proxyState = Signals.signal(initState);
|
||||
|
||||
return {
|
||||
get state() {
|
||||
|
||||
@ -2,6 +2,5 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "../../playground/renderer/src/plugin/remote/element.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('createComponentFunction', () => {
|
||||
it('', () => {});
|
||||
});
|
||||
@ -1,57 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeScope } from '../../../src/services/code-runtime';
|
||||
|
||||
describe('CodeScope', () => {
|
||||
it('should return initial values', () => {
|
||||
const initValue = { a: 1, b: 2 };
|
||||
const scope = new CodeScope(initValue);
|
||||
expect(scope.value.a).toBe(1);
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('inject should add new values', () => {
|
||||
const scope = new CodeScope({});
|
||||
scope.set('c', 3);
|
||||
expect(scope.value.c).toBe(3);
|
||||
});
|
||||
|
||||
it('inject should not overwrite existing values without force', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
expect(scope.value.a).not.toBe(2);
|
||||
scope.set('a', 3);
|
||||
expect(scope.value.a).toBe(3);
|
||||
});
|
||||
|
||||
it('setValue should merge values by default', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
scope.setValue({ b: 2 });
|
||||
expect(scope.value.a).toBe(1);
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('setValue should replace values when replace is true', () => {
|
||||
const initValue = { a: 1 };
|
||||
const scope = new CodeScope(initValue);
|
||||
scope.setValue({ b: 2 }, true);
|
||||
expect(scope.value.a).toBeUndefined();
|
||||
expect(scope.value.b).toBe(2);
|
||||
});
|
||||
|
||||
it('should create child scopes and respect scope hierarchy', () => {
|
||||
const parentValue = { a: 1, b: 2 };
|
||||
const childValue = { b: 3, c: 4 };
|
||||
|
||||
const parentScope = new CodeScope(parentValue);
|
||||
const childScope = parentScope.createChild(childValue);
|
||||
|
||||
expect(childScope.value.a).toBe(1); // Inherits from parent scope
|
||||
expect(childScope.value.b).toBe(3); // Overridden by child scope
|
||||
expect(childScope.value.c).toBe(4); // Unique to child scope
|
||||
expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties
|
||||
});
|
||||
});
|
||||
@ -1,36 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LifeCycleService, LifecyclePhase } from '../../src/services/lifeCycleService';
|
||||
|
||||
const sleep = () => new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
describe('LifeCycleService', () => {
|
||||
it('it works', async () => {
|
||||
let result = '';
|
||||
const lifeCycle = new LifeCycleService();
|
||||
|
||||
lifeCycle.when(LifecyclePhase.Ready).then(() => {
|
||||
result += '1';
|
||||
});
|
||||
lifeCycle.when(LifecyclePhase.Ready).finally(() => {
|
||||
result += '2';
|
||||
});
|
||||
lifeCycle.when(LifecyclePhase.Inited).then(() => {
|
||||
result += '3';
|
||||
});
|
||||
lifeCycle.when(LifecyclePhase.Inited).finally(() => {
|
||||
result += '4';
|
||||
});
|
||||
|
||||
lifeCycle.phase = LifecyclePhase.Ready;
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(result).toEqual('12');
|
||||
|
||||
lifeCycle.phase = LifecyclePhase.Inited;
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(result).toEqual('1234');
|
||||
});
|
||||
});
|
||||
@ -1,79 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { walk } from '../../src/utils/node';
|
||||
|
||||
describe('sync walker', () => {
|
||||
it('down', () => {
|
||||
const ast = {
|
||||
hasMask: true,
|
||||
visible: false,
|
||||
footer: false,
|
||||
cancelProps: {
|
||||
text: false,
|
||||
type: 'normal',
|
||||
},
|
||||
confirmState: '确定',
|
||||
confirmStyle: 'primary',
|
||||
footerActions: 'cancel,ok',
|
||||
className: 'dialog_lkz6xvcv',
|
||||
confirmText: {
|
||||
en_US: 'Confirm',
|
||||
use: '',
|
||||
zh_CN: '确定',
|
||||
type: 'JSExpression',
|
||||
value: `({"en_US":"OK","key":"i18n-xgse6q6a","type":"i18n","zh_CN":"确定"})[this.utils.getLocale()]`,
|
||||
key: 'i18n-xgse6q6a',
|
||||
extType: 'i18n',
|
||||
},
|
||||
autoFocus: true,
|
||||
title: {
|
||||
mock: {
|
||||
en_US: 'Dialog Title',
|
||||
use: '',
|
||||
zh_CN: 'Dialog标题',
|
||||
type: 'JSExpression',
|
||||
value: `({"en_US":"Dialog Title","key":"i18n-0m3kaceq","type":"i18n","zh_CN":"Dialog标题"})[this.utils.getLocale()]`,
|
||||
key: 'i18n-0m3kaceq',
|
||||
extType: 'i18n',
|
||||
},
|
||||
type: 'JSExpression',
|
||||
value: 'state.dialogInfo && state.dialogInfo.title',
|
||||
},
|
||||
closeable: [
|
||||
'esc',
|
||||
'mask',
|
||||
{
|
||||
type: 'JSExpression',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
cancelText: {
|
||||
en_US: 'Cancel',
|
||||
use: '',
|
||||
zh_CN: '取消',
|
||||
type: 'JSExpression',
|
||||
value: `({"en_US":"Cancel","key":"i18n-wtq23279","type":"i18n","zh_CN":"取消"})[this.utils.getLocale()]`,
|
||||
key: 'i18n-wtq23279',
|
||||
extType: 'i18n',
|
||||
},
|
||||
width: '800px',
|
||||
footerAlign: 'right',
|
||||
popupOutDialog: true,
|
||||
__style__: ':root {}',
|
||||
fieldId: 'dialog_case',
|
||||
height: '500px',
|
||||
};
|
||||
|
||||
const newAst = walk(ast, {
|
||||
enter(node, parent, key, index) {
|
||||
if (node.type === 'JSExpression') {
|
||||
this.replace({
|
||||
type: '1',
|
||||
value: '2',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(newAst, Object.is(newAst, ast));
|
||||
});
|
||||
});
|
||||
@ -1,98 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { someValue, mapValue } from '../../src/utils/value';
|
||||
|
||||
describe('someValue', () => {
|
||||
it('should return false for non-plain objects or empty objects', () => {
|
||||
expect(someValue([], (val) => val > 10)).toBe(false);
|
||||
expect(someValue({}, (val) => val > 10)).toBe(false);
|
||||
expect(someValue(null, (val) => val > 10)).toBe(false);
|
||||
expect(someValue(undefined, (val) => val > 10)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the predicate matches object value', () => {
|
||||
const obj = { a: 5, b: { c: 15 } };
|
||||
expect(someValue(obj, (val) => val.c > 10)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the predicate matches nested array element', () => {
|
||||
const obj = { a: [1, 2, { d: 14 }] };
|
||||
expect(someValue(obj, (val) => val.d > 10)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the predicate does not match any value', () => {
|
||||
const obj = { a: 5, b: { c: 9 } };
|
||||
expect(someValue(obj, (val) => val.c > 10)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle primitives in object values', () => {
|
||||
const obj = { a: 1, b: 'string', c: true };
|
||||
const strPredicate = (val: any) => typeof val.b === 'string';
|
||||
expect(someValue(obj, strPredicate)).toBe(true);
|
||||
|
||||
const boolPredicate = (val: any) => typeof val.c === 'boolean';
|
||||
expect(someValue(obj, boolPredicate)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle deep nesting with mixed arrays and objects', () => {
|
||||
const complexObj = { a: { b: [1, 2, { c: 3 }, [{ d: 4 }]] } };
|
||||
expect(someValue(complexObj, (val) => val.d === 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle functions and undefined values', () => {
|
||||
const objWithFunc = { a: () => {}, b: undefined };
|
||||
const funcPredicate = (val: any) => typeof val.a === 'function';
|
||||
expect(someValue(objWithFunc, funcPredicate)).toBe(true);
|
||||
|
||||
const undefinedPredicate = (val: any) => val.b === undefined;
|
||||
expect(someValue(objWithFunc, undefinedPredicate)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapValue', () => {
|
||||
const predicate = (obj: any) => obj && obj.process;
|
||||
const processor = (obj: any, paths: any[]) => ({ ...obj, processed: true, paths });
|
||||
|
||||
it('should not process object if it does not match the predicate', () => {
|
||||
const obj = { a: 3, b: { c: 4 } };
|
||||
expect(mapValue(obj, predicate, processor)).toEqual(obj);
|
||||
});
|
||||
|
||||
it('should process object that matches the predicate', () => {
|
||||
const obj = { a: { process: true } };
|
||||
expect(mapValue(obj, predicate, processor)).toEqual({
|
||||
a: { process: true, processed: true, paths: ['a'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested objects and arrays with various types of predicates', () => {
|
||||
const complexObj = {
|
||||
a: { key: 'value' },
|
||||
b: [{ key: 'value' }, undefined, null, 0, false],
|
||||
c: () => {},
|
||||
};
|
||||
const truthyPredicate = (obj: any) => 'key' in obj && obj.key === 'value';
|
||||
const falsePredicate = (obj: any) => false;
|
||||
|
||||
expect(mapValue(complexObj, truthyPredicate, processor)).toEqual({
|
||||
a: { key: 'value', processed: true, paths: ['a'] },
|
||||
b: [{ key: 'value', processed: true, paths: ['b', 0] }, undefined, null, 0, false],
|
||||
c: complexObj.c,
|
||||
});
|
||||
|
||||
expect(mapValue(complexObj, falsePredicate, processor)).toEqual(complexObj);
|
||||
});
|
||||
|
||||
it('should process nested object and arrays that match the predicate', () => {
|
||||
const nestedObj = {
|
||||
a: { key: 'value', nested: { key: 'value' } },
|
||||
};
|
||||
const predicate = (obj: any) => 'key' in obj;
|
||||
|
||||
const result = mapValue(nestedObj, predicate, processor);
|
||||
|
||||
expect(result).toEqual({
|
||||
a: { key: 'value', processed: true, paths: ['a'], nested: { key: 'value' } },
|
||||
});
|
||||
expect(result.a.nested).not.toHaveProperty('processed');
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
/* --------------- api -------------------- */
|
||||
export { createRenderer } from './main';
|
||||
export { IBoostsService, IExtensionHostService } from './services/extension';
|
||||
export { IExtensionHostService } from './services/extension';
|
||||
export { definePackageLoader, IPackageManagementService } from './services/package';
|
||||
export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService';
|
||||
export { IComponentTreeModelService } from './services/model';
|
||||
|
||||
@ -1,54 +1,78 @@
|
||||
import { invariant, InstantiationService } from '@alilc/lowcode-shared';
|
||||
import { ICodeRuntimeService } from './services/code-runtime';
|
||||
import type { AppOptions, RendererApplication } from './types';
|
||||
import { CodeRuntimeService, ICodeRuntimeService } from './services/code-runtime';
|
||||
import {
|
||||
IExtensionHostService,
|
||||
type RenderAdapter,
|
||||
type IRenderObject,
|
||||
ExtensionHostService,
|
||||
} from './services/extension';
|
||||
import { IPackageManagementService } from './services/package';
|
||||
import { ISchemaService } from './services/schema';
|
||||
import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService';
|
||||
import type { AppOptions, RendererApplication } from './types';
|
||||
import { IPackageManagementService, PackageManagementService } from './services/package';
|
||||
import { ISchemaService, SchemaService } from './services/schema';
|
||||
import { ILifeCycleService, LifecyclePhase, LifeCycleService } from './services/lifeCycleService';
|
||||
import { IRuntimeIntlService, RuntimeIntlService } from './services/runtimeIntlService';
|
||||
import { IRuntimeUtilService, RuntimeUtilService } from './services/runtimeUtilService';
|
||||
|
||||
/**
|
||||
* 创建 createRenderer 的辅助函数
|
||||
* @param schema
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function createRenderer<RenderObject = IRenderObject>(
|
||||
renderAdapter: RenderAdapter<RenderObject>,
|
||||
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
|
||||
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
|
||||
|
||||
const accessor = new InstantiationService({ defaultScope: 'Singleton' });
|
||||
let mode: 'development' | 'production' = 'production';
|
||||
const instantiationService = new InstantiationService();
|
||||
|
||||
const schemaService = accessor.get(ISchemaService);
|
||||
const packageManagementService = accessor.get(IPackageManagementService);
|
||||
const codeRuntimeService = accessor.get(ICodeRuntimeService);
|
||||
const lifeCycleService = accessor.get(ILifeCycleService);
|
||||
const extensionHostService = accessor.get(IExtensionHostService);
|
||||
// create services
|
||||
const lifeCycleService = new LifeCycleService();
|
||||
instantiationService.container.set(ILifeCycleService, lifeCycleService);
|
||||
|
||||
return async (options) => {
|
||||
if (options.mode) mode = options.mode;
|
||||
const schemaService = new SchemaService(options.schema);
|
||||
instantiationService.container.set(ISchemaService, schemaService);
|
||||
|
||||
// valid schema
|
||||
schemaService.initialize(options.schema);
|
||||
codeRuntimeService.initialize(options.codeRuntime ?? {});
|
||||
await lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
|
||||
const codeRuntimeService = instantiationService.createInstance(
|
||||
CodeRuntimeService,
|
||||
options.codeRuntime,
|
||||
);
|
||||
instantiationService.container.set(ICodeRuntimeService, codeRuntimeService);
|
||||
|
||||
const renderObject = await renderAdapter(accessor);
|
||||
const packageManagementService = instantiationService.createInstance(PackageManagementService);
|
||||
instantiationService.container.set(IPackageManagementService, packageManagementService);
|
||||
|
||||
const utils = schemaService.get('utils');
|
||||
const runtimeUtilService = instantiationService.createInstance(RuntimeUtilService, utils);
|
||||
instantiationService.container.set(IRuntimeUtilService, runtimeUtilService);
|
||||
|
||||
const defaultLocale = schemaService.get('config.defaultLocale');
|
||||
const i18ns = schemaService.get('i18n', {});
|
||||
const runtimeIntlService = instantiationService.createInstance(
|
||||
RuntimeIntlService,
|
||||
defaultLocale,
|
||||
i18ns,
|
||||
);
|
||||
instantiationService.container.set(IRuntimeIntlService, runtimeIntlService);
|
||||
|
||||
const extensionHostService = new ExtensionHostService(
|
||||
lifeCycleService,
|
||||
packageManagementService,
|
||||
schemaService,
|
||||
codeRuntimeService,
|
||||
runtimeIntlService,
|
||||
runtimeUtilService,
|
||||
);
|
||||
instantiationService.container.set(IExtensionHostService, extensionHostService);
|
||||
|
||||
lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
|
||||
|
||||
const renderObject = await renderAdapter(instantiationService);
|
||||
|
||||
await extensionHostService.registerPlugin(options.plugins ?? []);
|
||||
// 先加载插件提供 package loader
|
||||
await packageManagementService.loadPackages(options.packages ?? []);
|
||||
|
||||
await lifeCycleService.setPhase(LifecyclePhase.Ready);
|
||||
lifeCycleService.setPhase(LifecyclePhase.Ready);
|
||||
|
||||
const app: RendererApplication<RenderObject> = {
|
||||
get mode() {
|
||||
return mode;
|
||||
return __DEV__ ? 'development' : 'production';
|
||||
},
|
||||
schema: schemaService,
|
||||
packageManager: packageManagementService,
|
||||
@ -57,12 +81,12 @@ export function createRenderer<RenderObject = IRenderObject>(
|
||||
use: (plugin) => {
|
||||
return extensionHostService.registerPlugin(plugin);
|
||||
},
|
||||
destroy: async () => {
|
||||
return lifeCycleService.setPhase(LifecyclePhase.Destroying);
|
||||
destroy: () => {
|
||||
lifeCycleService.setPhase(LifecyclePhase.Destroying);
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
if (__DEV__) {
|
||||
Object.defineProperty(app, '__options', { get: () => options });
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { createDecorator, invariant, Provide, type StringDictionary } from '@alilc/lowcode-shared';
|
||||
import {
|
||||
createDecorator,
|
||||
invariant,
|
||||
Disposable,
|
||||
type StringDictionary,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime';
|
||||
import { ISchemaService } from '../schema';
|
||||
|
||||
export interface ICodeRuntimeService {
|
||||
readonly rootRuntime: ICodeRuntime;
|
||||
|
||||
initialize(options: CodeRuntimeOptions): void;
|
||||
|
||||
createCodeRuntime<T extends StringDictionary = StringDictionary>(
|
||||
options?: CodeRuntimeOptions<T>,
|
||||
): ICodeRuntime<T>;
|
||||
@ -13,12 +17,23 @@ export interface ICodeRuntimeService {
|
||||
|
||||
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
|
||||
|
||||
@Provide(ICodeRuntimeService)
|
||||
export class CodeRuntimeService implements ICodeRuntimeService {
|
||||
export class CodeRuntimeService extends Disposable implements ICodeRuntimeService {
|
||||
rootRuntime: ICodeRuntime;
|
||||
|
||||
initialize(options?: CodeRuntimeOptions) {
|
||||
constructor(
|
||||
options: CodeRuntimeOptions = {},
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
) {
|
||||
super();
|
||||
this.rootRuntime = new CodeRuntime(options);
|
||||
|
||||
this.addDispose(
|
||||
this.schemaService.onSchemaUpdate(({ key, data }) => {
|
||||
if (key === 'constants') {
|
||||
this.rootRuntime.getScope().set('constants', data);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
createCodeRuntime<T extends StringDictionary = StringDictionary>(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createDecorator, Provide, type StringDictionary } from '@alilc/lowcode-shared';
|
||||
import { type StringDictionary } from '@alilc/lowcode-shared';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
|
||||
import { IRuntimeUtilService } from '../runtimeUtilService';
|
||||
@ -24,17 +24,14 @@ export interface IBoostsApi {
|
||||
/**
|
||||
* 提供了与运行时交互的接口
|
||||
*/
|
||||
export interface IBoostsService {
|
||||
export interface IBoostsManager {
|
||||
extend(name: string, value: any, force?: boolean): void;
|
||||
extend(value: StringDictionary, force?: boolean): void;
|
||||
|
||||
toExpose<Extends>(): IBoosts<Extends>;
|
||||
}
|
||||
|
||||
export const IBoostsService = createDecorator<IBoostsService>('boostsService');
|
||||
|
||||
@Provide(IBoostsService)
|
||||
export class BoostsService implements IBoostsService {
|
||||
export class BoostsManager implements IBoostsManager {
|
||||
private builtInApis: IBoostsApi;
|
||||
|
||||
private extendsValue: StringDictionary = {};
|
||||
@ -42,16 +39,16 @@ export class BoostsService implements IBoostsService {
|
||||
private _expose: any;
|
||||
|
||||
constructor(
|
||||
@ICodeRuntimeService codeRuntimeService: ICodeRuntimeService,
|
||||
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
||||
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
||||
codeRuntimeService: ICodeRuntimeService,
|
||||
runtimeIntlService: IRuntimeIntlService,
|
||||
runtimeUtilService: IRuntimeUtilService,
|
||||
) {
|
||||
this.builtInApis = {
|
||||
get codeRuntime() {
|
||||
return codeRuntimeService.rootRuntime;
|
||||
},
|
||||
intl: this.runtimeIntlService,
|
||||
util: this.runtimeUtilService,
|
||||
intl: runtimeIntlService,
|
||||
util: runtimeUtilService,
|
||||
temporaryUse: (name, value) => {
|
||||
this.extend(name, value);
|
||||
},
|
||||
|
||||
@ -1,102 +1,112 @@
|
||||
import { createDecorator, Provide, EventEmitter } from '@alilc/lowcode-shared';
|
||||
import { createDecorator, CyclicDependencyError, Disposable, Graph } from '@alilc/lowcode-shared';
|
||||
import { type Plugin, type PluginContext } from './plugin';
|
||||
import { IBoostsService } from './boosts';
|
||||
import { BoostsManager } from './boosts';
|
||||
import { IPackageManagementService } from '../package';
|
||||
import { ISchemaService } from '../schema';
|
||||
import { ILifeCycleService } from '../lifeCycleService';
|
||||
import { KeyValueStore } from '../../utils/store';
|
||||
|
||||
interface IPluginRuntime extends Plugin {
|
||||
status: 'setup' | 'ready';
|
||||
}
|
||||
import { ICodeRuntimeService } from '../code-runtime';
|
||||
import { IRuntimeIntlService } from '../runtimeIntlService';
|
||||
import { IRuntimeUtilService } from '../runtimeUtilService';
|
||||
|
||||
export interface IExtensionHostService {
|
||||
readonly boostsManager: BoostsManager;
|
||||
|
||||
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
||||
|
||||
getPlugin(name: string): Plugin | undefined;
|
||||
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
export const IExtensionHostService =
|
||||
createDecorator<IExtensionHostService>('pluginManagementService');
|
||||
|
||||
@Provide(IExtensionHostService)
|
||||
export class ExtensionHostService implements IExtensionHostService {
|
||||
private pluginRuntimes: IPluginRuntime[] = [];
|
||||
export class ExtensionHostService extends Disposable implements IExtensionHostService {
|
||||
boostsManager: BoostsManager;
|
||||
|
||||
private eventEmitter: EventEmitter;
|
||||
|
||||
private pluginSetupContext: PluginContext;
|
||||
private _activePlugins = new Set<string>();
|
||||
private _pluginStore = new Map<string, Plugin>();
|
||||
private _pluginDependencyGraph = new Graph<string>((name) => name);
|
||||
private _pluginSetupContext: PluginContext;
|
||||
|
||||
constructor(
|
||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||
@IBoostsService private boostsService: IBoostsService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
lifeCycleService: ILifeCycleService,
|
||||
packageManagementService: IPackageManagementService,
|
||||
schemaService: ISchemaService,
|
||||
codeRuntimeService: ICodeRuntimeService,
|
||||
runtimeIntlService: IRuntimeIntlService,
|
||||
runtimeUtilService: IRuntimeUtilService,
|
||||
) {
|
||||
this.eventEmitter = new EventEmitter('ExtensionHost');
|
||||
this.pluginSetupContext = {
|
||||
eventEmitter: this.eventEmitter,
|
||||
globalState: new KeyValueStore(),
|
||||
boosts: this.boostsService.toExpose(),
|
||||
schema: this.schemaService,
|
||||
packageManager: this.packageManagementService,
|
||||
super();
|
||||
|
||||
whenLifeCylePhaseChange: (phase, listener) => {
|
||||
return this.lifeCycleService.when(phase, listener);
|
||||
this.boostsManager = new BoostsManager(
|
||||
codeRuntimeService,
|
||||
runtimeIntlService,
|
||||
runtimeUtilService,
|
||||
);
|
||||
|
||||
this._pluginSetupContext = {
|
||||
globalState: new Map(),
|
||||
boosts: this.boostsManager.toExpose(),
|
||||
schema: schemaService,
|
||||
packageManager: packageManagementService,
|
||||
|
||||
whenLifeCylePhaseChange: (phase) => {
|
||||
return lifeCycleService.when(phase);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async registerPlugin(plugins: Plugin | Plugin[]) {
|
||||
plugins = Array.isArray(plugins) ? plugins : [plugins];
|
||||
const items = (Array.isArray(plugins) ? plugins : [plugins]).filter(
|
||||
(plugin) => !this._pluginStore.has(plugin.name),
|
||||
);
|
||||
|
||||
for (const item of items) {
|
||||
this._pluginStore.set(item.name, item);
|
||||
}
|
||||
|
||||
await this._doRegisterPlugins(items);
|
||||
}
|
||||
|
||||
private async _doRegisterPlugins(plugins: Plugin[]) {
|
||||
for (const plugin of plugins) {
|
||||
if (this.pluginRuntimes.find((item) => item.name === plugin.name)) {
|
||||
console.warn(`${plugin.name} 插件已注册`);
|
||||
continue;
|
||||
this._pluginDependencyGraph.lookupOrInsertNode(plugin.name);
|
||||
|
||||
if (plugin.dependsOn) {
|
||||
for (const dependency of plugin.dependsOn) {
|
||||
this._pluginDependencyGraph.insertEdge(plugin.name, dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const roots = this._pluginDependencyGraph.roots();
|
||||
|
||||
if (roots.length === 0 || roots.every((node) => !this._pluginStore.has(node.data))) {
|
||||
if (this._pluginDependencyGraph.isEmpty()) {
|
||||
throw new CyclicDependencyError(this._pluginDependencyGraph);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const pluginRuntime = plugin as IPluginRuntime;
|
||||
|
||||
pluginRuntime.status = 'ready';
|
||||
this.pluginRuntimes.push(pluginRuntime);
|
||||
|
||||
await this.doSetupPlugin(pluginRuntime);
|
||||
for (const { data } of roots) {
|
||||
const plugin = this._pluginStore.get(data);
|
||||
if (plugin) {
|
||||
await this._doSetupPlugin(plugin);
|
||||
this._pluginDependencyGraph.removeNode(plugin.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doSetupPlugin(pluginRuntime: IPluginRuntime) {
|
||||
if (pluginRuntime.status === 'setup') return;
|
||||
private async _doSetupPlugin(plugin: Plugin) {
|
||||
if (this._activePlugins.has(plugin.name)) return;
|
||||
|
||||
const isSetup = (name: string) => {
|
||||
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');
|
||||
return setupPlugins.some((p) => p.name === name);
|
||||
};
|
||||
|
||||
if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginRuntime.setup(this.pluginSetupContext);
|
||||
pluginRuntime.status = 'setup';
|
||||
|
||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
||||
const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready');
|
||||
const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep)));
|
||||
if (readyPlugin) {
|
||||
await this.doSetupPlugin(readyPlugin);
|
||||
}
|
||||
await plugin.setup(this._pluginSetupContext);
|
||||
this._activePlugins.add(plugin.name);
|
||||
this.addDispose(plugin);
|
||||
}
|
||||
|
||||
getPlugin(name: string): Plugin | undefined {
|
||||
return this.pluginRuntimes.find((item) => item.name === name);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const plugin of this.pluginRuntimes) {
|
||||
await plugin.destory?.();
|
||||
}
|
||||
return this._pluginStore.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type EventEmitter, type StringDictionary } from '@alilc/lowcode-shared';
|
||||
import { type StringDictionary, type IDisposable } from '@alilc/lowcode-shared';
|
||||
import { type IBoosts } from './boosts';
|
||||
import { ILifeCycleService } from '../lifeCycleService';
|
||||
import { type ISchemaService } from '../schema';
|
||||
@ -6,23 +6,29 @@ import { type IPackageManagementService } from '../package';
|
||||
import { type IStore } from '../../utils/store';
|
||||
|
||||
export interface PluginContext<BoostsExtends = object> {
|
||||
eventEmitter: EventEmitter;
|
||||
globalState: IStore<StringDictionary, string>;
|
||||
|
||||
boosts: IBoosts<BoostsExtends>;
|
||||
|
||||
schema: Pick<ISchemaService, 'get' | 'set'>;
|
||||
|
||||
packageManager: IPackageManagementService;
|
||||
/**
|
||||
* 生命周期变更事件
|
||||
*/
|
||||
|
||||
whenLifeCylePhaseChange: ILifeCycleService['when'];
|
||||
}
|
||||
|
||||
export interface Plugin<BoostsExtends = object> {
|
||||
export interface Plugin<BoostsExtends = object> extends IDisposable {
|
||||
/**
|
||||
* 插件的 name 作为唯一标识,并不可重复。
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 插件启动函数
|
||||
* @param context 插件能力上下文
|
||||
*/
|
||||
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
|
||||
destory?(): void | Promise<void>;
|
||||
/**
|
||||
* 插件的依赖插件
|
||||
*/
|
||||
dependsOn?: string[];
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type InstanceAccessor } from '@alilc/lowcode-shared';
|
||||
import { type IInstantiationService } from '@alilc/lowcode-shared';
|
||||
|
||||
export interface IRenderObject {
|
||||
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
|
||||
@ -6,5 +6,5 @@ export interface IRenderObject {
|
||||
}
|
||||
|
||||
export interface RenderAdapter<Render> {
|
||||
(accessor: InstanceAccessor): Render | Promise<Render>;
|
||||
(instantiationService: IInstantiationService): Render | Promise<Render>;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Provide, createDecorator, EventEmitter, EventDisposable } from '@alilc/lowcode-shared';
|
||||
import { createDecorator, Barrier } from '@alilc/lowcode-shared';
|
||||
|
||||
/**
|
||||
* 生命周期阶段
|
||||
@ -28,13 +28,15 @@ export interface ILifeCycleService {
|
||||
*/
|
||||
phase: LifecyclePhase;
|
||||
|
||||
setPhase(phase: LifecyclePhase): Promise<void>;
|
||||
setPhase(phase: LifecyclePhase): void;
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when a certain lifecycle phase
|
||||
* has started.
|
||||
*/
|
||||
when(phase: LifecyclePhase, listener: () => void | Promise<void>): EventDisposable;
|
||||
when(phase: LifecyclePhase): Promise<void>;
|
||||
|
||||
onWillDestory(): void;
|
||||
}
|
||||
|
||||
export function LifecyclePhaseToString(phase: LifecyclePhase): string {
|
||||
@ -52,9 +54,8 @@ export function LifecyclePhaseToString(phase: LifecyclePhase): string {
|
||||
|
||||
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
|
||||
|
||||
@Provide(ILifeCycleService)
|
||||
export class LifeCycleService implements ILifeCycleService {
|
||||
private readonly phaseWhen = new EventEmitter();
|
||||
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
|
||||
|
||||
private _phase = LifecyclePhase.Starting;
|
||||
|
||||
@ -62,7 +63,7 @@ export class LifeCycleService implements ILifeCycleService {
|
||||
return this._phase;
|
||||
}
|
||||
|
||||
async setPhase(value: LifecyclePhase) {
|
||||
setPhase(value: LifecyclePhase) {
|
||||
if (value < this._phase) {
|
||||
throw new Error('Lifecycle cannot go backwards');
|
||||
}
|
||||
@ -73,10 +74,26 @@ export class LifeCycleService implements ILifeCycleService {
|
||||
|
||||
this._phase = value;
|
||||
|
||||
await this.phaseWhen.emit(LifecyclePhaseToString(value));
|
||||
const barrier = this.phaseWhen.get(this._phase);
|
||||
if (barrier) {
|
||||
barrier.open();
|
||||
this.phaseWhen.delete(this._phase);
|
||||
}
|
||||
}
|
||||
|
||||
when(phase: LifecyclePhase, listener: () => void | Promise<void>) {
|
||||
return this.phaseWhen.on(LifecyclePhaseToString(phase), listener);
|
||||
async when(phase: LifecyclePhase): Promise<void> {
|
||||
if (phase <= this._phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
let barrier = this.phaseWhen.get(phase);
|
||||
if (!barrier) {
|
||||
barrier = new Barrier();
|
||||
this.phaseWhen.set(phase, barrier);
|
||||
}
|
||||
|
||||
await barrier.wait();
|
||||
}
|
||||
|
||||
onWillDestory(): void {}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
createDecorator,
|
||||
Provide,
|
||||
invariant,
|
||||
type ComponentTree,
|
||||
type StringDictionary,
|
||||
@ -33,7 +32,6 @@ export const IComponentTreeModelService = createDecorator<IComponentTreeModelSer
|
||||
'componentTreeModelService',
|
||||
);
|
||||
|
||||
@Provide(IComponentTreeModelService)
|
||||
export class ComponentTreeModelService implements IComponentTreeModelService {
|
||||
constructor(
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './loader';
|
||||
export * from './package';
|
||||
export * from './managementService';
|
||||
|
||||
@ -4,16 +4,15 @@ import {
|
||||
type ProCodeComponent,
|
||||
type ComponentMap,
|
||||
createDecorator,
|
||||
Provide,
|
||||
specTypes,
|
||||
exportByReference,
|
||||
mapPackageToUniqueId,
|
||||
type Reference,
|
||||
Disposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { get as lodashGet } from 'lodash-es';
|
||||
import { PackageLoader } from './loader';
|
||||
import { get as lodashGet, differenceWith, isEqual } from 'lodash-es';
|
||||
import { PackageLoader } from './package';
|
||||
import { ISchemaService } from '../schema';
|
||||
import { ILifeCycleService } from '../lifeCycleService';
|
||||
|
||||
export interface NormalizedPackage {
|
||||
id: string;
|
||||
@ -50,8 +49,7 @@ export const IPackageManagementService = createDecorator<IPackageManagementServi
|
||||
'packageManagementService',
|
||||
);
|
||||
|
||||
@Provide(IPackageManagementService)
|
||||
export class PackageManagementService implements IPackageManagementService {
|
||||
export class PackageManagementService extends Disposable implements IPackageManagementService {
|
||||
private componentsRecord: Record<string, any> = {};
|
||||
|
||||
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
||||
@ -62,13 +60,18 @@ export class PackageManagementService implements IPackageManagementService {
|
||||
|
||||
private packageLoaders: PackageLoader[] = [];
|
||||
|
||||
constructor(
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
) {
|
||||
this.schemaService.onChange('componentsMap', (componentsMaps) => {
|
||||
this.resolveComponentMaps(componentsMaps);
|
||||
});
|
||||
constructor(@ISchemaService private schemaService: ISchemaService) {
|
||||
super();
|
||||
|
||||
this.addDispose(
|
||||
this.schemaService.onSchemaUpdate(({ key, previous, data }) => {
|
||||
if (key === 'componentsMap') {
|
||||
// todo: add remove ...
|
||||
const diff = differenceWith(data, previous, isEqual);
|
||||
if (diff.length > 0) this.resolveComponentMaps(diff);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async loadPackages(packages: Package[]) {
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
import {
|
||||
createDecorator,
|
||||
Provide,
|
||||
Intl,
|
||||
type IntlApi,
|
||||
type Locale,
|
||||
type Translations,
|
||||
Platform,
|
||||
type LocaleTranslationsMap,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
||||
import { ICodeRuntimeService } from './code-runtime';
|
||||
import { ISchemaService } from './schema';
|
||||
|
||||
export interface MessageDescriptor {
|
||||
key: string;
|
||||
@ -18,8 +15,6 @@ export interface MessageDescriptor {
|
||||
}
|
||||
|
||||
export interface IRuntimeIntlService {
|
||||
locale: string;
|
||||
|
||||
localize(descriptor: MessageDescriptor): string;
|
||||
|
||||
setLocale(locale: Locale): void;
|
||||
@ -31,32 +26,21 @@ export interface IRuntimeIntlService {
|
||||
|
||||
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
|
||||
|
||||
@Provide(IRuntimeIntlService)
|
||||
export class RuntimeIntlService implements IRuntimeIntlService {
|
||||
private intl: Intl = new Intl();
|
||||
|
||||
public locale: string = Platform.platformLocale;
|
||||
|
||||
constructor(
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
defaultLocale: string | undefined,
|
||||
i18nTranslations: LocaleTranslationsMap,
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
|
||||
const config = this.schemaService.get('config');
|
||||
const i18nTranslations = this.schemaService.get('i18n');
|
||||
if (defaultLocale) this.setLocale(defaultLocale);
|
||||
|
||||
if (config?.defaultLocale) {
|
||||
this.setLocale(config.defaultLocale);
|
||||
}
|
||||
if (i18nTranslations) {
|
||||
Object.keys(i18nTranslations).forEach((key) => {
|
||||
this.addTranslations(key, i18nTranslations[key]);
|
||||
});
|
||||
}
|
||||
for (const key of Object.keys(i18nTranslations)) {
|
||||
this.addTranslations(key, i18nTranslations[key]);
|
||||
}
|
||||
|
||||
this.injectScope();
|
||||
});
|
||||
this._injectScope();
|
||||
}
|
||||
|
||||
localize(descriptor: MessageDescriptor): string {
|
||||
@ -83,7 +67,7 @@ export class RuntimeIntlService implements IRuntimeIntlService {
|
||||
this.intl.addTranslations(locale, translations);
|
||||
}
|
||||
|
||||
private injectScope(): void {
|
||||
private _injectScope(): void {
|
||||
const exposed: IntlApi = {
|
||||
i18n: (key, params) => {
|
||||
return this.localize({ key, params });
|
||||
|
||||
@ -2,14 +2,11 @@ import {
|
||||
type AnyFunction,
|
||||
type UtilDescription,
|
||||
createDecorator,
|
||||
Provide,
|
||||
type StringDictionary,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { isPlainObject } from 'lodash-es';
|
||||
import { IPackageManagementService } from './package';
|
||||
import { ICodeRuntimeService } from './code-runtime';
|
||||
import { ISchemaService } from './schema';
|
||||
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
||||
|
||||
export interface IRuntimeUtilService {
|
||||
add(utilItem: UtilDescription, force?: boolean): void;
|
||||
@ -20,25 +17,18 @@ export interface IRuntimeUtilService {
|
||||
|
||||
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
||||
|
||||
@Provide(IRuntimeUtilService)
|
||||
export class RuntimeUtilService implements IRuntimeUtilService {
|
||||
private utilsMap: Map<string, any> = new Map();
|
||||
|
||||
constructor(
|
||||
utils: UtilDescription[] = [],
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
||||
@ISchemaService private schemaService: ISchemaService,
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
) {
|
||||
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
|
||||
this.injectScope();
|
||||
});
|
||||
|
||||
this.schemaService.onChange('utils', (utils = []) => {
|
||||
for (const util of utils) {
|
||||
this.add(util);
|
||||
}
|
||||
});
|
||||
for (const util of utils) {
|
||||
this.add(util);
|
||||
}
|
||||
this.injectScope();
|
||||
}
|
||||
|
||||
add(utilItem: UtilDescription, force?: boolean): void;
|
||||
|
||||
@ -1,85 +1,63 @@
|
||||
import {
|
||||
type Project,
|
||||
createDecorator,
|
||||
Provide,
|
||||
EventEmitter,
|
||||
type EventDisposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { Disposable, type Project, createDecorator, Events } from '@alilc/lowcode-shared';
|
||||
import { isObject, isEqual, get as lodashGet, set as lodashSet } from 'lodash-es';
|
||||
import { schemaValidation } from './validation';
|
||||
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
|
||||
import { ICodeRuntimeService } from '../code-runtime';
|
||||
import { type IStore, KeyValueStore } from '../../utils/store';
|
||||
|
||||
export interface NormalizedSchema extends Project {}
|
||||
|
||||
export type NormalizedSchemaKey = keyof NormalizedSchema;
|
||||
|
||||
export type SchemaUpdateEvent = { key: string; previous: any; data: any };
|
||||
|
||||
export interface ISchemaService {
|
||||
initialize(schema: Project): void;
|
||||
readonly onSchemaUpdate: Events.Event<SchemaUpdateEvent>;
|
||||
|
||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
|
||||
get<T>(key: string): T | undefined;
|
||||
get<T>(key: string, defaultValue?: T): T;
|
||||
|
||||
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void>;
|
||||
|
||||
onChange<K extends NormalizedSchemaKey>(
|
||||
key: K,
|
||||
listener: (v: NormalizedSchema[K]) => void,
|
||||
): EventDisposable;
|
||||
set(key: string, value: any): void;
|
||||
}
|
||||
|
||||
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
||||
|
||||
@Provide(ISchemaService)
|
||||
export class SchemaService implements ISchemaService {
|
||||
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
|
||||
NormalizedSchema,
|
||||
NormalizedSchemaKey
|
||||
>({
|
||||
setterValidation: schemaValidation,
|
||||
});
|
||||
export class SchemaService extends Disposable implements ISchemaService {
|
||||
private store: NormalizedSchema;
|
||||
|
||||
private notifyEmiiter = new EventEmitter();
|
||||
private _observer = this.addDispose(new Events.Observable<SchemaUpdateEvent>());
|
||||
|
||||
constructor(
|
||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||
) {
|
||||
this.onChange('constants', (value = {}) => {
|
||||
this.codeRuntimeService.rootRuntime.getScope().set('constants', value);
|
||||
});
|
||||
readonly onSchemaUpdate = this._observer.subscribe;
|
||||
|
||||
this.lifeCycleService.when(LifecyclePhase.Destroying, () => {
|
||||
this.notifyEmiiter.removeAll();
|
||||
});
|
||||
}
|
||||
constructor(schema: unknown) {
|
||||
super();
|
||||
|
||||
initialize(schema: unknown): void {
|
||||
if (!isObject(schema)) {
|
||||
throw Error('schema must a object');
|
||||
}
|
||||
|
||||
Object.keys(schema).forEach((key) => {
|
||||
// @ts-expect-error: ignore initialization
|
||||
this.set(key, schema[key]);
|
||||
});
|
||||
}
|
||||
this.store = {} as any;
|
||||
for (const key of Object.keys(schema)) {
|
||||
const value = (schema as any)[key];
|
||||
|
||||
async set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void> {
|
||||
if (value !== this.get(key)) {
|
||||
this.store.set(key, value);
|
||||
await this.notifyEmiiter.emit(key, value);
|
||||
// todo: schemas validate
|
||||
const valid = schemaValidation(key as any, value);
|
||||
if (valid !== true) {
|
||||
throw new Error(
|
||||
`failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
||||
return this.store.get(key) as NormalizedSchema[K];
|
||||
set(key: string, value: any): void {
|
||||
const previous = this.get(key);
|
||||
if (!isEqual(previous, value)) {
|
||||
lodashSet(this.store, key, value);
|
||||
this._observer.notify({ key, previous, data: value });
|
||||
}
|
||||
}
|
||||
|
||||
onChange<K extends keyof NormalizedSchema>(
|
||||
key: K,
|
||||
listener: (v: NormalizedSchema[K]) => void | Promise<void>,
|
||||
): EventDisposable {
|
||||
return this.notifyEmiiter.on(key, listener);
|
||||
get<T>(key: string, defaultValue?: T): T {
|
||||
return (lodashGet(this.store, key) ?? defaultValue) as T;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,12 +38,12 @@ const SCHEMA_VALIDATIONS_OPTIONS: Partial<ValidOptionRecord> = {
|
||||
},
|
||||
};
|
||||
|
||||
export function schemaValidation<K extends keyof Project>(key: K, value: Project[K]) {
|
||||
export function schemaValidation(key: string, value: unknown) {
|
||||
if (!SCHEMA_KEYS.includes(key)) {
|
||||
return `schema 的字段名必须是${JSON.stringify(SCHEMA_KEYS)}中的一个`;
|
||||
}
|
||||
|
||||
const validOption = SCHEMA_VALIDATIONS_OPTIONS[key];
|
||||
const validOption = (SCHEMA_VALIDATIONS_OPTIONS as any)[key];
|
||||
|
||||
if (validOption) {
|
||||
const result = validOption.valid(value);
|
||||
|
||||
@ -9,10 +9,6 @@ export interface AppOptions {
|
||||
schema: Project;
|
||||
packages?: Package[];
|
||||
plugins?: Plugin[];
|
||||
/**
|
||||
* 运行模式
|
||||
*/
|
||||
mode?: 'development' | 'production';
|
||||
/**
|
||||
* code runtime 设置选项
|
||||
*/
|
||||
@ -32,5 +28,5 @@ export type RendererApplication<Render = unknown> = {
|
||||
|
||||
use(plugin: Plugin): Promise<void>;
|
||||
|
||||
destroy(): Promise<void>;
|
||||
destroy(): void;
|
||||
} & Render;
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { type StringDictionary } from '@alilc/lowcode-shared';
|
||||
|
||||
/**
|
||||
* MapLike interface
|
||||
*/
|
||||
export interface IStore<O, K extends keyof O> {
|
||||
readonly size: number;
|
||||
|
||||
get(key: K, defaultValue: O[K]): O[K];
|
||||
get(key: K, defaultValue?: O[K]): O[K] | undefined;
|
||||
|
||||
set(key: K, value: O[K]): void;
|
||||
|
||||
delete(key: K): void;
|
||||
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export class KeyValueStore<O = StringDictionary, K extends keyof O = keyof O>
|
||||
implements IStore<O, K>
|
||||
{
|
||||
private readonly store = new Map();
|
||||
|
||||
private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
|
||||
|
||||
constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) {
|
||||
if (options?.setterValidation) {
|
||||
this.setterValidation = options.setterValidation;
|
||||
}
|
||||
}
|
||||
|
||||
get(key: K, defaultValue: O[K]): O[K];
|
||||
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
|
||||
get(key: K, defaultValue?: O[K]): O[K] | undefined {
|
||||
const value = this.store.get(key);
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
set(key: K, value: O[K]): void {
|
||||
if (this.setterValidation) {
|
||||
const valid = this.setterValidation(key, value);
|
||||
|
||||
if (valid !== true) {
|
||||
console.warn(`failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: K): void {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.store.size;
|
||||
}
|
||||
}
|
||||
@ -3,5 +3,4 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "../shared/src/utils/node.ts"]
|
||||
}
|
||||
|
||||
@ -2,6 +2,5 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Barrier } from '../../src';
|
||||
|
||||
describe('Barrier', () => {
|
||||
it('waits for barrier to open', async () => {
|
||||
const barrier = new Barrier();
|
||||
|
||||
setTimeout(() => {
|
||||
barrier.open();
|
||||
}, 500); // Simulate opening the barrier after 500ms
|
||||
|
||||
const start = Date.now();
|
||||
await barrier.wait(); // Async operation should await here
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(barrier.isOpen()).toBeTruthy();
|
||||
expect(duration).toBeGreaterThanOrEqual(500); // Ensures we waited for at least 500ms
|
||||
});
|
||||
|
||||
it('mutiple', async () => {
|
||||
let result = '';
|
||||
const b1 = new Barrier();
|
||||
const b2 = new Barrier();
|
||||
|
||||
async function run1() {
|
||||
await b1.wait();
|
||||
}
|
||||
async function run2() {
|
||||
await b2.wait();
|
||||
}
|
||||
|
||||
run1().then(() => {
|
||||
result += 1;
|
||||
});
|
||||
run1().finally(() => {
|
||||
result += 2;
|
||||
});
|
||||
|
||||
run2().then(() => {
|
||||
result += 3;
|
||||
});
|
||||
run2().finally(() => {
|
||||
result += 4;
|
||||
});
|
||||
|
||||
b1.open();
|
||||
|
||||
await run1();
|
||||
|
||||
b2.open();
|
||||
|
||||
await run2();
|
||||
|
||||
expect(result).toBe('1234');
|
||||
});
|
||||
});
|
||||
@ -24,12 +24,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abraham/reflection": "^0.12.0",
|
||||
"@formatjs/intl": "^2.10.2",
|
||||
"@vue/reactivity": "^3.4.23",
|
||||
"inversify": "^6.0.2",
|
||||
"inversify-binding-decorators": "^4.0.0",
|
||||
"hookable": "^5.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"store": "^2.0.12"
|
||||
},
|
||||
|
||||
208
packages/shared/src/common/disposable.ts
Normal file
208
packages/shared/src/common/disposable.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { createSingleCallFunction, Iterable } from '../utils';
|
||||
|
||||
/**
|
||||
* An object that performs a cleanup operation when `.dispose()` is called.
|
||||
*
|
||||
* Some examples of how disposables are used:
|
||||
*
|
||||
* - An event listener that removes itself when `.dispose()` is called.
|
||||
* - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called.
|
||||
* - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `thing` is {@link IDisposable disposable}.
|
||||
*/
|
||||
export function isDisposable<E>(thing: E): thing is E & IDisposable {
|
||||
return (
|
||||
typeof thing === 'object' &&
|
||||
thing !== null &&
|
||||
typeof (thing as unknown as IDisposable).dispose === 'function' &&
|
||||
(thing as unknown as IDisposable).dispose.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export abstract class Disposable implements IDisposable {
|
||||
static Noop: IDisposable = { dispose: noop };
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `o` to the collection of disposables managed by this object.
|
||||
*/
|
||||
protected addDispose<T extends IDisposable>(o: T): T {
|
||||
if ((o as unknown as Disposable) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
return this._store.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a collection of disposable values.
|
||||
*
|
||||
* This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
|
||||
* `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
|
||||
* store that has already been disposed of.
|
||||
*/
|
||||
export class DisposableStore implements IDisposable {
|
||||
static DISABLE_DISPOSED_WARNING = false;
|
||||
|
||||
private readonly _toDispose = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables and mark this object as disposed.
|
||||
*
|
||||
* Any future disposables added to this object will be disposed of on `add`.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDisposed = true;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return `true` if this object has been disposed of.
|
||||
*/
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables but do not mark this object as disposed.
|
||||
*/
|
||||
clear(): void {
|
||||
if (this._toDispose.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispose(this._toDispose);
|
||||
} finally {
|
||||
this._toDispose.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new {@link IDisposable disposable} to the collection.
|
||||
*/
|
||||
add<T extends IDisposable>(o: T): T {
|
||||
if (!o) {
|
||||
return o;
|
||||
}
|
||||
if ((o as unknown as DisposableStore) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
|
||||
if (this._isDisposed) {
|
||||
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
|
||||
console.warn(
|
||||
new Error(
|
||||
'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!',
|
||||
).stack,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._toDispose.add(o);
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
|
||||
* disposable even when the disposable is not part in the store.
|
||||
*/
|
||||
delete<T extends IDisposable>(o: T): void {
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
if ((o as unknown as DisposableStore) === this) {
|
||||
throw new Error('Cannot dispose a disposable on itself!');
|
||||
}
|
||||
this._toDispose.delete(o);
|
||||
o.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the value from the store, but does not dispose it.
|
||||
*/
|
||||
deleteAndLeak<T extends IDisposable>(o: T): void {
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
if (this._toDispose.has(o)) {
|
||||
this._toDispose.delete(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the value(s) passed in.
|
||||
*/
|
||||
export function dispose<T extends IDisposable>(disposable: T): T;
|
||||
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
|
||||
export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(
|
||||
disposables: A,
|
||||
): A;
|
||||
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
|
||||
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
|
||||
export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
|
||||
if (Iterable.is(arg)) {
|
||||
const errors: any[] = [];
|
||||
|
||||
for (const d of arg) {
|
||||
if (d) {
|
||||
try {
|
||||
d.dispose();
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 1) {
|
||||
throw errors[0];
|
||||
} else if (errors.length > 1) {
|
||||
throw new AggregateError(errors, 'Encountered errors while disposing of store');
|
||||
}
|
||||
|
||||
return Array.isArray(arg) ? [] : arg;
|
||||
} else if (arg) {
|
||||
arg.dispose();
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a function that implements dispose into an {@link IDisposable}.
|
||||
*
|
||||
* @param fn Clean up function, guaranteed to be called only **once**.
|
||||
*/
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
return {
|
||||
dispose: createSingleCallFunction(() => {
|
||||
fn();
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple disposable values into a single {@link IDisposable}.
|
||||
*/
|
||||
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
||||
return toDisposable(() => dispose(disposables));
|
||||
}
|
||||
7
packages/shared/src/common/errors.ts
Normal file
7
packages/shared/src/common/errors.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function illegalArgument(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal argument: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal argument');
|
||||
}
|
||||
}
|
||||
@ -1,132 +1,59 @@
|
||||
import { Hookable, type HookKeys } from 'hookable';
|
||||
import { Disposable, IDisposable, toDisposable } from './disposable';
|
||||
|
||||
type ArrayT<T> = T extends any[] ? T : [T];
|
||||
export type Event<T> = (listener: (arg: T, thisArg?: any) => any) => IDisposable;
|
||||
|
||||
export type Event<T = any> = (listener: EventListener<T>) => EventDisposable;
|
||||
export type EventListener<T = any> = (...arguments_: ArrayT<T>) => Promise<void> | void;
|
||||
export type EventDisposable = () => void;
|
||||
export class Observable<T> {
|
||||
private _isDisposed = false;
|
||||
|
||||
export interface IEmitter<T = any> {
|
||||
on: Event<T>;
|
||||
emit(...args: ArrayT<T>): void;
|
||||
emitAsync(...args: ArrayT<T>): Promise<void>;
|
||||
clear(): void;
|
||||
}
|
||||
private _event?: Event<T>;
|
||||
private _listeners?: Set<(arg: T) => void>;
|
||||
|
||||
export class Emitter<T = any[]> implements IEmitter<T> {
|
||||
private events: EventListener<T>[] = [];
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
on(fn: EventListener<T>): EventDisposable {
|
||||
this.events.push(fn);
|
||||
|
||||
return () => {
|
||||
this.events = this.events.filter((e) => e !== fn);
|
||||
};
|
||||
this._listeners?.clear();
|
||||
this._listeners = undefined;
|
||||
this._event = undefined;
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
emit(...args: ArrayT<T>) {
|
||||
for (const event of this.events) {
|
||||
event.call(null, ...args);
|
||||
notify(arg: T): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
this._listeners?.forEach((listener) => listener(arg));
|
||||
}
|
||||
|
||||
/**
|
||||
* For the public to allow to subscribe to events from this Observable
|
||||
*/
|
||||
get subscribe(): Event<T> {
|
||||
if (!this._event) {
|
||||
this._event = (listener: (arg: T) => void, thisArg?: any) => {
|
||||
if (this._isDisposed) {
|
||||
return Disposable.Noop;
|
||||
}
|
||||
|
||||
if (thisArg) {
|
||||
listener = listener.bind(thisArg);
|
||||
}
|
||||
|
||||
if (!this._listeners) this._listeners = new Set();
|
||||
this._listeners.add(listener);
|
||||
|
||||
return toDisposable(() => {
|
||||
this._removeListener(listener);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return this._event;
|
||||
}
|
||||
|
||||
private _removeListener(listener: (arg: T) => void) {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
if (this._listeners?.has(listener)) {
|
||||
this._listeners.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
async emitAsync(...args: ArrayT<T>) {
|
||||
for (const event of this.events) {
|
||||
await event.call(null, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IEventEmitter<
|
||||
EventT extends Record<string, any> = Record<string, EventListener>,
|
||||
EventNameT extends HookKeys<EventT> = HookKeys<EventT>,
|
||||
> {
|
||||
/**
|
||||
* 监听事件
|
||||
* add monitor to a event
|
||||
* @param event 事件名称
|
||||
* @param listener 事件回调
|
||||
*/
|
||||
on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable;
|
||||
|
||||
/**
|
||||
* 添加只运行一次的监听事件
|
||||
* @param event 事件名称
|
||||
* @param listener 事件回调
|
||||
*/
|
||||
once(event: EventNameT, listener: EventT[EventNameT]): void;
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* emit a message for a event
|
||||
* @param event 事件名称
|
||||
* @param args 事件参数
|
||||
*/
|
||||
emit(event: EventNameT, ...args: any): Promise<any>;
|
||||
|
||||
/**
|
||||
* 取消监听事件
|
||||
* cancel a monitor from a event
|
||||
* @param event 事件名称
|
||||
* @param listener 事件回调
|
||||
*/
|
||||
off(event: EventNameT, listener: EventT[EventNameT]): void;
|
||||
|
||||
/**
|
||||
* 监听事件,会在其他回调函数之前执行
|
||||
* @param event 事件名称
|
||||
* @param listener 事件回调
|
||||
*/
|
||||
prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable;
|
||||
|
||||
/**
|
||||
* 清除所有事件监听
|
||||
*/
|
||||
removeAll(): void;
|
||||
}
|
||||
|
||||
export class EventEmitter<
|
||||
EventT extends Record<string, any> = Record<string, EventListener<any[]>>,
|
||||
EventNameT extends HookKeys<EventT> = HookKeys<EventT>,
|
||||
> implements IEventEmitter<EventT, EventNameT>
|
||||
{
|
||||
private namespace: string | undefined;
|
||||
private hooks = new Hookable<EventT, EventNameT>();
|
||||
|
||||
constructor(namespace?: string) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable {
|
||||
return this.hooks.hook(event, listener);
|
||||
}
|
||||
|
||||
once(event: EventNameT, listener: EventT[EventNameT]): void {
|
||||
this.hooks.hookOnce(event, listener);
|
||||
}
|
||||
|
||||
async emit(event: EventNameT, ...args: any) {
|
||||
return this.hooks.callHook(event, ...args);
|
||||
}
|
||||
|
||||
off(event: EventNameT, listener: EventT[EventNameT]): void {
|
||||
this.hooks.removeHook(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听事件,会在其他回调函数之前执行
|
||||
* @param event 事件名称
|
||||
* @param listener 事件回调
|
||||
*/
|
||||
prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable {
|
||||
return this.hooks.hook(`${event}:before` as EventNameT, listener);
|
||||
}
|
||||
|
||||
removeAll(): void {
|
||||
this.hooks.removeAllHooks();
|
||||
}
|
||||
}
|
||||
|
||||
112
packages/shared/src/common/graph.ts
Normal file
112
packages/shared/src/common/graph.ts
Normal file
@ -0,0 +1,112 @@
|
||||
export class Node<T> {
|
||||
readonly incoming = new Map<string, Node<T>>();
|
||||
readonly outgoing = new Map<string, Node<T>>();
|
||||
|
||||
constructor(
|
||||
readonly key: string,
|
||||
readonly data: T,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有向图
|
||||
*/
|
||||
export class Graph<T> {
|
||||
private readonly _nodes = new Map<string, Node<T>>();
|
||||
|
||||
constructor(private readonly _hashFn: (element: T) => string) {}
|
||||
|
||||
roots(): Node<T>[] {
|
||||
const ret: Node<T>[] = [];
|
||||
for (const node of this._nodes.values()) {
|
||||
if (node.outgoing.size === 0) {
|
||||
ret.push(node);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
insertEdge(from: T, to: T): void {
|
||||
const fromNode = this.lookupOrInsertNode(from);
|
||||
const toNode = this.lookupOrInsertNode(to);
|
||||
|
||||
fromNode.outgoing.set(toNode.key, toNode);
|
||||
toNode.incoming.set(fromNode.key, fromNode);
|
||||
}
|
||||
|
||||
removeNode(data: T): void {
|
||||
const key = this._hashFn(data);
|
||||
this._nodes.delete(key);
|
||||
for (const node of this._nodes.values()) {
|
||||
node.outgoing.delete(key);
|
||||
node.incoming.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
lookupOrInsertNode(data: T): Node<T> {
|
||||
const key = this._hashFn(data);
|
||||
let node = this._nodes.get(key);
|
||||
|
||||
if (!node) {
|
||||
node = new Node(key, data);
|
||||
this._nodes.set(key, node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
lookup(data: T): Node<T> | undefined {
|
||||
return this._nodes.get(this._hashFn(data));
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._nodes.size === 0;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const data: string[] = [];
|
||||
for (const [key, value] of this._nodes) {
|
||||
data.push(
|
||||
`${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[...value.outgoing.keys()].join(',')}]\n`,
|
||||
);
|
||||
}
|
||||
return data.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴力搜索,仅用于排查故障
|
||||
*/
|
||||
findCycleSlow() {
|
||||
for (const [id, node] of this._nodes) {
|
||||
const seen = new Set<string>([id]);
|
||||
const res = this._findCycle(node, seen);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
|
||||
for (const [id, outgoing] of node.outgoing) {
|
||||
if (seen.has(id)) {
|
||||
return [...seen, id].join(' -> ');
|
||||
}
|
||||
seen.add(id);
|
||||
const value = this._findCycle(outgoing, seen);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
seen.delete(id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class CyclicDependencyError extends Error {
|
||||
constructor(graph: Graph<any>) {
|
||||
super('cyclic dependency between services');
|
||||
this.message =
|
||||
graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,16 @@
|
||||
import * as Platform from './platform';
|
||||
import * as Events from './event';
|
||||
import * as Signals from './signals';
|
||||
|
||||
export { Platform };
|
||||
export { Events, Signals };
|
||||
|
||||
export * from './event';
|
||||
export * from './platform';
|
||||
export * from './logger';
|
||||
export * from './intl';
|
||||
export * from './instantiation';
|
||||
export * from './signals';
|
||||
|
||||
export * from './keyCodes';
|
||||
export * from './errors';
|
||||
export * from './disposable';
|
||||
|
||||
export * from './linkedList';
|
||||
export * from './graph';
|
||||
|
||||
59
packages/shared/src/common/instantiation/container.ts
Normal file
59
packages/shared/src/common/instantiation/container.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Identifies a bean of type `T`.
|
||||
* The name Bean comes from Spring(Java)
|
||||
*/
|
||||
export interface BeanIdentifier<T> {
|
||||
(...args: any[]): void;
|
||||
type: T;
|
||||
}
|
||||
|
||||
export class CtorDescriptor<T> {
|
||||
constructor(
|
||||
readonly ctor: Constructor<T>,
|
||||
readonly staticArguments: any[] = [],
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BeanContainer {
|
||||
private _entries = new Map<BeanIdentifier<any>, any>();
|
||||
|
||||
constructor(...entries: [BeanIdentifier<any>, any][]) {
|
||||
for (const [id, instance] of entries) {
|
||||
this.set(id, instance);
|
||||
}
|
||||
}
|
||||
|
||||
set<T>(id: BeanIdentifier<T>, instance: T | CtorDescriptor<T>): T | CtorDescriptor<T> {
|
||||
const result = this._entries.get(id);
|
||||
this._entries.set(id, instance);
|
||||
return result;
|
||||
}
|
||||
|
||||
has(id: BeanIdentifier<any>): boolean {
|
||||
return this._entries.has(id);
|
||||
}
|
||||
|
||||
get<T>(id: BeanIdentifier<T>): T | CtorDescriptor<T> {
|
||||
return this._entries.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
export type Constructor<T = any> = new (...args: any[]) => T;
|
||||
|
||||
const TARGET = '$TARGET$';
|
||||
const DEPENDENCIES = '$DEPENDENCIES$';
|
||||
|
||||
export function mapDepsToBeanId(beanId: BeanIdentifier<any>, target: Constructor, index: number) {
|
||||
if ((target as any)[TARGET] === target) {
|
||||
(target as any)[DEPENDENCIES].push({ beanId, index });
|
||||
} else {
|
||||
(target as any)[DEPENDENCIES] = [{ beanId, index }];
|
||||
(target as any)[TARGET] = target;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBeanDependecies(
|
||||
target: Constructor,
|
||||
): { id: BeanIdentifier<any>; index: number }[] {
|
||||
return (target as any)[DEPENDENCIES] || [];
|
||||
}
|
||||
@ -1,34 +1,17 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { fluentProvide } from 'inversify-binding-decorators';
|
||||
import { type BeanIdentifier, type Constructor, mapDepsToBeanId } from './container';
|
||||
|
||||
/**
|
||||
* Identifies a service of type `T`.
|
||||
*/
|
||||
export interface ServiceIdentifier<T> {
|
||||
(...args: any[]): void;
|
||||
type: T;
|
||||
}
|
||||
const idsMap = new Map<string, BeanIdentifier<any>>();
|
||||
|
||||
export type Constructor<T = any> = new (...args: any[]) => T;
|
||||
export function createDecorator<T>(beanId: string): BeanIdentifier<T> {
|
||||
if (idsMap.has(beanId)) {
|
||||
return idsMap.get(beanId)!;
|
||||
}
|
||||
|
||||
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
||||
const id = <any>(
|
||||
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
||||
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||
}
|
||||
);
|
||||
id.toString = () => serviceId;
|
||||
const id = <any>function (target: Constructor, _: string, indexOrPropertyDescriptor: any): any {
|
||||
return mapDepsToBeanId(id, target, indexOrPropertyDescriptor);
|
||||
};
|
||||
id.toString = () => beanId;
|
||||
|
||||
idsMap.set(beanId, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export const Injectable = injectable;
|
||||
|
||||
export function Provide<T>(serviceId: ServiceIdentifier<T>, isSingleTon?: boolean) {
|
||||
const ret = fluentProvide(serviceId.toString());
|
||||
|
||||
if (isSingleTon) {
|
||||
return ret.inSingletonScope().done();
|
||||
}
|
||||
return ret.done();
|
||||
}
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './instantiationService';
|
||||
export * from './decorators';
|
||||
export { createDecorator } from './decorators';
|
||||
export { CtorDescriptor, BeanContainer } from './container';
|
||||
export type { Constructor } from './container';
|
||||
|
||||
@ -1,44 +1,41 @@
|
||||
import '@abraham/reflection';
|
||||
import { Container, interfaces, injectable } from 'inversify';
|
||||
import { buildProviderModule } from 'inversify-binding-decorators';
|
||||
import { ServiceIdentifier, Constructor, createDecorator } from './decorators';
|
||||
import { isDisposable } from '../disposable';
|
||||
import { Graph, CyclicDependencyError } from '../graph';
|
||||
import {
|
||||
type BeanIdentifier,
|
||||
BeanContainer,
|
||||
type Constructor,
|
||||
getBeanDependecies,
|
||||
CtorDescriptor,
|
||||
} from './container';
|
||||
import { createDecorator } from './decorators';
|
||||
|
||||
export interface InstanceAccessor {
|
||||
get<T>(id: ServiceIdentifier<T>): T;
|
||||
get<T>(id: BeanIdentifier<T>): T;
|
||||
}
|
||||
|
||||
export interface IInstantiationService {
|
||||
get<T>(serviceIdentifier: ServiceIdentifier<T>): T;
|
||||
readonly container: BeanContainer;
|
||||
|
||||
bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>): void;
|
||||
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T>;
|
||||
|
||||
set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void;
|
||||
|
||||
invokeFunction<R, TS extends any[] = []>(
|
||||
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
||||
...args: TS
|
||||
invokeFunction<R, Args extends any[] = []>(
|
||||
fn: (accessor: InstanceAccessor, ...args: Args) => R,
|
||||
...args: Args
|
||||
): R;
|
||||
|
||||
createInstance<T extends Constructor>(App: T): InstanceType<T>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
|
||||
|
||||
export class InstantiationService implements IInstantiationService {
|
||||
container: Container;
|
||||
private _activeInstantiations = new Set<BeanIdentifier<any>>();
|
||||
|
||||
constructor(options?: interfaces.ContainerOptions) {
|
||||
this.container = new Container(options);
|
||||
this.set(IInstantiationService, this);
|
||||
this.container.load(buildProviderModule());
|
||||
}
|
||||
private _isDisposed = false;
|
||||
private readonly _beansToMaybeDispose = new Set<any>();
|
||||
|
||||
get<T>(serviceIdentifier: ServiceIdentifier<T>) {
|
||||
return this.container.get<T>(serviceIdentifier.toString());
|
||||
}
|
||||
|
||||
set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void {
|
||||
this.container.bind<T>(serviceIdentifier).toConstantValue(instance);
|
||||
constructor(public readonly container: BeanContainer = new BeanContainer()) {
|
||||
this.container.set(IInstantiationService, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,20 +45,166 @@ export class InstantiationService implements IInstantiationService {
|
||||
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
||||
...args: TS
|
||||
): R {
|
||||
this._throwIfDisposed();
|
||||
|
||||
const accessor: InstanceAccessor = {
|
||||
get: (id) => {
|
||||
return this.get(id);
|
||||
const result = this._getOrCreateInstance(id);
|
||||
if (!result) {
|
||||
throw new Error(`[invokeFunction] unknown service '${id}'`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
return fn(accessor, ...args);
|
||||
}
|
||||
|
||||
bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) {
|
||||
this.container.bind<T>(serviceIdentifier).to(constructor);
|
||||
/**
|
||||
* 创建实例
|
||||
*/
|
||||
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T> {
|
||||
this._throwIfDisposed();
|
||||
|
||||
const beanDependencies = getBeanDependecies(Ctor).sort((a, b) => a.index - b.index);
|
||||
const beanArgs = [];
|
||||
|
||||
for (const dependency of beanDependencies) {
|
||||
const instance = this._getOrCreateInstance(dependency.id);
|
||||
if (!instance) {
|
||||
throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.id}.`);
|
||||
}
|
||||
|
||||
beanArgs.push(instance);
|
||||
}
|
||||
|
||||
// 检查传入参数的个数,进行参数微调
|
||||
const firstArgPos = beanDependencies.length > 0 ? beanDependencies[0].index : args.length;
|
||||
if (args.length !== firstArgPos) {
|
||||
const delta = firstArgPos - args.length;
|
||||
if (delta > 0) {
|
||||
args = args.concat(new Array(delta));
|
||||
} else {
|
||||
args = args.slice(0, firstArgPos);
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.construct<any, InstanceType<T>>(Ctor, args.concat(beanArgs));
|
||||
}
|
||||
|
||||
createInstance<T extends Constructor>(App: T) {
|
||||
injectable()(App);
|
||||
return this.container.resolve<InstanceType<T>>(App);
|
||||
private _getOrCreateInstance<T>(id: BeanIdentifier<T>): T {
|
||||
const thing = this.container.get(id);
|
||||
if (thing instanceof CtorDescriptor) {
|
||||
return this._safeCreateAndCacheInstance<T>(id, thing);
|
||||
} else {
|
||||
return thing;
|
||||
}
|
||||
}
|
||||
|
||||
private _safeCreateAndCacheInstance<T>(id: BeanIdentifier<T>, desc: CtorDescriptor<T>): T {
|
||||
if (this._activeInstantiations.has(id)) {
|
||||
throw new Error(`[createInstance] illegal state - RECURSIVELY instantiating ${id}`);
|
||||
}
|
||||
this._activeInstantiations.add(id);
|
||||
try {
|
||||
return this._createAndCacheServiceInstance(id, desc);
|
||||
} finally {
|
||||
this._activeInstantiations.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
private _createAndCacheServiceInstance<T>(id: BeanIdentifier<T>, desc: CtorDescriptor<T>): T {
|
||||
const graph = new Graph<{ id: BeanIdentifier<T>; desc: CtorDescriptor<T> }>((data) =>
|
||||
data.id.toString(),
|
||||
);
|
||||
|
||||
let cycleCount = 0;
|
||||
const stack = [{ id, desc }];
|
||||
const seen = new Set<string>();
|
||||
|
||||
while (stack.length > 0) {
|
||||
const item = stack.pop()!;
|
||||
|
||||
if (seen.has(item.id.toString())) {
|
||||
continue;
|
||||
}
|
||||
seen.add(item.id.toString());
|
||||
|
||||
graph.lookupOrInsertNode(item);
|
||||
|
||||
// 一个较弱但有效的循环检查启发式方法
|
||||
if (cycleCount++ > 1000) {
|
||||
throw new CyclicDependencyError(graph);
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!instanceOrDesc) {
|
||||
throw new Error(
|
||||
`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (instanceOrDesc instanceof CtorDescriptor) {
|
||||
const d = {
|
||||
id: dependency.id,
|
||||
desc: instanceOrDesc,
|
||||
};
|
||||
graph.insertEdge(item, d);
|
||||
stack.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const roots = graph.roots();
|
||||
|
||||
// if there is no more roots but still
|
||||
// nodes in the graph we have a cycle
|
||||
if (roots.length === 0) {
|
||||
if (!graph.isEmpty()) {
|
||||
throw new CyclicDependencyError(graph);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (const { data } of roots) {
|
||||
// Repeat the check for this still being a service sync descriptor. That's because
|
||||
// instantiating a dependency might have side-effect and recursively trigger instantiation
|
||||
// so that some dependencies are now fullfilled already.
|
||||
const instanceOrDesc = this.container.get(data.id);
|
||||
if (instanceOrDesc instanceof CtorDescriptor) {
|
||||
// create instance and overwrite the service collections
|
||||
const instance = this.createInstance(
|
||||
instanceOrDesc.ctor,
|
||||
instanceOrDesc.staticArguments,
|
||||
);
|
||||
this._beansToMaybeDispose.add(instance);
|
||||
this.container.set(data.id, instance);
|
||||
}
|
||||
graph.removeNode(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.container.get(id) as T;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
// dispose all services created by this service
|
||||
for (const candidate of this._beansToMaybeDispose) {
|
||||
if (isDisposable(candidate)) {
|
||||
candidate.dispose();
|
||||
}
|
||||
}
|
||||
this._beansToMaybeDispose.clear();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
private _throwIfDisposed(): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('InstantiationService has been disposed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1302
packages/shared/src/common/keyCodes.ts
Normal file
1302
packages/shared/src/common/keyCodes.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,13 @@ export function platformToString(platform: PlatformEnum): PlatformName {
|
||||
}
|
||||
}
|
||||
|
||||
export const enum OperatingSystem {
|
||||
Windows = 1,
|
||||
Macintosh = 2,
|
||||
Linux = 3
|
||||
}
|
||||
export const OS = (isMacintosh || isIOS ? OperatingSystem.Macintosh : (isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
|
||||
|
||||
export let platform: PlatformEnum = PlatformEnum.Unknown;
|
||||
if (isMacintosh) {
|
||||
platform = PlatformEnum.Mac;
|
||||
|
||||
33
packages/shared/src/utils/functional.ts
Normal file
33
packages/shared/src/utils/functional.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { AnyFunction } from '../types';
|
||||
|
||||
/**
|
||||
* Given a function, returns a function that is only calling that function once.
|
||||
*/
|
||||
export function createSingleCallFunction<T extends AnyFunction>(
|
||||
this: unknown,
|
||||
fn: T,
|
||||
fnDidRunCallback?: () => void,
|
||||
): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return function (...args: any[]) {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
|
||||
didCall = true;
|
||||
if (fnDidRunCallback) {
|
||||
try {
|
||||
result = fn.apply(_this, args);
|
||||
} finally {
|
||||
fnDidRunCallback();
|
||||
}
|
||||
} else {
|
||||
result = fn.apply(_this, args);
|
||||
}
|
||||
|
||||
return result;
|
||||
} as unknown as T;
|
||||
}
|
||||
@ -8,3 +8,4 @@ export * from './types';
|
||||
export * from './async';
|
||||
export * from './node';
|
||||
export * from './resource';
|
||||
export * from './functional';
|
||||
|
||||
@ -5,3 +5,7 @@ export function first<T>(iterable: Iterable<T>): T | undefined {
|
||||
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
|
||||
return !iterable || iterable[Symbol.iterator]().next().done === true;
|
||||
}
|
||||
|
||||
export function is<T = any>(thing: any): thing is Iterable<T> {
|
||||
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
@ -4,5 +4,4 @@
|
||||
"allowJs": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user