@mirinjs/cli 0.0.1-alpha.14 → 0.0.1-alpha.15

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/cli",
3
- "version": "0.0.1-alpha.14",
3
+ "version": "0.0.1-alpha.15",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,11 +27,11 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.0.1-alpha.14",
31
- "mirinjs": "0.0.1-alpha.14"
30
+ "create-mirinjs": "0.0.1-alpha.15",
31
+ "mirinjs": "0.0.1-alpha.15"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.0.1-alpha.14"
34
+ "@mirinjs/darwin-arm64": "0.0.1-alpha.15"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
package/src/build.ts CHANGED
@@ -21,6 +21,7 @@ import { join } from "node:path";
21
21
  import { buildAppBundle } from "./bundle.ts";
22
22
  import { resolveArtifacts } from "./artifacts.ts";
23
23
  import { sweepBuildTemps } from "./temps.ts";
24
+ import { normalizeSidecars, compileWorkers } from "./extras.ts";
24
25
 
25
26
  export interface BuildResult {
26
27
  /** Path to the assembled .app. */
@@ -87,6 +88,10 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
87
88
  await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
88
89
  await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
89
90
 
91
+ // Extra assets: resolve sidecar binaries + compile any extra worker entries.
92
+ const sidecars = normalizeSidecars(projectDir, config.sidecars);
93
+ const extraWorkers = await compileWorkers(projectDir, config.workers, join(work, "workers"), true);
94
+
90
95
  // 5. assemble + sign
91
96
  console.log("[mirin build] assembling .app…");
92
97
  rmSync(join(outDir, `${appName}.app`), { recursive: true, force: true });
@@ -111,6 +116,8 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
111
116
  workerJs,
112
117
  manifestJson: JSON.stringify({ windows: config.windows }),
113
118
  versionJson,
119
+ sidecars,
120
+ workers: extraWorkers,
114
121
  },
115
122
  });
116
123
 
package/src/bundle.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { $ } from "bun";
15
- import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs";
15
+ import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, chmodSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
 
18
18
  const FRAMEWORK = "Chromium Embedded Framework.framework";
@@ -45,9 +45,43 @@ export interface BundleOptions {
45
45
  workerJs?: string; // bundled main-process Worker entry -> Resources/worker.js
46
46
  manifestJson?: string; // serialized manifest -> Resources/mirin.manifest.json
47
47
  versionJson?: string; // serialized version.json -> Resources/version.json (updater)
48
+ /** Bundled sidecar binaries -> Resources/sidecars/<name> (copied + signed). */
49
+ sidecars?: SidecarBundle[];
50
+ /** Compiled extra-worker JS (name -> abs path) -> Resources/workers/<name>.js. */
51
+ workers?: Record<string, string>;
48
52
  };
49
53
  }
50
54
 
55
+ /** A sidecar binary to bundle: logical name, source path, hardened-runtime entitlements. */
56
+ export interface SidecarBundle {
57
+ name: string;
58
+ src: string;
59
+ entitlements: string[];
60
+ }
61
+
62
+ /** Short entitlement name -> the full `com.apple.security.cs.*` key. */
63
+ const SIDECAR_ENTITLEMENT_KEYS: Record<string, string> = {
64
+ "allow-jit": "com.apple.security.cs.allow-jit",
65
+ "allow-unsigned-executable-memory": "com.apple.security.cs.allow-unsigned-executable-memory",
66
+ "disable-library-validation": "com.apple.security.cs.disable-library-validation",
67
+ };
68
+
69
+ function entitlementsPlist(names: string[]): string {
70
+ const keys = names
71
+ .map((n) => SIDECAR_ENTITLEMENT_KEYS[n])
72
+ .filter(Boolean)
73
+ .map((k) => ` <key>${k}</key><true/>`)
74
+ .join("\n");
75
+ return `<?xml version="1.0" encoding="UTF-8"?>
76
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
77
+ <plist version="1.0">
78
+ <dict>
79
+ ${keys}
80
+ </dict>
81
+ </plist>
82
+ `;
83
+ }
84
+
51
85
  /** The 10 standard iconset renditions (point size + @1x/@2x pixel size). */
