@mui/internal-code-infra 0.0.3-canary.1 → 0.0.3-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/package.json +21 -19
- package/src/babel-config.mjs +27 -8
- package/src/cli/babel.mjs +1 -12
- package/src/cli/cmdBuild.mjs +152 -1
- package/src/cli/cmdCopyFiles.mjs +8 -14
- package/src/cli/cmdExtractErrorCodes.mjs +41 -0
- package/src/cli/cmdJsonLint.mjs +16 -23
- package/src/cli/index.mjs +8 -5
- package/src/cli/typescript.mjs +13 -7
- package/src/eslint/material-ui/config.mjs +4 -1
- package/src/markdownlint/duplicate-h1.mjs +69 -0
- package/src/markdownlint/git-diff.mjs +31 -0
- package/src/markdownlint/index.mjs +62 -0
- package/src/markdownlint/straight-quotes.mjs +26 -0
- package/src/markdownlint/table-alignment.mjs +42 -0
- package/src/markdownlint/terminal-language.mjs +19 -0
- package/src/untyped-plugins.d.ts +7 -0
- package/src/utils/build.mjs +79 -0
- package/src/utils/extractErrorCodes.mjs +168 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.0.3-canary.
|
|
3
|
+
"version": "0.0.3-canary.10",
|
|
4
4
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,31 +14,33 @@
|
|
|
14
14
|
"./package.json": "./package.json",
|
|
15
15
|
"./prettier": "./src/prettier.mjs",
|
|
16
16
|
"./eslint": "./src/eslint/index.mjs",
|
|
17
|
-
"./babel-config": "./src/babel-config.mjs"
|
|
17
|
+
"./babel-config": "./src/babel-config.mjs",
|
|
18
|
+
"./markdownlint": "./src/markdownlint/index.mjs"
|
|
18
19
|
},
|
|
19
20
|
"bin": {
|
|
20
21
|
"code-infra": "./bin/code-infra.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@argos-ci/core": "^4.1.
|
|
24
|
+
"@argos-ci/core": "^4.1.4",
|
|
24
25
|
"@babel/cli": "^7.28.3",
|
|
25
|
-
"@babel/core": "^7.28.
|
|
26
|
+
"@babel/core": "^7.28.4",
|
|
27
|
+
"@babel/plugin-syntax-jsx": "^7.27.1",
|
|
26
28
|
"@babel/plugin-syntax-typescript": "^7.27.1",
|
|
27
29
|
"@babel/plugin-transform-runtime": "^7.28.3",
|
|
28
30
|
"@babel/preset-env": "^7.28.3",
|
|
29
31
|
"@babel/preset-react": "^7.27.1",
|
|
30
32
|
"@babel/preset-typescript": "^7.27.1",
|
|
31
33
|
"@eslint/compat": "^1.3.2",
|
|
32
|
-
"@eslint/js": "^9.
|
|
33
|
-
"@next/eslint-plugin-next": "^15.5.
|
|
34
|
+
"@eslint/js": "^9.35.0",
|
|
35
|
+
"@next/eslint-plugin-next": "^15.5.3",
|
|
34
36
|
"@octokit/auth-action": "^6.0.1",
|
|
35
37
|
"@octokit/rest": "^22.0.0",
|
|
36
|
-
"@pnpm/find-workspace-dir": "^1000.1.
|
|
38
|
+
"@pnpm/find-workspace-dir": "^1000.1.3",
|
|
37
39
|
"babel-plugin-optimize-clsx": "^2.6.2",
|
|
38
40
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
|
39
41
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
|
40
42
|
"babel-plugin-transform-remove-imports": "^1.8.0",
|
|
41
|
-
"chalk": "^5.6.
|
|
43
|
+
"chalk": "^5.6.2",
|
|
42
44
|
"eslint-config-prettier": "^10.1.8",
|
|
43
45
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
44
46
|
"eslint-module-utils": "^2.12.1",
|
|
@@ -48,19 +50,19 @@
|
|
|
48
50
|
"eslint-plugin-react": "^7.37.5",
|
|
49
51
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
|
50
52
|
"eslint-plugin-react-hooks": "^6.0.0-rc1",
|
|
51
|
-
"eslint-plugin-testing-library": "^7.
|
|
53
|
+
"eslint-plugin-testing-library": "^7.7.0",
|
|
52
54
|
"execa": "^9.6.0",
|
|
53
55
|
"git-url-parse": "^16.1.0",
|
|
54
|
-
"globals": "^16.
|
|
56
|
+
"globals": "^16.4.0",
|
|
55
57
|
"globby": "^14.1.0",
|
|
56
58
|
"lodash-es": "^4.17.21",
|
|
57
59
|
"minimatch": "^10.0.3",
|
|
58
60
|
"semver": "^7.7.2",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
61
|
+
"typescript-eslint": "^8.43.0",
|
|
60
62
|
"yargs": "^18.0.0",
|
|
61
|
-
"@mui/internal-babel-plugin-
|
|
62
|
-
"@mui/internal-babel-plugin-
|
|
63
|
-
"@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.
|
|
63
|
+
"@mui/internal-babel-plugin-display-name": "1.0.4-canary.7",
|
|
64
|
+
"@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.10",
|
|
65
|
+
"@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.21"
|
|
64
66
|
},
|
|
65
67
|
"peerDependencies": {
|
|
66
68
|
"eslint": "^9.0.0",
|
|
@@ -75,11 +77,11 @@
|
|
|
75
77
|
"@types/estree-jsx": "^1.0.5",
|
|
76
78
|
"@types/lodash-es": "^4.17.12",
|
|
77
79
|
"@types/yargs": "^17.0.33",
|
|
78
|
-
"@typescript-eslint/parser": "^8.
|
|
79
|
-
"@typescript-eslint/rule-tester": "^8.
|
|
80
|
-
"eslint": "^9.
|
|
80
|
+
"@typescript-eslint/parser": "^8.43.0",
|
|
81
|
+
"@typescript-eslint/rule-tester": "^8.43.0",
|
|
82
|
+
"eslint": "^9.35.0",
|
|
81
83
|
"prettier": "^3.6.2",
|
|
82
|
-
"typescript-eslint": "^8.
|
|
84
|
+
"typescript-eslint": "^8.43.0"
|
|
83
85
|
},
|
|
84
86
|
"files": [
|
|
85
87
|
"bin",
|
|
@@ -91,7 +93,7 @@
|
|
|
91
93
|
"publishConfig": {
|
|
92
94
|
"access": "public"
|
|
93
95
|
},
|
|
94
|
-
"gitSha": "
|
|
96
|
+
"gitSha": "b5dcc0b6d364d0474376eda1be73c681ffaaa6f8",
|
|
95
97
|
"scripts": {
|
|
96
98
|
"typescript": "tsc -p tsconfig.json",
|
|
97
99
|
"test": "pnpm -w test --project @mui/internal-code-infra",
|
package/src/babel-config.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import pluginRemovePropTypes from 'babel-plugin-transform-react-remove-prop-type
|
|
|
13
13
|
* @param {boolean} [param0.debug]
|
|
14
14
|
* @param {boolean} [param0.optimizeClsx]
|
|
15
15
|
* @param {boolean} [param0.removePropTypes]
|
|
16
|
-
* @param {boolean} [param0.
|
|
16
|
+
* @param {boolean} [param0.noResolveImports]
|
|
17
17
|
* @param {'cjs' | 'esm'} param0.bundle
|
|
18
18
|
* @param {string | null} param0.outExtension - Specify the output file extension.
|
|
19
19
|
* @param {string} param0.runtimeVersion
|
|
@@ -23,7 +23,7 @@ export function getBaseConfig({
|
|
|
23
23
|
debug = false,
|
|
24
24
|
optimizeClsx = false,
|
|
25
25
|
removePropTypes = false,
|
|
26
|
-
|
|
26
|
+
noResolveImports = false,
|
|
27
27
|
bundle,
|
|
28
28
|
runtimeVersion,
|
|
29
29
|
outExtension,
|
|
@@ -81,7 +81,7 @@ export function getBaseConfig({
|
|
|
81
81
|
plugins.push([pluginOptimizeClsx, {}, 'babel-plugin-optimize-clsx']);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
if (bundle === 'esm' && !
|
|
84
|
+
if (bundle === 'esm' && !noResolveImports) {
|
|
85
85
|
plugins.push([
|
|
86
86
|
pluginResolveImports,
|
|
87
87
|
{ outExtension },
|
|
@@ -119,20 +119,39 @@ export function getBaseConfig({
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* @
|
|
122
|
+
* @typedef {Object} Options
|
|
123
|
+
* @prop {'esm' | 'cjs'} [Options.bundle]
|
|
124
|
+
* @prop {boolean} [Options.noResolveImports]
|
|
125
|
+
* @prop {undefined} [options.env]
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {import('@babel/core').ConfigAPI | Options} api
|
|
130
|
+
* @returns {import('@babel/core').TransformOptions}
|
|
123
131
|
*/
|
|
124
132
|
export default function getBabelConfig(api) {
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
/** @type {'esm' | 'cjs'} */
|
|
134
|
+
let bundle;
|
|
135
|
+
/** @type {boolean} */
|
|
136
|
+
let noResolveImports;
|
|
137
|
+
|
|
138
|
+
if (api.env) {
|
|
139
|
+
// legacy
|
|
140
|
+
bundle = api.env(['regressions', 'stable']) ? 'esm' : 'cjs';
|
|
141
|
+
noResolveImports = api.env('test') || process.env.NODE_ENV === 'test';
|
|
142
|
+
} else {
|
|
143
|
+
bundle = api.bundle || 'esm';
|
|
144
|
+
noResolveImports = api.noResolveImports || false;
|
|
145
|
+
}
|
|
127
146
|
|
|
128
147
|
return getBaseConfig({
|
|
129
148
|
debug: process.env.MUI_BUILD_VERBOSE === 'true',
|
|
130
|
-
bundle
|
|
149
|
+
bundle,
|
|
131
150
|
outExtension: process.env.MUI_OUT_FILE_EXTENSION || null,
|
|
132
151
|
// any package needs to declare 7.25.0 as a runtime dependency. default is ^7.0.0
|
|
133
152
|
runtimeVersion: process.env.MUI_BABEL_RUNTIME_VERSION || '^7.25.0',
|
|
134
153
|
optimizeClsx: process.env.MUI_OPTIMIZE_CLSX === 'true',
|
|
135
154
|
removePropTypes: process.env.MUI_REMOVE_PROP_TYPES === 'true',
|
|
136
|
-
|
|
155
|
+
noResolveImports,
|
|
137
156
|
});
|
|
138
157
|
}
|
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.
|
package/src/cli/cmdBuild.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
+
import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
|
|
2
3
|
import { $ } from 'execa';
|
|
4
|
+
import { globby } from 'globby';
|
|
3
5
|
import set from 'lodash-es/set.js';
|
|
4
6
|
import * as fs from 'node:fs/promises';
|
|
5
7
|
import * as path from 'node:path';
|
|
6
|
-
import {
|
|
8
|
+
import { sep as posixSep } from 'node:path/posix';
|
|
9
|
+
|
|
10
|
+
import { getOutExtension, isMjsBuild, mapConcurrently, validatePkgJson } from '../utils/build.mjs';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* @typedef {Object} Args
|
|
@@ -18,6 +22,7 @@ import { getOutExtension, isMjsBuild, validatePkgJson } from '../utils/build.mjs
|
|
|
18
22
|
* @property {boolean} skipPackageJson - Whether to skip generating the package.json file in the bundle output.
|
|
19
23
|
* @property {boolean} skipMainCheck - Whether to skip checking for main field in package.json.
|
|
20
24
|
* @property {string[]} ignore - Globs to be ignored by Babel.
|
|
25
|
+
* @property {string[]} [copy] - Files/Directories to be copied. Can be a glob pattern.
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
28
|
const validBundles = [
|
|
@@ -298,6 +303,13 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
298
303
|
type: 'boolean',
|
|
299
304
|
default: false,
|
|
300
305
|
description: 'Skip checking for main field in package.json.',
|
|
306
|
+
})
|
|
307
|
+
.option('copy', {
|
|
308
|
+
type: 'string',
|
|
309
|
+
array: true,
|
|
310
|
+
description:
|
|
311
|
+
'Files/Directories to be copied to the output directory. Can be a glob pattern.',
|
|
312
|
+
default: [],
|
|
301
313
|
});
|
|
302
314
|
},
|
|
303
315
|
async handler(args) {
|
|
@@ -442,5 +454,144 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
442
454
|
outputDir: buildDir,
|
|
443
455
|
addTypes: buildTypes,
|
|
444
456
|
});
|
|
457
|
+
|
|
458
|
+
await copyHandler({
|
|
459
|
+
cwd,
|
|
460
|
+
globs: args.copy ?? [],
|
|
461
|
+
buildDir,
|
|
462
|
+
verbose: args.verbose,
|
|
463
|
+
});
|
|
445
464
|
},
|
|
446
465
|
});
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {Object} param0
|
|
469
|
+
* @param {string} param0.cwd - The current working directory.
|
|
470
|
+
* @param {string[]} [param0.globs=[]] - Extra files to copy, can be specified as `source:target` pairs or just `source`.
|
|
471
|
+
* @param {string} param0.buildDir - The build directory to copy to.
|
|
472
|
+
* @param {boolean} [param0.verbose=false] - Whether to suppress output.
|
|
473
|
+
* @returns {Promise<void>}
|
|
474
|
+
*/
|
|
475
|
+
async function copyHandler({ cwd, globs = [], buildDir, verbose = false }) {
|
|
476
|
+
/**
|
|
477
|
+
* @type {(string|{targetPath: string; sourcePath: string})[]}
|
|
478
|
+
*/
|
|
479
|
+
const defaultFiles = [];
|
|
480
|
+
const workspaceDir = await findWorkspaceDir(cwd);
|
|
481
|
+
if (!workspaceDir) {
|
|
482
|
+
throw new Error('Workspace directory not found');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const localOrRootFiles = [
|
|
486
|
+
[path.join(cwd, 'README.md'), path.join(workspaceDir, 'README.md')],
|
|
487
|
+
[path.join(cwd, 'LICENSE'), path.join(workspaceDir, 'LICENSE')],
|
|
488
|
+
[path.join(cwd, 'CHANGELOG.md'), path.join(workspaceDir, 'CHANGELOG.md')],
|
|
489
|
+
];
|
|
490
|
+
await Promise.all(
|
|
491
|
+
localOrRootFiles.map(async (filesToCopy) => {
|
|
492
|
+
for (const file of filesToCopy) {
|
|
493
|
+
if (
|
|
494
|
+
// eslint-disable-next-line no-await-in-loop
|
|
495
|
+
await fs.stat(file).then(
|
|
496
|
+
() => true,
|
|
497
|
+
() => false,
|
|
498
|
+
)
|
|
499
|
+
) {
|
|
500
|
+
defaultFiles.push(file);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
if (globs.length) {
|
|
508
|
+
const res = globs.map((globPattern) => {
|
|
509
|
+
const [pattern, baseDir] = globPattern.split(':');
|
|
510
|
+
return { pattern, baseDir };
|
|
511
|
+
});
|
|
512
|
+
/**
|
|
513
|
+
* Avoids redundant globby calls for the same pattern.
|
|
514
|
+
*
|
|
515
|
+
* @type {Map<string, Promise<string[]>>}
|
|
516
|
+
*/
|
|
517
|
+
const globToResMap = new Map();
|
|
518
|
+
|
|
519
|
+
const result = await Promise.all(
|
|
520
|
+
res.map(async ({ pattern, baseDir }) => {
|
|
521
|
+
if (!globToResMap.has(pattern)) {
|
|
522
|
+
const promise = globby(pattern, { cwd });
|
|
523
|
+
globToResMap.set(pattern, promise);
|
|
524
|
+
}
|
|
525
|
+
const files = await globToResMap.get(pattern);
|
|
526
|
+
return { files: files ?? [], baseDir };
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
globToResMap.clear();
|
|
530
|
+
|
|
531
|
+
result.forEach(({ files, baseDir }) => {
|
|
532
|
+
files.forEach((file) => {
|
|
533
|
+
const sourcePath = path.resolve(cwd, file);
|
|
534
|
+
// Use posix separator for the relative paths. So devs can only specify globs with `/` even on Windows.
|
|
535
|
+
const pathSegments = file.split(posixSep);
|
|
536
|
+
const relativePath =
|
|
537
|
+
// Use index 2 (when required) since users can also specify paths like `./src/index.js`
|
|
538
|
+
pathSegments.slice(pathSegments[0] === '.' ? 2 : 1).join(posixSep) || file;
|
|
539
|
+
const targetPath = baseDir
|
|
540
|
+
? path.resolve(buildDir, baseDir, relativePath)
|
|
541
|
+
: path.resolve(buildDir, relativePath);
|
|
542
|
+
defaultFiles.push({ sourcePath, targetPath });
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!defaultFiles.length) {
|
|
548
|
+
if (verbose) {
|
|
549
|
+
console.log('⓿ No files to copy.');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
await mapConcurrently(
|
|
553
|
+
defaultFiles,
|
|
554
|
+
async (file) => {
|
|
555
|
+
if (typeof file === 'string') {
|
|
556
|
+
const sourcePath = file;
|
|
557
|
+
const fileName = path.basename(file);
|
|
558
|
+
const targetPath = path.join(buildDir, fileName);
|
|
559
|
+
await recursiveCopy({ source: sourcePath, target: targetPath, verbose });
|
|
560
|
+
} else {
|
|
561
|
+
await fs.mkdir(path.dirname(file.targetPath), { recursive: true });
|
|
562
|
+
await recursiveCopy({ source: file.sourcePath, target: file.targetPath, verbose });
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
20,
|
|
566
|
+
);
|
|
567
|
+
console.log(`📋 Copied ${defaultFiles.length} files.`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Recursively copies files and directories from a source path to a target path.
|
|
572
|
+
*
|
|
573
|
+
* @async
|
|
574
|
+
* @param {Object} options - The options for copying files.
|
|
575
|
+
* @param {string} options.source - The source path to copy from.
|
|
576
|
+
* @param {string} options.target - The target path to copy to.
|
|
577
|
+
* @param {boolean} [options.verbose=true] - If true, suppresses console output.
|
|
578
|
+
* @returns {Promise<boolean>} Resolves when the copy operation is complete.
|
|
579
|
+
* @throws {Error} Throws if an error occurs other than the source not existing.
|
|
580
|
+
*/
|
|
581
|
+
async function recursiveCopy({ source, target, verbose = true }) {
|
|
582
|
+
try {
|
|
583
|
+
await fs.cp(source, target, { recursive: true });
|
|
584
|
+
if (verbose) {
|
|
585
|
+
console.log(`Copied ${source} to ${target}`);
|
|
586
|
+
}
|
|
587
|
+
return true;
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (/** @type {{ code: string }} */ (err).code !== 'ENOENT') {
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
592
|
+
if (verbose) {
|
|
593
|
+
console.warn(`Source does not exist: ${source}`);
|
|
594
|
+
}
|
|
595
|
+
throw err;
|
|
596
|
+
}
|
|
597
|
+
}
|
package/src/cli/cmdCopyFiles.mjs
CHANGED
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
});
|
package/src/cli/cmdJsonLint.mjs
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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()
|
package/src/cli/typescript.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { globby } from 'globby';
|
|
|
8
8
|
import * as fs from 'node:fs/promises';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
|
+
import { mapConcurrently } from '../utils/build.mjs';
|
|
11
12
|
|
|
12
13
|
const $$ = $({ stdio: 'inherit' });
|
|
13
14
|
|
|
@@ -27,7 +28,8 @@ export async function emitDeclarations(tsconfig, outDir) {
|
|
|
27
28
|
--emitDeclarationOnly
|
|
28
29
|
--noEmit false
|
|
29
30
|
--composite false
|
|
30
|
-
--incremental false
|
|
31
|
+
--incremental false
|
|
32
|
+
--declarationMap false`;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -80,8 +82,9 @@ async function postProcessDeclarations({ directory }) {
|
|
|
80
82
|
[pluginRemoveImports, { test: /\.css$/ }],
|
|
81
83
|
];
|
|
82
84
|
|
|
83
|
-
await
|
|
84
|
-
dtsFiles
|
|
85
|
+
await mapConcurrently(
|
|
86
|
+
dtsFiles,
|
|
87
|
+
async (dtsFile) => {
|
|
85
88
|
const result = await babel.transformFileAsync(dtsFile, {
|
|
86
89
|
configFile: false,
|
|
87
90
|
plugins: babelPlugins,
|
|
@@ -92,7 +95,8 @@ async function postProcessDeclarations({ directory }) {
|
|
|
92
95
|
} else {
|
|
93
96
|
console.error('failed to transform', dtsFile);
|
|
94
97
|
}
|
|
95
|
-
}
|
|
98
|
+
},
|
|
99
|
+
20,
|
|
96
100
|
);
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -107,11 +111,13 @@ async function renameDeclarations({ directory }) {
|
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
113
|
console.log(`Renaming d.ts files to d.mts in ${directory}`);
|
|
110
|
-
await
|
|
111
|
-
dtsFiles
|
|
114
|
+
await mapConcurrently(
|
|
115
|
+
dtsFiles,
|
|
116
|
+
async (dtsFile) => {
|
|
112
117
|
const newFileName = dtsFile.replace(/\.d\.ts$/, '.d.mts');
|
|
113
118
|
await fs.rename(dtsFile, newFileName);
|
|
114
|
-
}
|
|
119
|
+
},
|
|
120
|
+
20,
|
|
115
121
|
);
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -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': [
|
|
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': [
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {[string, string]} Attr
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} Token
|
|
7
|
+
* @property {string} type
|
|
8
|
+
* @property {string} info
|
|
9
|
+
* @property {string} tag
|
|
10
|
+
* @property {string} content
|
|
11
|
+
* @property {number} lineNumber
|
|
12
|
+
* @property {Attr[]} attrs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} OnErrorObj
|
|
17
|
+
* @property {number} lineNumber
|
|
18
|
+
* @property {string} [detail]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {(err: OnErrorObj) => void} OnError
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} MdParams
|
|
27
|
+
* @property {string} name
|
|
28
|
+
* @property {string[]} lines
|
|
29
|
+
* @property {Token[]} tokens
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// This rule is an extension of MD025/no-multiple-top-level-headings.
|
|
33
|
+
// The rule is buggy https://github.com/DavidAnson/markdownlint/pull/1109
|
|
34
|
+
// but also blog headers don't tell you that h1 is already injected.
|
|
35
|
+
export default {
|
|
36
|
+
names: ['duplicateH1'],
|
|
37
|
+
description: 'Multiple top-level headings in the same document.',
|
|
38
|
+
tags: ['headings'],
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
41
|
+
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
42
|
+
*/
|
|
43
|
+
function: (params, onError) => {
|
|
44
|
+
/**
|
|
45
|
+
* @type {number|boolean}
|
|
46
|
+
*/
|
|
47
|
+
let hasTopLevelHeading = false;
|
|
48
|
+
params.tokens.forEach((token) => {
|
|
49
|
+
if (token.type === 'heading_open' && token.tag === 'h1') {
|
|
50
|
+
// Avoid duplicate errors with MD025.
|
|
51
|
+
if (hasTopLevelHeading !== false && hasTopLevelHeading !== 1) {
|
|
52
|
+
onError({
|
|
53
|
+
lineNumber: token.lineNumber,
|
|
54
|
+
});
|
|
55
|
+
} else if (params.name.includes('/docs/pages/blog/')) {
|
|
56
|
+
onError({
|
|
57
|
+
lineNumber: token.lineNumber,
|
|
58
|
+
detail: 'In the blog, the h1 is already added using the markdown header.title value.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Store the first h1 of the page.
|
|
63
|
+
if (hasTopLevelHeading === false) {
|
|
64
|
+
hasTopLevelHeading = token.lineNumber;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
names: ['gitDiff'],
|
|
3
|
+
description: 'Respect the format output of git diff.',
|
|
4
|
+
tags: ['spaces'],
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
+
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
+
*/
|
|
9
|
+
function: (params, onError) => {
|
|
10
|
+
params.tokens.forEach((token) => {
|
|
11
|
+
if (token.type === 'fence' && token.info === 'diff') {
|
|
12
|
+
token.content.split('\n').forEach((line, index) => {
|
|
13
|
+
if (
|
|
14
|
+
line[0] !== ' ' &&
|
|
15
|
+
line[0] !== '-' &&
|
|
16
|
+
line[0] !== '+' &&
|
|
17
|
+
line !== '' &&
|
|
18
|
+
line.indexOf('@@ ') !== 0 &&
|
|
19
|
+
line.indexOf('diff --git ') !== 0 &&
|
|
20
|
+
line.indexOf('index ') !== 0
|
|
21
|
+
) {
|
|
22
|
+
onError({
|
|
23
|
+
lineNumber: token.lineNumber + index + 1,
|
|
24
|
+
detail: `The line start with "+" or "-" or " ": ${line}`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import straightQuotes from './straight-quotes.mjs';
|
|
2
|
+
import gitDiff from './git-diff.mjs';
|
|
3
|
+
import tableAlignment from './table-alignment.mjs';
|
|
4
|
+
import terminalLanguage from './terminal-language.mjs';
|
|
5
|
+
import duplicateH1 from './duplicate-h1.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a base configuration for markdownlint.
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {(typeof straightQuotes)[]} [options.rules] - An array of custom rules to include.
|
|
11
|
+
* @param {string[]} [options.ignores] - An array of glob patterns to ignore.
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export function createBaseConfig(options = {}) {
|
|
15
|
+
const { rules = [], ignores = [] } = options;
|
|
16
|
+
// https://github.com/DavidAnson/markdownlint#rules--aliases
|
|
17
|
+
return {
|
|
18
|
+
config: {
|
|
19
|
+
default: true,
|
|
20
|
+
MD004: false, // MD004/ul-style. Buggy
|
|
21
|
+
MD009: {
|
|
22
|
+
// MD009/no-trailing-spaces
|
|
23
|
+
br_spaces: 0,
|
|
24
|
+
strict: true,
|
|
25
|
+
list_item_empty_lines: false,
|
|
26
|
+
},
|
|
27
|
+
MD013: false, // MD013/line-length. Already handled by Prettier.
|
|
28
|
+
MD014: false, // MD014/commands-show-output. It's OK.
|
|
29
|
+
MD024: { siblings_only: true }, // MD024/no-duplicate-heading/no-duplicate-header
|
|
30
|
+
MD025: {
|
|
31
|
+
// Heading level
|
|
32
|
+
level: 1,
|
|
33
|
+
// RegExp for matching title in front matter
|
|
34
|
+
front_matter_title: '',
|
|
35
|
+
},
|
|
36
|
+
MD033: false, // MD033/no-inline-html. We use it from time to time, it's fine.
|
|
37
|
+
MD034: false, // MD034/no-bare-urls. Not a concern for us, our Markdown interpreter supports it.
|
|
38
|
+
MD028: false, // MD028/no-blanks-blockquote prevent double blockquote
|
|
39
|
+
MD029: false, // MD029/ol-prefix. Buggy
|
|
40
|
+
MD031: false, // MD031/blanks-around-fences Some code blocks inside li
|
|
41
|
+
MD036: false, // MD036/no-emphasis-as-heading/no-emphasis-as-header. It's OK.
|
|
42
|
+
MD051: false, // MD051/link-fragments. Many false positives in the changelog.
|
|
43
|
+
MD052: false, // MD052/reference-links-images. Many false positives in the changelog.
|
|
44
|
+
MD059: false, // MD059/descriptive-link-text. Does not allow links on text like "link", whereas we redirect to "Link" component.
|
|
45
|
+
straightQuotes: true,
|
|
46
|
+
gitDiff: true,
|
|
47
|
+
tableAlignment: true,
|
|
48
|
+
terminalLanguage: true,
|
|
49
|
+
duplicateH1: true,
|
|
50
|
+
},
|
|
51
|
+
customRules: [straightQuotes, gitDiff, tableAlignment, terminalLanguage, duplicateH1, ...rules],
|
|
52
|
+
ignores: [
|
|
53
|
+
'CHANGELOG.old.md',
|
|
54
|
+
'**/node_modules/**',
|
|
55
|
+
'**/build/**',
|
|
56
|
+
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
57
|
+
'docs/public/**',
|
|
58
|
+
'docs/export/**',
|
|
59
|
+
...ignores,
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const nonStraightQuotes = /[‘’“”]/;
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
names: ['straightQuotes'],
|
|
5
|
+
description: 'Only allow straight quotes.',
|
|
6
|
+
tags: ['spelling'],
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
9
|
+
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
10
|
+
*/
|
|
11
|
+
function: (params, onError) => {
|
|
12
|
+
params.lines.forEach((line, lineNumber) => {
|
|
13
|
+
// It will match
|
|
14
|
+
// opening single quote: \xE2\x80\x98
|
|
15
|
+
// closing single quote: \xE2\x80\x99
|
|
16
|
+
// opening double quote: \xE2\x80\x9C
|
|
17
|
+
// closing double quote: \xE2\x80\x9D
|
|
18
|
+
if (nonStraightQuotes.test(line)) {
|
|
19
|
+
onError({
|
|
20
|
+
lineNumber: lineNumber + 1,
|
|
21
|
+
detail: `For line: ${line}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./duplicate-h1.mjs').Attr[]} attrs
|
|
3
|
+
* @returns {Record<string, string>}
|
|
4
|
+
*/
|
|
5
|
+
function attr(attrs) {
|
|
6
|
+
return (attrs || []).reduce((acc, item) => ({ ...acc, [item[0]]: item[1] }), {});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
names: ['tableAlignment'],
|
|
11
|
+
description: 'Set table alignment.',
|
|
12
|
+
tags: ['table'],
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
15
|
+
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
16
|
+
*/
|
|
17
|
+
function: (params, onError) => {
|
|
18
|
+
params.tokens.forEach((token) => {
|
|
19
|
+
// This is wrong:
|
|
20
|
+
// | Version | Supported |
|
|
21
|
+
// | ------- | ------------------ |
|
|
22
|
+
//
|
|
23
|
+
// The second column should be left aligned because it contains text:
|
|
24
|
+
// | Version | Supported |
|
|
25
|
+
// | ------- | :----------------- |
|
|
26
|
+
//
|
|
27
|
+
// However, columns that includes numbers should be right aligned:
|
|
28
|
+
// | Version | Supported |
|
|
29
|
+
// | ------: | :----------------- |
|
|
30
|
+
//
|
|
31
|
+
// More details: https://ux.stackexchange.com/questions/24066/what-is-the-best-practice-for-data-table-cell-content-alignment
|
|
32
|
+
//
|
|
33
|
+
// In this check we expect the style to be 'text-align:right' or equivalent.
|
|
34
|
+
if (token.type === 'th_open' && attr(token.attrs).style == null) {
|
|
35
|
+
onError({
|
|
36
|
+
lineNumber: token.lineNumber,
|
|
37
|
+
detail: `${params.lines[token.lineNumber - 1]}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
names: ['terminalLanguage'],
|
|
3
|
+
description: 'Set the right language for terminal code.',
|
|
4
|
+
tags: ['code'],
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
+
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
+
*/
|
|
9
|
+
function: (params, onError) => {
|
|
10
|
+
params.tokens.forEach((token) => {
|
|
11
|
+
if (token.type === 'fence' && token.info === 'sh') {
|
|
12
|
+
onError({
|
|
13
|
+
lineNumber: token.lineNumber,
|
|
14
|
+
detail: `Use "bash" instead of "sh".`,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
};
|
package/src/untyped-plugins.d.ts
CHANGED
|
@@ -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
|
|
package/src/utils/build.mjs
CHANGED
|
@@ -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
|
+
}
|