@bobfrankston/npmglobalize 1.0.197 → 1.0.198
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 +39 -0
- package/lib.d.ts +26 -0
- package/lib.js +144 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -532,6 +532,45 @@ not yet change its behavior based on it. Behavior wiring (e.g. resolving
|
|
|
532
532
|
`file:` deps to the latest published npm version instead of walking siblings)
|
|
533
533
|
is planned.
|
|
534
534
|
|
|
535
|
+
### `upstream` — who consumes this package
|
|
536
|
+
|
|
537
|
+
Bookkeeping, not a setting. When a package with `file:` deps publishes,
|
|
538
|
+
npmglobalize appends an entry to each **dependency's** `.globalize.json5`:
|
|
539
|
+
|
|
540
|
+
```json5
|
|
541
|
+
{
|
|
542
|
+
"install": true,
|
|
543
|
+
|
|
544
|
+
// FYI: packages that depend on this one (immediate consumers,
|
|
545
|
+
// recorded when each of them publishes). Nothing is updated
|
|
546
|
+
// automatically; follow each path's own .globalize.json5 to
|
|
547
|
+
// walk further out.
|
|
548
|
+
"upstream": [
|
|
549
|
+
{"path":"Y:\\dev\\utils\\winpos","version":"2.0.51","updated":"2026-08-09"},
|
|
550
|
+
],
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
So publishing `winpos` (which has `"@bobfrankston/msger": "file:../msgx/msger"`)
|
|
555
|
+
records winpos in **msger's** config. Each entry carries the consumer's
|
|
556
|
+
checkout path, its version at the time, and the date.
|
|
557
|
+
|
|
558
|
+
Only **immediate** consumers are recorded. A full consumer tree is a walk, not
|
|
559
|
+
a copy: follow each entry's path and read that package's own `upstream` list.
|
|
560
|
+
|
|
561
|
+
The list is **FYI for now** — it is recorded, preserved across publishes, and
|
|
562
|
+
printed in the Release Summary of the package that owns it. Nothing is
|
|
563
|
+
rebuilt, republished, or reinstalled on its behalf. An entry is added the
|
|
564
|
+
first time a consumer publishes after this feature landed, and refreshed in
|
|
565
|
+
place on every publish after that.
|
|
566
|
+
|
|
567
|
+
The entry is written into the dependency's own checkout, then committed there
|
|
568
|
+
(`Record upstream <consumer>@<version>`) and pushed if that repo has a remote.
|
|
569
|
+
Only `.globalize.json5` is staged and committed, by pathspec — anything else
|
|
570
|
+
the dependency had staged or modified is left exactly as it was. If the
|
|
571
|
+
dependency isn't a git repo, or ignores `.globalize.json5`, the file is still
|
|
572
|
+
written and the commit is skipped.
|
|
573
|
+
|
|
535
574
|
## Common Workflows
|
|
536
575
|
|
|
537
576
|
### Standard Release
|
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
|
+
/** One package that depends on this one, recorded in this package's
|
|
29
|
+
* .globalize.json5 when that package publishes. */
|
|
30
|
+
export interface UpstreamEntry {
|
|
31
|
+
/** Absolute path of the consumer's checkout */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Consumer's version at the time it was recorded */
|
|
34
|
+
version: string;
|
|
35
|
+
/** Date the entry was last written (YYYY-MM-DD) */
|
|
36
|
+
updated: string;
|
|
37
|
+
}
|
|
28
38
|
/** Options for the globalize operation */
|
|
29
39
|
export interface GlobalizeOptions {
|
|
30
40
|
/** Bump type: patch (default), minor, major */
|
|
@@ -132,6 +142,12 @@ export interface GlobalizeOptions {
|
|
|
132
142
|
* that rely on workspace cross-refs / -public-deps cascade. Opt in when
|
|
133
143
|
* the assumption holds. */
|
|
134
144
|
importCheck?: boolean;
|
|
145
|
+
/** Bookkeeping, not a CLI option: the packages that depend on this one,
|
|
146
|
+
* written into this package's .globalize.json5 when each of them
|
|
147
|
+
* publishes. Immediate consumers only - a full consumer tree is walked by
|
|
148
|
+
* following each entry's own .globalize.json5. FYI for now: recorded and
|
|
149
|
+
* displayed, nothing is updated automatically. */
|
|
150
|
+
upstream?: UpstreamEntry[];
|
|
135
151
|
/** Internal: signals this call is from workspace orchestrator */
|
|
136
152
|
/** Skip the upfront dep-graph prescan */
|
|
137
153
|
noPrescan?: boolean;
|
|
@@ -289,6 +305,16 @@ export declare function prescanDepGraph(baseDir: string, visited?: Set<string>,
|
|
|
289
305
|
export declare function restoreDeps(pkg: any, verbose?: boolean): boolean;
|
|
290
306
|
/** Check if .dependencies exist (already transformed) */
|
|
291
307
|
export declare function hasBackup(pkg: any): boolean;
|
|
308
|
+
/** Record this package in the `upstream` list of each package it depends on
|
|
309
|
+
* via file:, so a library knows who consumes it.
|
|
310
|
+
*
|
|
311
|
+
* Immediate consumers only — each entry's own .globalize.json5 carries its
|
|
312
|
+
* consumers, so the full tree is a walk rather than a copy. Nothing acts on
|
|
313
|
+
* the list yet; it's FYI for a future "update my consumers" pass.
|
|
314
|
+
*
|
|
315
|
+
* Best-effort: a dep whose config can't be written is reported and skipped,
|
|
316
|
+
* never fatal - this runs after a successful publish. */
|
|
317
|
+
export declare function recordUpstream(cwd: string, dryRun?: boolean, verbose?: boolean): Promise<void>;
|
|
292
318
|
/** Get the latest git tag (if any) */
|
|
293
319
|
export declare function getLatestGitTag(cwd: string): string | null;
|
|
294
320
|
/** Check if a git tag exists */
|
package/lib.js
CHANGED
|
@@ -240,6 +240,12 @@ export function writeConfig(dir, config, explicitKeys) {
|
|
|
240
240
|
const existing = readConfig(dir);
|
|
241
241
|
// Filter out temporary flags and default values (unless explicitly set)
|
|
242
242
|
const filtered = {};
|
|
243
|
+
// upstream is bookkeeping written by recordUpstream, never a CLI option, so
|
|
244
|
+
// it isn't in the options object a normal publish passes here. Carry it
|
|
245
|
+
// across or this package's own next publish would erase its consumer list.
|
|
246
|
+
if (config.upstream === undefined && existing.upstream !== undefined) {
|
|
247
|
+
config = { ...config, upstream: existing.upstream };
|
|
248
|
+
}
|
|
243
249
|
const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'publicDeps', 'forcePublish', 'once', 'cleanNestedModules']);
|
|
244
250
|
for (const [key, value] of Object.entries(config)) {
|
|
245
251
|
if (omitKeys.has(key))
|
|
@@ -263,6 +269,19 @@ export function writeConfig(dir, config, explicitKeys) {
|
|
|
263
269
|
const entries = Object.entries(filtered);
|
|
264
270
|
if (entries.length > 0) {
|
|
265
271
|
entries.forEach(([key, value]) => {
|
|
272
|
+
// upstream is a list of records - one per line beats one long line
|
|
273
|
+
if (key === 'upstream' && Array.isArray(value)) {
|
|
274
|
+
lines.push(' // FYI: packages that depend on this one (immediate consumers,');
|
|
275
|
+
lines.push(' // recorded when each of them publishes). Nothing is updated');
|
|
276
|
+
lines.push(' // automatically; follow each path\'s own .globalize.json5 to');
|
|
277
|
+
lines.push(' // walk further out.');
|
|
278
|
+
lines.push(' "upstream": [');
|
|
279
|
+
for (const entry of value) {
|
|
280
|
+
lines.push(` ${JSON.stringify(entry)},`);
|
|
281
|
+
}
|
|
282
|
+
lines.push(' ],');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
266
285
|
const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
|
|
267
286
|
const comma = ','; // JSON5 allows trailing commas
|
|
268
287
|
// Add inline comment for clarity
|
|
@@ -1541,6 +1560,117 @@ export function restoreDeps(pkg, verbose = false) {
|
|
|
1541
1560
|
export function hasBackup(pkg) {
|
|
1542
1561
|
return DEP_KEYS.some(key => pkg['.' + key]);
|
|
1543
1562
|
}
|
|
1563
|
+
/** Every file: dep target of a package, by absolute path.
|
|
1564
|
+
* Looks in the .dependencies backup too, so it finds them whether or not the
|
|
1565
|
+
* file: paths have been restored yet (--nofiles leaves ^versions in place). */
|
|
1566
|
+
function fileDepTargets(pkg, baseDir) {
|
|
1567
|
+
const targets = new Set();
|
|
1568
|
+
for (const key of DEP_KEYS) {
|
|
1569
|
+
for (const source of [pkg[key], pkg['.' + key]]) {
|
|
1570
|
+
if (!source)
|
|
1571
|
+
continue;
|
|
1572
|
+
for (const value of Object.values(source)) {
|
|
1573
|
+
if (isFileRef(value)) {
|
|
1574
|
+
targets.add(resolveFilePath(value, baseDir));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return [...targets];
|
|
1580
|
+
}
|
|
1581
|
+
/** Commit (and push) the .globalize.json5 we just wrote in a dependency's repo,
|
|
1582
|
+
* so the consumer list doesn't sit as an uncommitted change in someone else's
|
|
1583
|
+
* checkout until their next publish.
|
|
1584
|
+
*
|
|
1585
|
+
* Stages and commits that one pathspec, never `git add -A`: whatever else the
|
|
1586
|
+
* dependency's owner had staged or modified stays untouched, so a publish here
|
|
1587
|
+
* can't sweep up someone else's work in progress. */
|
|
1588
|
+
async function commitUpstreamEntry(target, consumer, verbose) {
|
|
1589
|
+
if (!runCommand('git', ['rev-parse', '--git-dir'], { cwd: target, silent: true }).success) {
|
|
1590
|
+
return; // Not a git repo - the file is written, there's nothing to commit
|
|
1591
|
+
}
|
|
1592
|
+
// Stage it first: a config file the dependency has never had before is
|
|
1593
|
+
// untracked, and `git commit -- <path>` rejects a pathspec git doesn't know.
|
|
1594
|
+
const add = runCommand('git', ['add', '--', '.globalize.json5'], { cwd: target, silent: true });
|
|
1595
|
+
if (!add.success) {
|
|
1596
|
+
// Deliberately ignored in that repo - respect it, the file is still written
|
|
1597
|
+
if (/ignored by/i.test(add.stderr + add.output)) {
|
|
1598
|
+
if (verbose)
|
|
1599
|
+
console.log(colors.dim(` .globalize.json5 is git-ignored in ${target} - not committing`));
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
console.error(colors.yellow(` Warning: could not stage upstream entry in ${target}: ${add.stderr.trim() || add.output.trim()}`));
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const commit = runCommand('git', ['commit', '-m', `Record upstream ${consumer}`, '--', '.globalize.json5'], { cwd: target, silent: true });
|
|
1606
|
+
if (!commit.success) {
|
|
1607
|
+
// Re-recording an unchanged entry leaves nothing to commit - not a problem
|
|
1608
|
+
const combined = (commit.output + ' ' + commit.stderr).toLowerCase();
|
|
1609
|
+
if (combined.includes('nothing to commit') || combined.includes('no changes added'))
|
|
1610
|
+
return;
|
|
1611
|
+
console.error(colors.yellow(` Warning: could not commit upstream entry in ${target}: ${commit.stderr.trim() || commit.output.trim()}`));
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (verbose)
|
|
1615
|
+
console.log(colors.dim(` Committed upstream entry in ${target}`));
|
|
1616
|
+
const hasRemote = runCommand('git', ['remote'], { cwd: target, silent: true }).output.trim().length > 0;
|
|
1617
|
+
if (hasRemote)
|
|
1618
|
+
await pushWithProtection(target, verbose);
|
|
1619
|
+
}
|
|
1620
|
+
/** Record this package in the `upstream` list of each package it depends on
|
|
1621
|
+
* via file:, so a library knows who consumes it.
|
|
1622
|
+
*
|
|
1623
|
+
* Immediate consumers only — each entry's own .globalize.json5 carries its
|
|
1624
|
+
* consumers, so the full tree is a walk rather than a copy. Nothing acts on
|
|
1625
|
+
* the list yet; it's FYI for a future "update my consumers" pass.
|
|
1626
|
+
*
|
|
1627
|
+
* Best-effort: a dep whose config can't be written is reported and skipped,
|
|
1628
|
+
* never fatal - this runs after a successful publish. */
|
|
1629
|
+
export async function recordUpstream(cwd, dryRun = false, verbose = false) {
|
|
1630
|
+
let pkg;
|
|
1631
|
+
try {
|
|
1632
|
+
pkg = readPackageJson(cwd);
|
|
1633
|
+
}
|
|
1634
|
+
catch {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const targets = fileDepTargets(pkg, cwd);
|
|
1638
|
+
if (targets.length === 0)
|
|
1639
|
+
return;
|
|
1640
|
+
const entry = {
|
|
1641
|
+
path: path.resolve(cwd),
|
|
1642
|
+
version: pkg.version,
|
|
1643
|
+
updated: new Date().toISOString().slice(0, 10)
|
|
1644
|
+
};
|
|
1645
|
+
for (const target of targets) {
|
|
1646
|
+
try {
|
|
1647
|
+
if (!fs.existsSync(path.join(target, 'package.json')))
|
|
1648
|
+
continue;
|
|
1649
|
+
const targetConfig = readConfig(target);
|
|
1650
|
+
const upstream = [...(targetConfig.upstream ?? [])];
|
|
1651
|
+
// Path identity, case-insensitive: the same checkout reached twice
|
|
1652
|
+
// updates its entry rather than adding a second one.
|
|
1653
|
+
const same = (p) => path.resolve(p).toLowerCase() === entry.path.toLowerCase();
|
|
1654
|
+
const idx = upstream.findIndex(u => same(u.path));
|
|
1655
|
+
if (idx >= 0)
|
|
1656
|
+
upstream[idx] = entry;
|
|
1657
|
+
else
|
|
1658
|
+
upstream.push(entry);
|
|
1659
|
+
if (dryRun) {
|
|
1660
|
+
console.log(` [dry-run] Would record ${pkg.name}@${entry.version} upstream of ${path.basename(target)}`);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
writeConfig(target, { ...targetConfig, upstream });
|
|
1664
|
+
if (verbose) {
|
|
1665
|
+
console.log(colors.dim(` Recorded upstream ${pkg.name}@${entry.version} in ${target}`));
|
|
1666
|
+
}
|
|
1667
|
+
await commitUpstreamEntry(target, `${pkg.name}@${entry.version}`, verbose);
|
|
1668
|
+
}
|
|
1669
|
+
catch (error) {
|
|
1670
|
+
console.error(colors.yellow(` Warning: could not record upstream in ${target}: ${error.message}`));
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1544
1674
|
/** Get the latest git tag (if any) */
|
|
1545
1675
|
export function getLatestGitTag(cwd) {
|
|
1546
1676
|
try {
|
|
@@ -7455,6 +7585,9 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7455
7585
|
else if (!files) {
|
|
7456
7586
|
console.log('Keeping npm versions (--nofiles mode).');
|
|
7457
7587
|
}
|
|
7588
|
+
// Tell each file: dependency that this package consumes it. Runs after the
|
|
7589
|
+
// publish so the version recorded is the one that actually shipped.
|
|
7590
|
+
await recordUpstream(cwd, dryRun, verbose);
|
|
7458
7591
|
console.log(`${timestamp()} Done!`);
|
|
7459
7592
|
// Run final audit report if not already run
|
|
7460
7593
|
const auditAlreadyRun = (fix || updateDeps) && (transformResult.transformed || alreadyTransformed || updateDeps);
|
|
@@ -7491,6 +7624,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7491
7624
|
if (files && transformResult.transformed) {
|
|
7492
7625
|
console.log(` Restored file: deps: ${colors.green('✓')}`);
|
|
7493
7626
|
}
|
|
7627
|
+
// Who consumes what was just published. FYI only - nothing was updated for
|
|
7628
|
+
// them; this is the list you'd walk if you wanted to.
|
|
7629
|
+
{
|
|
7630
|
+
const consumers = readConfig(cwd).upstream ?? [];
|
|
7631
|
+
if (consumers.length > 0) {
|
|
7632
|
+
console.log(` Upstream consumers (not updated): ${consumers.length}`);
|
|
7633
|
+
for (const c of consumers) {
|
|
7634
|
+
console.log(` ${colors.dim('·')} ${path.basename(c.path)} v${c.version} ${colors.dim(`(${c.updated}, ${c.path})`)}`);
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7494
7638
|
// Surface non-default settings the user may have forgotten about
|
|
7495
7639
|
{
|
|
7496
7640
|
const surprises = [];
|