Skip to content

Commit aa5f08d

Browse files
committed
perf(generator): modify files in memory to reduce I/O
BREAKING CHANGE: `beforeMerge` and `afterMerge` are now called before and after each `eMerging`, respectively, and provide directory information and file information
1 parent 33b7b1b commit aa5f08d

File tree

5 files changed

+112
-83
lines changed

5 files changed

+112
-83
lines changed

src/Dir.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { FileInfo, Status } from './File';
22
import np from 'normalize-path';
3+
import { isDir } from './utils';
4+
import VError from 'verror';
35

46
import type { Required } from 'utility-types';
5-
import VError from 'verror';
7+
68

79
type DirInfoConstructorOptions = {
810
parent?: DirInfo | null;
@@ -46,13 +48,7 @@ export class DirInfo implements Status {
4648
// build tree
4749
const oriDirInfo = fs.readdirSync(this.pathname, { withFileTypes: true });
4850
oriDirInfo.forEach((f) => {
49-
if (
50-
f.isDirectory() ||
51-
(f.isSymbolicLink() &&
52-
fs
53-
.statSync(fs.readlinkSync(path.resolve(this.pathname, f.name)))
54-
.isDirectory())
55-
) {
51+
if (isDir(path.resolve(this.pathname, f.name))) {
5652
this.dirMap[f.name] = DirInfo.build({
5753
parent: this,
5854
pathname: path.join(this.pathname, f.name),

src/File.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface Status {
88
exist: boolean;
99
}
1010

11-
type JsonObj = Record<string | number, object | Array<unknown>>;
11+
type JsonObj = Record<string | number, object | Array<unknown> | string | number>;
1212

1313
type FileInfoConstructorOptions = {
1414
pathname: string;

src/Generator.ts

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,21 @@
11
import '@/utils/helper';
22

33
import ora from 'ora';
4-
import { nanoid } from 'nanoid';
54
import { FileInfo } from './File';
65
import { GeneratorSource } from './Source';
7-
import { useEnvVar } from './utils';
86
import { Hook, HookHelper } from './Hook';
7+
import { DirInfo } from './Dir';
8+
import np from 'normalize-path';
99

1010
import type { Ora } from 'ora';
1111
import type { SourceInfo } from './Source';
1212
import type { Hooks } from './Hook';
1313

14-
const { __dir_cache_generator } = useEnvVar();
15-
16-
/**
17-
* src write into dest
18-
*/
19-
export const copyFile = async (options: {
20-
src: string;
21-
dest: string;
22-
hook: Hook;
23-
}) => {
24-
const { src, dest, hook } = options;
25-
if (!fs.existsSync(dest)) {
26-
fs.copyFileSync(src, dest);
27-
return;
28-
}
29-
const srcFile = FileInfo.build({ pathname: src });
30-
const destFile = FileInfo.build({
31-
pathname: path.join(dest, srcFile.filename),
32-
});
33-
fs.writeFileSync(
34-
destFile.pathname,
35-
hook.hasHook('onMerging')
36-
? await hook.callHook('onMerging', { src: srcFile, dest: destFile })
37-
: srcFile.getContent(),
38-
);
39-
};
40-
41-
export const copy = async (options: {
42-
src: string;
43-
dest: string;
44-
hook: Hook;
45-
}) => {
46-
const { src, dest, hook } = options;
47-
const filenameList = fs.readdirSync(src);
48-
fs.ensureDirSync(dest);
49-
for (const filename of filenameList) {
50-
const srcPath = path.join(src, filename);
51-
const destPath = path.join(dest, filename);
52-
const state = fs.lstatSync(srcPath);
53-
if (state.isDirectory()) copy({ src: srcPath, dest: destPath, hook });
54-
// TODO: should i use less I/O and modify files at memory?
55-
else await copyFile({ src: srcPath, dest, hook });
56-
}
57-
};
58-
5914
export class Generator<N extends string = string> {
60-
private hash: string = nanoid();
61-
62-
private tempPathname: string = path.resolve(
63-
__dir_cache_generator,
64-
this.hash,
65-
);
66-
6715
private source: GeneratorSource<N>;
6816
private target: SourceInfo;
17+
private targetDirInfo: DirInfo;
18+
private curInjectDirInfo: DirInfo;
6919

7020
private baseHook: Hook;
7121
private injectHookMap: Record<N, Hook> = {} as Record<N, Hook>;
@@ -79,7 +29,9 @@ export class Generator<N extends string = string> {
7929
const { source, target } = options;
8030
this.source = source;
8131
this.target = target;
82-
fs.ensureDirSync(this.tempPathname);
32+
this.targetDirInfo = DirInfo.build({
33+
pathname: this.target.pathname,
34+
});
8335
}
8436

8537
public static async build<N extends string = string>(options: {
@@ -120,6 +72,66 @@ export class Generator<N extends string = string> {
12072
return generator;
12173
}
12274

75+
/**
76+
* src write into dest
77+
*/
78+
private async copyFile(options: {
79+
src: FileInfo;
80+
dest: FileInfo;
81+
hook: Hook;
82+
}) {
83+
const { src, dest, hook } = options;
84+
85+
await hook.callHook('beforeMerge', {
86+
srcDir: this.curInjectDirInfo,
87+
destDir: this.targetDirInfo,
88+
src,
89+
dest,
90+
});
91+
92+
if (!hook.hasHook('onMerging')) {
93+
dest.setContent(src.getContent());
94+
} else {
95+
dest.setContent(
96+
await hook.callHook('onMerging', {
97+
src,
98+
dest,
99+
}),
100+
);
101+
}
102+
103+
await hook.callHook('afterMerge', {
104+
srcDir: this.curInjectDirInfo,
105+
destDir: this.targetDirInfo,
106+
src,
107+
dest,
108+
});
109+
}
110+
111+
private async copy(options: { src: DirInfo; dest: DirInfo; hook: Hook }) {
112+
const { src, dest, hook } = options;
113+
const map = src.getMap();
114+
for (const item of Object.values(map)) {
115+
const destRelPath = np(path.relative(src.pathname, item.pathname));
116+
if (item.isDir)
117+
await this.copy({
118+
src: item,
119+
dest: dest.ensure(destRelPath, {
120+
type: 'dir',
121+
}),
122+
hook,
123+
});
124+
else
125+
await this.copyFile({
126+
src: item as unknown as FileInfo,
127+
dest: dest.ensure(destRelPath, {
128+
type: 'file',
129+
}),
130+
hook,
131+
});
132+
}
133+
}
134+
123135
private setBaseHook(hook: Hook) {
124136
this.baseHook = hook;
125137
}
@@ -132,10 +144,12 @@ export class Generator<N extends string = string> {
132144
hookName: N,
133145
...args: Parameters<Hooks[N]>
134146
) {
147+
this.spinner.stop();
135148
await this.baseHook.callHook(hookName, ...args);
136149
for (const hook of Object.values<Hook>(this.injectHookMap)) {
137150
await hook.callHook(hookName, ...args);
138151
}
152+
$.verbose = false;
139153
}
140154

141155
private async generateBase() {
@@ -144,11 +158,15 @@ export class Generator<N extends string = string> {
144158
`Generate base - ${chalk.blueBright(this.source.baseSource.name)}`,
145159
);
146160

147-
copy({
148-
src: this.source.getBaseFilesPathname(),
149-
dest: this.tempPathname,
161+
this.curInjectDirInfo = DirInfo.build({
162+
pathname: this.source.getBaseFilesPathname(),
163+
});
164+
await this.copy({
165+
src: this.curInjectDirInfo,
166+
dest: this.targetDirInfo,
150167
hook: this.baseHook,
151168
});
169+
this.curInjectDirInfo = null;
152170
}
153171

154172
private async generateInject() {
@@ -157,11 +175,17 @@ export class Generator<N extends string = string> {
157175
`Generate inject - ${chalk.cyanBright(injectSourceInfo.name)}`,
158176
);
159177

160-
copy({
161-
src: this.source.getInjectFilesPathnameByName(injectSourceInfo.name),
162-
dest: this.tempPathname,
178+
this.curInjectDirInfo = DirInfo.build({
179+
pathname: this.source.getInjectFilesPathnameByName(
180+
injectSourceInfo.name,
181+
),
182+
});
183+
await this.copy({
184+
src: this.curInjectDirInfo,
185+
dest: this.targetDirInfo,
163186
hook: this.injectHookMap[injectSourceInfo.name],
164187
});
188+
this.curInjectDirInfo = null;
165189
}
166190
}
167191

@@ -184,15 +208,11 @@ export class Generator<N extends string = string> {
184208

185209
public async generate() {
186210
try {
187-
await this.callHooks('beforeMerge');
188211

189212
await this.generateBase();
190213
await this.generateInject();
191214
await this.output();
192215

193-
this.spinner.stop();
194-
await this.callHooks('afterMerge');
195-
196216
await this.installDeps();
197217

198218
this.spinner.succeed(

src/qa.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,16 @@ export const getRepoOptions = async (
125125

126126
return answer;
127127
};
128+
129+
export const getConfirm = async (msg: string) => {
130+
return (
131+
await inquirer.prompt<{ confirm: boolean }>([
132+
{
133+
name: 'confirm',
134+
message: msg,
135+
type: 'confirm',
136+
default: false,
137+
},
138+
])
139+
).confirm;
140+
};

src/utils.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@ import { fileURLToPath } from 'url';
55
import { homedir } from 'os';
66

77
const __dir_home = homedir();
8-
const __dir_src_root = path.resolve(
9-
path.dirname(fileURLToPath(import.meta.url)),
10-
'./',
11-
);
12-
const __dir_cache = path.resolve(
13-
__dir_home,
14-
'./.cache/@setup',
15-
);
8+
const __dir_cache = path.resolve(__dir_home, './.cache/@setup');
169
fs.ensureDir(__dir_cache);
1710
const __dir_cache_git = path.resolve(__dir_cache, './__git_cache__');
1811
fs.ensureDir(__dir_cache_git);
@@ -25,7 +18,6 @@ const __dir_cache_test = path.resolve(__dir_cache, './__test_cache__');
2518
fs.ensureDir(__dir_cache_test);
2619

2720
export const useEnvVar = () => ({
28-
__dir_src_root,
2921
__dir_cache,
3022
__dir_cache_git,
3123
__dir_cache_generator,
@@ -63,6 +55,14 @@ export function hasKeys<T extends object = object>(options: {
6355
return true;
6456
}
6557

58+
export function isDir(pathname: string) {
59+
const f = fs.statSync(pathname);
60+
return (
61+
f.isDirectory() ||
62+
(f.isSymbolicLink() && fs.statSync(fs.readlinkSync(pathname)).isDirectory())
63+
);
64+
}
65+
6666
export type OmitFirst<T extends unknown[]> = T extends [unknown, ...infer R]
6767
? R
6868
: never;

0 commit comments

Comments
 (0)