@nativedesktop/host 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativedesktop/host",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Resolves the prebuilt NativeDesktop host binary for the current platform",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,10 +20,11 @@
20
20
  "main": "./src/index.ts",
21
21
  "types": "./src/index.ts",
22
22
  "exports": {
23
- ".": "./src/index.ts"
23
+ ".": "./src/index.ts",
24
+ "./cef": "./src/cef.ts"
24
25
  },
25
26
  "optionalDependencies": {
26
- "@nativedesktop/host-darwin-arm64": "0.1.2",
27
- "@nativedesktop/host-linux-x64": "0.1.2"
27
+ "@nativedesktop/host-darwin-arm64": "0.3.0",
28
+ "@nativedesktop/host-linux-x64": "0.3.0"
28
29
  }
29
30
  }
@@ -0,0 +1,86 @@
1
+ // CEF resolution order. The engine backends read the same three places in the
2
+ // same sequence natively, so the order here is a contract, not a preference.
3
+ import { describe, expect, test } from "bun:test";
4
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import {
8
+ bundleCefDir,
9
+ cefCacheRoot,
10
+ cefDistDir,
11
+ cefLoadableName,
12
+ cefPlatformKey,
13
+ cefRootCandidates,
14
+ isCefRoot,
15
+ resolveCefRoot,
16
+ } from "./cef.ts";
17
+
18
+ describe("cefPlatformKey", () => {
19
+ test("maps os/arch onto the names cef-builds.spotifycdn.com uses", () => {
20
+ expect(cefPlatformKey("darwin", "arm64")).toBe("macosarm64");
21
+ expect(cefPlatformKey("darwin", "x64")).toBe("macosx64");
22
+ expect(cefPlatformKey("linux", "x64")).toBe("linux64");
23
+ expect(cefPlatformKey("linux", "arm64")).toBe("linuxarm64");
24
+ });
25
+
26
+ test("rejects a platform CEF has no build for", () => {
27
+ expect(() => cefPlatformKey("win32", "arm64")).toThrow("no CEF build");
28
+ });
29
+ });
30
+
31
+ describe("cefCacheRoot", () => {
32
+ test("honors XDG_CACHE_HOME, else ~/.cache", () => {
33
+ expect(cefCacheRoot({ XDG_CACHE_HOME: "/x", HOME: "/home/k" })).toBe("/x/nativedesktop/cef");
34
+ expect(cefCacheRoot({ HOME: "/home/k" })).toBe("/home/k/.cache/nativedesktop/cef");
35
+ });
36
+
37
+ test("the dist dir is keyed version-platform", () => {
38
+ expect(cefDistDir("151.3.23", "macosarm64", { HOME: "/home/k" }))
39
+ .toBe("/home/k/.cache/nativedesktop/cef/151.3.23-macosarm64");
40
+ });
41
+ });
42
+
43
+ describe("cefRootCandidates", () => {
44
+ test("ND_CEF_ROOT, then the bundle, then the dev cache", () => {
45
+ const candidates = cefRootCandidates({
46
+ cefPlatform: "macosarm64",
47
+ version: "151.3.23",
48
+ bundleRoot: "/apps/Demo.app",
49
+ env: { ND_CEF_ROOT: "/opt/cef", HOME: "/home/k" },
50
+ });
51
+ expect(candidates).toEqual([
52
+ "/opt/cef",
53
+ "/apps/Demo.app/Contents/Frameworks",
54
+ "/home/k/.cache/nativedesktop/cef/151.3.23-macosarm64/Release",
55
+ ]);
56
+ });
57
+
58
+ test("without ND_CEF_ROOT or a bundle only the cache remains", () => {
59
+ expect(cefRootCandidates({ cefPlatform: "linux64", version: "151.3.23", env: { HOME: "/home/k" } }))
60
+ .toEqual(["/home/k/.cache/nativedesktop/cef/151.3.23-linux64/Release"]);
61
+ });
62
+
63
+ test("a linux bundle keeps CEF under lib/cef", () => {
64
+ expect(bundleCefDir("/build/AppDir", "linux64")).toBe("/build/AppDir/lib/cef");
65
+ });
66
+ });
67
+
68
+ describe("resolveCefRoot", () => {
69
+ test("skips candidates that hold no loadable and takes the first that does", () => {
70
+ const home = mkdtempSync(join(tmpdir(), "nd-cef-"));
71
+ const empty = join(home, "empty");
72
+ mkdirSync(empty, { recursive: true });
73
+ const cache = join(home, ".cache", "nativedesktop", "cef", "151.3.23-linux64", "Release");
74
+ mkdirSync(cache, { recursive: true });
75
+ writeFileSync(join(cache, cefLoadableName("linux64")), "");
76
+
77
+ const env = { HOME: home, ND_CEF_ROOT: empty };
78
+ expect(isCefRoot(empty, "linux64")).toBe(false);
79
+ expect(resolveCefRoot({ cefPlatform: "linux64", version: "151.3.23", env })).toBe(cache);
80
+ });
81
+
82
+ test("undefined when nothing on disk answers", () => {
83
+ const home = mkdtempSync(join(tmpdir(), "nd-cef-"));
84
+ expect(resolveCefRoot({ cefPlatform: "linux64", version: "151.3.23", env: { HOME: home } })).toBeUndefined();
85
+ });
86
+ });
package/src/cef.ts ADDED
@@ -0,0 +1,123 @@
1
+ // @nativedesktop/host/cef: where the Chromium Embedded Framework lives for a
2
+ // given platform. CEF is never linked into a host binary (it is dlopened at
3
+ // runtime), so `nd dev`, `nd package`, `nd doctor` and both engine backends all
4
+ // have to agree on one search order. This module is that order for the
5
+ // TypeScript side; the Zig and Swift loaders read the same three places in the
6
+ // same sequence.
7
+ //
8
+ // 1. ND_CEF_ROOT an explicit directory, ahead of everything else
9
+ // 2. the app bundle Contents/Frameworks (mac), lib/cef (linux)
10
+ // 3. the dev cache ~/.cache/nativedesktop/cef/<version>-<platform>/Release
11
+ //
12
+ // Nothing here downloads anything: resolution answers from what is already on
13
+ // disk. Fetching and staging a dist is `nd package`'s job (packages/nd).
14
+ import { existsSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join, resolve } from "node:path";
17
+
18
+ /** CEF release the framework pins. `webview.cef.version` overrides it. */
19
+ export const DEFAULT_CEF_VERSION = "151.3.23";
20
+
21
+ /** The CEF subprocess executable, built beside the appkit host binary and
22
+ * copied into each of the five helper .apps a packaged macOS bundle carries. */
23
+ export const CEF_HELPER_BINARY_NAME = "nd-cef-helper";
24
+
25
+ /** Platform names as cef-builds.spotifycdn.com spells them. */
26
+ export type CefPlatform = "macosarm64" | "macosx64" | "linux64" | "linuxarm64";
27
+
28
+ const CEF_PLATFORMS: Record<string, CefPlatform> = {
29
+ "darwin-arm64": "macosarm64",
30
+ "darwin-x64": "macosx64",
31
+ "linux-x64": "linux64",
32
+ "linux-arm64": "linuxarm64",
33
+ };
34
+
35
+ /** The CEF platform key for an os/arch pair, e.g. "macosarm64". */
36
+ export function cefPlatformKey(platform: string = process.platform, arch: string = process.arch): CefPlatform {
37
+ const key = CEF_PLATFORMS[`${platform}-${arch}`];
38
+ if (!key) throw new Error(`@nativedesktop/host: no CEF build for "${platform}-${arch}"`);
39
+ return key;
40
+ }
41
+
42
+ /** True for the two macOS keys, which carry a framework instead of a .so. */
43
+ export function isMacCefPlatform(cefPlatform: CefPlatform): boolean {
44
+ return cefPlatform.startsWith("macos");
45
+ }
46
+
47
+ /** The loadable, relative to the directory that holds it. Its presence is what
48
+ * makes a candidate directory a CEF root. */
49
+ export function cefLoadableName(cefPlatform: CefPlatform): string {
50
+ return isMacCefPlatform(cefPlatform)
51
+ ? "Chromium Embedded Framework.framework/Chromium Embedded Framework"
52
+ : "libcef.so";
53
+ }
54
+
55
+ /** ~/.cache/nativedesktop/cef, or $XDG_CACHE_HOME/nativedesktop/cef. */
56
+ export function cefCacheRoot(env: Record<string, string | undefined> = process.env): string {
57
+ const base = env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache");
58
+ return join(base, "nativedesktop", "cef");
59
+ }
60
+
61
+ /** Extracted dist for a version+platform: <cache>/<version>-<platform>, the
62
+ * tarball's contents with its top-level cef_binary_… directory stripped. */
63
+ export function cefDistDir(
64
+ version: string,
65
+ cefPlatform: CefPlatform,
66
+ env: Record<string, string | undefined> = process.env,
67
+ ): string {
68
+ return join(cefCacheRoot(env), `${version}-${cefPlatform}`);
69
+ }
70
+
71
+ export interface CefRootOptions {
72
+ cefPlatform: CefPlatform;
73
+ version?: string;
74
+ /** A packaged bundle to search: the .app on macOS, the AppDir on Linux. */
75
+ bundleRoot?: string;
76
+ env?: Record<string, string | undefined>;
77
+ }
78
+
79
+ /** The three candidate directories, in resolution order, whether or not they exist. */
80
+ export function cefRootCandidates(opts: CefRootOptions): string[] {
81
+ const env = opts.env ?? process.env;
82
+ const version = opts.version ?? DEFAULT_CEF_VERSION;
83
+ const candidates: string[] = [];
84
+ if (env.ND_CEF_ROOT) candidates.push(resolve(env.ND_CEF_ROOT));
85
+ if (opts.bundleRoot) candidates.push(bundleCefDir(opts.bundleRoot, opts.cefPlatform));
86
+ candidates.push(join(cefDistDir(version, opts.cefPlatform, env), "Release"));
87
+ return candidates;
88
+ }
89
+
90
+ /** Where a packaged bundle keeps its CEF payload. */
91
+ export function bundleCefDir(bundleRoot: string, cefPlatform: CefPlatform): string {
92
+ return isMacCefPlatform(cefPlatform)
93
+ ? join(bundleRoot, "Contents", "Frameworks")
94
+ : join(bundleRoot, "lib", "cef");
95
+ }
96
+
97
+ /** A directory counts as a CEF root once it holds the platform's loadable. */
98
+ export function isCefRoot(dir: string, cefPlatform: CefPlatform): boolean {
99
+ return existsSync(join(dir, cefLoadableName(cefPlatform)));
100
+ }
101
+
102
+ /** First candidate that actually holds the loadable, or undefined. */
103
+ export function resolveCefRoot(opts: CefRootOptions): string | undefined {
104
+ return cefRootCandidates(opts).find((dir) => isCefRoot(dir, opts.cefPlatform));
105
+ }
106
+
107
+ /**
108
+ * Basenames of the CEF artifacts a bundle would carry. The doctor check that
109
+ * proves an engine="system" bundle ships zero Chromium bytes matches on these,
110
+ * so one list covers both platforms' layouts.
111
+ */
112
+ export const CEF_ARTIFACT_NAMES: readonly string[] = [
113
+ "Chromium Embedded Framework.framework",
114
+ "libcef.so",
115
+ "chrome-sandbox",
116
+ "icudtl.dat",
117
+ "resources.pak",
118
+ "chrome_100_percent.pak",
119
+ "chrome_200_percent.pak",
120
+ "v8_context_snapshot.bin",
121
+ "libvk_swiftshader.so",
122
+ "vk_swiftshader_icd.json",
123
+ ];
package/src/index.ts CHANGED
@@ -20,7 +20,8 @@
20
20
  // built on first run.
