@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.
@@ -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",
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
- "@next/eslint-plugin-next": "^15.0.0",
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
- "typescript-eslint": "^8.35.1"
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": "1c8c4a4b8bf8b6b3c54d8333c43a098a7c7d84f8",
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));
@@ -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
+ }