chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
const { defineFlatConfig } = require('@flowgram.ai/eslint-config');
module.exports = defineFlatConfig({
preset: 'web',
packageRoot: __dirname,
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'ExportAllDeclaration',
message:
'Do not re-export everything from another modules, you should explicitly specify the members to be exported.',
},
],
},
});
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@flowgram.ai/command",
"version": "0.1.8",
"homepage": "https://flowgram.ai/",
"repository": "https://github.com/bytedance/flowgram.ai",
"license": "MIT",
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/index.js"
},
"main": "./dist/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "npm run build:fast -- --dts-resolve",
"build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output",
"build:watch": "npm run build:fast -- --dts-resolve",
"clean": "rimraf dist",
"ts-check": "tsc --noEmit",
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
},
"dependencies": {
"@flowgram.ai/utils": "workspace:*",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2"
},
"devDependencies": {
"@flowgram.ai/eslint-config": "workspace:*",
"@flowgram.ai/ts-config": "workspace:*",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.0.0",
"jsdom": "^26.1.0",
"tsup": "^8.0.1",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { ContainerModule } from 'inversify';
import { bindContributionProvider } from '@flowgram.ai/utils';
import { CommandService } from './command-service';
import { CommandRegistry, CommandRegistryFactory, CommandContribution } from './command';
export const CommandContainerModule = new ContainerModule(bind => {
bindContributionProvider(bind, CommandContribution);
bind(CommandRegistry).toSelf().inSingletonScope();
bind(CommandService).toService(CommandRegistry);
bind(CommandRegistryFactory).toFactory(ctx => () => ctx.container.get(CommandRegistry));
});
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { type Disposable, type Event } from '@flowgram.ai/utils';
import { type CommandEvent } from './command';
export const CommandService = Symbol('CommandService');
/**
* command service 执行接口
*/
export interface CommandService extends Disposable {
/**
* command 事件执行前触发事件
*/
readonly onWillExecuteCommand: Event<CommandEvent>;
/**
* command 事件执行完成后触发
*/
readonly onDidExecuteCommand: Event<CommandEvent>;
/**
* 执行 command
*/
executeCommand<T>(command: string, ...args: any[]): Promise<T | undefined>;
}
+387
View File
@@ -0,0 +1,387 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { injectable, multiInject, optional } from 'inversify';
import { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';
import { type CommandService } from './command-service';
export interface Command {
/**
* id,唯一 key
*/
id: string;
/**
* 展示用 label
*/
label?: string;
/**
* 在一些明确的场景下,部分只展示简短的 label 即可
*/
shortLabel?: string;
/**
* 展示用 command icon
*/
icon?: string | React.ReactNode | ((props: any) => React.ReactNode);
/**
* 暂不使用
*/
category?: string;
}
export namespace Command {
export enum Default {
ZOOM_IN = 'ZOOM_IN',
ZOOM_OUT = 'ZOOM_OUT',
DELETE = 'DELETE',
COPY = 'COPY',
PASTE = 'PASTE',
UNDO = 'UNDO',
REDO = 'REDO',
VIEW_CLOSE_ALL_WIDGET = 'view.closeAllWidget',
VIEW_CLOSE_CURRENT_WIDGET = 'view.closeCurrentWidget',
VIEW_REOPEN_LAST_WIDGET = 'view.reopenLastWidget',
VIEW_CLOSE_OTHER_WIDGET = 'view.closeOtherWidget',
VIEW_CLOSE_BOTTOM_PANEL = 'view.closeBottomPannel',
VIEW_OPEN_NEXT_TAB = 'view.openNextTab',
VIEW_OEPN_LAST_TAB = 'view.openLastTab',
VIEW_FULL_SCREEN = 'view.fullScreen',
VIEW_SAVING_WIDGET_CLOSE_CONFIRM = 'view.savingWidgetCloseConfirm',
VIEW_SHORTCUTS = 'view.shortcuts',
VIEW_PREFERENCES = 'view.preferences',
VIEW_LOG = 'view.log',
VIEW_PROBLEMS = 'view.problems',
}
/**
* 判断是否是 command
*/
export function is(arg: Command | any): arg is Command {
return !!arg && arg === Object(arg) && 'id' in arg;
}
}
export interface CommandHandler {
/**
* handler 执行函数
*/
execute(...args: any[]): any;
/**
* 该 handler 是否可以执行
*/
isEnabled?(...args: any[]): boolean;
/**
* 预留 contextMenu 用,该 handler 是否可见
*/
isVisible?(...args: any[]): boolean;
/**
* 预留 contextMenu 用,该 handler 是否可以触发
*/
isToggled?(...args: any[]): boolean;
}
export interface CommandEvent {
/**
* commandId
*/
commandId: string;
/**
* 参数
*/
args: any[];
}
export const CommandContribution = Symbol('CommandContribution');
export interface CommandContribution {
/**
* 注册 command
*/
registerCommands(commands: CommandService): void;
}
/**
* 当前正在运行的 command
*/
interface CommandExecuting {
/**
* commandid
*/
id: string;
/**
* 参数
*/
args: any[];
/**
* 正在进行的 promise
*/
promise?: Promise<any>;
}
namespace CommandExecuting {
/**
* 获取正在运行的 command 单个实例
*/
export function findSimple(
arrs: Set<CommandExecuting>,
newCmd: CommandExecuting,
): CommandExecuting | undefined {
for (const item of arrs.values()) {
if (
item.id === newCmd.id &&
item.args.length === newCmd.args.length &&
item.args.every((arg, index) => (newCmd as any)[index] === arg)
) {
return item;
}
}
}
}
export const CommandRegistryFactory = 'CommandRegistryFactory';
@injectable()
export class CommandRegistry implements CommandService {
protected readonly _handlers: { [id: string]: CommandHandler[] } = {};
protected readonly _commands: { [id: string]: Command } = {};
protected readonly _commandExecutings = new Set<CommandExecuting>();
protected readonly toUnregisterCommands = new Map<string, Disposable>();
protected readonly onDidExecuteCommandEmitter = new Emitter<CommandEvent>();
readonly onDidExecuteCommand = this.onDidExecuteCommandEmitter.event;
protected readonly onWillExecuteCommandEmitter = new Emitter<CommandEvent>();
readonly onWillExecuteCommand = this.onWillExecuteCommandEmitter.event;
@multiInject(CommandContribution)
@optional()
protected readonly contributions: CommandContribution[];
init() {
for (const contrib of this.contributions) {
contrib.registerCommands(this);
}
}
/**
* 当前所有 command
*/
get commands(): Command[] {
const commands: Command[] = [];
for (const id of this.commandIds) {
const cmd = this.getCommand(id);
if (cmd) {
commands.push(cmd);
}
}
return commands;
}
/**
* 当前所有 commandid
*/
get commandIds(): string[] {
return Object.keys(this._commands);
}
/**
* registerCommand
*/
registerCommand(id: string, handler?: CommandHandler): Disposable;
registerCommand(command: Command, handler?: CommandHandler): Disposable;
registerCommand(commandOrId: string | Command, handler?: CommandHandler): Disposable {
const command: Command = typeof commandOrId === 'string' ? { id: commandOrId } : commandOrId;
if (this._commands[command.id]) {
console.warn(`A command ${command.id} is already registered.`);
return Disposable.NULL;
}
const toDispose = new DisposableCollection(this.doRegisterCommand(command));
if (handler) {
toDispose.push(this.registerHandler(command.id, handler));
}
this.toUnregisterCommands.set(command.id, toDispose);
toDispose.push(Disposable.create(() => this.toUnregisterCommands.delete(command.id)));
return toDispose;
}
/**
* unregisterCommand
*/
unregisterCommand(command: Command): void;
unregisterCommand(id: string): void;
unregisterCommand(commandOrId: Command | string): void {
const id = Command.is(commandOrId) ? commandOrId.id : commandOrId;
const toUnregister = this.toUnregisterCommands.get(id);
if (toUnregister) {
toUnregister.dispose();
}
}
/**
* 注册 handler
*/
registerHandler(commandId: string, handler: CommandHandler): Disposable {
let handlers = this._handlers[commandId];
if (!handlers) {
this._handlers[commandId] = handlers = [];
}
handlers.unshift(handler);
return {
dispose: () => {
const idx = handlers.indexOf(handler);
if (idx >= 0) {
handlers.splice(idx, 1);
}
},
};
}
/**
* 预留 contextMenu 用,该 handler 是否可见
*/
isVisible(command: string, ...args: any[]): boolean {
return typeof this.getVisibleHandler(command, ...args) !== 'undefined';
}
/**
* command 是否可用
*/
isEnabled(command: string, ...args: any[]): boolean {
return typeof this.getActiveHandler(command, ...args) !== 'undefined';
}
/**
* 预留 contextMenu 用,该 handler 是否可以触发
*/
isToggled(command: string, ...args: any[]): boolean {
return typeof this.getToggledHandler(command, ...args) !== 'undefined';
}
/**
* 执行 command,会先判断是否可以执行,不会重复执行
*/
async executeCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {
const handler = this.getActiveHandler(commandId, ...args);
const execInfo: CommandExecuting = { id: commandId, args };
const simpleExecInfo = CommandExecuting.findSimple(this._commandExecutings, execInfo);
if (simpleExecInfo) {
return execInfo.promise;
}
if (handler) {
try {
this._commandExecutings.add(execInfo);
this.onWillExecuteCommandEmitter.fire({ commandId, args });
const promise = handler.execute(...args);
execInfo.promise = promise;
const result = await promise;
this.onDidExecuteCommandEmitter.fire({ commandId, args });
return result;
} finally {
this._commandExecutings.delete(execInfo);
}
}
}
getVisibleHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
const handlers = this._handlers[commandId];
if (handlers) {
for (const handler of handlers) {
try {
if (!handler.isVisible || handler.isVisible(...args)) {
return handler;
}
} catch (error) {
console.error(error);
}
}
}
return undefined;
}
getActiveHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
const handlers = this._handlers[commandId];
if (handlers) {
for (const handler of handlers) {
try {
if (!handler.isEnabled || handler.isEnabled(...args)) {
return handler;
}
} catch (error) {
console.error(error);
}
}
}
return undefined;
}
/**
* 获取 command 对应的所有 handler
*/
getAllHandlers(commandId: string): CommandHandler[] {
const handlers = this._handlers[commandId];
return handlers ? handlers.slice() : [];
}
getToggledHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
const handlers = this._handlers[commandId];
if (handlers) {
for (const handler of handlers) {
try {
if (handler.isToggled && handler.isToggled(...args)) {
return handler;
}
} catch (error) {
console.error(error);
}
}
}
return undefined;
}
/**
* 获取 command
*/
getCommand(id: string): Command | undefined {
return this._commands[id];
}
protected doRegisterCommand(command: Command): Disposable {
this._commands[command.id] = command;
return {
dispose: () => {
delete this._commands[command.id];
},
};
}
/**
* 更新 command
*/
public updateCommand(id: string, command: Partial<Omit<Command, 'id'>>) {
if (this._commands[id]) {
this._commands[id] = {
...this._commands[id],
...command,
};
}
}
dispose() {
this.onWillExecuteCommandEmitter.dispose();
this.onDidExecuteCommandEmitter.dispose();
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export {
CommandContribution,
type CommandHandler,
CommandRegistry,
Command,
CommandRegistryFactory,
} from './command';
export { CommandService } from './command-service';
export { CommandContainerModule } from './command-container-module';
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { defineConfig } from 'vitest/config';
export default defineConfig({
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
globals: true,
mockReset: false,
environment: 'jsdom',
include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/lib/**', // lib 编译结果忽略掉
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
],
},
});
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import 'reflect-metadata';