@bobfrankston/npmglobalize 1.0.214 → 1.0.216
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/README.md +17 -0
- package/cli.js +14 -1
- package/lib.d.ts +19 -0
- package/lib.js +74 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -562,6 +562,23 @@ Settings can be saved in `.globalize.json5`:
|
|
|
562
562
|
|
|
563
563
|
Configuration persists across runs. CLI flags override config file.
|
|
564
564
|
|
|
565
|
+
### `.stale` — retired directories
|
|
566
|
+
|
|
567
|
+
A directory that is no longer live or maintained can be retired in place by
|
|
568
|
+
dropping a `.stale` file into it — empty, or better holding one line saying why
|
|
569
|
+
and when. It is a marker file, not a setting in `.globalize.json5`, because
|
|
570
|
+
stale is a property of the directory (most retired directories never had a
|
|
571
|
+
config) and a file is findable without parsing.
|
|
572
|
+
|
|
573
|
+
npmglobalize refuses to build, publish or install a directory marked `.stale`,
|
|
574
|
+
and refuses to cascade into a `file:` dependency that is marked. Both are
|
|
575
|
+
reported as **errors**, not skips: a live package that depends on a retired one
|
|
576
|
+
has a reference that needs fixing. Remove the marker to make the directory live
|
|
577
|
+
again.
|
|
578
|
+
|
|
579
|
+
`cleanprev.ps1` (in `%OneDrive%\xfer\bin`) strips `node_modules`, lock files and
|
|
580
|
+
tsc output from every `.stale` directory the same way it does for `prev/`.
|
|
581
|
+
|
|
565
582
|
### `usePaths` — Standalone packages
|
|
566
583
|
|
|
567
584
|
Default: `true`. Set to `false` (or pass `-no-use-paths` / `-nup`) to mark a
|
package/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* npmglobalize CLI - Transform file: dependencies to npm versions for publishing
|
|
4
4
|
*/
|
|
5
|
-
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations } from './lib.js';
|
|
5
|
+
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations, canonicalCase } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { colors } from './colors.js';
|
|
@@ -549,6 +549,19 @@ export async function main() {
|
|
|
549
549
|
process.exit(1);
|
|
550
550
|
}
|
|
551
551
|
}
|
|
552
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction.
|
|
553
|
+
// `process.cwd()` carries whatever letter case the shell was started with
|
|
554
|
+
// (`y:\dev\...` from a lowercase prompt). Every `file:` junction npm writes
|
|
555
|
+
// inherits that case, and mixed `y:`/`Y:` junction targets are exactly what
|
|
556
|
+
// makes arborist crash with "Cannot read properties of null (reading
|
|
557
|
+
// 'package')" — see `caseDiffers` in lib.ts. Canonicalizing once up front
|
|
558
|
+
// means every npm the cascade spawns writes junctions with the on-disk
|
|
559
|
+
// spelling, so the repair in verifyDepPathCase stops being needed again.
|
|
560
|
+
const canonicalCwd = canonicalCase(cwd);
|
|
561
|
+
if (canonicalCwd && canonicalCwd !== cwd) {
|
|
562
|
+
cwd = canonicalCwd;
|
|
563
|
+
process.chdir(cwd);
|
|
564
|
+
}
|
|
552
565
|
// Report-only TS7-readiness scan (no transform / build / publish).
|
|
553
566
|
if (cliOptions.ts7Report) {
|
|
554
567
|
printTs7Report(cwd);
|
package/lib.d.ts
CHANGED
|
@@ -287,6 +287,25 @@ export type UnpublishedDep = {
|
|
|
287
287
|
path: string;
|
|
288
288
|
reason: 'new' | 'update';
|
|
289
289
|
};
|
|
290
|
+
/** The canonical on-disk spelling of `target`, or null if it does not exist.
|
|
291
|
+
* Windows answers path queries case-insensitively, so the only way to learn how
|
|
292
|
+
* a directory is really spelled is to ask its parent to list it — a segment at a
|
|
293
|
+
* time, from the root down. The drive letter is always uppercased.
|
|
294
|
+
*
|
|
295
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: exported so cli.ts can
|
|
296
|
+
* canonicalize the working directory at startup (see there for why). Deliberately
|
|
297
|
+
* NOT `fs.realpathSync.native`: that would also resolve a junction the user is
|
|
298
|
+
* standing inside, which is a behaviour change nobody asked for. */
|
|
299
|
+
export declare function canonicalCase(target: string): string;
|
|
300
|
+
/** A directory retired in place carries a `.stale` marker file (Bob's convention,
|
|
301
|
+
* 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
|
|
302
|
+
* is empty or holds a line saying why and when. Returns that line (or a fixed
|
|
303
|
+
* phrase when empty), or null when the directory is not marked.
|
|
304
|
+
*
|
|
305
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
|
|
306
|
+
* than a flag in .globalize.json5: stale is a property of a directory, most stale
|
|
307
|
+
* directories never had a config, and a file is findable without parsing. */
|
|
308
|
+
export declare function staleMarker(dir: string): string;
|
|
290
309
|
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
291
310
|
* the spec in package.json and the junction npm created in node_modules.
|
|
292
311
|
*
|
package/lib.js
CHANGED
|
@@ -1323,8 +1323,13 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
|
|
|
1323
1323
|
/** The canonical on-disk spelling of `target`, or null if it does not exist.
|
|
1324
1324
|
* Windows answers path queries case-insensitively, so the only way to learn how
|
|
1325
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
|
-
|
|
1326
|
+
* time, from the root down. The drive letter is always uppercased.
|
|
1327
|
+
*
|
|
1328
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: exported so cli.ts can
|
|
1329
|
+
* canonicalize the working directory at startup (see there for why). Deliberately
|
|
1330
|
+
* NOT `fs.realpathSync.native`: that would also resolve a junction the user is
|
|
1331
|
+
* standing inside, which is a behaviour change nobody asked for. */
|
|
1332
|
+
export function canonicalCase(target) {
|
|
1328
1333
|
const full = path.resolve(target);
|
|
1329
1334
|
if (!fs.existsSync(full))
|
|
1330
1335
|
return null;
|
|
@@ -1347,12 +1352,43 @@ function canonicalCase(target) {
|
|
|
1347
1352
|
return cur;
|
|
1348
1353
|
}
|
|
1349
1354
|
/** True when two paths name the same place but are spelled with different case
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1355
|
+
* anywhere — the drive letter included.
|
|
1356
|
+
*
|
|
1357
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. This used to strip the
|
|
1358
|
+
* root before comparing, on the theory that `y:` vs `Y:` "resolves identically
|
|
1359
|
+
* everywhere". It does on the filesystem; it does NOT inside npm's arborist.
|
|
1360
|
+
* Traced with an instrumented `loadActual` on dbitemx / pings / itemw: arborist
|
|
1361
|
+
* keeps the junction target's case in `realpath` (an exact-string index) but
|
|
1362
|
+
* computes `location` with `path.win32.relative`, which folds drive-letter case.
|
|
1363
|
+
* One directory reached as `Y:\...` and `y:\...` becomes two target nodes at one
|
|
1364
|
+
* location; the second clobbers the first, every Link into the first is left with
|
|
1365
|
+
* `target === null`, and the next same-case Link crashes on `target.package` —
|
|
1366
|
+
* the "Cannot read properties of null (reading 'package')" failure. npm writes
|
|
1367
|
+
* the junction target with whatever drive case the cwd had, so a lowercase shell
|
|
1368
|
+
* is all it takes. Full trace: `.llm/arborist-trace.md`. */
|
|
1352
1369
|
function caseDiffers(a, b) {
|
|
1353
|
-
const strip = (p) => p.replace(/[\\/]+$/, '')
|
|
1370
|
+
const strip = (p) => p.replace(/[\\/]+$/, '');
|
|
1354
1371
|
return strip(a) !== strip(b);
|
|
1355
1372
|
}
|
|
1373
|
+
/** A directory retired in place carries a `.stale` marker file (Bob's convention,
|
|
1374
|
+
* 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
|
|
1375
|
+
* is empty or holds a line saying why and when. Returns that line (or a fixed
|
|
1376
|
+
* phrase when empty), or null when the directory is not marked.
|
|
1377
|
+
*
|
|
1378
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
|
|
1379
|
+
* than a flag in .globalize.json5: stale is a property of a directory, most stale
|
|
1380
|
+
* directories never had a config, and a file is findable without parsing. */
|
|
1381
|
+
export function staleMarker(dir) {
|
|
1382
|
+
const marker = path.join(dir, '.stale');
|
|
1383
|
+
if (!fs.existsSync(marker))
|
|
1384
|
+
return null;
|
|
1385
|
+
let note = '';
|
|
1386
|
+
try {
|
|
1387
|
+
note = fs.readFileSync(marker, 'utf8').split(/\r?\n/).map(l => l.trim()).find(l => l) || '';
|
|
1388
|
+
}
|
|
1389
|
+
catch { /* unreadable note: still stale */ }
|
|
1390
|
+
return note || 'no reason recorded in .stale';
|
|
1391
|
+
}
|
|
1356
1392
|
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
1357
1393
|
* the spec in package.json and the junction npm created in node_modules.
|
|
1358
1394
|
*
|
|
@@ -2311,6 +2347,15 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2311
2347
|
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
2312
2348
|
if (!fs.existsSync(path.join(target, 'package.json')))
|
|
2313
2349
|
continue;
|
|
2350
|
+
// 2026-09-06 — Claude Code (Fable 5.1): never install into, or walk
|
|
2351
|
+
// through, a dep that is retired (`.stale`). Reported as an error so the
|
|
2352
|
+
// stale reference in package.json gets fixed rather than papered over.
|
|
2353
|
+
const targetStale = staleMarker(target);
|
|
2354
|
+
if (targetStale) {
|
|
2355
|
+
console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not installing into a retired directory.`));
|
|
2356
|
+
recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2314
2359
|
let targetPkg;
|
|
2315
2360
|
try {
|
|
2316
2361
|
targetPkg = readPackageJson(target);
|
|
@@ -3488,7 +3533,7 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
|
|
|
3488
3533
|
const deps = pkg?.[key];
|
|
3489
3534
|
if (!deps || typeof deps !== 'object')
|
|
3490
3535
|
continue;
|
|
3491
|
-
for (const [, spec] of Object.entries(deps)) {
|
|
3536
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
3492
3537
|
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
3493
3538
|
continue;
|
|
3494
3539
|
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
@@ -3497,6 +3542,18 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
|
|
|
3497
3542
|
const targetAbs = path.resolve(target);
|
|
3498
3543
|
if (visited.has(targetAbs))
|
|
3499
3544
|
continue;
|
|
3545
|
+
// 2026-09-06 — Claude Code (Fable 5.1): a retired (`.stale`) dep is not
|
|
3546
|
+
// built. Counts as a failed dep so the consumer's build stops here too,
|
|
3547
|
+
// unless -force says to carry on.
|
|
3548
|
+
const targetStale = staleMarker(target);
|
|
3549
|
+
if (targetStale) {
|
|
3550
|
+
console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not building a retired directory.`));
|
|
3551
|
+
recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
|
|
3552
|
+
allOk = false;
|
|
3553
|
+
if (!opts.force)
|
|
3554
|
+
return false;
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3500
3557
|
// Recurse first (deps before consumer)
|
|
3501
3558
|
const childOk = await buildFileDepsTopologically(target, opts, visited);
|
|
3502
3559
|
if (!childOk)
|
|
@@ -6124,6 +6181,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6124
6181
|
publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
|
|
6125
6182
|
publicDeps = false, // -public-deps: cascade public visibility to all deps
|
|
6126
6183
|
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, importCheck = false } = options;
|
|
6184
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: a directory marked
|
|
6185
|
+
// `.stale` is retired. Refuse to build, publish or install it — and because the
|
|
6186
|
+
// dep cascade reaches deps through this same function, refuse to cascade into
|
|
6187
|
+
// one too. The refusal is an error, not a skip: a live package depending on a
|
|
6188
|
+
// stale one is a fact the user has to act on.
|
|
6189
|
+
const staleNote = staleMarker(cwd);
|
|
6190
|
+
if (staleNote) {
|
|
6191
|
+
console.error(colors.red(`✗ ${cwd} is marked .stale (${staleNote}). Not publishing a retired directory.`));
|
|
6192
|
+
recordBuildIssue(path.basename(cwd), 'error', `Directory is marked .stale (${staleNote}); remove the marker to publish it again.`);
|
|
6193
|
+
return false;
|
|
6194
|
+
}
|
|
6127
6195
|
// Show tool version only for recursive dep calls (CLI already prints it at startup)
|
|
6128
6196
|
const toolVersion = getToolVersion();
|
|
6129
6197
|
if (!options._fromWorkspace && !options._fromCli) {
|