@mirinjs/cli 0.1.0-alpha.14 → 0.1.0-alpha.16

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.
@@ -0,0 +1,91 @@
1
+ import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const CEF_LOCALE = /^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/;
5
+ const MAC_VARIANTS = new Set(["FEMININE", "MASCULINE", "NEUTER"]);
6
+
7
+ /** Validate and normalize an optional CEF locale allowlist. */
8
+ export function normalizeCefLocales(locales: unknown): string[] | undefined {
9
+ if (locales == null) return undefined;
10
+ if (!Array.isArray(locales) || locales.length === 0) {
11
+ throw new Error("[mirin] cef.locales must contain at least one locale.");
12
+ }
13
+
14
+ const seen = new Set<string>();
15
+ const normalized: string[] = [];
16
+ for (const value of locales) {
17
+ if (typeof value !== "string" || !CEF_LOCALE.test(value)) {
18
+ throw new Error(`[mirin] invalid CEF locale ${JSON.stringify(value)}.`);
19
+ }
20
+ const locale = value.replaceAll("_", "-");
21
+ const key = locale.toLowerCase();
22
+ if (!seen.has(key)) {
23
+ seen.add(key);
24
+ normalized.push(locale);
25
+ }
26
+ }
27
+ return normalized;
28
+ }
29
+
30
+ /** Copy selected Windows/Linux `locales/<locale>.pak` files, or every locale when unset. */
31
+ export function copyFlatCefLocales(
32
+ source: string,
33
+ destination: string,
34
+ locales: string[] | undefined,
35
+ ): void {
36
+ if (!locales) {
37
+ cpSync(source, destination, { recursive: true });
38
+ return;
39
+ }
40
+
41
+ const entries = readdirSync(source, { withFileTypes: true }).filter(
42
+ (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".pak"),
43
+ );
44
+ const byLocale = new Map(
45
+ entries.map((entry) => [localeKey(entry.name.slice(0, -".pak".length)), entry.name]),
46
+ );
47
+
48
+ mkdirSync(destination, { recursive: true });
49
+ for (const locale of locales) {
50
+ const file = byLocale.get(localeKey(locale));
51
+ if (!file) throw new Error(`[mirin] CEF locale "${locale}" is not available in ${source}.`);
52
+ cpSync(join(source, file), join(destination, file));
53
+ }
54
+ }
55
+
56
+ /** Remove unselected macOS `.lproj` locale directories from a copied CEF framework. */
57
+ export function pruneMacCefLocales(framework: string, locales: string[] | undefined): void {
58
+ if (!locales) return;
59
+
60
+ const resources = join(framework, "Resources");
61
+ const localeDirs = readdirSync(resources, { withFileTypes: true })
62
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith(".lproj"))
63
+ .map((entry) => entry.name);
64
+ const available = new Set(localeDirs.map((name) => name.slice(0, -".lproj".length)));
65
+ const keep = new Set<string>();
66
+
67
+ for (const locale of locales) {
68
+ const requested = locale.replaceAll("-", "_");
69
+ const language = requested.split("_", 1)[0] ?? requested;
70
+ const root = available.has(requested)
71
+ ? requested
72
+ : available.has(language)
73
+ ? language
74
+ : undefined;
75
+ if (!root) throw new Error(`[mirin] CEF locale "${locale}" is not available in ${resources}.`);
76
+
77
+ keep.add(`${root}.lproj`);
78
+ for (const variant of MAC_VARIANTS) {
79
+ const name = `${root}_${variant}.lproj`;
80
+ if (localeDirs.includes(name)) keep.add(name);
81
+ }
82
+ }
83
+
84
+ for (const directory of localeDirs) {
85
+ if (!keep.has(directory)) rmSync(join(resources, directory), { recursive: true, force: true });
86
+ }
87
+ }
88
+
89
+ function localeKey(locale: string): string {
90
+ return locale.replaceAll("_", "-").toLowerCase();
91
+ }
@@ -15,9 +15,10 @@
15
15
  * what mirin-core derives at runtime (engine::derive_subprocess_path, host.ts).
16
16
  */
