253 lines · 11.0 KB · JavaScript
RawDownload
// 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();