52
86
  const ICONSET_RENDITIONS = [
53
87
  { name: "icon_16x16.png", px: 16 },
@@ -182,6 +216,28 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
182
216
  if (opts.resources?.versionJson != null) {
183
217
  writeFileSync(join(resources, "version.json"), opts.resources.versionJson);
184
218
  }
219
+ // Extra Bun Worker bundles -> Resources/workers/<name>.js (resolveWorker()).
220
+ if (opts.resources?.workers && Object.keys(opts.resources.workers).length) {
221
+ const workersDir = join(resources, "workers");
222
+ mkdirSync(workersDir, { recursive: true });
223
+ for (const [name, src] of Object.entries(opts.resources.workers)) {
224
+ cpSync(src, join(workersDir, `${name}.js`));
225
+ }
226
+ }
227
+ // Sidecar binaries -> Resources/sidecars/<name> (chmod +x; signed below). Paths
228
+ // collected here so the codesign loop can sign them inside-out with the app.
229
+ const sidecarDests: SidecarBundle[] = [];
230
+ if (opts.resources?.sidecars?.length) {
231
+ const sidecarsDir = join(resources, "sidecars");
232
+ mkdirSync(sidecarsDir, { recursive: true });
233
+ for (const sc of opts.resources.sidecars) {
234
+ if (!existsSync(sc.src)) throw new Error(`sidecar "${sc.name}" not found: ${sc.src}`);
235
+ const dest = join(sidecarsDir, sc.name);
236
+ cpSync(sc.src, dest);
237
+ chmodSync(dest, 0o755);
238
+ sidecarDests.push({ name: sc.name, src: dest, entitlements: sc.entitlements });
239
+ }
240
+ }
185
241
 
186
242
  for (const { suffix, id } of HELPER_TYPES) {
187
243
  const name = `${appName} Helper${suffix}`;
@@ -249,6 +305,18 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
249
305
  await sign(cef);
250
306
  // 3. our FFI core dylib.
251
307
  await sign(join(macos, "libmirin_core.dylib"));
308
+ // 3b. sidecars: hardened runtime + timestamp; per-binary entitlements only
309
+ // when the spec asks (most CLIs need none — over-entitling is a smell).
310
+ for (const sc of sidecarDests) {
311
+ if (sc.entitlements.length) {
312
+ const ent = join(opts.outDir, `_sidecar_${sc.name}.plist`);
313
+ writeFileSync(ent, entitlementsPlist(sc.entitlements));
314
+ await $`codesign --force --timestamp --options runtime --entitlements ${ent} --sign ${identity} ${sc.src}`.quiet();
315
+ rmSync(ent, { force: true });
316
+ } else {
317
+ await sign(sc.src);
318
+ }
319
+ }
252
320
  // 4. each helper: the inner executable, then the .app wrapper (entitlements
253
321
  // on both — the renderer/GPU helpers are what actually JIT).
254
322
  for (const { suffix } of HELPER_TYPES) {
@@ -263,6 +331,9 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
263
331
  } else {
264
332
  // Ad-hoc: enough to launch locally; not distributable or notarizable.
265
333
  await $`codesign --force --sign ${identity} ${cef}`.quiet();
334
+ for (const sc of sidecarDests) {
335
+ await $`codesign --force --sign ${identity} ${sc.src}`.quiet();
336
+ }
266
337
  for (const { suffix } of HELPER_TYPES) {
267
338
  await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
268
339
  }
package/src/dev.ts CHANGED
@@ -9,13 +9,17 @@
9
9
  */
10
10
 
11
11
  import { $ } from "bun";
12
- import { mkdirSync } from "node:fs";
12
+ import { mkdirSync, rmSync, symlinkSync, existsSync } from "node:fs";
13
+ import { createServer } from "node:net";
13
14
  import { join } from "node:path";
14
15
  import { buildAppBundle } from "./bundle.ts";
15
16
  import { resolveArtifacts } from "./artifacts.ts";
16
17
  import { sweepBuildTemps } from "./temps.ts";
18
+ import { normalizeSidecars, compileWorkers } from "./extras.ts";
17
19
 
18
- const DEV_URL = "http://localhost:5173";
20
+ /** Vite's default port. `mirin dev` probes upward from here for a free one, so a
21
+ * second dev session (or anything already on 5173) doesn't collide. */
22
+ const DEV_PORT_BASE = 5173;
19
23
 
20
24
  export async function dev(projectDir = process.cwd()): Promise<number> {
21
25
  const work = join(projectDir, ".mirin");
@@ -40,6 +44,19 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
40
44
  await $`bun build --compile ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
41
45
  await $`bun build ${mainEntry} --target=bun --outfile ${workerJs}`.cwd(projectDir);
42
46
 
47
+ // Extra assets (dev): compile workers into .mirin/workers and symlink sidecar
48
+ // binaries into .mirin/sidecars (no copy/sign in dev — they run unsigned locally).
49
+ const workersDir = join(work, "workers");
50
+ await compileWorkers(projectDir, config.workers, workersDir, false);
51
+ const sidecarsDir = join(work, "sidecars");
52
+ mkdirSync(sidecarsDir, { recursive: true });
53
+ for (const sc of normalizeSidecars(projectDir, config.sidecars)) {
54
+ const link = join(sidecarsDir, sc.name);
55
+ rmSync(link, { force: true });
56
+ if (existsSync(sc.src)) symlinkSync(sc.src, link);
57
+ else console.warn(`[mirin dev] sidecar "${sc.name}" not found: ${sc.src}`);
58
+ }
59
+
43
60
  // --- assemble the dev .app ---
44
61
  console.log("[mirin dev] assembling dev bundle…");
45
62
  const { app, exe } = await buildAppBundle({
@@ -53,13 +70,21 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
53
70
  icon: config.icon ? join(projectDir, config.icon) : undefined,
54
71
  });
55
72
 
56
- // --- start Vite ---
57
- console.log("[mirin dev] starting Vite dev server…");
58
- const vite = Bun.spawn(["bunx", "vite", "--clearScreen", "false"], {
59
- cwd: projectDir,
60
- stdio: ["ignore", "inherit", "inherit"],
61
- });
62
- await waitForUrl(DEV_URL, 15_000);
73
+ // --- start Vite on a free port so concurrent dev sessions don't collide ---
74
+ // `--port <free> --strictPort` pins Vite to the port we probed (overriding any
75
+ // port/strictPort in the app's vite.config) and passes the real URL to the app,
76
+ // so the host and Vite never disagree about which port is in use.
77
+ const port = await findFreePort(DEV_PORT_BASE);
78
+ const devUrl = `http://localhost:${port}`;
79
+ console.log(`[mirin dev] starting Vite dev server on ${devUrl}…`);
80
+ const vite = Bun.spawn(
81
+ ["bunx", "vite", "--clearScreen", "false", "--port", String(port), "--strictPort"],
82
+ {
83
+ cwd: projectDir,
84
+ stdio: ["ignore", "inherit", "inherit"],
85
+ },
86
+ );
87
+ await waitForUrl(devUrl, 15_000);
63
88
 
64
89
  // --- launch the app ---
65
90
  console.log(`[mirin dev] launching ${appName}…`);
@@ -69,9 +94,11 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
69
94
  ...process.env,
70
95
  MIRIN_CORE: join(app, "Contents", "MacOS", "libmirin_core.dylib"),
71
96
  MIRIN_WORKER: workerJs,
72
- MIRIN_DEV_URL: DEV_URL,
97
+ MIRIN_DEV_URL: devUrl,
73
98
  MIRIN_MANIFEST_JSON: JSON.stringify({ windows: config.windows }),
74
99
  MIRIN_CONFIG_JSON: "{}",
100
+ MIRIN_SIDECAR_DIR: sidecarsDir,
101
+ MIRIN_WORKERS_DIR: workersDir,
75
102
  },
76
103
  stdio: ["ignore", "inherit", "inherit"],
77
104
  });
@@ -88,6 +115,32 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
88
115
  return code;
89
116
  }
