@mui/internal-code-infra 0.0.3-canary.4 → 0.0.3-canary.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-code-infra",
3
- "version": "0.0.3-canary.4",
3
+ "version": "0.0.3-canary.6",
4
4
  "description": "Infra scripts and configs to be used across MUI repos.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "@argos-ci/core": "^4.1.2",
25
25
  "@babel/cli": "^7.28.3",
26
26
  "@babel/core": "^7.28.4",
27
+ "@babel/plugin-syntax-jsx": "^7.27.1",
27
28
  "@babel/plugin-syntax-typescript": "^7.27.1",
28
29
  "@babel/plugin-transform-runtime": "^7.28.3",
29
30
  "@babel/preset-env": "^7.28.3",
@@ -60,8 +61,8 @@
60
61
  "typescript-eslint": "^8.42.0",
61
62
  "yargs": "^18.0.0",
62
63
  "@mui/internal-babel-plugin-display-name": "1.0.4-canary.7",
63
- "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.20",
64
- "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.9"
64
+ "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.10",
65
+ "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.20"
65
66
  },
66
67
  "peerDependencies": {
67
68
  "eslint": "^9.0.0",
@@ -92,7 +93,7 @@
92
93
  "publishConfig": {
93
94
  "access": "public"
94
95
  },
95
- "gitSha": "f9fb2233d5f6d847f6205b345bb829a712eea199",
96
+ "gitSha": "b76c4e6f0a445cd70cf69bb4513802a108d24e2c",
96
97
  "scripts": {
97
98
  "typescript": "tsc -p tsconfig.json",
98
99
  "test": "pnpm -w test --project @mui/internal-code-infra",
package/src/cli/babel.mjs CHANGED
@@ -6,6 +6,7 @@ import { globby } from 'globby';
6
6
  import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
  import { $ } from 'execa';
9
+ import { BASE_IGNORES } from '../utils/build.mjs';
9
10
 
10
11
  const TO_TRANSFORM_EXTENSIONS = ['.js', '.ts', '.tsx'];
11
12
 
@@ -64,18 +65,6 @@ export async function cjsCopy({ from, to }) {
64
65
  * @property {string} [runtimeModule] - The runtime module to replace the errors with.
65
66
  */
66
67
 
67
- const BASE_IGNORES = [
68
- '**/*.test.js',
69
- '**/*.test.ts',
70
- '**/*.test.tsx',
71
- '**/*.spec.js',
72
- '**/*.spec.ts',
73
- '**/*.spec.tsx',
74
- '**/*.d.ts',
75
- '**/*.test/*.*',
76
- '**/test-cases/*.*',
77
- ];
78
-
79
68
  /**
80
69
  * @param {Object} options
81
70
  * @param {boolean} [options.verbose=false] - Whether to enable verbose logging.
@@ -2,6 +2,7 @@ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
2
2
  import { globby } from 'globby';
3
3
  import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { mapConcurrently } from '../utils/build.mjs';
5
6
 
6
7
  /**
7
8
  * @typedef {Object} Args
@@ -105,20 +106,13 @@ async function processGlobs({ globs, cwd, silent = true, buildDir }) {
105
106
  });
106
107
  });
107
108
 
108
- const concurrency = filesToProcess.length > 100 ? 100 : filesToProcess.length;
109
- const iterator = filesToProcess[Symbol.iterator]();
110
- const workers = [];
111
- for (let i = 0; i < concurrency; i += 1) {
112
- workers.push(
113
- Promise.resolve().then(async () => {
114
- for (const file of iterator) {
115
- // eslint-disable-next-line no-await-in-loop
116
- await recursiveCopy({ source: file.sourcePath, target: file.targetPath, silent });
117
- }
118
- }),
119
- );
120
- }
121
- await Promise.all(workers);
109
+ await mapConcurrently(
110
+ filesToProcess,
111
+ async (file) => {
112
+ await recursiveCopy({ source: file.sourcePath, target: file.targetPath, silent });
113
+ },
114
+ 50,
115
+ );
122
116
  return filesToProcess.length;
123
117
  }
124
118
 
@@ -0,0 +1,41 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import { markFn, measureFn } from '../utils/build.mjs';
4
+
5
+ /**
6
+ * @typedef {import('../utils/extractErrorCodes.mjs').Args} Args
7
+ */
8
+
9
+ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
10
+ command: 'extract-error-codes',
11
+ describe: 'Extracts error codes from package(s).',
12
+ builder(yargs) {
13
+ return yargs
14
+ .option('errorCodesPath', {
15
+ type: 'string',
16
+ describe: 'The output path to a json file to write the extracted error codes.',
17
+ demandOption: true,
18
+ })
19
+ .option('detection', {
20
+ type: 'string',
21
+ describe: 'The detection strategy to use when extracting error codes.',
22
+ choices: ['opt-in', 'opt-out'],
23
+ default: 'opt-in',
24
+ })
25
+ .option('skip', {
26
+ type: 'array',
27
+ describe: 'List of package names to skip.',
28
+ default: [],
29
+ });
30
+ },
31
+ async handler(args) {
32
+ const commandName = /** @type {string} */ (args._[0]);
33
+ await markFn(commandName, async () => {
34
+ const module = await import('../utils/extractErrorCodes.mjs');
35
+ await module.default(args);
36
+ });
37
+ console.log(
38
+ `✅ Extracted error codes in ${(measureFn(commandName).duration / 1000.0).toFixed(3)}s`,
39
+ );
40
+ },
41
+ });
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import fs from 'node:fs/promises';
5
5
  import { globby } from 'globby';
6
6
  import path from 'node:path';
7
+ import { mapConcurrently } from '../utils/build.mjs';
7
8
 
8
9
  /**
9
10
  * @typedef {Object} Args
@@ -42,33 +43,25 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
42
43
  followSymbolicLinks: false,
43
44
  });
44
45
 
45
- const fileIterator = filenames[Symbol.iterator]();
46
- const concurrency = Math.min(20, filenames.length);
47
46
  let passed = true;
48
- const workers = [];
49
47
 
50
- for (let i = 0; i < concurrency; i += 1) {
51
- // eslint-disable-next-line @typescript-eslint/no-loop-func
52
- const worker = Promise.resolve().then(async () => {
53
- for (const filename of fileIterator) {
54
- // eslint-disable-next-line no-await-in-loop
55
- const content = await fs.readFile(path.join(cwd, filename), { encoding: 'utf8' });
56
- try {
57
- JSON.parse(content);
58
- if (!args.silent) {
59
- // eslint-disable-next-line no-console
60
- console.log(passMessage(filename));
61
- }
62
- } catch (error) {
63
- passed = false;
64
- console.error(failMessage(`Error parsing ${filename}:\n\n${String(error)}`));
48
+ await mapConcurrently(
49
+ filenames,
50
+ async (filename) => {
51
+ const content = await fs.readFile(path.join(cwd, filename), { encoding: 'utf8' });
52
+ try {
53
+ JSON.parse(content);
54
+ if (!args.silent) {
55
+ // eslint-disable-next-line no-console
56
+ console.log(passMessage(filename));
65
57
  }
58
+ } catch (error) {
59
+ passed = false;
60
+ console.error(failMessage(`Error parsing ${filename}:\n\n${String(error)}`));
66
61
  }
67
- });
68
- workers.push(worker);
69
- }
70
-
71
- await Promise.allSettled(workers);
62
+ },
63
+ 20,
64
+ );
72
65
  if (!passed) {
73
66
  throw new Error('❌ At least one file did not pass. Check the console output');
74
67
  }
package/src/cli/index.mjs CHANGED
@@ -5,6 +5,7 @@ import { hideBin } from 'yargs/helpers';
5
5
  import cmdArgosPush from './cmdArgosPush.mjs';
6
6
  import cmdBuild from './cmdBuild.mjs';
7
7
  import cmdCopyFiles from './cmdCopyFiles.mjs';
8
+ import cmdExtractErrorCodes from './cmdExtractErrorCodes.mjs';
8
9
  import cmdJsonLint from './cmdJsonLint.mjs';
9
10
  import cmdListWorkspaces from './cmdListWorkspaces.mjs';
10
11
  import cmdPublish from './cmdPublish.mjs';
@@ -15,14 +16,16 @@ const pkgJson = createRequire(import.meta.url)('../../package.json');
15
16
 
16
17
  yargs()
17
18
  .scriptName('code-infra')
19
+ .usage('$0 <command> [args]')
20
+ .command(cmdArgosPush)
21
+ .command(cmdBuild)
22
+ .command(cmdCopyFiles)
23
+ .command(cmdExtractErrorCodes)
24
+ .command(cmdJsonLint)
25
+ .command(cmdListWorkspaces)
18
26
  .command(cmdPublish)
19
27
  .command(cmdPublishCanary)
20
- .command(cmdListWorkspaces)
21
- .command(cmdJsonLint)
22
- .command(cmdArgosPush)
23
28
  .command(cmdSetVersionOverrides)
24
- .command(cmdCopyFiles)
25
- .command(cmdBuild)
26
29
  .demandCommand(1, 'You need at least one command before moving on')
27
30
  .strict()
28
31
  .help()
@@ -411,7 +411,10 @@ export function createCoreConfig(options = {}) {
411
411
  'material-ui/no-styled-box': 'error',
412
412
  'material-ui/straight-quotes': 'off',
413
413
 
414
- 'react-hooks/exhaustive-deps': ['error', { additionalHooks: 'useEnhancedEffect' }],
414
+ 'react-hooks/exhaustive-deps': [
415
+ 'error',
416
+ { additionalHooks: '(useEnhancedEffect|useIsoLayoutEffect)' },
417
+ ],
415
418
  'react-hooks/rules-of-hooks': 'error',
416
419
 
417
420
  'react/default-props-match-prop-types': [
@@ -102,6 +102,13 @@ declare module '@babel/plugin-transform-runtime' {
102
102
  export default plugin;
103
103
  }
104
104
 
105
+ declare module '@babel/plugin-syntax-jsx' {
106
+ import type { PluginItem } from '@babel/core';
107
+
108
+ declare const plugin: PluginItem;
109
+ export default plugin;
110
+ }
111
+
105
112
  declare module '@babel/plugin-syntax-typescript' {
106
113
  import type { PluginItem } from '@babel/core';
107
114
 
@@ -68,3 +68,82 @@ export function validatePkgJson(packageJson, options = {}) {
68
68
  throw error;
69
69
  }
70
70
  }
71
+
72
+ /**
73
+ * Marks the start and end of a function execution for performance measurement.
74
+ * Uses the Performance API to create marks and measure the duration.
75
+ * @function
76
+ * @template {() => Promise<any>} F
77
+ * @param {string} label
78
+ * @param {() => ReturnType<F>} fn
79
+ * @returns {Promise<ReturnType<F>>}
80
+ */
81
+ export async function markFn(label, fn) {
82
+ const startMark = `${label}-start`;
83
+ const endMark = `${label}-end`;
84
+ performance.mark(startMark);
85
+ const result = await fn();
86
+ performance.mark(endMark);
87
+ performance.measure(label, startMark, endMark);
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * @param {string} label
93
+ */
94
+ export function measureFn(label) {
95
+ const startMark = `${label}-start`;
96
+ const endMark = `${label}-end`;
97
+ return performance.measure(label, startMark, endMark);
98
+ }
99
+
100
+ export const BASE_IGNORES = [
101
+ '**/*.test.js',
102
+ '**/*.test.ts',
103
+ '**/*.test.tsx',
104
+ '**/*.spec.js',
105
+ '**/*.spec.ts',
106
+ '**/*.spec.tsx',
107
+ '**/*.d.ts',
108
+ '**/*.test/*.*',
109
+ '**/test-cases/*.*',
110
+ ];
111
+
112
+ /**
113
+ * A utility to map a function over an array of items in a worker pool.
114
+ *
115
+ * This function will create a pool of workers and distribute the items to be processed among them.
116
+ * Each worker will process items sequentially, but multiple workers will run in parallel.
117
+ *
118
+ * @function
119
+ * @template T
120
+ * @template R
121
+ * @param {T[]} items
122
+ * @param {(item: T) => Promise<R>} mapper
123
+ * @param {number} concurrency
124
+ * @returns {Promise<(R|Error)[]>}
125
+ */
126
+ export async function mapConcurrently(items, mapper, concurrency) {
127
+ if (!items.length) {
128
+ return Promise.resolve([]); // nothing to do
129
+ }
130
+ const itemIterator = items.entries();
131
+ const count = Math.min(concurrency, items.length);
132
+ const workers = [];
133
+ /**
134
+ * @type {(R|Error)[]}
135
+ */
136
+ const results = new Array(items.length);
137
+ for (let i = 0; i < count; i += 1) {
138
+ const worker = Promise.resolve().then(async () => {
139
+ for (const [index, item] of itemIterator) {
140
+ // eslint-disable-next-line no-await-in-loop
141
+ const res = await mapper(item);
142
+ results[index] = res;
143
+ }
144
+ });
145
+ workers.push(worker);
146
+ }
147
+ await Promise.all(workers);
148
+ return results;
149
+ }
@@ -0,0 +1,168 @@
1
+ /* eslint-disable no-console */
2
+ import { types as babelTypes, parseAsync, traverse } from '@babel/core';
3
+ import babelSyntaxJsx from '@babel/plugin-syntax-jsx';
4
+ import babelSyntaxTypescript from '@babel/plugin-syntax-typescript';
5
+ import { findMessageNode } from '@mui/internal-babel-plugin-minify-errors';
6
+ import { globby } from 'globby';
7
+ import * as fs from 'node:fs/promises';
8
+ import * as path from 'node:path';
9
+
10
+ import { getWorkspacePackages } from '../cli/pnpm.mjs';
11
+ import { BASE_IGNORES, mapConcurrently } from './build.mjs';
12
+
13
+ /**
14
+ * @typedef {Object} Args
15
+ * @property {string} errorCodesPath - The output path to write the extracted error codes.
16
+ * @property {string[]} [skip=[]] - List of package names to skip. By default, all workspace packages are considered.
17
+ * @property {import('@mui/internal-babel-plugin-minify-errors').Options['detection']} [detection='opt-in'] - The detection strategy to use when extracting error codes.
18
+ */
19
+
20
+ /**
21
+ * Gets all relevant files for a package to parse.
22
+ *
23
+ * @param {import('../cli/pnpm.mjs').PublicPackage} pkg
24
+ * @returns {Promise<string[]>} An array of file paths.
25
+ */
26
+ async function getFilesForPackage(pkg) {
27
+ const srcPath = path.join(pkg.path, 'src');
28
+ const srcPathExists = await fs
29
+ .stat(srcPath)
30
+ .then((stat) => stat.isDirectory())
31
+ .catch(() => false);
32
+ // Implementation to extract error codes from all files in the directory
33
+ const cwd = srcPathExists ? srcPath : pkg.path;
34
+ const files = await globby('**/*.{js,ts,jsx,tsx,cjs,mjs,cts}', {
35
+ ignore: [
36
+ '**/node_modules/**',
37
+ '**/dist/**',
38
+ '**/build/**',
39
+ 'scripts',
40
+ '**/__{tests,fixtures,mock,mocks}__/**',
41
+ ...BASE_IGNORES,
42
+ ],
43
+ cwd,
44
+ });
45
+ return files.map((file) => path.join(cwd, file));
46
+ }
47
+
48
+ /**
49
+ * Extracts error codes from all files in a directory.
50
+ * @param {string[]} files
51
+ * @param {Set<string>} errors
52
+ * @param {import('@mui/internal-babel-plugin-minify-errors').Options['detection']} [detection='opt-in']
53
+ */
54
+ async function extractErrorCodesForWorkspace(files, errors, detection = 'opt-in') {
55
+ await mapConcurrently(
56
+ files,
57
+ async (fullPath) => {
58
+ const code = await fs.readFile(fullPath, 'utf8');
59
+ const ast = await parseAsync(code, {
60
+ filename: fullPath,
61
+ sourceType: 'module',
62
+ plugins: [[babelSyntaxTypescript, { isTSX: true }], [babelSyntaxJsx]],
63
+ configFile: false,
64
+ babelrc: false,
65
+ browserslistConfigFile: false,
66
+ code: false,
67
+ });
68
+ if (!ast) {
69
+ throw new Error(`Failed to parse ${fullPath}`);
70
+ }
71
+ traverse(ast, {
72
+ NewExpression(newExpressionPath) {
73
+ const { message } =
74
+ findMessageNode(babelTypes, newExpressionPath, {
75
+ detection,
76
+ missingError: 'annotate',
77
+ }) ?? {};
78
+ if (message) {
79
+ errors.add(message.message);
80
+ }
81
+ },
82
+ });
83
+ },
84
+ 30,
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Extracts error codes from all workspace packages.
90
+ * @param {Args} args
91
+ */
92
+ export default async function extractErrorCodes(args) {
93
+ /**
94
+ * @type {Set<string>}
95
+ */
96
+ const errors = new Set();
97
+
98
+ // Find relevant files
99
+
100
+ const basePackages = await getWorkspacePackages({
101
+ publicOnly: true,
102
+ });
103
+ const { skip: skipPackages = [], errorCodesPath, detection = 'opt-in' } = args;
104
+ const packages = basePackages.filter(
105
+ (pkg) =>
106
+ // Ignore obvious packages that do not have user-facing errors
107
+ !pkg.name.startsWith('@mui/internal-') &&
108
+ !pkg.name.startsWith('@mui-internal/') &&
109
+ !skipPackages.includes(pkg.name),
110
+ );
111
+ const files = await Promise.all(packages.map((pkg) => getFilesForPackage(pkg)));
112
+ packages.forEach((pkg, index) => {
113
+ console.log(
114
+ `🔍 ${pkg.name}: Found ${files[index].length} file${files[index].length > 1 ? 's' : ''}`,
115
+ );
116
+ });
117
+
118
+ // Extract error codes from said files.
119
+ const filesToProcess = files.flat();
120
+ console.log(`🔍 Extracting error codes from ${filesToProcess.length} files...`);
121
+ await extractErrorCodesForWorkspace(filesToProcess, errors, detection);
122
+
123
+ // Write error codes to the specified file.
124
+ const errorCodeFilePath = path.resolve(errorCodesPath);
125
+ const fileExists = await fs
126
+ .stat(errorCodeFilePath)
127
+ .then((stat) => stat.isFile())
128
+ .catch((ex) => {
129
+ if (ex.code === 'ENOENT') {
130
+ return false;
131
+ }
132
+ return new Error(ex.message);
133
+ });
134
+
135
+ if (fileExists instanceof Error) {
136
+ throw fileExists;
137
+ }
138
+ /**
139
+ * @type {Record<string, string>}
140
+ */
141
+ const existingErrorCodes =
142
+ fileExists === true ? JSON.parse(await fs.readFile(errorCodeFilePath, 'utf-8')) : {};
143
+ const inverseLookupCode = new Map(
144
+ Object.entries(existingErrorCodes).map(([key, value]) => [value, Number(key)]),
145
+ );
146
+ const originalErrorCount = inverseLookupCode.size;
147
+ Array.from(errors).forEach((error) => {
148
+ if (!inverseLookupCode.has(error)) {
149
+ inverseLookupCode.set(error, inverseLookupCode.size + 1);
150
+ }
151
+ });
152
+ const finalErrorCodes = Array.from(inverseLookupCode.entries()).reduce((acc, [message, code]) => {
153
+ acc[code] = message;
154
+ return acc;
155
+ }, /** @type {Record<string, string>} */ ({}));
156
+ if (!fileExists) {
157
+ await fs.mkdir(path.dirname(errorCodeFilePath), { recursive: true });
158
+ }
159
+ const newErrorCount = inverseLookupCode.size - originalErrorCount;
160
+ if (newErrorCount === 0) {
161
+ console.log(`✅ No new error codes found.`);
162
+ } else {
163
+ console.log(
164
+ `📝 Wrote ${newErrorCount} new error code${newErrorCount > 1 ? 's' : ''} to "${errorCodesPath}"`,
165
+ );
166
+ await fs.writeFile(errorCodeFilePath, `${JSON.stringify(finalErrorCodes, null, 2)}\n`);
167
+ }
168
+ }