@flowgram.ai/cli 0.1.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +2 -0
- package/dist/index.cjs +493 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +472 -0
- package/package.json +40 -0
- package/src/create-app/index.ts +130 -0
- package/src/index.ts +31 -0
- package/src/materials/index.ts +93 -0
- package/src/materials/materials.ts +134 -0
- package/src/utils/import.ts +127 -0
- package/src/utils/npm.ts +43 -0
- package/src/utils/project.ts +100 -0
- package/src/utils/traverse-file.ts +60 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
import { copyMaterial, listAllMaterials, Material } from "./materials";
|
|
10
|
+
import { loadNpm } from "../utils/npm";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { Project } from "../utils/project";
|
|
13
|
+
|
|
14
|
+
export async function syncMaterial(materialName?: string) {
|
|
15
|
+
// materialName can be undefined
|
|
16
|
+
console.log(chalk.bgGreenBright("Welcome to @flowgram.ai form-materials!"));
|
|
17
|
+
|
|
18
|
+
const project = await Project.getSingleton();
|
|
19
|
+
project.printInfo();
|
|
20
|
+
|
|
21
|
+
if (!project.flowgramVersion) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Please install @flowgram.ai/fixed-layout-editor or @flowgram.ai/free-layout-editor",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const formMaterialPath = await loadNpm("@flowgram.ai/form-materials");
|
|
28
|
+
const formMaterialSrc = path.join(formMaterialPath, "src");
|
|
29
|
+
|
|
30
|
+
const materials: Material[] = listAllMaterials(formMaterialSrc);
|
|
31
|
+
|
|
32
|
+
let material: Material | undefined; // material can be undefined
|
|
33
|
+
|
|
34
|
+
// Check if materialName is provided and exists in materials
|
|
35
|
+
if (materialName) {
|
|
36
|
+
const selectedMaterial = materials.find(
|
|
37
|
+
(m) => `${m.type}/${m.name}` === materialName,
|
|
38
|
+
);
|
|
39
|
+
if (selectedMaterial) {
|
|
40
|
+
material = selectedMaterial;
|
|
41
|
+
console.log(chalk.green(`Using material: ${materialName}`));
|
|
42
|
+
} else {
|
|
43
|
+
console.log(
|
|
44
|
+
chalk.yellow(
|
|
45
|
+
`Material "${materialName}" not found. Please select from the list:`,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If material not found or materialName not provided, prompt user to select
|
|
52
|
+
if (!material) {
|
|
53
|
+
// User select one component
|
|
54
|
+
const result = await inquirer.prompt<{
|
|
55
|
+
material: Material; // Specify type for prompt result
|
|
56
|
+
}>([
|
|
57
|
+
{
|
|
58
|
+
type: "list",
|
|
59
|
+
name: "material",
|
|
60
|
+
message: "Select one material to add:",
|
|
61
|
+
choices: [
|
|
62
|
+
...materials.map((_material) => ({
|
|
63
|
+
name: `${_material.type}/${_material.name}`,
|
|
64
|
+
value: _material,
|
|
65
|
+
})),
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
material = result.material;
|
|
70
|
+
}
|
|
71
|
+
// Ensure material is defined before proceeding
|
|
72
|
+
if (!material) {
|
|
73
|
+
console.error(chalk.red("No material selected. Exiting."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Copy the materials to the project
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.bold("The following materials will be added to your project"),
|
|
80
|
+
);
|
|
81
|
+
console.log(material);
|
|
82
|
+
let { packagesToInstall } = copyMaterial(material, project, formMaterialPath);
|
|
83
|
+
|
|
84
|
+
// 4. Install the dependencies
|
|
85
|
+
await project.addDependencies(packagesToInstall);
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.bold("These npm dependencies is added to your package.json"),
|
|
88
|
+
);
|
|
89
|
+
packagesToInstall.forEach((_package) => {
|
|
90
|
+
console.log(`- ${_package}`);
|
|
91
|
+
})
|
|
92
|
+
console.log(chalk.bold("\nPlease run npm install to install dependencies"));
|
|
93
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
|
|
10
|
+
import { traverseRecursiveFiles } from "../utils/traverse-file";
|
|
11
|
+
import { replaceImport, traverseFileImports } from "../utils/import";
|
|
12
|
+
import { Project } from "../utils/project"; // Import ProjectInfo
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Added type definitions
|
|
18
|
+
export interface Material {
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
path: string;
|
|
22
|
+
[key: string]: any; // For other properties from config.json
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const _types: string[] = [
|
|
26
|
+
"components",
|
|
27
|
+
"effects",
|
|
28
|
+
"plugins",
|
|
29
|
+
"shared",
|
|
30
|
+
"typings",
|
|
31
|
+
"validate",
|
|
32
|
+
"form-plugins",
|
|
33
|
+
"hooks",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function listAllMaterials(formMaterialSrc: string): Material[] {
|
|
37
|
+
const _materials: Material[] = [];
|
|
38
|
+
|
|
39
|
+
for (const _type of _types) {
|
|
40
|
+
// 在 Node.js 中,import.meta.dirname 不可用,可使用 import.meta.url 结合 url 模块来获取目录路径
|
|
41
|
+
|
|
42
|
+
const materialsPath: string = path.join(formMaterialSrc, _type);
|
|
43
|
+
_materials.push(
|
|
44
|
+
...fs
|
|
45
|
+
.readdirSync(materialsPath)
|
|
46
|
+
.map((_path: string) => {
|
|
47
|
+
if (_path === "index.ts") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: _path, // Assuming the folder name is the material name
|
|
53
|
+
type: _type,
|
|
54
|
+
path: path.join(materialsPath, _path),
|
|
55
|
+
} as Material;
|
|
56
|
+
})
|
|
57
|
+
.filter((material): material is Material => material !== null),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return _materials;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const getFormMaterialDependencies = (
|
|
65
|
+
formMaterialPath: string,
|
|
66
|
+
): Record<string, string> => {
|
|
67
|
+
const packageJsonPath: string = path.join(formMaterialPath, "package.json");
|
|
68
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
69
|
+
|
|
70
|
+
return packageJson.dependencies;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const copyMaterial = (
|
|
74
|
+
material: Material,
|
|
75
|
+
project: Project,
|
|
76
|
+
formMaterialPath: string,
|
|
77
|
+
): {
|
|
78
|
+
packagesToInstall: string[];
|
|
79
|
+
} => {
|
|
80
|
+
const formMaterialDependencies = getFormMaterialDependencies(formMaterialPath);
|
|
81
|
+
|
|
82
|
+
const sourceDir: string = material.path;
|
|
83
|
+
const materialRoot: string = path.join(
|
|
84
|
+
project.projectPath,
|
|
85
|
+
"src",
|
|
86
|
+
"form-materials",
|
|
87
|
+
`${material.type}`,
|
|
88
|
+
);
|
|
89
|
+
const targetDir = path.join(materialRoot, material.name);
|
|
90
|
+
const packagesToInstall: Set<string> = new Set();
|
|
91
|
+
|
|
92
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
for (const file of traverseRecursiveFiles(targetDir)) {
|
|
95
|
+
if ([".ts", ".tsx"].includes(file.suffix)) {
|
|
96
|
+
for (const importDeclaration of traverseFileImports(file.content)) {
|
|
97
|
+
const { source } = importDeclaration;
|
|
98
|
+
|
|
99
|
+
if (source.startsWith("@/")) {
|
|
100
|
+
// is inner import
|
|
101
|
+
console.log(
|
|
102
|
+
`Replace Import from ${source} to @flowgram.ai/form-materials`,
|
|
103
|
+
);
|
|
104
|
+
file.replace((content) =>
|
|
105
|
+
replaceImport(content, importDeclaration, [
|
|
106
|
+
{ ...importDeclaration, source: "@flowgram.ai/form-materials" },
|
|
107
|
+
]),
|
|
108
|
+
);
|
|
109
|
+
packagesToInstall.add(
|
|
110
|
+
`@flowgram.ai/form-materials@${project.flowgramVersion}`,
|
|
111
|
+
);
|
|
112
|
+
} else if (!source.startsWith(".") && !source.startsWith("react")) {
|
|
113
|
+
// check if is in form material dependencies
|
|
114
|
+
const [dep, version] =
|
|
115
|
+
Object.entries(formMaterialDependencies).find(([_key]) =>
|
|
116
|
+
source.startsWith(_key),
|
|
117
|
+
) || [];
|
|
118
|
+
if (!dep) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (dep.startsWith("@flowgram.ai/")) {
|
|
122
|
+
packagesToInstall.add(`${dep}@${project.flowgramVersion}`);
|
|
123
|
+
} else {
|
|
124
|
+
packagesToInstall.add(`${dep}@${version}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
packagesToInstall: [...packagesToInstall],
|
|
133
|
+
};
|
|
134
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cases
|
|
8
|
+
* import { A, B } from 'module';
|
|
9
|
+
* import A from 'module';
|
|
10
|
+
* import * as C from 'module';
|
|
11
|
+
* import D, { type E, F } from 'module';
|
|
12
|
+
* import A, { B as B1 } from 'module';
|
|
13
|
+
*/
|
|
14
|
+
interface ImportDeclaration {
|
|
15
|
+
// origin statement
|
|
16
|
+
statement: string;
|
|
17
|
+
|
|
18
|
+
// import { A, B } from 'module';
|
|
19
|
+
namedImports?: {
|
|
20
|
+
local?: string;
|
|
21
|
+
imported: string;
|
|
22
|
+
typeOnly?: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
|
|
25
|
+
// import A from 'module';
|
|
26
|
+
defaultImport?: string;
|
|
27
|
+
|
|
28
|
+
// import * as C from 'module';
|
|
29
|
+
namespaceImport?: string;
|
|
30
|
+
|
|
31
|
+
source: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function assembleImport(declaration: ImportDeclaration): string {
|
|
35
|
+
const { namedImports, defaultImport, namespaceImport, source } = declaration;
|
|
36
|
+
const importClauses = [];
|
|
37
|
+
if (namedImports) {
|
|
38
|
+
importClauses.push(
|
|
39
|
+
`{ ${namedImports
|
|
40
|
+
.map(
|
|
41
|
+
({ local, imported, typeOnly }) =>
|
|
42
|
+
`${typeOnly ? 'type ' : ''}${imported}${local ? ` as ${local}` : ''}`
|
|
43
|
+
)
|
|
44
|
+
.join(', ')} }`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (defaultImport) {
|
|
48
|
+
importClauses.push(defaultImport);
|
|
49
|
+
}
|
|
50
|
+
if (namespaceImport) {
|
|
51
|
+
importClauses.push(`* as ${namespaceImport}`);
|
|
52
|
+
}
|
|
53
|
+
return `import ${importClauses.join(', ')} from '${source}'`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function replaceImport(
|
|
57
|
+
fileContent: string,
|
|
58
|
+
origin: ImportDeclaration,
|
|
59
|
+
replaceTo: ImportDeclaration[]
|
|
60
|
+
): string {
|
|
61
|
+
const replaceImportStatements = replaceTo.map(assembleImport);
|
|
62
|
+
// replace origin statement
|
|
63
|
+
return fileContent.replace(origin.statement, replaceImportStatements.join('\n'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function* traverseFileImports(fileContent: string): Generator<ImportDeclaration> {
|
|
67
|
+
// 匹配所有 import 语句的正则表达式
|
|
68
|
+
const importRegex =
|
|
69
|
+
/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;
|
|
70
|
+
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = importRegex.exec(fileContent)) !== null) {
|
|
73
|
+
const [fullMatch, defaultPart, namespacePart, namedPart, defaultPart2, source] = match;
|
|
74
|
+
|
|
75
|
+
const declaration: ImportDeclaration = {
|
|
76
|
+
statement: fullMatch,
|
|
77
|
+
source: source,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 处理默认导入
|
|
81
|
+
const defaultImport = defaultPart?.trim() || defaultPart2?.trim();
|
|
82
|
+
if (defaultImport && !namespacePart && !namedPart) {
|
|
83
|
+
declaration.defaultImport = defaultImport;
|
|
84
|
+
} else if (defaultImport && (namespacePart || namedPart)) {
|
|
85
|
+
declaration.defaultImport = defaultImport;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 处理命名空间导入 (* as)
|
|
89
|
+
if (namespacePart) {
|
|
90
|
+
declaration.namespaceImport = namespacePart.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 处理命名导入
|
|
94
|
+
if (namedPart) {
|
|
95
|
+
const namedImports = [];
|
|
96
|
+
const namedItems = namedPart
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((item) => item.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
|
|
101
|
+
for (const item of namedItems) {
|
|
102
|
+
const typeOnly = item.startsWith('type ');
|
|
103
|
+
const cleanItem = typeOnly ? item.slice(5).trim() : item;
|
|
104
|
+
|
|
105
|
+
if (cleanItem.includes(' as ')) {
|
|
106
|
+
const [imported, local] = cleanItem.split(' as ').map((s) => s.trim());
|
|
107
|
+
namedImports.push({
|
|
108
|
+
imported,
|
|
109
|
+
local,
|
|
110
|
+
typeOnly,
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
namedImports.push({
|
|
114
|
+
imported: cleanItem,
|
|
115
|
+
typeOnly,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (namedImports.length > 0) {
|
|
121
|
+
declaration.namedImports = namedImports;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
yield declaration;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/utils/npm.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import download from 'download';
|
|
10
|
+
|
|
11
|
+
export async function getLatestVersion(packageName: string): Promise<string> {
|
|
12
|
+
return execSync(`npm view ${packageName} version --tag=latest`)
|
|
13
|
+
.toString()
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadNpm(packageName: string): Promise<string> {
|
|
18
|
+
const packageLatestVersion = await getLatestVersion(packageName);
|
|
19
|
+
|
|
20
|
+
const packagePath = path.join(
|
|
21
|
+
__dirname,
|
|
22
|
+
`./.download/${packageName}-${packageLatestVersion}`,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (existsSync(packagePath)) {
|
|
26
|
+
return packagePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// download else
|
|
30
|
+
try {
|
|
31
|
+
const tarballUrl = execSync(
|
|
32
|
+
`npm view ${packageName}@${packageLatestVersion} dist.tarball`,
|
|
33
|
+
)
|
|
34
|
+
.toString()
|
|
35
|
+
.trim();
|
|
36
|
+
await download(tarballUrl, packagePath, { extract: true, strip: 1 });
|
|
37
|
+
return packagePath;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Error downloading or extracting package: ${error}`);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { getLatestVersion } from "./npm";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
interface PackageJson {
|
|
12
|
+
dependencies: { [key: string]: string };
|
|
13
|
+
devDependencies?: { [key: string]: string };
|
|
14
|
+
peerDependencies?: { [key: string]: string };
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Project {
|
|
19
|
+
flowgramVersion?: string;
|
|
20
|
+
|
|
21
|
+
projectPath: string;
|
|
22
|
+
|
|
23
|
+
packageJsonPath: string;
|
|
24
|
+
|
|
25
|
+
packageJson: PackageJson;
|
|
26
|
+
|
|
27
|
+
protected constructor() {}
|
|
28
|
+
|
|
29
|
+
async init() {
|
|
30
|
+
// get nearest package.json
|
|
31
|
+
let projectPath: string = process.cwd();
|
|
32
|
+
|
|
33
|
+
while (
|
|
34
|
+
projectPath !== "/" &&
|
|
35
|
+
!existsSync(path.join(projectPath, "package.json"))
|
|
36
|
+
) {
|
|
37
|
+
projectPath = path.join(projectPath, "..");
|
|
38
|
+
}
|
|
39
|
+
if (projectPath === "/") {
|
|
40
|
+
throw new Error("Please run this command in a valid project");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.projectPath = projectPath;
|
|
44
|
+
|
|
45
|
+
this.packageJsonPath = path.join(projectPath, "package.json");
|
|
46
|
+
this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, "utf8"));
|
|
47
|
+
|
|
48
|
+
this.flowgramVersion =
|
|
49
|
+
this.packageJson.dependencies["@flowgram.ai/fixed-layout-editor"] ||
|
|
50
|
+
this.packageJson.dependencies["@flowgram.ai/free-layout-editor"] ||
|
|
51
|
+
this.packageJson.dependencies["@flowgram.ai/editor"];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async addDependency(dependency: string) {
|
|
55
|
+
let name: string;
|
|
56
|
+
let version: string;
|
|
57
|
+
|
|
58
|
+
// 处理作用域包(如 @types/react@1.0.0)
|
|
59
|
+
const lastAtIndex = dependency.lastIndexOf("@");
|
|
60
|
+
|
|
61
|
+
if (lastAtIndex <= 0) {
|
|
62
|
+
// 没有@符号 或者@在开头(如 @types/react)
|
|
63
|
+
name = dependency;
|
|
64
|
+
version = await getLatestVersion(name);
|
|
65
|
+
} else {
|
|
66
|
+
// 正常分割包名和版本
|
|
67
|
+
name = dependency.substring(0, lastAtIndex);
|
|
68
|
+
version = dependency.substring(lastAtIndex + 1);
|
|
69
|
+
|
|
70
|
+
// 如果版本部分为空,获取最新版本
|
|
71
|
+
if (!version.trim()) {
|
|
72
|
+
version = await getLatestVersion(name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.packageJson.dependencies[name] = version;
|
|
77
|
+
writeFileSync(
|
|
78
|
+
this.packageJsonPath,
|
|
79
|
+
JSON.stringify(this.packageJson, null, 2),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async addDependencies(dependencies: string[]) {
|
|
84
|
+
for (const dependency of dependencies) {
|
|
85
|
+
await this.addDependency(dependency);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
printInfo() {
|
|
90
|
+
console.log(chalk.bold("Project Info:"));
|
|
91
|
+
console.log(chalk.black(` - Flowgram Version: ${this.flowgramVersion}`));
|
|
92
|
+
console.log(chalk.black(` - Project Path: ${this.projectPath}`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static async getSingleton() {
|
|
96
|
+
const info = new Project();
|
|
97
|
+
await info.init();
|
|
98
|
+
return info;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
|
|
9
|
+
class File {
|
|
10
|
+
content: string;
|
|
11
|
+
|
|
12
|
+
isUtf8: boolean;
|
|
13
|
+
|
|
14
|
+
relativePath: string;
|
|
15
|
+
|
|
16
|
+
path: string;
|
|
17
|
+
|
|
18
|
+
suffix: string;
|
|
19
|
+
|
|
20
|
+
constructor(filePath: string, public root: string = '/') {
|
|
21
|
+
this.path = filePath;
|
|
22
|
+
this.relativePath = path.relative(this.root, this.path);
|
|
23
|
+
this.suffix = path.extname(this.path);
|
|
24
|
+
|
|
25
|
+
// Check if exists
|
|
26
|
+
if (!fs.existsSync(this.path)) {
|
|
27
|
+
throw Error(`File ${path} Not Exists`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If no utf-8, skip
|
|
31
|
+
try {
|
|
32
|
+
this.content = fs.readFileSync(this.path, 'utf-8');
|
|
33
|
+
this.isUtf8 = true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
this.isUtf8 = false;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
replace(updater: (content: string) => string) {
|
|
41
|
+
if (!this.isUtf8) {
|
|
42
|
+
console.warn('Not UTF-8 file skipped: ', this.path);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.content = updater(this.content);
|
|
46
|
+
fs.writeFileSync(this.path, this.content, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function* traverseRecursiveFiles(folder: string): Generator<File> {
|
|
51
|
+
const files = fs.readdirSync(folder);
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const filePath = path.join(folder, file);
|
|
54
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
55
|
+
yield* traverseRecursiveFiles(filePath);
|
|
56
|
+
} else {
|
|
57
|
+
yield new File(filePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|