@bobfrankston/npmglobalize 1.0.206 → 1.0.207

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/lib.d.ts +20 -0
  2. package/lib.js +136 -0
  3. 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 = [];
@@ -6601,6 +6720,23 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6601
6720
  }
6602
6721
  }
6603
6722
  }
6723
+ // Vet file: path casing before transformDeps swaps the specs out — this is the
6724
+ // last moment the real paths are still in hand. Repairs the manifest and the
6725
+ // node_modules junction together; fixing either alone is undone by the other.
6726
+ const caseCheck = verifyDepPathCase(cwd, pkg);
6727
+ if (caseCheck.specs.length || caseCheck.links.length) {
6728
+ for (const fix of caseCheck.specs)
6729
+ console.log(colors.green(` ✓ Corrected file: path case in package.json — ${fix}`));
6730
+ for (const fix of caseCheck.links)
6731
+ console.log(colors.green(` ✓ Corrected node_modules link case — ${fix}`));
6732
+ if (caseCheck.specs.length)
6733
+ writePackageJson(cwd, pkg);
6734
+ 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.`);
6735
+ }
6736
+ for (const gone of caseCheck.missing) {
6737
+ console.error(colors.red(` ✗ file: dependency path does not exist — ${gone}`));
6738
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', `file: dependency path does not exist — ${gone}`);
6739
+ }
6604
6740
  // Transform dependencies
6605
6741
  if (verbose) {
6606
6742
  console.log('Transforming file: dependencies...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.206",
3
+ "version": "1.0.207",
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.8",
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.8",
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",