@bobfrankston/npmglobalize 1.0.205 → 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 +30 -0
- package/lib.js +241 -1
- package/package.json +5 -5
package/lib.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export declare function clearBuildIssues(): void;
|
|
|
25
25
|
/** Extract the first TypeScript error line from build output for the summary.
|
|
26
26
|
* Returns a short string like "file.ts(42,5): error TS2339: Property 'foo' ..." */
|
|
27
27
|
export declare function extractFirstTscError(output: string): string | null;
|
|
28
|
+
/** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
|
|
29
|
+
* on the file being compiled is frequently not that file's fault: the copy of X in
|
|
30
|
+
* node_modules carries no `.d.ts` whatsoever. That happens when X was published at
|
|
31
|
+
* a moment its declaration output was not on disk, so the tarball ships JS only and
|
|
32
|
+
* every consumer resolving X through the registry fails identically. tsc's stock
|
|
33
|
+
* advice — `npm i --save-dev @types/bobfrankston__hlib` — then sends the user after
|
|
34
|
+
* a types package that does not and will never exist, which is worse than no advice.
|
|
35
|
+
* Recognize the shape and say what actually fixes it.
|
|
36
|
+
* Returns one diagnosis line per untyped module; empty when TS7016 has another cause. */
|
|
37
|
+
export declare function diagnoseUntypedDeps(cwd: string, buildOutput: string): string[];
|
|
28
38
|
/** One package that depends on this one, recorded in this package's
|
|
29
39
|
* .globalize.json5 when that package publishes. */
|
|
30
40
|
export interface UpstreamEntry {
|
|
@@ -277,6 +287,26 @@ export type UnpublishedDep = {
|
|
|
277
287
|
path: string;
|
|
278
288
|
reason: 'new' | 'update';
|
|
279
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
|
+
};
|
|
280
310
|
export declare function transformDeps(pkg: any, baseDir: string, verbose?: boolean, forcePublish?: boolean): {
|
|
281
311
|
transformed: boolean;
|
|
282
312
|
unpublished: UnpublishedDep[];
|
package/lib.js
CHANGED
|
@@ -79,6 +79,97 @@ export function extractFirstTscError(output) {
|
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
+
/** Does `dir` contain any `.d.ts` at all? Bounded, and never descends into a
|
|
83
|
+
* nested `node_modules` — we are asking about this package's own output. */
|
|
84
|
+
function hasDeclarationFiles(dir, depth = 3) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
if (e.isFile() && e.name.endsWith('.d.ts'))
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (depth <= 0)
|
|
97
|
+
return false;
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.'))
|
|
100
|
+
continue;
|
|
101
|
+
if (hasDeclarationFiles(path.join(dir, e.name), depth - 1))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/** The spec a consumer declares for `name`. `.dependencies` is checked first
|
|
107
|
+
* because during a publish the live `file:` paths live there while
|
|
108
|
+
* `dependencies` temporarily holds the npm refs that were swapped in. */
|
|
109
|
+
function declaredDepSpec(pkg, name) {
|
|
110
|
+
for (const bucket of ['.dependencies', 'dependencies', 'devDependencies']) {
|
|
111
|
+
const spec = pkg?.[bucket]?.[name];
|
|
112
|
+
if (typeof spec === 'string')
|
|
113
|
+
return spec;
|
|
114
|
+
}
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
/** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
|
|
118
|
+
* on the file being compiled is frequently not that file's fault: the copy of X in
|
|
119
|
+
* node_modules carries no `.d.ts` whatsoever. That happens when X was published at
|
|
120
|
+
* a moment its declaration output was not on disk, so the tarball ships JS only and
|
|
121
|
+
* every consumer resolving X through the registry fails identically. tsc's stock
|
|
122
|
+
* advice — `npm i --save-dev @types/bobfrankston__hlib` — then sends the user after
|
|
123
|
+
* a types package that does not and will never exist, which is worse than no advice.
|
|
124
|
+
* Recognize the shape and say what actually fixes it.
|
|
125
|
+
* Returns one diagnosis line per untyped module; empty when TS7016 has another cause. */
|
|
126
|
+
export function diagnoseUntypedDeps(cwd, buildOutput) {
|
|
127
|
+
if (!buildOutput)
|
|
128
|
+
return [];
|
|
129
|
+
const named = new Set();
|
|
130
|
+
const re = /error TS7016: Could not find a declaration file for module '([^']+)'/g;
|
|
131
|
+
for (let m = re.exec(buildOutput); m; m = re.exec(buildOutput))
|
|
132
|
+
named.add(m[1]);
|
|
133
|
+
if (!named.size)
|
|
134
|
+
return [];
|
|
135
|
+
let consumer = {};
|
|
136
|
+
try {
|
|
137
|
+
consumer = readPackageJson(cwd);
|
|
138
|
+
}
|
|
139
|
+
catch { /* an unreadable consumer still leaves the dep diagnosable */ }
|
|
140
|
+
const scopeOf = (n) => n.startsWith('@') && n.includes('/') ? n.slice(0, n.indexOf('/')) : '';
|
|
141
|
+
const lines = [];
|
|
142
|
+
for (const name of named) {
|
|
143
|
+
if (name.startsWith('.'))
|
|
144
|
+
continue; // a relative import — a code bug, not a packaging one
|
|
145
|
+
const installed = path.join(cwd, 'node_modules', ...name.split('/'));
|
|
146
|
+
if (!fs.existsSync(path.join(installed, 'package.json')))
|
|
147
|
+
continue;
|
|
148
|
+
if (hasDeclarationFiles(installed))
|
|
149
|
+
continue; // declarations are present; TS7016 came from something else
|
|
150
|
+
let version = '';
|
|
151
|
+
try {
|
|
152
|
+
version = readPackageJson(installed).version || '';
|
|
153
|
+
}
|
|
154
|
+
catch { /* version is decoration here */ }
|
|
155
|
+
const at = version ? `@${version}` : '';
|
|
156
|
+
// A `file:` spec points at live source we can look at directly, which tells
|
|
157
|
+
// us whether the missing declarations are a build problem or a stale install.
|
|
158
|
+
const spec = declaredDepSpec(consumer, name);
|
|
159
|
+
const source = spec.startsWith('file:') ? path.resolve(cwd, spec.slice('file:'.length)) : '';
|
|
160
|
+
if (source && fs.existsSync(source)) {
|
|
161
|
+
lines.push(hasDeclarationFiles(source)
|
|
162
|
+
? `${name} resolves to a copy in node_modules with no .d.ts, but its source at ${source} has them — the install is stale. Run \`npm install\` in ${cwd}.`
|
|
163
|
+
: `${name}'s source at ${source} emits no .d.ts — set "declaration": true in its tsconfig and rebuild it.`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const ownScope = scopeOf(name) && scopeOf(name) === scopeOf(consumer?.name || '');
|
|
167
|
+
lines.push(ownScope
|
|
168
|
+
? `${name}${at} was published with no .d.ts in the tarball, so every consumer that installs it from npm fails this way — it is not a problem in ${consumer?.name || 'this package'}. Rebuild and republish ${name} (\`npmglobalize\` in its source directory), or depend on its source with a file: path. Ignore tsc's @types/… suggestion; no such package exists.`
|
|
169
|
+
: `${name}${at} ships no type declarations. Install its @types package if one exists, or add a .d.ts declaring the module.`);
|
|
170
|
+
}
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
82
173
|
/**
|
|
83
174
|
* Remove 'nul' files from a directory tree (Windows reserved name issue).
|
|
84
175
|
* These files break git and npm on Windows. Uses \\?\ prefix to bypass name validation.
|
|
@@ -1229,6 +1320,125 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
|
|
|
1229
1320
|
return false;
|
|
1230
1321
|
}
|
|
1231
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
|
+
}
|
|
1232
1442
|
export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
|
|
1233
1443
|
let transformed = false;
|
|
1234
1444
|
const unpublished = [];
|
|
@@ -3218,8 +3428,21 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3218
3428
|
if (buildOutput)
|
|
3219
3429
|
console.error(buildOutput);
|
|
3220
3430
|
console.error(colors.red(`Build failed in ${pkg.name || cwd}`));
|
|
3431
|
+
const label = pkg.name || path.basename(cwd);
|
|
3432
|
+
// tsc reports a missing-declarations failure against the file that imports the
|
|
3433
|
+
// dep, and suggests an @types package that does not exist for private scopes.
|
|
3434
|
+
// When the real cause is a dep installed without any .d.ts, say so plainly and
|
|
3435
|
+
// put THAT in the summary — the raw tsc line is already echoed above.
|
|
3436
|
+
const untyped = diagnoseUntypedDeps(cwd, buildOutput);
|
|
3437
|
+
if (untyped.length) {
|
|
3438
|
+
for (const line of untyped) {
|
|
3439
|
+
console.error(colors.yellow(` ${line}`));
|
|
3440
|
+
recordBuildIssue(label, 'error', line);
|
|
3441
|
+
}
|
|
3442
|
+
return false;
|
|
3443
|
+
}
|
|
3221
3444
|
const firstErr = extractFirstTscError(buildOutput);
|
|
3222
|
-
recordBuildIssue(
|
|
3445
|
+
recordBuildIssue(label, 'error', firstErr || 'Build failed');
|
|
3223
3446
|
return false;
|
|
3224
3447
|
}
|
|
3225
3448
|
/** Walk `file:` deps depth-first (deps before consumers) and build each one
|
|
@@ -6497,6 +6720,23 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6497
6720
|
}
|
|
6498
6721
|
}
|
|
6499
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
|
+
}
|
|
6500
6740
|
// Transform dependencies
|
|
6501
6741
|
if (verbose) {
|
|
6502
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,8 +33,8 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
35
35
|
"@bobfrankston/importgen": "^0.1.40",
|
|
36
|
-
"@bobfrankston/themecolors": "^0.1.
|
|
37
|
-
"@bobfrankston/userconfig": "^1.0.
|
|
36
|
+
"@bobfrankston/themecolors": "^0.1.9",
|
|
37
|
+
"@bobfrankston/userconfig": "^1.0.11",
|
|
38
38
|
"@npmcli/package-json": "^7.0.4",
|
|
39
39
|
"json5": "^2.2.3",
|
|
40
40
|
"libnpmversion": "^8.0.3",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
63
63
|
"@bobfrankston/importgen": "^0.1.40",
|
|
64
|
-
"@bobfrankston/themecolors": "^0.1.
|
|
65
|
-
"@bobfrankston/userconfig": "^1.0.
|
|
64
|
+
"@bobfrankston/themecolors": "^0.1.9",
|
|
65
|
+
"@bobfrankston/userconfig": "^1.0.11",
|
|
66
66
|
"@npmcli/package-json": "^7.0.4",
|
|
67
67
|
"json5": "^2.2.3",
|
|
68
68
|
"libnpmversion": "^8.0.3",
|