@augment-vir/node 32.2.0 → 32.2.2

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.
@@ -30,6 +30,10 @@ export declare function resetDirContents(rootDir: string, contents: Readonly<Dir
30
30
  /**
31
31
  * Write {@link DirContents} to a directory.
32
32
  *
33
+ * Keys are always relative to `rootDir`. A key that traverses above it (such as `'../escaped.txt'`)
34
+ * throws instead of writing, so contents built from an untrusted source cannot reach outside the
35
+ * directory they were meant for.
36
+ *
33
37
  * @category Node : File
34
38
  * @category Package : @augment-vir/node
35
39
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -2,6 +2,7 @@ import { check } from '@augment-vir/assert';
2
2
  import { getObjectTypedEntries } from '@augment-vir/common';
3
3
  import { readdir, readFile, rm, stat } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { doesPathContain } from '../path/contains.js';
5
6
  import { writeFileAndDir } from './write.js';
6
7
  /**
7
8
  * Read all contents within a directory and store them in an object. Optionally recursive.
@@ -26,7 +27,7 @@ export async function readAllDirContents(dir, { recursive = false, excludeList,
26
27
  }
27
28
  const isFile = (await stat(filePath)).isFile();
28
29
  const contents = isFile
29
- ? (await readFile(filePath)).toString()
30
+ ? await readFile(filePath, 'utf8')
30
31
  : recursive
31
32
  ? await readAllDirContents(filePath, {
32
33
  recursive,
@@ -63,6 +64,10 @@ export async function resetDirContents(rootDir, contents) {
63
64
  /**
64
65
  * Write {@link DirContents} to a directory.
65
66
  *
67
+ * Keys are always relative to `rootDir`. A key that traverses above it (such as `'../escaped.txt'`)
68
+ * throws instead of writing, so contents built from an untrusted source cannot reach outside the
69
+ * directory they were meant for.
70
+ *
66
71
  * @category Node : File
67
72
  * @category Package : @augment-vir/node
68
73
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -70,6 +75,12 @@ export async function resetDirContents(rootDir, contents) {
70
75
  export async function writeDirContents(rootDir, contents) {
71
76
  await Promise.all(getObjectTypedEntries(contents).map(async ([relativePath, content,]) => {
72
77
  const fullPath = join(rootDir, relativePath);
78
+ if (!doesPathContain({
79
+ potentialParentPath: rootDir,
80
+ potentialChildPath: fullPath,
81
+ })) {
82
+ throw new Error(`Cannot write '${relativePath}': it resolves outside of '${rootDir}'.`);
83
+ }
73
84
  if (check.isString(content)) {
74
85
  await writeFileAndDir(fullPath, content);
75
86
  }
@@ -1,31 +1,26 @@
1
1
  import { assert, check } from '@augment-vir/assert';
2
- import { awaitedBlockingMap, getObjectTypedKeys, log, typedObjectFromEntries, } from '@augment-vir/common';
2
+ import { awaitedBlockingMap, getObjectTypedKeys, log, shellQuote, typedObjectFromEntries, } from '@augment-vir/common';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { lstat, readdir, stat } from 'node:fs/promises';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { isOperatingSystem, OperatingSystem } from '../os/operating-system.js';
7
7
  const grepBinPath = '/usr/bin/grep';
8
- function shellQuote(input) {
9
- return [
10
- "'",
11
- input.replaceAll("'", String.raw `'\''`),
12
- "'",
13
- ].join('');
14
- }
15
8
  function recursiveFlag({ recursive, followSymLinks, }) {
16
9
  if (!recursive) {
17
10
  return '';
18
11
  }
19
- else if (!followSymLinks) {
12
+ else if (followSymLinks) {
13
+ /**
14
+ * BSD `grep` (macOS) requires `-S` to follow symlinks while recursing, but GNU `grep`
15
+ * (Linux) has no `-S` flag and instead follows all symlinks with `-R`. Only one of these
16
+ * branches can run on a given operating system.
17
+ */
18
+ /* node:coverage ignore next */
19
+ return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
20
+ }
21
+ else {
20
22
  return '--recursive';
21
23
  }
22
- /**
23
- * BSD `grep` (macOS) requires `-S` to follow symlinks while recursing, but GNU `grep` (Linux)
24
- * has no `-S` flag and instead follows all symlinks with `-R`. Only one of these branches can
25
- * run on a given operating system.
26
- */
27
- /* node:coverage ignore next */
28
- return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
29
24
  }
