@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.
- package/lib.d.ts +20 -0
- package/lib.js +136 -0
- 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.
|
|
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.
|
|
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",
|