akanjs 3.0.0-beta.30 → 3.0.0-beta.32

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.
Files changed (39) hide show
  1. package/client/translator.ts +26 -0
  2. package/index.ts +29 -4
  3. package/package.json +1 -1
  4. package/server/rscWorker.tsx +12 -1
  5. package/server/rscWorkerHost.ts +20 -1
  6. package/server/webRouter.ts +4 -1
  7. package/types/client/translator.d.ts +1 -0
  8. package/types/index.d.ts +28 -4
  9. package/types/server/rscWorkerHost.d.ts +1 -0
  10. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +20 -4
  11. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +7 -0
  12. package/types/vendor/@akanjs/native/packages/cli/src/lib/addons.d.ts +19 -0
  13. package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +95 -0
  14. package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +21 -1
  15. package/types/vendor/@akanjs/native/packages/cli/src/lib/signing.d.ts +2 -2
  16. package/types/vendor/@akanjs/native/packages/cli/src/lib/toolchains.d.ts +9 -0
  17. package/types/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.d.ts +30 -0
  18. package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop.d.ts +8 -2
  19. package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +11 -0
  20. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +2 -0
  21. package/ui/System/Client.tsx +9 -16
  22. package/ui/System/SSR.tsx +1 -1
  23. package/vendor/@akanjs/native/packages/cli/src/api.ts +53 -7
  24. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +38 -4
  25. package/vendor/@akanjs/native/packages/cli/src/config.ts +7 -0
  26. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +217 -0
  27. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +1 -0
  28. package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +357 -0
  29. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +34 -1
  30. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +1 -0
  31. package/vendor/@akanjs/native/packages/cli/src/lib/signing.ts +4 -3
  32. package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +24 -0
  33. package/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.ts +155 -0
  34. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +30 -13
  35. package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +84 -0
  36. package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +4 -1
  37. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +115 -43
  38. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +29 -2
  39. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +12 -1
@@ -1,6 +1,7 @@
1
1
  import type { ParsedArgs } from "../lib/args.ts";
2
2
  import { type Launched, type LaunchOptions } from "../lib/launch.ts";
3
3
  import type { BuildContext } from "../lib/prepare.ts";
4
+ import { type DesktopArch } from "../lib/updates.ts";
4
5
  export declare const NATIVE_DIR: string;
5
6
  export type DesktopOs = "macos" | "windows" | "linux";
6
7
  /** The cdylib cargo builds, per OS. */
@@ -17,14 +18,19 @@ export declare function resourcesOf(os: DesktopOs, artifact: string): string;
17
18
  */
18
19
  export declare function cargoTargetDir(): string;
19
20
  /** Builds (incrementally) the Rust cdylib and returns its path. */
20
- export declare function buildNativeLibrary(os: DesktopOs, release: boolean): Promise<string>;
21
+ export declare function buildNativeLibrary(os: DesktopOs, release: boolean, arch?: DesktopArch): Promise<string>;
21
22
  /**
22
23
  * The app never reads a .env or bunfig.toml from the folder it is started in: Bun's standalone
23
24
  * executables do by default, which would let a file next to the app set its environment and
24
25
  * runtime options.
25
26
  */
26
27
  export declare const COMPILE_FLAGS: string[];
27
- /** bun build --compile of the main thread entry and the plugin host Worker into `outfile`. */
28
+ /** The CPU a desktop build makes code for. */
29
+ export declare function targetArch(ctx: Pick<BuildContext, "arch">): DesktopArch;
30
+ /**
31
+ * bun build --compile of the main thread entry and the plugin host Worker into `outfile`, for the build's CPU: another
32
+ * CPU's Bun is a cross-compilation target Bun downloads once.
33
+ */
28
34
  export declare function compileExecutable(ctx: BuildContext, outfile: string, extra?: string[]): Promise<void>;
29
35
  /** The resources folder every desktop app reads (see the top of this file). */
30
36
  export declare function writeDesktopResources(ctx: BuildContext, resources: string, os: DesktopOs, shellExtra?: Record<string, unknown>): void;
