npm-workspaces-publish-tool 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 jk89
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # npm-workspaces-publish-tool
2
+
3
+ A CLI tool for automating npm package publishing in monorepositories. This tool handles dependency resolution, version validation, build sequencing, and safe publishing with workspace dependency management. Note this is designed for use
4
+ for mono-repos and will not work for projects without npm workspaces.
5
+
6
+ ## Features
7
+
8
+ - Automatic dependency graph analysis (topological sorting)
9
+ - Smart change detection since last release
10
+ - Version validation and increment checking
11
+ - Git status verification (prevents publishing with uncommitted changes)
12
+ - Workspace dependency management (converts `*` dependencies to exact versions during publish)
13
+ - Dry run mode for testing publish workflow
14
+ - Build sequencing in dependency order
15
+ - Safe rollback of package.json files after publishing
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g npm-workspaces-publish-tool
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ nw-publish [--dry-run]
27
+ ```
28
+
29
+ ### Command Options
30
+
31
+ - `--dry-run`: Runs validation and preview without actual publishing
32
+
33
+ ## Workflow
34
+ 1. **Validation Phase**
35
+ - Builds packages in dependency order
36
+ - Verifies all modified packages have incremented versions
37
+ - Ensures git status is clean
38
+ - Checks for unpushed commits
39
+
40
+ 2. **Publishing Phase**
41
+ - Replaces workspace `*` dependencies with exact versions
42
+ - Publishes packages in topological order
43
+ - Restores original `package.json` files after publishing
44
+
45
+ ## Requirements
46
+ - Node.js 16+
47
+ - Git
48
+ - npm workspaces monorepo structure
49
+ - All packages must have valid `semver` versions
50
+ - Build scripts defined in `package.json` (if needed)
51
+
52
+ ## License
53
+ MIT © Jonathan Kelsey
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "npm-workspaces-publish-tool",
3
+ "version": "0.0.1",
4
+ "description": "An unopinionated tool to assist publishing of npm based mono-repo workspaces",
5
+ "main": "build/src/cli.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "nw-publish": "build/src/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "echo \"Error: no test specified\" && exit 1",
13
+ "relink": "npm unlink nw-publish && npm run build && npm link"
14
+ },
15
+ "author": "Jonathan Kelsey",
16
+ "license": "MIT",
17
+ "devDependencies": {
18
+ "@types/node": "^24.0.10",
19
+ "@types/semver": "^7.7.0",
20
+ "type-fest": "^4.41.0",
21
+ "typescript": "^5.8.3"
22
+ },
23
+ "dependencies": {
24
+ "commander": "^14.0.0",
25
+ "semver": "^7.7.2",
26
+ "workspace-tools": "^0.38.4"
27
+ }
28
+ }
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { readFileSync, writeFileSync } from 'fs';
4
+ import { createRequire } from 'module';
5
+ import { dirname, join, relative, resolve } from 'path';
6
+ import { cwd as getCwd } from 'process';
7
+ import semver from 'semver';
8
+ import { createDependencyMap, getChangesBetweenRefs, getPackageInfos, getWorkspaces, git, } from 'workspace-tools';
9
+ import { execSync } from 'child_process';
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../../package.json');
12
+ const cwd = getCwd();
13
+ function getLastTag() {
14
+ const result = git(['tag', '--list', 'v*', '--sort=-v:refname'], { cwd });
15
+ if (!result.success)
16
+ return null;
17
+ const tags = result.stdout.split('\n').filter(Boolean);
18
+ return tags[0] || null;
19
+ }
20
+ function calculateWorkspaceInDegree(packageInfos, dependencies) {
21
+ const inDegree = new Map();
22
+ for (const pkgName of Object.keys(packageInfos)) {
23
+ inDegree.set(pkgName, 0);
24
+ }
25
+ for (const [pkgName, deps] of dependencies) {
26
+ for (const _ of deps) {
27
+ inDegree.set(pkgName, (inDegree.get(pkgName) ?? 0) + 1);
28
+ }
29
+ }
30
+ return inDegree;
31
+ }
32
+ function getReleaseOrderFromInDegree(inDegree, dependencies) {
33
+ const queue = Array.from(inDegree.entries())
34
+ .filter(([_, count]) => count === 0)
35
+ .map(([pkg]) => pkg);
36
+ const order = [];
37
+ while (queue.length > 0) {
38
+ const current = queue.shift();
39
+ order.push(current);
40
+ for (const [pkg, deps] of dependencies) {
41
+ if (deps.has(current)) {
42
+ const newCount = inDegree.get(pkg) - 1;
43
+ inDegree.set(pkg, newCount);
44
+ if (newCount === 0) {
45
+ queue.push(pkg);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return order;
51
+ }
52
+ function getDirtyMap(workspaces, lastTag) {
53
+ const dirtyMap = new Map();
54
+ const changedFiles = lastTag
55
+ ? getChangesBetweenRefs(lastTag, 'HEAD', [], '', cwd)
56
+ : [];
57
+ for (const ws of workspaces) {
58
+ const isNew = !lastTag;
59
+ const isDirty = changedFiles.some((f) => f.startsWith(ws.path + '/'));
60
+ if (isNew) {
61
+ dirtyMap.set(ws.name, 'new');
62
+ }
63
+ else if (isDirty) {
64
+ dirtyMap.set(ws.name, 'dirty');
65
+ }
66
+ else {
67
+ dirtyMap.set(ws.name, 'unchanged');
68
+ }
69
+ }
70
+ return dirtyMap;
71
+ }
72
+ /**
73
+ * Returns version changes only for packages marked as 'dirty' in dirtyMap,
74
+ * comparing current package.json version to the version at the last tag.
75
+ * Has a `versionIncremented` field indicating if version was increased semver-wise.
76
+ */
77
+ function getDirtyPackagesVersionChanges(workspaces, dirtyMap, lastTag, cwd) {
78
+ const result = new Map();
79
+ if (!lastTag)
80
+ return result;
81
+ for (const ws of workspaces) {
82
+ if (dirtyMap.get(ws.name) !== 'dirty')
83
+ continue;
84
+ const pkgPath = resolve(cwd, ws.path, 'package.json');
85
+ let currentPkg;
86
+ try {
87
+ currentPkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
88
+ }
89
+ catch (err) {
90
+ throw new Error(`Failed to parse current package.json for package "${ws.name}" at path "${pkgPath}": ${err}`);
91
+ }
92
+ const newVersion = currentPkg.version;
93
+ const previousPkgRaw = git(['show', `${lastTag}:${ws.path}/package.json`], { cwd });
94
+ if (!previousPkgRaw.success) {
95
+ result.set(ws.name, {
96
+ oldVersion: null,
97
+ newVersion,
98
+ versionChanged: true,
99
+ versionIncremented: true,
100
+ });
101
+ continue;
102
+ }
103
+ let previousPkg;
104
+ try {
105
+ previousPkg = JSON.parse(previousPkgRaw.stdout);
106
+ }
107
+ catch (err) {
108
+ throw new Error(`Failed to parse package.json from git at tag "${lastTag}" for package "${ws.name}": ${err}`);
109
+ }
110
+ const oldVersion = previousPkg.version;
111
+ const versionChanged = newVersion !== oldVersion;
112
+ const versionIncremented = oldVersion === null ||
113
+ (semver.valid(newVersion) && semver.valid(oldVersion))
114
+ ? semver.gt(newVersion, oldVersion)
115
+ : false;
116
+ result.set(ws.name, {
117
+ oldVersion,
118
+ newVersion,
119
+ versionChanged,
120
+ versionIncremented,
121
+ });
122
+ }
123
+ return result;
124
+ }
125
+ // Check git status to ensure all changes for release are committed appropriately
126
+ function verifyCleanGitStatus(workspaces, dirtyMap, cwd) {
127
+ // Files changed since last commit
128
+ const changedFilesRaw = git(['diff', '--name-only', 'HEAD'], { cwd })
129
+ .stdout.trim()
130
+ .split('\n')
131
+ .filter(Boolean);
132
+ // Files staged for commit
133
+ const stagedFilesRaw = git(['diff', '--cached', '--name-only'], { cwd })
134
+ .stdout.trim()
135
+ .split('\n')
136
+ .filter(Boolean);
137
+ // Files unstaged but changed (working dir different from index)
138
+ const unstagedFilesRaw = git(['diff', '--name-only'], { cwd })
139
+ .stdout.trim()
140
+ .split('\n')
141
+ .filter(Boolean);
142
+ // Get untracked files from git status
143
+ const status = git(['status', '--porcelain'], { cwd })
144
+ .stdout.trim()
145
+ .split('\n')
146
+ .filter(Boolean);
147
+ const untrackedFilesRaw = status
148
+ .filter((line) => line.startsWith('??'))
149
+ .map((line) => line.slice(3));
150
+ // Prepare map of workspace issues
151
+ const workspaceIssues = new Map();
152
+ for (const ws of workspaces) {
153
+ workspaceIssues.set(ws.name, {
154
+ stagedPackageJsonDirty: false,
155
+ unstagedPackageJsonDirty: false,
156
+ stagedFilesNotCommitted: [],
157
+ unstagedFilesNotStaged: [],
158
+ untrackedFiles: [],
159
+ });
160
+ }
161
+ function findWorkspaceForFile(filePath) {
162
+ const absoluteFilePath = resolve(cwd, filePath).replace(/\\/g, '/');
163
+ for (const ws of workspaces) {
164
+ const wsPathNormalized = ws.path.replace(/\\/g, '/');
165
+ if (absoluteFilePath === wsPathNormalized ||
166
+ absoluteFilePath.startsWith(wsPathNormalized + '/')) {
167
+ return ws;
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+ // Helper sets for quick lookup
173
+ const changedFiles = new Set(changedFilesRaw);
174
+ const stagedFiles = new Set(stagedFilesRaw);
175
+ const unstagedFiles = new Set(unstagedFilesRaw);
176
+ const untrackedFiles = new Set(untrackedFilesRaw);
177
+ for (const filePath of changedFiles) {
178
+ const ws = findWorkspaceForFile(filePath);
179
+ if (!ws)
180
+ continue;
181
+ if (dirtyMap.get(ws.name) !== 'new' &&
182
+ dirtyMap.get(ws.name) !== 'dirty')
183
+ continue;
184
+ const issues = workspaceIssues.get(ws.name);
185
+ const isPackageJson = filePath.endsWith('package.json');
186
+ const isStaged = stagedFiles.has(filePath);
187
+ const isUnstaged = unstagedFiles.has(filePath);
188
+ // FIXME here
189
+ if (isPackageJson) {
190
+ if (!isStaged) {
191
+ issues.unstagedPackageJsonDirty = true;
192
+ }
193
+ else {
194
+ issues.stagedPackageJsonDirty = true;
195
+ }
196
+ }
197
+ else {
198
+ if (!isStaged) {
199
+ issues.unstagedFilesNotStaged.push(filePath);
200
+ }
201
+ else {
202
+ issues.stagedFilesNotCommitted.push(filePath);
203
+ }
204
+ }
205
+ }
206
+ for (const filePath of untrackedFilesRaw) {
207
+ const ws = findWorkspaceForFile(filePath);
208
+ if (!ws)
209
+ continue;
210
+ if (dirtyMap.get(ws.name) !== 'new' &&
211
+ dirtyMap.get(ws.name) !== 'dirty')
212
+ continue;
213
+ workspaceIssues.get(ws.name).untrackedFiles.push(filePath);
214
+ }
215
+ let hasIssues = false;
216
+ for (const [wsName, issues] of workspaceIssues.entries()) {
217
+ const { stagedPackageJsonDirty, unstagedPackageJsonDirty, stagedFilesNotCommitted, unstagedFilesNotStaged, untrackedFiles, } = issues;
218
+ if (!stagedPackageJsonDirty &&
219
+ !unstagedPackageJsonDirty &&
220
+ stagedFilesNotCommitted.length === 0 &&
221
+ unstagedFilesNotStaged.length === 0 &&
222
+ untrackedFiles.length === 0) {
223
+ continue;
224
+ }
225
+ hasIssues = true;
226
+ console.error(`❌ Workspace '${wsName}' has issues preventing publish:`);
227
+ const ws = workspaces.find((w) => w.name === wsName);
228
+ const missingFiles = [];
229
+ if (unstagedPackageJsonDirty) {
230
+ const relativePkgJson = relative(cwd, join(ws.path, 'package.json')).replace(/\\/g, '/');
231
+ missingFiles.push(relativePkgJson);
232
+ }
233
+ for (const file of unstagedFilesNotStaged) {
234
+ missingFiles.push(file);
235
+ }
236
+ if (missingFiles.length > 0) {
237
+ console.error(' ⚠️ These files have changes since last commit but are NOT staged. Please stage them:');
238
+ for (const f of missingFiles) {
239
+ console.error(` - ${f}`);
240
+ }
241
+ }
242
+ // fix this this is not relative to the workspace root.. FIXME
243
+ if (stagedPackageJsonDirty) {
244
+ console.error(' ⚠️ package.json is staged but not committed. Please commit it.');
245
+ }
246
+ if (stagedFilesNotCommitted.length > 0) {
247
+ console.error(' ⚠️ These files are staged but not committed. Please commit them:');
248
+ for (const f of stagedFilesNotCommitted)
249
+ console.error(` - ${f}`);
250
+ }
251
+ if (untrackedFiles.length > 0) {
252
+ console.error(' ⚠️ Untracked files (consider adding or ignoring):');
253
+ for (const f of untrackedFiles)
254
+ console.error(` - ${f}`);
255
+ }
256
+ }
257
+ // Check if root package.json has unstaged changes not committed
258
+ const rootPackageJsonPath = 'package.json';
259
+ const isRootPackageJsonChanged = changedFiles.has(rootPackageJsonPath);
260
+ const isRootPackageJsonStaged = stagedFiles.has(rootPackageJsonPath);
261
+ const isRootPackageJsonUnstaged = unstagedFiles.has(rootPackageJsonPath);
262
+ if (isRootPackageJsonChanged) {
263
+ if (!isRootPackageJsonStaged) {
264
+ console.error(`❌ Root package.json has unstaged changes. Please stage or discard them before publishing.`);
265
+ hasIssues = true;
266
+ }
267
+ if (isRootPackageJsonStaged) {
268
+ console.error(`❌ Root package.json is staged but not committed. Please commit it before publishing.`);
269
+ hasIssues = true;
270
+ }
271
+ }
272
+ if (hasIssues) {
273
+ console.error('Please commit or stash the above changes before publishing.');
274
+ return false;
275
+ }
276
+ return true;
277
+ }
278
+ function validatePublish() {
279
+ const lastMonoRepoTag = getLastTag();
280
+ //console.log('lastMonoRepoTag', lastMonoRepoTag);
281
+ const workspaces = getWorkspaces(cwd);
282
+ //console.log('workspaces', workspaces);
283
+ const packageInfos = getPackageInfos(cwd); // this is a map of package.json "name" field and an unpackaked packagejson with packJsonPath extended into it
284
+ //const dependencyMap = createDependencyMap(packageInfos);
285
+ /*console.log(packageInfos);
286
+ console.log('---------------------------------------');*/
287
+ const workspacePackageNames = Object.keys(packageInfos); // nodes
288
+ //console.log('workspacePackageNames', workspacePackageNames);
289
+ //console.log('---------------------------------------');
290
+ //console.log(dependencyMap);
291
+ //const packageGraph = createPackageGraph(packageInfos);
292
+ //console.log('---------------------------------------');
293
+ //console.log('packageGraph', packageGraph);
294
+ const { dependencies, dependents } = createDependencyMap(packageInfos);
295
+ const roots = workspacePackageNames.filter((name) => !dependencies.get(name)?.size);
296
+ const visited = new Set();
297
+ function buildNode(name) {
298
+ if (visited.has(name)) {
299
+ return { name, children: [] };
300
+ }
301
+ visited.add(name);
302
+ const kids = Array.from(dependents.get(name) ?? []);
303
+ return { name, children: kids.map(buildNode) };
304
+ }
305
+ const tree = roots.map(buildNode);
306
+ console.log('tree', JSON.stringify(tree, null, 4));
307
+ const inDegree = calculateWorkspaceInDegree(packageInfos, dependencies);
308
+ console.log('inDegree', inDegree);
309
+ // Determine release order
310
+ const releaseOrder = getReleaseOrderFromInDegree(inDegree, dependencies);
311
+ console.log('releaseOrder', releaseOrder);
312
+ // Determine status of each workspace
313
+ const dirtyMap = getDirtyMap(workspaces, lastMonoRepoTag);
314
+ console.log('dirtyMap', dirtyMap);
315
+ // Determine which dirty packages have had their version field updated (which permits them to be published) if we have dirty packages which have not all been incremented
316
+ // Determine which dirty packages have version changes
317
+ const dirtyVersionChanges = getDirtyPackagesVersionChanges(workspaces, dirtyMap, lastMonoRepoTag, cwd);
318
+ let hasError = false;
319
+ for (const [pkgName, { oldVersion, newVersion, versionChanged, versionIncremented },] of dirtyVersionChanges.entries()) {
320
+ if (!semver.valid(newVersion)) {
321
+ console.error(`❌ Package "${pkgName}" has an invalid current version: "${newVersion}".`);
322
+ hasError = true;
323
+ continue;
324
+ }
325
+ if (!versionChanged) {
326
+ console.error(`❌ Package "${pkgName}" was modified but the version has not been updated (still "${newVersion}").`);
327
+ hasError = true;
328
+ // Print changed files directly from git using path as pattern
329
+ const ws = workspaces.find((w) => w.name === pkgName);
330
+ try {
331
+ const changedFiles = getChangesBetweenRefs(lastMonoRepoTag, 'HEAD', [], ws.path, // use path as pattern for precision
332
+ cwd);
333
+ if (changedFiles.length > 0) {
334
+ console.error(` ↪ Changed files in "${pkgName}":`);
335
+ for (const file of changedFiles) {
336
+ console.error(` - ${file.replace(ws.path + '/', '')}`);
337
+ }
338
+ }
339
+ }
340
+ catch (err) {
341
+ console.error(` ⚠️ Failed to get changed files for "${pkgName}": ${err}`);
342
+ }
343
+ continue;
344
+ }
345
+ if (!versionIncremented) {
346
+ console.error(`❌ Package "${pkgName}" version changed from "${oldVersion}" to "${newVersion}", but the new version is not greater (semver).`);
347
+ hasError = true;
348
+ continue;
349
+ }
350
+ console.log(`✅ Package "${pkgName}" is ready to publish. Version increased from "${oldVersion}" to "${newVersion}".`);
351
+ }
352
+ if (hasError) {
353
+ console.error('\nFix the above issues before publishing.\n');
354
+ process.exit(1);
355
+ }
356
+ // Now if we get this far we have a possible combination of 'unchanged', 'dirty' but incremented correctly, or new.
357
+ // Before we set of ensure that we have commited all of our changes. And that our git status is clean.
358
+ const cleanGitStatus = verifyCleanGitStatus(workspaces, dirtyMap, cwd);
359
+ if (!cleanGitStatus)
360
+ process.exit(1);
361
+ // Print publish summary
362
+ console.log('\nPublish summary:\n');
363
+ const packagesToRelease = [];
364
+ for (const pkgName of releaseOrder) {
365
+ const status = dirtyMap.get(pkgName);
366
+ const versionInfo = dirtyVersionChanges.get(pkgName);
367
+ if (status === 'new') {
368
+ packagesToRelease.push(pkgName);
369
+ const ws = workspaces.find((w) => w.name === pkgName);
370
+ const currentVersion = JSON.parse(readFileSync(resolve(cwd, ws.path, 'package.json'), 'utf8')).version;
371
+ console.log(`🆕 ${pkgName} @ ${currentVersion} (new)`);
372
+ }
373
+ if (status === 'dirty' && versionInfo?.versionIncremented) {
374
+ packagesToRelease.push(pkgName);
375
+ console.log(`⬆️ ${pkgName}: ${versionInfo.oldVersion} → ${versionInfo.newVersion} (version incremented)`);
376
+ }
377
+ if (status === 'unchanged') {
378
+ const ws = workspaces.find((w) => w.name === pkgName);
379
+ const currentVersion = JSON.parse(readFileSync(resolve(cwd, ws.path, 'package.json'), 'utf8')).version;
380
+ console.log(`✔️ ${pkgName} @ ${currentVersion} (unchanged)`);
381
+ }
382
+ }
383
+ console.log('');
384
+ // Now return useful information to the main program
385
+ return { releaseOrder, packageInfos };
386
+ }
387
+ export function replaceWorkspaceDepsWithVersions(packageInfos) {
388
+ for (const pkgName in packageInfos) {
389
+ const pkgInfo = packageInfos[pkgName];
390
+ let changed = false;
391
+ [
392
+ 'dependencies',
393
+ 'devDependencies',
394
+ 'peerDependencies',
395
+ 'optionalDependencies',
396
+ ].forEach((depType) => {
397
+ const deps = pkgInfo.packageJson[depType];
398
+ if (!deps)
399
+ return;
400
+ for (const depName in deps) {
401
+ if (deps[depName] === '*') {
402
+ const wsPkg = packageInfos[depName];
403
+ if (wsPkg) {
404
+ deps[depName] = wsPkg.packageJson.version;
405
+ changed = true;
406
+ }
407
+ }
408
+ }
409
+ });
410
+ if (changed) {
411
+ writeFileSync(pkgInfo.packageJsonPath, JSON.stringify(pkgInfo.packageJson, null, 2));
412
+ }
413
+ }
414
+ }
415
+ export function restoreWorkspaceDepsToStar(packageInfos) {
416
+ for (const pkgName in packageInfos) {
417
+ const pkgInfo = packageInfos[pkgName];
418
+ let changed = false;
419
+ [
420
+ 'dependencies',
421
+ 'devDependencies',
422
+ 'peerDependencies',
423
+ 'optionalDependencies',
424
+ ].forEach((depType) => {
425
+ const deps = pkgInfo.packageJson[depType];
426
+ if (!deps)
427
+ return;
428
+ for (const depName in deps) {
429
+ if (typeof deps[depName] === 'string' &&
430
+ deps[depName] !== '*' &&
431
+ packageInfos[depName]) {
432
+ deps[depName] = '*';
433
+ changed = true;
434
+ }
435
+ }
436
+ });
437
+ if (changed) {
438
+ writeFileSync(pkgInfo.packageJsonPath, JSON.stringify(pkgInfo.packageJson, null, 2));
439
+ }
440
+ }
441
+ }
442
+ export function runNpmPublishInReleaseOrder(releaseOrder, packageInfos) {
443
+ for (const packageName of releaseOrder) {
444
+ const pkgInfo = packageInfos[packageName];
445
+ const pkgDir = dirname(pkgInfo.packageJsonPath);
446
+ execSync('npm publish', { cwd: pkgDir, stdio: 'inherit' });
447
+ }
448
+ }
449
+ program.name(pkg.name).description(pkg.description).version(pkg.version);
450
+ program
451
+ .command('publish')
452
+ .description('Publish packages')
453
+ .option('--dry-run', 'Run the publish command without making changes')
454
+ .action((options) => {
455
+ const { packageInfos, releaseOrder } = validatePublish();
456
+ if (options.dryRun) {
457
+ console.log('Dry run: no publishing will be performed.');
458
+ }
459
+ else {
460
+ try {
461
+ console.log('Replacing workspace dependencies "*" with actual versions...');
462
+ replaceWorkspaceDepsWithVersions(packageInfos);
463
+ console.log('Publishing packages in release order...');
464
+ runNpmPublishInReleaseOrder(releaseOrder, packageInfos);
465
+ console.log('Publish process completed.');
466
+ }
467
+ catch (error) {
468
+ console.error('Publishing failed:', error);
469
+ }
470
+ finally {
471
+ console.log('Restoring workspace dependencies back to "*" ...');
472
+ try {
473
+ restoreWorkspaceDepsToStar(packageInfos);
474
+ console.log('Restoration complete.');
475
+ }
476
+ catch (restoreError) {
477
+ console.error('Failed to restore workspace dependencies:', restoreError);
478
+ }
479
+ }
480
+ }
481
+ });
482
+ program.parse(process.argv);