next.config.utils.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. // workaround by https://github.com/martpie/next-transpile-modules/issues/143#issuecomment-817467144
  2. const fs = require('node:fs');
  3. const path = require('node:path');
  4. const nodeModulesPaths = [
  5. path.resolve(__dirname, '../../node_modules'),
  6. path.resolve(__dirname, '../../../../node_modules'),
  7. ];
  8. /**
  9. * @typedef { { ignorePackageNames: string[] } } Opts
  10. */
  11. /** @type {Opts} */
  12. const defaultOpts = { ignorePackageNames: [] };
  13. /**
  14. * @param scopes {string[]}
  15. */
  16. exports.listScopedPackages = (scopes, opts = defaultOpts) => {
  17. /** @type {string[]} */
  18. const scopedPackages = [];
  19. nodeModulesPaths.forEach((nodeModulesPath) => {
  20. fs.readdirSync(nodeModulesPath)
  21. .filter((name) => scopes.includes(name))
  22. .forEach((scope) => {
  23. fs.readdirSync(path.resolve(nodeModulesPath, scope))
  24. .filter((name) => !name.startsWith('.'))
  25. .forEach((folderName) => {
  26. const packageJsonPath = path.resolve(
  27. nodeModulesPath,
  28. scope,
  29. folderName,
  30. 'package.json',
  31. );
  32. if (fs.existsSync(packageJsonPath)) {
  33. const { name } = require(packageJsonPath);
  34. if (!opts.ignorePackageNames.includes(name)) {
  35. scopedPackages.push(name);
  36. }
  37. }
  38. });
  39. });
  40. });
  41. return scopedPackages;
  42. };
  43. /**
  44. * @param prefixes {string[]}
  45. */
  46. /**
  47. * Webpack plugin that logs eager (initial) vs lazy (async-only) module counts.
  48. * Attach to client-side dev builds only.
  49. */
  50. exports.createChunkModuleStatsPlugin = () => ({
  51. apply(compiler) {
  52. compiler.hooks.done.tap('ChunkModuleStatsPlugin', (stats) => {
  53. const { compilation } = stats;
  54. const initialModuleIds = new Set();
  55. const asyncModuleIds = new Set();
  56. for (const chunk of compilation.chunks) {
  57. const target = chunk.canBeInitial() ? initialModuleIds : asyncModuleIds;
  58. for (const module of compilation.chunkGraph.getChunkModulesIterable(
  59. chunk,
  60. )) {
  61. target.add(module.identifier());
  62. }
  63. }
  64. // Modules that appear ONLY in async chunks
  65. const asyncOnlyCount = [...asyncModuleIds].filter(
  66. (id) => !initialModuleIds.has(id),
  67. ).length;
  68. // biome-ignore lint/suspicious/noConsole: Dev-only module stats for compilation analysis
  69. console.log(
  70. `[ChunkModuleStats] initial: ${initialModuleIds.size}, async-only: ${asyncOnlyCount}, total: ${compilation.modules.size}`,
  71. );
  72. // Dump module details to file for analysis (only for large compilations)
  73. if (
  74. initialModuleIds.size > 500 &&
  75. process.env.DUMP_INITIAL_MODULES === '1'
  76. ) {
  77. const asyncOnlyIds = [...asyncModuleIds].filter(
  78. (id) => !initialModuleIds.has(id),
  79. );
  80. const analyzeModuleSet = (moduleIds, title, filename) => {
  81. const packageCounts = {};
  82. const appModules = [];
  83. for (const id of moduleIds) {
  84. const nmIdx = id.lastIndexOf('node_modules/');
  85. if (nmIdx !== -1) {
  86. const rest = id.slice(nmIdx + 'node_modules/'.length);
  87. const pkg = rest.startsWith('@')
  88. ? rest.split('/').slice(0, 2).join('/')
  89. : rest.split('/')[0];
  90. packageCounts[pkg] = (packageCounts[pkg] || 0) + 1;
  91. } else {
  92. appModules.push(id);
  93. }
  94. }
  95. const sorted = Object.entries(packageCounts).sort(
  96. (a, b) => b[1] - a[1],
  97. );
  98. const lines = [`# ${title}`, ''];
  99. lines.push(`Total modules: ${moduleIds.length ?? moduleIds.size}`);
  100. lines.push(`App modules (non-node_modules): ${appModules.length}`);
  101. lines.push(`node_modules packages: ${sorted.length}`);
  102. lines.push('');
  103. lines.push('## Top Packages by Module Count');
  104. lines.push('| # | Package | Modules |');
  105. lines.push('|---|---------|---------|');
  106. for (let i = 0; i < sorted.length; i++) {
  107. const [pkg, count] = sorted[i];
  108. lines.push(`| ${i + 1} | ${pkg} | ${count} |`);
  109. }
  110. lines.push('');
  111. lines.push('## App Modules (first 200)');
  112. for (const m of appModules.slice(0, 200)) {
  113. lines.push(`- ${m}`);
  114. }
  115. const outPath = path.resolve(compiler.outputPath, '..', filename);
  116. fs.writeFileSync(outPath, lines.join('\n'));
  117. // biome-ignore lint/suspicious/noConsole: Dev-only module stats dump path
  118. console.log(
  119. `[ChunkModuleStats] Dumped ${title.toLowerCase()} to ${outPath}`,
  120. );
  121. };
  122. analyzeModuleSet(
  123. initialModuleIds,
  124. 'Initial Chunk Module Analysis',
  125. 'initial-modules-analysis.md',
  126. );
  127. analyzeModuleSet(
  128. asyncOnlyIds,
  129. 'Async-Only Chunk Module Analysis',
  130. 'async-modules-analysis.md',
  131. );
  132. }
  133. });
  134. },
  135. });
  136. exports.listPrefixedPackages = (prefixes, opts = defaultOpts) => {
  137. /** @type {string[]} */
  138. const prefixedPackages = [];
  139. nodeModulesPaths.forEach((nodeModulesPath) => {
  140. fs.readdirSync(nodeModulesPath)
  141. .filter((name) => prefixes.some((prefix) => name.startsWith(prefix)))
  142. .filter((name) => !name.startsWith('.'))
  143. .forEach((folderName) => {
  144. const packageJsonPath = path.resolve(
  145. nodeModulesPath,
  146. folderName,
  147. 'package.json',
  148. );
  149. if (fs.existsSync(packageJsonPath)) {
  150. const { name } = require(packageJsonPath);
  151. if (!opts.ignorePackageNames.includes(name)) {
  152. prefixedPackages.push(name);
  153. }
  154. }
  155. });
  156. });
  157. return prefixedPackages;
  158. };