@@ -0,0 +1,11 @@
1
+ import type { BuildContext } from "../lib/prepare.ts";
2
+ /** The launcher entry (freedesktop Desktop Entry): what a menu shows and which link schemes open the app. */
3
+ export declare function desktopEntry(input: {
4
+ name: string;
5
+ fileName: string;
6
+ schemes: string[];
7
+ }): string;
8
+ /** AppRun: the runtime starts it from the mounted image; it starts the app from beside itself. */
9
+ export declare function appRun(exe: string): string;
10
+ /** Writes the AppImage beside the app folder and answers its path. */
11
+ export declare function buildAppImage(ctx: BuildContext, folder: string): Promise<string>;
@@ -27,6 +27,8 @@ export interface InstallerInput {
27
27
  bootstrapper: string;
28
28
  stopScript: string;
29
29
  outFile: string;
30
+ /** A command that signs the uninstaller makensis writes into the setup program, `%1` its path. */
31
+ signUninstaller?: string;
30
32
  }
31
33
  export declare function installerScript(input: InstallerInput): string;
32
34
  /**
@@ -282,27 +282,20 @@ export const ClientBridge = ({ env, lang, theme, prefix, wsConnect = true }: Cli
282
282
  Client.Bridge = ClientBridge;
283
283
 
284
284
  function restoreDocumentTheme(layoutTheme: AkanTheme | undefined): void {
285
- const cookieTheme = getCookie("theme");
286
285
 
287
- if (cookieTheme) {
288
- document.documentElement.setAttribute("data-theme", cookieTheme);
289
- st.do.setTheme(cookieTheme);
290
- return;
291
- }
292
- applyThemePolicy(layoutTheme ?? "system");
293
- }
294
-
295
- function applyThemePolicy(theme: AkanTheme): void {
296
- if (theme === "css") {
286
+ const theme = getCookie("theme") ?? resolveThemePolicy(layoutTheme ?? "system");
287
+ if (theme === null) {
297
288
  document.documentElement.removeAttribute("data-theme");
298
289
  return;
299
290
  }
300
- if (theme === "system") {
301
- const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
302
- document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
303
- return;
304
- }
305
291
  document.documentElement.setAttribute("data-theme", theme);
292
+ st.do.setTheme(theme);
293
+ }
294
+
295
+ function resolveThemePolicy(theme: AkanTheme): string | null {
296
+ if (theme === "css") return null;
297
+ if (theme === "system") return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
298
+ return theme;
306
299
  }
307
300
 
308
301
  function buildSearchParams(entries: Iterable<[string, string]>): Record<string, string | string[]> {
package/ui/System/SSR.tsx CHANGED
@@ -51,7 +51,7 @@ const SSRProvider = ({
51
51
 
52
52
  if (allDictionary) for (const [lng, dict] of Object.entries(allDictionary)) Translator.replace(lng, dict);
53
53
 
54
- const activeDictionary = allDictionary?.[activeLocale] ?? dictionary;
54
+ const activeDictionary = (allDictionary && Translator.withDefaultLocale(allDictionary, activeLocale)) ?? dictionary;
55
55
  const pageState = getRequestFrameState<PageState>() ?? defaultPageState;
56
56
 
57
57
  return (
@@ -10,6 +10,12 @@ import { CancelledError, withSignal } from "./lib/exec.ts";
10
10
  import { type HmrContext, lanAddress, resolveDevEntry, startHmrServer } from "./lib/hmr.ts";
11
11
  import type { Device, DeviceSelector, Launched } from "./lib/launch.ts";
12
12
  import { CliError, type LogEvent, type LogSink, log, ToolchainError, ToolError, withLogSink } from "./lib/log.ts";
13
+ import {
14
+ type MacosBuild,
15
+ type MacosNotarization,
16
+ type MacosSigning,
17
+ macosDistributionFromEnv,
18
+ } from "./lib/macossigning.ts";
13
19
  import {
14
20
  type AndroidSigning,
15
21
  type BuildContext,
@@ -18,6 +24,7 @@ import {
18
24
  type IosBuild,
19
25
  type IosSigning,
20
26
  type IosSigningResult,
27
+ type LinuxBuild,
21
28
  prepare,
22
29
  SigningError,
23
30
  WebBuildError,
@@ -28,12 +35,14 @@ import { ConfigError, type Project, projectFromConfig } from "./lib/project.ts";
28
35
  import { assertServerOfChannel, assertSigningKey, publishRelease } from "./lib/publish.ts";
29
36
  import {
30
37
  assertChannel,
38
+ type DesktopArch,
31
39
  generateUpdateKey,
32
40
  type UpdateManifest,
33
41
  updateKeyPath,
34
42
  webManifest,
35
43
  writeWebBundle,
36
44
  } from "./lib/updates.ts";
45
+ import { type WindowsSigning, windowsSigningFromEnv } from "./lib/windowssigning.ts";
37
46
  import { androidDevices, launchAndroid } from "./platforms/android.ts";
38
47
  import { PLATFORM_TARGETS, type TargetPlatform } from "./platforms/index.ts";
39
48
  import { iosDevices, physicalIosDevice } from "./platforms/ios.ts";
@@ -41,19 +50,31 @@ import { iosDevices, physicalIosDevice } from "./platforms/ios.ts";
41
50
  export type {
42
51
  AkanNativeConfig,
43
52
  AndroidSigning,
53
+ DesktopArch,
44
54
  Device,
45
55
  DeviceSelector,
46
56
  DoctorCheck,
47
57
  IosBuild,
48
58
  IosSigning,
49
59
  IosSigningResult,
60
+ LinuxBuild,
50
61
  LogEvent,
62
+ MacosBuild,
63
+ MacosNotarization,
64
+ MacosSigning,
51
65
  TargetPlatform,
52
66
  WindowsBuild,
67
+ WindowsSigning,
53
68
  };
69
+ /**
70
+ * The macOS signing and notarization the AKAN_NATIVE_MACOS_* variables name, for a caller that keeps them in its
71
+ * environment (the API itself reads no variable): pass the result as `macos`.
72
+ */
73
+ /** The Authenticode settings the AKAN_NATIVE_WINDOWS_* variables name: pass the result as `windows.signing`. */
74
+ export { macosDistributionFromEnv, windowsSigningFromEnv };
54
75
 
