craft-native 0.0.87 → 0.0.89

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/README.md CHANGED
@@ -314,6 +314,77 @@ import { orbstackStyles, renderOrbStackSidebar, orbstackDemoData } from 'craft-n
314
314
  const html = renderOrbStackSidebar(orbstackDemoData)
315
315
  ```
316
316
 
317
+ ## Auto-Updating
318
+
319
+ `AutoUpdater` fetches a manifest, verifies the download, replaces the app bundle,
320
+ and relaunches.
321
+
322
+ ```ts
323
+ import { AutoUpdater } from 'craft-native'
324
+
325
+ const updater = new AutoUpdater({
326
+ updateUrl: 'https://github.com/you/app/releases/latest/download/update.json',
327
+ currentVersion: '1.0.0',
328
+ appPath: '/Applications/MyApp.app',
329
+ autoDownload: false,
330
+ macos: { teamId: 'ABCDE12345' },
331
+ })
332
+
333
+ updater.on('update-available', info => console.log('available:', info.version))
334
+ updater.on('download-progress', p => console.log(`${p.percent}%`))
335
+
336
+ if (await updater.checkForUpdates()) {
337
+ await updater.downloadUpdate()
338
+ await updater.installUpdate()
339
+ }
340
+ ```
341
+
342
+ ### macOS trust
343
+
344
+ The manifest's SHA-256 proves the download matches what the manifest asked for. It
345
+ says nothing about *who published it* — whoever can rewrite the manifest can rewrite
346
+ the hash with it. So on macOS the staged bundle is also put through the checks
347
+ Gatekeeper runs at launch, before anything is swapped:
348
+
349
+ - `codesign --verify --deep --strict` — the signature covers the bytes on disk
350
+ - `spctl -a -t exec` — Apple has notarized this build
351
+ - `TeamIdentifier` equals `macos.teamId` — *you* published it
352
+
353
+ Pin `teamId`. Without it an updater accepts any notarized bundle, and notarization is
354
+ available to every Apple developer account there is. `@stacksjs/desktop`'s
355
+ `createSelfUpdater` fills it in from the signature on the running copy, which is the
356
+ version most apps want.
357
+
358
+ ### Replacing the bundle
359
+
360
+ The swap is deliberately not `rm -rf app && cp -R new app`:
361
+
362
+ - **`ditto`, not `cp -R`.** `cp` and `fs.cpSync` drop the extended attributes and ACLs
363
+ a code signature is computed over, so a byte-identical copy fails `codesign --verify`.
364
+ - **Two renames, not a delete.** The new bundle lands on the destination volume first,
365
+ then the old one is renamed aside and the new one renamed in. Both are atomic within
366
+ a directory, so there is no window in which an interrupted update leaves the user
367
+ with no app.
368
+ - **Quarantine last.** The flag is cleared only after verification passes — clearing it
369
+ first is how an updater becomes a way to install anything at all.
370
+
371
+ These are exported on their own for installers and CI: `verifyBundleTrust`,
372
+ `readBundleIdentity`, `extractBundle`, `extractBundleFromDmg`, `extractBundleFromZip`,
373
+ `swapBundle`, `dittoBundle`, `clearQuarantine`, `canReplaceBundle`.
374
+
375
+ ### Relaunching
376
+
377
+ `restartApp` opens the bundle and exits. Pass `relaunch` when the updater does not run
378
+ in the process the user launched — an agent behind a webview that calls
379
+ `process.exit(0)` kills a child, leaves the window on a dead server, and relaunches
380
+ nothing.
381
+
382
+ ### Deltas
383
+
384
+ Delta updates are used only when `bspatch` or `xdelta3` is on the machine. Neither
385
+ ships with macOS or a default Linux install, so a manifest that offers a delta falls
386
+ back to the full bundle rather than failing.
387
+
317
388
  ## Examples
318
389
 
319
390
  See the [examples directory](../examples-ts) for more:
@@ -49,6 +49,15 @@ export interface WindowState {
49
49
  * Window creation options
50
50
  */
51
51
  export interface WindowCreateOptions {
52
+ /**
53
+ * A stable name for this window, so opening it twice reaches the same one.
54
+ *
55
+ * Without it every `create()` gets a fresh generated id and the host has no
56
+ * way to tell "open Settings" from "open a second Settings" — which is what
57
+ * Cmd+, pressed twice looks like from here. Name the window and the second
58
+ * call brings the first forward instead.
59
+ */
60
+ id?: string;
52
61
  /** Window title */
53
62
  title?: string;
54
63
  /** Window width */
@@ -93,6 +102,14 @@ export interface WindowCreateOptions {
93
102
  skipTaskbar?: boolean;
94
103
  /** Whether titlebar is hidden */
95
104
  titlebarHidden?: boolean;
105
+ /**
106
+ * Whether the Web Inspector is available in this window.
107
+ *
108
+ * Defaults to off for a window opened from the page: an app built with
109
+ * `--no-devtools` should not grow a right-click Inspect Element by opening
110
+ * its own Settings.
111
+ */
112
+ devTools?: boolean;
96
113
  /** Draw native macOS sidebar material behind a web-rendered sidebar */
97
114
  webSidebarMaterial?: boolean;
98
115
  /** Width of the native material backdrop behind a web-rendered sidebar */
@@ -101,6 +118,22 @@ export interface WindowCreateOptions {
101
118
  webSidebarMaterialOpacity?: number;
102
119
  /** Draw that material behind the whole web view instead of a leading strip */
103
120
  webWindowMaterial?: boolean;
121
+ /**
122
+ * Whether Craft draws its own controls beside the window buttons — the
123
+ * sidebar toggle and two history arrows. Defaults to on for a window with a
124
+ * web material behind it, which otherwise has nothing up there at all. Turn
125
+ * it off in a page that draws its own history row.
126
+ */
127
+ chromeControls?: boolean;
128
+ /**
129
+ * Whether the page's storage — `localStorage`, IndexedDB, cookies — survives
130
+ * a quit and is shared with the app's other windows.
131
+ *
132
+ * Off by default: the ephemeral store costs no disk I/O at startup. Any app
133
+ * that keeps a preference wants it on, and a *second* window that keeps one
134
+ * must have it on, or it writes where the first window cannot read.
135
+ */
136
+ persistentStorage?: boolean;
104
137
  /** Titlebar style (macOS) */
105
138
  titlebarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
106
139
  /** Vibrancy effect (macOS) */
package/dist/cli.js CHANGED
@@ -1053,9 +1053,11 @@ __export(exports_src, {
1053
1053
  syncWebAssets: () => syncWebAssets,
1054
1054
  showSimulator: () => showSimulator,
1055
1055
  run: () => run,
1056
+ resolveRuntimeDir: () => resolveRuntimeDir,
1056
1057
  renderWatchEntitlements: () => renderWatchEntitlements,
1057
1058
  renderUsageDescriptions: () => renderUsageDescriptions,
1058
1059
  renderUrlTypes: () => renderUrlTypes,
1060
+ renderRuntimeSettings: () => renderRuntimeSettings,
1059
1061
  renderPrivacyManifest: () => renderPrivacyManifest,
1060
1062
  renderOrientations: () => renderOrientations,
1061
1063
  renderEntitlements: () => renderEntitlements,
@@ -1064,6 +1066,7 @@ __export(exports_src, {
1064
1066
  pickSimulator: () => pickSimulator,
1065
1067
  orderSimulators: () => orderSimulators,
1066
1068
  open: () => open,
1069
+ installRuntime: () => installRuntime,
1067
1070
  init: () => init,
1068
1071
  build: () => build,
1069
1072
  bootSimulator: () => bootSimulator
@@ -1193,6 +1196,14 @@ ${appGroups}</dict>
1193
1196
  </plist>
1194
1197
  `;
