mirinjs 0.1.0-alpha.24 → 0.1.0-alpha.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirinjs",
3
- "version": "0.1.0-alpha.24",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "Build desktop apps with Bun, TypeScript, and Chromium (CEF). The Bun-native Electron alternative.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -183,6 +183,14 @@ export interface MirinConfig {
183
183
  * app worker owns AppKit/CEF (see docs/architecture.md).
184
184
  */
185
185
  workers?: Record<string, string>;
186
+ /**
187
+ * Extra directories bundled verbatim into the app (`Contents/Resources/<name>`
188
+ * on macOS, `resources/<name>` elsewhere), for assets the main process reads
189
+ * at runtime: a bundled server, templates, data files. Maps a filename-safe
190
+ * name to a project-relative directory. Resolve one with `resolveResource(name)`;
191
+ * under `mirin dev` it points at the source directory.
192
+ */
193
+ resources?: Record<string, string>;
186
194
  /**
187
195
  * Custom URL schemes this app registers as a handler for (macOS
188
196
  * `CFBundleURLTypes`), e.g. `["anko"]` so `anko://…` links launch the app (or
package/src/index.ts CHANGED
@@ -62,7 +62,7 @@ export type { MenuItemTemplate, MenuRole } from "./menu.ts";
62
62
  export { menu } from "./menu.ts";
63
63
  export type { NotificationOptions } from "./notification.ts";
64
64
  export { notification } from "./notification.ts";
65
- export { NotAttachedError, resolveWorker } from "./runtime.ts";
65
+ export { NotAttachedError, resolveResource, resolveWorker } from "./runtime.ts";
66
66
  export { globalShortcut } from "./shortcut.ts";
67
67
  export type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
68
68
  export { resolveSidecar, sidecar } from "./sidecar.ts";
package/src/logger.ts CHANGED
@@ -14,7 +14,9 @@
14
14
  * Every emitted line is also recorded to the devtools sink (docs/agent-devtools.md).
15
15
  */
16
16
 
17
+ import { format } from "node:util";
17
18
  import { record } from "./devtools/sink.ts";
19
+ import { writeLine } from "./shared/console.ts";
18
20
  import { firstError, formatArgs, formatStack } from "./shared/format.ts";
19
21
 
20
22
  export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
@@ -97,8 +99,7 @@ export class Logger {
97
99
  const name = this.#scope ? `[mirin:${this.#scope}]` : "[mirin]";
98
100
  const prefix = useColor ? `${COLOR.dim}${name}${COLOR.reset}` : name;
99
101
  const tag = useColor ? `${COLOR[level]}${level}${COLOR.reset}` : level;
100
- const out = level === "warn" || level === "error" ? console.error : console.log;
101
- out(`${prefix} ${tag}`, ...args);
102
+ writeLine(level === "warn" || level === "error" ? 2 : 1, `${prefix} ${tag} ${format(...args)}`);
102
103
  this.#record(level, args);
103
104
  }
104
105
 
package/src/runtime.ts CHANGED
@@ -4,12 +4,13 @@
4
4
  * API lives in app/index.ts and the feature modules, which subscribe here.
5
5
  */
6
6
 
7
- import { join } from "node:path";
7
+ import { isAbsolute, join } from "node:path";
8
8
  import { workerData } from "node:worker_threads";
9
9
  import type { DevtoolsConfig, WindowConfig } from "./config/index.ts";
10
10
  import { parseDevtoolsConfig } from "./devtools/options.ts";
11
11
  import { Core } from "./native.ts";
12
12
  import { RpcServer } from "./rpc-server.ts";
13
+ import { installWorkerConsole } from "./shared/console.ts";
13
14
 