30
25
  function isValidMaxCount(maxCount) {
31
26
  return (maxCount == undefined ||
@@ -156,12 +151,14 @@ function formatGrepCommand(args) {
156
151
  const operandDelimiterIndex = args.indexOf('--');
157
152
  return [
158
153
  'grep',
159
- ...args.map((arg, index) => shellQuote(redactGrepArgForLogging({
160
- arg,
161
- index,
162
- operandDelimiterIndex,
163
- previousArg: args[index - 1],
164
- }))),
154
+ ...args.map((arg, index) => {
155
+ return shellQuote(redactGrepArgForLogging({
156
+ arg,
157
+ index,
158
+ operandDelimiterIndex,
159
+ previousArg: args[index - 1],
160
+ }));
161
+ }),
165
162
  ].join(' ');
166
163
  }
167
164
  function replaceGrepCountOutputArg({ args, replacement, }) {
@@ -186,7 +183,9 @@ function extractOptionalStringArray(input) {
186
183
  else if (!check.isArray(input) || !input.every(check.isString)) {
187
184
  return undefined;
188
185
  }
189
- return input.filter(check.isTruthy);
186
+ else {
187
+ return input.filter(check.isTruthy);
188
+ }
190
189
  }
191
190
  function extractString(input) {
192
191
  return check.isString(input) && input ? input : undefined;
@@ -247,18 +246,20 @@ function createSearchLocation(grepSearchLocation) {
247
246
  ],
248
247
  };
249
248
  }
250
- else if (grepSearchLocation.dirs != undefined) {
249
+ else if (grepSearchLocation.dirs == undefined) {
250
+ return dir
251
+ ? {
252
+ dirs: [
253
+ dir,
254
+ ],
255
+ }
256
+ : undefined;
257
+ }
258
+ else {
251
259
  return {
252
260
  dirs: extractStringArray(grepSearchLocation.dirs),
253
261
  };
254
262
  }
255
- return dir
256
- ? {
257
- dirs: [
258
- dir,
259
- ],
260
- }
261
- : undefined;
262
263
  }
263
264
  function resolveSearchPart({ cwd, searchPart, }) {
264
265
  return cwd && !isAbsolute(searchPart) ? resolve(cwd, searchPart) : searchPart;
@@ -322,11 +323,13 @@ async function createSearchParts({ cwd, followSymLinks, recursive, searchLocatio
322
323
  return searchLocation.dirs
323
324
  ? recursive
324
325
  ? filteredSearchParts
325
- : (await awaitedBlockingMap(filteredSearchParts, (dir) => readDirectDirSearchParts({
326
- cwd,
327
- dir,
328
- followSymLinks,
329
- }))).flat()
326
+ : (await awaitedBlockingMap(filteredSearchParts, (dir) => {
327
+ return readDirectDirSearchParts({
328
+ cwd,
329
+ dir,
330
+ followSymLinks,
331
+ });
332
+ })).flat()
330
333
  : filteredSearchParts;
331
334
  }
332
335
  function createGrepCountEntry({ countString, fileName }) {
@@ -364,7 +367,7 @@ function parseNullDelimitedGrepRecords({ stdout }) {
364
367
  }
365
368
  return records;
366
369
  }
367
- /* node:coverage ignore next 26 */
370
+ /* node:coverage ignore next 28 */
368
371
  function parseColonDelimitedGrepCountOutput(stdout) {
369
372
  return typedObjectFromEntries(stdout
370
373
  .trimEnd()
@@ -380,12 +383,14 @@ function parseColonDelimitedGrepCountOutput(stdout) {
380
383
  });
381
384
  })
382
385
  .filter(check.isTruthy)
383
- .map((entry) => [
384
- entry.key,
385
- entry.value,
386
- ]));
386
+ .map((entry) => {
387
+ return [
388
+ entry.key,
389
+ entry.value,
390
+ ];
391
+ }));
387
392
  }
388
- /* node:coverage ignore next 20 */
393
+ /* node:coverage ignore next 22 */
389
394
  function parseGrepCountOutput(stdout) {
390
395
  return stdout.includes('\0')
391
396
  ? typedObjectFromEntries(parseNullDelimitedGrepRecords({
@@ -398,10 +403,12 @@ function parseGrepCountOutput(stdout) {
398
403
  });
399
404
  })
400
405
  .filter(check.isTruthy)
401
- .map((entry) => [
402
- entry.key,
403
- entry.value,
404
- ]))
406
+ .map((entry) => {
407
+ return [
408
+ entry.key,
409
+ entry.value,
410
+ ];
411
+ }))
405
412
  : parseColonDelimitedGrepCountOutput(stdout);
406
413
  }
407
414
  /* node:coverage ignore next 7 */
@@ -478,20 +485,24 @@ async function runGrepCountFallback({ cwd, grepArgs, }) {
478
485
  });
479
486
  }))
480
487
  .filter(check.isTruthy)
481
- .map((entry) => [
482
- entry.key,
483
- entry.value,
484
- ]));
488
+ .map((entry) => {
489
+ return [
490
+ entry.key,
491
+ entry.value,
492
+ ];
493
+ }));
485
494
  }
486
495
  function parseGrepFilesOnlyOutput(stdout) {
487
496
  return typedObjectFromEntries(
488
497
  /* node:coverage ignore next */
489
498
  (stdout.includes('\0') ? stdout.split('\0') : stdout.trimEnd().split('\n'))
490
499
  .filter(check.isTruthy)
491
- .map((entry) => [
492
- entry,
493
- [],
494
- ]));
500
+ .map((entry) => {
501
+ return [
502
+ entry,
503
+ [],
504
+ ];
505
+ }));
495
506
  }
