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

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.
Files changed (145) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +21 -7
  3. package/build/babel-config.d.mts +5 -1
  4. package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
  5. package/build/brokenLinksChecker/index.d.mts +192 -2
  6. package/build/changelog/fetchChangelogs.d.mts +3 -0
  7. package/build/changelog/types.d.ts +3 -1
  8. package/build/cli/cmdArgosPush.d.mts +8 -2
  9. package/build/cli/cmdBuild.d.mts +48 -2
  10. package/build/cli/cmdCopyFiles.d.mts +14 -2
  11. package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
  12. package/build/cli/cmdGenerateChangelog.d.mts +17 -2
  13. package/build/cli/cmdGithubAuth.d.mts +2 -2
  14. package/build/cli/cmdListWorkspaces.d.mts +14 -2
  15. package/build/cli/cmdNetlifyIgnore.d.mts +12 -2
  16. package/build/cli/cmdPublish.d.mts +23 -4
  17. package/build/cli/cmdPublishCanary.d.mts +20 -2
  18. package/build/cli/cmdPublishNewPackage.d.mts +10 -2
  19. package/build/cli/cmdRenameScope.d.mts +18 -0
  20. package/build/cli/cmdSetVersionOverrides.d.mts +5 -2
  21. package/build/cli/cmdVale.d.mts +46 -0
  22. package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
  23. package/build/eslint/baseConfig.d.mts +3 -1
  24. package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
  25. package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
  26. package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
  27. package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
  28. package/build/eslint/mui/rules/no-presentation-role.d.mts +5 -0
  29. package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +8 -2
  30. package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
  31. package/build/remark/config.d.mts +43 -0
  32. package/build/remark/createLintTester.d.mts +10 -0
  33. package/build/remark/firstBlockHeading.d.mts +4 -0
  34. package/build/remark/gitDiff.d.mts +2 -0
  35. package/build/remark/noSpaceInLinks.d.mts +2 -0
  36. package/build/remark/straightQuotes.d.mts +2 -0
  37. package/build/remark/tableAlignment.d.mts +2 -0
  38. package/build/remark/terminalLanguage.d.mts +2 -0
  39. package/build/utils/babel.d.mts +7 -1
  40. package/build/utils/build.d.mts +59 -37
  41. package/build/utils/extractErrorCodes.d.mts +9 -0
  42. package/build/utils/git.d.mts +16 -0
  43. package/build/utils/github.d.mts +13 -1
  44. package/build/utils/pnpm.d.mts +176 -17
  45. package/build/utils/testUtils.d.mts +28 -0
  46. package/build/utils/typescript.d.mts +6 -16
  47. package/package.json +77 -53
  48. package/src/babel-config.mjs +3 -1
  49. package/src/brokenLinksChecker/crawlWorker.mjs +240 -0
  50. package/src/brokenLinksChecker/index.mjs +263 -188
  51. package/src/changelog/fetchChangelogs.mjs +8 -2
  52. package/src/changelog/renderChangelog.mjs +1 -1
  53. package/src/changelog/types.ts +3 -1
  54. package/src/cli/cmdBuild.mjs +51 -85
  55. package/src/cli/cmdListWorkspaces.mjs +7 -1
  56. package/src/cli/cmdNetlifyIgnore.mjs +30 -5
  57. package/src/cli/cmdPublish.mjs +79 -37
  58. package/src/cli/cmdPublishCanary.mjs +96 -24
  59. package/src/cli/cmdPublishNewPackage.mjs +31 -8
  60. package/src/cli/cmdRenameScope.mjs +76 -0
  61. package/src/cli/cmdSetVersionOverrides.mjs +8 -9
  62. package/src/cli/cmdVale.mjs +513 -0
  63. package/src/cli/index.mjs +4 -0
  64. package/src/cli/packageJson.d.ts +2 -3
  65. package/src/eslint/baseConfig.mjs +51 -19
  66. package/src/eslint/docsConfig.mjs +2 -1
  67. package/src/eslint/mui/config.mjs +24 -4
  68. package/src/eslint/mui/index.mjs +6 -0
  69. package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
  70. package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
  71. package/src/eslint/mui/rules/no-presentation-role.mjs +60 -0
  72. package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
  73. package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
  74. package/src/estree-typescript.d.ts +1 -1
  75. package/src/remark/config.mjs +157 -0
  76. package/src/remark/createLintTester.mjs +19 -0
  77. package/src/remark/firstBlockHeading.mjs +87 -0
  78. package/src/remark/gitDiff.mjs +43 -0
  79. package/src/remark/noSpaceInLinks.mjs +42 -0
  80. package/src/remark/straightQuotes.mjs +31 -0
  81. package/src/remark/tableAlignment.mjs +23 -0
  82. package/src/remark/terminalLanguage.mjs +19 -0
  83. package/src/stylelint/index.mjs +2 -2
  84. package/src/untyped-plugins.d.ts +14 -14
  85. package/src/utils/build.mjs +604 -270
  86. package/src/utils/git.mjs +27 -7
  87. package/src/utils/pnpm.mjs +444 -53
  88. package/src/utils/testUtils.mjs +53 -0
  89. package/src/utils/typescript.mjs +23 -42
  90. package/vale/.vale.ini +1 -0
  91. package/vale/styles/MUI/CorrectReferenceAllCases.yml +42 -0
  92. package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
  93. package/vale/styles/MUI/GoogleLatin.yml +11 -0
  94. package/vale/styles/MUI/MuiBrandName.yml +22 -0
  95. package/vale/styles/MUI/NoBritish.yml +112 -0
  96. package/vale/styles/MUI/NoCompanyName.yml +17 -0
  97. package/build/markdownlint/duplicate-h1.d.mts +0 -56
  98. package/build/markdownlint/git-diff.d.mts +0 -11
  99. package/build/markdownlint/index.d.mts +0 -52
  100. package/build/markdownlint/straight-quotes.d.mts +0 -11
  101. package/build/markdownlint/table-alignment.d.mts +0 -11
  102. package/build/markdownlint/terminal-language.d.mts +0 -11
  103. package/src/brokenLinksChecker/__fixtures__/static-site/broken-links.html +0 -20
  104. package/src/brokenLinksChecker/__fixtures__/static-site/broken-targets.html +0 -22
  105. package/src/brokenLinksChecker/__fixtures__/static-site/example.md +0 -20
  106. package/src/brokenLinksChecker/__fixtures__/static-site/external-links.html +0 -21
  107. package/src/brokenLinksChecker/__fixtures__/static-site/ignored-page.html +0 -17
  108. package/src/brokenLinksChecker/__fixtures__/static-site/index.html +0 -28
  109. package/src/brokenLinksChecker/__fixtures__/static-site/known-targets.json +0 -5
  110. package/src/brokenLinksChecker/__fixtures__/static-site/nested/page.html +0 -21
  111. package/src/brokenLinksChecker/__fixtures__/static-site/orphaned-page.html +0 -20
  112. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-api-links.html +0 -20
  113. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-custom-targets.html +0 -24
  114. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-ignored-content.html +0 -28
  115. package/src/brokenLinksChecker/__fixtures__/static-site/page-with-known-target-links.html +0 -19
  116. package/src/brokenLinksChecker/__fixtures__/static-site/unclosed-tags.html +0 -1
  117. package/src/brokenLinksChecker/__fixtures__/static-site/valid.html +0 -20
  118. package/src/brokenLinksChecker/__fixtures__/static-site/with-anchors.html +0 -31
  119. package/src/brokenLinksChecker/index.test.ts +0 -261
  120. package/src/changelog/categorizeCommits.test.ts +0 -319
  121. package/src/changelog/filterCommits.test.ts +0 -363
  122. package/src/changelog/parseCommitLabels.test.ts +0 -509
  123. package/src/changelog/renderChangelog.test.ts +0 -378
  124. package/src/changelog/sortSections.test.ts +0 -199
  125. package/src/eslint/mui/rules/add-undef-to-optional.test.mjs +0 -361
  126. package/src/eslint/mui/rules/consistent-production-guard.test.mjs +0 -162
  127. package/src/eslint/mui/rules/disallow-active-elements-as-key-event-target.test.mjs +0 -66
  128. package/src/eslint/mui/rules/disallow-react-api-in-server-components.test.mjs +0 -305
  129. package/src/eslint/mui/rules/docgen-ignore-before-comment.test.mjs +0 -52
  130. package/src/eslint/mui/rules/flatten-parentheses.test.mjs +0 -245
  131. package/src/eslint/mui/rules/mui-name-matches-component-name.test.mjs +0 -247
  132. package/src/eslint/mui/rules/no-empty-box.test.mjs +0 -40
  133. package/src/eslint/mui/rules/no-styled-box.test.mjs +0 -73
  134. package/src/eslint/mui/rules/require-dev-wrapper.test.mjs +0 -265
  135. package/src/eslint/mui/rules/rules-of-use-theme-variants.test.mjs +0 -149
  136. package/src/eslint/mui/rules/straight-quotes.test.mjs +0 -67
  137. package/src/markdownlint/duplicate-h1.mjs +0 -69
  138. package/src/markdownlint/git-diff.mjs +0 -31
  139. package/src/markdownlint/index.mjs +0 -62
  140. package/src/markdownlint/straight-quotes.mjs +0 -26
  141. package/src/markdownlint/table-alignment.mjs +0 -42
  142. package/src/markdownlint/terminal-language.mjs +0 -19
  143. package/src/utils/build.test.mjs +0 -705
  144. package/src/utils/template.test.mjs +0 -133
  145. package/src/utils/typescript.test.mjs +0 -380
