@mui/internal-code-infra 0.0.2-canary.9 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ /* eslint-disable no-console */
2
+ import { $ } from 'execa';
3
+ import set from 'lodash-es/set.js';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import { getOutExtension, isMjsBuild } from '../utils/build.mjs';
7
+
8
+ /**
9
+ * @typedef {Object} Args
10
+ * @property {import('../utils/build.mjs').BundleType[]} bundle - The bundles to build.
11
+ * @property {boolean} hasLargeFiles - The large files to build.
12
+ * @property {boolean} skipBundlePackageJson - Whether to skip generating a package.json file in the /esm folder.
13
+ * @property {string} cjsOutDir - The directory to copy the cjs files to.
14
+ * @property {boolean} verbose - Whether to enable verbose logging.
15
+ * @property {boolean} buildTypes - Whether to build types for the package.
16
+ * @property {boolean} skipTsc - Whether to build types for the package.
17
+ * @property {boolean} skipBabelRuntimeCheck - Whether to skip checking for Babel runtime dependencies in the package.
18
+ * @property {boolean} skipPackageJson - Whether to skip generating the package.json file in the bundle output.
19
+ * @property {string[]} ignore - Globs to be ignored by Babel.
20
+ */
21
+
22
+ const validBundles = [
23
+ // build for node using commonJS modules
24
+ 'cjs',
25
+ // build with a hardcoded target using ES6 modules
26
+ 'esm',
27
+ ];
28
+
29
+ /**
30
+ * @param {Object} options
31
+ * @param {string} options.name - The name of the package.
32
+ * @param {string} options.version - The version of the package.
33
+ * @param {string} options.license - The license of the package.
34
+ * @param {import('../utils/build.mjs').BundleType} options.bundle
35
+ * @param {string} options.outputDir
36
+ */
37
+ async function addLicense({ name, version, license, bundle, outputDir }) {
38
+ const outExtension = getOutExtension(bundle);
39
+ const file = path.join(outputDir, `index${outExtension}`);
40
+ if (
41
+ !(await fs.stat(file).then(
42
+ (stats) => stats.isFile(),
43
+ () => false,
44
+ ))
45
+ ) {
46
+ return;
47
+ }
48
+ const content = await fs.readFile(file, { encoding: 'utf8' });
49
+ await fs.writeFile(
50
+ file,
51
+ `/**
52
+ * ${name} v${version}
53
+ *
54
+ * @license ${license}
55
+ * This source code is licensed under the ${license} license found in the
56
+ * LICENSE file in the root directory of this source tree.
57
+ */
58
+ ${content}`,
59
+ { encoding: 'utf8' },
60
+ );
61
+ console.log(`License added to ${file}`);
62
+ }
63
+
64
+ /**
65
+ * @param {Object} param0
66
+ * @param {string | Record<string, string>} param0.importPath
67
+ * @param {string} param0.key
68
+ * @param {string} param0.cwd
69
+ * @param {string} param0.dir
70
+ * @param {string} param0.type
71
+ * @param {Object} param0.newExports
72
+ * @param {string} param0.typeOutExtension
73
+ * @param {string} param0.outExtension
74
+ * @param {boolean} param0.addTypes
75
+ * @returns {Promise<{path: string[], importPath: string | Record<string, string | undefined>}>}
76
+ */
77
+ async function createExportsFor({
78
+ importPath,
79
+ key,
80
+ cwd,
81
+ dir,
82
+ type,
83
+ newExports,
84
+ typeOutExtension,
85
+ outExtension,
86
+ addTypes,
87
+ }) {
88
+ let srcPath = typeof importPath === 'string' ? importPath : importPath['mui-src'];
89
+ const rest = typeof importPath === 'string' ? {} : { ...importPath };
90
+ delete rest['mui-src'];
91
+
92
+ const exportFileExists = srcPath.includes('*')
93
+ ? true
94
+ : await fs.stat(path.join(cwd, srcPath)).then(
95
+ (stats) => stats.isFile() || stats.isDirectory(),
96
+ () => false,
97
+ );
98
+ if (!exportFileExists) {
99
+ throw new Error(
100
+ `The import path "${srcPath}" for export "${key}" does not exist in the package. Either remove the export or add the file/folder to the package.`,
101
+ );
102
+ }
103
+ srcPath = srcPath.replace(/\.\/src\//, `./${dir === '.' ? '' : `${dir}/`}`);
104
+ const ext = path.extname(srcPath);
105
+
106
+ if (ext === '.css') {
107
+ set(newExports, [key], srcPath);
108
+ return {
109
+ path: [key],
110
+ importPath: srcPath,
111
+ };
112
+ }
113
+ return {
114
+ path: [key, type === 'cjs' ? 'require' : 'import'],
115
+ importPath: {
116
+ ...rest,
117
+ types: addTypes ? srcPath.replace(ext, typeOutExtension) : undefined,
118
+ default: srcPath.replace(ext, outExtension),
119
+ },
120
+ };
121
+ }
122
+
123
+ /**
124
+ * @param {Object} param0
125
+ * @param {any} param0.packageJson - The package.json content.
126
+ * @param {{type: import('../utils/build.mjs').BundleType; dir: string}[]} param0.bundles
127
+ * @param {string} param0.outputDir
128
+ * @param {string} param0.cwd
129
+ * @param {boolean} param0.addTypes - Whether to add type declarations for the package.
130
+ */
131
+ async function writePackageJson({ packageJson, bundles, outputDir, cwd, addTypes = false }) {
132
+ delete packageJson.scripts;
133
+ delete packageJson.publishConfig?.directory;
134
+ delete packageJson.devDependencies;
135
+ delete packageJson.imports;
136
+
137
+ packageJson.type = packageJson.type || 'commonjs';
138
+
139
+ /**
140
+ * @type {Record<string, string | Record<string, string> | null>}
141
+ */
142
+ const originalExports = packageJson.exports || {};
143
+ delete packageJson.exports;
144
+ /**
145
+ * @type {Record<string, string | Record<string, string> | null>}
146
+ */
147
+ const newExports = {
148
+ './package.json': './package.json',
149
+ };
150
+
151
+ await Promise.all(
152
+ bundles.map(async ({ type, dir }) => {
153
+ const outExtension = getOutExtension(type);
154
+ const typeOutExtension = getOutExtension(type, true);
155
+ const indexFileExists = await fs.stat(path.join(outputDir, dir, `index${outExtension}`)).then(
156
+ (stats) => stats.isFile(),
157
+ () => false,
158
+ );
159
+ const typeFileExists =
160
+ addTypes &&
161
+ (await fs.stat(path.join(outputDir, dir, `index${typeOutExtension}`)).then(
162
+ (stats) => stats.isFile(),
163
+ () => false,
164
+ ));
165
+ const dirPrefix = dir === '.' ? '' : `${dir}/`;
166
+ const exportDir = `./${dirPrefix}index${outExtension}`;
167
+ const typeExportDir = `./${dirPrefix}index${typeOutExtension}`;
168
+
169
+ if (indexFileExists) {
170
+ // skip `packageJson.module` to support parcel and some older bundlers
171
+ if (type === 'cjs') {
172
+ packageJson.main = exportDir;
173
+ }
174
+ set(newExports, ['.', type === 'cjs' ? 'require' : 'import'], {
175
+ types: typeFileExists ? typeExportDir : undefined,
176
+ default: exportDir,
177
+ });
178
+ }
179
+ if (typeFileExists && type === 'cjs') {
180
+ packageJson.types = typeExportDir;
181
+ }
182
+ const exportKeys = Object.keys(originalExports);
183
+ // need to maintain the order of exports
184
+ for (const key of exportKeys) {
185
+ const importPath = originalExports[key];
186
+ if (!importPath) {
187
+ set(newExports, [key], null);
188
+ return;
189
+ }
190
+ // eslint-disable-next-line no-await-in-loop
191
+ const res = await createExportsFor({
192
+ importPath,
193
+ key,
194
+ cwd,
195
+ dir,
196
+ type,
197
+ newExports,
198
+ typeOutExtension,
199
+ outExtension,
200
+ addTypes,
201
+ });
202
+ set(newExports, res.path, res.importPath);
203
+ }
204
+ }),
205
+ );
206
+ bundles.forEach(({ dir }) => {
207
+ if (dir !== '.') {
208
+ newExports[`./${dir}`] = null;
209
+ }
210
+ });
211
+
212
+ packageJson.exports = newExports;
213
+
214
+ await fs.writeFile(
215
+ path.join(outputDir, 'package.json'),
216
+ JSON.stringify(packageJson, null, 2),
217
+ 'utf-8',
218
+ );
219
+ }
220
+
221
+ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
222
+ command: 'build',
223
+ describe: 'Builds the package for publishing.',
224
+ builder(yargs) {
225
+ return yargs
226
+ .option('bundle', {
227
+ array: true,
228
+ demandOption: true,
229
+ type: 'string',
230
+ choices: validBundles,
231
+ description: 'Bundles to output',
232
+ default: ['esm', 'cjs'],
233
+ })
234
+ .option('hasLargeFiles', {
235
+ type: 'boolean',
236
+ default: false,
237
+ describe: 'Set to `true` if you know you are transpiling large files.',
238
+ })
239
+ .option('skipBundlePackageJson', {
240
+ type: 'boolean',
241
+ default: false,
242
+ describe:
243
+ "Set to `true` if you don't want to generate a package.json file in the bundle output.",
244
+ })
245
+ .option('cjsOutDir', {
246
+ default: '.',
247
+ type: 'string',
248
+ description: 'The directory to output the cjs files to.',
249
+ })
250
+ .option('verbose', {
251
+ type: 'boolean',
252
+ default: false,
253
+ description: 'Enable verbose logging.',
254
+ })
255
+ .option('buildTypes', {
256
+ type: 'boolean',
257
+ default: true,
258
+ description: 'Whether to build types for the package.',
259
+ })
260
+ .option('skipTsc', {
261
+ type: 'boolean',
262
+ default: false,
263
+ description: 'Skip running TypeScript compiler (tsc) for building types.',
264
+ })
265
+ .option('ignore', {
266
+ type: 'string',
267
+ array: true,
268
+ description: 'Extra globs to be ignored by Babel.',
269
+ default: [],
270
+ })
271
+ .option('skipBabelRuntimeCheck', {
272
+ type: 'boolean',
273
+ default: false,
274
+ description: 'Skip checking for Babel runtime dependencies in the package.',
275
+ })
276
+ .option('skipPackageJson', {
277
+ type: 'boolean',
278
+ default: false,
279
+ description: 'Skip generating the package.json file in the bundle output.',
280
+ });
281
+ },
282
+ async handler(args) {
283
+ const {
284
+ bundle: bundles,
285
+ hasLargeFiles,
286
+ skipBundlePackageJson,
287
+ cjsOutDir = '.',
288
+ verbose = false,
289
+ ignore: extraIgnores,
290
+ buildTypes,
291
+ skipTsc,
292
+ skipBabelRuntimeCheck = false,
293
+ skipPackageJson = false,
294
+ } = args;
295
+
296
+ const cwd = process.cwd();
297
+ const pkgJsonPath = path.join(cwd, 'package.json');
298
+ const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, { encoding: 'utf8' }));
299
+ const buildDirBase = packageJson.publishConfig?.directory;
300
+ if (!buildDirBase) {
301
+ throw new Error(
302
+ `No build directory specified in "${packageJson.name}" package.json. Specify it in the "publishConfig.directory" field.`,
303
+ );
304
+ }
305
+ if (packageJson.private === false) {
306
+ throw new Error(
307
+ `Remove the field "private": false from "${packageJson.name}" package.json. This is redundant.`,
308
+ );
309
+ }
310
+ const buildDir = path.join(cwd, buildDirBase);
311
+
312
+ console.log(`Selected output directory: "${buildDirBase}"`);
313
+
314
+ await fs.rm(buildDir, { recursive: true, force: true });
315
+
316
+ let babelRuntimeVersion = packageJson.dependencies['@babel/runtime'];
317
+ if (babelRuntimeVersion === 'catalog:') {
318
+ // resolve the version from the given package
319
+ // outputs the pnpm-workspace.yaml config as json
320
+ const { stdout: configStdout } = await $`pnpm config list --json`;
321
+ const pnpmWorkspaceConfig = JSON.parse(configStdout);
322
+ babelRuntimeVersion = pnpmWorkspaceConfig.catalog['@babel/runtime'];
323
+ }
324
+
325
+ if (!babelRuntimeVersion && !skipBabelRuntimeCheck) {
326
+ throw new Error(
327
+ 'package.json needs to have a dependency on `@babel/runtime` when building with `@babel/plugin-transform-runtime`.',
328
+ );
329
+ }
330
+
331
+ if (!bundles.length) {
332
+ console.error('No bundles specified. Use --bundle to specify which bundles to build.');
333
+ return;
334
+ }
335
+
336
+ const babelMod = await import('./babel.mjs');
337
+ const relativeOutDirs = {
338
+ cjs: cjsOutDir,
339
+ esm: 'esm',
340
+ };
341
+ const sourceDir = path.join(cwd, 'src');
342
+
343
+ // js build start
344
+ await Promise.all(
345
+ bundles.map(async (bundle) => {
346
+ const outExtension = getOutExtension(bundle);
347
+ const relativeOutDir = relativeOutDirs[bundle];
348
+ const outputDir = path.join(buildDir, relativeOutDir);
349
+ await fs.mkdir(outputDir, { recursive: true });
350
+
351
+ const promises = [];
352
+
353
+ promises.push(
354
+ babelMod.babelBuild({
355
+ cwd,
356
+ sourceDir,
357
+ outDir: outputDir,
358
+ babelRuntimeVersion,
359
+ hasLargeFiles,
360
+ bundle,
361
+ verbose,
362
+ optimizeClsx:
363
+ packageJson.dependencies.clsx !== undefined ||
364
+ packageJson.dependencies.classnames !== undefined,
365
+ removePropTypes: packageJson.dependencies['prop-types'] !== undefined,
366
+ pkgVersion: packageJson.version,
367
+ ignores: extraIgnores,
368
+ outExtension,
369
+ }),
370
+ );
371
+
372
+ if (buildDir !== outputDir && !skipBundlePackageJson && !isMjsBuild) {
373
+ // @TODO - Not needed if the output extension is .mjs. Remove this before PR merge.
374
+ promises.push(
375
+ fs.writeFile(
376
+ path.join(outputDir, 'package.json'),
377
+ JSON.stringify({
378
+ type: bundle === 'esm' ? 'module' : 'commonjs',
379
+ sideEffects: packageJson.sideEffects ?? false,
380
+ }),
381
+ ),
382
+ );
383
+ }
384
+
385
+ await Promise.all(promises);
386
+ await addLicense({
387
+ bundle,
388
+ license: packageJson.license,
389
+ name: packageJson.name,
390
+ version: packageJson.version,
391
+ outputDir,
392
+ });
393
+ }),
394
+ );
395
+ // js build end
396
+
397
+ if (buildTypes) {
398
+ const tsMod = await import('./typescript.mjs');
399
+ /**
400
+ * @type {{type: import('../utils/build.mjs').BundleType, dir: string}[]};
401
+ */
402
+ const bundleMap = bundles.map((type) => ({
403
+ type,
404
+ dir: relativeOutDirs[type],
405
+ }));
406
+
407
+ await tsMod.createTypes({
408
+ bundles: bundleMap,
409
+ srcDir: sourceDir,
410
+ cwd,
411
+ skipTsc,
412
+ isMjsBuild,
413
+ buildDir,
414
+ });
415
+ }
416
+ if (skipPackageJson) {
417
+ console.log('Skipping package.json generation in the output directory.');
418
+ return;
419
+ }
420
+
421
+ const normalizedCjsOutDir = cjsOutDir === '.' || cjsOutDir === './' ? '.' : cjsOutDir;
422
+ await writePackageJson({
423
+ cwd,
424
+ packageJson,
425
+ bundles: bundles.map((type) => ({
426
+ type,
427
+ dir: type === 'esm' ? 'esm' : normalizedCjsOutDir || '.',
428
+ })),
429
+ outputDir: buildDir,
430
+ addTypes: buildTypes,
431
+ });
432
+ },
433
+ });
@@ -0,0 +1,214 @@
1
+ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
2
+ import { globby } from 'globby';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ /**
7
+ * @typedef {Object} Args
8
+ * @property {boolean} [silent] Run in silent mode without logging
9
+ * @property {boolean} [excludeDefaults] Exclude default files from the copy operation
10
+ * @property {string[]} [glob] Glob patterns to copy
11
+ * @property {string[]} [files] Extra files to copy
12
+ */
13
+
14
+ /**
15
+ * Check if a file exists.
16
+ * @param {string} filePath
17
+ * @returns {Promise<boolean>}
18
+ */
19
+ async function fileOrDirExists(filePath) {
20
+ try {
21
+ await fs.stat(filePath);
22
+ return true;
23
+ } catch (err) {
24
+ if (/** @type {{ code: string }} */ (err).code === 'ENOENT') {
25
+ return false;
26
+ }
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Recursively copies files and directories from a source path to a target path.
33
+ *
34
+ * @async
35
+ * @param {Object} options - The options for copying files.
36
+ * @param {string} options.source - The source path to copy from.
37
+ * @param {string} options.target - The target path to copy to.
38
+ * @param {boolean} [options.silent=false] - If true, suppresses console output.
39
+ * @returns {Promise<boolean>} Resolves when the copy operation is complete.
40
+ * @throws {Error} Throws if an error occurs other than the source not existing.
41
+ */
42
+ async function recursiveCopy({ source, target, silent = false }) {
43
+ try {
44
+ await fs.cp(source, target, { recursive: true });
45
+ if (!silent) {
46
+ // eslint-disable-next-line no-console
47
+ console.log(`Copied ${source} to ${target}`);
48
+ }
49
+ return true;
50
+ } catch (err) {
51
+ if (/** @type {{ code: string }} */ (err).code !== 'ENOENT') {
52
+ throw err;
53
+ }
54
+ if (!silent) {
55
+ console.warn(`Source does not exist: ${source}`);
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Process glob patterns and copy matching files.
63
+ * @param {Object} param0
64
+ * @param {boolean} [param0.silent=true] - Whether to suppress output.
65
+ * @param {string[]} param0.globs - The glob patterns to process.
66
+ * @param {string} param0.cwd - The current working directory.
67
+ * @param {string} param0.buildDir - The build directory.
68
+ * @returns {Promise<number>}
69
+ */
70
+ async function processGlobs({ globs, cwd, silent = true, buildDir }) {
71
+ const res = globs.map((globPattern) => {
72
+ const [pattern, baseDir] = globPattern.split(':');
73
+ return { pattern, baseDir };
74
+ });
75
+
76
+ /**
77
+ * Avoids redundant globby calls for the same pattern.
78
+ *
79
+ * @type {Map<string, Promise<string[]>>}
80
+ */
81
+ const globToResMap = new Map();
82
+
83
+ const result = await Promise.all(
84
+ res.map(async ({ pattern, baseDir }) => {
85
+ if (!globToResMap.has(pattern)) {
86
+ const promise = globby(pattern, { cwd });
87
+ globToResMap.set(pattern, promise);
88
+ }
89
+ const files = await globToResMap.get(pattern);
90
+ return { files: files ?? [], baseDir };
91
+ }),
92
+ );
93
+ globToResMap.clear();
94
+
95
+ const filesToProcess = result.flatMap(({ files, baseDir }) => {
96
+ return files.map((file) => {
97
+ const sourcePath = path.resolve(cwd, file);
98
+ const pathSegments = file.split(path.sep);
99
+ const relativePath = pathSegments.slice(1).join(path.sep);
100
+ const targetPath = baseDir
101
+ ? path.resolve(buildDir, baseDir, relativePath)
102
+ : path.resolve(buildDir, relativePath);
103
+ const targetDir = path.dirname(targetPath);
104
+ return { targetDir, targetPath, sourcePath };
105
+ });
106
+ });
107
+
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);
122
+ return filesToProcess.length;
123
+ }
124
+
125
+ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
126
+ command: 'copy-files [files...]',
127
+ describe: 'Copy files from source to target paths within the build directory.',
128
+ builder: (yargs) => {
129
+ return yargs
130
+ .option('silent', {
131
+ type: 'boolean',
132
+ default: true,
133
+ description: "Don't log file names.",
134
+ })
135
+ .option('excludeDefaults', {
136
+ type: 'boolean',
137
+ default: false,
138
+ description:
139
+ 'Exclude default files from the copy operation (includes readme, license, changelog).',
140
+ })
141
+ .option('glob', {
142
+ type: 'string',
143
+ array: true,
144
+ description: 'Glob pattern to match files.',
145
+ })
146
+ .positional('files', {
147
+ type: 'string',
148
+ describe: 'Files to copy, can be specified as `source:target` pairs or just `source`.',
149
+ array: true,
150
+ default: [],
151
+ });
152
+ },
153
+ handler: async (args) => {
154
+ const { silent = false, excludeDefaults = false, glob: globs = [] } = args;
155
+ const cwd = process.cwd();
156
+ const pkgJson = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf-8'));
157
+ /** @type {string} */
158
+ const buildDir = pkgJson.publishConfig?.directory || 'build';
159
+ const extraFiles = args.files ?? [];
160
+ /** @type {string[]} */
161
+ const defaultFiles = [];
162
+ const workspaceDir = await findWorkspaceDir(cwd);
163
+ if (!workspaceDir) {
164
+ throw new Error('Workspace directory not found');
165
+ }
166
+ const localOrRootFiles = [
167
+ [path.join(cwd, 'README.md'), path.join(workspaceDir, 'README.md')],
168
+ [path.join(cwd, 'LICENSE'), path.join(workspaceDir, 'LICENSE')],
169
+ [path.join(cwd, 'CHANGELOG.md'), path.join(workspaceDir, 'CHANGELOG.md')],
170
+ ];
171
+ await Promise.all(
172
+ localOrRootFiles.map(async (files) => {
173
+ for (const file of files) {
174
+ // eslint-disable-next-line no-await-in-loop
175
+ if (await fileOrDirExists(file)) {
176
+ defaultFiles.push(file);
177
+ break;
178
+ }
179
+ }
180
+ }),
181
+ );
182
+
183
+ const filesToCopy = [...(excludeDefaults ? [] : defaultFiles), ...extraFiles];
184
+ let result = filesToCopy.length;
185
+
186
+ if (filesToCopy.length) {
187
+ await Promise.all(
188
+ filesToCopy.map(async (file) => {
189
+ const [sourcePath, targetPath] = path.isAbsolute(file)
190
+ ? [file, undefined]
191
+ : file.split(':');
192
+ const resolvedSourcePath = path.resolve(cwd, sourcePath);
193
+ const resolvedTargetPath = path.resolve(buildDir, targetPath ?? path.basename(file));
194
+ return recursiveCopy({
195
+ source: resolvedSourcePath,
196
+ target: resolvedTargetPath,
197
+ silent,
198
+ });
199
+ }),
200
+ );
201
+ }
202
+
203
+ if (globs.length) {
204
+ result += await processGlobs({
205
+ globs,
206
+ cwd,
207
+ buildDir,
208
+ silent,
209
+ });
210
+ }
211
+ // eslint-disable-next-line no-console
212
+ console.log(`Copied ${result} files.`);
213
+ },
214
+ });