forked from floydspace/serverless-esbuild
-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.ts
137 lines (116 loc) · 4.68 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
import type { FileSystem } from '@effect/platform';
import { NodeFileSystem } from '@effect/platform-node';
import archiver from 'archiver';
import { bestzip } from 'bestzip';
import { type Cause, Effect, Option } from 'effect';
import execa from 'execa';
import fs from 'fs-extra';
import path from 'path';
import type { IFile, IFiles } from './types';
import FS, { FSyncLayer, makePath, makeTempPathScoped, safeFileExists } from './utils/effect-fs';
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 findUpEffect = (
names: string[],
directory = process.cwd()
): Effect.Effect<string, Cause.NoSuchElementException, FileSystem.FileSystem> => {
const dir = path.resolve(directory);
return Effect.all(names.map((name) => safeFileExists(path.join(dir, name)))).pipe(
Effect.flatMap((exist) => {
if (exist.some(Boolean)) return Option.some(dir);
if (isPathRoot(dir)) return Option.none();
return findUpEffect(names, path.dirname(dir));
})
);
};
/**
* Find a file by walking up parent directories
*/
export const findUp = (name: string) =>
findUpEffect([name]).pipe(
Effect.orElseSucceed(() => undefined),
Effect.provide(FSyncLayer),
Effect.runSync
);
/**
* Forwards `rootDir` or finds project root folder.
*/
export const findProjectRoot = (rootDir?: string) =>
Effect.fromNullable(rootDir).pipe(
Effect.orElse(() => findUpEffect(['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'])),
Effect.orElseSucceed(() => undefined),
Effect.provide(FSyncLayer),
Effect.runSync
);
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, path.extname(zipPath))}-${Date.now().toString()}`;
const copyFileEffect = (temp: string) => (file: IFile) => FS.copy(file.rootPath, path.join(temp, file.localPath));
const bestZipEffect = (temp: string) =>
Effect.tryPromise(() => bestzip({ source: '*', destination: zipPath, cwd: temp }));
const nodeZipEffect = Effect.tryPromise(() => nodeZip(zipPath, filesPathList));
const archiveEffect = makeTempPathScoped(tempDirName).pipe(
// copy all required files from origin path to (sometimes modified) target path
Effect.tap((temp) => Effect.all(filesPathList.map(copyFileEffect(temp)), { discard: true })),
// prepare zip folder
Effect.tap(() => makePath(path.dirname(zipPath))),
// zip the temporary directory
Effect.andThen((temp) => (useNativeZip ? bestZipEffect(temp) : nodeZipEffect)),
Effect.scoped
);
await archiveEffect.pipe(Effect.provide(NodeFileSystem.layer), Effect.runPromise);
};
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;
};