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 +21 -0
- package/README.md +53 -0
- package/build/package.json +28 -0
- package/build/src/bin/cli.js +482 -0
- package/build/src/bin/cli2.js +444 -0
- package/build/src/bin/cli3.js +548 -0
- package/build/src/cli.js +551 -0
- package/build/src/diff.js +1 -0
- package/build/src/index.js +1 -0
- package/package.json +28 -0
- package/src/cli.ts +783 -0
- package/tsconfig.json +116 -0
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);
|