feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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'];
}

View File

@@ -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;
}
}

View 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';

View File

@@ -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;
}

View 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,
];
}
}

View 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));
}
}

View 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);

View File

@@ -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 },
]);
});
});

View File

@@ -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);
}

View File

@@ -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 = {};
}
}
}

View 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);
}
});
});

View 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);
}
}

View File

@@ -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),
);
}
}