@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.
- package/README.md +1 -1
- package/bin/code-infra.mjs +1 -0
- package/package.json +47 -28
- package/src/babel-config.mjs +138 -0
- package/src/cli/babel.mjs +156 -0
- package/src/cli/cmdArgosPush.mjs +116 -0
- package/src/cli/cmdBuild.mjs +433 -0
- package/src/cli/cmdCopyFiles.mjs +214 -0
- package/src/cli/cmdJsonLint.mjs +27 -31
- package/src/cli/cmdListWorkspaces.mjs +38 -43
- package/src/cli/cmdPublish.mjs +40 -44
- package/src/cli/cmdPublishCanary.mjs +10 -21
- package/src/cli/cmdSetVersionOverrides.mjs +110 -0
- package/src/cli/index.mjs +16 -2
- package/src/cli/pnpm.mjs +62 -23
- package/src/cli/typescript.mjs +175 -0
- package/src/eslint/baseConfig.mjs +35 -20
- package/src/eslint/material-ui/config.mjs +317 -2
- package/src/eslint/testConfig.mjs +7 -0
- package/src/untyped-plugins.d.ts +53 -0
- package/src/utils/build.mjs +20 -0
- package/src/eslint/airbnb/base.mjs +0 -113
- package/src/eslint/airbnb/typescript.mjs +0 -126
|
@@ -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
|
+
});
|