1195
1198
  }
1199
+ function renderRuntimeSettings() {
1200
+ return [
1201
+ ' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
1202
+ ' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
1203
+ ' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
1204
+ ].join(`
1205
+ `);
1206
+ }
1196
1207
  function renderPrivacyManifest(config) {
1197
1208
  const privacy = config.privacy ?? {};
1198
1209
  const collected = privacy.collectedDataTypes ?? [];
@@ -1296,6 +1307,55 @@ function syncWebAssets(source, output) {
1296
1307
  throw new Error(`Web asset directory must contain index.html: ${source}`);
1297
1308
  }
1298
1309
  }
1310
+ function resolveRuntimeDir(override) {
1311
+ if (override === null)
1312
+ return null;
1313
+ const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
1314
+ if (!dir)
1315
+ return null;
1316
+ if (!existsSync4(dir)) {
1317
+ const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
1318
+ throw new Error(`${source} ${dir}, which does not exist.`);
1319
+ }
1320
+ return dir;
1321
+ }
1322
+ async function installRuntime(output, runtimeDir) {
1323
+ const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
1324
+ const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
1325
+ if (present.length === 0) {
1326
+ throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
1327
+ }
1328
+ return { sdk, archives, present };
1329
+ });
1330
+ const dest = join2(output, "Runtime");
1331
+ rmSync2(dest, { recursive: true, force: true });
1332
+ for (const { sdk, archives, present } of resolved) {
1333
+ const sdkDir = join2(dest, sdk);
1334
+ mkdirSync2(sdkDir, { recursive: true });
1335
+ const target = join2(sdkDir, "libcraft-ios.a");
1336
+ if (present.length === 1) {
1337
+ const missing = archives.filter((a) => !present.includes(a));
1338
+ if (missing.length > 0) {
1339
+ console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1340
+ }
1341
+ cpSync2(join2(runtimeDir, present[0]), target);
1342
+ } else {
1343
+ await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
1344
+ }
1345
+ }
1346
+ return true;
1347
+ }
1348
+ async function refreshRuntime(output, override) {
1349
+ if (!existsSync4(join2(output, "Runtime")))
1350
+ return;
1351
+ const dir = resolveRuntimeDir(override);
1352
+ if (!dir) {
1353
+ console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
1354
+ return;
1355
+ }
1356
+ await installRuntime(output, dir);
1357
+ console.log(" Refreshed the Zig runtime from", dir);
1358
+ }
1299
1359
  async function init(options) {
1300
1360
  const { name, bundleId, teamId, output } = options;
1301
1361
  console.log(`
@@ -1368,7 +1428,14 @@ async function init(options) {
1368
1428
  SWIFT_VERSION: "5.0"
1369
1429
  SKIP_INSTALL: YES`);
1370
1430
  }
