@nubjs/nub 0.0.17 → 0.0.19

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.
Files changed (3) hide show
  1. package/bin/launch.js +105 -18
  2. package/package.json +10 -14
  3. package/postinstall.js +0 -55
package/bin/launch.js CHANGED
@@ -1,40 +1,127 @@
1
1
  "use strict";
2
2
  // Shared launcher used by bin/nub and bin/nubx.
3
3
  //
4
- // On POSIX, postinstall.js replaces bin/nub and bin/nubx with direct symlinks to
5
- // the platform binary, so this module never runs on the hot path — `nub`/`nubx`
6
- // exec the native Rust binary directly (no Node bootstrap, preserving cold-start).
7
- // It DOES run on Windows (npm's generated nub.cmd / nubx.cmd invoke `node bin/nub`
8
- // there is no symlink fast path there) and as a fallback on any platform where
9
- // postinstall could not create the symlink.
4
+ // bin/nub and bin/nubx ship as committed `#!/usr/bin/env node` shims because the
5
+ // cross-platform @nubjs/nub package cannot ship a native binary (it doesn't know
6
+ // the target platform at publish time). On Windows that is the whole story: npm's
7
+ // generated nub.cmd / nubx.cmd invoke `node bin/nub`, which spawns the platform
8
+ // .exe (no shebang/symlink fast path on Windows).
10
9
  //
11
- // The Rust CLI selects its verb from argv[0]'s basename (nub vs nubx vs node — see
12
- // crates/nub-cli/src/cli.rs Argv0::detect). spawnSync's `argv0` option sets that
13
- // basename for the child without changing process.execPath, so the binary still
14
- // resolves its sibling runtime/ directory by walking up from its real location.
10
+ // On its FIRST POSIX invocation this launcher SELF-HEALS: it rewrites the on-PATH
11
+ // entry that dispatched it — the package manager's bin shim (pnpm cmd-shim) or
12
+ // symlink (npm/bun) into a MINIMAL `#!/bin/sh` trampoline that exec's the native
13
+ // binary directly. Every later call then resolves PATH -> that tiny sh trampoline
14
+ // -> native, skipping Node entirely (~native cold-start; the sh hop is ~1-2ms on
15
+ // Linux dash/busybox, ~4ms on macOS bash — vs ~50ms for this Node launcher).
16
+ //
17
+ // CRITICAL: the heal target is a minimal sh SCRIPT, never a native binary. A
18
+ // script->binary swap has an irreducible TOCTOU race (the kernel reads `#!/bin/sh`
19
+ // then `/bin/sh` reopens the path and finds a Mach-O -> "cannot execute binary
20
+ // file"; ~24% under a concurrent burst). A script->script swap (both `#!/bin/sh`,
21
+ // always valid scripts) is race-free by construction — a concurrent `/bin/sh`
22
+ // reopening the entry mid-swap always reads a valid trampoline (measured 0/600 vs
23
+ // 146/600). So the heal needs no lock: it is best-effort, atomic (write temp +
24
+ // rename), verify-before-clobber, and a no-op on Windows.
25
+ //
26
+ // The native binary selects its verb from argv[0]'s basename (nub vs nubx); the
27
+ // healed trampoline exec's bin/<verb> in the platform package (which ships both
28
+ // names), so no argv0 override is needed past the heal.
15
29
  const { spawnSync } = require("child_process");
30
+ const fs = require("fs");
31
+ const path = require("path");
16
32
  const { platformPackage } = require("../platform.js");
17
33
 
