forked from floydspace/serverless-esbuild
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.ts
145 lines (123 loc) · 4.58 KB
/
utils.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
import { bestzip } from 'bestzip';
import archiver from 'archiver';
import execa from 'execa';
import { pipe } from 'fp-ts/lib/function';
import * as IO from 'fp-ts/lib/IO';
import * as IOO from 'fp-ts/lib/IOOption';
import * as TE from 'fp-ts/lib/TaskEither';
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import {
copyTask,
mkdirpTask,
removeTask,
safeFileExistsIO,
taskEitherToPromise,
taskFromPromise,
} from './utils/fp-fs';
import type { IFile, IFiles } from './types';
export class SpawnError extends Error {
constructor(message: string, public stdout: string, public stderr: string) {
super(message);
}
toString() {
return `${this.message}\n${this.stderr}`;
}
}
/**
* Executes a child process without limitations on stdout and stderr.
* On error (exit code is not 0), it rejects with a SpawnProcessError that contains the stdout and stderr streams,
* on success it returns the streams in an object.
* @param {string} command - Command
* @param {string[]} [args] - Arguments
* @param {Object} [options] - Options for child_process.spawn
*/
export function spawnProcess(command: string, args: string[], options: execa.Options) {
return execa(command, args, options);
}
const rootOf = (p: string) => path.parse(path.resolve(p)).root;
const isPathRoot = (p: string) => rootOf(p) === path.resolve(p);
const findUpIO = (name: string, directory = process.cwd()): IOO.IOOption<string> =>
pipe(path.resolve(directory), (dir) =>
pipe(
safeFileExistsIO(path.join(dir, name)),
IO.chain((exists: boolean) => {
if (exists) return IOO.some(dir);
if (isPathRoot(dir)) return IOO.none;
return findUpIO(name, path.dirname(dir));
})
)
);
/**
* Find a file by walking up parent directories
*/
export const findUp = (name: string) => pipe(findUpIO(name), IOO.toUndefined)();
/**
* Forwards `rootDir` or finds project root folder.
*/
export const findProjectRoot = (rootDir?: string) =>
pipe(
IOO.fromNullable(rootDir),
IOO.fold(() => findUpIO('yarn.lock'), IOO.of),
IOO.fold(() => findUpIO('pnpm-lock.yaml'), IOO.of),
IOO.fold(() => findUpIO('package-lock.json'), IOO.of),
IOO.toUndefined
)();
export const humanSize = (size: number) => {
const exponent = Math.floor(Math.log(size) / Math.log(1024));
const sanitized = (size / 1024 ** exponent).toFixed(2);
return `${sanitized} ${['B', 'KB', 'MB', 'GB', 'TB'][exponent]}`;
};
export const zip = async (zipPath: string, filesPathList: IFiles, useNativeZip = false): Promise<void> => {
// create a temporary directory to hold the final zip structure
const tempDirName = `${path.basename(zipPath).slice(0, -4)}-${Date.now().toString()}`;
const tempDirPath = path.join(os.tmpdir(), tempDirName);
const copyFileTask = (file: IFile) => copyTask(file.rootPath, path.join(tempDirPath, file.localPath));
const copyFilesTask = TE.traverseArray(copyFileTask);
const bestZipTask = taskFromPromise(() => bestzip({ source: '*', destination: zipPath, cwd: tempDirPath }));
const nodeZipTask = taskFromPromise(() => nodeZip(zipPath, filesPathList));
await pipe(
// create the random temporary folder
mkdirpTask(tempDirPath),
// copy all required files from origin path to (sometimes modified) target path
TE.chain(() => copyFilesTask(filesPathList)),
// prepare zip folder
TE.chain(() => mkdirpTask(path.dirname(zipPath))),
// zip the temporary directory
TE.chain(() => (useNativeZip ? bestZipTask : nodeZipTask)),
// delete the temporary folder
TE.chain(() => removeTask(tempDirPath)),
taskEitherToPromise
);
};
function nodeZip(zipPath: string, filesPathList: IFiles): Promise<void> {
const zipArchive = archiver.create('zip');
const output = fs.createWriteStream(zipPath);
// write zip
output.on('open', () => {
zipArchive.pipe(output);
filesPathList.forEach((file) => {
const stats = fs.statSync(file.rootPath);
if (stats.isDirectory()) return;
zipArchive.append(fs.readFileSync(file.rootPath), {
name: file.localPath,
mode: stats.mode,
date: new Date(0), // necessary to get the same hash when zipping the same content
});
});
zipArchive.finalize();
});
return new Promise((resolve, reject) => {
output.on('close', resolve);
zipArchive.on('error', (err) => reject(err));
});
}
export function trimExtension(entry: string) {
return entry.slice(0, -path.extname(entry).length);
}
export const isEmpty = (obj: Record<string, unknown>) => {
// eslint-disable-next-line no-unreachable-loop
for (const _i in obj) return false;
return true;
};