craft-native 0.0.396 → 0.0.408
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/README.md +7 -0
- package/dist/android/templates/CraftBridge.kt.template +2 -0
- package/dist/android/templates/CraftNative.kt.template +7 -0
- package/dist/android/templates/CraftNativeServices.kt.template +2 -1
- package/dist/android/templates/CraftNativeShell.kt.template +12 -2
- package/dist/android/templates/LocationRecordingService.kt.template +125 -6
- package/dist/android/templates/MainActivityNative.kt.template +21 -1
- package/dist/api/mobile.d.ts +59 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +54 -7
- package/dist/index.js +54 -7
- package/dist/ios/src/index.d.ts +7 -0
- package/dist/ios/templates/CraftApp.swift +268 -12
- package/dist/ios/templates/CraftHybrid.swift +1 -0
- package/dist/ios/templates/CraftNativeActions.swift +3 -1
- package/dist/ios/templates/CraftNativeScreen.swift +1 -0
- package/dist/ios/templates/CraftNativeUI.swift +89 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +17 -1
- package/dist/macos/src/index.d.ts +14 -3
- package/dist/macos/src/index.js +124 -27
- package/dist/macos/src/toolchain.d.ts +56 -0
- package/dist/macos/templates/CraftMacApplication.swift +54 -9
- package/dist/macos/templates/CraftMacRenderer.swift +376 -18
- package/dist/macos/templates/CraftMacRuntime.swift +114 -1
- package/dist/macos/templates/CraftMacWebView.swift +416 -0
- package/dist/mobile.js +53 -6
- package/package.json +1 -1
|
@@ -7629,6 +7629,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7629
7629
|
appearance.configureWithOpaqueBackground()
|
|
7630
7630
|
appearance.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
7631
7631
|
appearance.shadowColor = .clear
|
|
7632
|
+
CraftChromeFonts.apply(to: appearance)
|
|
7632
7633
|
navigationItem.standardAppearance = appearance
|
|
7633
7634
|
navigationItem.scrollEdgeAppearance = appearance
|
|
7634
7635
|
navigationItem.compactAppearance = appearance
|
|
@@ -517,6 +517,95 @@ enum CraftSymbols {
|
|
|
517
517
|
/// controller, which offers no way to say which style it wants, so the first
|
|
518
518
|
/// call teaches that controller's class to answer from here. Only that exact
|
|
519
519
|
/// class is touched, and only once.
|
|
520
|
+
/// The app's typefaces on UIKit's own chrome. A screen's text follows the
|
|
521
|
+
/// stx tree's `fontFamily`; the navigation bar, the tab bar and bar button
|
|
522
|
+
/// labels are UIKit's, outside that tree, so a branded app drew them in the
|
|
523
|
+
/// system face beside screens in its own. `fontFamily` / `headingFontFamily`
|
|
524
|
+
/// (CSS stacks, as the screens use) put the same faces there, through the
|
|
525
|
+
/// appearance proxies every bar starts from.
|
|
526
|
+
enum CraftChromeFonts {
|
|
527
|
+
/// The packaged family a body or heading stack resolved to; nil keeps the system face.
|
|
528
|
+
private(set) static var body: String?
|
|
529
|
+
private(set) static var heading: String?
|
|
530
|
+
|
|
531
|
+
/// The first family of a CSS stack the app can draw. A generic family
|
|
532
|
+
/// ahead of any packaged one means the system face: nil.
|
|
533
|
+
static func family(_ stack: String?) -> String? {
|
|
534
|
+
guard let stack else { return nil }
|
|
535
|
+
for part in stack.split(separator: ",") {
|
|
536
|
+
let name = part.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
|
|
537
|
+
switch name.lowercased() {
|
|
538
|
+
case "": continue
|
|
539
|
+
case "sans-serif", "ui-sans-serif", "system-ui", "-apple-system", "serif", "ui-serif", "monospace", "ui-monospace": return nil
|
|
540
|
+
default: if UIFont(name: name, size: 12) != nil { return name }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return nil
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// `family` at a chrome size, bold for a heavy weight and scaled with Dynamic Type.
|
|
547
|
+
static func font(_ family: String?, size: CGFloat, weight: UIFont.Weight, textStyle: UIFont.TextStyle) -> UIFont? {
|
|
548
|
+
guard let family, let custom = UIFont(name: family, size: size) else { return nil }
|
|
549
|
+
var traits = custom.fontDescriptor.symbolicTraits
|
|
550
|
+
if weight >= .semibold { traits.insert(.traitBold) }
|
|
551
|
+
let descriptor = custom.fontDescriptor.withSymbolicTraits(traits) ?? custom.fontDescriptor
|
|
552
|
+
return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: UIFont(descriptor: descriptor, size: size))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
static var titleFont: UIFont? { font(heading, size: 17, weight: .semibold, textStyle: .headline) }
|
|
556
|
+
static var largeTitleFont: UIFont? { font(heading, size: 34, weight: .bold, textStyle: .largeTitle) }
|
|
557
|
+
static var tabLabelFont: UIFont? { font(body, size: 10, weight: .medium, textStyle: .caption2) }
|
|
558
|
+
static var barButtonFont: UIFont? { font(body, size: 17, weight: .regular, textStyle: .body) }
|
|
559
|
+
|
|
560
|
+
static func configure(_ config: CraftConfig) {
|
|
561
|
+
body = family(config.fontFamily)
|
|
562
|
+
heading = family(config.headingFontFamily) ?? body
|
|
563
|
+
if let title = titleFont, let large = largeTitleFont {
|
|
564
|
+
let bar = UINavigationBar.appearance()
|
|
565
|
+
bar.titleTextAttributes = [.font: title]
|
|
566
|
+
bar.largeTitleTextAttributes = [.font: large]
|
|
567
|
+
}
|
|
568
|
+
if let label = tabLabelFont {
|
|
569
|
+
for state in [UIControl.State.normal, .selected] {
|
|
570
|
+
UITabBarItem.appearance().setTitleTextAttributes([.font: label], for: state)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if let button = barButtonFont {
|
|
574
|
+
for state in [UIControl.State.normal, .highlighted, .disabled] {
|
|
575
|
+
UIBarButtonItem.appearance().setTitleTextAttributes([.font: button], for: state)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// The label face on a tab bar. The Liquid Glass bar reads its labels from
|
|
581
|
+
/// its own appearance and ignores UITabBarItem's proxy, so the face goes on
|
|
582
|
+
/// a copy of the bar's appearance, which keeps its material untouched.
|
|
583
|
+
static func apply(to tabBar: UITabBar) {
|
|
584
|
+
guard let label = tabLabelFont else { return }
|
|
585
|
+
func labelled(_ appearance: UITabBarAppearance) -> UITabBarAppearance {
|
|
586
|
+
let copy = appearance.copy()
|
|
587
|
+
for layout in [copy.stackedLayoutAppearance, copy.inlineLayoutAppearance, copy.compactInlineLayoutAppearance] {
|
|
588
|
+
for state in [layout.normal, layout.selected, layout.focused, layout.disabled] {
|
|
589
|
+
state.titleTextAttributes[.font] = label
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return copy
|
|
593
|
+
}
|
|
594
|
+
tabBar.standardAppearance = labelled(tabBar.standardAppearance)
|
|
595
|
+
if let edge = tabBar.scrollEdgeAppearance { tabBar.scrollEdgeAppearance = labelled(edge) }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/// The title faces on a bar appearance a screen builds itself (an opaque bar).
|
|
599
|
+
static func apply(to appearance: UINavigationBarAppearance) {
|
|
600
|
+
if let title = titleFont { appearance.titleTextAttributes[.font] = title }
|
|
601
|
+
if let large = largeTitleFont { appearance.largeTitleTextAttributes[.font] = large }
|
|
602
|
+
if let button = barButtonFont {
|
|
603
|
+
appearance.buttonAppearance.normal.titleTextAttributes[.font] = button
|
|
604
|
+
appearance.doneButtonAppearance.normal.titleTextAttributes[.font] = button
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
520
609
|
enum CraftStatusBar {
|
|
521
610
|
static var style: UIStatusBarStyle = .default
|
|
522
611
|
private static var taught = Set<ObjectIdentifier>()
|
|
@@ -17,7 +17,7 @@ import Darwin
|
|
|
17
17
|
/// openurl <url> opens a link through the system, answering "Open in the app?"
|
|
18
18
|
/// universallink <https-url> waits up to 60s for an OS-verified link; never opens a browser
|
|
19
19
|
/// shot <name> screenshot → shots/<name>.png
|
|
20
|
-
/// tree <name> accessibility tree → shots/<name>.txt
|
|
20
|
+
/// tree <name> accessibility tree + identified snapshot values → shots/<name>.txt / <name>-values.json
|
|
21
21
|
/// tap <text> first hittable element whose label contains text
|
|
22
22
|
/// tapif <text> tap it when it is on screen, else carry on (the big timer's "Show the controls")
|
|
23
23
|
/// tapid <identifier>
|
|
@@ -367,6 +367,22 @@ final class DriverTests: XCTestCase {
|
|
|
367
367
|
post("/save?name=\(arg).png", shot.pngRepresentation)
|
|
368
368
|
case "tree":
|
|
369
369
|
post("/save?name=\(arg).txt", Data(app.debugDescription.utf8))
|
|
370
|
+
// debugDescription abbreviates values further than the snapshot.
|
|
371
|
+
// Retain snapshot values separately; long receipts chunk themselves.
|
|
372
|
+
do {
|
|
373
|
+
var values: [[String: String]] = []
|
|
374
|
+
func collect(_ node: XCUIElementSnapshot) {
|
|
375
|
+
if !node.identifier.isEmpty, let value = node.value as? String {
|
|
376
|
+
values.append(["id": node.identifier, "label": node.label, "value": value])
|
|
377
|
+
}
|
|
378
|
+
for child in node.children { collect(child) }
|
|
379
|
+
}
|
|
380
|
+
collect(try app.snapshot())
|
|
381
|
+
let data = try JSONSerialization.data(withJSONObject: values, options: [.sortedKeys])
|
|
382
|
+
post("/save?name=\(arg)-values.json", data)
|
|
383
|
+
} catch {
|
|
384
|
+
XCTFail("Could not retain accessibility values: \(error)")
|
|
385
|
+
}
|
|
370
386
|
case "tap":
|
|
371
387
|
if let target = waitFor(arg, 10) { press(target); note("tap \(arg): ok") }
|
|
372
388
|
else { XCTFail("Element not found"); note("FAIL tap \(arg): NOT FOUND") }
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
export { macosToolchain, pantryExecutable, resolveMacOSToolchain, resolveZig, toolchainCacheDir } from './toolchain.js';
|
|
2
|
+
export type { MacOSToolchain } from './toolchain.js';
|
|
1
3
|
export interface MacOSNativeBuildOptions {
|
|
2
4
|
outdir: string;
|
|
3
5
|
/** A compiled stx native route bundle, never HTML. */
|
|
4
6
|
nativeBundle: string;
|
|
7
|
+
/** Defaults to the pinned compiler, installed by Pantry (see `resolveMacOSToolchain`). */
|
|
5
8
|
zig?: string;
|
|
6
|
-
/** zig-js checkout and its public static archive. No system JSC fallback. */
|
|
7
|
-
zigJS
|
|
9
|
+
/** zig-js checkout and its public static archive. Default: the pinned revision, fetched and built once. No system JSC fallback. */
|
|
10
|
+
zigJS?: {
|
|
8
11
|
source: string;
|
|
9
12
|
library: string;
|
|
10
13
|
};
|
|
14
|
+
/** Where the toolchain is cached; default `~/.cache/craft/toolchain`. */
|
|
15
|
+
toolchainDir?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Link WebKit so screens can show web pages in a `<WebView tab="...">` and drive
|
|
18
|
+
* tabs through `craft.web` (a browser, a help viewer). Off by default: the host
|
|
19
|
+
* then links no WebKit at all, and `WebView` reports itself unsupported.
|
|
20
|
+
*/
|
|
21
|
+
webkit?: boolean;
|
|
11
22
|
}
|
|
12
|
-
/** Compile Craft's AppKit host and the shared Zig layout core. No WebKit. */
|
|
23
|
+
/** Compile Craft's AppKit host and the shared Zig layout core. No WebKit unless `webkit` is set. */
|
|
13
24
|
export declare function buildMacOSNative(options: MacOSNativeBuildOptions): Promise<{
|
|
14
25
|
executable: string;
|
|
15
26
|
bundle: string;
|
package/dist/macos/src/index.js
CHANGED
|
@@ -1,18 +1,109 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../macos/src/index.ts
|
|
3
3
|
import { cp, mkdir } from "fs/promises";
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import { join, resolve } from "path";
|
|
4
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
import { join as join2, resolve } from "path";
|
|
6
|
+
import process2 from "process";
|
|
7
|
+
|
|
8
|
+
// ../macos/src/toolchain.ts
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
6
12
|
import process from "process";
|
|
13
|
+
var macosToolchain = {
|
|
14
|
+
zig: "0.18.0-dev.252+86dc5ce25",
|
|
15
|
+
sources: {
|
|
16
|
+
"zig-js": { repository: "zig-utils/zig-js", revision: "f0072c95780712d1bb789280e08e7e390e83ab94", paths: ["src", "include", "tests", "tools", "docs", "bench", "conformance"] },
|
|
17
|
+
"zig-regex": { repository: "zig-utils/zig-regex", revision: "fdd9c2e333ea6ac2fd06d080d939ef7f11d61b1b", paths: ["src"] },
|
|
18
|
+
"zig-gc": { repository: "zig-utils/zig-gc", revision: "0285ef801dd221a4ae8e88b31a09ae31627657a3", paths: ["src"] }
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var defaultRun = async (args, cwd) => {
|
|
22
|
+
const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
23
|
+
const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
|
24
|
+
return { exitCode, stdout, stderr };
|
|
25
|
+
};
|
|
26
|
+
function toolchainCacheDir(env = process.env) {
|
|
27
|
+
return env.CRAFT_TOOLCHAIN_DIR || join(env.XDG_CACHE_HOME || join(env.HOME || homedir(), ".cache"), "craft", "toolchain");
|
|
28
|
+
}
|
|
29
|
+
function pantryExecutable(env = process.env, exists = existsSync) {
|
|
30
|
+
const home = env.HOME || homedir();
|
|
31
|
+
return [env.PANTRY_BIN, join(process.cwd(), "pantry/.bin/pantry"), join(home, ".local/bin/pantry")].find((path) => path && exists(path)) ?? "pantry";
|
|
32
|
+
}
|
|
33
|
+
async function resolveZig(options = {}) {
|
|
34
|
+
const env = options.env ?? process.env;
|
|
35
|
+
if (env.CRAFT_ZIG)
|
|
36
|
+
return env.CRAFT_ZIG;
|
|
37
|
+
const cache = options.cacheDir ?? toolchainCacheDir(env);
|
|
38
|
+
const zig = join(cache, "pantry/ziglang.org", `v${macosToolchain.zig}`, "bin/zig");
|
|
39
|
+
if (existsSync(zig))
|
|
40
|
+
return zig;
|
|
41
|
+
const run = options.run ?? defaultRun;
|
|
42
|
+
mkdirSync(cache, { recursive: true });
|
|
43
|
+
options.log?.(`Installing Zig ${macosToolchain.zig} with Pantry`);
|
|
44
|
+
const result = await run([pantryExecutable(env), "install", `ziglang.org@${macosToolchain.zig}`, "--no-save", "--ignore-scripts"], cache);
|
|
45
|
+
if (result.exitCode !== 0 || !existsSync(zig))
|
|
46
|
+
throw new Error(`Pantry could not install ziglang.org@${macosToolchain.zig} into ${cache}: ${result.stderr?.trim().slice(-400) ?? ""}. Install Pantry (https://pantry.dev) or set CRAFT_ZIG.`);
|
|
47
|
+
return zig;
|
|
48
|
+
}
|
|
49
|
+
async function fetchSource(name, cache, run, log) {
|
|
50
|
+
const pin = macosToolchain.sources[name];
|
|
51
|
+
const directory = join(cache, "sources", name);
|
|
52
|
+
const git = async (args) => {
|
|
53
|
+
const result = await run(["git", ...args], directory);
|
|
54
|
+
if (result.exitCode !== 0)
|
|
55
|
+
throw new Error(`git ${args.join(" ")} failed in ${directory}: ${result.stderr?.trim() ?? ""}`);
|
|
56
|
+
return result.stdout.trim();
|
|
57
|
+
};
|
|
58
|
+
mkdirSync(directory, { recursive: true });
|
|
59
|
+
if (!existsSync(join(directory, ".git"))) {
|
|
60
|
+
await git(["init", "--quiet"]);
|
|
61
|
+
await git(["remote", "add", "origin", `https://github.com/${pin.repository}.git`]);
|
|
62
|
+
await git(["sparse-checkout", "set", ...pin.paths]);
|
|
63
|
+
}
|
|
64
|
+
const head = await run(["git", "rev-parse", "HEAD"], directory);
|
|
65
|
+
if (head.exitCode !== 0 || head.stdout.trim() !== pin.revision) {
|
|
66
|
+
log?.(`Fetching ${name} ${pin.revision.slice(0, 8)}`);
|
|
67
|
+
await git(["fetch", "--quiet", "--filter=blob:none", "--depth=1", "origin", pin.revision]);
|
|
68
|
+
await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
|
|
69
|
+
}
|
|
70
|
+
return directory;
|
|
71
|
+
}
|
|
72
|
+
async function resolveMacOSToolchain(options = {}) {
|
|
73
|
+
const env = options.env ?? process.env;
|
|
74
|
+
const run = options.run ?? defaultRun;
|
|
75
|
+
const cache = options.cacheDir ?? toolchainCacheDir(env);
|
|
76
|
+
const zig = await resolveZig({ ...options, cacheDir: cache });
|
|
77
|
+
const source = await fetchSource("zig-js", cache, run, options.log);
|
|
78
|
+
await fetchSource("zig-regex", cache, run, options.log);
|
|
79
|
+
await fetchSource("zig-gc", cache, run, options.log);
|
|
80
|
+
const library = join(source, "zig-out/lib/libzig-js.a");
|
|
81
|
+
const stamp = join(source, "zig-out/.craft-toolchain");
|
|
82
|
+
const wanted = `${macosToolchain.zig}
|
|
83
|
+
${macosToolchain.sources["zig-js"].revision}
|
|
84
|
+
${macosToolchain.sources["zig-regex"].revision}
|
|
85
|
+
${macosToolchain.sources["zig-gc"].revision}
|
|
86
|
+
`;
|
|
87
|
+
if (!existsSync(library) || !existsSync(stamp) || readFileSync(stamp, "utf8") !== wanted) {
|
|
88
|
+
options.log?.("Building the zig-js engine");
|
|
89
|
+
const result = await run([zig, "build", "home-tool-bootstrap", "-Doptimize=ReleaseFast"], source);
|
|
90
|
+
if (result.exitCode !== 0 || !existsSync(library))
|
|
91
|
+
throw new Error(`zig-js did not build (${library}): ${result.stderr?.trim() ?? ""}`);
|
|
92
|
+
writeFileSync(stamp, wanted);
|
|
93
|
+
}
|
|
94
|
+
return { zig, zigJS: { source, library } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ../macos/src/index.ts
|
|
7
98
|
async function buildMacOSNative(options) {
|
|
8
|
-
if (
|
|
99
|
+
if (process2.platform !== "darwin")
|
|
9
100
|
throw new Error("The AppKit host must be built on macOS.");
|
|
10
101
|
const outdir = resolve(options.outdir);
|
|
11
|
-
const templates =
|
|
12
|
-
const sourceRoot =
|
|
13
|
-
const layout =
|
|
14
|
-
const mutation =
|
|
15
|
-
if (!
|
|
102
|
+
const templates = join2(import.meta.dir, "../templates");
|
|
103
|
+
const sourceRoot = join2(import.meta.dir, "../..");
|
|
104
|
+
const layout = existsSync2(join2(sourceRoot, "zig/src/native_layout.zig")) ? join2(sourceRoot, "zig/src/native_layout.zig") : join2(templates, "native_layout.zig");
|
|
105
|
+
const mutation = existsSync2(join2(sourceRoot, "ios/templates/CraftNativeMutation.swift")) ? join2(sourceRoot, "ios/templates/CraftNativeMutation.swift") : join2(templates, "CraftNativeMutation.swift");
|
|
106
|
+
if (!existsSync2(options.nativeBundle))
|
|
16
107
|
throw new Error(`Native STX bundle not found: ${options.nativeBundle}`);
|
|
17
108
|
await mkdir(outdir, { recursive: true });
|
|
18
109
|
async function run(args) {
|
|
@@ -20,38 +111,39 @@ async function buildMacOSNative(options) {
|
|
|
20
111
|
if (await child.exited !== 0)
|
|
21
112
|
throw new Error(`Native build failed: ${args[0]}`);
|
|
22
113
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const layoutLibrary = join(outdir, "libcraft-layout.a");
|
|
114
|
+
const toolchain = options.zig && options.zigJS ? undefined : await resolveMacOSToolchain({ cacheDir: options.toolchainDir, log: (line) => console.log(line) });
|
|
115
|
+
const zig = options.zig || toolchain.zig;
|
|
116
|
+
const layoutLibrary = join2(outdir, "libcraft-layout.a");
|
|
27
117
|
await run([zig, "build-lib", layout, "-O", "ReleaseSafe", "-static", "-lc", `-femit-bin=${layoutLibrary}`]);
|
|
28
|
-
const executable =
|
|
29
|
-
const engine = options.zigJS;
|
|
30
|
-
if (!
|
|
118
|
+
const executable = join2(outdir, "craft-native-macos");
|
|
119
|
+
const engine = options.zigJS || toolchain.zigJS;
|
|
120
|
+
if (!existsSync2(engine.library))
|
|
31
121
|
throw new Error(`Build zig-js first: ${engine.library}`);
|
|
32
|
-
if (!
|
|
122
|
+
if (!existsSync2(join2(engine.source, "include/module.modulemap")))
|
|
33
123
|
throw new Error("zig-js must expose its owned ZigJS Swift module.");
|
|
34
|
-
const bridge =
|
|
35
|
-
const dispatch =
|
|
36
|
-
const assembly =
|
|
37
|
-
await run(["xcrun", "clang", "-fobjc-arc", "-fblocks", "-Wno-incomplete-implementation", "-I",
|
|
38
|
-
await run([zig, "build-obj",
|
|
124
|
+
const bridge = join2(outdir, "zig-js-objc.o");
|
|
125
|
+
const dispatch = join2(outdir, "zig-js-dispatch.o");
|
|
126
|
+
const assembly = join2(engine.source, `src/objc_abi_${process2.arch === "arm64" ? "aarch64" : "x86_64"}.S`);
|
|
127
|
+
await run(["xcrun", "clang", "-fobjc-arc", "-fblocks", "-Wno-incomplete-implementation", "-I", join2(engine.source, "include"), "-include", join2(engine.source, "include/ZigJS.h"), "-c", join2(engine.source, "src/objc_bridge.m"), "-o", bridge]);
|
|
128
|
+
await run([zig, "build-obj", join2(engine.source, "src/objc_abi_dispatch.zig"), assembly, "-O", "ReleaseFast", `-femit-bin=${dispatch}`]);
|
|
129
|
+
const web = options.webkit ? ["-D", "CRAFT_WEBKIT", "-framework", "WebKit", join2(templates, "CraftMacWebView.swift")] : [];
|
|
39
130
|
await run([
|
|
40
131
|
"swiftc",
|
|
41
132
|
"-O",
|
|
42
133
|
"-swift-version",
|
|
43
134
|
"5",
|
|
44
135
|
"-I",
|
|
45
|
-
|
|
136
|
+
join2(engine.source, "include"),
|
|
46
137
|
"-framework",
|
|
47
138
|
"Cocoa",
|
|
48
139
|
"-framework",
|
|
49
140
|
"AVKit",
|
|
50
141
|
"-framework",
|
|
51
142
|
"UserNotifications",
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
143
|
+
...web,
|
|
144
|
+
join2(templates, "CraftMacRuntime.swift"),
|
|
145
|
+
join2(templates, "CraftMacRenderer.swift"),
|
|
146
|
+
join2(templates, "CraftMacApplication.swift"),
|
|
55
147
|
mutation,
|
|
56
148
|
bridge,
|
|
57
149
|
dispatch,
|
|
@@ -65,11 +157,16 @@ async function buildMacOSNative(options) {
|
|
|
65
157
|
"-o",
|
|
66
158
|
executable
|
|
67
159
|
]);
|
|
68
|
-
const bundle =
|
|
160
|
+
const bundle = join2(outdir, "native-screen.js");
|
|
69
161
|
if (resolve(options.nativeBundle) !== bundle)
|
|
70
162
|
await cp(options.nativeBundle, bundle);
|
|
71
163
|
return { executable, bundle };
|
|
72
164
|
}
|
|
73
165
|
export {
|
|
74
|
-
buildMacOSNative
|
|
166
|
+
buildMacOSNative,
|
|
167
|
+
macosToolchain,
|
|
168
|
+
pantryExecutable,
|
|
169
|
+
resolveMacOSToolchain,
|
|
170
|
+
resolveZig,
|
|
171
|
+
toolchainCacheDir
|
|
75
172
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the AppKit host is built with. Pinned here, beside the Swift that needs
|
|
3
|
+
* it, so an app never copies a lock file of its own: the Zig compiler (installed
|
|
4
|
+
* by Pantry, which mirrors Zig dev builds) and the zig-js engine with the two
|
|
5
|
+
* libraries it builds against (fetched at an exact revision).
|
|
6
|
+
*/
|
|
7
|
+
export declare const macosToolchain: {
|
|
8
|
+
readonly zig: '0.18.0-dev.252+86dc5ce25';
|
|
9
|
+
readonly sources: {
|
|
10
|
+
readonly 'zig-js': {
|
|
11
|
+
readonly repository: 'zig-utils/zig-js';
|
|
12
|
+
readonly revision: 'f0072c95780712d1bb789280e08e7e390e83ab94';
|
|
13
|
+
readonly paths: readonly ['src', 'include', 'tests', 'tools', 'docs', 'bench', 'conformance'];
|
|
14
|
+
};
|
|
15
|
+
readonly 'zig-regex': {
|
|
16
|
+
readonly repository: 'zig-utils/zig-regex';
|
|
17
|
+
readonly revision: 'fdd9c2e333ea6ac2fd06d080d939ef7f11d61b1b';
|
|
18
|
+
readonly paths: readonly ['src'];
|
|
19
|
+
};
|
|
20
|
+
readonly 'zig-gc': {
|
|
21
|
+
readonly repository: 'zig-utils/zig-gc';
|
|
22
|
+
readonly revision: '0285ef801dd221a4ae8e88b31a09ae31627657a3';
|
|
23
|
+
readonly paths: readonly ['src'];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export interface MacOSToolchain {
|
|
28
|
+
zig: string;
|
|
29
|
+
zigJS: {
|
|
30
|
+
source: string;
|
|
31
|
+
library: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type ToolchainRunner = (args: string[], cwd?: string) => Promise<{
|
|
35
|
+
exitCode: number;
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr?: string;
|
|
38
|
+
}>;
|
|
39
|
+
export interface ResolveToolchainOptions {
|
|
40
|
+
/** Where the compiler and sources are kept. Default: `$CRAFT_TOOLCHAIN_DIR`, else `~/.cache/craft/toolchain`. */
|
|
41
|
+
cacheDir?: string;
|
|
42
|
+
env?: Record<string, string | undefined>;
|
|
43
|
+
run?: ToolchainRunner;
|
|
44
|
+
log?: (line: string) => void;
|
|
45
|
+
}
|
|
46
|
+
export declare function toolchainCacheDir(env?: Record<string, string | undefined>): string;
|
|
47
|
+
/** The Pantry executable: PANTRY_BIN, the project's own, the user's, else `pantry` on PATH. */
|
|
48
|
+
export declare function pantryExecutable(env?: Record<string, string | undefined>, exists?: (path: string) => boolean): string;
|
|
49
|
+
/**
|
|
50
|
+
* The pinned Zig compiler. `CRAFT_ZIG` wins when set (a deliberate override,
|
|
51
|
+
* e.g. CI); otherwise Pantry installs exactly the pinned version into the
|
|
52
|
+
* cache. Zig is never taken from PATH: the wrong snapshot builds nothing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveZig(options?: ResolveToolchainOptions): Promise<string>;
|
|
55
|
+
/** Zig, the zig-js checkout at its pinned revision and its built static library; everything missing is fetched and built once. */
|
|
56
|
+
export declare function resolveMacOSToolchain(options?: ResolveToolchainOptions): Promise<MacOSToolchain>;
|
|
@@ -4,6 +4,13 @@ import CoreImage
|
|
|
4
4
|
import UserNotifications
|
|
5
5
|
import UniformTypeIdentifiers
|
|
6
6
|
|
|
7
|
+
/// A window that tells its screen when Escape reaches it unhandled: the screen hears it as the desktop
|
|
8
|
+
/// command `escape` (close the open item, dismiss a sheet).
|
|
9
|
+
final class CraftMacWindow: NSWindow {
|
|
10
|
+
weak var runtime: CraftMacRuntime?
|
|
11
|
+
override func cancelOperation(_ sender: Any?) { runtime?.send("CRAFT_EVENT", ["event": "craftDesktopCommand", "detail": ["id": "escape"]]) }
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
final class CraftMacScene: NSObject, NSWindowDelegate {
|
|
8
15
|
let name: String
|
|
9
16
|
let window: NSWindow
|
|
@@ -14,7 +21,10 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
|
|
|
14
21
|
runtime = CraftMacRuntime(route: route, params: options["params"] as? [String: Any] ?? [:], network: application.network)
|
|
15
22
|
renderer = CraftMacRenderer(runtime: runtime)
|
|
16
23
|
let width = number(options["width"]) ?? 1120, height = number(options["height"]) ?? 740
|
|
17
|
-
|
|
24
|
+
let transparent = (options["titlebarStyle"] as? String ?? application.titlebarStyle) == "transparent"
|
|
25
|
+
let panel = CraftMacWindow(contentRect: CGRect(x: 0, y: 0, width: width, height: height), styleMask: transparent ? [.titled, .closable, .resizable, .miniaturizable, .fullSizeContentView] : [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false)
|
|
26
|
+
panel.runtime = runtime
|
|
27
|
+
window = panel
|
|
18
28
|
super.init()
|
|
19
29
|
runtime.scene = self; renderer.window = window
|
|
20
30
|
window.title = options["title"] as? String ?? application.appName
|
|
@@ -54,6 +64,10 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
54
64
|
var bundle = ""
|
|
55
65
|
var initialRoute = "Main"
|
|
56
66
|
var titlebarStyle = "standard"
|
|
67
|
+
/// Apps that ship their own agent (Attic) hand out a session cookie at
|
|
68
|
+
/// `/native/session`. An app that talks to a hosted API has no such
|
|
69
|
+
/// endpoint and starts with `--no-session`.
|
|
70
|
+
var requiresSession = true
|
|
57
71
|
var mainWindowOptions: [String: Any] = [:]
|
|
58
72
|
var network: CraftMacNetwork!
|
|
59
73
|
var scenes: [String: CraftMacScene] = [:]
|
|
@@ -70,6 +84,11 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
70
84
|
DispatchQueue.main.async { self?.appearanceChanged() }
|
|
71
85
|
}
|
|
72
86
|
NSApp.activate(ignoringOtherApps: true)
|
|
87
|
+
if !requiresSession {
|
|
88
|
+
open(name: "main", route: initialRoute, options: mainWindowOptions.merging(["title": appName]) { _, new in new })
|
|
89
|
+
startTestDriver()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
73
92
|
// The agent issues an HttpOnly cookie. It stays in URLSession, shared
|
|
74
93
|
// by native windows, and is never handed to the JavaScript context.
|
|
75
94
|
let request = URLRequest(url: network.base.appendingPathComponent("native/session"))
|
|
@@ -174,7 +193,11 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
174
193
|
if let old = bar.items.first(where: { $0.title == title }) { bar.removeItem(old) }
|
|
175
194
|
let menu = NSMenu(title: title); menu.autoenablesItems = false
|
|
176
195
|
for item in raw["items"] as? [[String: Any]] ?? [] { menu.addItem(menuItem(item, target: self, action: #selector(command(_:)))) }
|
|
177
|
-
|
|
196
|
+
let item = NSMenuItem(title: menu.title, action: nil, keyEquivalent: ""); item.submenu = menu
|
|
197
|
+
// A File menu is the first after the app menu; the others sit
|
|
198
|
+
// between Edit and Window, where a Mac app's own menus belong.
|
|
199
|
+
let index = title == "File" ? 1 : (bar.items.firstIndex(where: { $0.title == "Window" }) ?? bar.items.count)
|
|
200
|
+
bar.insertItem(item, at: min(index, bar.items.count))
|
|
178
201
|
}; reply(true)
|
|
179
202
|
case "contextMenu":
|
|
180
203
|
let target = CraftMacMenuTarget(), menu = NSMenu(); menu.autoenablesItems = false
|
|
@@ -298,6 +321,16 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
298
321
|
guard box.window?.isVisible == true, !box.isHidden, box.frame.width > 0, box.frame.height > 0 else { return ["ok": false, "error": "Control is not visible"] }
|
|
299
322
|
if let control = box.control as? NSControl, !control.isEnabled { return ["ok": false, "error": "Control is disabled"] }
|
|
300
323
|
switch kind {
|
|
324
|
+
case "key":
|
|
325
|
+
// Return, Tab, Escape and the arrows, as the field editor receives them: they reach the same delegate commands a keystroke does.
|
|
326
|
+
let commands = ["return": #selector(NSResponder.insertNewline(_:)), "tab": #selector(NSResponder.insertTab(_:)), "escape": #selector(NSResponder.cancelOperation(_:)),
|
|
327
|
+
"up": #selector(NSResponder.moveUp(_:)), "down": #selector(NSResponder.moveDown(_:)), "backspace": #selector(NSResponder.deleteBackward(_:))]
|
|
328
|
+
guard let name = action["value"] as? String, let command = commands[name] else { return ["ok": false, "error": "Unknown key"] }
|
|
329
|
+
let target: NSView? = (box.control as? NSScrollView)?.documentView ?? box.control
|
|
330
|
+
guard let target, scene.window.makeFirstResponder(target) else { return ["ok": false, "error": "Control cannot take focus"] }
|
|
331
|
+
let editor = (target as? NSTextField)?.currentEditor() as? NSTextView ?? target as? NSTextView
|
|
332
|
+
guard let editor else { return ["ok": false, "error": "Not a text input"] }
|
|
333
|
+
editor.doCommand(by: command)
|
|
301
334
|
case "click": if let button = box.control as? NSButton { button.performClick(nil) } else { box.pressed() }
|
|
302
335
|
case "fill":
|
|
303
336
|
let value = action["value"] as? String ?? ""
|
|
@@ -343,18 +376,30 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
343
376
|
|
|
344
377
|
@main struct CraftMacMain {
|
|
345
378
|
static func main() throws {
|
|
346
|
-
|
|
379
|
+
// A bundled app is launched by Finder with no arguments: its settings are
|
|
380
|
+
// `Contents/Resources/craft-app.json`, written by the build. A flag on the
|
|
381
|
+
// command line still wins, so a development run can point elsewhere.
|
|
382
|
+
let packaged = Bundle.main.url(forResource: "craft-app", withExtension: "json")
|
|
383
|
+
.flatMap { try? Data(contentsOf: $0) }
|
|
384
|
+
.flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] } ?? [:]
|
|
385
|
+
func argument(_ flag: String, key: String? = nil, fallback: String = "") -> String {
|
|
386
|
+
if let index = CommandLine.arguments.firstIndex(of: flag), CommandLine.arguments.indices.contains(index + 1) { return CommandLine.arguments[index + 1] }
|
|
387
|
+
if let key, let value = packaged[key] { return String(describing: value) }
|
|
388
|
+
return fallback
|
|
389
|
+
}
|
|
347
390
|
let application = CraftMacApplication.shared
|
|
348
391
|
if CommandLine.arguments.contains("--version") { print("craft native macOS renderer 0.0.172 (zig-js)"); return }
|
|
349
|
-
guard let api = URL(string: argument("--api-url")), ["http", "https"].contains(api.scheme ?? ""), api.host != nil else { throw NSError(domain: "Craft", code: 1, userInfo: [NSLocalizedDescriptionKey: "--api-url must name the application's HTTP agent"] ) }
|
|
350
|
-
let
|
|
392
|
+
guard let api = URL(string: argument("--api-url", key: "apiUrl")), ["http", "https"].contains(api.scheme ?? ""), api.host != nil else { throw NSError(domain: "Craft", code: 1, userInfo: [NSLocalizedDescriptionKey: "--api-url must name the application's HTTP agent"] ) }
|
|
393
|
+
let bundledScreens = Bundle.main.path(forResource: "native-screen", ofType: "js") ?? ""
|
|
394
|
+
let bundlePath = argument("--native-bundle", fallback: bundledScreens)
|
|
351
395
|
guard !bundlePath.isEmpty else { throw NSError(domain: "Craft", code: 2, userInfo: [NSLocalizedDescriptionKey: "--native-bundle must name a compiled STX bundle"] ) }
|
|
352
396
|
application.bundle = try String(contentsOfFile: bundlePath, encoding: .utf8)
|
|
353
|
-
application.appName = argument("--title", fallback: "Craft")
|
|
354
|
-
application.initialRoute = argument("--route", fallback: "Main")
|
|
355
|
-
application.titlebarStyle = argument("--titlebar-style", fallback: "standard")
|
|
397
|
+
application.appName = argument("--title", key: "title", fallback: "Craft")
|
|
398
|
+
application.initialRoute = argument("--route", key: "route", fallback: "Main")
|
|
399
|
+
application.titlebarStyle = argument("--titlebar-style", key: "titlebarStyle", fallback: "standard")
|
|
400
|
+
application.requiresSession = !CommandLine.arguments.contains("--no-session") && packaged["session"] as? Bool != false
|
|
356
401
|
for (flag, key) in [("--width", "width"), ("--height", "height"), ("--min-width", "minWidth"), ("--min-height", "minHeight")] {
|
|
357
|
-
if let value = Double(argument(flag)), value.isFinite, value > 0 { application.mainWindowOptions[key] = value }
|
|
402
|
+
if let value = Double(argument(flag, key: key)), value.isFinite, value > 0 { application.mainWindowOptions[key] = value }
|
|
358
403
|
}
|
|
359
404
|
application.network = CraftMacNetwork(base: api)
|
|
360
405
|
let testDirectory = argument("--test-dir")
|