@agiflowai/scaffold-mcp 0.0.0
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/LICENSE +661 -0
- package/README.md +264 -0
- package/dist/ScaffoldConfigLoader-CI0T6zdG.js +142 -0
- package/dist/ScaffoldConfigLoader-DhthV6xq.js +3 -0
- package/dist/ScaffoldService-CDhYAUrp.js +3 -0
- package/dist/ScaffoldService-CnJ1nj1v.js +408 -0
- package/dist/TemplateService-CnxvhRVW.js +64 -0
- package/dist/TemplateService-PmTU3_On.js +3 -0
- package/dist/VariableReplacementService-Bq0GDhTo.js +65 -0
- package/dist/VariableReplacementService-CrxFJrqU.js +3 -0
- package/dist/index.js +2812 -0
- package/package.json +65 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as fs$1 from "fs-extra";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
//#region rolldown:runtime
|
|
7
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/services/ScaffoldProcessingService.ts
|
|
11
|
+
/**
|
|
12
|
+
* Shared service for common scaffolding operations like processing templates and tracking files
|
|
13
|
+
*/
|
|
14
|
+
var ScaffoldProcessingService = class {
|
|
15
|
+
constructor(fileSystem, variableReplacer) {
|
|
16
|
+
this.fileSystem = fileSystem;
|
|
17
|
+
this.variableReplacer = variableReplacer;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Process a target path for variable replacement, handling both files and directories
|
|
21
|
+
*/
|
|
22
|
+
async processTargetForVariableReplacement(targetPath, variables) {
|
|
23
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.variableReplacer.processFilesForVariableReplacement(targetPath, variables);
|
|
24
|
+
else await this.variableReplacer.replaceVariablesInFile(targetPath, variables);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Track all created files, handling both single files and directories
|
|
28
|
+
*/
|
|
29
|
+
async trackCreatedFiles(targetPath, createdFiles) {
|
|
30
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.trackCreatedFilesRecursive(targetPath, createdFiles);
|
|
31
|
+
else createdFiles.push(targetPath);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Track all existing files, handling both single files and directories
|
|
35
|
+
*/
|
|
36
|
+
async trackExistingFiles(targetPath, existingFiles) {
|
|
37
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.trackExistingFilesRecursive(targetPath, existingFiles);
|
|
38
|
+
else existingFiles.push(targetPath);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Copy source to target, then process templates and track files
|
|
42
|
+
* Now supports tracking existing files separately from created files
|
|
43
|
+
* Automatically handles .liquid template files by stripping the extension
|
|
44
|
+
*/
|
|
45
|
+
async copyAndProcess(sourcePath, targetPath, variables, createdFiles, existingFiles) {
|
|
46
|
+
await this.fileSystem.ensureDir(path.dirname(targetPath));
|
|
47
|
+
if (await this.fileSystem.pathExists(targetPath) && existingFiles) {
|
|
48
|
+
await this.trackExistingFiles(targetPath, existingFiles);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let actualSourcePath = sourcePath;
|
|
52
|
+
if (!await this.fileSystem.pathExists(sourcePath)) {
|
|
53
|
+
const liquidSourcePath = `${sourcePath}.liquid`;
|
|
54
|
+
if (await this.fileSystem.pathExists(liquidSourcePath)) actualSourcePath = liquidSourcePath;
|
|
55
|
+
else throw new Error(`Source file not found: ${sourcePath} (also tried ${liquidSourcePath})`);
|
|
56
|
+
}
|
|
57
|
+
await this.fileSystem.copy(actualSourcePath, targetPath);
|
|
58
|
+
await this.processTargetForVariableReplacement(targetPath, variables);
|
|
59
|
+
await this.trackCreatedFiles(targetPath, createdFiles);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Recursively collect all file paths in a directory for created files
|
|
63
|
+
*/
|
|
64
|
+
async trackCreatedFilesRecursive(dirPath, createdFiles) {
|
|
65
|
+
let items = [];
|
|
66
|
+
try {
|
|
67
|
+
items = await this.fileSystem.readdir(dirPath);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`Cannot read directory ${dirPath}: ${error}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (!item) continue;
|
|
74
|
+
const itemPath = path.join(dirPath, item);
|
|
75
|
+
try {
|
|
76
|
+
const stat = await this.fileSystem.stat(itemPath);
|
|
77
|
+
if (stat.isDirectory()) await this.trackCreatedFilesRecursive(itemPath, createdFiles);
|
|
78
|
+
else if (stat.isFile()) createdFiles.push(itemPath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(`Cannot stat ${itemPath}: ${error}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Recursively collect all file paths in a directory for existing files
|
|
86
|
+
*/
|
|
87
|
+
async trackExistingFilesRecursive(dirPath, existingFiles) {
|
|
88
|
+
let items = [];
|
|
89
|
+
try {
|
|
90
|
+
items = await this.fileSystem.readdir(dirPath);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`Cannot read directory ${dirPath}: ${error}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const item of items) {
|
|
96
|
+
if (!item) continue;
|
|
97
|
+
const itemPath = path.join(dirPath, item);
|
|
98
|
+
try {
|
|
99
|
+
const stat = await this.fileSystem.stat(itemPath);
|
|
100
|
+
if (stat.isDirectory()) await this.trackExistingFilesRecursive(itemPath, existingFiles);
|
|
101
|
+
else if (stat.isFile()) existingFiles.push(itemPath);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn(`Cannot stat ${itemPath}: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/services/TemplatesManager.ts
|
|
111
|
+
var TemplatesManager = class TemplatesManager {
|
|
112
|
+
static SCAFFOLD_CONFIG_FILE = "scaffold.yaml";
|
|
113
|
+
static TEMPLATES_FOLDER = "templates";
|
|
114
|
+
static TOOLKIT_CONFIG_FILE = "toolkit.yaml";
|
|
115
|
+
/**
|
|
116
|
+
* Find the templates directory by searching upwards from the starting path.
|
|
117
|
+
*
|
|
118
|
+
* Algorithm:
|
|
119
|
+
* 1. Start from the provided path (default: current working directory)
|
|
120
|
+
* 2. Search upwards to find the workspace root (where .git exists or filesystem root)
|
|
121
|
+
* 3. Check if toolkit.yaml exists at workspace root
|
|
122
|
+
* - If yes, read templatesPath from toolkit.yaml
|
|
123
|
+
* - If no, default to 'templates' folder in workspace root
|
|
124
|
+
* 4. Verify the templates directory exists
|
|
125
|
+
*
|
|
126
|
+
* @param startPath - The path to start searching from (defaults to process.cwd())
|
|
127
|
+
* @returns The absolute path to the templates directory
|
|
128
|
+
* @throws Error if templates directory is not found
|
|
129
|
+
*/
|
|
130
|
+
static async findTemplatesPath(startPath = process.cwd()) {
|
|
131
|
+
const workspaceRoot = await TemplatesManager.findWorkspaceRoot(startPath);
|
|
132
|
+
const toolkitConfigPath = path.join(workspaceRoot, TemplatesManager.TOOLKIT_CONFIG_FILE);
|
|
133
|
+
if (await fs$1.pathExists(toolkitConfigPath)) {
|
|
134
|
+
const yaml = await import("js-yaml");
|
|
135
|
+
const content = await fs$1.readFile(toolkitConfigPath, "utf-8");
|
|
136
|
+
const config = yaml.load(content);
|
|
137
|
+
if (config?.templatesPath) {
|
|
138
|
+
const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
|
|
139
|
+
if (await fs$1.pathExists(templatesPath$1)) return templatesPath$1;
|
|
140
|
+
else throw new Error(`Templates path specified in toolkit.yaml does not exist: ${templatesPath$1}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const templatesPath = path.join(workspaceRoot, TemplatesManager.TEMPLATES_FOLDER);
|
|
144
|
+
if (await fs$1.pathExists(templatesPath)) return templatesPath;
|
|
145
|
+
throw new Error(`Templates folder not found at ${templatesPath}.\nEither create a 'templates' folder or specify templatesPath in toolkit.yaml`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Find the workspace root by searching upwards for .git folder
|
|
149
|
+
*/
|
|
150
|
+
static async findWorkspaceRoot(startPath) {
|
|
151
|
+
let currentPath = path.resolve(startPath);
|
|
152
|
+
const rootPath = path.parse(currentPath).root;
|
|
153
|
+
while (true) {
|
|
154
|
+
const gitPath = path.join(currentPath, ".git");
|
|
155
|
+
if (await fs$1.pathExists(gitPath)) return currentPath;
|
|
156
|
+
if (currentPath === rootPath) return process.cwd();
|
|
157
|
+
currentPath = path.dirname(currentPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get the templates path synchronously.
|
|
162
|
+
* Use this when you need immediate access and are sure templates exist.
|
|
163
|
+
*
|
|
164
|
+
* @param startPath - The path to start searching from (defaults to process.cwd())
|
|
165
|
+
* @returns The absolute path to the templates directory
|
|
166
|
+
* @throws Error if templates directory is not found
|
|
167
|
+
*/
|
|
168
|
+
static findTemplatesPathSync(startPath = process.cwd()) {
|
|
169
|
+
const workspaceRoot = TemplatesManager.findWorkspaceRootSync(startPath);
|
|
170
|
+
const toolkitConfigPath = path.join(workspaceRoot, TemplatesManager.TOOLKIT_CONFIG_FILE);
|
|
171
|
+
if (fs$1.pathExistsSync(toolkitConfigPath)) {
|
|
172
|
+
const yaml = __require("js-yaml");
|
|
173
|
+
const content = fs$1.readFileSync(toolkitConfigPath, "utf-8");
|
|
174
|
+
const config = yaml.load(content);
|
|
175
|
+
if (config?.templatesPath) {
|
|
176
|
+
const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
|
|
177
|
+
if (fs$1.pathExistsSync(templatesPath$1)) return templatesPath$1;
|
|
178
|
+
else throw new Error(`Templates path specified in toolkit.yaml does not exist: ${templatesPath$1}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const templatesPath = path.join(workspaceRoot, TemplatesManager.TEMPLATES_FOLDER);
|
|
182
|
+
if (fs$1.pathExistsSync(templatesPath)) return templatesPath;
|
|
183
|
+
throw new Error(`Templates folder not found at ${templatesPath}.\nEither create a 'templates' folder or specify templatesPath in toolkit.yaml`);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Find the workspace root synchronously by searching upwards for .git folder
|
|
187
|
+
*/
|
|
188
|
+
static findWorkspaceRootSync(startPath) {
|
|
189
|
+
let currentPath = path.resolve(startPath);
|
|
190
|
+
const rootPath = path.parse(currentPath).root;
|
|
191
|
+
while (true) {
|
|
192
|
+
const gitPath = path.join(currentPath, ".git");
|
|
193
|
+
if (fs$1.pathExistsSync(gitPath)) return currentPath;
|
|
194
|
+
if (currentPath === rootPath) return process.cwd();
|
|
195
|
+
currentPath = path.dirname(currentPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if templates are initialized at the given path
|
|
200
|
+
*
|
|
201
|
+
* @param templatesPath - Path to check for templates
|
|
202
|
+
* @returns true if templates folder exists and is a directory
|
|
203
|
+
*/
|
|
204
|
+
static async isInitialized(templatesPath) {
|
|
205
|
+
if (!await fs$1.pathExists(templatesPath)) return false;
|
|
206
|
+
return (await fs$1.stat(templatesPath)).isDirectory();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get the scaffold config file name
|
|
210
|
+
*/
|
|
211
|
+
static getConfigFileName() {
|
|
212
|
+
return TemplatesManager.SCAFFOLD_CONFIG_FILE;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the templates folder name
|
|
216
|
+
*/
|
|
217
|
+
static getTemplatesFolderName() {
|
|
218
|
+
return TemplatesManager.TEMPLATES_FOLDER;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/services/ScaffoldService.ts
|
|
224
|
+
var ScaffoldService = class {
|
|
225
|
+
templatesRootPath;
|
|
226
|
+
processingService;
|
|
227
|
+
constructor(fileSystem, scaffoldConfigLoader, variableReplacer, templatesRootPath) {
|
|
228
|
+
this.fileSystem = fileSystem;
|
|
229
|
+
this.scaffoldConfigLoader = scaffoldConfigLoader;
|
|
230
|
+
this.variableReplacer = variableReplacer;
|
|
231
|
+
this.templatesRootPath = templatesRootPath || TemplatesManager.findTemplatesPathSync();
|
|
232
|
+
this.processingService = new ScaffoldProcessingService(fileSystem, variableReplacer);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Scaffold a new project from a boilerplate template
|
|
236
|
+
*/
|
|
237
|
+
async useBoilerplate(options) {
|
|
238
|
+
try {
|
|
239
|
+
const { projectName, packageName, targetFolder, templateFolder, boilerplateName, variables = {} } = options;
|
|
240
|
+
const targetPath = path.isAbsolute(targetFolder) ? path.join(targetFolder, projectName) : path.join(process.cwd(), targetFolder, projectName);
|
|
241
|
+
const templatePath = path.join(this.templatesRootPath, templateFolder);
|
|
242
|
+
const validationResult = await this.scaffoldConfigLoader.validateTemplate(templatePath, "boilerplate");
|
|
243
|
+
if (!validationResult.isValid) return {
|
|
244
|
+
success: false,
|
|
245
|
+
message: `Template validation failed: ${[...validationResult.errors, ...validationResult.missingFiles.map((f) => `Template file not found: ${f}`)].join("; ")}`
|
|
246
|
+
};
|
|
247
|
+
if (await this.fileSystem.pathExists(targetPath)) return {
|
|
248
|
+
success: false,
|
|
249
|
+
message: `Directory ${targetPath} already exists`
|
|
250
|
+
};
|
|
251
|
+
const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath);
|
|
252
|
+
if (!architectConfig || !architectConfig.boilerplate) return {
|
|
253
|
+
success: false,
|
|
254
|
+
message: `Invalid architect configuration: missing 'boilerplate' section in scaffold.yaml`
|
|
255
|
+
};
|
|
256
|
+
const boilerplateArray = architectConfig.boilerplate;
|
|
257
|
+
let config;
|
|
258
|
+
if (Array.isArray(boilerplateArray)) {
|
|
259
|
+
config = boilerplateArray.find((b) => b.name === boilerplateName);
|
|
260
|
+
if (!config) return {
|
|
261
|
+
success: false,
|
|
262
|
+
message: `Boilerplate '${boilerplateName}' not found in scaffold configuration`
|
|
263
|
+
};
|
|
264
|
+
} else config = architectConfig.boilerplate;
|
|
265
|
+
const allVariables = {
|
|
266
|
+
...variables,
|
|
267
|
+
projectName,
|
|
268
|
+
packageName
|
|
269
|
+
};
|
|
270
|
+
return await this.processScaffold({
|
|
271
|
+
config,
|
|
272
|
+
targetPath,
|
|
273
|
+
templatePath,
|
|
274
|
+
allVariables,
|
|
275
|
+
scaffoldType: "boilerplate"
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
message: `Error scaffolding boilerplate: ${error instanceof Error ? error.message : String(error)}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Scaffold a new feature into an existing project
|
|
286
|
+
*/
|
|
287
|
+
async useFeature(options) {
|
|
288
|
+
try {
|
|
289
|
+
const { projectPath, templateFolder, featureName, variables = {} } = options;
|
|
290
|
+
const targetPath = path.resolve(projectPath);
|
|
291
|
+
const templatePath = path.join(this.templatesRootPath, templateFolder);
|
|
292
|
+
const projectName = path.basename(targetPath);
|
|
293
|
+
const validationResult = await this.scaffoldConfigLoader.validateTemplate(templatePath, "features");
|
|
294
|
+
if (!validationResult.isValid) return {
|
|
295
|
+
success: false,
|
|
296
|
+
message: `Template validation failed: ${[...validationResult.errors, ...validationResult.missingFiles.map((f) => `Template file not found: ${f}`)].join("; ")}`
|
|
297
|
+
};
|
|
298
|
+
if (!await this.fileSystem.pathExists(targetPath)) return {
|
|
299
|
+
success: false,
|
|
300
|
+
message: `Target directory ${targetPath} does not exist. Please create the parent directory first.`
|
|
301
|
+
};
|
|
302
|
+
const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath);
|
|
303
|
+
if (!architectConfig || !architectConfig.features) return {
|
|
304
|
+
success: false,
|
|
305
|
+
message: `Invalid architect configuration: missing 'features' section in scaffold.yaml`
|
|
306
|
+
};
|
|
307
|
+
const featureArray = architectConfig.features;
|
|
308
|
+
let config;
|
|
309
|
+
if (Array.isArray(featureArray)) {
|
|
310
|
+
config = featureArray.find((f) => f.name === featureName);
|
|
311
|
+
if (!config) return {
|
|
312
|
+
success: false,
|
|
313
|
+
message: `Feature '${featureName}' not found in scaffold configuration`
|
|
314
|
+
};
|
|
315
|
+
} else config = architectConfig.features;
|
|
316
|
+
const allVariables = {
|
|
317
|
+
...variables,
|
|
318
|
+
projectName,
|
|
319
|
+
appPath: targetPath,
|
|
320
|
+
appName: projectName
|
|
321
|
+
};
|
|
322
|
+
return await this.processScaffold({
|
|
323
|
+
config,
|
|
324
|
+
targetPath,
|
|
325
|
+
templatePath,
|
|
326
|
+
allVariables,
|
|
327
|
+
scaffoldType: "feature"
|
|
328
|
+
});
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
message: `Error scaffolding feature: ${error instanceof Error ? error.message : String(error)}`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Common scaffolding processing logic shared by both useBoilerplate and useFeature
|
|
338
|
+
*/
|
|
339
|
+
async processScaffold(params) {
|
|
340
|
+
const { config, targetPath, templatePath, allVariables, scaffoldType } = params;
|
|
341
|
+
console.log("Config generator:", config.generator);
|
|
342
|
+
console.log("Config:", JSON.stringify(config, null, 2));
|
|
343
|
+
if (config.generator) {
|
|
344
|
+
console.log("Using custom generator:", config.generator);
|
|
345
|
+
try {
|
|
346
|
+
const generator = (await import(path.join(templatePath, "generators", config.generator))).default;
|
|
347
|
+
if (typeof generator !== "function") return {
|
|
348
|
+
success: false,
|
|
349
|
+
message: `Invalid generator: ${config.generator} does not export a default function`
|
|
350
|
+
};
|
|
351
|
+
return await generator({
|
|
352
|
+
variables: allVariables,
|
|
353
|
+
config,
|
|
354
|
+
targetPath,
|
|
355
|
+
templatePath,
|
|
356
|
+
fileSystem: this.fileSystem,
|
|
357
|
+
scaffoldConfigLoader: this.scaffoldConfigLoader,
|
|
358
|
+
variableReplacer: this.variableReplacer,
|
|
359
|
+
ScaffoldProcessingService: this.processingService.constructor,
|
|
360
|
+
getRootPath: () => {
|
|
361
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
362
|
+
return path.join(__dirname, "../../../../..");
|
|
363
|
+
},
|
|
364
|
+
getProjectPath: (projectPath) => {
|
|
365
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
366
|
+
const rootPath = path.join(__dirname, "../../../../..");
|
|
367
|
+
return projectPath.replace(rootPath, "").replace("/", "");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
message: `Error loading or executing generator ${config.generator}: ${error instanceof Error ? error.message : String(error)}`
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const parsedIncludes = [];
|
|
378
|
+
const warnings = [];
|
|
379
|
+
if (config.includes && Array.isArray(config.includes)) for (const includeEntry of config.includes) {
|
|
380
|
+
const parsed = this.scaffoldConfigLoader.parseIncludeEntry(includeEntry, allVariables);
|
|
381
|
+
if (!this.scaffoldConfigLoader.shouldIncludeFile(parsed.conditions, allVariables)) continue;
|
|
382
|
+
parsedIncludes.push(parsed);
|
|
383
|
+
const targetFilePath = path.join(targetPath, parsed.targetPath);
|
|
384
|
+
if (await this.fileSystem.pathExists(targetFilePath)) warnings.push(`File/folder ${parsed.targetPath} already exists and will be preserved`);
|
|
385
|
+
}
|
|
386
|
+
await this.fileSystem.ensureDir(targetPath);
|
|
387
|
+
const createdFiles = [];
|
|
388
|
+
const existingFiles = [];
|
|
389
|
+
for (const parsed of parsedIncludes) {
|
|
390
|
+
const sourcePath = path.join(templatePath, parsed.sourcePath);
|
|
391
|
+
const targetFilePath = path.join(targetPath, parsed.targetPath);
|
|
392
|
+
await this.processingService.copyAndProcess(sourcePath, targetFilePath, allVariables, createdFiles, existingFiles);
|
|
393
|
+
}
|
|
394
|
+
let message = `Successfully scaffolded ${scaffoldType} at ${targetPath}`;
|
|
395
|
+
if (existingFiles.length > 0) message += `. ${existingFiles.length} existing file(s) were preserved`;
|
|
396
|
+
message += ". Run 'pnpm install' to install dependencies.";
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
message,
|
|
400
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
401
|
+
createdFiles: createdFiles.length > 0 ? createdFiles : void 0,
|
|
402
|
+
existingFiles: existingFiles.length > 0 ? existingFiles : void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
export { ScaffoldService, TemplatesManager };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Liquid } from "liquidjs";
|
|
2
|
+
|
|
3
|
+
//#region src/services/TemplateService.ts
|
|
4
|
+
var TemplateService = class {
|
|
5
|
+
liquid;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.liquid = new Liquid({
|
|
8
|
+
strictFilters: false,
|
|
9
|
+
strictVariables: false
|
|
10
|
+
});
|
|
11
|
+
this.setupCustomFilters();
|
|
12
|
+
}
|
|
13
|
+
toPascalCase(str) {
|
|
14
|
+
const camelCase = str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "");
|
|
15
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
16
|
+
}
|
|
17
|
+
setupCustomFilters() {
|
|
18
|
+
this.liquid.registerFilter("camelCase", (str) => {
|
|
19
|
+
return str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "");
|
|
20
|
+
});
|
|
21
|
+
this.liquid.registerFilter("pascalCase", (str) => {
|
|
22
|
+
return this.toPascalCase(str);
|
|
23
|
+
});
|
|
24
|
+
this.liquid.registerFilter("titleCase", (str) => {
|
|
25
|
+
return this.toPascalCase(str);
|
|
26
|
+
});
|
|
27
|
+
this.liquid.registerFilter("kebabCase", (str) => {
|
|
28
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
29
|
+
});
|
|
30
|
+
this.liquid.registerFilter("snakeCase", (str) => {
|
|
31
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
32
|
+
});
|
|
33
|
+
this.liquid.registerFilter("upperCase", (str) => {
|
|
34
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toUpperCase();
|
|
35
|
+
});
|
|
36
|
+
this.liquid.registerFilter("lower", (str) => str.toLowerCase());
|
|
37
|
+
this.liquid.registerFilter("upper", (str) => str.toUpperCase());
|
|
38
|
+
this.liquid.registerFilter("pluralize", (str) => {
|
|
39
|
+
if (str.endsWith("y")) return `${str.slice(0, -1)}ies`;
|
|
40
|
+
else if (str.endsWith("s") || str.endsWith("sh") || str.endsWith("ch") || str.endsWith("x") || str.endsWith("z")) return `${str}es`;
|
|
41
|
+
else return `${str}s`;
|
|
42
|
+
});
|
|
43
|
+
this.liquid.registerFilter("singularize", (str) => {
|
|
44
|
+
if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
|
|
45
|
+
else if (str.endsWith("es")) return str.slice(0, -2);
|
|
46
|
+
else if (str.endsWith("s") && !str.endsWith("ss")) return str.slice(0, -1);
|
|
47
|
+
else return str;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
renderString(template, variables) {
|
|
51
|
+
try {
|
|
52
|
+
return this.liquid.parseAndRenderSync(template, variables);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(`LiquidJS rendering error: ${error}`);
|
|
55
|
+
return template;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
containsTemplateVariables(content) {
|
|
59
|
+
return [/\{\{.*?\}\}/, /\{%.*?%\}/].some((pattern) => pattern.test(content));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { TemplateService };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
//#region src/services/VariableReplacementService.ts
|
|
4
|
+
var VariableReplacementService = class {
|
|
5
|
+
binaryExtensions = [
|
|
6
|
+
".png",
|
|
7
|
+
".jpg",
|
|
8
|
+
".jpeg",
|
|
9
|
+
".gif",
|
|
10
|
+
".ico",
|
|
11
|
+
".woff",
|
|
12
|
+
".woff2",
|
|
13
|
+
".ttf",
|
|
14
|
+
".eot",
|
|
15
|
+
".pdf",
|
|
16
|
+
".zip",
|
|
17
|
+
".tar",
|
|
18
|
+
".gz",
|
|
19
|
+
".exe",
|
|
20
|
+
".dll",
|
|
21
|
+
".so",
|
|
22
|
+
".dylib"
|
|
23
|
+
];
|
|
24
|
+
constructor(fileSystem, templateService) {
|
|
25
|
+
this.fileSystem = fileSystem;
|
|
26
|
+
this.templateService = templateService;
|
|
27
|
+
}
|
|
28
|
+
async processFilesForVariableReplacement(dirPath, variables) {
|
|
29
|
+
let items = [];
|
|
30
|
+
try {
|
|
31
|
+
items = await this.fileSystem.readdir(dirPath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(`Skipping directory ${dirPath}: ${error}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
if (!item) continue;
|
|
38
|
+
const itemPath = path.join(dirPath, item);
|
|
39
|
+
try {
|
|
40
|
+
const stat = await this.fileSystem.stat(itemPath);
|
|
41
|
+
if (stat.isDirectory()) await this.processFilesForVariableReplacement(itemPath, variables);
|
|
42
|
+
else if (stat.isFile()) await this.replaceVariablesInFile(itemPath, variables);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn(`Skipping item ${itemPath}: ${error}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async replaceVariablesInFile(filePath, variables) {
|
|
49
|
+
try {
|
|
50
|
+
if (this.isBinaryFile(filePath)) return;
|
|
51
|
+
const content = await this.fileSystem.readFile(filePath, "utf8");
|
|
52
|
+
const renderedContent = this.templateService.renderString(content, variables);
|
|
53
|
+
await this.fileSystem.writeFile(filePath, renderedContent, "utf8");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`Skipping file ${filePath}: ${error}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
isBinaryFile(filePath) {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
return this.binaryExtensions.includes(ext);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { VariableReplacementService };
|