This commit is contained in:
@@ -0,0 +1 @@
|
||||
.download
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import '../dist/index.js';
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { defineFlatConfig } from '@flowgram.ai/eslint-config';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineFlatConfig({
|
||||
preset: 'web',
|
||||
packageRoot: __dirname,
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: '18',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@flowgram.ai/cli",
|
||||
"version": "0.1.8",
|
||||
"description": "A CLI tool to create demo projects or sync materials",
|
||||
"repository": "https://github.com/bytedance/flowgram.ai",
|
||||
"bin": {
|
||||
"flowgram-cli": "./bin/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"bin",
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist",
|
||||
"watch": "tsup src/index.ts --format esm,cjs --out-dir dist --watch",
|
||||
"start": "node bin/index.js",
|
||||
"lint": "eslint ./src --cache",
|
||||
"lint:fix": "eslint ./src --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^9.1.0",
|
||||
"commander": "^11.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"tar": "7.4.3",
|
||||
"inquirer": "^12.9.4",
|
||||
"ignore": "~7.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flowgram.ai/eslint-config": "workspace:*",
|
||||
"@types/download": "8.0.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "^18",
|
||||
"@types/inquirer": "^9.0.9",
|
||||
"tsup": "^8.0.1",
|
||||
"eslint": "^9.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import * as tar from 'tar';
|
||||
import inquirer from 'inquirer';
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const updateFlowGramVersions = (dependencies: any[], latestVersion: string) => {
|
||||
for (const packageName in dependencies) {
|
||||
if (packageName.startsWith('@flowgram.ai')) {
|
||||
dependencies[packageName] = latestVersion;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 https 下载文件
|
||||
function downloadFile(url: string, dest: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
fs.unlink(dest, () => reject(err));
|
||||
});
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlink(dest, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const createApp = async (projectName?: string) => {
|
||||
console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!'));
|
||||
const latest = execSync('npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest')
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
let folderName = '';
|
||||
|
||||
if (!projectName) {
|
||||
// 询问用户选择 demo 项目
|
||||
const { repo } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'repo',
|
||||
message: 'Select a demo to create:',
|
||||
choices: [
|
||||
{ name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },
|
||||
{ name: 'Free Layout Demo', value: 'demo-free-layout' },
|
||||
{ name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },
|
||||
{ name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' },
|
||||
{ name: 'Free Layout Nextjs Demo', value: 'demo-nextjs' },
|
||||
{ name: 'Free Layout Vite Demo Simple', value: 'demo-vite' },
|
||||
{ name: 'Demo Playground for infinite canvas', value: 'demo-playground' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
folderName = repo;
|
||||
} else {
|
||||
if (
|
||||
[
|
||||
'fixed-layout',
|
||||
'free-layout',
|
||||
'fixed-layout-simple',
|
||||
'free-layout-simple',
|
||||
'playground',
|
||||
'nextjs',
|
||||
].includes(projectName)
|
||||
) {
|
||||
folderName = `demo-${projectName}`;
|
||||
} else {
|
||||
console.error('Invalid projectName. Please run "npx create-app" to choose demo.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const targetDir = path.join(process.cwd());
|
||||
|
||||
// 下载 npm 包的 tarball
|
||||
const downloadPackage = async () => {
|
||||
try {
|
||||
const url = `https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`;
|
||||
const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);
|
||||
|
||||
console.log(chalk.blue(`Downloading ${url} ...`));
|
||||
await downloadFile(url, tempTarballPath);
|
||||
|
||||
fs.ensureDirSync(targetDir);
|
||||
|
||||
await tar.x({
|
||||
file: tempTarballPath,
|
||||
C: targetDir,
|
||||
});
|
||||
|
||||
fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName));
|
||||
|
||||
fs.unlinkSync(tempTarballPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error downloading or extracting package`);
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const res = await downloadPackage();
|
||||
|
||||
// 替换 package.json 内部的 @flowgram.ai 包版本为 latest
|
||||
const pkgJsonPath = path.join(targetDir, folderName, 'package.json');
|
||||
const data = fs.readFileSync(pkgJsonPath, 'utf-8');
|
||||
|
||||
const packageLatestVersion = execSync('npm view @flowgram.ai/core version --tag=latest latest')
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const jsonData = JSON.parse(data);
|
||||
if (jsonData.dependencies) {
|
||||
updateFlowGramVersions(jsonData.dependencies, packageLatestVersion);
|
||||
}
|
||||
|
||||
if (jsonData.devDependencies) {
|
||||
updateFlowGramVersions(jsonData.devDependencies, packageLatestVersion);
|
||||
}
|
||||
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
||||
|
||||
if (res) {
|
||||
console.log(chalk.green(`${folderName} Demo project created successfully!`));
|
||||
console.log(chalk.yellow('Run the following commands to start:'));
|
||||
console.log(chalk.cyan(` cd ${folderName}`));
|
||||
console.log(chalk.cyan(' npm install'));
|
||||
console.log(chalk.cyan(' npm start'));
|
||||
} else {
|
||||
console.log(chalk.red('Download failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading repo:', error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { traverseRecursiveTsFiles } from '../utils/ts-file';
|
||||
import { Project } from '../utils/project';
|
||||
import { loadNpm } from '../utils/npm';
|
||||
import { Material } from '../materials/material';
|
||||
|
||||
export async function findUsedMaterials() {
|
||||
// materialName can be undefined
|
||||
console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));
|
||||
|
||||
const project = await Project.getSingleton();
|
||||
project.printInfo();
|
||||
|
||||
const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');
|
||||
const materials: Material[] = Material.listAll(formMaterialPkg);
|
||||
|
||||
const allUsedMaterials = new Set<Material>();
|
||||
|
||||
const exportName2Material = new Map<string, Material>();
|
||||
|
||||
for (const material of materials) {
|
||||
if (!material.indexFile) {
|
||||
console.warn(`Material ${material.name} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);
|
||||
|
||||
material.allExportNames.forEach((exportName) => {
|
||||
exportName2Material.set(exportName, material);
|
||||
});
|
||||
}
|
||||
|
||||
for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {
|
||||
const fileMaterials = new Set<Material>();
|
||||
|
||||
let fileImportPrinted = false;
|
||||
for (const importDeclaration of tsFile.imports) {
|
||||
if (
|
||||
!importDeclaration.source.startsWith('@flowgram.ai/form-materials') ||
|
||||
!importDeclaration.namedImports?.length
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fileImportPrinted) {
|
||||
fileImportPrinted = true;
|
||||
console.log(chalk.bold(`\n👀 Searching ${tsFile.path}`));
|
||||
}
|
||||
|
||||
console.log(`🔍 ${importDeclaration.statement}`);
|
||||
|
||||
if (importDeclaration.namedImports) {
|
||||
importDeclaration.namedImports.forEach((namedImport) => {
|
||||
const material = exportName2Material.get(namedImport.imported);
|
||||
|
||||
if (material) {
|
||||
fileMaterials.add(material);
|
||||
allUsedMaterials.add(material);
|
||||
console.log(`import ${chalk.bold(material.fullName)} by ${namedImport.imported}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n📦 All used materials:'));
|
||||
console.log([...allUsedMaterials].map((_material) => _material.fullName).join(','));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { updateFlowgramVersion } from './update-version';
|
||||
import { syncMaterial } from './materials';
|
||||
import { findUsedMaterials } from './find-materials';
|
||||
import { createApp } from './create-app';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.name('flowgram-cli').version('1.0.0').description('Flowgram CLI');
|
||||
|
||||
program
|
||||
.command('create-app')
|
||||
.description('Create a new flowgram project')
|
||||
.argument('[string]', 'Project name')
|
||||
.action(async (projectName) => {
|
||||
await createApp(projectName);
|
||||
});
|
||||
|
||||
program
|
||||
.command('materials')
|
||||
.description('Sync materials to the project')
|
||||
.argument(
|
||||
'[string]',
|
||||
'Material name or names\nExample 1: components/variable-selector \nExample2: components/variable-selector,effect/provideJsonSchemaOutputs'
|
||||
)
|
||||
.option('--refresh-project-imports', 'Refresh project imports to copied materials', false)
|
||||
.option('--target-material-root-dir <string>', 'Target directory to copy materials')
|
||||
.option('--select-multiple', 'Select multiple materials', false)
|
||||
.action(async (materialName, options) => {
|
||||
await syncMaterial({
|
||||
materialName,
|
||||
refreshProjectImports: options.refreshProjectImports,
|
||||
targetMaterialRootDir: options.targetMaterialRootDir
|
||||
? path.join(process.cwd(), options.targetMaterialRootDir)
|
||||
: undefined,
|
||||
selectMultiple: options.selectMultiple,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('find-used-materials')
|
||||
.description('Find used materials in the project')
|
||||
.action(async () => {
|
||||
await findUsedMaterials();
|
||||
});
|
||||
|
||||
program
|
||||
.command('update-version')
|
||||
.description('Update flowgram version in the project')
|
||||
.argument('[string]', 'Flowgram version')
|
||||
.action(async (version) => {
|
||||
await updateFlowgramVersion(version);
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { traverseRecursiveTsFiles } from '../utils/ts-file';
|
||||
import { SyncMaterialContext } from './types';
|
||||
|
||||
interface CopyMaterialReturn {
|
||||
packagesToInstall: string[];
|
||||
}
|
||||
|
||||
export const copyMaterials = (ctx: SyncMaterialContext): CopyMaterialReturn => {
|
||||
const { selectedMaterials, project, formMaterialPkg, targetFormMaterialRoot } = ctx;
|
||||
const formMaterialDependencies = formMaterialPkg.dependencies;
|
||||
const packagesToInstall: Set<string> = new Set();
|
||||
|
||||
for (const material of selectedMaterials) {
|
||||
const sourceDir: string = material.sourceDir;
|
||||
const targetDir: string = path.join(targetFormMaterialRoot, material.type, material.name);
|
||||
|
||||
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
||||
|
||||
for (const file of traverseRecursiveTsFiles(targetDir)) {
|
||||
for (const importDeclaration of file.imports) {
|
||||
const { source } = importDeclaration;
|
||||
|
||||
if (source.startsWith('@/')) {
|
||||
// is inner import
|
||||
console.log(`Replace Import from ${source} to @flowgram.ai/form-materials`);
|
||||
file.replaceImport(
|
||||
[importDeclaration],
|
||||
[{ ...importDeclaration, source: '@flowgram.ai/form-materials' }]
|
||||
);
|
||||
packagesToInstall.add(`@flowgram.ai/form-materials@${project.flowgramVersion}`);
|
||||
} else if (!source.startsWith('.') && !source.startsWith('react')) {
|
||||
// check if is in form material dependencies
|
||||
const [dep, version] =
|
||||
Object.entries(formMaterialDependencies).find(([_key]) => source.startsWith(_key)) ||
|
||||
[];
|
||||
if (!dep) {
|
||||
continue;
|
||||
}
|
||||
if (dep.startsWith('@flowgram.ai/')) {
|
||||
packagesToInstall.add(`${dep}@${project.flowgramVersion}`);
|
||||
} else {
|
||||
packagesToInstall.add(`${dep}@${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
packagesToInstall: [...packagesToInstall],
|
||||
};
|
||||
};
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { Project } from '../utils/project';
|
||||
import { loadNpm } from '../utils/npm';
|
||||
import { MaterialCliOptions, SyncMaterialContext } from './types';
|
||||
import { getSelectedMaterials } from './select';
|
||||
import { executeRefreshProjectImport } from './refresh-project-import';
|
||||
import { Material } from './material';
|
||||
import { copyMaterials } from './copy';
|
||||
|
||||
export async function syncMaterial(cliOpts: MaterialCliOptions) {
|
||||
const { refreshProjectImports, targetMaterialRootDir } = cliOpts;
|
||||
|
||||
// materialName can be undefined
|
||||
console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));
|
||||
|
||||
const project = await Project.getSingleton();
|
||||
project.printInfo();
|
||||
|
||||
// where to place all material in target project
|
||||
const targetFormMaterialRoot =
|
||||
targetMaterialRootDir || path.join(project.projectPath, 'src', 'form-materials');
|
||||
console.log(chalk.black(` - Target material root: ${targetFormMaterialRoot}`));
|
||||
|
||||
if (!project.flowgramVersion) {
|
||||
throw new Error(
|
||||
chalk.red(
|
||||
'❌ Please install @flowgram.ai/fixed-layout-editor or @flowgram.ai/free-layout-editor'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');
|
||||
|
||||
let selectedMaterials: Material[] = await getSelectedMaterials(cliOpts, formMaterialPkg);
|
||||
|
||||
// Ensure material is defined before proceeding
|
||||
if (!selectedMaterials.length) {
|
||||
console.error(chalk.red('No material selected. Exiting.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const context: SyncMaterialContext = {
|
||||
selectedMaterials: selectedMaterials,
|
||||
project,
|
||||
formMaterialPkg,
|
||||
cliOpts,
|
||||
targetFormMaterialRoot,
|
||||
};
|
||||
|
||||
// Copy the materials to the project
|
||||
console.log(chalk.bold('🚀 The following materials will be added to your project'));
|
||||
console.log(selectedMaterials.map((material) => `📦 ${material.fullName}`).join('\n'));
|
||||
console.log('\n');
|
||||
|
||||
let { packagesToInstall } = copyMaterials(context);
|
||||
|
||||
// Refresh project imports
|
||||
if (refreshProjectImports) {
|
||||
console.log(chalk.bold('🚀 Refresh imports in your project'));
|
||||
executeRefreshProjectImport(context);
|
||||
}
|
||||
|
||||
// Install the dependencies
|
||||
await project.addDependencies(packagesToInstall);
|
||||
console.log(chalk.bold('\n✅ These npm dependencies is added to your package.json'));
|
||||
packagesToInstall.forEach((_package) => {
|
||||
console.log(`- ${_package}`);
|
||||
});
|
||||
console.log(chalk.bold(chalk.bold('\n➡️ Please run npm install to install dependencies\n')));
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { readdirSync } from 'fs';
|
||||
|
||||
import { getIndexTsFile } from '../utils/ts-file';
|
||||
import { LoadedNpmPkg } from '../utils/npm';
|
||||
|
||||
export class Material {
|
||||
protected static _all_materials_cache: Material[] = [];
|
||||
|
||||
static ALL_TYPES = [
|
||||
'components',
|
||||
'effects',
|
||||
'plugins',
|
||||
'shared',
|
||||
'validate',
|
||||
'form-plugins',
|
||||
'hooks',
|
||||
];
|
||||
|
||||
constructor(public type: string, public name: string, public formMaterialPkg: LoadedNpmPkg) {}
|
||||
|
||||
get fullName() {
|
||||
return `${this.type}/${this.name}`;
|
||||
}
|
||||
|
||||
get sourceDir() {
|
||||
return path.join(this.formMaterialPkg.srcPath, this.type, this.name);
|
||||
}
|
||||
|
||||
get indexFile() {
|
||||
return getIndexTsFile(this.sourceDir);
|
||||
}
|
||||
|
||||
get allExportNames() {
|
||||
return this.indexFile?.allExportNames || [];
|
||||
}
|
||||
|
||||
static listAll(formMaterialPkg: LoadedNpmPkg): Material[] {
|
||||
if (!this._all_materials_cache.length) {
|
||||
this._all_materials_cache = Material.ALL_TYPES.map((type) => {
|
||||
const materialsPath: string = path.join(formMaterialPkg.srcPath, type);
|
||||
return readdirSync(materialsPath)
|
||||
.map((_path: string) => {
|
||||
if (_path === 'index.ts') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Material(type, _path, formMaterialPkg);
|
||||
})
|
||||
.filter((material): material is Material => material !== null);
|
||||
}).flat();
|
||||
}
|
||||
|
||||
return this._all_materials_cache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { traverseRecursiveTsFiles } from '../utils/ts-file';
|
||||
import { ImportDeclaration, NamedImport } from '../utils/import';
|
||||
import { SyncMaterialContext } from './types';
|
||||
import { Material } from './material';
|
||||
|
||||
export function executeRefreshProjectImport(context: SyncMaterialContext) {
|
||||
const { selectedMaterials, project, targetFormMaterialRoot } = context;
|
||||
|
||||
const exportName2Material = new Map<string, Material>();
|
||||
|
||||
const targetModule = `@/${path.relative(project.srcPath, targetFormMaterialRoot)}`;
|
||||
|
||||
for (const material of selectedMaterials) {
|
||||
if (!material.indexFile) {
|
||||
console.warn(`Material ${material.name} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);
|
||||
|
||||
material.allExportNames.forEach((exportName) => {
|
||||
exportName2Material.set(exportName, material);
|
||||
});
|
||||
}
|
||||
|
||||
for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {
|
||||
for (const importDeclaration of tsFile.imports) {
|
||||
if (importDeclaration.source.startsWith('@flowgram.ai/form-materials')) {
|
||||
// Import Module and its related named Imported
|
||||
const restImports: NamedImport[] = [];
|
||||
const importMap: Record<string, NamedImport[]> = {};
|
||||
|
||||
if (!importDeclaration.namedImports) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const nameImport of importDeclaration.namedImports) {
|
||||
const material = exportName2Material.get(nameImport.imported);
|
||||
if (material) {
|
||||
const importModule = `${targetModule}/${material.fullName}`;
|
||||
importMap[importModule] = importMap[importModule] || [];
|
||||
importMap[importModule].push(nameImport);
|
||||
} else {
|
||||
restImports.push(nameImport);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(importMap).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextImports: ImportDeclaration[] = Object.entries(importMap).map(
|
||||
([importModule, namedImports]) => ({
|
||||
...importDeclaration,
|
||||
namedImports,
|
||||
source: importModule,
|
||||
})
|
||||
);
|
||||
|
||||
if (restImports?.length) {
|
||||
nextImports.unshift({
|
||||
...importDeclaration,
|
||||
namedImports: restImports,
|
||||
});
|
||||
}
|
||||
|
||||
tsFile.replaceImport([importDeclaration], nextImports);
|
||||
console.log(chalk.green(`🔄 Refresh Imports In: ${tsFile.path}`));
|
||||
|
||||
console.log(
|
||||
`From:\n${importDeclaration.statement}\nTo:\n${nextImports
|
||||
.map((item) => item.statement)
|
||||
.join('\n')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { LoadedNpmPkg } from '../utils/npm';
|
||||
import { MaterialCliOptions } from './types';
|
||||
import { Material } from './material';
|
||||
|
||||
export const getSelectedMaterials = async (
|
||||
cliOpts: MaterialCliOptions,
|
||||
formMaterialPkg: LoadedNpmPkg
|
||||
) => {
|
||||
const { materialName, selectMultiple } = cliOpts;
|
||||
|
||||
const materials: Material[] = Material.listAll(formMaterialPkg);
|
||||
|
||||
let selectedMaterials: Material[] = [];
|
||||
|
||||
// 1. Check if materialName is provided and exists in materials
|
||||
if (materialName) {
|
||||
selectedMaterials = materialName
|
||||
.split(',')
|
||||
.map((_name) => materials.find((_m) => _m.fullName === _name.trim())!)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// 2. If material not found or materialName not provided, prompt user to select
|
||||
if (!selectedMaterials.length) {
|
||||
console.log(chalk.yellow(`Material "${materialName}" not found. Please select from the list:`));
|
||||
|
||||
const choices = materials.map((_material) => ({
|
||||
name: _material.fullName,
|
||||
value: _material,
|
||||
}));
|
||||
if (selectMultiple) {
|
||||
// User select one component
|
||||
const result = await inquirer.prompt<{
|
||||
material: Material[]; // Specify type for prompt result
|
||||
}>([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'material',
|
||||
message: 'Select multiple materials to add:',
|
||||
choices: choices,
|
||||
},
|
||||
]);
|
||||
selectedMaterials = result.material;
|
||||
} else {
|
||||
// User select one component
|
||||
const result = await inquirer.prompt<{
|
||||
material: Material; // Specify type for prompt result
|
||||
}>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'material',
|
||||
message: 'Select one material to add:',
|
||||
choices: choices,
|
||||
},
|
||||
]);
|
||||
selectedMaterials = [result.material];
|
||||
}
|
||||
}
|
||||
|
||||
return selectedMaterials;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Project } from '../utils/project';
|
||||
import { LoadedNpmPkg } from '../utils/npm';
|
||||
import { Material } from './material';
|
||||
|
||||
export interface MaterialCliOptions {
|
||||
materialName?: string;
|
||||
refreshProjectImports?: boolean;
|
||||
targetMaterialRootDir?: string;
|
||||
selectMultiple?: boolean;
|
||||
}
|
||||
|
||||
export interface SyncMaterialContext {
|
||||
selectedMaterials: Material[];
|
||||
project: Project;
|
||||
formMaterialPkg: LoadedNpmPkg;
|
||||
cliOpts: MaterialCliOptions;
|
||||
targetFormMaterialRoot: string;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getLatestVersion } from '../utils/npm';
|
||||
import { traverseRecursiveFiles } from '../utils/file';
|
||||
|
||||
export async function updateFlowgramVersion(inputVersion?: string) {
|
||||
console.log(chalk.bold('🚀 Welcome to @flowgram.ai update-version helper'));
|
||||
|
||||
// Get latest version
|
||||
const latestVersion = await getLatestVersion('@flowgram.ai/editor');
|
||||
const currentPath = process.cwd();
|
||||
console.log('- Latest flowgram version: ', latestVersion);
|
||||
console.log('- Current Path: ', currentPath);
|
||||
|
||||
// User Input flowgram version, default is latestVersion
|
||||
const flowgramVersion: string = inputVersion || latestVersion;
|
||||
|
||||
for (const file of traverseRecursiveFiles(currentPath)) {
|
||||
if (file.path.endsWith('package.json')) {
|
||||
console.log('👀 Find package.json: ', file.path);
|
||||
let updated = false;
|
||||
const json = JSON.parse(file.content);
|
||||
if (json.dependencies) {
|
||||
for (const key in json.dependencies) {
|
||||
if (key.startsWith('@flowgram.ai/')) {
|
||||
updated = true;
|
||||
json.dependencies[key] = flowgramVersion;
|
||||
console.log(`- Update ${key} to ${flowgramVersion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json.devDependencies) {
|
||||
for (const key in json.devDependencies) {
|
||||
if (key.startsWith('@flowgram.ai/')) {
|
||||
updated = true;
|
||||
json.devDependencies[key] = flowgramVersion;
|
||||
console.log(`- Update ${key} to ${flowgramVersion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
file.write(JSON.stringify(json, null, 2));
|
||||
console.log(`✅ ${file.path} Updated`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export function extractNamedExports(content: string) {
|
||||
const valueExports = [];
|
||||
const typeExports = [];
|
||||
|
||||
// Collect all type definition names
|
||||
const typeDefinitions = new Set();
|
||||
const typePatterns = [/\b(?:type|interface)\s+(\w+)/g, /\bexport\s+(?:type|interface)\s+(\w+)/g];
|
||||
|
||||
let match;
|
||||
for (const pattern of typePatterns) {
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
typeDefinitions.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Match various export patterns
|
||||
const exportPatterns = [
|
||||
// export const/var/let/function/class/type/interface
|
||||
/\bexport\s+(const|var|let|function|class|type|interface)\s+(\w+)/g,
|
||||
// export { name1, name2 }
|
||||
/\bexport\s*\{([^}]+)\}/g,
|
||||
// export { name as alias }
|
||||
/\bexport\s*\{[^}]*\b(\w+)\s+as\s+(\w+)[^}]*\}/g,
|
||||
// export default function name()
|
||||
/\bexport\s+default\s+(?:function|class)\s+(\w+)/g,
|
||||
// export type { Type1, Type2 }
|
||||
/\bexport\s+type\s*\{([^}]+)\}/g,
|
||||
// export type { Original as Alias }
|
||||
/\bexport\s+type\s*\{[^}]*\b(\w+)\s+as\s+(\w+)[^}]*\}/g,
|
||||
];
|
||||
|
||||
// Handle first pattern: export const/var/let/function/class/type/interface
|
||||
exportPatterns[0].lastIndex = 0;
|
||||
while ((match = exportPatterns[0].exec(content)) !== null) {
|
||||
const [, kind, name] = match;
|
||||
if (kind === 'type' || kind === 'interface' || typeDefinitions.has(name)) {
|
||||
typeExports.push(name);
|
||||
} else {
|
||||
valueExports.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle second pattern: export { name1, name2 }
|
||||
exportPatterns[1].lastIndex = 0;
|
||||
while ((match = exportPatterns[1].exec(content)) !== null) {
|
||||
const exportsList = match[1]
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && !item.includes(' as '));
|
||||
|
||||
for (const name of exportsList) {
|
||||
if (name.startsWith('type ')) {
|
||||
typeExports.push(name.replace('type ', '').trim());
|
||||
} else if (typeDefinitions.has(name)) {
|
||||
typeExports.push(name);
|
||||
} else {
|
||||
valueExports.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle third pattern: export { name as alias }
|
||||
exportPatterns[2].lastIndex = 0;
|
||||
while ((match = exportPatterns[2].exec(content)) !== null) {
|
||||
const [, original, alias] = match;
|
||||
if (typeDefinitions.has(original)) {
|
||||
typeExports.push(alias);
|
||||
} else {
|
||||
valueExports.push(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle fourth pattern: export default function name()
|
||||
exportPatterns[3].lastIndex = 0;
|
||||
while ((match = exportPatterns[3].exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
if (typeDefinitions.has(name)) {
|
||||
typeExports.push(name);
|
||||
} else {
|
||||
valueExports.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle fifth pattern: export type { Type1, Type2 }
|
||||
exportPatterns[4].lastIndex = 0;
|
||||
while ((match = exportPatterns[4].exec(content)) !== null) {
|
||||
const exportsList = match[1]
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && !item.includes(' as '));
|
||||
|
||||
for (const name of exportsList) {
|
||||
typeExports.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sixth pattern: export type { Original as Alias }
|
||||
exportPatterns[5].lastIndex = 0;
|
||||
while ((match = exportPatterns[5].exec(content)) !== null) {
|
||||
const [, original, alias] = match;
|
||||
typeExports.push(alias);
|
||||
}
|
||||
|
||||
// Deduplicate and sort
|
||||
return {
|
||||
values: [...new Set(valueExports)].sort(),
|
||||
types: [...new Set(typeExports)].sort(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import ignore, { Ignore } from 'ignore';
|
||||
|
||||
export class File {
|
||||
content: string;
|
||||
|
||||
isUtf8: boolean;
|
||||
|
||||
relativePath: string;
|
||||
|
||||
path: string;
|
||||
|
||||
suffix: string;
|
||||
|
||||
constructor(filePath: string, public root: string = '/') {
|
||||
this.path = filePath;
|
||||
this.relativePath = path.relative(this.root, this.path);
|
||||
this.suffix = path.extname(this.path);
|
||||
|
||||
// Check if exists
|
||||
if (!fs.existsSync(this.path)) {
|
||||
throw Error(`File ${path} Not Exists`);
|
||||
}
|
||||
|
||||
// If no utf-8, skip
|
||||
try {
|
||||
this.content = fs.readFileSync(this.path, 'utf-8');
|
||||
this.isUtf8 = true;
|
||||
} catch (e) {
|
||||
this.isUtf8 = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
replace(updater: (content: string) => string) {
|
||||
if (!this.isUtf8) {
|
||||
console.warn('Not UTF-8 file skipped: ', this.path);
|
||||
return;
|
||||
}
|
||||
this.content = updater(this.content);
|
||||
fs.writeFileSync(this.path, this.content, 'utf-8');
|
||||
}
|
||||
|
||||
write(nextContent: string) {
|
||||
this.content = nextContent;
|
||||
fs.writeFileSync(this.path, this.content, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
export function* traverseRecursiveFilePaths(
|
||||
folder: string,
|
||||
ig: Ignore = ignore().add('.git'),
|
||||
root: string = folder
|
||||
): Generator<string> {
|
||||
const files = fs.readdirSync(folder);
|
||||
|
||||
// add .gitignore to ignore if exists
|
||||
if (fs.existsSync(path.join(folder, '.gitignore'))) {
|
||||
ig.add(fs.readFileSync(path.join(folder, '.gitignore'), 'utf-8'));
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(folder, file);
|
||||
|
||||
if (ig.ignores(path.relative(root, filePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
yield* traverseRecursiveFilePaths(filePath, ig, root);
|
||||
} else {
|
||||
yield filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* traverseRecursiveFiles(folder: string): Generator<File> {
|
||||
for (const filePath of traverseRecursiveFilePaths(folder)) {
|
||||
yield new File(filePath, folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface NamedImport {
|
||||
local?: string;
|
||||
imported: string;
|
||||
typeOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cases
|
||||
* import { A, B } from 'module';
|
||||
* import A from 'module';
|
||||
* import * as C from 'module';
|
||||
* import D, { type E, F } from 'module';
|
||||
* import A, { B as B1 } from 'module';
|
||||
*/
|
||||
export interface ImportDeclaration {
|
||||
// origin statement
|
||||
statement: string;
|
||||
|
||||
// import { A, B } from 'module';
|
||||
namedImports?: NamedImport[];
|
||||
|
||||
// import A from 'module';
|
||||
defaultImport?: string;
|
||||
|
||||
// import * as C from 'module';
|
||||
namespaceImport?: string;
|
||||
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function assembleImport(declaration: ImportDeclaration): string {
|
||||
const { namedImports, defaultImport, namespaceImport, source } = declaration;
|
||||
const importClauses = [];
|
||||
if (namedImports) {
|
||||
importClauses.push(
|
||||
`{ ${namedImports
|
||||
.map(
|
||||
({ local, imported, typeOnly }) =>
|
||||
`${typeOnly ? 'type ' : ''}${imported}${local ? ` as ${local}` : ''}`
|
||||
)
|
||||
.join(', ')} }`
|
||||
);
|
||||
}
|
||||
if (defaultImport) {
|
||||
importClauses.push(defaultImport);
|
||||
}
|
||||
if (namespaceImport) {
|
||||
importClauses.push(`* as ${namespaceImport}`);
|
||||
}
|
||||
return `import ${importClauses.join(', ')} from '${source}';`;
|
||||
}
|
||||
|
||||
export function replaceImport(
|
||||
fileContent: string,
|
||||
origin: ImportDeclaration,
|
||||
replaceTo: ImportDeclaration[]
|
||||
): string {
|
||||
const replaceImportStatements = replaceTo.map(assembleImport);
|
||||
// replace origin statement
|
||||
return fileContent.replace(origin.statement, replaceImportStatements.join('\n'));
|
||||
}
|
||||
|
||||
export function* traverseFileImports(fileContent: string): Generator<ImportDeclaration> {
|
||||
// 匹配所有 import 语句的正则表达式
|
||||
const importRegex =
|
||||
/import\s+([^{}*,]*?)?(?:\s*\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,?)?(?:\s*\{([^}]*)\}\s*,?)?(?:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,?)?\s*from\s*['"`]([^'"`]+)['"`]\;?/g;
|
||||
|
||||
let match;
|
||||
while ((match = importRegex.exec(fileContent)) !== null) {
|
||||
const [fullMatch, defaultPart, namespacePart, namedPart, defaultPart2, source] = match;
|
||||
|
||||
const declaration: ImportDeclaration = {
|
||||
statement: fullMatch,
|
||||
source: source,
|
||||
};
|
||||
|
||||
// 处理默认导入
|
||||
const defaultImport = defaultPart?.trim() || defaultPart2?.trim();
|
||||
if (defaultImport && !namespacePart && !namedPart) {
|
||||
declaration.defaultImport = defaultImport;
|
||||
} else if (defaultImport && (namespacePart || namedPart)) {
|
||||
declaration.defaultImport = defaultImport;
|
||||
}
|
||||
|
||||
// 处理命名空间导入 (* as)
|
||||
if (namespacePart) {
|
||||
declaration.namespaceImport = namespacePart.trim();
|
||||
}
|
||||
|
||||
// 处理命名导入
|
||||
if (namedPart) {
|
||||
const namedImports = [];
|
||||
const namedItems = namedPart
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const item of namedItems) {
|
||||
const typeOnly = item.startsWith('type ');
|
||||
const cleanItem = typeOnly ? item.slice(5).trim() : item;
|
||||
|
||||
if (cleanItem.includes(' as ')) {
|
||||
const [imported, local] = cleanItem.split(' as ').map((s) => s.trim());
|
||||
namedImports.push({
|
||||
imported,
|
||||
local,
|
||||
typeOnly,
|
||||
});
|
||||
} else {
|
||||
namedImports.push({
|
||||
imported: cleanItem,
|
||||
typeOnly,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (namedImports.length > 0) {
|
||||
declaration.namedImports = namedImports;
|
||||
}
|
||||
}
|
||||
|
||||
yield declaration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import * as tar from 'tar';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export class LoadedNpmPkg {
|
||||
constructor(public name: string, public version: string, public path: string) {}
|
||||
|
||||
get srcPath() {
|
||||
return path.join(this.path, 'src');
|
||||
}
|
||||
|
||||
get distPath() {
|
||||
return path.join(this.path, 'dist');
|
||||
}
|
||||
|
||||
protected _packageJson: Record<string, any>;
|
||||
|
||||
get packageJson() {
|
||||
if (!this._packageJson) {
|
||||
this._packageJson = JSON.parse(readFileSync(path.join(this.path, 'package.json'), 'utf8'));
|
||||
}
|
||||
return this._packageJson;
|
||||
}
|
||||
|
||||
get dependencies(): Record<string, string> {
|
||||
return this.packageJson.dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 https 下载文件
|
||||
function downloadFile(url: string, dest: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
fs.unlink(dest, () => reject(err));
|
||||
});
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlink(dest, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLatestVersion(packageName: string): Promise<string> {
|
||||
return execSync(`npm view ${packageName} version --tag=latest`).toString().trim();
|
||||
}
|
||||
|
||||
export async function loadNpm(packageName: string): Promise<LoadedNpmPkg> {
|
||||
const packageLatestVersion = await getLatestVersion(packageName);
|
||||
|
||||
const packagePath = path.join(__dirname, `./.download/${packageName}-${packageLatestVersion}`);
|
||||
|
||||
if (existsSync(packagePath)) {
|
||||
return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取 tarball 地址
|
||||
const tarballUrl = execSync(`npm view ${packageName}@${packageLatestVersion} dist.tarball`)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
// 临时 tarball 路径
|
||||
const tempTarballPath = path.join(
|
||||
__dirname,
|
||||
`./.download/${packageName}-${packageLatestVersion}.tgz`
|
||||
);
|
||||
|
||||
// 确保目录存在
|
||||
fs.ensureDirSync(path.dirname(tempTarballPath));
|
||||
|
||||
// 下载 tarball
|
||||
await downloadFile(tarballUrl, tempTarballPath);
|
||||
|
||||
fs.ensureDirSync(packagePath);
|
||||
|
||||
// 解压到目标目录
|
||||
await tar.x({
|
||||
file: tempTarballPath,
|
||||
cwd: packagePath,
|
||||
strip: 1,
|
||||
});
|
||||
|
||||
// 删除临时文件
|
||||
fs.unlinkSync(tempTarballPath);
|
||||
|
||||
return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading or extracting package`);
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getLatestVersion } from './npm';
|
||||
|
||||
interface PackageJson {
|
||||
dependencies: { [key: string]: string };
|
||||
devDependencies?: { [key: string]: string };
|
||||
peerDependencies?: { [key: string]: string };
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class Project {
|
||||
flowgramVersion?: string;
|
||||
|
||||
projectPath: string;
|
||||
|
||||
packageJsonPath: string;
|
||||
|
||||
packageJson: PackageJson;
|
||||
|
||||
srcPath: string;
|
||||
|
||||
protected constructor() {}
|
||||
|
||||
async init() {
|
||||
// get nearest package.json
|
||||
let projectPath: string = process.cwd();
|
||||
|
||||
while (projectPath !== '/' && !existsSync(path.join(projectPath, 'package.json'))) {
|
||||
projectPath = path.join(projectPath, '..');
|
||||
}
|
||||
if (projectPath === '/') {
|
||||
throw new Error('Please run this command in a valid project');
|
||||
}
|
||||
|
||||
this.projectPath = projectPath;
|
||||
|
||||
this.srcPath = path.join(projectPath, 'src');
|
||||
this.packageJsonPath = path.join(projectPath, 'package.json');
|
||||
this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf8'));
|
||||
|
||||
this.flowgramVersion =
|
||||
this.packageJson.dependencies['@flowgram.ai/fixed-layout-editor'] ||
|
||||
this.packageJson.dependencies['@flowgram.ai/free-layout-editor'] ||
|
||||
this.packageJson.dependencies['@flowgram.ai/editor'];
|
||||
}
|
||||
|
||||
async addDependency(dependency: string) {
|
||||
let name: string;
|
||||
let version: string;
|
||||
|
||||
// 处理作用域包(如 @types/react@1.0.0)
|
||||
const lastAtIndex = dependency.lastIndexOf('@');
|
||||
|
||||
if (lastAtIndex <= 0) {
|
||||
// 没有@符号 或者@在开头(如 @types/react)
|
||||
name = dependency;
|
||||
version = await getLatestVersion(name);
|
||||
} else {
|
||||
// 正常分割包名和版本
|
||||
name = dependency.substring(0, lastAtIndex);
|
||||
version = dependency.substring(lastAtIndex + 1);
|
||||
|
||||
// 如果版本部分为空,获取最新版本
|
||||
if (!version.trim()) {
|
||||
version = await getLatestVersion(name);
|
||||
}
|
||||
}
|
||||
|
||||
this.packageJson.dependencies[name] = version;
|
||||
writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));
|
||||
}
|
||||
|
||||
async addDependencies(dependencies: string[]) {
|
||||
for (const dependency of dependencies) {
|
||||
await this.addDependency(dependency);
|
||||
}
|
||||
}
|
||||
|
||||
writeToPackageJsonFile() {
|
||||
writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));
|
||||
}
|
||||
|
||||
printInfo() {
|
||||
console.log(chalk.bold('Project Info:'));
|
||||
console.log(chalk.black(` - Flowgram Version: ${this.flowgramVersion}`));
|
||||
console.log(chalk.black(` - Project Path: ${this.projectPath}`));
|
||||
}
|
||||
|
||||
static async getSingleton() {
|
||||
const info = new Project();
|
||||
await info.init();
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import path, { join } from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { assembleImport, ImportDeclaration, traverseFileImports } from './import';
|
||||
import { File, traverseRecursiveFilePaths } from './file';
|
||||
import { extractNamedExports } from './export';
|
||||
|
||||
class TsFile extends File {
|
||||
exports: {
|
||||
values: string[];
|
||||
types: string[];
|
||||
} = {
|
||||
values: [],
|
||||
types: [],
|
||||
};
|
||||
|
||||
imports: ImportDeclaration[] = [];
|
||||
|
||||
get allExportNames() {
|
||||
return [...this.exports.values, ...this.exports.types];
|
||||
}
|
||||
|
||||
constructor(filePath: string, root?: string) {
|
||||
super(filePath, root);
|
||||
|
||||
this.exports = extractNamedExports(fs.readFileSync(filePath, 'utf-8'));
|
||||
this.imports = Array.from(traverseFileImports(fs.readFileSync(filePath, 'utf-8')));
|
||||
}
|
||||
|
||||
addImport(importDeclarations: ImportDeclaration[]) {
|
||||
importDeclarations.forEach((importDeclaration) => {
|
||||
importDeclaration.statement = assembleImport(importDeclaration);
|
||||
});
|
||||
// add in last import statement
|
||||
this.replace((content) => {
|
||||
const lastImportStatement = this.imports[this.imports.length - 1];
|
||||
return content.replace(
|
||||
lastImportStatement.statement,
|
||||
`${lastImportStatement?.statement}\n${importDeclarations.map((item) => item.statement)}\n`
|
||||
);
|
||||
});
|
||||
this.imports.push(...importDeclarations);
|
||||
}
|
||||
|
||||
removeImport(importDeclarations: ImportDeclaration[]) {
|
||||
this.replace((content) =>
|
||||
importDeclarations.reduce((prev, cur) => prev.replace(cur.statement, ''), content)
|
||||
);
|
||||
this.imports = this.imports.filter((item) => !importDeclarations.includes(item));
|
||||
}
|
||||
|
||||
replaceImport(oldImports: ImportDeclaration[], newImports: ImportDeclaration[]) {
|
||||
newImports.forEach((importDeclaration) => {
|
||||
importDeclaration.statement = assembleImport(importDeclaration);
|
||||
});
|
||||
this.replace((content) => {
|
||||
oldImports.forEach((oldImport, idx) => {
|
||||
const replaceTo = newImports[idx];
|
||||
if (replaceTo) {
|
||||
content = content.replace(oldImport.statement, replaceTo.statement);
|
||||
this.imports.map((_import) => {
|
||||
if (_import.statement === oldImport.statement) {
|
||||
_import = replaceTo;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content = content.replace(oldImport.statement, '');
|
||||
this.imports = this.imports.filter(
|
||||
(_import) => _import.statement !== oldImport.statement
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const restNewImports = newImports.slice(oldImports.length);
|
||||
if (restNewImports.length > 0) {
|
||||
const lastImportStatement = newImports[oldImports.length - 1].statement;
|
||||
content = content.replace(
|
||||
lastImportStatement,
|
||||
`${lastImportStatement}
|
||||
${restNewImports.map((item) => item.statement).join('\n')}
|
||||
`
|
||||
);
|
||||
}
|
||||
this.imports.push(...restNewImports);
|
||||
|
||||
return content;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function* traverseRecursiveTsFiles(folder: string): Generator<TsFile> {
|
||||
for (const filePath of traverseRecursiveFilePaths(folder)) {
|
||||
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
||||
yield new TsFile(filePath, folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getIndexTsFile(folder: string): TsFile | undefined {
|
||||
// ts or tsx
|
||||
const files = fs.readdirSync(folder);
|
||||
for (const file of files) {
|
||||
if (file === 'index.ts' || file === 'index.tsx') {
|
||||
return new TsFile(path.join(folder, file), folder);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"strictPropertyInitialization": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitAny": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["es6", "dom", "es2020", "es2019.Array"]
|
||||
},
|
||||
"include": ["./src"],
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
shims: true,
|
||||
})
|
||||
Reference in New Issue
Block a user