node-assets.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import { createHash } from "node:crypto"
  2. import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
  3. import { createRequire } from "node:module"
  4. import path from "node:path"
  5. import { fileURLToPath } from "node:url"
  6. import { getNodeAssets } from "@opentui/core/node-assets"
  7. import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target"
  8. const dir = path.resolve(import.meta.dirname, "..")
  9. // Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript
  10. // portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration.
  11. // Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup;
  12. // native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory.
  13. export type NodeAsset = {
  14. readonly key: string
  15. readonly source: string
  16. }
  17. async function files(root: string, current = root): Promise<string[]> {
  18. return (
  19. await Promise.all(
  20. (await readdir(current, { withFileTypes: true })).map((entry) => {
  21. const target = path.join(current, entry.name)
  22. return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
  23. }),
  24. )
  25. ).flat()
  26. }
  27. export async function collectNodeAssets(target: NodeTarget) {
  28. const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
  29. const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
  30. const assets: NodeAsset[] = [
  31. ...getNodeAssets({
  32. platform: target.platform,
  33. arch: target.arch,
  34. ...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
  35. }),
  36. { key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
  37. {
  38. key: photonWasmAsset,
  39. source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset),
  40. },
  41. ...attentionSoundAssets.map((key) => ({
  42. key,
  43. source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
  44. })),
  45. ...(await files(ptyRoot))
  46. .filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
  47. .map((relative) => ({
  48. key: `${target.nodePtyPackage}/${relative}`,
  49. source: path.join(ptyRoot, relative),
  50. })),
  51. ]
  52. await Promise.all(assets.map((asset) => stat(asset.source)))
  53. return assets
  54. }
  55. export async function hashNodeAssets(assets: readonly NodeAsset[]) {
  56. const hash = createHash("sha256")
  57. for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) {
  58. hash.update(asset.key)
  59. hash.update(await readFile(asset.source))
  60. }
  61. return hash.digest("hex").slice(0, 16)
  62. }
  63. export async function copyNodeAssets(assets: readonly NodeAsset[]) {
  64. const root = path.join(dir, "dist-node", "assets")
  65. await Promise.all(
  66. assets.map(async (asset) => {
  67. const target = path.join(root, asset.key)
  68. await mkdir(path.dirname(target), { recursive: true })
  69. await copyFile(asset.source, target)
  70. }),
  71. )
  72. }
  73. export async function seaAssetMap() {
  74. const root = path.join(dir, "dist-node", "assets")
  75. return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
  76. }