90
117
 
118
+ /** First free TCP port at or above `start` on loopback. */
119
+ async function findFreePort(start: number): Promise<number> {
120
+ for (let port = start; port < start + 100; port++) {
121
+ if (await isPortFree(port)) return port;
122
+ }
123
+ throw new Error(`[mirin dev] no free port in ${start}–${start + 99}`);
124
+ }
125
+
126
+ /** Free only if neither IPv4 nor IPv6 loopback reports the port in use — Vite
127
+ * binds `localhost` (often `::1`), so a v4-only probe would miss an IPv6 holder. */
128
+ async function isPortFree(port: number): Promise<boolean> {
129
+ const [v4, v6] = await Promise.all([portInUse(port, "127.0.0.1"), portInUse(port, "::1")]);
130
+ return !v4 && !v6;
131
+ }
132
+
133
+ function portInUse(port: number, host: string): Promise<boolean> {
134
+ return new Promise((resolve) => {
135
+ const srv = createServer();
136
+ // Only EADDRINUSE means taken; other errors (e.g. EADDRNOTAVAIL when IPv6 is
137
+ // off) just mean we can't test that family — treat as not-in-use.
138
+ srv.once("error", (err: NodeJS.ErrnoException) => resolve(err.code === "EADDRINUSE"));
139
+ srv.once("listening", () => srv.close(() => resolve(false)));
140
+ srv.listen(port, host);
141
+ });
142
+ }
143
+
91
144
  async function waitForUrl(url: string, timeoutMs: number): Promise<void> {
92
145
  const deadline = Date.now() + timeoutMs;
93
146
  while (Date.now() < deadline) {
package/src/extras.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared helpers for the two "extra asset" config blocks — `sidecars` (bundled
3
+ * binaries) and `workers` (extra Bun Worker entries) — used by both `mirin build`
4
+ * and `mirin dev`. Keeps the config normalization + worker compilation in one
5
+ * place so the two CLI paths stay in sync.
6
+ */
7
+
8
+ import { $ } from "bun";
9
+ import { join } from "node:path";
10
+
11
+ /** Config shapes (mirrors packages/mirin/src/config.ts; CLI can't import the runtime). */
12
+ export type SidecarConfig = Record<string, string | { bin: string; entitlements?: string[] }>;
13
+ export type WorkersConfig = Record<string, string>;
14
+
15
+ /** A sidecar resolved to an absolute source path + its requested entitlements. */
16
+ export interface NormalizedSidecar {
17
+ name: string;
18
+ /** Absolute path to the source binary. */
19
+ src: string;
20
+ /** Short entitlement names (e.g. "allow-jit"); empty for most CLIs. */
21
+ entitlements: string[];
22
+ }
23
+
24
+ /** Resolve `config.sidecars` to absolute source paths. */
25
+ export function normalizeSidecars(
26
+ projectDir: string,
27
+ sidecars: SidecarConfig | undefined,
28
+ ): NormalizedSidecar[] {
29
+ return Object.entries(sidecars ?? {}).map(([name, value]) => {
30
+ const spec = typeof value === "string" ? { bin: value } : value;
31
+ return {
32
+ name,
33
+ src: join(projectDir, spec.bin),
34
+ entitlements: spec.entitlements ?? [],
35
+ };
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Compile each extra-worker entry to `<outDir>/<name>.js`. Returns name → path.
41
+ * `minify` matches the surrounding build (prod minifies; dev doesn't).
42
+ */
43
+ export async function compileWorkers(
44
+ projectDir: string,
45
+ workers: WorkersConfig | undefined,
46
+ outDir: string,
47
+ minify: boolean,
48
+ ): Promise<Record<string, string>> {
49
+ const out: Record<string, string> = {};
50
+ for (const [name, entry] of Object.entries(workers ?? {})) {
51
+ const js = join(outDir, `${name}.js`);
52
+ const src = join(projectDir, entry);
53
+ if (minify) {
54
+ await $`bun build ${src} --target=bun --minify --outfile ${js}`.cwd(projectDir);
55
+ } else {
56
+ await $`bun build ${src} --target=bun --outfile ${js}`.cwd(projectDir);
57
+ }
58
+ out[name] = js;
59
+ }
60
+ return out;
61
+ }