fix(workflow): create node script (#1856)

main
July 2 months ago committed by GitHub
parent 042f2299f0
commit 2ffd7a8221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 790
      common/config/subspaces/default/pnpm-lock.yaml
  2. 7
      frontend/packages/workflow/playground/package.json
  3. 18
      frontend/packages/workflow/playground/scripts/create-node/index.js
  4. 193
      frontend/packages/workflow/playground/scripts/create-node/index.ts
  5. 150
      frontend/packages/workflow/playground/scripts/create-node/plopfile.js
  6. 0
      frontend/packages/workflow/playground/scripts/create-node/templates/constants.ts.eta
  7. 0
      frontend/packages/workflow/playground/scripts/create-node/templates/data-transformer.ts.eta
  8. 2
      frontend/packages/workflow/playground/scripts/create-node/templates/form-meta.tsx.eta
  9. 2
      frontend/packages/workflow/playground/scripts/create-node/templates/form.tsx.eta
  10. 2
      frontend/packages/workflow/playground/scripts/create-node/templates/index.ts.eta
  11. 2
      frontend/packages/workflow/playground/scripts/create-node/templates/index.ts.hbs
  12. 2
      frontend/packages/workflow/playground/scripts/create-node/templates/node-content.tsx.eta
  13. 14
      frontend/packages/workflow/playground/scripts/create-node/templates/node-registry.ts.eta
  14. 5
      frontend/packages/workflow/playground/scripts/create-node/templates/node-test.ts.eta
  15. 5
      frontend/packages/workflow/playground/scripts/create-node/templates/node-test.ts.hbs
  16. 0
      frontend/packages/workflow/playground/scripts/create-node/templates/types.ts.eta
  17. 2
      frontend/packages/workflow/playground/src/components/node-render/node-render-new/content/index.tsx
  18. 2
      frontend/packages/workflow/playground/src/node-registries/index.ts
  19. 2
      frontend/packages/workflow/playground/src/nodes-v2/constants.ts
  20. 3
      frontend/packages/workflow/playground/tsconfig.misc.json

File diff suppressed because it is too large Load Diff

@ -27,7 +27,7 @@
},
"scripts": {
"build": "exit 0",
"create:node": "plop --plopfile ./scripts/create-node/plopfile.js",
"create:node": "node ./scripts/create-node/index.js",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
@ -168,6 +168,7 @@
"@coze-arch/tea": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@inquirer/prompts": "^7.8.4",
"@lezer/common": "^1.2.2",
"@monaco-editor/react": "^4.5.2",
"@rspack/core": "0.6.0",
@ -184,11 +185,12 @@
"@types/semver": "^7.3.4",
"@vitest/coverage-v8": "~3.0.5",
"debug": "^4.3.4",
"esbuild-register": "^3.6.0",
"eta": "^3.5.0",
"fp-ts": "^2.5.0",
"i18next": ">= 19.0.0",
"less": "^3.13.1",
"monaco-editor": "^0.45.0",
"plop": "~4.0.1",
"prop-types": "^15.5.7",
"react": "~18.2.0",
"react-dom": "~18.2.0",
@ -199,6 +201,7 @@
"styled-components": ">=4",
"stylelint": "^15.11.0",
"tailwindcss": "~3.3.3",
"ts-morph": "^20.0.0",
"typescript": "~5.8.2",
"utility-types": "^3.10.0",
"vite": "^4.3.9",

@ -0,0 +1,18 @@
/*
* 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.
*/
require('esbuild-register');
require('./index.ts');

@ -0,0 +1,193 @@
/*
* 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 path from 'path';
import fs from 'fs';
import { Project, SyntaxKind, type SourceFile } from 'ts-morph';
import { camelCase, upperFirst, snakeCase, toUpper } from 'lodash-es';
import { Eta } from 'eta';
import { input, confirm } from '@inquirer/prompts';
const tsProject = new Project({});
class InsertSourceCode {
source: SourceFile;
constructor(private sourcePath: string) {
this.source = tsProject.addSourceFileAtPath(this.sourcePath);
}
addNamedExport(name: string, specifier: string) {
const allExports = this.source.getExportDeclarations();
const exist = allExports.some(
e =>
e.getModuleSpecifierValue() === specifier &&
e.getNamedExports().some(i => i.getName() === name),
);
if (exist) {
console.warn(
` export ${name} in file ${this.sourcePath} already exists.`,
);
}
this.source.addExportDeclaration({
namedExports: [name],
moduleSpecifier: specifier,
});
}
addNamedImport(name: string, specifier: string) {
const allImports = this.source.getImportDeclarations();
const exist = allImports.some(
e =>
e.getModuleSpecifierValue() === specifier &&
e.getNamedImports().some(i => i.getName() === name),
);
if (exist) {
console.warn(
` import ${name} in file ${this.sourcePath} already exists.`,
);
}
this.source.addImportDeclaration({
namedImports: [name],
moduleSpecifier: specifier,
});
}
getVariableValue<T extends SyntaxKind>(name: string, kind: T) {
return this.source
.getVariableDeclaration(name)
?.getInitializer()
?.asKindOrThrow<T>(kind);
}
save() {
return this.source.save();
}
}
interface Options {
name: string;
camelCaseName: string;
pascalCaseName: string;
constantName: string;
registryName: string;
isSupportTest: boolean;
}
const ROOT_DIR = process.cwd();
function copyTemplateFiles(options: Options) {
const { name, camelCaseName, constantName, pascalCaseName, isSupportTest } =
options;
const templateDir = path.join(__dirname, 'templates');
const sourceDir = path.join(ROOT_DIR, `./src/node-registries/${name}`);
const eta = new Eta({ views: templateDir });
if (!fs.existsSync(sourceDir)) {
fs.mkdirSync(sourceDir, { recursive: true });
}
const templates = fs.readdirSync(templateDir);
templates.forEach(temp => {
const str = eta.render(temp, {
PASCAL_NAME_PLACE_HOLDER: pascalCaseName,
CAMEL_NAME_PLACE_HOLDER: camelCaseName,
CONSTANT_NAME_PLACE_HOLDER: constantName,
IS_SUPPORT_TEST: isSupportTest,
});
fs.writeFileSync(
path.join(sourceDir, temp.replace(/\.eta$/, '')),
str,
'utf-8',
);
});
}
async function insertSourceCode(options: Options) {
const { pascalCaseName, registryName, name } = options;
// node-registries/index.ts
const nodeRegistriesIndex = new InsertSourceCode(
path.join(ROOT_DIR, './src/node-registries/index.ts'),
);
nodeRegistriesIndex.addNamedExport(registryName, `./${name}`);
await nodeRegistriesIndex.save();
// src/nodes-v2/constants.ts;
const nodeV2Constants = new InsertSourceCode(
path.join(ROOT_DIR, './src/nodes-v2/constants.ts'),
);
nodeV2Constants.addNamedImport(registryName, '@/node-registries');
nodeV2Constants
.getVariableValue('NODES_V2', SyntaxKind.ArrayLiteralExpression)
?.addElement(registryName, { useNewLines: true });
await nodeV2Constants.save();
// components/node-render/node-render-new/content/index.tsx
const nodeRenderContentIndex = new InsertSourceCode(
path.join(
ROOT_DIR,
'./src/components/node-render/node-render-new/content/index.tsx',
),
);
nodeRenderContentIndex.addNamedImport(
`${pascalCaseName}Content`,
`@/node-registries/${name}`,
);
nodeRenderContentIndex
.getVariableValue('ContentMap', SyntaxKind.ObjectLiteralExpression)
?.addPropertyAssignment({
name: `[StandardNodeType.${pascalCaseName}]`,
initializer: `${pascalCaseName}Content`,
});
await nodeRenderContentIndex.save();
}
async function main() {
const name = await input({
message:
'Enter component name (use "-" as separator), e.g."database-create":',
required: true,
});
const camelCaseName = await input({
message: 'Use camelCase (lower camel) for variable prefixes:',
default: camelCase(name),
required: true,
});
const pascalCaseName = await input({
message: 'Use PascalCase (Upper Camel) for class names:',
default: upperFirst(camelCaseName),
required: true,
});
const isSupportTest = await confirm({
message: 'Is single-node testing supported?',
default: false,
});
const constantName = toUpper(snakeCase(name));
const registryName = `${constantName}_NODE_REGISTRY`;
const options = {
name,
camelCaseName,
pascalCaseName,
constantName,
registryName,
isSupportTest,
};
copyTemplateFiles(options);
await insertSourceCode(options);
console.log('done.');
}
main();

@ -1,150 +0,0 @@
const path = require('path');
const fs = require('fs');
const ROOT_DIR = process.cwd();
// Tool function aa-bb-cc - > AaBbCc
const getPascalName = name =>
name
.split('-')
.map(s => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('');
// Tool function aa-bb-cc - > aaBbCc
const getCamelName = name =>
name
.split('-')
.map((s, i) => (i === 0 ? s : s.slice(0, 1).toUpperCase() + s.slice(1)))
.join('');
// Tool function aa-bb-cc - > AA_BB_CC
const getConstantName = name =>
name
.split('-')
.map(s => s.toUpperCase())
.join('_');
module.exports = plop => {
// Register a new action to add new node registration information in the export and registration files
plop.setActionType('registryNode', async answers => {
const { name, pascalName, supportTest } = answers;
const constantName = getConstantName(name);
const registryName = `${constantName}_NODE_REGISTRY`;
// Modify the export file
const nodeExportFilePath = './src/node-registries/index.ts';
const nodeContent = fs.readFileSync(nodeExportFilePath, 'utf8');
const nodeContentNew = nodeContent.replace(
'// cli 脚本插入标识(registry),请勿修改/删除此行注释',
`export { ${registryName} } from './${name}';
// The cli script inserts the identifier (registry), please do not modify/delete this line comment `,
);
fs.writeFileSync(nodeExportFilePath, nodeContentNew, 'utf8');
// Modify registration documents
const nodeRegistryFilePath = './src/nodes-v2/constants.ts';
const nodeRegistryContent = fs.readFileSync(nodeRegistryFilePath, 'utf8');
const nodeRegistryContentNew = nodeRegistryContent
.replace(
'// cli 脚本插入标识(import),请勿修改/删除此行注释',
`${registryName},
// The cli script inserts the identity (import), please do not modify/delete this line comment `,
)
.replace(
'// cli 脚本插入标识(registry),请勿修改/删除此行注释',
`// cli 脚本插入标识(registry),请勿修改/删除此行注释
${registryName},`,
);
fs.writeFileSync(nodeRegistryFilePath, nodeRegistryContentNew, 'utf8');
// Modify the node-content registration file
const nodeContentRegistryFilePath =
'./src/components/node-render/node-render-new/content/index.tsx';
const nodeContentRegistryContent = fs.readFileSync(
nodeContentRegistryFilePath,
'utf8',
);
const nodeContentRegistryContentNew = nodeContentRegistryContent
.replace(
'// cli 脚本插入标识(import),请勿修改/删除此行注释',
`import { ${pascalName}Content } from '@/node-registries/${name}';
// The cli script inserts the identity (import), please do not modify/delete this line comment `,
)
.replace(
'// cli 脚本插入标识(registry),请勿修改/删除此行注释',
`[StandardNodeType.${pascalName}]: ${pascalName}Content,
// The cli script inserts the identifier (registry), please do not modify/delete this line comment `,
);
fs.writeFileSync(
nodeContentRegistryFilePath,
nodeContentRegistryContentNew,
'utf8',
);
// If the node does not need to support single-node testing, delete the node-test file
const testFilePath = path.resolve(
ROOT_DIR,
`./src/node-registries/${name}/node-test.ts`,
);
if (!supportTest && fs.existsSync(testFilePath)) {
fs.unlinkSync(testFilePath);
}
return `节点 ${name} 已注册`;
});
// Register a new generator for creating new node directories and files
plop.setGenerator('create node', {
description: 'generate template',
prompts: [
{
type: 'input',
name: 'name',
message:
'请输入组件名称,以"-"(空格)分隔,用于生成目录名称, eg: "database-create"',
},
{
type: 'input',
name: 'pascalName',
message:
'请确认大写驼峰命名,用于类名,注意特殊命名: http -> HTTP ,而不是 http -> Http: ',
default: answers => getPascalName(answers.name),
},
{
type: 'input',
name: 'camelName',
message:
'请确认小写驼峰命名,用于变量前缀,注意特殊命名: my-ai -> myAI,而不是 my-ai -> myAi: ',
default: answers => getCamelName(answers.name),
},
{
type: 'confirm',
name: 'supportTest',
message: '是否支持单节点测试?',
default: false,
},
],
actions: data => {
const { name, pascalName, camelName, supportTest } = data;
const constantName = getConstantName(data.name);
const actions = [
{
type: 'addMany',
destination: path.resolve(ROOT_DIR, `./src/node-registries/${name}`),
templateFiles: 'templates',
data: {
PASCAL_NAME_PLACE_HOLDER: pascalName,
CAMEL_NAME_PLACE_HOLDER: camelName,
CONSTANT_NAME_PLACE_HOLDER: constantName,
SUPPORT_TEST: supportTest,
},
},
{
type: 'registryNode',
},
];
return actions;
},
});
};

@ -13,7 +13,7 @@ import { type FormData } from './types';
import { FormRender } from './form';
import { transformOnInit, transformOnSubmit } from './data-transformer';
export const {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META: FormMetaV2<FormData> = {
export const <%= it.CONSTANT_NAME_PLACE_HOLDER %>_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <FormRender />,

@ -17,7 +17,7 @@ export const FormRender = () => (
<OutputsField
title={I18n.t('workflow_detail_node_output')}
tooltip={I18n.t('node_http_response_data')}
id="{{CAMEL_NAME_PLACE_HOLDER}}-node-outputs"
id="<%= it.CAMEL_NAME_PLACE_HOLDER %>-node-outputs"
name="outputs"
topLevelReadonly={true}
customReadonly

@ -0,0 +1,2 @@
export { <%= it.CONSTANT_NAME_PLACE_HOLDER %>_NODE_REGISTRY } from './node-registry';
export { <%= it.PASCAL_NAME_PLACE_HOLDER %>Content } from './node-content';

@ -1,2 +0,0 @@
export { {{CONSTANT_NAME_PLACE_HOLDER}}_NODE_REGISTRY } from './node-registry';
export { {{PASCAL_NAME_PLACE_HOLDER}}Content } from './node-content';

@ -1,6 +1,6 @@
import { InputParameters, Outputs } from '../common/components';
export function {{PASCAL_NAME_PLACE_HOLDER}}Content() {
export function <%= it.PASCAL_NAME_PLACE_HOLDER %>Content() {
return (
<>
<InputParameters />

@ -7,21 +7,19 @@ import {
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META } from './form-meta';
import { <%= it.CONSTANT_NAME_PLACE_HOLDER %>_FORM_META } from './form-meta';
import { INPUT_PATH } from './constants';
{{#if SUPPORT_TEST}}
import { test, type NodeTestMeta } from './node-test';
{{/if}}
export const {{CONSTANT_NAME_PLACE_HOLDER}}_NODE_REGISTRY: WorkflowNodeRegistry{{#if SUPPORT_TEST}}<NodeTestMeta>{{/if}} = {
type: StandardNodeType.{{PASCAL_NAME_PLACE_HOLDER}},
export const <%= it.CONSTANT_NAME_PLACE_HOLDER %>_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
type: StandardNodeType.<%= it.PASCAL_NAME_PLACE_HOLDER %>,
meta: {
nodeDTOType: StandardNodeType.{{PASCAL_NAME_PLACE_HOLDER}},
nodeDTOType: StandardNodeType.<%= it.PASCAL_NAME_PLACE_HOLDER %>,
size: { width: 360, height: 130.7 },
nodeMetaPath: DEFAULT_NODE_META_PATH,
outputsPath: DEFAULT_OUTPUTS_PATH,
inputParametersPath: INPUT_PATH,
test{{#unless SUPPORT_TEST}}: false{{/unless}},
test,
},
formMeta: {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META,
formMeta: <%= it.CONSTANT_NAME_PLACE_HOLDER %>_FORM_META,
};

@ -0,0 +1,5 @@
import type { NodeTestMeta } from '@/test-run-kit';
const test: NodeTestMeta = <%= it.IS_SUPPORT_TEST %>;
export { test, type NodeTestMeta };

@ -1,5 +0,0 @@
import type { NodeTestMeta } from '@/test-run-kit';
const test: NodeTestMeta = true;
export { test, type NodeTestMeta };

@ -52,7 +52,6 @@ import { DatabaseDeleteContent } from './database-delete-content';
import { DatabaseCreateContent } from './database-create-content';
import { DatabaseContent } from './database-content';
import { CommonContent } from './common-content';
// CLI script insert ID (import), do not modify/delete this line comment
import styles from './index.module.less';
@ -91,7 +90,6 @@ const ContentMap = {
[StandardNodeType.Api]: PluginContent,
[StandardNodeType.Variable]: VariableContent,
[StandardNodeType.JsonStringify]: JsonStringifyContent,
// The cli script inserts the identifier (registry), do not modify/delete this line comment
};
/**

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CODE_NODE_REGISTRY } from './code';
export { COMMENT_NODE_REGISTRY } from './comment';
export { DATABASE_NODE_REGISTRY } from './database/database-base';
@ -48,4 +47,3 @@ export { PLUGIN_NODE_REGISTRY } from './plugin';
export { SUB_WORKFLOW_NODE_REGISTRY } from './sub-workflow';
export { VARIABLE_NODE_REGISTRY } from './variable';
export { JSON_STRINGIFY_NODE_REGISTRY } from './json-stringify';
// The cli script inserts the identifier (registry), do not modify/delete this line comment

@ -52,7 +52,6 @@ import {
SUB_WORKFLOW_NODE_REGISTRY,
VARIABLE_NODE_REGISTRY,
JSON_STRINGIFY_NODE_REGISTRY,
// CLI script insert ID (import), do not modify/delete this line comment
} from '@/node-registries';
import {
@ -69,7 +68,6 @@ import {
} from './chat';
export const NODES_V2 = [
// The cli script inserts the identifier (registry), do not modify/delete this line comment
JSON_STRINGIFY_NODE_REGISTRY,
IF_NODE_REGISTRY,
INTENT_NODE_REGISTRY,

@ -6,7 +6,8 @@
"stories",
"vitest.config.ts",
"tailwind.config.ts",
"src/**/*.test.ts"
"src/**/*.test.ts",
"scripts/**/*.ts"
],
"exclude": ["dist"],
"references": [

Loading…
Cancel
Save