@mui/internal-code-infra 0.0.4-canary.57 → 0.0.4-canary.58

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.
@@ -1,13 +1,41 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { globby } from 'globby';
4
- import { minimatch } from 'minimatch';
5
4
  import * as semver from 'semver';
6
5
 
7
6
  /**
8
7
  * @typedef {'esm' | 'cjs'} BundleType
9
8
  */
10
9
 
10
+ /**
11
+ * @typedef {Object} BundleMeta
12
+ * @property {BundleType} type
13
+ * @property {string} dir
14
+ * @property {'import' | 'require'} condition - The package.json condition this bundle maps to.
15
+ * @property {string} outExtension
16
+ * @property {string} typeOutExtension
17
+ */
18
+
19
+ /**
20
+ * Source files in JS/TS-like languages that the build compiles. These are the
21
+ * only leaves that get an extension swap + `import`/`require`/`types` conditions.
22
+ * Other files under `src/` (e.g. `.css`, `.json`) are copied verbatim and only
23
+ * get their `./src/` prefix rewritten.
24
+ */
25
+ const JS_TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
26
+
27
+ export const BASE_IGNORES = [
28
+ '**/*.test.js',
29
+ '**/*.test.ts',
30
+ '**/*.test.tsx',
31
+ '**/*.spec.js',
32
+ '**/*.spec.ts',
33
+ '**/*.spec.tsx',
34
+ '**/*.d.ts',
35
+ '**/*.test/*.*',
36
+ '**/test-cases/*.*',
37
+ ];
38
+
11
39
  /**
12
40
  * @param {BundleType} bundle
13
41
  * @param {Object} [options]
@@ -49,215 +77,510 @@ function sortExportConditions(conditions) {
49
77
  }
50
78
 
51
79
  /**
52
- * @param {Object} param0
53
- * @param {NonNullable<import('../cli/packageJson').PackageJson.Exports>} param0.importPath
54
- * @param {string} param0.key
55
- * @param {string} param0.cwd
56
- * @param {string} param0.dir
57
- * @param {string} param0.type
58
- * @param {import('../cli/packageJson').PackageJson.ExportConditions} param0.newExports
59
- * @param {string} param0.typeOutExtension
60
- * @param {string} param0.outExtension
61
- * @param {boolean} param0.addTypes
62
- * @returns {Promise<void>}
80
+ * Recursively fills in the `default` condition and stabilizes key order for a
81
+ * single condition value. Bundles run in parallel, so `import`/`require`
82
+ * insertion order would otherwise depend on Promise timing. Nested condition
83
+ * objects (e.g. `{ node: {...}, default: {...} }`) are handled at every level
84
+ * that carries `import`/`require`. Non-condition values (plain strings, bare
85
+ * specifiers, `null`) are returned untouched.
86
+ * @param {any} value
87
+ * @param {boolean} addTypes
88
+ * @param {string} kind - Used for error messages, e.g. `export` or `import`.
89
+ * @param {string} key - Used for error messages.
90
+ * @returns {any}
63
91
  */
