@nubjs/nub 0.7.2 → 0.7.3

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/bin/launch.js CHANGED
@@ -218,7 +218,58 @@ function cmdShimLeadsToUs(dir, verb, ourReal) {
218
218
  return false;
219
219
  }
220
220
 
221
- // WINDOWS: put a real `<verb>.exe` next to npm's shims and CHANGE NOTHING ELSE.
221
+ // Place `src` at `dest` as a hardlink, idempotently. Extracted from the .exe path so
222
+ // the shell sidecar below gets identical staleness + fallback semantics.
223
+ //
224
+ // Idempotent, and correct across an upgrade: `npm i -g` extracts a NEW binary at a
225
+ // new inode, so an existing file from a previous version is stale and must be
226
+ // re-linked. Comparing ino+dev is exact for a hardlink; the size fallback covers
227
+ // the copy path, where ino necessarily differs.
228
+ function stageWindowsLink(srcPath, srcStat, dest) {
229
+ try {
230
+ const cur = fs.statSync(dest);
231
+ if ((cur.ino && cur.ino === srcStat.ino && cur.dev === srcStat.dev) || cur.size === srcStat.size) return;
232
+ fs.rmSync(dest, { force: true });
233
+ } catch {}
234
+ try {
235
+ fs.linkSync(srcPath, dest);
236
+ } catch {
237
+ // EXDEV (prefix on a different volume from the store) or a filesystem without
238
+ // hardlinks: fall back to a copy. Costs the file's size on disk once, which is
239
+ // why it is the fallback and not the default.
240
+ try { fs.copyFileSync(srcPath, dest); } catch {}
241
+ }
242
+ }
243
+
244
+ // The nub-owned subdirectory the bundled POSIX shell is carried into. Must match
245
+ // NUB_SHELL_SUBDIR in crates/nub-cli/src/cli.rs (resolve_bundled_busybox).
246
+ const SHELL_SUBDIR = "nub-sh";
247
+
248
+ // Carry the bundled POSIX shell along with the .exe we just linked.
249
+ //
250
+ // `nub run` executes script bodies through the bundled busybox-w32 `sh`, and the
251
+ // binary resolves it RELATIVE TO ITSELF (cli.rs resolve_bundled_busybox). The win32
252
+ // package lays `busybox.exe` beside `bin/nub.exe`, so that holds — until the .exe is
253
+ // hardlinked into npm's global bin dir, whose parent has no sidecar, and every
254
+ // `nub run` then dies "bundled POSIX shell (busybox.exe) was not found" (#687).
255
+ //
256
+ // It goes in a SUBDIRECTORY, not beside the .exe, because `dir` is by construction a
257
+ // directory on the user's PATH: a bare `busybox.exe` there would shadow a busybox the
258
+ // user installed themselves, which is the same shadowing hazard cmdShimLeadsToUs
259
+ // exists to prevent — and worse here, since the name is not even ours. A subdirectory
260
+ // is not searched by PATH, so it is invisible to command resolution.
261
+ //
262
+ // A missing source is not an error: a NEWER launcher can run against an OLDER platform
263
+ // package that predates the bundled shell (<0.6.0), and those installs never had one.
264
+ function stageWindowsShell(pkgBinDir, dir) {
265
+ const from = path.join(pkgBinDir, "busybox.exe");
266
+ let st; try { st = fs.statSync(from); } catch { return; }
267
+ const into = path.join(dir, SHELL_SUBDIR);
268
+ try { fs.mkdirSync(into, { recursive: true }); } catch { return; }
269
+ stageWindowsLink(from, st, path.join(into, "busybox.exe"));
270
+ }
271
+
272
+ // WINDOWS: put a real `<verb>.exe` next to npm's shims, plus the shell it needs.
222
273
  //
223
274
  // The heal below is POSIX-only because there is no shebang or symlink fast path on
224
275
  // Windows — every call goes cmd.exe -> nub.cmd -> node -> spawn nub.exe, and the node
