mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 04:03:05 +00:00
perf(plugins): minify client bundles while preserving interop and notices (#8076)
Reduce plugin client payloads with whitespace and syntax minification after eager host-export interop. Preserve identifiers, lowered async behavior and dependency license notices; leave server bundles readable. Verify worker-produced code through registration, byte-reduction, immutable-export and retained-notice regressions. Carries forward #7625 on an upstream branch. Pin Playwright-installing CI jobs to Ubuntu 24.04 after the ubuntu-latest rollout selected an unsupported browser-install platform. Follow-up payload review motivated by Matt Cowger's plugin-loading discussion: https://discord.com/channels/1481169421832814616/1563217536269680744 Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com>
This commit is contained in:
co-authored by
luke
RoundRedDot
parent
2c23cd4927
commit
abc23d7721
+11
-10
@@ -133,11 +133,12 @@ jobs:
|
||||
if: ${{ !cancelled() && steps.install.outcome == 'success' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.sdk != 'false') }}
|
||||
run: npm run typecheck:examples --workspace=@getpaseo/client
|
||||
|
||||
# The locked Playwright installers do not support ubuntu-latest's Ubuntu 26.04.
|
||||
typecheck:
|
||||
name: typecheck
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.quality != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -395,7 +396,7 @@ jobs:
|
||||
name: app-tests
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.app != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -429,7 +430,7 @@ jobs:
|
||||
name: playwright (shard 1/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "1/8"
|
||||
PLAYWRIGHT_ARTIFACT: "1"
|
||||
@@ -485,7 +486,7 @@ jobs:
|
||||
name: playwright (shard 2/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "2/8"
|
||||
PLAYWRIGHT_ARTIFACT: "2"
|
||||
@@ -495,7 +496,7 @@ jobs:
|
||||
name: playwright (shard 3/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "3/8"
|
||||
PLAYWRIGHT_ARTIFACT: "3"
|
||||
@@ -505,7 +506,7 @@ jobs:
|
||||
name: playwright (shard 4/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "4/8"
|
||||
PLAYWRIGHT_ARTIFACT: "4"
|
||||
@@ -515,7 +516,7 @@ jobs:
|
||||
name: playwright (shard 5/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "5/8"
|
||||
PLAYWRIGHT_ARTIFACT: "5"
|
||||
@@ -525,7 +526,7 @@ jobs:
|
||||
name: playwright (shard 6/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "6/8"
|
||||
PLAYWRIGHT_ARTIFACT: "6"
|
||||
@@ -535,7 +536,7 @@ jobs:
|
||||
name: playwright (shard 7/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "7/8"
|
||||
PLAYWRIGHT_ARTIFACT: "7"
|
||||
@@ -545,7 +546,7 @@ jobs:
|
||||
name: playwright (shard 8/8)
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PLAYWRIGHT_SHARD: "8/8"
|
||||
PLAYWRIGHT_ARTIFACT: "8"
|
||||
|
||||
@@ -257,6 +257,22 @@ function makeHermesInteropEager(code: string): string {
|
||||
return code.replaceAll("get: () => from[key]", "value: from[key]");
|
||||
}
|
||||
|
||||
async function minifyClientBundle(code: string): Promise<string> {
|
||||
const { transform } = loadEsbuild();
|
||||
// Minify after the eager replace. Whitespace minify rewrites the getter this
|
||||
// replace matches, and identifier minify can delete the helper entirely.
|
||||
const result = await transform(code, {
|
||||
minifyWhitespace: true,
|
||||
minifySyntax: true,
|
||||
minifyIdentifiers: false,
|
||||
target: "es2020",
|
||||
supported: { "async-await": false },
|
||||
charset: "ascii",
|
||||
legalComments: "inline",
|
||||
});
|
||||
return result.code;
|
||||
}
|
||||
|
||||
function runtimeSpecifierError(
|
||||
specifier: string,
|
||||
target: PluginBuildTarget | "shared",
|
||||
@@ -368,7 +384,10 @@ async function compileTarget(entryPath: string, target: PluginBuildTarget): Prom
|
||||
checkSharedDependencies(result.metafile.inputs, pluginDirectory);
|
||||
const output = result.outputFiles[0]?.text;
|
||||
if (!output) throw new Error(`Plugin ${target} compilation produced no output`);
|
||||
return wrapCommonJsBundle(makeHermesInteropEager(output));
|
||||
const eager = makeHermesInteropEager(output);
|
||||
let prepared = eager;
|
||||
if (target === "client") prepared = await minifyClientBundle(eager);
|
||||
return wrapCommonJsBundle(prepared);
|
||||
}
|
||||
|
||||
export async function compilePlugin(entryPaths: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import * as zod from "zod";
|
||||
import { ZodError } from "zod";
|
||||
import {
|
||||
compilePlugin,
|
||||
@@ -569,6 +570,101 @@ export default function contribute(client) {
|
||||
expect(clientBundle).toContain("__async");
|
||||
});
|
||||
|
||||
it("reduces client bytes while preserving registration and leaving server output readable", async () => {
|
||||
const entries = await createSplitPlugin();
|
||||
const source = `import { z } from "zod";
|
||||
const commandSchema = z.object({ id: z.string(), title: z.string() });
|
||||
export default function contribute(client) {
|
||||
const command = commandSchema.parse({ id: "open", title: "Open – plugin" });
|
||||
client.addCommandCenterItem({
|
||||
...command,
|
||||
icon: "Scan",
|
||||
context: "global",
|
||||
onSelect() { return Promise.resolve(command.title); },
|
||||
});
|
||||
return () => undefined;
|
||||
}`;
|
||||
await Promise.all([writeFile(entries.client, source), writeFile(entries.server, source)]);
|
||||
const { clientBundle, serverBundle } = await compilePlugin(entries);
|
||||
if (clientBundle === null || serverBundle === null) {
|
||||
throw new Error("compilePlugin returned no bundle");
|
||||
}
|
||||
|
||||
for (const bundle of [clientBundle, serverBundle]) {
|
||||
const registered: { id: string; title: string; onSelect: () => Promise<string> }[] = [];
|
||||
const exports = new Function(`return ${bundle}`)()((specifier: string) => {
|
||||
expect(specifier).toBe("zod");
|
||||
return zod;
|
||||
});
|
||||
const cleanup = exports.default({
|
||||
addCommandCenterItem: (command: (typeof registered)[number]) => registered.push(command),
|
||||
});
|
||||
expect(registered).toMatchObject([{ id: "open", title: "Open – plugin" }]);
|
||||
await expect(registered[0]!.onSelect()).resolves.toBe("Open – plugin");
|
||||
expect(cleanup()).toBeUndefined();
|
||||
}
|
||||
expect(Buffer.byteLength(clientBundle)).toBeLessThan(Buffer.byteLength(serverBundle) * 0.9);
|
||||
expect(clientBundle.split("").every((char) => char.charCodeAt(0) <= 0x7f)).toBe(true);
|
||||
expect(serverBundle).toContain('title: "Open');
|
||||
});
|
||||
|
||||
it("retains dependency license notices while minifying executable client code", async () => {
|
||||
const entries = await createSplitPlugin();
|
||||
const dependency = path.join(entries.directory, "node_modules/licensed-dependency");
|
||||
const notice = "Copyright Example Authors. SPDX-License-Identifier: MIT";
|
||||
await mkdir(dependency, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(dependency, "package.json"),
|
||||
JSON.stringify({ name: "licensed-dependency", main: "index.js" }),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(dependency, "index.js"),
|
||||
`/*! ${notice} */
|
||||
exports.title = function title(input) {
|
||||
const trimmed = input.trim();
|
||||
return "Licensed " + trimmed;
|
||||
};`,
|
||||
);
|
||||
const source = `import { title } from "licensed-dependency";
|
||||
export default function contribute() { return title(" plugin "); }`;
|
||||
await Promise.all([writeFile(entries.client, source), writeFile(entries.server, source)]);
|
||||
const { clientBundle, serverBundle } = await compilePlugin(entries);
|
||||
if (clientBundle === null || serverBundle === null) {
|
||||
throw new Error("compilePlugin returned no bundle");
|
||||
}
|
||||
|
||||
for (const bundle of [clientBundle, serverBundle]) {
|
||||
expect(bundle).toContain(notice);
|
||||
const exports = new Function(`return ${bundle}`)()(() => {
|
||||
throw new Error("The dependency must be bundled");
|
||||
});
|
||||
expect(exports.default()).toBe("Licensed plugin");
|
||||
}
|
||||
expect(Buffer.byteLength(clientBundle)).toBeLessThan(Buffer.byteLength(serverBundle) * 0.9);
|
||||
});
|
||||
|
||||
it("keeps distinct host exports eager when minifying in the worker", async () => {
|
||||
const entries = await createSplitPlugin();
|
||||
await writeFile(
|
||||
entries.client,
|
||||
`import * as host from "zod";
|
||||
export default function contribute() { return [host.first, host.second]; }`,
|
||||
);
|
||||
const { clientBundle } = await compilePlugin({ client: entries.client, server: null });
|
||||
const host = { first: "first export", second: "second export" };
|
||||
const factory = new Function(`return ${clientBundle}`)() as (
|
||||
require: (specifier: string) => typeof host,
|
||||
) => { default: () => unknown };
|
||||
function requireHost(specifier: string): typeof host {
|
||||
expect(specifier).toBe("zod");
|
||||
return host;
|
||||
}
|
||||
const exports = factory(requireHost);
|
||||
host.first = "changed first export";
|
||||
host.second = "changed second export";
|
||||
expect(exports.default()).toEqual(["first export", "second export"]);
|
||||
});
|
||||
|
||||
it("rejects node imports from the client entry", async () => {
|
||||
const entries = await createSplitPlugin();
|
||||
await writeFile(
|
||||
|
||||
Reference in New Issue
Block a user