1371
- const projectYml = projectYmlTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1431
+ const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1432
+ const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
1433
+ if (hasRuntime) {
1434
+ console.log(" Linked the Zig runtime from", runtimeDir);
1435
+ } else {
1436
+ rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
1437
+ }
1438
+ const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1372
1439
  ${nativeDependencies.join(`
1373
1440
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1374
1441
  `));
@@ -1459,6 +1526,7 @@ async function build(options) {
1459
1526
  syncWebAssets(htmlPath, output);
1460
1527
  console.log(` Synced: ${htmlPath} \u2192 dist/`);
1461
1528
  }
1529
+ await refreshRuntime(output, options.runtimeDir);
1462
1530
  if (!generateProject)
1463
1531
  return;
1464
1532
  try {
@@ -1580,7 +1648,7 @@ async function run(options) {
1580
1648
  console.log(" 4. Click Run (\u25B6\uFE0F)");
1581
1649
  }
1582
1650
  }
1583
- var $, TEMPLATES_DIR, DEFAULT_CONFIG;
1651
+ var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
1584
1652
  var init_src = __esm(() => {
1585
1653
  ({ $ } = globalThis.Bun);
1586
1654
  TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
@@ -1633,6 +1701,10 @@ var init_src = __esm(() => {
1633
1701
  orientations: ["portrait"],
1634
1702
  deviceFamilies: ["iphone", "ipad"]
1635
1703
  };
1704
+ RUNTIME_ARCHIVES = {
1705
+ device: ["libcraft-ios.a"],
1706
+ simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
1707
+ };
1636
1708
  });
1637
1709
 
1638
1710
  // dist/android/src/index.js
@@ -4191,7 +4263,7 @@ function craftBinaryNotFoundMessage(triedPath) {
4191
4263
  `);
4192
4264
  }
4193
4265
  // package.json
4194
- var version = "0.0.87";
4266
+ var version = "0.0.89";
4195
4267
 
4196
4268
  // bin/cli.ts
4197
4269
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];