@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 +4 -4
- package/src/build.ts +7 -0
- package/src/bundle.ts +72 -1
- package/src/dev.ts +63 -10
- package/src/extras.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
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.
|
|
31
|
-
"mirinjs": "0.0.1-alpha.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
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:
|
|
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
|
+
}
|