feat(material): use tsx in cli (#295)

This commit is contained in:
Yiwei Mao 2025-05-30 14:42:38 +08:00 committed by GitHub
parent 8a6b50ba78
commit b477181502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 198 additions and 124 deletions

View File

@ -1961,9 +1961,15 @@ importers:
'@flowgram.ai/ts-config':
specifier: workspace:*
version: link:../../../config/ts-config
'@types/inquirer':
specifier: 9.0.7
version: 9.0.7
'@types/lodash':
specifier: ^4.14.137
version: 4.17.13
'@types/node':
specifier: ^18
version: 18.19.68
'@types/react':
specifier: ^18
version: 18.3.16

View File

@ -1,30 +1,31 @@
#!/usr/bin/env node
#!/usr/bin/env -S npx tsx@latest
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import { Command } from 'commander';
import chalk from 'chalk';
import { bfsMaterials, copyMaterial, listAllMaterials } from './materials.js';
import { getProjectInfo, installDependencies } from './project.js';
import { getProjectInfo, installDependencies, ProjectInfo } from './project.js';
import { bfsMaterials, copyMaterial, listAllMaterials, Material } from './materials.js';
const program = new Command();
program
.version('1.0.0')
.version('1.0.1')
.description('Add official materials to your project')
.argument('[materialName]', 'Optional material name to skip selection (format: type/name)')
.action(async (materialName) => {
.action(async (materialName?: string) => {
// materialName can be undefined
console.log(chalk.bgGreenBright('Welcome to @flowgram.ai/form-materials CLI!'));
const projectInfo = getProjectInfo();
const projectInfo: ProjectInfo = getProjectInfo();
console.log(chalk.bold('Project Info:'));
console.log(chalk.black(` - Flowgram Version: ${projectInfo.flowgramVersion}`));
console.log(chalk.black(` - Project Path: ${projectInfo.projectPath}`));
const materials = listAllMaterials();
const materials: Material[] = listAllMaterials();
let material;
let material: Material | undefined; // material can be undefined
// Check if materialName is provided and exists in materials
if (materialName) {
@ -42,7 +43,9 @@ program
// If material not found or materialName not provided, prompt user to select
if (!material) {
// User select one component
const result = await inquirer.prompt([
const result = await inquirer.prompt<{
material: Material; // Specify type for prompt result
}>([
{
type: 'list',
name: 'material',
@ -57,18 +60,23 @@ program
]);
material = result.material;
}
// Ensure material is defined before proceeding
if (!material) {
console.error(chalk.red('No material selected. Exiting.'));
process.exit(1);
}
console.log(material);
// 3. Get the component dependencies by BFS (include depMaterials and depPackages)
const { allMaterials, allPackages } = bfsMaterials(material, materials);
const { allMaterials, allPackages } = bfsMaterials(material!, materials);
// 4. Install the dependencies
let flowgramPackage = `@flowgram.ai/editor`;
if (projectInfo.flowgramVersion !== 'workspace:*') {
flowgramPackage = `@flowgram.ai/editor@${projectInfo.flowgramVersion}`;
}
const packagesToInstall = [flowgramPackage, ...allPackages];
const packagesToInstall: string[] = [flowgramPackage, ...allPackages];
console.log(chalk.bold('These npm dependencies will be added to your project'));
console.log(packagesToInstall);
@ -77,8 +85,9 @@ program
// 5. Copy the materials to the project
console.log(chalk.bold('These Materials will be added to your project'));
console.log(allMaterials);
allMaterials.forEach((material) => {
copyMaterial(material, projectInfo);
allMaterials.forEach((mat: Material) => {
// Add type for mat
copyMaterial(mat, projectInfo);
});
});

View File

@ -1,93 +0,0 @@
import fs from 'fs';
import path from 'path';
const _types = ['components', 'effects', 'utils', 'typings'];
export function listAllMaterials() {
const _materials = [];
for (const _type of _types) {
const materialsPath = path.join(import.meta.dirname, '..', 'src', _type);
_materials.push(
...fs
.readdirSync(materialsPath)
.map((_path) => {
if (_path === 'index.ts') {
return null;
}
const config = fs.readFileSync(path.join(materialsPath, _path, 'config.json'), 'utf8');
return {
...JSON.parse(config),
type: _type,
path: path.join(materialsPath, _path),
};
})
.filter(Boolean)
);
}
return _materials;
}
export function bfsMaterials(material, _materials = []) {
function findConfigByName(name) {
return _materials.find(
(_config) => _config.name === name || `${_config.type}/${_config.name}` === name
);
}
const queue = [material];
const allMaterials = new Set();
const allPackages = new Set();
while (queue.length > 0) {
const _material = queue.shift();
if (allMaterials.has(_material)) {
continue;
}
allMaterials.add(_material);
if (_material.depPackages) {
for (const _package of _material.depPackages) {
allPackages.add(_package);
}
}
if (_material.depMaterials) {
for (const _materialName of _material.depMaterials) {
queue.push(findConfigByName(_materialName));
}
}
}
return {
allMaterials: Array.from(allMaterials),
allPackages: Array.from(allPackages),
};
}
export const copyMaterial = (material, projectInfo) => {
const sourceDir = material.path;
const materialRoot = path.join(
projectInfo.projectPath,
'src',
'form-materials',
`${material.type}`
);
const targetDir = path.join(materialRoot, material.name);
fs.cpSync(sourceDir, targetDir, { recursive: true });
let materialRootIndexTs = '';
if (fs.existsSync(path.join(materialRoot, 'index.ts'))) {
materialRootIndexTs = fs.readFileSync(path.join(materialRoot, 'index.ts'), 'utf8');
}
if (!materialRootIndexTs.includes(material.name)) {
fs.writeFileSync(
path.join(materialRoot, 'index.ts'),
`${materialRootIndexTs}${materialRootIndexTs.endsWith('\n') ? '' : '\n'}export * from './${
material.name
}';\n`
);
}
};

View File

@ -0,0 +1,137 @@
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs';
import { ProjectInfo } from './project'; // Import ProjectInfo
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Added type definitions
export interface Material {
name: string;
type: string;
path: string;
depPackages?: string[];
depMaterials?: string[];
[key: string]: any; // For other properties from config.json
}
const _types: string[] = ['components', 'effects', 'utils', 'typings'];
export function listAllMaterials(): Material[] {
const _materials: Material[] = [];
for (const _type of _types) {
// 在 Node.js 中import.meta.dirname 不可用,可使用 import.meta.url 结合 url 模块来获取目录路径
const materialsPath: string = path.join(__dirname, '..', 'src', _type);
_materials.push(
...fs
.readdirSync(materialsPath)
.map((_path: string) => {
if (_path === 'index.ts') {
return null;
}
const configPath = path.join(materialsPath, _path, 'config.json');
// Check if config.json exists before reading
if (!fs.existsSync(configPath)) {
console.warn(
`Warning: config.json not found for material at ${path.join(materialsPath, _path)}`
);
return null;
}
const configContent = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configContent);
return {
...config,
name: _path, // Assuming the folder name is the material name
type: _type,
path: path.join(materialsPath, _path),
} as Material;
})
.filter((material): material is Material => material !== null)
);
}
return _materials;
}
interface BfsResult {
allMaterials: Material[];
allPackages: string[];
}
export function bfsMaterials(material: Material, _materials: Material[] = []): BfsResult {
function findConfigByName(name: string): Material | undefined {
return _materials.find(
(_config) => _config.name === name || `${_config.type}/${_config.name}` === name
);
}
const queue: (Material | undefined)[] = [material]; // Queue can hold undefined if findConfigByName returns undefined
const allMaterials = new Set<Material>();
const allPackages = new Set<string>();
while (queue.length > 0) {
const _material = queue.shift();
if (!_material || allMaterials.has(_material)) {
// Check if _material is defined
continue;
}
allMaterials.add(_material);
if (_material.depPackages) {
for (const _package of _material.depPackages) {
allPackages.add(_package);
}
}
if (_material.depMaterials) {
for (const _materialName of _material.depMaterials) {
const depMaterial = findConfigByName(_materialName);
if (depMaterial) {
// Ensure dependent material is found before adding to queue
queue.push(depMaterial);
} else {
console.warn(
`Warning: Dependent material "${_materialName}" not found for material "${_material.name}".`
);
}
}
}
}
return {
allMaterials: Array.from(allMaterials),
allPackages: Array.from(allPackages),
};
}
export const copyMaterial = (material: Material, projectInfo: ProjectInfo): void => {
const sourceDir: string = material.path;
const materialRoot: string = path.join(
projectInfo.projectPath,
'src',
'form-materials',
`${material.type}`
);
const targetDir = path.join(materialRoot, material.name);
fs.cpSync(sourceDir, targetDir, { recursive: true });
let materialRootIndexTs: string = '';
const indexTsPath = path.join(materialRoot, 'index.ts');
if (fs.existsSync(indexTsPath)) {
materialRootIndexTs = fs.readFileSync(indexTsPath, 'utf8');
}
if (!materialRootIndexTs.includes(material.name)) {
fs.writeFileSync(
indexTsPath,
`${materialRootIndexTs}${materialRootIndexTs.endsWith('\n') ? '' : '\n'}export * from './${
material.name
}';\n`
);
}
};

View File

@ -1,10 +1,25 @@
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import fs from 'fs';
import { execSync } from 'child_process';
export function getProjectInfo() {
// Added type definitions
interface PackageJson {
dependencies: { [key: string]: string };
devDependencies?: { [key: string]: string };
peerDependencies?: { [key: string]: string };
[key: string]: any;
}
export interface ProjectInfo {
projectPath: string;
packageJsonPath: string;
packageJson: PackageJson;
flowgramVersion: string;
}
export function getProjectInfo(): ProjectInfo {
// get nearest package.json
let projectPath = process.cwd();
let projectPath: string = process.cwd();
while (projectPath !== '/' && !fs.existsSync(path.join(projectPath, 'package.json'))) {
projectPath = path.join(projectPath, '..');
@ -14,12 +29,11 @@ export function getProjectInfo() {
throw new Error('Please run this command in a valid project');
}
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageJsonPath: string = path.join(projectPath, 'package.json');
const packageJson: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// fixed layout or free layout
const flowgramVersion =
const flowgramVersion: string | undefined =
packageJson.dependencies['@flowgram.ai/fixed-layout-editor'] ||
packageJson.dependencies['@flowgram.ai/free-layout-editor'] ||
packageJson.dependencies['@flowgram.ai/editor'];
@ -34,12 +48,12 @@ export function getProjectInfo() {
projectPath,
packageJsonPath,
packageJson,
flowgramVersion,
flowgramVersion, // TypeScript will ensure this is string due to the check above
};
}
export function findRushJson(startPath) {
let currentPath = startPath;
export function findRushJson(startPath: string): string | null {
let currentPath: string = startPath;
while (currentPath !== '/' && !fs.existsSync(path.join(currentPath, 'rush.json'))) {
currentPath = path.join(currentPath, '..');
}
@ -49,7 +63,7 @@ export function findRushJson(startPath) {
return null;
}
export function installDependencies(packages, projectInfo) {
export function installDependencies(packages: string[], projectInfo: ProjectInfo): void {
if (fs.existsSync(path.join(projectInfo.projectPath, 'yarn.lock'))) {
execSync(`yarn add ${packages.join(' ')}`, { stdio: 'inherit' });
return;

View File

@ -4,7 +4,6 @@
"homepage": "https://flowgram.ai/",
"repository": "https://github.com/bytedance/flowgram.ai",
"license": "MIT",
"type": "module",
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/esm/index.js",
@ -14,7 +13,7 @@
"module": "./dist/esm/index.js",
"types": "./dist/index.d.ts",
"bin": {
"flowgram-form-materials": "./bin/index.js"
"flowgram-form-materials": "./bin/index.ts"
},
"files": [
"dist",
@ -48,8 +47,10 @@
"@flowgram.ai/eslint-config": "workspace:*",
"@flowgram.ai/ts-config": "workspace:*",
"@types/lodash": "^4.14.137",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/inquirer": "9.0.7",
"@types/styled-components": "^5",
"eslint": "^8.54.0",
"react": "^18",

View File

@ -1,8 +1,8 @@
{
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
"compilerOptions": {
"jsx": "react",
"jsx": "react"
},
"include": ["./src"],
"include": ["./src", "./bin/**/*.ts"],
"exclude": ["node_modules"]
}