akanjs 3.0.1 → 3.0.2
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/CHANGELOG.md +11 -0
- package/base/base.ts +1 -1
- package/client/cookie.ts +4 -6
- package/common/Logger.ts +10 -0
- package/dictionary/base.dictionary.ts +7 -0
- package/dictionary/trans.ts +7 -1
- package/document/documentQuery.ts +6 -1
- package/document/index.ts +1 -0
- package/document/into.ts +15 -7
- package/document/liveRowScope.ts +79 -0
- package/fetch/client/fetchClient.ts +64 -12
- package/fetch/client/httpClient.ts +13 -4
- package/index.ts +22 -0
- package/package.json +1 -1
- package/server/akanOption.ts +20 -1
- package/server/akanServer.ts +5 -0
- package/server/mcp/McpAuth.ts +3 -2
- package/server/mcp/McpExecutionContext.ts +15 -14
- package/server/mcp/McpRateLimiter.ts +18 -67
- package/server/proxy/webProxyRunner.ts +2 -0
- package/server/resolver/database.resolver.ts +28 -13
- package/server/resolver/signal.resolver.ts +75 -35
- package/server/rscWorkerHost.ts +16 -7
- package/server/webRouter.ts +3 -0
- package/service/index.ts +1 -0
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/rateLimit.adaptor.ts +15 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +119 -27
- package/service/predefinedAdaptor/sql/types.ts +14 -12
- package/service/rateLimiter.ts +80 -0
- package/signal/base.signal.ts +7 -6
- package/signal/endpointRateLimit.ts +159 -0
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +6 -5
- package/signal/serializer/fetch.serializer.ts +15 -4
- package/signal/signalContext.ts +148 -67
- package/signal/slice.ts +30 -9
- package/signal/types.ts +8 -0
- package/store/agent/AgentCursor.ts +2 -1
- package/test/signalTestRuntime.ts +2 -1
- package/types/base/base.d.ts +1 -1
- package/types/client/cookie.d.ts +1 -1
- package/types/common/Logger.d.ts +2 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/documentQuery.d.ts +5 -1
- package/types/document/index.d.ts +1 -0
- package/types/document/into.d.ts +7 -7
- package/types/document/liveRowScope.d.ts +9 -0
- package/types/fetch/client/fetchClient.d.ts +9 -1
- package/types/index.d.ts +32 -0
- package/types/server/akanOption.d.ts +7 -1
- package/types/server/mcp/McpExecutionContext.d.ts +4 -1
- package/types/server/mcp/McpRateLimiter.d.ts +2 -6
- package/types/server/rscWorkerHost.d.ts +3 -0
- package/types/service/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/rateLimit.adaptor.d.ts +10 -0
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -10
- package/types/service/predefinedAdaptor/sql/types.d.ts +13 -17
- package/types/service/rateLimiter.d.ts +24 -0
- package/types/signal/endpointRateLimit.d.ts +37 -0
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +33 -15
- package/types/signal/slice.d.ts +17 -5
- package/types/signal/types.d.ts +8 -0
- package/types/test/signalTestRuntime.d.ts +1 -1
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/ui/recipe/inputRecipe.d.ts +3 -3
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +33 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/bplist.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/dmg.d.ts +59 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/dsStore.d.ts +26 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/macAlias.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +0 -1
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +3 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +6 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/deeplinks.d.ts +9 -2
- package/types/webkit/RenderFailure.d.ts +6 -0
- package/ui/Input.tsx +7 -1
- package/ui/recipe/buttonRecipe.ts +1 -0
- package/ui/recipe/inputRecipe.ts +13 -7
- package/ui/styles.css +1 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +8 -1
- package/vendor/@akanjs/native/native/macos/dmg/background.html +18 -0
- package/vendor/@akanjs/native/native/macos/dmg/background.png +0 -0
- package/vendor/@akanjs/native/native/macos/dmg/background@2x.png +0 -0
- package/vendor/@akanjs/native/packages/cli/src/config.ts +23 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/bplist.ts +91 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/dmg.ts +331 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/dsStore.ts +191 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/macAlias.ts +101 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +1 -28
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +6 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +17 -7
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +6 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +5 -2
- package/vendor/@akanjs/native/packages/desktop/src/deeplinks.ts +33 -10
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +1 -1
- package/webkit/RenderFailure.tsx +32 -0
- package/webkit/RenderLayer.tsx +22 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export interface AliasTarget {
|
|
4
|
+
/** The HFS+ volume name (the disk image's label). */
|
|
5
|
+
volumeName: string;
|
|
6
|
+
/**
|
|
7
|
+
* The volume header's createDate as it stands, in HFS seconds. HFS+ writes it on the local clock of whatever formatted
|
|
8
|
+
* the volume (TN1150), unlike every other date, so it is read off the image rather than computed here.
|
|
9
|
+
*/
|
|
10
|
+
volumeCreated: number;
|
|
11
|
+
/** Where the volume mounts: /Volumes/<name>. */
|
|
12
|
+
volumePath: string;
|
|
13
|
+
kind: "file" | "folder";
|
|
14
|
+
name: string;
|
|
15
|
+
cnid: number;
|
|
16
|
+
created: Date;
|
|
17
|
+
parentName: string;
|
|
18
|
+
parentCnid: number;
|
|
19
|
+
/** The CNIDs of the folders above the target, nearest first, the volume's root left out. */
|
|
20
|
+
cnidPath: number[];
|
|
21
|
+
/** From the volume's root, starting with "/". */
|
|
22
|
+
posixPath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Seconds since 1904-01-01 UTC, the HFS epoch. */
|
|
26
|
+
export function hfsTime(date: Date): number {
|
|
27
|
+
return Math.max(0, Math.floor(date.getTime() / 1000) + 2_082_844_800) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const EJECTABLE_DISK = 5;
|
|
31
|
+
|
|
32
|
+
/** A Carbon name: ':' is the path separator there, so a name spells it '/'. */
|
|
33
|
+
function pascal(text: string, size: number): Uint8Array {
|
|
34
|
+
const bytes = new TextEncoder().encode(text.replaceAll(":", "/")).subarray(0, size - 1);
|
|
35
|
+
const out = new Uint8Array(size);
|
|
36
|
+
out[0] = bytes.length;
|
|
37
|
+
out.set(bytes, 1);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function utf16WithLength(text: string): Uint8Array {
|
|
42
|
+
const out = new Uint8Array(2 + text.length * 2);
|
|
43
|
+
const view = new DataView(out.buffer);
|
|
44
|
+
view.setUint16(0, text.length);
|
|
45
|
+
for (let i = 0; i < text.length; i++) view.setUint16(2 + i * 2, text.charCodeAt(i));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function aliasRecord(target: AliasTarget): Uint8Array {
|
|
50
|
+
const fixed = new Uint8Array(150);
|
|
51
|
+
const view = new DataView(fixed.buffer);
|
|
52
|
+
view.setUint16(6, 2);
|
|
53
|
+
view.setUint16(8, target.kind === "folder" ? 1 : 0);
|
|
54
|
+
fixed.set(pascal(target.volumeName, 28), 10);
|
|
55
|
+
|
|
56
|
+
view.setUint32(38, target.volumeCreated);
|
|
57
|
+
fixed.set(new TextEncoder().encode("H+"), 42);
|
|
58
|
+
view.setUint16(44, EJECTABLE_DISK);
|
|
59
|
+
view.setUint32(46, target.parentCnid);
|
|
60
|
+
fixed.set(pascal(target.name, 64), 50);
|
|
61
|
+
view.setUint32(114, target.cnid);
|
|
62
|
+
view.setUint32(118, hfsTime(target.created));
|
|
63
|
+
view.setInt16(130, -1);
|
|
64
|
+
view.setInt16(132, -1);
|
|
65
|
+
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const cnids = new Uint8Array(target.cnidPath.length * 4);
|
|
68
|
+
for (const [idx, cnid] of target.cnidPath.entries()) new DataView(cnids.buffer).setUint32(idx * 4, cnid);
|
|
69
|
+
const carbonPath = [target.volumeName, ...target.posixPath.split("/").filter(Boolean)]
|
|
70
|
+
.map((part) => part.replaceAll(":", "/"))
|
|
71
|
+
.join(":");
|
|
72
|
+
const extras: [number, Uint8Array][] = [
|
|
73
|
+
[0, encoder.encode(target.parentName.replaceAll(":", "/"))],
|
|
74
|
+
[1, cnids],
|
|
75
|
+
[2, encoder.encode(carbonPath)],
|
|
76
|
+
[14, utf16WithLength(target.name)],
|
|
77
|
+
[15, utf16WithLength(target.volumeName)],
|
|
78
|
+
[18, encoder.encode(target.posixPath)],
|
|
79
|
+
[19, encoder.encode(target.volumePath)],
|
|
80
|
+
];
|
|
81
|
+
const parts: Uint8Array[] = [fixed];
|
|
82
|
+
for (const [tag, data] of extras) {
|
|
83
|
+
const head = new Uint8Array(4);
|
|
84
|
+
new DataView(head.buffer).setInt16(0, tag);
|
|
85
|
+
new DataView(head.buffer).setUint16(2, data.length);
|
|
86
|
+
parts.push(head, data, ...(data.length % 2 ? [new Uint8Array(1)] : []));
|
|
87
|
+
}
|
|
88
|
+
const end = new Uint8Array(4);
|
|
89
|
+
new DataView(end.buffer).setInt16(0, -1);
|
|
90
|
+
parts.push(end);
|
|
91
|
+
|
|
92
|
+
const length = parts.reduce((sum, part) => sum + part.length, 0);
|
|
93
|
+
const record = new Uint8Array(length);
|
|
94
|
+
let at = 0;
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
record.set(part, at);
|
|
97
|
+
at += part.length;
|
|
98
|
+
}
|
|
99
|
+
new DataView(record.buffer).setUint16(4, length);
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { mkdirSync, mkdtempSync, rmSync,
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { exec, execOrThrow } from "./exec.ts";
|
|
@@ -283,33 +283,6 @@ export async function zipForNotary(appPath: string, zip: string): Promise<void>
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
/** A compressed disk image of the app beside a link to /Applications, the window people drag it into. */
|
|
286
|
-
export async function buildDmg(appPath: string, dmgPath: string, volumeName: string, workDir: string): Promise<void> {
|
|
287
|
-
const staging = join(workDir, "dmg");
|
|
288
|
-
rmSync(staging, { recursive: true, force: true });
|
|
289
|
-
mkdirSync(staging, { recursive: true });
|
|
290
|
-
await execOrThrow(["ditto", appPath, join(staging, basename(appPath))], { echo: false });
|
|
291
|
-
symlinkSync("/Applications", join(staging, "Applications"));
|
|
292
|
-
rmSync(dmgPath, { force: true });
|
|
293
|
-
await execOrThrow(
|
|
294
|
-
[
|
|
295
|
-
"hdiutil",
|
|
296
|
-
"create",
|
|
297
|
-
"-volname",
|
|
298
|
-
volumeName,
|
|
299
|
-
"-srcfolder",
|
|
300
|
-
staging,
|
|
301
|
-
"-ov",
|
|
302
|
-
"-fs",
|
|
303
|
-
"HFS+",
|
|
304
|
-
"-format",
|
|
305
|
-
"UDZO",
|
|
306
|
-
dmgPath,
|
|
307
|
-
],
|
|
308
|
-
{ echo: false },
|
|
309
|
-
);
|
|
310
|
-
rmSync(staging, { recursive: true, force: true });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
286
|
/** What Gatekeeper says of a notarized app or disk image, as a downloaded copy would be judged. */
|
|
314
287
|
export async function assessGatekeeper(file: string, type: "execute" | "open"): Promise<void> {
|
|
315
288
|
const args =
|
|
@@ -8,6 +8,7 @@ import { manifestPermissionProblems, type PermissionSet, resolveAcl } from "./ac
|
|
|
8
8
|
import { unknownConfigKeys } from "./configkeys.ts";
|
|
9
9
|
import { validateCsp, validateExternalSchemes } from "./csp.ts";
|
|
10
10
|
import { type DesktopServerConfig, validateDesktopServer } from "./desktop-server.ts";
|
|
11
|
+
import { type ResolvedDmg, validateDmg } from "./dmg.ts";
|
|
11
12
|
import { type EnvConfig, validateEnvConfig } from "./env.ts";
|
|
12
13
|
import { CliError } from "./log.ts";
|
|
13
14
|
import { type PermissionsConfig, validatePermissions } from "./permissions.ts";
|
|
@@ -18,7 +19,8 @@ import { type UpdatesConfig, validateUpdates } from "./updates.ts";
|
|
|
18
19
|
|
|
19
20
|
const CONFIG_FILES = ["akan-native.config.ts", "akan-native.config.js", "akan-native.config.mjs"];
|
|
20
21
|
|
|
21
|
-
export interface ResolvedConfig
|
|
22
|
+
export interface ResolvedConfig
|
|
23
|
+
extends Omit<AkanNativeConfig, "icon" | "splash" | "permissions" | "updates" | "desktop"> {
|
|
22
24
|
app: Required<AkanNativeConfig["app"]>;
|
|
23
25
|
web: Required<Pick<AkanNativeConfig["web"], "dir" | "base">> & { build?: string; devEntry?: string };
|
|
24
26
|
plugins: string[];
|
|
@@ -35,6 +37,7 @@ export interface ResolvedConfig extends Omit<AkanNativeConfig, "icon" | "splash"
|
|
|
35
37
|
server?: DesktopServerConfig;
|
|
36
38
|
/** Absolute. */
|
|
37
39
|
bin?: string;
|
|
40
|
+
dmg: ResolvedDmg;
|
|
38
41
|
};
|
|
39
42
|
updates: UpdatesConfig | null;
|
|
40
43
|
/** Absolute image path. */
|
|
@@ -212,6 +215,7 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
|
|
|
212
215
|
if (bin !== undefined && !(binDir && existsSync(binDir) && statSync(binDir).isDirectory()))
|
|
213
216
|
problems.push(`desktop.bin must be a folder (got ${JSON.stringify(bin)})`);
|
|
214
217
|
const desktopServer = validateDesktopServer(raw.desktop, appDir, problems);
|
|
218
|
+
const dmg = validateDmg(raw.desktop?.dmg, appDir, problems);
|
|
215
219
|
if (raw.keyboard?.resize !== undefined && raw.keyboard.resize !== "resize" && raw.keyboard.resize !== "none") {
|
|
216
220
|
problems.push(`keyboard.resize must be "resize" or "none" (got ${JSON.stringify(raw.keyboard.resize)})`);
|
|
217
221
|
}
|
|
@@ -281,6 +285,7 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
|
|
|
281
285
|
},
|
|
282
286
|
...(desktopServer ? { server: desktopServer } : {}),
|
|
283
287
|
...(binDir ? { bin: binDir } : {}),
|
|
288
|
+
dmg,
|
|
284
289
|
},
|
|
285
290
|
updates,
|
|
286
291
|
icon: icon ? { ...icon, image: resolve(appDir, icon.image) } : null,
|
|
@@ -11,15 +11,20 @@ import type { BuildContext } from "../lib/prepare.ts";
|
|
|
11
11
|
import { ensureInstalled, TOOLCHAIN } from "../lib/toolchains.ts";
|
|
12
12
|
import { targetArch } from "./desktop.ts";
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The launcher entry (freedesktop Desktop Entry): what a menu shows and which link schemes open the app. Named and
|
|
16
|
+
* matched by the app id, the window's program name (native/desktop lib.rs): AppImageLauncher and appimaged rename the
|
|
17
|
+
* file they install, so StartupWMClass is what still ties the window to it.
|
|
18
|
+
*/
|
|
19
|
+
export function desktopEntry(input: { id: string; name: string; fileName: string; schemes: string[] }): string {
|
|
16
20
|
const escape = (value: string) => value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
|
|
17
21
|
return [
|
|
18
22
|
"[Desktop Entry]",
|
|
19
23
|
"Type=Application",
|
|
20
24
|
`Name=${escape(input.name)}`,
|
|
21
25
|
`Exec=${input.fileName} %u`,
|
|
22
|
-
`Icon=${input.
|
|
26
|
+
`Icon=${input.id}`,
|
|
27
|
+
`StartupWMClass=${input.id}`,
|
|
23
28
|
"Terminal=false",
|
|
24
29
|
"Categories=Utility;",
|
|
25
30
|
...(input.schemes.length ? [`MimeType=${input.schemes.map((s) => `x-scheme-handler/${s};`).join("")}`] : []),
|
|
@@ -59,13 +64,18 @@ export async function buildAppImage(ctx: BuildContext, folder: string): Promise<
|
|
|
59
64
|
writeFileSync(join(appDir, "AppRun"), appRun(config.app.fileName));
|
|
60
65
|
chmodSync(join(appDir, "AppRun"), 0o755);
|
|
61
66
|
writeFileSync(
|
|
62
|
-
join(appDir, `${config.app.
|
|
63
|
-
desktopEntry({
|
|
67
|
+
join(appDir, `${config.app.id}.desktop`),
|
|
68
|
+
desktopEntry({
|
|
69
|
+
id: config.app.id,
|
|
70
|
+
name: config.app.name,
|
|
71
|
+
fileName: config.app.fileName,
|
|
72
|
+
schemes: config.deepLinks.schemes,
|
|
73
|
+
}),
|
|
64
74
|
);
|
|
65
75
|
const art = iconArt(config);
|
|
66
76
|
if (art) {
|
|
67
|
-
writeFileSync(join(appDir, `${config.app.
|
|
68
|
-
symlinkSync(`${config.app.
|
|
77
|
+
writeFileSync(join(appDir, `${config.app.id}.png`), encodePng(resize(art.master, 256, 256)));
|
|
78
|
+
symlinkSync(`${config.app.id}.png`, join(appDir, ".DirIcon"));
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
log.step("installer: AppImage");
|
|
@@ -4,8 +4,10 @@ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ParsedArgs } from "../lib/args.ts";
|
|
6
6
|
import { iconArt, windowIcon } from "../lib/icons.ts";
|
|
7
|
+
import { resize } from "../lib/image.ts";
|
|
7
8
|
import type { Launched, LaunchOptions } from "../lib/launch.ts";
|
|
8
9
|
import { dim, log } from "../lib/log.ts";
|
|
10
|
+
import { encodePng } from "../lib/png.ts";
|
|
9
11
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
10
12
|
import {
|
|
11
13
|
buildNativeLibrary,
|
|
@@ -39,7 +41,10 @@ export async function buildLinux(ctx: BuildContext): Promise<string> {
|
|
|
39
41
|
const resources = resourcesOf("linux", dir);
|
|
40
42
|
writeDesktopResources(ctx, resources, "linux");
|
|
41
43
|
const art = iconArt(config);
|
|
42
|
-
if (art)
|
|
44
|
+
if (art) {
|
|
45
|
+
writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
|
|
46
|
+
writeFileSync(join(resources, "icon.png"), encodePng(resize(art.master, 256, 256)));
|
|
47
|
+
}
|
|
43
48
|
if (ctx.linux?.appImage) ctx.artifacts.push({ kind: "installer", path: await buildAppImage(ctx, dir) });
|
|
44
49
|
const size = await folderSize(dir);
|
|
45
50
|
if (size) log.info(dim(`size ${size}`));
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { basename, join } from "node:path";
|
|
16
16
|
import type { ParsedArgs } from "../lib/args.ts";
|
|
17
|
+
import { buildDmg } from "../lib/dmg.ts";
|
|
17
18
|
import { execOrThrow } from "../lib/exec.ts";
|
|
18
19
|
import { icns, iconArt, macosIconImage } from "../lib/icons.ts";
|
|
19
20
|
import type { Launched, LaunchOptions } from "../lib/launch.ts";
|
|
@@ -21,7 +22,6 @@ import { dim, log } from "../lib/log.ts";
|
|
|
21
22
|
import {
|
|
22
23
|
appEntitlements,
|
|
23
24
|
assessGatekeeper,
|
|
24
|
-
buildDmg,
|
|
25
25
|
codesign,
|
|
26
26
|
notarize,
|
|
27
27
|
type ResolvedMacosIdentity,
|
|
@@ -144,7 +144,10 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
|
|
|
144
144
|
if (ctx.macos?.dmg) {
|
|
145
145
|
const dmg = join(outDir, `${config.app.fileName}-${config.app.version}-${ctx.arch ?? hostArch()}.dmg`);
|
|
146
146
|
log.step(`dmg: ${basename(dmg)}`);
|
|
147
|
-
await buildDmg(appPath, dmg, config.app.name, join(outDir, "gen")
|
|
147
|
+
await buildDmg(appPath, dmg, config.app.name, join(outDir, "gen"), {
|
|
148
|
+
layout: config.desktop.dmg,
|
|
149
|
+
volumeIcon: join(resources, "AppIcon.icns"),
|
|
150
|
+
});
|
|
148
151
|
|
|
149
152
|
if (identity.sign !== "-") await codesign(identity, dmg, { hardened: false, timestamp: team });
|
|
150
153
|
if (notarization) {
|
|
@@ -25,12 +25,19 @@ export function desktopExec(exe: string): string {
|
|
|
25
25
|
return `"${exe.replace(/[\\"`$]/g, (c) => `\\${c}`)}" %u`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function desktopEntry(
|
|
28
|
+
export function desktopEntry(
|
|
29
|
+
name: string,
|
|
30
|
+
exe: string,
|
|
31
|
+
schemes: readonly string[],
|
|
32
|
+
{ id, icon = false }: { id?: string; icon?: boolean } = {},
|
|
33
|
+
): string {
|
|
29
34
|
return [
|
|
30
35
|
"[Desktop Entry]",
|
|
31
36
|
"Type=Application",
|
|
32
37
|
`Name=${name.replace(/[\r\n]/g, " ")}`,
|
|
33
38
|
`Exec=${desktopExec(exe)}`,
|
|
39
|
+
...(id && icon ? [`Icon=${id}`] : []),
|
|
40
|
+
...(id ? [`StartupWMClass=${id}`] : []),
|
|
34
41
|
"Terminal=false",
|
|
35
42
|
|
|
36
43
|
"NoDisplay=true",
|
|
@@ -69,17 +76,28 @@ function writeIfChanged(path: string, text: string): boolean {
|
|
|
69
76
|
return true;
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
/** Copies the app's icon into the user's hicolor theme as <app id>.png; false without one. */
|
|
80
|
+
function installIcon(dataHome: string, id: string, icon: string | undefined): boolean {
|
|
81
|
+
if (!icon || !existsSync(icon)) return false;
|
|
82
|
+
const target = join(dataHome, "icons", "hicolor", "256x256", "apps", `${id}.png`);
|
|
83
|
+
const bytes = readFileSync(icon);
|
|
84
|
+
if (existsSync(target) && readFileSync(target).equals(bytes)) return true;
|
|
85
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
86
|
+
writeFileSync(target, bytes);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
async function registerLinux(
|
|
73
91
|
app: { id: string; name: string },
|
|
74
92
|
exe: string,
|
|
75
93
|
schemes: readonly string[],
|
|
76
|
-
env: Env,
|
|
77
|
-
home: string,
|
|
78
|
-
runner: Runner,
|
|
94
|
+
{ env, home, runner, icon }: { env: Env; home: string; runner: Runner; icon?: string },
|
|
79
95
|
): Promise<void> {
|
|
80
96
|
const file = `${app.id}.desktop`;
|
|
81
|
-
const
|
|
82
|
-
const
|
|
97
|
+
const dataHome = xdg(env.XDG_DATA_HOME, join(home, ".local", "share"));
|
|
98
|
+
const applications = join(dataHome, "applications");
|
|
99
|
+
const entry = desktopEntry(app.name, exe, schemes, { id: app.id, icon: installIcon(dataHome, app.id, icon) });
|
|
100
|
+
const entryChanged = writeIfChanged(join(applications, file), entry);
|
|
83
101
|
const listPath = join(xdg(env.XDG_CONFIG_HOME, join(home, ".config")), "mimeapps.list");
|
|
84
102
|
const list = existsSync(listPath) ? readFileSync(listPath, "utf8") : "";
|
|
85
103
|
writeIfChanged(listPath, withDefaults(list, file, schemes));
|
|
@@ -128,16 +146,21 @@ async function registerWindows(
|
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
|
|
131
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Makes this executable the current user's handler of the app's schemes (Windows, Linux). `icon`: a 256 px PNG the
|
|
151
|
+
* Linux entry shows (resources/icon.png).
|
|
152
|
+
*/
|
|
132
153
|
export async function registerDeepLinks(
|
|
133
154
|
app: { id: string; name: string },
|
|
134
155
|
schemes: readonly string[],
|
|
135
|
-
options: { exe?: string; platform?: NodeJS.Platform; env?: Env; home?: string; run?: Runner } = {},
|
|
156
|
+
options: { exe?: string; platform?: NodeJS.Platform; env?: Env; home?: string; run?: Runner; icon?: string } = {},
|
|
136
157
|
): Promise<void> {
|
|
137
158
|
const platform = options.platform ?? process.platform;
|
|
138
159
|
if (!schemes.length || (platform !== "win32" && platform !== "linux")) return;
|
|
139
|
-
const
|
|
160
|
+
const env = options.env ?? process.env;
|
|
161
|
+
|
|
162
|
+
const exe = options.exe ?? (env.APPIMAGE || process.execPath);
|
|
140
163
|
const runner = options.run ?? run;
|
|
141
164
|
if (platform === "win32") await registerWindows(app, exe, schemes, runner);
|
|
142
|
-
else await registerLinux(app, exe, schemes,
|
|
165
|
+
else await registerLinux(app, exe, schemes, { env, home: options.home ?? homedir(), runner, icon: options.icon });
|
|
143
166
|
}
|
|
@@ -447,7 +447,7 @@ export function startHost(plugins: DesktopPlugin[]): void {
|
|
|
447
447
|
if (typeof launch.exit === "number") return;
|
|
448
448
|
|
|
449
449
|
openLinks(process.argv.slice(2));
|
|
450
|
-
registerDeepLinks(boot.app, deepLinkSchemes).catch((error) =>
|
|
450
|
+
registerDeepLinks(boot.app, deepLinkSchemes, { icon: join(paths.resources, "icon.png") }).catch((error) =>
|
|
451
451
|
console.warn("[akan-native] cannot register the deep link schemes", error),
|
|
452
452
|
);
|
|
453
453
|
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Translator } from "akanjs/client";
|
|
3
|
+
import { parseAkanI18nEnv } from "akanjs/common";
|
|
4
|
+
|
|
5
|
+
const errorKeyPattern = /^[a-zA-Z][A-Za-z0-9]*\.error\.[A-Za-z0-9_]+$/;
|
|
6
|
+
|
|
7
|
+
const messageOf = (error: unknown, lang: string) => {
|
|
8
|
+
if (!(error instanceof Error) || !errorKeyPattern.test(error.message))
|
|
9
|
+
return Translator.translateByLocale(lang, "base.pageLoadFailed");
|
|
10
|
+
const { data } = error as { data?: Record<string, string | number> };
|
|
11
|
+
return Translator.translateByLocale(lang, error.message, data);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface RenderFailureProps {
|
|
15
|
+
error: unknown;
|
|
16
|
+
onRetry: () => void;
|
|
17
|
+
}
|
|
18
|
+
export const RenderFailure = ({ error, onRetry }: RenderFailureProps) => {
|
|
19
|
+
const lang = Translator.getActiveLocale() ?? parseAkanI18nEnv().defaultLocale;
|
|
20
|
+
return (
|
|
21
|
+
<div role="alert" className="flex min-h-[60vh] flex-col items-center justify-center gap-4 p-6 text-center">
|
|
22
|
+
<p className="max-w-sm text-foreground/70 text-sm">{messageOf(error, lang)}</p>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={onRetry}
|
|
26
|
+
className="rounded-field bg-primary px-4 py-2 font-medium text-primary-foreground text-sm"
|
|
27
|
+
>
|
|
28
|
+
{Translator.translateByLocale(lang, "base.retry")}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
package/webkit/RenderLayer.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { debugFrame, RouteDefinition, type RouteRender, router, usePathCtx } from "akanjs/client";
|
|
3
3
|
import { isThenable, Logger } from "akanjs/common";
|
|
4
|
-
import { createContext, createElement, memo, type ReactNode, useContext, useEffect, useRef } from "react";
|
|
4
|
+
import { createContext, createElement, memo, type ReactNode, useContext, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { CsrFrameDump } from "./csrFrameDump";
|
|
6
|
+
import { RenderFailure } from "./RenderFailure";
|
|
6
7
|
import { useFetch } from "./useFetch";
|
|
7
8
|
import { usePageActivity } from "./usePageActivity";
|
|
8
9
|
|
|
@@ -17,6 +18,8 @@ interface RenderedRoute {
|
|
|
17
18
|
record: ReturnType<typeof CsrFrameDump.track> | null;
|
|
18
19
|
/** A redirect was issued while the render ran, so what it drew is not the page. */
|
|
19
20
|
redirected: boolean;
|
|
21
|
+
/** The render rejected while its page stayed: what the layer draws instead, with a retry. */
|
|
22
|
+
failure: { error: unknown } | null;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
interface RenderLayerProps {
|
|
@@ -36,6 +39,7 @@ export const RenderLayer = memo(({ renders, index, params, searchParams, leaf =
|
|
|
36
39
|
const routeRender = renders[index];
|
|
37
40
|
const isAsyncRender = isAsyncRouteRender(routeRender);
|
|
38
41
|
const renderedRef = useRef<RenderedRoute | null>(null);
|
|
42
|
+
const [, setFailures] = useState(0);
|
|
39
43
|
const activity = usePageActivity();
|
|
40
44
|
|
|
41
45
|
const onScreenRef = useRef(activity === "current");
|
|
@@ -75,13 +79,18 @@ export const RenderLayer = memo(({ renders, index, params, searchParams, leaf =
|
|
|
75
79
|
result,
|
|
76
80
|
record: isThenable(result) ? CsrFrameDump.track(layer, result as Promise<ReactNode>) : null,
|
|
77
81
|
redirected: false,
|
|
82
|
+
failure: null,
|
|
78
83
|
};
|
|
79
84
|
if (isThenable(result))
|
|
80
85
|
(result as Promise<ReactNode>).then(
|
|
81
86
|
() => {
|
|
82
87
|
next.redirected = router.redirectCount() !== redirectsBefore;
|
|
83
88
|
},
|
|
84
|
-
() =>
|
|
89
|
+
(error: unknown) => {
|
|
90
|
+
if (router.redirectCount() !== redirectsBefore || renderedRef.current !== next) return;
|
|
91
|
+
next.failure = { error };
|
|
92
|
+
setFailures((count) => count + 1);
|
|
93
|
+
},
|
|
85
94
|
);
|
|
86
95
|
renderedRef.current = next;
|
|
87
96
|
}
|
|
@@ -98,6 +107,17 @@ export const RenderLayer = memo(({ renders, index, params, searchParams, leaf =
|
|
|
98
107
|
}, [record]);
|
|
99
108
|
if (!routeRender) return null;
|
|
100
109
|
if (!isAsyncRender) return createElement(routeRender.render as never, { children, params, searchParams } as never);
|
|
110
|
+
const failure = renderedRef.current?.failure;
|
|
111
|
+
if (failure)
|
|
112
|
+
return (
|
|
113
|
+
<RenderFailure
|
|
114
|
+
error={failure.error}
|
|
115
|
+
onRetry={() => {
|
|
116
|
+
renderedRef.current = null;
|
|
117
|
+
setFailures((count) => count + 1);
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
101
121
|
if (!value) return <>{composeLoadingFallback(renders.slice(index), params)}</>;
|
|
102
122
|
return <layerChildContext.Provider value={children}>{value}</layerChildContext.Provider>;
|
|
103
123
|
});
|