@mui/internal-bundle-size-checker 1.0.9-canary.82 → 1.0.9-canary.83

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 CHANGED
@@ -66,7 +66,9 @@ export default defineConfig(async () => {
66
66
  importedNames: ['Button'],
67
67
  // When externals is not specified, peer dependencies will be automatically excluded
68
68
  },
69
- // Expand a package into one entry per export from its package.json
69
+ // Expand a package into one entry per export from its package.json.
70
+ // Wildcard subpath exports (e.g. `"./*"`) are expanded against the files
71
+ // present in the installed package directory.
70
72
  { id: '@mui/material', expand: true },
71
73
  // Expand with glob exclusions (matched against the export subpath, e.g. `styles/colors`)
72
74
  {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolves a package's subpath exports, expanding wildcard patterns (e.g. `./*`)
3
+ * against the files present in the package directory.
4
+ */
5
+ export type ExactExport = {
6
+ key: string;
7
+ isNull: boolean;
8
+ };
9
+ export type PatternExport = {
10
+ key: string;
11
+ prefix: string;
12
+ suffix: string;
13
+ base: number;
14
+ isNull: boolean;
15
+ target: string | undefined;
16
+ };
17
+ export type CollectedExports = {
18
+ exact: ExactExport[];
19
+ patterns: PatternExport[];
20
+ };
21
+ /**
22
+ * Reads a package's `exports` field and returns its concrete subpath export
23
+ * paths, expanding any wildcard subpath patterns (e.g. `./*`) against the
24
+ * files present in the package directory.
25
+ * @param {string} pkgJson - Path to the package's package.json
26
+ * @returns {Promise<string[]>}
27
+ */
28
+ export declare function findExportedPaths(pkgJson: string): Promise<string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-bundle-size-checker",
3
- "version": "1.0.9-canary.82",
3
+ "version": "1.0.9-canary.83",
4
4
  "author": "MUI Team",
5
5
  "description": "Bundle size checker for MUI packages.",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "@types/micromatch": "4.0.10",
46
46
  "@types/yargs": "17.0.35"
47
47
  },
48
- "gitSha": "d6c8fad78de5f289af6c558394477d0c75b3eb88",
48
+ "gitSha": "0f811814fc911cd354856dae0195666fdc46c7fb",
49
49
  "scripts": {
50
50
  "build": "tsgo -p tsconfig.build.json",
51
51
  "test": "pnpm -w test --project @mui/internal-bundle-size-checker",
@@ -2,12 +2,12 @@
2
2
  * Utility to load the bundle-size-checker configuration
3
3
  */
4
4
 
5
- import fs from 'node:fs/promises';
6
5
  import path from 'node:path';
7
6
  import envCi from 'env-ci';
8
7
  import * as module from 'node:module';
9
8
  import * as url from 'node:url';
10
9
  import micromatch from 'micromatch';
10
+ import { findExportedPaths } from './findExportedPaths.js';
11
11
 
12
12
  /**
13
13
  * @typedef {import('./types.js').BundleSizeCheckerConfigObject} BundleSizeCheckerConfigObject
@@ -104,36 +104,6 @@ export function applyUploadConfigDefaults(uploadConfig, ciInfo) {
104
104
  return result;
105
105
  }
106
106
 
107
- /**
108
- * @param {{ [s: string]: any; } | ArrayLike<any>} exportsObj
109
- * @returns {string[]} Array of export paths
110
- */
111
- function findExports(exportsObj) {
112
- const paths = [];
113
- for (const [key, value] of Object.entries(exportsObj)) {
114
- // ignore null values
115
- if (!value) {
116
- continue;
117
- }
118
- if (key.startsWith('.')) {
119
- paths.push(key);
120
- } else {
121
- paths.push(...findExports(value));
122
- }
123
- }
124
- return paths;
125
- }
126
-
127
- /**
128
- * @param {import("fs").PathLike | fs.FileHandle} pkgJson
129
- * @returns {Promise<string[]>}
130
- */
131
- async function findExportedPaths(pkgJson) {
132
- const pkgContent = await fs.readFile(pkgJson, 'utf8');
133
- const { exports = {} } = JSON.parse(pkgContent);
134
- return findExports(exports);
135
- }
136
-
137
107
  /**
138
108
  * Checks if the given import source is a top-level package
139
109
  * @param {string} importSrc - The import source string
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Resolves a package's subpath exports, expanding wildcard patterns (e.g. `./*`)
3
+ * against the files present in the package directory.
4
+ */
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+
9
+ /**
10
+ * @typedef {{ key: string; isNull: boolean }} ExactExport
11
+ * @typedef {{
12
+ * key: string;
13
+ * prefix: string;
14
+ * suffix: string;
15
+ * base: number;
16
+ * isNull: boolean;
17
+ * target: string | undefined;
18
+ * }} PatternExport
19
+ * @typedef {{ exact: ExactExport[]; patterns: PatternExport[] }} CollectedExports
20
+ */
21
+
22
+ /**
23
+ * Resolves an export value to the first target path string containing a `*`,
24
+ * following condition objects and arrays. Node requires every condition of a
25
+ * subpath pattern to place the `*` identically, so any matching target is
26
+ * enough to enumerate the available stems.
27
+ * @param {unknown} value
28
+ * @returns {string | undefined}
29
+ */
30
+ function findWildcardTarget(value) {
31
+ if (typeof value === 'string') {
32
+ return value.includes('*') ? value : undefined;
33
+ }
34
+ if (Array.isArray(value)) {
35
+ for (const item of value) {
36
+ const target = findWildcardTarget(item);
37
+ if (target) {
38
+ return target;
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+ if (value && typeof value === 'object') {
44
+ // Prefer runtime conditions; `types` resolves to .d.ts files, which aren't
45
+ // what gets bundled, so only fall back to it when nothing else matches.
46
+ let typesFallback;
47
+ for (const [condition, conditionValue] of Object.entries(value)) {
48
+ const target = findWildcardTarget(conditionValue);
49
+ if (target) {
50
+ if (condition === 'types') {
51
+ typesFallback = typesFallback ?? target;
52
+ } else {
53
+ return target;
54
+ }
55
+ }
56
+ }
57
+ return typesFallback;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ /**
63
+ * Collects the subpath exports of a package, splitting them into exact keys and
64
+ * wildcard pattern keys. Both kinds keep their `null` (blocking) state so the
65
+ * resolver can reproduce Node's most-specific-match semantics.
66
+ * @param {Record<string, unknown>} exportsObj
67
+ * @returns {CollectedExports}
68
+ */
69
+ function collectExports(exportsObj) {
70
+ /** @type {CollectedExports} */
71
+ const collected = { exact: [], patterns: [] };
72
+ for (const [key, value] of Object.entries(exportsObj)) {
73
+ if (!key.startsWith('.')) {
74
+ // Top-level condition sugar (e.g. `{ import, require }`); recurse to find
75
+ // any nested subpath keys. Condition objects have none, so this is inert
76
+ // for them, but preserves the previous flattening behavior.
77
+ if (value && typeof value === 'object') {
78
+ const nested = collectExports(/** @type {Record<string, unknown>} */ (value));
79
+ collected.exact.push(...nested.exact);
80
+ collected.patterns.push(...nested.patterns);
81
+ }
82
+ continue;
83
+ }
84
+
85
+ const isNull = value === null;
86
+ if (key.includes('*')) {
87
+ const starIndex = key.indexOf('*');
88
+ collected.patterns.push({
89
+ key,
90
+ prefix: key.slice(0, starIndex),
91
+ suffix: key.slice(starIndex + 1),
92
+ // The length of the static prefix decides specificity: a longer prefix
93
+ // is a more specific match (matches Node's PATTERN_KEY_COMPARE).
94
+ base: starIndex,
95
+ isNull,
96
+ target: isNull ? undefined : findWildcardTarget(value),
97
+ });
98
+ } else {
99
+ collected.exact.push({ key, isNull });
100
+ }
101
+ }
102
+ return collected;
103
+ }
104
+
105
+ /**
106
+ * Finds the most specific pattern matching an export path, mirroring Node's
107
+ * resolution: the pattern with the longest static prefix wins, then the longest
108
+ * key. The `*` placeholder may span path separators.
109
+ * @param {string} exportPath
110
+ * @param {PatternExport[]} patterns
111
+ * @returns {PatternExport | undefined}
112
+ */
113
+ function bestPatternMatch(exportPath, patterns) {
114
+ let best;
115
+ for (const pattern of patterns) {
116
+ if (
117
+ exportPath.length >= pattern.prefix.length + pattern.suffix.length &&
118
+ exportPath.startsWith(pattern.prefix) &&
119
+ exportPath.endsWith(pattern.suffix)
120
+ ) {
121
+ if (
122
+ !best ||
123
+ pattern.base > best.base ||
124
+ (pattern.base === best.base && pattern.key.length > best.key.length)
125
+ ) {
126
+ best = pattern;
127
+ }
128
+ }
129
+ }
130
+ return best;
131
+ }
132
+
133
+ /**
134
+ * Expands a single wildcard subpath export (e.g. key `./*`, target
135
+ * `./dist/*.js`) into concrete subpath keys by globbing the package directory.
136
+ * Mirrors Node's subpath pattern semantics where `*` is a substring
137
+ * placeholder that may span path separators.
138
+ * @param {string} key
139
+ * @param {string} target
140
+ * @param {string} pkgDir
141
+ * @returns {Promise<string[]>}
142
+ */
143
+ async function expandWildcardKey(key, target, pkgDir) {
144
+ const keyStarIndex = key.indexOf('*');
145
+ const keyPrefix = key.slice(0, keyStarIndex);
146
+ const keySuffix = key.slice(keyStarIndex + 1);
147
+
148
+ const targetStarIndex = target.indexOf('*');
149
+ const targetPrefix = target.slice(0, targetStarIndex);
150
+ const targetSuffix = target.slice(targetStarIndex + 1);
151
+
152
+ // Limit the filesystem walk to the static directory portion of the target.
153
+ const dirEnd = targetPrefix.lastIndexOf('/');
154
+ const globDir = dirEnd >= 0 ? targetPrefix.slice(0, dirEnd + 1) : '';
155
+ const globPattern = `${globDir.startsWith('./') ? globDir.slice(2) : globDir}**`;
156
+
157
+ const expanded = [];
158
+ for await (const dirent of fs.glob(globPattern, { cwd: pkgDir, withFileTypes: true })) {
159
+ if (!dirent.isFile()) {
160
+ continue;
161
+ }
162
+ const absPath = path.join(dirent.parentPath, dirent.name);
163
+ const relPath = `./${path.relative(pkgDir, absPath).split(path.sep).join('/')}`;
164
+ if (relPath.startsWith('./node_modules/')) {
165
+ continue;
166
+ }
167
+ if (relPath.startsWith(targetPrefix) && relPath.endsWith(targetSuffix)) {
168
+ const stem =
169
+ targetSuffix.length > 0
170
+ ? relPath.slice(targetPrefix.length, relPath.length - targetSuffix.length)
171
+ : relPath.slice(targetPrefix.length);
172
+ if (stem.length > 0) {
173
+ expanded.push(`${keyPrefix}${stem}${keySuffix}`);
174
+ }
175
+ }
176
+ }
177
+ return expanded;
178
+ }
179
+
180
+ /**
181
+ * Reads a package's `exports` field and returns its concrete subpath export
182
+ * paths, expanding any wildcard subpath patterns (e.g. `./*`) against the
183
+ * files present in the package directory.
184
+ * @param {string} pkgJson - Path to the package's package.json
185
+ * @returns {Promise<string[]>}
186
+ */
187
+ export async function findExportedPaths(pkgJson) {
188
+ const pkgContent = await fs.readFile(pkgJson, 'utf8');
189
+ const { exports = {} } = JSON.parse(pkgContent);
190
+ const pkgDir = path.dirname(pkgJson);
191
+
192
+ const { exact, patterns } = collectExports(exports);
193
+ const exactIsNullByKey = new Map(exact.map((entry) => [entry.key, entry.isNull]));
194
+
195
+ // Candidate export paths: explicit (non-null) exact exports, plus everything
196
+ // produced by expanding the non-null wildcard patterns against the files on
197
+ // disk. Null patterns are never expanded; they only ever block.
198
+ const candidates = new Set();
199
+ for (const entry of exact) {
200
+ if (!entry.isNull) {
201
+ candidates.add(entry.key);
202
+ }
203
+ }
204
+ const expandedGroups = await Promise.all(
205
+ patterns
206
+ .filter((pattern) => !pattern.isNull && pattern.target)
207
+ .map((pattern) =>
208
+ expandWildcardKey(pattern.key, /** @type {string} */ (pattern.target), pkgDir),
209
+ ),
210
+ );
211
+ for (const group of expandedGroups) {
212
+ for (const exportPath of group) {
213
+ candidates.add(exportPath);
214
+ }
215
+ }
216
+
217
+ // Keep each candidate only if its most specific matching export key is not
218
+ // blocked by `null`. Exact keys take priority over patterns; among patterns
219
+ // the longest static prefix wins. This reproduces Node's cascade, where a
220
+ // deeper non-null pattern un-blocks paths under a shallower null pattern.
221
+ const result = [];
222
+ for (const exportPath of candidates) {
223
+ const exactIsNull = exactIsNullByKey.get(exportPath);
224
+ let exported;
225
+ if (exactIsNull !== undefined) {
226
+ exported = !exactIsNull;
227
+ } else {
228
+ const best = bestPatternMatch(exportPath, patterns);
229
+ exported = best ? !best.isNull : false;
230
+ }
231
+ if (exported) {
232
+ result.push(exportPath);
233
+ }
234
+ }
235
+
236
+ result.sort();
237
+ return result;
238
+ }
@@ -0,0 +1,54 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { findExportedPaths } from './findExportedPaths.js';
5
+
6
+ const fixturesDir = fileURLToPath(new URL('./__fixtures__/exports', import.meta.url));
7
+
8
+ /**
9
+ * @param {string} name
10
+ * @returns {string}
11
+ */
12
+ function pkgJson(name) {
13
+ return path.join(fixturesDir, name, 'package.json');
14
+ }
15
+
16
+ describe('findExportedPaths', () => {
17
+ it('returns concrete subpaths unchanged when there are no wildcards', async () => {
18
+ expect(await findExportedPaths(pkgJson('static-only'))).toEqual([
19
+ '.',
20
+ './package.json',
21
+ './utils',
22
+ ]);
23
+ });
24
+
25
+ it('expands a wildcard subpath export against files on disk', async () => {
26
+ expect(await findExportedPaths(pkgJson('wildcard-basic'))).toEqual(['.', './a', './b']);
27
+ });
28
+
29
+ it('expands wildcards across path separators, matching Node subpath semantics', async () => {
30
+ expect(await findExportedPaths(pkgJson('wildcard-nested'))).toEqual(['./a', './sub/c']);
31
+ });
32
+
33
+ it('resolves the wildcard target from runtime conditions, ignoring types', async () => {
34
+ expect(await findExportedPaths(pkgJson('wildcard-conditions'))).toEqual(['./a', './b']);
35
+ });
36
+
37
+ it('drops paths blocked by a null negation pattern', async () => {
38
+ expect(await findExportedPaths(pkgJson('wildcard-negation'))).toEqual(['./a']);
39
+ });
40
+
41
+ it('resolves null patterns by specificity, letting a deeper pattern un-block', async () => {
42
+ // `./foo/bar/*` is null, but `./foo/bar/baz/*` is a more specific match, so
43
+ // `./foo/bar/baz/z` stays exported while everything else under `./foo/bar/`
44
+ // (including deeper paths) is blocked.
45
+ expect(await findExportedPaths(pkgJson('wildcard-cascade'))).toEqual([
46
+ './foo/a',
47
+ './foo/bar/baz/z',
48
+ ]);
49
+ });
50
+
51
+ it('drops wildcard keys whose value has no expandable target', async () => {
52
+ expect(await findExportedPaths(pkgJson('wildcard-no-target'))).toEqual([]);
53
+ });
54
+ });
package/src/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface ObjectEntry {
25
25
  importedNames?: string[]; // Optional array of named imports
26
26
  externals?: string[]; // Optional array of packages to exclude from the bundle
27
27
  track?: boolean; // Whether this bundle should be tracked in PR comments (defaults to false)
28
- expand?: boolean | { exclude?: string[] }; // Expand the entry to include all exports; pass `{ exclude }` with glob patterns (matched against export subpaths, e.g. `styles/**`) to skip specific paths
28
+ expand?: boolean | { exclude?: string[] }; // Expand the entry to include all exports. Wildcard subpath exports (e.g. `./*`) are expanded against the package's files on disk. Pass `{ exclude }` with glob patterns (matched against export subpaths, e.g. `styles/**`) to skip specific paths
29
29
  }
30
30
 
31
31
  export type EntryPoint = StringEntry | ObjectEntry;