17
17
 
18
- import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs";
19
- import { join, extname } from "node:path";
20
- import { makeWindowsIcon } from "./icon-win.ts";
18
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
19
+ import { extname, join } from "node:path";
20
+ import { makeWindowsIcon } from "../../icons/windows/index.ts";
21
+ import { copyFlatCefLocales } from "../shared/cef-locales.ts";
21
22
 
22
23
  /** Source dir (vendor/cef on Windows) must contain libcef.dll. */
23
24
  const CEF_MARKER = "libcef.dll";
@@ -26,7 +27,7 @@ const CEF_MARKER = "libcef.dll";
26
27
  * distribution (libcef.lib, include/, cmake/, archive.json, bootstrap*.exe) is
27
28
  * build-time only and intentionally left out. */
28
29
  const CEF_RUNTIME_EXTS = new Set([".dll", ".bin", ".pak", ".dat"]);
29
- const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json", "locales"];
30
+ const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json"];
30
31
 
31
32
  export interface WinBundleOptions {
32
33
  appName: string; // also the host exe stem → <appName>.exe
@@ -35,6 +36,8 @@ export interface WinBundleOptions {
35
36
  coreDll: string; // mirin_core.dll
36
37
  helperExe: string; // mirin-helper.exe
37
38
  cefPath: string; // dir containing the Windows CEF runtime (vendor/cef)
39
+ /** Production CEF locale allowlist. Undefined keeps every locale. */
40
+ cefLocales?: string[];
38
41
  /** App icon source (.ico / .iconset / .png) → <App>/icon.ico, set as the window icon. */
39
42
  icon?: string;
40
43
  /** Production-only resources placed under <App>/resources. */
@@ -66,7 +69,7 @@ export async function buildWindowsBundle(
66
69
  cpSync(opts.coreDll, join(app, "mirin_core.dll"));
67
70
  cpSync(opts.helperExe, join(app, "mirin-helper.exe"));
68
71
 
69
- copyCefRuntime(cefPath, app);
72
+ copyCefRuntime(cefPath, app, opts.cefLocales);
70
73
 
71
74
  // App icon → <App>/icon.ico, loaded at runtime by the core and set as the window
72
75
  // icon (taskbar / Alt-Tab / title). Sits beside the exe so the core finds it.
@@ -105,7 +108,7 @@ export async function buildWindowsBundle(
105
108
  }
106
109
 
107
110
  /** Copy the CEF runtime (dlls, paks, snapshot, icu data, locales) into `dest`. */
108
- function copyCefRuntime(cefPath: string, dest: string): void {
111
+ function copyCefRuntime(cefPath: string, dest: string, locales: string[] | undefined): void {
109
112
  for (const entry of readdirSync(cefPath)) {
110
113
  if (CEF_RUNTIME_EXTS.has(extname(entry).toLowerCase())) {
111
114
  cpSync(join(cefPath, entry), join(dest, entry));
@@ -115,4 +118,5 @@ function copyCefRuntime(cefPath: string, dest: string): void {
115
118
  const src = join(cefPath, extra);
116
119
  if (existsSync(src)) cpSync(src, join(dest, extra), { recursive: true });
117
120
  }
121
+ copyFlatCefLocales(join(cefPath, "locales"), join(dest, "locales"), locales);
118
122
  }
@@ -0,0 +1 @@
1
+ export * from "./app.ts";
package/src/dev.ts CHANGED
@@ -8,25 +8,26 @@
8
8
  * 5. launch the app pointed at the Vite URL, with RPC injected into the webview
9
9
  */
10
10
 
11
- import { $ } from "bun";
12
- import { mkdirSync, rmSync, symlinkSync, existsSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
13
12
  import { createServer } from "node:net";
13
+ import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
- import { buildAppBundle } from "./bundle.ts";
16
- import { buildWindowsBundle } from "./bundle-win.ts";
15
+ import { $ } from "bun";
16
+ import { resolveArtifacts } from "./artifacts.ts";
17
17
  import {
18
18
  buildLinuxBundle,
19
- resolveLinuxIconPng,
20
- resolveLinuxDesktopIconPng,
21
19
  desktopEntry,
22
- } from "./bundle-linux.ts";
23
- import { homedir } from "node:os";
24
- import { resolveArtifacts } from "./artifacts.ts";
20
+ resolveLinuxDesktopIconPng,
21
+ resolveLinuxIconPng,
22
+ } from "./bundle/linux/index.ts";
23
+ import { buildAppBundle } from "./bundle/macos/index.ts";
24
+ import { buildWindowsBundle } from "./bundle/windows/index.ts";
25
25
 
26
26
  const IS_WINDOWS = process.platform === "win32";
27
27
  const IS_LINUX = process.platform === "linux";
28
+
29
+ import { compileWorkers, normalizeSidecars } from "./extras.ts";
28
30
  import { sweepBuildTemps } from "./temps.ts";
29
- import { normalizeSidecars, compileWorkers } from "./extras.ts";
30
31
 
31
32
  /** Vite's default port. `mirin dev` probes upward from here for a free one, so a
32
33
  * second dev session (or anything already on 5173) doesn't collide. */
@@ -87,26 +88,26 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
87
88
  icon: iconSrc,
88
89
  })
89
90
  : IS_LINUX
90
- ? await buildLinuxBundle({
91
- appName,
92
- outDir: work,
93
- hostExe,
94
- coreDll: artifacts.coreDylib,
95
- helperExe: artifacts.helperBin,
96
- cefPath: artifacts.cefPath,
97
- icon: iconSrc,
98
- })
99
- : await buildAppBundle({
100
- appName,
101
- bundleId,
102
- outDir: work,
103
- hostExe,
104
- coreDylib: artifacts.coreDylib,
105
- helperBin: artifacts.helperBin,
106
- cefPath: artifacts.cefPath,
107
- icon: iconSrc,
108
- urlSchemes: config.urlSchemes,
109
- });
91
+ ? await buildLinuxBundle({
92
+ appName,
93
+ outDir: work,
94
+ hostExe,
95
+ coreDll: artifacts.coreDylib,
96
+ helperExe: artifacts.helperBin,
97
+ cefPath: artifacts.cefPath,
98
+ icon: iconSrc,
99
+ })
100
+ : await buildAppBundle({
101
+ appName,
102
+ bundleId,
103
+ outDir: work,
104
+ hostExe,
105
+ coreDylib: artifacts.coreDylib,
106
+ helperBin: artifacts.helperBin,
107
+ cefPath: artifacts.cefPath,
108
+ icon: iconSrc,
109
+ urlSchemes: config.urlSchemes,
110
+ });
110
111
 
111
112
  // Linux: install a dev `.desktop` entry so cosmic's dock can resolve the app's
112
113
  // icon. cosmic matches a running X11 window's `WM_CLASS` (which the core sets to
@@ -138,7 +139,17 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
138
139
  const devUrl = `http://127.0.0.1:${port}`;
139
140
  console.log(`[mirin dev] starting Vite dev server on ${devUrl}…`);
140
141
  const vite = Bun.spawn(
141
- ["bunx", "vite", "--clearScreen", "false", "--host", "127.0.0.1", "--port", String(port), "--strictPort"],
142
+ [
143
+ "bunx",
144
+ "vite",
145
+ "--clearScreen",
146
+ "false",
147
+ "--host",
148
+ "127.0.0.1",
149
+ "--port",
150
+ String(port),
151
+ "--strictPort",
152
+ ],
142
153
  {
143
154
  cwd: projectDir,
144
155
  stdio: ["ignore", "inherit", "inherit"],
@@ -155,13 +166,17 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
155
166
  MIRIN_CORE: IS_WINDOWS
156
167
  ? join(app, "mirin_core.dll")
157
168
  : IS_LINUX
158
- ? join(app, "libmirin_core.so")
159
- : join(app, "Contents", "MacOS", "libmirin_core.dylib"),
169
+ ? join(app, "libmirin_core.so")
170
+ : join(app, "Contents", "MacOS", "libmirin_core.dylib"),
160
171
  // Linux: the flat app dir holds libcef.so + libmirin_core.so; put it on the
161
172
  // loader path so the host's dlopen and libmirin_core's NEEDED libcef.so (and
162
173
  // the spawned mirin-helper) all resolve. (macOS uses the framework loader;
163
174
  // Windows the exe-dir search — neither needs this.)
164
- ...(IS_LINUX ? { LD_LIBRARY_PATH: `${app}${process.env.LD_LIBRARY_PATH ? ":" + process.env.LD_LIBRARY_PATH : ""}` } : {}),
175
+ ...(IS_LINUX
176
+ ? {
177
+ LD_LIBRARY_PATH: `${app}${process.env.LD_LIBRARY_PATH ? ":" + process.env.LD_LIBRARY_PATH : ""}`,
178
+ }
179
+ : {}),
165
180
  MIRIN_WORKER: workerJs,
166
181
  MIRIN_DEV_URL: devUrl,
167
182
  MIRIN_MANIFEST_JSON: JSON.stringify({ id: bundleId, windows: config.windows }),
package/src/dmg.ts CHANGED
@@ -15,10 +15,10 @@
15
15
  * stapled so a fresh download opens without a Gatekeeper prompt.
16
16
  */
17
17
 
18
- import { $ } from "bun";
19
- import { cpSync, mkdirSync, rmSync, symlinkSync, existsSync } from "node:fs";
20
- import { join, basename, isAbsolute } from "node:path";
18
+ import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
21
19
  import { tmpdir } from "node:os";
20
+ import { basename, isAbsolute, join } from "node:path";
21
+ import { $ } from "bun";
22
22
 
23
23
  export interface DmgOptions {
24
24
  volumeName?: string;
@@ -148,12 +148,20 @@ export async function buildDmg(input: BuildDmgInput): Promise<string> {
148
148
  let built = false;
149
149
  if (wantsLayout(options)) {
150
150
  try {
151
- await buildLaidOutDmg({ staging, vol, format, dmgPath, options, projectDir, appFile: basename(app) });
151
+ await buildLaidOutDmg({
152
+ staging,
153
+ vol,
154
+ format,
155
+ dmgPath,
156
+ options,
157
+ projectDir,
158
+ appFile: basename(app),
159
+ });
152
160
  built = true;
153
161
  } catch (e) {
154
162
  console.warn(
155
163
  `[mirin release] DMG layout failed (${e instanceof Error ? e.message : e}); ` +
156
- `falling back to a plain DMG.`,
164
+ "falling back to a plain DMG.",
157
165
  );
158
166
  rmSync(dmgPath, { force: true });
159
167
  }
@@ -190,9 +198,14 @@ async function buildLaidOutDmg(args: {
190
198
  const { staging, vol, format, dmgPath, options, projectDir, appFile } = args;
191
199
  const win = options.windowSize ?? { width: 640, height: 400 };
192
200
  const iconSize = options.iconSize ?? 128;
193
- const appPos = options.appPosition ?? { x: Math.round(win.width * 0.25), y: Math.round(win.height * 0.5) };
194
- const appsPos =
195
- options.applicationsPosition ?? { x: Math.round(win.width * 0.75), y: Math.round(win.height * 0.5) };
201
+ const appPos = options.appPosition ?? {
202
+ x: Math.round(win.width * 0.25),
203
+ y: Math.round(win.height * 0.5),
204
+ };
205
+ const appsPos = options.applicationsPosition ?? {
206
+ x: Math.round(win.width * 0.75),
207
+ y: Math.round(win.height * 0.5),
208
+ };
196
209
 
197
210
  // Stage a background image (if any) under a hidden folder Finder can reference.
198
211
  let bgFile: string | undefined;
@@ -214,8 +227,7 @@ async function buildLaidOutDmg(args: {
214
227
  await $`hdiutil create -volname ${vol} -srcfolder ${staging} -fs HFS+ -format UDRW -ov ${rw}`.quiet();
215
228
 
216
229
  // Attach without auto-opening a Finder window; parse the real mountpoint.
217
- const attach =
218
- await $`hdiutil attach ${rw} -readwrite -noverify -noautoopen`.text();
230
+ const attach = await $`hdiutil attach ${rw} -readwrite -noverify -noautoopen`.text();
219
231
  const mount = attach
220
232
  .split("\n")
221
233
  .map((l) => l.trim())
package/src/extras.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  * place so the two CLI paths stay in sync.
6
6
  */
7
7
 
8
+ import { mkdirSync } from "node:fs";
9
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
8
10
  import { $ } from "bun";
9
- import { join } from "node:path";
10
11
 
11
- /** Config shapes (mirrors packages/mirin/src/config.ts; CLI can't import the runtime). */
12
+ /** Config shapes (mirrors packages/mirin/src/config/index.ts; CLI can't import the runtime). */
12
13
  export type SidecarConfig = Record<string, string | { bin: string; entitlements?: string[] }>;
13
14
  export type WorkersConfig = Record<string, string>;
14
15
 
@@ -21,16 +22,47 @@ export interface NormalizedSidecar {
21
22
  entitlements: string[];
22
23
  }
23
24
 
25
+ const SAFE_EXTRA_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
26
+
27
+ /**
28
+ * Extra asset names become bundle filenames, so keep them as path segments. This
29
+ * prevents config keys such as "../tool" from escaping resources/sidecars or
30
+ * resources/workers in dev and production bundles.
31
+ */
32
+ export function safeExtraAssetName(name: string, label: string): string {
33
+ if (!SAFE_EXTRA_NAME.test(name) || name === "." || name === "..") {
34
+ throw new Error(
35
+ `[mirin] invalid ${label} "${name}" — use letters, digits, ".", "_" or "-", ` +
36
+ "and do not include path separators.",
37
+ );
38
+ }
39
+ return name;
40
+ }
41
+
42
+ function projectRelativePath(projectDir: string, file: string, label: string): string {
43
+ if (isAbsolute(file)) {
44
+ throw new Error(`[mirin] ${label} must be relative to the project root: ${file}`);
45
+ }
46
+ const root = resolve(projectDir);
47
+ const full = resolve(root, file);
48
+ const rel = relative(root, full);
49
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
50
+ throw new Error(`[mirin] ${label} escapes the project root: ${file}`);
51
+ }
52
+ return full;
53
+ }
54
+
24
55
  /** Resolve `config.sidecars` to absolute source paths. */
25
56
  export function normalizeSidecars(
26
57
  projectDir: string,
27
58
  sidecars: SidecarConfig | undefined,
28
59
  ): NormalizedSidecar[] {
29
60
  return Object.entries(sidecars ?? {}).map(([name, value]) => {
61
+ const safeName = safeExtraAssetName(name, "sidecar name");
30
62
  const spec = typeof value === "string" ? { bin: value } : value;
31
63
  return {
32
- name,
33
- src: join(projectDir, spec.bin),
64
+ name: safeName,
65
+ src: projectRelativePath(projectDir, spec.bin, `sidecar "${safeName}" path`),
34
66
  entitlements: spec.entitlements ?? [],
35
67
  };
36
68
  });
@@ -46,16 +78,21 @@ export async function compileWorkers(
46
78
  outDir: string,
47
79
  minify: boolean,
48
80
  ): Promise<Record<string, string>> {
81
+ mkdirSync(outDir, { recursive: true });
49
82
  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);
83
+ const compiled = Object.entries(workers ?? {}).map(async ([name, entry]) => {
84
+ const safeName = safeExtraAssetName(name, "worker name");
85
+ const js = join(outDir, `${safeName}.js`);
86
+ const src = projectRelativePath(projectDir, entry, `worker "${safeName}" entry`);
53
87
  if (minify) {
54
88
  await $`bun build ${src} --target=bun --minify --outfile ${js}`.cwd(projectDir);
55
89
  } else {
56
90
  await $`bun build ${src} --target=bun --outfile ${js}`.cwd(projectDir);
57
91
  }
58
- out[name] = js;
92
+ return { name: safeName, js };
93
+ });
94
+ for (const worker of await Promise.all(compiled)) {
95
+ out[worker.name] = worker.js;
59
96
  }
60
97
  return out;
61
98
  }
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
11
- import { inflateSync } from "node:zlib";
12
11
  import { join } from "node:path";
12
+ import { inflateSync } from "node:zlib";
13
13
 
14
14
  /** Standard Windows icon sizes; intermediate sizes are downscaled from these by
15
15
  * the shell, so the 256 entry keeps large views crisp. */
@@ -57,7 +57,8 @@ function decodePng(buf: Buffer): { width: number; height: number; rgba: Buffer }
57
57
  if (bitDepth !== 8 || interlace !== 0) {
58
58
  throw new Error(`unsupported PNG (depth ${bitDepth}, interlace ${interlace})`);
59
59
  }
60
- const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
60
+ const channels =
61
+ colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
61
62
  if (!channels) throw new Error(`unsupported color type ${colorType}`);
62
63
 
63
64
  const raw = inflateSync(Buffer.concat(idat));
@@ -239,7 +240,9 @@ export function makeWindowsIcon(
239
240
  entries.push({ size: width, data: dibEntry(width, rgba), png: false });
240
241
  }
241
242
  } catch (e) {
242
- console.warn(`[mirin] icon: cannot decode ${iconSrc} (${e instanceof Error ? e.message : e})`);
243
+ console.warn(
244
+ `[mirin] icon: cannot decode ${iconSrc} (${e instanceof Error ? e.message : e})`,
245
+ );
243
246
  }
244
247
  }
245
248
 
@@ -0,0 +1 @@
1
+ export * from "./icon.ts";
package/src/index.ts CHANGED
@@ -4,14 +4,17 @@
4
4
  */
5
5
 
6
6
  import { resolve } from "node:path";
7
- import { dev } from "./dev.ts";
7
+ import { scaffold } from "create-mirinjs";
8
8
  import { build } from "./build.ts";
9
+ import { dev } from "./dev.ts";
10
+ import { parseLinuxFormats } from "./package/linux/index.ts";
9
11
  import { release } from "./release.ts";
10
- import { scaffold } from "create-mirinjs";
11
- import type { LinuxPackageFormat } from "mirinjs";
12
12
 
13
13
  /** Minimal flag parser: `--k=v`, `--k v`, and boolean `--k`; the rest are positionals. */
14
- function parseArgs(argv: string[]): { positionals: string[]; opts: Record<string, string | boolean> } {
14
+ function parseArgs(argv: string[]): {
15
+ positionals: string[];
16
+ opts: Record<string, string | boolean>;
17
+ } {
15
18
  const opts: Record<string, string | boolean> = {};
16
19
  const positionals: string[] = [];
17
20
  for (let i = 0; i < argv.length; i++) {
@@ -65,9 +68,7 @@ switch (command) {
65
68
  case "build": {
66
69
  const version = typeof opts.version === "string" ? opts.version : undefined;
67
70
  const target = typeof opts["linux-target"] === "string" ? opts["linux-target"] : undefined;
68
- const linuxFormats = target
69
- ? (target.split(",").map((s) => s.trim()).filter(Boolean) as LinuxPackageFormat[])
70
- : undefined;
71
+ const linuxFormats = target ? parseLinuxFormats(target) : undefined;
71
72
  const packageLinux = opts.linux === true || linuxFormats != null;
72
73
  await build(process.cwd(), { version, packageLinux, linuxFormats });
73
74
  process.exit(0);
@@ -10,11 +10,11 @@
10
10
  * back to NSIS / the portable `.zip` when it's absent.
11
11
  */
12
12
 
13
- import { $ } from "bun";
14
13
  import { existsSync, rmSync, writeFileSync } from "node:fs";
15
- import { join, isAbsolute } from "node:path";
14
+ import { isAbsolute, join } from "node:path";
15
+ import { $ } from "bun";
16
16
  import type { InnoConfig } from "mirinjs";
17
- import { makeWindowsIcon } from "./icon-win.ts";
17
+ import { makeWindowsIcon } from "./icons/windows/index.ts";
18
18
 
19
19
  export interface BuildInnoInput {
20
20
  appDir: string; // the assembled app folder (build/<App>)
@@ -39,7 +39,8 @@ const v = (s: string) => s.replace(/\{/g, "{{");
39
39
 
40
40
  /** Build the installer with `iscc`, returning the setup.exe path. */
41
41
  export async function buildInnoInstaller(input: BuildInnoInput): Promise<string> {
42
- const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } = input;
42
+ const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
43
+ input;
43
44
  const out = join(outDir, fileName);
44
45
  rmSync(out, { force: true });
45
46
  if (!fileName.endsWith(".exe")) throw new Error("inno: fileName must end with .exe");
@@ -53,7 +54,8 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
53
54
  const changeDir = options.allowChangeInstallDir !== false && !minimal;
54
55
  const publisher = options.publisher ?? appName;
55
56
  const installDir =
56
- options.installDir ?? (perMachine ? "{autopf}\\" + appName : "{localappdata}\\Programs\\" + appName);
57
+ options.installDir ??
58
+ (perMachine ? "{autopf}\\" + appName : "{localappdata}\\Programs\\" + appName);
57
59
 
58
60
  // Setup icon: an explicit .ico, else the app's bundled icon.ico.
59
61
  let icon: string | undefined;
@@ -80,7 +82,10 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
80
82
  L.push(`PrivilegesRequired=${perMachine ? "admin" : "lowest"}`);
81
83
  L.push(`OutputDir=${outDir}`);
82
84
  L.push(`OutputBaseFilename=${baseName}`);
83
- L.push("Compression=lzma2/max");
85
+ // CEF is the bulk of the installer. Normal compression plus two independent
86
+ // LZMA2 blocks is substantially faster on CI with a modest size tradeoff.
87
+ L.push("Compression=lzma2/normal");
88
+ L.push("LZMANumBlockThreads=2");
84
89
  L.push("SolidCompression=yes");
85
90
  L.push("ArchitecturesAllowed=x64compatible");
86
91
  L.push("ArchitecturesInstallIn64BitMode=x64compatible");
@@ -96,17 +101,23 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
96
101
  if (options.include) L.push("", "; --- user include ---", options.include, "");
97
102
 
98
103
  L.push("", "[Files]");
99
- L.push(`Source: "${appDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs`);
104
+ L.push(
105
+ `Source: "${appDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs`,
106
+ );
100
107
 
101
108
  if (desktop) {
102
109
  L.push("", "[Tasks]");
103
- L.push(`Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional:"`);
110
+ L.push(
111
+ `Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional:"`,
112
+ );
104
113
  }
105
114
 
106
115
  L.push("", "[Icons]");
107
116
  if (startMenu) L.push(`Name: "{group}\\${v(appName)}"; Filename: "{app}\\${exeName}"`);
108
117
  if (desktop) {
109
- L.push(`Name: "{autodesktop}\\${v(appName)}"; Filename: "{app}\\${exeName}"; Tasks: desktopicon`);
118
+ L.push(
119
+ `Name: "{autodesktop}\\${v(appName)}"; Filename: "{app}\\${exeName}"; Tasks: desktopicon`,
120
+ );
110
121
  }
111
122
 
112
123
  if (runAfter) {
@@ -12,11 +12,11 @@
12
12
  * falls back to the portable `.zip` when it's absent.
13
13
  */
14
14
 
15
- import { $ } from "bun";
16
15
  import { existsSync, rmSync, writeFileSync } from "node:fs";
17
- import { join, isAbsolute } from "node:path";
16
+ import { isAbsolute, join } from "node:path";
17
+ import { $ } from "bun";
18
18
  import type { NsisConfig } from "mirinjs";
19
- import { makeWindowsIcon } from "./icon-win.ts";
19
+ import { makeWindowsIcon } from "./icons/windows/index.ts";
20
20
 
21
21
  export interface BuildNsisInput {
22
22
  /** The assembled app folder (build/<App>). */
@@ -50,7 +50,8 @@ const resolve = (projectDir: string, p: string) => (isAbsolute(p) ? p : join(pro
50
50
 
51
51
  /** Build the installer with `makensis`, returning the setup.exe path. */
52
52
  export async function buildNsisInstaller(input: BuildNsisInput): Promise<string> {
53
- const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } = input;
53
+ const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
54
+ input;
54
55
  const out = join(outDir, fileName);
55
56
  rmSync(out, { force: true });
56
57
 
@@ -63,12 +64,16 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
63
64
  const publisher = options.publisher ?? appName;
64
65
  const root = perMachine ? "HKLM" : "HKCU";
65
66
  const installDir =
66
- options.installDir ?? (perMachine ? `$PROGRAMFILES64\\${appName}` : `$LOCALAPPDATA\\Programs\\${appName}`);
67
+ options.installDir ??
68
+ (perMachine ? `$PROGRAMFILES64\\${appName}` : `$LOCALAPPDATA\\Programs\\${appName}`);
67
69
 
68
70
  // Installer/uninstaller icon: an explicit .ico, else the app's bundled icon.ico.
69
71
  let icon: string | undefined;
70
72
  if (options.installerIcon) {
71
- icon = makeWindowsIcon(resolve(projectDir, options.installerIcon), join(outDir, "_installer.ico"));
73
+ icon = makeWindowsIcon(
74
+ resolve(projectDir, options.installerIcon),
75
+ join(outDir, "_installer.ico"),
76
+ );
72
77
  } else if (existsSync(join(appDir, "icon.ico"))) {
73
78
  icon = join(appDir, "icon.ico");
74
79
  }
@@ -85,7 +90,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
85
90
  L.push(`OutFile "${nsis(out)}"`);
86
91
  L.push(`InstallDir "${installDir}"`);
87
92
  L.push(`RequestExecutionLevel ${perMachine ? "admin" : "user"}`);
88
- L.push(`!define MUI_ABORTWARNING`);
93
+ L.push("!define MUI_ABORTWARNING");
89
94
  if (icon) {
90
95
  L.push(`!define MUI_ICON "${nsis(icon)}"`);
91
96
  L.push(`!define MUI_UNICON "${nsis(icon)}"`);
@@ -127,14 +132,15 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
127
132
  // Estimated size for Add/Remove Programs.
128
133
  L.push(` \${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2`);
129
134
  L.push(` IntFmt $0 "0x%08X" $0`);
130
- const reg = (name: string, val: string) => L.push(` WriteRegStr ${root} "${key}" "${name}" "${val}"`);
135
+ const reg = (name: string, val: string) =>
136
+ L.push(` WriteRegStr ${root} "${key}" "${name}" "${val}"`);
131
137
  reg("DisplayName", nsis(appName));
132
138
  reg("DisplayVersion", nsis(version));
133
139
  reg("Publisher", nsis(publisher));
134
140
  reg("DisplayIcon", `$INSTDIR\\${nsis(exeName)}`);
135
141
  reg("InstallLocation", "$INSTDIR");
136
- reg("UninstallString", '$INSTDIR\\Uninstall.exe');
137
- reg("QuietUninstallString", '$INSTDIR\\Uninstall.exe /S');
142
+ reg("UninstallString", "$INSTDIR\\Uninstall.exe");
143
+ reg("QuietUninstallString", "$INSTDIR\\Uninstall.exe /S");
138
144
  L.push(` WriteRegDWORD ${root} "${key}" "NoModify" 1`);
139
145
  L.push(` WriteRegDWORD ${root} "${key}" "NoRepair" 1`);
140
146
  L.push(` WriteRegDWORD ${root} "${key}" "EstimatedSize" $0`);
@@ -142,7 +148,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
142
148
  L.push("SectionEnd");
143
149
 
144
150
  // Uninstall section.
145
- L.push("Section \"Uninstall\"");
151
+ L.push('Section "Uninstall"');
146
152
  L.push(` SetShellVarContext ${ctx}`);
147
153
  if (desktop) L.push(` Delete "$DESKTOP\\${nsis(appName)}.lnk"`);
148
154
  if (startMenu) {
@@ -0,0 +1,2 @@
1
+ export * from "./package.ts";
2
+ export * from "./types.ts";