@akanjs/devkit 3.0.0-beta.31 → 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.
- package/akanConfig/akanConfig.test.ts +12 -0
- package/akanConfig/akanConfig.ts +34 -2
- package/akanConfig/types.ts +1 -0
- package/mobile/desktopBin.test.ts +5 -0
- package/mobile/desktopBin.ts +9 -8
- package/mobile/desktopServerStage.test.ts +49 -1
- package/mobile/desktopServerStage.ts +45 -5
- package/mobile/nativeApp.test.ts +7 -0
- package/mobile/nativeApp.ts +47 -9
- package/mobile/nativeConfig.test.ts +24 -0
- package/mobile/nativeConfig.ts +2 -1
- package/package.json +2 -2
|
@@ -477,6 +477,18 @@ describe("AkanAppConfig", () => {
|
|
|
477
477
|
).toThrow("unknown basePath");
|
|
478
478
|
});
|
|
479
479
|
|
|
480
|
+
test("a desktop server is true or { omit }, its names trimmed, deduplicated and sorted", () => {
|
|
481
|
+
const resolve = (server: unknown) =>
|
|
482
|
+
new AkanAppConfig(app, [], packageJson, { native: { desktop: { server } } } as never, baseDevEnv).native.targets
|
|
483
|
+
.default.desktop?.server;
|
|
484
|
+
|
|
485
|
+
expect(resolve(true)).toBe(true);
|
|
486
|
+
expect(resolve({ omit: [" rclnodejs", "protobufjs", "rclnodejs"] })).toEqual({ omit: ["protobufjs", "rclnodejs"] });
|
|
487
|
+
expect(resolve({})).toEqual({ omit: [] });
|
|
488
|
+
expect(() => resolve({ omit: "rclnodejs" })).toThrow("native.desktop.server.omit in apps/");
|
|
489
|
+
expect(() => resolve({ exclude: ["rclnodejs"] })).toThrow("native.desktop.server.exclude in apps/");
|
|
490
|
+
});
|
|
491
|
+
|
|
480
492
|
test("an app without basePaths has one target, default, with no basePath and the section's settings", () => {
|
|
481
493
|
const config = new AkanAppConfig(
|
|
482
494
|
app,
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type AkanNativeAppConfig,
|
|
18
18
|
type AkanNativeAppResult,
|
|
19
19
|
type AkanNativeDeepLinks,
|
|
20
|
+
type AkanNativeDesktopConfig,
|
|
20
21
|
type AkanNativeSettings,
|
|
21
22
|
type AkanNativeTarget,
|
|
22
23
|
type AkanNativeUpdatesConfig,
|
|
@@ -309,7 +310,15 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
309
310
|
...(merged.android
|
|
310
311
|
? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
|
|
311
312
|
: {}),
|
|
312
|
-
...(merged.desktop
|
|
313
|
+
...(merged.desktop
|
|
314
|
+
? {
|
|
315
|
+
desktop: AkanAppConfig.#desktopServer(
|
|
316
|
+
AkanAppConfig.#platformIndexPath(merged.desktop),
|
|
317
|
+
`${where}.desktop.server`,
|
|
318
|
+
configPath,
|
|
319
|
+
),
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
313
322
|
appName: merged.appName ?? this.app.name,
|
|
314
323
|
appId: merged.appId ?? appId,
|
|
315
324
|
version: merged.version ?? version,
|
|
@@ -321,6 +330,22 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
321
330
|
);
|
|
322
331
|
return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
|
|
323
332
|
}
|
|
333
|
+
//? `true` or `{ omit }`: each omitted name trimmed once, so two targets that omit the same packages stage one server.
|
|
334
|
+
static #desktopServer<T extends { server?: AkanNativeDesktopConfig["server"] }>(
|
|
335
|
+
desktop: T,
|
|
336
|
+
where: string,
|
|
337
|
+
configPath: string,
|
|
338
|
+
): T {
|
|
339
|
+
const { server } = desktop;
|
|
340
|
+
if (server === undefined || typeof server === "boolean") return desktop;
|
|
341
|
+
if (!AkanAppConfig.#isPlainObject(server))
|
|
342
|
+
throw new Error(`${where} in ${configPath} must be true or { omit: string[] }.`);
|
|
343
|
+
const omit = server.omit;
|
|
344
|
+
if (omit !== undefined && (!Array.isArray(omit) || omit.some((name) => typeof name !== "string" || !name.trim())))
|
|
345
|
+
throw new Error(`${where}.omit in ${configPath} must be a list of package names.`);
|
|
346
|
+
const names = [...new Set((omit ?? []).map((name) => name.trim()))].sort();
|
|
347
|
+
return { ...desktop, server: { omit: names } };
|
|
348
|
+
}
|
|
324
349
|
static #platformIndexPath<T extends { indexPath?: string }>(section: T): T {
|
|
325
350
|
const { indexPath: rawIndexPath, ...rest } = section;
|
|
326
351
|
const indexPath = normalizeIndexPath(rawIndexPath);
|
|
@@ -376,7 +401,8 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
376
401
|
"activity",
|
|
377
402
|
"files",
|
|
378
403
|
],
|
|
379
|
-
desktop: ["indexPath", "server", "recovery", "window", "screenCapture"],
|
|
404
|
+
desktop: ["indexPath", "server", "recovery", "window", "screenCapture", "entitlements"],
|
|
405
|
+
desktopServer: ["omit"],
|
|
380
406
|
deepLinks: ["schemes", "domains"],
|
|
381
407
|
} as const;
|
|
382
408
|
//* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
|
|
@@ -408,6 +434,12 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
408
434
|
check(settings.ios, AkanAppConfig.#nativeKeys.ios, `${where}.ios`, "ios");
|
|
409
435
|
check(settings.android, AkanAppConfig.#nativeKeys.android, `${where}.android`, "android");
|
|
410
436
|
check(settings.desktop, AkanAppConfig.#nativeKeys.desktop, `${where}.desktop`, "desktop");
|
|
437
|
+
check(
|
|
438
|
+
settings.desktop?.server,
|
|
439
|
+
AkanAppConfig.#nativeKeys.desktopServer,
|
|
440
|
+
`${where}.desktop.server`,
|
|
441
|
+
"desktopServer",
|
|
442
|
+
);
|
|
411
443
|
check(settings.deepLinks, AkanAppConfig.#nativeKeys.deepLinks, `${where}.deepLinks`, "deepLinks");
|
|
412
444
|
}
|
|
413
445
|
static readonly #androidFilePattern = /^(?:res\/[^/]+\/[^/]+|assets\/.+)$/;
|
package/akanConfig/types.ts
CHANGED
|
@@ -48,6 +48,11 @@ afterAll(() => files.stop(true));
|
|
|
48
48
|
const hash = (bytes: Uint8Array | string) => new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
49
49
|
|
|
50
50
|
describe("DesktopBin", () => {
|
|
51
|
+
test("a build carries the file of the CPU it is for", () => {
|
|
52
|
+
expect(DesktopBin.platform("x64")).toBe(`${process.platform}-x64` as BinPlatform);
|
|
53
|
+
expect(DesktopBin.platform("ia32")).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
51
56
|
test("carries the app's own entries and those of the libs it depends on, the app's first", () => {
|
|
52
57
|
const own = { ffmpeg: { [here]: { path: "/a/ffmpeg" } } };
|
|
53
58
|
const lib = { ffmpeg: { [here]: { path: "/lib/ffmpeg" } }, ffprobe: { [here]: { path: "/lib/ffprobe" } } };
|
package/mobile/desktopBin.ts
CHANGED
|
@@ -11,18 +11,19 @@ import {
|
|
|
11
11
|
import type { App } from "../commandDecorators";
|
|
12
12
|
|
|
13
13
|
type BinPlatforms = AkanBinConfig[string];
|
|
14
|
+
export type DesktopArch = "arm64" | "x64";
|
|
14
15
|
|
|
15
|
-
//* A desktop app is built on
|
|
16
|
-
//* byte into the folder the app carries as `desktop.bin`: no extended attribute, so
|
|
17
|
-
//* into the bundle.
|
|
16
|
+
//* A desktop app is built on its own OS, for this computer's CPU or the one `--arch` names, so only that platform's file
|
|
17
|
+
//* is fetched. It is copied byte for byte into the folder the app carries as `desktop.bin`: no extended attribute, so
|
|
18
|
+
//* no quarantine flag, travels into the bundle.
|
|
18
19
|
export class DesktopBin {
|
|
19
20
|
constructor(
|
|
20
21
|
readonly app: App,
|
|
21
22
|
readonly config: AkanAppConfig,
|
|
22
23
|
) {}
|
|
23
24
|
|
|
24
|
-
static platform(): BinPlatform | null {
|
|
25
|
-
const platform = `${process.platform}-${
|
|
25
|
+
static platform(arch: string = process.arch): BinPlatform | null {
|
|
26
|
+
const platform = `${process.platform}-${arch}`;
|
|
26
27
|
return (binPlatforms as readonly string[]).includes(platform) ? (platform as BinPlatform) : null;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -76,14 +77,14 @@ export class DesktopBin {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/** Copies each chosen executable into `dir`, emptied first, and answers the names it carries. */
|
|
79
|
-
async stage(dir: string): Promise<string[]> {
|
|
80
|
+
async stage(dir: string, arch: DesktopArch = process.arch as DesktopArch): Promise<string[]> {
|
|
80
81
|
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? (await this.app.scan({ write: false }));
|
|
81
82
|
const chosen = DesktopBin.select(this.config, scanInfo.getLibs());
|
|
82
83
|
if (!chosen.size) return [];
|
|
83
|
-
const platform = DesktopBin.platform();
|
|
84
|
+
const platform = DesktopBin.platform(arch);
|
|
84
85
|
if (!platform)
|
|
85
86
|
throw new Error(
|
|
86
|
-
`bin: this
|
|
87
|
+
`bin: this desktop app is for ${process.platform}-${arch}, and a bin names only ${binPlatforms.join(", ")}.`,
|
|
87
88
|
);
|
|
88
89
|
await rm(dir, { recursive: true, force: true });
|
|
89
90
|
await mkdir(dir, { recursive: true });
|
|
@@ -23,7 +23,7 @@ const rootPackageJson: PackageJson = {
|
|
|
23
23
|
name: "repo",
|
|
24
24
|
version: "1.0.0",
|
|
25
25
|
description: "repo",
|
|
26
|
-
dependencies: { "@external/runtime": "2.0.0", pg: "8.0.0" },
|
|
26
|
+
dependencies: { "@external/runtime": "2.0.0", pg: "8.0.0", rclnodejs: "1.0.0" },
|
|
27
27
|
};
|
|
28
28
|
const appConfig = (config: object = {}) =>
|
|
29
29
|
new AkanAppConfig({ name: "portal" } as never, [], rootPackageJson, config, baseDevEnv);
|
|
@@ -59,6 +59,17 @@ describe("DesktopServerStage", () => {
|
|
|
59
59
|
for (const driver of drivers) expect(Object.keys(built.dependencies ?? {})).toContain(driver);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test("leaves out what desktop.server.omit names, an external lib included, while the image keeps it", () => {
|
|
63
|
+
const config = appConfig({ externalLibs: ["rclnodejs", "@external/runtime"] });
|
|
64
|
+
const built = config.getProductionPackageJson();
|
|
65
|
+
|
|
66
|
+
const { dependencies } = DesktopServerStage.packageJson(config, built, ["rclnodejs"]);
|
|
67
|
+
|
|
68
|
+
expect(Object.keys(dependencies ?? {})).toContain("@external/runtime");
|
|
69
|
+
expect(Object.keys(dependencies ?? {})).not.toContain("rclnodejs");
|
|
70
|
+
expect(Object.keys(config.getProductionPackageJson().dependencies ?? {})).toContain("rclnodejs");
|
|
71
|
+
});
|
|
72
|
+
|
|
62
73
|
test("keeps the image's trusted packages, so their install scripts run in the app's server too", () => {
|
|
63
74
|
const config = appConfig({ trustedDependencies: ["@external/runtime"], externalLibs: ["@external/runtime"] });
|
|
64
75
|
expect(DesktopServerStage.packageJson(config, config.getProductionPackageJson()).trustedDependencies).toEqual([
|
|
@@ -148,6 +159,43 @@ describe("DesktopServerStage", () => {
|
|
|
148
159
|
expect(Object.keys(installed.dependencies ?? {}).sort()).toEqual(["react", "react-dom"]);
|
|
149
160
|
});
|
|
150
161
|
|
|
162
|
+
test("an omitted package another dependency still installs stops the stage, naming who needs it", async () => {
|
|
163
|
+
const installing =
|
|
164
|
+
(pulled: boolean) =>
|
|
165
|
+
async (...args: unknown[]) => {
|
|
166
|
+
const nodeModules = path.join((args[2] as { cwd: string }).cwd, "node_modules");
|
|
167
|
+
await write(path.join(nodeModules, "react/package.json"), JSON.stringify({ name: "react" }));
|
|
168
|
+
if (pulled) {
|
|
169
|
+
await write(path.join(nodeModules, "rclnodejs/package.json"), JSON.stringify({ name: "rclnodejs" }));
|
|
170
|
+
await write(
|
|
171
|
+
path.join(nodeModules, "@robot/bridge/package.json"),
|
|
172
|
+
JSON.stringify({ name: "@robot/bridge", dependencies: { rclnodejs: "^1" } }),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
};
|
|
177
|
+
await (await stageApp(installing(false))).stage.install(undefined, ["rclnodejs"]);
|
|
178
|
+
await expect((await stageApp(installing(true))).stage.install(undefined, ["rclnodejs"])).rejects.toThrow(
|
|
179
|
+
"another dependency still installs: rclnodejs (needed by @robot/bridge)",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("installs another CPU's optional packages for that CPU", () => {
|
|
184
|
+
const other = process.arch === "arm64" ? "x64" : "arm64";
|
|
185
|
+
expect(DesktopServerStage.installArgs()).toEqual(["install", "--production", "--prefer-offline"]);
|
|
186
|
+
expect(DesktopServerStage.installArgs(process.arch as "arm64" | "x64")).toEqual([
|
|
187
|
+
"install",
|
|
188
|
+
"--production",
|
|
189
|
+
"--prefer-offline",
|
|
190
|
+
]);
|
|
191
|
+
expect(DesktopServerStage.installArgs(other)).toEqual([
|
|
192
|
+
"install",
|
|
193
|
+
"--production",
|
|
194
|
+
"--prefer-offline",
|
|
195
|
+
`--cpu=${other}`,
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
151
199
|
test("an install that fails says it needs the registry or a Bun cache that holds the packages", async () => {
|
|
152
200
|
const { stage } = await stageApp(async () => {
|
|
153
201
|
throw new Error("error: GET https://registry.npmjs.org/scheduler - ConnectionRefused");
|
|
@@ -85,7 +85,8 @@ export class DesktopServerStage {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
//? `omit` (the target's `desktop.server.omit`) wins over externalLibs: the image keeps those packages, the app does not.
|
|
89
|
+
static packageJson(config: AkanAppConfig, built: PackageJson, omit: string[] = []): PackageJson {
|
|
89
90
|
const otherDrivers = new Set(
|
|
90
91
|
config.database.modes
|
|
91
92
|
.filter((mode) => mode !== "single")
|
|
@@ -93,24 +94,47 @@ export class DesktopServerStage {
|
|
|
93
94
|
);
|
|
94
95
|
const dependencies = Object.entries(built.dependencies ?? {}).filter(
|
|
95
96
|
([name]) =>
|
|
96
|
-
|
|
97
|
+
!omit.includes(name) &&
|
|
98
|
+
(config.externalLibs.includes(name) || (name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name))),
|
|
97
99
|
);
|
|
98
100
|
return { ...built, dependencies: Object.fromEntries(dependencies) };
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/** The packages in `nodeModules` that depend on `name`: what still pulls an omitted package in. */
|
|
104
|
+
static async dependentsOf(nodeModules: string, name: string): Promise<string[]> {
|
|
105
|
+
const dependents = new Set<string>();
|
|
106
|
+
for await (const manifest of new Bun.Glob("**/package.json").scan({ cwd: nodeModules, onlyFiles: true })) {
|
|
107
|
+
const json = (await Bun.file(path.join(nodeModules, manifest))
|
|
108
|
+
.json()
|
|
109
|
+
.catch(() => null)) as PackageJson | null;
|
|
110
|
+
if (!json?.name || json.name === name) continue;
|
|
111
|
+
const declared = { ...json.dependencies, ...json.optionalDependencies, ...json.peerDependencies };
|
|
112
|
+
if (name in declared) dependents.add(json.name);
|
|
113
|
+
}
|
|
114
|
+
return [...dependents].sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//? `--cpu` installs another CPU's optional packages: an addon's prebuilt binary for the app's CPU, not this computer's.
|
|
118
|
+
static installArgs(arch?: "arm64" | "x64"): string[] {
|
|
119
|
+
return ["install", "--production", "--prefer-offline", ...(arch && arch !== process.arch ? [`--cpu=${arch}`] : [])];
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
//* The packages come from the config alone, so they install before `akan build` runs: a machine that cannot install
|
|
102
123
|
//* them stops at once instead of after the whole build.
|
|
103
|
-
async install() {
|
|
124
|
+
async install(arch?: "arm64" | "x64", omit: string[] = []) {
|
|
104
125
|
const config = await this.app.getConfig();
|
|
105
126
|
DesktopServerStage.assertCarriable(config);
|
|
106
127
|
await rm(this.dir, { recursive: true, force: true });
|
|
107
128
|
await mkdir(this.dir, { recursive: true });
|
|
129
|
+
const built = config.getProductionPackageJson();
|
|
130
|
+
for (const name of omit.filter((name) => !(name in (built.dependencies ?? {}))))
|
|
131
|
+
this.app.logger.warn(`desktop.server.omit names ${name}, which the server does not depend on directly.`);
|
|
108
132
|
await writeFile(
|
|
109
133
|
path.join(this.dir, "package.json"),
|
|
110
|
-
JSON.stringify(DesktopServerStage.packageJson(config,
|
|
134
|
+
JSON.stringify(DesktopServerStage.packageJson(config, built, omit), null, 2),
|
|
111
135
|
);
|
|
112
136
|
try {
|
|
113
|
-
await this.app.spawn(process.execPath,
|
|
137
|
+
await this.app.spawn(process.execPath, DesktopServerStage.installArgs(arch), { cwd: this.dir });
|
|
114
138
|
} catch (error) {
|
|
115
139
|
throw new Error(
|
|
116
140
|
`The desktop app's server could not install its packages in ${this.dir}: it needs the npm registry, or a Bun cache that already holds every one of them.\n${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -119,6 +143,22 @@ export class DesktopServerStage {
|
|
|
119
143
|
}
|
|
120
144
|
//? Launcher links only: a link out of the app bundle breaks its signature, and the server runs none of them.
|
|
121
145
|
await rm(path.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
146
|
+
await this.#assertOmitted(omit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//? Leaving a package out of package.json keeps out only what nothing else needs; another dependency still installs it.
|
|
150
|
+
async #assertOmitted(omit: string[]) {
|
|
151
|
+
const nodeModules = path.join(this.dir, "node_modules");
|
|
152
|
+
const kept: string[] = [];
|
|
153
|
+
for (const name of omit) {
|
|
154
|
+
if (!(await Bun.file(path.join(nodeModules, name, "package.json")).exists())) continue;
|
|
155
|
+
const dependents = await DesktopServerStage.dependentsOf(nodeModules, name);
|
|
156
|
+
kept.push(`${name} (needed by ${dependents.join(", ") || "a package that names it"})`);
|
|
157
|
+
}
|
|
158
|
+
if (kept.length)
|
|
159
|
+
throw new Error(
|
|
160
|
+
`desktop.server.omit leaves out packages another dependency still installs: ${kept.join("; ")}. Omit those too, or keep the package.`,
|
|
161
|
+
);
|
|
122
162
|
}
|
|
123
163
|
|
|
124
164
|
//? Copies the build beside what `install` put in the stage, so it runs after both.
|
package/mobile/nativeApp.test.ts
CHANGED
|
@@ -86,6 +86,13 @@ describe("NativeApp", () => {
|
|
|
86
86
|
).toMatch(/^portal\/admin ios ready · iPhone 16 · http:\/\/localhost:8283 · 12\.\ds$/);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
test("an installer is what a person downloads, on the desktops that have one", () => {
|
|
90
|
+
expect(() => NativeApp.assertInstaller("windows", true)).not.toThrow();
|
|
91
|
+
expect(() => NativeApp.assertInstaller("macos", true)).not.toThrow();
|
|
92
|
+
expect(() => NativeApp.assertInstaller("linux", true)).not.toThrow();
|
|
93
|
+
expect(() => NativeApp.assertInstaller("ios", true)).toThrow(/not for ios/);
|
|
94
|
+
});
|
|
95
|
+
|
|
89
96
|
test("opens a dev build on its target's home, the CSR shell under the locale", () => {
|
|
90
97
|
expect(new NativeApp(fakeApp(), target({ indexPath: "/explore" })).startPath("en", "ios")).toBe(
|
|
91
98
|
"/en/explore?csr=true&akanMobileTarget=default&akanMobileIndexPath=%2Fexplore",
|
package/mobile/nativeApp.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AkanNativeConfig } from "@akanjs/native/config";
|
|
|
3
3
|
import type { NativeEnv } from "akanjs";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
5
|
import { Executor } from "../executors";
|
|
6
|
-
import { DesktopBin } from "./desktopBin";
|
|
6
|
+
import { type DesktopArch, DesktopBin } from "./desktopBin";
|
|
7
7
|
import type { DesktopServerBundle } from "./desktopServerStage";
|
|
8
8
|
import {
|
|
9
9
|
type DesktopPlatform,
|
|
@@ -38,8 +38,10 @@ export interface NativeBuildOptions {
|
|
|
38
38
|
profile?: "debug" | "release";
|
|
39
39
|
/** The server a desktop app carries (the target's `desktop.server`), staged by DesktopServerStage. */
|
|
40
40
|
server?: DesktopServerBundle;
|
|
41
|
-
/**
|
|
41
|
+
/** What a person downloads beside the app: a Windows NSIS setup program, a macOS dmg, a Linux AppImage. */
|
|
42
42
|
installer?: boolean;
|
|
43
|
+
/** The CPU the desktop app runs on, of the same OS; a macOS app is arm64 only. Default this computer's. */
|
|
44
|
+
arch?: DesktopArch;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export interface NativeDevOptions extends NativeRunOptions {
|
|
@@ -104,10 +106,12 @@ export class NativeApp {
|
|
|
104
106
|
server,
|
|
105
107
|
platform,
|
|
106
108
|
stageBin = true,
|
|
109
|
+
arch,
|
|
107
110
|
}: {
|
|
108
111
|
server?: DesktopServerBundle;
|
|
109
112
|
platform: NativePlatform;
|
|
110
113
|
stageBin?: boolean;
|
|
114
|
+
arch?: DesktopArch;
|
|
111
115
|
}) {
|
|
112
116
|
const [appConfig, plugins, nativePlugins] = await Promise.all([
|
|
113
117
|
this.app.getConfig(),
|
|
@@ -115,7 +119,7 @@ export class NativeApp {
|
|
|
115
119
|
NativePluginFolders.of(this.app),
|
|
116
120
|
]);
|
|
117
121
|
const desktop = platform === "macos" || platform === "windows" || platform === "linux";
|
|
118
|
-
const carried = desktop && stageBin ? await new DesktopBin(this.app, appConfig).stage(this.binDir) : [];
|
|
122
|
+
const carried = desktop && stageBin ? await new DesktopBin(this.app, appConfig).stage(this.binDir, arch) : [];
|
|
119
123
|
return NativeConfig.build({
|
|
120
124
|
appPath: this.app.cwdPath,
|
|
121
125
|
target: this.target.config,
|
|
@@ -131,10 +135,10 @@ export class NativeApp {
|
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
/** The API and the config it is about to build, refused here when the runtime would refuse it later. */
|
|
134
|
-
async prepare(platform: NativePlatform, server?: DesktopServerBundle) {
|
|
138
|
+
async prepare(platform: NativePlatform, server?: DesktopServerBundle, arch?: DesktopArch) {
|
|
135
139
|
const [api, { config, warnings }] = await Promise.all([
|
|
136
140
|
NativeApi.load(this.app.cwdPath),
|
|
137
|
-
this.config({ platform, ...(server ? { server } : {}) }),
|
|
141
|
+
this.config({ platform, ...(server ? { server } : {}), ...(arch ? { arch } : {}) }),
|
|
138
142
|
]);
|
|
139
143
|
for (const warning of warnings) this.app.logger.warn(warning);
|
|
140
144
|
this.#assertValid(api, config);
|
|
@@ -175,15 +179,35 @@ export class NativeApp {
|
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
|
|
178
|
-
async build(
|
|
182
|
+
async build(
|
|
183
|
+
platform: NativePlatform,
|
|
184
|
+
{ profile = "release", server, installer = false, arch }: NativeBuildOptions = {},
|
|
185
|
+
) {
|
|
179
186
|
NativeApp.#assertServerPlatform(platform, server);
|
|
180
|
-
|
|
187
|
+
NativeApp.assertInstaller(platform, installer);
|
|
181
188
|
await this.assembleWeb(platform);
|
|
182
|
-
const { api, config } = await this.prepare(platform, server);
|
|
189
|
+
const { api, config } = await this.prepare(platform, server, arch);
|
|
183
190
|
return await api.build({
|
|
184
191
|
...this.#task(platform, config),
|
|
185
192
|
profile,
|
|
186
|
-
...(
|
|
193
|
+
...(arch ? { arch } : {}),
|
|
194
|
+
...(platform === "windows"
|
|
195
|
+
? {
|
|
196
|
+
windows: {
|
|
197
|
+
...(installer ? { installer } : {}),
|
|
198
|
+
...(profile === "release" ? NativeApp.#windowsSigning(api) : {}),
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
: {}),
|
|
202
|
+
...(platform === "macos"
|
|
203
|
+
? {
|
|
204
|
+
macos: {
|
|
205
|
+
...(profile === "release" ? api.macosDistributionFromEnv(process.env) : {}),
|
|
206
|
+
...(installer ? { dmg: true } : {}),
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
...(platform === "linux" && installer ? { linux: { appImage: true } } : {}),
|
|
187
211
|
});
|
|
188
212
|
}
|
|
189
213
|
|
|
@@ -280,6 +304,9 @@ export class NativeApp {
|
|
|
280
304
|
platform,
|
|
281
305
|
out: this.updatesDir,
|
|
282
306
|
...(channel ? { channel } : {}),
|
|
307
|
+
//? The updater keeps the installed app's signature, so a release is signed as the downloaded app was.
|
|
308
|
+
...(platform === "macos" ? { macos: api.macosDistributionFromEnv(process.env) } : {}),
|
|
309
|
+
...(platform === "windows" ? { windows: NativeApp.#windowsSigning(api) } : {}),
|
|
283
310
|
});
|
|
284
311
|
}
|
|
285
312
|
|
|
@@ -392,6 +419,17 @@ export class NativeApp {
|
|
|
392
419
|
return `/${home.join("/")}?${params}`;
|
|
393
420
|
}
|
|
394
421
|
|
|
422
|
+
static #windowsSigning(api: NativeBuildApiModule) {
|
|
423
|
+
const signing = api.windowsSigningFromEnv(process.env);
|
|
424
|
+
return signing ? { signing } : {};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//* `installer` is the file a person downloads: a setup program on Windows, a disk image on macOS, an AppImage on Linux.
|
|
428
|
+
static assertInstaller(platform: NativePlatform, installer: boolean) {
|
|
429
|
+
if (installer && (platform === "ios" || platform === "android"))
|
|
430
|
+
throw new Error(`An installer is built for a desktop app (Windows, macOS, Linux), not for ${platform}.`);
|
|
431
|
+
}
|
|
432
|
+
|
|
395
433
|
//? A phone runs no Bun, so only a desktop app can carry the server.
|
|
396
434
|
static #assertServerPlatform(platform: NativePlatform, server: DesktopServerBundle | undefined) {
|
|
397
435
|
if (server && (platform === "ios" || platform === "android"))
|
|
@@ -341,6 +341,30 @@ describe("NativeConfig.build", () => {
|
|
|
341
341
|
expect((await NativeApi.load(repoApp)).validateConfig(config, { appDir: root })).toEqual([]);
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
+
test("a desktop app's own entitlements reach the macOS build, and the runtime accepts them", async () => {
|
|
345
|
+
const root = await makeTempRoot();
|
|
346
|
+
const entitlements = { "com.apple.security.cs.disable-library-validation": true };
|
|
347
|
+
const { config } = NativeConfig.build({
|
|
348
|
+
appPath: root,
|
|
349
|
+
target: {
|
|
350
|
+
...minimalTarget,
|
|
351
|
+
permissions: [],
|
|
352
|
+
deepLinks: undefined,
|
|
353
|
+
android: undefined,
|
|
354
|
+
desktop: { entitlements },
|
|
355
|
+
},
|
|
356
|
+
webDir: path.join(root, "web"),
|
|
357
|
+
contributions: [],
|
|
358
|
+
locales: ["en"],
|
|
359
|
+
platform: "macos",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(config.native?.macos).toEqual({ entitlements });
|
|
363
|
+
await mkdir(path.join(root, "web"), { recursive: true });
|
|
364
|
+
await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
|
|
365
|
+
expect((await NativeApi.load(repoApp)).validateConfig(config, { appDir: root })).toEqual([]);
|
|
366
|
+
});
|
|
367
|
+
|
|
344
368
|
test("an app with updates ships the updates plugin and its settings", () => {
|
|
345
369
|
const updates = { url: "https://releases.example.com/board", publicKey: `${"a".repeat(43)}=`, channel: "pilot" };
|
|
346
370
|
const { config } = NativeConfig.build({
|
package/mobile/nativeConfig.ts
CHANGED
|
@@ -161,7 +161,8 @@ export class NativeConfig {
|
|
|
161
161
|
...Object.entries(target.ios?.files ?? {}).map(([to, from]) => ({ from: abs(from), to: `ios/${to}` })),
|
|
162
162
|
...Object.entries(target.android?.files ?? {}).map(([to, from]) => ({ from: abs(from), to: `android/${to}` })),
|
|
163
163
|
];
|
|
164
|
-
const
|
|
164
|
+
const macos = NativeConfig.#compact({ entitlements: target.desktop?.entitlements });
|
|
165
|
+
const native = NativeConfig.#compact({ ios, macos, android, resources });
|
|
165
166
|
const googleServices = target.android?.googleServices;
|
|
166
167
|
const pushAndroid = target.android?.push;
|
|
167
168
|
const desktop = NativeConfig.#compact<NonNullable<AkanNativeConfig["desktop"]>>({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.32",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@earendil-works/pi-tui": "0.86.1",
|
|
48
48
|
"@inquirer/prompts": "^8.4.3",
|
|
49
49
|
"@tailwindcss/node": "^4.3.0",
|
|
50
|
-
"akanjs": "3.0.0-beta.
|
|
50
|
+
"akanjs": "3.0.0-beta.32",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"dayjs": "^1.11.20",
|