@mui/internal-code-infra 0.0.4-canary.1 → 0.0.4-canary.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,7 +35,7 @@ This is stored in the `docs` top-level directory.
35
35
 
36
36
  ### Adding and publishing new packages
37
37
 
38
- Whenever news packages are added to the repo (that will get published to npm) or a private package is turned into a public one, follow the below steps before invoking the publish workflow of the previous section.
38
+ Whenever new packages are added to the repo (that will get published to npm) or a private package is turned into a public one, follow the below steps before invoking the publish workflow of the previous section.
39
39
 
40
40
  1. Goto your repo's code base on your system, open terminal and run:
41
41
 
@@ -1,3 +1,7 @@
1
+ export type ReactCompilationMode = 'annotation' | 'syntax' | 'infer' | 'all';
2
+ /**
3
+ * @typedef {'annotation' | 'syntax' | 'infer' | 'all'} ReactCompilationMode
4
+ */
1
5
  /**
2
6
  * @param {Object} param0
3
7
  * @param {boolean} [param0.debug]
@@ -8,7 +12,7 @@
8
12
  * @param {string | null} param0.outExtension - Specify the output file extension.
9
13
  * @param {string} param0.runtimeVersion
10
14
  * @param {string} [param0.reactCompilerReactVersion]
11
- * @param {string} [param0.reactCompilerMode]
15
+ * @param {ReactCompilationMode} [param0.reactCompilerMode]
12
16
  * @returns {import('@babel/core').TransformOptions} The base Babel configuration.
13
17
  */
