Skip to content

Instantly share code, notes, and snippets.

@robin-drexler
Last active March 10, 2026 21:07
Show Gist options
  • Select an option

  • Save robin-drexler/ce6916aba2ed0bcf55970764eb4d5e52 to your computer and use it in GitHub Desktop.

Select an option

Save robin-drexler/ce6916aba2ed0bcf55970764eb4d5e52 to your computer and use it in GitHub Desktop.
Shopify extension bundle size analyzer
#!/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);
});
{
"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"
}
}
}
}
{
"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