@nubjs/nub 0.0.29 → 0.0.31

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 (2) hide show
  1. package/package.json +9 -9
  2. package/postinstall.js +132 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubjs/nub",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
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",
@@ -18,13 +18,13 @@
18
18
  "postinstall.js"
19
19
  ],
20
20
  "optionalDependencies": {
21
- "@nubjs/nub-darwin-arm64": "0.0.29",
22
- "@nubjs/nub-darwin-x64": "0.0.29",
23
- "@nubjs/nub-linux-x64": "0.0.29",
24
- "@nubjs/nub-linux-x64-musl": "0.0.29",
25
- "@nubjs/nub-linux-arm64": "0.0.29",
26
- "@nubjs/nub-linux-arm64-musl": "0.0.29",
27
- "@nubjs/nub-win32-x64": "0.0.29",
28
- "@nubjs/nub-win32-arm64": "0.0.29"
21
+ "@nubjs/nub-darwin-arm64": "0.0.31",
22
+ "@nubjs/nub-darwin-x64": "0.0.31",
23
+ "@nubjs/nub-linux-x64": "0.0.31",
24
+ "@nubjs/nub-linux-x64-musl": "0.0.31",
25
+ "@nubjs/nub-linux-arm64": "0.0.31",
26
+ "@nubjs/nub-linux-arm64-musl": "0.0.31",
27
+ "@nubjs/nub-win32-x64": "0.0.31",
28
+ "@nubjs/nub-win32-arm64": "0.0.31"
29
29
  }
30
30
  }
package/postinstall.js CHANGED
@@ -1,4 +1,24 @@
1
1
  "use strict";
