diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 7dc738f345..394eb7e8e3 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -1,4 +1,6 @@ import esbuild from 'esbuild'; +import path from 'path'; +import { explore } from 'source-map-explorer'; // List of all modules accepted in ModulesMap const moduleNames = [ @@ -35,8 +37,14 @@ function formatBytes(bytes: number) { return `${formatted} KiB`; } -// Gets the bundled size in bytes of an array of named exports from 'ably/modules' -function getImportSize(modules: string[]) { +interface BundleInfo { + byteSize: number; + code: Uint8Array; + sourceMap: Uint8Array; +} + +// Uses esbuild to create a bundle containing the named exports from 'ably/modules' +function getBundleInfo(modules: string[]): BundleInfo { const outfile = modules.join(''); const result = esbuild.buildSync({ stdin: { @@ -48,9 +56,36 @@ function getImportSize(modules: string[]) { bundle: true, outfile, write: false, + sourcemap: 'external', }); - return result.metafile.outputs[outfile].bytes; + const pathHasBase = (component: string) => { + return (outputFile: esbuild.OutputFile) => { + return path.parse(outputFile.path).base === component; + }; + }; + + const codeOutputFile = result.outputFiles.find(pathHasBase(outfile))!; + const sourceMapOutputFile = result.outputFiles.find(pathHasBase(`${outfile}.map`))!; + + return { + byteSize: result.metafile.outputs[outfile].bytes, + code: codeOutputFile.contents, + sourceMap: sourceMapOutputFile.contents, + }; +} + +// Gets the bundled size in bytes of an array of named exports from 'ably/modules' +function getImportSize(modules: string[]) { + const bundleInfo = getBundleInfo(modules); + return bundleInfo.byteSize; +} + +async function runSourceMapExplorer(bundleInfo: BundleInfo) { + return explore({ + code: Buffer.from(bundleInfo.code), + map: Buffer.from(bundleInfo.sourceMap), + }); } function printAndCheckModuleSizes() { @@ -112,11 +147,96 @@ function printAndCheckFunctionSizes() { return errors; } -(function run() { +// Performs a sense check that there are no unexpected files making a large contribution to the BaseRealtime bundle size. +async function checkBaseRealtimeFiles() { + const baseRealtimeBundleInfo = getBundleInfo(['BaseRealtime']); + const exploreResult = await runSourceMapExplorer(baseRealtimeBundleInfo); + + const files = exploreResult.bundles[0].files; + delete files['[sourceMappingURL]']; + delete files['[unmapped]']; + delete files['[EOLs]']; + + const thresholdBytes = 100; + const filesAboveThreshold = Object.entries(files).filter((file) => file[1].size >= thresholdBytes); + + // These are the files that are allowed to contribute >= `threshold` bytes to the BaseRealtime bundle. + // + // The threshold is chosen pretty arbitrarily. There are some files (e.g. presencemessage.ts) whose bulk should not be included in the BaseRealtime bundle, but which make a small contribution to the bundle (probably because we make use of one exported constant or something; I haven’t looked into it). + const allowedFiles = new Set([ + 'src/common/constants/HttpStatusCodes.ts', + 'src/common/constants/TransportName.ts', + 'src/common/constants/XHRStates.ts', + 'src/common/lib/client/auth.ts', + 'src/common/lib/client/baseclient.ts', + 'src/common/lib/client/baserealtime.ts', + 'src/common/lib/client/channelstatechange.ts', + 'src/common/lib/client/connection.ts', + 'src/common/lib/client/connectionstatechange.ts', + 'src/common/lib/client/realtimechannel.ts', + 'src/common/lib/transport/connectionerrors.ts', + 'src/common/lib/transport/connectionmanager.ts', + 'src/common/lib/transport/messagequeue.ts', + 'src/common/lib/transport/protocol.ts', + 'src/common/lib/transport/transport.ts', + 'src/common/lib/types/devicedetails.ts', + 'src/common/lib/types/errorinfo.ts', + 'src/common/lib/types/message.ts', + 'src/common/lib/types/protocolmessage.ts', + 'src/common/lib/types/pushchannelsubscription.ts', // TODO why? https://github.com/ably/ably-js/issues/1506 + 'src/common/lib/util/defaults.ts', + 'src/common/lib/util/eventemitter.ts', + 'src/common/lib/util/logger.ts', + 'src/common/lib/util/multicaster.ts', + 'src/common/lib/util/utils.ts', + 'src/platform/web/config.ts', + 'src/platform/web/lib/http/http.ts', + 'src/platform/web/lib/util/bufferutils.ts', + 'src/platform/web/lib/util/defaults.ts', + 'src/platform/web/lib/util/hmac-sha256.ts', + 'src/platform/web/lib/util/webstorage.ts', + 'src/platform/web/modules.ts', + ]); + + const errors: Error[] = []; + + // Check that no files other than those allowed above make a large contribution to the bundle size + for (const file of filesAboveThreshold) { + if (!allowedFiles.has(file[0])) { + errors.push( + new Error( + `Unexpected file ${file[0]}, contributes ${file[1].size}B to BaseRealtime, more than allowed ${thresholdBytes}B` + ) + ); + } + } + + // Check that there are no stale entries in the allowed list + for (const allowedFile of Array.from(allowedFiles)) { + const file = files[allowedFile]; + + if (file) { + if (file.size < thresholdBytes) { + errors.push( + new Error( + `File ${allowedFile} contributes ${file.size}B, which is less than the expected minimum of ${thresholdBytes}B. Remove it from the \`allowedFiles\` list.` + ) + ); + } + } else { + errors.push(new Error(`File ${allowedFile} is referenced in \`allowedFiles\` but does not contribute to bundle`)); + } + } + + return errors; +} + +(async function run() { const errors: Error[] = []; errors.push(...printAndCheckModuleSizes()); errors.push(...printAndCheckFunctionSizes()); + errors.push(...(await checkBaseRealtimeFiles())); if (errors.length > 0) { for (const error of errors) {