mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Split up Wasm binaries so Cloudflare deployments stay below 25 MB per file
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -112,6 +112,8 @@ jobs:
|
|||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
# Split the Wasm binary to fit Cloudflare Pages' file size limit (see `wasmSplitting` in `frontend/vite.config.ts`)
|
||||||
|
SPLIT_WASM: "1"
|
||||||
run: mold -run cargo run build web${{ inputs.debug && ' debug' || '' }}
|
run: mold -run cargo run build web${{ inputs.debug && ' debug' || '' }}
|
||||||
|
|
||||||
- name: 📤 Publish to Cloudflare Pages
|
- name: 📤 Publish to Cloudflare Pages
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
import type { MessageName, SubscriptionsRouter } from "/src/subscriptions-router";
|
import type { MessageName, SubscriptionsRouter } from "/src/subscriptions-router";
|
||||||
import { loadDemoArtwork } from "/src/utility-functions/network";
|
import { loadDemoArtwork } from "/src/utility-functions/network";
|
||||||
import { operatingSystem } from "/src/utility-functions/platform";
|
import { operatingSystem } from "/src/utility-functions/platform";
|
||||||
import init, { EditorWrapper, receiveNativeMessage } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import { initWasm } from "/src/utility-functions/wasm-loader";
|
||||||
|
import { EditorWrapper, receiveNativeMessage } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
import type { FrontendMessage } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FrontendMessage } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
let subscriptions: SubscriptionsRouter | undefined = undefined;
|
let subscriptions: SubscriptionsRouter | undefined = undefined;
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Initialize the editor wrapper
|
// Initialize the editor wrapper
|
||||||
const wrapper = await init();
|
const wrapper = await initWasm();
|
||||||
for (const [name, f] of Object.entries(wrapper)) {
|
for (const [name, f] of Object.entries(wrapper)) {
|
||||||
if (name.startsWith("__node_registry")) f();
|
if (name.startsWith("__node_registry")) f();
|
||||||
}
|
}
|
||||||
|
|||||||
3
frontend/src/global.d.ts
vendored
3
frontend/src/global.d.ts
vendored
@@ -6,6 +6,9 @@ interface Window {
|
|||||||
receiveNativeMessage?: (buffer: ArrayBuffer) => void;
|
receiveNativeMessage?: (buffer: ArrayBuffer) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build-time constant injected by the `wasmSplitting` plugin in `vite.config.ts`
|
||||||
|
declare const __WASM_PART_COUNT__: number;
|
||||||
|
|
||||||
// Graphite's custom "pointerlockmove" event dispatched by input.ts for pointer lock in the CEF desktop app
|
// Graphite's custom "pointerlockmove" event dispatched by input.ts for pointer lock in the CEF desktop app
|
||||||
interface WindowEventMap {
|
interface WindowEventMap {
|
||||||
pointerlockmove: CustomEvent<{ x: number; y: number }>;
|
pointerlockmove: CustomEvent<{ x: number; y: number }>;
|
||||||
|
|||||||
25
frontend/src/utility-functions/wasm-loader.ts
Normal file
25
frontend/src/utility-functions/wasm-loader.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import init from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
import wasmBinaryUrl from "/wrapper/pkg/graphite_wasm_wrapper_bg.wasm?url";
|
||||||
|
|
||||||
|
// Initializes the editor's Wasm module, rejoining the parts that CI deployments split the binary into
|
||||||
|
// to fit under the single-file size limit (see `wasmSplitting` in `vite.config.ts`)
|
||||||
|
export async function initWasm() {
|
||||||
|
// Local and native builds keep the binary whole, letting the wasm-bindgen glue code load it directly
|
||||||
|
if (__WASM_PART_COUNT__ <= 1) return init();
|
||||||
|
|
||||||
|
// Fetch all parts in parallel (served from the service worker's precache once it is installed)
|
||||||
|
const partRequests = [];
|
||||||
|
for (let index = 0; index < __WASM_PART_COUNT__; index += 1) {
|
||||||
|
partRequests.push(fetch(wasmBinaryUrl.replace(/\.wasm$/, `-part${index}.wasm`)));
|
||||||
|
}
|
||||||
|
const partResponses = await Promise.all(partRequests);
|
||||||
|
|
||||||
|
const failedResponse = partResponses.find((response) => !response.ok);
|
||||||
|
if (failedResponse) throw new Error(`Failed to fetch Wasm binary part (status ${failedResponse.status}): ${failedResponse.url}`);
|
||||||
|
|
||||||
|
// Rejoin the parts and hand them to wasm-bindgen as a single response, with the MIME type needed for streaming compilation
|
||||||
|
const parts = await Promise.all(partResponses.map((response) => response.blob()));
|
||||||
|
const joined = new Response(new Blob(parts), { headers: { "Content-Type": "application/wasm" } });
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
return init({ module_or_path: joined });
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { copyFileSync, cpSync, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
import { copyFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
@@ -11,7 +11,17 @@ const projectRootDir = path.resolve(__dirname);
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
return {
|
return {
|
||||||
plugins: [svelteGlobalStyles(), webkitUserSelectPrefix(), svelte(), staticAssets(), mode !== "native" && thirdPartyLicenses(), mode !== "native" && serviceWorker()],
|
plugins: [
|
||||||
|
svelteGlobalStyles(),
|
||||||
|
webkitUserSelectPrefix(),
|
||||||
|
svelte(),
|
||||||
|
staticAssets(),
|
||||||
|
mode !== "native" && thirdPartyLicenses(),
|
||||||
|
mode !== "native" && wasmSplitting(),
|
||||||
|
mode !== "native" && serviceWorker(),
|
||||||
|
],
|
||||||
|
// Default for builds that exclude the `wasmSplitting` plugin, which overrides this when active
|
||||||
|
define: { __WASM_PART_COUNT__: "1" },
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [{ find: /\/..\/branding\/(.*\.svg)/, replacement: path.resolve(projectRootDir, "../branding", "$1?raw") }],
|
alias: [{ find: /\/..\/branding\/(.*\.svg)/, replacement: path.resolve(projectRootDir, "../branding", "$1?raw") }],
|
||||||
},
|
},
|
||||||
@@ -136,6 +146,48 @@ function thirdPartyLicenses(): PluginOption {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Splits the Wasm binary into parts small enough for Cloudflare Pages' 25 MiB single-file limit, rejoined at runtime by `initWasm()` in `src/utility-functions/wasm-loader.ts`.
|
||||||
|
// Only active when the `SPLIT_WASM` environment variable is set, which CI does for deployments; local builds keep the single file.
|
||||||
|
function wasmSplitting(): PluginOption {
|
||||||
|
const PART_SIZE = 24 * 1024 * 1024;
|
||||||
|
|
||||||
|
let partCount = 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "wasm-splitting",
|
||||||
|
config(_, { command }) {
|
||||||
|
// Measure the Wasm binary (already built by `cargo run build web` before Vite runs) to decide how many parts are needed
|
||||||
|
if (command === "build" && process.env.SPLIT_WASM) {
|
||||||
|
const wasmPath = path.resolve(projectRootDir, "wrapper/pkg/graphite_wasm_wrapper_bg.wasm");
|
||||||
|
if (!existsSync(wasmPath)) throw new Error(`SPLIT_WASM is set but the Wasm binary is missing at ${wasmPath}`);
|
||||||
|
partCount = Math.ceil(statSync(wasmPath).size / PART_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bake the part count into the bundle so `initWasm()` knows how many parts to fetch and rejoin
|
||||||
|
return { define: { __WASM_PART_COUNT__: String(partCount) } };
|
||||||
|
},
|
||||||
|
// Synchronous so it completes before the `serviceWorker` plugin's `writeBundle` collects the precache manifest
|
||||||
|
writeBundle(options) {
|
||||||
|
if (partCount <= 1) return;
|
||||||
|
|
||||||
|
const assetsDir = path.join(options.dir || "dist", "assets");
|
||||||
|
const wasmFileName = readdirSync(assetsDir).find((name) => name.startsWith("graphite_wasm_wrapper_bg-") && name.endsWith(".wasm"));
|
||||||
|
if (!wasmFileName) throw new Error("Could not find the emitted Wasm asset to split");
|
||||||
|
const wasmPath = path.join(assetsDir, wasmFileName);
|
||||||
|
|
||||||
|
const contents = readFileSync(wasmPath);
|
||||||
|
if (Math.ceil(contents.length / PART_SIZE) !== partCount) throw new Error("Wasm binary size changed during the build, invalidating the baked-in part count");
|
||||||
|
|
||||||
|
// Replace the single Wasm file with its parts so only they get deployed and precached
|
||||||
|
for (let index = 0; index < partCount; index += 1) {
|
||||||
|
const partName = wasmFileName.replace(/\.wasm$/, `-part${index}.wasm`);
|
||||||
|
writeFileSync(path.join(assetsDir, partName), contents.subarray(index * PART_SIZE, (index + 1) * PART_SIZE));
|
||||||
|
}
|
||||||
|
rmSync(wasmPath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function serviceWorker(): PluginOption {
|
function serviceWorker(): PluginOption {
|
||||||
// Files that should never be precached
|
// Files that should never be precached
|
||||||
const EXCLUDED_FILES = new Set(["service-worker.js"]);
|
const EXCLUDED_FILES = new Set(["service-worker.js"]);
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ pub fn build_wasm_steps(release: bool, native: bool) -> Vec<Expression> {
|
|||||||
|
|
||||||
if release {
|
if release {
|
||||||
let wasm_file = pkg_dir.join(format!("{OUT_NAME}_bg.wasm"));
|
let wasm_file = pkg_dir.join(format!("{OUT_NAME}_bg.wasm"));
|
||||||
// `-Oz` (size over speed) keeps us under Cloudflare Pages' 25 MiB single-file cap.
|
// `-O3` favors runtime speed over binary size and `-g` preserves the name section,
|
||||||
// `-g` preserves the name section, which the panic hook reads at runtime to spot node-graph panics (see wrapper `lib.rs`).
|
// which the panic hook reads at runtime to spot node-graph panics (see wrapper `lib.rs`).
|
||||||
steps.push(cmd!("wasm-opt", "-Oz", "-g", &wasm_file, "-o", &wasm_file));
|
steps.push(cmd!("wasm-opt", "-O3", "-g", &wasm_file, "-o", &wasm_file));
|
||||||
}
|
}
|
||||||
|
|
||||||
steps
|
steps
|
||||||
|
|||||||
Reference in New Issue
Block a user