18
- function resolveBinary() {
34
+ function resolveBinary(verb) {
19
35
  const { key, pkg } = platformPackage();
20
36
  if (!pkg) {
21
37
  console.error(`@nubjs/nub: no prebuilt binary for ${key}`);
22
38
  process.exit(1);
23
39
  }
40
+ const ext = process.platform === "win32" ? ".exe" : "";
24
41
  try {
25
- return require.resolve(`${pkg}/bin/nub${process.platform === "win32" ? ".exe" : ""}`);
42
+ return require.resolve(`${pkg}/bin/${verb}${ext}`);
26
43
  } catch {
27
- console.error(`@nubjs/nub: the ${pkg} package is not installed. Try: npm rebuild @nubjs/nub`);
28
- process.exit(1);
44
+ // bin/nubx may be absent on an older platform package; fall back to bin/nub.
45
+ try {
46
+ return require.resolve(`${pkg}/bin/nub${ext}`);
47
+ } catch {
48
+ console.error(`@nubjs/nub: the ${pkg} package is not installed. Try: npm rebuild @nubjs/nub`);
49
+ process.exit(1);
50
+ }
29
51
  }
30
52
  }
31
53
 
32
- // argv0Name: the basename the child should see as argv[0] ("nub" or "nubx"). When
33
- // omitted the child sees the binary path, whose basename is "nub" — the default verb.
54
+ // POSIX single-quote a string for safe embedding in the sh trampoline.
55
+ function shq(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
56
+
57
+ // Verify a PATH entry demonstrably resolves to OUR launcher before replacing it —
58
+ // never clobber an unrelated `nub` (there is an unrelated nub@1.0.0 on npm). For a
59
+ // symlink, realpath(entry) must equal our launcher's realpath. For a pnpm cmd-shim
60
+ // (a regular #!/bin/sh file), every quoted path it references is $basedir-resolved
61
+ // and realpath'd; one must equal our launcher. Comparing realpaths (not substrings)
62
+ // matches pnpm's fresh AND regenerated shim forms and rejects comment-only mentions.
63
+ function leadsToUs(entry, st, ourReal) {
64
+ try {
65
+ if (st.isSymbolicLink()) {
66
+ try { return fs.realpathSync(entry) === ourReal; } catch { return false; }
67
+ }
68
+ if (st.isFile()) {
69
+ const body = fs.readFileSync(entry, "utf8");
70
+ const basedir = path.dirname(entry);
71
+ const quoted = body.match(/"([^"]+)"/g) || [];
72
+ for (const q of quoted) {
73
+ let p = q.slice(1, -1).replace(/\$\{?basedir\}?/g, basedir);
74
+ if (!p.includes("/")) continue;
75
+ if (!path.isAbsolute(p)) p = path.join(basedir, p);
76
+ try { if (fs.realpathSync(p) === ourReal) return true; } catch {}
77
+ }
78
+ }
79
+ } catch {}
80
+ return false;
81
+ }
82
+
83
+ // Best-effort, never throws. Rewrite the on-PATH `<verb>` entry that dispatched us
84
+ // into a minimal sh trampoline -> the native binary. POSIX only.
85
+ function healPathEntry(verb, nativePath) {
86
+ if (process.platform === "win32") return;
87
+ try {
88
+ const ourBin = path.join(__dirname, verb); // .../@nubjs/nub/bin/<verb>
89
+ let ourReal; try { ourReal = fs.realpathSync(ourBin); } catch { ourReal = ourBin; }
90
+ let nativeReal; try { nativeReal = fs.realpathSync(nativePath); } catch { nativeReal = nativePath; }
91
+ const content = `#!/bin/sh\nexec ${shq(nativeReal)} "$@"\n`;
92
+
93
+ for (const dir of (process.env.PATH || "").split(path.delimiter)) {
94
+ if (!dir) continue;
95
+ const entry = path.join(dir, verb);
96
+ let st;
97
+ try { st = fs.lstatSync(entry); } catch { continue; }
98
+ if (!leadsToUs(entry, st, ourReal)) continue;
99
+ // Atomic replace: write a unique temp in the SAME dir, then rename over the
100
+ // entry (rename is atomic on POSIX; script->script means no exec-format race).
101
+ const tmp = path.join(dir, `.${verb}.nub.${process.pid}.${Date.now()}.tmp`);
102
+ try {
103
+ fs.writeFileSync(tmp, content, { mode: 0o755 });
104
+ fs.chmodSync(tmp, 0o755);
105
+ fs.renameSync(tmp, entry);
106
+ } catch {
107
+ try { fs.unlinkSync(tmp); } catch {}
108
+ }
109
+ break; // the first matching PATH entry is the one that dispatched us
110
+ }
111
+ } catch {}
112
+ }
113
+
114
+ // argv0Name: the verb this stub represents ("nubx" for bin/nubx; undefined => nub).
34
115
  module.exports = function launch(argv0Name) {
35
- const binPath = resolveBinary();
116
+ const verb = argv0Name || "nub";
117
+ const binPath = resolveBinary(verb);
118
+ // Self-heal the PATH entry on first POSIX call so later calls skip Node entirely.
119
+ healPathEntry(verb, binPath);
120
+ // This call still runs through Node; spawn the native binary. argv0 basename of
121
+ // binPath is the verb (bin/nub or bin/nubx), so the Rust CLI dispatches correctly
122
+ // without an argv0 override.
36
123
  const opts = { stdio: "inherit", windowsHide: true };
37
- if (argv0Name) opts.argv0 = argv0Name;
124
+ if (argv0Name) opts.argv0 = argv0Name; // belt-and-suspenders for the bin/nub fallback path
38
125
  const result = spawnSync(binPath, process.argv.slice(2), opts);
39
126
  if (result.error) {
40
127
  console.error(`@nubjs/nub: failed to launch ${binPath}: ${result.error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubjs/nub",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
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",
@@ -11,20 +11,16 @@
11
11
  },
12
12
  "files": [
13
13
  "bin",
14
- "platform.js",
15
- "postinstall.js"
14
+ "platform.js"
16
15
  ],
17
- "scripts": {
18
- "postinstall": "node postinstall.js"
19
- },
20
16
  "optionalDependencies": {
21
- "@nubjs/nub-darwin-arm64": "0.0.17",
22
- "@nubjs/nub-darwin-x64": "0.0.17",
23
- "@nubjs/nub-linux-x64": "0.0.17",
24
- "@nubjs/nub-linux-x64-musl": "0.0.17",
25
- "@nubjs/nub-linux-arm64": "0.0.17",
26
- "@nubjs/nub-linux-arm64-musl": "0.0.17",
27
- "@nubjs/nub-win32-x64": "0.0.17",
28
- "@nubjs/nub-win32-arm64": "0.0.17"
17
+ "@nubjs/nub-darwin-arm64": "0.0.19",
18
+ "@nubjs/nub-darwin-x64": "0.0.19",
19
+ "@nubjs/nub-linux-x64": "0.0.19",
20
+ "@nubjs/nub-linux-x64-musl": "0.0.19",
21
+ "@nubjs/nub-linux-arm64": "0.0.19",
22
+ "@nubjs/nub-linux-arm64-musl": "0.0.19",
23
+ "@nubjs/nub-win32-x64": "0.0.19",
24
+ "@nubjs/nub-win32-arm64": "0.0.19"
29
25
  }
30
26
  }
package/postinstall.js DELETED
@@ -1,55 +0,0 @@
1
- const { platform } = process;
2
- const { mkdirSync, unlinkSync, symlinkSync, copyFileSync, chmodSync } = require("fs");
3
- const { join } = require("path");
4
- const { platformPackage } = require("./platform.js");
5
-
6
- const { key, pkg } = platformPackage();
7
-
8
- if (!pkg) {
9
- console.error(`@nubjs/nub: no prebuilt binary for ${key}`);
10
- process.exit(0);
11
- }
12
-
13
- // Windows: there is no symlink fast path. npm's generated nub.cmd / nubx.cmd invoke
14
- // the JS launchers (bin/nub, bin/nubx), which resolve and spawn the platform .exe at
15
- // runtime. Nothing to do at install time — leave the launchers in place.
16
- if (platform === "win32") {
17
- process.exit(0);
18
- }
19
-
20
- let binSrc;
21
- try {
22
- binSrc = require.resolve(`${pkg}/bin/nub`);
23
- } catch {
24
- // optionalDependency not installed (wrong platform) — leave the JS launchers as a
25
- // fallback; they print an actionable error if invoked.
26
- process.exit(0);
27
- }
28
-
29
- // POSIX fast path: replace each JS launcher with a direct symlink to the platform
30
- // binary, so `nub` and `nubx` exec the native Rust binary with no Node bootstrap.
31
- // Both names point at the SAME binary; the Rust CLI selects its verb from argv[0]'s
32
- // basename (nub vs nubx — see crates/nub-cli/src/cli.rs Argv0::detect), and
33
- // process.execPath still resolves to the platform binary so the sibling runtime/
34
- // directory is found by walking up.
35
- const binDir = join(__dirname, "bin");
36
- mkdirSync(binDir, { recursive: true });
37
- for (const name of ["nub", "nubx"]) {
38
- const dest = join(binDir, name);
39
- try { unlinkSync(dest); } catch {}
40
- try {
41
- symlinkSync(binSrc, dest);
42
- chmodSync(dest, 0o755);
43
- } catch {
44
- // Fallback: copy if symlink fails (e.g. cross-device). Slower path resolution
45
- // but correct — argv[0] still resolves to a binary named nub/nubx.
46
- try {
47
- copyFileSync(binSrc, dest);
48
- chmodSync(dest, 0o755);
49
- } catch (err) {
50
- console.error(`@nubjs/nub: failed to install ${name} binary: ${err.message}`);
51
- process.exit(0);
52
- }
53
- }
54
- }
55
- chmodSync(binSrc, 0o755);