feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type interfaces } from 'inversify';
|
||||
|
||||
export const ContainerFactory = Symbol('ContainerFactory');
|
||||
|
||||
export interface ContainerFactory {
|
||||
createChild: interfaces.Container['createChild'];
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
|
||||
export enum ContextKey {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
editorFocus = 'editorFocus',
|
||||
}
|
||||
|
||||
export const ContextMatcher = Symbol('ContextMatcher');
|
||||
|
||||
export interface ContextMatcher {
|
||||
/**
|
||||
* 判断 expression 是否命中上下文
|
||||
*/
|
||||
match: (expression: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 context key 上下文管理
|
||||
*/
|
||||
@injectable()
|
||||
export class ContextKeyService implements ContextMatcher {
|
||||
private _contextKeys: Map<string, unknown> = new Map();
|
||||
|
||||
public constructor() {
|
||||
this._contextKeys.set(ContextKey.editorFocus, true);
|
||||
}
|
||||
|
||||
public setContext(key: string, value: unknown): void {
|
||||
this._contextKeys.set(key, value);
|
||||
}
|
||||
|
||||
public getContext<T>(key: string): T {
|
||||
return this._contextKeys.get(key) as T;
|
||||
}
|
||||
|
||||
public match(expression: string): boolean {
|
||||
const keys = Array.from(this._contextKeys.keys());
|
||||
const func = new Function(...keys, `return ${expression};`);
|
||||
const res = func(...keys.map(k => this._contextKeys.get(k)));
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
41
frontend/packages/project-ide/core/src/common/index.ts
Normal file
41
frontend/packages/project-ide/core/src/common/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
createLifecyclePlugin,
|
||||
definePluginCreator,
|
||||
loadPlugins,
|
||||
Plugin,
|
||||
PluginContext,
|
||||
type PluginCreator,
|
||||
type PluginsProvider,
|
||||
type PluginConfig,
|
||||
type PluginBindConfig,
|
||||
} from './plugin';
|
||||
export {
|
||||
ContextKey,
|
||||
ContextKeyService,
|
||||
ContextMatcher,
|
||||
} from './context-key-service';
|
||||
|
||||
export { LifecycleContribution } from './lifecycle-contribution';
|
||||
export { OpenerService, OpenHandler, type OpenerOptions } from './open-service';
|
||||
export { ContainerFactory } from './container-factory';
|
||||
export { StorageService, LocalStorageService } from './storage-service';
|
||||
export { WindowService } from './window-service';
|
||||
export { Path } from './path';
|
||||
export { URI, URIHandler } from './uri';
|
||||
export { prioritizeAllSync, prioritizeAll } from './prioritizeable';
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
import { type MaybePromise } from '@flowgram-adapter/common';
|
||||
|
||||
export const LifecycleContribution = Symbol('LifecycleContribution');
|
||||
/**
|
||||
* IDE 全局生命周期注册
|
||||
*/
|
||||
export interface LifecycleContribution {
|
||||
/**
|
||||
* IDE 注册阶段
|
||||
*/
|
||||
onInit?(): void;
|
||||
/**
|
||||
* IDE loading 阶段, 一般用于加载全局配置,如 i18n 数据
|
||||
*/
|
||||
onLoading?(): MaybePromise<void>;
|
||||
/**
|
||||
* IDE 布局初始化阶段,在 onLoading 之后执行
|
||||
*/
|
||||
onLayoutInit?(): MaybePromise<void>;
|
||||
/**
|
||||
* IDE 开始执行, 可以加载业务逻辑
|
||||
*/
|
||||
onStart?(): MaybePromise<void>;
|
||||
/**
|
||||
* 在浏览器 `beforeunload` 之前执行,如果返回true,则会阻止
|
||||
*/
|
||||
onWillDispose?(): boolean | void;
|
||||
/**
|
||||
* IDE 销毁
|
||||
*/
|
||||
onDispose?(): void;
|
||||
}
|
||||
128
frontend/packages/project-ide/core/src/common/open-service.ts
Normal file
128
frontend/packages/project-ide/core/src/common/open-service.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { named, injectable, inject } from 'inversify';
|
||||
import {
|
||||
ContributionProvider,
|
||||
type MaybePromise,
|
||||
Emitter,
|
||||
Disposable,
|
||||
type Event,
|
||||
} from '@flowgram-adapter/common';
|
||||
|
||||
import { type URI } from './uri';
|
||||
import { prioritizeAll } from './prioritizeable';
|
||||
|
||||
export interface OpenerOptions {}
|
||||
|
||||
export const OpenHandler = Symbol('OpenHandler');
|
||||
export interface OpenHandler {
|
||||
canHandle: (uri: URI, options?: OpenerOptions) => MaybePromise<number>;
|
||||
open: (uri: URI, options?: OpenerOptions) => MaybePromise<object | undefined>;
|
||||
}
|
||||
|
||||
export const OpenerService = Symbol('OpenerService');
|
||||
export interface OpenerService {
|
||||
/**
|
||||
* 跳转定位
|
||||
* @param uri
|
||||
* @param options
|
||||
*/
|
||||
open: (uri: URI, options?: OpenerOptions) => Promise<object | undefined>;
|
||||
|
||||
/**
|
||||
* 某个请求触发
|
||||
*/
|
||||
onURIOpen: Event<{ uri: URI; options?: OpenerOptions }>;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DefaultOpenerService implements OpenerService {
|
||||
protected readonly customEditorOpenHandlers: OpenHandler[] = [];
|
||||
|
||||
protected readonly onDidChangeOpenersEmitter = new Emitter<void>();
|
||||
|
||||
protected readonly onURIOpenEmitter = new Emitter<{
|
||||
uri: URI;
|
||||
options?: OpenerOptions;
|
||||
}>();
|
||||
|
||||
readonly onDidChangeOpeners = this.onDidChangeOpenersEmitter.event;
|
||||
|
||||
readonly onURIOpen = this.onURIOpenEmitter.event;
|
||||
|
||||
constructor(
|
||||
@inject(ContributionProvider)
|
||||
@named(OpenHandler)
|
||||
protected readonly handlersProvider: ContributionProvider<OpenHandler>,
|
||||
) {}
|
||||
|
||||
async open(uri: URI, options?: OpenerOptions): Promise<object | undefined> {
|
||||
const opener = await this.getOpener(uri, options);
|
||||
const result = await opener.open(uri, options);
|
||||
this.onURIOpenEmitter.fire({ uri, options });
|
||||
return result;
|
||||
}
|
||||
|
||||
addHandler(openHandler: OpenHandler): Disposable {
|
||||
this.customEditorOpenHandlers.push(openHandler);
|
||||
this.onDidChangeOpenersEmitter.fire();
|
||||
|
||||
return Disposable.create(() => {
|
||||
this.customEditorOpenHandlers.splice(
|
||||
this.customEditorOpenHandlers.indexOf(openHandler),
|
||||
1,
|
||||
);
|
||||
this.onDidChangeOpenersEmitter.fire();
|
||||
});
|
||||
}
|
||||
|
||||
protected async prioritize(
|
||||
uri: URI,
|
||||
options?: OpenerOptions,
|
||||
): Promise<OpenHandler[]> {
|
||||
const prioritized = await prioritizeAll<any>(
|
||||
this.getHandlers(),
|
||||
async (handler: any) => {
|
||||
try {
|
||||
return await handler.canHandle(uri, options);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
return prioritized.map((p: any) => p.value) as OpenHandler[];
|
||||
}
|
||||
|
||||
async getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler> {
|
||||
const handlers = await this.prioritize(uri, options);
|
||||
if (handlers.length >= 1) {
|
||||
return handlers[0];
|
||||
}
|
||||
return Promise.reject(new Error(`There is no opener for ${uri}.`));
|
||||
}
|
||||
|
||||
async getOpeners(uri?: URI, options?: OpenerOptions): Promise<OpenHandler[]> {
|
||||
return uri ? this.prioritize(uri, options) : this.getHandlers();
|
||||
}
|
||||
|
||||
protected getHandlers(): OpenHandler[] {
|
||||
return [
|
||||
...this.handlersProvider.getContributions(),
|
||||
...this.customEditorOpenHandlers,
|
||||
];
|
||||
}
|
||||
}
|
||||
223
frontend/packages/project-ide/core/src/common/path.ts
Normal file
223
frontend/packages/project-ide/core/src/common/path.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/prefer-as-const */
|
||||
export class Path {
|
||||
static separator: '/' = '/';
|
||||
|
||||
readonly isAbsolute: boolean;
|
||||
|
||||
readonly isRoot: boolean;
|
||||
|
||||
readonly root: Path | undefined;
|
||||
|
||||
readonly base: string;
|
||||
|
||||
readonly name: string;
|
||||
|
||||
readonly ext: string;
|
||||
|
||||
private readonly raw: string;
|
||||
|
||||
/**
|
||||
* The raw should be normalized, meaning that only '/' is allowed as a path separator.
|
||||
*/
|
||||
constructor(raw: string) {
|
||||
this.raw = Path.normalizeDrive(raw);
|
||||
const firstIndex = raw.indexOf(Path.separator);
|
||||
const lastIndex = raw.lastIndexOf(Path.separator);
|
||||
this.isAbsolute = firstIndex === 0;
|
||||
this.base = lastIndex === -1 ? raw : raw.substr(lastIndex + 1);
|
||||
this.isRoot =
|
||||
this.isAbsolute &&
|
||||
firstIndex === lastIndex &&
|
||||
(!this.base || Path.isDrive(this.base));
|
||||
this.root = this.computeRoot();
|
||||
|
||||
const extIndex = this.base.lastIndexOf('.');
|
||||
this.name = extIndex === -1 ? this.base : this.base.substr(0, extIndex);
|
||||
this.ext = extIndex === -1 ? '' : this.base.substr(extIndex);
|
||||
}
|
||||
|
||||
private _dir: Path;
|
||||
|
||||
/**
|
||||
* Returns the parent directory if it exists (`hasDir === true`) or `this` otherwise.
|
||||
*/
|
||||
get dir(): Path {
|
||||
if (this._dir === undefined) {
|
||||
this._dir = this.computeDir();
|
||||
}
|
||||
return this._dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this has a parent directory, `false` otherwise.
|
||||
*
|
||||
* _This implementation returns `true` if and only if this is not the root dir and
|
||||
* there is a path separator in the raw path._
|
||||
*/
|
||||
get hasDir(): boolean {
|
||||
return !this.isRoot && this.raw.lastIndexOf(Path.separator) !== -1;
|
||||
}
|
||||
|
||||
static isDrive(segment: string): boolean {
|
||||
return segment.endsWith(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* vscode-uri always normalizes drive letters to lower case:
|
||||
* https://github.com/Microsoft/vscode-uri/blob/b1d3221579f97f28a839b6f996d76fc45e9964d8/src/index.ts#L1025
|
||||
* Theia path should be adjusted to this.
|
||||
*/
|
||||
static normalizeDrive(path: string): string {
|
||||
// lower-case windows drive letters in /C:/fff or C:/fff
|
||||
if (
|
||||
path.length >= 3 &&
|
||||
path.charCodeAt(0) === 47 /* '/' */ &&
|
||||
path.charCodeAt(2) === 58 /* ':' */
|
||||
) {
|
||||
const code = path.charCodeAt(1);
|
||||
if (code >= 65 /* A */ && code <= 90 /* Z */) {
|
||||
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
|
||||
}
|
||||
} else if (path.length >= 2 && path.charCodeAt(1) === 58 /* ':' */) {
|
||||
const code = path.charCodeAt(0);
|
||||
if (code >= 65 /* A */ && code <= 90 /* Z */) {
|
||||
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
join(...paths: string[]): Path {
|
||||
const relativePath = paths.filter(s => !!s).join(Path.separator);
|
||||
if (!relativePath) {
|
||||
return this;
|
||||
}
|
||||
if (this.raw.endsWith(Path.separator)) {
|
||||
return new Path(this.raw + relativePath);
|
||||
}
|
||||
return new Path(this.raw + Path.separator + relativePath);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.raw;
|
||||
}
|
||||
|
||||
relative(path: Path): Path | undefined {
|
||||
if (this.raw === path.raw) {
|
||||
return new Path('');
|
||||
}
|
||||
if (!this.raw || !path.raw) {
|
||||
return undefined;
|
||||
}
|
||||
const raw = this.base ? this.raw + Path.separator : this.raw;
|
||||
if (!path.raw.startsWith(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const relativePath = path.raw.substr(raw.length);
|
||||
return new Path(relativePath);
|
||||
}
|
||||
|
||||
isEqualOrParent(path: Path): boolean {
|
||||
return !!this.relative(path);
|
||||
}
|
||||
|
||||
relativity(path: Path): number {
|
||||
const relative = this.relative(path);
|
||||
if (relative) {
|
||||
const relativeStr = relative.toString();
|
||||
if (relativeStr === '') {
|
||||
return 0;
|
||||
}
|
||||
return relativeStr.split(Path.separator).length;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* return a normalized Path, resolving '..' and '.' segments
|
||||
*/
|
||||
normalize(): Path {
|
||||
const trailingSlash = this.raw.endsWith('/');
|
||||
const pathArray = this.toString().split('/');
|
||||
const resultArray: string[] = [];
|
||||
pathArray.forEach((value, index) => {
|
||||
if (!value || value === '.') {
|
||||
return;
|
||||
}
|
||||
if (value === '..') {
|
||||
if (
|
||||
resultArray.length &&
|
||||
resultArray[resultArray.length - 1] !== '..'
|
||||
) {
|
||||
resultArray.pop();
|
||||
} else if (!this.isAbsolute) {
|
||||
resultArray.push('..');
|
||||
}
|
||||
} else {
|
||||
resultArray.push(value);
|
||||
}
|
||||
});
|
||||
if (resultArray.length === 0) {
|
||||
if (this.isRoot) {
|
||||
return new Path('/');
|
||||
} else {
|
||||
return new Path('.');
|
||||
}
|
||||
}
|
||||
return new Path(
|
||||
(this.isAbsolute ? '/' : '') +
|
||||
resultArray.join('/') +
|
||||
(trailingSlash ? '/' : ''),
|
||||
);
|
||||
}
|
||||
|
||||
protected computeRoot(): Path | undefined {
|
||||
// '/' -> '/'
|
||||
// '/c:' -> '/c:'
|
||||
if (this.isRoot) {
|
||||
return this;
|
||||
}
|
||||
// 'foo/bar' -> `undefined`
|
||||
if (!this.isAbsolute) {
|
||||
return undefined;
|
||||
}
|
||||
const index = this.raw.indexOf(Path.separator, Path.separator.length);
|
||||
if (index === -1) {
|
||||
// '/foo/bar' -> '/'
|
||||
return new Path(Path.separator);
|
||||
}
|
||||
// '/c:/foo/bar' -> '/c:'
|
||||
// '/foo/bar' -> '/'
|
||||
return new Path(this.raw.substr(0, index)).root;
|
||||
}
|
||||
|
||||
protected computeDir(): Path {
|
||||
if (!this.hasDir) {
|
||||
return this;
|
||||
}
|
||||
const lastIndex = this.raw.lastIndexOf(Path.separator);
|
||||
if (this.isAbsolute) {
|
||||
const firstIndex = this.raw.indexOf(Path.separator);
|
||||
if (firstIndex === lastIndex) {
|
||||
return new Path(this.raw.substr(0, firstIndex + 1));
|
||||
}
|
||||
}
|
||||
return new Path(this.raw.substr(0, lastIndex));
|
||||
}
|
||||
}
|
||||
245
frontend/packages/project-ide/core/src/common/plugin.ts
Normal file
245
frontend/packages/project-ide/core/src/common/plugin.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule, type interfaces } from 'inversify';
|
||||
import type { MaybePromise } from '@flowgram-adapter/common';
|
||||
|
||||
import { LifecycleContribution } from './lifecycle-contribution';
|
||||
|
||||
export interface PluginContext {
|
||||
/**
|
||||
* 获取 IOC 容器
|
||||
*/
|
||||
container: interfaces.Container;
|
||||
/**
|
||||
* 获取 IOC 容器的 单例模块
|
||||
* @param identifier
|
||||
*/
|
||||
get: <T>(identifier: interfaces.ServiceIdentifier<T>) => T;
|
||||
|
||||
/**
|
||||
* 获取 IOC 容器的 多例模块
|
||||
*/
|
||||
getAll: <T>(identifier: interfaces.ServiceIdentifier<T>) => T[];
|
||||
}
|
||||
|
||||
export const PluginContext = Symbol('PluginContext');
|
||||
|
||||
export interface PluginBindConfig {
|
||||
bind: interfaces.Bind;
|
||||
unbind: interfaces.Unbind;
|
||||
isBound: interfaces.IsBound;
|
||||
rebind: interfaces.Rebind;
|
||||
}
|
||||
|
||||
interface PluginLifeCycle<CTX extends PluginContext, OPTS> {
|
||||
/**
|
||||
* IDE 注册阶段
|
||||
*/
|
||||
onInit?: (ctx: CTX, opts: OPTS) => void;
|
||||
/**
|
||||
* IDE loading 阶段, 一般用于加载全局配置,如 i18n 数据
|
||||
*/
|
||||
onLoading?: (ctx: CTX, opts: OPTS) => MaybePromise<void>;
|
||||
/**
|
||||
* IDE 布局初始化阶段,在 onLoading 之后执行
|
||||
*/
|
||||
onLayoutInit?: (ctx: CTX, opts: OPTS) => MaybePromise<void>;
|
||||
/**
|
||||
* IDE 开始执行, 可以加载业务逻辑
|
||||
*/
|
||||
onStart?: (ctx: CTX, opts: OPTS) => MaybePromise<void>;
|
||||
/**
|
||||
* 在浏览器 `beforeunload` 之前执行,如果返回true,则会阻止
|
||||
*/
|
||||
onWillDispose?: (ctx: CTX, opts: OPTS) => boolean | void;
|
||||
/**
|
||||
* IDE 销毁
|
||||
*/
|
||||
onDispose?: (ctx: CTX, opts: OPTS) => void;
|
||||
}
|
||||
|
||||
export interface PluginConfig<OPTS, CTX extends PluginContext = PluginContext>
|
||||
extends PluginLifeCycle<CTX, OPTS> {
|
||||
/**
|
||||
* 插件 IOC 注册, 等价于 containerModule
|
||||
* @param ctx
|
||||
*/
|
||||
onBind?: (bindConfig: PluginBindConfig, opts: OPTS) => void;
|
||||
/**
|
||||
* IOC 模块,用于更底层的插件扩展
|
||||
*/
|
||||
containerModules?: interfaces.ContainerModule[];
|
||||
}
|
||||
|
||||
export const Plugin = Symbol('Plugin');
|
||||
|
||||
export interface Plugin<Options = any> {
|
||||
options: Options;
|
||||
pluginId: string;
|
||||
initPlugin: () => void;
|
||||
contributionKeys?: interfaces.ServiceIdentifier[];
|
||||
containerModules?: interfaces.ContainerModule[];
|
||||
}
|
||||
|
||||
export interface PluginsProvider<CTX extends PluginContext = PluginContext> {
|
||||
(ctx: CTX): Plugin[];
|
||||
}
|
||||
|
||||
export type PluginCreator<Options> = (opts: Options) => Plugin<Options>;
|
||||
|
||||
export function loadPlugins(
|
||||
plugins: Plugin[],
|
||||
container: interfaces.Container,
|
||||
): void {
|
||||
const pluginInitSet = new Set<string>();
|
||||
const modules: interfaces.ContainerModule[] = plugins.reduce(
|
||||
(res, plugin) => {
|
||||
if (!pluginInitSet.has(plugin.pluginId)) {
|
||||
plugin.initPlugin();
|
||||
pluginInitSet.add(plugin.pluginId);
|
||||
}
|
||||
if (plugin.containerModules && plugin.containerModules.length > 0) {
|
||||
for (const module of plugin.containerModules) {
|
||||
// 去重
|
||||
if (!res.includes(module)) {
|
||||
res.push(module);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
[] as interfaces.ContainerModule[],
|
||||
);
|
||||
modules.forEach(module => container.load(module));
|
||||
plugins.forEach(plugin => {
|
||||
if (plugin.contributionKeys) {
|
||||
for (const contribution of plugin.contributionKeys) {
|
||||
container.bind(contribution).toConstantValue(plugin.options);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toLifecycleContainerModule<
|
||||
Options,
|
||||
CTX extends PluginContext = PluginContext,
|
||||
>(
|
||||
config: PluginLifeCycle<CTX, Options>,
|
||||
opts: Options,
|
||||
): interfaces.ContainerModule {
|
||||
return new ContainerModule(bind => {
|
||||
bind(LifecycleContribution).toDynamicValue(ctx => {
|
||||
const pluginContext = ctx.container.get<CTX>(PluginContext)!;
|
||||
return {
|
||||
onInit: () => config.onInit?.(pluginContext, opts),
|
||||
onLoading: () => config.onLoading?.(pluginContext, opts),
|
||||
onLayoutInit: () => config.onLayoutInit?.(pluginContext, opts),
|
||||
onStart: () => config.onStart?.(pluginContext, opts),
|
||||
onWillDispose: () => config.onWillDispose?.(pluginContext, opts),
|
||||
onDispose: () => config.onDispose?.(pluginContext, opts),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let pluginIndex = 0;
|
||||
export function definePluginCreator<
|
||||
Options,
|
||||
CTX extends PluginContext = PluginContext,
|
||||
>(
|
||||
config: {
|
||||
containerModules?: interfaces.ContainerModule[];
|
||||
contributionKeys?: interfaces.ServiceIdentifier[];
|
||||
} & PluginConfig<Options, CTX>,
|
||||
): PluginCreator<Options> {
|
||||
const { contributionKeys } = config;
|
||||
pluginIndex += 1;
|
||||
const pluginId = `IDE_${pluginIndex}`;
|
||||
return (opts: Options) => {
|
||||
const containerModules: interfaces.ContainerModule[] = [];
|
||||
let isInit = false;
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
initPlugin: () => {
|
||||
// 防止 plugin 被上层业务多次 init
|
||||
if (isInit) {
|
||||
return;
|
||||
}
|
||||
isInit = true;
|
||||
|
||||
if (config.containerModules) {
|
||||
containerModules.push(...config.containerModules);
|
||||
}
|
||||
if (config.onBind) {
|
||||
containerModules.push(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
config.onBind!(
|
||||
{
|
||||
bind,
|
||||
unbind,
|
||||
isBound,
|
||||
rebind,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (
|
||||
config.onInit ||
|
||||
config.onLoading ||
|
||||
config.onLayoutInit ||
|
||||
config.onStart ||
|
||||
config.onWillDispose ||
|
||||
config.onDispose
|
||||
) {
|
||||
containerModules.push(
|
||||
toLifecycleContainerModule<Options, CTX>(config, opts),
|
||||
);
|
||||
}
|
||||
},
|
||||
options: opts,
|
||||
contributionKeys,
|
||||
containerModules,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* createLifecyclePlugin({
|
||||
* // IOC 注册
|
||||
* onBind(bind) {
|
||||
* bind('xxx').toSelf().inSingletonScope()
|
||||
* },
|
||||
* // IDE 初始化
|
||||
* onInit() {
|
||||
* },
|
||||
* // IDE 销毁
|
||||
* onDispose() {
|
||||
* },
|
||||
* // IOC 模块
|
||||
* containerModules: [new ContainerModule(() => {})]
|
||||
* })
|
||||
*/
|
||||
export const createLifecyclePlugin = <
|
||||
CTX extends PluginContext = PluginContext,
|
||||
>(
|
||||
options: PluginConfig<undefined, CTX>,
|
||||
) => definePluginCreator<undefined, CTX>(options)(undefined);
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
compare,
|
||||
isValid,
|
||||
toPriority,
|
||||
prioritizeAll,
|
||||
toPrioritySync,
|
||||
prioritizeAllSync,
|
||||
} from './prioritizeable';
|
||||
|
||||
describe('Priority', () => {
|
||||
test('Priority.compare', () => {
|
||||
expect(
|
||||
compare({ priority: 1, value: 1 }, { priority: 2, value: 1 }),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Priority.isValid', () => {
|
||||
expect(isValid({ priority: 1, value: 1 })).toBeTruthy();
|
||||
expect(isValid({ priority: 0, value: 1 })).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Priority.toPriority', async () => {
|
||||
expect(await toPriority(2, async () => 1)).toEqual({
|
||||
priority: 1,
|
||||
value: 2,
|
||||
});
|
||||
expect(await toPriority([2, 3], async () => 1)).toEqual([
|
||||
{ priority: 1, value: 2 },
|
||||
{ priority: 1, value: 3 },
|
||||
]);
|
||||
expect(await prioritizeAll([2, 3], async () => 1)).toEqual([
|
||||
{ priority: 1, value: 2 },
|
||||
{ priority: 1, value: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Priority.toPrioritySync', async () => {
|
||||
expect(await toPrioritySync([2, 3], () => 1)).toEqual([
|
||||
{ priority: 1, value: 2 },
|
||||
{ priority: 1, value: 3 },
|
||||
]);
|
||||
expect(await prioritizeAllSync([2, 3], () => 1)).toEqual([
|
||||
{ priority: 1, value: 2 },
|
||||
{ priority: 1, value: 3 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MaybeArray, type MaybePromise } from '@flowgram-adapter/common';
|
||||
|
||||
export interface Priority<T> {
|
||||
readonly priority: number;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
type GetPriority<T> = (value: T) => MaybePromise<number>;
|
||||
type GetPrioritySync<T> = (value: T) => number;
|
||||
|
||||
export function isValid<T>(p: Priority<T>): boolean {
|
||||
return p.priority > 0;
|
||||
}
|
||||
|
||||
export function compare<T>(p: Priority<T>, p2: Priority<T>): number {
|
||||
return p2.priority - p.priority;
|
||||
}
|
||||
|
||||
export async function toPriority<T>(
|
||||
rawValue: MaybePromise<T>,
|
||||
getPriority: GetPriority<T>,
|
||||
): Promise<Priority<T>>;
|
||||
export async function toPriority<T>(
|
||||
rawValue: MaybePromise<T>[],
|
||||
getPriority: GetPriority<T>,
|
||||
): Promise<Priority<T>[]>;
|
||||
export async function toPriority<T>(
|
||||
rawValue: MaybeArray<MaybePromise<T>>,
|
||||
getPriority: GetPriority<T>,
|
||||
): Promise<MaybeArray<Priority<T>>> {
|
||||
if (rawValue instanceof Array) {
|
||||
return Promise.all(rawValue.map(v => toPriority(v, getPriority)));
|
||||
}
|
||||
const value = await rawValue;
|
||||
const priority = await getPriority(value);
|
||||
return { priority, value };
|
||||
}
|
||||
|
||||
export function toPrioritySync<T>(
|
||||
rawValue: T[],
|
||||
getPriority: GetPrioritySync<T>,
|
||||
): Priority<T>[] {
|
||||
return rawValue.map(v => ({
|
||||
value: v,
|
||||
priority: getPriority(v),
|
||||
}));
|
||||
}
|
||||
|
||||
export function prioritizeAllSync<T>(
|
||||
values: T[],
|
||||
getPriority: GetPrioritySync<T>,
|
||||
): Priority<T>[] {
|
||||
const priority = toPrioritySync(values, getPriority);
|
||||
return priority.filter(isValid).sort(compare);
|
||||
}
|
||||
|
||||
export async function prioritizeAll<T>(
|
||||
values: MaybePromise<T>[],
|
||||
getPriority: GetPriority<T>,
|
||||
): Promise<Priority<T>[]> {
|
||||
const priority = await toPriority(values, getPriority);
|
||||
return priority.filter(isValid).sort(compare);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable, postConstruct } from 'inversify';
|
||||
|
||||
export const StorageService = Symbol('StorageService');
|
||||
|
||||
/**
|
||||
* 存储数据到缓存
|
||||
*/
|
||||
export interface StorageService {
|
||||
/**
|
||||
* Stores the given data under the given key.
|
||||
*/
|
||||
setData: <T>(key: string, data: T) => void;
|
||||
|
||||
/**
|
||||
* Returns the data stored for the given key or the provided default value if nothing is stored for the given key.
|
||||
*/
|
||||
getData: (<T>(key: string, defaultValue: T) => T) &
|
||||
(<T>(key: string) => T | undefined);
|
||||
}
|
||||
|
||||
interface LocalStorage {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LocalStorageService implements StorageService {
|
||||
private storage: LocalStorage;
|
||||
|
||||
private _prefix = 'flowide:';
|
||||
|
||||
setData<T>(key: string, data: T): void {
|
||||
this.storage[this.prefix(key)] = JSON.stringify(data);
|
||||
}
|
||||
|
||||
getData<T>(key: string, defaultValue?: T): T {
|
||||
const result = this.storage[this.prefix(key)];
|
||||
if (result === undefined) {
|
||||
return defaultValue as any;
|
||||
}
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
prefix(key: string) {
|
||||
return `${this._prefix}${key}`;
|
||||
}
|
||||
|
||||
setPrefix(prefix: string) {
|
||||
this._prefix = prefix;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
this.storage = window.localStorage;
|
||||
} else {
|
||||
this.storage = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
35
frontend/packages/project-ide/core/src/common/uri.spec.ts
Normal file
35
frontend/packages/project-ide/core/src/common/uri.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { URI } from './uri';
|
||||
|
||||
describe('uri', () => {
|
||||
it('toString', () => {
|
||||
const uris = [
|
||||
'https://www.abc.com/',
|
||||
'file:///root/abc',
|
||||
'file:///root/abc?query=1',
|
||||
'file:///root/abc?query=1#fragment',
|
||||
'abc:///root/abc',
|
||||
'abc:///project/:projectId/job/:jobId',
|
||||
];
|
||||
for (const uriStr of uris) {
|
||||
expect(new URI(uriStr).toString()).toEqual(uriStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
354
frontend/packages/project-ide/core/src/common/uri.ts
Normal file
354
frontend/packages/project-ide/core/src/common/uri.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { URI as Uri } from 'vscode-uri';
|
||||
|
||||
import { prioritizeAllSync, prioritizeAll } from './prioritizeable';
|
||||
import { Path } from './path';
|
||||
|
||||
export class URI {
|
||||
private readonly codeUri: Uri;
|
||||
|
||||
constructor(uri: string | Uri = '') {
|
||||
if (uri instanceof Uri) {
|
||||
this.codeUri = uri;
|
||||
} else {
|
||||
this.codeUri = Uri.parse(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private _path: Path | undefined;
|
||||
|
||||
get path(): Path {
|
||||
if (this._path === undefined) {
|
||||
this._path = new Path(this.codeUri.path);
|
||||
}
|
||||
return this._path;
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
const { base } = this.path;
|
||||
if (base) {
|
||||
return base;
|
||||
}
|
||||
if (this.path.isRoot) {
|
||||
return this.path.toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all uri from the current to the top most.
|
||||
*/
|
||||
get allLocations(): URI[] {
|
||||
const locations: URI[] = [];
|
||||
let location: URI = this;
|
||||
while (!location.path.isRoot && location.path.hasDir) {
|
||||
locations.push(location);
|
||||
location = location.parent;
|
||||
}
|
||||
locations.push(location);
|
||||
return locations;
|
||||
}
|
||||
|
||||
get parent(): URI {
|
||||
if (this.path.isRoot) {
|
||||
return this;
|
||||
}
|
||||
return this.withPath(this.path.dir);
|
||||
}
|
||||
|
||||
get scheme(): string {
|
||||
return this.codeUri.scheme;
|
||||
}
|
||||
|
||||
get authority(): string {
|
||||
return this.codeUri.authority;
|
||||
}
|
||||
|
||||
get query(): string {
|
||||
return this.codeUri.query;
|
||||
}
|
||||
|
||||
get fragment(): string {
|
||||
return this.codeUri.fragment;
|
||||
}
|
||||
|
||||
get queryObject(): { [key: string]: string | undefined } {
|
||||
return URI.queryStringToObject(this.query);
|
||||
}
|
||||
|
||||
static getDistinctParents(uris: URI[]): URI[] {
|
||||
const result: URI[] = [];
|
||||
uris.forEach((uri, i) => {
|
||||
if (
|
||||
!uris.some(
|
||||
(otherUri, index) => index !== i && otherUri.isEqualOrParent(uri),
|
||||
)
|
||||
) {
|
||||
result.push(uri);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
relative(uri: URI): Path | undefined {
|
||||
if (this.authority !== uri.authority || this.scheme !== uri.scheme) {
|
||||
return undefined;
|
||||
}
|
||||
return this.path.relative(uri.path);
|
||||
}
|
||||
|
||||
resolve(path: string | Path): URI {
|
||||
return this.withPath(this.path.join(path.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with the given scheme
|
||||
*/
|
||||
withScheme(scheme: string): URI {
|
||||
const newCodeUri = Uri.from({
|
||||
...this.codeUri.toJSON(),
|
||||
scheme,
|
||||
});
|
||||
return new URI(newCodeUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with the given authority
|
||||
*/
|
||||
withAuthority(authority: string): URI {
|
||||
const newCodeUri = Uri.from({
|
||||
...this.codeUri.toJSON(),
|
||||
scheme: this.codeUri.scheme,
|
||||
authority,
|
||||
});
|
||||
return new URI(newCodeUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* return this URI without a authority
|
||||
*/
|
||||
withoutAuthority(): URI {
|
||||
return this.withAuthority('');
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with the given path
|
||||
*/
|
||||
withPath(path: string | Path): URI {
|
||||
const newCodeUri = Uri.from({
|
||||
...this.codeUri.toJSON(),
|
||||
scheme: this.codeUri.scheme,
|
||||
path: path.toString(),
|
||||
});
|
||||
return new URI(newCodeUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* return this URI without a path
|
||||
*/
|
||||
withoutPath(): URI {
|
||||
return this.withPath('');
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with the given query
|
||||
*/
|
||||
withQuery(query: string): URI {
|
||||
const newCodeUri = Uri.from({
|
||||
...this.codeUri.toJSON(),
|
||||
scheme: this.codeUri.scheme,
|
||||
query,
|
||||
});
|
||||
return new URI(newCodeUri);
|
||||
}
|
||||
|
||||
addQueryObject(queryObj: { [key: string]: string | undefined }): URI {
|
||||
queryObj = { ...this.queryObject, ...queryObj };
|
||||
return this.withQuery(URI.objectToQueryString(queryObj));
|
||||
}
|
||||
|
||||
removeQueryObject(key: string): URI {
|
||||
const queryObj = { ...this.queryObject } as { [key: string]: string };
|
||||
if (key in queryObj) {
|
||||
delete queryObj[key];
|
||||
}
|
||||
return this.withQuery(URI.objectToQueryString(queryObj));
|
||||
}
|
||||
|
||||
/**
|
||||
* return this URI without a query
|
||||
*/
|
||||
withoutQuery(): URI {
|
||||
return this.withQuery('');
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with the given fragment
|
||||
*/
|
||||
withFragment(fragment: string): URI {
|
||||
const newCodeUri = Uri.from({
|
||||
...this.codeUri.toJSON(),
|
||||
scheme: this.codeUri.scheme,
|
||||
fragment,
|
||||
});
|
||||
return new URI(newCodeUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* return this URI without a fragment
|
||||
*/
|
||||
withoutFragment(): URI {
|
||||
return this.withFragment('');
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new URI replacing the current with its normalized path, resolving '..' and '.' segments
|
||||
*/
|
||||
normalizePath(): URI {
|
||||
return this.withPath(this.path.normalize());
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.scheme}://${this.authority}${this.path.toString()}${
|
||||
this.query ? `?${this.query}` : ''
|
||||
}${this.fragment ? `#${this.fragment}` : ''}`;
|
||||
}
|
||||
|
||||
isEqualOrParent(uri: URI | string): boolean {
|
||||
uri = typeof uri === 'string' ? new URI(uri) : uri;
|
||||
return (
|
||||
this.authority === uri.authority &&
|
||||
this.scheme === uri.scheme &&
|
||||
this.path.isEqualOrParent(uri.path)
|
||||
);
|
||||
}
|
||||
|
||||
match(uri: URI) {
|
||||
const path = `/${uri.authority}${uri.path.toString()}`;
|
||||
const params: any[] = [];
|
||||
const pattern = `/${this.authority}${this.path.toString()}`; // 以 / 开头
|
||||
let regexpSource = pattern
|
||||
.replace(/\/*\*?$/, '') // 去掉末尾的 / 和 /*
|
||||
.replace(/[\\.*+^${}|()[\]]/g, '\\$&') // 转译一些特殊字符
|
||||
.replace(/\/:([\w-]+)(\?)?/g, (_, paramName, optional) => {
|
||||
// 收集 url 上的参数 /:param/, /:param?/
|
||||
params.push({
|
||||
paramName,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
optional: optional != null,
|
||||
});
|
||||
// 是否是可选参数
|
||||
return optional ? '/?([^\\/]+)?' : '/([^\\/]+)';
|
||||
});
|
||||
if (pattern.endsWith('*')) {
|
||||
params.push({ paramName: '*' });
|
||||
// 也许路径只有 *
|
||||
regexpSource += pattern === '/*' ? '(.*)$' : '(?:\\/(.+)|\\/*)$';
|
||||
} else {
|
||||
regexpSource += '\\/*$';
|
||||
}
|
||||
const matcher = new RegExp(regexpSource);
|
||||
return !!path.match(matcher);
|
||||
}
|
||||
}
|
||||
|
||||
export interface URIHandler {
|
||||
canHandle: (uri: URI) => number | boolean;
|
||||
}
|
||||
|
||||
export namespace URIHandler {
|
||||
/**
|
||||
* 上层注册的优先级最高
|
||||
*/
|
||||
export const MAX_PRIORITY = 500;
|
||||
/**
|
||||
* 默认兜底
|
||||
*/
|
||||
export const DEFAULT_PRIORITY = 0;
|
||||
|
||||
/**
|
||||
* 优先级排序
|
||||
* @param uri
|
||||
* @param handlers
|
||||
*/
|
||||
export async function findAsync<T extends URIHandler>(
|
||||
uri: URI,
|
||||
handlers: T[],
|
||||
): Promise<T> {
|
||||
const prioritized = await prioritizeAll<T>(handlers, async handler => {
|
||||
const priority = handler.canHandle(uri);
|
||||
// boolean 情况默认采用 500
|
||||
if (typeof priority === 'boolean') {
|
||||
return priority ? MAX_PRIORITY : DEFAULT_PRIORITY;
|
||||
}
|
||||
return priority;
|
||||
});
|
||||
return prioritized[0]?.value as T;
|
||||
}
|
||||
export function findSync<T extends URIHandler>(uri: URI, handlers: T[]): T {
|
||||
const prioritized = prioritizeAllSync<T>(handlers, handler => {
|
||||
const priority = handler.canHandle(uri);
|
||||
// boolean 情况默认采用 500
|
||||
if (typeof priority === 'boolean') {
|
||||
return priority ? MAX_PRIORITY : DEFAULT_PRIORITY;
|
||||
}
|
||||
return priority;
|
||||
});
|
||||
return prioritized[0]?.value as T;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace URI {
|
||||
export function objectToQueryString(obj?: {
|
||||
[key: string]: string | undefined;
|
||||
}): string {
|
||||
if (!obj) {
|
||||
return '';
|
||||
}
|
||||
return Object.keys(obj)
|
||||
.map(key => `${key}=${obj[key] || ''}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function queryStringToObject(queryString: string): {
|
||||
[key: string]: string | undefined;
|
||||
} {
|
||||
return queryString.split('&').reduce((obj, key: string) => {
|
||||
const [k, value] = key.split('=');
|
||||
obj[k] = value;
|
||||
return obj;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}, {} as any);
|
||||
}
|
||||
|
||||
export function isSubPath(target: string, source: string): boolean {
|
||||
const targetURI = new URI(target);
|
||||
const sourceURI = new URI(source);
|
||||
return sourceURI.path.isEqualOrParent(targetURI.path);
|
||||
}
|
||||
|
||||
export function isURIStringEqual(target: string, source: string): boolean {
|
||||
const targetURI = new URI(target);
|
||||
const sourceURI = new URI(source);
|
||||
return sourceURI.toString() === targetURI.toString();
|
||||
}
|
||||
|
||||
export function isSubOrEqual(target: string, source: string): boolean {
|
||||
return isSubPath(target, source) || isURIStringEqual(target, source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { type Event, Emitter } from '@flowgram-adapter/common';
|
||||
|
||||
import { type LifecycleContribution } from './lifecycle-contribution';
|
||||
|
||||
@injectable()
|
||||
export class WindowService implements LifecycleContribution {
|
||||
protected onUnloadEmitter = new Emitter<void>();
|
||||
|
||||
protected onBeforeUnloadEmitter = new Emitter<BeforeUnloadEvent>();
|
||||
|
||||
get onUnload(): Event<void> {
|
||||
return this.onUnloadEmitter.event;
|
||||
}
|
||||
|
||||
get onBeforeUnload(): Event<BeforeUnloadEvent> {
|
||||
return this.onBeforeUnloadEmitter.event;
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.registerUnloadListeners();
|
||||
}
|
||||
|
||||
protected registerUnloadListeners(): void {
|
||||
window.addEventListener('unload', () => this.onUnloadEmitter.fire());
|
||||
window.addEventListener('beforeunload', e =>
|
||||
this.onBeforeUnloadEmitter.fire(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user