rman 2.21.0 → 2.21.1

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.
@@ -119,7 +119,9 @@ async function remove(file, dryRun) {
119
119
  function pruneEmptyDirs(dir, includeSelf, dryRun) {
120
120
  if (dryRun || !fs.existsSync(dir))
121
121
  return;
122
- const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true });
122
+ /** Not through a link: a link to a directory is not a directory of this one to prune - removing
123
+ * it as one is `ENOTDIR`, and walking into it reaches somewhere else entirely. */
124
+ const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true, followSymbolicLinks: false });
123
125
  entries.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
124
126
  for (const rel of entries) {
125
127
  const abs = path.join(dir, rel);
@@ -140,23 +142,48 @@ function pruneEmptyDirs(dir, includeSelf, dryRun) {
140
142
  * matching files *inside* an otherwise-deleted directory, leaving the rest of it gone and the
141
143
  * protected files (and their now non-empty parent) in place.
142
144
  */
145
+ /* **A symbolic link is removed as a link, and never followed.** A build directory routinely holds
146
+ * one - `build/node_modules/@scope/sibling` pointing at another package's own build - and walking
147
+ * through it deleted *that* package's output, then failed `rmdir` on the link with `ENOTDIR`.
148
+ * Measured on `panates/syncbridge`: cleaning `syncbuild` emptied `common/build`, and the run
149
+ * reported only `0 succeeded, 1 failed`. fast-glob follows links by default, so every glob here
150
+ * says `followSymbolicLinks: false`. */
143
151
  async function cleanGlobs(dirname, include, exclude, dryRun) {
144
152
  if (!include.length)
145
153
  return [];
146
- const matches = await fg(include, { cwd: dirname, ignore: exclude, onlyFiles: false, dot: true, absolute: true });
154
+ const matches = await fg(include, {
155
+ cwd: dirname,
156
+ ignore: exclude,
157
+ onlyFiles: false,
158
+ dot: true,
159
+ absolute: true,
160
+ followSymbolicLinks: false,
161
+ });
147
162
  const protectedFiles = exclude.length
148
- ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true }))
163
+ ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true, followSymbolicLinks: false }))
149
164
  : new Set();
150
165
  const removed = [];
151
166
  for (const m of matches) {
152
- const stat = await fs.promises.stat(m).catch(() => undefined);
167
+ /** `lstat`, not `stat`: a link is removed as the link it is, and never read as the thing it
168
+ * points at. */
169
+ const stat = await fs.promises.lstat(m).catch(() => undefined);
153
170
  if (!stat)
154
171
  continue;
155
172
  if (stat.isDirectory()) {
156
- const filesInside = await fg('**', { cwd: m, onlyFiles: true, dot: true, absolute: true });
157
- for (const f of filesInside) {
173
+ /** Files *and* links inside, and a link never entered - see `cleanGlobs`' own note. */
174
+ const inside = await fg('**', {
175
+ cwd: m,
176
+ onlyFiles: false,
177
+ dot: true,
178
+ absolute: true,
179
+ followSymbolicLinks: false,
180
+ });
181
+ for (const f of inside) {
158
182
  if (protectedFiles.has(f))
159
183
  continue;
184
+ const entry = await fs.promises.lstat(f).catch(() => undefined);
185
+ if (!entry || entry.isDirectory())
186
+ continue;
160
187
  await remove(f, dryRun);
161
188
  removed.push(path.relative(dirname, f));
162
189
  }
@@ -208,6 +235,8 @@ async function cleanTsArtifacts(pkg, dryRun) {
208
235
  onlyFiles: true,
209
236
  dot: true,
210
237
  absolute: true,
238
+ /** A link to a directory elsewhere is not this package's source to sweep. */
239
+ followSymbolicLinks: false,
211
240
  });
212
241
  const removed = [];
213
242
  for (const f of files) {
@@ -277,6 +306,7 @@ async function cleanTsBuildInfo(dirname, dryRun) {
277
306
  onlyFiles: true,
278
307
  dot: true,
279
308
  absolute: true,
309
+ followSymbolicLinks: false,
280
310
  });
281
311
  for (const f of files)
282
312
  await remove(f, dryRun);
@@ -310,7 +340,12 @@ async function cleanPackage(pkg, item, panelEnabled, dryRun, logger) {
310
340
  }
311
341
  catch (e) {
312
342
  item.status = 'failed';
313
- item.log.push(e?.message ?? String(e));
343
+ const message = e?.message ?? String(e);
344
+ item.log.push(message);
345
+ /** The panel replays `log` in its recap; with no panel nothing does, and the run said only
346
+ * `0 succeeded, 1 failed` - which is how the link bug above went unexplained. */
347
+ if (!panelEnabled)
348
+ logger.error(colors.red('clean'), colors.cyan(item.name), message);
314
349
  throw e;
315
350
  }
316
351
  finally {
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.21.0';
1
+ export const version = '2.21.1';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.21.0",
4
+ "version": "2.21.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {