@flowgram.ai/test-run-plugin 0.1.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.eslintrc.cjs +17 -0
  2. package/.rush/temp/chunked-rush-logs/test-run-plugin.build.chunks.jsonl +21 -0
  3. package/.rush/temp/package-deps_build.json +47 -0
  4. package/.rush/temp/shrinkwrap-deps.json +161 -0
  5. package/dist/esm/index.js +731 -0
  6. package/dist/esm/index.js.map +1 -0
  7. package/dist/index.d.mts +314 -0
  8. package/dist/index.d.ts +314 -0
  9. package/dist/index.js +770 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +56 -0
  12. package/rush-logs/test-run-plugin.build.log +21 -0
  13. package/src/create-test-run-plugin.ts +39 -0
  14. package/src/form-engine/contexts.ts +16 -0
  15. package/src/form-engine/fields/create-field.tsx +32 -0
  16. package/src/form-engine/fields/general-field.tsx +35 -0
  17. package/src/form-engine/fields/index.ts +9 -0
  18. package/src/form-engine/fields/object-field.tsx +21 -0
  19. package/src/form-engine/fields/reactive-field.tsx +57 -0
  20. package/src/form-engine/fields/recursion-field.tsx +31 -0
  21. package/src/form-engine/fields/schema-field.tsx +32 -0
  22. package/src/form-engine/form/form.tsx +38 -0
  23. package/src/form-engine/form/index.ts +6 -0
  24. package/src/form-engine/hooks/index.ts +8 -0
  25. package/src/form-engine/hooks/use-create-form.ts +69 -0
  26. package/src/form-engine/hooks/use-field.ts +13 -0
  27. package/src/form-engine/hooks/use-form.ts +13 -0
  28. package/src/form-engine/index.ts +19 -0
  29. package/src/form-engine/model/index.ts +89 -0
  30. package/src/form-engine/types.ts +56 -0
  31. package/src/form-engine/utils.ts +64 -0
  32. package/src/index.ts +22 -0
  33. package/src/reactive/hooks/index.ts +7 -0
  34. package/src/reactive/hooks/use-create-form.ts +90 -0
  35. package/src/reactive/hooks/use-test-run-service.ts +10 -0
  36. package/src/reactive/index.ts +6 -0
  37. package/src/services/config.ts +42 -0
  38. package/src/services/form/factory.ts +9 -0
  39. package/src/services/form/form.ts +78 -0
  40. package/src/services/form/index.ts +8 -0
  41. package/src/services/form/manager.ts +43 -0
  42. package/src/services/index.ts +14 -0
  43. package/src/services/pipeline/factory.ts +9 -0
  44. package/src/services/pipeline/index.ts +12 -0
  45. package/src/services/pipeline/pipeline.ts +143 -0
  46. package/src/services/pipeline/plugin.ts +11 -0
  47. package/src/services/pipeline/tap.ts +34 -0
  48. package/src/services/store.ts +27 -0
  49. package/src/services/test-run.ts +100 -0
  50. package/src/types.ts +29 -0
  51. package/tsconfig.json +12 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { ReactiveState } from '@flowgram.ai/reactive';