2
+ // Install-time fixups, run as the INSTALLER by npm's postinstall hook. Two
3
+ // jobs, both best-effort and silent on any failure:
4
+ //
5
+ // 1. chmod +x the platform binary (chmodExecutable)
6
+ // 2. refresh existing ~/.nub/shims hardlinks (refreshShims)
7
+
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+
12
+ // "@nubjs/nub-<platform>" or undefined (unsupported platform, or platform.js
13
+ // absent — shouldn't happen; the launcher handles both at runtime).
14
+ function platformPkg() {
15
+ try {
16
+ return require("./platform.js").platformPackage().pkg;
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
2
22
  // Set the execute bit on the platform binary at INSTALL time.
3
23
  //
4
24
  // npm normalizes file modes on extract: a file referenced by a package's `bin`
@@ -19,21 +39,7 @@
19
39
  // Best-effort and silent: a missing/already-executable binary, an unsupported
20
40
  // platform, or a PM that ran us in a sandbox are all non-fatal — the launcher's
21
41
  // runtime chmod is the second line of defense.
22
-
23
- const fs = require("fs");
24
- const path = require("path");
25
-
26
- function chmodExecutable() {
27
- let platformPackage;
28
- try {
29
- ({ platformPackage } = require("./platform.js"));
30
- } catch {
31
- return; // platform.js absent (shouldn't happen) — let the launcher handle it.
32
- }
33
-
34
- const { pkg } = platformPackage();
35
- if (!pkg) return; // unsupported platform — nothing to chmod.
36
-
42
+ function chmodExecutable(pkg) {
37
43
  const ext = process.platform === "win32" ? ".exe" : "";
38
44
  for (const verb of ["nub", "nubx"]) {
39
45
  let binPath;
@@ -53,4 +59,114 @@ function chmodExecutable() {
53
59
  }
54
60
  }
55
61
 
56
- chmodExecutable();
62
+ // Re-link existing PM shims to the freshly-installed binary.
63
+ //
64
+ // `nub pm shim` populates ~/.nub/shims with HARDLINKS to the nub binary
65
+ // (crates/nub-core/src/pm/shim.rs; spec: wiki/research/package-manager-shims.md).
66
+ // An `npm i -g @nubjs/nub` upgrade extracts a NEW binary — new inode — so the
67
+ // shims keep executing the OLD bytes until re-linked. This is the installer-side
68
+ // re-link: if (and only if) a shims dir already exists, point every entry we own
69
+ // back at the fresh binary. It never CREATES the dir or missing entries — shims
70
+ // are `nub pm shim`'s explicit opt-in; this only refreshes one. (Under
71
+ // `sudo npm i -g`, os.homedir() is root's HOME, which has no shims dir — the
72
+ // correct do-nothing path.) Same names, same remove-then-link with hardlink →
73
+ // cross-device copy fallback, same dev+ino currency check as the Rust installer.
74
+ //
75
+ // Best-effort like everything here: any per-entry failure skips that entry — a
76
+ // stale shim is degraded, not broken (the shim-dir `nub` entry re-execs the real
77
+ // nub found past the shim dir, and `nub pm shim` re-links by hand).
78
+ const SHIM_NAMES = ["npm", "npx", "pnpm", "pnpx", "yarn", "yarnpkg", "nub"];
79
+
80
+ function refreshShims(pkg) {
81
+ const ext = process.platform === "win32" ? ".exe" : "";
82
+ let binPath, binStat;
83
+ try {
84
+ binPath = require.resolve(`${pkg}/bin/nub${ext}`);
85
+ binStat = fs.statSync(binPath);
86
+ } catch {
87
+ return;
88
+ }
89
+
90
+ const shimDir = path.join(os.homedir(), ".nub", "shims");
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(shimDir); // ENOENT/ENOTDIR = no opt-in, do nothing
94
+ } catch {
95
+ return;
96
+ }
97
+
98
+ // Writers on the shim dir serialize on ~/.nub/shims.lock (the Rust ShimLock —
99
+ // see shim.rs). Take it O_EXCL; steal a stale one (>30s old = the holder died
100
+ // mid-operation); a FRESH lock means a live `nub pm shim`/`unshim` is rewriting
101
+ // the dir right now — skip the refresh rather than interleave or block the
102
+ // install. Lock failures for any other reason (read-only ~/.nub) proceed
103
+ // unlocked, matching the Rust side's best-effort posture.
104
+ const lockPath = shimDir + ".lock";
105
+ let locked = false;
106
+ try {
107
+ fs.writeFileSync(lockPath, "", { flag: "wx" });
108
+ locked = true;
109
+ } catch (e) {
110
+ if (e && e.code === "EEXIST") {
111
+ let stale = true; // unreadable mtime counts as stale, like the Rust impl
112
+ try {
113
+ stale = Date.now() - fs.statSync(lockPath).mtimeMs > 30_000;
114
+ } catch {}
115
+ if (!stale) return;
116
+ try {
117
+ fs.unlinkSync(lockPath);
118
+ fs.writeFileSync(lockPath, "", { flag: "wx" });
119
+ locked = true;
120
+ } catch {
121
+ return;
122
+ }
123
+ }
124
+ }
125
+
126
+ let refreshed = 0;
127
+ try {
128
+ for (const name of SHIM_NAMES) {
129
+ const file = name + ext; // Windows shims are <name>.exe
130
+ if (!entries.includes(file)) continue; // refresh-only: never create
131
+ const target = path.join(shimDir, file);
132
+ try {
133
+ let st;
134
+ try {
135
+ st = fs.statSync(target);
136
+ } catch {} // broken entry — fall through and re-link it
137
+ if (st && st.dev === binStat.dev && st.ino === binStat.ino) {
138
+ continue; // already a hardlink of the fresh bytes (re-run, npm rebuild)
139
+ }
140
+ // Remove-then-link, same documented non-atomic window as the Rust
141
+ // installer (a concurrent exec of this exact name can hit ENOENT for
142
+ // microseconds; the lock above serializes writers, not readers).
143
+ fs.unlinkSync(target);
144
+ try {
145
+ fs.linkSync(binPath, target); // zero disk; +x travels with the inode
146
+ } catch {
147
+ fs.copyFileSync(binPath, target); // shim dir on another filesystem
148
+ fs.chmodSync(target, 0o755); // a copy doesn't inherit the inode's +x
149
+ }
150
+ refreshed++;
151
+ } catch {
152
+ // Skip this entry silently — degraded, not broken (see above).
153
+ }
154
+ }
155
+ } finally {
156
+ if (locked) {
157
+ try {
158
+ fs.unlinkSync(lockPath);
159
+ } catch {}
160
+ }
161
+ }
162
+
163
+ if (refreshed > 0) {
164
+ console.log(`refreshed ${refreshed} nub shim${refreshed === 1 ? "" : "s"} in ~/.nub/shims`);
165
+ }
166
+ }
167
+
168
+ const pkg = platformPkg();
169
+ if (pkg) {
170
+ chmodExecutable(pkg);
171
+ refreshShims(pkg); // after chmod, so the linked inode already carries +x
172
+ }