@bobfrankston/npmglobalize 1.0.149 → 1.0.150
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 +22 -0
- package/ignorepatterns.json5 +1 -1
- package/lib.d.ts +1 -0
- package/lib.js +154 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,28 @@ Errors abort (unless `--force`); warnings prompt to continue.
|
|
|
94
94
|
|
|
95
95
|
Skip the prescan with `-no-prescan` / `-nps`.
|
|
96
96
|
|
|
97
|
+
#### Workspace skip prescan
|
|
98
|
+
|
|
99
|
+
In workspace mode, before processing begins, each package is also checked to decide whether it actually needs work. A package is **skipped** (no rebuild, no version bump, no publish) only if **all** of these are true:
|
|
100
|
+
|
|
101
|
+
1. **Working tree clean** — `git status --porcelain .` reports no uncommitted changes for that package's directory.
|
|
102
|
+
2. **Version already on npm** — the version in its `package.json` is published to the registry, and no non-bookkeeping commit (anything outside "Pre-release commit" / "Restore file: dependencies" / "Pre-version cleanup" / "Untrack node_modules") is newer than that version's publish timestamp.
|
|
103
|
+
3. **Build is fresh** — every `.ts` source file (excluding `.d.ts`) has a sibling `.js` whose mtime is ≥ the `.ts` mtime. A missing `.js` or an older `.js` counts as stale.
|
|
104
|
+
4. **No sibling workspace `file:` dep flagged for work** — if another workspace package depends on this one via `file:`, and that dep is being updated in this run, this one is also processed (propagates through the graph in topological order).
|
|
105
|
+
|
|
106
|
+
If any condition fails, the package is processed. The prescan prints one line per package with `⟳` for "will process" (and the reason) or `✓` for "skip". Example:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
⟳ mlproc — uncommitted changes
|
|
110
|
+
⟳ pzip — stale build (index.ts newer than index.js)
|
|
111
|
+
⟳ stage — dep pzip is being updated
|
|
112
|
+
✓ puller — skip (clean, published, build fresh)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The summary row for a skipped package shows `– name v1.0.0 (skipped — already up to date)`.
|
|
116
|
+
|
|
117
|
+
Use `--force` or `--force-publish` to bypass the skip prescan and process every package.
|
|
118
|
+
|
|
97
119
|
**Force republish** all file: dependencies even if versions exist:
|
|
98
120
|
```bash
|
|
99
121
|
npmglobalize --force-publish
|
package/ignorepatterns.json5
CHANGED
package/lib.d.ts
CHANGED
package/lib.js
CHANGED
|
@@ -810,6 +810,49 @@ function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
|
|
|
810
810
|
}
|
|
811
811
|
return false;
|
|
812
812
|
}
|
|
813
|
+
/** Walk a dir and return true if any `.ts` source has mtime newer than its sibling `.js`
|
|
814
|
+
* (or the `.js` is missing). Skips node_modules/prev/cruft/.git and declaration files. */
|
|
815
|
+
function hasStaleBuild(rootDir) {
|
|
816
|
+
const SKIP = new Set(['node_modules', 'prev', 'cruft', '.git']);
|
|
817
|
+
function walk(dir) {
|
|
818
|
+
let entries;
|
|
819
|
+
try {
|
|
820
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
return { stale: false };
|
|
824
|
+
}
|
|
825
|
+
for (const e of entries) {
|
|
826
|
+
const full = path.join(dir, e.name);
|
|
827
|
+
if (e.isDirectory()) {
|
|
828
|
+
if (SKIP.has(e.name.toLowerCase()))
|
|
829
|
+
continue;
|
|
830
|
+
const sub = walk(full);
|
|
831
|
+
if (sub.stale)
|
|
832
|
+
return sub;
|
|
833
|
+
}
|
|
834
|
+
else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
|
|
835
|
+
const jsPath = full.slice(0, -3) + '.js';
|
|
836
|
+
try {
|
|
837
|
+
const tsStat = fs.statSync(full);
|
|
838
|
+
let jsStat;
|
|
839
|
+
try {
|
|
840
|
+
jsStat = fs.statSync(jsPath);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
return { stale: true, example: `${e.name} (no .js)` };
|
|
844
|
+
}
|
|
845
|
+
if (tsStat.mtimeMs > jsStat.mtimeMs) {
|
|
846
|
+
return { stale: true, example: `${e.name} newer than ${path.basename(jsPath)}` };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch { /* unreadable — ignore */ }
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { stale: false };
|
|
853
|
+
}
|
|
854
|
+
return walk(rootDir);
|
|
855
|
+
}
|
|
813
856
|
/** Check if local package directory has changes newer than the npm-published version.
|
|
814
857
|
* Detects uncommitted changes or commits made after the version was published. */
|
|
815
858
|
function hasLocalChanges(packageName, version, targetPath, verbose) {
|
|
@@ -3143,9 +3186,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3143
3186
|
}
|
|
3144
3187
|
// Don't set "private": true in package.json - that blocks all publishing
|
|
3145
3188
|
console.log(`Package '${pkg.name}' will publish as PRIVATE (restricted access).`);
|
|
3146
|
-
if (!currentAccess) {
|
|
3147
|
-
console.log(colors.dim(` First publish - requires paid npm account`));
|
|
3148
|
-
}
|
|
3149
3189
|
}
|
|
3150
3190
|
else if (effectiveNpmVisibility === 'public') {
|
|
3151
3191
|
// User explicitly wants public (or confirmed via prompt)
|
|
@@ -3908,7 +3948,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3908
3948
|
}
|
|
3909
3949
|
catch (error) {
|
|
3910
3950
|
let autoFixed = false;
|
|
3911
|
-
console.error(colors.red(
|
|
3951
|
+
console.error(colors.red(`ERROR: Version bump failed in ${cwd} (${pkg.name || '<unnamed>'}):`), error.message);
|
|
3912
3952
|
// Show additional error details if available
|
|
3913
3953
|
if (error.stderr || error.stdout || error.code) {
|
|
3914
3954
|
if (error.stderr)
|
|
@@ -4065,8 +4105,39 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4065
4105
|
return false;
|
|
4066
4106
|
}
|
|
4067
4107
|
}
|
|
4108
|
+
else if (/paths are ignored by one of your \.gitignore files/i.test(combinedOutput)) {
|
|
4109
|
+
// git add refused to stage files (usually package.json) because a
|
|
4110
|
+
// .gitignore rule matches. Ask git which exact line is to blame.
|
|
4111
|
+
const ignoredMatch = combinedOutput.match(/paths are ignored by one of your \.gitignore files:\s*\n([\s\S]*?)(?:hint:|$)/i);
|
|
4112
|
+
const ignoredFiles = (ignoredMatch ? ignoredMatch[1] : '')
|
|
4113
|
+
.split('\n').map(s => s.trim()).filter(Boolean);
|
|
4114
|
+
console.error(colors.red(`\n.gitignore in ${cwd} is blocking required file(s):`));
|
|
4115
|
+
for (const f of ignoredFiles) {
|
|
4116
|
+
const chk = spawnSafe('git', ['-C', cwd, 'check-ignore', '-v', f], { encoding: 'utf-8', stdio: 'pipe' });
|
|
4117
|
+
if (chk.status === 0 && chk.stdout?.trim()) {
|
|
4118
|
+
// Output format: "<source>:<line>:<pattern>\t<file>"
|
|
4119
|
+
const m = chk.stdout.trim().match(/^(.+?):(\d+):(.+?)\t(.+)$/);
|
|
4120
|
+
if (m) {
|
|
4121
|
+
const [, src, line, pattern, file] = m;
|
|
4122
|
+
console.error(colors.red(` ${file} — matched by ${src}:${line} pattern: ${pattern}`));
|
|
4123
|
+
if (pattern.trim() === '*') {
|
|
4124
|
+
console.error(colors.yellow(` → Line ${line} is a bare "*" which ignores EVERY file. Delete it.`));
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
else {
|
|
4128
|
+
console.error(colors.red(` ${f}`));
|
|
4129
|
+
console.error(colors.dim(` ${chk.stdout.trim()}`));
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
else {
|
|
4133
|
+
console.error(colors.red(` ${f}`));
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
console.error(colors.yellow(`\nEdit ${path.join(cwd, '.gitignore')} to remove/narrow the blocking pattern,`));
|
|
4137
|
+
console.error(colors.yellow(`or exempt with an explicit negation, e.g.: !package.json`));
|
|
4138
|
+
}
|
|
4068
4139
|
else if (error.message?.includes('unknown git error')) {
|
|
4069
|
-
console.error(colors.yellow(
|
|
4140
|
+
console.error(colors.yellow(`Unknown git error in ${cwd} — check git hooks, signing, or permissions`));
|
|
4070
4141
|
}
|
|
4071
4142
|
else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
|
|
4072
4143
|
// GitHub push protection blocked a push (from postversion script)
|
|
@@ -4637,9 +4708,84 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
4637
4708
|
}
|
|
4638
4709
|
}
|
|
4639
4710
|
}
|
|
4711
|
+
// Prescan: decide which packages actually need processing so we don't waste
|
|
4712
|
+
// time rebuilding+republishing ones with no relevant changes.
|
|
4713
|
+
// A package is SKIPPED only if ALL of:
|
|
4714
|
+
// - working tree clean (no uncommitted changes)
|
|
4715
|
+
// - current version is on npm AND no commits newer than its publish time
|
|
4716
|
+
// - build not stale (every .ts source has a .js no older than it)
|
|
4717
|
+
// - no sibling workspace file: dep was itself flagged for processing
|
|
4718
|
+
const flaggedForWork = new Set();
|
|
4719
|
+
const skipReasons = new Map();
|
|
4720
|
+
if (!options.force && !options.forcePublish) {
|
|
4721
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4722
|
+
console.log(` Prescan: checking ${filteredOrder.length} package(s)...`);
|
|
4723
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4724
|
+
const wsDeps = new Map(); // pkg → workspace-sibling file: deps
|
|
4725
|
+
for (const pkgInfo of packages) {
|
|
4726
|
+
const sibs = new Set();
|
|
4727
|
+
const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
|
|
4728
|
+
for (const [dn, dv] of Object.entries(deps)) {
|
|
4729
|
+
if (typeof dv === 'string' && dv.startsWith('file:') && wsNameSet.has(dn)) {
|
|
4730
|
+
sibs.add(dn);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
wsDeps.set(pkgInfo.name, sibs);
|
|
4734
|
+
}
|
|
4735
|
+
for (const pkgName of filteredOrder) {
|
|
4736
|
+
const pkgInfo = packages.find(p => p.name === pkgName);
|
|
4737
|
+
if (!pkgInfo)
|
|
4738
|
+
continue;
|
|
4739
|
+
let reason;
|
|
4740
|
+
const status = spawnSafe('git', ['-C', pkgInfo.dir, 'status', '--porcelain', '.'], { encoding: 'utf-8', stdio: 'pipe', shell: true });
|
|
4741
|
+
if (status.status === 0 && status.stdout.trim()) {
|
|
4742
|
+
reason = 'uncommitted changes';
|
|
4743
|
+
}
|
|
4744
|
+
else {
|
|
4745
|
+
const version = pkgInfo.pkg.version;
|
|
4746
|
+
if (!checkVersionExists(pkgInfo.name, version)) {
|
|
4747
|
+
reason = `v${version} not on npm`;
|
|
4748
|
+
}
|
|
4749
|
+
else if (hasLocalChanges(pkgInfo.name, version, pkgInfo.dir, false)) {
|
|
4750
|
+
reason = 'commits since last publish';
|
|
4751
|
+
}
|
|
4752
|
+
else {
|
|
4753
|
+
const stale = hasStaleBuild(pkgInfo.dir);
|
|
4754
|
+
if (stale.stale) {
|
|
4755
|
+
reason = `stale build (${stale.example})`;
|
|
4756
|
+
}
|
|
4757
|
+
else {
|
|
4758
|
+
const flaggedSib = [...(wsDeps.get(pkgName) || [])].find(s => flaggedForWork.has(s));
|
|
4759
|
+
if (flaggedSib)
|
|
4760
|
+
reason = `dep ${flaggedSib} is being updated`;
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
if (reason) {
|
|
4765
|
+
flaggedForWork.add(pkgName);
|
|
4766
|
+
console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason}`);
|
|
4767
|
+
}
|
|
4768
|
+
else {
|
|
4769
|
+
skipReasons.set(pkgName, 'clean, published, build fresh');
|
|
4770
|
+
console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh)`);
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
console.log('');
|
|
4774
|
+
if (flaggedForWork.size === 0) {
|
|
4775
|
+
console.log(colors.green('Nothing to do — all packages up to date.'));
|
|
4776
|
+
console.log('');
|
|
4777
|
+
return { success: true, packages: [], publishOrder };
|
|
4778
|
+
}
|
|
4779
|
+
console.log(colors.dim(`Prescan: ${flaggedForWork.size} to process, ${skipReasons.size} skip. (Use --force to process all.)`));
|
|
4780
|
+
console.log('');
|
|
4781
|
+
}
|
|
4640
4782
|
// Process each package in dependency order
|
|
4641
4783
|
const results = [];
|
|
4642
4784
|
for (const pkgName of filteredOrder) {
|
|
4785
|
+
if (skipReasons.has(pkgName)) {
|
|
4786
|
+
results.push({ name: pkgName, dir: packages.find(p => p.name === pkgName).dir, success: true, version: packages.find(p => p.name === pkgName).pkg.version, skipped: true });
|
|
4787
|
+
continue;
|
|
4788
|
+
}
|
|
4643
4789
|
const pkgInfo = packages.find(p => p.name === pkgName);
|
|
4644
4790
|
if (!pkgInfo)
|
|
4645
4791
|
continue;
|
|
@@ -4692,10 +4838,11 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
4692
4838
|
console.log(allSuccess ? colors.green('✓ Workspace Summary') : colors.red('✗ Workspace Summary'));
|
|
4693
4839
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4694
4840
|
for (const r of results) {
|
|
4695
|
-
const status = r.success ? colors.green('✓') : colors.red('✗');
|
|
4841
|
+
const status = r.skipped ? colors.dim('–') : (r.success ? colors.green('✓') : colors.red('✗'));
|
|
4696
4842
|
const ver = r.version ? ` v${r.version}` : '';
|
|
4697
4843
|
const err = r.error ? colors.red(` (${r.error})`) : '';
|
|
4698
|
-
|
|
4844
|
+
const tag = r.skipped ? colors.dim(' (skipped — already up to date)') : '';
|
|
4845
|
+
console.log(` ${status} ${r.name}${ver}${err}${tag}`);
|
|
4699
4846
|
}
|
|
4700
4847
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4701
4848
|
console.log('');
|