mirror of
https://github.com/vector-im/element-call.git
synced 2026-11-07 00:52:28 +00:00
A library build inlines an asset as a data URL the moment its import loses `no-inline`, and emits the standalone app's store worker the moment a module the component reaches imports it; neither warns, and the package grows by 11 MB without anyone seeing it in a diff. It had been published at 18 MB for several releases before it was measured. `scripts/check-component-bundle.mjs` now runs at the end of the component build and fails it on a data URL over 16 kB in a module a host bundles, an absolute `/assets/` reference, the store worker under `assets/`, or a main module over 4 MB, each with a message saying what to do. It passes only once both the assets and the store worker fixes are in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
93 lines
3.5 KiB
JavaScript
93 lines
3.5 KiB
JavaScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
/**
|
|
* Checks the component build's output for what a host must not be handed.
|
|
*
|
|
* A library build inlines every asset it meets as a data URL unless the import
|
|
* says otherwise, and does so silently: one import without `no-inline` and the
|
|
* module grows by the asset, encoded a third larger, that every host then
|
|
* downloads and parses before the first call. MediaPipe's runtime alone is
|
|
* 11 MB. A reference to a file by an absolute path is just as silent and
|
|
* breaks at runtime instead, since a host serves the module from wherever it
|
|
* likes. And the standalone app's client store worker, which carries a copy of
|
|
* the Matrix SDK and its crypto, has no business in a package whose host
|
|
* supplies the client: the bundler emits it on loading its import, used or
|
|
* not (see src/utils/initClient.ts). This fails the build on each of those.
|
|
*/
|
|
|
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
const dist = "component/dist";
|
|
|
|
/** The longest data URL a chunk may carry: enough for an icon, not a sound. */
|
|
const inlineLimit = 16 * 1024;
|
|
|
|
/**
|
|
* The module itself, in bytes. Well above where it sits with its assets as
|
|
* files, so that only an asset finding its way back in trips it.
|
|
*/
|
|
const moduleBudget = 4 * 1024 * 1024;
|
|
|
|
/**
|
|
* The modules a host bundles: the entry points and their chunks, at the top of
|
|
* the package. What sits under `assets/` is fetched as it is, by URL.
|
|
*/
|
|
async function modules() {
|
|
const names = await readdir(dist);
|
|
return names.filter((name) => name.endsWith(".js")).map((n) => join(dist, n));
|
|
}
|
|
|
|
const problems = [];
|
|
|
|
for (const name of await readdir(join(dist, "assets")))
|
|
if (name.includes("IndexedDBWorker"))
|
|
problems.push(
|
|
`${dist}/assets/${name}: the standalone app's client store worker is ` +
|
|
`in the package. Something the component reaches imports the worker; ` +
|
|
`keep it on the standalone app's side (see src/utils/initClient.ts ` +
|
|
`and the \`loadClient\` prop of ClientProvider).`,
|
|
);
|
|
|
|
for (const file of await modules()) {
|
|
const code = await readFile(file, "utf8");
|
|
|
|
for (const [, type, payload] of code.matchAll(
|
|
/data:([a-z0-9/.+-]*);base64,([A-Za-z0-9+/=]*)/g,
|
|
))
|
|
if (payload.length > inlineLimit)
|
|
problems.push(
|
|
`${file}: a ${type} asset of ${Math.round(payload.length / 1024)} kB ` +
|
|
`is inlined as a data URL. Import it with \`?url&no-inline\` so ` +
|
|
`that it is emitted as a file.`,
|
|
);
|
|
|
|
for (const [reference] of code.matchAll(/["'`]\/assets\/[^"'`]+["'`]/g))
|
|
problems.push(
|
|
`${file}: ${reference} is an absolute path, which only resolves ` +
|
|
`against an origin that serves the package at its root. The build ` +
|
|
`is meant to render relative references; check \`base\` in ` +
|
|
`vite-component.config.ts.`,
|
|
);
|
|
}
|
|
|
|
const moduleSize = (await stat(join(dist, "element-call.js"))).size;
|
|
if (moduleSize > moduleBudget)
|
|
problems.push(
|
|
`${dist}/element-call.js is ${Math.round(moduleSize / 1024 / 1024)} MB, ` +
|
|
`over its ${moduleBudget / 1024 / 1024} MB budget. Something large ` +
|
|
`has been bundled into the module that a host would rather fetch when ` +
|
|
`it is needed.`,
|
|
);
|
|
|
|
if (problems.length > 0) {
|
|
console.error(`The component build is not fit to publish:\n`);
|
|
for (const problem of problems) console.error(` ${problem}\n`);
|
|
process.exit(1);
|
|
}
|