@@ -10,6 +10,7 @@ import * as semver from 'semver';
10
10
  import {
11
11
  createPackageBin,
12
12
  createPackageExports,
13
+ createPackageImports,
13
14
  getOutExtension,
14
15
  mapConcurrently,
15
16
  validatePkgJson,
@@ -30,7 +31,8 @@ import {
30
31
  * @property {string[]} [copy] - Files/Directories to be copied. Can be a glob pattern.
31
32
  * @property {boolean} [enableReactCompiler] - Whether to use the React compiler.
32
33
  * @property {boolean} [tsgo] - Whether to build types using typescript native (tsgo).
33
- * @property {boolean} [flat] - Builds the package in a flat structure without subdirectories for each module type.
34
+ * @property {boolean} [flat] - Deprecated no-op; flat builds are always used.
35
+ * @property {boolean} expand - Whether to enumerate glob patterns in exports/imports into concrete entries.
34
36
  */
35
37
 
36
38
  const validBundles = [
@@ -45,13 +47,12 @@ const validBundles = [
45
47
  * @param {string} options.name - The name of the package.
46
48
  * @param {string} options.version - The version of the package.
47
49
  * @param {string} options.license - The license of the package.
48
- * @param {boolean} options.isFlat - Whether the build is flat structure.
49
50
  * @param {'module' | 'commonjs'} options.packageType - The package.json type field.
50
51
  * @param {import('../utils/build.mjs').BundleType} options.bundle
51
52
  * @param {string} options.outputDir
52
53
  */
53
- async function addLicense({ name, version, license, bundle, outputDir, isFlat, packageType }) {
54
- const outExtension = getOutExtension(bundle, { isFlat, packageType });
54
+ async function addLicense({ name, version, license, bundle, outputDir, packageType }) {
55
+ const outExtension = getOutExtension(bundle, { packageType });
55
56
  const file = path.join(outputDir, `index${outExtension}`);
56
57
  if (
57
58
  !(await fs.stat(file).then(
@@ -80,11 +81,11 @@ ${content}`,
80
81
  /**
81
82
  * @param {Object} param0
82
83
  * @param {import('./packageJson').PackageJson} param0.packageJson - The package.json content.
83
- * @param {{type: import('../utils/build.mjs').BundleType; dir: string}[]} param0.bundles
84
+ * @param {import('../utils/build.mjs').BundleType[]} param0.bundles
84
85
  * @param {string} param0.outputDir
85
86
  * @param {string} param0.cwd
86
87
  * @param {boolean} param0.addTypes - Whether to add type declarations for the package.
87
- * @param {boolean} param0.isFlat - Whether the build is flat structure.
88
+ * @param {boolean} [param0.expand] - Whether to enumerate glob patterns into concrete entries.
88
89
  * @param {'module' | 'commonjs'} param0.packageType - The package.json type field.
89
90
  */
90
91
  async function writePackageJson({
@@ -93,37 +94,48 @@ async function writePackageJson({
93
94
  outputDir,
94
95
  cwd,
95
96
  addTypes = false,
96
- isFlat = false,
97
+ expand = true,
97
98
  packageType,
98
99
  }) {
99
100
  delete packageJson.scripts;
100
101
  delete packageJson.publishConfig?.directory;
101
102
  delete packageJson.devDependencies;
102
- delete packageJson.imports;
103
103
 
104
104
  const resolvedPackageType = packageType || packageJson.type || 'commonjs';
105
105
  packageJson.type = resolvedPackageType;
106
106
 
107
107
  const originalExports = packageJson.exports;
108
108
  delete packageJson.exports;
109
+ const originalImports = packageJson.imports;
110
+ delete packageJson.imports;
109
111
  const originalBin = packageJson.bin;
110
112
  delete packageJson.bin;
111
113
 
112
- const {
113
- exports: packageExports,
114
- main,
115
- types,
116
- } = await createPackageExports({
117
- exports: originalExports,
118
- bundles,
119
- outputDir,
120
- cwd,
121
- addTypes,
122
- isFlat,
123
- packageType: resolvedPackageType,
124
- });
114
+ const [{ exports: packageExports, main, types }, packageImports] = await Promise.all([
115
+ createPackageExports(originalExports, {
116
+ bundles,
117
+ outputDir,
118
+ cwd,
119
+ addTypes,
120
+ expand,
121
+ packageType: resolvedPackageType,
122
+ }),
123
+ originalImports
124
+ ? createPackageImports(originalImports, {
125
+ bundles,
126
+ cwd,
127
+ outputDir,
128
+ addTypes,
129
+ expand,
130
+ packageType: resolvedPackageType,
131
+ })
132
+ : Promise.resolve(undefined),
133
+ ]);
125
134
 
126
135
  packageJson.exports = packageExports;
136
+ if (packageImports) {
137
+ packageJson.imports = packageImports;
138
+ }
127
139
  if (main) {
128
140
  packageJson.main = main;
129
141
  }
@@ -131,11 +143,9 @@ async function writePackageJson({
131
143
  packageJson.types = types;
132
144
  }
133
145
 
134
- const bin = await createPackageBin({
135
- bin: originalBin,
146
+ const bin = await createPackageBin(originalBin, {
136
147
  bundles,
137
148
  cwd,
138
- isFlat,
139
149
  packageType: resolvedPackageType,
140
150
  });
141
151
  if (bin) {
@@ -225,16 +235,23 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
225
235
  })
226
236
  .option('flat', {
227
237
  type: 'boolean',
228
- default: process.env.MUI_BUILD_FLAT === '1',
238
+ deprecated:
239
+ 'Flat builds are now always used; this flag is a no-op and will be removed in a future release.',
240
+ default: true,
241
+ description:
242
+ '@deprecated Flat builds are always used now. The package is always built in a flat structure without subdirectories for each module type.',
243
+ })
244
+ .option('expand', {
245
+ type: 'boolean',
246
+ default: true,
229
247
  description:
230
- 'Builds the package in a flat structure without subdirectories for each module type.',
248
+ 'Enumerate glob patterns in the package.json "exports"/"imports" into concrete entries. Use --no-expand to keep them as Node runtime subpath patterns.',
231
249
  });
232
250
  },
233
251
  async handler(args) {
234
252
  const {
235
253
  bundle: bundles,
236
254
  hasLargeFiles,
237
- skipBundlePackageJson,
238
255
  verbose = false,
239
256
  ignore: extraIgnores,
240
257
  buildTypes,
@@ -255,9 +272,6 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
255
272
  const packageType = packageJson.type === 'module' ? 'module' : 'commonjs';
256
273
 
257
274
  console.log(`Selected output directory: "${buildDirBase}"`);
258
- if (args.flat) {
259
- console.log('Building package in flat structure.');
260
- }
261
275
 
262
276
  await fs.rm(buildDir, { recursive: true, force: true });
263
277
 
@@ -282,15 +296,6 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
282
296
  }
283
297
 
284
298
  const { build: babelBuild, cjsCopy } = await import('../utils/babel.mjs');
285
- const relativeOutDirs = !args.flat
286
- ? {
287
- cjs: '.',
288
- esm: 'esm',
289
- }
290
- : {
291
- cjs: '.',
292
- esm: '.',
293
- };
294
299
  const sourceDir = path.join(cwd, 'src');
295
300
  const reactVersion =
296
301
  semver.minVersion(packageJson.peerDependencies?.react || '')?.version ?? 'latest';
@@ -306,13 +311,10 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
306
311
  await Promise.all(
307
312
  bundles.map(async (bundle) => {
308
313
  const outExtension = getOutExtension(bundle, {
309
- isFlat: !!args.flat,
310
314
  isType: false,
311
315
  packageType,
312
316
  });
313
- const relativeOutDir = relativeOutDirs[bundle];
314
- const outputDir = path.join(buildDir, relativeOutDir);
315
- await fs.mkdir(outputDir, { recursive: true });
317
+ await fs.mkdir(buildDir, { recursive: true });
316
318
 
317
319
  const promises = [];
318
320
 
@@ -320,7 +322,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
320
322
  babelBuild({
321
323
  cwd,
322
324
  sourceDir,
323
- outDir: outputDir,
325
+ outDir: buildDir,
324
326
  babelRuntimeVersion,
325
327
  hasLargeFiles,
326
328
  bundle,
@@ -340,61 +342,28 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
340
342
  }),
341
343
  );
342
344
 
343
- if (buildDir !== outputDir && !skipBundlePackageJson && !args.flat) {
344
- // @TODO - Not needed if the output extension is .mjs. Remove this before PR merge.
345
- promises.push(
346
- fs.writeFile(
347
- path.join(outputDir, 'package.json'),
348
- JSON.stringify({
349
- type: bundle === 'esm' ? 'module' : 'commonjs',
350
- sideEffects: packageJson.sideEffects ?? false,
351
- }),
352
- ),
353
- );
354
- }
355
-
356
- if (!args.flat) {
357
- // cjs for reexporting from commons only modules.
358
- // @NOTE: If we need to rely more on this we can think about setting up
359
- // a separate commonjs => commonjs build for .cjs files to .cjs
360
- // `--extensions-.cjs --out-file-extension .cjs`
361
- promises.push(cjsCopy({ from: sourceDir, to: outputDir }));
362
- }
363
-
364
345
  await Promise.all(promises);
365
346
  await addLicense({
366
347
  bundle,
367
348
  license: packageJson.license,
368
349
  name: packageJson.name,
369
350
  version: packageJson.version,
370
- outputDir,
371
- isFlat: !!args.flat,
351
+ outputDir: buildDir,
372
352
  packageType,
373
353
  });
374
354
  }),
375
355
  );
376
356
 
377
- if (args.flat) {
378
- await cjsCopy({ from: sourceDir, to: buildDir });
379
- }
357
+ await cjsCopy({ from: sourceDir, to: buildDir });
380
358
  // js build end
381
359
 
382
360
  if (buildTypes) {
383
361
  const tsMod = await import('../utils/typescript.mjs');
384
- /**
385
- * @type {{type: import('../utils/build.mjs').BundleType, dir: string}[]};
386
- */
387
- const bundleMap = bundles.map((type) => ({
388
- type,
389
- dir: relativeOutDirs[type],
390
- }));
391
-
392
362
  await tsMod.createTypes({
393
- bundles: bundleMap,
363
+ bundles,
394
364
  srcDir: sourceDir,
395
365
  cwd,
396
366
  skipTsc,
397
- isFlat: !!args.flat,
398
367
  buildDir,
399
368
  useTsgo,
400
369
  packageType,
@@ -409,13 +378,10 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
409
378
  await writePackageJson({
410
379
  cwd,
411
380
  packageJson,
412
- bundles: bundles.map((type) => ({
413
- type,
414
- dir: relativeOutDirs[type],
415
- })),
381
+ bundles,
416
382
  outputDir: buildDir,
417
383
  addTypes: buildTypes,
418
- isFlat: !!args.flat,
384
+ expand: args.expand,
419
385
  packageType,
420
386
  });
421
387
 
@@ -61,7 +61,13 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
61
61
  console.log(pkg.path);
62
62
  });
63
63
  } else if (output === 'publish-dir') {
64
- // TODO: Remove this option once https://github.com/stackblitz-labs/pkg.pr.new/issues/389 is resolved
64
+ // Works around https://github.com/stackblitz-labs/pkg.pr.new/issues/389: pkg-pr-new rewrites
65
+ // workspace:* deps to its URLs in the source package.json, but `pnpm pack` honors
66
+ // publishConfig.directory and packs the build package.json, whose workspace:* deps are left to
67
+ // resolve to plain versions. Pointing pkg-pr-new at the build dir makes both act on the same file.
68
+ // Note: #389 was closed by pkg-pr-new#499, but that only fixed the tarball location, not the dep
69
+ // resolution — verified still broken on 0.0.78. Do not remove until the packed deps are URLs
70
+ // without this. Removing it prematurely regressed publishing once (see mui-public#1354).
65
71
  // Print publish directories (package.json publishConfig.directory or package path)
66
72
  const publishDirs = await Promise.all(
67
73
  packages.map(async (pkg) => {
@@ -6,6 +6,7 @@
6
6
  * @typedef {Object} Args
7
7
  * @property {string[]} workspaces - List of workspace names to process
8
8
  * @property {boolean} [check] - Check mode - error if the generated content differs from current
9
+ * @property {string} [baseBranch] - Branch to compare PRs against (default: master)
9
10
  */
10
11
 
11
12
  import * as fs from 'node:fs/promises';
@@ -15,16 +16,29 @@ import { toPosixPath } from '../utils/path.mjs';
15
16
  import { getTransitiveDependencies, getWorkspacePackages } from '../utils/pnpm.mjs';
16
17
 
17
18
  /**
18
- * Generate the ignore command string for netlify.toml
19
+ * Generate the ignore command string for netlify.toml.
20
+ *
21
+ * Production and branch deploys (every Netlify $CONTEXT other than
22
+ * "deploy-preview") always build, so downstream plugins (e.g. e2e triggers)
23
+ * run on every deploy and catch regressions in external dependencies even
24
+ * when the commit doesn't touch the watched paths.
25
+ *
26
+ * Only deploy-previews (PR previews) are eligible to be skipped: they diff
27
+ * against the merge-base with origin/<baseBranch>. This way a PR rebase whose
28
+ * head commit doesn't touch the watched paths still rebuilds when the PR as a
29
+ * whole introduces changes to them — otherwise downstream plugins silently
30
+ * never run.
31
+ *
19
32
  * @param {string[]} paths - Array of paths to include in the ignore command
20
33
  * @param {string} packagePath - Absolute path to the package directory
21
34
  * @param {string} workspaceRoot - Absolute path to the workspace root
35
+ * @param {string} baseBranch - Branch to compare PRs against
22
36
  * @returns {string} The ignore command string
23
37
  */
24
- function generateIgnoreCommand(paths, packagePath, workspaceRoot) {
38
+ function generateIgnoreCommand(paths, packagePath, workspaceRoot, baseBranch) {
25
39
  const relFromBase = `${toPosixPath(path.relative(packagePath, workspaceRoot))}/`;
26
40
  const pathsStr = paths.join(' ');
27
- return ` ignore = "cd ${relFromBase} && git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ${pathsStr}"`;
41
+ return ` ignore = """cd ${relFromBase} && [ "$CONTEXT" = "deploy-preview" ] && git fetch origin ${baseBranch} --depth=500 -q && git diff --quiet FETCH_HEAD...$COMMIT_REF -- ${pathsStr}"""`;
28
42
  }
29
43
 
30
44
  /**
@@ -94,6 +108,12 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
94
108
  default: false,
95
109
  describe: 'Check if the netlify.toml needs updating without modifying it',
96
110
  })
111
+ .option('base-branch', {
112
+ type: 'string',
113
+ default: 'master',
114
+ describe:
115
+ "Branch to compare PRs against (the site's production branch on Netlify). Production and branch deploys always rebuild regardless of this value.",
116
+ })
97
117
  .example('$0 netlify-ignore @mui/internal-docs-infra', 'Update netlify.toml for a workspace')
98
118
  .example(
99
119
  '$0 netlify-ignore @mui/internal-docs-infra @mui/internal-code-infra',
@@ -105,7 +125,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
105
125
  );
106
126
  },
107
127
  handler: async (argv) => {
108
- const { workspaces, check = false } = argv;
128
+ const { workspaces, check = false, baseBranch = 'master' } = argv;
109
129
 
110
130
  // Get the workspace root
111
131
  const workspaceRoot = await findWorkspaceDir(process.cwd());
@@ -157,7 +177,12 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
157
177
  const allPaths = [...relativePaths, 'pnpm-lock.yaml'];
158
178
 
159
179
  // Generate the ignore command for this workspace
160
- const newIgnoreCommand = generateIgnoreCommand(allPaths, workspacePath, workspaceRoot);
180
+ const newIgnoreCommand = generateIgnoreCommand(
181
+ allPaths,
182
+ workspacePath,
183
+ workspaceRoot,
184
+ baseBranch,
185
+ );
161
186
 
162
187
  // Update or check the netlify.toml file
163
188
  await updateNetlifyToml(tomlPath, newIgnoreCommand, check);
@@ -5,6 +5,7 @@
5
5
  /**
6
6
  * @typedef {import('../utils/pnpm.mjs').PublicPackage} PublicPackage
7
7
  * @typedef {import('../utils/pnpm.mjs').PublishOptions} PublishOptions
8
+ * @typedef {import('../utils/pnpm.mjs').PublishSummaryEntry} PublishSummaryEntry
8
9
  */
9
10
 
10
11
  import select from '@inquirer/select';
@@ -18,11 +19,13 @@ import * as semver from 'semver';
18
19
 
19
20
  import { persistentAuthStrategy } from '../utils/github.mjs';
20
21
  import {
22
+ getPackagesNeedingManualPublish,
23
+ getReleaseVersion,
21
24
  getWorkspacePackages,
22
25
  publishPackages,
23
26
  validatePublishDependencies,
24
27
  } from '../utils/pnpm.mjs';
25
- import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';
28
+ import { getCurrentGitSha, getRepositoryInfo, remoteGitTagExists } from '../utils/git.mjs';
26
29
 
27
30
  const isCI = envCI().isCi;
28
31
 
@@ -40,16 +43,6 @@ function getOctokit() {
40
43
  * @property {string[]} [filter] Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
41
44
  */
42
45
 
43
- /**
44
- * Get the version to release from the root package.json
45
- * @returns {Promise<string | null>} Version string
46
- */
47
- async function getReleaseVersion() {
48
- const result = await $`pnpm pkg get version`;
49
- const version = JSON.parse(result.stdout.trim());
50
- return semver.valid(version);
51
- }
52
-
53
46
  /**
54
47
  * Parse changelog to extract content for a specific version
55
48
  * @param {string} changelogPath - Path to CHANGELOG.md
@@ -139,8 +132,19 @@ async function checkGitHubReleaseExists(owner, repo, version) {
139
132
  */
140
133
  async function createGitTag(version, dryRun = false) {
141
134
  const tagName = `v${version}`;
135
+ // The CI smoke-test runs with a read-only token that can't push. It sets this
136
+ // so we tolerate the push failure explicitly, instead of assuming any dry-run
137
+ // permission error is expected.
138
+ const isPublishTest = process.env.IS_PUBLISH_TEST === 'true';
142
139
 
143
140
  try {
141
+ // Skip if the tag already exists locally. Tag creation is local and fails if it exists.
142
+ const { stdout: existingTag } = await $`git tag -l ${tagName}`;
143
+ if (existingTag.trim()) {
144
+ console.log(`šŸ·ļø Git tag ${tagName} already exists, skipping`);
145
+ return;
146
+ }
147
+
144
148
  await $({
145
149
  env: {
146
150
  ...process.env,
@@ -148,10 +152,28 @@ async function createGitTag(version, dryRun = false) {
148
152
  GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
149
153
  },
150
154
  })`git tag -a ${tagName} -m ${`Version ${version}`}`;
151
- const pushArgs = dryRun ? ['--dry-run'] : [];
152
- await $({ stdio: 'inherit' })`git push origin ${tagName} ${pushArgs}`;
153
155
 
154
- console.log(`šŸ·ļø Created and pushed git tag ${tagName}${dryRun ? ' (dry-run)' : ''}`);
156
+ if (dryRun) {
157
+ // `git push --dry-run` still authenticates and needs push access. When the token
158
+ // can push, validate it for real; when it can't, only tolerate the failure if this
159
+ // is an explicit publish smoke-test and the error is a genuine permission denial.
160
+ try {
161
+ // Don't inherit stdio so stderr is captured and matchable below.
162
+ await $`git push origin ${tagName} --dry-run`;
163
+ console.log(`šŸ·ļø Created git tag ${tagName} (dry-run)`);
164
+ } catch (/** @type {any} */ pushError) {
165
+ const message = pushError.stderr || pushError.message || '';
166
+ if (isPublishTest && /permission|403|denied/i.test(message)) {
167
+ console.log(`šŸ·ļø Created git tag ${tagName} (dry-run, no push permission, skipped push)`);
168
+ return;
169
+ }
170
+ throw pushError;
171
+ }
172
+ return;
173
+ }
174
+ await $({ stdio: 'inherit' })`git push origin ${tagName}`;
175
+
176
+ console.log(`šŸ·ļø Created and pushed git tag ${tagName}`);
155
177
  } catch (/** @type {any} */ error) {
156
178
  throw new Error(`Failed to create git tag: ${error.message}`);
157
179
  }
@@ -194,18 +216,11 @@ async function validateGitHubRelease(version) {
194
216
  * Publish packages to npm
195
217
  * @param {PublicPackage[]} packages - Packages to publish
196
218
  * @param {PublishOptions} options - Publishing options
197
- * @returns {Promise<void>}
219
+ * @returns {Promise<PublishSummaryEntry[]>}
198
220
  */
199
221
  async function publishToNpm(packages, options) {
200
- console.log('\nšŸ“¦ Publishing packages to npm...');
201
- console.log(`šŸ“‹ Found ${packages.length} packages:`);
202
- packages.forEach((pkg) => {
203
- console.log(` • ${pkg.name}@${pkg.version}`);
204
- });
205
-
206
222
  // Use pnpm's built-in duplicate checking - no need to check versions ourselves
207
- await publishPackages(packages, options);
208
- console.log('āœ… Successfully published to npm');
223
+ return publishPackages(packages, options);
209
224
  }
210
225
 
211
226
  /**
@@ -301,9 +316,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
301
316
  // Get all packages
302
317
  console.log('šŸ” Discovering all workspace packages...');
303
318
 
304
- const allPackages = await getWorkspacePackages({ publicOnly: true, filter });
319
+ const filteredPackages = await getWorkspacePackages({ publicOnly: true, filter });
305
320
 
306
- if (allPackages.length === 0) {
321
+ if (filteredPackages.length === 0) {
307
322
  console.log(
308
323
  `āš ļø No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
309
324
  );
@@ -313,12 +328,17 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
313
328
  if (filter.length > 0) {
314
329
  console.log('šŸ” Validating workspace dependencies for filtered packages...');
315
330
 
316
- const { issues } = await validatePublishDependencies(allPackages);
331
+ const { issues } = await validatePublishDependencies(filteredPackages);
317
332
 
318
333
  if (issues.length > 0) {
319
- throw new Error('Invalid dependencies structure of packages to be published.', {
320
- cause: issues,
321
- });
334
+ throw new Error(
335
+ `Invalid dependencies structure of packages to be published -
336
+ ${issues.join('\n ')}
337
+ `,
338
+ {
339
+ cause: issues,
340
+ },
341
+ );
322
342
  }
323
343
 
324
344
  console.log('āœ… All workspace dependency requirements satisfied');
@@ -338,21 +358,41 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
338
358
  githubReleaseData = await validateGitHubRelease(version);
339
359
  }
340
360
 
341
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
361
+ const newPackages = await getPackagesNeedingManualPublish(filteredPackages);
342
362
 
343
363
  if (newPackages.length > 0) {
364
+ const newPackageNames = newPackages.map((pkg) => pkg.name).join(', ');
344
365
  throw new Error(
345
- `The following packages are new and need to be published manually first: ${newPackages.join(
346
- ', ',
347
- )}. Read more about it here: https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`,
366
+ `The following packages are new and need to be published to npm manually first: ${newPackageNames}. Read more about it here: https://github.com/mui/mui-public/blob/master/packages/code-infra/README.md#adding-and-publishing-new-packages`,
348
367
  );
349
368
  }
350
369
 
351
370
  // Publish to npm (pnpm handles duplicate checking automatically)
352
371
  // No git checks, we'll do our own
353
- await publishToNpm(allPackages, { dryRun, noGitChecks: true, tag });
372
+ console.log('\nšŸ“¦ Publishing packages to npm...');
373
+ const publishedPackages = await publishToNpm(filteredPackages, {
374
+ dryRun,
375
+ noGitChecks: true,
376
+ tag,
377
+ });
378
+
379
+ if (publishedPackages.length === 0) {
380
+ console.log('ā„¹ļø No packages were published (all may already be up to date on npm)');
381
+ console.log('\nšŸ Nothing to publish, skipping git tag and GitHub release.');
382
+ return;
383
+ }
384
+
385
+ publishedPackages.forEach((pkg) => {
386
+ console.log(`āœ… Published ${pkg.name}@${pkg.version}`);
387
+ });
354
388
 
355
- await createGitTag(version, dryRun);
389
+ // Tag the root version when it's new. Arbitrary package publishes that don't
390
+ // bump the root version don't create a new tag, so skip in that case.
391
+ if (await remoteGitTagExists(`v${version}`)) {
392
+ console.log(`ā„¹ļø Git tag v${version} already exists, skipping tag creation.`);
393
+ } else {
394
+ await createGitTag(version, dryRun);
395
+ }
356
396
 
357
397
  // Create GitHub release or git tag after successful npm publishing
358
398
  if (githubRelease && githubReleaseData) {
@@ -380,11 +420,13 @@ const PUBLISH_WORKFLOW_ID = `.github/${WORKFLOW_PATH}`;
380
420
  */
381
421
  async function triggerLocalGithubPublishWorkflow(opts) {
382
422
  console.log(`šŸ” Checking if there are new packages to publish in the workspace...`);
383
- const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
423
+ const newPackages = await getPackagesNeedingManualPublish(
424
+ await getWorkspacePackages({ publicOnly: true }),
425
+ );
384
426
  if (newPackages.length) {
385
427
  console.warn(
386
428
  `āš ļø Found new packages that should be published to npm first before triggering a release:
387
- * ${newPackages.map((pkg) => pkg.name).join(' * ')}
429
+ ${newPackages.map((pkg) => ` * ${pkg.name}`).join('\n')}
388
430
  Please run the command "${chalk.bold('pnpm code-infra publish-new-package')}" first to publish and configure npm.`,
389
431
  );
390
432
  return;