55
76
  /** semver of this API. A caller checks the major before it relies on anything here. */
56
- export const API_VERSION = "0.9.0";
77
+ export const API_VERSION = "0.10.0";
57
78
 
58
79
  export type AkanNativeErrorCode =
59
80
  | "CONFIG_INVALID"
@@ -156,7 +177,15 @@ export function validateConfig(config: AkanNativeConfig, options: { appDir: stri
156
177
 
157
178
  /** Builds the app. Default profile release (like `akan-native build`). `ios.device`: an iPhone build (signed; release adds an .ipa). */
158
179
  export function build(
159
- options: TaskOptions & { profile?: BuildProfile; ios?: IosBuild; windows?: WindowsBuild },
180
+ options: TaskOptions & {
181
+ profile?: BuildProfile;
182
+ ios?: IosBuild;
183
+ windows?: WindowsBuild;
184
+ macos?: MacosBuild;
185
+ linux?: LinuxBuild;
186
+ /** Desktop: the CPU the app runs on, of the same OS; a macOS app is arm64 only. Default this computer's. */
187
+ arch?: DesktopArch;
188
+ },
160
189
  ): Promise<BuildResult> {
161
190
  return task(
162
191
  options,
@@ -165,6 +194,9 @@ export function build(
165
194
  await buildIn(options, options.profile ?? "release", options.mode ?? "production", warnings, {
166
195
  ...(options.ios ? { ios: options.ios } : {}),
167
196
  ...(options.windows ? { windows: options.windows } : {}),
197
+ ...(options.macos ? { macos: options.macos } : {}),
198
+ ...(options.linux ? { linux: options.linux } : {}),
199
+ ...(options.arch ? { arch: options.arch } : {}),
168
200
  })
169
201
  ).result,
170
202
  );
@@ -289,11 +321,22 @@ export function checkPublishUpdate(options: PublishCheckOptions): { channel: str
289
321
  * its files under `out` (default <appDir>/.akan/native/updates), signed with the key updateKeygen() made.
290
322
  */
291
323
  export function publishUpdate(
292
- options: TaskOptions & { platform: Exclude<TargetPlatform, "web">; channel?: string; out?: string },
324
+ options: TaskOptions & {
325
+ platform: Exclude<TargetPlatform, "web">;
326
+ channel?: string;
327
+ out?: string;
328
+ /** The signature a macOS release carries: the installed app's own, or the updater refuses it. No dmg is made. */
329
+ macos?: Pick<MacosBuild, "signing" | "notarize">;
330
+ /** Authenticode for a Windows release's files. No setup program is made. */
331
+ windows?: Pick<WindowsBuild, "signing">;
332
+ },
293
333
  ): Promise<PublishResult> {
294
334
  return task(options, async (warnings) => {
295
335
  const { channel, out } = checkPublishUpdate(options);
296
- const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings);
336
+ const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings, {
337
+ ...(options.macos ? { macos: { ...options.macos, dmg: false } } : {}),
338
+ ...(options.windows?.signing ? { windows: { signing: options.windows.signing } } : {}),
339
+ });
297
340
  const { dir, manifest } = await publishRelease(ctx, options.platform, artifact, out, channel).catch((error) => {
298
341
  throw toAkanNativeError(error, "CONFIG_INVALID");
299
342
  });
@@ -633,7 +676,10 @@ async function buildIn(
633
676
  profile: BuildProfile,
634
677
  mode: string,
635
678
  warnings: string[],
636
- extra: Pick<BuildOptions, "android" | "ios" | "windows" | "devServer" | "startPath"> = {},
679
+ extra: Pick<
680
+ BuildOptions,
681
+ "android" | "ios" | "windows" | "macos" | "linux" | "arch" | "devServer" | "startPath"
682
+ > = {},
637
683
  ): Promise<{ ctx: BuildContext; artifact: string; result: BuildResult }> {
638
684
  const started = performance.now();
639
685
  const appDir = resolve(options.appDir);
@@ -693,10 +739,10 @@ function describe(platform: TargetPlatform, path: string, ctx: BuildContext): Ar
693
739
  case "web":
694
740
  return { kind: "web", path, signing: "none" };
695
741
  case "macos":
696
- return { kind: "app", path, signing: "adhoc" };
742
+ return { kind: "app", path, signing: ctx.signedAs ?? "adhoc" };
697
743
  case "windows":
698
744
  case "linux":
699
- return { kind: "folder", path, signing: "none" };
745
+ return { kind: "folder", path, signing: ctx.signedAs ?? "none" };
700
746
  case "ios":
701
747
  return ctx.ios?.device
702
748
  ? { kind: "app", path, signing: ctx.signedAs ?? "development", device: "device" }
@@ -3,19 +3,33 @@
3
3
  import { relative } from "node:path";
4
4
  import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
5
5
  import { bold, CliError, dim, log } from "../lib/log.ts";
6
+ import { macosDistributionFromEnv } from "../lib/macossigning.ts";
6
7
  import { type BuildContext, type BuildProfile, prepare } from "../lib/prepare.ts";
7
8
  import { findAppDir, loadProject } from "../lib/project.ts";
9
+ import type { DesktopArch } from "../lib/updates.ts";
10
+ import { windowsSigningFromEnv } from "../lib/windowssigning.ts";
8
11
  import { PLATFORM_TARGETS, TARGETS, type TargetPlatform } from "../platforms/index.ts";
9
12
  import { physicalIosDevice } from "../platforms/ios.ts";
10
13
 
11
14
  export const BOOLEAN_FLAGS = ["skip-web-build", "open", "release", "debug", "headless", "aab", "installer"];
12
15
 
13
16
  /** Flags of every build: the app, the env mode (.env.<mode>), the profile, the web build. */
14
- export const BUILD_FLAGS = ["app", "mode", "release", "debug", "skip-web-build", "aab", "installer", "device", "build"];
17
+ export const BUILD_FLAGS = [
18
+ "app",
19
+ "mode",
20
+ "release",
21
+ "debug",
22
+ "skip-web-build",
23
+ "aab",
24
+ "installer",
25
+ "device",
26
+ "build",
27
+ "arch",
28
+ ];
15
29
  /** Flags of launching a build (web server, simulators, emulators). */
16
30
  export const LAUNCH_FLAGS = ["port", "host", "open", "headless", "device", "avd"];
17
31
 
18
- export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>]`;
32
+ export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>] [--arch <arm64|x64>]`;
19
33
  export const RUN_USAGE = `akan-native run <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--release] [--skip-web-build] [--port <n>] [--open] [--headless] [--device <name>] [--avd <name>] [--start <path>]`;
20
34
 
21
35
  export interface BuildDefaults {
@@ -57,7 +71,8 @@ export async function buildFromArgs(
57
71
  if (project.plugins.length) log.info(dim(`plugins: ${project.plugins.map((p) => p.manifest.id).join(", ")}`));
58
72
 
59
73
  if (args.flags.aab === true && platform !== "android") throw new CliError("--aab is for android", 2);
60
- if (args.flags.installer === true && platform !== "windows") throw new CliError("--installer is for windows", 2);
74
+ if (args.flags.installer === true && platform !== "windows" && platform !== "macos" && platform !== "linux")
75
+ throw new CliError("--installer is for windows (a setup program), macos (a dmg) and linux (an AppImage)", 2);
61
76
 
62
77
  const device = args.flags.device;
63
78
  const iphone = platform === "ios" && typeof device === "string" ? await physicalIosDevice(device) : null;
@@ -70,7 +85,26 @@ export async function buildFromArgs(
70
85
  startPath: extra.startPath,
71
86
 
72
87
  ...(args.flags.aab === true ? { android: { bundle: true } } : {}),
73
- ...(args.flags.installer === true ? { windows: { installer: true } } : {}),
88
+ ...(platform === "windows"
89
+ ? {
90
+ windows: {
91
+ ...(args.flags.installer === true ? { installer: true } : {}),
92
+
93
+ ...(profile === "release" ? { signing: windowsSigningFromEnv() } : {}),
94
+ },
95
+ }
96
+ : {}),
97
+
98
+ ...(platform === "macos"
99
+ ? {
100
+ macos: {
101
+ ...(profile === "release" ? macosDistributionFromEnv() : {}),
102
+ ...(args.flags.installer === true ? { dmg: true } : {}),
103
+ },
104
+ }
105
+ : {}),
106
+ ...(platform === "linux" && args.flags.installer === true ? { linux: { appImage: true } } : {}),
107
+ ...(stringFlag(args, "arch") ? { arch: stringFlag(args, "arch") as DesktopArch } : {}),
74
108
 
75
109
  ...(iosDevice
76
110
  ? { ios: { device: true, ...(iphone ? { target: { udid: iphone.udid, name: iphone.name } } : {}) } }
@@ -140,6 +140,13 @@ export interface AkanNativeConfig {
140
140
  /** Entitlements (e.g. keychain-access-groups). associated-domains comes from deepLinks.domains. */
141
141
  entitlements?: Record<string, NativeValue>;
142
142
  };
143
+ macos?: {
144
+ /**
145
+ * Entitlements of the main executable, over the hardened runtime's (allow-jit, allow-unsigned-executable-memory)
146
+ * and those the usage texts ask for (the camera's, the microphone's). Read when the app is signed with a team.
147
+ */
148
+ entitlements?: Record<string, NativeValue>;
149
+ };
143
150
  android?: {
144
151
  /** XML at the <manifest> level (uses-permission, queries, …). ${applicationId} is replaced. */
145
152
  manifest?: string[];
@@ -0,0 +1,217 @@
1
+
2
+
3
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
+ import { dirname, join, relative, sep } from "node:path";
5
+
6
+ export type AddonOs = "macos" | "windows" | "linux";
7
+ export type AddonArch = "arm64" | "x64";
8
+
9
+ export interface BinaryInfo {
10
+ format: "mach-o" | "elf" | "pe";
11
+ /** Every architecture the file holds (a fat Mach-O holds several). */
12
+ archs: (AddonArch | "other")[];
13
+ /** Libraries it loads by an absolute path, and the absolute search paths it names (rpath, runpath). */
14
+ absoluteLinks: string[];
15
+ }
16
+
17
+ export interface AddonReport {
18
+ problems: string[];
19
+ warnings: string[];
20
+ }
21
+
22
+ const FORMAT: Record<AddonOs, BinaryInfo["format"]> = { macos: "mach-o", windows: "pe", linux: "elf" };
23
+
24
+ /** What a desktop build refuses and what it warns about in `serverDir`'s `.node` files, by package. */
25
+ export function addonReport(serverDir: string, os: AddonOs, arch: AddonArch): AddonReport {
26
+ const problems: string[] = [];
27
+ const warnings: string[] = [];
28
+ const byPackage = new Map<string, string[]>();
29
+ const files = listFiles(serverDir);
30
+ for (const file of files.filter((f) => f.endsWith(".node"))) {
31
+ const root = packageRoot(serverDir, file);
32
+ byPackage.set(root, [...(byPackage.get(root) ?? []), file]);
33
+ }
34
+
35
+ for (const gyp of files.filter((f) => f.endsWith(`${sep}binding.gyp`))) {
36
+ const root = dirname(gyp);
37
+ if (packageRoot(serverDir, gyp) !== root || byPackage.has(root)) continue;
38
+ problems.push(
39
+ `${shown(serverDir, root)}: a native addon (binding.gyp) with no compiled binary; its install script did not run (add it to trustedDependencies) or it cannot build here`,
40
+ );
41
+ }
42
+ for (const [root, files] of [...byPackage].sort(([a], [b]) => a.localeCompare(b))) {
43
+ const name = shown(serverDir, root);
44
+ const loadable = files.flatMap((file) => {
45
+ const info = binaryInfo(readFileSync(file));
46
+ return info && info.format === FORMAT[os] && info.archs.includes(arch) ? [{ file, info }] : [];
47
+ });
48
+ if (!loadable.length) {
49
+ const found = files.map((file) => describeBinary(binaryInfo(readFileSync(file)))).join(", ");
50
+ problems.push(`${name}: no ${os}-${arch} binary (has ${found}); install it for that platform or drop it`);
51
+ continue;
52
+ }
53
+ for (const { file, info } of loadable)
54
+ for (const link of info.absoluteLinks.filter((l) => !systemPath(os, l)))
55
+ problems.push(
56
+ `${shown(serverDir, file)} links ${link}, which a user's computer does not have: bundle the library or drop the addon`,
57
+ );
58
+ const compiled = existsSync(join(root, "binding.gyp")) && loadable.every(({ file }) => isBuildOutput(root, file));
59
+ if (compiled)
60
+ warnings.push(
61
+ `${name} was compiled on this computer at install (binding.gyp, no prebuilt binary): check that it runs on a computer with nothing else installed`,
62
+ );
63
+ }
64
+ return { problems: problems.sort(), warnings: warnings.sort() };
65
+ }
66
+
67
+ /** A path inside the server as the report names it: relative, `/`-separated on every OS. */
68
+ const shown = (base: string, path: string) => relative(base, path).split(sep).join("/") || ".";
69
+
70
+ function listFiles(dir: string): string[] {
71
+ if (!existsSync(dir)) return [];
72
+ return (readdirSync(dir, { recursive: true }) as string[])
73
+ .map((file) => join(dir, file))
74
+ .filter((file) => statSync(file).isFile());
75
+ }
76
+
77
+ /** The folder of the package a file belongs to: the deepest node_modules/<name> (or <@scope>/<name>) above it. */
78
+ export function packageRoot(base: string, file: string): string {
79
+ const parts = relative(base, file).split(sep);
80
+ const at = parts.lastIndexOf("node_modules");
81
+ if (at < 0) return base;
82
+ const width = parts[at + 1]?.startsWith("@") ? 3 : 2;
83
+ return join(base, ...parts.slice(0, at + width));
84
+ }
85
+
86
+ function isBuildOutput(root: string, file: string): boolean {
87
+ return relative(root, file).split(sep)[0] === "build";
88
+ }
89
+
90
+ function systemPath(os: AddonOs, path: string): boolean {
91
+ if (os === "macos") return path.startsWith("/usr/lib/") || path.startsWith("/System/Library/");
92
+ return /^\/(usr\/)?lib(64)?(\/|$)/.test(path);
93
+ }
94
+
95
+ function describeBinary(info: BinaryInfo | null): string {
96
+ if (!info) return "an unknown file";
97
+ const os = info.format === "mach-o" ? "macos" : info.format === "elf" ? "linux" : "windows";
98
+ return `${os}-${info.archs.join("+")}`;
99
+ }
100
+
101
+ /** The format, CPUs and absolute links of a Mach-O, ELF or PE file; null for anything else. */
102
+ export function binaryInfo(bytes: Buffer): BinaryInfo | null {
103
+ if (bytes.length < 64) return null;
104
+ if (bytes.readUInt32BE(0) === 0x7f454c46) return elfInfo(bytes);
105
+ if (bytes.readUInt16LE(0) === 0x5a4d) return peInfo(bytes);
106
+ const magic = bytes.readUInt32LE(0);
107
+ if (magic === 0xfeedfacf || magic === 0xfeedface) return machoInfo([machoSlice(bytes, 0)]);
108
+ if (bytes.readUInt32BE(0) === 0xcafebabe) {
109
+ const count = bytes.readUInt32BE(4);
110
+
111
+ if (count === 0 || count >= 45) return null;
112
+ const slices = Array.from({ length: count }, (_, i) => {
113
+ const at = 8 + i * 20;
114
+ return machoSlice(
115
+ bytes.subarray(bytes.readUInt32BE(at + 8), bytes.readUInt32BE(at + 8) + bytes.readUInt32BE(at + 12)),
116
+ 0,
117
+ );
118
+ });
119
+ return machoInfo(slices);
120
+ }
121
+ return null;
122
+ }
123
+
124
+ const MACHO_CPU: Record<number, AddonArch> = { 16777223: "x64", 16777228: "arm64" };
125
+ const LC_LOAD_DYLIB = 0xc;
126
+ const LC_LOAD_WEAK_DYLIB = 0x80000018;
127
+ const LC_REEXPORT_DYLIB = 0x8000001f;
128
+ const LC_RPATH = 0x8000001c;
129
+
130
+ interface MachoSlice {
131
+ arch: AddonArch | "other";
132
+ links: string[];
133
+ }
134
+
135
+ function machoSlice(bytes: Buffer, at: number): MachoSlice {
136
+ const is64 = bytes.readUInt32LE(at) === 0xfeedfacf;
137
+ const arch = MACHO_CPU[bytes.readUInt32LE(at + 4)] ?? "other";
138
+ const count = bytes.readUInt32LE(at + 16);
139
+ let offset = at + (is64 ? 32 : 28);
140
+ const links: string[] = [];
141
+ for (let i = 0; i < count && offset + 8 <= bytes.length; i++) {
142
+ const cmd = bytes.readUInt32LE(offset);
143
+ const size = bytes.readUInt32LE(offset + 4);
144
+ if (size < 8) break;
145
+ if (cmd === LC_LOAD_DYLIB || cmd === LC_LOAD_WEAK_DYLIB || cmd === LC_REEXPORT_DYLIB || cmd === LC_RPATH) {
146
+ const name = cString(bytes, offset + bytes.readUInt32LE(offset + 8), offset + size);
147
+ if (name.startsWith("/")) links.push(name);
148
+ }
149
+ offset += size;
150
+ }
151
+ return { arch, links };
152
+ }
153
+
154
+ function machoInfo(slices: MachoSlice[]): BinaryInfo {
155
+ return {
156
+ format: "mach-o",
157
+ archs: slices.map((s) => s.arch),
158
+ absoluteLinks: [...new Set(slices.flatMap((s) => s.links))],
159
+ };
160
+ }
161
+
162
+ const ELF_MACHINE: Record<number, AddonArch> = { 62: "x64", 183: "arm64" };
163
+ const PT_LOAD = 1;
164
+ const PT_DYNAMIC = 2;
165
+ const DT_STRTAB = 5;
166
+ const DT_RPATH = 15;
167
+ const DT_RUNPATH = 29;
168
+
169
+ function elfInfo(bytes: Buffer): BinaryInfo {
170
+ const arch = ELF_MACHINE[bytes.readUInt16LE(18)] ?? "other";
171
+
172
+ if (bytes[4] !== 2 || bytes[5] !== 1) return { format: "elf", archs: [arch], absoluteLinks: [] };
173
+ const phoff = Number(bytes.readBigUInt64LE(32));
174
+ const phentsize = bytes.readUInt16LE(54);
175
+ const phnum = bytes.readUInt16LE(56);
176
+ const loads: { vaddr: number; offset: number; size: number }[] = [];
177
+ let dynamic: { offset: number; size: number } | null = null;
178
+ for (let i = 0; i < phnum; i++) {
179
+ const at = phoff + i * phentsize;
180
+ if (at + 56 > bytes.length) break;
181
+ const type = bytes.readUInt32LE(at);
182
+ const offset = Number(bytes.readBigUInt64LE(at + 8));
183
+ const vaddr = Number(bytes.readBigUInt64LE(at + 16));
184
+ const size = Number(bytes.readBigUInt64LE(at + 32));
185
+ if (type === PT_LOAD) loads.push({ vaddr, offset, size });
186
+ if (type === PT_DYNAMIC) dynamic = { offset, size };
187
+ }
188
+ if (!dynamic) return { format: "elf", archs: [arch], absoluteLinks: [] };
189
+ const entries: { tag: number; value: number }[] = [];
190
+ for (let at = dynamic.offset; at + 16 <= dynamic.offset + dynamic.size && at + 16 <= bytes.length; at += 16) {
191
+ const tag = Number(bytes.readBigInt64LE(at));
192
+ if (tag === 0) break;
193
+ entries.push({ tag, value: Number(bytes.readBigUInt64LE(at + 8)) });
194
+ }
195
+ const strtab = entries.find((e) => e.tag === DT_STRTAB)?.value;
196
+ const load = strtab === undefined ? undefined : loads.find((l) => strtab >= l.vaddr && strtab < l.vaddr + l.size);
197
+ if (strtab === undefined || !load) return { format: "elf", archs: [arch], absoluteLinks: [] };
198
+ const base = strtab - load.vaddr + load.offset;
199
+ const paths = entries
200
+ .filter((e) => e.tag === DT_RPATH || e.tag === DT_RUNPATH)
201
+ .flatMap((e) => cString(bytes, base + e.value, bytes.length).split(":"))
202
+ .filter((p) => p.startsWith("/"));
203
+ return { format: "elf", archs: [arch], absoluteLinks: [...new Set(paths)] };
204
+ }
205
+
206
+ const PE_MACHINE: Record<number, AddonArch> = { 34404: "x64", 43620: "arm64" };
207
+
208
+ function peInfo(bytes: Buffer): BinaryInfo | null {
209
+ const at = bytes.readUInt32LE(0x3c);
210
+ if (at + 6 > bytes.length || bytes.readUInt32BE(at) !== 0x50450000) return null;
211
+ return { format: "pe", archs: [PE_MACHINE[bytes.readUInt16LE(at + 4)] ?? "other"], absoluteLinks: [] };
212
+ }
213
+
214
+ function cString(bytes: Buffer, start: number, end: number): string {
215
+ const stop = bytes.indexOf(0, start);
216
+ return bytes.toString("utf8", start, stop < 0 || stop > end ? end : stop);
217
+ }
@@ -24,6 +24,7 @@ const KNOWN: Shape = {
24
24
  deepLinks: { schemes: true, domains: { "[]": { host: true, pathPrefixes: true } } },
25
25
  native: {
26
26
  ios: { infoPlist: true, entitlements: true },
27
+ macos: { entitlements: true },
27
28
  android: { manifest: true, application: true, activity: true },
28
29
  resources: { "[]": { from: true, to: true } },
29
30
  },