14
15
  export interface ManifestWindowConfig extends WindowConfig {
15
16
  name: string;
@@ -32,6 +33,8 @@ export interface Runtime {
32
33
  sidecarDir?: string;
33
34
  /** Dir holding bundled extra-worker JS (for `resolveWorker`). */
34
35
  workersDir?: string;
36
+ /** Declared resource directories: source paths in dev, bundle-relative names in prod. */
37
+ resources: Record<string, string>;
35
38
  /** `devtools` block from the manifest, resolved by the devtools module. */
36
39
  devtools?: DevtoolsConfig;
37
40
  }
@@ -137,7 +140,11 @@ export function claimBootSlot(): boolean {
137
140
  export function boot(): void {
138
141
  const data = (workerData ?? {}) as {
139
142
  corePath?: string;
140
- manifest?: { windows?: Record<string, WindowConfig>; devtools?: unknown };
143
+ manifest?: {
144
+ windows?: Record<string, WindowConfig>;
145
+ devtools?: unknown;
146
+ resources?: Record<string, string>;
147
+ };
141
148
  id?: string;
142
149
  devUrl?: string;
143
150
  resourcesDir?: string;
@@ -146,6 +153,7 @@ export function boot(): void {
146
153
  };
147
154
  const corePath = data.corePath ?? process.env.MIRIN_CORE;
148
155
  if (!corePath) return; // not under the host; the API stays detached
156
+ installWorkerConsole();
149
157
  if (!claimBootSlot()) {
150
158
  console.error(
151
159
  "[mirin] refusing to boot a second mirinjs copy in this process — the bundle " +
@@ -199,6 +207,7 @@ export function boot(): void {
199
207
  corePath,
200
208
  sidecarDir: data.sidecarDir,
201
209
  workersDir: data.workersDir,
210
+ resources: data.manifest?.resources ?? {},
202
211
  devtools: parseDevtoolsConfig(data.manifest?.devtools),
203
212
  };
204
213
  core.onEvent(dispatch);
@@ -210,6 +219,19 @@ export function boot(): void {
210
219
  * Works in `mirin dev` and the built `.app`. Extra workers run off the main
211
220
  * thread and must not call window/native APIs.
212
221
  */
222
+ /**
223
+ * Resolve a bundled resource directory (declared in `mirin.config.ts` `resources`)
224
+ * to an absolute path. Under `mirin dev` this is the source directory; in a built
225
+ * app it lives under the bundle's resources directory.
226
+ */
227
+ export function resolveResource(name: string): string {
228
+ const entry = current?.resources[name];
229
+ if (!current || entry === undefined) throw new NotAttachedError(`resolveResource("${name}")`);
230
+ if (isAbsolute(entry)) return entry;
231
+ if (!current.resourcesDir) throw new NotAttachedError(`resolveResource("${name}")`);
232
+ return join(current.resourcesDir, entry);
233
+ }
234
+
213
235
  export function resolveWorker(name: string): string {
214
236
  const dir = current?.workersDir;
215
237
  if (!dir) throw new NotAttachedError("resolveWorker");
@@ -0,0 +1,46 @@
1
+ import { writeSync } from "node:fs";
2
+ import { format } from "node:util";
3
+ import { record } from "../devtools/sink.ts";
4
+ import type { DevEventLevel } from "../devtools/types.ts";
5
+ import { firstError, formatArgs, formatStack } from "./format.ts";
6
+
7
+ export type LineWriter = (fd: 1 | 2, line: string) => void;
8
+
9
+ // The app Worker's console normally hands output to the main thread, which sits
10
+ // inside CEF's message loop for the life of the app, so nothing is ever written.
11
+ // A direct fd write from the Worker thread bypasses that.
12
+ export function writeLine(fd: 1 | 2, line: string): void {
13
+ try {
14
+ writeSync(fd, line.endsWith("\n") ? line : `${line}\n`);
15
+ } catch {
16
+ // A closed pipe (parent gone) must not take the app down with a log line.
17
+ }
18
+ }
19
+
20
+ const CONSOLE_LEVELS = {
21
+ log: "info",
22
+ info: "info",
23
+ debug: "debug",
24
+ warn: "warn",
25
+ error: "error",
26
+ } as const satisfies Record<string, DevEventLevel>;
27
+
28
+ export type ConsoleMethod = keyof typeof CONSOLE_LEVELS;
29
+
30
+ /** Route `console.*` from the Worker to fd 1/2 directly and into the devtools sink. */
31
+ export function installWorkerConsole(write: LineWriter = writeLine): void {
32
+ for (const method of Object.keys(CONSOLE_LEVELS) as ConsoleMethod[]) {
33
+ const level = CONSOLE_LEVELS[method];
34
+ console[method] = (...args: unknown[]) => {
35
+ write(level === "warn" || level === "error" ? 2 : 1, format(...args));
36
+ const error = firstError(args);
37
+ record({
38
+ src: "main",
39
+ level,
40
+ type: "console",
41
+ msg: formatArgs(args),
42
+ data: { method, ...(error !== undefined ? { stack: formatStack(error) } : {}) },
43
+ });
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,45 @@
1
+ // Loose semver: "1.2.3", "v1.2.3", "1.2.3-nightly.4". Numeric segments compare
2
+ // as numbers, anything else lexically; a prerelease sorts below its release.
3
+ function parse(version: string): { main: string[]; pre: string[] } {
4
+ const trimmed = version.trim().replace(/^v/i, "");
5
+ const plus = trimmed.indexOf("+");
6
+ const noBuild = plus === -1 ? trimmed : trimmed.slice(0, plus);
7
+ const dash = noBuild.indexOf("-");
8
+ const main = (dash === -1 ? noBuild : noBuild.slice(0, dash)).split(".");
9
+ const pre = dash === -1 ? [] : noBuild.slice(dash + 1).split(".");
10
+ return { main, pre };
11
+ }
12
+
13
+ function compareSegment(a: string | undefined, b: string | undefined): number {
14
+ if (a === undefined && b === undefined) return 0;
15
+ if (a === undefined) return -1;
16
+ if (b === undefined) return 1;
17
+ const na = /^\d+$/.test(a) ? Number(a) : null;
18
+ const nb = /^\d+$/.test(b) ? Number(b) : null;
19
+ if (na !== null && nb !== null) return na === nb ? 0 : na < nb ? -1 : 1;
20
+ if (na !== null) return -1;
21
+ if (nb !== null) return 1;
22
+ return a === b ? 0 : a < b ? -1 : 1;
23
+ }
24
+
25
+ /** Negative when `a` is older than `b`, zero when equal, positive when newer. */
26
+ export function compareVersions(a: string, b: string): number {
27
+ const pa = parse(a);
28
+ const pb = parse(b);
29
+ for (let i = 0; i < Math.max(pa.main.length, pb.main.length); i++) {
30
+ const c = compareSegment(pa.main[i] ?? "0", pb.main[i] ?? "0");
31
+ if (c !== 0) return c;
32
+ }
33
+ if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
34
+ if (pa.pre.length === 0) return 1;
35
+ if (pb.pre.length === 0) return -1;
36
+ for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
37
+ const c = compareSegment(pa.pre[i], pb.pre[i]);
38
+ if (c !== 0) return c;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ export function isNewerVersion(candidate: string, current: string): boolean {
44
+ return compareVersions(candidate, current) > 0;
45
+ }
@@ -25,6 +25,7 @@ import { downloadVerifiedArtifact, verifyFileSha256 } from "./lib/integrity.ts";
25
25
  import { parseManifest } from "./lib/manifest.ts";
26
26
  import { IS_LINUX, IS_MAC, IS_WINDOWS, platformName } from "./lib/platform.ts";
27
27
  import { artifactUrl, assertTrustedUpdateUrl, trustedBaseUrl } from "./lib/urls.ts";
28
+ import { isNewerVersion } from "./lib/version.ts";
28
29
  import type {
29
30
  Listener,
30
31
  Manifest,
@@ -85,7 +86,7 @@ export class Updater {
85
86
  platform: platformName(),
86
87
  arch: process.arch,
87
88
  });
88
- if (!m.version || m.version === v.version) {
89
+ if (!m.version || !isNewerVersion(m.version, v.version)) {
89
90
  this.#manifest = null;
90
91
  this.#setStatus("idle");
91
92
  return null;