@bobfrankston/npmglobalize 1.0.206 → 1.0.208
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/lib.d.ts +20 -0
- package/lib.js +197 -4
- package/package.json +3 -3
package/lib.d.ts
CHANGED
|
@@ -287,6 +287,26 @@ export type UnpublishedDep = {
|
|
|
287
287
|
path: string;
|
|
288
288
|
reason: 'new' | 'update';
|
|
289
289
|
};
|
|
290
|
+
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
291
|
+
* the spec in package.json and the junction npm created in node_modules.
|
|
292
|
+
*
|
|
293
|
+
* npm resolves a `file:` spec by string concatenation against the cwd and writes
|
|
294
|
+
* the result as the link target — it never canonicalizes case, and never checks
|
|
295
|
+
* that the target exists. On Windows that rots invisibly: `projects/nodejs/x`
|
|
296
|
+
* resolves fine when the directory is really `projects/NodeJS/x`, so nothing
|
|
297
|
+
* complains until the path reaches somewhere case matters. Two places it does:
|
|
298
|
+
* WSL and Linux deploys can't resolve it at all, and npm's own arborist keys
|
|
299
|
+
* nodes by path string, so one directory reached under two spellings becomes two
|
|
300
|
+
* nodes and tree loading dies with "Cannot read properties of null".
|
|
301
|
+
*
|
|
302
|
+
* Both records must be repaired together. Fixing only the manifest leaves the
|
|
303
|
+
* stale junction in place (npm will not recreate a link it thinks is satisfied);
|
|
304
|
+
* fixing only the junction lets the next `npm install` write the bad case back. */
|
|
305
|
+
export declare function verifyDepPathCase(cwd: string, pkg: any): {
|
|
306
|
+
specs: string[];
|
|
307
|
+
links: string[];
|
|
308
|
+
missing: string[];
|
|
309
|
+
};
|
|
290
310
|
export declare function transformDeps(pkg: any, baseDir: string, verbose?: boolean, forcePublish?: boolean): {
|
|
291
311
|
transformed: boolean;
|
|
292
312
|
unpublished: UnpublishedDep[];
|
package/lib.js
CHANGED
|
@@ -1320,6 +1320,125 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
|
|
|
1320
1320
|
return false;
|
|
1321
1321
|
}
|
|
1322
1322
|
}
|
|
1323
|
+
/** The canonical on-disk spelling of `target`, or null if it does not exist.
|
|
1324
|
+
* Windows answers path queries case-insensitively, so the only way to learn how
|
|
1325
|
+
* a directory is really spelled is to ask its parent to list it — a segment at a
|
|
1326
|
+
* time, from the root down. */
|
|
1327
|
+
function canonicalCase(target) {
|
|
1328
|
+
const full = path.resolve(target);
|
|
1329
|
+
if (!fs.existsSync(full))
|
|
1330
|
+
return null;
|
|
1331
|
+
const parsed = path.parse(full);
|
|
1332
|
+
const parts = full.slice(parsed.root.length).split(/[\\/]/).filter(Boolean);
|
|
1333
|
+
let cur = parsed.root.toUpperCase();
|
|
1334
|
+
for (const part of parts) {
|
|
1335
|
+
let entries;
|
|
1336
|
+
try {
|
|
1337
|
+
entries = fs.readdirSync(cur);
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
const real = entries.find(e => e.toLowerCase() === part.toLowerCase());
|
|
1343
|
+
if (!real)
|
|
1344
|
+
return null;
|
|
1345
|
+
cur = path.join(cur, real);
|
|
1346
|
+
}
|
|
1347
|
+
return cur;
|
|
1348
|
+
}
|
|
1349
|
+
/** True when two paths name the same place but are spelled with different case
|
|
1350
|
+
* in a directory name. A bare drive-letter difference (`y:` vs `Y:`) doesn't
|
|
1351
|
+
* count — it resolves identically everywhere, including WSL's /mnt/y. */
|
|
1352
|
+
function caseDiffers(a, b) {
|
|
1353
|
+
const strip = (p) => p.replace(/[\\/]+$/, '').slice(path.parse(p).root.length);
|
|
1354
|
+
return strip(a) !== strip(b);
|
|
1355
|
+
}
|
|
1356
|
+
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
1357
|
+
* the spec in package.json and the junction npm created in node_modules.
|
|
1358
|
+
*
|
|
1359
|
+
* npm resolves a `file:` spec by string concatenation against the cwd and writes
|
|
1360
|
+
* the result as the link target — it never canonicalizes case, and never checks
|
|
1361
|
+
* that the target exists. On Windows that rots invisibly: `projects/nodejs/x`
|
|
1362
|
+
* resolves fine when the directory is really `projects/NodeJS/x`, so nothing
|
|
1363
|
+
* complains until the path reaches somewhere case matters. Two places it does:
|
|
1364
|
+
* WSL and Linux deploys can't resolve it at all, and npm's own arborist keys
|
|
1365
|
+
* nodes by path string, so one directory reached under two spellings becomes two
|
|
1366
|
+
* nodes and tree loading dies with "Cannot read properties of null".
|
|
1367
|
+
*
|
|
1368
|
+
* Both records must be repaired together. Fixing only the manifest leaves the
|
|
1369
|
+
* stale junction in place (npm will not recreate a link it thinks is satisfied);
|
|
1370
|
+
* fixing only the junction lets the next `npm install` write the bad case back. */
|
|
1371
|
+
export function verifyDepPathCase(cwd, pkg) {
|
|
1372
|
+
const specs = [];
|
|
1373
|
+
const links = [];
|
|
1374
|
+
const missing = [];
|
|
1375
|
+
for (const bucket of ['dependencies', 'devDependencies', '.dependencies']) {
|
|
1376
|
+
const deps = pkg?.[bucket];
|
|
1377
|
+
if (!deps || typeof deps !== 'object')
|
|
1378
|
+
continue;
|
|
1379
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
1380
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
1381
|
+
continue;
|
|
1382
|
+
const abs = path.resolve(cwd, spec.slice('file:'.length));
|
|
1383
|
+
const canonical = canonicalCase(abs);
|
|
1384
|
+
if (!canonical) {
|
|
1385
|
+
missing.push(`${name} -> ${spec} (no such directory)`);
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (!caseDiffers(abs, canonical))
|
|
1389
|
+
continue;
|
|
1390
|
+
// Keep the spec relative, the way it was written.
|
|
1391
|
+
const rel = path.relative(cwd, canonical).split(path.sep).join('/');
|
|
1392
|
+
deps[name] = `file:${rel}`;
|
|
1393
|
+
specs.push(`${name}: ${spec} -> file:${rel}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
// The junction npm already made from the old spelling.
|
|
1397
|
+
for (const bucket of ['dependencies', 'devDependencies', '.dependencies']) {
|
|
1398
|
+
const deps = pkg?.[bucket];
|
|
1399
|
+
if (!deps || typeof deps !== 'object')
|
|
1400
|
+
continue;
|
|
1401
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
1402
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
1403
|
+
continue;
|
|
1404
|
+
const linkPath = path.join(cwd, 'node_modules', ...name.split('/'));
|
|
1405
|
+
let stat;
|
|
1406
|
+
try {
|
|
1407
|
+
stat = fs.lstatSync(linkPath);
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (!stat.isSymbolicLink())
|
|
1413
|
+
continue;
|
|
1414
|
+
let current;
|
|
1415
|
+
try {
|
|
1416
|
+
current = fs.readlinkSync(linkPath).replace(/[\\/]+$/, '');
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const canonical = canonicalCase(current);
|
|
1422
|
+
if (!canonical || !caseDiffers(current, canonical))
|
|
1423
|
+
continue;
|
|
1424
|
+
try {
|
|
1425
|
+
// On Windows a directory junction is removed with rmdir, not unlink,
|
|
1426
|
+
// and neither follows the link — which matters, because deleting
|
|
1427
|
+
// through it would take the real package tree with it.
|
|
1428
|
+
if (process.platform === 'win32')
|
|
1429
|
+
fs.rmdirSync(linkPath);
|
|
1430
|
+
else
|
|
1431
|
+
fs.unlinkSync(linkPath);
|
|
1432
|
+
fs.symlinkSync(canonical, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1433
|
+
links.push(`${name}: ${current} -> ${canonical}`);
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
missing.push(`${name}: could not repair link (${error.message})`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return { specs, links, missing };
|
|
1441
|
+
}
|
|
1323
1442
|
export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
|
|
1324
1443
|
let transformed = false;
|
|
1325
1444
|
const unpublished = [];
|
|
@@ -4180,6 +4299,56 @@ async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
|
|
|
4180
4299
|
process.stdout.write(' timed out\n');
|
|
4181
4300
|
return false;
|
|
4182
4301
|
}
|
|
4302
|
+
/** After a global WSL install, confirm a NON-interactive shell actually resolves
|
|
4303
|
+
* the binaries we just installed, rather than older copies of them.
|
|
4304
|
+
*
|
|
4305
|
+
* WSL's npm prefix is per-user (~/.npm-global), and that directory reaches PATH
|
|
4306
|
+
* only via ~/.bashrc — which returns immediately when the shell is not
|
|
4307
|
+
* interactive. Every `wsl <cmd>` is non-interactive, so it searches the default
|
|
4308
|
+
* PATH from /etc/environment instead, where a stale /usr/local/bin entry left by
|
|
4309
|
+
* an earlier root-prefix install keeps winning. The install genuinely succeeds
|
|
4310
|
+
* and the version reported by `wsl <tool>` is genuinely stale, which is exactly
|
|
4311
|
+
* why this went unnoticed for four months. Repair with a symlink rather than a
|
|
4312
|
+
* copy, so it tracks later installs on its own. */
|
|
4313
|
+
async function verifyWslGlobalBins(cwd) {
|
|
4314
|
+
let names = [];
|
|
4315
|
+
try {
|
|
4316
|
+
const pkg = readPackageJson(cwd);
|
|
4317
|
+
if (typeof pkg?.bin === 'string')
|
|
4318
|
+
names = [String(pkg.name || '').split('/').pop()].filter(Boolean);
|
|
4319
|
+
else if (pkg?.bin && typeof pkg.bin === 'object')
|
|
4320
|
+
names = Object.keys(pkg.bin);
|
|
4321
|
+
}
|
|
4322
|
+
catch {
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
if (!names.length)
|
|
4326
|
+
return;
|
|
4327
|
+
const prefixResult = await runCommandAsync('wsl', ['npm', 'config', 'get', 'prefix'], { silent: true });
|
|
4328
|
+
const prefix = (prefixResult.output || '').trim();
|
|
4329
|
+
if (!prefixResult.success || !prefix.startsWith('/'))
|
|
4330
|
+
return;
|
|
4331
|
+
for (const name of names) {
|
|
4332
|
+
const expected = `${prefix}/bin/${name}`;
|
|
4333
|
+
// `bash -c` is non-interactive — the same shape as a bare `wsl <cmd>`,
|
|
4334
|
+
// which is the case that breaks. A login shell would mask the problem.
|
|
4335
|
+
const lookup = await runCommandAsync('wsl', ['bash', '-c', `command -v ${name} || true`], { silent: true });
|
|
4336
|
+
const found = (lookup.output || '').trim();
|
|
4337
|
+
if (!found || found === expected)
|
|
4338
|
+
continue;
|
|
4339
|
+
console.log(colors.yellow(` WSL resolves ${name} to ${found}, not the copy just installed at ${expected}`));
|
|
4340
|
+
if (!found.startsWith('/usr/local/bin/')) {
|
|
4341
|
+
recordBuildIssue(name, 'warning', `WSL resolves ${name} to ${found} instead of ${expected}, so scripts run a different version than an interactive shell does. Left alone: only /usr/local/bin shadows are repaired automatically.`);
|
|
4342
|
+
continue;
|
|
4343
|
+
}
|
|
4344
|
+
const repair = await runCommandAsync('wsl', ['bash', '-c', `sudo -n rm -f ${found} && sudo -n ln -s ${expected} ${found}`], { silent: true });
|
|
4345
|
+
if (repair.success) {
|
|
4346
|
+
console.log(colors.green(` ✓ Repaired WSL shadow: ${found} -> ${expected}`));
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
recordBuildIssue(name, 'warning', `WSL resolves ${name} to a stale ${found}. Repair with: wsl sudo rm ${found} && wsl sudo ln -s ${expected} ${found}`);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4183
4352
|
export async function installInWsl(wslArgs, opts = {}) {
|
|
4184
4353
|
// Same trust rule as the Windows installs: allow our own packages'
|
|
4185
4354
|
// install scripts, leave third-party ones gated. Probed against WSL's
|
|
@@ -4198,9 +4367,16 @@ export async function installInWsl(wslArgs, opts = {}) {
|
|
|
4198
4367
|
process.stderr.write(r.stderr);
|
|
4199
4368
|
return r;
|
|
4200
4369
|
};
|
|
4370
|
+
// Every success path leaves through here, so the shadow check cannot be
|
|
4371
|
+
// skipped by whichever retry happened to be the one that worked.
|
|
4372
|
+
const succeed = async (fixed) => {
|
|
4373
|
+
if (opts.cwd && wslArgs.includes('-g'))
|
|
4374
|
+
await verifyWslGlobalBins(opts.cwd);
|
|
4375
|
+
return { success: true, fixed };
|
|
4376
|
+
};
|
|
4201
4377
|
let result = await runOnce();
|
|
4202
4378
|
if (result.success)
|
|
4203
|
-
return
|
|
4379
|
+
return await succeed(false);
|
|
4204
4380
|
let combined = (result.output || '') + '\n' + (result.stderr || '');
|
|
4205
4381
|
// EACCES on a root-owned npm prefix → switch to a user prefix and retry.
|
|
4206
4382
|
if (/EACCES/.test(combined) && /\/usr\/(?:local\/)?lib\/node_modules/.test(combined)) {
|
|
@@ -4216,7 +4392,7 @@ export async function installInWsl(wslArgs, opts = {}) {
|
|
|
4216
4392
|
console.log(colors.green('✓ WSL npm prefix set to ~/.npm-global; PATH appended to ~/.bashrc'));
|
|
4217
4393
|
result = await runOnce();
|
|
4218
4394
|
if (result.success)
|
|
4219
|
-
return
|
|
4395
|
+
return await succeed(true);
|
|
4220
4396
|
combined = (result.output || '') + '\n' + (result.stderr || '');
|
|
4221
4397
|
}
|
|
4222
4398
|
// E404 on a scoped package has TWO causes, and npm gives the same error for
|
|
@@ -4233,7 +4409,7 @@ export async function installInWsl(wslArgs, opts = {}) {
|
|
|
4233
4409
|
console.log(colors.green('✓ Synced npm token to WSL'));
|
|
4234
4410
|
result = await runOnce();
|
|
4235
4411
|
if (result.success)
|
|
4236
|
-
return
|
|
4412
|
+
return await succeed(true);
|
|
4237
4413
|
}
|
|
4238
4414
|
else {
|
|
4239
4415
|
console.error(colors.yellow(' Could not authenticate WSL npm. Run `wsl npm login` (or sync your ~/.npmrc token) and retry.'));
|
|
@@ -4245,7 +4421,7 @@ export async function installInWsl(wslArgs, opts = {}) {
|
|
|
4245
4421
|
if (await waitForNpmVersionInWsl(spec)) {
|
|
4246
4422
|
result = await runOnce();
|
|
4247
4423
|
if (result.success)
|
|
4248
|
-
return
|
|
4424
|
+
return await succeed(true);
|
|
4249
4425
|
}
|
|
4250
4426
|
else {
|
|
4251
4427
|
console.error(colors.yellow(` ${spec} still not visible to WSL's npm after waiting — try the WSL install again shortly.`));
|
|
@@ -6601,6 +6777,23 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6601
6777
|
}
|
|
6602
6778
|
}
|
|
6603
6779
|
}
|
|
6780
|
+
// Vet file: path casing before transformDeps swaps the specs out — this is the
|
|
6781
|
+
// last moment the real paths are still in hand. Repairs the manifest and the
|
|
6782
|
+
// node_modules junction together; fixing either alone is undone by the other.
|
|
6783
|
+
const caseCheck = verifyDepPathCase(cwd, pkg);
|
|
6784
|
+
if (caseCheck.specs.length || caseCheck.links.length) {
|
|
6785
|
+
for (const fix of caseCheck.specs)
|
|
6786
|
+
console.log(colors.green(` ✓ Corrected file: path case in package.json — ${fix}`));
|
|
6787
|
+
for (const fix of caseCheck.links)
|
|
6788
|
+
console.log(colors.green(` ✓ Corrected node_modules link case — ${fix}`));
|
|
6789
|
+
if (caseCheck.specs.length)
|
|
6790
|
+
writePackageJson(cwd, pkg);
|
|
6791
|
+
recordBuildIssue(pkg.name || path.basename(cwd), 'warning', `Corrected ${caseCheck.specs.length} file: path spec(s) and ${caseCheck.links.length} node_modules link(s) whose letter case did not match the directory on disk. npm does not canonicalize these, and the wrong case breaks WSL, Linux deploys, and npm's own tree loading.`);
|
|
6792
|
+
}
|
|
6793
|
+
for (const gone of caseCheck.missing) {
|
|
6794
|
+
console.error(colors.red(` ✗ file: dependency path does not exist — ${gone}`));
|
|
6795
|
+
recordBuildIssue(pkg.name || path.basename(cwd), 'error', `file: dependency path does not exist — ${gone}`);
|
|
6796
|
+
}
|
|
6604
6797
|
// Transform dependencies
|
|
6605
6798
|
if (verbose) {
|
|
6606
6799
|
console.log('Transforming file: dependencies...');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.208",
|
|
4
4
|
"description": "Transform file: dependencies to npm versions for publishing",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
35
35
|
"@bobfrankston/importgen": "^0.1.40",
|
|
36
|
-
"@bobfrankston/themecolors": "^0.1.
|
|
36
|
+
"@bobfrankston/themecolors": "^0.1.9",
|
|
37
37
|
"@bobfrankston/userconfig": "^1.0.11",
|
|
38
38
|
"@npmcli/package-json": "^7.0.4",
|
|
39
39
|
"json5": "^2.2.3",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
63
63
|
"@bobfrankston/importgen": "^0.1.40",
|
|
64
|
-
"@bobfrankston/themecolors": "^0.1.
|
|
64
|
+
"@bobfrankston/themecolors": "^0.1.9",
|
|
65
65
|
"@bobfrankston/userconfig": "^1.0.11",
|
|
66
66
|
"@npmcli/package-json": "^7.0.4",
|
|
67
67
|
"json5": "^2.2.3",
|