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 +71 -0
- package/dist/api/window.d.ts +33 -0
- package/dist/cli.js +75 -3
- package/dist/index.cjs +384 -103
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +372 -91
- 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/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
|
@@ -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
|
|
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.89";
|
|
4195
4267
|
|
|
4196
4268
|
// bin/cli.ts
|
|
4197
4269
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|