janela 0.13.0 → 0.13.1
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/bin/janela.mjs +44 -291
- package/bin/lib.mjs +347 -0
- package/package.json +10 -2
- package/shim/android/app.cc +9 -0
- package/shim/ios/app.cc +10 -0
package/bin/janela.mjs
CHANGED
|
@@ -18,6 +18,11 @@ import { createServer } from "node:net";
|
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
+
import {
|
|
22
|
+
ANDROID_ABI, ANDROID_TARGET_SDK, androidConf, ffiManifest, iosConf,
|
|
23
|
+
libraryProfile, mimeFor, NAME_RE, patchPeSubsystem, PeError,
|
|
24
|
+
rewriteHostSpecifier, suggestName,
|
|
25
|
+
} from "./lib.mjs";
|
|
21
26
|
|
|
22
27
|
const KIT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
28
|
const require = createRequire(join(KIT, "package.json"));
|
|
@@ -81,30 +86,28 @@ function viteConfigPath(root) {
|
|
|
81
86
|
return null;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// How to run the project's own vite.
|
|
90
|
+
//
|
|
91
|
+
// The `.bin` entry is a .cmd shim on Windows and, since the CVE-2024-27980
|
|
92
|
+
// fix, Node refuses to spawn a .cmd without a shell. `janela dev` passed
|
|
93
|
+
// `shell: true` and worked; `janela build` did not, so building any Vite
|
|
94
|
+
// template on Windows died with `spawnSync ...\vite.cmd EINVAL`. Prefer
|
|
95
|
+
// vite's own JS entry run with this Node: no shim, no shell, and identical on
|
|
96
|
+
// every platform. The shim stays as a fallback for layouts that hide the
|
|
97
|
+
// package but keep the bin.
|
|
98
|
+
function viteCommand(root) {
|
|
99
|
+
const js = join(root, "node_modules", "vite", "bin", "vite.js");
|
|
100
|
+
if (existsSync(js)) return { argv: [process.execPath, js], shell: false };
|
|
101
|
+
|
|
102
|
+
const shim = join(root, "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite");
|
|
103
|
+
if (existsSync(shim)) return { argv: [shim], shell: process.platform === "win32" };
|
|
104
|
+
|
|
105
|
+
fail(
|
|
106
|
+
"this project has a vite config but no local vite — run your package manager's " +
|
|
107
|
+
"install first (npm install / pnpm install)",
|
|
108
|
+
);
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
const MIME = {
|
|
96
|
-
".css": "text/css", ".js": "text/javascript", ".json": "application/json",
|
|
97
|
-
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
98
|
-
".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif",
|
|
99
|
-
".svg": "image/svg+xml", ".ico": "image/x-icon",
|
|
100
|
-
".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
|
|
101
|
-
".mp3": "audio/mpeg", ".mp4": "video/mp4", ".webm": "video/webm",
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
function mimeFor(p) {
|
|
105
|
-
const dot = p.lastIndexOf(".");
|
|
106
|
-
return (dot < 0 ? null : MIME[p.slice(dot).toLowerCase()]) ?? "application/octet-stream";
|
|
107
|
-
}
|
|
108
111
|
|
|
109
112
|
// A dist-relative reference ("/assets/x.js", "./assets/x.js") → absolute path,
|
|
110
113
|
// or null when it points outside the build (a CDN URL, a data: URI, an anchor).
|
|
@@ -186,7 +189,8 @@ function frontendHtml(root, conf, devUrl) {
|
|
|
186
189
|
|
|
187
190
|
if (viteConfigPath(root)) {
|
|
188
191
|
console.log("janela: building the frontend with vite");
|
|
189
|
-
|
|
192
|
+
const viteCmd = viteCommand(root);
|
|
193
|
+
run([...viteCmd.argv, "build"], { cwd: root, shell: viteCmd.shell });
|
|
190
194
|
const distDir = resolve(root, conf.frontend?.dist ?? "dist");
|
|
191
195
|
const html = inlineDist(distDir);
|
|
192
196
|
console.log(`janela: frontend inlined (${(Buffer.byteLength(html) / 1024).toFixed(0)} kB)`);
|
|
@@ -274,8 +278,6 @@ function webview2Include(cacheDir) {
|
|
|
274
278
|
// Everything below is simulator-only: a device build additionally needs a
|
|
275
279
|
// signing identity and a provisioning profile, which is its own project.
|
|
276
280
|
|
|
277
|
-
const IOS_MIN_VERSION = "15.0";
|
|
278
|
-
const IOS_PREFIX = "jl_";
|
|
279
281
|
|
|
280
282
|
function iosDeviceOrFail(name) {
|
|
281
283
|
const json = capture(["xcrun", "simctl", "list", "devices", "available", "--json"]);
|
|
@@ -296,86 +298,9 @@ function iosDeviceOrFail(name) {
|
|
|
296
298
|
return pick;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
function iosConf(conf) {
|
|
300
|
-
const ios = conf.ios ?? {};
|
|
301
|
-
return {
|
|
302
|
-
identifier: ios.identifier ?? conf.identifier,
|
|
303
|
-
displayName: ios.displayName ?? conf.window?.title ?? conf.name,
|
|
304
|
-
minimumVersion: String(ios.minimumVersion ?? IOS_MIN_VERSION),
|
|
305
|
-
device: ios.device ?? null,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
301
|
// The library profile: one export carries every command, so a project's own
|
|
310
302
|
// commands need no ABI of their own. `janelaEmit` is the reverse channel the
|
|
311
303
|
// shell registers before init.
|
|
312
|
-
function libraryProfile() {
|
|
313
|
-
return {
|
|
314
|
-
profile_format: 1,
|
|
315
|
-
name: "janela",
|
|
316
|
-
entry: "./entry.ts",
|
|
317
|
-
emission: "llvm",
|
|
318
|
-
abi: {
|
|
319
|
-
prefix: IOS_PREFIX,
|
|
320
|
-
init_symbol: `${IOS_PREFIX}init`,
|
|
321
|
-
sink_register_symbol: `${IOS_PREFIX}set_panic_sink`,
|
|
322
|
-
collect_symbol: `${IOS_PREFIX}collect`,
|
|
323
|
-
result_reset_symbol: `${IOS_PREFIX}reset`,
|
|
324
|
-
callback_register_symbol: `${IOS_PREFIX}set_callback`,
|
|
325
|
-
},
|
|
326
|
-
exports: [
|
|
327
|
-
{
|
|
328
|
-
export: "handleInvoke",
|
|
329
|
-
symbol: `${IOS_PREFIX}handle_invoke`,
|
|
330
|
-
params: ["string", "string"],
|
|
331
|
-
returns: "string",
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
export: "indexHtml",
|
|
335
|
-
symbol: `${IOS_PREFIX}index_html`,
|
|
336
|
-
params: [],
|
|
337
|
-
returns: "string",
|
|
338
|
-
},
|
|
339
|
-
// The shell calls these back on the main queue when work it owns comes
|
|
340
|
-
// due. They are the mirror of the desktop shim's wv_on_timer: the
|
|
341
|
-
// library parks a continuation under an id and is re-entered with it.
|
|
342
|
-
{
|
|
343
|
-
export: "onTimer",
|
|
344
|
-
symbol: `${IOS_PREFIX}on_timer`,
|
|
345
|
-
params: ["f64"],
|
|
346
|
-
returns: "void",
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
export: "onFsDone",
|
|
350
|
-
symbol: `${IOS_PREFIX}on_fs_done`,
|
|
351
|
-
params: ["f64", "bool", "string"],
|
|
352
|
-
returns: "void",
|
|
353
|
-
},
|
|
354
|
-
// Deliberately its own export rather than reusing onFsDone, whose
|
|
355
|
-
// signature would fit: a dialog result arriving through the file-I/O
|
|
356
|
-
// path would read as a bug for as long as the code lived.
|
|
357
|
-
{
|
|
358
|
-
export: "onDialogDone",
|
|
359
|
-
symbol: `${IOS_PREFIX}on_dialog_done`,
|
|
360
|
-
params: ["f64", "bool", "string"],
|
|
361
|
-
returns: "void",
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
// TS -> shell. A channel handler must never re-enter the library (see
|
|
365
|
-
// upstream #263: violations silently appear to work), so every one of
|
|
366
|
-
// these only records the request; the shell acts on its own queue and
|
|
367
|
-
// re-enters through an export above on a later turn.
|
|
368
|
-
callbacks: [
|
|
369
|
-
{ name: "janelaEmit", params: ["string", "string"], returns: "void" },
|
|
370
|
-
{ name: "hostSchedule", params: ["f64", "f64"], returns: "void" },
|
|
371
|
-
{ name: "hostSettle", params: ["f64", "string"], returns: "void" },
|
|
372
|
-
{ name: "hostReadFile", params: ["f64", "string"], returns: "void" },
|
|
373
|
-
{ name: "hostWriteFile", params: ["f64", "string", "string"], returns: "void" },
|
|
374
|
-
{ name: "hostOpenDialog", params: ["f64", "string"], returns: "void" },
|
|
375
|
-
],
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
304
|
function iosPlist(conf, iconFiles = []) {
|
|
380
305
|
const ios = iosConf(conf);
|
|
381
306
|
// The asset-catalogue route needs actool; the CFBundleIconFiles list is the
|
|
@@ -498,34 +423,11 @@ async function devIos(root) {
|
|
|
498
423
|
// backend needs a companion class because android.webkit.WebView is a Java API
|
|
499
424
|
// and native code cannot define a class to receive its callbacks.
|
|
500
425
|
|
|
501
|
-
const ANDROID_MIN_SDK = 26;
|
|
502
|
-
const ANDROID_TARGET_SDK = 34;
|
|
503
|
-
const ANDROID_ABI = "arm64-v8a";
|
|
504
426
|
|
|
505
427
|
/// Android package names are Java package names: dot-separated identifiers,
|
|
506
428
|
/// so no hyphens. A janela project may be called `my-app`, which makes the
|
|
507
429
|
/// default identifier `dev.janela.my-app` — legal everywhere else and not
|
|
508
430
|
/// here, so each segment is coerced rather than failing the build.
|
|
509
|
-
function androidPackage(id) {
|
|
510
|
-
return id
|
|
511
|
-
.split(".")
|
|
512
|
-
.map((seg) => {
|
|
513
|
-
const cleaned = seg.replace(/[^A-Za-z0-9_]/g, "_");
|
|
514
|
-
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
515
|
-
})
|
|
516
|
-
.join(".");
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function androidConf(conf) {
|
|
520
|
-
const a = conf.android ?? {};
|
|
521
|
-
return {
|
|
522
|
-
applicationId: androidPackage(a.applicationId ?? a.identifier ?? conf.identifier),
|
|
523
|
-
label: a.label ?? conf.window?.title ?? conf.name,
|
|
524
|
-
minSdk: String(a.minSdk ?? ANDROID_MIN_SDK),
|
|
525
|
-
device: a.device ?? null,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
431
|
/// The SDK pieces an Android build needs, or a message saying which is absent.
|
|
530
432
|
function androidSdk() {
|
|
531
433
|
const home =
|
|
@@ -835,128 +737,6 @@ function buildShim(cacheDir) {
|
|
|
835
737
|
|
|
836
738
|
// ---- FFI manifest ---------------------------------------------------------
|
|
837
739
|
|
|
838
|
-
const STR = (name, symbol) => ({ name, symbol, params: ["i32", "string"], returns: "i32" });
|
|
839
|
-
|
|
840
|
-
function ffiManifest(shimLib) {
|
|
841
|
-
const functions = [
|
|
842
|
-
{ name: "wvCreate", symbol: "wv_create", params: ["i32"], returns: "i32" },
|
|
843
|
-
STR("wvSetTitle", "wv_set_title"),
|
|
844
|
-
{ name: "wvSetSize", symbol: "wv_set_size", params: ["i32", "i32", "i32", "i32"], returns: "i32" },
|
|
845
|
-
STR("wvSetHtml", "wv_set_html"),
|
|
846
|
-
STR("wvInit", "wv_init"),
|
|
847
|
-
STR("wvEval", "wv_eval"),
|
|
848
|
-
STR("wvBind", "wv_bind"),
|
|
849
|
-
STR("wvReply", "wv_reply"),
|
|
850
|
-
// Retained handlers (format 4): registered once, valid for the app's
|
|
851
|
-
// lifetime, so wv_run is a plain blocking call. The request rides in as a
|
|
852
|
-
// `string` param (format 3) rather than a byte-at-a-time drain.
|
|
853
|
-
{
|
|
854
|
-
name: "wvOnInvoke", symbol: "wv_on_invoke",
|
|
855
|
-
params: [
|
|
856
|
-
"i32",
|
|
857
|
-
{ callback: { id: "inv", params: ["string", { context: "inv" }], returns: "i32", lifetime: "retained" } },
|
|
858
|
-
{ context: "inv" },
|
|
859
|
-
],
|
|
860
|
-
returns: "i32",
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
name: "wvOnTimer", symbol: "wv_on_timer",
|
|
864
|
-
params: [
|
|
865
|
-
"i32",
|
|
866
|
-
{ callback: { id: "timer", params: ["i32", { context: "timer" }], returns: "void", lifetime: "retained" } },
|
|
867
|
-
{ context: "timer" },
|
|
868
|
-
],
|
|
869
|
-
returns: "i32",
|
|
870
|
-
},
|
|
871
|
-
{ name: "wvRun", symbol: "wv_run", params: ["i32"], returns: "i32" },
|
|
872
|
-
{ name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
|
|
873
|
-
// async: the held-reply table (deferred returns) plus shell-owned
|
|
874
|
-
// scheduling — TS parks a continuation id, the shell calls it back due.
|
|
875
|
-
{ name: "wvDefer", symbol: "wv_defer", params: ["i32"], returns: "i32" },
|
|
876
|
-
{ name: "wvResolve", symbol: "wv_resolve", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
877
|
-
{ name: "wvSchedule", symbol: "wv_schedule", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
878
|
-
// async file I/O: the blocking syscall runs on a shim worker thread
|
|
879
|
-
{ name: "wvFsRead", symbol: "wv_fs_read", params: ["i32", "string"], returns: "i32" },
|
|
880
|
-
{ name: "wvFsWrite", symbol: "wv_fs_write", params: ["i32", "string", "string"], returns: "i32" },
|
|
881
|
-
// Job accessors, shared by file I/O and dialogs: both are work whose
|
|
882
|
-
// answer cannot be produced during the FFI call that starts it.
|
|
883
|
-
{ name: "wvJobStatus", symbol: "wv_job_status", params: ["i32", "i32"], returns: "i32" },
|
|
884
|
-
{ name: "wvJobSize", symbol: "wv_job_size", params: ["i32", "i32"], returns: "f64" },
|
|
885
|
-
{
|
|
886
|
-
// One slice per call, so a large payload decodes across several UI turns
|
|
887
|
-
// instead of stalling on all of it at once. Returns the bytes covered.
|
|
888
|
-
name: "wvJobTakeAt", symbol: "wv_job_take_at",
|
|
889
|
-
params: [
|
|
890
|
-
"i32", "i32", "f64", "f64",
|
|
891
|
-
{ callback: { id: "sink", params: ["string", { context: "sink" }], returns: "void", lifetime: "call" } },
|
|
892
|
-
{ context: "sink" },
|
|
893
|
-
],
|
|
894
|
-
returns: "f64",
|
|
895
|
-
},
|
|
896
|
-
{ name: "wvJobFree", symbol: "wv_job_free", params: ["i32", "i32"], returns: "i32" },
|
|
897
|
-
// Native dialogs: the modal runs on a later UI-thread turn, so asking for
|
|
898
|
-
// one never blocks the invoke that asked. Options ride as plain params
|
|
899
|
-
// (kind, flags, title, defaultPath, defaultName, filters).
|
|
900
|
-
{
|
|
901
|
-
name: "wvDialog", symbol: "wv_dialog",
|
|
902
|
-
params: ["i32", "i32", "i32", "string", "string", "string", "string"],
|
|
903
|
-
returns: "i32",
|
|
904
|
-
},
|
|
905
|
-
{ name: "wvSetFullscreen", symbol: "wv_set_fullscreen", params: ["i32", "i32"], returns: "i32" },
|
|
906
|
-
];
|
|
907
|
-
|
|
908
|
-
if (process.platform === "win32") {
|
|
909
|
-
// MinGW ignores MSVC's #pragma comment(lib, ...), so the Win32 imports the
|
|
910
|
-
// WebView2 backend needs are named explicitly. scriptc's own win32 lane
|
|
911
|
-
// already adds advapi32/iphlpapi/ws2_32, so those are omitted here.
|
|
912
|
-
// `c++` pulls libc++ for the shim's std::string/exceptions.
|
|
913
|
-
//
|
|
914
|
-
// `pthread` (mingw's libwinpthread) is here to work around an upstream
|
|
915
|
-
// scriptc bug: its runtime calls clock_gettime/nanosleep, which mingw
|
|
916
|
-
// declares in <time.h> but implements in winpthreads, and scriptc's win32
|
|
917
|
-
// link never adds it. Without this the link dies with
|
|
918
|
-
// "undefined symbol: clock_gettime" — reproducible with a plain
|
|
919
|
-
// `scriptc build hello.ts` on Windows, no FFI involved.
|
|
920
|
-
return {
|
|
921
|
-
ffi_format: 4,
|
|
922
|
-
functions,
|
|
923
|
-
libraries: [shimLib],
|
|
924
|
-
system_libraries: [
|
|
925
|
-
"c++", "pthread",
|
|
926
|
-
"ole32", "oleaut32", "shlwapi", "shell32", "user32", "version", "gdi32",
|
|
927
|
-
// GetOpenFileNameW / GetSaveFileNameW for the native file dialogs.
|
|
928
|
-
"comdlg32",
|
|
929
|
-
],
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (process.platform === "darwin") {
|
|
934
|
-
// scriptc has no -framework support, but `libraries` entries are passed to
|
|
935
|
-
// the link as plain input files and ld64 accepts .tbd stubs.
|
|
936
|
-
const sdk = capture(["xcrun", "--sdk", "macosx", "--show-sdk-path"]);
|
|
937
|
-
return {
|
|
938
|
-
ffi_format: 4,
|
|
939
|
-
functions,
|
|
940
|
-
libraries: [
|
|
941
|
-
shimLib,
|
|
942
|
-
join(sdk, "System/Library/Frameworks/WebKit.framework/WebKit.tbd"),
|
|
943
|
-
join(sdk, "System/Library/Frameworks/Cocoa.framework/Cocoa.tbd"),
|
|
944
|
-
],
|
|
945
|
-
system_libraries: ["c++"],
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
return {
|
|
949
|
-
ffi_format: 4,
|
|
950
|
-
functions,
|
|
951
|
-
libraries: [shimLib],
|
|
952
|
-
system_libraries: [
|
|
953
|
-
"stdc++", "webkit2gtk-4.1", "javascriptcoregtk-4.1", "gtk-3", "gdk-3",
|
|
954
|
-
"soup-3.0", "gio-2.0", "gobject-2.0", "glib-2.0", "gmodule-2.0",
|
|
955
|
-
"pango-1.0", "pangocairo-1.0", "harfbuzz", "atk-1.0", "cairo",
|
|
956
|
-
"cairo-gobject", "gdk_pixbuf-2.0", "z", "pthread",
|
|
957
|
-
],
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
740
|
|
|
961
741
|
// ---- Windows subsystem ------------------------------------------------------
|
|
962
742
|
|
|
@@ -972,34 +752,16 @@ function ffiManifest(shimLib) {
|
|
|
972
752
|
// the loader whether to allocate a console. Every offset is checked before
|
|
973
753
|
// anything is written, and a file that does not look like a console-subsystem
|
|
974
754
|
// PE is left alone.
|
|
975
|
-
const IMAGE_SUBSYSTEM_WINDOWS_GUI = 2;
|
|
976
|
-
const IMAGE_SUBSYSTEM_WINDOWS_CUI = 3;
|
|
977
755
|
|
|
978
756
|
function makeGuiSubsystem(exePath) {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
757
|
+
let result;
|
|
758
|
+
try {
|
|
759
|
+
result = patchPeSubsystem(readFileSync(exePath));
|
|
760
|
+
} catch (e) {
|
|
761
|
+
if (e instanceof PeError) fail(`${exePath} ${e.message}`);
|
|
762
|
+
throw e;
|
|
982
763
|
}
|
|
983
|
-
|
|
984
|
-
if (peOff + 24 > buf.length || buf.readUInt32LE(peOff) !== 0x00004550) {
|
|
985
|
-
fail(`${exePath} has no PE signature at ${peOff}`);
|
|
986
|
-
}
|
|
987
|
-
// Optional header starts after the 4-byte signature and 20-byte COFF header;
|
|
988
|
-
// Subsystem sits at +68 in both PE32 (0x10b) and PE32+ (0x20b).
|
|
989
|
-
const optOff = peOff + 24;
|
|
990
|
-
const magic = buf.readUInt16LE(optOff);
|
|
991
|
-
if (magic !== 0x10b && magic !== 0x20b) {
|
|
992
|
-
fail(`${exePath} has an unrecognised optional header magic 0x${magic.toString(16)}`);
|
|
993
|
-
}
|
|
994
|
-
const subOff = optOff + 68;
|
|
995
|
-
if (subOff + 2 > buf.length) fail(`${exePath} is truncated before its Subsystem field`);
|
|
996
|
-
const current = buf.readUInt16LE(subOff);
|
|
997
|
-
if (current === IMAGE_SUBSYSTEM_WINDOWS_GUI) return;
|
|
998
|
-
if (current !== IMAGE_SUBSYSTEM_WINDOWS_CUI) {
|
|
999
|
-
fail(`${exePath} has an unexpected subsystem ${current}; refusing to rewrite it`);
|
|
1000
|
-
}
|
|
1001
|
-
buf.writeUInt16LE(IMAGE_SUBSYSTEM_WINDOWS_GUI, subOff);
|
|
1002
|
-
writeFileSync(exePath, buf);
|
|
764
|
+
if (result.patched) writeFileSync(exePath, result.buf);
|
|
1003
765
|
}
|
|
1004
766
|
|
|
1005
767
|
// ---- build ----------------------------------------------------------------
|
|
@@ -1187,10 +949,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
|
1187
949
|
// as well.
|
|
1188
950
|
writeFileSync(
|
|
1189
951
|
join(buildDir, "main.ts"),
|
|
1190
|
-
readFileSync(mainSrc, "utf8")
|
|
1191
|
-
/(\bfrom\s*)(['"])janela\/host\2/g,
|
|
1192
|
-
"$1$2./janela$2",
|
|
1193
|
-
),
|
|
952
|
+
rewriteHostSpecifier(readFileSync(mainSrc, "utf8")),
|
|
1194
953
|
);
|
|
1195
954
|
|
|
1196
955
|
const html = frontendHtml(root, conf, devUrl);
|
|
@@ -1261,7 +1020,12 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
|
1261
1020
|
if (ios) return buildIos(root, conf, buildDir, outDir);
|
|
1262
1021
|
if (android) return buildAndroid(root, conf, buildDir, outDir);
|
|
1263
1022
|
|
|
1264
|
-
writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib
|
|
1023
|
+
writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib, {
|
|
1024
|
+
macSdkPath:
|
|
1025
|
+
process.platform === "darwin"
|
|
1026
|
+
? capture(["xcrun", "--sdk", "macosx", "--show-sdk-path"])
|
|
1027
|
+
: null,
|
|
1028
|
+
}), null, 2) + "\n");
|
|
1265
1029
|
|
|
1266
1030
|
console.log("janela: compiling TypeScript to a native binary");
|
|
1267
1031
|
// An explicit --out is used verbatim, so the PE suffix is ours to add.
|
|
@@ -1374,20 +1138,9 @@ function copyTemplate(from, to, name) {
|
|
|
1374
1138
|
// allowed because people type them and every downstream use accepts them —
|
|
1375
1139
|
// Android application ids in particular *prefer* them, since a Java package
|
|
1376
1140
|
// segment cannot contain a hyphen (see androidApplicationId).
|
|
1377
|
-
const NAME_RE = /^[a-z][a-z0-9_-]*$/;
|
|
1378
1141
|
|
|
1379
1142
|
// Best-effort repair of a rejected name, so the error can suggest something
|
|
1380
1143
|
// that would have worked instead of only stating the rule.
|
|
1381
|
-
function suggestName(raw) {
|
|
1382
|
-
const s = String(raw)
|
|
1383
|
-
.toLowerCase()
|
|
1384
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
1385
|
-
.replace(/^[^a-z]+/, "")
|
|
1386
|
-
.replace(/-{2,}/g, "-")
|
|
1387
|
-
.replace(/[-_]+$/, "");
|
|
1388
|
-
return NAME_RE.test(s) ? s : "";
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
1144
|
function init(name, template) {
|
|
1392
1145
|
if (!name) {
|
|
1393
1146
|
fail(
|
|
@@ -1484,11 +1237,11 @@ async function dev(root) {
|
|
|
1484
1237
|
const port = await freePort();
|
|
1485
1238
|
devUrl = `http://localhost:${port}/`;
|
|
1486
1239
|
console.log(`janela: starting the vite dev server on ${devUrl}`);
|
|
1487
|
-
|
|
1240
|
+
const viteCmd = viteCommand(root);
|
|
1241
|
+
vite = spawn(viteCmd.argv[0], [...viteCmd.argv.slice(1), "--port", String(port), "--strictPort"], {
|
|
1488
1242
|
cwd: root,
|
|
1489
1243
|
stdio: "inherit",
|
|
1490
|
-
|
|
1491
|
-
shell: process.platform === "win32",
|
|
1244
|
+
shell: viteCmd.shell,
|
|
1492
1245
|
});
|
|
1493
1246
|
const stop = () => { if (vite && vite.exitCode === null) vite.kill(); };
|
|
1494
1247
|
process.on("exit", stop);
|
package/bin/lib.mjs
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers shared by the CLI and its tests.
|
|
3
|
+
*
|
|
4
|
+
* `bin/janela.mjs` is build orchestration: it shells out, writes files and
|
|
5
|
+
* exits. The decisions inside it — how a name is validated, how a config's
|
|
6
|
+
* defaults fall back, which symbols an FFI manifest must declare, which bytes
|
|
7
|
+
* of a PE may be rewritten — are pure, and a wrong answer from any of them
|
|
8
|
+
* fails quietly rather than loudly: a missing FFI declaration makes the
|
|
9
|
+
* runtime fail to link, and a mis-coerced Android id is rejected by aapt2
|
|
10
|
+
* only at package time.
|
|
11
|
+
*
|
|
12
|
+
* So they live here, where a test can call them directly. Nothing in this
|
|
13
|
+
* file touches the filesystem, the network or a subprocess.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
// ---- project names ---------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const NAME_RE = /^[a-z][a-z0-9_-]*$/;
|
|
21
|
+
|
|
22
|
+
export const MIME = {
|
|
23
|
+
".css": "text/css", ".js": "text/javascript", ".json": "application/json",
|
|
24
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
25
|
+
".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif",
|
|
26
|
+
".svg": "image/svg+xml", ".ico": "image/x-icon",
|
|
27
|
+
".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
|
|
28
|
+
".mp3": "audio/mpeg", ".mp4": "video/mp4", ".webm": "video/webm",
|
|
29
|
+
};
|
|
30
|
+
export const STR = (name, symbol) => ({ name, symbol, params: ["i32", "string"], returns: "i32" });
|
|
31
|
+
export const IOS_MIN_VERSION = "15.0";
|
|
32
|
+
export const IOS_PREFIX = "jl_";
|
|
33
|
+
export const ANDROID_MIN_SDK = 26;
|
|
34
|
+
export const ANDROID_TARGET_SDK = 34;
|
|
35
|
+
export const ANDROID_ABI = "arm64-v8a";
|
|
36
|
+
export const IMAGE_SUBSYSTEM_WINDOWS_GUI = 2;
|
|
37
|
+
export const IMAGE_SUBSYSTEM_WINDOWS_CUI = 3;
|
|
38
|
+
|
|
39
|
+
export function suggestName(raw) {
|
|
40
|
+
const s = String(raw)
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
43
|
+
.replace(/^[^a-z]+/, "")
|
|
44
|
+
.replace(/-{2,}/g, "-")
|
|
45
|
+
.replace(/[-_]+$/, "");
|
|
46
|
+
return NAME_RE.test(s) ? s : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function androidPackage(id) {
|
|
50
|
+
return id
|
|
51
|
+
.split(".")
|
|
52
|
+
.map((seg) => {
|
|
53
|
+
const cleaned = seg.replace(/[^A-Za-z0-9_]/g, "_");
|
|
54
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
55
|
+
})
|
|
56
|
+
.join(".");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function androidConf(conf) {
|
|
60
|
+
const a = conf.android ?? {};
|
|
61
|
+
return {
|
|
62
|
+
applicationId: androidPackage(a.applicationId ?? a.identifier ?? conf.identifier),
|
|
63
|
+
label: a.label ?? conf.window?.title ?? conf.name,
|
|
64
|
+
minSdk: String(a.minSdk ?? ANDROID_MIN_SDK),
|
|
65
|
+
device: a.device ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function iosConf(conf) {
|
|
70
|
+
const ios = conf.ios ?? {};
|
|
71
|
+
return {
|
|
72
|
+
identifier: ios.identifier ?? conf.identifier,
|
|
73
|
+
displayName: ios.displayName ?? conf.window?.title ?? conf.name,
|
|
74
|
+
minimumVersion: String(ios.minimumVersion ?? IOS_MIN_VERSION),
|
|
75
|
+
device: ios.device ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function libraryProfile() {
|
|
80
|
+
return {
|
|
81
|
+
profile_format: 1,
|
|
82
|
+
name: "janela",
|
|
83
|
+
entry: "./entry.ts",
|
|
84
|
+
emission: "llvm",
|
|
85
|
+
abi: {
|
|
86
|
+
prefix: IOS_PREFIX,
|
|
87
|
+
init_symbol: `${IOS_PREFIX}init`,
|
|
88
|
+
sink_register_symbol: `${IOS_PREFIX}set_panic_sink`,
|
|
89
|
+
collect_symbol: `${IOS_PREFIX}collect`,
|
|
90
|
+
result_reset_symbol: `${IOS_PREFIX}reset`,
|
|
91
|
+
callback_register_symbol: `${IOS_PREFIX}set_callback`,
|
|
92
|
+
},
|
|
93
|
+
exports: [
|
|
94
|
+
{
|
|
95
|
+
export: "handleInvoke",
|
|
96
|
+
symbol: `${IOS_PREFIX}handle_invoke`,
|
|
97
|
+
params: ["string", "string"],
|
|
98
|
+
returns: "string",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
export: "indexHtml",
|
|
102
|
+
symbol: `${IOS_PREFIX}index_html`,
|
|
103
|
+
params: [],
|
|
104
|
+
returns: "string",
|
|
105
|
+
},
|
|
106
|
+
// The shell calls these back on the main queue when work it owns comes
|
|
107
|
+
// due. They are the mirror of the desktop shim's wv_on_timer: the
|
|
108
|
+
// library parks a continuation under an id and is re-entered with it.
|
|
109
|
+
{
|
|
110
|
+
export: "onTimer",
|
|
111
|
+
symbol: `${IOS_PREFIX}on_timer`,
|
|
112
|
+
params: ["f64"],
|
|
113
|
+
returns: "void",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
export: "onFsDone",
|
|
117
|
+
symbol: `${IOS_PREFIX}on_fs_done`,
|
|
118
|
+
params: ["f64", "bool", "string"],
|
|
119
|
+
returns: "void",
|
|
120
|
+
},
|
|
121
|
+
// Deliberately its own export rather than reusing onFsDone, whose
|
|
122
|
+
// signature would fit: a dialog result arriving through the file-I/O
|
|
123
|
+
// path would read as a bug for as long as the code lived.
|
|
124
|
+
{
|
|
125
|
+
export: "onDialogDone",
|
|
126
|
+
symbol: `${IOS_PREFIX}on_dialog_done`,
|
|
127
|
+
params: ["f64", "bool", "string"],
|
|
128
|
+
returns: "void",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
// TS -> shell. A channel handler must never re-enter the library (see
|
|
132
|
+
// upstream #263: violations silently appear to work), so every one of
|
|
133
|
+
// these only records the request; the shell acts on its own queue and
|
|
134
|
+
// re-enters through an export above on a later turn.
|
|
135
|
+
callbacks: [
|
|
136
|
+
{ name: "janelaEmit", params: ["string", "string"], returns: "void" },
|
|
137
|
+
{ name: "hostSchedule", params: ["f64", "f64"], returns: "void" },
|
|
138
|
+
{ name: "hostSettle", params: ["f64", "string"], returns: "void" },
|
|
139
|
+
{ name: "hostReadFile", params: ["f64", "string"], returns: "void" },
|
|
140
|
+
{ name: "hostWriteFile", params: ["f64", "string", "string"], returns: "void" },
|
|
141
|
+
{ name: "hostOpenDialog", params: ["f64", "string"], returns: "void" },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function mimeFor(p) {
|
|
147
|
+
const dot = p.lastIndexOf(".");
|
|
148
|
+
return (dot < 0 ? null : MIME[p.slice(dot).toLowerCase()]) ?? "application/octet-stream";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The FFI manifest a desktop build hands to scriptc: every shim symbol the
|
|
153
|
+
* runtime calls, plus the per-platform link inputs.
|
|
154
|
+
*
|
|
155
|
+
* `platform` and `macSdkPath` are parameters rather than ambient lookups so
|
|
156
|
+
* that every branch is reachable from any host — a manifest that silently
|
|
157
|
+
* loses a declaration fails at link time, on one platform, which is the worst
|
|
158
|
+
* place to find out.
|
|
159
|
+
*/
|
|
160
|
+
export function ffiManifest(shimLib, { platform = process.platform, macSdkPath = null } = {}) {
|
|
161
|
+
const functions = [
|
|
162
|
+
{ name: "wvCreate", symbol: "wv_create", params: ["i32"], returns: "i32" },
|
|
163
|
+
STR("wvSetTitle", "wv_set_title"),
|
|
164
|
+
{ name: "wvSetSize", symbol: "wv_set_size", params: ["i32", "i32", "i32", "i32"], returns: "i32" },
|
|
165
|
+
STR("wvSetHtml", "wv_set_html"),
|
|
166
|
+
STR("wvInit", "wv_init"),
|
|
167
|
+
STR("wvEval", "wv_eval"),
|
|
168
|
+
STR("wvBind", "wv_bind"),
|
|
169
|
+
STR("wvReply", "wv_reply"),
|
|
170
|
+
// Retained handlers (format 4): registered once, valid for the app's
|
|
171
|
+
// lifetime, so wv_run is a plain blocking call. The request rides in as a
|
|
172
|
+
// `string` param (format 3) rather than a byte-at-a-time drain.
|
|
173
|
+
{
|
|
174
|
+
name: "wvOnInvoke", symbol: "wv_on_invoke",
|
|
175
|
+
params: [
|
|
176
|
+
"i32",
|
|
177
|
+
{ callback: { id: "inv", params: ["string", { context: "inv" }], returns: "i32", lifetime: "retained" } },
|
|
178
|
+
{ context: "inv" },
|
|
179
|
+
],
|
|
180
|
+
returns: "i32",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "wvOnTimer", symbol: "wv_on_timer",
|
|
184
|
+
params: [
|
|
185
|
+
"i32",
|
|
186
|
+
{ callback: { id: "timer", params: ["i32", { context: "timer" }], returns: "void", lifetime: "retained" } },
|
|
187
|
+
{ context: "timer" },
|
|
188
|
+
],
|
|
189
|
+
returns: "i32",
|
|
190
|
+
},
|
|
191
|
+
{ name: "wvRun", symbol: "wv_run", params: ["i32"], returns: "i32" },
|
|
192
|
+
{ name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
|
|
193
|
+
// async: the held-reply table (deferred returns) plus shell-owned
|
|
194
|
+
// scheduling — TS parks a continuation id, the shell calls it back due.
|
|
195
|
+
{ name: "wvDefer", symbol: "wv_defer", params: ["i32"], returns: "i32" },
|
|
196
|
+
{ name: "wvResolve", symbol: "wv_resolve", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
197
|
+
{ name: "wvSchedule", symbol: "wv_schedule", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
198
|
+
// async file I/O: the blocking syscall runs on a shim worker thread
|
|
199
|
+
{ name: "wvFsRead", symbol: "wv_fs_read", params: ["i32", "string"], returns: "i32" },
|
|
200
|
+
{ name: "wvFsWrite", symbol: "wv_fs_write", params: ["i32", "string", "string"], returns: "i32" },
|
|
201
|
+
// Job accessors, shared by file I/O and dialogs: both are work whose
|
|
202
|
+
// answer cannot be produced during the FFI call that starts it.
|
|
203
|
+
{ name: "wvJobStatus", symbol: "wv_job_status", params: ["i32", "i32"], returns: "i32" },
|
|
204
|
+
{ name: "wvJobSize", symbol: "wv_job_size", params: ["i32", "i32"], returns: "f64" },
|
|
205
|
+
{
|
|
206
|
+
// One slice per call, so a large payload decodes across several UI turns
|
|
207
|
+
// instead of stalling on all of it at once. Returns the bytes covered.
|
|
208
|
+
name: "wvJobTakeAt", symbol: "wv_job_take_at",
|
|
209
|
+
params: [
|
|
210
|
+
"i32", "i32", "f64", "f64",
|
|
211
|
+
{ callback: { id: "sink", params: ["string", { context: "sink" }], returns: "void", lifetime: "call" } },
|
|
212
|
+
{ context: "sink" },
|
|
213
|
+
],
|
|
214
|
+
returns: "f64",
|
|
215
|
+
},
|
|
216
|
+
{ name: "wvJobFree", symbol: "wv_job_free", params: ["i32", "i32"], returns: "i32" },
|
|
217
|
+
// Native dialogs: the modal runs on a later UI-thread turn, so asking for
|
|
218
|
+
// one never blocks the invoke that asked. Options ride as plain params
|
|
219
|
+
// (kind, flags, title, defaultPath, defaultName, filters).
|
|
220
|
+
{
|
|
221
|
+
name: "wvDialog", symbol: "wv_dialog",
|
|
222
|
+
params: ["i32", "i32", "i32", "string", "string", "string", "string"],
|
|
223
|
+
returns: "i32",
|
|
224
|
+
},
|
|
225
|
+
{ name: "wvSetFullscreen", symbol: "wv_set_fullscreen", params: ["i32", "i32"], returns: "i32" },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
if (platform === "win32") {
|
|
229
|
+
// MinGW ignores MSVC's #pragma comment(lib, ...), so the Win32 imports the
|
|
230
|
+
// WebView2 backend needs are named explicitly. scriptc's own win32 lane
|
|
231
|
+
// already adds advapi32/iphlpapi/ws2_32, so those are omitted here.
|
|
232
|
+
// `c++` pulls libc++ for the shim's std::string/exceptions.
|
|
233
|
+
//
|
|
234
|
+
// `pthread` (mingw's libwinpthread) is here to work around an upstream
|
|
235
|
+
// scriptc bug: its runtime calls clock_gettime/nanosleep, which mingw
|
|
236
|
+
// declares in <time.h> but implements in winpthreads, and scriptc's win32
|
|
237
|
+
// link never adds it. Without this the link dies with
|
|
238
|
+
// "undefined symbol: clock_gettime" — reproducible with a plain
|
|
239
|
+
// `scriptc build hello.ts` on Windows, no FFI involved.
|
|
240
|
+
return {
|
|
241
|
+
ffi_format: 4,
|
|
242
|
+
functions,
|
|
243
|
+
libraries: [shimLib],
|
|
244
|
+
system_libraries: [
|
|
245
|
+
"c++", "pthread",
|
|
246
|
+
"ole32", "oleaut32", "shlwapi", "shell32", "user32", "version", "gdi32",
|
|
247
|
+
// GetOpenFileNameW / GetSaveFileNameW for the native file dialogs.
|
|
248
|
+
"comdlg32",
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (platform === "darwin") {
|
|
254
|
+
// scriptc has no -framework support, but `libraries` entries are passed to
|
|
255
|
+
// the link as plain input files and ld64 accepts .tbd stubs.
|
|
256
|
+
if (!macSdkPath) throw new Error("ffiManifest: macSdkPath is required on darwin");
|
|
257
|
+
const sdk = macSdkPath;
|
|
258
|
+
return {
|
|
259
|
+
ffi_format: 4,
|
|
260
|
+
functions,
|
|
261
|
+
libraries: [
|
|
262
|
+
shimLib,
|
|
263
|
+
join(sdk, "System/Library/Frameworks/WebKit.framework/WebKit.tbd"),
|
|
264
|
+
join(sdk, "System/Library/Frameworks/Cocoa.framework/Cocoa.tbd"),
|
|
265
|
+
],
|
|
266
|
+
system_libraries: ["c++"],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
ffi_format: 4,
|
|
271
|
+
functions,
|
|
272
|
+
libraries: [shimLib],
|
|
273
|
+
system_libraries: [
|
|
274
|
+
"stdc++", "webkit2gtk-4.1", "javascriptcoregtk-4.1", "gtk-3", "gdk-3",
|
|
275
|
+
"soup-3.0", "gio-2.0", "gobject-2.0", "glib-2.0", "gmodule-2.0",
|
|
276
|
+
"pango-1.0", "pangocairo-1.0", "harfbuzz", "atk-1.0", "cairo",
|
|
277
|
+
"cairo-gobject", "gdk_pixbuf-2.0", "z", "pthread",
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// ---- host specifier rewrite -------------------------------------------------
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Rewrite a project's `from "janela/host"` to the runtime copy the CLI places
|
|
285
|
+
* beside it in `.janela/build/`.
|
|
286
|
+
*
|
|
287
|
+
* A project imports the package specifier so that it resolves in an editor
|
|
288
|
+
* against the installed package; the build compiles against the local copy
|
|
289
|
+
* instead, which keeps it static (no node_modules resolution) and keeps
|
|
290
|
+
* working once "janela/host" exports values and not only types.
|
|
291
|
+
*
|
|
292
|
+
* Deliberately narrow: only a specifier in `from` position, only that exact
|
|
293
|
+
* module, either quote style. A mention inside a comment or a longer
|
|
294
|
+
* specifier such as "janela/host-extras" is left alone.
|
|
295
|
+
*/
|
|
296
|
+
export function rewriteHostSpecifier(src) {
|
|
297
|
+
return src.replace(/(\bfrom\s*)(['"])janela\/host\2/g, "$1$2./janela$2");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- Windows subsystem ------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/** A PE the patcher refused to touch, with `code` naming the reason. */
|
|
303
|
+
export class PeError extends Error {
|
|
304
|
+
constructor(code, message) {
|
|
305
|
+
super(message);
|
|
306
|
+
this.code = code;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Flip a console-subsystem PE to the GUI subsystem, in place, on a copy of
|
|
312
|
+
* the caller's buffer.
|
|
313
|
+
*
|
|
314
|
+
* Returns `{ patched: false }` when the image is already GUI, or
|
|
315
|
+
* `{ patched: true, buf }` with the rewritten bytes. Anything that does not
|
|
316
|
+
* look like the console-subsystem PE we just linked throws a `PeError`
|
|
317
|
+
* instead of being modified — the whole point is to touch exactly one field
|
|
318
|
+
* of exactly one shape of file.
|
|
319
|
+
*/
|
|
320
|
+
export function patchPeSubsystem(input) {
|
|
321
|
+
const buf = Buffer.from(input);
|
|
322
|
+
if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) {
|
|
323
|
+
throw new PeError("no-mz", "not a PE image (no MZ header)");
|
|
324
|
+
}
|
|
325
|
+
const peOff = buf.readUInt32LE(0x3c);
|
|
326
|
+
if (peOff + 24 > buf.length || buf.readUInt32LE(peOff) !== 0x00004550) {
|
|
327
|
+
throw new PeError("no-pe", `no PE signature at ${peOff}`);
|
|
328
|
+
}
|
|
329
|
+
// Optional header starts after the 4-byte signature and 20-byte COFF header;
|
|
330
|
+
// Subsystem sits at +68 in both PE32 (0x10b) and PE32+ (0x20b).
|
|
331
|
+
const optOff = peOff + 24;
|
|
332
|
+
const magic = buf.readUInt16LE(optOff);
|
|
333
|
+
if (magic !== 0x10b && magic !== 0x20b) {
|
|
334
|
+
throw new PeError("bad-magic", `unrecognised optional header magic 0x${magic.toString(16)}`);
|
|
335
|
+
}
|
|
336
|
+
const subOff = optOff + 68;
|
|
337
|
+
if (subOff + 2 > buf.length) {
|
|
338
|
+
throw new PeError("truncated", "truncated before its Subsystem field");
|
|
339
|
+
}
|
|
340
|
+
const current = buf.readUInt16LE(subOff);
|
|
341
|
+
if (current === IMAGE_SUBSYSTEM_WINDOWS_GUI) return { patched: false };
|
|
342
|
+
if (current !== IMAGE_SUBSYSTEM_WINDOWS_CUI) {
|
|
343
|
+
throw new PeError("unexpected-subsystem", `unexpected subsystem ${current}; refusing to rewrite it`);
|
|
344
|
+
}
|
|
345
|
+
buf.writeUInt16LE(IMAGE_SUBSYSTEM_WINDOWS_GUI, subOff);
|
|
346
|
+
return { patched: true, buf };
|
|
347
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,5 +56,13 @@
|
|
|
56
56
|
"templates/",
|
|
57
57
|
"vendor-webview/core/include/",
|
|
58
58
|
"vendor-webview/LICENSE"
|
|
59
|
-
]
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"test": "npm run test:unit && npm run test:types",
|
|
62
|
+
"test:unit": "node --test test/unit/*.test.mjs",
|
|
63
|
+
"test:types": "node test/types/run.mjs"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"typescript": "^5.9.3"
|
|
67
|
+
}
|
|
60
68
|
}
|
package/shim/android/app.cc
CHANGED
|
@@ -328,6 +328,15 @@ void finish_fs(double id, bool ok, std::string payload) {
|
|
|
328
328
|
void host_read_file(void *, double id, const char *path, size_t path_len) {
|
|
329
329
|
std::string p = resolve_path(std::string(path, path_len));
|
|
330
330
|
std::thread([id, p] {
|
|
331
|
+
// As on iOS: a directory opens as an ifstream and reads as empty, which
|
|
332
|
+
// would report success with no content. Desktop answers EISDIR; keep the
|
|
333
|
+
// message identical across platforms.
|
|
334
|
+
struct stat st;
|
|
335
|
+
if (::stat(p.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
336
|
+
finish_fs(id, false,
|
|
337
|
+
"EISDIR: illegal operation on a directory, read '" + p + "'");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
331
340
|
std::ifstream in(p, std::ios::binary);
|
|
332
341
|
if (!in) {
|
|
333
342
|
finish_fs(id, false,
|
package/shim/ios/app.cc
CHANGED
|
@@ -277,6 +277,16 @@ void finish_fs(double id, bool ok, std::string payload) {
|
|
|
277
277
|
void host_read_file(void *, double id, const char *path, size_t path_len) {
|
|
278
278
|
std::string p = resolve_path(std::string(path, path_len));
|
|
279
279
|
dispatch_async(fs_queue(), ^{
|
|
280
|
+
// A directory opens cleanly as an ifstream on Apple platforms and then
|
|
281
|
+
// reads as empty, so without this check readFileAsync would report
|
|
282
|
+
// success with no content. Desktop already answers EISDIR here; the
|
|
283
|
+
// message is kept identical so app code can treat the platforms alike.
|
|
284
|
+
struct stat st;
|
|
285
|
+
if (::stat(p.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
286
|
+
finish_fs(id, false,
|
|
287
|
+
"EISDIR: illegal operation on a directory, read '" + p + "'");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
280
290
|
std::ifstream in(p, std::ios::binary);
|
|
281
291
|
if (!in) {
|
|
282
292
|
finish_fs(id, false, "ENOENT: no such file or directory, open '" + p + "'");
|