14
18
  export declare function getBaseConfig({ debug, optimizeClsx, removePropTypes, noResolveImports, bundle, runtimeVersion, outExtension, reactCompilerReactVersion, reactCompilerMode }: {
@@ -20,7 +24,7 @@ export declare function getBaseConfig({ debug, optimizeClsx, removePropTypes, no
20
24
  outExtension: string | null;
21
25
  runtimeVersion: string;
22
26
  reactCompilerReactVersion?: string;
23
- reactCompilerMode?: string;
27
+ reactCompilerMode?: ReactCompilationMode;
24
28
  }): import('@babel/core').TransformOptions;
25
29
  export type Options = {
26
30
  bundle?: 'esm' | 'cjs';
@@ -4,12 +4,14 @@ export type Args = {
4
4
  publicOnly?: boolean;
5
5
  output?: 'json' | 'path' | 'name' | 'publish-dir';
6
6
  sinceRef?: string;
7
+ filter?: string[];
7
8
  };
8
9
  /**
9
10
  * @typedef {Object} Args
10
11
  * @property {boolean} [publicOnly] - Whether to filter to only public packages
11
12
  * @property {'json'|'path'|'name'|'publish-dir'} [output] - Output format (name, path, or json)
12
13
  * @property {string} [sinceRef] - Git reference to filter changes since
14
+ * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
13
15
  */
14
16
  declare const _default: import("yargs").CommandModule<{}, Args>;
15
17
  export default /** @type {import('yargs').CommandModule<{}, Args>} */ _default;
@@ -7,6 +7,7 @@ export type Args = {
7
7
  tag: string;
8
8
  ci: boolean;
9
9
  sha?: string;
10
+ filter?: string[];
10
11
  };
11
12
  declare const _default: import("yargs").CommandModule<{}, Args>;
12
13
  export default /** @type {import('yargs').CommandModule<{}, Args>} */ _default;
@@ -5,6 +5,7 @@ export type PublishOptions = import('../utils/pnpm.mjs').PublishOptions;
5
5
  export type Args = {
6
6
  dryRun?: boolean;
7
7
  githubRelease?: boolean;
8
+ filter?: string[];
8
9
  };
9
10
  export type Commit = {
10
11
  sha: string;
@@ -31,6 +31,7 @@ export type GetWorkspacePackagesOptions = {
31
31
  publicOnly?: boolean;
32
32
  nonPublishedOnly?: boolean;
33
33
  cwd?: string;
34
+ filter?: string[];
34
35
  };
35
36
  export declare function getWorkspacePackages(options?: {
36
37
  publicOnly: true;
@@ -56,6 +57,39 @@ export declare function getPackageVersionInfo(packageName: string, baseVersion:
56
57
  * @returns {Promise<void>}
57
58
  */
58
59
  export declare function publishPackages(packages: PublicPackage[], options?: PublishOptions): Promise<void>;
60
+ export type GetTransitiveDependenciesOptions = {
61
+ workspacePathByName?: Map<string, string>;
62
+ includeDev?: boolean;
63
+ };
64
+ /**
65
+ * @typedef {Object} GetTransitiveDependenciesOptions
66
+ * @property {Map<string, string>} [workspacePathByName] - Map of workspace package name to directory path
67
+ * @property {boolean} [includeDev=true] - Whether to include devDependencies in the traversal
68
+ */
69
+ /**
70
+ * Get all transitive workspace dependencies for a set of packages.
71
+ *
72
+ * Traverses `dependencies`, `peerDependencies`, and optionally `devDependencies`,
73
+ * following only packages that exist in `workspacePathByName`. Results are cached
74
+ * per package so each package is read from disk at most once regardless of how many
75
+ * roots depend on it.
76
+ *
77
+ * @param {string[]} packageNames - Package names to start the traversal from
78
+ * @param {GetTransitiveDependenciesOptions} [options]
79
+ * @returns {Promise<Set<string>>} All reachable workspace package names, including the input packages themselves
80
+ */
81
+ export declare function getTransitiveDependencies(packageNames: string[], options?: GetTransitiveDependenciesOptions): Promise<Set<string>>;
82
+ /**
83
+ * Validate that a set of packages covers all of their transitive workspace dependencies,
84
+ * and that none of those dependencies are private (which would make them unpublishable).
85
+ *
86
+ * @param {PublicPackage[]} packages - The packages intended for publishing
87
+ * @returns {Promise<{issues: string[]}>}
88
+ * List of human-readable issue strings. Empty when the dependency set is valid.
89
+ */
90
+ export declare function validatePublishDependencies(packages: PublicPackage[]): Promise<{
91
+ issues: string[];
92
+ }>;
59
93
  /**
60
94
  * Read package.json from a directory
61
95
  * @param {string} packagePath - Path to package directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.0.4-canary.1",
3
+ "version": "0.0.4-canary.10",
4
4
  "description": "Infra scripts and configs to be used across MUI repos.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,13 +43,16 @@
43
43
  "./brokenLinksChecker": {
44
44
  "types": "./build/brokenLinksChecker/index.d.mts",
45
45
  "default": "./src/brokenLinksChecker/index.mjs"
46
+ },
47
+ "./build-env": {
48
+ "types": "./src/build-env.d.ts"
46
49
  }
47
50
  },
48
51
  "bin": {
49
52
  "code-infra": "./bin/code-infra.mjs"
50
53
  },
51
54
  "dependencies": {
52
- "@argos-ci/core": "^4.5.0",
55
+ "@argos-ci/core": "^5.1.1",
53
56
  "@babel/cli": "^7.28.6",
54
57
  "@babel/core": "^7.29.0",
55
58
  "@babel/plugin-syntax-jsx": "^7.28.6",
@@ -58,19 +61,19 @@
58
61
  "@babel/preset-env": "^7.29.0",
59
62
  "@babel/preset-react": "^7.28.5",
60
63
  "@babel/preset-typescript": "^7.28.5",
61
- "@eslint/compat": "^2.0.2",
64
+ "@eslint/compat": "^2.0.3",
62
65
  "@eslint/js": "^10.0.1",
63
- "@eslint/json": "^1.0.1",
64
- "@inquirer/confirm": "^6.0.4",
65
- "@inquirer/select": "^5.0.4",
66
+ "@eslint/json": "^1.1.0",
67
+ "@inquirer/confirm": "^6.0.8",
68
+ "@inquirer/select": "^5.1.0",
66
69
  "@napi-rs/keyring": "^1.2.0",
67
70
  "@octokit/auth-action": "^6.0.2",
68
71
  "@octokit/oauth-methods": "^6.0.2",
69
72
  "@octokit/rest": "^22.0.1",
70
73
  "@pnpm/find-workspace-dir": "^1000.1.4",
71
- "@typescript-eslint/types": "^8.56.1",
72
- "@typescript-eslint/utils": "^8.56.1",
73
- "@vitest/eslint-plugin": "^1.6.9",
74
+ "@typescript-eslint/types": "^8.57.1",
75
+ "@typescript-eslint/utils": "^8.57.1",
76
+ "@vitest/eslint-plugin": "^1.6.11",
74
77
  "babel-plugin-optimize-clsx": "^2.6.2",
75
78
  "babel-plugin-react-compiler": "^1.0.0",
76
79
  "babel-plugin-transform-import-meta": "^2.3.3",
@@ -78,13 +81,13 @@
78
81
  "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
79
82
  "babel-plugin-transform-remove-imports": "^1.8.1",
80
83
  "chalk": "^5.6.2",
81
- "clipboardy": "^5.3.0",
84
+ "clipboardy": "^5.3.1",
82
85
  "content-type": "^1.0.5",
83
86
  "env-ci": "^11.2.0",
84
87
  "eslint-config-prettier": "^10.1.8",
85
88
  "eslint-import-resolver-typescript": "^4.4.4",
86
89
  "eslint-module-utils": "^2.12.1",
87
- "eslint-plugin-compat": "^6.2.0",
90
+ "eslint-plugin-compat": "^7.0.1",
88
91
  "eslint-plugin-import": "^2.32.0",
89
92
  "eslint-plugin-jsx-a11y": "^6.10.2",
90
93
  "eslint-plugin-mocha": "^11.2.0",
@@ -92,14 +95,14 @@
92
95
  "eslint-plugin-react-compiler": "^19.1.0-rc.2",
93
96
  "eslint-plugin-react-hooks": "^7.0.1",
94
97
  "eslint-plugin-testing-library": "^7.16.0",
95
- "es-toolkit": "^1.44.0",
98
+ "es-toolkit": "^1.45.1",
96
99
  "execa": "^9.6.1",
97
100
  "git-url-parse": "^16.1.0",
98
- "globals": "^16.5.0",
101
+ "globals": "^17.4.0",
99
102
  "globby": "^16.1.1",
100
- "minimatch": "^10.2.2",
101
- "node-html-parser": "^7.0.2",
102
- "open": "^10.2.0",
103
+ "minimatch": "^10.2.4",
104
+ "node-html-parser": "^7.1.0",
105
+ "open": "^11.0.0",
103
106
  "postcss-styled-syntax": "^0.7.1",
104
107
  "regexp.escape": "^2.0.1",
105
108
  "rehype-slug": "^6.0.0",
@@ -110,12 +113,12 @@
110
113
  "resolve-pkg-maps": "^1.0.0",
111
114
  "semver": "^7.7.4",
112
115
  "stylelint-config-standard": "^40.0.0",
113
- "typescript-eslint": "^8.56.1",
116
+ "typescript-eslint": "^8.57.1",
114
117
  "unified": "^11.0.5",
115
118
  "yargs": "^18.0.0",
116
- "@mui/internal-babel-plugin-display-name": "1.0.4-canary.13",
117
- "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.32",
118
- "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.22"
119
+ "@mui/internal-babel-plugin-display-name": "1.0.4-canary.14",
120
+ "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.33",
121
+ "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.24"
119
122
  },
120
123
  "peerDependencies": {
121
124
  "@next/eslint-plugin-next": "*",
@@ -143,13 +146,13 @@
143
146
  "@types/estree-jsx": "1.0.5",
144
147
  "@types/regexp.escape": "2.0.0",
145
148
  "@types/yargs": "17.0.35",
146
- "@typescript-eslint/parser": "8.56.1",
147
- "@typescript-eslint/rule-tester": "8.56.1",
148
- "eslint": "10.0.2",
149
+ "@typescript-eslint/parser": "8.57.1",
150
+ "@typescript-eslint/rule-tester": "8.57.1",
151
+ "eslint": "10.0.3",
149
152
  "get-port": "7.1.0",
150
153
  "prettier": "3.8.1",
151
- "serve": "14.2.5",
152
- "typescript-eslint": "8.56.1"
154
+ "serve": "14.2.6",
155
+ "typescript-eslint": "8.57.1"
153
156
  },
154
157
  "files": [
155
158
  "bin",
@@ -161,10 +164,10 @@
161
164
  "publishConfig": {
162
165
  "access": "public"
163
166
  },
164
- "gitSha": "4fc3292722f406f8eae122597b124749bf54476d",
167
+ "gitSha": "b989d008bd34fbf9fb9d3395da2f6a32536f574d",
165
168
  "scripts": {
166
169
  "build": "tsgo -p tsconfig.build.json",
167
- "typescript": "tsgo -p tsconfig.json",
170
+ "typescript": "tsgo -noEmit",
168
171
  "test": "pnpm -w test --project @mui/internal-code-infra",
169
172
  "test:copy": "rm -rf build && node bin/code-infra.mjs copy-files --glob \"src/cli/*.mjs\" --glob \"src/eslint/**/*.mjs:esm\""
170
173
  }
@@ -10,6 +10,10 @@ import pluginTransformImportMeta from 'babel-plugin-transform-import-meta';
10
10
  import pluginTransformInlineEnvVars from 'babel-plugin-transform-inline-environment-variables';
11
11
  import pluginRemovePropTypes from 'babel-plugin-transform-react-remove-prop-types';
12
12
 
13
+ /**
14
+ * @typedef {'annotation' | 'syntax' | 'infer' | 'all'} ReactCompilationMode
15
+ */
16
+
13
17
  /**
14
18
  * @param {Object} param0
15
19
  * @param {boolean} [param0.debug]
@@ -20,7 +24,7 @@ import pluginRemovePropTypes from 'babel-plugin-transform-react-remove-prop-type
20
24
  * @param {string | null} param0.outExtension - Specify the output file extension.
21
25
  * @param {string} param0.runtimeVersion
22
26
  * @param {string} [param0.reactCompilerReactVersion]
23
- * @param {string} [param0.reactCompilerMode]
27
+ * @param {ReactCompilationMode} [param0.reactCompilerMode]
24
28
  * @returns {import('@babel/core').TransformOptions} The base Babel configuration.
25
29
  */
26
30
  export function getBaseConfig({
@@ -183,6 +187,6 @@ export default function getBabelConfig(api) {
183
187
  removePropTypes: process.env.MUI_REMOVE_PROP_TYPES === 'true',
184
188
  noResolveImports,
185
189
  reactCompilerReactVersion: process.env.MUI_REACT_COMPILER_REACT_VERSION,
186
- reactCompilerMode: process.env.MUI_REACT_COMPILER_MODE,
190
+ reactCompilerMode: /** @type {ReactCompilationMode} */ (process.env.MUI_REACT_COMPILER_MODE),
187
191
  });
188
192
  }
@@ -0,0 +1,13 @@
1
+ export {};
2
+
3
+ declare global {
4
+ interface Env {
5
+ NODE_ENV?: 'production' | undefined;
6
+ }
7
+
8
+ interface Process {
9
+ env: Env;
10
+ }
11
+
12
+ const process: Process;
13
+ }
@@ -15,6 +15,7 @@ import { getWorkspacePackages } from '../utils/pnpm.mjs';
15
15
  * @property {boolean} [publicOnly] - Whether to filter to only public packages
16
16
  * @property {'json'|'path'|'name'|'publish-dir'} [output] - Output format (name, path, or json)
17
17
  * @property {string} [sinceRef] - Git reference to filter changes since
18
+ * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
18
19
  */
19
20
 
20
21
  export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
@@ -37,13 +38,19 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
37
38
  .option('since-ref', {
38
39
  type: 'string',
39
40
  description: 'Filter packages changed since git reference',
41
+ })
42
+ .option('filter', {
43
+ type: 'string',
44
+ array: true,
45
+ description:
46
+ 'Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.',
40
47
  });
41
48
  },
42
49
  handler: async (argv) => {
43
- const { publicOnly = false, output = 'name', sinceRef } = argv;
50
+ const { publicOnly = false, output = 'name', sinceRef, filter = [] } = argv;
44
51
 
45
52
  // Get packages using our helper function
46
- const packages = await getWorkspacePackages({ sinceRef, publicOnly });
53
+ const packages = await getWorkspacePackages({ sinceRef, publicOnly, filter });
47
54
 
48
55
  if (output === 'json') {
49
56
  // Serialize packages to JSON
@@ -12,93 +12,7 @@ import * as fs from 'node:fs/promises';
12
12
  import * as path from 'node:path';
13
13
  import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
14
14
  import { toPosixPath } from '../utils/path.mjs';
15
- import { getWorkspacePackages } from '../utils/pnpm.mjs';
16
-
17
- /**
18
- * Get all workspace dependencies (direct and transitive) from a package
19
- * @param {string} packageName - Package name
20
- * @param {Map<string, string>} workspaceMap - Map of workspace name to path
21
- * @param {Map<string, Promise<Set<string>>>} cache - Cache of package resolution promises
22
- * @returns {Promise<Set<string>>} Set of workspace package names (dependencies only, not including the package itself)
23
- */
24
- async function getWorkspaceDependenciesRecursive(packageName, workspaceMap, cache) {
25
- // Check cache first
26
- const cached = cache.get(packageName);
27
- if (cached) {
28
- return cached;
29
- }
30
-
31
- // Create the resolution promise
32
- const promise = (async () => {
33
- const packagePath = workspaceMap.get(packageName);
34
- if (!packagePath) {
35
- throw new Error(`Workspace "${packageName}" not found in the repository`);
36
- }
37
-
38
- const packageJsonPath = path.join(packagePath, 'package.json');
39
- const content = await fs.readFile(packageJsonPath, 'utf8');
40
- const packageJson = JSON.parse(content);
41
-
42
- // Collect all dependency names
43
- /** @type {Set<string>} */
44
- const allDeps = new Set();
45
- if (packageJson.dependencies) {
46
- Object.keys(packageJson.dependencies).forEach((dep) => allDeps.add(dep));
47
- }
48
- if (packageJson.devDependencies) {
49
- Object.keys(packageJson.devDependencies).forEach((dep) => allDeps.add(dep));
50
- }
51
- if (packageJson.peerDependencies) {
52
- Object.keys(packageJson.peerDependencies).forEach((dep) => allDeps.add(dep));
53
- }
54
-
55
- // Filter to only workspace dependencies
56
- const workspaceDeps = Array.from(allDeps).filter((dep) => workspaceMap.has(dep));
57
-
58
- // Recursively process workspace dependencies in parallel
59
- const recursiveResults = await Promise.all(
60
- workspaceDeps.map(async (dep) => {
61
- return getWorkspaceDependenciesRecursive(dep, workspaceMap, cache);
62
- }),
63
- );
64
-
65
- // Merge all results using flatMap
66
- return new Set(recursiveResults.flatMap((result) => Array.from(result)).concat(workspaceDeps));
67
- })();
68
-
69
- // Store in cache before returning
70
- cache.set(packageName, promise);
71
-
72
- return promise;
73
- }
74
-
75
- /**
76
- * Get transitive workspace dependencies for a list of workspace names
77
- * @param {string[]} workspaceNames - Array of workspace names
78
- * @param {Map<string, string>} workspaceMap - Map of workspace name to path
79
- * @returns {Promise<Set<string>>} Set of workspace package names (including requested packages and all their dependencies)
80
- */
81
- async function getTransitiveDependencies(workspaceNames, workspaceMap) {
82
- // Shared cache for all workspace dependency resolution
83
- const cache = new Map();
84
-
85
- // Validate all workspace names exist
86
- for (const workspaceName of workspaceNames) {
87
- if (!workspaceMap.has(workspaceName)) {
88
- throw new Error(`Workspace "${workspaceName}" not found in the repository`);
89
- }
90
- }
91
-
92
- // Process each requested workspace in parallel
93
- const workspaceResults = await Promise.all(
94
- workspaceNames.map((workspaceName) =>
95
- getWorkspaceDependenciesRecursive(workspaceName, workspaceMap, cache),
96
- ),
97
- );
98
-
99
- // Merge all results using flatMap and add the original package names
100
- return new Set(workspaceNames.concat(workspaceResults.flatMap((result) => Array.from(result))));
101
- }
15
+ import { getTransitiveDependencies, getWorkspacePackages } from '../utils/pnpm.mjs';
102
16
 
103
17
  /**
104
18
  * Generate the ignore command string for netlify.toml
@@ -220,7 +134,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
220
134
  console.log(`Processing ${workspaceName}...`);
221
135
 
222
136
  // Get transitive dependencies for this specific workspace
223
- const dependencyNames = await getTransitiveDependencies([workspaceName], workspaceMap);
137
+ const dependencyNames = await getTransitiveDependencies([workspaceName], {
138
+ workspacePathByName: workspaceMap,
139
+ });
224
140
 
225
141
  // Convert package names to relative paths (normalize to POSIX separators for git)
226
142
  const relativePaths = Array.from(dependencyNames)
@@ -17,7 +17,11 @@ import * as fs from 'node:fs/promises';
17
17
  import * as semver from 'semver';
18
18
 
19
19
  import { persistentAuthStrategy } from '../utils/github.mjs';
20
- import { getWorkspacePackages, publishPackages } from '../utils/pnpm.mjs';
20
+ import {
21
+ getWorkspacePackages,
22
+ publishPackages,
23
+ validatePublishDependencies,
24
+ } from '../utils/pnpm.mjs';
21
25
  import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
22
26
 
23
27
  const isCI = envCI().isCi;
@@ -33,6 +37,7 @@ function getOctokit() {
33
37
  * @property {string} tag NPM dist tag to publish to
34
38
  * @property {boolean} ci Runs in CI environment
35
39
  * @property {string} [sha] Git SHA to use for the GitHub release workflow (local only)
40
+ * @property {string[]} [filter] Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
36
41
  */
37
42
 
38
43
  /**
@@ -260,10 +265,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
260
265
  .option('sha', {
261
266
  type: 'string',
262
267
  description: 'Git SHA to use for the GitHub release workflow (local only)',
268
+ })
269
+ .option('filter', {
270
+ type: 'string',
271
+ array: true,
272
+ description:
273
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
263
274
  });
264
275
  },
265
276
  handler: async (argv) => {
266
- const { dryRun = false, githubRelease = false, tag = 'latest', sha } = argv;
277
+ const { dryRun = false, githubRelease = false, tag = 'latest', sha, filter = [] } = argv;
267
278
 
268
279
  if (isCI && !argv.ci) {
269
280
  console.error(
@@ -290,13 +301,29 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
290
301
  // Get all packages
291
302
  console.log('🔍 Discovering all workspace packages...');
292
303
 
293
- const allPackages = await getWorkspacePackages({ publicOnly: true });
304
+ const allPackages = await getWorkspacePackages({ publicOnly: true, filter });
294
305
 
295
306
  if (allPackages.length === 0) {
296
- console.log('⚠️ No public packages found in workspace');
307
+ console.log(
308
+ `⚠️ No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
309
+ );
297
310
  return;
298
311
  }
299
312
 
313
+ if (filter.length > 0) {
314
+ console.log('🔍 Validating workspace dependencies for filtered packages...');
315
+
316
+ const { issues } = await validatePublishDependencies(allPackages);
317
+
318
+ if (issues.length > 0) {
319
+ throw new Error('Invalid dependencies structure of packages to be published.', {
320
+ cause: issues,
321
+ });
322
+ }
323
+
324
+ console.log('✅ All workspace dependency requirements satisfied');
325
+ }
326
+
300
327
  // Get version from root package.json
301
328
  const version = await getReleaseVersion();
302
329
 
@@ -16,10 +16,12 @@ import * as semver from 'semver';
16
16
 
17
17
  import {
18
18
  getPackageVersionInfo,
19
+ getTransitiveDependencies,
19
20
  getWorkspacePackages,
20
21
  publishPackages,
21
22
  readPackageJson,
22
23
  semverMax,
24
+ validatePublishDependencies,
23
25
  writePackageJson,
24
26
  } from '../utils/pnpm.mjs';
25
27
  import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
@@ -28,6 +30,7 @@ import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
28
30
  * @typedef {Object} Args
29
31
  * @property {boolean} [dryRun] - Whether to run in dry-run mode
30
32
  * @property {boolean} [githubRelease] - Whether to create GitHub releases for canary packages
33
+ * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
31
34
  */
32
35
 
33
36
  const CANARY_TAG = 'canary';
@@ -118,80 +121,6 @@ function cleanupCommitMessage(message) {
118
121
  return `${prefix}${msg}`.trim();
119
122
  }
120
123
 
121
- async function getPackageToDependencyMap() {
122
- /**
123
- * @type {(PublicPackage & { dependencies: Record<string, unknown>; private: boolean; })[]}
124
- */
125
- const packagesWithDeps = JSON.parse(
126
- (await $`pnpm ls -r --json --exclude-peers --only-projects --prod`).stdout,
127
- );
128
- /** @type {Record<string, string[]>} */
129
- const directPkgDependencies = packagesWithDeps
130
- .filter((pkg) => !pkg.private)
131
- .reduce((acc, pkg) => {
132
- if (!pkg.name) {
133
- return acc;
134
- }
135
- const deps = Object.keys(pkg.dependencies || {});
136
- if (!deps.length) {
137
- return acc;
138
- }
139
- acc[pkg.name] = deps;
140
- return acc;
141
- }, /** @type {Record<string, string[]>} */ ({}));
142
- return directPkgDependencies;
143
- }
144
-
145
- /**
146
- * @param {Record<string, string[]>} pkgGraph
147
- */
148
- function resolveTransitiveDependencies(pkgGraph = {}) {
149
- // Compute transitive (nested) dependencies limited to workspace packages and avoid cycles.
150
- const workspacePkgNames = new Set(Object.keys(pkgGraph));
151
- const nestedMap = /** @type {Record<string, string[]>} */ ({});
152
-
153
- /**
154
- *
155
- * @param {string} pkgName
156
- * @returns {string[]}
157
- */
158
- const getTransitiveDeps = (pkgName) => {
159
- /**
160
- * @type {Set<string>}
161
- */
162
- const seen = new Set();
163
- const stack = (pkgGraph[pkgName] || []).slice();
164
-
165
- while (stack.length) {
166
- const dep = stack.pop();
167
- if (!dep || seen.has(dep)) {
168
- continue;
169
- }
170
- // Only consider workspace packages for transitive expansion
171
- if (!workspacePkgNames.has(dep)) {
172
- // still record external deps as direct deps but don't traverse into them
173
- seen.add(dep);
174
- continue;
175
- }
176
- seen.add(dep);
177
- const children = pkgGraph[dep] || [];
178
- for (const c of children) {
179
- if (!seen.has(c)) {
180
- stack.push(c);
181
- }
182
- }
183
- }
184
-
185
- return Array.from(seen);
186
- };
187
-
188
- for (const name of Object.keys(pkgGraph)) {
189
- nestedMap[name] = getTransitiveDeps(name);
190
- }
191
-
192
- return nestedMap;
193
- }
194
-
195
124
  /**
196
125
  * Prepare changelog data for packages using GitHub API
197
126
  * @param {PublicPackage[]} packagesToPublish - Packages that will be published
@@ -224,14 +153,20 @@ async function prepareChangelogsFromGitCli(packagesToPublish, allPackages, canar
224
153
  }
225
154
  }),
226
155
  );
227
- // Second pass: check for dependency updates in other packages not part of git history
228
- const pkgDependencies = await getPackageToDependencyMap();
229
- const transitiveDependencies = resolveTransitiveDependencies(pkgDependencies);
156
+ // Second pass: check for dependency updates in other packages not part of git history.
157
+ const workspacePathByName = new Map(allPackages.map((pkg) => [pkg.name, pkg.path]));
158
+ const publishedNames = new Set(packagesToPublish.map((p) => p.name));
159
+
160
+ const transitiveDepSets = await Promise.all(
161
+ allPackages.map((pkg) =>
162
+ getTransitiveDependencies([pkg.name], { includeDev: false, workspacePathByName }),
163
+ ),
164
+ );
230
165
 
231
166
  for (let i = 0; i < allPackages.length; i += 1) {
232
167
  const pkg = allPackages[i];
233
- const depsToPublish = (transitiveDependencies[pkg.name] ?? []).filter((dep) =>
234
- packagesToPublish.some((p) => p.name === dep),
168
+ const depsToPublish = [...transitiveDepSets[i]].filter(
169
+ (dep) => dep !== pkg.name && publishedNames.has(dep),
235
170
  );
236
171
  if (depsToPublish.length === 0) {
237
172
  continue;
@@ -495,7 +430,11 @@ async function publishCanaryVersions(
495
430
  let publishSuccess = false;
496
431
  try {
497
432
  console.log(`📤 Publishing ${packagesToPublish.length} canary versions...`);
498
- await publishPackages(packagesToPublish, { ...options, noGitChecks: true, tag: CANARY_TAG });
433
+ await publishPackages(packagesToPublish, {
434
+ dryRun: options.dryRun,
435
+ noGitChecks: true,
436
+ tag: CANARY_TAG,
437
+ });
499
438
 
500
439
  packagesToPublish.forEach((pkg) => {
501
440
  const canaryVersion = canaryVersions.get(pkg.name);
@@ -542,10 +481,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
542
481
  type: 'boolean',
543
482
  default: false,
544
483
  description: 'Create GitHub releases for published packages',
484
+ })
485
+ .option('filter', {
486
+ type: 'string',
487
+ array: true,
488
+ description:
489
+ 'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
545
490
  });
546
491
  },
547
492
  handler: async (argv) => {
548
- const { dryRun = false, githubRelease = false } = argv;
493
+ const { dryRun = false, githubRelease = false, filter = [] } = argv;
549
494
 
550
495
  const options = { dryRun, githubRelease };
551
496
 
@@ -557,22 +502,41 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
557
502
  console.log('📝 GitHub releases will be created for published packages\n');
558
503
  }
559
504
 
560
- // Always get all packages first
505
+ // All public packages — needed by publishCanaryVersions to bump versions and update
506
+ // package.json across the entire workspace, even for packages not being published.
561
507
  console.log('🔍 Discovering all workspace packages...');
562
- const allPackages = await getWorkspacePackages({ publicOnly: true });
508
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
563
509
 
564
- if (allPackages.length === 0) {
565
- console.log('⚠️ No public packages found in workspace');
510
+ if (filteredPackages.length === 0) {
511
+ console.log(
512
+ `⚠️ No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
513
+ );
566
514
  return;
567
515
  }
568
516
 
569
- // Check for canary tag to determine selective publishing
517
+ if (filter.length > 0) {
518
+ console.log('🔍 Validating workspace dependencies for filtered packages...');
519
+
520
+ const { issues } = await validatePublishDependencies(filteredPackages);
521
+
522
+ if (issues.length > 0) {
523
+ throw new Error('Invalid dependencies structure of packages to be published.', {
524
+ cause: issues,
525
+ });
526
+ }
527
+
528
+ console.log('✅ All workspace dependency requirements satisfied');
529
+ }
530
+
531
+ // Check for canary tag to determine selective publishing.
532
+ // --filter is applied on top of sinceRef: publish only packages that have
533
+ // changed since the last canary tag AND match the filter.
570
534
  const canaryTag = await getLastCanaryTag();
571
535
 
572
536
  console.log('🔍 Checking for packages changed since canary tag...');
573
537
  const packages = canaryTag
574
- ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true })
575
- : allPackages;
538
+ ? await getWorkspacePackages({ sinceRef: canaryTag, publicOnly: true, filter })
539
+ : filteredPackages;
576
540
 
577
541
  console.log(`📋 Found ${packages.length} packages(s) for canary publishing:`);
578
542
  packages.forEach((pkg) => {
@@ -581,7 +545,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
581
545
 
582
546
  // Fetch version info for all packages in parallel
583
547
  console.log('\n🔍 Fetching package version information...');
584
- const versionInfoPromises = allPackages.map(async (pkg) => {
548
+ const versionInfoPromises = filteredPackages.map(async (pkg) => {
585
549
  const versionInfo = await getPackageVersionInfo(pkg.name, pkg.version);
586
550
  return { packageName: pkg.name, versionInfo };
587
551
  });
@@ -593,7 +557,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
593
557
  packageVersionInfo.set(packageName, versionInfo);
594
558
  }
595
559
 
596
- await publishCanaryVersions(packages, allPackages, packageVersionInfo, options);
560
+ await publishCanaryVersions(packages, filteredPackages, packageVersionInfo, options);
597
561
 
598
562
  console.log('\n🏁 Publishing complete!');
599
563
  },
@@ -48,6 +48,7 @@ import * as semver from 'semver';
48
48
  * @property {boolean} [publicOnly=false] - Whether to filter to only public packages
49
49
  * @property {boolean} [nonPublishedOnly=false] - Whether to filter to only non-published packages. It by default means public packages yet to be published.
50
50
  * @property {string} [cwd] - Current working directory to run pnpm command in
51
+ * @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
51
52
  */
52
53
 
53
54
  /**
@@ -73,10 +74,15 @@ import * as semver from 'semver';
73
74
  * @returns {Promise<(PrivatePackage | PublicPackage)[]>} Array of packages
74
75
  */
75
76
  export async function getWorkspacePackages(options = {}) {
76
- const { sinceRef = null, publicOnly = false, nonPublishedOnly = false } = options;
77
+ const { sinceRef = null, publicOnly = false, nonPublishedOnly = false, filter = [] } = options;
77
78
 
78
79
  // Build command with conditional filter
79
80
  const filterArg = sinceRef ? ['--filter', `...[${sinceRef}]`] : [];
81
+ if (filter.length > 0) {
82
+ filter.forEach((f) => {
83
+ filterArg.push('--filter', f);
84
+ });
85
+ }
80
86
  const result = options.cwd
81
87
  ? await $({ cwd: options.cwd })`pnpm ls -r --json --depth -1 ${filterArg}`
82
88
  : await $`pnpm ls -r --json --depth -1 ${filterArg}`;
@@ -175,6 +181,132 @@ export async function publishPackages(packages, options = {}) {
175
181
  await $({ stdio: 'inherit' })`pnpm -r publish --access public --tag=${tag} ${args}`;
176
182
  }
177
183
 
184
+ /**
185
+ * @typedef {Object} GetTransitiveDependenciesOptions
186
+ * @property {Map<string, string>} [workspacePathByName] - Map of workspace package name to directory path
187
+ * @property {boolean} [includeDev=true] - Whether to include devDependencies in the traversal
188
+ */
189
+
190
+ /**
191
+ * Get all transitive workspace dependencies for a set of packages.
192
+ *
193
+ * Traverses `dependencies`, `peerDependencies`, and optionally `devDependencies`,
194
+ * following only packages that exist in `workspacePathByName`. Results are cached
195
+ * per package so each package is read from disk at most once regardless of how many
196
+ * roots depend on it.
197
+ *
198
+ * @param {string[]} packageNames - Package names to start the traversal from
199
+ * @param {GetTransitiveDependenciesOptions} [options]
200
+ * @returns {Promise<Set<string>>} All reachable workspace package names, including the input packages themselves
201
+ */
202
+ export async function getTransitiveDependencies(packageNames, options = {}) {
203
+ const { includeDev = true, workspacePathByName = new Map() } = options;
204
+
205
+ /** @type {Map<string, Promise<Set<string>>>} */
206
+ const cache = new Map();
207
+
208
+ /**
209
+ * @param {string} packageName
210
+ * @returns {Promise<Set<string>>}
211
+ */
212
+ function collectDeps(packageName) {
213
+ const cached = cache.get(packageName);
214
+ if (cached) {
215
+ return cached;
216
+ }
217
+
218
+ const promise = (async () => {
219
+ const packagePath = workspacePathByName.get(packageName);
220
+ if (!packagePath) {
221
+ throw new Error(`Workspace "${packageName}" not found`);
222
+ }
223
+
224
+ const pkgJson = await readPackageJson(packagePath);
225
+ const allDeps = new Set([
226
+ ...Object.keys(pkgJson.dependencies ?? {}),
227
+ ...(includeDev ? Object.keys(pkgJson.devDependencies ?? {}) : []),
228
+ ...Object.keys(pkgJson.peerDependencies ?? {}),
229
+ ]);
230
+ const workspaceDeps = [...allDeps].filter((dep) => workspacePathByName.has(dep));
231
+
232
+ const recursiveResults = await Promise.all(workspaceDeps.map(collectDeps));
233
+ return new Set([...workspaceDeps, ...recursiveResults.flatMap((s) => [...s])]);
234
+ })();
235
+
236
+ cache.set(packageName, promise);
237
+ return promise;
238
+ }
239
+
240
+ for (const name of packageNames) {
241
+ if (!workspacePathByName.has(name)) {
242
+ throw new Error(`Workspace "${name}" not found`);
243
+ }
244
+ }
245
+
246
+ const results = await Promise.all(packageNames.map(collectDeps));
247
+ return new Set([...packageNames, ...results.flatMap((s) => [...s])]);
248
+ }
249
+
250
+ /**
251
+ * Validate that a set of packages covers all of their transitive workspace dependencies,
252
+ * and that none of those dependencies are private (which would make them unpublishable).
253
+ *
254
+ * @param {PublicPackage[]} packages - The packages intended for publishing
255
+ * @returns {Promise<{issues: string[]}>}
256
+ * List of human-readable issue strings. Empty when the dependency set is valid.
257
+ */
258
+ export async function validatePublishDependencies(packages) {
259
+ const allWorkspacePackages = await getWorkspacePackages();
260
+
261
+ /** @type {Map<string, PublicPackage | PrivatePackage>} */
262
+ const workspacePackageByName = new Map(
263
+ allWorkspacePackages.flatMap((pkg) => (pkg.name ? [[pkg.name, pkg]] : [])),
264
+ );
265
+ const workspacePathByName = new Map(
266
+ allWorkspacePackages.flatMap((pkg) => (pkg.name ? [[pkg.name, pkg.path]] : [])),
267
+ );
268
+
269
+ const publishedNames = new Set(packages.map((pkg) => pkg.name));
270
+ const transitiveDeps = await getTransitiveDependencies(
271
+ packages.map((pkg) => pkg.name),
272
+ { includeDev: false, workspacePathByName },
273
+ );
274
+
275
+ /** @type {Set<string>} */
276
+ const privateButRequired = new Set();
277
+ /** @type {Set<string>} */
278
+ const missingFromPublish = new Set();
279
+
280
+ for (const depName of transitiveDeps) {
281
+ if (publishedNames.has(depName)) {
282
+ continue;
283
+ }
284
+ const workspacePkg = workspacePackageByName.get(depName);
285
+ if (workspacePkg?.isPrivate) {
286
+ privateButRequired.add(depName);
287
+ } else {
288
+ missingFromPublish.add(depName);
289
+ }
290
+ }
291
+
292
+ /** @type {string[]} */
293
+ const issues = [];
294
+
295
+ if (privateButRequired.size > 0) {
296
+ issues.push(
297
+ `The following private workspace packages are required as dependencies but cannot be published: ${[...privateButRequired].join(', ')}`,
298
+ );
299
+ }
300
+
301
+ if (missingFromPublish.size > 0) {
302
+ issues.push(
303
+ `The following workspace packages are required as dependencies but are not included in the publish set: ${[...missingFromPublish].join(', ')}. Add them to the --filter list.`,
304
+ );
305
+ }
306
+
307
+ return { issues };
308
+ }
309
+
178
310
  /**
179
311
  * Read package.json from a directory
180
312
  * @param {string} packagePath - Path to package directory