janela 0.1.2 → 0.3.0
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 +135 -26
- package/bin/janela.mjs +382 -46
- package/package.json +3 -3
- package/runtime/janela.ts +289 -122
- package/shim/wvshim.cc +726 -67
- package/templates/index.html +31 -0
- package/templates/main.ts +62 -19
- package/templates/react/deps.json +4 -0
- package/templates/react/files/index.html +11 -0
- package/templates/react/files/janela.conf.json +10 -0
- package/templates/react/files/src/App.css +3 -0
- package/templates/react/files/src/App.jsx +38 -0
- package/templates/react/files/src/main.jsx +9 -0
- package/templates/react/files/src-host/main.ts +42 -0
- package/templates/react/files/vite.config.js +6 -0
- package/templates/solid/deps.json +4 -0
- package/templates/solid/files/index.html +11 -0
- package/templates/solid/files/janela.conf.json +10 -0
- package/templates/solid/files/src/App.css +3 -0
- package/templates/solid/files/src/App.jsx +35 -0
- package/templates/solid/files/src/main.jsx +4 -0
- package/templates/solid/files/src-host/main.ts +42 -0
- package/templates/solid/files/vite.config.js +6 -0
- package/templates/svelte/deps.json +7 -0
- package/templates/svelte/files/index.html +11 -0
- package/templates/svelte/files/janela.conf.json +10 -0
- package/templates/svelte/files/src/App.svelte +33 -0
- package/templates/svelte/files/src/main.js +4 -0
- package/templates/svelte/files/src-host/main.ts +42 -0
- package/templates/svelte/files/svelte.config.js +3 -0
- package/templates/svelte/files/vite.config.js +6 -0
- package/templates/vue/deps.json +4 -0
- package/templates/vue/files/index.html +11 -0
- package/templates/vue/files/janela.conf.json +10 -0
- package/templates/vue/files/src/App.vue +39 -0
- package/templates/vue/files/src/main.js +4 -0
- package/templates/vue/files/src-host/main.ts +42 -0
- package/templates/vue/files/vite.config.js +6 -0
package/bin/janela.mjs
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
// webview.h, the vendored webview, the scriptc runtime library, the FFI
|
|
11
11
|
// manifest — lives in this package and is assembled into .janela/ at build time.
|
|
12
12
|
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import {
|
|
15
|
-
cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync,
|
|
15
|
+
cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
|
+
import { createServer } from "node:net";
|
|
17
18
|
import { createRequire } from "node:module";
|
|
18
19
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +55,149 @@ function loadConf(root) {
|
|
|
54
55
|
return c;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// ---- frontend: plain HTML, or a Vite app -----------------------------------
|
|
59
|
+
//
|
|
60
|
+
// A project is in "Vite mode" when it has a vite config at its root; otherwise
|
|
61
|
+
// index.html is inlined verbatim, exactly as janela has always done.
|
|
62
|
+
//
|
|
63
|
+
// There is no file server behind the window: the shim hands the webview one
|
|
64
|
+
// HTML document (webview_set_html). So a Vite build is flattened into that one
|
|
65
|
+
// document — JS inlined as a module script, CSS as a <style>, everything else
|
|
66
|
+
// as a data: URI. The alternative designs (embedding the dist tree and serving
|
|
67
|
+
// it from a localhost HTTP server in the shim, or registering a custom URI
|
|
68
|
+
// scheme per platform) each cost a platform-specific implementation in C++ and
|
|
69
|
+
// buy nothing until an app outgrows a single document.
|
|
70
|
+
|
|
71
|
+
const VITE_CONFIGS = [
|
|
72
|
+
"vite.config.js", "vite.config.ts", "vite.config.mjs",
|
|
73
|
+
"vite.config.mts", "vite.config.cjs", "vite.config.cts",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function viteConfigPath(root) {
|
|
77
|
+
for (const f of VITE_CONFIGS) {
|
|
78
|
+
const p = join(root, f);
|
|
79
|
+
if (existsSync(p)) return p;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function viteBin(root) {
|
|
85
|
+
const p = join(root, "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite");
|
|
86
|
+
if (!existsSync(p)) {
|
|
87
|
+
fail(
|
|
88
|
+
"this project has a vite config but no local vite — run your package manager's " +
|
|
89
|
+
"install first (npm install / pnpm install)",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
|
|
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
|
+
|
|
109
|
+
// A dist-relative reference ("/assets/x.js", "./assets/x.js") → absolute path,
|
|
110
|
+
// or null when it points outside the build (a CDN URL, a data: URI, an anchor).
|
|
111
|
+
function distAsset(distDir, ref) {
|
|
112
|
+
if (!ref || /^(https?:|data:|blob:|#|mailto:)/i.test(ref)) return null;
|
|
113
|
+
const clean = ref.split("?")[0].split("#")[0].replace(/^\.?\//, "");
|
|
114
|
+
if (!clean) return null;
|
|
115
|
+
const p = join(distDir, clean);
|
|
116
|
+
return existsSync(p) && statSync(p).isFile() ? p : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function dataUri(file) {
|
|
120
|
+
return `data:${mimeFor(file)};base64,${readFileSync(file).toString("base64")}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function attr(tag, name) {
|
|
124
|
+
const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"));
|
|
125
|
+
return m ? m[1] : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// url(...) inside CSS → data: URIs, so a stylesheet's images and fonts survive
|
|
129
|
+
// the flattening too.
|
|
130
|
+
function inlineCssUrls(css, distDir, cssFile) {
|
|
131
|
+
return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (whole, _q, ref) => {
|
|
132
|
+
const rel = ref.startsWith("/")
|
|
133
|
+
? distAsset(distDir, ref)
|
|
134
|
+
: distAsset(distDir, relative(distDir, join(dirname(cssFile), ref)));
|
|
135
|
+
return rel ? `url("${dataUri(rel)}")` : whole;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Flatten dist/ into one self-contained HTML document.
|
|
140
|
+
function inlineDist(distDir) {
|
|
141
|
+
const htmlPath = join(distDir, "index.html");
|
|
142
|
+
if (!existsSync(htmlPath)) fail(`vite build produced no ${relative(process.cwd(), htmlPath)}`);
|
|
143
|
+
let html = readFileSync(htmlPath, "utf8");
|
|
144
|
+
|
|
145
|
+
// Preloads only matter when there are separate files to fetch.
|
|
146
|
+
html = html.replace(/<link\b[^>]*\brel\s*=\s*["'](?:modulepreload|preload|prefetch)["'][^>]*>\s*/gi, "");
|
|
147
|
+
|
|
148
|
+
html = html.replace(/<script\b([^>]*)><\/script>/gi, (whole, attrs) => {
|
|
149
|
+
const file = distAsset(distDir, attr(attrs, "src"));
|
|
150
|
+
if (!file) return whole;
|
|
151
|
+
const type = /\btype\s*=\s*["']module["']/i.test(attrs) ? ' type="module"' : "";
|
|
152
|
+
// A literal </script> inside the code would close this tag early.
|
|
153
|
+
const js = readFileSync(file, "utf8").replace(/<\/script/gi, "<\\/script");
|
|
154
|
+
return `<script${type}>${js}</script>`;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
html = html.replace(/<link\b([^>]*)>/gi, (whole, attrs) => {
|
|
158
|
+
const href = attr(attrs, "href");
|
|
159
|
+
const file = distAsset(distDir, href);
|
|
160
|
+
if (!file) return whole;
|
|
161
|
+
if (/\brel\s*=\s*["']stylesheet["']/i.test(attrs)) {
|
|
162
|
+
const css = inlineCssUrls(readFileSync(file, "utf8"), distDir, file).replace(/<\/style/gi, "<\\/style");
|
|
163
|
+
return `<style>${css}</style>`;
|
|
164
|
+
}
|
|
165
|
+
return whole.replace(href, dataUri(file));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Anything still pointing at a file in dist (favicons, <img>, <source>).
|
|
169
|
+
html = html.replace(/\b(src|href)\s*=\s*["']([^"']+)["']/gi, (whole, name, ref) => {
|
|
170
|
+
const file = distAsset(distDir, ref);
|
|
171
|
+
return file ? `${name}="${dataUri(file)}"` : whole;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return html;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The document handed to the webview. In dev mode it is a stub that hands the
|
|
178
|
+
// window over to the Vite server: location.replace leaves no history entry, and
|
|
179
|
+
// the janela bootstrap is injected per-document (webview_init), so the served
|
|
180
|
+
// page gets window.janela exactly like an inlined one.
|
|
181
|
+
function frontendHtml(root, conf, devUrl) {
|
|
182
|
+
if (devUrl) {
|
|
183
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${conf.name}</title></head>` +
|
|
184
|
+
`<body><script>location.replace(${JSON.stringify(devUrl)});</script></body></html>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (viteConfigPath(root)) {
|
|
188
|
+
console.log("janela: building the frontend with vite");
|
|
189
|
+
run([viteBin(root), "build"], { cwd: root });
|
|
190
|
+
const distDir = resolve(root, conf.frontend?.dist ?? "dist");
|
|
191
|
+
const html = inlineDist(distDir);
|
|
192
|
+
console.log(`janela: frontend inlined (${(Buffer.byteLength(html) / 1024).toFixed(0)} kB)`);
|
|
193
|
+
return html;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const htmlSrc = join(root, "index.html");
|
|
197
|
+
if (!existsSync(htmlSrc)) fail("missing index.html");
|
|
198
|
+
return readFileSync(htmlSrc, "utf8");
|
|
199
|
+
}
|
|
200
|
+
|
|
57
201
|
// ---- Windows toolchain ------------------------------------------------------
|
|
58
202
|
|
|
59
203
|
// scriptc's win32 lane is MinGW-shaped twice over: its runtime uses POSIX
|
|
@@ -166,25 +310,60 @@ function ffiManifest(shimLib) {
|
|
|
166
310
|
STR("wvInit", "wv_init"),
|
|
167
311
|
STR("wvEval", "wv_eval"),
|
|
168
312
|
STR("wvBind", "wv_bind"),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
313
|
+
STR("wvReply", "wv_reply"),
|
|
314
|
+
// Retained handlers (format 4): registered once, valid for the app's
|
|
315
|
+
// lifetime, so wv_run is a plain blocking call. The request rides in as a
|
|
316
|
+
// `string` param (format 3) rather than a byte-at-a-time drain.
|
|
317
|
+
{
|
|
318
|
+
name: "wvOnInvoke", symbol: "wv_on_invoke",
|
|
319
|
+
params: [
|
|
320
|
+
"i32",
|
|
321
|
+
{ callback: { id: "inv", params: ["string", { context: "inv" }], returns: "i32", lifetime: "retained" } },
|
|
322
|
+
{ context: "inv" },
|
|
323
|
+
],
|
|
324
|
+
returns: "i32",
|
|
325
|
+
},
|
|
173
326
|
{
|
|
174
|
-
name: "
|
|
327
|
+
name: "wvOnTick", symbol: "wv_on_tick",
|
|
175
328
|
params: [
|
|
176
329
|
"i32",
|
|
177
|
-
{ callback: { id: "
|
|
178
|
-
{ context: "
|
|
330
|
+
{ callback: { id: "tick", params: [{ context: "tick" }], returns: "void", lifetime: "retained" } },
|
|
331
|
+
{ context: "tick" },
|
|
179
332
|
],
|
|
180
333
|
returns: "i32",
|
|
181
334
|
},
|
|
335
|
+
{ name: "wvRun", symbol: "wv_run", params: ["i32"], returns: "i32" },
|
|
182
336
|
{ name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
|
|
183
337
|
// async: deferred returns + the UI-thread pump behind app.defer/sleep
|
|
184
338
|
{ name: "wvDefer", symbol: "wv_defer", params: ["i32"], returns: "i32" },
|
|
185
339
|
{ name: "wvResolve", symbol: "wv_resolve", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
186
340
|
{ name: "wvTickStart", symbol: "wv_tick_start", params: ["i32", "i32"], returns: "i32" },
|
|
187
341
|
{ name: "wvTickStop", symbol: "wv_tick_stop", params: ["i32"], returns: "i32" },
|
|
342
|
+
// async file I/O: the blocking syscall runs on a shim worker thread
|
|
343
|
+
{ name: "wvFsRead", symbol: "wv_fs_read", params: ["i32", "string"], returns: "i32" },
|
|
344
|
+
{ name: "wvFsWrite", symbol: "wv_fs_write", params: ["i32", "string", "string"], returns: "i32" },
|
|
345
|
+
// Job accessors, shared by file I/O and dialogs: both are work whose
|
|
346
|
+
// answer cannot be produced during the FFI call that starts it.
|
|
347
|
+
{ name: "wvJobStatus", symbol: "wv_job_status", params: ["i32", "i32"], returns: "i32" },
|
|
348
|
+
{
|
|
349
|
+
name: "wvJobTake", symbol: "wv_job_take",
|
|
350
|
+
params: [
|
|
351
|
+
"i32", "i32",
|
|
352
|
+
{ callback: { id: "sink", params: ["string", { context: "sink" }], returns: "void", lifetime: "call" } },
|
|
353
|
+
{ context: "sink" },
|
|
354
|
+
],
|
|
355
|
+
returns: "i32",
|
|
356
|
+
},
|
|
357
|
+
{ name: "wvJobFree", symbol: "wv_job_free", params: ["i32", "i32"], returns: "i32" },
|
|
358
|
+
// Native dialogs: the modal runs on a later UI-thread turn, so asking for
|
|
359
|
+
// one never blocks the invoke that asked. Options ride as plain params
|
|
360
|
+
// (kind, flags, title, defaultPath, defaultName, filters).
|
|
361
|
+
{
|
|
362
|
+
name: "wvDialog", symbol: "wv_dialog",
|
|
363
|
+
params: ["i32", "i32", "i32", "string", "string", "string", "string"],
|
|
364
|
+
returns: "i32",
|
|
365
|
+
},
|
|
366
|
+
{ name: "wvSetFullscreen", symbol: "wv_set_fullscreen", params: ["i32", "i32"], returns: "i32" },
|
|
188
367
|
];
|
|
189
368
|
|
|
190
369
|
if (process.platform === "win32") {
|
|
@@ -200,12 +379,14 @@ function ffiManifest(shimLib) {
|
|
|
200
379
|
// "undefined symbol: clock_gettime" — reproducible with a plain
|
|
201
380
|
// `scriptc build hello.ts` on Windows, no FFI involved.
|
|
202
381
|
return {
|
|
203
|
-
ffi_format:
|
|
382
|
+
ffi_format: 4,
|
|
204
383
|
functions,
|
|
205
384
|
libraries: [shimLib],
|
|
206
385
|
system_libraries: [
|
|
207
386
|
"c++", "pthread",
|
|
208
387
|
"ole32", "oleaut32", "shlwapi", "shell32", "user32", "version", "gdi32",
|
|
388
|
+
// GetOpenFileNameW / GetSaveFileNameW for the native file dialogs.
|
|
389
|
+
"comdlg32",
|
|
209
390
|
],
|
|
210
391
|
};
|
|
211
392
|
}
|
|
@@ -215,7 +396,7 @@ function ffiManifest(shimLib) {
|
|
|
215
396
|
// the link as plain input files and ld64 accepts .tbd stubs.
|
|
216
397
|
const sdk = capture(["xcrun", "--sdk", "macosx", "--show-sdk-path"]);
|
|
217
398
|
return {
|
|
218
|
-
ffi_format:
|
|
399
|
+
ffi_format: 4,
|
|
219
400
|
functions,
|
|
220
401
|
libraries: [
|
|
221
402
|
shimLib,
|
|
@@ -226,7 +407,7 @@ function ffiManifest(shimLib) {
|
|
|
226
407
|
};
|
|
227
408
|
}
|
|
228
409
|
return {
|
|
229
|
-
ffi_format:
|
|
410
|
+
ffi_format: 4,
|
|
230
411
|
functions,
|
|
231
412
|
libraries: [shimLib],
|
|
232
413
|
system_libraries: [
|
|
@@ -238,9 +419,55 @@ function ffiManifest(shimLib) {
|
|
|
238
419
|
};
|
|
239
420
|
}
|
|
240
421
|
|
|
422
|
+
// ---- Windows subsystem ------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
// A console-subsystem .exe makes Windows open a console window behind the UI.
|
|
425
|
+
// The fix is normally `-mwindows` at link time, but scriptc exposes no way to
|
|
426
|
+
// pass a linker flag: `system_libraries` entries are validated as bare library
|
|
427
|
+
// names and explicitly rejected if they start with "-", `libraries` entries
|
|
428
|
+
// must resolve to existing files, and no env var is read for extra flags
|
|
429
|
+
// (checked in @scriptc/compiler 0.0.35: backend/cc.js, ffi/profile.js).
|
|
430
|
+
//
|
|
431
|
+
// So the subsystem byte is rewritten in the linked PE instead. The entry point
|
|
432
|
+
// is untouched — MinGW's mainCRTStartup runs either way; the field only tells
|
|
433
|
+
// the loader whether to allocate a console. Every offset is checked before
|
|
434
|
+
// anything is written, and a file that does not look like a console-subsystem
|
|
435
|
+
// PE is left alone.
|
|
436
|
+
const IMAGE_SUBSYSTEM_WINDOWS_GUI = 2;
|
|
437
|
+
const IMAGE_SUBSYSTEM_WINDOWS_CUI = 3;
|
|
438
|
+
|
|
439
|
+
function makeGuiSubsystem(exePath) {
|
|
440
|
+
const buf = readFileSync(exePath);
|
|
441
|
+
if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) {
|
|
442
|
+
fail(`${exePath} is not a PE image (no MZ header)`);
|
|
443
|
+
}
|
|
444
|
+
const peOff = buf.readUInt32LE(0x3c);
|
|
445
|
+
if (peOff + 24 > buf.length || buf.readUInt32LE(peOff) !== 0x00004550) {
|
|
446
|
+
fail(`${exePath} has no PE signature at ${peOff}`);
|
|
447
|
+
}
|
|
448
|
+
// Optional header starts after the 4-byte signature and 20-byte COFF header;
|
|
449
|
+
// Subsystem sits at +68 in both PE32 (0x10b) and PE32+ (0x20b).
|
|
450
|
+
const optOff = peOff + 24;
|
|
451
|
+
const magic = buf.readUInt16LE(optOff);
|
|
452
|
+
if (magic !== 0x10b && magic !== 0x20b) {
|
|
453
|
+
fail(`${exePath} has an unrecognised optional header magic 0x${magic.toString(16)}`);
|
|
454
|
+
}
|
|
455
|
+
const subOff = optOff + 68;
|
|
456
|
+
if (subOff + 2 > buf.length) fail(`${exePath} is truncated before its Subsystem field`);
|
|
457
|
+
const current = buf.readUInt16LE(subOff);
|
|
458
|
+
if (current === IMAGE_SUBSYSTEM_WINDOWS_GUI) return;
|
|
459
|
+
if (current !== IMAGE_SUBSYSTEM_WINDOWS_CUI) {
|
|
460
|
+
fail(`${exePath} has an unexpected subsystem ${current}; refusing to rewrite it`);
|
|
461
|
+
}
|
|
462
|
+
buf.writeUInt16LE(IMAGE_SUBSYSTEM_WINDOWS_GUI, subOff);
|
|
463
|
+
writeFileSync(exePath, buf);
|
|
464
|
+
}
|
|
465
|
+
|
|
241
466
|
// ---- build ----------------------------------------------------------------
|
|
242
467
|
|
|
243
|
-
|
|
468
|
+
// `devUrl` points the window at a running vite server instead of inlining the
|
|
469
|
+
// frontend; `gui` asks for a GUI-subsystem .exe on Windows (build, not dev).
|
|
470
|
+
function build(root, { devUrl = null, gui = true } = {}) {
|
|
244
471
|
const conf = loadConf(root);
|
|
245
472
|
const buildDir = join(root, ".janela", "build");
|
|
246
473
|
const cacheDir = join(root, ".janela", "cache");
|
|
@@ -255,12 +482,10 @@ function build(root) {
|
|
|
255
482
|
if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
|
|
256
483
|
cpSync(mainSrc, join(buildDir, "main.ts"));
|
|
257
484
|
|
|
258
|
-
const
|
|
259
|
-
if (!existsSync(htmlSrc)) fail("missing index.html");
|
|
260
|
-
const html = readFileSync(htmlSrc, "utf8");
|
|
485
|
+
const html = frontendHtml(root, conf, devUrl);
|
|
261
486
|
writeFileSync(
|
|
262
487
|
join(buildDir, "frontend.ts"),
|
|
263
|
-
`// Generated by janela
|
|
488
|
+
`// Generated by janela — do not edit.\nexport const INDEX_HTML: string = ${JSON.stringify(html)};\n`,
|
|
264
489
|
);
|
|
265
490
|
|
|
266
491
|
const w = conf.window;
|
|
@@ -298,6 +523,14 @@ function build(root) {
|
|
|
298
523
|
// inside the .exe rather than a side-by-side .pdb, so Windows benefits too.)
|
|
299
524
|
run(["strip", bin]);
|
|
300
525
|
|
|
526
|
+
// `janela dev` keeps the console subsystem so console.log from a command is
|
|
527
|
+
// visible in the terminal; a shipped `janela build` must not flash a console
|
|
528
|
+
// window behind the UI, and loses stdout as the price.
|
|
529
|
+
if (process.platform === "win32" && gui) {
|
|
530
|
+
makeGuiSubsystem(bin);
|
|
531
|
+
console.log("janela: linked as a GUI-subsystem .exe (no console window)");
|
|
532
|
+
}
|
|
533
|
+
|
|
301
534
|
if (process.platform === "darwin") {
|
|
302
535
|
const bundle = join(outDir, `${conf.name}.app`);
|
|
303
536
|
mkdirSync(join(bundle, "Contents", "MacOS"), { recursive: true });
|
|
@@ -330,8 +563,25 @@ function build(root) {
|
|
|
330
563
|
|
|
331
564
|
// ---- init -----------------------------------------------------------------
|
|
332
565
|
|
|
333
|
-
|
|
334
|
-
|
|
566
|
+
const TEMPLATES = ["vanilla", "vue", "react", "svelte", "solid"];
|
|
567
|
+
|
|
568
|
+
// Copy a template tree, substituting the project name in text files.
|
|
569
|
+
function copyTemplate(from, to, name) {
|
|
570
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
571
|
+
const src = join(from, entry.name);
|
|
572
|
+
const dst = join(to, entry.name);
|
|
573
|
+
if (entry.isDirectory()) {
|
|
574
|
+
mkdirSync(dst, { recursive: true });
|
|
575
|
+
copyTemplate(src, dst, name);
|
|
576
|
+
} else {
|
|
577
|
+
writeFileSync(dst, readFileSync(src, "utf8").replaceAll("__NAME__", name));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function init(name, template) {
|
|
583
|
+
if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) fail("usage: janela init <name> [--template <t>] (lowercase, digits, dashes)");
|
|
584
|
+
if (!TEMPLATES.includes(template)) fail(`unknown template '${template}' (${TEMPLATES.join(", ")})`);
|
|
335
585
|
const dir = resolve(process.cwd(), name);
|
|
336
586
|
if (existsSync(dir)) fail(`${name}/ already exists`);
|
|
337
587
|
mkdirSync(join(dir, "src-host"), { recursive: true });
|
|
@@ -343,44 +593,130 @@ function init(name) {
|
|
|
343
593
|
? `^${kitPkg.version}`
|
|
344
594
|
: `file:${relative(dir, KIT) || "."}`;
|
|
345
595
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
596
|
+
const pkg = {
|
|
597
|
+
name,
|
|
598
|
+
private: true,
|
|
599
|
+
scripts: { dev: "janela dev", build: "janela build" },
|
|
600
|
+
devDependencies: { janela: janelaDep },
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
if (template === "vanilla") {
|
|
604
|
+
// The zero-dependency shape janela has always scaffolded: no frontend
|
|
605
|
+
// toolchain, no npm install needed before the first build.
|
|
606
|
+
const t = (f) => readFileSync(join(KIT, "templates", f), "utf8").replaceAll("__NAME__", name);
|
|
607
|
+
writeFileSync(join(dir, "index.html"), t("index.html"));
|
|
608
|
+
writeFileSync(join(dir, "src-host", "main.ts"), t("main.ts"));
|
|
609
|
+
writeFileSync(join(dir, "janela.conf.json"), t("janela.conf.json"));
|
|
610
|
+
} else {
|
|
611
|
+
const tdir = join(KIT, "templates", template);
|
|
612
|
+
copyTemplate(join(tdir, "files"), dir, name);
|
|
613
|
+
const extra = JSON.parse(readFileSync(join(tdir, "deps.json"), "utf8"));
|
|
614
|
+
pkg.type = "module";
|
|
615
|
+
Object.assign(pkg.devDependencies, extra.devDependencies ?? {});
|
|
616
|
+
if (extra.dependencies) pkg.dependencies = extra.dependencies;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
writeFileSync(join(dir, ".gitignore"), ".janela/\nnode_modules/\ndist/\n");
|
|
620
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
621
|
+
|
|
622
|
+
const install = template === "vanilla" ? "" : "npm install && ";
|
|
623
|
+
console.log(`janela: created ${name}/ (${template}) — next: cd ${name} && ${install}janela dev`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ---- dev --------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
function freePort() {
|
|
629
|
+
return new Promise((ok, no) => {
|
|
630
|
+
const s = createServer();
|
|
631
|
+
s.on("error", no);
|
|
632
|
+
s.listen(0, "127.0.0.1", () => {
|
|
633
|
+
const { port } = s.address();
|
|
634
|
+
s.close(() => ok(port));
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function waitForServer(url, child, timeoutMs = 30000) {
|
|
640
|
+
const deadline = Date.now() + timeoutMs;
|
|
641
|
+
while (Date.now() < deadline) {
|
|
642
|
+
if (child.exitCode !== null) fail(`the vite dev server exited (code ${child.exitCode})`);
|
|
643
|
+
try {
|
|
644
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
645
|
+
if (r.ok || r.status === 404) return;
|
|
646
|
+
} catch {
|
|
647
|
+
// not listening yet
|
|
648
|
+
}
|
|
649
|
+
await new Promise((ok) => setTimeout(ok, 150));
|
|
650
|
+
}
|
|
651
|
+
fail(`the vite dev server did not answer at ${url} within ${timeoutMs / 1000}s`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function dev(root) {
|
|
655
|
+
let vite = null;
|
|
656
|
+
let devUrl = null;
|
|
657
|
+
|
|
658
|
+
if (viteConfigPath(root)) {
|
|
659
|
+
const port = await freePort();
|
|
660
|
+
devUrl = `http://localhost:${port}/`;
|
|
661
|
+
console.log(`janela: starting the vite dev server on ${devUrl}`);
|
|
662
|
+
vite = spawn(viteBin(root), ["--port", String(port), "--strictPort"], {
|
|
663
|
+
cwd: root,
|
|
664
|
+
stdio: "inherit",
|
|
665
|
+
// Windows resolves .cmd shims through the shell.
|
|
666
|
+
shell: process.platform === "win32",
|
|
667
|
+
});
|
|
668
|
+
const stop = () => { if (vite && vite.exitCode === null) vite.kill(); };
|
|
669
|
+
process.on("exit", stop);
|
|
670
|
+
process.on("SIGINT", () => { stop(); process.exit(130); });
|
|
671
|
+
await waitForServer(devUrl, vite);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Console subsystem on Windows: dev is where you want the logs.
|
|
675
|
+
const bin = build(root, { devUrl, gui: false });
|
|
676
|
+
console.log("janela: running (close the window or Ctrl-C to stop)");
|
|
677
|
+
// The frontend hot-reloads through vite; host changes need another `dev`.
|
|
678
|
+
run([bin]);
|
|
679
|
+
if (vite && vite.exitCode === null) vite.kill();
|
|
365
680
|
}
|
|
366
681
|
|
|
367
682
|
// ---- main -----------------------------------------------------------------
|
|
368
683
|
|
|
369
|
-
const
|
|
684
|
+
const argv = process.argv.slice(2);
|
|
685
|
+
const cmd = argv[0];
|
|
686
|
+
|
|
687
|
+
function flag(name, fallback) {
|
|
688
|
+
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
|
689
|
+
if (eq) return eq.slice(name.length + 3);
|
|
690
|
+
const i = argv.indexOf(`--${name}`);
|
|
691
|
+
if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--")) return argv[i + 1];
|
|
692
|
+
return fallback;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Everything after the subcommand that is neither a flag nor a flag's value.
|
|
696
|
+
function positionals() {
|
|
697
|
+
const out = [];
|
|
698
|
+
for (let i = 1; i < argv.length; i++) {
|
|
699
|
+
const a = argv[i];
|
|
700
|
+
if (!a.startsWith("--")) { out.push(a); continue; }
|
|
701
|
+
if (!a.includes("=") && argv[i + 1] && !argv[i + 1].startsWith("--")) i++;
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
|
|
370
706
|
switch (cmd) {
|
|
371
707
|
case "init":
|
|
372
|
-
init(
|
|
708
|
+
init(positionals()[0], flag("template", "vanilla"));
|
|
373
709
|
break;
|
|
374
710
|
case "build":
|
|
375
711
|
build(process.cwd());
|
|
376
712
|
break;
|
|
377
|
-
case "dev":
|
|
378
|
-
|
|
379
|
-
console.log("janela: running (close the window or Ctrl-C to stop)");
|
|
380
|
-
run([bin]);
|
|
713
|
+
case "dev":
|
|
714
|
+
await dev(process.cwd());
|
|
381
715
|
break;
|
|
382
|
-
}
|
|
383
716
|
default:
|
|
384
|
-
console.log(
|
|
717
|
+
console.log(
|
|
718
|
+
"usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
|
|
719
|
+
" janela build | janela dev",
|
|
720
|
+
);
|
|
385
721
|
process.exit(cmd ? 1 : 0);
|
|
386
722
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"jn": "bin/janela.mjs"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"scriptc": "0.0.
|
|
11
|
+
"scriptc": "0.0.35"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Mateus Lage",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"scriptc"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
33
|
+
"node": ">=24"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"bin/",
|