@nativedesktop/cli 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 +2 -2
- package/src/config.test.ts +54 -0
- package/src/config.ts +87 -0
- package/src/index.ts +25 -4
- package/src/package/cef.test.ts +332 -0
- package/src/package/cef.ts +454 -0
- package/src/package/doctor.test.ts +84 -0
- package/src/package/doctor.ts +91 -3
- package/src/package/identity.test.ts +14 -0
- package/src/package/linux.ts +17 -1
- package/src/package/mac.ts +61 -3
- package/src/package/modules.test.ts +22 -0
- package/src/package/modules.ts +42 -4
- package/src/package/payload.test.ts +32 -1
- package/src/package/payload.ts +19 -3
- package/src/package/templates.ts +47 -0
package/src/package/linux.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { $ } from "bun";
|
|
|
6
6
|
import { chmodSync, cpSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
8
|
import { resolveHostBinary } from "@nativedesktop/host";
|
|
9
|
-
import
|
|
9
|
+
import { cefPlatformKey } from "@nativedesktop/host/cef";
|
|
10
|
+
import { type NativeDesktopConfig, resolveCefSchemes, resolveWebViewEngine } from "../config.ts";
|
|
11
|
+
import { applyCefLinuxPlan, cefVersionFor, ensureCefDist, planCefLinux } from "./cef.ts";
|
|
10
12
|
import { installLinuxIcon } from "./icons.ts";
|
|
11
13
|
import { buildAppRun, buildDesktopEntry, buildMimeInfoXml, type ResolvedIdentity } from "./identity.ts";
|
|
12
14
|
import { assemblePayload } from "./payload.ts";
|
|
@@ -28,11 +30,15 @@ export async function packageLinuxApp(
|
|
|
28
30
|
}
|
|
29
31
|
mkdirSync(join(appdir, "usr", "bin"), { recursive: true });
|
|
30
32
|
|
|
33
|
+
const engine = resolveWebViewEngine(config, "linux");
|
|
34
|
+
const schemes = resolveCefSchemes(config);
|
|
31
35
|
const payload = await assemblePayload({
|
|
32
36
|
appDir,
|
|
33
37
|
config,
|
|
34
38
|
identity,
|
|
35
39
|
appRoot: join(appdir, "app"),
|
|
40
|
+
engine,
|
|
41
|
+
schemes,
|
|
36
42
|
entry: options.entry,
|
|
37
43
|
compile: options.compile,
|
|
38
44
|
});
|
|
@@ -43,6 +49,8 @@ export async function packageLinuxApp(
|
|
|
43
49
|
slug: identity.slug,
|
|
44
50
|
appId: identity.id,
|
|
45
51
|
pluginPaths: payload.pluginPaths,
|
|
52
|
+
engine,
|
|
53
|
+
schemes,
|
|
46
54
|
}));
|
|
47
55
|
chmodSync(join(appdir, "AppRun"), 0o755);
|
|
48
56
|
writeFileSync(join(appdir, `${identity.slug}.desktop`), buildDesktopEntry(identity, linux));
|
|
@@ -64,6 +72,14 @@ export async function packageLinuxApp(
|
|
|
64
72
|
cpSync(bunPath, join(appdir, "usr", "bin", "bun"), { dereference: true });
|
|
65
73
|
chmodSync(join(appdir, "usr", "bin", "bun"), 0o755);
|
|
66
74
|
|
|
75
|
+
if (engine === "chromium") {
|
|
76
|
+
const version = cefVersionFor(config.webview?.cef);
|
|
77
|
+
const distRoot = await ensureCefDist({ cefPlatform: cefPlatformKey("linux", process.arch), version });
|
|
78
|
+
const plan = planCefLinux({ distRoot, appDir: appdir, locales: config.webview?.cef?.locales });
|
|
79
|
+
await applyCefLinuxPlan(plan);
|
|
80
|
+
console.error(`ND_PACKAGE_CEF ${plan.root} version=${version} locales=${plan.locales.length}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
let bundlePath = appdir;
|
|
68
84
|
const format = options.format ?? linux?.format ?? "appimage";
|
|
69
85
|
if (format === "appimage") {
|
package/src/package/mac.ts
CHANGED
|
@@ -6,7 +6,18 @@ import { $ } from "bun";
|
|
|
6
6
|
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
8
|
import { resolveHostBinary } from "@nativedesktop/host";
|
|
9
|
-
import
|
|
9
|
+
import { cefPlatformKey } from "@nativedesktop/host/cef";
|
|
10
|
+
import { type NativeDesktopConfig, resolveCefSchemes, resolveWebViewEngine } from "../config.ts";
|
|
11
|
+
import {
|
|
12
|
+
applyCefMacPlan,
|
|
13
|
+
type CefMacPlan,
|
|
14
|
+
cefMacSignTargets,
|
|
15
|
+
cefVersionFor,
|
|
16
|
+
ensureCefDist,
|
|
17
|
+
ensureCefHelperBinary,
|
|
18
|
+
planCefMac,
|
|
19
|
+
writeCefHelperEntitlements,
|
|
20
|
+
} from "./cef.ts";
|
|
10
21
|
import { installMacIcon } from "./icons.ts";
|
|
11
22
|
import { buildInfoPlist, type ResolvedIdentity } from "./identity.ts";
|
|
12
23
|
import { assemblePayload } from "./payload.ts";
|
|
@@ -14,6 +25,33 @@ import { ENTITLEMENTS_PLIST } from "./templates.ts";
|
|
|
14
25
|
import { maybePublishUpdate } from "./updates.ts";
|
|
15
26
|
import type { PackageOptions, PackageResult } from "./index.ts";
|
|
16
27
|
|
|
28
|
+
/** Downloads (or reuses) the pinned CEF dist and stages the framework plus the
|
|
29
|
+
* five helper .apps into Contents/Frameworks. Returns the plan so the signing
|
|
30
|
+
* pass below can walk the same targets. */
|
|
31
|
+
async function stageCefMac(
|
|
32
|
+
config: NativeDesktopConfig,
|
|
33
|
+
identity: ResolvedIdentity,
|
|
34
|
+
contents: string,
|
|
35
|
+
hostBinary: string,
|
|
36
|
+
minimumSystemVersion: string,
|
|
37
|
+
): Promise<CefMacPlan> {
|
|
38
|
+
if (!identity.id) throw new Error('nd: webview engine "chromium" needs app.id (the CEF helper bundle ids derive from it)');
|
|
39
|
+
const version = cefVersionFor(config.webview?.cef);
|
|
40
|
+
const distRoot = await ensureCefDist({ cefPlatform: cefPlatformKey("darwin", process.arch), version });
|
|
41
|
+
const plan = planCefMac({
|
|
42
|
+
distRoot,
|
|
43
|
+
contents,
|
|
44
|
+
appName: identity.name,
|
|
45
|
+
appId: identity.id,
|
|
46
|
+
version: identity.version,
|
|
47
|
+
minimumSystemVersion,
|
|
48
|
+
helperBinary: await ensureCefHelperBinary(hostBinary),
|
|
49
|
+
});
|
|
50
|
+
applyCefMacPlan(plan);
|
|
51
|
+
console.error(`ND_PACKAGE_CEF ${plan.framework.to} version=${version}`);
|
|
52
|
+
return plan;
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
/** Notarization decision: an explicit flag/config wins; the auto default needs credentials AND a signed bundle. */
|
|
18
56
|
export function resolveNotarize(explicit: boolean | undefined, creds: boolean, signed: boolean): boolean {
|
|
19
57
|
return explicit ?? (creds && signed);
|
|
@@ -34,11 +72,14 @@ export async function packageMacApp(
|
|
|
34
72
|
mkdirSync(join(c, "Resources"), { recursive: true });
|
|
35
73
|
mkdirSync(join(c, "Frameworks"), { recursive: true });
|
|
36
74
|
|
|
75
|
+
const engine = resolveWebViewEngine(config, "mac");
|
|
37
76
|
await assemblePayload({
|
|
38
77
|
appDir,
|
|
39
78
|
config,
|
|
40
79
|
identity,
|
|
41
80
|
appRoot: join(c, "Resources", "app"),
|
|
81
|
+
engine,
|
|
82
|
+
schemes: resolveCefSchemes(config),
|
|
42
83
|
entry: options.entry,
|
|
43
84
|
compile: options.compile,
|
|
44
85
|
});
|
|
@@ -55,7 +96,12 @@ export async function packageMacApp(
|
|
|
55
96
|
cpSync(bunPath, join(c, "MacOS", "bun"), { dereference: true });
|
|
56
97
|
chmodSync(join(c, "MacOS", "bun"), 0o755);
|
|
57
98
|
|
|
58
|
-
const
|
|
99
|
+
const minimumSystemVersion = mac?.minimumSystemVersion ?? "26.0";
|
|
100
|
+
const cefPlan = engine === "chromium"
|
|
101
|
+
? await stageCefMac(config, identity, c, hostBinary, minimumSystemVersion)
|
|
102
|
+
: undefined;
|
|
103
|
+
|
|
104
|
+
const iconFile = await installMacIcon(identity, appDir, join(c, "Resources"), minimumSystemVersion);
|
|
59
105
|
const customXml = mac?.infoPlist ? readFileSync(resolve(appDir, mac.infoPlist), "utf8") : undefined;
|
|
60
106
|
writeFileSync(join(c, "Info.plist"), buildInfoPlist(identity, mac, customXml, iconFile));
|
|
61
107
|
|
|
@@ -73,10 +119,22 @@ export async function packageMacApp(
|
|
|
73
119
|
const plugins = existsSync(nativeDir)
|
|
74
120
|
? [...new Bun.Glob("*.dylib").scanSync({ cwd: nativeDir, absolute: true })].sort()
|
|
75
121
|
: [];
|
|
122
|
+
if (cefPlan) {
|
|
123
|
+
// The renderer and GPU helpers carry entitlements the app's own do not,
|
|
124
|
+
// and `--deep` re-signs nested code with whatever it was handed, which
|
|
125
|
+
// would drop them. So the CEF payload is enumerated and signed
|
|
126
|
+
// inside-out here, and the outer sign below drops --deep.
|
|
127
|
+
const helperEnt = writeCefHelperEntitlements(dist);
|
|
128
|
+
for (const target of cefMacSignTargets(cefPlan)) {
|
|
129
|
+
if (target.jit) await $`codesign --force --options runtime --entitlements ${helperEnt} --sign ${sign} ${target.path}`;
|
|
130
|
+
else await $`codesign --force --options runtime --sign ${sign} ${target.path}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
76
133
|
for (const nested of [join(c, "MacOS", "bun"), exe, ...plugins]) {
|
|
77
134
|
await $`codesign --force --options runtime --entitlements ${ent} --sign ${sign} ${nested}`;
|
|
78
135
|
}
|
|
79
|
-
await $`codesign --force --
|
|
136
|
+
if (cefPlan) await $`codesign --force --options runtime --entitlements ${ent} --sign ${sign} ${app}`;
|
|
137
|
+
else await $`codesign --force --deep --options runtime --entitlements ${ent} --sign ${sign} ${app}`;
|
|
80
138
|
await $`codesign --verify --strict ${app}`;
|
|
81
139
|
console.error(`ND_PACKAGE_APP_SIGNED ${app} identity=${sign === "-" ? "adhoc" : "developer-id"}`);
|
|
82
140
|
}
|
|
@@ -142,4 +142,26 @@ describe("assertResolvableEntries", () => {
|
|
|
142
142
|
writeFileSync(join(carrier, "package.json"), JSON.stringify({ name: "host-binary", files: ["bin"] }));
|
|
143
143
|
expect(() => assertResolvableEntries(root, ["host-binary"])).not.toThrow();
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
test("tolerates subpath-only exports (@nativedesktop/native's shape)", () => {
|
|
147
|
+
const root = mkdtempSync(join(tmpdir(), "nd-modules-"));
|
|
148
|
+
const headers = join(root, "node_modules", "header-carrier");
|
|
149
|
+
mkdirSync(headers, { recursive: true });
|
|
150
|
+
writeFileSync(
|
|
151
|
+
join(headers, "package.json"),
|
|
152
|
+
JSON.stringify({ name: "header-carrier", exports: { "./include/nd.h": "./include/nd.h" } }),
|
|
153
|
+
);
|
|
154
|
+
expect(() => assertResolvableEntries(root, ["header-carrier"])).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("still throws when exports declares a root entry that is missing", () => {
|
|
158
|
+
const root = mkdtempSync(join(tmpdir(), "nd-modules-"));
|
|
159
|
+
const broken = join(root, "node_modules", "needs-build-exports");
|
|
160
|
+
mkdirSync(broken, { recursive: true });
|
|
161
|
+
writeFileSync(
|
|
162
|
+
join(broken, "package.json"),
|
|
163
|
+
JSON.stringify({ name: "needs-build-exports", exports: { ".": "./dist/index.js" } }),
|
|
164
|
+
);
|
|
165
|
+
expect(() => assertResolvableEntries(root, ["needs-build-exports"])).toThrow("bun run build");
|
|
166
|
+
});
|
|
145
167
|
});
|
package/src/package/modules.ts
CHANGED
|
@@ -10,9 +10,14 @@ interface QueueEntry {
|
|
|
10
10
|
spec: string;
|
|
11
11
|
/** Directory the spec resolves from (the requiring package's real root). */
|
|
12
12
|
anchor: string;
|
|
13
|
+
/** Second anchor to try, used for peers whose consumer does not supply one. */
|
|
14
|
+
fallbackAnchor?: string;
|
|
13
15
|
/** Dest-relative dir of the requiring package, for version-conflict nesting. */
|
|
14
16
|
requirer?: string;
|
|
15
17
|
optional: boolean;
|
|
18
|
+
/** A peerDependency: the CONSUMER's copy is the correct one, so an existing
|
|
19
|
+
* flat claim wins outright instead of nesting a second copy. */
|
|
20
|
+
peer?: boolean;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
const skipNodeModules = (src: string) => basename(src) !== "node_modules";
|
|
@@ -63,12 +68,24 @@ export function flattenRuntimeModules({ appDir, extraRoots = [], dest }: Flatten
|
|
|
63
68
|
|
|
64
69
|
while (queue.length) {
|
|
65
70
|
const entry = queue.shift()!;
|
|
71
|
+
// A peer the app already provides is settled: taking the dependency's own
|
|
72
|
+
// copy instead is what puts two reacts in one bundle.
|
|
73
|
+
if (entry.peer && claimed.has(entry.spec)) continue;
|
|
66
74
|
let root: string;
|
|
67
75
|
try {
|
|
68
76
|
root = packageRoot(entry.spec, entry.anchor);
|
|
69
77
|
} catch (err) {
|
|
70
|
-
if (entry.
|
|
71
|
-
|
|
78
|
+
if (entry.fallbackAnchor) {
|
|
79
|
+
try {
|
|
80
|
+
root = packageRoot(entry.spec, entry.fallbackAnchor);
|
|
81
|
+
} catch {
|
|
82
|
+
if (entry.optional) continue;
|
|
83
|
+
throw new Error(`nd: cannot resolve runtime dependency "${entry.spec}" from ${entry.anchor}: ${err}`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (entry.optional) continue;
|
|
87
|
+
throw new Error(`nd: cannot resolve runtime dependency "${entry.spec}" from ${entry.anchor}: ${err}`);
|
|
88
|
+
}
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
const existing = claimed.get(entry.spec);
|
|
@@ -94,14 +111,35 @@ export function flattenRuntimeModules({ appDir, extraRoots = [], dest }: Flatten
|
|
|
94
111
|
for (const spec of Object.keys(pkg.dependencies ?? {})) {
|
|
95
112
|
queue.push({ spec, anchor: root, requirer: destDir, optional: false });
|
|
96
113
|
}
|
|
97
|
-
for (const spec of
|
|
114
|
+
for (const spec of Object.keys(pkg.optionalDependencies ?? {})) {
|
|
98
115
|
queue.push({ spec, anchor: root, requirer: destDir, optional: true });
|
|
99
116
|
}
|
|
117
|
+
// Peers resolve from the APP, not from the package that declares them. A
|
|
118
|
+
// `file:` linked dependency living outside the app tree resolves its own
|
|
119
|
+
// peer to its own copy, and a second react in the bundle is the classic
|
|
120
|
+
// "Invalid hook call" at launch. The package's root is only the fallback,
|
|
121
|
+
// for a peer the app does not provide at all.
|
|
122
|
+
for (const spec of Object.keys(pkg.peerDependencies ?? {})) {
|
|
123
|
+
queue.push({ spec, anchor, fallbackAnchor: root, requirer: destDir, optional: true, peer: true });
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
return [...claimed.keys()];
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Whether `exports` offers the package's ROOT specifier. A string is one; an
|
|
132
|
+
* object is one only through a `"."` key or the conditions shorthand (keys that
|
|
133
|
+
* are not subpaths). A subpath-only map (`{"./include/nd.h": …}`, which is what
|
|
134
|
+
* `@nativedesktop/native` publishes) declares no root entry, so failing to
|
|
135
|
+
* resolve the bare name is correct rather than a missing build.
|
|
136
|
+
*/
|
|
137
|
+
function declaresRootEntry(exports: unknown): boolean {
|
|
138
|
+
if (typeof exports === "string") return true;
|
|
139
|
+
if (exports === null || typeof exports !== "object") return false;
|
|
140
|
+
return Object.keys(exports).some((key) => key === "." || !key.startsWith("."));
|
|
141
|
+
}
|
|
142
|
+
|
|
105
143
|
/**
|
|
106
144
|
* Asserts each flat package's main/exports entry resolves inside the bundle.
|
|
107
145
|
* Catches a copied-but-unbuilt package (e.g. @nativedesktop/react without its
|
|
@@ -119,7 +157,7 @@ export function assertResolvableEntries(appRoot: string, names: string[]): void
|
|
|
119
157
|
module?: string;
|
|
120
158
|
exports?: unknown;
|
|
121
159
|
};
|
|
122
|
-
if (pkg.main === undefined && pkg.module === undefined && pkg.exports
|
|
160
|
+
if (pkg.main === undefined && pkg.module === undefined && !declaresRootEntry(pkg.exports)) continue;
|
|
123
161
|
throw new Error(
|
|
124
162
|
`nd: bundled package "${name}" has no resolvable entry (its build output is missing). ` +
|
|
125
163
|
`Run \`bun run build\` in ${name} and package again.`,
|
|
@@ -82,6 +82,8 @@ describe("assemblePayload", () => {
|
|
|
82
82
|
config: { package: { workspaceRoot: "../..", include: ["shared"], compile: false } },
|
|
83
83
|
identity,
|
|
84
84
|
appRoot,
|
|
85
|
+
engine: "chromium",
|
|
86
|
+
schemes: ["nbext"],
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
expect(result).toEqual({ entry: "apps/fixture/src/main.tsx", cwd: "apps/fixture", pluginPaths: [] });
|
|
@@ -96,6 +98,8 @@ describe("assemblePayload", () => {
|
|
|
96
98
|
entry: "apps/fixture/src/main.tsx",
|
|
97
99
|
cwd: "apps/fixture",
|
|
98
100
|
pluginPaths: [],
|
|
101
|
+
engine: "chromium",
|
|
102
|
+
schemes: ["nbext"],
|
|
99
103
|
});
|
|
100
104
|
});
|
|
101
105
|
|
|
@@ -117,7 +121,7 @@ describe("assemblePayload", () => {
|
|
|
117
121
|
for (const dir of ["MacOS", "Resources", "Frameworks"]) mkdirSync(join(contents, dir), { recursive: true });
|
|
118
122
|
const appRoot = join(contents, "Resources", "app");
|
|
119
123
|
|
|
120
|
-
const result = await assemblePayload({ appDir, config: {}, identity, appRoot });
|
|
124
|
+
const result = await assemblePayload({ appDir, config: {}, identity, appRoot, engine: "system", schemes: [] });
|
|
121
125
|
|
|
122
126
|
expect(result).toEqual({ entry: "dist/main.tsx", cwd: ".", pluginPaths: [] });
|
|
123
127
|
expect(existsSync(join(appRoot, "dist/main.tsx"))).toBe(true);
|
|
@@ -125,6 +129,31 @@ describe("assemblePayload", () => {
|
|
|
125
129
|
expect(existsSync(join(appRoot, "dist", "mac"))).toBe(false);
|
|
126
130
|
});
|
|
127
131
|
|
|
132
|
+
test("--out elsewhere: a previous run's bundle is left out of the payload", async () => {
|
|
133
|
+
const appDir = mkdtempSync(join(tmpdir(), "nd-payload-out-"));
|
|
134
|
+
mkdirSync(join(appDir, "src"), { recursive: true });
|
|
135
|
+
writeFileSync(join(appDir, "package.json"), JSON.stringify({ name: "fixture", version: "1.0.0" }));
|
|
136
|
+
writeFileSync(join(appDir, "src", "main.tsx"), "export {};\n");
|
|
137
|
+
mkdirSync(join(appDir, "dist"), { recursive: true });
|
|
138
|
+
writeFileSync(join(appDir, "dist", "main.tsx"), "export {};\n");
|
|
139
|
+
// What `nd package mac` wrote last time, in the default output dir.
|
|
140
|
+
mkdirSync(join(appDir, "dist", "mac", "Fixture.app", "Contents", "Frameworks"), { recursive: true });
|
|
141
|
+
writeFileSync(join(appDir, "dist", "mac", "Fixture.app", "Contents", "Frameworks", "big.bin"), "x");
|
|
142
|
+
|
|
143
|
+
const appRoot = join(mkdtempSync(join(tmpdir(), "nd-payload-out2-")), "mac", "Fixture.app", "Contents", "Resources", "app");
|
|
144
|
+
await assemblePayload({
|
|
145
|
+
appDir,
|
|
146
|
+
config: { package: { compile: false, entry: "dist/main.tsx" } },
|
|
147
|
+
identity,
|
|
148
|
+
appRoot,
|
|
149
|
+
engine: "system",
|
|
150
|
+
schemes: [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(existsSync(join(appRoot, "dist/main.tsx"))).toBe(true);
|
|
154
|
+
expect(existsSync(join(appRoot, "dist", "mac"))).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
128
157
|
test("root-level entry ships the whole app dir minus node_modules", async () => {
|
|
129
158
|
const root = mkdtempSync(join(tmpdir(), "nd-payload-"));
|
|
130
159
|
const appDir = join(root, "app");
|
|
@@ -140,6 +169,8 @@ describe("assemblePayload", () => {
|
|
|
140
169
|
config: { package: { entry: "main.tsx", compile: false } },
|
|
141
170
|
identity,
|
|
142
171
|
appRoot,
|
|
172
|
+
engine: "system",
|
|
173
|
+
schemes: [],
|
|
143
174
|
});
|
|
144
175
|
|
|
145
176
|
expect(result).toEqual({ entry: "main.tsx", cwd: ".", pluginPaths: [] });
|
package/src/package/payload.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// workspace-relative path and relative imports keep resolving packaged.
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { buildNativePlugins, type NativeDesktopConfig, type PackageConfig } from "../config.ts";
|
|
7
|
+
import { buildNativePlugins, type NativeDesktopConfig, type PackageConfig, type WebViewEngine } from "../config.ts";
|
|
8
8
|
import type { ResolvedIdentity } from "./identity.ts";
|
|
9
9
|
import { assertResolvableEntries, flattenRuntimeModules } from "./modules.ts";
|
|
10
10
|
|
|
@@ -40,6 +40,12 @@ export interface PayloadOptions {
|
|
|
40
40
|
identity: ResolvedIdentity;
|
|
41
41
|
/** The bundle's app root (Resources/app or AppDir/app). */
|
|
42
42
|
appRoot: string;
|
|
43
|
+
/** Engine this bundle was staged for. Recorded in the manifest so the
|
|
44
|
+
* packaged launch path hands the host what `nd dev` hands it, with no shell
|
|
45
|
+
* in between. */
|
|
46
|
+
engine: WebViewEngine;
|
|
47
|
+
/** Launch-declared CEF schemes, recorded for the same reason. */
|
|
48
|
+
schemes: string[];
|
|
43
49
|
/** CLI --entry override (app-relative source entry). */
|
|
44
50
|
entry?: string;
|
|
45
51
|
/** false forces raw source (--no-compile). */
|
|
@@ -108,14 +114,22 @@ export async function assemblePayload(o: PayloadOptions): Promise<PayloadResult>
|
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
// Bundles a previous run left in the output dir. Under the default config
|
|
118
|
+
// they sit inside the compile outDir ("dist/mac", "dist/linux" beside
|
|
119
|
+
// "dist/main.tsx"), so without this a run whose own output lands elsewhere
|
|
120
|
+
// (`--out`) ships the last .app inside the new one, CEF payload and all.
|
|
121
|
+
const bundleOut = resolve(workspaceRoot, pkg.outDir ?? "dist");
|
|
122
|
+
const platformOutputs = new Set([join(bundleOut, "mac"), join(bundleOut, "linux")]);
|
|
123
|
+
const skipBuiltBundles = (src: string) => !platformOutputs.has(resolve(src));
|
|
124
|
+
|
|
111
125
|
mkdirSync(destAppDir, { recursive: true });
|
|
112
126
|
const entryDir = entry.includes("/") ? entry.split("/")[0]! : null;
|
|
113
127
|
if (entryDir) {
|
|
114
|
-
|
|
128
|
+
const skip = (src: string) => skipNodeModules(src) && skipBuiltBundles(src);
|
|
129
|
+
copyPayloadTree(join(appDir, entryDir), join(destAppDir, entryDir), o.appRoot, skip);
|
|
115
130
|
} else {
|
|
116
131
|
// Root-level entry: ship the whole app dir (minus node_modules and any
|
|
117
132
|
// build output nested inside it, which would recurse into the copy).
|
|
118
|
-
const bundleOut = resolve(workspaceRoot, pkg.outDir ?? "dist");
|
|
119
133
|
const skip = (src: string) => skipNodeModules(src) && resolve(src) !== bundleOut;
|
|
120
134
|
copyPayloadTree(appDir, destAppDir, o.appRoot, skip);
|
|
121
135
|
}
|
|
@@ -154,6 +168,8 @@ export async function assemblePayload(o: PayloadOptions): Promise<PayloadResult>
|
|
|
154
168
|
entry: appEntry,
|
|
155
169
|
cwd,
|
|
156
170
|
pluginPaths,
|
|
171
|
+
engine: o.engine,
|
|
172
|
+
schemes: o.schemes,
|
|
157
173
|
}, null, 2)}\n`);
|
|
158
174
|
|
|
159
175
|
return { entry: appEntry, cwd, pluginPaths };
|
package/src/package/templates.ts
CHANGED
|
@@ -47,6 +47,44 @@ export const ENTITLEMENTS_PLIST =
|
|
|
47
47
|
" <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n <true/>\n" +
|
|
48
48
|
"</dict>\n</plist>\n";
|
|
49
49
|
|
|
50
|
+
export interface CefHelperPlistFields {
|
|
51
|
+
/** "<App> Helper (Renderer)" and friends. Also the executable name. */
|
|
52
|
+
name: string;
|
|
53
|
+
id: string;
|
|
54
|
+
version: string;
|
|
55
|
+
minimumSystemVersion: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Info.plist for one CEF helper .app. LSUIElement keeps the five subprocesses
|
|
59
|
+
* out of the Dock and the app switcher; without it every renderer launch would
|
|
60
|
+
* flash a second icon. */
|
|
61
|
+
export function cefHelperPlist(f: CefHelperPlistFields): string {
|
|
62
|
+
return infoPlistSkeleton({
|
|
63
|
+
name: f.name,
|
|
64
|
+
displayName: f.name,
|
|
65
|
+
id: f.id,
|
|
66
|
+
executable: f.name,
|
|
67
|
+
version: f.version,
|
|
68
|
+
minimumSystemVersion: f.minimumSystemVersion,
|
|
69
|
+
}).replace("</dict>\n</plist>", " <key>LSUIElement</key><string>1</string>\n</dict>\n</plist>");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Entitlements for the renderer and GPU helpers. V8 and ANGLE map executable
|
|
73
|
+
* pages the hardened runtime refuses without these, and the renderer loads the
|
|
74
|
+
* CEF framework from the outer app's signature, which library validation would
|
|
75
|
+
* reject. CEF's own sample app ships this exact set. */
|
|
76
|
+
export function cefHelperEntitlements(): string {
|
|
77
|
+
return (
|
|
78
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
79
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' +
|
|
80
|
+
'<plist version="1.0">\n<dict>\n' +
|
|
81
|
+
" <key>com.apple.security.cs.allow-jit</key>\n <true/>\n" +
|
|
82
|
+
" <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n <true/>\n" +
|
|
83
|
+
" <key>com.apple.security.cs.disable-library-validation</key>\n <true/>\n" +
|
|
84
|
+
"</dict>\n</plist>\n"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
export interface AppRunSpec {
|
|
51
89
|
/** App-root-relative script the host boots. */
|
|
52
90
|
entry: string;
|
|
@@ -57,6 +95,10 @@ export interface AppRunSpec {
|
|
|
57
95
|
appId?: string;
|
|
58
96
|
/** App-root-relative native plugin paths. */
|
|
59
97
|
pluginPaths: string[];
|
|
98
|
+
/** Engine the AppDir was staged for. */
|
|
99
|
+
engine?: string;
|
|
100
|
+
/** Launch-declared CEF schemes. */
|
|
101
|
+
schemes?: string[];
|
|
60
102
|
}
|
|
61
103
|
|
|
62
104
|
export function appRunTemplate(s: AppRunSpec): string {
|
|
@@ -66,6 +108,11 @@ export function appRunTemplate(s: AppRunSpec): string {
|
|
|
66
108
|
`export ND_SCRIPT="$HERE/app/${s.entry}"\n` +
|
|
67
109
|
'export PATH="$HERE/usr/bin:$PATH"\n';
|
|
68
110
|
if (s.appId) script += `export ND_APP_ID="${s.appId}"\n`;
|
|
111
|
+
// Every CEF process reads both of these, and both have to be settled before
|
|
112
|
+
// cef_initialize. The :- form keeps an explicit override ahead of the bundle,
|
|
113
|
+
// matching how the mac bootstrap defers to an already-set variable.
|
|
114
|
+
if (s.engine) script += `export ND_WEBVIEW_ENGINE="\${ND_WEBVIEW_ENGINE:-${s.engine}}"\n`;
|
|
115
|
+
if (s.schemes?.length) script += `export ND_CEF_SCHEMES="\${ND_CEF_SCHEMES:-${s.schemes.join(",")}}"\n`;
|
|
69
116
|
if (s.pluginPaths.length) {
|
|
70
117
|
script += 'export ND_PLUGINS="1"\n';
|
|
71
118
|
script += `export ND_PLUGIN_PATHS="${s.pluginPaths.map((p) => `$HERE/app/${p}`).join(":")}"\n`;
|