mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-13 20:36:34 +00:00
refactor: diService、eventManager
This commit is contained in:
parent
9d0f178a06
commit
006b9b615e
@ -9,7 +9,7 @@ import {
|
|||||||
Iterable,
|
Iterable,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { ICommand, ICommandHandler } from './command';
|
import { ICommand, ICommandHandler } from './command';
|
||||||
import { Extensions, Registry } from '../common/registry';
|
import { Extensions, Registry } from '../extension/registry';
|
||||||
import { ICommandService } from './commandService';
|
import { ICommandService } from './commandService';
|
||||||
|
|
||||||
export type ICommandsMap = Map<string, ICommand>;
|
export type ICommandsMap = Map<string, ICommand>;
|
||||||
@ -26,7 +26,7 @@ export interface ICommandRegistry {
|
|||||||
getCommands(): ICommandsMap;
|
getCommands(): ICommandsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommandsRegistry implements ICommandRegistry {
|
class CommandsRegistryImpl implements ICommandRegistry {
|
||||||
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
||||||
|
|
||||||
private readonly _didRegisterCommandEmitter = new Emitter<string>();
|
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 { createDecorator, IInstantiationService } from '@alilc/lowcode-shared';
|
||||||
import { Registry, Extensions } from '../common/registry';
|
import { CommandsRegistry } from './commandRegistry';
|
||||||
import { ICommandRegistry } from './commandRegistry';
|
|
||||||
|
|
||||||
export interface ICommandService {
|
export interface ICommandService {
|
||||||
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
||||||
@ -8,7 +7,6 @@ export interface ICommandService {
|
|||||||
|
|
||||||
export const ICommandService = createDecorator<ICommandService>('commandService');
|
export const ICommandService = createDecorator<ICommandService>('commandService');
|
||||||
|
|
||||||
@Provide(ICommandService)
|
|
||||||
export class CommandService implements ICommandService {
|
export class CommandService implements ICommandService {
|
||||||
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
|
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
|
||||||
|
|
||||||
@ -17,7 +15,7 @@ export class CommandService implements ICommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private tryExecuteCommand(id: string, args: any[]): Promise<any> {
|
private tryExecuteCommand(id: string, args: any[]): Promise<any> {
|
||||||
const command = Registry.as<ICommandRegistry>(Extensions.Command).getCommand(id);
|
const command = CommandsRegistry.getCommand(id);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return Promise.reject(new Error(`command '${id}' not found`));
|
return Promise.reject(new Error(`command '${id}' not found`));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
types,
|
types,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { isUndefined, isObject } from 'lodash-es';
|
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';
|
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
|
||||||
|
|
||||||
export interface IConfigurationRegistry {
|
export interface IConfigurationRegistry {
|
||||||
@ -133,9 +133,9 @@ export const allSettings: {
|
|||||||
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
||||||
} = { properties: {}, patternProperties: {} };
|
} = { properties: {}, patternProperties: {} };
|
||||||
|
|
||||||
export class ConfigurationRegistry implements IConfigurationRegistry {
|
export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
||||||
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
||||||
private configurationDefaultsOverrides: Map<
|
private readonly configurationDefaultsOverrides: Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
configurationDefaultOverrides: IConfigurationDefaultOverride[];
|
configurationDefaultOverrides: IConfigurationDefaultOverride[];
|
||||||
@ -143,8 +143,8 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
private readonly configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||||
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
private readonly excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||||
private overrideIdentifiers = new Set<string>();
|
private overrideIdentifiers = new Set<string>();
|
||||||
|
|
||||||
private propertiesChangeEmitter = new Emitter<{
|
private propertiesChangeEmitter = new Emitter<{
|
||||||
@ -645,4 +645,6 @@ function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean {
|
|||||||
return a.id === b.id && a.version === b.version;
|
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 {
|
import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared';
|
||||||
createDecorator,
|
|
||||||
Emitter,
|
|
||||||
Provide,
|
|
||||||
type Event,
|
|
||||||
type EventListener,
|
|
||||||
} from '@alilc/lowcode-shared';
|
|
||||||
import {
|
import {
|
||||||
Configuration,
|
Configuration,
|
||||||
DefaultConfiguration,
|
DefaultConfiguration,
|
||||||
@ -69,7 +63,6 @@ export interface IConfigurationService {
|
|||||||
|
|
||||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||||
|
|
||||||
@Provide(IConfigurationService)
|
|
||||||
export class ConfigurationService implements IConfigurationService {
|
export class ConfigurationService implements IConfigurationService {
|
||||||
private configuration: Configuration;
|
private configuration: Configuration;
|
||||||
private readonly defaultConfiguration: DefaultConfiguration;
|
private readonly defaultConfiguration: DefaultConfiguration;
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import {
|
|||||||
type IOverrides,
|
type IOverrides,
|
||||||
} from './configurationModel';
|
} from './configurationModel';
|
||||||
import {
|
import {
|
||||||
|
ConfigurationRegistry,
|
||||||
type IConfigurationPropertySchema,
|
type IConfigurationPropertySchema,
|
||||||
type IConfigurationRegistry,
|
type IConfigurationRegistry,
|
||||||
type IRegisteredConfigurationPropertySchema,
|
type IRegisteredConfigurationPropertySchema,
|
||||||
} from './configurationRegistry';
|
} from './configurationRegistry';
|
||||||
import { Registry, Extensions } from '../common/registry';
|
|
||||||
import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es';
|
import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
IInspectValue,
|
IInspectValue,
|
||||||
@ -37,8 +37,8 @@ export class DefaultConfiguration {
|
|||||||
|
|
||||||
initialize(): ConfigurationModel {
|
initialize(): ConfigurationModel {
|
||||||
this.resetConfigurationModel();
|
this.resetConfigurationModel();
|
||||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(
|
ConfigurationRegistry.onDidUpdateConfiguration(({ properties }) =>
|
||||||
({ properties }) => this.onDidUpdateConfiguration([...properties]),
|
this.onDidUpdateConfiguration([...properties]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.configurationModel;
|
return this.configurationModel;
|
||||||
@ -56,19 +56,14 @@ export class DefaultConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onDidUpdateConfiguration(properties: string[]): void {
|
private onDidUpdateConfiguration(properties: string[]): void {
|
||||||
this.updateConfigurationModel(
|
this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties());
|
||||||
properties,
|
|
||||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(),
|
|
||||||
);
|
|
||||||
this.emitter.emit({ defaults: this.configurationModel, properties });
|
this.emitter.emit({ defaults: this.configurationModel, properties });
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetConfigurationModel(): void {
|
private resetConfigurationModel(): void {
|
||||||
this._configurationModel = ConfigurationModel.createEmptyModel();
|
this._configurationModel = ConfigurationModel.createEmptyModel();
|
||||||
|
|
||||||
const properties = Registry.as<IConfigurationRegistry>(
|
const properties = ConfigurationRegistry.getConfigurationProperties();
|
||||||
Extensions.Configuration,
|
|
||||||
).getConfigurationProperties();
|
|
||||||
|
|
||||||
this.updateConfigurationModel(Object.keys(properties), properties);
|
this.updateConfigurationModel(Object.keys(properties), properties);
|
||||||
}
|
}
|
||||||
@ -156,9 +151,7 @@ class ConfigurationModelParser {
|
|||||||
raw: any,
|
raw: any,
|
||||||
options?: ConfigurationParseOptions,
|
options?: ConfigurationParseOptions,
|
||||||
): IConfigurationModel & { hasExcludedProperties?: boolean } {
|
): IConfigurationModel & { hasExcludedProperties?: boolean } {
|
||||||
const configurationProperties = Registry.as<IConfigurationRegistry>(
|
const configurationProperties = ConfigurationRegistry.getConfigurationProperties();
|
||||||
Extensions.Configuration,
|
|
||||||
).getConfigurationProperties();
|
|
||||||
const filtered = this.filter(raw, configurationProperties, true, options);
|
const filtered = this.filter(raw, configurationProperties, true, options);
|
||||||
|
|
||||||
raw = filtered.raw;
|
raw = filtered.raw;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { type IConfigurationRegistry, type IConfigurationNode } from '../configuration';
|
import { ConfigurationRegistry, type IConfigurationNode } from '../configuration';
|
||||||
import { Registry, Extensions } from '../common/registry';
|
|
||||||
import { type ExtensionInitializer, type IExtensionInstance } from './extension';
|
import { type ExtensionInitializer, type IExtensionInstance } from './extension';
|
||||||
import { invariant } from '@alilc/lowcode-shared';
|
import { invariant } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
@ -19,9 +18,8 @@ export class ExtensionHost {
|
|||||||
initializer: ExtensionInitializer,
|
initializer: ExtensionInitializer,
|
||||||
preferenceConfigurations: IConfigurationNode[],
|
preferenceConfigurations: IConfigurationNode[],
|
||||||
) {
|
) {
|
||||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
|
||||||
this.configurationProperties =
|
this.configurationProperties =
|
||||||
configurationRegistry.registerConfigurations(preferenceConfigurations);
|
ConfigurationRegistry.registerConfigurations(preferenceConfigurations);
|
||||||
|
|
||||||
this.instance = initializer({});
|
this.instance = initializer({});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export class ExtensionManagement {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.validateExtension(extension, override)) return;
|
if (!this.validateExtension(extension, override)) return;
|
||||||
|
|
||||||
const metadata = extension.meta ?? {};
|
const metadata = extension.metadata ?? {};
|
||||||
const host = new ExtensionHost(
|
const host = new ExtensionHost(
|
||||||
extension.name,
|
extension.name,
|
||||||
extension,
|
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 { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement';
|
||||||
import { type IFunctionExtension } from './extension';
|
import { type IFunctionExtension } from './extension';
|
||||||
import { ExtensionHost } from './extensionHost';
|
import { ExtensionHost } from './extensionHost';
|
||||||
@ -15,7 +15,6 @@ export interface IExtensionService {
|
|||||||
|
|
||||||
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
||||||
|
|
||||||
@Provide(IExtensionService)
|
|
||||||
export class ExtensionService implements IExtensionService {
|
export class ExtensionService implements IExtensionService {
|
||||||
private extensionManagement = new ExtensionManagement();
|
private extensionManagement = new ExtensionManagement();
|
||||||
|
|
||||||
|
|||||||
@ -41,5 +41,6 @@ export const Registry: IRegistry = new RegistryImpl();
|
|||||||
export const Extensions = {
|
export const Extensions = {
|
||||||
Configuration: 'base.contributions.configuration',
|
Configuration: 'base.contributions.configuration',
|
||||||
Command: 'base.contributions.command',
|
Command: 'base.contributions.command',
|
||||||
|
Keybinding: 'base.contributions.keybinding',
|
||||||
Widget: 'base.contributions.widget',
|
Widget: 'base.contributions.widget',
|
||||||
};
|
};
|
||||||
@ -4,5 +4,9 @@ export * from './resource';
|
|||||||
export * from './command';
|
export * from './command';
|
||||||
|
|
||||||
// test
|
// test
|
||||||
export * from './common/registry';
|
export * from './extension/registry';
|
||||||
export * from './main';
|
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 {
|
export interface IKeybindingItem {
|
||||||
keybinding: Keybinding | null;
|
keybinding: Keybinding | null;
|
||||||
command: string | null;
|
command: string | null;
|
||||||
@ -24,3 +29,81 @@ export interface IKeybindings {
|
|||||||
secondary?: number[];
|
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 {
|
import {
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
|
||||||
type Package,
|
type Package,
|
||||||
type Reference,
|
type Reference,
|
||||||
mapPackageToUniqueId,
|
mapPackageToUniqueId,
|
||||||
@ -22,7 +21,6 @@ export interface IResourceService {
|
|||||||
|
|
||||||
export const IResourceService = createDecorator<IResourceService>('resourceService');
|
export const IResourceService = createDecorator<IResourceService>('resourceService');
|
||||||
|
|
||||||
@Provide(IResourceService)
|
|
||||||
export class ResourceService implements IResourceService {
|
export class ResourceService implements IResourceService {
|
||||||
private resourceModel = new ResourceModel();
|
private resourceModel = new ResourceModel();
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Extensions, Registry } from '../../extension/extension';
|
import { WidgetRegistry } from '../widget/widgetRegistry';
|
||||||
import { IWidgetRegistry } from '../widget/widgetRegistry';
|
|
||||||
|
|
||||||
export const enum LayoutParts {
|
export const enum LayoutParts {
|
||||||
TopBar = 1,
|
TopBar = 1,
|
||||||
@ -21,7 +20,7 @@ export interface ILayout {
|
|||||||
|
|
||||||
export class Layout<View> implements ILayout {
|
export class Layout<View> implements ILayout {
|
||||||
constructor(public mainContainer: HTMLElement) {
|
constructor(public mainContainer: HTMLElement) {
|
||||||
Registry.as<IWidgetRegistry<View>>(Extensions.Widget).onDidRegister(() => {});
|
WidgetRegistry.onDidRegister(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPart(part: LayoutParts): void {}
|
registerPart(part: LayoutParts): void {}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared';
|
import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared';
|
||||||
import { IWidget } from './widget';
|
import { IWidget } from './widget';
|
||||||
import { Extensions, Registry } from '../../extension/extension';
|
import { Extensions, Registry } from '../../extension/registry';
|
||||||
|
|
||||||
export interface IWidgetRegistry<View> {
|
export interface IWidgetRegistry<View> {
|
||||||
onDidRegister: Event<IWidget<View>[]>;
|
onDidRegister: Event<IWidget<View>[]>;
|
||||||
@ -12,7 +12,7 @@ export interface IWidgetRegistry<View> {
|
|||||||
getWidgets(): IWidget<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 _widgets: Map<string, IWidget<View>> = new Map();
|
||||||
|
|
||||||
private emitter = new Emitter<IWidget<View>[]>();
|
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 {
|
export interface IWorkbenchService {
|
||||||
initialize(): void;
|
initialize(): void;
|
||||||
@ -6,7 +6,6 @@ export interface IWorkbenchService {
|
|||||||
|
|
||||||
export const IWorkbenchService = createDecorator<IWorkbenchService>('workbenchService');
|
export const IWorkbenchService = createDecorator<IWorkbenchService>('workbenchService');
|
||||||
|
|
||||||
@Provide(IWorkbenchService)
|
|
||||||
export class WorkbenchService implements IWorkbenchService {
|
export class WorkbenchService implements IWorkbenchService {
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
console.log('workbench service');
|
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 interface IWorkspaceService {}
|
||||||
|
|
||||||
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
||||||
|
|
||||||
@Provide(IWorkspaceService)
|
|
||||||
export class WorkspaceService implements 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 {
|
export interface ITheme {
|
||||||
type: string;
|
type: string;
|
||||||
@ -14,7 +14,6 @@ export interface IThemeService {
|
|||||||
|
|
||||||
export const IThemeService = createDecorator<IThemeService>('themeService');
|
export const IThemeService = createDecorator<IThemeService>('themeService');
|
||||||
|
|
||||||
@Provide(IThemeService)
|
|
||||||
export class ThemeService implements IThemeService {
|
export class ThemeService implements IThemeService {
|
||||||
private activeTheme: ITheme;
|
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 {
|
import {
|
||||||
IBoostsService,
|
IBoostsManager,
|
||||||
IComponentTreeModelService,
|
IComponentTreeModelService,
|
||||||
ILifeCycleService,
|
ILifeCycleService,
|
||||||
IPackageManagementService,
|
IPackageManagementService,
|
||||||
ISchemaService,
|
ISchemaService,
|
||||||
|
IExtensionHostService,
|
||||||
} from '@alilc/lowcode-renderer-core';
|
} from '@alilc/lowcode-renderer-core';
|
||||||
import { InstanceAccessor } from '@alilc/lowcode-shared';
|
import { InstanceAccessor } from '@alilc/lowcode-shared';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
@ -16,7 +17,7 @@ export interface IRendererContext {
|
|||||||
|
|
||||||
readonly packageManager: IPackageManagementService;
|
readonly packageManager: IPackageManagementService;
|
||||||
|
|
||||||
readonly boostsManager: IBoostsService;
|
readonly boostsManager: IBoostsManager;
|
||||||
|
|
||||||
readonly componentTreeModel: IComponentTreeModelService;
|
readonly componentTreeModel: IComponentTreeModelService;
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ export const getRenderInstancesByAccessor = (accessor: InstanceAccessor) => {
|
|||||||
return {
|
return {
|
||||||
schema: accessor.get(ISchemaService),
|
schema: accessor.get(ISchemaService),
|
||||||
packageManager: accessor.get(IPackageManagementService),
|
packageManager: accessor.get(IPackageManagementService),
|
||||||
boostsManager: accessor.get(IBoostsService),
|
boostsManager: accessor.get(IExtensionHostService).boostsManager,
|
||||||
componentTreeModel: accessor.get(IComponentTreeModelService),
|
componentTreeModel: accessor.get(IComponentTreeModelService),
|
||||||
lifeCycle: accessor.get(ILifeCycleService),
|
lifeCycle: accessor.get(ILifeCycleService),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import {
|
|||||||
type AnyFunction,
|
type AnyFunction,
|
||||||
type StringDictionary,
|
type StringDictionary,
|
||||||
specTypes,
|
specTypes,
|
||||||
computed,
|
Signals,
|
||||||
watch,
|
|
||||||
invariant,
|
invariant,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
@ -60,10 +59,10 @@ function createReactiveStore<Snapshot = StringDictionary>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (getter) {
|
if (getter) {
|
||||||
const computedValue = computed<any>(() => getter(target));
|
const computedValue = Signals.computed<any>(() => getter(target));
|
||||||
|
|
||||||
cleanups.push(
|
cleanups.push(
|
||||||
watch(
|
Signals.watch(
|
||||||
computedValue,
|
computedValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
@ -76,8 +75,8 @@ function createReactiveStore<Snapshot = StringDictionary>(
|
|||||||
);
|
);
|
||||||
} else if (valueGetter) {
|
} else if (valueGetter) {
|
||||||
const initValue = mapValue(target, filter, (node: any, paths) => {
|
const initValue = mapValue(target, filter, (node: any, paths) => {
|
||||||
const computedValue = computed(() => valueGetter(node));
|
const computedValue = Signals.computed(() => valueGetter(node));
|
||||||
const unwatch = watch(computedValue, (newValue) => {
|
const unwatch = Signals.watch(computedValue, (newValue) => {
|
||||||
waitPathToSetValueMap.set(paths, newValue);
|
waitPathToSetValueMap.set(paths, newValue);
|
||||||
|
|
||||||
if (!isFlushPending && !isFlushing) {
|
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';
|
import { isPlainObject } from 'lodash-es';
|
||||||
|
|
||||||
export function reactiveStateFactory(initState: StringDictionary): InstanceStateApi {
|
export function reactiveStateFactory(initState: StringDictionary): InstanceStateApi {
|
||||||
const proxyState = signal(initState);
|
const proxyState = Signals.signal(initState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get state() {
|
get state() {
|
||||||
|
|||||||
@ -2,6 +2,5 @@
|
|||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"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 -------------------- */
|
/* --------------- api -------------------- */
|
||||||
export { createRenderer } from './main';
|
export { createRenderer } from './main';
|
||||||
export { IBoostsService, IExtensionHostService } from './services/extension';
|
export { IExtensionHostService } from './services/extension';
|
||||||
export { definePackageLoader, IPackageManagementService } from './services/package';
|
export { definePackageLoader, IPackageManagementService } from './services/package';
|
||||||
export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService';
|
export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService';
|
||||||
export { IComponentTreeModelService } from './services/model';
|
export { IComponentTreeModelService } from './services/model';
|
||||||
|
|||||||
@ -1,54 +1,78 @@
|
|||||||
import { invariant, InstantiationService } from '@alilc/lowcode-shared';
|
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 {
|
import {
|
||||||
IExtensionHostService,
|
IExtensionHostService,
|
||||||
type RenderAdapter,
|
type RenderAdapter,
|
||||||
type IRenderObject,
|
type IRenderObject,
|
||||||
|
ExtensionHostService,
|
||||||
} from './services/extension';
|
} from './services/extension';
|
||||||
import { IPackageManagementService } from './services/package';
|
import { IPackageManagementService, PackageManagementService } from './services/package';
|
||||||
import { ISchemaService } from './services/schema';
|
import { ISchemaService, SchemaService } from './services/schema';
|
||||||
import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService';
|
import { ILifeCycleService, LifecyclePhase, LifeCycleService } from './services/lifeCycleService';
|
||||||
import type { AppOptions, RendererApplication } from './types';
|
import { IRuntimeIntlService, RuntimeIntlService } from './services/runtimeIntlService';
|
||||||
|
import { IRuntimeUtilService, RuntimeUtilService } from './services/runtimeUtilService';
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 createRenderer 的辅助函数
|
|
||||||
* @param schema
|
|
||||||
* @param options
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function createRenderer<RenderObject = IRenderObject>(
|
export function createRenderer<RenderObject = IRenderObject>(
|
||||||
renderAdapter: RenderAdapter<RenderObject>,
|
renderAdapter: RenderAdapter<RenderObject>,
|
||||||
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
|
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
|
||||||
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
|
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
|
||||||
|
|
||||||
const accessor = new InstantiationService({ defaultScope: 'Singleton' });
|
const instantiationService = new InstantiationService();
|
||||||
let mode: 'development' | 'production' = 'production';
|
|
||||||
|
|
||||||
const schemaService = accessor.get(ISchemaService);
|
// create services
|
||||||
const packageManagementService = accessor.get(IPackageManagementService);
|
const lifeCycleService = new LifeCycleService();
|
||||||
const codeRuntimeService = accessor.get(ICodeRuntimeService);
|
instantiationService.container.set(ILifeCycleService, lifeCycleService);
|
||||||
const lifeCycleService = accessor.get(ILifeCycleService);
|
|
||||||
const extensionHostService = accessor.get(IExtensionHostService);
|
|
||||||
|
|
||||||
return async (options) => {
|
return async (options) => {
|
||||||
if (options.mode) mode = options.mode;
|
const schemaService = new SchemaService(options.schema);
|
||||||
|
instantiationService.container.set(ISchemaService, schemaService);
|
||||||
|
|
||||||
// valid schema
|
const codeRuntimeService = instantiationService.createInstance(
|
||||||
schemaService.initialize(options.schema);
|
CodeRuntimeService,
|
||||||
codeRuntimeService.initialize(options.codeRuntime ?? {});
|
options.codeRuntime,
|
||||||
await lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
|
);
|
||||||
|
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 ?? []);
|
await extensionHostService.registerPlugin(options.plugins ?? []);
|
||||||
// 先加载插件提供 package loader
|
// 先加载插件提供 package loader
|
||||||
await packageManagementService.loadPackages(options.packages ?? []);
|
await packageManagementService.loadPackages(options.packages ?? []);
|
||||||
|
|
||||||
await lifeCycleService.setPhase(LifecyclePhase.Ready);
|
lifeCycleService.setPhase(LifecyclePhase.Ready);
|
||||||
|
|
||||||
const app: RendererApplication<RenderObject> = {
|
const app: RendererApplication<RenderObject> = {
|
||||||
get mode() {
|
get mode() {
|
||||||
return mode;
|
return __DEV__ ? 'development' : 'production';
|
||||||
},
|
},
|
||||||
schema: schemaService,
|
schema: schemaService,
|
||||||
packageManager: packageManagementService,
|
packageManager: packageManagementService,
|
||||||
@ -57,12 +81,12 @@ export function createRenderer<RenderObject = IRenderObject>(
|
|||||||
use: (plugin) => {
|
use: (plugin) => {
|
||||||
return extensionHostService.registerPlugin(plugin);
|
return extensionHostService.registerPlugin(plugin);
|
||||||
},
|
},
|
||||||
destroy: async () => {
|
destroy: () => {
|
||||||
return lifeCycleService.setPhase(LifecyclePhase.Destroying);
|
lifeCycleService.setPhase(LifecyclePhase.Destroying);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'development') {
|
if (__DEV__) {
|
||||||
Object.defineProperty(app, '__options', { get: () => options });
|
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 { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime';
|
||||||
|
import { ISchemaService } from '../schema';
|
||||||
|
|
||||||
export interface ICodeRuntimeService {
|
export interface ICodeRuntimeService {
|
||||||
readonly rootRuntime: ICodeRuntime;
|
readonly rootRuntime: ICodeRuntime;
|
||||||
|
|
||||||
initialize(options: CodeRuntimeOptions): void;
|
|
||||||
|
|
||||||
createCodeRuntime<T extends StringDictionary = StringDictionary>(
|
createCodeRuntime<T extends StringDictionary = StringDictionary>(
|
||||||
options?: CodeRuntimeOptions<T>,
|
options?: CodeRuntimeOptions<T>,
|
||||||
): ICodeRuntime<T>;
|
): ICodeRuntime<T>;
|
||||||
@ -13,12 +17,23 @@ export interface ICodeRuntimeService {
|
|||||||
|
|
||||||
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
|
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
|
||||||
|
|
||||||
@Provide(ICodeRuntimeService)
|
export class CodeRuntimeService extends Disposable implements ICodeRuntimeService {
|
||||||
export class CodeRuntimeService implements ICodeRuntimeService {
|
|
||||||
rootRuntime: ICodeRuntime;
|
rootRuntime: ICodeRuntime;
|
||||||
|
|
||||||
initialize(options?: CodeRuntimeOptions) {
|
constructor(
|
||||||
|
options: CodeRuntimeOptions = {},
|
||||||
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
this.rootRuntime = new CodeRuntime(options);
|
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>(
|
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 { isObject } from 'lodash-es';
|
||||||
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
|
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
|
||||||
import { IRuntimeUtilService } from '../runtimeUtilService';
|
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(name: string, value: any, force?: boolean): void;
|
||||||
extend(value: StringDictionary, force?: boolean): void;
|
extend(value: StringDictionary, force?: boolean): void;
|
||||||
|
|
||||||
toExpose<Extends>(): IBoosts<Extends>;
|
toExpose<Extends>(): IBoosts<Extends>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IBoostsService = createDecorator<IBoostsService>('boostsService');
|
export class BoostsManager implements IBoostsManager {
|
||||||
|
|
||||||
@Provide(IBoostsService)
|
|
||||||
export class BoostsService implements IBoostsService {
|
|
||||||
private builtInApis: IBoostsApi;
|
private builtInApis: IBoostsApi;
|
||||||
|
|
||||||
private extendsValue: StringDictionary = {};
|
private extendsValue: StringDictionary = {};
|
||||||
@ -42,16 +39,16 @@ export class BoostsService implements IBoostsService {
|
|||||||
private _expose: any;
|
private _expose: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ICodeRuntimeService codeRuntimeService: ICodeRuntimeService,
|
codeRuntimeService: ICodeRuntimeService,
|
||||||
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
|
runtimeIntlService: IRuntimeIntlService,
|
||||||
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
|
runtimeUtilService: IRuntimeUtilService,
|
||||||
) {
|
) {
|
||||||
this.builtInApis = {
|
this.builtInApis = {
|
||||||
get codeRuntime() {
|
get codeRuntime() {
|
||||||
return codeRuntimeService.rootRuntime;
|
return codeRuntimeService.rootRuntime;
|
||||||
},
|
},
|
||||||
intl: this.runtimeIntlService,
|
intl: runtimeIntlService,
|
||||||
util: this.runtimeUtilService,
|
util: runtimeUtilService,
|
||||||
temporaryUse: (name, value) => {
|
temporaryUse: (name, value) => {
|
||||||
this.extend(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 { type Plugin, type PluginContext } from './plugin';
|
||||||
import { IBoostsService } from './boosts';
|
import { BoostsManager } from './boosts';
|
||||||
import { IPackageManagementService } from '../package';
|
import { IPackageManagementService } from '../package';
|
||||||
import { ISchemaService } from '../schema';
|
import { ISchemaService } from '../schema';
|
||||||
import { ILifeCycleService } from '../lifeCycleService';
|
import { ILifeCycleService } from '../lifeCycleService';
|
||||||
import { KeyValueStore } from '../../utils/store';
|
import { ICodeRuntimeService } from '../code-runtime';
|
||||||
|
import { IRuntimeIntlService } from '../runtimeIntlService';
|
||||||
interface IPluginRuntime extends Plugin {
|
import { IRuntimeUtilService } from '../runtimeUtilService';
|
||||||
status: 'setup' | 'ready';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IExtensionHostService {
|
export interface IExtensionHostService {
|
||||||
|
readonly boostsManager: BoostsManager;
|
||||||
|
|
||||||
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
|
||||||
|
|
||||||
getPlugin(name: string): Plugin | undefined;
|
getPlugin(name: string): Plugin | undefined;
|
||||||
|
|
||||||
dispose(): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IExtensionHostService =
|
export const IExtensionHostService =
|
||||||
createDecorator<IExtensionHostService>('pluginManagementService');
|
createDecorator<IExtensionHostService>('pluginManagementService');
|
||||||
|
|
||||||
@Provide(IExtensionHostService)
|
export class ExtensionHostService extends Disposable implements IExtensionHostService {
|
||||||
export class ExtensionHostService implements IExtensionHostService {
|
boostsManager: BoostsManager;
|
||||||
private pluginRuntimes: IPluginRuntime[] = [];
|
|
||||||
|
|
||||||
private eventEmitter: EventEmitter;
|
private _activePlugins = new Set<string>();
|
||||||
|
private _pluginStore = new Map<string, Plugin>();
|
||||||
private pluginSetupContext: PluginContext;
|
private _pluginDependencyGraph = new Graph<string>((name) => name);
|
||||||
|
private _pluginSetupContext: PluginContext;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
lifeCycleService: ILifeCycleService,
|
||||||
@IBoostsService private boostsService: IBoostsService,
|
packageManagementService: IPackageManagementService,
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
schemaService: ISchemaService,
|
||||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
codeRuntimeService: ICodeRuntimeService,
|
||||||
|
runtimeIntlService: IRuntimeIntlService,
|
||||||
|
runtimeUtilService: IRuntimeUtilService,
|
||||||
) {
|
) {
|
||||||
this.eventEmitter = new EventEmitter('ExtensionHost');
|
super();
|
||||||
this.pluginSetupContext = {
|
|
||||||
eventEmitter: this.eventEmitter,
|
|
||||||
globalState: new KeyValueStore(),
|
|
||||||
boosts: this.boostsService.toExpose(),
|
|
||||||
schema: this.schemaService,
|
|
||||||
packageManager: this.packageManagementService,
|
|
||||||
|
|
||||||
whenLifeCylePhaseChange: (phase, listener) => {
|
this.boostsManager = new BoostsManager(
|
||||||
return this.lifeCycleService.when(phase, listener);
|
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[]) {
|
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) {
|
for (const plugin of plugins) {
|
||||||
if (this.pluginRuntimes.find((item) => item.name === plugin.name)) {
|
this._pluginDependencyGraph.lookupOrInsertNode(plugin.name);
|
||||||
console.warn(`${plugin.name} 插件已注册`);
|
|
||||||
continue;
|
if (plugin.dependsOn) {
|
||||||
|
for (const dependency of plugin.dependsOn) {
|
||||||
|
this._pluginDependencyGraph.insertEdge(plugin.name, dependency);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginRuntime = plugin as IPluginRuntime;
|
|
||||||
|
|
||||||
pluginRuntime.status = 'ready';
|
|
||||||
this.pluginRuntimes.push(pluginRuntime);
|
|
||||||
|
|
||||||
await this.doSetupPlugin(pluginRuntime);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doSetupPlugin(pluginRuntime: IPluginRuntime) {
|
while (true) {
|
||||||
if (pluginRuntime.status === 'setup') return;
|
const roots = this._pluginDependencyGraph.roots();
|
||||||
|
|
||||||
const isSetup = (name: string) => {
|
if (roots.length === 0 || roots.every((node) => !this._pluginStore.has(node.data))) {
|
||||||
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');
|
if (this._pluginDependencyGraph.isEmpty()) {
|
||||||
return setupPlugins.some((p) => p.name === name);
|
throw new CyclicDependencyError(this._pluginDependencyGraph);
|
||||||
};
|
}
|
||||||
|
break;
|
||||||
if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await pluginRuntime.setup(this.pluginSetupContext);
|
for (const { data } of roots) {
|
||||||
pluginRuntime.status = 'setup';
|
const plugin = this._pluginStore.get(data);
|
||||||
|
if (plugin) {
|
||||||
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
|
await this._doSetupPlugin(plugin);
|
||||||
const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready');
|
this._pluginDependencyGraph.removeNode(plugin.name);
|
||||||
const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep)));
|
|
||||||
if (readyPlugin) {
|
|
||||||
await this.doSetupPlugin(readyPlugin);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _doSetupPlugin(plugin: Plugin) {
|
||||||
|
if (this._activePlugins.has(plugin.name)) return;
|
||||||
|
|
||||||
|
await plugin.setup(this._pluginSetupContext);
|
||||||
|
this._activePlugins.add(plugin.name);
|
||||||
|
this.addDispose(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
getPlugin(name: string): Plugin | undefined {
|
getPlugin(name: string): Plugin | undefined {
|
||||||
return this.pluginRuntimes.find((item) => item.name === name);
|
return this._pluginStore.get(name);
|
||||||
}
|
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
for (const plugin of this.pluginRuntimes) {
|
|
||||||
await plugin.destory?.();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { type IBoosts } from './boosts';
|
||||||
import { ILifeCycleService } from '../lifeCycleService';
|
import { ILifeCycleService } from '../lifeCycleService';
|
||||||
import { type ISchemaService } from '../schema';
|
import { type ISchemaService } from '../schema';
|
||||||
@ -6,23 +6,29 @@ import { type IPackageManagementService } from '../package';
|
|||||||
import { type IStore } from '../../utils/store';
|
import { type IStore } from '../../utils/store';
|
||||||
|
|
||||||
export interface PluginContext<BoostsExtends = object> {
|
export interface PluginContext<BoostsExtends = object> {
|
||||||
eventEmitter: EventEmitter;
|
|
||||||
globalState: IStore<StringDictionary, string>;
|
globalState: IStore<StringDictionary, string>;
|
||||||
|
|
||||||
boosts: IBoosts<BoostsExtends>;
|
boosts: IBoosts<BoostsExtends>;
|
||||||
|
|
||||||
schema: Pick<ISchemaService, 'get' | 'set'>;
|
schema: Pick<ISchemaService, 'get' | 'set'>;
|
||||||
|
|
||||||
packageManager: IPackageManagementService;
|
packageManager: IPackageManagementService;
|
||||||
/**
|
|
||||||
* 生命周期变更事件
|
|
||||||
*/
|
|
||||||
whenLifeCylePhaseChange: ILifeCycleService['when'];
|
whenLifeCylePhaseChange: ILifeCycleService['when'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plugin<BoostsExtends = object> {
|
export interface Plugin<BoostsExtends = object> extends IDisposable {
|
||||||
/**
|
/**
|
||||||
* 插件的 name 作为唯一标识,并不可重复。
|
* 插件的 name 作为唯一标识,并不可重复。
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* 插件启动函数
|
||||||
|
* @param context 插件能力上下文
|
||||||
|
*/
|
||||||
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
|
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
|
||||||
destory?(): void | Promise<void>;
|
/**
|
||||||
|
* 插件的依赖插件
|
||||||
|
*/
|
||||||
dependsOn?: string[];
|
dependsOn?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type InstanceAccessor } from '@alilc/lowcode-shared';
|
import { type IInstantiationService } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
export interface IRenderObject {
|
export interface IRenderObject {
|
||||||
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
|
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
|
||||||
@ -6,5 +6,5 @@ export interface IRenderObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderAdapter<Render> {
|
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;
|
phase: LifecyclePhase;
|
||||||
|
|
||||||
setPhase(phase: LifecyclePhase): Promise<void>;
|
setPhase(phase: LifecyclePhase): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a promise that resolves when a certain lifecycle phase
|
* Returns a promise that resolves when a certain lifecycle phase
|
||||||
* has started.
|
* has started.
|
||||||
*/
|
*/
|
||||||
when(phase: LifecyclePhase, listener: () => void | Promise<void>): EventDisposable;
|
when(phase: LifecyclePhase): Promise<void>;
|
||||||
|
|
||||||
|
onWillDestory(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LifecyclePhaseToString(phase: LifecyclePhase): string {
|
export function LifecyclePhaseToString(phase: LifecyclePhase): string {
|
||||||
@ -52,9 +54,8 @@ export function LifecyclePhaseToString(phase: LifecyclePhase): string {
|
|||||||
|
|
||||||
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
|
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
|
||||||
|
|
||||||
@Provide(ILifeCycleService)
|
|
||||||
export class LifeCycleService implements ILifeCycleService {
|
export class LifeCycleService implements ILifeCycleService {
|
||||||
private readonly phaseWhen = new EventEmitter();
|
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
|
||||||
|
|
||||||
private _phase = LifecyclePhase.Starting;
|
private _phase = LifecyclePhase.Starting;
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ export class LifeCycleService implements ILifeCycleService {
|
|||||||
return this._phase;
|
return this._phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPhase(value: LifecyclePhase) {
|
setPhase(value: LifecyclePhase) {
|
||||||
if (value < this._phase) {
|
if (value < this._phase) {
|
||||||
throw new Error('Lifecycle cannot go backwards');
|
throw new Error('Lifecycle cannot go backwards');
|
||||||
}
|
}
|
||||||
@ -73,10 +74,26 @@ export class LifeCycleService implements ILifeCycleService {
|
|||||||
|
|
||||||
this._phase = value;
|
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>) {
|
async when(phase: LifecyclePhase): Promise<void> {
|
||||||
return this.phaseWhen.on(LifecyclePhaseToString(phase), listener);
|
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 {
|
import {
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
|
||||||
invariant,
|
invariant,
|
||||||
type ComponentTree,
|
type ComponentTree,
|
||||||
type StringDictionary,
|
type StringDictionary,
|
||||||
@ -33,7 +32,6 @@ export const IComponentTreeModelService = createDecorator<IComponentTreeModelSer
|
|||||||
'componentTreeModelService',
|
'componentTreeModelService',
|
||||||
);
|
);
|
||||||
|
|
||||||
@Provide(IComponentTreeModelService)
|
|
||||||
export class ComponentTreeModelService implements IComponentTreeModelService {
|
export class ComponentTreeModelService implements IComponentTreeModelService {
|
||||||
constructor(
|
constructor(
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
@ISchemaService private schemaService: ISchemaService,
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './loader';
|
export * from './package';
|
||||||
export * from './managementService';
|
export * from './managementService';
|
||||||
|
|||||||
@ -4,16 +4,15 @@ import {
|
|||||||
type ProCodeComponent,
|
type ProCodeComponent,
|
||||||
type ComponentMap,
|
type ComponentMap,
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
|
||||||
specTypes,
|
specTypes,
|
||||||
exportByReference,
|
exportByReference,
|
||||||
mapPackageToUniqueId,
|
mapPackageToUniqueId,
|
||||||
type Reference,
|
type Reference,
|
||||||
|
Disposable,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { get as lodashGet } from 'lodash-es';
|
import { get as lodashGet, differenceWith, isEqual } from 'lodash-es';
|
||||||
import { PackageLoader } from './loader';
|
import { PackageLoader } from './package';
|
||||||
import { ISchemaService } from '../schema';
|
import { ISchemaService } from '../schema';
|
||||||
import { ILifeCycleService } from '../lifeCycleService';
|
|
||||||
|
|
||||||
export interface NormalizedPackage {
|
export interface NormalizedPackage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -50,8 +49,7 @@ export const IPackageManagementService = createDecorator<IPackageManagementServi
|
|||||||
'packageManagementService',
|
'packageManagementService',
|
||||||
);
|
);
|
||||||
|
|
||||||
@Provide(IPackageManagementService)
|
export class PackageManagementService extends Disposable implements IPackageManagementService {
|
||||||
export class PackageManagementService implements IPackageManagementService {
|
|
||||||
private componentsRecord: Record<string, any> = {};
|
private componentsRecord: Record<string, any> = {};
|
||||||
|
|
||||||
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
|
||||||
@ -62,13 +60,18 @@ export class PackageManagementService implements IPackageManagementService {
|
|||||||
|
|
||||||
private packageLoaders: PackageLoader[] = [];
|
private packageLoaders: PackageLoader[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(@ISchemaService private schemaService: ISchemaService) {
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
super();
|
||||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
|
||||||
) {
|
this.addDispose(
|
||||||
this.schemaService.onChange('componentsMap', (componentsMaps) => {
|
this.schemaService.onSchemaUpdate(({ key, previous, data }) => {
|
||||||
this.resolveComponentMaps(componentsMaps);
|
if (key === 'componentsMap') {
|
||||||
});
|
// todo: add remove ...
|
||||||
|
const diff = differenceWith(data, previous, isEqual);
|
||||||
|
if (diff.length > 0) this.resolveComponentMaps(diff);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPackages(packages: Package[]) {
|
async loadPackages(packages: Package[]) {
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
|
||||||
Intl,
|
Intl,
|
||||||
type IntlApi,
|
type IntlApi,
|
||||||
type Locale,
|
type Locale,
|
||||||
type Translations,
|
type Translations,
|
||||||
Platform,
|
type LocaleTranslationsMap,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
|
||||||
import { ICodeRuntimeService } from './code-runtime';
|
import { ICodeRuntimeService } from './code-runtime';
|
||||||
import { ISchemaService } from './schema';
|
|
||||||
|
|
||||||
export interface MessageDescriptor {
|
export interface MessageDescriptor {
|
||||||
key: string;
|
key: string;
|
||||||
@ -18,8 +15,6 @@ export interface MessageDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IRuntimeIntlService {
|
export interface IRuntimeIntlService {
|
||||||
locale: string;
|
|
||||||
|
|
||||||
localize(descriptor: MessageDescriptor): string;
|
localize(descriptor: MessageDescriptor): string;
|
||||||
|
|
||||||
setLocale(locale: Locale): void;
|
setLocale(locale: Locale): void;
|
||||||
@ -31,32 +26,21 @@ export interface IRuntimeIntlService {
|
|||||||
|
|
||||||
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
|
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
|
||||||
|
|
||||||
@Provide(IRuntimeIntlService)
|
|
||||||
export class RuntimeIntlService implements IRuntimeIntlService {
|
export class RuntimeIntlService implements IRuntimeIntlService {
|
||||||
private intl: Intl = new Intl();
|
private intl: Intl = new Intl();
|
||||||
|
|
||||||
public locale: string = Platform.platformLocale;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
defaultLocale: string | undefined,
|
||||||
|
i18nTranslations: LocaleTranslationsMap,
|
||||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
@ISchemaService private schemaService: ISchemaService,
|
|
||||||
) {
|
) {
|
||||||
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
|
if (defaultLocale) this.setLocale(defaultLocale);
|
||||||
const config = this.schemaService.get('config');
|
|
||||||
const i18nTranslations = this.schemaService.get('i18n');
|
|
||||||
|
|
||||||
if (config?.defaultLocale) {
|
for (const key of Object.keys(i18nTranslations)) {
|
||||||
this.setLocale(config.defaultLocale);
|
|
||||||
}
|
|
||||||
if (i18nTranslations) {
|
|
||||||
Object.keys(i18nTranslations).forEach((key) => {
|
|
||||||
this.addTranslations(key, i18nTranslations[key]);
|
this.addTranslations(key, i18nTranslations[key]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.injectScope();
|
this._injectScope();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localize(descriptor: MessageDescriptor): string {
|
localize(descriptor: MessageDescriptor): string {
|
||||||
@ -83,7 +67,7 @@ export class RuntimeIntlService implements IRuntimeIntlService {
|
|||||||
this.intl.addTranslations(locale, translations);
|
this.intl.addTranslations(locale, translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private injectScope(): void {
|
private _injectScope(): void {
|
||||||
const exposed: IntlApi = {
|
const exposed: IntlApi = {
|
||||||
i18n: (key, params) => {
|
i18n: (key, params) => {
|
||||||
return this.localize({ key, params });
|
return this.localize({ key, params });
|
||||||
|
|||||||
@ -2,14 +2,11 @@ import {
|
|||||||
type AnyFunction,
|
type AnyFunction,
|
||||||
type UtilDescription,
|
type UtilDescription,
|
||||||
createDecorator,
|
createDecorator,
|
||||||
Provide,
|
|
||||||
type StringDictionary,
|
type StringDictionary,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { isPlainObject } from 'lodash-es';
|
import { isPlainObject } from 'lodash-es';
|
||||||
import { IPackageManagementService } from './package';
|
import { IPackageManagementService } from './package';
|
||||||
import { ICodeRuntimeService } from './code-runtime';
|
import { ICodeRuntimeService } from './code-runtime';
|
||||||
import { ISchemaService } from './schema';
|
|
||||||
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
|
|
||||||
|
|
||||||
export interface IRuntimeUtilService {
|
export interface IRuntimeUtilService {
|
||||||
add(utilItem: UtilDescription, force?: boolean): void;
|
add(utilItem: UtilDescription, force?: boolean): void;
|
||||||
@ -20,25 +17,18 @@ export interface IRuntimeUtilService {
|
|||||||
|
|
||||||
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
|
||||||
|
|
||||||
@Provide(IRuntimeUtilService)
|
|
||||||
export class RuntimeUtilService implements IRuntimeUtilService {
|
export class RuntimeUtilService implements IRuntimeUtilService {
|
||||||
private utilsMap: Map<string, any> = new Map();
|
private utilsMap: Map<string, any> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
utils: UtilDescription[] = [],
|
||||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
||||||
@IPackageManagementService private packageManagementService: IPackageManagementService,
|
@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) {
|
for (const util of utils) {
|
||||||
this.add(util);
|
this.add(util);
|
||||||
}
|
}
|
||||||
});
|
this.injectScope();
|
||||||
}
|
}
|
||||||
|
|
||||||
add(utilItem: UtilDescription, force?: boolean): void;
|
add(utilItem: UtilDescription, force?: boolean): void;
|
||||||
|
|||||||
@ -1,85 +1,63 @@
|
|||||||
import {
|
import { Disposable, type Project, createDecorator, Events } from '@alilc/lowcode-shared';
|
||||||
type Project,
|
import { isObject, isEqual, get as lodashGet, set as lodashSet } from 'lodash-es';
|
||||||
createDecorator,
|
|
||||||
Provide,
|
|
||||||
EventEmitter,
|
|
||||||
type EventDisposable,
|
|
||||||
} from '@alilc/lowcode-shared';
|
|
||||||
import { isObject } from 'lodash-es';
|
|
||||||
import { schemaValidation } from './validation';
|
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 interface NormalizedSchema extends Project {}
|
||||||
|
|
||||||
export type NormalizedSchemaKey = keyof NormalizedSchema;
|
export type NormalizedSchemaKey = keyof NormalizedSchema;
|
||||||
|
|
||||||
|
export type SchemaUpdateEvent = { key: string; previous: any; data: any };
|
||||||
|
|
||||||
export interface ISchemaService {
|
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>;
|
set(key: string, value: any): void;
|
||||||
|
|
||||||
onChange<K extends NormalizedSchemaKey>(
|
|
||||||
key: K,
|
|
||||||
listener: (v: NormalizedSchema[K]) => void,
|
|
||||||
): EventDisposable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
|
||||||
|
|
||||||
@Provide(ISchemaService)
|
export class SchemaService extends Disposable implements ISchemaService {
|
||||||
export class SchemaService implements ISchemaService {
|
private store: NormalizedSchema;
|
||||||
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
|
|
||||||
NormalizedSchema,
|
|
||||||
NormalizedSchemaKey
|
|
||||||
>({
|
|
||||||
setterValidation: schemaValidation,
|
|
||||||
});
|
|
||||||
|
|
||||||
private notifyEmiiter = new EventEmitter();
|
private _observer = this.addDispose(new Events.Observable<SchemaUpdateEvent>());
|
||||||
|
|
||||||
constructor(
|
readonly onSchemaUpdate = this._observer.subscribe;
|
||||||
@ILifeCycleService private lifeCycleService: ILifeCycleService,
|
|
||||||
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
|
|
||||||
) {
|
|
||||||
this.onChange('constants', (value = {}) => {
|
|
||||||
this.codeRuntimeService.rootRuntime.getScope().set('constants', value);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lifeCycleService.when(LifecyclePhase.Destroying, () => {
|
constructor(schema: unknown) {
|
||||||
this.notifyEmiiter.removeAll();
|
super();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(schema: unknown): void {
|
|
||||||
if (!isObject(schema)) {
|
if (!isObject(schema)) {
|
||||||
throw Error('schema must a object');
|
throw Error('schema must a object');
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(schema).forEach((key) => {
|
this.store = {} as any;
|
||||||
// @ts-expect-error: ignore initialization
|
for (const key of Object.keys(schema)) {
|
||||||
this.set(key, schema[key]);
|
const value = (schema as any)[key];
|
||||||
});
|
|
||||||
|
// 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 : ''}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void> {
|
this.set(key, value);
|
||||||
if (value !== this.get(key)) {
|
|
||||||
this.store.set(key, value);
|
|
||||||
await this.notifyEmiiter.emit(key, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
|
set(key: string, value: any): void {
|
||||||
return this.store.get(key) as NormalizedSchema[K];
|
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>(
|
get<T>(key: string, defaultValue?: T): T {
|
||||||
key: K,
|
return (lodashGet(this.store, key) ?? defaultValue) as T;
|
||||||
listener: (v: NormalizedSchema[K]) => void | Promise<void>,
|
|
||||||
): EventDisposable {
|
|
||||||
return this.notifyEmiiter.on(key, listener);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)) {
|
if (!SCHEMA_KEYS.includes(key)) {
|
||||||
return `schema 的字段名必须是${JSON.stringify(SCHEMA_KEYS)}中的一个`;
|
return `schema 的字段名必须是${JSON.stringify(SCHEMA_KEYS)}中的一个`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validOption = SCHEMA_VALIDATIONS_OPTIONS[key];
|
const validOption = (SCHEMA_VALIDATIONS_OPTIONS as any)[key];
|
||||||
|
|
||||||
if (validOption) {
|
if (validOption) {
|
||||||
const result = validOption.valid(value);
|
const result = validOption.valid(value);
|
||||||
|
|||||||
@ -9,10 +9,6 @@ export interface AppOptions {
|
|||||||
schema: Project;
|
schema: Project;
|
||||||
packages?: Package[];
|
packages?: Package[];
|
||||||
plugins?: Plugin[];
|
plugins?: Plugin[];
|
||||||
/**
|
|
||||||
* 运行模式
|
|
||||||
*/
|
|
||||||
mode?: 'development' | 'production';
|
|
||||||
/**
|
/**
|
||||||
* code runtime 设置选项
|
* code runtime 设置选项
|
||||||
*/
|
*/
|
||||||
@ -32,5 +28,5 @@ export type RendererApplication<Render = unknown> = {
|
|||||||
|
|
||||||
use(plugin: Plugin): Promise<void>;
|
use(plugin: Plugin): Promise<void>;
|
||||||
|
|
||||||
destroy(): Promise<void>;
|
destroy(): void;
|
||||||
} & Render;
|
} & 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": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src", "../shared/src/utils/node.ts"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,5 @@
|
|||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"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"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abraham/reflection": "^0.12.0",
|
|
||||||
"@formatjs/intl": "^2.10.2",
|
"@formatjs/intl": "^2.10.2",
|
||||||
"@vue/reactivity": "^3.4.23",
|
"@vue/reactivity": "^3.4.23",
|
||||||
"inversify": "^6.0.2",
|
|
||||||
"inversify-binding-decorators": "^4.0.0",
|
|
||||||
"hookable": "^5.5.3",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"store": "^2.0.12"
|
"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 class Observable<T> {
|
||||||
export type EventListener<T = any> = (...arguments_: ArrayT<T>) => Promise<void> | void;
|
private _isDisposed = false;
|
||||||
export type EventDisposable = () => void;
|
|
||||||
|
|
||||||
export interface IEmitter<T = any> {
|
private _event?: Event<T>;
|
||||||
on: Event<T>;
|
private _listeners?: Set<(arg: T) => void>;
|
||||||
emit(...args: ArrayT<T>): void;
|
|
||||||
emitAsync(...args: ArrayT<T>): Promise<void>;
|
dispose(): void {
|
||||||
clear(): void;
|
if (this._isDisposed) return;
|
||||||
|
|
||||||
|
this._listeners?.clear();
|
||||||
|
this._listeners = undefined;
|
||||||
|
this._event = undefined;
|
||||||
|
this._isDisposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Emitter<T = any[]> implements IEmitter<T> {
|
notify(arg: T): void {
|
||||||
private events: EventListener<T>[] = [];
|
if (this._isDisposed) return;
|
||||||
|
|
||||||
on(fn: EventListener<T>): EventDisposable {
|
this._listeners?.forEach((listener) => listener(arg));
|
||||||
this.events.push(fn);
|
}
|
||||||
|
|
||||||
return () => {
|
/**
|
||||||
this.events = this.events.filter((e) => e !== fn);
|
* 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);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(...args: ArrayT<T>) {
|
return this._event;
|
||||||
for (const event of this.events) {
|
|
||||||
event.call(null, ...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async emitAsync(...args: ArrayT<T>) {
|
private _removeListener(listener: (arg: T) => void) {
|
||||||
for (const event of this.events) {
|
if (this._isDisposed) return;
|
||||||
await event.call(null, ...args);
|
|
||||||
|
if (this._listeners?.has(listener)) {
|
||||||
|
this._listeners.delete(listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 './logger';
|
||||||
export * from './intl';
|
export * from './intl';
|
||||||
export * from './instantiation';
|
export * from './instantiation';
|
||||||
export * from './signals';
|
|
||||||
|
export * from './keyCodes';
|
||||||
|
export * from './errors';
|
||||||
|
export * from './disposable';
|
||||||
|
|
||||||
export * from './linkedList';
|
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 { type BeanIdentifier, type Constructor, mapDepsToBeanId } from './container';
|
||||||
import { fluentProvide } from 'inversify-binding-decorators';
|
|
||||||
|
|
||||||
/**
|
const idsMap = new Map<string, BeanIdentifier<any>>();
|
||||||
* Identifies a service of type `T`.
|
|
||||||
*/
|
export function createDecorator<T>(beanId: string): BeanIdentifier<T> {
|
||||||
export interface ServiceIdentifier<T> {
|
if (idsMap.has(beanId)) {
|
||||||
(...args: any[]): void;
|
return idsMap.get(beanId)!;
|
||||||
type: T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Constructor<T = any> = new (...args: any[]) => T;
|
const id = <any>function (target: Constructor, _: string, indexOrPropertyDescriptor: any): any {
|
||||||
|
return mapDepsToBeanId(id, target, indexOrPropertyDescriptor);
|
||||||
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
};
|
||||||
const id = <any>(
|
id.toString = () => beanId;
|
||||||
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
|
||||||
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
id.toString = () => serviceId;
|
|
||||||
|
|
||||||
|
idsMap.set(beanId, id);
|
||||||
return 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 './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 { isDisposable } from '../disposable';
|
||||||
import { Container, interfaces, injectable } from 'inversify';
|
import { Graph, CyclicDependencyError } from '../graph';
|
||||||
import { buildProviderModule } from 'inversify-binding-decorators';
|
import {
|
||||||
import { ServiceIdentifier, Constructor, createDecorator } from './decorators';
|
type BeanIdentifier,
|
||||||
|
BeanContainer,
|
||||||
|
type Constructor,
|
||||||
|
getBeanDependecies,
|
||||||
|
CtorDescriptor,
|
||||||
|
} from './container';
|
||||||
|
import { createDecorator } from './decorators';
|
||||||
|
|
||||||
export interface InstanceAccessor {
|
export interface InstanceAccessor {
|
||||||
get<T>(id: ServiceIdentifier<T>): T;
|
get<T>(id: BeanIdentifier<T>): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstantiationService {
|
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, Args extends any[] = []>(
|
||||||
|
fn: (accessor: InstanceAccessor, ...args: Args) => R,
|
||||||
invokeFunction<R, TS extends any[] = []>(
|
...args: Args
|
||||||
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
|
||||||
...args: TS
|
|
||||||
): R;
|
): R;
|
||||||
|
|
||||||
createInstance<T extends Constructor>(App: T): InstanceType<T>;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
|
export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
|
||||||
|
|
||||||
export class InstantiationService implements IInstantiationService {
|
export class InstantiationService implements IInstantiationService {
|
||||||
container: Container;
|
private _activeInstantiations = new Set<BeanIdentifier<any>>();
|
||||||
|
|
||||||
constructor(options?: interfaces.ContainerOptions) {
|
private _isDisposed = false;
|
||||||
this.container = new Container(options);
|
private readonly _beansToMaybeDispose = new Set<any>();
|
||||||
this.set(IInstantiationService, this);
|
|
||||||
this.container.load(buildProviderModule());
|
|
||||||
}
|
|
||||||
|
|
||||||
get<T>(serviceIdentifier: ServiceIdentifier<T>) {
|
constructor(public readonly container: BeanContainer = new BeanContainer()) {
|
||||||
return this.container.get<T>(serviceIdentifier.toString());
|
this.container.set(IInstantiationService, this);
|
||||||
}
|
|
||||||
|
|
||||||
set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void {
|
|
||||||
this.container.bind<T>(serviceIdentifier).toConstantValue(instance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,20 +45,166 @@ export class InstantiationService implements IInstantiationService {
|
|||||||
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
||||||
...args: TS
|
...args: TS
|
||||||
): R {
|
): R {
|
||||||
|
this._throwIfDisposed();
|
||||||
|
|
||||||
const accessor: InstanceAccessor = {
|
const accessor: InstanceAccessor = {
|
||||||
get: (id) => {
|
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);
|
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}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstance<T extends Constructor>(App: T) {
|
beanArgs.push(instance);
|
||||||
injectable()(App);
|
}
|
||||||
return this.container.resolve<InstanceType<T>>(App);
|
|
||||||
|
// 检查传入参数的个数,进行参数微调
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
export let platform: PlatformEnum = PlatformEnum.Unknown;
|
||||||
if (isMacintosh) {
|
if (isMacintosh) {
|
||||||
platform = PlatformEnum.Mac;
|
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 './async';
|
||||||
export * from './node';
|
export * from './node';
|
||||||
export * from './resource';
|
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 {
|
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
|
||||||
return !iterable || iterable[Symbol.iterator]().next().done === true;
|
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,
|
"allowJs": true,
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user