import { build } from 'esbuild'; import { readFile, rm } from 'node:fs/promises'; import { gzipSync } from 'node:zlib'; const LOGIN_ENTRY_GZIP_LIMIT = 10_000; const STUDENT_ENTRY_GZIP_LIMIT = 20_000; const TEACHER_ENTRY_GZIP_LIMIT = 20_000; const SETTINGS_ENTRY_GZIP_LIMIT = 20_000; const ADMIN_ENTRY_GZIP_LIMIT = 20_000; // Лимит общих chunk'ов поднят с 75 000 до 85 000 на этапе 5 (волна 1): // компоненты дизайн-системы (ConfirmDialog, AsyncCombobox) стали общими // chunk'ами — их теперь используют и React-вкладки админ-панели, поэтому // они переехали из entrypoint'ов студента/настроек в общий кэшируемый пул. const SHARED_CHUNKS_GZIP_LIMIT = 85_000; // Лимит холодной загрузки входа поднят с 80 000 до 85 000 на этапе 5 (волна 1): // общие компоненты дизайн-системы (ConfirmDialog, AsyncCombobox) стали общими // chunk'ами — их теперь используют и React-вкладки админ-панели. Сам вход // login не изменился, вырос только объём общих кэшируемых chunk'ов. const COLD_LOAD_GZIP_LIMIT = 85_000; const STUDENT_COLD_LOAD_GZIP_LIMIT = 90_000; const TEACHER_COLD_LOAD_GZIP_LIMIT = 90_000; const SETTINGS_COLD_LOAD_GZIP_LIMIT = 90_000; const ADMIN_COLD_LOAD_GZIP_LIMIT = 90_000; // Lazy-chunkи вкладок админ-панели: каждая вкладка загружается // отдельным chunk'ом при открытии, поэтому бюджет отдельный от // холодной загрузки оболочки. const ADMIN_TAB_CHUNKS_GZIP_LIMIT = 150_000; await rm('dist/react', { force: true, recursive: true }); const result = await build({ entryPoints: { login: 'react/login/main.jsx', student: 'react/student/main.jsx', teacher: 'react/teacher/main.jsx', settings: 'react/settings/main.jsx', admin: 'react/admin/main.jsx', 'shared/runtime': 'react/shared/runtime-entry.jsx' }, bundle: true, chunkNames: 'shared/chunks/[name]-[hash]', define: { 'process.env.NODE_ENV': '"production"' }, format: 'esm', jsx: 'automatic', legalComments: 'none', metafile: true, minify: true, outdir: 'dist/react', platform: 'browser', splitting: true, target: ['es2022'] }); const outputs = result.metafile.outputs; function outputBytes(path) { return outputs[path]?.bytes ?? 0; } async function gzipSize(path) { return gzipSync(await readFile(path)).byteLength; } const sharedChunkPaths = Object.keys(outputs) .filter(path => path.startsWith('dist/react/shared/chunks/') && path.endsWith('.js')); // Chunkи, достижимые только из entrypoint'а админ-панели (lazy-загрузчики // React-вкладок react/admin/**), — в общий runtime-бюджет и холодную загрузку // других страниц они не входят. Классифицируем по достижимости от entrypoint'ов, // потому что модуль react/shared, используемый только одной вкладкой, попадает // в её chunk и не является общим. const ENTRY_OUTPUT_PATHS = Object.freeze({ login: 'dist/react/login.js', student: 'dist/react/student.js', teacher: 'dist/react/teacher.js', settings: 'dist/react/settings.js', admin: 'dist/react/admin.js', runtime: 'dist/react/shared/runtime.js' }); function chunkReachability() { const reach = new Map(); for (const [entryName, entryPath] of Object.entries(ENTRY_OUTPUT_PATHS)) { const stack = [entryPath]; const seen = new Set(); while (stack.length > 0) { const path = stack.pop(); if (seen.has(path) || !outputs[path]) continue; seen.add(path); for (const imported of outputs[path].imports || []) { stack.push(imported.path); } } for (const path of seen) { if (!reach.has(path)) reach.set(path, new Set()); reach.get(path).add(entryName); } } return reach; } const reach = chunkReachability(); const adminLazyChunkPaths = sharedChunkPaths.filter(path => { const entries = reach.get(path); return entries !== undefined && entries.size > 0 && [...entries].every(name => name === 'admin'); }); const runtimeChunkPaths = sharedChunkPaths.filter(path => !adminLazyChunkPaths.includes(path)); const sharedChunkBytes = runtimeChunkPaths.reduce((total, path) => total + outputBytes(path), 0); const sharedChunkGzipBytes = ( await Promise.all(runtimeChunkPaths.map(gzipSize)) ).reduce((total, bytes) => total + bytes, 0); const adminLazyChunkBytes = adminLazyChunkPaths.reduce((total, path) => total + outputBytes(path), 0); const adminLazyChunkGzipBytes = ( await Promise.all(adminLazyChunkPaths.map(gzipSize)) ).reduce((total, bytes) => total + bytes, 0); const entryReports = []; for (const [name, path, limit, coldLimit] of [ ['входа', 'dist/react/login.js', LOGIN_ENTRY_GZIP_LIMIT, COLD_LOAD_GZIP_LIMIT], ['студента', 'dist/react/student.js', STUDENT_ENTRY_GZIP_LIMIT, STUDENT_COLD_LOAD_GZIP_LIMIT], ['преподавателя', 'dist/react/teacher.js', TEACHER_ENTRY_GZIP_LIMIT, TEACHER_COLD_LOAD_GZIP_LIMIT], ['настроек', 'dist/react/settings.js', SETTINGS_ENTRY_GZIP_LIMIT, SETTINGS_COLD_LOAD_GZIP_LIMIT], ['админ-панели', 'dist/react/admin.js', ADMIN_ENTRY_GZIP_LIMIT, ADMIN_COLD_LOAD_GZIP_LIMIT] ]) { const rawBytes = outputBytes(path); const gzipBytes = await gzipSize(path); const coldLoadGzipBytes = gzipBytes + sharedChunkGzipBytes; entryReports.push({ name, path, rawBytes, gzipBytes, coldLoadGzipBytes, limit, coldLimit }); if (gzipBytes > limit) { throw new Error( `React entrypoint ${name} превышает бюджет: ${gzipBytes} байт gzip при лимите ${limit}` ); } if (coldLoadGzipBytes > coldLimit) { throw new Error( `Холодная загрузка ${name} превышает бюджет: ${coldLoadGzipBytes} байт gzip при лимите ${coldLimit}` ); } } if (sharedChunkGzipBytes > SHARED_CHUNKS_GZIP_LIMIT) { throw new Error( `Общие React-chunk превышают бюджет: ${sharedChunkGzipBytes} байт gzip при лимите ${SHARED_CHUNKS_GZIP_LIMIT}` ); } if (adminLazyChunkGzipBytes > ADMIN_TAB_CHUNKS_GZIP_LIMIT) { throw new Error( `Lazy-chunkи вкладок админки превышают бюджет: ${adminLazyChunkGzipBytes} байт gzip` + ` при лимите ${ADMIN_TAB_CHUNKS_GZIP_LIMIT}` ); } if (runtimeChunkPaths.length === 0) { throw new Error('Сборка не создала общий кэшируемый React-chunk'); } for (const report of entryReports) { console.log( `React entrypoint ${report.name}: ${report.rawBytes} байт, ${report.gzipBytes} байт gzip, ` + `холодная загрузка ${report.coldLoadGzipBytes} байт gzip` ); } console.log(`Общие React-chunk: ${sharedChunkBytes} байт, ${sharedChunkGzipBytes} байт gzip`); console.log( `Lazy-chunkи вкладок админки: ${adminLazyChunkPaths.length} chunk, ` + `${adminLazyChunkBytes} байт, ${adminLazyChunkGzipBytes} байт gzip` );