akanjs 3.0.0-beta.26 → 3.0.0-beta.28
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/base/baseEnv.ts +19 -5
- package/client/csrTypes.ts +2 -0
- package/client/device.ts +28 -12
- package/client/index.ts +1 -0
- package/client/native/accessibility.ts +1 -0
- package/client/native/app-state.ts +1 -0
- package/client/native/app.ts +1 -0
- package/client/native/appearance.ts +1 -0
- package/client/native/auth-session.ts +1 -0
- package/client/native/autostart.ts +1 -0
- package/client/native/badge.ts +1 -0
- package/client/native/biometric.ts +1 -0
- package/client/native/browser.ts +1 -0
- package/client/native/camera.ts +1 -0
- package/client/native/clipboard.ts +1 -0
- package/client/native/contacts.ts +1 -0
- package/client/native/device.ts +1 -0
- package/client/native/dialog.ts +1 -0
- package/client/native/dock.ts +1 -0
- package/client/native/file-picker.ts +1 -0
- package/client/native/filesystem.ts +1 -0
- package/client/native/geolocation.ts +1 -0
- package/client/native/global-shortcut.ts +1 -0
- package/client/native/haptics.ts +1 -0
- package/client/native/http.ts +1 -0
- package/client/native/iap.ts +1 -0
- package/client/native/keep-awake.ts +1 -0
- package/client/native/keyboard.ts +1 -0
- package/client/native/local-notifications.ts +1 -0
- package/client/native/menu.ts +1 -0
- package/client/native/network.ts +1 -0
- package/client/native/opener.ts +1 -0
- package/client/native/preferences.ts +1 -0
- package/client/native/push.ts +1 -0
- package/client/native/screen-orientation.ts +1 -0
- package/client/native/screen.ts +1 -0
- package/client/native/secure-storage.ts +1 -0
- package/client/native/share.ts +1 -0
- package/client/native/single-instance.ts +1 -0
- package/client/native/splash-screen.ts +1 -0
- package/client/native/sqlite.ts +1 -0
- package/client/native/toast.ts +1 -0
- package/client/native/tray.ts +1 -0
- package/client/native/updates.ts +1 -0
- package/client/native/volume.ts +1 -0
- package/client/native/window-state.ts +1 -0
- package/client/native/window.ts +1 -0
- package/client/native.ts +31 -1
- package/client/resolveServerUrl.ts +11 -0
- package/client/router.ts +8 -0
- package/common/Logger.ts +26 -2
- package/common/RouteLayering.ts +52 -0
- package/common/index.ts +1 -0
- package/index.ts +115 -6
- package/native/desktop.ts +5 -0
- package/package.json +11 -1
- package/server/akanApp.ts +23 -3
- package/server/akanAppHeaders.ts +4 -1
- package/server/akanServer.ts +27 -7
- package/server/index.ts +1 -0
- package/server/lifecycle/orphanGroup.ts +21 -0
- package/server/nativeFile.ts +107 -0
- package/server/ops/opsRoute.ts +3 -2
- package/server/routeTreeBuilder.ts +14 -39
- package/server/routing/apiRouter.ts +26 -3
- package/server/routing/hostAllowlist.ts +24 -0
- package/server/selfExec.ts +25 -0
- package/server/types.tsx +3 -1
- package/server/webRouter.ts +7 -4
- package/service/ipcTypes.ts +4 -0
- package/signal/CrossSiteGuard.ts +7 -0
- package/test/signalTestRuntime.ts +8 -2
- package/types/base/baseEnv.d.ts +1 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/client/index.d.ts +1 -0
- package/types/client/native/accessibility.d.ts +1 -0
- package/types/client/native/app-state.d.ts +1 -0
- package/types/client/native/app.d.ts +1 -0
- package/types/client/native/appearance.d.ts +1 -0
- package/types/client/native/auth-session.d.ts +1 -0
- package/types/client/native/autostart.d.ts +1 -0
- package/types/client/native/badge.d.ts +1 -0
- package/types/client/native/biometric.d.ts +1 -0
- package/types/client/native/browser.d.ts +1 -0
- package/types/client/native/camera.d.ts +1 -0
- package/types/client/native/clipboard.d.ts +1 -0
- package/types/client/native/contacts.d.ts +1 -0
- package/types/client/native/device.d.ts +1 -0
- package/types/client/native/dialog.d.ts +1 -0
- package/types/client/native/dock.d.ts +1 -0
- package/types/client/native/file-picker.d.ts +1 -0
- package/types/client/native/filesystem.d.ts +1 -0
- package/types/client/native/geolocation.d.ts +1 -0
- package/types/client/native/global-shortcut.d.ts +1 -0
- package/types/client/native/haptics.d.ts +1 -0
- package/types/client/native/http.d.ts +1 -0
- package/types/client/native/iap.d.ts +1 -0
- package/types/client/native/keep-awake.d.ts +1 -0
- package/types/client/native/keyboard.d.ts +1 -0
- package/types/client/native/local-notifications.d.ts +1 -0
- package/types/client/native/menu.d.ts +1 -0
- package/types/client/native/network.d.ts +1 -0
- package/types/client/native/opener.d.ts +1 -0
- package/types/client/native/preferences.d.ts +1 -0
- package/types/client/native/push.d.ts +1 -0
- package/types/client/native/screen-orientation.d.ts +1 -0
- package/types/client/native/screen.d.ts +1 -0
- package/types/client/native/secure-storage.d.ts +1 -0
- package/types/client/native/share.d.ts +1 -0
- package/types/client/native/single-instance.d.ts +1 -0
- package/types/client/native/splash-screen.d.ts +1 -0
- package/types/client/native/sqlite.d.ts +1 -0
- package/types/client/native/toast.d.ts +1 -0
- package/types/client/native/tray.d.ts +1 -0
- package/types/client/native/updates.d.ts +1 -0
- package/types/client/native/volume.d.ts +1 -0
- package/types/client/native/window-state.d.ts +1 -0
- package/types/client/native/window.d.ts +1 -0
- package/types/client/native.d.ts +14 -2
- package/types/client/resolveServerUrl.d.ts +1 -0
- package/types/client/router.d.ts +3 -0
- package/types/common/RouteLayering.d.ts +28 -0
- package/types/common/index.d.ts +1 -0
- package/types/index.d.ts +112 -6
- package/types/native/desktop.d.ts +3 -0
- package/types/server/akanAppHeaders.d.ts +2 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/lifecycle/orphanGroup.d.ts +7 -0
- package/types/server/nativeFile.d.ts +10 -0
- package/types/server/routing/apiRouter.d.ts +3 -1
- package/types/server/routing/hostAllowlist.d.ts +8 -0
- package/types/server/selfExec.d.ts +8 -0
- package/types/server/types.d.ts +3 -1
- package/types/service/ipcTypes.d.ts +16 -0
- package/types/signal/CrossSiteGuard.d.ts +1 -0
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +57 -4
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +48 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +8 -2
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +10 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +4 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/macos.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +38 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +8 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/grants.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/main.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +13 -2
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +27 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +21 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +79 -0
- package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +5 -0
- package/types/vendor/@akanjs/native/plugins/file-picker/src/index.d.ts +20 -1
- package/types/vendor/@akanjs/native/plugins/push/src/index.d.ts +9 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +28 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/common.d.ts +10 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/desktop.d.ts +18 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/index.d.ts +28 -0
- package/types/vendor/@akanjs/native/plugins/volume/src.d.ts +1 -0
- package/types/webkit/csrFrameDump.d.ts +42 -0
- package/types/webkit/nativeNavigation.d.ts +1 -0
- package/types/webkit/nativePurchase.d.ts +6 -2
- package/types/webkit/nativeUpdates.d.ts +8 -0
- package/ui/Agent/Attach.tsx +6 -2
- package/ui/CsrImage.tsx +3 -2
- package/ui/System/CSR.tsx +4 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +5 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +161 -25
- package/vendor/@akanjs/native/native/desktop/src/volume.rs +184 -0
- package/vendor/@akanjs/native/native/desktop/src/win/mod.rs +3 -1
- package/vendor/@akanjs/native/native/desktop/src/win/volume.rs +71 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
- package/vendor/@akanjs/native/packages/cli/src/api.ts +142 -12
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +5 -3
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +10 -71
- package/vendor/@akanjs/native/packages/cli/src/config.ts +41 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +58 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +10 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +38 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +104 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +85 -5
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +1 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +6 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +16 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +44 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +436 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +2 -0
- package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
- package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +20 -0
- package/vendor/@akanjs/native/packages/desktop/src/grants.ts +47 -0
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +102 -14
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +59 -11
- package/vendor/@akanjs/native/packages/desktop/src/paths.ts +38 -4
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +28 -0
- package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +159 -0
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +493 -0
- package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +14 -0
- package/vendor/@akanjs/native/plugins/app/native-plugin.json +2 -2
- package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
- package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
- package/vendor/@akanjs/native/plugins/app/src/web.ts +3 -0
- package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +39 -2
- package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +36 -1
- package/vendor/@akanjs/native/plugins/push/android/PushPlugin.kt +9 -1
- package/vendor/@akanjs/native/plugins/push/ios/PushPlugin.swift +4 -0
- package/vendor/@akanjs/native/plugins/push/src/index.ts +10 -0
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +464 -452
- package/vendor/@akanjs/native/plugins/volume/android/VolumePlugin.kt +107 -0
- package/vendor/@akanjs/native/plugins/volume/native-plugin.json +14 -0
- package/vendor/@akanjs/native/plugins/volume/src/common.ts +28 -0
- package/vendor/@akanjs/native/plugins/volume/src/desktop.ts +185 -0
- package/vendor/@akanjs/native/plugins/volume/src/index.ts +32 -0
- package/webkit/CsrRouteTable.ts +15 -35
- package/webkit/RenderLayer.tsx +71 -6
- package/webkit/bootCsr.tsx +11 -8
- package/webkit/csrFrameDump.ts +67 -0
- package/webkit/nativeNavigation.ts +17 -0
- package/webkit/nativePurchase.ts +12 -2
- package/webkit/nativeUpdates.ts +55 -0
- package/webkit/useCsrValues.ts +27 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { exec } from "../lib/exec.ts";
|
|
6
|
+
import { CliError, log, ToolchainError } from "../lib/log.ts";
|
|
7
|
+
import type { BuildContext } from "../lib/prepare.ts";
|
|
8
|
+
import { akanNativeHome } from "../lib/toolchains.ts";
|
|
9
|
+
import { hostArch } from "../lib/updates.ts";
|
|
10
|
+
import { windowsVersion } from "./windows.ts";
|
|
11
|
+
|
|
12
|
+
/** Microsoft's permanent link to the Evergreen Bootstrapper (MicrosoftEdgeWebview2Setup.exe). */
|
|
13
|
+
export const WEBVIEW2_BOOTSTRAPPER_URL = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
|
|
14
|
+
/** The WebView2 Runtime's EdgeUpdate client id; its `pv` value is the installed version. */
|
|
15
|
+
const WEBVIEW2_CLIENT = "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
|
|
16
|
+
/** MAX_PATH less its NUL: what a PC without long paths enabled (the Windows default) can open or unpack. */
|
|
17
|
+
export const WINDOWS_MAX_PATH = 259;
|
|
18
|
+
/** `C:\Users\` + a 20-character user name + `\AppData\Local\Programs\`. */
|
|
19
|
+
const INSTALL_ROOT = "C:\\Users\\".length + 20 + "\\AppData\\Local\\Programs\\".length;
|
|
20
|
+
/** `.update-<version>-<sequence>\`: an update unpacks next to the app, under the app's name again, before the swap. */
|
|
21
|
+
const STAGING = ".update-".length + 24 + 1;
|
|
22
|
+
|
|
23
|
+
export interface InstallerInput {
|
|
24
|
+
name: string;
|
|
25
|
+
id: string;
|
|
26
|
+
exe: string;
|
|
27
|
+
version: string;
|
|
28
|
+
build: number;
|
|
29
|
+
/** The built app folder, copied whole. */
|
|
30
|
+
folder: string;
|
|
31
|
+
/** Its top-level files and folders: what an update or uninstall removes. */
|
|
32
|
+
entries: { name: string; dir: boolean }[];
|
|
33
|
+
icon?: string;
|
|
34
|
+
bootstrapper: string;
|
|
35
|
+
stopScript: string;
|
|
36
|
+
outFile: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `$` is NSIS's variable sigil; the names that reach a script cannot hold a quote (config.ts app.name). */
|
|
40
|
+
const nsis = (value: string) => value.replace(/\$/g, "$$$$");
|
|
41
|
+
|
|
42
|
+
const removal = (entries: InstallerInput["entries"]) =>
|
|
43
|
+
entries
|
|
44
|
+
.map(({ name, dir }) => (dir ? ` RMDir /r "$INSTDIR\\${nsis(name)}"` : ` Delete "$INSTDIR\\${nsis(name)}"`))
|
|
45
|
+
.join("\n");
|
|
46
|
+
|
|
47
|
+
const moveIn = (entries: InstallerInput["entries"]) =>
|
|
48
|
+
entries.map(({ name }) => ` Rename "$INSTDIR.setup-new\\${nsis(name)}" "$INSTDIR\\${nsis(name)}"`).join("\n");
|
|
49
|
+
|
|
50
|
+
export function installerScript(input: InstallerInput): string {
|
|
51
|
+
const version = windowsVersion(input.version, input.build);
|
|
52
|
+
const icon = input.icon ? `!define MUI_ICON "${nsis(input.icon)}"\n!define MUI_UNICON "${nsis(input.icon)}"\n` : "";
|
|
53
|
+
return `Unicode true
|
|
54
|
+
ManifestDPIAware true
|
|
55
|
+
ManifestLongPathAware true
|
|
56
|
+
SetCompressor /SOLID lzma
|
|
57
|
+
RequestExecutionLevel user
|
|
58
|
+
|
|
59
|
+
!include "MUI2.nsh"
|
|
60
|
+
!include "FileFunc.nsh"
|
|
61
|
+
!include "LogicLib.nsh"
|
|
62
|
+
|
|
63
|
+
!define APP_NAME "${nsis(input.name)}"
|
|
64
|
+
!define EXE "${nsis(input.exe)}"
|
|
65
|
+
!define APP_ID "${nsis(input.id)}"
|
|
66
|
+
!define UNINSTALL_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${input.id}"
|
|
67
|
+
!define RUN_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
|
68
|
+
!define STARTUP_APPROVED_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run"
|
|
69
|
+
!define WEBVIEW2_KEY "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\${WEBVIEW2_CLIENT}"
|
|
70
|
+
|
|
71
|
+
Name "\${APP_NAME}"
|
|
72
|
+
OutFile "${nsis(input.outFile)}"
|
|
73
|
+
InstallDir "$LOCALAPPDATA\\Programs\\\${APP_NAME}"
|
|
74
|
+
BrandingText "\${APP_NAME} ${nsis(input.version)}"
|
|
75
|
+
VIProductVersion "${version}"
|
|
76
|
+
VIAddVersionKey "ProductName" "\${APP_NAME}"
|
|
77
|
+
VIAddVersionKey "ProductVersion" "${nsis(input.version)}"
|
|
78
|
+
VIAddVersionKey "FileVersion" "${version}"
|
|
79
|
+
VIAddVersionKey "FileDescription" "\${APP_NAME} Setup"
|
|
80
|
+
|
|
81
|
+
${icon}!define MUI_FINISHPAGE_RUN "$INSTDIR\\\${EXE}"
|
|
82
|
+
!insertmacro MUI_PAGE_INSTFILES
|
|
83
|
+
!insertmacro MUI_PAGE_FINISH
|
|
84
|
+
!insertmacro MUI_UNPAGE_CONFIRM
|
|
85
|
+
!insertmacro MUI_UNPAGE_INSTFILES
|
|
86
|
+
!insertmacro MUI_LANGUAGE "English"
|
|
87
|
+
!insertmacro MUI_LANGUAGE "Korean"
|
|
88
|
+
|
|
89
|
+
Var AppStopped
|
|
90
|
+
Var RunAfter
|
|
91
|
+
|
|
92
|
+
Function RunApp
|
|
93
|
+
\${If} \${FileExists} "$INSTDIR\\\${EXE}"
|
|
94
|
+
SetOutPath "$LOCALAPPDATA"
|
|
95
|
+
Exec '"$INSTDIR\\\${EXE}"'
|
|
96
|
+
\${EndIf}
|
|
97
|
+
FunctionEnd
|
|
98
|
+
|
|
99
|
+
!macro Fail MESSAGE
|
|
100
|
+
DetailPrint "\${MESSAGE}"
|
|
101
|
+
\${IfNot} \${Silent}
|
|
102
|
+
MessageBox MB_ICONSTOP|MB_OK "\${MESSAGE}"
|
|
103
|
+
\${EndIf}
|
|
104
|
+
\${If} $AppStopped == 1
|
|
105
|
+
\${AndIf} $RunAfter == 1
|
|
106
|
+
Call RunApp
|
|
107
|
+
\${EndIf}
|
|
108
|
+
SetErrorLevel 2
|
|
109
|
+
Abort
|
|
110
|
+
!macroend
|
|
111
|
+
|
|
112
|
+
!macro RemoveEntries
|
|
113
|
+
${removal(input.entries)}
|
|
114
|
+
!macroend
|
|
115
|
+
|
|
116
|
+
!macro RemoveApp
|
|
117
|
+
\${If} \${FileExists} "$INSTDIR\\\${EXE}"
|
|
118
|
+
RMDir /r "$INSTDIR"
|
|
119
|
+
\${Else}
|
|
120
|
+
!insertmacro RemoveEntries
|
|
121
|
+
\${EndIf}
|
|
122
|
+
!macroend
|
|
123
|
+
|
|
124
|
+
; Windows refuses to rename a folder while a file in it is open: a stopped process's handles, and an antivirus
|
|
125
|
+
; scan of files just written, close a moment later. $R2 is "done" once the rename went through.
|
|
126
|
+
!macro RenameRetry FROM TO
|
|
127
|
+
StrCpy $R2 0
|
|
128
|
+
\${Do}
|
|
129
|
+
ClearErrors
|
|
130
|
+
Rename "\${FROM}" "\${TO}"
|
|
131
|
+
\${IfNot} \${Errors}
|
|
132
|
+
StrCpy $R2 "done"
|
|
133
|
+
\${ExitDo}
|
|
134
|
+
\${EndIf}
|
|
135
|
+
IntOp $R2 $R2 + 1
|
|
136
|
+
Sleep 250
|
|
137
|
+
\${LoopWhile} $R2 < 40
|
|
138
|
+
!macroend
|
|
139
|
+
|
|
140
|
+
!macro RemoveSiblings SUFFIX
|
|
141
|
+
FindFirst $R0 $R1 "$INSTDIR.\${SUFFIX}*"
|
|
142
|
+
\${DoWhile} $R1 != ""
|
|
143
|
+
RMDir /r "$INSTDIR\\..\\$R1"
|
|
144
|
+
FindNext $R0 $R1
|
|
145
|
+
\${Loop}
|
|
146
|
+
FindClose $R0
|
|
147
|
+
!macroend
|
|
148
|
+
|
|
149
|
+
Function WebView2Version
|
|
150
|
+
SetRegView 64
|
|
151
|
+
ReadRegStr $0 HKLM "\${WEBVIEW2_KEY}" "pv"
|
|
152
|
+
SetRegView 32
|
|
153
|
+
\${If} $0 == ""
|
|
154
|
+
\${OrIf} $0 == "0.0.0.0"
|
|
155
|
+
ReadRegStr $0 HKLM "\${WEBVIEW2_KEY}" "pv"
|
|
156
|
+
\${EndIf}
|
|
157
|
+
\${If} $0 == ""
|
|
158
|
+
\${OrIf} $0 == "0.0.0.0"
|
|
159
|
+
ReadRegStr $0 HKCU "\${WEBVIEW2_KEY}" "pv"
|
|
160
|
+
\${EndIf}
|
|
161
|
+
\${If} $0 == "0.0.0.0"
|
|
162
|
+
StrCpy $0 ""
|
|
163
|
+
\${EndIf}
|
|
164
|
+
FunctionEnd
|
|
165
|
+
|
|
166
|
+
Function EnsureWebView2
|
|
167
|
+
Call WebView2Version
|
|
168
|
+
\${If} $0 != ""
|
|
169
|
+
DetailPrint "WebView2 Runtime $0"
|
|
170
|
+
Return
|
|
171
|
+
\${EndIf}
|
|
172
|
+
DetailPrint "Installing the WebView2 Runtime"
|
|
173
|
+
File "/oname=$PLUGINSDIR\\MicrosoftEdgeWebview2Setup.exe" "${nsis(input.bootstrapper)}"
|
|
174
|
+
ExecWait '"$PLUGINSDIR\\MicrosoftEdgeWebview2Setup.exe" /silent /install' $1
|
|
175
|
+
DetailPrint "WebView2 Runtime setup exited with $1"
|
|
176
|
+
Call WebView2Version
|
|
177
|
+
\${If} $0 != ""
|
|
178
|
+
DetailPrint "WebView2 Runtime $0"
|
|
179
|
+
Return
|
|
180
|
+
\${EndIf}
|
|
181
|
+
\${If} \${Silent}
|
|
182
|
+
!insertmacro Fail "The WebView2 Runtime could not be installed (setup exited with $1), and \${APP_NAME} does not start without it."
|
|
183
|
+
\${EndIf}
|
|
184
|
+
MessageBox MB_ICONEXCLAMATION|MB_OKCANCEL "The WebView2 Runtime could not be installed (setup exited with $1), and \${APP_NAME} does not start without it. Install \${APP_NAME} anyway?" IDOK webview2_anyway
|
|
185
|
+
SetErrorLevel 2
|
|
186
|
+
Abort
|
|
187
|
+
webview2_anyway:
|
|
188
|
+
DetailPrint "Installing without the WebView2 Runtime"
|
|
189
|
+
FunctionEnd
|
|
190
|
+
|
|
191
|
+
Section "Install"
|
|
192
|
+
\${If} \${Silent}
|
|
193
|
+
\${GetParameters} $0
|
|
194
|
+
ClearErrors
|
|
195
|
+
\${GetOptions} $0 "/RUN" $1
|
|
196
|
+
\${IfNot} \${Errors}
|
|
197
|
+
StrCpy $RunAfter 1
|
|
198
|
+
\${EndIf}
|
|
199
|
+
\${EndIf}
|
|
200
|
+
; A setup cut off between its two renames left the app only in .setup-old.
|
|
201
|
+
\${DirState} "$INSTDIR" $0
|
|
202
|
+
\${If} $0 == -1
|
|
203
|
+
\${AndIf} \${FileExists} "$INSTDIR.setup-old\\\${EXE}"
|
|
204
|
+
Rename "$INSTDIR.setup-old" "$INSTDIR"
|
|
205
|
+
\${EndIf}
|
|
206
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
207
|
+
\${If} \${FileExists} "$INSTDIR\\\${EXE}"
|
|
208
|
+
\${OrIfNot} \${FileExists} "$INSTDIR.setup-old\\\${EXE}"
|
|
209
|
+
RMDir /r "$INSTDIR.setup-old"
|
|
210
|
+
\${EndIf}
|
|
211
|
+
\${DirState} "$INSTDIR" $0
|
|
212
|
+
\${If} $0 == 1
|
|
213
|
+
\${AndIfNot} \${FileExists} "$INSTDIR\\\${EXE}"
|
|
214
|
+
\${AndIf} "$INSTDIR" != "$LOCALAPPDATA\\Programs\\\${APP_NAME}"
|
|
215
|
+
!insertmacro Fail "$INSTDIR holds other files. Install into an empty folder, or where \${APP_NAME} is installed."
|
|
216
|
+
\${EndIf}
|
|
217
|
+
InitPluginsDir
|
|
218
|
+
Call EnsureWebView2
|
|
219
|
+
SetOutPath "$INSTDIR.setup-new"
|
|
220
|
+
ClearErrors
|
|
221
|
+
File /r "${nsis(input.folder)}\\*"
|
|
222
|
+
\${If} \${Errors}
|
|
223
|
+
SetOutPath "$LOCALAPPDATA"
|
|
224
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
225
|
+
!insertmacro Fail "Some files could not be written beside $INSTDIR. Windows refuses a path over 260 characters unless long paths are enabled."
|
|
226
|
+
\${EndIf}
|
|
227
|
+
; SetOutPath is also the installer's working folder, which Windows does not let anyone rename.
|
|
228
|
+
SetOutPath "$LOCALAPPDATA"
|
|
229
|
+
File "/oname=$PLUGINSDIR\\stop-app.ps1" "${nsis(input.stopScript)}"
|
|
230
|
+
nsExec::Exec 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\\stop-app.ps1" "$INSTDIR"'
|
|
231
|
+
Pop $0
|
|
232
|
+
StrCpy $AppStopped 1
|
|
233
|
+
\${If} \${FileExists} "$INSTDIR\\\${EXE}"
|
|
234
|
+
!insertmacro RenameRetry "$INSTDIR" "$INSTDIR.setup-old"
|
|
235
|
+
\${If} $R2 != "done"
|
|
236
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
237
|
+
!insertmacro Fail "$INSTDIR is in use and could not be replaced; \${APP_NAME} is left as it was."
|
|
238
|
+
\${EndIf}
|
|
239
|
+
!insertmacro RenameRetry "$INSTDIR.setup-new" "$INSTDIR"
|
|
240
|
+
\${If} $R2 != "done"
|
|
241
|
+
!insertmacro RenameRetry "$INSTDIR.setup-old" "$INSTDIR"
|
|
242
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
243
|
+
!insertmacro Fail "The new files could not take the place of $INSTDIR; \${APP_NAME} is left as it was."
|
|
244
|
+
\${EndIf}
|
|
245
|
+
RMDir /r "$INSTDIR.setup-old"
|
|
246
|
+
\${Else}
|
|
247
|
+
!insertmacro RemoveEntries
|
|
248
|
+
RMDir "$INSTDIR"
|
|
249
|
+
\${DirState} "$INSTDIR" $0
|
|
250
|
+
\${If} $0 == 1
|
|
251
|
+
ClearErrors
|
|
252
|
+
${moveIn(input.entries)}
|
|
253
|
+
\${If} \${Errors}
|
|
254
|
+
!insertmacro Fail "Some files could not be moved into $INSTDIR."
|
|
255
|
+
\${EndIf}
|
|
256
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
257
|
+
\${Else}
|
|
258
|
+
!insertmacro RenameRetry "$INSTDIR.setup-new" "$INSTDIR"
|
|
259
|
+
\${If} $R2 != "done"
|
|
260
|
+
RMDir /r "$INSTDIR.setup-new"
|
|
261
|
+
!insertmacro Fail "$INSTDIR could not be made."
|
|
262
|
+
\${EndIf}
|
|
263
|
+
\${EndIf}
|
|
264
|
+
\${EndIf}
|
|
265
|
+
ClearErrors
|
|
266
|
+
WriteUninstaller "$INSTDIR.uninstall.exe"
|
|
267
|
+
\${If} \${Errors}
|
|
268
|
+
!insertmacro Fail "$INSTDIR.uninstall.exe could not be written: install into a folder under one this user may write to."
|
|
269
|
+
\${EndIf}
|
|
270
|
+
CreateShortcut "$SMPROGRAMS\\\${APP_NAME}.lnk" "$INSTDIR\\\${EXE}"
|
|
271
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayName" "\${APP_NAME}"
|
|
272
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayVersion" "${nsis(input.version)}"
|
|
273
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\\\${EXE}"
|
|
274
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
|
|
275
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "UninstallString" '"$INSTDIR.uninstall.exe"'
|
|
276
|
+
WriteRegStr HKCU "\${UNINSTALL_KEY}" "QuietUninstallString" '"$INSTDIR.uninstall.exe" /S'
|
|
277
|
+
WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "NoModify" 1
|
|
278
|
+
WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "NoRepair" 1
|
|
279
|
+
\${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
|
280
|
+
WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "EstimatedSize" $0
|
|
281
|
+
\${If} $RunAfter == 1
|
|
282
|
+
Call RunApp
|
|
283
|
+
\${EndIf}
|
|
284
|
+
SectionEnd
|
|
285
|
+
|
|
286
|
+
Function un.onInit
|
|
287
|
+
ReadRegStr $INSTDIR HKCU "\${UNINSTALL_KEY}" "InstallLocation"
|
|
288
|
+
\${If} $INSTDIR == ""
|
|
289
|
+
\${IfNot} \${Silent}
|
|
290
|
+
MessageBox MB_ICONSTOP|MB_OK "\${APP_NAME} is not installed for this user."
|
|
291
|
+
\${EndIf}
|
|
292
|
+
SetErrorLevel 2
|
|
293
|
+
Abort
|
|
294
|
+
\${EndIf}
|
|
295
|
+
FunctionEnd
|
|
296
|
+
|
|
297
|
+
Section "Uninstall"
|
|
298
|
+
InitPluginsDir
|
|
299
|
+
File "/oname=$PLUGINSDIR\\stop-app.ps1" "${nsis(input.stopScript)}"
|
|
300
|
+
nsExec::Exec 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\\stop-app.ps1" "$INSTDIR"'
|
|
301
|
+
Pop $0
|
|
302
|
+
Delete "$SMPROGRAMS\\\${APP_NAME}.lnk"
|
|
303
|
+
RMDir /r "$INSTDIR.previous"
|
|
304
|
+
!insertmacro RemoveSiblings "update-"
|
|
305
|
+
!insertmacro RemoveSiblings "failed-"
|
|
306
|
+
!insertmacro RemoveSiblings "setup-"
|
|
307
|
+
!insertmacro RemoveApp
|
|
308
|
+
RMDir "$INSTDIR"
|
|
309
|
+
Delete "$INSTDIR.uninstall.exe"
|
|
310
|
+
DeleteRegKey HKCU "\${UNINSTALL_KEY}"
|
|
311
|
+
DeleteRegValue HKCU "\${RUN_KEY}" "\${APP_ID}"
|
|
312
|
+
DeleteRegValue HKCU "\${STARTUP_APPROVED_KEY}" "\${APP_ID}"
|
|
313
|
+
RMDir /r "$LOCALAPPDATA\\\${APP_ID}\\akan-native-updates"
|
|
314
|
+
Delete "$LOCALAPPDATA\\\${APP_ID}\\akan-native-relaunch.json"
|
|
315
|
+
RMDir "$LOCALAPPDATA\\\${APP_ID}"
|
|
316
|
+
SectionEnd
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Stops every process running from the install folder, and waits (up to 10 s) until they are gone. Through
|
|
322
|
+
* WMI: the installer is a 32-bit program, so its PowerShell is too, and a 32-bit process cannot read a
|
|
323
|
+
* 64-bit one's path (Get-Process's Path comes back empty).
|
|
324
|
+
*/
|
|
325
|
+
export const STOP_SCRIPT = `param([string]$Dir)
|
|
326
|
+
$prefix = [IO.Path]::GetFullPath($Dir).TrimEnd('\\') + '\\'
|
|
327
|
+
$running = { Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } }
|
|
328
|
+
& $running | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
329
|
+
for ($i = 0; $i -lt 50 -and (& $running); $i++) { Start-Sleep -Milliseconds 200 }
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
/** The longest path under `folder`, relative and with Windows separators. Links are not followed. */
|
|
333
|
+
export function deepestPath(folder: string): string {
|
|
334
|
+
let deepest = "";
|
|
335
|
+
const walk = (dir: string, prefix: string) => {
|
|
336
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
337
|
+
const path = `${prefix}${entry.name}`;
|
|
338
|
+
if (path.length > deepest.length) deepest = path;
|
|
339
|
+
if (entry.isDirectory()) walk(join(dir, entry.name), `${path}\\`);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
walk(folder, "");
|
|
343
|
+
return deepest;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** How long `relative` gets on a typical PC while an update unpacks, the longest it ever is: root, app, staging, app. */
|
|
347
|
+
export function installedPathLength(appName: string, relative: string): number {
|
|
348
|
+
return INSTALL_ROOT + appName.length + STAGING + appName.length + 1 + relative.length;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** makensis: AKAN_NATIVE_MAKENSIS, PATH, then where the NSIS installer puts it. */
|
|
352
|
+
export function findMakensis(env: Record<string, string | undefined> = process.env): string | null {
|
|
353
|
+
if (env.AKAN_NATIVE_MAKENSIS) return env.AKAN_NATIVE_MAKENSIS;
|
|
354
|
+
const onPath = Bun.which("makensis");
|
|
355
|
+
if (onPath) return onPath;
|
|
356
|
+
for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
|
|
357
|
+
const candidate = base ? join(base, "NSIS", "makensis.exe") : null;
|
|
358
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The Evergreen Bootstrapper, downloaded once into the akan-native cache and checked. */
|
|
364
|
+
async function webview2Bootstrapper(): Promise<string> {
|
|
365
|
+
const file = join(akanNativeHome(), "cache", "webview2", "MicrosoftEdgeWebview2Setup.exe");
|
|
366
|
+
if (!existsSync(file)) {
|
|
367
|
+
log.info(`downloading the WebView2 Evergreen Bootstrapper (${WEBVIEW2_BOOTSTRAPPER_URL})`);
|
|
368
|
+
const response = await fetch(WEBVIEW2_BOOTSTRAPPER_URL, { redirect: "follow" });
|
|
369
|
+
if (!response.ok) throw new CliError(`the WebView2 bootstrapper download answered ${response.status}`);
|
|
370
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
371
|
+
if (bytes.length < 100_000 || bytes[0] !== 0x4d || bytes[1] !== 0x5a)
|
|
372
|
+
throw new CliError("the WebView2 bootstrapper download is not a Windows program");
|
|
373
|
+
mkdirSync(join(file, ".."), { recursive: true });
|
|
374
|
+
writeFileSync(file, bytes);
|
|
375
|
+
}
|
|
376
|
+
if (process.platform === "win32") {
|
|
377
|
+
const signer = await exec(
|
|
378
|
+
[
|
|
379
|
+
"powershell.exe",
|
|
380
|
+
"-NoProfile",
|
|
381
|
+
"-NonInteractive",
|
|
382
|
+
"-Command",
|
|
383
|
+
"$s = Get-AuthenticodeSignature -LiteralPath $env:AKAN_NATIVE_WEBVIEW2_SETUP; if ($s.Status -eq 'Valid') { $s.SignerCertificate.Subject }",
|
|
384
|
+
],
|
|
385
|
+
{ echo: false, env: { AKAN_NATIVE_WEBVIEW2_SETUP: file } },
|
|
386
|
+
);
|
|
387
|
+
if (!signer.stdout.includes("O=Microsoft Corporation"))
|
|
388
|
+
throw new CliError(`${file} is not signed by Microsoft; delete it to download it again`);
|
|
389
|
+
}
|
|
390
|
+
return file;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Writes the setup program next to the app folder and answers its path. */
|
|
394
|
+
export async function buildWindowsInstaller(ctx: BuildContext, folder: string): Promise<string> {
|
|
395
|
+
const makensis = findMakensis();
|
|
396
|
+
if (!makensis)
|
|
397
|
+
throw new ToolchainError(
|
|
398
|
+
"NSIS (makensis) builds the installer: winget install NSIS.NSIS on Windows (or set AKAN_NATIVE_MAKENSIS)",
|
|
399
|
+
);
|
|
400
|
+
const { config } = ctx.project;
|
|
401
|
+
const gen = join(ctx.outDir, "gen");
|
|
402
|
+
mkdirSync(gen, { recursive: true });
|
|
403
|
+
const stopScript = join(gen, "stop-app.ps1");
|
|
404
|
+
writeFileSync(stopScript, STOP_SCRIPT);
|
|
405
|
+
const icon = join(gen, "app.ico");
|
|
406
|
+
const outFile = join(ctx.outDir, `${config.app.fileName}-${config.app.version}-${hostArch()}-setup.exe`);
|
|
407
|
+
const script = join(gen, "installer.nsi");
|
|
408
|
+
const deepest = deepestPath(folder);
|
|
409
|
+
const longest = installedPathLength(config.app.name, deepest);
|
|
410
|
+
if (longest > WINDOWS_MAX_PATH - 20)
|
|
411
|
+
log.warn(
|
|
412
|
+
`${deepest} reaches about ${longest} characters under %LOCALAPPDATA%\\Programs while an update unpacks: Windows refuses paths over ${WINDOWS_MAX_PATH} unless long paths are enabled, so the installer or an update fails on such a PC. Shorten app.name or that path.`,
|
|
413
|
+
);
|
|
414
|
+
writeFileSync(
|
|
415
|
+
script,
|
|
416
|
+
`${installerScript({
|
|
417
|
+
name: config.app.name,
|
|
418
|
+
id: config.app.id,
|
|
419
|
+
exe: `${config.app.fileName}.exe`,
|
|
420
|
+
version: config.app.version,
|
|
421
|
+
build: config.app.build,
|
|
422
|
+
folder,
|
|
423
|
+
entries: readdirSync(folder).map((name) => ({ name, dir: statSync(join(folder, name)).isDirectory() })),
|
|
424
|
+
...(existsSync(icon) ? { icon } : {}),
|
|
425
|
+
bootstrapper: await webview2Bootstrapper(),
|
|
426
|
+
stopScript,
|
|
427
|
+
outFile,
|
|
428
|
+
})}`,
|
|
429
|
+
);
|
|
430
|
+
log.step("installer: makensis");
|
|
431
|
+
const result = await exec([makensis, "/V2", script], { echo: false });
|
|
432
|
+
if (result.code !== 0 || !existsSync(outFile))
|
|
433
|
+
throw new CliError(`makensis failed:\n${(result.stdout + result.stderr).trim().split("\n").slice(-20).join("\n")}`);
|
|
434
|
+
log.info(`installer ${outFile} (${Math.round(readFileSync(outFile).length / 1024 / 1024)} MB)`);
|
|
435
|
+
return outFile;
|
|
436
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
runExecutable,
|
|
18
18
|
writeDesktopResources,
|
|
19
19
|
} from "./desktop.ts";
|
|
20
|
+
import { buildWindowsInstaller } from "./windows-installer.ts";
|
|
20
21
|
|
|
21
22
|
/** Characters left for the app id under C:\Users\<name>\AppData\Local\ before WebView2's own paths reach MAX_PATH. */
|
|
22
23
|
const WEBVIEW2_ID_BUDGET = 40;
|
|
@@ -70,6 +71,7 @@ export async function buildWindows(ctx: BuildContext): Promise<string> {
|
|
|
70
71
|
const resources = resourcesOf("windows", dir);
|
|
71
72
|
writeDesktopResources(ctx, resources, "windows");
|
|
72
73
|
if (art) writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
|
|
74
|
+
if (ctx.windows?.installer) ctx.artifacts.push({ kind: "installer", path: await buildWindowsInstaller(ctx, dir) });
|
|
73
75
|
return dir;
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -93,7 +93,7 @@ export function runtime(): Runtime {
|
|
|
93
93
|
};
|
|
94
94
|
if (rt.transport) w.addEventListener?.("pageshow", (e) => e.persisted && w.location?.reload());
|
|
95
95
|
|
|
96
|
-
if (boot.dev && rt.transport
|
|
96
|
+
if (boot.dev && rt.transport) forwardConsole(boot.platform);
|
|
97
97
|
}
|
|
98
98
|
return g.__runtime as Runtime;
|
|
99
99
|
}
|
|
@@ -102,7 +102,7 @@ export function runtime(): Runtime {
|
|
|
102
102
|
* Dev builds on native hosts: mirror console output and uncaught errors to the host,
|
|
103
103
|
* which prints them next to its own logs (WV-3).
|
|
104
104
|
*/
|
|
105
|
-
function forwardConsole(
|
|
105
|
+
function forwardConsole(platform: AkanNativeBoot["platform"]): void {
|
|
106
106
|
|
|
107
107
|
const g = akanNativeGlobal() as { __consolePatched?: boolean };
|
|
108
108
|
if (g.__consolePatched) return;
|
|
@@ -110,13 +110,32 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
110
110
|
const c = globalThis.console as unknown as Record<string, (...args: unknown[]) => void>;
|
|
111
111
|
const format = (value: unknown): string => {
|
|
112
112
|
if (typeof value === "string") return value;
|
|
113
|
-
|
|
113
|
+
|
|
114
|
+
if (value instanceof Error) {
|
|
115
|
+
const head = `${value.name}: ${value.message}`;
|
|
116
|
+
return value.stack?.startsWith(head) ? value.stack : `${head}${value.stack ? `\n${value.stack}` : ""}`;
|
|
117
|
+
}
|
|
114
118
|
try {
|
|
115
119
|
return JSON.stringify(value) ?? String(value);
|
|
116
120
|
} catch {
|
|
117
121
|
return String(value);
|
|
118
122
|
}
|
|
119
123
|
};
|
|
124
|
+
|
|
125
|
+
const render = (args: unknown[]): string => {
|
|
126
|
+
const [first, ...rest] = args;
|
|
127
|
+
if (typeof first !== "string" || !/%[sdifoOc]/.test(first)) return args.map(format).join(" ");
|
|
128
|
+
const text = first.replace(/%([sdifoOc%])/g, (match, spec: string) => {
|
|
129
|
+
if (spec === "%") return "%";
|
|
130
|
+
if (rest.length === 0) return match;
|
|
131
|
+
const value = rest.shift();
|
|
132
|
+
if (spec === "c") return "";
|
|
133
|
+
if (spec === "d" || spec === "i") return String(Number.parseInt(String(value), 10));
|
|
134
|
+
if (spec === "f") return String(Number(value));
|
|
135
|
+
return format(value);
|
|
136
|
+
});
|
|
137
|
+
return [text, ...rest.map(format)].join(" ");
|
|
138
|
+
};
|
|
120
139
|
const send = (level: string, args: unknown[]) => {
|
|
121
140
|
const rt = akanNativeGlobal().__runtime as Runtime | undefined;
|
|
122
141
|
if (!rt?.transport || !rt.boot.dev) return;
|
|
@@ -125,10 +144,12 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
125
144
|
id: ++rt.nextId,
|
|
126
145
|
plugin: CONSOLE_PLUGIN,
|
|
127
146
|
method: level,
|
|
128
|
-
args: { message: args
|
|
147
|
+
args: { message: render(args).replace(/\n+$/, "") },
|
|
129
148
|
};
|
|
130
149
|
rt.transport.send(request).catch(() => {});
|
|
131
150
|
};
|
|
151
|
+
|
|
152
|
+
if (platform === "android") send("attach", []);
|
|
132
153
|
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
133
154
|
const original = c[level]!.bind(globalThis.console);
|
|
134
155
|
c[level] = (...args: unknown[]) => {
|
|
@@ -137,7 +158,7 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
137
158
|
};
|
|
138
159
|
}
|
|
139
160
|
const w = globalThis as { addEventListener?: (type: string, fn: (e: any) => void) => void };
|
|
140
|
-
w.addEventListener?.("error", (e) => send("error", [
|
|
161
|
+
w.addEventListener?.("error", (e) => send("error", ["uncaught:", e.error ?? e.message]));
|
|
141
162
|
w.addEventListener?.("unhandledrejection", (e) => send("error", ["unhandled rejection:", e.reason]));
|
|
142
163
|
}
|
|
143
164
|
|
|
@@ -22,6 +22,7 @@ import type { Lifecycle } from "./lifecycle.ts";
|
|
|
22
22
|
import type {
|
|
23
23
|
DesktopContext,
|
|
24
24
|
DesktopPlugin,
|
|
25
|
+
DesktopServerStatus,
|
|
25
26
|
DocumentInfo,
|
|
26
27
|
DocumentScope,
|
|
27
28
|
EmitTarget,
|
|
@@ -40,7 +41,14 @@ export const SETUP_WAIT_MS = 5000;
|
|
|
40
41
|
|
|
41
42
|
export interface HostServices {
|
|
42
43
|
app: AppInfo;
|
|
44
|
+
dev?: boolean;
|
|
43
45
|
appDataDir: string;
|
|
46
|
+
/** paths.ts appLocalDataDir; appDataDir when not given. */
|
|
47
|
+
appLocalDataDir?: string;
|
|
48
|
+
/** desktop.bin in the app's resources, or null. */
|
|
49
|
+
binDir?: string | null;
|
|
50
|
+
/** The carried server (resources/server.json), or null. */
|
|
51
|
+
server?: DesktopServerStatus | null;
|
|
44
52
|
/** Sends an event (numbered for its document) to a window's page. */
|
|
45
53
|
emit(window: number, message: BridgeEvent): void;
|
|
46
54
|
/**
|
|
@@ -203,6 +211,7 @@ export function createDispatcher(
|
|
|
203
211
|
): Dispatcher {
|
|
204
212
|
const byId = new Map(plugins.map((p) => [p.id, p]));
|
|
205
213
|
let dataDirReady = false;
|
|
214
|
+
let localDataDirReady = false;
|
|
206
215
|
const launch: Launch = { window: {} };
|
|
207
216
|
let launching = true;
|
|
208
217
|
const duringLaunch = (plugin: DesktopPlugin, what: string, apply: () => void) => {
|
|
@@ -215,6 +224,7 @@ export function createDispatcher(
|
|
|
215
224
|
if (!ctx) {
|
|
216
225
|
ctx = {
|
|
217
226
|
app: services.app,
|
|
227
|
+
dev: services.dev === true,
|
|
218
228
|
get appDataDir() {
|
|
219
229
|
if (!dataDirReady) {
|
|
220
230
|
mkdirSync(services.appDataDir, { recursive: true });
|
|
@@ -222,6 +232,16 @@ export function createDispatcher(
|
|
|
222
232
|
}
|
|
223
233
|
return services.appDataDir;
|
|
224
234
|
},
|
|
235
|
+
get appLocalDataDir() {
|
|
236
|
+
const dir = services.appLocalDataDir ?? services.appDataDir;
|
|
237
|
+
if (!localDataDirReady) {
|
|
238
|
+
mkdirSync(dir, { recursive: true });
|
|
239
|
+
localDataDirReady = true;
|
|
240
|
+
}
|
|
241
|
+
return dir;
|
|
242
|
+
},
|
|
243
|
+
binDir: services.binDir ?? null,
|
|
244
|
+
server: services.server ?? null,
|
|
225
245
|
emit: (event, data, target) => deliver(plugin.id, event, data, target),
|
|
226
246
|
registerFile: (path, mime) => services.registerFile(path, mime),
|
|
227
247
|
shell: (op, args) =>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
4
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export type GrantMode = "read" | "write" | "folder";
|
|
9
|
+
|
|
10
|
+
export interface FileGrant {
|
|
11
|
+
path: string;
|
|
12
|
+
mode: GrantMode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const grants = new Map<string, FileGrant>();
|
|
16
|
+
|
|
17
|
+
export function devGrantKeyFile(home = homedir()): string {
|
|
18
|
+
return process.env.AKAN_NATIVE_DEV_GRANT_KEY || join(home, ".akan", "native", "dev-file-grant.key");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function grantFile(path: string, mode: GrantMode, { dev = false }: { dev?: boolean } = {}): string {
|
|
22
|
+
if (dev) {
|
|
23
|
+
const body = `${mode}:${Buffer.from(path).toString("base64url")}`;
|
|
24
|
+
return `dev:${body}:${createHmac("sha256", devGrantKey()).update(body).digest("base64url")}`;
|
|
25
|
+
}
|
|
26
|
+
const id = randomBytes(18).toString("base64url");
|
|
27
|
+
grants.set(id, { path, mode });
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveGrant(id: unknown): FileGrant | null {
|
|
32
|
+
return typeof id === "string" ? (grants.get(id) ?? null) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function devGrantKey(file = devGrantKeyFile()): Buffer {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(file);
|
|
38
|
+
} catch {
|
|
39
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(file, randomBytes(32), { mode: 0o600, flag: "wx" });
|
|
42
|
+
chmodSync(file, 0o600);
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
return readFileSync(file);
|
|
46
|
+
}
|
|
47
|
+
}
|