@bobfrankston/npmglobalize 1.0.159 → 1.0.161
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 +27 -0
- package/cli.js +3 -0
- package/ignorepatterns.json5 +2 -1
- package/lib.d.ts +17 -3
- package/lib.js +144 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,6 +189,31 @@ npmglobalize -np # --nopublish (formerly --apply)
|
|
|
189
189
|
npmglobalize --cleanup # Restore original file: references
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
### 📝 Release Notes via `.commitmsg`
|
|
193
|
+
|
|
194
|
+
For multi-line or reusable release notes, write them to a `.commitmsg` file in the package root instead of passing them on the command line:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
cat > .commitmsg <<'EOF'
|
|
198
|
+
Added foo feature
|
|
199
|
+
Fixed bar regression
|
|
200
|
+
EOF
|
|
201
|
+
npmglobalize
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Behavior:
|
|
205
|
+
- If `-m` / `-message` is **not** given and `.commitmsg` exists, its contents are used as the commit message (and force a release even if the working tree is otherwise clean).
|
|
206
|
+
- After a successful `npm publish`, npmglobalize:
|
|
207
|
+
1. Appends the contents to `npmchanges.md` under a `## v<version> — <YYYY-MM-DD>` header (creating the file if needed)
|
|
208
|
+
2. Deletes `.commitmsg`
|
|
209
|
+
3. Commits both changes as `Log v<version> to npmchanges.md` and pushes
|
|
210
|
+
|
|
211
|
+
Notes:
|
|
212
|
+
- **Git/GitHub only.** npm publish does not consume git commit messages; `npmchanges.md` lives in the git repo and on GitHub but is excluded from the published npm tarball (the standard `*.md` rule keeps only `README.md`).
|
|
213
|
+
- If both `-m` and `.commitmsg` are present, `-m` wins and `.commitmsg` is left alone (not consumed).
|
|
214
|
+
- If publish fails, `.commitmsg` is preserved for the next attempt.
|
|
215
|
+
- `.commitmsg` is auto-added to `.npmignore` (security pattern) so it never leaks into the tarball.
|
|
216
|
+
|
|
192
217
|
### 🔧 Git Integration & Error Recovery
|
|
193
218
|
|
|
194
219
|
**Automatic tag conflict resolution**:
|
|
@@ -288,6 +313,8 @@ Publishing requires being on a branch so commits and tags can be properly tracke
|
|
|
288
313
|
-nopublish, -np Just transform, don't publish (persisted to config)
|
|
289
314
|
-cleanup Restore file: dependencies from .dependencies backup
|
|
290
315
|
-m, -message <msg> Custom commit message (forces release even without changes)
|
|
316
|
+
If -m not given, a `.commitmsg` file (if present) is used instead.
|
|
317
|
+
See "Release Notes via .commitmsg" below.
|
|
291
318
|
```
|
|
292
319
|
|
|
293
320
|
### Dependency Options
|
package/cli.js
CHANGED
|
@@ -27,6 +27,9 @@ Release Options:
|
|
|
27
27
|
-nopublish, -np Just transform, don't publish (persisted to config)
|
|
28
28
|
-cleanup Restore from .dependencies
|
|
29
29
|
-m, -message <msg> Custom commit message (forces release even without changes)
|
|
30
|
+
If -m not given and a .commitmsg file exists in cwd, its
|
|
31
|
+
contents are used. After a successful publish it is appended
|
|
32
|
+
to npmchanges.md (with version header) and deleted.
|
|
30
33
|
|
|
31
34
|
Dependency Options:
|
|
32
35
|
-update-deps, -ud Update package.json to latest (minor/patch only, safe)
|
package/ignorepatterns.json5
CHANGED
package/lib.d.ts
CHANGED
|
@@ -243,12 +243,26 @@ export declare function parseVersionTag(tag: string): number[] | null;
|
|
|
243
243
|
export declare function compareVersions(a: number[], b: number[]): number;
|
|
244
244
|
/** Fix version/tag mismatches */
|
|
245
245
|
export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
|
|
246
|
+
/** Return declared deps (dependencies + devDependencies) that don't resolve from
|
|
247
|
+
* `pkgDir`. Skips `file:`/`workspace:`/`link:` specs — those are handled elsewhere.
|
|
248
|
+
* A declared dep that doesn't resolve means `package.json` and installed
|
|
249
|
+
* `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
|
|
250
|
+
export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
246
251
|
/** Walk `file:` deps transitively and run `npm install` in any target whose
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
252
|
+
* declared deps aren't all resolvable from its directory. Covers both
|
|
253
|
+
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
254
|
+
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
250
255
|
* Cycle-safe via the shared `visited` set. */
|
|
251
256
|
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): void;
|
|
257
|
+
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
258
|
+
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
259
|
+
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
260
|
+
* the root `node_modules/` stale — the case where `member/` dirs exist but
|
|
261
|
+
* the actual package (e.g. `marked`) is not hoisted. */
|
|
262
|
+
export declare function ensureWorkspaceDepModules(rootDir: string, members: Array<{
|
|
263
|
+
dir: string;
|
|
264
|
+
pkg: any;
|
|
265
|
+
}>, verbose?: boolean): void;
|
|
252
266
|
/** Run a command and return success status */
|
|
253
267
|
export declare function runCommand(cmd: string, args: string[], options?: {
|
|
254
268
|
silent?: boolean;
|
package/lib.js
CHANGED
|
@@ -1322,10 +1322,51 @@ function restoreNestedDepModules(stashed, verbose) {
|
|
|
1322
1322
|
}
|
|
1323
1323
|
}
|
|
1324
1324
|
}
|
|
1325
|
+
/** Walk up the directory tree looking for `node_modules/<depName>/package.json`.
|
|
1326
|
+
* Matches Node's module resolution so hoisted workspace deps are found. */
|
|
1327
|
+
function depResolves(startDir, depName) {
|
|
1328
|
+
let dir = path.resolve(startDir);
|
|
1329
|
+
const segs = depName.split('/');
|
|
1330
|
+
while (true) {
|
|
1331
|
+
const candidate = path.join(dir, 'node_modules', ...segs, 'package.json');
|
|
1332
|
+
if (fs.existsSync(candidate))
|
|
1333
|
+
return true;
|
|
1334
|
+
const parent = path.dirname(dir);
|
|
1335
|
+
if (parent === dir)
|
|
1336
|
+
return false;
|
|
1337
|
+
dir = parent;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/** Return declared deps (dependencies + devDependencies) that don't resolve from
|
|
1341
|
+
* `pkgDir`. Skips `file:`/`workspace:`/`link:` specs — those are handled elsewhere.
|
|
1342
|
+
* A declared dep that doesn't resolve means `package.json` and installed
|
|
1343
|
+
* `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
|
|
1344
|
+
export function missingDeps(pkgDir, pkg) {
|
|
1345
|
+
const missing = [];
|
|
1346
|
+
for (const key of ['dependencies', 'devDependencies']) {
|
|
1347
|
+
const deps = pkg?.[key];
|
|
1348
|
+
if (!deps || typeof deps !== 'object')
|
|
1349
|
+
continue;
|
|
1350
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
1351
|
+
if (typeof spec !== 'string')
|
|
1352
|
+
continue;
|
|
1353
|
+
if (spec.startsWith('file:') || spec.startsWith('workspace:') || spec.startsWith('link:'))
|
|
1354
|
+
continue;
|
|
1355
|
+
if (!depResolves(pkgDir, name))
|
|
1356
|
+
missing.push(name);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return missing;
|
|
1360
|
+
}
|
|
1361
|
+
function formatMissingReason(missing) {
|
|
1362
|
+
if (missing.length === 1)
|
|
1363
|
+
return `missing dep: ${missing[0]}`;
|
|
1364
|
+
return `${missing.length} missing deps: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ', …' : ''}`;
|
|
1365
|
+
}
|
|
1325
1366
|
/** Walk `file:` deps transitively and run `npm install` in any target whose
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1367
|
+
* declared deps aren't all resolvable from its directory. Covers both
|
|
1368
|
+
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
1369
|
+
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
1329
1370
|
* Cycle-safe via the shared `visited` set. */
|
|
1330
1371
|
export function ensureFileDepModules(cwd, verbose = false, visited = new Set()) {
|
|
1331
1372
|
const abs = path.resolve(cwd);
|
|
@@ -1339,11 +1380,13 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1339
1380
|
catch {
|
|
1340
1381
|
return;
|
|
1341
1382
|
}
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1383
|
+
const cwdMissing = missingDeps(abs, pkg);
|
|
1384
|
+
if (cwdMissing.length > 0) {
|
|
1385
|
+
// Only clear a stale lockfile if node_modules is entirely absent (fresh clone).
|
|
1386
|
+
// For partial-sync cases, keep the lockfile so pinned versions stay honored.
|
|
1387
|
+
const nm = path.join(abs, 'node_modules');
|
|
1345
1388
|
const lock = path.join(abs, 'package-lock.json');
|
|
1346
|
-
if (fs.existsSync(lock)) {
|
|
1389
|
+
if (!fs.existsSync(nm) && fs.existsSync(lock)) {
|
|
1347
1390
|
if (verbose)
|
|
1348
1391
|
console.log(colors.dim(` removing stale ${lock}`));
|
|
1349
1392
|
try {
|
|
@@ -1351,7 +1394,7 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1351
1394
|
}
|
|
1352
1395
|
catch { /* best-effort */ }
|
|
1353
1396
|
}
|
|
1354
|
-
console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs}`));
|
|
1397
|
+
console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
|
|
1355
1398
|
const r = runCommand('npm', ['install'], { cwd: abs, silent: !verbose });
|
|
1356
1399
|
if (!r.success) {
|
|
1357
1400
|
console.error(colors.red(` ✗ npm install failed in ${abs}`));
|
|
@@ -1376,12 +1419,11 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1376
1419
|
catch {
|
|
1377
1420
|
continue;
|
|
1378
1421
|
}
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (hasDeps && !fs.existsSync(nm)) {
|
|
1422
|
+
const targetMissing = missingDeps(target, targetPkg);
|
|
1423
|
+
if (targetMissing.length > 0) {
|
|
1424
|
+
const nm = path.join(target, 'node_modules');
|
|
1383
1425
|
const lock = path.join(target, 'package-lock.json');
|
|
1384
|
-
if (fs.existsSync(lock)) {
|
|
1426
|
+
if (!fs.existsSync(nm) && fs.existsSync(lock)) {
|
|
1385
1427
|
if (verbose)
|
|
1386
1428
|
console.log(colors.dim(` removing stale ${lock}`));
|
|
1387
1429
|
try {
|
|
@@ -1389,7 +1431,7 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1389
1431
|
}
|
|
1390
1432
|
catch { /* best-effort */ }
|
|
1391
1433
|
}
|
|
1392
|
-
console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target})`));
|
|
1434
|
+
console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
|
|
1393
1435
|
const r = runCommand('npm', ['install'], { cwd: target, silent: !verbose });
|
|
1394
1436
|
if (!r.success) {
|
|
1395
1437
|
console.error(colors.red(` ✗ npm install failed in ${target}`));
|
|
@@ -1401,6 +1443,38 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1401
1443
|
}
|
|
1402
1444
|
}
|
|
1403
1445
|
}
|
|
1446
|
+
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
1447
|
+
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
1448
|
+
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
1449
|
+
* the root `node_modules/` stale — the case where `member/` dirs exist but
|
|
1450
|
+
* the actual package (e.g. `marked`) is not hoisted. */
|
|
1451
|
+
export function ensureWorkspaceDepModules(rootDir, members, verbose = false) {
|
|
1452
|
+
const root = path.resolve(rootDir);
|
|
1453
|
+
let rootPkg;
|
|
1454
|
+
try {
|
|
1455
|
+
rootPkg = readPackageJson(root);
|
|
1456
|
+
}
|
|
1457
|
+
catch {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const allMissing = new Set();
|
|
1461
|
+
for (const name of missingDeps(root, rootPkg))
|
|
1462
|
+
allMissing.add(name);
|
|
1463
|
+
for (const m of members) {
|
|
1464
|
+
for (const name of missingDeps(m.dir, m.pkg))
|
|
1465
|
+
allMissing.add(name);
|
|
1466
|
+
}
|
|
1467
|
+
if (allMissing.size === 0)
|
|
1468
|
+
return;
|
|
1469
|
+
const list = [...allMissing];
|
|
1470
|
+
console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
|
|
1471
|
+
const r = runCommand('npm', ['install'], { cwd: root, silent: !verbose });
|
|
1472
|
+
if (!r.success) {
|
|
1473
|
+
console.error(colors.red(` ✗ npm install failed in ${root}`));
|
|
1474
|
+
if (r.stderr)
|
|
1475
|
+
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1404
1478
|
/** Run npm install -g with retries for registry propagation delay */
|
|
1405
1479
|
function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
|
|
1406
1480
|
let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
|
|
@@ -3845,11 +3919,29 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3845
3919
|
}
|
|
3846
3920
|
// Re-check git status after all transformations and potential commits
|
|
3847
3921
|
currentGitStatus = getGitStatus(cwd);
|
|
3922
|
+
// If no -m given, fall back to .commitmsg file contents (one-shot changelog entry).
|
|
3923
|
+
// Consumed (appended to npmchanges.md + deleted) only after a successful publish.
|
|
3924
|
+
const commitMsgPath = path.join(cwd, '.commitmsg');
|
|
3925
|
+
let commitMsgFromFile = null;
|
|
3926
|
+
let effectiveMessage = message;
|
|
3927
|
+
if (!effectiveMessage && fs.existsSync(commitMsgPath)) {
|
|
3928
|
+
try {
|
|
3929
|
+
const content = fs.readFileSync(commitMsgPath, 'utf-8').trim();
|
|
3930
|
+
if (content) {
|
|
3931
|
+
commitMsgFromFile = content;
|
|
3932
|
+
effectiveMessage = content;
|
|
3933
|
+
console.log(colors.cyan(` Using .commitmsg for commit message (${content.split('\n')[0].slice(0, 60)}${content.length > 60 ? '…' : ''})`));
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
catch (err) {
|
|
3937
|
+
console.error(colors.yellow(` Warning: could not read .commitmsg: ${err.message}`));
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3848
3940
|
// Check if there are changes to commit or a custom message
|
|
3849
3941
|
// Skip this check for first publish (currentAccess null) or just-initialized repos
|
|
3850
3942
|
const isFirstPublish = !currentAccess;
|
|
3851
3943
|
let skipVersionBump = false;
|
|
3852
|
-
if (!currentGitStatus.hasUncommitted && !
|
|
3944
|
+
if (!currentGitStatus.hasUncommitted && !effectiveMessage && !justInitialized && !isFirstPublish) {
|
|
3853
3945
|
// Check if the current version is actually on npm (it might have failed to publish previously)
|
|
3854
3946
|
const versionCheck = spawnSafe('npm', ['view', `${pkg.name}@${pkg.version}`, 'version'], {
|
|
3855
3947
|
shell: process.platform === 'win32',
|
|
@@ -4000,7 +4092,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4000
4092
|
}
|
|
4001
4093
|
// Git operations
|
|
4002
4094
|
if (currentGitStatus.hasUncommitted) {
|
|
4003
|
-
const commitMsg =
|
|
4095
|
+
const commitMsg = effectiveMessage || 'Pre-release commit';
|
|
4004
4096
|
console.log(`${timestamp()} Committing changes: ${commitMsg}`);
|
|
4005
4097
|
if (!dryRun) {
|
|
4006
4098
|
// Remove 'nul' files that break git on Windows
|
|
@@ -4627,6 +4719,36 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4627
4719
|
const finalAccess = effectiveNpmVisibility || currentAccess || (isScoped ? 'restricted' : 'public');
|
|
4628
4720
|
const accessLabel = (finalAccess === 'restricted' || finalAccess === 'private') ? 'PRIVATE' : 'PUBLIC';
|
|
4629
4721
|
console.log(`${timestamp()} ${colors.green(`✓ Published to npm as ${accessLabel}`)}`);
|
|
4722
|
+
// Consume .commitmsg: append to npmchanges.md, delete the file, commit+push.
|
|
4723
|
+
// Only runs if .commitmsg was actually used as the commit message.
|
|
4724
|
+
if (commitMsgFromFile) {
|
|
4725
|
+
try {
|
|
4726
|
+
const publishedVersion = readPackageJson(cwd).version;
|
|
4727
|
+
const npmChangesPath = path.join(cwd, 'npmchanges.md');
|
|
4728
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
4729
|
+
const header = `## v${publishedVersion} — ${date}\n\n`;
|
|
4730
|
+
const entry = header + commitMsgFromFile.trim() + '\n\n';
|
|
4731
|
+
const existing = fs.existsSync(npmChangesPath)
|
|
4732
|
+
? fs.readFileSync(npmChangesPath, 'utf-8')
|
|
4733
|
+
: '# npm Publish Changes\n\n';
|
|
4734
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
4735
|
+
fs.writeFileSync(npmChangesPath, existing + sep + entry);
|
|
4736
|
+
try {
|
|
4737
|
+
fs.unlinkSync(commitMsgPath);
|
|
4738
|
+
}
|
|
4739
|
+
catch { /* ignore */ }
|
|
4740
|
+
runCommand('git', ['add', 'npmchanges.md', '.commitmsg'], { cwd, silent: true });
|
|
4741
|
+
const logResult = gitCommit(`Log v${publishedVersion} to npmchanges.md`, cwd);
|
|
4742
|
+
if (logResult.success) {
|
|
4743
|
+
console.log(colors.green(` ✓ Appended to npmchanges.md and removed .commitmsg`));
|
|
4744
|
+
if (currentGitStatus.hasRemote)
|
|
4745
|
+
pushWithProtection(cwd, verbose);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
catch (err) {
|
|
4749
|
+
console.error(colors.yellow(` Warning: could not update npmchanges.md: ${err.message}`));
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4630
4752
|
}
|
|
4631
4753
|
else {
|
|
4632
4754
|
console.log(` [dry-run] Would run: npm publish ${quiet ? '--quiet' : ''}`);
|
|
@@ -4931,6 +5053,12 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
4931
5053
|
}
|
|
4932
5054
|
}
|
|
4933
5055
|
}
|
|
5056
|
+
// Sync workspace-root node_modules with member package.json files. Catches
|
|
5057
|
+
// the case where a dep was added to a member package.json but `npm install`
|
|
5058
|
+
// wasn't re-run — the builds would otherwise fail resolving the new dep.
|
|
5059
|
+
if (!options.dryRun) {
|
|
5060
|
+
ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
|
|
5061
|
+
}
|
|
4934
5062
|
// Prescan: decide which packages actually need processing so we don't waste
|
|
4935
5063
|
// time rebuilding+republishing ones with no relevant changes.
|
|
4936
5064
|
// A package is SKIPPED only if ALL of:
|