forked from r2d2bzh/js-backend-rules
-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.js
139 lines (123 loc) · 4.18 KB
/
utils.js
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
/* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
import { spawn as childSpawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { URL } from 'node:url';
import FileHound from 'filehound';
import gitRemoteOriginUrl from 'git-remote-origin-url';
import yaml from 'js-yaml';
import { readJSONFile } from '@r2d2bzh/js-rules';
export const spawn =
(exec, ...arguments_) =>
(cwd = '.') =>
new Promise((resolve, reject) =>
childSpawn(exec, arguments_, { cwd, stdio: ['ignore', 'ignore', 'inherit'] }).on('close', (code) => {
const commandDesc = `'${exec} ${arguments_.join(' ')}' in ${cwd}`;
return code === 0 ? resolve(`${commandDesc} succeeded`) : reject(new Error(`${commandDesc} failed (${code})`));
}),
);
export const findDirectoriesWith = async (glob) => {
const findGlobNotInNodeModules = await FileHound.create()
.path('.')
.discard('(^|.*/)node_modules/.*')
// The following rule is currently needed because of share symbolic links that are followed by filehound:
// https://github.com/nspragg/filehound/issues/77#issuecomment-1106130404
.discard('.*/share/.*')
.match(glob)
.find();
return findGlobNotInNodeModules.map((p) => path.dirname(p));
};
export const writeJSONFile = async (path, content) => {
try {
await fs.writeFile(path, `${JSON.stringify(content, undefined, 2)}\n`, { encoding: 'utf8' });
} catch (error) {
throw new Error(`failed to write JSON to ${path} (${error.message})`);
}
};
export const mergeInJSONFile = async (path, ...objects) => {
const original = await readJSONFile(path);
return writeJSONFile(path, merge(original, ...objects));
};
const identity = (x) => x;
export const pipe = (functions) => {
let pipe = identity;
for (const function_ of functions) {
pipe = compose(pipe)(function_);
}
return pipe;
};
const compose = (g) => (f) => async (x) => f(await g(x));
export const extractValue = (path) => (object) => {
let extractedValue = object;
for (const field of path) {
extractedValue = extractedValue?.[field];
if (extractedValue === undefined) {
return;
}
}
return extractedValue;
};
export const extractValueAs = (path, key, mapper = (v) => v) => {
const extractValueFrom = extractValue(path);
return (object) => {
const value = mapper(extractValueFrom(object));
return value ? { [key]: value } : {};
};
};
export const getProjectPath = async () =>
pipe([sanitizeGitURL, getURLPathname, removeDotGit])(await gitRemoteOriginUrl());
export const emptyObjectOnException = async (_function) => {
try {
return await _function();
} catch {
return {};
}
};
export const readYAMLFile = async (path) => {
try {
// The purpose of this library is to scaffold based on existing project content
// eslint-disable-next-line security/detect-non-literal-fs-filename
const document = await fs.readFile(path);
return yaml.load(document);
} catch (error) {
throw new Error(`failed to extract YAML from ${path} (${error.message})`);
}
};
const merge = (original, ...overrides) => {
let merge = original;
for (const override of overrides) {
merge = mergeTwo(merge, override);
}
return merge;
};
const mergeTwo = (original, override) => {
try {
return original.constructor.name === override.constructor.name ? mergeSameType(original, override) : override;
} catch {
return override;
}
};
const mergeSameType = (original, override) => {
switch (original.constructor.name) {
case 'Object': {
const merge = { ...original };
for (const [key, value] of Object.entries(override)) {
merge[key] = mergeTwo(original[key], value);
}
return merge;
}
case 'Array': {
return [...new Set([...original, ...override])].sort();
}
default: {
return override;
}
}
};
const sanitizeGitURL = (url) =>
url.startsWith('git@') ? `git+ssh://${url.replace(/:([^:]*)$/, (match, p) => `/${p}`)}` : url;
const getURLPathname = (url) => {
const { pathname } = new URL(url);
return pathname;
};
const removeDotGit = (string) => (string.endsWith('.git') ? string.slice(0, -4) : string);