diff --git a/package-lock.json b/package-lock.json index daad2ba0e0..36a86b59cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,12 @@ "@netlify/zip-it-and-ship-it": "^14.1.8", "@opentelemetry/api": "^1.8.0", "@playwright/test": "^1.43.1", + "@types/adm-zip": "^0.5.7", "@types/node": "^20.12.7", "@types/picomatch": "^3.0.0", "@types/uuid": "^10.0.0", "@vercel/nft": "^0.30.0", + "adm-zip": "^0.5.16", "cheerio": "^1.0.0-rc.12", "clean-package": "^2.2.0", "esbuild": "^0.25.0", @@ -47,6 +49,7 @@ "path-to-regexp": "^6.2.1", "picomatch": "^4.0.2", "prettier": "^3.2.5", + "pretty-bytes": "^7.1.0", "semver": "^7.6.0", "typescript": "^5.4.5", "unionfs": "^4.5.4", @@ -5812,6 +5815,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -6605,6 +6618,16 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -29008,6 +29031,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-ms": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", @@ -36562,6 +36598,15 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -37144,6 +37189,12 @@ "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true }, + "adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true + }, "agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -37185,8 +37236,7 @@ "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { @@ -38620,8 +38670,7 @@ } }, "dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "version": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", "dev": true, "requires": { @@ -52796,6 +52845,12 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, + "pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "dev": true + }, "pretty-ms": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", diff --git a/package.json b/package.json index 92d30153bc..4b5a3d78c2 100644 --- a/package.json +++ b/package.json @@ -57,18 +57,20 @@ "@netlify/build": "^35.1.7", "@netlify/config": "^24.0.4", "@netlify/edge-bundler": "^14.5.5", - "@netlify/edge-functions-bootstrap": "^2.14.0", "@netlify/edge-functions": "^2.17.1", + "@netlify/edge-functions-bootstrap": "^2.14.0", "@netlify/eslint-config-node": "^7.0.1", "@netlify/functions": "^4.2.7", "@netlify/serverless-functions-api": "^2.5.0", "@netlify/zip-it-and-ship-it": "^14.1.8", "@opentelemetry/api": "^1.8.0", "@playwright/test": "^1.43.1", + "@types/adm-zip": "^0.5.7", "@types/node": "^20.12.7", "@types/picomatch": "^3.0.0", "@types/uuid": "^10.0.0", "@vercel/nft": "^0.30.0", + "adm-zip": "^0.5.16", "cheerio": "^1.0.0-rc.12", "clean-package": "^2.2.0", "esbuild": "^0.25.0", @@ -90,6 +92,7 @@ "path-to-regexp": "^6.2.1", "picomatch": "^4.0.2", "prettier": "^3.2.5", + "pretty-bytes": "^7.1.0", "semver": "^7.6.0", "typescript": "^5.4.5", "unionfs": "^4.5.4", diff --git a/src/build/functions/utils.ts b/src/build/functions/utils.ts new file mode 100644 index 0000000000..dead4449b5 --- /dev/null +++ b/src/build/functions/utils.ts @@ -0,0 +1,44 @@ +import { stat } from 'node:fs/promises' +import { join as posixJoin, sep as posixSep } from 'node:path/posix' + +import { trace } from '@opentelemetry/api' +import { wrapTracer } from '@opentelemetry/api/experimental' +// eslint-disable-next-line import/no-extraneous-dependencies +import AdmZip from 'adm-zip' +// eslint-disable-next-line import/no-extraneous-dependencies +import prettyBytes from 'pretty-bytes' + +import { PluginContext, SERVER_HANDLER_NAME } from '../plugin-context.js' + +const tracer = wrapTracer(trace.getTracer('Next runtime')) + +/** Copies the runtime dist folder to the lambda */ +export const checkBundleSize = async (ctx: PluginContext) => { + const LAMBDA_MAX_SIZE = 1024 * 1024 * 250 // 250MB + const TOP_N_ENTRIES = 5 + const LEVELS = 3 + + await tracer.withActiveSpan('checkBundleSize', async () => { + const bundleFileName: string = posixJoin( + ctx.constants.FUNCTIONS_DIST, + `${SERVER_HANDLER_NAME}.zip`, + ) + const bundleSize = await stat(bundleFileName).then(({ size }) => size) + if (bundleSize < LAMBDA_MAX_SIZE) { + return + } + + const zip = new AdmZip(bundleFileName) + const bundleContentSizes: Record = {} + for (const entry of zip.getEntries()) { + const entryName = entry.entryName.split(posixSep).slice(0, LEVELS).join(posixSep) + bundleContentSizes[entryName] = (bundleContentSizes[entryName] || 0) + entry.header.size + } + + // eslint-disable-next-line id-length + const sortedBundleContentSizes = Object.entries(bundleContentSizes).sort((a, b) => b[1] - a[1]) + for (const [dir, size] of sortedBundleContentSizes.slice(0, TOP_N_ENTRIES)) { + console.log(`${prettyBytes(size)} \t${dir}`) + } + }) +} diff --git a/src/index.ts b/src/index.ts index 27d9c1ff7b..4ecb5e3d8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { } from './build/content/static.js' import { clearStaleEdgeHandlers, createEdgeHandlers } from './build/functions/edge.js' import { clearStaleServerHandlers, createServerHandler } from './build/functions/server.js' +import { checkBundleSize } from './build/functions/utils.js' import { setImageConfig } from './build/image-cdn.js' import { PluginContext } from './build/plugin-context.js' import { @@ -110,7 +111,10 @@ export const onPostBuild = async (options: NetlifyPluginOptions) => { } await tracer.withActiveSpan('onPostBuild', async () => { - await publishStaticDir(new PluginContext(options)) + const ctx = new PluginContext(options) + + await publishStaticDir(ctx) + await checkBundleSize(ctx) }) }