@mui/internal-code-infra 0.0.2-canary.3 → 0.0.2-canary.4
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/bin/code-infra.mjs +1 -0
- package/package.json +14 -5
- package/src/cli/cmdPublish.mjs +328 -0
- package/src/cli/cmdPublishCanary.mjs +251 -0
- package/src/cli/index.mjs +7 -0
- package/src/cli/pnpm.mjs +170 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../src/cli/index.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.0.2-canary.
|
|
3
|
+
"version": "0.0.2-canary.4",
|
|
4
4
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
"default": "./src/eslint/index.mjs"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"code-infra": "./bin/code-infra.mjs"
|
|
23
|
+
},
|
|
21
24
|
"dependencies": {
|
|
25
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
26
|
+
"@next/eslint-plugin-next": "^15.3.3",
|
|
27
|
+
"@octokit/rest": "^22.0.0",
|
|
22
28
|
"eslint-config-airbnb": "^19.0.4",
|
|
23
29
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
24
30
|
"eslint-config-prettier": "^10.1.5",
|
|
@@ -31,20 +37,23 @@
|
|
|
31
37
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
|
32
38
|
"eslint-plugin-react-hooks": "^6.0.0-rc.1",
|
|
33
39
|
"eslint-plugin-testing-library": "^7.5.3",
|
|
34
|
-
"
|
|
40
|
+
"execa": "^7.2.0",
|
|
41
|
+
"git-url-parse": "^16.1.0",
|
|
35
42
|
"globals": "^16.2.0",
|
|
36
43
|
"minimatch": "^10.0.3",
|
|
37
|
-
"
|
|
44
|
+
"semver": "^7.7.2",
|
|
45
|
+
"typescript-eslint": "^8.35.1",
|
|
46
|
+
"yargs": "^17.7.2"
|
|
38
47
|
},
|
|
39
48
|
"peerDependencies": {
|
|
40
49
|
"eslint": "^9.0.0",
|
|
41
50
|
"prettier": "^3.5.3"
|
|
42
51
|
},
|
|
43
52
|
"devDependencies": {
|
|
44
|
-
"@next/eslint-plugin-next": "^15.3.3",
|
|
45
53
|
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
|
|
46
54
|
"@types/estree": "^1.0.8",
|
|
47
55
|
"@types/estree-jsx": "^1.0.5",
|
|
56
|
+
"@types/yargs": "^17.0.33",
|
|
48
57
|
"@typescript-eslint/parser": "^8.35.0",
|
|
49
58
|
"@typescript-eslint/rule-tester": "^8.35.0",
|
|
50
59
|
"eslint": "^9.29.0",
|
|
@@ -60,7 +69,7 @@
|
|
|
60
69
|
"publishConfig": {
|
|
61
70
|
"access": "public"
|
|
62
71
|
},
|
|
63
|
-
"gitSha": "
|
|
72
|
+
"gitSha": "f4857c7ef738a9ea4cc6d6c150de2ea00702a296",
|
|
64
73
|
"scripts": {
|
|
65
74
|
"typescript": "tsc -p tsconfig.json",
|
|
66
75
|
"test": "pnpm -w test --project @mui/internal-code-infra"
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./pnpm.mjs').Package} Package
|
|
7
|
+
* @typedef {import('./pnpm.mjs').PublishOptions} PublishOptions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Octokit } from '@octokit/rest';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import * as semver from 'semver';
|
|
13
|
+
import gitUrlParse from 'git-url-parse';
|
|
14
|
+
import { $ } from 'execa';
|
|
15
|
+
import { getWorkspacePackages, publishPackages } from './pnpm.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} Args
|
|
19
|
+
* @property {boolean} dry-run Run in dry-run mode without publishing
|
|
20
|
+
* @property {boolean} no-git-checks - Skip git checks before publishing
|
|
21
|
+
* @property {boolean} provenance - Enable provenance tracking for the publish
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the version to release from the root package.json
|
|
26
|
+
* @returns {Promise<string>} Version string
|
|
27
|
+
*/
|
|
28
|
+
async function getReleaseVersion() {
|
|
29
|
+
const result = await $`pnpm pkg get version`;
|
|
30
|
+
const versionData = JSON.parse(result.stdout.trim());
|
|
31
|
+
const version = versionData.version;
|
|
32
|
+
|
|
33
|
+
const validVersion = semver.valid(version);
|
|
34
|
+
if (!validVersion) {
|
|
35
|
+
throw new Error(`Invalid version in root package.json: ${version}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return validVersion;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse changelog to extract content for a specific version
|
|
43
|
+
* @param {string} changelogPath - Path to CHANGELOG.md
|
|
44
|
+
* @param {string} version - Version to extract
|
|
45
|
+
* @returns {Promise<string>} Changelog content for the version
|
|
46
|
+
*/
|
|
47
|
+
async function parseChangelog(changelogPath, version) {
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(changelogPath, 'utf8');
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
|
|
52
|
+
const versionHeader = `## ${version}`;
|
|
53
|
+
const startIndex = lines.findIndex((line) => line.startsWith(versionHeader));
|
|
54
|
+
|
|
55
|
+
if (startIndex === -1) {
|
|
56
|
+
throw new Error(`Version ${version} not found in changelog`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Skip the version header and find content start
|
|
60
|
+
let contentStartIndex = startIndex + 1;
|
|
61
|
+
|
|
62
|
+
// Skip whitespace and comment lines
|
|
63
|
+
while (contentStartIndex < lines.length) {
|
|
64
|
+
const line = lines[contentStartIndex].trim();
|
|
65
|
+
if (line === '' || line.startsWith('<!--')) {
|
|
66
|
+
contentStartIndex += 1;
|
|
67
|
+
} else {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if first content line is a date line
|
|
73
|
+
if (contentStartIndex < lines.length) {
|
|
74
|
+
const line = lines[contentStartIndex].trim();
|
|
75
|
+
// Remove leading/trailing underscores if present
|
|
76
|
+
const cleanLine = line.replace(/^_+|_+$/g, '');
|
|
77
|
+
// Try to parse as date
|
|
78
|
+
if (cleanLine && !Number.isNaN(Date.parse(cleanLine))) {
|
|
79
|
+
contentStartIndex += 1; // Skip date line
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find the end of this version's content (next ## header)
|
|
84
|
+
let endIndex = lines.length;
|
|
85
|
+
for (let i = contentStartIndex; i < lines.length; i += 1) {
|
|
86
|
+
if (lines[i].startsWith('## ')) {
|
|
87
|
+
endIndex = i;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.slice(contentStartIndex, endIndex).join('\n').trim();
|
|
93
|
+
} catch (/** @type {any} */ error) {
|
|
94
|
+
if (error.code === 'ENOENT') {
|
|
95
|
+
throw new Error('CHANGELOG.md not found');
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if GitHub release already exists
|
|
103
|
+
* @param {Octokit} octokit - GitHub API client
|
|
104
|
+
* @param {string} owner - Repository owner
|
|
105
|
+
* @param {string} repo - Repository name
|
|
106
|
+
* @param {string} version - Version to check
|
|
107
|
+
* @returns {Promise<boolean>} True if release exists
|
|
108
|
+
*/
|
|
109
|
+
async function checkGitHubReleaseExists(octokit, owner, repo, version) {
|
|
110
|
+
try {
|
|
111
|
+
await octokit.repos.getReleaseByTag({ owner, repo, tag: `v${version}` });
|
|
112
|
+
return true;
|
|
113
|
+
} catch (/** @type {any} */ error) {
|
|
114
|
+
if (error.status === 404) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get current repository info from git remote
|
|
123
|
+
* @returns {Promise<{owner: string, repo: string}>} Repository owner and name
|
|
124
|
+
*/
|
|
125
|
+
async function getRepositoryInfo() {
|
|
126
|
+
try {
|
|
127
|
+
const result = await $`git remote get-url origin`;
|
|
128
|
+
const url = result.stdout.trim();
|
|
129
|
+
|
|
130
|
+
const parsed = gitUrlParse(url);
|
|
131
|
+
if (parsed.source !== 'github.com') {
|
|
132
|
+
throw new Error('Repository is not hosted on GitHub');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
owner: parsed.owner,
|
|
137
|
+
repo: parsed.name,
|
|
138
|
+
};
|
|
139
|
+
} catch (/** @type {any} */ error) {
|
|
140
|
+
throw new Error(`Failed to get repository info: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get current git SHA
|
|
146
|
+
* @returns {Promise<string>} Current git commit SHA
|
|
147
|
+
*/
|
|
148
|
+
async function getCurrentGitSha() {
|
|
149
|
+
const result = await $`git rev-parse HEAD`;
|
|
150
|
+
return result.stdout.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create and push a git tag
|
|
155
|
+
* @param {string} version - Version to tag
|
|
156
|
+
* @param {boolean} [dryRun=false] - Whether to run in dry-run mode
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
async function createGitTag(version, dryRun = false) {
|
|
160
|
+
const tagName = `v${version}`;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await $`git tag ${tagName}`;
|
|
164
|
+
const pushArgs = dryRun ? ['--dry-run'] : [];
|
|
165
|
+
await $({ stdio: 'inherit' })`git push origin ${tagName} ${pushArgs}`;
|
|
166
|
+
|
|
167
|
+
console.log(`🏷️ Created and pushed git tag ${tagName}${dryRun ? ' (dry-run)' : ''}`);
|
|
168
|
+
} catch (/** @type {any} */ error) {
|
|
169
|
+
throw new Error(`Failed to create git tag: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate GitHub release requirements
|
|
175
|
+
* @param {string} version - Version to validate
|
|
176
|
+
* @returns {Promise<{changelogContent: string, repoInfo: {owner: string, repo: string}}>}
|
|
177
|
+
*/
|
|
178
|
+
async function validateGitHubRelease(version) {
|
|
179
|
+
console.log('🔍 Validating GitHub release requirements...');
|
|
180
|
+
|
|
181
|
+
// Check if CHANGELOG.md exists and parse it
|
|
182
|
+
console.log(`📄 Parsing CHANGELOG.md for version ${version}...`);
|
|
183
|
+
const changelogContent = await parseChangelog('CHANGELOG.md', version);
|
|
184
|
+
console.log('✅ Found changelog content for version');
|
|
185
|
+
|
|
186
|
+
// Get repository info
|
|
187
|
+
const repoInfo = await getRepositoryInfo();
|
|
188
|
+
console.log(`📂 Repository: ${repoInfo.owner}/${repoInfo.repo}`);
|
|
189
|
+
|
|
190
|
+
// Check if release already exists on GitHub
|
|
191
|
+
const octokit = new Octokit({
|
|
192
|
+
auth: process.env.GITHUB_TOKEN,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(`🔍 Checking if GitHub release v${version} already exists...`);
|
|
196
|
+
const releaseExists = await checkGitHubReleaseExists(
|
|
197
|
+
octokit,
|
|
198
|
+
repoInfo.owner,
|
|
199
|
+
repoInfo.repo,
|
|
200
|
+
version,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (releaseExists) {
|
|
204
|
+
throw new Error(`GitHub release v${version} already exists`);
|
|
205
|
+
}
|
|
206
|
+
console.log('✅ GitHub release does not exist yet');
|
|
207
|
+
|
|
208
|
+
return { changelogContent, repoInfo };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Publish packages to npm
|
|
213
|
+
* @param {Package[]} packages - Packages to publish
|
|
214
|
+
* @param {PublishOptions} options - Publishing options
|
|
215
|
+
* @returns {Promise<void>}
|
|
216
|
+
*/
|
|
217
|
+
async function publishToNpm(packages, options) {
|
|
218
|
+
console.log('\n📦 Publishing packages to npm...');
|
|
219
|
+
console.log(`📋 Found ${packages.length} packages:`);
|
|
220
|
+
packages.forEach((pkg) => {
|
|
221
|
+
console.log(` • ${pkg.name}@${pkg.version}`);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Use pnpm's built-in duplicate checking - no need to check versions ourselves
|
|
225
|
+
await publishPackages(packages, 'latest', options);
|
|
226
|
+
console.log('✅ Successfully published to npm');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create GitHub release after npm publishing
|
|
231
|
+
* @param {string} version - Version to release
|
|
232
|
+
* @param {string} changelogContent - Changelog content
|
|
233
|
+
* @param {{owner: string, repo: string}} repoInfo - Repository info
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
async function createRelease(version, changelogContent, repoInfo) {
|
|
237
|
+
console.log('\n🚀 Creating GitHub draft release...');
|
|
238
|
+
|
|
239
|
+
const octokit = new Octokit({
|
|
240
|
+
auth: process.env.GITHUB_TOKEN,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const sha = await getCurrentGitSha();
|
|
244
|
+
|
|
245
|
+
await octokit.repos.createRelease({
|
|
246
|
+
owner: repoInfo.owner,
|
|
247
|
+
repo: repoInfo.repo,
|
|
248
|
+
tag_name: `v${version}`,
|
|
249
|
+
target_commitish: sha,
|
|
250
|
+
name: `v${version}`,
|
|
251
|
+
body: changelogContent,
|
|
252
|
+
draft: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log(
|
|
256
|
+
`✅ Created draft release v${version} at https://github.com/${repoInfo.owner}/${repoInfo.repo}/releases`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
261
|
+
command: 'publish',
|
|
262
|
+
describe: 'Publish packages to npm',
|
|
263
|
+
builder: (yargs) => {
|
|
264
|
+
return yargs
|
|
265
|
+
.option('dry-run', {
|
|
266
|
+
type: 'boolean',
|
|
267
|
+
default: false,
|
|
268
|
+
description: 'Run in dry-run mode without publishing',
|
|
269
|
+
})
|
|
270
|
+
.option('no-git-checks', {
|
|
271
|
+
type: 'boolean',
|
|
272
|
+
default: false,
|
|
273
|
+
description: 'Skip git checks before publishing',
|
|
274
|
+
})
|
|
275
|
+
.option('provenance', {
|
|
276
|
+
type: 'boolean',
|
|
277
|
+
default: false,
|
|
278
|
+
description: 'Enable provenance tracking for the publish',
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
handler: async (argv) => {
|
|
282
|
+
const { dryRun = false, provenance = false, githubRelease = false } = argv;
|
|
283
|
+
|
|
284
|
+
const options = { dryRun, provenance };
|
|
285
|
+
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
console.log('🧪 Running in DRY RUN mode - no actual publishing will occur\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (provenance) {
|
|
291
|
+
console.log('🔐 Provenance enabled - packages will include provenance information\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Get all packages
|
|
295
|
+
console.log('🔍 Discovering all workspace packages...');
|
|
296
|
+
const allPackages = await getWorkspacePackages();
|
|
297
|
+
|
|
298
|
+
if (allPackages.length === 0) {
|
|
299
|
+
console.log('⚠️ No public packages found in workspace');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Get version from root package.json
|
|
304
|
+
const version = await getReleaseVersion();
|
|
305
|
+
console.log(`📋 Release version: ${version}`);
|
|
306
|
+
|
|
307
|
+
// Early validation for GitHub release (before any publishing)
|
|
308
|
+
let githubReleaseData = null;
|
|
309
|
+
if (githubRelease) {
|
|
310
|
+
githubReleaseData = await validateGitHubRelease(version);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Publish to npm (pnpm handles duplicate checking automatically)
|
|
314
|
+
await publishToNpm(allPackages, options);
|
|
315
|
+
|
|
316
|
+
// Create GitHub release or git tag after successful npm publishing
|
|
317
|
+
if (githubRelease && githubReleaseData && !dryRun) {
|
|
318
|
+
await createRelease(version, githubReleaseData.changelogContent, githubReleaseData.repoInfo);
|
|
319
|
+
} else if (githubRelease && dryRun) {
|
|
320
|
+
console.log('\n🚀 Would create GitHub draft release (dry-run)');
|
|
321
|
+
} else {
|
|
322
|
+
// Create git tag when not doing GitHub release
|
|
323
|
+
await createGitTag(version, dryRun);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log('\n🏁 Publishing complete!');
|
|
327
|
+
},
|
|
328
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./pnpm.mjs').Package} Package
|
|
7
|
+
* @typedef {import('./pnpm.mjs').VersionInfo} VersionInfo
|
|
8
|
+
* @typedef {import('./pnpm.mjs').PublishOptions} PublishOptions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { $ } from 'execa';
|
|
12
|
+
import * as semver from 'semver';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} Args
|
|
16
|
+
* @property {boolean} [dryRun] - Whether to run in dry-run mode
|
|
17
|
+
* @property {boolean} [provenance] - Whether to include provenance information
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
getWorkspacePackages,
|
|
22
|
+
getPackageVersionInfo,
|
|
23
|
+
publishPackages,
|
|
24
|
+
readPackageJson,
|
|
25
|
+
writePackageJson,
|
|
26
|
+
getCurrentGitSha,
|
|
27
|
+
semverMax,
|
|
28
|
+
} from './pnpm.mjs';
|
|
29
|
+
|
|
30
|
+
const CANARY_TAG = 'canary';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if the canary git tag exists
|
|
34
|
+
* @returns {Promise<string|null>} Canary tag name if exists, null otherwise
|
|
35
|
+
*/
|
|
36
|
+
async function getLastCanaryTag() {
|
|
37
|
+
// Remove local canary tag first to avoid conflicts during fetch
|
|
38
|
+
try {
|
|
39
|
+
await $`git tag -d ${CANARY_TAG}`;
|
|
40
|
+
} catch {
|
|
41
|
+
// Tag might not exist locally, which is fine
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await $`git fetch origin tag ${CANARY_TAG}`;
|
|
45
|
+
const { stdout: remoteCanaryTag } = await $`git ls-remote --tags origin ${CANARY_TAG}`;
|
|
46
|
+
return remoteCanaryTag.trim() ? CANARY_TAG : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create or update the canary git tag
|
|
51
|
+
* @param {boolean} [dryRun=false] - Whether to run in dry-run mode
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
async function createCanaryTag(dryRun = false) {
|
|
55
|
+
try {
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
console.log('🏷️ Would update and push canary tag (dry-run)');
|
|
58
|
+
} else {
|
|
59
|
+
await $`git tag -f ${CANARY_TAG}`;
|
|
60
|
+
await $`git push origin ${CANARY_TAG} --force`;
|
|
61
|
+
console.log('🏷️ Updated and pushed canary tag');
|
|
62
|
+
}
|
|
63
|
+
} catch (/** @type {any} */ error) {
|
|
64
|
+
console.error('Failed to create/push canary tag:', error.message);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Publish canary versions with updated dependencies
|
|
71
|
+
* @param {Package[]} packagesToPublish - Packages that need canary publishing
|
|
72
|
+
* @param {Package[]} allPackages - All workspace packages
|
|
73
|
+
* @param {Map<string, VersionInfo>} packageVersionInfo - Version info map
|
|
74
|
+
* @param {PublishOptions} [options={}] - Publishing options
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
async function publishCanaryVersions(
|
|
78
|
+
packagesToPublish,
|
|
79
|
+
allPackages,
|
|
80
|
+
packageVersionInfo,
|
|
81
|
+
options = {},
|
|
82
|
+
) {
|
|
83
|
+
console.log('\n🔥 Publishing canary versions...');
|
|
84
|
+
|
|
85
|
+
// Early return if no packages need canary publishing
|
|
86
|
+
if (packagesToPublish.length === 0) {
|
|
87
|
+
console.log('✅ No packages have changed since last canary publish');
|
|
88
|
+
await createCanaryTag(options.dryRun);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const gitSha = await getCurrentGitSha();
|
|
93
|
+
const canaryVersions = new Map();
|
|
94
|
+
const originalPackageJsons = new Map();
|
|
95
|
+
|
|
96
|
+
// First pass: determine canary version numbers for all packages
|
|
97
|
+
const changedPackageNames = new Set(packagesToPublish.map((pkg) => pkg.name));
|
|
98
|
+
|
|
99
|
+
for (const pkg of allPackages) {
|
|
100
|
+
const versionInfo = packageVersionInfo.get(pkg.name);
|
|
101
|
+
if (!versionInfo) {
|
|
102
|
+
throw new Error(`No version info found for package ${pkg.name}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (changedPackageNames.has(pkg.name)) {
|
|
106
|
+
// Generate new canary version for changed packages
|
|
107
|
+
const baseVersion = versionInfo.latestCanaryVersion
|
|
108
|
+
? semverMax(versionInfo.latestCanaryVersion, pkg.version)
|
|
109
|
+
: pkg.version;
|
|
110
|
+
const canaryVersion = semver.inc(baseVersion, 'prerelease', 'canary');
|
|
111
|
+
canaryVersions.set(pkg.name, canaryVersion);
|
|
112
|
+
console.log(`🏷️ ${pkg.name}: ${canaryVersion} (new)`);
|
|
113
|
+
} else if (versionInfo.latestCanaryVersion) {
|
|
114
|
+
// Reuse existing canary version for unchanged packages
|
|
115
|
+
canaryVersions.set(pkg.name, versionInfo.latestCanaryVersion);
|
|
116
|
+
console.log(`🏷️ ${pkg.name}: ${versionInfo.latestCanaryVersion} (reused)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Second pass: read and update ALL package.json files in parallel
|
|
121
|
+
const packageUpdatePromises = allPackages.map(async (pkg) => {
|
|
122
|
+
const originalPackageJson = await readPackageJson(pkg.path);
|
|
123
|
+
|
|
124
|
+
const canaryVersion = canaryVersions.get(pkg.name);
|
|
125
|
+
if (canaryVersion) {
|
|
126
|
+
const updatedPackageJson = {
|
|
127
|
+
...originalPackageJson,
|
|
128
|
+
version: canaryVersion,
|
|
129
|
+
gitSha,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await writePackageJson(pkg.path, updatedPackageJson);
|
|
133
|
+
console.log(`📝 Updated ${pkg.name} package.json to ${canaryVersion}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { pkg, originalPackageJson };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const updateResults = await Promise.all(packageUpdatePromises);
|
|
140
|
+
|
|
141
|
+
// Build the original package.json map
|
|
142
|
+
for (const { pkg, originalPackageJson } of updateResults) {
|
|
143
|
+
originalPackageJsons.set(pkg.name, originalPackageJson);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Run release build after updating package.json files
|
|
147
|
+
console.log('\n🔨 Running release build...');
|
|
148
|
+
await $({ stdio: 'inherit' })`pnpm release:build`;
|
|
149
|
+
console.log('✅ Release build completed successfully');
|
|
150
|
+
|
|
151
|
+
// Third pass: publish only the changed packages using recursive publish
|
|
152
|
+
let publishSuccess = false;
|
|
153
|
+
try {
|
|
154
|
+
console.log(`📤 Publishing ${packagesToPublish.length} canary versions...`);
|
|
155
|
+
await publishPackages(packagesToPublish, 'canary', { ...options, noGitChecks: true });
|
|
156
|
+
|
|
157
|
+
packagesToPublish.forEach((pkg) => {
|
|
158
|
+
const canaryVersion = canaryVersions.get(pkg.name);
|
|
159
|
+
console.log(`✅ Published ${pkg.name}@${canaryVersion}`);
|
|
160
|
+
});
|
|
161
|
+
publishSuccess = true;
|
|
162
|
+
} finally {
|
|
163
|
+
// Always restore original package.json files in parallel
|
|
164
|
+
console.log('\n🔄 Restoring original package.json files...');
|
|
165
|
+
const restorePromises = allPackages.map(async (pkg) => {
|
|
166
|
+
const originalPackageJson = originalPackageJsons.get(pkg.name);
|
|
167
|
+
await writePackageJson(pkg.path, originalPackageJson);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await Promise.all(restorePromises);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (publishSuccess) {
|
|
174
|
+
// Create/update the canary tag after successful publish
|
|
175
|
+
await createCanaryTag(options.dryRun);
|
|
176
|
+
console.log('\n🎉 All canary versions published successfully!');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
181
|
+
command: 'publish-canary',
|
|
182
|
+
describe: 'Publish canary packages to npm',
|
|
183
|
+
builder: (yargs) => {
|
|
184
|
+
return yargs
|
|
185
|
+
.option('dry-run', {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
default: false,
|
|
188
|
+
description: 'Run in dry-run mode without publishing',
|
|
189
|
+
})
|
|
190
|
+
.option('provenance', {
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
default: false,
|
|
193
|
+
description: 'Include provenance information in published packages',
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
handler: async (argv) => {
|
|
197
|
+
const { dryRun = false, provenance = false } = argv;
|
|
198
|
+
|
|
199
|
+
const options = { dryRun, provenance };
|
|
200
|
+
|
|
201
|
+
if (dryRun) {
|
|
202
|
+
console.log('🧪 Running in DRY RUN mode - no actual publishing will occur\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (provenance) {
|
|
206
|
+
console.log('🔐 Provenance enabled - packages will include provenance information\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Always get all packages first
|
|
210
|
+
console.log('🔍 Discovering all workspace packages...');
|
|
211
|
+
const allPackages = await getWorkspacePackages();
|
|
212
|
+
|
|
213
|
+
if (allPackages.length === 0) {
|
|
214
|
+
console.log('⚠️ No public packages found in workspace');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check for canary tag to determine selective publishing
|
|
219
|
+
const canaryTag = await getLastCanaryTag();
|
|
220
|
+
|
|
221
|
+
console.log(
|
|
222
|
+
canaryTag
|
|
223
|
+
? '🔍 Checking for packages changed since canary tag...'
|
|
224
|
+
: '🔍 No canary tag found, will publish all packages',
|
|
225
|
+
);
|
|
226
|
+
const packages = canaryTag ? await getWorkspacePackages(canaryTag) : allPackages;
|
|
227
|
+
|
|
228
|
+
console.log(`📋 Found ${packages.length} packages that need canary publishing:`);
|
|
229
|
+
packages.forEach((pkg) => {
|
|
230
|
+
console.log(` • ${pkg.name}@${pkg.version}`);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Fetch version info for all packages in parallel
|
|
234
|
+
console.log('\n🔍 Fetching package version information...');
|
|
235
|
+
const versionInfoPromises = allPackages.map(async (pkg) => {
|
|
236
|
+
const versionInfo = await getPackageVersionInfo(pkg.name, pkg.version);
|
|
237
|
+
return { packageName: pkg.name, versionInfo };
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const versionInfoResults = await Promise.all(versionInfoPromises);
|
|
241
|
+
const packageVersionInfo = new Map();
|
|
242
|
+
|
|
243
|
+
for (const { packageName, versionInfo } of versionInfoResults) {
|
|
244
|
+
packageVersionInfo.set(packageName, versionInfo);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await publishCanaryVersions(packages, allPackages, packageVersionInfo, options);
|
|
248
|
+
|
|
249
|
+
console.log('\n🏁 Publishing complete!');
|
|
250
|
+
},
|
|
251
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
|
|
4
|
+
import cmdPublish from './cmdPublish.mjs';
|
|
5
|
+
import cmdPublishCanary from './cmdPublishCanary.mjs';
|
|
6
|
+
|
|
7
|
+
yargs().command(cmdPublish).command(cmdPublishCanary).help().parse(hideBin(process.argv));
|
package/src/cli/pnpm.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { $ } from 'execa';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as semver from 'semver';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} Package
|
|
10
|
+
* @property {string} name - Package name
|
|
11
|
+
* @property {string} version - Package version
|
|
12
|
+
* @property {string} path - Package directory path
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} VersionInfo
|
|
17
|
+
* @property {boolean} currentVersionExists - Whether current version exists on npm
|
|
18
|
+
* @property {string|null} latestCanaryVersion - Latest canary version if available
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} PublishOptions
|
|
23
|
+
* @property {boolean} [dryRun] - Whether to run in dry-run mode
|
|
24
|
+
* @property {boolean} [provenance] - Whether to include provenance information
|
|
25
|
+
* @property {boolean} [noGitChecks] - Whether to skip git checks
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} PnpmListResultItem
|
|
30
|
+
* @property {string} [name] - Package name
|
|
31
|
+
* @property {string} [version] - Package version
|
|
32
|
+
* @property {string} path - Package directory path
|
|
33
|
+
* @property {boolean} private - Whether the package is private
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get all workspace packages that are public
|
|
38
|
+
* @param {string|null} [sinceRef] - Git reference to filter changes since
|
|
39
|
+
* @returns {Promise<Package[]>} Array of public packages
|
|
40
|
+
*/
|
|
41
|
+
export async function getWorkspacePackages(sinceRef = null) {
|
|
42
|
+
// Build command with conditional filter
|
|
43
|
+
const filterArg = sinceRef ? ['--filter', `...[${sinceRef}]`] : [];
|
|
44
|
+
const result = await $`pnpm ls -r --json --depth -1 ${filterArg}`;
|
|
45
|
+
/** @type {PnpmListResultItem[]} */
|
|
46
|
+
const packageData = JSON.parse(result.stdout);
|
|
47
|
+
|
|
48
|
+
// Filter out private packages and format the response
|
|
49
|
+
const publicPackages = packageData
|
|
50
|
+
.filter((pkg) => !pkg.private)
|
|
51
|
+
.map((pkg) => {
|
|
52
|
+
if (!pkg.name || !pkg.version) {
|
|
53
|
+
throw new Error(`Invalid package data: ${JSON.stringify(pkg)}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
name: pkg.name,
|
|
57
|
+
version: pkg.version,
|
|
58
|
+
path: pkg.path,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return publicPackages;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get package version info from registry
|
|
67
|
+
* @param {string} packageName - Name of the package
|
|
68
|
+
* @param {string} baseVersion - Base version to check
|
|
69
|
+
* @returns {Promise<VersionInfo>} Version information
|
|
70
|
+
*/
|
|
71
|
+
export async function getPackageVersionInfo(packageName, baseVersion) {
|
|
72
|
+
try {
|
|
73
|
+
// Check if current stable version exists
|
|
74
|
+
let currentVersionExists = false;
|
|
75
|
+
try {
|
|
76
|
+
await $`pnpm view ${packageName}@${baseVersion} version`;
|
|
77
|
+
currentVersionExists = true;
|
|
78
|
+
} catch {
|
|
79
|
+
currentVersionExists = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get canary dist-tag to find latest canary version
|
|
83
|
+
const canaryResult = await $`pnpm view ${packageName} dist-tags.canary`;
|
|
84
|
+
const latestCanaryVersion = semver.valid(canaryResult.stdout.trim());
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
currentVersionExists,
|
|
88
|
+
latestCanaryVersion,
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
currentVersionExists: false,
|
|
93
|
+
latestCanaryVersion: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Publish packages with the given options
|
|
100
|
+
* @param {Package[]} packages - Packages to publish
|
|
101
|
+
* @param {string} tag - npm tag to publish with
|
|
102
|
+
* @param {PublishOptions} [options={}] - Publishing options
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
export async function publishPackages(packages, tag, options = {}) {
|
|
106
|
+
const args = [];
|
|
107
|
+
|
|
108
|
+
// Add package filters
|
|
109
|
+
packages.forEach((pkg) => {
|
|
110
|
+
args.push('--filter', pkg.name);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Add conditional flags
|
|
114
|
+
if (options.dryRun) {
|
|
115
|
+
args.push('--dry-run');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.noGitChecks) {
|
|
119
|
+
args.push('--no-git-checks');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Set up environment variables
|
|
123
|
+
/** @type {Record<string, string>} */
|
|
124
|
+
const env = {};
|
|
125
|
+
if (options.provenance) {
|
|
126
|
+
env.NPM_CONFIG_PROVENANCE = 'true';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await $({ stdio: 'inherit', env })`pnpm -r publish --access public --tag=${tag} ${args}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read package.json from a directory
|
|
134
|
+
* @param {string} packagePath - Path to package directory
|
|
135
|
+
* @returns {Promise<Object>} Parsed package.json content
|
|
136
|
+
*/
|
|
137
|
+
export async function readPackageJson(packagePath) {
|
|
138
|
+
const content = await fs.readFile(path.join(packagePath, 'package.json'), 'utf8');
|
|
139
|
+
return JSON.parse(content);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Write package.json to a directory
|
|
144
|
+
* @param {string} packagePath - Path to package directory
|
|
145
|
+
* @param {Object} packageJson - Package.json object to write
|
|
146
|
+
* @returns {Promise<void>}
|
|
147
|
+
*/
|
|
148
|
+
export async function writePackageJson(packagePath, packageJson) {
|
|
149
|
+
const content = `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
150
|
+
await fs.writeFile(path.join(packagePath, 'package.json'), content);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get current git SHA
|
|
155
|
+
* @returns {Promise<string>} Current git commit SHA
|
|
156
|
+
*/
|
|
157
|
+
export async function getCurrentGitSha() {
|
|
158
|
+
const result = await $`git rev-parse HEAD`;
|
|
159
|
+
return result.stdout.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the maximum semver version between two versions
|
|
164
|
+
* @param {string} a
|
|
165
|
+
* @param {string} b
|
|
166
|
+
* @returns {string} The maximum semver version
|
|
167
|
+
*/
|
|
168
|
+
export function semverMax(a, b) {
|
|
169
|
+
return semver.gt(a, b) ? a : b;
|
|
170
|
+
}
|