7
+
8
+ import { getUniqueFieldName, mergeFieldPath } from '../utils';
9
+ import { FormSchema, FormSchemaType, FormSchemaModelState } from '../types';
10
+
11
+ export class FormSchemaModel implements FormSchema {
12
+ name?: string;
13
+
14
+ type?: FormSchemaType;
15
+
16
+ defaultValue?: any;
17
+
18
+ properties?: Record<string, FormSchema>;
19
+
20
+ ['x-index']?: number;
21
+
22
+ ['x-component']?: string;
23
+
24
+ ['x-component-props']?: Record<string, unknown>;
25
+
26
+ ['x-decorator']?: string;
27
+
28
+ ['x-decorator-props']?: Record<string, unknown>;
29
+
30
+ [key: string]: any;
31
+
32
+ path: string[] = [];
33
+
34
+ state = new ReactiveState<FormSchemaModelState>({ disabled: false });
35
+
36
+ get componentType() {
37
+ return this['x-component'];
38
+ }
39
+
40
+ get componentProps() {
41
+ return this['x-component-props'];
42
+ }
43
+
44
+ get decoratorType() {
45
+ return this['x-decorator'];
46
+ }
47
+
48
+ get decoratorProps() {
49
+ return this['x-decorator-props'];
50
+ }
51
+
52
+ get uniqueName() {
53
+ return getUniqueFieldName(...this.path);
54
+ }
55
+
56
+ constructor(json: FormSchema, path: string[] = []) {
57
+ this.fromJSON(json);
58
+ this.path = path;
59
+ }
60
+
61
+ private fromJSON(json: FormSchema) {
62
+ Object.entries(json).forEach(([key, value]) => {
63
+ this[key] = value;
64
+ });
65
+ }
66
+
67
+ getPropertyList() {
68
+ const orderProperties: FormSchemaModel[] = [];
69
+ const unOrderProperties: FormSchemaModel[] = [];
70
+ Object.entries(this.properties || {}).forEach(([key, item]) => {
71
+ const index = item['x-index'];
72
+ const defaultValues = this.defaultValue;
73
+ /**
74
+ * The upper layer's default value has a higher priority than its own default value,
75
+ * because the upper layer's default value ultimately comes from the outside world.
76
+ */
77
+ if (typeof defaultValues === 'object' && defaultValues !== null && key in defaultValues) {
78
+ item.defaultValue = defaultValues[key];
79
+ }
80
+ const current = new FormSchemaModel(item, mergeFieldPath(this.path, key));
81
+ if (index !== undefined && !isNaN(index)) {
82
+ orderProperties[index] = current;
83
+ } else {
84
+ unOrderProperties.push(current);
85
+ }
86
+ });
87
+ return orderProperties.concat(unOrderProperties).filter((item) => !!item);
88
+ }
89
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React from 'react';
7
+
8
+ import type { Validate, FieldState } from '@flowgram.ai/form';
9
+
10
+ /** field type */
11
+ export type FormSchemaType = 'string' | 'number' | 'boolean' | 'object' | string;
12
+
13
+ export type FormSchemaValidate = Validate;
14
+
15
+ export interface FormSchema {
16
+ /** core */
17
+ name?: string;
18
+ type?: FormSchemaType;
19
+ defaultValue?: any;
20
+
21
+ /** children */
22
+ properties?: Record<string, FormSchema>;
23
+
24
+ /** ui */
25
+ title?: string | React.ReactNode;
26
+ description?: string | React.ReactNode;
27
+ ['x-index']?: number;
28
+ ['x-visible']?: boolean;
29
+ ['x-hidden']?: boolean;
30
+ ['x-component']?: string;
31
+ ['x-component-props']?: Record<string, unknown>;
32
+ ['x-decorator']?: string;
33
+ ['x-decorator-props']?: Record<string, unknown>;
34
+
35
+ /** rule */
36
+ required?: boolean;
37
+ ['x-validator']?: FormSchemaValidate;
38
+
39
+ /** custom */
40
+ [key: string]: any;
41
+ }
42
+
43
+ export type FormComponentProps = {
44
+ type?: FormSchemaType;
45
+ disabled?: boolean;
46
+ [key: string]: any;
47
+ } & FormSchema['x-component-props'] &
48
+ FormSchema['x-decorator-props'] &
49
+ Partial<FieldState>;
50
+ export type FormComponent = React.FunctionComponent<any>;
51
+ export type FormComponents = Record<string, FormComponent>;
52
+
53
+ /** ui state */
54
+ export interface FormSchemaModelState {
55
+ disabled: boolean;
56
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { createElement } from 'react';
7
+
8
+ import type { FormSchema, FormSchemaValidate, FormComponentProps } from './types';
9
+
10
+ /** Splice form item unique name */
11
+ export const getUniqueFieldName = (...args: (string | undefined)[]) =>
12
+ args.filter((path) => path).join('.');
13
+
14
+ export const mergeFieldPath = (path?: string[], name?: string) =>
15
+ [...(path || []), name].filter((i): i is string => Boolean(i));
16
+
17
+ /** Create validation rules */
18
+ export const createValidate = (schema: FormSchema) => {
19
+ const rules: Record<string, FormSchemaValidate> = {};
20
+
21
+ visit(schema);
22
+
23
+ return rules;
24
+
25
+ function visit(current: FormSchema, name?: string) {
26
+ if (name && current['x-validator']) {
27
+ rules[name] = current['x-validator'];
28
+ }
29
+ if (current.type === 'object' && current.properties) {
30
+ Object.entries(current.properties).forEach(([key, value]) => {
31
+ visit(value, getUniqueFieldName(name, key));
32
+ });
33
+ }
34
+ }
35
+ };
36
+
37
+ export const connect = <T = any>(
38
+ Component: React.FunctionComponent<any>,
39
+ mapProps: (p: FormComponentProps) => T
40
+ ) => {
41
+ const Connected = (props: FormComponentProps) => {
42
+ const mappedProps = mapProps(props);
43
+ return createElement(Component, mappedProps, (mappedProps as any).children);
44
+ };
45
+
46
+ return Connected;
47
+ };
48
+
49
+ export const isFormEmpty = (schema: FormSchema) => {
50
+ /** is not general field and not has children */
51
+ const isEmpty = (s: FormSchema): boolean => {
52
+ if (!s.type || s.type === 'object' || !s.name) {
53
+ return Object.entries(schema.properties || {})
54
+ .map(([key, value]) => ({
55
+ name: key,
56
+ ...value,
57
+ }))
58
+ .every(isFormEmpty);
59
+ }
60
+ return false;
61
+ };
62
+
63
+ return isEmpty(schema);
64
+ };
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { createTestRunPlugin } from './create-test-run-plugin';
7
+ export { useCreateForm, useTestRunService } from './reactive';
8
+
9
+ export {
10
+ FormEngine,
11
+ connect,
12
+ type FormInstance,
13
+ type FormEngineProps,
14
+ type FormSchema,
15
+ type FormComponentProps,
16
+ } from './form-engine';
17
+
18
+ export {
19
+ type TestRunPipelinePlugin,
20
+ TestRunPipelineEntity,
21
+ type TestRunPipelineEntityCtx,
22
+ } from './services';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { useCreateForm } from './use-create-form';
7
+ export { useTestRunService } from './use-test-run-service';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { useEffect, useMemo, useState } from 'react';
7
+
8
+ import { DisposableCollection } from '@flowgram.ai/utils';
9
+ import type { FlowNodeEntity } from '@flowgram.ai/document';
10
+
11
+ import { TestRunFormEntity } from '../../services/form/form';
12
+ import { FormEngineProps, isFormEmpty } from '../../form-engine';
13
+ import { useTestRunService } from './use-test-run-service';
14
+
15
+ interface UseFormOptions {
16
+ node?: FlowNodeEntity;
17
+ /** form loading */
18
+ loadingRenderer?: React.ReactNode;
19
+ /** form empty */
20
+ emptyRenderer?: React.ReactNode;
21
+ defaultValues?: FormEngineProps['defaultValues'];
22
+ onMounted?: FormEngineProps['onMounted'];
23
+ onUnmounted?: FormEngineProps['onUnmounted'];
24
+ onFormValuesChange?: FormEngineProps['onFormValuesChange'];
25
+ }
26
+
27
+ export const useCreateForm = ({
28
+ node,
29
+ loadingRenderer,
30
+ emptyRenderer,
31
+ defaultValues,
32
+ onMounted,
33
+ onUnmounted,
34
+ onFormValuesChange,
35
+ }: UseFormOptions) => {
36
+ const testRun = useTestRunService();
37
+ const [loading, setLoading] = useState(false);
38
+ const [form, setForm] = useState<TestRunFormEntity | null>(null);
39
+ const renderer = useMemo(() => {
40
+ if (loading || !form) {
41
+ return loadingRenderer;
42
+ }
43
+
44
+ const isEmpty = isFormEmpty(form.schema);
45
+
46
+ return form.render({
47
+ defaultValues,
48
+ onFormValuesChange,
49
+ children: isEmpty ? emptyRenderer : null,
50
+ });
51
+ }, [form, loading]);
52
+
53
+ const compute = async () => {
54
+ if (!node) {
55
+ return;
56
+ }
57
+ try {
58
+ setLoading(true);
59
+ const formEntity = await testRun.createForm(node);
60
+ setForm(formEntity);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ };
65
+
66
+ useEffect(() => {
67
+ compute();
68
+ }, [node]);
69
+
70
+ useEffect(() => {
71
+ if (!form) {
72
+ return;
73
+ }
74
+ const disposable = new DisposableCollection(
75
+ form.onFormMounted((data) => {
76
+ onMounted?.(data);
77
+ }),
78
+ form.onFormUnmounted(() => {
79
+ onUnmounted?.();
80
+ })
81
+ );
82
+ return () => disposable.dispose();
83
+ }, [form]);
84
+
85
+ return {
86
+ renderer,
87
+ loading,
88
+ form,
89
+ };
90
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { useService } from '@flowgram.ai/core';
7
+
8
+ import { TestRunService } from '../../services/test-run';
9
+
10
+ export const useTestRunService = () => useService<TestRunService>(TestRunService);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { useCreateForm, useTestRunService } from './hooks';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { FlowNodeType, FlowNodeEntity } from '@flowgram.ai/document';
7
+
8
+ import type { MaybePromise } from '../types';
9
+ import type { FormSchema, FormComponents } from '../form-engine';
10
+ import type { TestRunPipelinePlugin } from './pipeline';
11
+
12
+ type PropertiesFunctionParams = {
13
+ node: FlowNodeEntity;
14
+ };
15
+ export type NodeMap = Record<FlowNodeType, NodeTestConfig>;
16
+ export interface NodeTestConfig {
17
+ /** Enable node TestRun */
18
+ enabled?: boolean;
19
+ /** Input schema properties */
20
+ properties?:
21
+ | Record<string, FormSchema>
22
+ | ((params: PropertiesFunctionParams) => MaybePromise<Record<string, FormSchema>>);
23
+ }
24
+
25
+ export interface TestRunConfig {
26
+ components: FormComponents;
27
+ nodes: NodeMap;
28
+ plugins: (new () => TestRunPipelinePlugin)[];
29
+ }
30
+
31
+ export const TestRunConfig = Symbol('TestRunConfig');
32
+ export const defineConfig = (config: Partial<TestRunConfig>) => {
33
+ const defaultConfig: TestRunConfig = {
34
+ components: {},
35
+ nodes: {},
36
+ plugins: [],
37
+ };
38
+ return {
39
+ ...defaultConfig,
40
+ ...config,
41
+ };
42
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { TestRunFormEntity } from './form';
7
+
8
+ export const TestRunFormFactory = Symbol('TestRunFormFactory');
9
+ export type TestRunFormFactory = () => TestRunFormEntity;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { createElement, type ReactNode } from 'react';
7
+
8
+ import { nanoid } from 'nanoid';
9
+ import { injectable, inject } from 'inversify';
10
+ import { Emitter } from '@flowgram.ai/utils';
11
+
12
+ import { TestRunConfig } from '../config';
13
+ import { FormSchema, FormEngine, type FormInstance, type FormEngineProps } from '../../form-engine';
14
+
15
+ export type FormRenderProps = Omit<
16
+ FormEngineProps,
17
+ 'schema' | 'components' | 'onMounted' | 'onUnmounted'
18
+ >;
19
+
20
+ @injectable()
21
+ export class TestRunFormEntity {
22
+ @inject(TestRunConfig) private readonly config: TestRunConfig;
23
+
24
+ private _schema: FormSchema;
25
+
26
+ private initialized = false;
27
+
28
+ id = nanoid();
29
+
30
+ form: FormInstance | null = null;
31
+
32
+ onFormMountedEmitter = new Emitter<FormInstance>();
33
+
34
+ onFormMounted = this.onFormMountedEmitter.event;
35
+
36
+ onFormUnmountedEmitter = new Emitter<void>();
37
+
38
+ onFormUnmounted = this.onFormUnmountedEmitter.event;
39
+
40
+ get schema() {
41
+ return this._schema;
42
+ }
43
+
44
+ init(options: { schema: FormSchema }) {
45
+ if (this.initialized) return;
46
+
47
+ this._schema = options.schema;
48
+ this.initialized = true;
49
+ }
50
+
51
+ render(props?: FormRenderProps): ReactNode {
52
+ if (!this.initialized) {
53
+ return null;
54
+ }
55
+ const { children, ...restProps } = props || {};
56
+ return createElement(
57
+ FormEngine,
58
+ {
59
+ schema: this.schema,
60
+ components: this.config.components,
61
+ onMounted: (instance) => {
62
+ this.form = instance;
63
+ this.onFormMountedEmitter.fire(instance);
64
+ },
65
+ onUnmounted: this.onFormUnmountedEmitter.fire.bind(this.onFormUnmountedEmitter),
66
+ ...restProps,
67
+ },
68
+ children
69
+ );
70
+ }
71
+
72
+ dispose() {
73
+ this._schema = {};
74
+ this.form = null;
75
+ this.onFormMountedEmitter.dispose();
76
+ this.onFormUnmountedEmitter.dispose();
77
+ }
78
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { TestRunFormEntity } from './form';
7
+ export { TestRunFormFactory } from './factory';
8
+ export { TestRunFormManager } from './manager';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { inject, injectable } from 'inversify';
7
+
8
+ import type { TestRunFormEntity } from './form';
9
+ import { TestRunFormFactory } from './factory';
10
+
11
+ @injectable()
12
+ export class TestRunFormManager {
13
+ @inject(TestRunFormFactory) private readonly factory: TestRunFormFactory;
14
+
15
+ private entities = new Map<string, TestRunFormEntity>();
16
+
17
+ createForm() {
18
+ return this.factory();
19
+ }
20
+
21
+ getForm(id: string) {
22
+ return this.entities.get(id);
23
+ }
24
+
25
+ getAllForm() {
26
+ return Array.from(this.entities);
27
+ }
28
+
29
+ disposeForm(id: string) {
30
+ const form = this.entities.get(id);
31
+ if (!form) {
32
+ return;
33
+ }
34
+ form.dispose();
35
+ this.entities.delete(id);
36
+ }
37
+
38
+ disposeAllForm() {
39
+ for (const id of this.entities.keys()) {
40
+ this.disposeForm(id);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { TestRunService } from './test-run';
7
+ export { TestRunFormEntity, TestRunFormFactory } from './form';
8
+ export {
9
+ TestRunPipelineEntity,
10
+ TestRunPipelineFactory,
11
+ type TestRunPipelinePlugin,
12
+ type TestRunPipelineEntityCtx,
13
+ } from './pipeline';
14
+ export { TestRunConfig, defineConfig } from './config';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { TestRunPipelineEntity } from './pipeline';
7
+
8
+ export const TestRunPipelineFactory = Symbol('TestRunPipelineFactory');
9
+ export type TestRunPipelineFactory = () => TestRunPipelineEntity;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { TestRunPipelineFactory } from './factory';
7
+ export {
8
+ TestRunPipelineEntity,
9
+ type TestRunPipelineEntityOptions,
10
+ type TestRunPipelineEntityCtx,
11
+ } from './pipeline';
12
+ export { TestRunPipelinePlugin } from './plugin';