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,71 @@
|
|
|
1
|
+
//! The system output volume on Windows (volume plugin), with the ops and JSON of volume.rs: the default
|
|
2
|
+
//! render endpoint's IAudioEndpointVolume, which is what the taskbar's volume slider moves.
|
|
3
|
+
//!
|
|
4
|
+
//! - volume.watch is not an op here: the plugin host polls volume.get. Hearing changes would take an
|
|
5
|
+
//! IAudioEndpointVolumeCallback per endpoint and an IMMNotificationClient for a new default endpoint,
|
|
6
|
+
//! two COM objects kept alive for one event.
|
|
7
|
+
//! - Every endpoint has a software volume, so it is always settable.
|
|
8
|
+
|
|
9
|
+
use windows::Win32::Media::Audio::Endpoints::IAudioEndpointVolume;
|
|
10
|
+
use windows::Win32::Media::Audio::{eConsole, eRender, IMMDeviceEnumerator, MMDeviceEnumerator};
|
|
11
|
+
use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_ALL, CLSCTX_INPROC_SERVER};
|
|
12
|
+
|
|
13
|
+
use crate::json;
|
|
14
|
+
|
|
15
|
+
fn endpoint() -> Result<IAudioEndpointVolume, String> {
|
|
16
|
+
// COM is initialized on this thread (tao's OleInitialize, wry's CoInitializeEx).
|
|
17
|
+
unsafe {
|
|
18
|
+
let devices: IMMDeviceEnumerator =
|
|
19
|
+
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER).map_err(|e| format!("INTERNAL: cannot list the audio devices: {e}"))?;
|
|
20
|
+
let device = devices.GetDefaultAudioEndpoint(eRender, eConsole).map_err(|_| "NOT_FOUND: there is no audio output".to_string())?;
|
|
21
|
+
device.Activate::<IAudioEndpointVolume>(CLSCTX_ALL, None).map_err(|e| format!("UNSUPPORTED: the audio output has no volume control: {e}"))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub fn state_json(level: f32, muted: bool) -> String {
|
|
26
|
+
format!(r#"{{"level":{},"muted":{},"settable":true}}"#, (f64::from(level) * 1000.0).round() / 1000.0, muted)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn state(volume: &IAudioEndpointVolume) -> Result<String, String> {
|
|
30
|
+
unsafe {
|
|
31
|
+
let level = volume.GetMasterVolumeLevelScalar().map_err(|e| format!("INTERNAL: cannot read the volume: {e}"))?;
|
|
32
|
+
let muted = volume.GetMute().map_err(|e| format!("INTERNAL: cannot read the mute: {e}"))?.as_bool();
|
|
33
|
+
Ok(state_json(level, muted))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn set(cmd: &json::V) -> Result<String, String> {
|
|
38
|
+
let level = cmd.get("level").and_then(|v| v.as_f64()).filter(|v| (0.0..=1.0).contains(v)).ok_or("level must be a number from 0 to 1")?;
|
|
39
|
+
let volume = endpoint()?;
|
|
40
|
+
unsafe { volume.SetMasterVolumeLevelScalar(level as f32, std::ptr::null()) }.map_err(|e| format!("INTERNAL: cannot set the volume: {e}"))?;
|
|
41
|
+
state(&volume)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn mute(cmd: &json::V) -> Result<String, String> {
|
|
45
|
+
let muted = cmd.get("muted").and_then(|v| v.as_bool()).ok_or("muted must be a boolean")?;
|
|
46
|
+
let volume = endpoint()?;
|
|
47
|
+
unsafe { volume.SetMute(muted, std::ptr::null()) }.map_err(|e| format!("INTERNAL: cannot set the mute: {e}"))?;
|
|
48
|
+
state(&volume)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// volume.* shell ops; None for other ops.
|
|
52
|
+
pub fn shell_op(op: &str, cmd: &json::V) -> Option<Result<String, String>> {
|
|
53
|
+
let result = match op {
|
|
54
|
+
"volume.get" => endpoint().and_then(|volume| state(&volume)),
|
|
55
|
+
"volume.set" => set(cmd),
|
|
56
|
+
"volume.mute" => mute(cmd),
|
|
57
|
+
_ if op.starts_with("volume.") => Err(format!("unknown op {op}")),
|
|
58
|
+
_ => return None,
|
|
59
|
+
};
|
|
60
|
+
Some(result)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(test)]
|
|
64
|
+
mod tests {
|
|
65
|
+
use super::*;
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn state_shape() {
|
|
69
|
+
assert_eq!(state_json(0.49999997, true), r#"{"level":0.5,"muted":true,"settable":true}"#);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -13,10 +13,20 @@ enum AkanNativeLog {
|
|
|
13
13
|
logger.info("\(text, privacy: .public)")
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/// One `[page<+> <level>]` tag per line, the desktop host's grammar, which the akan CLI reads the level back from;
|
|
17
|
+
/// os_log takes the page's level too, so `log stream` and Console.app filter on it.
|
|
16
18
|
static func page(_ level: String, _ text: String) {
|
|
17
|
-
|
|
19
|
+
let lines = text.replacingOccurrences(of: "\\n+$", with: "", options: .regularExpression)
|
|
20
|
+
.components(separatedBy: "\n")
|
|
21
|
+
print(lines.enumerated().map { "[page\($0.offset == 0 ? "" : "+") \(level)] \($0.element)" }.joined(separator: "\n"))
|
|
18
22
|
fflush(stdout)
|
|
19
|
-
|
|
23
|
+
let message = lines.joined(separator: "\n")
|
|
24
|
+
switch level {
|
|
25
|
+
case "error": logger.error("\(message, privacy: .public)")
|
|
26
|
+
case "warn": logger.warning("\(message, privacy: .public)")
|
|
27
|
+
case "trace", "verbose", "debug": logger.debug("\(message, privacy: .public)")
|
|
28
|
+
default: logger.info("\(message, privacy: .public)")
|
|
29
|
+
}
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
4
5
|
import { type DoctorCheck, doctorChecks, hostPlatforms, runFixes } from "./commands/doctor.ts";
|
|
5
6
|
import type { AkanNativeConfig } from "./config.ts";
|
|
7
|
+
import { makeNativeBoot } from "./lib/boot.ts";
|
|
8
|
+
import { BUNDLE_FILE, compatProblems, readBundleInfo } from "./lib/compat.ts";
|
|
6
9
|
import { CancelledError, withSignal } from "./lib/exec.ts";
|
|
7
10
|
import { type HmrContext, lanAddress, resolveDevEntry, startHmrServer } from "./lib/hmr.ts";
|
|
8
11
|
import type { Device, DeviceSelector, Launched } from "./lib/launch.ts";
|
|
@@ -19,8 +22,18 @@ import {
|
|
|
19
22
|
SigningError,
|
|
20
23
|
WebBuildError,
|
|
21
24
|
WebInputError,
|
|
25
|
+
type WindowsBuild,
|
|
22
26
|
} from "./lib/prepare.ts";
|
|
23
27
|
import { ConfigError, type Project, projectFromConfig } from "./lib/project.ts";
|
|
28
|
+
import { assertSigningKey, publishRelease } from "./lib/publish.ts";
|
|
29
|
+
import {
|
|
30
|
+
assertChannel,
|
|
31
|
+
generateUpdateKey,
|
|
32
|
+
type UpdateManifest,
|
|
33
|
+
updateKeyPath,
|
|
34
|
+
webManifest,
|
|
35
|
+
writeWebBundle,
|
|
36
|
+
} from "./lib/updates.ts";
|
|
24
37
|
import { androidDevices, launchAndroid } from "./platforms/android.ts";
|
|
25
38
|
import { PLATFORM_TARGETS, type TargetPlatform } from "./platforms/index.ts";
|
|
26
39
|
import { iosDevices, physicalIosDevice } from "./platforms/ios.ts";
|
|
@@ -36,10 +49,11 @@ export type {
|
|
|
36
49
|
IosSigningResult,
|
|
37
50
|
LogEvent,
|
|
38
51
|
TargetPlatform,
|
|
52
|
+
WindowsBuild,
|
|
39
53
|
};
|
|
40
54
|
|
|
41
55
|
/** semver of this API. A caller checks the major before it relies on anything here. */
|
|
42
|
-
export const API_VERSION = "0.
|
|
56
|
+
export const API_VERSION = "0.8.0";
|
|
43
57
|
|
|
44
58
|
export type AkanNativeErrorCode =
|
|
45
59
|
| "CONFIG_INVALID"
|
|
@@ -97,7 +111,7 @@ export interface TaskOptions {
|
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
export interface Artifact {
|
|
100
|
-
kind: "app" | "apk" | "aab" | "ipa" | "folder" | "web";
|
|
114
|
+
kind: "app" | "apk" | "aab" | "ipa" | "folder" | "web" | "installer";
|
|
101
115
|
path: string;
|
|
102
116
|
signing: "none" | "adhoc" | "debug" | "development" | "distribution";
|
|
103
117
|
device?: "simulator" | "device";
|
|
@@ -141,18 +155,17 @@ export function validateConfig(config: AkanNativeConfig, options: { appDir: stri
|
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
/** Builds the app. Default profile release (like `akan-native build`). `ios.device`: an iPhone build (signed; release adds an .ipa). */
|
|
144
|
-
export function build(
|
|
158
|
+
export function build(
|
|
159
|
+
options: TaskOptions & { profile?: BuildProfile; ios?: IosBuild; windows?: WindowsBuild },
|
|
160
|
+
): Promise<BuildResult> {
|
|
145
161
|
return task(
|
|
146
162
|
options,
|
|
147
163
|
async (warnings) =>
|
|
148
164
|
(
|
|
149
|
-
await buildIn(
|
|
150
|
-
options,
|
|
151
|
-
options.
|
|
152
|
-
|
|
153
|
-
warnings,
|
|
154
|
-
options.ios ? { ios: options.ios } : {},
|
|
155
|
-
)
|
|
165
|
+
await buildIn(options, options.profile ?? "release", options.mode ?? "production", warnings, {
|
|
166
|
+
...(options.ios ? { ios: options.ios } : {}),
|
|
167
|
+
...(options.windows ? { windows: options.windows } : {}),
|
|
168
|
+
})
|
|
156
169
|
).result,
|
|
157
170
|
);
|
|
158
171
|
}
|
|
@@ -184,6 +197,123 @@ export function release(
|
|
|
184
197
|
});
|
|
185
198
|
}
|
|
186
199
|
|
|
200
|
+
/**
|
|
201
|
+
* An unsigned web bundle update (UP-2), for a signer that keeps the key off the build machine. Writes
|
|
202
|
+
* `<out>/files/<sha256>`, `<out>/manifest.template.json` (the manifest with `channel`, `sequence` and `bundle` left
|
|
203
|
+
* empty for the signer) and `<out>/bundle.json` (the native API the bundle needs, UP-3). Prepares the web bundle as a
|
|
204
|
+
* release build does, without building the native app.
|
|
205
|
+
*/
|
|
206
|
+
export function packUpdate(
|
|
207
|
+
options: TaskOptions & { platform: "ios" | "android"; out: string },
|
|
208
|
+
): Promise<{ manifest: UpdateManifest; out: string }> {
|
|
209
|
+
return task(options, async () => {
|
|
210
|
+
const appDir = resolve(options.appDir);
|
|
211
|
+
let project: Project;
|
|
212
|
+
try {
|
|
213
|
+
project = projectFromConfig(options.config, appDir, { resolveFrom: options.resolveFrom });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const out = resolve(options.out);
|
|
219
|
+
mkdirSync(out, { recursive: true });
|
|
220
|
+
const ctx = await prepare(project, options.platform, {
|
|
221
|
+
mode: options.mode ?? "production",
|
|
222
|
+
profile: "release",
|
|
223
|
+
skipWebBuild: options.skipWebBuild ?? false,
|
|
224
|
+
outDir: out,
|
|
225
|
+
envFiles: options.envFiles,
|
|
226
|
+
env: options.env,
|
|
227
|
+
api: true,
|
|
228
|
+
}).catch((error) => {
|
|
229
|
+
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
230
|
+
});
|
|
231
|
+
makeNativeBoot(ctx, options.platform);
|
|
232
|
+
const info = readBundleInfo(join(out, BUNDLE_FILE));
|
|
233
|
+
const manifest = webManifest({
|
|
234
|
+
app: project.config.app,
|
|
235
|
+
platform: options.platform,
|
|
236
|
+
channel: "",
|
|
237
|
+
nativeApi: info.nativeApi.hash,
|
|
238
|
+
sequence: 0,
|
|
239
|
+
bundle: "",
|
|
240
|
+
files: writeWebBundle(out, ctx.webDir, ctx.html, ctx.env),
|
|
241
|
+
});
|
|
242
|
+
writeFileSync(join(out, "manifest.template.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
243
|
+
return { manifest, out };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface PublishResult {
|
|
248
|
+
build: BuildResult;
|
|
249
|
+
/** `<out>/<os>-<arch>` (desktop) or `<out>/<platform>` (a phone's web bundle): what to upload under updates.url. */
|
|
250
|
+
dir: string;
|
|
251
|
+
bundle: string;
|
|
252
|
+
channel: string;
|
|
253
|
+
files: number;
|
|
254
|
+
/** Bytes of the files the manifest names. */
|
|
255
|
+
size: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A signed release for the updates plugin (UP-1, UP-2): a release build, then `<channel>.json`, its signature and
|
|
260
|
+
* its files under `out` (default <appDir>/.akan/native/updates), signed with the key updateKeygen() made.
|
|
261
|
+
*/
|
|
262
|
+
export function publishUpdate(
|
|
263
|
+
options: TaskOptions & { platform: Exclude<TargetPlatform, "web">; channel?: string; out?: string },
|
|
264
|
+
): Promise<PublishResult> {
|
|
265
|
+
return task(options, async (warnings) => {
|
|
266
|
+
if (!options.config.updates)
|
|
267
|
+
throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url, publicKey } to publish for");
|
|
268
|
+
try {
|
|
269
|
+
if (options.channel !== undefined) assertChannel(options.channel);
|
|
270
|
+
assertSigningKey(options.config);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
273
|
+
}
|
|
274
|
+
const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings);
|
|
275
|
+
const channel = options.channel ?? ctx.project.config.updates?.channel ?? "production";
|
|
276
|
+
const out = resolve(options.out ?? resolve(options.appDir, ".akan", "native", "updates"));
|
|
277
|
+
const { dir, manifest } = await publishRelease(ctx, options.platform, artifact, out, channel).catch((error) => {
|
|
278
|
+
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
build: result,
|
|
282
|
+
dir,
|
|
283
|
+
bundle: manifest.bundle,
|
|
284
|
+
channel,
|
|
285
|
+
files: manifest.files.length,
|
|
286
|
+
size: manifest.files.reduce((n, f) => n + f.size, 0),
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether a web bundle can run in the binary a store release shipped (UP-3): `shipped` is that build's bundle.json,
|
|
293
|
+
* `bundle` the one `packUpdate` wrote. Anything in `problems` means a new binary.
|
|
294
|
+
*/
|
|
295
|
+
export function compareBundles(shipped: string, bundle: string): { compatible: boolean; problems: string[] } {
|
|
296
|
+
const [before, now] = [readBundleInfo(resolve(shipped)), readBundleInfo(resolve(bundle))];
|
|
297
|
+
const problems =
|
|
298
|
+
before.app.id !== now.app.id
|
|
299
|
+
? [`${shipped} is for ${before.app.id}, the bundle for ${now.app.id}`]
|
|
300
|
+
: compatProblems(before.nativeApi.inputs, now.nativeApi.inputs);
|
|
301
|
+
return { compatible: problems.length === 0, problems };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The app's update signing key: made once (AKAN_NATIVE_UPDATE_KEY, else ~/.akan/native/keys/<app id>.update.key),
|
|
306
|
+
* then read. `publicKey` goes into the config's `updates.publicKey`; the key file never leaves the machine.
|
|
307
|
+
*/
|
|
308
|
+
export function updateKeygen(options: { config: AkanNativeConfig }): {
|
|
309
|
+
publicKey: string;
|
|
310
|
+
keyPath: string;
|
|
311
|
+
created: boolean;
|
|
312
|
+
} {
|
|
313
|
+
const keyPath = updateKeyPath(options.config.app.id);
|
|
314
|
+
return { ...generateUpdateKey(keyPath), keyPath };
|
|
315
|
+
}
|
|
316
|
+
|
|
187
317
|
/** Builds (default profile debug) and launches the app on a simulator, emulator, desktop or browser. */
|
|
188
318
|
export function run(
|
|
189
319
|
options: TaskOptions & {
|
|
@@ -483,7 +613,7 @@ async function buildIn(
|
|
|
483
613
|
profile: BuildProfile,
|
|
484
614
|
mode: string,
|
|
485
615
|
warnings: string[],
|
|
486
|
-
extra: Pick<BuildOptions, "android" | "ios" | "devServer" | "startPath"> = {},
|
|
616
|
+
extra: Pick<BuildOptions, "android" | "ios" | "windows" | "devServer" | "startPath"> = {},
|
|
487
617
|
): Promise<{ ctx: BuildContext; artifact: string; result: BuildResult }> {
|
|
488
618
|
const started = performance.now();
|
|
489
619
|
const appDir = resolve(options.appDir);
|
|
@@ -8,14 +8,14 @@ import { findAppDir, loadProject } from "../lib/project.ts";
|
|
|
8
8
|
import { PLATFORM_TARGETS, TARGETS, type TargetPlatform } from "../platforms/index.ts";
|
|
9
9
|
import { physicalIosDevice } from "../platforms/ios.ts";
|
|
10
10
|
|
|
11
|
-
export const BOOLEAN_FLAGS = ["skip-web-build", "open", "release", "debug", "headless", "aab"];
|
|
11
|
+
export const BOOLEAN_FLAGS = ["skip-web-build", "open", "release", "debug", "headless", "aab", "installer"];
|
|
12
12
|
|
|
13
13
|
/** Flags of every build: the app, the env mode (.env.<mode>), the profile, the web build. */
|
|
14
|
-
export const BUILD_FLAGS = ["app", "mode", "release", "debug", "skip-web-build", "aab", "device", "build"];
|
|
14
|
+
export const BUILD_FLAGS = ["app", "mode", "release", "debug", "skip-web-build", "aab", "installer", "device", "build"];
|
|
15
15
|
/** Flags of launching a build (web server, simulators, emulators). */
|
|
16
16
|
export const LAUNCH_FLAGS = ["port", "host", "open", "headless", "device", "avd"];
|
|
17
17
|
|
|
18
|
-
export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--device] [--build <n>]`;
|
|
18
|
+
export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>]`;
|
|
19
19
|
export const RUN_USAGE = `akan-native run <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--release] [--skip-web-build] [--port <n>] [--open] [--headless] [--device <name>] [--avd <name>] [--start <path>]`;
|
|
20
20
|
|
|
21
21
|
export interface BuildDefaults {
|
|
@@ -57,6 +57,7 @@ export async function buildFromArgs(
|
|
|
57
57
|
if (project.plugins.length) log.info(dim(`plugins: ${project.plugins.map((p) => p.manifest.id).join(", ")}`));
|
|
58
58
|
|
|
59
59
|
if (args.flags.aab === true && platform !== "android") throw new CliError("--aab is for android", 2);
|
|
60
|
+
if (args.flags.installer === true && platform !== "windows") throw new CliError("--installer is for windows", 2);
|
|
60
61
|
|
|
61
62
|
const device = args.flags.device;
|
|
62
63
|
const iphone = platform === "ios" && typeof device === "string" ? await physicalIosDevice(device) : null;
|
|
@@ -69,6 +70,7 @@ export async function buildFromArgs(
|
|
|
69
70
|
startPath: extra.startPath,
|
|
70
71
|
|
|
71
72
|
...(args.flags.aab === true ? { android: { bundle: true } } : {}),
|
|
73
|
+
...(args.flags.installer === true ? { windows: { installer: true } } : {}),
|
|
72
74
|
|
|
73
75
|
...(iosDevice
|
|
74
76
|
? { ios: { device: true, ...(iphone ? { target: { udid: iphone.udid, name: iphone.name } } : {}) } }
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import { existsSync,
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
4
|
import { join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
|
|
6
|
-
import { runtimeVersion } from "../lib/boot.ts";
|
|
7
|
-
import { BUNDLE_FILE, readBundleInfo } from "../lib/compat.ts";
|
|
8
6
|
import { bold, CliError, dim, log } from "../lib/log.ts";
|
|
9
7
|
import { findAppDir, loadProject } from "../lib/project.ts";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
hostArch,
|
|
13
|
-
sha256,
|
|
14
|
-
signManifest,
|
|
15
|
-
type UpdateFile,
|
|
16
|
-
type UpdateManifest,
|
|
17
|
-
updateKeyPath,
|
|
18
|
-
} from "../lib/updates.ts";
|
|
19
|
-
import { publishAppUpdate } from "../platforms/desktop-update.ts";
|
|
8
|
+
import { assertSigningKey, publishRelease } from "../lib/publish.ts";
|
|
9
|
+
import { assertChannel, generateUpdateKey, updateKeyPath } from "../lib/updates.ts";
|
|
20
10
|
import { BOOLEAN_FLAGS, buildFromArgs } from "./build.ts";
|
|
21
11
|
|
|
22
12
|
export const UPDATE_USAGE =
|
|
@@ -42,80 +32,29 @@ async function keygen(args: ParsedArgs): Promise<number> {
|
|
|
42
32
|
return 0;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/** Every file of a web bundle as the app serves it: index.html with the init script and CSP, public/, env.runtime.json. */
|
|
46
|
-
function webFiles(webDir: string, html: string, env: Record<string, string>): { path: string; data: Uint8Array }[] {
|
|
47
|
-
const files: { path: string; data: Uint8Array }[] = [];
|
|
48
|
-
const walk = (dir: string) => {
|
|
49
|
-
for (const name of readdirSync(dir).sort()) {
|
|
50
|
-
const path = join(dir, name);
|
|
51
|
-
if (statSync(path).isDirectory()) walk(path);
|
|
52
|
-
else {
|
|
53
|
-
const rel = relative(webDir, path);
|
|
54
|
-
files.push({ path: rel, data: rel === "index.html" ? new TextEncoder().encode(html) : readFileSync(path) });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
walk(webDir);
|
|
59
|
-
files.push({ path: "env.runtime.json", data: new TextEncoder().encode(JSON.stringify(env, null, 2)) });
|
|
60
|
-
return files;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
35
|
async function publish(args: ParsedArgs): Promise<number> {
|
|
64
36
|
const arg = args.positional[1];
|
|
65
37
|
const desktop = arg === "macos" || arg === "windows" || arg === "linux" ? arg : null;
|
|
66
38
|
if (!desktop && arg !== "ios" && arg !== "android") throw new CliError(`usage: ${UPDATE_USAGE}`, 2);
|
|
67
39
|
const platform = desktop ?? (arg as "ios" | "android");
|
|
40
|
+
const requested = stringFlag(args, "channel");
|
|
41
|
+
if (requested !== undefined) assertChannel(requested);
|
|
42
|
+
assertSigningKey((await loadProject(findAppDir(stringFlag(args, "app") ?? process.cwd()))).config);
|
|
68
43
|
const { ctx, artifact } = await buildFromArgs(
|
|
69
44
|
{ positional: [platform], flags: args.flags },
|
|
70
45
|
{ mode: "production", profile: "release" },
|
|
71
46
|
platform,
|
|
72
47
|
);
|
|
73
48
|
const { config, appDir } = ctx.project;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const channel = stringFlag(args, "channel") ?? config.updates.channel;
|
|
77
|
-
|
|
78
|
-
const out = join(outDir(args, appDir), desktop ? `${desktop}-${hostArch()}` : platform);
|
|
79
|
-
const keyPath = updateKeyPath(config.app.id);
|
|
80
|
-
const sequence = Math.floor(Date.now() / 1000);
|
|
81
|
-
|
|
82
|
-
let manifest: UpdateManifest;
|
|
83
|
-
if (desktop) {
|
|
84
|
-
manifest = await publishAppUpdate(ctx, desktop, artifact, out, channel, sequence);
|
|
85
|
-
} else {
|
|
86
|
-
const info = readBundleInfo(join(ctx.outDir, BUNDLE_FILE));
|
|
87
|
-
mkdirSync(join(out, "files"), { recursive: true });
|
|
88
|
-
const files: UpdateFile[] = webFiles(ctx.webDir, ctx.html, ctx.env).map(({ path, data }) => {
|
|
89
|
-
const hash = sha256(data);
|
|
90
|
-
const target = join(out, "files", hash);
|
|
91
|
-
if (!existsSync(target)) writeFileSync(target, data);
|
|
92
|
-
return { path, sha256: hash, size: data.length };
|
|
93
|
-
});
|
|
94
|
-
manifest = {
|
|
95
|
-
schema: 1,
|
|
96
|
-
kind: "web",
|
|
97
|
-
app: config.app.id,
|
|
98
|
-
platform,
|
|
99
|
-
channel,
|
|
100
|
-
nativeApi: info.nativeApi.hash,
|
|
101
|
-
sequence,
|
|
102
|
-
bundle: `${sequence}-${info.web.hash.slice(0, 8)}`,
|
|
103
|
-
runtimeVersion: runtimeVersion(),
|
|
104
|
-
version: config.app.version,
|
|
105
|
-
files,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const bytes = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
109
|
-
mkdirSync(out, { recursive: true });
|
|
110
|
-
writeFileSync(join(out, `${channel}.json`), bytes);
|
|
111
|
-
writeFileSync(join(out, `${channel}.json.sig`), `${signManifest(bytes, keyPath, config.updates.publicKey)}\n`);
|
|
49
|
+
const channel = requested ?? config.updates?.channel ?? "production";
|
|
50
|
+
const { dir, manifest } = await publishRelease(ctx, platform, artifact, outDir(args, appDir), channel);
|
|
112
51
|
const size = manifest.files.reduce((n, f) => n + f.size, 0);
|
|
113
52
|
log.ok(
|
|
114
|
-
`published ${bold(manifest.bundle)} to ${relative(process.cwd(),
|
|
53
|
+
`published ${bold(manifest.bundle)} to ${relative(process.cwd(), dir) || dir}/${channel}.json ${dim(`(${manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB${manifest.nativeApi ? `, native API ${manifest.nativeApi}` : ""})`)}`,
|
|
115
54
|
);
|
|
116
55
|
log.info(
|
|
117
56
|
dim(
|
|
118
|
-
`Upload ${relative(process.cwd(), join(
|
|
57
|
+
`Upload ${relative(process.cwd(), join(dir, ".."))} to ${config.updates?.url} (or try it with \`akan-native update serve\`).`,
|
|
119
58
|
),
|
|
120
59
|
);
|
|
121
60
|
return 0;
|
|
@@ -181,6 +181,11 @@ export interface AkanNativeConfig {
|
|
|
181
181
|
* or APIs the bundle uses. Android 10+ devices with Google Play update WebView themselves.
|
|
182
182
|
*/
|
|
183
183
|
minWebViewVersion?: number;
|
|
184
|
+
/**
|
|
185
|
+
* Media plays with sound without a tap first, as it does on iOS and the desktop (WebView's
|
|
186
|
+
* mediaPlaybackRequiresUserGesture off): an app nobody taps, such as a signage screen. Default false.
|
|
187
|
+
*/
|
|
188
|
+
autoplay?: boolean;
|
|
184
189
|
/**
|
|
185
190
|
* Appended to app.id in debug builds (e.g. ".debug"), so a debug build installs next to the
|
|
186
191
|
* release app. Deep links and app links must list the suffixed id for debug builds.
|
|
@@ -227,6 +232,42 @@ export interface AkanNativeConfig {
|
|
|
227
232
|
* hides the window: the app keeps running, a Dock click shows it again, Cmd+Q quits. Default true.
|
|
228
233
|
*/
|
|
229
234
|
quitOnLastWindowClosed?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* What a window does when its page's web process ends: a crash, a hang, out of memory. "errorPage"
|
|
237
|
+
* (default) loads the page again once and shows an error page when it ends again within a minute, so a
|
|
238
|
+
* page that crashes on load does not loop; the app quits when the webview's browser process ends
|
|
239
|
+
* (Windows). "reload" loads it again every time, waiting longer after each end in a row (1 s, doubling
|
|
240
|
+
* to a minute), and relaunches the app when the browser process ends: an app nobody attends, such as a
|
|
241
|
+
* kiosk or a signage screen. iOS and Android always start the page over.
|
|
242
|
+
*/
|
|
243
|
+
recovery?: "errorPage" | "reload";
|
|
244
|
+
/**
|
|
245
|
+
* The main window from its first frame: `fullscreen` (borderless, on the display the window opens on)
|
|
246
|
+
* and `skipTaskbar` (no taskbar button; Windows and Linux). A plugin's launch phase may still decide
|
|
247
|
+
* otherwise (DesktopContext.launch.setWindow). Default false for both.
|
|
248
|
+
*/
|
|
249
|
+
window?: { fullscreen?: boolean; skipTaskbar?: boolean };
|
|
250
|
+
/**
|
|
251
|
+
* Windows: "auto" answers the page's getDisplayMedia() with the first screen at once, without the picker or a
|
|
252
|
+
* user gesture: a remote-support session on an unattended screen. It is Chromium's switch for automated media
|
|
253
|
+
* tests (--use-fake-ui-for-media-stream), made for every media request, so keep it to an app whose pages ask
|
|
254
|
+
* for no camera or microphone. Default "picker". WebView2's choice by title follows the UI language, so none is
|
|
255
|
+
* offered. macOS and Linux ignore it.
|
|
256
|
+
*/
|
|
257
|
+
screenCapture?: "picker" | "auto";
|
|
258
|
+
/**
|
|
259
|
+
* A server the app starts beside its window (akanjs `native.desktop.server`). `dir` is copied to
|
|
260
|
+
* the app's resources (`server/`), and `entry` there runs on the app's own Bun with `env`, bound to
|
|
261
|
+
* a loopback port picked at launch; the page reads its URL as PUBLIC_AKAN_SERVER_URL. The launcher
|
|
262
|
+
* sets PORT, JWT_SECRET, the data folders (`<app local data>/server`) and the listen host itself.
|
|
263
|
+
*/
|
|
264
|
+
server?: { dir: string; entry: string; env?: Record<string, string> };
|
|
265
|
+
/**
|
|
266
|
+
* A folder of executables the app carries (akanjs `bin`): copied to the app's resources (`bin/`) and put
|
|
267
|
+
* first on the app's PATH at launch, so its plugins and its server find them by name before the computer's.
|
|
268
|
+
* macOS builds sign every Mach-O file in it.
|
|
269
|
+
*/
|
|
270
|
+
bin?: string;
|
|
230
271
|
};
|
|
231
272
|
/**
|
|
232
273
|
* App icon for every platform, generated from one square PNG, ideally 1024×1024 (CLI-8).
|
|
@@ -28,8 +28,15 @@ const KNOWN: Shape = {
|
|
|
28
28
|
resources: { "[]": { from: true, to: true } },
|
|
29
29
|
},
|
|
30
30
|
security: { csp: true, shell: { externalSchemes: true } },
|
|
31
|
-
android: { minWebViewVersion: true, debugAppIdSuffix: true, googleServices: true },
|
|
32
|
-
desktop: {
|
|
31
|
+
android: { minWebViewVersion: true, debugAppIdSuffix: true, googleServices: true, autoplay: true },
|
|
32
|
+
desktop: {
|
|
33
|
+
quitOnLastWindowClosed: true,
|
|
34
|
+
recovery: true,
|
|
35
|
+
window: { fullscreen: true, skipTaskbar: true },
|
|
36
|
+
screenCapture: true,
|
|
37
|
+
server: { dir: true, entry: true, env: true },
|
|
38
|
+
bin: true,
|
|
39
|
+
},
|
|
33
40
|
keyboard: { resize: true },
|
|
34
41
|
push: {
|
|
35
42
|
android: { channel: { id: true, name: true, importance: true, description: true }, smallIcon: true, color: true },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { LAUNCHER_ENV_KEYS } from "../../../desktop/src/server.ts";
|
|
6
|
+
import type { AkanNativeConfig } from "../config.ts";
|
|
7
|
+
|
|
8
|
+
export interface DesktopServerConfig {
|
|
9
|
+
/** Absolute. */
|
|
10
|
+
dir: string;
|
|
11
|
+
entry: string;
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16
|
+
const LAUNCHER_KEYS = new Set<string>(LAUNCHER_ENV_KEYS);
|
|
17
|
+
|
|
18
|
+
export function validateDesktopServer(
|
|
19
|
+
desktop: AkanNativeConfig["desktop"],
|
|
20
|
+
appDir: string,
|
|
21
|
+
problems: string[],
|
|
22
|
+
): DesktopServerConfig | null {
|
|
23
|
+
const server = desktop?.server;
|
|
24
|
+
if (server === undefined) return null;
|
|
25
|
+
if (!server || typeof server !== "object") {
|
|
26
|
+
problems.push("desktop.server must be an object with dir and entry");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const before = problems.length;
|
|
30
|
+
const dir = typeof server.dir === "string" && server.dir ? resolve(appDir, server.dir) : null;
|
|
31
|
+
if (!dir) problems.push("desktop.server.dir is required: the folder the server runs from");
|
|
32
|
+
else if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
33
|
+
problems.push(`desktop.server.dir: ${dir} is not a folder`);
|
|
34
|
+
const entry = typeof server.entry === "string" ? server.entry : "";
|
|
35
|
+
if (!entry || isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))
|
|
36
|
+
problems.push(
|
|
37
|
+
`desktop.server.entry must be a file inside desktop.server.dir (got ${JSON.stringify(server.entry)})`,
|
|
38
|
+
);
|
|
39
|
+
else if (dir && existsSync(dir) && !existsSync(join(dir, entry)))
|
|
40
|
+
problems.push(`desktop.server.entry: ${join(dir, entry)} not found`);
|
|
41
|
+
const env: Record<string, string> = {};
|
|
42
|
+
if (server.env !== undefined && (!server.env || typeof server.env !== "object" || Array.isArray(server.env)))
|
|
43
|
+
problems.push("desktop.server.env must be an object of strings");
|
|
44
|
+
else
|
|
45
|
+
for (const [key, value] of Object.entries(server.env ?? {})) {
|
|
46
|
+
|
|
47
|
+
const upper = key.toUpperCase();
|
|
48
|
+
if (!ENV_NAME.test(key)) problems.push(`desktop.server.env: ${JSON.stringify(key)} is not a variable name`);
|
|
49
|
+
else if (LAUNCHER_KEYS.has(upper))
|
|
50
|
+
problems.push(`desktop.server.env.${key}: the launcher sets it at every start`);
|
|
51
|
+
else if (upper === "PATH" && key !== "PATH") problems.push(`desktop.server.env.${key}: write it as PATH`);
|
|
52
|
+
else if (Object.keys(env).some((kept) => kept.toUpperCase() === upper))
|
|
53
|
+
problems.push(`desktop.server.env.${key}: Windows reads it as the same variable as another one here`);
|
|
54
|
+
else if (typeof value !== "string") problems.push(`desktop.server.env.${key} must be a string`);
|
|
55
|
+
else env[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return problems.length === before && dir ? { dir, entry, env } : null;
|
|
58
|
+
}
|
|
@@ -413,6 +413,8 @@ export async function startHmrServer(opts: HmrOptions): Promise<HmrServer> {
|
|
|
413
413
|
Bun.serve({
|
|
414
414
|
hostname: opts.hostname,
|
|
415
415
|
port: opts.port,
|
|
416
|
+
|
|
417
|
+
idleTimeout: 0,
|
|
416
418
|
async fetch(req, srv) {
|
|
417
419
|
const url = new URL(req.url);
|
|
418
420
|
if (relayed.has(url.pathname) && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
@@ -26,6 +26,10 @@ const DEV_SERVER_PAGE = `<!doctype html>
|
|
|
26
26
|
</html>
|
|
27
27
|
`;
|
|
28
28
|
|
|
29
|
+
export interface WindowsBuild {
|
|
30
|
+
installer?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
export interface BuildOptions {
|
|
30
34
|
mode: string;
|
|
31
35
|
profile: BuildProfile;
|
|
@@ -57,6 +61,8 @@ export interface BuildOptions {
|
|
|
57
61
|
android?: AndroidRelease;
|
|
58
62
|
/** iOS devices (akanjs readiness O1-2, O1-3). */
|
|
59
63
|
ios?: IosBuild;
|
|
64
|
+
/** Windows: an NSIS setup program next to the app folder (platforms/windows-installer.ts). */
|
|
65
|
+
windows?: WindowsBuild;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/** Which identity and profile an iOS device build signs with; unset: found in the keychain and Xcode's profiles. */
|
|
@@ -138,8 +144,9 @@ export interface BuildContext {
|
|
|
138
144
|
startPath?: string;
|
|
139
145
|
android?: AndroidRelease;
|
|
140
146
|
ios?: IosBuild;
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
windows?: WindowsBuild;
|
|
148
|
+
/** Artifacts besides the one the builder returns (an .aab next to the .apk, a Windows setup program). */
|
|
149
|
+
artifacts: { kind: "aab" | "ipa" | "installer"; path: string }[];
|
|
143
150
|
/** What the builder signed with, when it signs (Android: the app's release key or the debug key). */
|
|
144
151
|
signedAs?: "debug" | "development" | "distribution";
|
|
145
152
|
/** Called through the API (BuildOptions.api). */
|
|
@@ -271,6 +278,7 @@ export async function prepare(project: Project, platform: Platform, options: Bui
|
|
|
271
278
|
...(options.profile === "debug" && options.startPath ? { startPath: checkStartPath(options.startPath) } : {}),
|
|
272
279
|
...(options.android ? { android: options.android } : {}),
|
|
273
280
|
...(options.ios ? { ios: options.ios } : {}),
|
|
281
|
+
...(options.windows ? { windows: options.windows } : {}),
|
|
274
282
|
artifacts: [],
|
|
275
283
|
};
|
|
276
284
|
}
|