// Install the pinned Open Slide + Vite production closure from// tools/open-slide-runtime into src-tauri/presentation-runtime/ (a Tauri resource),// prune what the dev server never loads, prove the pruned tree still serves the// editor (scripts/check-presentation-runtime.mjs), and sign its Mach-O binaries// with the Developer ID in APPLE_SIGNING_IDENTITY (skipped without one).import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";import { dirname, join, relative } from "node:path";import { checkPresentationRuntime } from "./check-presentation-runtime.mjs";import { signMachOTree, signingIdentity } from "./lib/codesign.mjs";import { pruneNodeModules } from "./lib/runtime-prune.mjs";import { isMain, projectRoot, readJson, run, walkFiles } from "./lib/util.mjs"; export const removablePackageDirectories = [ // main and module both resolve into dist; Open Slide never imports this source tree. "emoji-picker-react/src",]; // Builds of webview-only packages that no import can resolve to: Node never// loads these packages, and Vite serves them to the webview with the import// and browser conditions.const removablePackageFiles = [ // react-router's exports map points only into dist/development. /^react-router\/dist\/production\//, // Base UI's CommonJS build, which only the require condition selects; its // ESM build is the .mjs beside each file. /^@base-ui\/(?:react|utils)\/.+\.js$/, // React DOM's server renderers for Node, Bun, and edge runtimes; in the // browser, react-dom/server and react-dom/static select the browser build. /^react-dom\/(?:cjs\/react-dom-server(?:-legacy)?|server|static)\.(?:bun|edge|node)\./,]; // Packages the runtime ships only a few files of, and none of their dependencies.const assetOnlyPackages = { // Open Slide's editor stylesheet imports `shadcn/tailwind.css`, which // Tailwind resolves through the package's exports; nothing loads the rest, // the shadcn CLI, whose closure (ts-morph, Babel, the MCP SDK, Express, ...) // was about a third of the installed tree. shadcn: ["package.json", "LICENSE.md", "dist/tailwind.css"],}; // Declared dependency edges the dev server never follows.const unusedDependencies = { // Vite 8 loads lightningcss only for `css.transformer: "lightningcss"` and // for build-time CSS minification; the dev server transforms CSS with // PostCSS. Tailwind pins an older lightningcss, so Vite's own copy is a // second native binary. Were Vite ever to load it, Node would resolve the // copy Tailwind loads instead. vite: ["lightningcss"],}; /** The directory of the package `name` that Node resolves from `directory`, or null. */function resolvePackage(runtimeRoot, directory, name) { for (let parent = directory; ; parent = dirname(parent)) { const candidate = join(parent, "node_modules", name); if (existsSync(join(candidate, "package.json"))) return candidate; if (parent === runtimeRoot) return null; }} /** * The package directories the runtime's dependencies reach, resolved as Node * resolves them. pnpm also installs packages that only an optional peer * dependency names: esbuild, the runtime's own test-time devDependency, * satisfies Vite's optional peer, but Vite 8 imports it only for * `build.minify: "esbuild"` and the deprecated `transformWithEsbuild`, never * to serve. An optional peer is therefore not an edge here. */function reachablePackages(runtimeRoot) { const reached = new Set(); const visit = (directory) => { if (reached.has(directory)) return; reached.add(directory); const manifest = readJson(join(directory, "package.json")); if (directory !== runtimeRoot && manifest.name in assetOnlyPackages) return; const unused = new Set(directory === runtimeRoot ? [] : unusedDependencies[manifest.name]); const peers = Object.keys(manifest.peerDependencies ?? {}) .filter((name) => !manifest.peerDependenciesMeta?.[name]?.optional); for (const name of [ ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.optionalDependencies ?? {}), ...peers, ]) { // Optional packages for another platform are not installed at all. const target = unused.has(name) ? null : resolvePackage(runtimeRoot, directory, name); if (target) visit(target); } }; visit(runtimeRoot); return reached;} /** The package directories directly inside `nodeModules`, scoped ones included. */function packageDirectories(nodeModules) { if (!existsSync(nodeModules)) return []; return readdirSync(nodeModules, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) .flatMap((entry) => { const path = join(nodeModules, entry.name); if (!entry.name.startsWith("@")) return [path]; return readdirSync(path, { withFileTypes: true }) .filter((scoped) => scoped.isDirectory()) .map((scoped) => join(path, scoped.name)); });} /** Delete everything under `directory` except the `kept` relative file paths. */function keepOnly(directory, kept) { for (const entry of readdirSync(directory, { withFileTypes: true })) { if (kept.includes(entry.name)) continue; const path = join(directory, entry.name); const prefix = `${entry.name}/`; const inside = kept.filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length)); if (entry.isDirectory() && inside.length > 0) keepOnly(path, inside); else rmSync(path, { recursive: true, force: true }); }} /** Remove the packages and package files the dev server can never load. */function pruneUnreachablePackages(nodeModules, reached) { // Command shims: the server imports these packages, it never runs their CLIs. rmSync(join(nodeModules, ".bin"), { recursive: true, force: true }); for (const path of packageDirectories(nodeModules)) { if (!reached.has(path)) { rmSync(path, { recursive: true, force: true }); continue; } const kept = assetOnlyPackages[readJson(join(path, "package.json")).name]; if (kept) keepOnly(path, kept); else pruneUnreachablePackages(join(path, "node_modules"), reached); }} export function prunePresentationRuntime(runtimeRoot) { const nodeModules = join(runtimeRoot, "node_modules"); pruneUnreachablePackages(nodeModules, reachablePackages(runtimeRoot)); for (const relativePath of removablePackageDirectories) { rmSync(join(nodeModules, relativePath), { recursive: true, force: true }); } // emoji-picker-react copies locale source data alongside compiled JS. // Open Slide uses the package entry, and locale subpaths resolve to JS; // keep every locale but omit its duplicate JSON/TS when the JS exists. const emojiData = join(nodeModules, "emoji-picker-react/dist/data"); if (existsSync(emojiData)) { for (const file of readdirSync(emojiData)) { if (/^emojis(?:-[\w-]+)?\.(?:json|ts)$/.test(file) && existsSync(join(emojiData, file.replace(/\.(?:json|ts)$/, ".js")))) { rmSync(join(emojiData, file)); } } } for (const path of walkFiles(nodeModules)) { if (removablePackageFiles.some((pattern) => pattern.test(relative(nodeModules, path)))) rmSync(path); } // Vite compiles TypeScript straight out of node_modules (Open Slide's own // app ships as src/app), so TypeScript sources stay here. pruneNodeModules(nodeModules, { platform: `${process.platform}-${process.arch}`, typeScriptSources: "keep" });} // Tauri's resource walker omits symbolic links. A normal pnpm layout therefore// ships the package store but drops node_modules/@open-slide/core, Vite, and// every other package entry that Node resolves at runtime. Hoisting makes the// package directories real; materialize the remaining executable links too so// the staged tree is exactly the tree copied into a release app.function materializeLinks(directory) { for (const entry of readdirSync(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isSymbolicLink()) { const sourcePath = realpathSync(path); const sourceIsDirectory = lstatSync(sourcePath).isDirectory(); rmSync(path, { recursive: true, force: true }); cpSync(sourcePath, path, { recursive: sourceIsDirectory, preserveTimestamps: true }); if (sourceIsDirectory) materializeLinks(path); } else if (entry.isDirectory()) { materializeLinks(path); } }} async function preparePresentationRuntime() { const source = join(projectRoot, "tools/open-slide-runtime"); const target = join(projectRoot, "src-tauri/presentation-runtime"); const stage = join(projectRoot, "node_modules/.cache/lattice/presentation-runtime"); rmSync(stage, { recursive: true, force: true }); mkdirSync(stage, { recursive: true }); const runtimeFiles = [ "package.json", "pnpm-lock.yaml", "server.mjs", "agent-chat.mjs", "agent-presence.mjs", "deck-error-overlay.mjs", "patch-contract.mjs", "lucide-open-slide.mjs", "locale-copy.mjs", "prebundled-dependencies.mjs", // pnpm applies the package.json's patchedDependencies during the install. "patches", ]; for (const file of runtimeFiles) { cpSync(join(source, file), join(stage, file), { recursive: true }); } run( "pnpm", ["install", "--prod", "--frozen-lockfile", "--ignore-scripts", "--config.node-linker=hoisted"], { cwd: stage }, ); materializeLinks(join(stage, "node_modules")); prunePresentationRuntime(stage); await checkPresentationRuntime(stage); const packageVersion = (name) => readJson(join(stage, "node_modules", name, "package.json")).version; writeFileSync( join(stage, "manifest.json"), JSON.stringify( { openSlideVersion: packageVersion("@open-slide/core"), // Set while the runtime is patched to an unreleased upstream commit. openSlideCommit: readJson(join(stage, "package.json")).openSlideCommit, viteVersion: packageVersion("vite"), }, null, 2, ), ); rmSync(target, { recursive: true, force: true }); renameSync(stage, target); writeFileSync( join(target, "placeholder.txt"), "Run `pnpm prepare:presentation` to stage the managed Open Slide runtime here.\n", ); if (!existsSync(join(target, "server.mjs"))) throw new Error("Presentation runtime staging failed"); if (!existsSync(join(target, "node_modules/@open-slide/core/package.json"))) { throw new Error("Presentation runtime package materialization failed"); } signPresentationRuntime(target);} function signPresentationRuntime(runtimeRoot) { const identity = signingIdentity(); if (!identity) { console.log("Skipping presentation runtime signing without APPLE_SIGNING_IDENTITY"); return; } if (process.platform !== "darwin") throw new Error("The presentation runtime can only be signed on macOS."); const signed = signMachOTree(runtimeRoot, { identity }); if (signed === 0) throw new Error(`No Mach-O binaries found in ${runtimeRoot}`); console.log(`Signed ${signed} presentation runtime binaries`);} if (isMain(import.meta.url)) await preparePresentationRuntime();
// Install the pinned Open Slide + Vite production closure from
// tools/open-slide-runtime into src-tauri/presentation-runtime/ (a Tauri resource),
// prune what the dev server never loads, prove the pruned tree still serves the
// editor (scripts/check-presentation-runtime.mjs), and sign its Mach-O binaries
// with the Developer ID in APPLE_SIGNING_IDENTITY (skipped without one).
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import { checkPresentationRuntime } from "./check-presentation-runtime.mjs";
import { signMachOTree, signingIdentity } from "./lib/codesign.mjs";
import { pruneNodeModules } from "./lib/runtime-prune.mjs";
import { isMain, projectRoot, readJson, run, walkFiles } from "./lib/util.mjs";
export const removablePackageDirectories = [
// main and module both resolve into dist; Open Slide never imports this source tree.
"emoji-picker-react/src",
];
// Builds of webview-only packages that no import can resolve to: Node never
// loads these packages, and Vite serves them to the webview with the import
// and browser conditions.
const removablePackageFiles = [
// react-router's exports map points only into dist/development.
/^react-router\/dist\/production\//,
// Base UI's CommonJS build, which only the require condition selects; its
// ESM build is the .mjs beside each file.
/^@base-ui\/(?:react|utils)\/.+\.js$/,
// React DOM's server renderers for Node, Bun, and edge runtimes; in the
// browser, react-dom/server and react-dom/static select the browser build.
/^react-dom\/(?:cjs\/react-dom-server(?:-legacy)?|server|static)\.(?:bun|edge|node)\./,
];
// Packages the runtime ships only a few files of, and none of their dependencies.
const assetOnlyPackages = {
// Open Slide's editor stylesheet imports `shadcn/tailwind.css`, which
// Tailwind resolves through the package's exports; nothing loads the rest,
// the shadcn CLI, whose closure (ts-morph, Babel, the MCP SDK, Express, ...)
// was about a third of the installed tree.
shadcn: ["package.json", "LICENSE.md", "dist/tailwind.css"],
};
// Declared dependency edges the dev server never follows.
const unusedDependencies = {
// Vite 8 loads lightningcss only for `css.transformer: "lightningcss"` and
// for build-time CSS minification; the dev server transforms CSS with
// PostCSS. Tailwind pins an older lightningcss, so Vite's own copy is a
// second native binary. Were Vite ever to load it, Node would resolve the
// copy Tailwind loads instead.
vite: ["lightningcss"],
};
/** The directory of the package `name` that Node resolves from `directory`, or null. */
function resolvePackage(runtimeRoot, directory, name) {
for (let parent = directory; ; parent = dirname(parent)) {
const candidate = join(parent, "node_modules", name);
if (existsSync(join(candidate, "package.json"))) return candidate;
if (parent === runtimeRoot) return null;
}
}
/**
* The package directories the runtime's dependencies reach, resolved as Node
* resolves them. pnpm also installs packages that only an optional peer
* dependency names: esbuild, the runtime's own test-time devDependency,
* satisfies Vite's optional peer, but Vite 8 imports it only for
* `build.minify: "esbuild"` and the deprecated `transformWithEsbuild`, never
* to serve. An optional peer is therefore not an edge here.
*/
function reachablePackages(runtimeRoot) {
const reached = new Set();
const visit = (directory) => {
if (reached.has(directory)) return;
reached.add(directory);
const manifest = readJson(join(directory, "package.json"));
if (directory !== runtimeRoot && manifest.name in assetOnlyPackages) return;
const unused = new Set(directory === runtimeRoot ? [] : unusedDependencies[manifest.name]);
const peers = Object.keys(manifest.peerDependencies ?? {})
.filter((name) => !manifest.peerDependenciesMeta?.[name]?.optional);
for (const name of [
...Object.keys(manifest.dependencies ?? {}),
...Object.keys(manifest.optionalDependencies ?? {}),
...peers,
]) {
// Optional packages for another platform are not installed at all.
const target = unused.has(name) ? null : resolvePackage(runtimeRoot, directory, name);
if (target) visit(target);
}
};
visit(runtimeRoot);
return reached;
}
/** The package directories directly inside `nodeModules`, scoped ones included. */
function packageDirectories(nodeModules) {
if (!existsSync(nodeModules)) return [];
return readdirSync(nodeModules, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
.flatMap((entry) => {
const path = join(nodeModules, entry.name);
if (!entry.name.startsWith("@")) return [path];
return readdirSync(path, { withFileTypes: true })
.filter((scoped) => scoped.isDirectory())
.map((scoped) => join(path, scoped.name));
});
}
/** Delete everything under `directory` except the `kept` relative file paths. */
function keepOnly(directory, kept) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (kept.includes(entry.name)) continue;
const path = join(directory, entry.name);
const prefix = `${entry.name}/`;
const inside = kept.filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length));
if (entry.isDirectory() && inside.length > 0) keepOnly(path, inside);
else rmSync(path, { recursive: true, force: true });
}
}
/** Remove the packages and package files the dev server can never load. */
function pruneUnreachablePackages(nodeModules, reached) {
// Command shims: the server imports these packages, it never runs their CLIs.
rmSync(join(nodeModules, ".bin"), { recursive: true, force: true });
for (const path of packageDirectories(nodeModules)) {
if (!reached.has(path)) {
rmSync(path, { recursive: true, force: true });
continue;
}
const kept = assetOnlyPackages[readJson(join(path, "package.json")).name];
if (kept) keepOnly(path, kept);
else pruneUnreachablePackages(join(path, "node_modules"), reached);
}
}
export function prunePresentationRuntime(runtimeRoot) {
const nodeModules = join(runtimeRoot, "node_modules");
pruneUnreachablePackages(nodeModules, reachablePackages(runtimeRoot));
for (const relativePath of removablePackageDirectories) {
rmSync(join(nodeModules, relativePath), { recursive: true, force: true });
}
// emoji-picker-react copies locale source data alongside compiled JS.
// Open Slide uses the package entry, and locale subpaths resolve to JS;
// keep every locale but omit its duplicate JSON/TS when the JS exists.
const emojiData = join(nodeModules, "emoji-picker-react/dist/data");
if (existsSync(emojiData)) {
for (const file of readdirSync(emojiData)) {
if (/^emojis(?:-[\w-]+)?\.(?:json|ts)$/.test(file) &&
existsSync(join(emojiData, file.replace(/\.(?:json|ts)$/, ".js")))) {
rmSync(join(emojiData, file));
}
}
}
for (const path of walkFiles(nodeModules)) {
if (removablePackageFiles.some((pattern) => pattern.test(relative(nodeModules, path)))) rmSync(path);
}
// Vite compiles TypeScript straight out of node_modules (Open Slide's own
// app ships as src/app), so TypeScript sources stay here.
pruneNodeModules(nodeModules, { platform: `${process.platform}-${process.arch}`, typeScriptSources: "keep" });
}
// Tauri's resource walker omits symbolic links. A normal pnpm layout therefore
// ships the package store but drops node_modules/@open-slide/core, Vite, and
// every other package entry that Node resolves at runtime. Hoisting makes the
// package directories real; materialize the remaining executable links too so
// the staged tree is exactly the tree copied into a release app.
function materializeLinks(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isSymbolicLink()) {
const sourcePath = realpathSync(path);
const sourceIsDirectory = lstatSync(sourcePath).isDirectory();
rmSync(path, { recursive: true, force: true });
cpSync(sourcePath, path, { recursive: sourceIsDirectory, preserveTimestamps: true });
if (sourceIsDirectory) materializeLinks(path);
} else if (entry.isDirectory()) {
materializeLinks(path);
}
}
}
async function preparePresentationRuntime() {
const source = join(projectRoot, "tools/open-slide-runtime");
const target = join(projectRoot, "src-tauri/presentation-runtime");
const stage = join(projectRoot, "node_modules/.cache/lattice/presentation-runtime");
rmSync(stage, { recursive: true, force: true });
mkdirSync(stage, { recursive: true });
const runtimeFiles = [
"package.json",
"pnpm-lock.yaml",
"server.mjs",
"agent-chat.mjs",
"agent-presence.mjs",
"deck-error-overlay.mjs",
"patch-contract.mjs",
"lucide-open-slide.mjs",
"locale-copy.mjs",
"prebundled-dependencies.mjs",
// pnpm applies the package.json's patchedDependencies during the install.
"patches",
];
for (const file of runtimeFiles) {
cpSync(join(source, file), join(stage, file), { recursive: true });
}
run(
"pnpm",
["install", "--prod", "--frozen-lockfile", "--ignore-scripts", "--config.node-linker=hoisted"],
{ cwd: stage },
);
materializeLinks(join(stage, "node_modules"));
prunePresentationRuntime(stage);
await checkPresentationRuntime(stage);
const packageVersion = (name) => readJson(join(stage, "node_modules", name, "package.json")).version;
writeFileSync(
join(stage, "manifest.json"),
JSON.stringify(
{
openSlideVersion: packageVersion("@open-slide/core"),
// Set while the runtime is patched to an unreleased upstream commit.
openSlideCommit: readJson(join(stage, "package.json")).openSlideCommit,
viteVersion: packageVersion("vite"),
},
null,
2,
),
);
rmSync(target, { recursive: true, force: true });
renameSync(stage, target);
writeFileSync(
join(target, "placeholder.txt"),
"Run `pnpm prepare:presentation` to stage the managed Open Slide runtime here.\n",
);
if (!existsSync(join(target, "server.mjs"))) throw new Error("Presentation runtime staging failed");
if (!existsSync(join(target, "node_modules/@open-slide/core/package.json"))) {
throw new Error("Presentation runtime package materialization failed");
}
signPresentationRuntime(target);
}
function signPresentationRuntime(runtimeRoot) {
const identity = signingIdentity();
if (!identity) {
console.log("Skipping presentation runtime signing without APPLE_SIGNING_IDENTITY");
return;
}
if (process.platform !== "darwin") throw new Error("The presentation runtime can only be signed on macOS.");
const signed = signMachOTree(runtimeRoot, { identity });
if (signed === 0) throw new Error(`No Mach-O binaries found in ${runtimeRoot}`);
console.log(`Signed ${signed} presentation runtime binaries`);
}
if (isMain(import.meta.url)) await preparePresentationRuntime();