Last active
March 10, 2026 21:07
-
-
Save robin-drexler/ce6916aba2ed0bcf55970764eb4d5e52 to your computer and use it in GitHub Desktop.
Shopify extension bundle size analyzer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const { execFileSync } = require("child_process"); | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const zlib = require("zlib"); | |
| const { AutoComplete } = require("enquirer"); | |
| const args = process.argv.slice(2); | |
| if (args.includes("--help") || args.includes("-h")) { | |
| console.log(` | |
| Usage: shopify-extension-size [extension] [options] | |
| Builds your Shopify app, then analyzes extension bundle sizes | |
| using esbuild metafiles. | |
| Arguments: | |
| extension Name of the extension to analyze, or "all". | |
| If omitted, shows an interactive picker. | |
| Options: | |
| --skip-build Skip the build step and analyze existing metafiles | |
| --html Generate an HTML treemap visualization per bundle | |
| (via esbuild-visualizer) | |
| -h, --help Show this help message | |
| Examples: | |
| npx shopify-extension-size # interactive picker | |
| npx shopify-extension-size checkout-ui # single extension | |
| npx shopify-extension-size all # all extensions | |
| npx shopify-extension-size all --html # all + HTML treemaps | |
| `); | |
| process.exit(0); | |
| } | |
| const html = args.includes("--html"); | |
| const skipBuild = args.includes("--skip-build"); | |
| const positional = args.filter((a) => !a.startsWith("--")); | |
| const extensionArg = positional[0]; | |
| function whichSync(bin) { | |
| try { | |
| return execFileSync("which", [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim(); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function shopifyBin() { | |
| const localBin = path.join(process.cwd(), "node_modules", ".bin", "shopify"); | |
| if (fs.existsSync(localBin)) return localBin; | |
| if (whichSync("shopify")) return "shopify"; | |
| console.error("shopify CLI not found. Please install @shopify/cli first."); | |
| process.exit(1); | |
| } | |
| async function main() { | |
| if (skipBuild) { | |
| console.log("\nSkipping build step\n"); | |
| } else { | |
| const bin = shopifyBin(); | |
| console.log(`\nBuilding with: ${bin} app build\n`); | |
| execFileSync(bin, ["app", "build"], { stdio: "inherit" }); | |
| } | |
| const extDir = path.join(process.cwd(), "extensions"); | |
| if (!fs.existsSync(extDir)) { | |
| console.error("No extensions/ directory found in CWD"); | |
| process.exit(1); | |
| } | |
| const extensions = fs | |
| .readdirSync(extDir, { withFileTypes: true }) | |
| .filter((d) => d.isDirectory()) | |
| .filter((d) => findFiles(path.join(extDir, d.name), ".metafile.json").length > 0) | |
| .map((d) => d.name); | |
| if (!extensions.length) { | |
| console.error("No UI extensions found (no metafiles in extensions/**)"); | |
| process.exit(1); | |
| } | |
| let selected; | |
| if (extensionArg === "all") { | |
| selected = extensions; | |
| } else if (extensionArg) { | |
| if (!extensions.includes(extensionArg)) { | |
| console.error( | |
| `Extension "${extensionArg}" not found.\nAvailable: ${extensions.join(", ")}` | |
| ); | |
| process.exit(1); | |
| } | |
| selected = [extensionArg]; | |
| } else { | |
| selected = await prompt(extensions); | |
| } | |
| for (const name of selected) { | |
| const dir = path.join(extDir, name); | |
| const metafiles = findFiles(dir, ".metafile.json"); | |
| if (!metafiles.length) { | |
| console.log(`\nNo metafiles found for "${name}"`); | |
| continue; | |
| } | |
| for (const mf of metafiles) { | |
| const meta = JSON.parse(fs.readFileSync(mf, "utf8")); | |
| const label = path.basename(mf); | |
| console.log(`\n${"═".repeat(60)}`); | |
| console.log(` ${name} — ${label}`); | |
| console.log("═".repeat(60)); | |
| for (const [outPath, info] of Object.entries(meta.outputs)) { | |
| if (outPath.endsWith(".map")) continue; | |
| printOutput(outPath, info); | |
| } | |
| if (html) { | |
| const htmlName = path.basename(mf).replace(".metafile.json", ".html"); | |
| const htmlPath = path.join(path.dirname(mf), htmlName); | |
| execFileSync("npx", ["esbuild-visualizer", "--metadata", mf, "--filename", htmlPath], { stdio: "inherit" }); | |
| console.log(`\n → ${htmlPath}`); | |
| } | |
| } | |
| } | |
| } | |
| function findFiles(dir, suffix) { | |
| const results = []; | |
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | |
| const full = path.join(dir, entry.name); | |
| if (entry.isDirectory()) results.push(...findFiles(full, suffix)); | |
| else if (entry.name.endsWith(suffix)) results.push(full); | |
| } | |
| return results; | |
| } | |
| function printOutput(outPath, info) { | |
| const total = info.bytes; | |
| let gzSize = null; | |
| const abs = path.resolve(outPath); | |
| if (fs.existsSync(abs)) { | |
| gzSize = zlib.gzipSync(fs.readFileSync(abs)).length; | |
| } | |
| console.log(`\n ${path.basename(outPath)}`); | |
| console.log( | |
| ` Size: ${fmt(total)}${gzSize != null ? ` │ Gzip: ${fmt(gzSize)}` : ""}` | |
| ); | |
| console.log(` ${"─".repeat(46)}`); | |
| const groups = {}; | |
| for (const [input, { bytesInOutput }] of Object.entries(info.inputs)) { | |
| const g = groupName(input); | |
| groups[g] = (groups[g] || 0) + bytesInOutput; | |
| } | |
| const sorted = Object.entries(groups).sort((a, b) => b[1] - a[1]); | |
| for (const [name, bytes] of sorted) { | |
| const pct = (bytes / total) * 100; | |
| const bar = "█".repeat(Math.max(1, Math.round(pct / 2.5))); | |
| console.log( | |
| ` ${fmt(bytes).padStart(10)} ${pct.toFixed(1).padStart(5)}% ${bar} ${name}` | |
| ); | |
| } | |
| } | |
| function groupName(p) { | |
| const i = p.lastIndexOf("node_modules/"); | |
| if (i === -1) return "<source>"; | |
| const after = p.slice(i + 13); | |
| if (after.startsWith("@")) { | |
| const [scope, pkg] = after.split("/"); | |
| return `${scope}/${pkg}`; | |
| } | |
| return after.split("/")[0]; | |
| } | |
| function fmt(bytes) { | |
| if (bytes < 1024) return `${bytes} B`; | |
| if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} kB`; | |
| return `${(bytes / 1024 ** 2).toFixed(1)} MB`; | |
| } | |
| async function prompt(extensions) { | |
| const answer = await new AutoComplete({ | |
| name: "extension", | |
| message: "Select extension (type to search)", | |
| choices: ["all", ...extensions], | |
| }).run(); | |
| return answer === "all" ? extensions : [answer]; | |
| } | |
| main().catch((e) => { | |
| console.error(e); | |
| process.exit(1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "shopify-extension-size", | |
| "version": "1.0.0", | |
| "lockfileVersion": 3, | |
| "requires": true, | |
| "packages": { | |
| "": { | |
| "name": "shopify-extension-size", | |
| "version": "1.0.0", | |
| "dependencies": { | |
| "enquirer": "^2.4.1", | |
| "esbuild-visualizer": "^0.6.0" | |
| }, | |
| "bin": { | |
| "shopify-extension-size": "index.js" | |
| } | |
| }, | |
| "node_modules/ansi-colors": { | |
| "version": "4.1.3", | |
| "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", | |
| "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=6" | |
| } | |
| }, | |
| "node_modules/ansi-regex": { | |
| "version": "5.0.1", | |
| "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", | |
| "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/ansi-styles": { | |
| "version": "4.3.0", | |
| "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", | |
| "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "color-convert": "^2.0.1" | |
| }, | |
| "engines": { | |
| "node": ">=8" | |
| }, | |
| "funding": { | |
| "url": "https://github.com/chalk/ansi-styles?sponsor=1" | |
| } | |
| }, | |
| "node_modules/cliui": { | |
| "version": "8.0.1", | |
| "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", | |
| "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", | |
| "license": "ISC", | |
| "dependencies": { | |
| "string-width": "^4.2.0", | |
| "strip-ansi": "^6.0.1", | |
| "wrap-ansi": "^7.0.0" | |
| }, | |
| "engines": { | |
| "node": ">=12" | |
| } | |
| }, | |
| "node_modules/color-convert": { | |
| "version": "2.0.1", | |
| "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", | |
| "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "color-name": "~1.1.4" | |
| }, | |
| "engines": { | |
| "node": ">=7.0.0" | |
| } | |
| }, | |
| "node_modules/color-name": { | |
| "version": "1.1.4", | |
| "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", | |
| "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", | |
| "license": "MIT" | |
| }, | |
| "node_modules/define-lazy-prop": { | |
| "version": "2.0.0", | |
| "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", | |
| "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/emoji-regex": { | |
| "version": "8.0.0", | |
| "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | |
| "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", | |
| "license": "MIT" | |
| }, | |
| "node_modules/enquirer": { | |
| "version": "2.4.1", | |
| "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", | |
| "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "ansi-colors": "^4.1.1", | |
| "strip-ansi": "^6.0.1" | |
| }, | |
| "engines": { | |
| "node": ">=8.6" | |
| } | |
| }, | |
| "node_modules/esbuild-visualizer": { | |
| "version": "0.6.0", | |
| "resolved": "https://registry.npmjs.org/esbuild-visualizer/-/esbuild-visualizer-0.6.0.tgz", | |
| "integrity": "sha512-oNK3JAhC7+re93VTtUdWJKTDVnA2qXPAjCAoaw9OxEFUXztszw3kcaK46u1U790T8FdUBAWv6F9Xt59P8nJCVA==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "open": "^8.4.0", | |
| "picomatch": "^2.3.1", | |
| "yargs": "^17.6.2" | |
| }, | |
| "bin": { | |
| "esbuild-visualizer": "dist/bin/cli.js" | |
| }, | |
| "engines": { | |
| "node": ">=18" | |
| } | |
| }, | |
| "node_modules/escalade": { | |
| "version": "3.2.0", | |
| "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", | |
| "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=6" | |
| } | |
| }, | |
| "node_modules/get-caller-file": { | |
| "version": "2.0.5", | |
| "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", | |
| "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", | |
| "license": "ISC", | |
| "engines": { | |
| "node": "6.* || 8.* || >= 10.*" | |
| } | |
| }, | |
| "node_modules/is-docker": { | |
| "version": "2.2.1", | |
| "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", | |
| "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", | |
| "license": "MIT", | |
| "bin": { | |
| "is-docker": "cli.js" | |
| }, | |
| "engines": { | |
| "node": ">=8" | |
| }, | |
| "funding": { | |
| "url": "https://github.com/sponsors/sindresorhus" | |
| } | |
| }, | |
| "node_modules/is-fullwidth-code-point": { | |
| "version": "3.0.0", | |
| "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", | |
| "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/is-wsl": { | |
| "version": "2.2.0", | |
| "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", | |
| "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "is-docker": "^2.0.0" | |
| }, | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/open": { | |
| "version": "8.4.2", | |
| "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", | |
| "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "define-lazy-prop": "^2.0.0", | |
| "is-docker": "^2.1.1", | |
| "is-wsl": "^2.2.0" | |
| }, | |
| "engines": { | |
| "node": ">=12" | |
| }, | |
| "funding": { | |
| "url": "https://github.com/sponsors/sindresorhus" | |
| } | |
| }, | |
| "node_modules/picomatch": { | |
| "version": "2.3.1", | |
| "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", | |
| "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=8.6" | |
| }, | |
| "funding": { | |
| "url": "https://github.com/sponsors/jonschlinkert" | |
| } | |
| }, | |
| "node_modules/require-directory": { | |
| "version": "2.1.1", | |
| "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | |
| "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", | |
| "license": "MIT", | |
| "engines": { | |
| "node": ">=0.10.0" | |
| } | |
| }, | |
| "node_modules/string-width": { | |
| "version": "4.2.3", | |
| "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", | |
| "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "emoji-regex": "^8.0.0", | |
| "is-fullwidth-code-point": "^3.0.0", | |
| "strip-ansi": "^6.0.1" | |
| }, | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/strip-ansi": { | |
| "version": "6.0.1", | |
| "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", | |
| "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "ansi-regex": "^5.0.1" | |
| }, | |
| "engines": { | |
| "node": ">=8" | |
| } | |
| }, | |
| "node_modules/wrap-ansi": { | |
| "version": "7.0.0", | |
| "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | |
| "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "ansi-styles": "^4.0.0", | |
| "string-width": "^4.1.0", | |
| "strip-ansi": "^6.0.0" | |
| }, | |
| "engines": { | |
| "node": ">=10" | |
| }, | |
| "funding": { | |
| "url": "https://github.com/chalk/wrap-ansi?sponsor=1" | |
| } | |
| }, | |
| "node_modules/y18n": { | |
| "version": "5.0.8", | |
| "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", | |
| "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", | |
| "license": "ISC", | |
| "engines": { | |
| "node": ">=10" | |
| } | |
| }, | |
| "node_modules/yargs": { | |
| "version": "17.7.2", | |
| "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", | |
| "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", | |
| "license": "MIT", | |
| "dependencies": { | |
| "cliui": "^8.0.1", | |
| "escalade": "^3.1.1", | |
| "get-caller-file": "^2.0.5", | |
| "require-directory": "^2.1.1", | |
| "string-width": "^4.2.3", | |
| "y18n": "^5.0.5", | |
| "yargs-parser": "^21.1.1" | |
| }, | |
| "engines": { | |
| "node": ">=12" | |
| } | |
| }, | |
| "node_modules/yargs-parser": { | |
| "version": "21.1.1", | |
| "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", | |
| "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", | |
| "license": "ISC", | |
| "engines": { | |
| "node": ">=12" | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "shopify-extension-size", | |
| "version": "1.0.0", | |
| "bin": "./index.js", | |
| "dependencies": { | |
| "esbuild-visualizer": "0.6.0", | |
| "enquirer": "2.4.1" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment