craft-native 0.0.86 → 0.0.88
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 +71 -0
- package/dist/api/window.d.ts +14 -1
- package/dist/cli.js +75 -3
- package/dist/index.cjs +386 -102
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +374 -90
- package/dist/ios/src/index.d.ts +74 -0
- package/dist/ios/src/index.js +73 -1
- package/dist/ios/templates/CraftApp.swift +510 -112
- package/dist/ios/templates/project.yml.template +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/updater/index.d.ts +104 -1
- package/dist/updater/macos-bundle.d.ts +153 -0
- package/package.json +2 -2
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:
|
package/dist/api/window.d.ts
CHANGED
|
@@ -97,8 +97,10 @@ export interface WindowCreateOptions {
|
|
|
97
97
|
webSidebarMaterial?: boolean;
|
|
98
98
|
/** Width of the native material backdrop behind a web-rendered sidebar */
|
|
99
99
|
webSidebarWidth?: number;
|
|
100
|
-
/** White/dark tint opacity over the native material backdrop */
|
|
100
|
+
/** White/dark tint opacity over the native material backdrop (sidebar span only) */
|
|
101
101
|
webSidebarMaterialOpacity?: number;
|
|
102
|
+
/** Draw that material behind the whole web view instead of a leading strip */
|
|
103
|
+
webWindowMaterial?: boolean;
|
|
102
104
|
/** Titlebar style (macOS) */
|
|
103
105
|
titlebarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
|
|
104
106
|
/** Vibrancy effect (macOS) */
|
|
@@ -332,6 +334,15 @@ export declare class Window {
|
|
|
332
334
|
* Set vibrancy effect (macOS)
|
|
333
335
|
*/
|
|
334
336
|
setVibrancy(vibrancy: WindowCreateOptions['vibrancy'] | null): Promise<void>;
|
|
337
|
+
/**
|
|
338
|
+
* Pin the window to light or dark, or hand it back to the OS (macOS).
|
|
339
|
+
*
|
|
340
|
+
* Everything native around the page — a material backdrop, a vibrancy view,
|
|
341
|
+
* the window buttons — resolves against the *window's* appearance. An app
|
|
342
|
+
* with its own light/dark control is the only thing that knows which it
|
|
343
|
+
* picked, so it has to say, or the page and its window disagree.
|
|
344
|
+
*/
|
|
345
|
+
setAppearance(appearance: 'light' | 'dark' | 'system'): Promise<void>;
|
|
335
346
|
/**
|
|
336
347
|
* Where the platform's window buttons are — read, not written.
|
|
337
348
|
*
|
|
@@ -454,6 +465,8 @@ declare class WindowManager {
|
|
|
454
465
|
setBackgroundColor: (color: string) => Promise<void>;
|
|
455
466
|
/** Set current window vibrancy (macOS) */
|
|
456
467
|
setVibrancy: (vibrancy: WindowCreateOptions['vibrancy'] | null) => Promise<void>;
|
|
468
|
+
/** Pin the current window to light or dark, or follow the OS (macOS) */
|
|
469
|
+
setAppearance: (appearance: 'light' | 'dark' | 'system') => Promise<void>;
|
|
457
470
|
/** Set current window resizable */
|
|
458
471
|
setResizable: (resizable: boolean) => Promise<void>;
|
|
459
472
|
/** Start moving current window from a native pointer event */
|
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
|
|
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.
|
|
4266
|
+
var version = "0.0.88";
|
|
4195
4267
|
|
|
4196
4268
|
// bin/cli.ts
|
|
4197
4269
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|