package-build-stats 8.0.2 → 8.1.0

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.
@@ -21,9 +21,15 @@ function makeRspackConfig({ packageName: _packageName, entry, externals, debug:
21
21
  mode: 'production',
22
22
  devtool: _debug ? 'source-map' : false,
23
23
  optimization: {
24
- runtimeChunk: { name: 'runtime' },
24
+ // Use 'multiple' to create a separate runtime chunk per entry point
25
+ // 'single' or { name: 'runtime' } shares one runtime across all entries,
26
+ // which breaks tree-shaking when building many exports simultaneously
27
+ runtimeChunk: 'multiple',
25
28
  realContentHash: false,
26
29
  minimize: minify,
30
+ // Enable tree-shaking optimizations
31
+ usedExports: true, // Mark unused exports for removal
32
+ sideEffects: true, // Respect package.json sideEffects field
27
33
  // Rspack automatically uses its built-in default minifiers:
28
34
  // - SwcJsMinimizerRspackPlugin for JS (SWC-based, very fast)
29
35
  // - LightningCssMinimizerRspackPlugin for CSS (Lightning CSS-based)
@@ -46,16 +46,31 @@ async function getAllPackageExports(packageString, options = {}) {
46
46
  }
47
47
  async function getPackageExportSizes(packageString, options = {}) {
48
48
  const startTime = perf_hooks_1.performance.now();
49
+ const timings = {};
49
50
  const { name: packageName, normalPath } = (0, common_utils_1.parsePackageString)(packageString);
51
+ const preparePathStart = perf_hooks_1.performance.now();
50
52
  const installPath = await installation_utils_1.default.preparePath(packageName);
53
+ timings.preparePath = perf_hooks_1.performance.now() - preparePathStart;
54
+ console.log(`[PERF] [ExportSizes] preparePath: ${timings.preparePath.toFixed(2)}ms`);
51
55
  try {
56
+ const installStart = perf_hooks_1.performance.now();
52
57
  await installPackage(packageString, installPath, options);
58
+ timings.install = perf_hooks_1.performance.now() - installStart;
59
+ console.log(`[PERF] [ExportSizes] installPackage: ${timings.install.toFixed(2)}ms`);
53
60
  // The package is installed in node_modules subdirectory
54
61
  const packagePath = normalPath || path_1.default.join(installPath, 'node_modules', packageName);
62
+ const getAllExportsStart = perf_hooks_1.performance.now();
55
63
  const exportMap = await (0, exports_utils_1.getAllExports)(packageString, packagePath, packageName, installPath);
64
+ timings.getAllExports = perf_hooks_1.performance.now() - getAllExportsStart;
65
+ console.log(`[PERF] [ExportSizes] getAllExports: ${timings.getAllExports.toFixed(2)}ms`);
56
66
  const exports = Object.keys(exportMap).filter(exp => !(exp === 'default'));
57
67
  debug('Got %d exports for %s', exports.length, packageString);
68
+ console.log(`[PERF] [ExportSizes] Found ${exports.length} exports`);
69
+ const externalsStart = perf_hooks_1.performance.now();
58
70
  const externals = (0, common_utils_1.getExternals)(packageName, installPath);
71
+ timings.getExternals = perf_hooks_1.performance.now() - externalsStart;
72
+ console.log(`[PERF] [ExportSizes] getExternals: ${timings.getExternals.toFixed(2)}ms`);
73
+ const buildStart = perf_hooks_1.performance.now();
59
74
  const builtDetails = await build_utils_1.default.buildPackageIgnoringMissingDeps({
60
75
  name: packageName,
61
76
  installPath,
@@ -66,6 +81,8 @@ async function getPackageExportSizes(packageString, options = {}) {
66
81
  includeDependencySizes: false,
67
82
  },
68
83
  });
84
+ timings.build = perf_hooks_1.performance.now() - buildStart;
85
+ console.log(`[PERF] [ExportSizes] buildPackage: ${timings.build.toFixed(2)}ms`);
69
86
  telemetry_utils_1.default.packageExportsSizes(packageString, startTime, true, options);
70
87
  return {
71
88
  ...builtDetails,
@@ -40,9 +40,14 @@ function getPackageJSONDetails(packageName, installPath) {
40
40
  }
41
41
  async function getPackageStats(packageString, options = {}) {
42
42
  const startTime = perf_hooks_1.performance.now();
43
+ const timings = {};
43
44
  const { name: packageName, isLocal } = (0, common_utils_1.parsePackageString)(packageString);
45
+ const preparePathStart = perf_hooks_1.performance.now();
44
46
  const installPath = await installation_utils_1.default.preparePath(packageName, options.client);
47
+ timings.preparePath = perf_hooks_1.performance.now() - preparePathStart;
48
+ console.log(`[PERF] preparePath: ${timings.preparePath.toFixed(2)}ms`);
45
49
  try {
50
+ const installStart = perf_hooks_1.performance.now();
46
51
  await installation_utils_1.default.installPackage(packageString, installPath, {
47
52
  isLocal,
48
53
  client: options.client,
@@ -50,7 +55,13 @@ async function getPackageStats(packageString, options = {}) {
50
55
  networkConcurrency: options.networkConcurrency,
51
56
  installTimeout: options.installTimeout,
52
57
  });
58
+ timings.install = perf_hooks_1.performance.now() - installStart;
59
+ console.log(`[PERF] installPackage: ${timings.install.toFixed(2)}ms`);
60
+ const externalsStart = perf_hooks_1.performance.now();
53
61
  const externals = (0, common_utils_1.getExternals)(packageName, installPath);
62
+ timings.getExternals = perf_hooks_1.performance.now() - externalsStart;
63
+ console.log(`[PERF] getExternals: ${timings.getExternals.toFixed(2)}ms`);
64
+ const parallelStart = perf_hooks_1.performance.now();
54
65
  const [pacakgeJSONDetails, builtDetails] = await Promise.all([
55
66
  getPackageJSONDetails(packageName, installPath),
56
67
  build_utils_1.default.buildPackageIgnoringMissingDeps({
@@ -65,6 +76,8 @@ async function getPackageStats(packageString, options = {}) {
65
76
  },
66
77
  }),
67
78
  ]);
79
+ timings.parallelBuild = perf_hooks_1.performance.now() - parallelStart;
80
+ console.log(`[PERF] parallel (packageJSON + build): ${timings.parallelBuild.toFixed(2)}ms`);
68
81
  const hasCSSAsset = builtDetails.assets.some(asset => asset.type === 'css');
69
82
  const mainAsset = builtDetails.assets.find(asset => asset.name === 'main' && asset.type === (hasCSSAsset ? 'css' : 'js'));
70
83
  if (!mainAsset) {
@@ -199,7 +199,11 @@ const BuildUtils = {
199
199
  gzip,
200
200
  };
201
201
  };
202
- const assetStatsPromises = ((_a = jsonStats === null || jsonStats === void 0 ? void 0 : jsonStats.assets) === null || _a === void 0 ? void 0 : _a.filter(asset => { var _a; return !((_a = asset.chunkNames) === null || _a === void 0 ? void 0 : _a.includes('runtime')); }).filter(asset => !asset.name.endsWith('LICENSE.txt')).map(getAssetStats)) || [];
202
+ const assetStatsPromises = ((_a = jsonStats === null || jsonStats === void 0 ? void 0 : jsonStats.assets) === null || _a === void 0 ? void 0 : _a.filter(asset => {
203
+ var _a;
204
+ return !((_a = asset.chunkNames) === null || _a === void 0 ? void 0 : _a.some(name => name === 'runtime' ||
205
+ (typeof name === 'string' && name.startsWith('runtime~'))));
206
+ }).filter(asset => !asset.name.endsWith('LICENSE.txt')).map(getAssetStats)) || [];
203
207
  const assetStats = await Promise.all(assetStatsPromises);
204
208
  telemetry_utils_1.default.assetsGZIPParseTime(name, perf_hooks_1.performance.now());
205
209
  let dependencySizeResults = {};
@@ -17,6 +17,7 @@ export declare function getAllExports(packageString: string, context: string, lo
17
17
  * Get exports details from code (compatibility function)
18
18
  *
19
19
  * This provides the same API as the existing getExportsDetails for backward compatibility
20
+ * Returns simple string arrays for exports (without moduleRequest info)
20
21
  */
21
22
  export declare function getExportsDetails(code: string, filename?: string): {
22
23
  exports: string[];
@@ -18,9 +18,13 @@ const promises_1 = __importDefault(require("fs/promises"));
18
18
  const telemetry_utils_1 = __importDefault(require("./telemetry.utils"));
19
19
  const perf_hooks_1 = require("perf_hooks");
20
20
  // Initialize resolver with ESM-first configuration
21
- // Following oxc-linter's resolver configuration:
22
21
  // - main_fields: ["module", "main"] - prioritize ESM entry points
23
- // - condition_names: ["module", "import"] - standard ESM export conditions
22
+ // - condition_names: ["import", "default", "require"] - ESM-first export conditions
23
+ // NOTE: We intentionally exclude "node" because Node.js conditional exports resolution
24
+ // uses the PACKAGE's exports field order (not our conditionNames order) to determine
25
+ // priority. Packages like Vue have "node" before "import" in their exports, so including
26
+ // "node" would resolve to CJS files instead of ESM. We keep "require" as a fallback for
27
+ // packages that only export via "require" condition.
24
28
  // - extensions: all common JS/TS extensions
25
29
  // - symlinks: false - keep paths as-is without resolving symlinks (matches enhanced-resolve behavior)
26
30
  const resolver = new oxc_resolver_1.ResolverFactory({
@@ -35,8 +39,8 @@ const resolver = new oxc_resolver_1.ResolverFactory({
35
39
  '.cts',
36
40
  '.json',
37
41
  ],
38
- mainFields: ['module', 'main'], // ESM-first: prioritize \"module\" field over \"main\"
39
- conditionNames: ['import', 'require', 'node', 'default'], // Standard export conditions
42
+ mainFields: ['module', 'main'], // ESM-first: prioritize "module" field over "main"
43
+ conditionNames: ['import', 'default', 'require'], // ESM-first: exclude "node" which resolves to CJS
40
44
  symlinks: false, // Don't resolve symlinks to match enhanced-resolve behavior
41
45
  });
42
46
  /**
@@ -47,6 +51,7 @@ function getExportsFromStaticExports(staticExports) {
47
51
  const exportAllLocations = [];
48
52
  staticExports.forEach(staticExport => {
49
53
  staticExport.entries.forEach((entry) => {
54
+ var _a, _b;
50
55
  // Skip type-only exports (TypeScript type exports)
51
56
  if (entry.isType) {
52
57
  return;
@@ -63,10 +68,16 @@ function getExportsFromStaticExports(staticExports) {
63
68
  case 'None': // export const foo = 1
64
69
  // Get the export name
65
70
  if (entry.exportName.kind === 'Name' && entry.exportName.name) {
66
- exports.push(entry.exportName.name);
71
+ exports.push({
72
+ name: entry.exportName.name,
73
+ moduleRequest: (_a = entry.moduleRequest) === null || _a === void 0 ? void 0 : _a.value, // Track the source module for re-exports
74
+ });
67
75
  }
68
76
  else if (entry.exportName.kind === 'Default') {
69
- exports.push('default');
77
+ exports.push({
78
+ name: 'default',
79
+ moduleRequest: (_b = entry.moduleRequest) === null || _b === void 0 ? void 0 : _b.value,
80
+ });
70
81
  }
71
82
  break;
72
83
  }
@@ -109,15 +120,26 @@ async function walkExportsRecursive(context, lookupPath, visited, rootContext, i
109
120
  }
110
121
  const { exports, exportAllLocations } = getExportsFromStaticExports(parseResult.module.staticExports);
111
122
  const resolvedExports = {};
112
- // Add direct exports from this module
113
- exports.forEach(exp => {
114
- // Use path.relative() to calculate the relative path from root to resolvedPath
123
+ // Add direct exports from this module, resolving re-exports to their source files
124
+ for (const exp of exports) {
125
+ let sourcePath = resolvedPath;
126
+ // If this is a re-export (export { foo } from './module'), resolve to the source file
127
+ if (exp.moduleRequest) {
128
+ try {
129
+ sourcePath = await resolveModule(path_1.default.dirname(resolvedPath), exp.moduleRequest);
130
+ }
131
+ catch {
132
+ // If resolution fails, fall back to current file
133
+ sourcePath = resolvedPath;
134
+ }
135
+ }
136
+ // Use path.relative() to calculate the relative path from root to sourcePath
115
137
  // This works correctly since symlinks are not resolved (symlinks: false in resolver config)
116
138
  const relativePath = root
117
- ? path_1.default.relative(root, resolvedPath)
118
- : path_1.default.basename(resolvedPath);
119
- resolvedExports[exp] = relativePath;
120
- });
139
+ ? path_1.default.relative(root, sourcePath)
140
+ : path_1.default.basename(sourcePath);
141
+ resolvedExports[exp.name] = relativePath;
142
+ }
121
143
  // Recursively process export * statements
122
144
  const promises = exportAllLocations.map(async (location) => {
123
145
  const starExports = await walkExportsRecursive(path_1.default.dirname(resolvedPath), location, visited, root);
@@ -162,10 +184,16 @@ async function getAllExports(packageString, context, lookupPath, installPath) {
162
184
  * Get exports details from code (compatibility function)
163
185
  *
164
186
  * This provides the same API as the existing getExportsDetails for backward compatibility
187
+ * Returns simple string arrays for exports (without moduleRequest info)
165
188
  */
166
189
  function getExportsDetails(code, filename = 'module.js') {
167
190
  const parseResult = (0, oxc_parser_1.parseSync)(filename, code, {
168
191
  sourceType: 'module',
169
192
  });
170
- return getExportsFromStaticExports(parseResult.module.staticExports);
193
+ const result = getExportsFromStaticExports(parseResult.module.staticExports);
194
+ // Return simple string array for backward compatibility
195
+ return {
196
+ exports: result.exports.map(exp => exp.name),
197
+ exportAllLocations: result.exportAllLocations,
198
+ };
171
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-build-stats",
3
- "version": "8.0.2",
3
+ "version": "8.1.0",
4
4
  "author": "Shubham Kanodia <shubham.kanodia10@gmail.com>",
5
5
  "repository": "https://github.com/pastelsky/package-build-stats",
6
6
  "publishConfig": {
@@ -38,6 +38,7 @@
38
38
  "compare:test": "tsx scripts/index.ts test",
39
39
  "compare:top": "tsx scripts/index.ts top",
40
40
  "compare:advanced": "tsx scripts/index.ts compare",
41
+ "perf:test": "tsx scripts/performance-test.ts",
41
42
  "prepublish": "rm -rf build && yarn check && tsc && yarn format"
42
43
  },
43
44
  "prettier": {
@@ -35,9 +35,15 @@ export default function makeRspackConfig({
35
35
  mode: 'production',
36
36
  devtool: _debug ? 'source-map' : false,
37
37
  optimization: {
38
- runtimeChunk: { name: 'runtime' },
38
+ // Use 'multiple' to create a separate runtime chunk per entry point
39
+ // 'single' or { name: 'runtime' } shares one runtime across all entries,
40
+ // which breaks tree-shaking when building many exports simultaneously
41
+ runtimeChunk: 'multiple',
39
42
  realContentHash: false,
40
43
  minimize: minify,
44
+ // Enable tree-shaking optimizations
45
+ usedExports: true, // Mark unused exports for removal
46
+ sideEffects: true, // Respect package.json sideEffects field
41
47
  // Rspack automatically uses its built-in default minifiers:
42
48
  // - SwcJsMinimizerRspackPlugin for JS (SWC-based, very fast)
43
49
  // - LightningCssMinimizerRspackPlugin for CSS (Lightning CSS-based)
@@ -62,27 +62,53 @@ export async function getPackageExportSizes(
62
62
  options: GetPackageStatsOptions = {},
63
63
  ) {
64
64
  const startTime = performance.now()
65
+ const timings: Record<string, number> = {}
66
+
65
67
  const { name: packageName, normalPath } = parsePackageString(packageString)
68
+
69
+ const preparePathStart = performance.now()
66
70
  const installPath = await InstallationUtils.preparePath(packageName)
71
+ timings.preparePath = performance.now() - preparePathStart
72
+ console.log(
73
+ `[PERF] [ExportSizes] preparePath: ${timings.preparePath.toFixed(2)}ms`,
74
+ )
67
75
 
68
76
  try {
77
+ const installStart = performance.now()
69
78
  await installPackage(packageString, installPath, options)
79
+ timings.install = performance.now() - installStart
80
+ console.log(
81
+ `[PERF] [ExportSizes] installPackage: ${timings.install.toFixed(2)}ms`,
82
+ )
70
83
 
71
84
  // The package is installed in node_modules subdirectory
72
85
  const packagePath =
73
86
  normalPath || path.join(installPath, 'node_modules', packageName)
87
+
88
+ const getAllExportsStart = performance.now()
74
89
  const exportMap = await getAllExports(
75
90
  packageString,
76
91
  packagePath,
77
92
  packageName,
78
93
  installPath, // Pass installPath as base for relative path calculation
79
94
  )
95
+ timings.getAllExports = performance.now() - getAllExportsStart
96
+ console.log(
97
+ `[PERF] [ExportSizes] getAllExports: ${timings.getAllExports.toFixed(2)}ms`,
98
+ )
80
99
 
81
100
  const exports = Object.keys(exportMap).filter(exp => !(exp === 'default'))
82
101
  debug('Got %d exports for %s', exports.length, packageString)
102
+ console.log(`[PERF] [ExportSizes] Found ${exports.length} exports`)
83
103
 
104
+ const externalsStart = performance.now()
84
105
  const externals = getExternals(packageName, installPath)
106
+ timings.getExternals = performance.now() - externalsStart
107
+ console.log(
108
+ `[PERF] [ExportSizes] getExternals: ${timings.getExternals.toFixed(2)}ms`,
109
+ )
85
110
 
111
+ const buildStart = performance.now()
86
112
  const builtDetails = await BuildUtils.buildPackageIgnoringMissingDeps({
87
113
  name: packageName,
88
114
  installPath,
@@ -93,6 +119,10 @@ export async function getPackageExportSizes(
93
119
  includeDependencySizes: false,
94
120
  },
95
121
  })
122
+ timings.build = performance.now() - buildStart
123
+ console.log(
124
+ `[PERF] [ExportSizes] buildPackage: ${timings.build.toFixed(2)}ms`,
125
+ )
96
126
 
97
127
  Telemetry.packageExportsSizes(packageString, startTime, true, options)
98
128
  return {
@@ -53,14 +53,20 @@ export default async function getPackageStats(
53
53
  options: GetPackageStatsOptions = {},
54
54
  ) {
55
55
  const startTime = performance.now()
56
+ const timings: Record<string, number> = {}
56
57
 
57
58
  const { name: packageName, isLocal } = parsePackageString(packageString)
58
59
 
60
+ const preparePathStart = performance.now()
59
61
  const installPath = await InstallationUtils.preparePath(
60
62
  packageName,
61
63
  options.client,
62
64
  )
65
+ timings.preparePath = performance.now() - preparePathStart
66
+ console.log(`[PERF] preparePath: ${timings.preparePath.toFixed(2)}ms`)
67
+
63
68
  try {
69
+ const installStart = performance.now()
64
70
  await InstallationUtils.installPackage(packageString, installPath, {
65
71
  isLocal,
66
72
  client: options.client,
@@ -68,9 +74,15 @@ export default async function getPackageStats(
68
74
  networkConcurrency: options.networkConcurrency,
69
75
  installTimeout: options.installTimeout,
70
76
  })
77
+ timings.install = performance.now() - installStart
78
+ console.log(`[PERF] installPackage: ${timings.install.toFixed(2)}ms`)
71
79
 
80
+ const externalsStart = performance.now()
72
81
  const externals = getExternals(packageName, installPath)
82
+ timings.getExternals = performance.now() - externalsStart
83
+ console.log(`[PERF] getExternals: ${timings.getExternals.toFixed(2)}ms`)
73
84
 
85
+ const parallelStart = performance.now()
74
86
  const [pacakgeJSONDetails, builtDetails] = await Promise.all([
75
87
  getPackageJSONDetails(packageName, installPath),
76
88
  BuildUtils.buildPackageIgnoringMissingDeps({
@@ -85,6 +97,10 @@ export default async function getPackageStats(
85
97
  },
86
98
  }),
87
99
  ])
100
+ timings.parallelBuild = performance.now() - parallelStart
101
+ console.log(
102
+ `[PERF] parallel (packageJSON + build): ${timings.parallelBuild.toFixed(2)}ms`,
103
+ )
88
104
 
89
105
  const hasCSSAsset = builtDetails.assets.some(asset => asset.type === 'css')
90
106
  const mainAsset = builtDetails.assets.find(
@@ -306,7 +306,14 @@ const BuildUtils = {
306
306
 
307
307
  const assetStatsPromises =
308
308
  jsonStats?.assets
309
- ?.filter(asset => !asset.chunkNames?.includes('runtime'))
309
+ ?.filter(
310
+ asset =>
311
+ !asset.chunkNames?.some(
312
+ name =>
313
+ name === 'runtime' ||
314
+ (typeof name === 'string' && name.startsWith('runtime~')),
315
+ ),
316
+ )
310
317
  .filter(asset => !asset.name.endsWith('LICENSE.txt'))
311
318
  .map(getAssetStats) || []
312
319
  const assetStats = await Promise.all(assetStatsPromises)
@@ -13,9 +13,13 @@ import Telemetry from './telemetry.utils'
13
13
  import { performance } from 'perf_hooks'
14
14
 
15
15
  // Initialize resolver with ESM-first configuration
16
- // Following oxc-linter's resolver configuration:
17
16
  // - main_fields: ["module", "main"] - prioritize ESM entry points
18
- // - condition_names: ["module", "import"] - standard ESM export conditions
17
+ // - condition_names: ["import", "default", "require"] - ESM-first export conditions
18
+ // NOTE: We intentionally exclude "node" because Node.js conditional exports resolution
19
+ // uses the PACKAGE's exports field order (not our conditionNames order) to determine
20
+ // priority. Packages like Vue have "node" before "import" in their exports, so including
21
+ // "node" would resolve to CJS files instead of ESM. We keep "require" as a fallback for
22
+ // packages that only export via "require" condition.
19
23
  // - extensions: all common JS/TS extensions
20
24
  // - symlinks: false - keep paths as-is without resolving symlinks (matches enhanced-resolve behavior)
21
25
  const resolver = new ResolverFactory({
@@ -30,19 +34,27 @@ const resolver = new ResolverFactory({
30
34
  '.cts',
31
35
  '.json',
32
36
  ],
33
- mainFields: ['module', 'main'], // ESM-first: prioritize \"module\" field over \"main\"
34
- conditionNames: ['import', 'require', 'node', 'default'], // Standard export conditions
37
+ mainFields: ['module', 'main'], // ESM-first: prioritize "module" field over "main"
38
+ conditionNames: ['import', 'default', 'require'], // ESM-first: exclude "node" which resolves to CJS
35
39
  symlinks: false, // Don't resolve symlinks to match enhanced-resolve behavior
36
40
  })
37
41
 
42
+ /**
43
+ * Represents a named export with its optional source module
44
+ */
45
+ type NamedExport = {
46
+ name: string
47
+ moduleRequest?: string // The source module for re-exports like `export { foo } from './module'`
48
+ }
49
+
38
50
  /**
39
51
  * Extract export information from parsed oxc module
40
52
  */
41
53
  function getExportsFromStaticExports(staticExports: StaticExport[]): {
42
- exports: string[]
54
+ exports: NamedExport[]
43
55
  exportAllLocations: string[]
44
56
  } {
45
- const exports: string[] = []
57
+ const exports: NamedExport[] = []
46
58
  const exportAllLocations: string[] = []
47
59
 
48
60
  staticExports.forEach(staticExport => {
@@ -65,9 +77,15 @@ function getExportsFromStaticExports(staticExports: StaticExport[]): {
65
77
  case 'None': // export const foo = 1
66
78
  // Get the export name
67
79
  if (entry.exportName.kind === 'Name' && entry.exportName.name) {
68
- exports.push(entry.exportName.name)
80
+ exports.push({
81
+ name: entry.exportName.name,
82
+ moduleRequest: entry.moduleRequest?.value, // Track the source module for re-exports
83
+ })
69
84
  } else if (entry.exportName.kind === 'Default') {
70
- exports.push('default')
85
+ exports.push({
86
+ name: 'default',
87
+ moduleRequest: entry.moduleRequest?.value,
88
+ })
71
89
  }
72
90
  break
73
91
  }
@@ -134,16 +152,31 @@ async function walkExportsRecursive(
134
152
 
135
153
  const resolvedExports: ResolvedExports = {}
136
154
 
137
- // Add direct exports from this module
138
- exports.forEach(exp => {
139
- // Use path.relative() to calculate the relative path from root to resolvedPath
155
+ // Add direct exports from this module, resolving re-exports to their source files
156
+ for (const exp of exports) {
157
+ let sourcePath = resolvedPath
158
+
159
+ // If this is a re-export (export { foo } from './module'), resolve to the source file
160
+ if (exp.moduleRequest) {
161
+ try {
162
+ sourcePath = await resolveModule(
163
+ path.dirname(resolvedPath),
164
+ exp.moduleRequest,
165
+ )
166
+ } catch {
167
+ // If resolution fails, fall back to current file
168
+ sourcePath = resolvedPath
169
+ }
170
+ }
171
+
172
+ // Use path.relative() to calculate the relative path from root to sourcePath
140
173
  // This works correctly since symlinks are not resolved (symlinks: false in resolver config)
141
174
  const relativePath = root
142
- ? path.relative(root, resolvedPath)
143
- : path.basename(resolvedPath)
175
+ ? path.relative(root, sourcePath)
176
+ : path.basename(sourcePath)
144
177
 
145
- resolvedExports[exp] = relativePath
146
- })
178
+ resolvedExports[exp.name] = relativePath
179
+ }
147
180
 
148
181
  // Recursively process export * statements
149
182
  const promises = exportAllLocations.map(async location => {
@@ -210,11 +243,18 @@ export async function getAllExports(
210
243
  * Get exports details from code (compatibility function)
211
244
  *
212
245
  * This provides the same API as the existing getExportsDetails for backward compatibility
246
+ * Returns simple string arrays for exports (without moduleRequest info)
213
247
  */
214
248
  export function getExportsDetails(code: string, filename = 'module.js') {
215
249
  const parseResult = parseSync(filename, code, {
216
250
  sourceType: 'module',
217
251
  })
218
252
 
219
- return getExportsFromStaticExports(parseResult.module.staticExports)
253
+ const result = getExportsFromStaticExports(parseResult.module.staticExports)
254
+
255
+ // Return simple string array for backward compatibility
256
+ return {
257
+ exports: result.exports.map(exp => exp.name),
258
+ exportAllLocations: result.exportAllLocations,
259
+ }
220
260
  }