Skip to content

Commit 36d52e3

Browse files
committed
feat(core): 重构 Creator 类并添加中间件支持
- 重构 Creator 类,优化文件处理逻辑 - 添加中间件支持,实现文件类型拦截和修改 - 新增 FileMeta 类型,包含更丰富的文件元数据 - 引入 EventEmitter,支持 start 和 end 事件 - 优化文件路径计算逻辑,提高代码可读性
1 parent 3db6e39 commit 36d52e3

File tree

1 file changed

+95
-63
lines changed

1 file changed

+95
-63
lines changed

src/creator.ts

Lines changed: 95 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import EventEmitter from 'node:events';
12
import fs from 'node:fs';
23
import path from 'node:path';
34
import process from 'node:process';
@@ -6,8 +7,11 @@ import ejs from 'ejs';
67
import fse from 'fs-extra';
78
import { glob } from 'glob';
89
import * as colors from 'picocolors';
10+
import type { W } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js';
11+
import { MiddleWare, type MiddleWareCallback } from './MiddleWare';
12+
import { TypedEvents } from './TypedEvents';
913
import { selectWriteMode } from './prompts';
10-
import { execCommand, isDirectory } from './utils';
14+
import { execCommand, isDirectory, normalizePath } from './utils';
1115

1216
export type Prompts = typeof prompts;
1317
export type Colors = typeof colors;
@@ -90,10 +94,7 @@ export type CreatorBuiltinData = {
9094
*/
9195
export type CreatorData<T> = CreatorBuiltinData & T;
9296

93-
/**
94-
* Metadata about files being processed
95-
*/
96-
export type WriteMeta = {
97+
export type FileTypes = {
9798
/**
9899
* Whether file uses EJS templating
99100
*/
@@ -106,6 +107,12 @@ export type WriteMeta = {
106107
* Whether file uses dot prefix
107108
*/
108109
isDotFile: boolean;
110+
};
111+
112+
/**
113+
* Metadata about files being processed
114+
*/
115+
export type FileMeta = FileTypes & {
109116
/**
110117
* Full path to source file
111118
*/
@@ -149,33 +156,23 @@ export type CreatorOptions<T> = {
149156
* Root directory containing templates
150157
*/
151158
templatesRoot: string;
152-
/**
153-
* Callback before template generation
154-
*/
155-
onStart?: (context: CreatorContext) => unknown | Promise<unknown>;
156159
/**
157160
* Extend template data with custom properties
158161
*/
159162
extendData?: (context: CreatorContext) => T | Promise<T>;
160-
/**
161-
* Control which files should be written
162-
*/
163-
canWrite?: (meta: WriteMeta, data: CreatorData<T>) => boolean | Promise<boolean>;
163+
164+
canWrite?: (meta: FileMeta, data: CreatorData<T>) => boolean | Promise<boolean>;
165+
canRender?: (meta: FileMeta, data: CreatorData<T>) => boolean | Promise<boolean>;
164166
/**
165167
* Custom file writing implementation
166168
*/
167-
doWrite?: (meta: WriteMeta, data: CreatorData<T>) => unknown | Promise<unknown>;
169+
doWrite?: (meta: FileMeta, data: CreatorData<T>) => unknown | Promise<unknown>;
168170
/**
169171
* Callback after each file is written
170172
*/
171-
onWritten?: (meta: WriteMeta, data: CreatorData<T>) => unknown | Promise<unknown>;
172-
/**
173-
* Callback after template generation completes
174-
*/
175-
onEnd?: (context: CreatorContext) => unknown | Promise<unknown>;
173+
onWritten?: (meta: FileMeta, data: CreatorData<T>) => unknown | Promise<unknown>;
176174
};
177175

178-
const normalizePath = (path: string) => path.replace(/\\/g, '/');
179176
const UNDERSCORE_FILE_PREFIX = '__';
180177
const DOT_FILE_PREFIX = '_';
181178
const EJS_FILE_SUFFIX = '.ejs';
@@ -185,7 +182,10 @@ const EJS_FILE_REGEX = /\.ejs$/i;
185182
* Main class for handling project creation
186183
* @template T - Type of custom data to extend with
187184
*/
188-
class Creator<T extends Record<string, unknown>> {
185+
export class Creator<T extends Record<string, unknown>> extends TypedEvents<{
186+
start: [context: CreatorContext];
187+
end: [context: CreatorContext];
188+
}> {
189189
context: CreatorContext = {
190190
cwd: '',
191191
templatesRoot: '',
@@ -202,11 +202,15 @@ class Creator<T extends Record<string, unknown>> {
202202
};
203203
data: CreatorData<T>;
204204

205+
fileMetaMW: MiddleWare<[meta: FileMeta, data: CreatorData<T>], Partial<FileTypes>>;
206+
205207
/**
206208
* Create a new Creator instance
207209
* @param {CreatorOptions<T>} options - Configuration options
208210
*/
209211
constructor(private readonly options: CreatorOptions<T>) {
212+
super();
213+
210214
const cwd = normalizePath(options.cwd || process.cwd());
211215
const projectRoot = normalizePath(path.resolve(cwd, options.projectPath || '.'));
212216
const { context } = this;
@@ -221,6 +225,18 @@ class Creator<T extends Record<string, unknown>> {
221225
: `create-${context.projectName}`;
222226

223227
this.data = { ctx: context } as CreatorData<T>;
228+
229+
this.fileMetaMW = new MiddleWare({
230+
cwd: context.templatesRoot,
231+
});
232+
}
233+
234+
fileIntercept(
235+
paths: string | string[],
236+
interceptor: MiddleWareCallback<[meta: FileMeta, data: CreatorData<T>], Partial<FileTypes>>,
237+
) {
238+
this.fileMetaMW.match(paths, interceptor);
239+
return this;
224240
}
225241

226242
async #check() {
@@ -266,44 +282,31 @@ class Creator<T extends Record<string, unknown>> {
266282

267283
async #generate() {
268284
const { context, options } = this;
269-
const files = await glob('**/*', {
285+
const paths = await glob('**/*', {
270286
nodir: true,
271287
cwd: context.templateRoot,
272288
dot: false,
273289
});
274290

275-
if (files.length === 0) {
291+
if (paths.length === 0) {
276292
prompts.cancel(`No files found in template(${context.templateName})`);
277293
process.exit(1);
278294
}
279295

280-
for (const sourcePath of files) {
296+
for (const sourcePath of paths) {
281297
const fileName = path.basename(sourcePath);
282298
const fileFolder = path.dirname(sourcePath);
283299
const sourceFile = normalizePath(path.join(context.templateRoot, sourcePath));
300+
284301
const isEjsFile = EJS_FILE_REGEX.test(sourcePath);
285302
const isUnderscoreFile = sourcePath.startsWith(UNDERSCORE_FILE_PREFIX);
286303
const isDotFile = !isUnderscoreFile && sourcePath.startsWith(DOT_FILE_PREFIX);
287304

288-
let start = 0;
289-
let end = undefined;
290-
let prefix = '';
291-
292-
if (isEjsFile) {
293-
end = -EJS_FILE_SUFFIX.length;
294-
}
295-
296-
if (isUnderscoreFile) {
297-
start = UNDERSCORE_FILE_PREFIX.length;
298-
prefix = '_';
299-
} else if (isDotFile) {
300-
start = DOT_FILE_PREFIX.length;
301-
prefix = '.';
302-
}
303-
305+
const { prefix, end, start } = calculateFileMate({ isDotFile, isEjsFile, isUnderscoreFile });
304306
const targetPath = normalizePath(path.join(fileFolder, prefix + fileName.slice(start, end)));
305307
const targetFile = normalizePath(path.join(context.projectRoot, targetPath));
306-
const writeMeta: WriteMeta = {
308+
309+
const fileMeta: FileMeta = {
307310
isDotFile,
308311
isEjsFile,
309312
isUnderscoreFile,
@@ -314,25 +317,44 @@ class Creator<T extends Record<string, unknown>> {
314317
targetFile,
315318
targetRoot: context.projectRoot,
316319
};
320+
const fileTypes = await this.fileMetaMW.when(path.join(context.templateName, sourcePath), fileMeta, this.data);
321+
322+
// 修改了 fileTypes
323+
if (fileTypes) {
324+
const { isDotFile, isEjsFile, isUnderscoreFile } = fileTypes;
325+
const { prefix, end, start } = calculateFileMate({ isDotFile, isEjsFile, isUnderscoreFile });
326+
const targetPath = normalizePath(path.join(fileFolder, prefix + fileName.slice(start, end)));
327+
const targetFile = normalizePath(path.join(context.projectRoot, targetPath));
328+
fileMeta.targetPath = targetPath;
329+
fileMeta.targetFile = targetFile;
330+
fileMeta.isDotFile = isDotFile || false;
331+
fileMeta.isEjsFile = isEjsFile || false;
332+
fileMeta.isUnderscoreFile = isUnderscoreFile || false;
333+
}
317334

318-
if (options.canWrite?.call(null, writeMeta, this.data) === false) {
335+
if ((await options.canWrite?.call(null, fileMeta, this.data)) === false) {
319336
continue;
320337
}
321338

322-
if (options.doWrite) {
323-
await options.doWrite(writeMeta, this.data);
324-
} else if (isEjsFile) {
325-
const template = await fse.readFile(sourceFile, 'utf8');
326-
fse.outputFileSync(targetFile, ejs.render(template, this.data));
327-
} else {
328-
fse.copySync(sourceFile, targetFile);
329-
}
339+
await this.#write(fileMeta);
340+
options.onWritten?.call(null, fileMeta, this.data);
341+
}
342+
}
343+
344+
async #write(fileMeta: FileMeta) {
345+
const { context, options } = this;
330346

331-
options.onWritten?.call(null, writeMeta, this.data);
347+
if (options.doWrite) {
348+
await options.doWrite(fileMeta, this.data);
349+
} else if (fileMeta.isEjsFile) {
350+
const template = await fse.readFile(fileMeta.sourceFile, 'utf8');
351+
fse.outputFileSync(fileMeta.targetFile, ejs.render(template, this.data));
352+
} else {
353+
fse.copySync(fileMeta.sourceFile, fileMeta.targetFile);
332354
}
333355
}
334356

335-
async start() {
357+
async create() {
336358
const { context, options } = this;
337359

338360
if (isDirectory(context.templatesRoot) === false) {
@@ -349,7 +371,8 @@ class Creator<T extends Record<string, unknown>> {
349371
process.exit(1);
350372
}
351373

352-
await options.onStart?.call(null, context);
374+
await this.emit('start', context);
375+
353376
context.templateName =
354377
templateNames.length === 1
355378
? templateNames[0]
@@ -365,17 +388,26 @@ class Creator<T extends Record<string, unknown>> {
365388
await this.#check();
366389
await this.#extend();
367390
await this.#generate();
368-
369-
await options.onEnd?.call(null, context);
391+
await this.emit('end', context);
370392
}
371393
}
372394

373-
/**
374-
* Build and start a new creator instance
375-
* @template T - Type of custom data to extend with
376-
* @param {CreatorOptions<T>} options - Configuration options
377-
* @returns {Promise<void>}
378-
*/
379-
export async function createCreator<T extends Record<string, unknown>>(options: CreatorOptions<T>) {
380-
await new Creator<T>(options).start();
395+
export function calculateFileMate({ isDotFile, isEjsFile, isUnderscoreFile }: Partial<FileTypes>) {
396+
let start = 0;
397+
let end = undefined;
398+
let prefix = '';
399+
400+
if (isEjsFile) {
401+
end = -EJS_FILE_SUFFIX.length;
402+
}
403+
404+
if (isUnderscoreFile) {
405+
start = UNDERSCORE_FILE_PREFIX.length;
406+
prefix = '_';
407+
} else if (isDotFile) {
408+
start = DOT_FILE_PREFIX.length;
409+
prefix = '.';
410+
}
411+
412+
return { start, end, prefix };
381413
}

0 commit comments

Comments
 (0)