build-strap 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,13 +120,35 @@ publish(
120
120
  ```
121
121
 
122
122
  ### buildLog
123
- Write out to the console in a timestamp prefixed format consistent with the rest of the build output.
123
+ Write out to the console in a timestamp prefixed format consistent with the rest of the build output. Output goes to `stderr` by default, so that `stdout` can be reserved for machine readable output.
124
124
  ```
125
125
  import { buildLog } from 'build-strap';
126
126
 
127
127
  buildLog('Hello world');
128
128
  ```
129
129
 
130
+ Pass a `Date` (as before) or an options object to control the timestamp and destination:
131
+ ```
132
+ buildLog('Hello world', new Date());
133
+ buildLog('to stdout instead', { stream: 'stdout' });
134
+ buildLog('to any writable stream', { stream: myWriteStream });
135
+ buildLog('or handle it yourself', { stream: (msg) => myLogger.info(msg) });
136
+ ```
137
+
138
+ The default destination can be changed globally (affects `run`/`runCli` logging too):
139
+ ```
140
+ import { getBuildLogStream, setBuildLogStream } from 'build-strap';
141
+
142
+ setBuildLogStream('stdout');
143
+ setBuildLogStream(process.stderr);
144
+ setBuildLogStream((msg) => myLogger.info(msg));
145
+ getBuildLogStream(); // current default
146
+ ```
147
+
148
+ CLI flags:
149
+ - `--buildLog-stdout` — default to `stdout` instead of `stderr`
150
+ - `--silence-buildLog` — suppress all `buildLog` output
151
+
130
152
  ## NPM Credentials
131
153
  In order to publish to NPM, proper credentials must be provided to the script. By default, these are read from the `NPM_CREDS` environment variable, but it is also possible to pass them as an argument to most functions. This is expected to be a JSON encoded string in the following format:
132
154
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "build-strap",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Node scripts for building things",
5
5
  "license": "MIT",
6
6
  "author": "Justin Murray <justin@murrayju.com>",
@@ -16,17 +16,17 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "bytes": "^3.1.1",
19
- "chokidar": "^3.6.0",
19
+ "chokidar": "^5.0.0",
20
20
  "cross-spawn": "^7.0.3",
21
21
  "fs-extra": "^11.2.0",
22
22
  "get-port": "^7.1.0",
23
- "glob": "^10.3.14",
24
- "is-reachable": "^5.1.1",
25
- "js-yaml": "^4.1.0",
23
+ "glob": "^13.0.6",
24
+ "is-reachable": "^6.1.0",
25
+ "js-yaml": "^5.4.0",
26
26
  "lodash-es": "^4.17.21",
27
27
  "moment": "^2.30.1",
28
28
  "node-fetch": "^3.3.2",
29
- "rimraf": "^5.0.5",
29
+ "rimraf": "^6.1.3",
30
30
  "stream-counter": "1",
31
31
  "tar": "^7.1.0"
32
32
  }
package/src/cert.d.ts CHANGED
@@ -3,8 +3,8 @@ export interface CertConfig {
3
3
  countryName?: string;
4
4
  emailAddress?: string;
5
5
  localityName?: string;
6
- organizationName?: string;
7
6
  organizationalUnitName?: string;
7
+ organizationName?: string;
8
8
  stateOrProvinceName?: string;
9
9
  }
10
10
  export declare const getCertConfig: () => CertConfig;
package/src/cert.js CHANGED
@@ -21,7 +21,7 @@ export async function generateCert(targetDir, force = process.argv.includes('--f
21
21
  return;
22
22
  }
23
23
  buildLog('Generating self-signed certificate...');
24
- const { commonName = 'app.company.com', countryName = 'US', emailAddress = 'contact@company.com', localityName = 'SomeCity', organizationName = 'Company', organizationalUnitName = 'Product', stateOrProvinceName = 'Pennsylvania', } = getCertConfig();
24
+ const { commonName = 'app.company.com', countryName = 'US', emailAddress = 'contact@company.com', localityName = 'SomeCity', organizationalUnitName = 'Product', organizationName = 'Company', stateOrProvinceName = 'Pennsylvania', } = getCertConfig();
25
25
  const hostname = os.hostname();
26
26
  buildLog(`hostname: ${hostname}`);
27
27
  const ipAddresses = getIpAddresses();
package/src/copy.d.ts CHANGED
@@ -1,5 +1,10 @@
1
+ /**
2
+ * Callback invoked after a copy completes. May be sync or async; a returned
3
+ * promise is awaited.
4
+ */
5
+ export type CopySrcCallback = () => Promise<void> | void;
1
6
  export interface CopySrcOptions {
2
- cbFn?: () => void;
7
+ cbFn?: CopySrcCallback;
3
8
  from: string;
4
9
  to: string;
5
10
  watch?: boolean;
package/src/copy.js CHANGED
@@ -1,11 +1,11 @@
1
- import chokidar from 'chokidar';
1
+ import { watch as chokidarWatch } from 'chokidar';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { onKillSignal } from './cp.js';
5
5
  import { cleanDir, copyDir } from './fs.js';
6
- import { buildLog } from './run.js';
6
+ import { buildLog, errorMessage } from './run.js';
7
7
  let timer = null;
8
- async function throttledCallback(cbFn) {
8
+ function throttledCallback(cbFn) {
9
9
  if (!cbFn || typeof cbFn !== 'function') {
10
10
  return;
11
11
  }
@@ -15,7 +15,16 @@ async function throttledCallback(cbFn) {
15
15
  }
16
16
  timer = setTimeout(() => {
17
17
  timer = null;
18
- cbFn();
18
+ // the callback may be async, so surface a rejection instead of leaving it
19
+ // floating on the timer
20
+ void (async () => {
21
+ try {
22
+ await cbFn();
23
+ }
24
+ catch (e) {
25
+ buildLog(`Error in copySrc callback: ${errorMessage(e)}`);
26
+ }
27
+ })();
19
28
  }, 200);
20
29
  }
21
30
  export async function copySrc({ cbFn, from, to, watch = process.argv.includes('--watch'), }) {
@@ -23,10 +32,13 @@ export async function copySrc({ cbFn, from, to, watch = process.argv.includes('-
23
32
  if (cbFn)
24
33
  await cbFn();
25
34
  if (watch) {
26
- const watcher = chokidar.watch([path.join(from, '/**/*')], {
35
+ // chokidar v4 removed glob support; watch the directory recursively instead
36
+ const watcher = chokidarWatch(from, {
27
37
  ignoreInitial: true,
28
38
  });
29
- watcher.on('all', async (event, filePath) => {
39
+ // chokidar's typings expect a void-returning listener, so the async work
40
+ // is wrapped and its rejection handled rather than left floating
41
+ const handleEvent = async (event, filePath) => {
30
42
  const start = new Date();
31
43
  const src = path.relative(from, filePath);
32
44
  const dest = path.join(to, src);
@@ -38,7 +50,9 @@ export async function copySrc({ cbFn, from, to, watch = process.argv.includes('-
38
50
  break;
39
51
  case 'unlink':
40
52
  case 'unlinkDir':
41
- cleanDir(dest, { dot: true });
53
+ // must be awaited: otherwise the log below (and the throttled
54
+ // callback) can fire before the delete actually completes
55
+ await cleanDir(dest, { dot: true });
42
56
  break;
43
57
  default:
44
58
  return;
@@ -47,7 +61,14 @@ export async function copySrc({ cbFn, from, to, watch = process.argv.includes('-
47
61
  const time = end.getTime() - start.getTime();
48
62
  buildLog(`${event} '${dest}' after ${time} ms`, end);
49
63
  throttledCallback(cbFn);
64
+ };
65
+ watcher.on('all', (event, filePath) => {
66
+ handleEvent(event, filePath).catch((e) => {
67
+ buildLog(`Error handling ${event} for ${filePath}: ${errorMessage(e)}`);
68
+ });
69
+ });
70
+ onKillSignal(() => {
71
+ void watcher.close();
50
72
  });
51
- onKillSignal(() => watcher.close());
52
73
  }
53
74
  }
package/src/cp.js CHANGED
@@ -63,7 +63,6 @@ export async function spawn(command, args, opts) {
63
63
  p.on('close', handleExit);
64
64
  p.on('exit', handleExit);
65
65
  // only fires if we failed to spawn
66
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
66
  p.on('error', (err) => {
68
67
  // Here we have a workaround that reverses a "feature" of cross-spawn on Windows. See:
69
68
  // https://github.com/moxystudio/node-cross-spawn/blob/master/lib/enoent.js#L23
@@ -79,7 +78,7 @@ export async function spawn(command, args, opts) {
79
78
  });
80
79
  }
81
80
  catch (err) {
82
- reject(err);
81
+ reject(err instanceof Error ? err : new Error(String(err)));
83
82
  }
84
83
  });
85
84
  }
@@ -2,7 +2,7 @@ import bytes from 'bytes';
2
2
  import { throttle } from 'lodash-es';
3
3
  import { spawn } from './cp.js';
4
4
  import { parseDockerDate } from './docker.js';
5
- import { buildLog } from './run.js';
5
+ import { buildLog, errorMessage } from './run.js';
6
6
  // calling this function in rapid succession can lead to errors.
7
7
  // prefer throttled version
8
8
  export async function unthrottledDockerContainerLs({ all, filter, } = {}) {
@@ -31,7 +31,7 @@ export async function unthrottledDockerContainerLs({ all, filter, } = {}) {
31
31
  return false;
32
32
  }
33
33
  },
34
- exited: /^Exited/.test(status),
34
+ exited: status.startsWith('Exited'),
35
35
  id,
36
36
  image,
37
37
  inspect: async () => (await dockerContainerInspect(id))?.[0],
@@ -50,7 +50,7 @@ export async function unthrottledDockerContainerLs({ all, filter, } = {}) {
50
50
  names,
51
51
  networks: Networks.split(',').map((n) => n.trim()),
52
52
  ports: Ports.split(',').map((p) => {
53
- const [, iface, src, dest] = p.match(/^([^:]+):(\d+)-\\u003e(\d+)/i) || [];
53
+ const [, iface, src, dest] = /^([^:]+):(\d+)-\\u003e(\d+)/i.exec(p) || [];
54
54
  return {
55
55
  dest: parseInt(dest, 10),
56
56
  iface,
@@ -73,14 +73,14 @@ export async function unthrottledDockerContainerLs({ all, filter, } = {}) {
73
73
  log(`<${this.name}> container stopped.`);
74
74
  }
75
75
  catch (e1) {
76
- log(`Failed to stop <${this.name}> container: ${e1 instanceof Error && e1.message}`);
76
+ log(`Failed to stop <${this.name}> container: ${errorMessage(e1)}`);
77
77
  try {
78
78
  log(`Killing container: <${this.name}>...`);
79
79
  await this.kill(false);
80
80
  log(`<${this.name}> container killed.`);
81
81
  }
82
82
  catch (e2) {
83
- log(`Failed to kill <${this.name}> container: ${e2 instanceof Error && e2.message}`);
83
+ log(`Failed to kill <${this.name}> container: ${errorMessage(e2)}`);
84
84
  }
85
85
  }
86
86
  }
@@ -113,7 +113,9 @@ export async function dockerContainerStop(id, ignoreErrors = true) {
113
113
  }
114
114
  catch (e) {
115
115
  if (!ignoreErrors) {
116
- throw new Error(`Failed to stop container(s): ${e instanceof Error && e.message}`);
116
+ throw new Error(`Failed to stop container(s): ${errorMessage(e)}`, {
117
+ cause: e,
118
+ });
117
119
  }
118
120
  }
119
121
  }
@@ -124,7 +126,9 @@ export async function dockerContainerKill(id, ignoreErrors = true) {
124
126
  }
125
127
  catch (e) {
126
128
  if (!ignoreErrors) {
127
- throw new Error(`Failed to kill container(s): ${e instanceof Error && e.message}`);
129
+ throw new Error(`Failed to kill container(s): ${errorMessage(e)}`, {
130
+ cause: e,
131
+ });
128
132
  }
129
133
  }
130
134
  }
@@ -134,7 +138,7 @@ export async function dockerTryStopContainer(id, name = '') {
134
138
  await dockerContainerStop(id, false);
135
139
  }
136
140
  catch (e) {
137
- buildLog(`Failed to stop ${name} container: ${e instanceof Error && e.message}`);
141
+ buildLog(`Failed to stop ${name} container: ${errorMessage(e)}`);
138
142
  }
139
143
  }
140
144
  }
@@ -145,10 +149,12 @@ export async function dockerContainerRm(id, ignoreErrors = true) {
145
149
  }
146
150
  catch (e) {
147
151
  if (ignoreErrors) {
148
- buildLog(`Warning (ignored Error): Failed to remove container(s): ${e instanceof Error && e.message}`);
152
+ buildLog(`Warning (ignored Error): Failed to remove container(s): ${errorMessage(e)}`);
149
153
  }
150
154
  else {
151
- throw new Error(`Failed to remove container(s): ${e instanceof Error && e.message}`);
155
+ throw new Error(`Failed to remove container(s): ${errorMessage(e)}`, {
156
+ cause: e,
157
+ });
152
158
  }
153
159
  }
154
160
  }
@@ -191,7 +197,6 @@ export async function dockerContainerRunDaemon({ cmd = [], image, runArgs = [],
191
197
  let container = await dockerContainerFind(id);
192
198
  let attempts = 0;
193
199
  // containers don't always show up in the list right away
194
- /* eslint-disable no-await-in-loop */
195
200
  while (!container && attempts < waitAttempts) {
196
201
  await new Promise((resolve) => {
197
202
  setTimeout(resolve, waitDuration);
@@ -199,7 +204,6 @@ export async function dockerContainerRunDaemon({ cmd = [], image, runArgs = [],
199
204
  container = await dockerContainerFind(id);
200
205
  attempts += 1;
201
206
  }
202
- /* eslint-enable no-await-in-loop */
203
207
  if (!container) {
204
208
  throw new Error(`Failed to find newly created container: ${id}`);
205
209
  }
@@ -221,27 +225,34 @@ export const dockerContainerWaitForStart = async (container, testFn, timeoutMs =
221
225
  curIndicator = (curIndicator + 1) % indicators.length;
222
226
  const { name } = container;
223
227
  buildLog(`Waiting for ${name} to fully start... ${indicator}`);
224
- await new Promise((resolve, reject) => {
225
- const check = (tries = 0) => {
226
- process.stdout.write(indicator);
227
- setTimeout(async () => {
228
- try {
229
- if (!(await container.isRunning())) {
230
- return reject(new Error(`The '${name}' container is no longer running.`));
231
- }
232
- if (await testFn(container)) {
233
- return resolve();
234
- }
235
- }
236
- catch {
237
- // ignore
238
- }
239
- return tries < maxAttempts
240
- ? check(tries + 1)
241
- : reject(new Error(`Timeout waiting for '${name}' container to start.`));
242
- }, timeoutMs);
243
- };
244
- check();
245
- });
246
- process.stdout.write('\n');
228
+ for (let tries = 0; tries <= maxAttempts; tries += 1) {
229
+ process.stdout.write(indicator);
230
+ await new Promise((resolve) => {
231
+ setTimeout(resolve, timeoutMs);
232
+ });
233
+ // A container that has definitively gone away will never become ready, so
234
+ // that case is terminal. An error from either check just means the state is
235
+ // currently unknown, so keep polling until maxAttempts is exhausted.
236
+ let gone = false;
237
+ let ready = false;
238
+ try {
239
+ if (await container.isRunning()) {
240
+ ready = await testFn(container);
241
+ }
242
+ else {
243
+ gone = true;
244
+ }
245
+ }
246
+ catch {
247
+ // state unknown; keep polling
248
+ }
249
+ if (gone) {
250
+ throw new Error(`The '${name}' container is no longer running.`);
251
+ }
252
+ if (ready) {
253
+ process.stdout.write('\n');
254
+ return;
255
+ }
256
+ }
257
+ throw new Error(`Timeout waiting for '${name}' container to start.`);
247
258
  };
package/src/docker.js CHANGED
@@ -11,7 +11,7 @@ import { cmdExists, isArm, isMac } from './env.js';
11
11
  import { downloadFile } from './fetch.js';
12
12
  import { mountDmg, unmountDmg } from './macos.js';
13
13
  import { getCfg, getPkgName, getPkgScope } from './pkg.js';
14
- import { buildLog } from './run.js';
14
+ import { buildLog, errorMessage } from './run.js';
15
15
  import { getMainBranch, getVersion } from './version.js';
16
16
  export function getDockerConfig() {
17
17
  return getCfg().docker || {};
@@ -51,12 +51,7 @@ export async function dockerTagVersion(imageId, repo = getDockerRepo()) {
51
51
  if (isRelease) {
52
52
  await dockerTag(imageId, prerelease
53
53
  ? [npm, `latest-${prerelease.split('.')[0]}`]
54
- : [
55
- 'latest',
56
- `${major}`,
57
- `${major}.${minor}`,
58
- `${major}.${minor}.${patch}`,
59
- ], repo);
54
+ : ['latest', major, `${major}.${minor}`, `${major}.${minor}.${patch}`], repo);
60
55
  }
61
56
  else if (branch === getMainBranch()) {
62
57
  await dockerTag(imageId, [`latest-${branch}`], repo);
@@ -77,8 +72,7 @@ export async function dockerImages(repo = getDockerRepo(), filter = null) {
77
72
  tag,
78
73
  };
79
74
  })
80
- .filter((m) => (m && m.id) != null &&
81
- (typeof filter === 'function' ? filter(m) : true));
75
+ .filter((m) => m?.id != null && (typeof filter === 'function' ? filter(m) : true));
82
76
  }
83
77
  export async function getDockerId(tag = 'latest', repo = null) {
84
78
  return (await dockerImages(repo, (m) => m.tag === tag)).map((m) => m.id)[0];
@@ -106,7 +100,7 @@ export async function dockerPush(tags = ['latest'], repo = getDockerRepo()) {
106
100
  }
107
101
  export async function dockerPull({ image, offline = process.argv.includes('--offline'), testUrl, }) {
108
102
  if (offline || !(await isReachable(testUrl || 'https://hub.docker.com'))) {
109
- if (offline === false) {
103
+ if (!offline) {
110
104
  throw new Error('Offline, cannot docker pull');
111
105
  }
112
106
  buildLog('It looks like you are offline, skipping docker pull');
@@ -122,7 +116,7 @@ export async function dockerRmi(images = [], ignoreErrors = true) {
122
116
  await prev;
123
117
  await spawn('docker', ['rmi', image], { stdio: 'inherit' }).catch((err) => {
124
118
  if (ignoreErrors) {
125
- buildLog(`Warning (ignored Error): Failed to rm image(s): ${err.message}`);
119
+ buildLog(`Warning (ignored Error): Failed to rm image(s): ${errorMessage(err)}`);
126
120
  }
127
121
  else {
128
122
  throw err;
@@ -169,7 +163,9 @@ export async function copyFilesFromDockerImage({ filePaths, ignoreErrors, imageI
169
163
  };
170
164
  const copyErrors = [];
171
165
  try {
172
- filePaths.forEach(async (fp) => {
166
+ // must be awaited: with forEach the copies were never waited on, so the
167
+ // container was killed mid-copy and copyErrors was always empty below
168
+ await Promise.all(filePaths.map(async (fp) => {
173
169
  // Try to copy from the Debug Folder
174
170
  try {
175
171
  await spawn('docker', ['cp', `${container.id}:${fp.from}`, fp.to], spawnOptions);
@@ -179,7 +175,7 @@ export async function copyFilesFromDockerImage({ filePaths, ignoreErrors, imageI
179
175
  copyErrors.push(error);
180
176
  buildLog(`Error copying file ${fp.from}`);
181
177
  }
182
- });
178
+ }));
183
179
  }
184
180
  finally {
185
181
  try {
@@ -1,9 +1,9 @@
1
1
  export interface DockerNetwork {
2
- IPv6: boolean;
3
2
  created: Date;
4
3
  driver: string;
5
4
  exists: () => Promise<boolean>;
6
5
  id: string;
6
+ IPv6: boolean;
7
7
  labels: string[];
8
8
  name: string;
9
9
  rm: () => Promise<void>;
@@ -12,11 +12,11 @@ export async function unthrottledDockerNetworkLs({ filter, } = {}) {
12
12
  .map((line) => {
13
13
  const { CreatedAt, Driver: driver, ID: id, IPv6, Labels, Name: name, Scope: scope, } = JSON.parse(line.trim());
14
14
  return {
15
- IPv6: IPv6 === 'true',
16
15
  created: parseDockerDate(CreatedAt),
17
16
  driver,
18
17
  exists: async () => !!(await dockerNetworkFind(id)),
19
18
  id,
19
+ IPv6: IPv6 === 'true',
20
20
  labels: Labels.split(',').map((l) => l.trim()),
21
21
  name,
22
22
  rm: async () => dockerNetworkRm(id),
@@ -65,9 +65,8 @@ export async function dockerNetworkRm(id) {
65
65
  */
66
66
  export async function dockerNetworkDelete(networkName) {
67
67
  let existing;
68
- // eslint-disable-next-line no-cond-assign, no-await-in-loop
68
+ // extra parens satisfy no-cond-assign's `except-parens` option
69
69
  while ((existing = await dockerNetworkFind(networkName))) {
70
- // eslint-disable-next-line no-await-in-loop
71
70
  await existing.rm();
72
71
  }
73
72
  }
@@ -1,5 +1,6 @@
1
1
  import { throttle } from 'lodash-es';
2
2
  import { spawn } from './cp.js';
3
+ import { errorMessage } from './run.js';
3
4
  // calling this function in rapid succession can lead to errors.
4
5
  // prefer throttled version
5
6
  export async function unthrottledDockerVolumeLs({ filter, } = {}) {
@@ -49,7 +50,9 @@ export async function dockerVolumeRm(id, ignoreErrors = true) {
49
50
  }
50
51
  catch (e) {
51
52
  if (!ignoreErrors) {
52
- throw new Error(`Failed to remove volume(s): ${e instanceof Error && e.message}`);
53
+ throw new Error(`Failed to remove volume(s): ${errorMessage(e)}`, {
54
+ cause: e,
55
+ });
53
56
  }
54
57
  }
55
58
  }
@@ -5,6 +5,10 @@ import { dockerImages, dockerRmi, getDockerRepo, getDockerTags, getUntaggedDocke
5
5
  import { buildLog } from './run.js';
6
6
  // flattens arrays of promises and/or promises for arrays
7
7
  // into a single array of (awaited) values
8
+ // The recursive shape here defeats both the type checker and the type-aware
9
+ // await rules: `input` may itself be an array of promises, so the awaits are
10
+ // load-bearing even where the static type looks non-thenable.
11
+ /* eslint-disable @typescript-eslint/await-thenable */
8
12
  async function flatten(input) {
9
13
  const result = await input;
10
14
  return Array.isArray(result)
@@ -40,7 +44,7 @@ export const dockerContainerCleanup = async ({ dryRun = process.argv.includes('-
40
44
  */
41
45
  export const dockerImageCleanup = async ({ buildImageIds = [], dryRun = process.argv.includes('--dry-run'), images = [], purgeAll = process.argv.includes('--purge-all'), purgeOld = !process.argv.includes('--keep-old'), repos = [getDockerRepo()], } = {}) => {
42
46
  // only match images without a :latest tag
43
- const filterLatest = (m) => purgeAll || !m.tag.match(/^latest(-\w+)?$/);
47
+ const filterLatest = (m) => purgeAll || !/^latest(-\w+)?$/.exec(m.tag);
44
48
  // only match images more than an hour old, AND without a :latest* tag
45
49
  const filterOld = (m) => purgeAll ||
46
50
  (filterLatest(m) && moment().subtract(1, 'h').isAfter(m.created));
@@ -1,13 +1,13 @@
1
1
  import { DockerContainer } from './docker.container.js';
2
2
  import { DockerNetwork, DockerNetworkCreateOptions } from './docker.network.js';
3
3
  import { DockerVolume, DockerVolumeCreateOptions } from './docker.volume.js';
4
- export type DockerComposeResourceTracker = {
4
+ export interface DockerComposeResourceTracker {
5
5
  cleaning: null | Promise<void>;
6
6
  containers: Map<string, DockerContainer>;
7
7
  networks: Map<string, DockerNetwork>;
8
8
  services: Map<string, DockerComposeService>;
9
9
  volumes: Map<string, DockerVolume>;
10
- };
10
+ }
11
11
  export declare const dockerComposeCreateResourceTracker: () => DockerComposeResourceTracker;
12
12
  type DockerComposeEnvDef = string[] | Record<string, string>;
13
13
  type DockerComposeULimitDef = number | {
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import getPort from 'get-port';
3
- import yaml from 'js-yaml';
3
+ import { load as yamlLoad } from 'js-yaml';
4
4
  import path from 'path';
5
5
  import { dockerContainerFind, dockerContainerRunDaemon, } from './docker.container.js';
6
6
  import { dockerPull } from './docker.js';
@@ -21,7 +21,7 @@ const defaultTracker = dockerComposeCreateResourceTracker();
21
21
  * Parse a docker-compose.yml file into a pojo representation
22
22
  * @param {string} filePath Path to the docker-compose.yml file
23
23
  */
24
- export const dockerComposeParse = async (filePath = './docker-compose.yml') => yaml.load(await fs.readFile(filePath, 'utf8'));
24
+ export const dockerComposeParse = async (filePath = './docker-compose.yml') => yamlLoad(await fs.readFile(filePath, 'utf8'));
25
25
  let dc = null;
26
26
  /**
27
27
  * Get the main docker-compose.yml from the root of the repo
@@ -153,7 +153,6 @@ export class DockerComposeService {
153
153
  * @param {number} max maximum number of names to try
154
154
  */
155
155
  async getNames(avoidConflicts = false, max = 100) {
156
- /* eslint-disable no-await-in-loop */
157
156
  for (let i = 0; i < max; i += 1) {
158
157
  const suffix = i ? `-${i}` : '';
159
158
  const testName = `${this.defaultContainerName}${suffix}`;
@@ -166,7 +165,6 @@ export class DockerComposeService {
166
165
  };
167
166
  }
168
167
  }
169
- /* eslint-enable no-await-in-loop */
170
168
  throw new Error(`Failed to find available container name for ${this.defaultContainerName}`);
171
169
  }
172
170
  // Get (available) port mappings
package/src/env.js CHANGED
@@ -8,7 +8,6 @@ const cacheBool = (key, fn) => {
8
8
  if (!boolCache.has(key)) {
9
9
  boolCache.set(key, fn());
10
10
  }
11
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
12
11
  return boolCache.get(key);
13
12
  };
14
13
  export const isMac = () => cacheBool('isMac', () => os.platform() === 'darwin');
@@ -22,7 +21,6 @@ const cacheString = (key, fn) => {
22
21
  if (!stringCache.has(key)) {
23
22
  stringCache.set(key, fn());
24
23
  }
25
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
26
24
  return stringCache.get(key);
27
25
  };
28
26
  export const shellType = () => cacheString('shellType', () => os.userInfo().shell?.split('/').pop() || null);
package/src/fs.js CHANGED
@@ -35,7 +35,7 @@ export const cleanDir = async (pattern, globOptions) => rimraf(pattern, { glob:
35
35
  export const producedPathExists = async (producer) => {
36
36
  try {
37
37
  const result = (await producer()).trim();
38
- return !!result && fs.pathExists(result);
38
+ return !!result && (await fs.pathExists(result));
39
39
  }
40
40
  catch {
41
41
  return false;
package/src/git.d.ts CHANGED
@@ -21,10 +21,10 @@ export declare function gitTagsAtCommit(commitish?: string): Promise<string[]>;
21
21
  * Useful for verifying that a release tag lives on the main branch.
22
22
  */
23
23
  export declare function gitIsAncestor(ancestorCommitish: string, descendantCommitish: string): Promise<boolean>;
24
- type GitInfo = {
24
+ interface GitInfo {
25
25
  branch: null | string;
26
26
  revision: string;
27
- };
27
+ }
28
28
  export declare function gitInfo(noCache?: boolean): Promise<GitInfo>;
29
29
  export declare const ensureGitLfsInstalled: () => Promise<void>;
30
30
  export declare const gitConfigRead: (key: string, global?: boolean) => Promise<string>;
package/src/npm.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { SpawnOptions, SpawnResult } from './cp.js';
2
- export type NpmCreds = {
2
+ export interface NpmCreds {
3
3
  email?: string;
4
4
  password?: string;
5
5
  username?: string;
6
- };
7
- export type NpmConfig = {
6
+ }
7
+ export interface NpmConfig {
8
8
  access?: 'public' | 'restricted';
9
9
  dryRun?: boolean;
10
10
  name?: string;
11
11
  publish?: boolean;
12
12
  registry?: string;
13
- };
13
+ }
14
14
  export declare function getNpmConfig(): NpmConfig;
15
15
  export declare const envNpmCreds: null | NpmCreds;
16
16
  export interface NpmOptions {
package/src/orbstack.js CHANGED
@@ -67,16 +67,12 @@ export const ensureOrbStackConfigured = async (desiredConfig) => {
67
67
  };
68
68
  if (!Object.entries(desiredConfig).every(([key, value]) => effectiveConfig[key] === value)) {
69
69
  buildLog('Adjusting OrbStack config...');
70
- const newConfig = {
70
+ const defaults = defaultConfig;
71
+ // Don't write values that already match the defaults
72
+ const newConfig = Object.fromEntries(Object.entries({
71
73
  ...existingConfig,
72
74
  ...desiredConfig,
73
- };
74
- for (const [key, value] of Object.entries(defaultConfig)) {
75
- if (newConfig[key] === value) {
76
- // Don't write default values
77
- delete newConfig[key];
78
- }
79
- }
75
+ }).filter(([key, value]) => defaults[key] !== value));
80
76
  await fs.writeJson(configPath, newConfig, { spaces: 2 });
81
77
  changed = true;
82
78
  }
package/src/pkg.js CHANGED
@@ -21,7 +21,7 @@ export function getPkgName(includeScope = false) {
21
21
  if (includeScope) {
22
22
  return pkg.name;
23
23
  }
24
- const [, , shortName] = pkg.name.match(pkgNameRegex) || [];
24
+ const [, , shortName] = pkgNameRegex.exec(pkg.name) || [];
25
25
  if (!shortName) {
26
26
  throw new Error('Package name in package.json has invalid format.');
27
27
  }
@@ -29,11 +29,11 @@ export function getPkgName(includeScope = false) {
29
29
  }
30
30
  export function getPkgScope() {
31
31
  const pkgName = getPkgName(true);
32
- return pkgName.match(pkgNameRegex)?.[1] || null;
32
+ return pkgNameRegex.exec(pkgName)?.[1] || null;
33
33
  }
34
34
  export function getPkgSafeName() {
35
35
  const pkgName = getPkgName(true);
36
- const match = pkgName.match(pkgNameRegex);
36
+ const match = pkgNameRegex.exec(pkgName);
37
37
  if (!match) {
38
38
  return null;
39
39
  }
package/src/run.d.ts CHANGED
@@ -1,5 +1,30 @@
1
1
  export declare function format(time?: Date): string;
2
- export declare function buildLog(msg: string, time?: Date): void;
2
+ /**
3
+ * Extract a human readable message from an unknown caught value.
4
+ *
5
+ * `catch` and promise rejections are typed `unknown`, and anything can be
6
+ * thrown. Interpolating such a value directly is unsafe: the common
7
+ * `${e instanceof Error && e.message}` shorthand renders the string "false"
8
+ * whenever the thrown value is not an Error, hiding the real cause.
9
+ */
10
+ export declare function errorMessage(err: unknown): string;
11
+ /**
12
+ * Where `buildLog` output should be written. Either the name of one of the
13
+ * standard streams, any writable stream, or a function that receives the
14
+ * fully formatted message.
15
+ */
16
+ export type BuildLogStream = 'stderr' | 'stdout' | NodeJS.WritableStream | ((msg: string) => void);
17
+ export interface BuildLogOptions {
18
+ /** Override the destination for this message only. */
19
+ stream?: BuildLogStream;
20
+ /** Timestamp to prefix the message with (defaults to now). */
21
+ time?: Date;
22
+ }
23
+ /** Change the destination used by `buildLog` when none is specified. */
24
+ export declare function setBuildLogStream(stream: BuildLogStream): void;
25
+ /** The destination currently used by `buildLog` when none is specified. */
26
+ export declare function getBuildLogStream(): BuildLogStream;
27
+ export declare function buildLog(msg: string, timeOrOptions?: BuildLogOptions | Date): void;
3
28
  export type RunnableModule<Args extends string[], Result> = ((...args: Args) => Promise<Result>) | {
4
29
  default: (...args: Args) => Promise<Result>;
5
30
  };
package/src/run.js CHANGED
@@ -1,16 +1,67 @@
1
1
  export function format(time = new Date()) {
2
2
  return time.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
3
3
  }
4
+ /**
5
+ * Extract a human readable message from an unknown caught value.
6
+ *
7
+ * `catch` and promise rejections are typed `unknown`, and anything can be
8
+ * thrown. Interpolating such a value directly is unsafe: the common
9
+ * `${e instanceof Error && e.message}` shorthand renders the string "false"
10
+ * whenever the thrown value is not an Error, hiding the real cause.
11
+ */
12
+ export function errorMessage(err) {
13
+ if (err instanceof Error) {
14
+ return err.message;
15
+ }
16
+ if (typeof err === 'string') {
17
+ return err;
18
+ }
19
+ try {
20
+ return JSON.stringify(err) ?? String(err);
21
+ }
22
+ catch {
23
+ return String(err);
24
+ }
25
+ }
4
26
  const silenceLogs = process.argv.includes('--silence-buildLog');
5
- export function buildLog(msg, time) {
27
+ // stderr by default, so that stdout can be used for machine readable output.
28
+ let defaultStream = process.argv.includes('--buildLog-stdout')
29
+ ? 'stdout'
30
+ : 'stderr';
31
+ /** Change the destination used by `buildLog` when none is specified. */
32
+ export function setBuildLogStream(stream) {
33
+ defaultStream = stream;
34
+ }
35
+ /** The destination currently used by `buildLog` when none is specified. */
36
+ export function getBuildLogStream() {
37
+ return defaultStream;
38
+ }
39
+ function writeLog(msg, stream) {
40
+ if (typeof stream === 'function') {
41
+ stream(msg);
42
+ }
43
+ else if (stream === 'stdout') {
44
+ process.stdout.write(`${msg}\n`);
45
+ }
46
+ else if (stream === 'stderr') {
47
+ process.stderr.write(`${msg}\n`);
48
+ }
49
+ else {
50
+ stream.write(`${msg}\n`);
51
+ }
52
+ }
53
+ export function buildLog(msg, timeOrOptions) {
6
54
  if (silenceLogs) {
7
55
  return;
8
56
  }
9
- console.info(`[${format(time || new Date())}] ${msg}`);
57
+ const { stream = defaultStream, time } = timeOrOptions instanceof Date
58
+ ? { time: timeOrOptions }
59
+ : (timeOrOptions ?? {});
60
+ writeLog(`[${format(time || new Date())}] ${msg}`, stream);
10
61
  }
11
62
  export async function run(fn, ...options) {
12
63
  if (fn == null) {
13
- throw new Error(`Invalid argument passed to run(${fn})`);
64
+ throw new Error(`Invalid argument passed to run(${String(fn)})`);
14
65
  }
15
66
  const task = typeof fn === 'function' ? fn : fn.default;
16
67
  if (typeof task !== 'function') {
@@ -38,7 +89,7 @@ export async function runCli({ argv = process.argv, defaultAction = 'build', pas
38
89
  ? argv.slice(3)
39
90
  : [];
40
91
  return run(module, ...args).catch((err) => {
41
- console.error((err && err.stack) || err);
92
+ console.error(err instanceof Error ? err.stack : err);
42
93
  process.exit(1);
43
94
  });
44
95
  }
package/src/tgz.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Readable } from 'stream';
2
- import { TarOptionsWithAliasesNoFile } from 'tar/dist/commonjs/options.js';
3
- export type ArtifactInfo = {
2
+ import { type TarOptionsWithAliasesAsyncNoFile } from 'tar';
3
+ export interface ArtifactInfo {
4
4
  contentType?: string;
5
5
  md5: string;
6
6
  sha1: string;
7
7
  sha512: string;
8
8
  size: number;
9
- };
9
+ }
10
10
  export declare const generateFileHash: (filePath: string, type: "md5" | "sha1" | "sha512") => Promise<string>;
11
11
  export declare function getArtifactInfo(artifact: string | Pick<Readable, 'pipe'>): Promise<ArtifactInfo>;
12
- export declare function tgzDir(srcDir: string, outPath: string, options?: TarOptionsWithAliasesNoFile): Promise<ArtifactInfo>;
12
+ export declare function tgzDir(srcDir: string, outPath: string, options?: TarOptionsWithAliasesAsyncNoFile): Promise<ArtifactInfo>;
package/src/tgz.js CHANGED
@@ -13,16 +13,24 @@ const generateHash = async (stream, type) => new Promise((resolve, reject) => {
13
13
  .on('end', function onEnd() {
14
14
  resolve(this.read());
15
15
  })
16
- .on('error', (err) => reject(err));
16
+ .on('error', (err) => {
17
+ reject(err);
18
+ });
17
19
  });
18
20
  export const generateFileHash = async (filePath, type) => generateHash(fs.createReadStream(filePath), type);
19
21
  const countBytes = async (stream) => new Promise((resolve, reject) => {
20
22
  const counter = new StreamCounter();
21
23
  stream
22
24
  .pipe(counter)
23
- .on('finish', () => resolve(counter.bytes))
24
- .on('end', () => resolve(counter.bytes))
25
- .on('error', (err) => reject(err));
25
+ .on('finish', () => {
26
+ resolve(counter.bytes);
27
+ })
28
+ .on('end', () => {
29
+ resolve(counter.bytes);
30
+ })
31
+ .on('error', (err) => {
32
+ reject(err);
33
+ });
26
34
  });
27
35
  const writeStreamToFile = async (stream, filePath) => new Promise((resolve, reject) => {
28
36
  stream
@@ -31,7 +39,9 @@ const writeStreamToFile = async (stream, filePath) => new Promise((resolve, reje
31
39
  buildLog(`Successfully wrote file: ${filePath}`);
32
40
  resolve();
33
41
  })
34
- .on('error', (err) => reject(err));
42
+ .on('error', (err) => {
43
+ reject(err);
44
+ });
35
45
  });
36
46
  // Take the given file (path or stream) and compute metadata for it.
37
47
  // md5 + sha hashes, and size in bytes
@@ -58,7 +68,7 @@ export async function tgzDir(srcDir, outPath, options) {
58
68
  // write the file to disk (outPath)
59
69
  const [info] = await Promise.all([
60
70
  getArtifactInfo(tgzStream),
61
- writeStreamToFile(tgzStream, `${outPath}`),
71
+ writeStreamToFile(tgzStream, outPath),
62
72
  ]);
63
73
  return {
64
74
  ...info,
package/src/version.d.ts CHANGED
@@ -11,10 +11,10 @@ declare enum RepoType {
11
11
  unknown = "unknown"
12
12
  }
13
13
  export declare function getRepoType(): Promise<RepoType>;
14
- type RepoInfo = {
14
+ interface RepoInfo {
15
15
  branch: null | string;
16
16
  revision: string;
17
- };
17
+ }
18
18
  export declare function getRepoInfo(): Promise<RepoInfo>;
19
19
  export interface SemverTag {
20
20
  major: string;
package/src/version.js CHANGED
@@ -4,7 +4,7 @@ import { getCfg, getPkg } from './pkg.js';
4
4
  import { buildLog } from './run.js';
5
5
  export function getBuild() {
6
6
  const arg = process.argv.find((el) => /^--buildNum=\d+$/.test(el));
7
- return (arg && arg.substring(11)) || process.env.BUILD_NUMBER || '0';
7
+ return arg?.substring(11) || process.env.BUILD_NUMBER || '0';
8
8
  }
9
9
  /**
10
10
  * The one long-lived branch of the repository. All work happens on short-lived
@@ -21,9 +21,11 @@ var RepoType;
21
21
  RepoType["hg"] = "hg";
22
22
  RepoType["unknown"] = "unknown";
23
23
  })(RepoType || (RepoType = {}));
24
- export async function getRepoType() {
24
+ // Returns a promise (rather than being `async`) because the body is synchronous
25
+ // but the exported signature is awaited by callers and must stay thenable.
26
+ export function getRepoType() {
25
27
  const { repoType } = getCfg();
26
- return (repoType ||
28
+ return Promise.resolve(repoType ||
27
29
  (fs.existsSync('./.git')
28
30
  ? RepoType.git
29
31
  : fs.existsSync('./.hg')
@@ -47,7 +49,7 @@ const semverTagRegex = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-
47
49
  * semver version tag (an optional leading `v` is allowed).
48
50
  */
49
51
  export function parseSemverTag(tag) {
50
- const [, major, minor, patch, prerelease] = tag.match(semverTagRegex) || [];
52
+ const [, major, minor, patch, prerelease] = semverTagRegex.exec(tag) || [];
51
53
  if (!major || !minor || !patch) {
52
54
  return null;
53
55
  }
@@ -113,7 +115,6 @@ export async function assertReleaseCommitIsOnMainBranch(mainBranch = getMainBran
113
115
  const { revision } = await getRepoInfo();
114
116
  const candidateRefs = [mainBranch, `origin/${mainBranch}`];
115
117
  for (const ref of candidateRefs) {
116
- // eslint-disable-next-line no-await-in-loop
117
118
  if (await gitIsAncestor(revision, ref)) {
118
119
  return;
119
120
  }