64
- async function createExportsFor({
65
- importPath,
66
- key,
67
- cwd,
68
- dir,
69
- type,
70
- newExports,
71
- typeOutExtension,
72
- outExtension,
73
- addTypes,
74
- }) {
75
- if (Array.isArray(importPath)) {
92
+ function finalizeConditionValue(value, addTypes, kind, key) {
93
+ if (value === null || typeof value !== 'object') {
94
+ return value;
95
+ }
96
+ if (Array.isArray(value)) {
76
97
  throw new Error(
77
- `Array form of package.json exports is not supported yet. Found in export "${key}".`,
98
+ `Array form of package.json ${kind}s is not supported yet. Found in ${kind} "${key}".`,
78
99
  );
79
100
  }
80
101
 
81
- let srcPath = typeof importPath === 'string' ? importPath : importPath['mui-src'];
82
- const rest = typeof importPath === 'string' ? {} : { ...importPath };
83
- delete rest['mui-src'];
102
+ for (const childKey of Object.keys(value)) {
103
+ value[childKey] = finalizeConditionValue(value[childKey], addTypes, kind, key);
104
+ }
84
105
 
85
- if (typeof srcPath !== 'string') {
86
- throw new Error(
87
- `Unsupported export for "${key}". Only a string or an object with "mui-src" field is supported for now.`,
88
- );
106
+ if (value.import || value.require) {
107
+ // Synthesize `default` from import/require (preferring ESM), but never clobber
108
+ // a user-authored `default` condition that sits alongside them.
109
+ if (value.default === undefined) {
110
+ const defaultExport = value.import || value.require;
111
+ if (addTypes) {
112
+ value.default = defaultExport;
113
+ } else {
114
+ value.default =
115
+ defaultExport && typeof defaultExport === 'object' && 'default' in defaultExport
116
+ ? defaultExport.default
117
+ : defaultExport;
118
+ }
119
+ }
120
+ return sortExportConditions(value);
89
121
  }
90
122
 
91
- const exportFileExists = srcPath.includes('*')
92
- ? true
93
- : await fs.stat(path.join(cwd, srcPath)).then(
94
- (stats) => stats.isFile() || stats.isDirectory(),
95
- () => false,
123
+ return value;
124
+ }
125
+
126
+ /**
127
+ * Applies {@link finalizeConditionValue} to every entry of a conditions map.
128
+ * @param {import('../cli/packageJson').PackageJson.ExportConditions} conditionsMap
129
+ * @param {boolean} addTypes
130
+ * @param {string} kind - Used for error messages, e.g. `export` or `import`.
131
+ * @returns {void}
132
+ */
133
+ function finalizeConditions(conditionsMap, addTypes, kind) {
134
+ for (const key of Object.keys(conditionsMap)) {
135
+ conditionsMap[key] = finalizeConditionValue(conditionsMap[key], addTypes, kind, key);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Returns the path relative to `src/` if `leaf` points inside the source tree
141
+ * (accepting both `./src/…` and `src/…`), otherwise `null`.
142
+ * @param {string} leaf
143
+ * @returns {string | null}
144
+ */
145
+ function srcRelative(leaf) {
146
+ if (leaf.startsWith('./src/')) {
147
+ return leaf.slice('./src/'.length);
148
+ }
149
+ if (leaf.startsWith('src/')) {
150
+ return leaf.slice('src/'.length);
151
+ }
152
+ return null;
153
+ }
154
+
155
+ /**
156
+ * Maps a source-relative path to its built location, optionally swapping the
157
+ * extension. `*` wildcards are preserved verbatim (Node resolves them at runtime).
158
+ * @param {string} rel - Path relative to `src/`.
159
+ * @param {string} dir - The bundle output dir (`.` for the root).
160
+ * @param {string} ext - The current extension (from `path.extname`).
161
+ * @param {string | null} newExt - The replacement extension, or `null` to keep `ext`.
162
+ * @returns {string}
163
+ */
164
+ function buildOutPath(rel, dir, ext, newExt) {
165
+ const dirPrefix = dir === '.' ? '' : `${dir}/`;
166
+ const base = `./${dirPrefix}${rel}`;
167
+ return newExt ? `${base.slice(0, base.length - ext.length)}${newExt}` : base;
168
+ }
169
+
170
+ /**
171
+ * @param {string} target
172
+ * @returns {Promise<boolean>}
173
+ */
174
+ function fileExists(target) {
175
+ return fs.stat(target).then(
176
+ (stats) => stats.isFile(),
177
+ () => false,
178
+ );
179
+ }
180
+
181
+ /**
182
+ * @param {string} target
183
+ * @returns {Promise<boolean>}
184
+ */
185
+ function fileOrDirExists(target) {
186
+ return fs.stat(target).then(
187
+ (stats) => stats.isFile() || stats.isDirectory(),
188
+ () => false,
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Rewrites a single leaf path for every bundle. A source JS/TS path becomes a
194
+ * per-bundle `{ types?, default }` (or bare out-path string when not adding
195
+ * types). A source asset (e.g. `.css`) gets only its `./src/` prefix rewritten.
196
+ * Anything else (a path already in the build output, or a bare specifier) is
197
+ * passed through verbatim — the escape hatch for `--copy`'d/custom paths.
198
+ * @param {string} leaf
199
+ * @param {Object} ctx
200
+ * @param {BundleMeta[]} ctx.bundleMetas
201
+ * @param {boolean} ctx.addTypes
202
+ * @param {string} ctx.cwd
203
+ * @param {string} [ctx.outputDir]
204
+ * @param {string} ctx.key
205
+ * @param {string} ctx.kind
206
+ * @returns {Promise<any>}
207
+ */
208
+ async function rewriteLeaf(leaf, ctx) {
209
+ const { bundleMetas, addTypes, cwd, outputDir, key, kind } = ctx;
210
+ const rel = srcRelative(leaf);
211
+
212
+ if (rel === null) {
213
+ // Not a source path: it already names a path present in the published
214
+ // output (via `--copy` or because it's there literally), or a bare
215
+ // specifier (external package). Emit verbatim.
216
+ if (outputDir && leaf.startsWith('.') && !leaf.includes('*')) {
217
+ const exists = await fileOrDirExists(path.join(outputDir, leaf));
218
+ if (!exists) {
219
+ // `--copy` runs after package.json generation, so the file may legitimately
220
+ // not be on disk yet — warn rather than fail.
221
+ console.warn(
222
+ `The ${kind} path "${leaf}" for "${key}" was not found in the build output. Ensure it is generated or copied into the output directory.`,
223
+ );
224
+ }
225
+ }
226
+ return leaf;
227
+ }
228
+
229
+ const hasGlob = leaf.includes('*');
230
+ if (!hasGlob) {
231
+ const exists = await fileOrDirExists(path.join(cwd, leaf));
232
+ if (!exists) {
233
+ throw new Error(
234
+ `The path "${leaf}" for ${kind} "${key}" does not exist in the package. Either remove the ${kind} or add the file/folder to the package.`,
96
235
  );
97
- if (!exportFileExists) {
236
+ }
237
+ }
238
+
239
+ const ext = path.extname(rel);
240
+ if (!JS_TS_EXTENSIONS.has(ext)) {
241
+ // Asset copied verbatim into the output: only rewrite the `./src/` prefix.
242
+ const meta = bundleMetas.find((bundle) => bundle.dir === '.') ?? bundleMetas[0];
243
+ return buildOutPath(rel, meta.dir, ext, null);
244
+ }
245
+
246
+ /** @type {Record<string, any>} */
247
+ const result = {};
248
+ for (const meta of bundleMetas) {
249
+ const outPath = buildOutPath(rel, meta.dir, ext, meta.outExtension);
250
+ result[meta.condition] = addTypes
251
+ ? { types: buildOutPath(rel, meta.dir, ext, meta.typeOutExtension), default: outPath }
252
+ : outPath;
253
+ }
254
+ return result;
255
+ }
256
+
257
+ /**
258
+ * Recursively rewrites an export/import entry value, rewriting every source-path
259
+ * leaf (including those nested inside standard condition objects) into its built
260
+ * equivalent. Condition keys and their order are preserved (conditions stay
261
+ * outer; the build-owned `import`/`require` split lives at the leaves).
262
+ * @param {import('../cli/packageJson').PackageJson.Exports} value
263
+ * @param {Object} ctx
264
+ * @param {BundleMeta[]} ctx.bundleMetas
265
+ * @param {boolean} ctx.addTypes
266
+ * @param {string} ctx.cwd
267
+ * @param {string} [ctx.outputDir]
268
+ * @param {string} ctx.key
269
+ * @param {string} ctx.kind
270
+ * @returns {Promise<any>}
271
+ */
272
+ async function rewriteEntryValue(value, ctx) {
273
+ if (value === null) {
274
+ return null;
275
+ }
276
+ if (Array.isArray(value)) {
98
277
  throw new Error(
99
- `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.`,
278
+ `Array form of package.json ${ctx.kind}s is not supported yet. Found in ${ctx.kind} "${ctx.key}".`,
100
279
  );
101
280
  }
102
- srcPath = srcPath.replace(/\.\/src\//, `./${dir === '.' ? '' : `${dir}/`}`);
103
- const ext = path.extname(srcPath);
281
+ if (typeof value === 'string') {
282
+ return rewriteLeaf(value, ctx);
283
+ }
284
+ if (typeof value === 'object') {
285
+ const entries = Object.entries(value);
286
+ const results = await Promise.all(entries.map(([, child]) => rewriteEntryValue(child, ctx)));
287
+ /** @type {Record<string, any>} */
288
+ const out = {};
289
+ entries.forEach(([condition], index) => {
290
+ out[condition] = results[index];
291
+ });
292
+ return out;
293
+ }
294
+ throw new Error(`Unsupported ${ctx.kind} value for "${ctx.key}".`);
295
+ }
296
+
297
+ /**
298
+ * Rewrites every entry of a conditions map, preserving declared key order.
299
+ * @param {import('../cli/packageJson').PackageJson.ExportConditions} conditionsMap
300
+ * @param {Object} baseCtx - The {@link rewriteEntryValue} context minus `key`.
301
+ * @param {BundleMeta[]} baseCtx.bundleMetas
302
+ * @param {boolean} baseCtx.addTypes
303
+ * @param {string} baseCtx.cwd
304
+ * @param {string} [baseCtx.outputDir]
305
+ * @param {string} baseCtx.kind
306
+ * @returns {Promise<import('../cli/packageJson').PackageJson.ExportConditions>}
307
+ */
308
+ async function rewriteConditionsMap(conditionsMap, baseCtx) {
309
+ const keys = Object.keys(conditionsMap);
310
+ const rewritten = await Promise.all(
311
+ keys.map((key) => rewriteEntryValue(conditionsMap[key], { ...baseCtx, key })),
312
+ );
313
+ /** @type {import('../cli/packageJson').PackageJson.ExportConditions} */
314
+ const result = {};
315
+ keys.forEach((key, index) => {
316
+ result[key] = rewritten[index];
317
+ });
318
+ return result;
319
+ }
104
320
 
105
- if (ext === '.css') {
106
- newExports[key] = srcPath;
107
- return;
321
+ /**
322
+ * Splits a pattern around its first `*` wildcard.
323
+ * @param {string} pattern
324
+ * @returns {{ prefix: string; suffix: string }}
325
+ */
326
+ function splitStar(pattern) {
327
+ const star = pattern.indexOf('*');
328
+ return { prefix: pattern.slice(0, star), suffix: pattern.slice(star + 1) };
329
+ }
330
+
331
+ /**
332
+ * Finds the source glob to expand inside an entry value: the plain string, else
333
+ * the `default` condition, else the first leaf that carries a `*`. The captured
334
+ * stem is applied to every sibling leaf so all conditions expand consistently.
335
+ * @param {import('../cli/packageJson').PackageJson.Exports} value
336
+ * @returns {string | undefined}
337
+ */
338
+ function primarySourcePattern(value) {
339
+ if (typeof value === 'string') {
340
+ return value.includes('*') ? value : undefined;
108
341
  }
342
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
343
+ if (typeof value.default === 'string' && value.default.includes('*')) {
344
+ return value.default;
345
+ }
346
+ for (const child of Object.values(value)) {
347
+ const found = primarySourcePattern(child);
348
+ if (found) {
349
+ return found;
350
+ }
351
+ }
352
+ }
353
+ return undefined;
354
+ }
109
355
 
110
- if (typeof newExports[key] === 'string' || Array.isArray(newExports[key])) {
111
- throw new Error(`The export "${key}" is already defined as a string or Array.`);
356
+ /**
357
+ * Substitutes the matched stem into the `*` of every leaf string in a value.
358
+ * @param {import('../cli/packageJson').PackageJson.Exports} value
359
+ * @param {string} stem
360
+ * @returns {any}
361
+ */
362
+ function substituteStem(value, stem) {
363
+ if (typeof value === 'string') {
364
+ return value.replace('*', stem);
112
365
  }
366
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
367
+ /** @type {Record<string, any>} */
368
+ const out = {};
369
+ for (const [condition, child] of Object.entries(value)) {
370
+ out[condition] = substituteStem(child, stem);
371
+ }
372
+ return out;
373
+ }
374
+ return value;
375
+ }
113
376
 
114
- newExports[key] ??= {};
115
- const exportPath = srcPath.replace(ext, outExtension);
116
- // eslint-disable-next-line no-nested-ternary
117
- newExports[key][type === 'cjs' ? 'require' : 'import'] = addTypes
118
- ? {
119
- ...rest,
120
- types: srcPath.replace(ext, typeOutExtension),
121
- default: exportPath,
122
- }
123
- : Object.keys(rest).length
124
- ? {
125
- ...rest,
126
- default: exportPath,
377
+ /**
378
+ * Drops source-path conditions whose file is missing for a glob-expanded entry,
379
+ * so a sibling glob that doesn't match every stem of the primary pattern omits
380
+ * that condition for the unmatched stems instead of failing the build. Only used
381
+ * on glob-expanded values; explicit (non-glob) entries still validate strictly.
382
+ * @param {import('../cli/packageJson').PackageJson.Exports} value
383
+ * @param {string} cwd
384
+ * @returns {Promise<any>}
385
+ */
386
+ async function pruneMissingSourceConditions(value, cwd) {
387
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
388
+ return value;
389
+ }
390
+ const entries = Object.entries(value);
391
+ const resolved = await Promise.all(
392
+ entries.map(async ([condition, child]) => {
393
+ if (typeof child === 'string') {
394
+ const rel = srcRelative(child);
395
+ if (
396
+ rel !== null &&
397
+ !child.includes('*') &&
398
+ !(await fileOrDirExists(path.join(cwd, child)))
399
+ ) {
400
+ return undefined;
127
401
  }
128
- : exportPath;
402
+ return /** @type {[string, any]} */ ([condition, child]);
403
+ }
404
+ const pruned = await pruneMissingSourceConditions(child, cwd);
405
+ if (
406
+ pruned &&
407
+ typeof pruned === 'object' &&
408
+ !Array.isArray(pruned) &&
409
+ Object.keys(pruned).length === 0
410
+ ) {
411
+ return undefined;
412
+ }
413
+ return /** @type {[string, any]} */ ([condition, pruned]);
414
+ }),
415
+ );
416
+ /** @type {Record<string, any>} */
417
+ const out = {};
418
+ for (const entry of resolved) {
419
+ if (entry) {
420
+ out[entry[0]] = entry[1];
421
+ }
422
+ }
423
+ return out;
424
+ }
425
+
426
+ /**
427
+ * Tests whether a subpath-pattern (or exact) key matches a concrete subpath.
428
+ * @param {string} key
429
+ * @param {string} subpath
430
+ * @returns {boolean}
431
+ */
432
+ function keyMatches(key, subpath) {
433
+ const star = key.indexOf('*');
434
+ if (star === -1) {
435
+ return key === subpath;
436
+ }
437
+ const base = key.slice(0, star);
438
+ const suffix = key.slice(star + 1);
439
+ return (
440
+ subpath.length >= base.length + suffix.length &&
441
+ subpath.startsWith(base) &&
442
+ subpath.endsWith(suffix)
443
+ );
129
444
  }
130
445
 
131
446
  /**
132
- * Expands glob patterns (containing `*`) in package.json export keys/values
133
- * into concrete entries by resolving them against actual files on disk.
447
+ * Selects the most-specific key that matches `subpath`, mirroring Node's
448
+ * resolution: an exact key beats any pattern, and among patterns the longest
449
+ * base (substring before `*`) wins, tie-broken by the longest suffix.
450
+ * @param {string} subpath
451
+ * @param {string[]} keys
452
+ * @returns {string | null}
453
+ */
454
+ function selectMostSpecificKey(subpath, keys) {
455
+ /** @type {string | null} */
456
+ let best = null;
457
+ let bestBase = -1;
458
+ let bestSuffix = -1;
459
+ for (const key of keys) {
460
+ if (!keyMatches(key, subpath)) {
461
+ continue;
462
+ }
463
+ const star = key.indexOf('*');
464
+ const base = star === -1 ? Infinity : star;
465
+ const suffix = star === -1 ? 0 : key.length - star - 1;
466
+ if (base > bestBase || (base === bestBase && suffix > bestSuffix)) {
467
+ best = key;
468
+ bestBase = base;
469
+ bestSuffix = suffix;
470
+ }
471
+ }
472
+ return best;
473
+ }
474
+
475
+ /**
476
+ * Expands glob patterns (containing `*`) in export/import keys into concrete
477
+ * entries resolved against files on disk. Each `*` matches a single path segment
478
+ * (use `--no-expand` to keep the pattern as a Node runtime subpath, whose `*`
479
+ * matches across `/`). Negation (`null`) keys are applied via most-specific-key
480
+ * selection rather than cascade-subtraction, so a deeper positive pattern still
481
+ * resolves under a shallower `null`, and a zero-match pattern warns instead of
482
+ * silently dropping.
134
483
  * @param {import('../cli/packageJson').PackageJson.ExportConditions} originalExports
135
484
  * @param {string} cwd
136
485
  * @returns {Promise<import('../cli/packageJson').PackageJson.ExportConditions>}
137
486
  */
138
487
  async function expandExportGlobs(originalExports, cwd) {
488
+ const allKeys = Object.keys(originalExports);
139
489
  /** @type {import('../cli/packageJson').PackageJson.ExportConditions} */
140
490
  const expandedExports = {};
141
491
 
142
- /**
143
- * @typedef {{
144
- * value: import('../cli/packageJson').PackageJson.Exports;
145
- * srcPattern: string;
146
- * srcPrefix: string;
147
- * srcSuffix: string;
148
- * keyPrefix: string;
149
- * keySuffix: string;
150
- * }} GlobEntry
151
- */
152
-
153
- // Collect entries that need glob expansion
154
- /** @type {GlobEntry[]} */
492
+ /** @type {{ key: string; value: import('../cli/packageJson').PackageJson.Exports; srcPattern: string }[]} */
155
493
  const globEntries = [];
156
494
 
157
- // Collect negation patterns (glob keys with null values)
158
- /** @type {string[]} */
159
- const negationPatterns = [];
160
-
161
495
  for (const [key, value] of Object.entries(originalExports)) {
162
- // Null value acts as a negation/exclusion
163
- if (value === null) {
164
- if (key.includes('*')) {
165
- negationPatterns.push(key);
166
- } else {
167
- delete expandedExports[key];
168
- }
169
- continue;
170
- }
171
-
172
496
  if (!key.includes('*')) {
497
+ // Exact keys (including exact `null` blocks) pass straight through.
173
498
  expandedExports[key] = value;
174
499
  continue;
175
500
  }
176
-
177
- // Extract the source pattern from the value
178
- /** @type {string | undefined} */
179
- let srcPattern;
180
- if (typeof value === 'string') {
181
- srcPattern = value;
182
- } else if (value && typeof value === 'object' && !Array.isArray(value)) {
183
- srcPattern = /** @type {string | undefined} */ (value['mui-src']);
501
+ if (value === null) {
502
+ // Negation pattern: carried through after expansion (see below).
503
+ continue;
184
504
  }
185
-
186
- if (typeof srcPattern !== 'string' || !srcPattern.includes('*')) {
505
+ const srcPattern = primarySourcePattern(value);
506
+ if (typeof srcPattern !== 'string') {
507
+ // Glob key whose value has no wildcard: pass through unchanged.
187
508
  expandedExports[key] = value;
188
509
  continue;
189
510
  }
190
-
191
- // Split patterns around the * wildcard
192
- const srcStarIndex = srcPattern.indexOf('*');
193
- const srcPrefix = srcPattern.substring(0, srcStarIndex);
194
- const srcSuffix = srcPattern.substring(srcStarIndex + 1);
195
-
196
- const keyStarIndex = key.indexOf('*');
197
- const keyPrefix = key.substring(0, keyStarIndex);
198
- const keySuffix = key.substring(keyStarIndex + 1);
199
-
200
- globEntries.push({
201
- value,
202
- srcPattern,
203
- srcPrefix,
204
- srcSuffix,
205
- keyPrefix,
206
- keySuffix,
207
- });
511
+ if (srcPattern.indexOf('*') !== srcPattern.lastIndexOf('*')) {
512
+ console.warn(
513
+ `The pattern "${srcPattern}" for "${key}" contains multiple "*" wildcards; only the first is supported.`,
514
+ );
515
+ }
516
+ globEntries.push({ key, value, srcPattern });
208
517
  }
209
518
 
210
- // Resolve all globby calls in parallel
211
519
  const globResults = await Promise.all(
212
- globEntries.map(({ srcPattern }) => globby(srcPattern, { cwd })),
520
+ globEntries.map(({ srcPattern }) => globby(srcPattern, { cwd, ignore: BASE_IGNORES })),
213
521
  );
214
522
 
523
+ /** @type {Set<string>} */
524
+ const usedNegations = new Set();
525
+ /** @type {string[]} */
526
+ const expandedObjectKeys = [];
527
+
215
528
  for (let i = 0; i < globEntries.length; i += 1) {
216
- const { value, srcPrefix, srcSuffix, keyPrefix, keySuffix } = globEntries[i];
217
- const matches = globResults[i];
529
+ const { key, value, srcPattern } = globEntries[i];
530
+ const { prefix: srcPrefix, suffix: srcSuffix } = splitStar(srcPattern);
531
+ const { prefix: keyPrefix, suffix: keySuffix } = splitStar(key);
218
532
 
219
533
  const stems = [];
220
- for (const match of matches) {
534
+ for (const match of globResults[i]) {
221
535
  if (match.startsWith(srcPrefix) && match.endsWith(srcSuffix)) {
222
- const stem =
223
- srcSuffix.length > 0
224
- ? match.substring(srcPrefix.length, match.length - srcSuffix.length)
225
- : match.substring(srcPrefix.length);
226
- if (stem.length > 0) {
536
+ const stem = match.slice(srcPrefix.length, match.length - srcSuffix.length);
537
+ if (stem) {
227
538
  stems.push(stem);
228
539
  }
229
540
  }
230
541
  }
231
-
232
542
  stems.sort();
233
543
 
234
- for (const stem of stems) {
235
- const expandedKey = `${keyPrefix}${stem}${keySuffix}`;
236
- const expandedSrcPath = `${srcPrefix}${stem}${srcSuffix}`;
544
+ if (stems.length === 0) {
545
+ console.warn(`No files matched the pattern "${srcPattern}" for "${key}".`);
546
+ continue;
547
+ }
237
548
 
238
- if (typeof value === 'string') {
239
- expandedExports[expandedKey] = expandedSrcPath;
240
- } else {
241
- expandedExports[expandedKey] = {
242
- ...value,
243
- 'mui-src': expandedSrcPath,
244
- };
549
+ for (const stem of stems) {
550
+ const concreteKey = `${keyPrefix}${stem}${keySuffix}`;
551
+ // Honour Node's most-specific-wins resolution: only emit this concrete
552
+ // entry when this positive pattern is the most-specific match. A deeper
553
+ // positive pattern emits its own entries; a `null` blocks it entirely.
554
+ const winner = selectMostSpecificKey(concreteKey, allKeys);
555
+ if (winner !== key) {
556
+ if (winner !== null && originalExports[winner] === null) {
557
+ usedNegations.add(winner);
558
+ }
559
+ continue;
560
+ }
561
+ expandedExports[concreteKey] = substituteStem(value, stem);
562
+ if (value && typeof value === 'object') {
563
+ expandedObjectKeys.push(concreteKey);
245
564
  }
246
565
  }
247
566
  }
248
567
 
249
- // Apply negation patterns: remove any expanded keys that match a null-valued glob.
250
- // If no keys matched, preserve the pattern itself with null to block that path at runtime.
251
- for (const pattern of negationPatterns) {
252
- let matched = false;
253
- for (const expandedKey of Object.keys(expandedExports)) {
254
- if (minimatch(expandedKey, pattern)) {
255
- delete expandedExports[expandedKey];
256
- matched = true;
257
- }
258
- }
259
- if (!matched) {
260
- expandedExports[pattern] = null;
568
+ // For glob-expanded condition objects, drop any condition whose source file is
569
+ // absent for this stem (a sibling glob need not match every primary stem).
570
+ await Promise.all(
571
+ expandedObjectKeys.map(async (concreteKey) => {
572
+ expandedExports[concreteKey] = await pruneMissingSourceConditions(
573
+ expandedExports[concreteKey],
574
+ cwd,
575
+ );
576
+ }),
577
+ );
578
+
579
+ // Carry through negation patterns that didn't claim any expanded entry so they
580
+ // still block their subtree at runtime.
581
+ for (const [key, value] of Object.entries(originalExports)) {
582
+ if (value === null && key.includes('*') && !usedNegations.has(key)) {
583
+ expandedExports[key] = null;
261
584
  }
262
585
  }
263
586
 
@@ -265,24 +588,46 @@ async function expandExportGlobs(originalExports, cwd) {
265
588
  }
266
589
 
267
590
  /**
268
- * @param {Object} param0
269
- * @param {import('../cli/packageJson').PackageJson['exports']} param0.exports
270
- * @param {{type: BundleType; dir: string}[]} param0.bundles
271
- * @param {string} param0.outputDir
272
- * @param {string} param0.cwd
273
- * @param {boolean} [param0.addTypes]
274
- * @param {boolean} [param0.isFlat]
275
- * @param {'module' | 'commonjs'} [param0.packageType]
591
+ * Builds the per-bundle metadata (output extensions + the `import`/`require`
592
+ * condition each bundle maps to).
593
+ * @param {{type: BundleType; dir: string}[]} bundles
594
+ * @param {boolean} isFlat
595
+ * @param {'module' | 'commonjs'} packageType
596
+ * @returns {BundleMeta[]}
597
+ */
598
+ function createBundleMetas(bundles, isFlat, packageType) {
599
+ return bundles.map(({ type, dir }) => ({
600
+ type,
601
+ dir,
602
+ condition: type === 'cjs' ? 'require' : 'import',
603
+ outExtension: getOutExtension(type, { isFlat, packageType }),
604
+ typeOutExtension: getOutExtension(type, { isFlat, isType: true, packageType }),
605
+ }));
606
+ }
607
+
608
+ /**
609
+ * @param {import('../cli/packageJson').PackageJson['exports']} packageExports
610
+ * @param {Object} options
611
+ * @param {{type: BundleType; dir: string}[]} options.bundles
612
+ * @param {string} options.outputDir
613
+ * @param {string} options.cwd
614
+ * @param {boolean} [options.addTypes]
615
+ * @param {boolean} [options.isFlat]
616
+ * @param {boolean} [options.expand] - Whether to enumerate glob patterns into concrete entries.
617
+ * @param {'module' | 'commonjs'} [options.packageType]
276
618
  */
277
- export async function createPackageExports({
278
- exports: packageExports,
279
- bundles,
280
- outputDir,
281
- cwd,
282
- addTypes = false,
283
- isFlat = false,
284
- packageType = 'commonjs',
285
- }) {
619
+ export async function createPackageExports(
620
+ packageExports,
621
+ {
622
+ bundles,
623
+ outputDir,
624
+ cwd,
625
+ addTypes = false,
626
+ isFlat = false,
627
+ expand = true,
628
+ packageType = 'commonjs',
629
+ },
630
+ ) {
286
631
  const resolvedPackageType = packageType === 'module' ? 'module' : 'commonjs';
287
632
  /**
288
633
  * @type {import('../cli/packageJson').PackageJson.ExportConditions}
@@ -291,7 +636,8 @@ export async function createPackageExports({
291
636
  typeof packageExports === 'string' || Array.isArray(packageExports)
292
637
  ? { '.': packageExports }
293
638
  : packageExports || {};
294
- const originalExports = isFlat ? await expandExportGlobs(rawExports, cwd) : rawExports;
639
+ const originalExports = expand ? await expandExportGlobs(rawExports, cwd) : rawExports;
640
+ const bundleMetas = createBundleMetas(bundles, isFlat, resolvedPackageType);
295
641
  /**
296
642
  * @type {import('../cli/packageJson').PackageJson.ExportConditions}
297
643
  */
@@ -305,73 +651,47 @@ export async function createPackageExports({
305
651
  exports: newExports,
306
652
  };
307
653
 
654
+ // Derive the `.` index entry (plus `main`/`types`) from the built index files.
308
655
  await Promise.all(
309
- bundles.map(async ({ type, dir }) => {
310
- const outExtension = getOutExtension(type, {
311
- isFlat,
312
- packageType: resolvedPackageType,
313
- });
314
- const typeOutExtension = getOutExtension(type, {
315
- isFlat,
316
- isType: true,
317
- packageType: resolvedPackageType,
318
- });
319
- const indexFileExists = await fs.stat(path.join(outputDir, dir, `index${outExtension}`)).then(
320
- (stats) => stats.isFile(),
321
- () => false,
656
+ bundleMetas.map(async (meta) => {
657
+ const indexFileExists = await fileExists(
658
+ path.join(outputDir, meta.dir, `index${meta.outExtension}`),
322
659
  );
323
660
  const typeFileExists =
324
661
  addTypes &&
325
- (await fs.stat(path.join(outputDir, dir, `index${typeOutExtension}`)).then(
326
- (stats) => stats.isFile(),
327
- () => false,
328
- ));
329
- const dirPrefix = dir === '.' ? '' : `${dir}/`;
330
- const exportDir = `./${dirPrefix}index${outExtension}`;
331
- const typeExportDir = `./${dirPrefix}index${typeOutExtension}`;
662
+ (await fileExists(path.join(outputDir, meta.dir, `index${meta.typeOutExtension}`)));
663
+ const dirPrefix = meta.dir === '.' ? '' : `${meta.dir}/`;
664
+ const exportDir = `./${dirPrefix}index${meta.outExtension}`;
665
+ const typeExportDir = `./${dirPrefix}index${meta.typeOutExtension}`;
332
666
 
333
667
  if (indexFileExists) {
334
668
  // skip `packageJson.module` to support parcel and some older bundlers
335
- if (type === 'cjs') {
669
+ if (meta.type === 'cjs') {
336
670
  result.main = exportDir;
337
671
  }
338
-
339
672
  if (typeof newExports['.'] === 'string' || Array.isArray(newExports['.'])) {
340
673
  throw new Error(`The export "." is already defined as a string or Array.`);
341
674
  }
342
-
343
675
  newExports['.'] ??= {};
344
- newExports['.'][type === 'cjs' ? 'require' : 'import'] = typeFileExists
345
- ? {
346
- types: typeExportDir,
347
- default: exportDir,
348
- }
676
+ newExports['.'][meta.condition] = typeFileExists
677
+ ? { types: typeExportDir, default: exportDir }
349
678
  : exportDir;
350
679
  }
351
- if (typeFileExists && type === 'cjs') {
680
+ if (typeFileExists && meta.type === 'cjs') {
352
681
  result.types = typeExportDir;
353
682
  }
354
- const exportKeys = Object.keys(originalExports);
355
- // need to maintain the order of exports
356
- for (const key of exportKeys) {
357
- const importPath = originalExports[key];
358
- if (!importPath) {
359
- newExports[key] = null;
360
- continue;
361
- }
362
- // eslint-disable-next-line no-await-in-loop
363
- await createExportsFor({
364
- importPath,
365
- key,
366
- cwd,
367
- dir,
368
- type,
369
- newExports,
370
- typeOutExtension,
371
- outExtension,
372
- addTypes,
373
- });
374
- }
683
+ }),
684
+ );
685
+
686
+ // Rewrite the user-configured entries, preserving their declared order.
687
+ Object.assign(
688
+ newExports,
689
+ await rewriteConditionsMap(originalExports, {
690
+ bundleMetas,
691
+ addTypes,
692
+ cwd,
693
+ outputDir,
694
+ kind: 'export',
375
695
  }),
376
696
  );
377
697
 
@@ -381,44 +701,84 @@ export async function createPackageExports({
381
701
  }
382
702
  });
383
703
 
384
- // Rebuild condition objects with stable key order; bundles run in parallel so
385
- // import/require insertion order would otherwise depend on Promise timing.
386
- Object.keys(newExports).forEach((key) => {
387
- const exportVal = newExports[key];
388
- if (Array.isArray(exportVal)) {
389
- throw new Error(
390
- `Array form of package.json exports is not supported yet. Found in export "${key}".`,
391
- );
392
- }
393
- if (exportVal && typeof exportVal === 'object' && (exportVal.import || exportVal.require)) {
394
- // Use ESM (import) for default if available, otherwise use require
395
- const defaultExport = exportVal.import || exportVal.require;
704
+ finalizeConditions(newExports, addTypes, 'export');
396
705
 
397
- if (addTypes) {
398
- exportVal.default = defaultExport;
399
- } else {
400
- exportVal.default =
401
- defaultExport && typeof defaultExport === 'object' && 'default' in defaultExport
402
- ? defaultExport.default
403
- : defaultExport;
404
- }
706
+ return result;
707
+ }
405
708
 
406
- newExports[key] = sortExportConditions(exportVal);
709
+ /**
710
+ * Generates the package.json `imports` field for the built package, rewriting
711
+ * internal subpath imports (keys starting with `#`) that point at source files
712
+ * into their built equivalents with `import`/`require`/`types` conditions.
713
+ *
714
+ * Mirrors {@link createPackageExports} but without the `.` / `package.json` /
715
+ * `main` / `types` index handling that only applies to public exports. Entries
716
+ * that resolve to bare specifiers (e.g. an external package) are passed through
717
+ * unchanged, since the `imports` field commonly aliases dependencies.
718
+ * @param {import('../cli/packageJson').PackageJson['imports']} packageImports
719
+ * @param {Object} options
720
+ * @param {{type: BundleType; dir: string}[]} options.bundles
721
+ * @param {string} options.cwd
722
+ * @param {string} [options.outputDir] - Used to verify non-source passthrough paths exist in the build output.
723
+ * @param {boolean} [options.addTypes]
724
+ * @param {boolean} [options.isFlat]
725
+ * @param {boolean} [options.expand] - Whether to enumerate glob patterns into concrete entries.
726
+ * @param {'module' | 'commonjs'} [options.packageType]
727
+ * @returns {Promise<import('../cli/packageJson').PackageJson.Imports | undefined>}
728
+ */
729
+ export async function createPackageImports(
730
+ packageImports,
731
+ {
732
+ bundles,
733
+ cwd,
734
+ outputDir,
735
+ addTypes = false,
736
+ isFlat = false,
737
+ expand = true,
738
+ packageType = 'commonjs',
739
+ },
740
+ ) {
741
+ if (!packageImports || Object.keys(packageImports).length === 0) {
742
+ return undefined;
743
+ }
744
+ for (const key of Object.keys(packageImports)) {
745
+ if (!key.startsWith('#')) {
746
+ throw new Error(
747
+ `Invalid import "${key}": all package.json "imports" keys must start with "#".`,
748
+ );
407
749
  }
750
+ }
751
+ const resolvedPackageType = packageType === 'module' ? 'module' : 'commonjs';
752
+ // `Imports` uses `#`-prefixed keys; treat it as a generic conditions map so the
753
+ // same glob/condition helpers used for exports can process it.
754
+ const rawImports = /** @type {import('../cli/packageJson').PackageJson.ExportConditions} */ (
755
+ packageImports
756
+ );
757
+ const originalImports = expand ? await expandExportGlobs(rawImports, cwd) : rawImports;
758
+ const bundleMetas = createBundleMetas(bundles, isFlat, resolvedPackageType);
759
+
760
+ const newImports = await rewriteConditionsMap(originalImports, {
761
+ bundleMetas,
762
+ addTypes,
763
+ cwd,
764
+ outputDir,
765
+ kind: 'import',
408
766
  });
409
767
 
410
- return result;
768
+ finalizeConditions(newImports, addTypes, 'import');
769
+
770
+ return newImports;
411
771
  }
412
772
 
413
773
  /**
414
- * @param {Object} param0
415
- * @param {import('../cli/packageJson').PackageJson['bin']} param0.bin
416
- * @param {{type: BundleType; dir: string}[]} param0.bundles
417
- * @param {string} param0.cwd
418
- * @param {boolean} [param0.isFlat]
419
- * @param {'module' | 'commonjs'} [param0.packageType]
774
+ * @param {import('../cli/packageJson').PackageJson['bin']} bin
775
+ * @param {Object} options
776
+ * @param {{type: BundleType; dir: string}[]} options.bundles
777
+ * @param {string} options.cwd
778
+ * @param {boolean} [options.isFlat]
779
+ * @param {'module' | 'commonjs'} [options.packageType]
420
780
  */
421
- export async function createPackageBin({ bin, bundles, cwd, isFlat = false, packageType }) {
781
+ export async function createPackageBin(bin, { bundles, cwd, isFlat = false, packageType }) {
422
782
  if (!bin) {
423
783
  return undefined;
424
784
  }
@@ -566,18 +926,6 @@ export function measureFn(label) {
566
926
  return performance.measure(label, startMark, endMark);
567
927
  }
568
928
 
569
- export const BASE_IGNORES = [
570
- '**/*.test.js',
571
- '**/*.test.ts',
572
- '**/*.test.tsx',
573
- '**/*.spec.js',
574
- '**/*.spec.ts',
575
- '**/*.spec.tsx',
576
- '**/*.d.ts',
577
- '**/*.test/*.*',
578
- '**/test-cases/*.*',
579
- ];
580
-
581
929
  /**
582
930
  * A utility to map a function over an array of items in a worker pool.
583
931
  *