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
+1
View File
@@ -0,0 +1 @@
.download
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/index.js';
+23
View File
@@ -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',
},
},
});
+45
View File
@@ -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/"
}
}
+162
View File
@@ -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;
}
};
+75
View File
@@ -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(','));
}
+63
View File
@@ -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);
+60
View File
@@ -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],
};
};
+78
View File
@@ -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')));
}
+61
View File
@@ -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')}`
);
}
}
}
}
+69
View File
@@ -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;
};
+23
View File
@@ -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;
}
+53
View File
@@ -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`);
}
}
}
}
+114
View File
@@ -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(),
};
}
+88
View File
@@ -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);
}
}
+129
View File
@@ -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;
}
}
+117
View File
@@ -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;
}
}
+103
View File
@@ -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;
}
}
+113
View File
@@ -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;
}
+20
View File
@@ -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"],
}
+10
View File
@@ -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,
})