-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpacker.ts
342 lines (299 loc) · 10.6 KB
/
packer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import Archiver from 'archiver';
import fse from 'fs-extra';
import type { WalkerOptions } from 'ignore-walk';
import IgnoreWalk from 'ignore-walk';
import os from 'os';
import path from 'path';
import { PackerOpts } from './types';
import { removeEndSlash } from './utils';
const DEFAULT_ARCHIVE_BASENAME = 'packed';
const DEFAULT_IGNORE_GLOBS = ['node_modules', '.git'];
function getAbsNormalized(base: string, relativePathNoSlashStart: string) {
return path.normalize(`${base}${path.sep}${relativePathNoSlashStart}`);
}
function replaceLastInstanceInString(input: string, replace: string, replacement: string) {
let output = input;
const foundIndex = input.lastIndexOf(replace);
if (foundIndex >= 0) {
output = output.slice(0, foundIndex) + replacement;
}
return output;
}
export function GloblistPacker({
rootDir: inputRootDir,
ignoreListFileNames = [],
useGitIgnoreFiles = true,
includeDefaultIgnores = true,
includeEmpty = false,
followSymlink = false,
outDir,
copyFilesTo,
archiveName,
archiveType = 'zip',
archiveRootDirName,
archiveOptions = {},
maxFileCount,
fileNameTransformer = () => true,
onStepChange = () => {},
verbose = false
}: PackerOpts) {
return new Promise(async (resolve, reject) => {
const logger = (...args: any[]) => {
if (verbose) {
console.log(...args);
}
};
/**
* Should be populated before, and then used during, the file walking process. Any directories in this path will be blocked from being added, **OR** their descendants.
* - Use with caution
*/
const blockFolders: string[] = [];
// Resolve some input paths
const rootDir: string = typeof inputRootDir === 'string' ? inputRootDir : process.cwd();
const rootDirUnslashedEnd = removeEndSlash(rootDir);
if (typeof copyFilesTo === 'string') {
if (path.isAbsolute(copyFilesTo)) {
copyFilesTo = removeEndSlash(copyFilesTo);
} else {
// Resolve relative path with rootDir
copyFilesTo = removeEndSlash(path.resolve(rootDir, copyFilesTo));
}
// Make sure that there is no recursive copying going on if the tool is ran more than once
blockFolders.push(copyFilesTo);
} else {
copyFilesTo = undefined;
}
// This might be used, depending on inputs
let tempDirPath: string | null = null;
// Safety check - if output is folder instead of archive, make sure output dir is not same as input
if (copyFilesTo) {
if (path.normalize(copyFilesTo) === path.normalize(rootDirUnslashedEnd)) {
throw new Error(
'Stopping process! - copyFilesTo is the same directory as rootDir - this would overrwrite files in-place and is likely unwanted.'
);
}
}
logger({
rootDir,
ignoreListFileNames,
useGitIgnoreFiles,
includeDefaultIgnores,
includeEmpty,
followSymlink,
outDir,
copyFilesTo,
archiveName,
archiveType,
archiveRootDirName,
archiveOptions,
maxFileCount,
verbose
});
const ignoreListFilesBasenames = ignoreListFileNames.map((i) => path.basename(i));
/**
* NOTE: Order ***really*** matters for ignores files array.
* @see https://github.com/npm/ignore-walk#options
*/
let ignoreFileNames = [];
// @TODO - Improve?
// This does not seem like the *optimal* approach, but unfortunately,
// is the only one that works with ignore-walk. Since it order matters,
// and the walker only takes files into account based on what actual
// directory they reside in, injecting an actual file into the root dir
// before it is walked is the only thing I can come up with right now.
// Post-filtering the file list with something like `ignore()` would not
// work because of the order issue (I could be excluding something that
// a user provided list explicitly approved with `!{pattern}`
const GENERATED_TEMP_IGNORE_FILENAME = `globlist-packer-defaults-${Date.now()}.ignore`;
const GENERATED_TEMP_IGNORE_PATH = `${rootDirUnslashedEnd}${path.sep}${GENERATED_TEMP_IGNORE_FILENAME}`;
if (includeDefaultIgnores) {
ignoreFileNames.push(GENERATED_TEMP_IGNORE_FILENAME);
DEFAULT_IGNORE_GLOBS.push(GENERATED_TEMP_IGNORE_FILENAME);
const collidingFileExists = await fse.pathExists(GENERATED_TEMP_IGNORE_PATH);
if (!collidingFileExists) {
await fse.writeFile(GENERATED_TEMP_IGNORE_PATH, DEFAULT_IGNORE_GLOBS.join('\n'));
} else {
throw new Error(`Fatal: Failed to create temporary ignore file at ${GENERATED_TEMP_IGNORE_PATH}`);
}
}
if (useGitIgnoreFiles) {
ignoreFileNames.push('.gitignore');
}
// Add user provided ignore lists last, so they can override everything else
// Remember: order matters; this must come last.
ignoreFileNames = ignoreFileNames.concat(ignoreListFilesBasenames);
logger(ignoreFileNames);
const walkerArgs: WalkerOptions = {
path: rootDirUnslashedEnd,
follow: followSymlink,
ignoreFiles: ignoreFileNames,
includeEmpty
};
onStepChange('Scanning input files');
const fileListResult = await IgnoreWalk(walkerArgs);
// IMMEDIATELY clean up the temp ignore file if used
if (includeDefaultIgnores) {
await fse.remove(GENERATED_TEMP_IGNORE_PATH);
logger(`Deleted ${GENERATED_TEMP_IGNORE_PATH}`);
}
if (maxFileCount && fileListResult.length > maxFileCount) {
return reject(`Matched file count of ${fileListResult.length} exceeds maxFileCount of ${maxFileCount}`);
}
logger('Scanned files', fileListResult);
// Start prepping for file copying, by readying the target directory
/**
* The path of the root (most parent) folder into which files are copied
*/
let rootDestDirPath: string;
/**
* The actual destination path for which files are cloned into. In the case of a pseudo parent (injected by options), this will differ from `rootDestDirPath`, otherwise, they should be equal
*/
let copyDestDirPath: string;
if (copyFilesTo) {
await fse.ensureDir(copyFilesTo);
rootDestDirPath = copyFilesTo;
} else {
// Use OS temp dir
tempDirPath = await fse.mkdtemp(`${os.tmpdir()}${path.sep}`);
rootDestDirPath = tempDirPath;
}
copyDestDirPath = rootDestDirPath;
// If (pseudo) root dir is required, go ahead and create it
if (archiveRootDirName) {
copyDestDirPath = `${removeEndSlash(rootDestDirPath)}${path.sep}${archiveRootDirName}`;
await fse.mkdirp(path.normalize(copyDestDirPath));
}
onStepChange('Copying files');
logger(`Copying ${fileListResult.length} file(s) to ${copyDestDirPath}`);
const blockedFiles: Array<{
absInputPath: string;
blockedBy: string;
}> = [];
const copiedFilesRelativePaths: string[] = [];
await Promise.all(
fileListResult.map(async (relativeFilePath) => {
// NOTE: the walker only returns file paths, not directories.
let absInputPath = getAbsNormalized(rootDir, relativeFilePath);
let absDestPath = getAbsNormalized(copyDestDirPath, relativeFilePath);
let baseName = path.basename(absInputPath);
// Check for high priority blocks
for (const blockFolder of blockFolders) {
if (absInputPath.includes(path.normalize(blockFolder))) {
blockedFiles.push({
absInputPath,
blockedBy: blockFolder
});
return;
}
}
// Allow user-specified override of filename, or omission
const userTransformResult = await fileNameTransformer({
fileAbsPath: absInputPath,
fileBaseName: baseName
});
if (userTransformResult === false) {
blockedFiles.push({
absInputPath,
blockedBy: 'user provided fileNameTransformer'
});
return;
}
if (typeof userTransformResult === 'string') {
// Return should be new *basename*
const updatedBaseName = userTransformResult;
absInputPath = replaceLastInstanceInString(absInputPath, baseName, updatedBaseName);
absDestPath = replaceLastInstanceInString(absDestPath, baseName, updatedBaseName);
baseName = updatedBaseName;
}
try {
await fse.copyFile(absInputPath, absDestPath);
} catch (e) {
// Try creating dir
const destDirPath = path.dirname(absDestPath);
await fse.mkdirp(destDirPath);
await fse.copyFile(absInputPath, absDestPath);
}
copiedFilesRelativePaths.push(relativeFilePath);
return;
})
);
if (verbose && blockedFiles.length) {
console.table(blockedFiles);
}
logger('Final list of copied files:', copiedFilesRelativePaths);
// Skip archiver step - just copied files
if (!!copyFilesTo) {
// Nothing else to do - we already copied files
onStepChange('Done!');
}
// Regular archiver mode
else {
let archiveFileNameBaseNoExt: string | undefined = undefined;
// Explicit option overrides everything else
if (archiveName) {
// Remove extension
archiveFileNameBaseNoExt = archiveName.replace(path.extname(archiveName), '');
}
// First default = based on ignore list
if (!archiveFileNameBaseNoExt) {
const firstNonGitIgnoreList = ignoreListFilesBasenames.filter((f) => f !== '.gitignore')[0];
if (firstNonGitIgnoreList) {
archiveFileNameBaseNoExt = firstNonGitIgnoreList.replace(path.extname(firstNonGitIgnoreList), '');
}
}
// Final fallback - hardcoded name
if (!archiveFileNameBaseNoExt) {
archiveFileNameBaseNoExt = DEFAULT_ARCHIVE_BASENAME;
}
// Add extension
const archiveExt = archiveType === 'tar' ? '.tgz' : '.zip';
const archiveBaseName = `${archiveFileNameBaseNoExt}${archiveExt}`;
// Prepare archive *stream*
let destinationDir = rootDirUnslashedEnd;
if (outDir) {
if (path.isAbsolute(outDir)) {
destinationDir = removeEndSlash(outDir);
} else {
// Resolve relative path with rootDir
destinationDir = removeEndSlash(path.resolve(rootDir, outDir));
}
}
// Create stream and archiver instance
const archiveAbsPath = `${destinationDir}${path.sep}${archiveBaseName}`;
const archiveOutStream = fse.createWriteStream(archiveAbsPath);
const archive = Archiver(archiveType, {
...archiveOptions,
zlib: {
level: 6,
...(archiveOptions.zlib || {})
}
});
// Attach listeners to archiver
archiveOutStream.on('close', async () => {
logger(`${archive.pointer()} total bytes`);
logger('archiver has been finalized and the output file descriptor has closed.');
// Cleanup temp dir
onStepChange('Cleaning Up');
await fse.remove(tempDirPath!);
logger(`Deleted ${tempDirPath}`);
onStepChange('Done!');
resolve(archiveAbsPath);
});
archive.on('error', reject);
archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
logger(err);
} else {
reject(err);
}
});
// Hookup pipe
archive.pipe(archiveOutStream);
// Archive the temp dir
onStepChange('Compressing');
archive.directory(rootDestDirPath, false);
onStepChange('Finalizing and saving archive');
archive.finalize();
}
});
}