@@ -226,15 +277,19 @@ function cmdShimLeadsToUs(dir, verb, ourReal) {
226
277
  // of `nub.cmd` by PATHEXT, so cmd.exe reaches the binary directly. Measured on
227
278
  // windows-latest, N=40: 95.6 -> 35.8 ms.
228
279
  //
229
- // DELIBERATELY ADD-ONLY. npm's `.ps1` and extensionless shims are left exactly as
230
- // generated: we are not the first package to start editing files npm owns (checked —
231
- // esbuild, bun and @pnpm/exe all modify only files inside their OWN package and never
232
- // touch the global bin dir). The cost is that PowerShell and every sh-family shell keep
233
- // preferring those shims and see no improvement — including nub's OWN Windows script
234
- // shell, the bundled busybox (cli.rs `resolve_bundled_busybox`), measured 170.3 -> 169.0
235
- // ms, i.e. nothing. `nub run` therefore does not benefit. That trade was made explicitly.
280
+ // ADD-ONLY WITH RESPECT TO FILES NPM OWNS. npm's `.ps1` and extensionless shims are
281
+ // left exactly as generated: we are not the first package to start editing files npm
282
+ // owns (checked — esbuild, bun and @pnpm/exe all modify only files inside their OWN
283
+ // package and never touch the global bin dir). The cost is that PowerShell and every
284
+ // sh-family shell keep preferring those shims and see no improvement — including nub's
285
+ // OWN Windows script shell, the bundled busybox (cli.rs `resolve_bundled_busybox`),
286
+ // measured 170.3 -> 169.0 ms, i.e. nothing. `nub run` therefore does not benefit on
287
+ // SPEED. It does depend on this function for CORRECTNESS: relocating the .exe moves
288
+ // the binary away from the sidecar it resolves relative to itself, which is what
289
+ // stageWindowsShell repairs (#687). Reading that trade as "nub run is unaffected"
290
+ // is how the regression shipped.
236
291
  //
237
- // TWO RESIDUES THIS SHAPE OWNS, both from the `.exe` being a file npm does not track:
292
+ // THREE RESIDUES THIS SHAPE OWNS, all from writing files npm does not track:
238
293
  //
239
294
  // UNINSTALL. `npm uninstall -g @nubjs/nub` removes only the shims npm generated;
240
295
  // cmd-shim never created `<verb>.exe` and npm has run no uninstall lifecycle script
@@ -242,14 +297,15 @@ function cmdShimLeadsToUs(dir, verb, ourReal) {
242
297
  // answering `nub` from cmd.exe after the user believes nub is gone — and on the
243
298
  // hardlink path the surviving link also keeps the binary's bytes on disk. This is a
244
299
  // real user-visible residue, not merely wasted space; do not describe it as "npm's
245
- // uninstall is unaffected".
300
+ // uninstall is unaffected". The `nub-sh/` shell dir survives the same way, but it is
301
+ // NOT on PATH, so it wastes space without answering any command.
246
302
  //
247
303
  // UPGRADE. Once the `.exe` wins PATHEXT, cmd.exe never dispatches through npm's `.cmd`
248
304
  // again, so THIS FUNCTION NEVER RUNS AGAIN for the users it serves and its currency
249
- // check below cannot fire for them. `postinstall.js` (dropStaleWindowsExe) removes the
250
- // file on every install so the next call re-heals against the new binary — but that
251
- // only runs when lifecycle scripts do, so an `--ignore-scripts` upgrade still leaves
252
- // cmd.exe executing the previous version silently.
305
+ // check below cannot fire for them. `postinstall.js` (dropStaleWindowsExe) removes both
306
+ // the file and `nub-sh/` on every install so the next call re-heals against the new
307
+ // binary — but that only runs when lifecycle scripts do, so an `--ignore-scripts`
308
+ // upgrade still leaves cmd.exe executing the previous version silently.
253
309
  //
254
310
  // Best-effort and silent, like every other heal step: any failure leaves a working
255
311
  // (slower) install rather than a broken one.
@@ -264,24 +320,11 @@ function healWindowsBinDir(verb, nativePath) {
264
320
  for (const dir of (process.env.PATH || "").split(path.delimiter)) {
265
321
  if (!dir) continue;
266
322
  if (!cmdShimLeadsToUs(dir, verb, ourReal)) continue;
267
- const dest = path.join(dir, `${verb}.exe`);
268
- // Idempotent, and correct across an upgrade: `npm i -g` extracts a NEW binary at a
269
- // new inode, so an existing .exe from a previous version is stale and must be
270
- // re-linked. Comparing ino+dev is exact for a hardlink; the size fallback covers
271
- // the copy path, where ino necessarily differs.
272
- try {
273
- const cur = fs.statSync(dest);
274
- if ((cur.ino && cur.ino === src.ino && cur.dev === src.dev) || cur.size === src.size) return;
275
- fs.rmSync(dest, { force: true });
276
- } catch {}
277
- try {
278
- fs.linkSync(nativeReal, dest);
279
- } catch {
280
- // EXDEV (prefix on a different volume from the store) or a filesystem without
281
- // hardlinks: fall back to a copy. Costs the binary's size on disk once, which is
282
- // why it is the fallback and not the default.
283
- try { fs.copyFileSync(nativeReal, dest); } catch {}
284
- }
323
+ stageWindowsLink(nativeReal, src, path.join(dir, `${verb}.exe`));
324
+ // Unconditional, NOT gated on the .exe having been (re)linked above: the
325
+ // 0.7.0-0.7.2 installs this fixes already carry a current .exe, so a shell
326
+ // staged only alongside a fresh link would never reach them.
327
+ stageWindowsShell(path.dirname(nativeReal), dir);
285
328
  break; // the first PATH entry that dispatches to us is the one that matters
286
329
  }
287
330
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubjs/nub",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "TypeScript-first developer supertool — a fast script runner and TS runtime powered by Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/nubjs/nub",
@@ -36,13 +36,13 @@
36
36
  "LICENSE"
37
37
  ],
38
38
  "optionalDependencies": {
39
- "@nubjs/nub-darwin-arm64": "0.7.2",
40
- "@nubjs/nub-darwin-x64": "0.7.2",
41
- "@nubjs/nub-linux-x64": "0.7.2",
42
- "@nubjs/nub-linux-x64-musl": "0.7.2",
43
- "@nubjs/nub-linux-arm64": "0.7.2",
44
- "@nubjs/nub-linux-arm64-musl": "0.7.2",
45
- "@nubjs/nub-win32-x64": "0.7.2",
46
- "@nubjs/nub-win32-arm64": "0.7.2"
39
+ "@nubjs/nub-darwin-arm64": "0.7.3",
40
+ "@nubjs/nub-darwin-x64": "0.7.3",
41
+ "@nubjs/nub-linux-x64": "0.7.3",
42
+ "@nubjs/nub-linux-x64-musl": "0.7.3",
43
+ "@nubjs/nub-linux-arm64": "0.7.3",
44
+ "@nubjs/nub-linux-arm64-musl": "0.7.3",
45
+ "@nubjs/nub-win32-x64": "0.7.3",
46
+ "@nubjs/nub-win32-arm64": "0.7.3"
47
47
  }
48
48
  }
package/postinstall.js CHANGED
@@ -199,6 +199,14 @@ function dropStaleWindowsExe() {
199
199
  // delete — there is a real unrelated `nub@1.0.0` on npm.
200
200
  if (!fs.existsSync(path.join(dir, `${verb}.cmd`))) continue;
201
201
  try { fs.rmSync(path.join(dir, `${verb}.exe`), { force: true }); } catch {}
202
+ // The bundled POSIX shell the launcher carried in beside that `.exe`
203
+ // (healWindowsBinDir -> `nub-sh/busybox.exe`). The heal re-stages this
204
+ // unconditionally, so unlike the `.exe` it is not stranded by being left here —
205
+ // but its currency check falls back to comparing SIZE when the inode differs,
206
+ // and a busybox that changed while keeping its size would be kept forever.
207
+ // Dropping the nub-owned dir at install time closes that, and costs one hardlink
208
+ // on the next call.
209
+ try { fs.rmSync(path.join(dir, "nub-sh"), { recursive: true, force: true }); } catch {}
202
210
  }
203
211
  }
204
212
  }