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 +1 -1
- package/src/config/manifest.ts +8 -0
- package/src/index.ts +1 -1
- package/src/logger.ts +3 -2
- package/src/runtime.ts +24 -2
- package/src/shared/console.ts +46 -0
- package/src/updater/lib/version.ts +45 -0
- package/src/updater/updater.ts +2 -1
package/package.json
CHANGED
package/src/config/manifest.ts
CHANGED
|
@@ -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
|
-
|
|
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?: {
|
|
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
|
+
}
|
package/src/updater/updater.ts
CHANGED
|
@@ -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
|
|
89
|
+
if (!m.version || !isNewerVersion(m.version, v.version)) {
|
|
89
90
|
this.#manifest = null;
|
|
90
91
|
this.#setStatus("idle");
|
|
91
92
|
return null;
|