21
21
  import { existsSync, statSync } from "node:fs";
22
22
  import { createRequire } from "node:module";
23
- import { resolve } from "node:path";
23
+ import { basename, dirname, resolve } from "node:path";
24
+ import { CEF_HELPER_BINARY_NAME } from "./cef.ts";
24
25
 
25
26
  const OS_NAMES: Record<string, string> = { darwin: "darwin", linux: "linux", win32: "windows" };
26
27
  const ARCH_NAMES: Record<string, string> = { arm64: "arm64", x64: "x64" };
@@ -199,6 +200,31 @@ export async function resolveHostBinary(
199
200
  throw new Error(`@nativedesktop/host: no ${backend} host binary for "${key}". ${hint}`);
200
201
  }
201
202
 
203
+ /**
204
+ * The CEF subprocess executable for an appkit host binary, built from the same
205
+ * SwiftPM package the host came from. Packaging copies it into the five helper
206
+ * .apps, and it is a separate product, so a checkout that has only ever run
207
+ * `nd dev` has the host built and this one missing.
208
+ *
209
+ * Returns undefined when the host binary is not a source-checkout build: a
210
+ * prebuilt platform package ships the helper beside the host, and there is
211
+ * nothing to build from.
212
+ */
213
+ export async function buildCefHelper(hostBinary: string): Promise<string | undefined> {
214
+ const buildDir = dirname(resolve(hostBinary));
215
+ const repoRoot = resolve(buildDir, "..", "..", "..");
216
+ if (!isSourceCheckout(repoRoot)) return undefined;
217
+ const configuration = basename(buildDir) === "debug" ? "debug" : "release";
218
+ process.stderr.write(`nd: building ${CEF_HELPER_BINARY_NAME} (first run)…\n`);
219
+ await run(
220
+ ["swift", "build", "-c", configuration, "--product", CEF_HELPER_BINARY_NAME],
221
+ resolve(repoRoot, "swift"),
222
+ appkitBuildEnv(),
223
+ );
224
+ const out = resolve(buildDir, CEF_HELPER_BINARY_NAME);
225
+ return existsSync(out) ? out : undefined;
226
+ }
227
+
202
228
  function backendBuildHint(backend: Backend): string {
203
229
  return backend === "gtk"
204
230
  ? "`zig build`"