496
507
  function parseGrepNormalOutput(stdout) {
497
508
  const fileMatches = new Map();
@@ -580,10 +591,12 @@ export async function grep(grepSearchPattern, grepSearchLocation, options = {})
580
591
  ? grepOptionArrays.includeFiles.map((includeFile) => `--include=${includeFile}`)
581
592
  : []),
582
593
  grepOptions.binary ? '--binary' : '',
583
- ...searchPatterns.flatMap((searchPattern) => [
584
- '-e',
585
- searchPattern,
586
- ]),
594
+ ...searchPatterns.flatMap((searchPattern) => {
595
+ return [
596
+ '-e',
597
+ searchPattern,
598
+ ];
599
+ }),
587
600
  '--',
588
601
  ...searchParts,
589
602
  ].filter(check.isTruthy);
@@ -15,7 +15,7 @@ import { writeFileAndDir } from './write.js';
15
15
  */
16
16
  export async function readJsonFile(path) {
17
17
  try {
18
- const contents = (await readFile(path)).toString();
18
+ const contents = await readFile(path, 'utf8');
19
19
  return JSON.parse(contents);
20
20
  }
21
21
  catch {
@@ -41,5 +41,7 @@ export async function readDirRecursive(dirPath) {
41
41
  export async function readDirFilesByExtension({ dirPath, extension, extensions, }) {
42
42
  const extensionsToCheck = extensions || [extension];
43
43
  const fileNames = await readdir(dirPath);
44
- return fileNames.filter((fileName) => extensionsToCheck.some((extensionToCheck) => fileName.endsWith(extensionToCheck)));
44
+ return fileNames.filter((fileName) => {
45
+ return extensionsToCheck.some((extensionToCheck) => fileName.endsWith(extensionToCheck));
46
+ });
45
47
  }
@@ -10,7 +10,7 @@ import { readFile } from 'node:fs/promises';
10
10
  */
11
11
  export async function readFileIfExists(path) {
12
12
  if (existsSync(path)) {
13
- return (await readFile(path)).toString();
13
+ return await readFile(path, 'utf8');
14
14
  }
15
15
  else {
16
16
  return undefined;
@@ -15,7 +15,9 @@ import { findAncestor, joinFilesToDir } from '../path/ancestor.js';
15
15
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
16
16
  */
17
17
  export async function findAllPackageJsonFilePaths(startDirPath) {
18
- const packageRootDir = findAncestor(startDirPath, (dir) => existsSync(join(dir, 'package-lock.json')));
18
+ const packageRootDir = findAncestor(startDirPath, (dir) => {
19
+ return existsSync(join(dir, 'package-lock.json'));
20
+ });
19
21
  if (!packageRootDir) {
20
22
  throw new Error(`Cannot find all package.json files: failed to find any directory with a package-lock.json file. Started at '${startDirPath}'.`);
21
23
  }
@@ -16,8 +16,10 @@ export async function readPackageJson(dirPath) {
16
16
  if (!packageJson) {
17
17
  throw new TypeError(`package.json file does not exist in '${dirPath}'`);
18
18
  }
19
- else if (!check.isObject(packageJson)) {
19
+ else if (check.isObject(packageJson)) {
20
+ return packageJson;
21
+ }
22
+ else {
20
23
  throw new TypeError(`Parsing package.json file did not return an object in '${dirPath}'`);
21
24
  }
22
- return packageJson;
23
25
  }
@@ -23,8 +23,10 @@ export function doesPathContain({ potentialParentPath, potentialChildPath, optio
23
23
  /** On Windows, paths on different drives yield an absolute relative path. */
24
24
  return false;
25
25
  }
26
- /** Contained if it does not traverse up out of parent. */
27
- return relativePath !== '..' && !relativePath.startsWith('..' + sep);
26
+ else {
27
+ /** Contained if it does not traverse up out of parent. */
28
+ return relativePath !== '..' && !relativePath.startsWith('..' + sep);
29
+ }
28
30
  }
29
31
  function normalizePath(inputPath) {
30
32
  const absolutePath = normalize(resolve(inputPath));
@@ -18,6 +18,12 @@ export declare function toPosixPath(maybeWindowsPath: string): string;
18
18
  * Use this to interpolate paths into bash commands. If the given path is not a Windows path, the
19
19
  * path structure will not be modified.
20
20
  *
21
+ * Each round of bash parsing halves the backslashes, so the count here assumes _two_ rounds: a
22
+ * command string that bash parses and then hands to something which parses it again. Four
23
+ * backslashes arrive as one. For a command that bash parses only once, this over-escapes; use
24
+ * `shellQuote` from `@augment-vir/common` instead, which needs no multiplication because bash does
25
+ * no escape processing inside single quotes.
26
+ *
21
27
  * @category Path : Node
22
28
  * @category Package : @augment-vir/node
23
29
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -34,6 +34,12 @@ export function toPosixPath(maybeWindowsPath) {
34
34
  * Use this to interpolate paths into bash commands. If the given path is not a Windows path, the
35
35
  * path structure will not be modified.
36
36
  *
37
+ * Each round of bash parsing halves the backslashes, so the count here assumes _two_ rounds: a
38
+ * command string that bash parses and then hands to something which parses it again. Four
39
+ * backslashes arrive as one. For a command that bash parses only once, this over-escapes; use
40
+ * `shellQuote` from `@augment-vir/common` instead, which needs no multiplication because bash does
41
+ * no escape processing inside single quotes.
42
+ *
37
43
  * @category Path : Node
38
44
  * @category Package : @augment-vir/node
39
45
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -1,9 +1,9 @@
1
1
  /* node:coverage disable */
2
2
  /** This file cannot be tested because it calls `process.exit`. */
3
3
  import { check } from '@augment-vir/assert';
4
+ import { shellQuote } from '@augment-vir/common';
4
5
  import { dirname, extname } from 'node:path';
5
6
  import { findNpmBinPath } from '../npm/find-bin-path.js';
6
- import { interpolationSafeWindowsPath } from '../path/os-path.js';
7
7
  import { extractRelevantArgs } from './relevant-args.js';
8
8
  import { runShellCommand } from './shell.js';
9
9
  /**
@@ -44,11 +44,13 @@ export async function runCliScript({ scriptPath, cliScriptFilePath, binName, })
44
44
  binName: runner.npx,
45
45
  startPath: dirname(cliScriptFilePath),
46
46
  }) || runner.npx;
47
- const results = await runShellCommand(interpolationSafeWindowsPath([
47
+ const results = await runShellCommand([
48
48
  runnerPath,
49
49
  scriptPath,
50
50
  ...args,
51
- ].join(' ')), {
51
+ ]
52
+ .map(shellQuote)
53
+ .join(' '), {
52
54
  hookUpToConsole: true,
53
55
  });
54
56
  process.exit(results.exitCode || 0);
@@ -11,7 +11,9 @@ import { findAncestor } from '../path/ancestor.js';
11
11
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
12
12
  */
13
13
  export function readTsconfig(startingPath) {
14
- const tsconfigDirPath = findAncestor(startingPath, (ancestorPath) => existsSync(join(ancestorPath, 'tsconfig.json')));
14
+ const tsconfigDirPath = findAncestor(startingPath, (ancestorPath) => {
15
+ return existsSync(join(ancestorPath, 'tsconfig.json'));
16
+ });
15
17
  const tsconfigPath = tsconfigDirPath ? join(tsconfigDirPath, 'tsconfig.json') : undefined;
16
18
  if (!tsconfigPath) {
17
19
  return undefined;
@@ -57,7 +57,7 @@ async function updateAllPackageJsonPaths(monoRepoPath) {
57
57
  async function fixAllSrcImports(monoRepoPath) {
58
58
  const filePaths = await findFilesThatNeedImportFixes(monoRepoPath);
59
59
  await Promise.all(filePaths.map(async (filePath) => {
60
- const contents = String(await readFile(filePath));
60
+ const contents = await readFile(filePath, 'utf8');
61
61
  const fixedContents = contents.replaceAll(/(from ["'][^'"]+?)\/src\/([^'"]+)['"];/g, "$1/dist/$2';");
62
62
  await writeFile(filePath, fixedContents);
63
63
  log.faint(`Fixed imports in ${relative(monoRepoPath, filePath)}`);
@@ -15,8 +15,7 @@
15
15
  * ```
16
16
  */
17
17
  import { log } from '@augment-vir/common';
18
- import { interpolationSafeWindowsPath, runShellCommand } from '@augment-vir/node';
19
- import { readFile, rm, writeFile } from 'node:fs/promises';
18
+ import { chmod, readFile, rm, writeFile } from 'node:fs/promises';
20
19
  import { join, sep } from 'node:path/posix';
21
20
  const packagesToFix = [
22
21
  {
@@ -63,7 +62,7 @@ async function fixTsBin(packageToFix) {
63
62
  force: true,
64
63
  });
65
64
  await writeFile(binFilePath, createBinFileContents(packageToFix));
66
- await runShellCommand(`chmod +x ${interpolationSafeWindowsPath(binFilePath)}`);
65
+ await chmod(binFilePath, 0o755);
67
66
  await fixPackageJson(packageToFix);
68
67
  log.success(`Fixed ${packageToFix.packageName} bin.`);
69
68
  }
@@ -72,7 +71,7 @@ async function fixPackageJson(packageToFix) {
72
71
  return;
73
72
  }
74
73
  const packageJsonPath = join(process.cwd(), 'node_modules', packageToFix.packageName, 'package.json');
75
- const original = String(await readFile(packageJsonPath));
74
+ const original = await readFile(packageJsonPath, 'utf8');
76
75
  await writeFile(packageJsonPath, original
77
76
  .replace('"main": "dist/index.js"', '"main": "src/index.ts"')
78
77
  .replace('"module": "dist/index.js"', '"module": "src/index.ts"'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augment-vir/node",
3
- "version": "32.2.0",
3
+ "version": "32.2.2",
4
4
  "description": "A collection of augments, helpers types, functions, and classes only for Node.js (backend) JavaScript environments.",
5
5
  "keywords": [
6
6
  "augment",
@@ -38,22 +38,22 @@
38
38
  "test:update": "npm test"
39
39
  },
40
40
  "dependencies": {
41
- "@augment-vir/assert": "^32.2.0",
42
- "@augment-vir/common": "^32.2.0",
43
- "@date-vir/duration": "^8.6.1",
44
- "ansi-styles": "^6.2.3",
41
+ "@augment-vir/assert": "^32.2.2",
42
+ "@augment-vir/common": "^32.2.2",
43
+ "@date-vir/duration": "^9.0.0",
44
+ "ansi-styles": "^7.0.0",
45
45
  "sanitize-filename": "^1.6.4",
46
46
  "terminate": "^2.8.0",
47
- "tsx": "^4.22.4",
48
- "typed-event-target": "^4.3.1"
47
+ "tsx": "^4.23.11",
48
+ "typed-event-target": "^4.3.3"
49
49
  },
50
50
  "devDependencies": {
51
- "@augment-vir/test": "^32.2.0",
52
- "@types/node": "^26.0.1",
53
- "@web/dev-server-esbuild": "^1.0.5",
54
- "@web/test-runner": "^0.20.2",
55
- "@web/test-runner-playwright": "^0.11.1",
56
- "c8": "^11.0.0",
51
+ "@augment-vir/test": "^32.2.2",
52
+ "@types/node": "^26.2.0",
53
+ "@web/dev-server-esbuild": "^2.0.0",
54
+ "@web/test-runner": "^1.0.0",
55
+ "@web/test-runner-playwright": "^1.0.0",
56
+ "c8": "^12.0.0",
57
57
  "istanbul-smart-text-reporter": "^1.1.5",
58
58
  "typescript": "^6.0.3"
59
59
  },
@@ -2,6 +2,7 @@ import {check} from '@augment-vir/assert';
2
2
  import {getObjectTypedEntries} from '@augment-vir/common';
3
3
  import {readdir, readFile, rm, stat} from 'node:fs/promises';
4
4
  import {join} from 'node:path';
5
+ import {doesPathContain} from '../path/contains.js';
5
6
  import {writeFileAndDir} from './write.js';
6
7
 
7
8
  /**
@@ -52,7 +53,7 @@ export async function readAllDirContents(
52
53
 
53
54
  const isFile = (await stat(filePath)).isFile();
54
55
  const contents = isFile
55
- ? (await readFile(filePath)).toString()
56
+ ? await readFile(filePath, 'utf8')
56
57
  : recursive
57
58
  ? await readAllDirContents(filePath, {
58
59
  recursive,
@@ -98,6 +99,10 @@ export async function resetDirContents(
98
99
  /**
99
100
  * Write {@link DirContents} to a directory.
100
101
  *
102
+ * Keys are always relative to `rootDir`. A key that traverses above it (such as `'../escaped.txt'`)
103
+ * throws instead of writing, so contents built from an untrusted source cannot reach outside the
104
+ * directory they were meant for.
105
+ *
101
106
  * @category Node : File
102
107
  * @category Package : @augment-vir/node
103
108
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -113,6 +118,18 @@ export async function writeDirContents(
113
118
  content,
114
119
  ]) => {
115
120
  const fullPath = join(rootDir, relativePath);
121
+
122
+ if (
123
+ !doesPathContain({
124
+ potentialParentPath: rootDir,
125
+ potentialChildPath: fullPath,
126
+ })
127
+ ) {
128
+ throw new Error(
129
+ `Cannot write '${relativePath}': it resolves outside of '${rootDir}'.`,
130
+ );
131
+ }
132
+
116
133
  if (check.isString(content)) {
117
134
  await writeFileAndDir(fullPath, content);
118
135
  } else {
@@ -3,6 +3,7 @@ import {
3
3
  awaitedBlockingMap,
4
4
  getObjectTypedKeys,
5
5
  log,
6
+ shellQuote,
6
7
  typedObjectFromEntries,
7
8
  type IsEqual,
8
9
  type PartialWithUndefined,
@@ -180,31 +181,23 @@ export type GrepSearchPattern = RequireExactlyOne<{
180
181
 
181
182
  const grepBinPath = '/usr/bin/grep';
182
183
 
183
- function shellQuote(input: string) {
184
- return [
185
- "'",
186
- input.replaceAll("'", String.raw`'\''`),
187
- "'",
188
- ].join('');
189
- }
190
-
191
184
  function recursiveFlag({
192
185
  recursive,
193
186
  followSymLinks,
194
187
  }: Readonly<Pick<GrepOptions, 'recursive' | 'followSymLinks'>>): string {
195
188
  if (!recursive) {
196
189
  return '';
197
- } else if (!followSymLinks) {
190
+ } else if (followSymLinks) {
191
+ /**
192
+ * BSD `grep` (macOS) requires `-S` to follow symlinks while recursing, but GNU `grep`
193
+ * (Linux) has no `-S` flag and instead follows all symlinks with `-R`. Only one of these
194
+ * branches can run on a given operating system.
195
+ */
196
+ /* node:coverage ignore next */
197
+ return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
198
+ } else {
198
199
  return '--recursive';
199
200
  }
200
-
201
- /**
202
- * BSD `grep` (macOS) requires `-S` to follow symlinks while recursing, but GNU `grep` (Linux)
203
- * has no `-S` flag and instead follows all symlinks with `-R`. Only one of these branches can
204
- * run on a given operating system.
205
- */
206
- /* node:coverage ignore next */
207
- return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
208
201
  }
209
202
 
210
203
  function isValidMaxCount(maxCount: unknown) {
@@ -380,16 +373,16 @@ function formatGrepCommand(args: ReadonlyArray<string>) {
380
373
 
381
374
  return [
382
375
  'grep',
383
- ...args.map((arg, index) =>
384
- shellQuote(
376
+ ...args.map((arg, index) => {
377
+ return shellQuote(
385
378
  redactGrepArgForLogging({
386
379
  arg,
387
380
  index,
388
381
  operandDelimiterIndex,
389
382
  previousArg: args[index - 1],
390
383
  }),
391
- ),
392
- ),
384
+ );
385
+ }),
393
386
  ].join(' ');
394
387
  }
395
388
 
@@ -430,9 +423,9 @@ function extractOptionalStringArray(input: unknown) {
430
423
  return [];
431
424
  } else if (!check.isArray(input) || !input.every(check.isString)) {
432
425
  return undefined;
426
+ } else {
427
+ return input.filter(check.isTruthy);
433
428
  }
434
-
435
- return input.filter(check.isTruthy);
436
429
  }
437
430
 
438
431
  function extractString(input: unknown) {
@@ -519,19 +512,19 @@ function createSearchLocation(
519
512
  file,
520
513
  ],
521
514
  };
522
- } else if (grepSearchLocation.dirs != undefined) {
515
+ } else if (grepSearchLocation.dirs == undefined) {
516
+ return dir
517
+ ? {
518
+ dirs: [
519
+ dir,
520
+ ],
521
+ }
522
+ : undefined;
523
+ } else {
523
524
  return {
524
525
  dirs: extractStringArray(grepSearchLocation.dirs),
525
526
  };
526
527
  }
527
-
528
- return dir
529
- ? {
530
- dirs: [
531
- dir,
532
- ],
533
- }
534
- : undefined;
535
528
  }
536
529
 
537
530
  function resolveSearchPart({
@@ -657,13 +650,13 @@ async function createSearchParts({
657
650
  ? recursive
658
651
  ? filteredSearchParts
659
652
  : (
660
- await awaitedBlockingMap(filteredSearchParts, (dir) =>
661
- readDirectDirSearchParts({
653
+ await awaitedBlockingMap(filteredSearchParts, (dir) => {
654
+ return readDirectDirSearchParts({
662
655
  cwd,
663
656
  dir,
664
657
  followSymLinks,
665
- }),
666
- )
658
+ });
659
+ })
667
660
  ).flat()
668
661
  : filteredSearchParts;
669
662
  }
@@ -724,7 +717,7 @@ function parseNullDelimitedGrepRecords({stdout}: Readonly<{stdout: string}>) {
724
717
  return records;
725
718
  }
726
719
 
727
- /* node:coverage ignore next 26 */
720
+ /* node:coverage ignore next 28 */
728
721
  function parseColonDelimitedGrepCountOutput(stdout: string) {
729
722
  return typedObjectFromEntries(
730
723
  stdout
@@ -745,14 +738,16 @@ function parseColonDelimitedGrepCountOutput(stdout: string) {
745
738
  });
746
739
  })
747
740
  .filter(check.isTruthy)
748
- .map((entry) => [
749
- entry.key,
750
- entry.value,
751
- ]),
741
+ .map((entry) => {
742
+ return [
743
+ entry.key,
744
+ entry.value,
745
+ ];
746
+ }),
752
747
  );
753
748
  }
754
749
 
755
- /* node:coverage ignore next 20 */
750
+ /* node:coverage ignore next 22 */
756
751
  function parseGrepCountOutput(stdout: string) {
757
752
  return stdout.includes('\0')
758
753
  ? typedObjectFromEntries(
@@ -766,10 +761,12 @@ function parseGrepCountOutput(stdout: string) {
766
761
  });
767
762
  })
768
763
  .filter(check.isTruthy)
769
- .map((entry) => [
770
- entry.key,
771
- entry.value,
772
- ]),
764
+ .map((entry) => {
765
+ return [
766
+ entry.key,
767
+ entry.value,
768
+ ];
769
+ }),
773
770
  )
774
771
  : parseColonDelimitedGrepCountOutput(stdout);
775
772
  }
@@ -885,10 +882,12 @@ async function runGrepCountFallback({
885
882
  )
886
883
  )
887
884
  .filter(check.isTruthy)
888
- .map((entry) => [
889
- entry.key,
890
- entry.value,
891
- ]),
885
+ .map((entry) => {
886
+ return [
887
+ entry.key,
888
+ entry.value,
889
+ ];
890
+ }),
892
891
  );
893
892
  }
894
893
 
@@ -897,10 +896,12 @@ function parseGrepFilesOnlyOutput(stdout: string) {
897
896
  /* node:coverage ignore next */
898
897
  (stdout.includes('\0') ? stdout.split('\0') : stdout.trimEnd().split('\n'))
899
898
  .filter(check.isTruthy)
900
- .map((entry) => [
901
- entry,
902
- [],
903
- ]),
899
+ .map((entry) => {
900
+ return [
901
+ entry,
902
+ [],
903
+ ];
904
+ }),
904
905
  );
905
906
  }
906
907
 
@@ -1025,10 +1026,12 @@ export async function grep<const CountOnly extends boolean = false>(
1025
1026
  ? grepOptionArrays.includeFiles.map((includeFile) => `--include=${includeFile}`)
1026
1027
  : []),
1027
1028
  grepOptions.binary ? '--binary' : '',
1028
- ...searchPatterns.flatMap((searchPattern) => [
1029
- '-e',
1030
- searchPattern,
1031
- ]),
1029
+ ...searchPatterns.flatMap((searchPattern) => {
1030
+ return [
1031
+ '-e',
1032
+ searchPattern,
1033
+ ];
1034
+ }),
1032
1035
  '--',
1033
1036
  ...searchParts,
1034
1037
  ].filter(check.isTruthy);
@@ -22,7 +22,7 @@ import {writeFileAndDir} from './write.js';
22
22
  */
23
23
  export async function readJsonFile(path: string): Promise<JsonCompatibleValue | undefined> {
24
24
  try {
25
- const contents = (await readFile(path)).toString();
25
+ const contents = await readFile(path, 'utf8');
26
26
  return JSON.parse(contents);
27
27
  } catch {
28
28
  return undefined;
@@ -63,7 +63,7 @@ export async function readDirFilesByExtension({
63
63
 
64
64
  const fileNames = await readdir(dirPath);
65
65
 
66
- return fileNames.filter((fileName) =>
67
- extensionsToCheck.some((extensionToCheck) => fileName.endsWith(extensionToCheck)),
68
- );
66
+ return fileNames.filter((fileName) => {
67
+ return extensionsToCheck.some((extensionToCheck) => fileName.endsWith(extensionToCheck));
68
+ });
69
69
  }
@@ -11,7 +11,7 @@ import {readFile} from 'node:fs/promises';
11
11
  */
12
12
  export async function readFileIfExists(path: string): Promise<string | undefined> {
13
13
  if (existsSync(path)) {
14
- return (await readFile(path)).toString();
14
+ return await readFile(path, 'utf8');
15
15
  } else {
16
16
  return undefined;
17
17
  }
@@ -16,9 +16,9 @@ import {findAncestor, joinFilesToDir} from '../path/ancestor.js';
16
16
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
17
17
  */
18
18
  export async function findAllPackageJsonFilePaths(startDirPath: string) {
19
- const packageRootDir = findAncestor(startDirPath, (dir) =>
20
- existsSync(join(dir, 'package-lock.json')),
21
- );
19
+ const packageRootDir = findAncestor(startDirPath, (dir) => {
20
+ return existsSync(join(dir, 'package-lock.json'));
21
+ });
22
22
 
23
23
  if (!packageRootDir) {
24
24
  throw new Error(
@@ -18,9 +18,9 @@ export async function readPackageJson(dirPath: string): Promise<PackageJson> {
18
18
 
19
19
  if (!packageJson) {
20
20
  throw new TypeError(`package.json file does not exist in '${dirPath}'`);
21
- } else if (!check.isObject(packageJson)) {
21
+ } else if (check.isObject(packageJson)) {
22
+ return packageJson;
23
+ } else {
22
24
  throw new TypeError(`Parsing package.json file did not return an object in '${dirPath}'`);
23
25
  }
24
-
25
- return packageJson;
26
26
  }
@@ -41,10 +41,10 @@ export function doesPathContain({
41
41
  } else if (isAbsolute(relativePath)) {
42
42
  /** On Windows, paths on different drives yield an absolute relative path. */
43
43
  return false;
44
+ } else {
45
+ /** Contained if it does not traverse up out of parent. */
46
+ return relativePath !== '..' && !relativePath.startsWith('..' + sep);
44
47
  }
45
-
46
- /** Contained if it does not traverse up out of parent. */
47
- return relativePath !== '..' && !relativePath.startsWith('..' + sep);
48
48
  }
49
49
 
50
50
  function normalizePath(inputPath: string): string {
@@ -36,6 +36,12 @@ export function toPosixPath(maybeWindowsPath: string): string {
36
36
  * Use this to interpolate paths into bash commands. If the given path is not a Windows path, the
37
37
  * path structure will not be modified.
38
38
  *
39
+ * Each round of bash parsing halves the backslashes, so the count here assumes _two_ rounds: a
40
+ * command string that bash parses and then hands to something which parses it again. Four
41
+ * backslashes arrive as one. For a command that bash parses only once, this over-escapes; use
42
+ * `shellQuote` from `@augment-vir/common` instead, which needs no multiplication because bash does
43
+ * no escape processing inside single quotes.
44
+ *
39
45
  * @category Path : Node
40
46
  * @category Package : @augment-vir/node
41
47
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
@@ -2,9 +2,9 @@
2
2
  /** This file cannot be tested because it calls `process.exit`. */
3
3
 
4
4
  import {check} from '@augment-vir/assert';
5
+ import {shellQuote} from '@augment-vir/common';
5
6
  import {dirname, extname} from 'node:path';
6
7
  import {findNpmBinPath} from '../npm/find-bin-path.js';
7
- import {interpolationSafeWindowsPath} from '../path/os-path.js';
8
8
  import {extractRelevantArgs} from './relevant-args.js';
9
9
  import {runShellCommand} from './shell.js';
10
10
 
@@ -57,13 +57,13 @@ export async function runCliScript({
57
57
  }) || runner.npx;
58
58
 
59
59
  const results = await runShellCommand(
60
- interpolationSafeWindowsPath(
61
- [
62
- runnerPath,
63
- scriptPath,
64
- ...args,
65
- ].join(' '),
66
- ),
60
+ [
61
+ runnerPath,
62
+ scriptPath,
63
+ ...args,
64
+ ]
65
+ .map(shellQuote)
66
+ .join(' '),
67
67
  {
68
68
  hookUpToConsole: true,
69
69
  },
@@ -12,9 +12,9 @@ import {findAncestor} from '../path/ancestor.js';
12
12
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
13
13
  */
14
14
  export function readTsconfig(startingPath: string) {
15
- const tsconfigDirPath = findAncestor(startingPath, (ancestorPath) =>
16
- existsSync(join(ancestorPath, 'tsconfig.json')),
17
- );
15
+ const tsconfigDirPath = findAncestor(startingPath, (ancestorPath) => {
16
+ return existsSync(join(ancestorPath, 'tsconfig.json'));
17
+ });
18
18
  const tsconfigPath = tsconfigDirPath ? join(tsconfigDirPath, 'tsconfig.json') : undefined;
19
19
 
20
20
  if (!tsconfigPath) {
@@ -76,7 +76,7 @@ async function fixAllSrcImports(monoRepoPath: string) {
76
76
 
77
77
  await Promise.all(
78
78
  filePaths.map(async (filePath) => {
79
- const contents = String(await readFile(filePath));
79
+ const contents = await readFile(filePath, 'utf8');
80
80
  const fixedContents = contents.replaceAll(
81
81
  /(from ["'][^'"]+?)\/src\/([^'"]+)['"];/g,
82
82
  "$1/dist/$2';",
@@ -17,8 +17,7 @@
17
17
  */
18
18
 
19
19
  import {log} from '@augment-vir/common';
20
- import {interpolationSafeWindowsPath, runShellCommand} from '@augment-vir/node';
21
- import {readFile, rm, writeFile} from 'node:fs/promises';
20
+ import {chmod, readFile, rm, writeFile} from 'node:fs/promises';
22
21
  import {join, sep} from 'node:path/posix';
23
22
 
24
23
  type PackageToFix = {
@@ -75,7 +74,7 @@ async function fixTsBin(packageToFix: Readonly<PackageToFix>) {
75
74
  force: true,
76
75
  });
77
76
  await writeFile(binFilePath, createBinFileContents(packageToFix));
78
- await runShellCommand(`chmod +x ${interpolationSafeWindowsPath(binFilePath)}`);
77
+ await chmod(binFilePath, 0o755);
79
78
  await fixPackageJson(packageToFix);
80
79
  log.success(`Fixed ${packageToFix.packageName} bin.`);
81
80
  }
@@ -91,7 +90,7 @@ async function fixPackageJson(packageToFix: Readonly<PackageToFix>) {
91
90
  packageToFix.packageName,
92
91
  'package.json',
93
92
  );
94
- const original = String(await readFile(packageJsonPath));
93
+ const original = await readFile(packageJsonPath, 'utf8');
95
94
  await writeFile(
96
95
  packageJsonPath,
97
96
  original