/* * 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(name: string, kind: T) { return this.source .getVariableDeclaration(name) ?.getInitializer() ?.asKindOrThrow(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();