craft-native 0.0.272 → 0.0.278

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.
@@ -338,7 +338,7 @@ async function createAndroidEmulator(options) {
338
338
  const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
339
339
  const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
340
340
  const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
341
- const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
341
+ const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || homeActivity.startsWith("com.google.android.googlesdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
342
342
  booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && (!requiresPlayStore || playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome;
343
343
  }
344
344
  }
@@ -16,6 +16,8 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
16
16
  private var pendingChildren = JSONArray()
17
17
  private var styleKey: String? = null
18
18
  private var styleVersion = 0
19
+ /** The prefix of a style key that is a vector style's URL, not raster style JSON. */
20
+ private val vectorStylePrefix = "uri:"
19
21
  private var readyStyle: Style? = null
20
22
  private var initialCameraKey: String? = null
21
23
  private var reportedRegion: LatLngBounds? = null
@@ -146,7 +148,10 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
146
148
  styleKey = nextStyle
147
149
  readyStyle = null
148
150
  val version = ++styleVersion
149
- nativeMap.setStyle(Style.Builder().fromJson(nextStyle)) { loaded ->
151
+ // A vector style (the standard basemap) loads from its URL; the
152
+ // raster ones are composed here as style JSON.
153
+ val builder = if (nextStyle.startsWith(vectorStylePrefix)) Style.Builder().fromUri(nextStyle.removePrefix(vectorStylePrefix)) else Style.Builder().fromJson(nextStyle)
154
+ nativeMap.setStyle(builder) { loaded ->
150
155
  if (!destroyed && version == styleVersion) {
151
156
  readyStyle = loaded
152
157
  installOverlays(loaded)
@@ -183,6 +188,20 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
183
188
  }
184
189
 
185
190
  private fun basemapStyle(): String {
191
+ // `basemap`: the named presets, drawn as well as each can be here.
192
+ // Standard is OpenFreeMap's vector style - sharp at any density, no
193
+ // key - where it was raster tiles from tile.openstreetmap.org, whose
194
+ // policy does not allow an app's traffic. Outdoors is topography with
195
+ // contours; satellite and hybrid are Esri imagery.
196
+ when (pendingProps.optString("basemap", "").lowercase()) {
197
+ "standard" -> {
198
+ attribution.text = "OpenFreeMap © OpenMapTiles · Data © OpenStreetMap contributors"
199
+ return vectorStylePrefix + "https://tiles.openfreemap.org/styles/liberty"
200
+ }
201
+ "outdoors" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", "Topography: Esri, USGS, NOAA, OpenStreetMap contributors", labels = false)
202
+ "satellite" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "Imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community", labels = false)
203
+ "hybrid" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "Imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community", labels = true)
204
+ }
186
205
  val custom = pendingProps.optJSONObject("tileOverlay")
187
206
  val template = custom?.optString("urlTemplate").orEmpty()
188
207
  val credit = custom?.optString("attribution").orEmpty()
@@ -212,6 +231,18 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
212
231
  return JSONObject().put("version", 8).put("sources", sources).put("layers", layers).toString()
213
232
  }
214
233
 
234
+ private fun rasterStyle(url: String, credit: String, labels: Boolean): String {
235
+ val sources = JSONObject().put("basemap", rasterSource(url, credit))
236
+ val layers = JSONArray().put(JSONObject().put("id", "basemap").put("type", "raster").put("source", "basemap"))
237
+ attribution.text = credit
238
+ if (labels) {
239
+ sources.put("labels", rasterSource("https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}", "Labels © Esri"))
240
+ layers.put(JSONObject().put("id", "labels").put("type", "raster").put("source", "labels"))
241
+ attribution.text = "$credit · Labels © Esri"
242
+ }
243
+ return JSONObject().put("version", 8).put("sources", sources).put("layers", layers).toString()
244
+ }
245
+
215
246
  private fun rasterSource(url: String, credit: String): JSONObject = JSONObject()
216
247
  .put("type", "raster").put("tiles", JSONArray().put(url)).put("tileSize", 256).put("maxzoom", 19).put("attribution", credit)
217
248
 
@@ -119,6 +119,10 @@ class CraftNativeMutationDocument {
119
119
  // be reconciled so the host can move its existing view.
120
120
  requiresFullRender = true
121
121
  }
122
+ "replaceChildren" -> {
123
+ val parentId = replaceChildren(operation, candidateNodes)
124
+ if (parentId !in affectedNodeIds) affectedNodeIds.add(parentId)
125
+ }
122
126
  "removeNode" -> {
123
127
  val parent = candidateNodes[operation.optString("id")]?.parent
124
128
  if (parent == null) requiresFullRender = true
@@ -257,6 +261,37 @@ class CraftNativeMutationDocument {
257
261
  return previousParentId
258
262
  }
259
263
 
264
+ /** Replaces a container's ordered child snapshot in one atomic mutation. */
265
+ private fun replaceChildren(operation: JSONObject, nodes: MutableMap<String, Node>): String {
266
+ val parentId = requiredId(operation, "parentId", "replaceChildren")
267
+ val parent = nodes[parentId] ?: fail("UNKNOWN_PARENT", "node $parentId does not exist")
268
+ if (!acceptsChildren(parent.type)) fail("INVALID_PARENT", "node $parentId cannot contain child nodes")
269
+ val values = operation.optJSONArray("children")
270
+ ?: fail("INVALID_CHILDREN", "replaceChildren.children must be an array")
271
+ val childIds = mutableListOf<String>()
272
+ for (index in 0 until values.length()) {
273
+ val childId = values.opt(index) as? String
274
+ if (childId.isNullOrBlank()) {
275
+ fail("INVALID_CHILDREN", "replaceChildren.children must contain non-empty ids")
276
+ }
277
+ if (childId in childIds) fail("DUPLICATE_CHILD", "child $childId appears more than once")
278
+ val child = nodes[childId] ?: fail("UNKNOWN_NODE", "node $childId does not exist")
279
+ if (childId == parentId || parentId in descendants(childId, nodes)) {
280
+ fail("CYCLE", "attaching node $childId would create a cycle")
281
+ }
282
+ if (child.parent != null && child.parent != parentId) {
283
+ fail("NODE_ATTACHED", "node $childId already has a parent")
284
+ }
285
+ childIds.add(childId)
286
+ }
287
+ parent.children.filterNot { it in childIds }.forEach { childId ->
288
+ (descendants(childId, nodes) + childId).forEach { nodes.remove(it) }
289
+ }
290
+ parent.children = childIds
291
+ childIds.forEach { nodes[it]?.parent = parentId }
292
+ return parentId
293
+ }
294
+
260
295
  private fun removeNode(
261
296
  operation: JSONObject,
262
297
  nodes: MutableMap<String, Node>,
@@ -17,8 +17,8 @@ export type { HttpClientOptions, RequestOptions, HttpResponse, WebSocketOptions
17
17
  export { crypto, uuid, randomString, hmac, timingSafeEqual, hashPassword, verifyPassword } from './crypto.js';
18
18
  export { env, getPlatform, isDesktop, isMobile, isCraft, getSystemInfo, exec, spawn, ChildProcess, cwd, homeDir, tempDir, exit, argv, open } from './process.js';
19
19
  export type { Platform, SystemInfo, ExecOptions, ExecResult, SpawnOptions } from './process.js';
20
- export { device, haptics, permissions, camera, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
21
- export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
20
+ export { device, haptics, permissions, camera, files, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
21
+ export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, FileInfo, PickFileOptions, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
22
22
  export { carplay, appClips, liveActivities, sharePlay, storeKit, appIntents, tipKit, focusFilters } from './ios-advanced.js';
23
23
  export type { CarPlayTemplateType, CarPlayListItem, CarPlayGridItem, CarPlayTemplate, AppClipInvocation, LiveActivityContentState, LiveActivityAttributes, LiveActivityConfig, LiveActivityHandle, SharePlaySessionState, SharePlayParticipant, SharePlayActivity, ProductType, Product, Transaction, IntentParameterType, IntentParameter, AppIntent, TipDisplayFrequency, Tip, FocusStatus, FocusFilter } from './ios-advanced.js';
24
24
  export { materialYou, photoPicker, workManager, foregroundService, predictiveBack, appLanguage, widgets as androidWidgets, playBilling } from './android-advanced.js';
@@ -314,6 +314,30 @@ export interface PhotoResult {
314
314
  /** MIME type */
315
315
  mimeType: string;
316
316
  }
317
+ /** A file copied into the app-owned native file area. */
318
+ export interface FileInfo {
319
+ /** App-owned path suitable for `fetch` FormData and sharing. */
320
+ path: string;
321
+ /** Original (sanitized) file name. */
322
+ name: string;
323
+ /** Preferred MIME type, or `application/octet-stream` when unknown. */
324
+ type: string;
325
+ /** Number of bytes in the copied file. */
326
+ size: number;
327
+ }
328
+ /** General document picker options. */
329
+ export interface PickFileOptions {
330
+ /** MIME types, UTIs, or filename extensions (for example `pdf` or `public.data`). */
331
+ contentTypes?: string[];
332
+ /** Maximum number of documents; native hosts clamp this to 1–20. */
333
+ maxSelections?: number;
334
+ /** Maximum size of each document in bytes; native hosts default to 512 MiB. */
335
+ maxBytes?: number;
336
+ }
337
+ /** General document picker API. */
338
+ export declare const files: {
339
+ pickFile(options?: PickFileOptions): Promise<FileInfo[]>;
340
+ };
317
341
  /**
318
342
  * Camera and image picker API.
319
343
  *
@@ -1835,6 +1859,7 @@ declare const mobile: {
1835
1859
  haptics: typeof haptics;
1836
1860
  permissions: typeof permissions;
1837
1861
  camera: typeof camera;
1862
+ files: typeof files;
1838
1863
  biometrics: typeof biometrics;
1839
1864
  secureStorage: typeof secureStorage;
1840
1865
  location: typeof location;
package/dist/cli.js CHANGED
@@ -141,7 +141,7 @@ import {
141
141
  writeFileSync as writeFileSync3
142
142
  } from "fs";
143
143
  import { homedir } from "os";
144
- import { basename, join as join2 } from "path";
144
+ import { basename, join as join2, resolve } from "path";
145
145
  import { deflateRawSync } from "zlib";
146
146
  function crc32(data) {
147
147
  let c = 4294967295;
@@ -244,7 +244,7 @@ async function packageMacOS(config, outDir) {
244
244
  const opts = config.macos || {};
245
245
  const hardenedRuntime = !opts.appStore;
246
246
  const appBundlePath = join2(outDir, `${name}.app`);
247
- const appBundle = createMacOSAppBundle({
247
+ const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
248
248
  name,
249
249
  version,
250
250
  bundleId,
@@ -327,19 +327,21 @@ async function packageMacOS(config, outDir) {
327
327
  async function notarizeIfRequested(opts, artifactPath, format) {
328
328
  if (!opts.notarize)
329
329
  return [];
330
- if (!opts.appleId || !opts.applePassword || !opts.teamId) {
330
+ if (!opts.notaryKeychainProfile && !opts.notaryApiKey && (!opts.appleId || !opts.applePassword || !opts.teamId)) {
331
331
  return [{
332
332
  success: false,
333
333
  platform: "macos",
334
334
  format: `${format} (notarize)`,
335
- error: "Notarization requires macos.appleId, macos.applePassword and macos.teamId"
335
+ error: "Notarization requires a keychain profile, API key, or Apple ID/password/team ID"
336
336
  }];
337
337
  }
338
338
  const submitted = await runTool("xcrun", notarytoolArguments({
339
339
  artifactPath,
340
340
  appleId: opts.appleId,
341
341
  applePassword: opts.applePassword,
342
- teamId: opts.teamId
342
+ teamId: opts.teamId,
343
+ keychainProfile: opts.notaryKeychainProfile,
344
+ apiKey: opts.notaryApiKey
343
345
  }));
344
346
  if (!submitted.success)
345
347
  return [{ success: false, platform: "macos", format: `${format} (notarize)`, error: submitted.error }];
@@ -571,6 +573,13 @@ function productbuildArguments(opts) {
571
573
  ];
572
574
  }
573
575
  function notarytoolArguments(opts) {
576
+ const base = ["notarytool", "submit", opts.artifactPath, "--wait"];
577
+ if (opts.keychainProfile)
578
+ return [...base, "--keychain-profile", opts.keychainProfile];
579
+ if (opts.apiKey)
580
+ return [...base, "--key", opts.apiKey.path, "--key-id", opts.apiKey.keyId, "--issuer", opts.apiKey.issuer];
581
+ if (!opts.appleId || !opts.applePassword || !opts.teamId)
582
+ throw new Error("Notarization credentials are incomplete");
574
583
  return [
575
584
  "notarytool",
576
585
  "submit",
@@ -584,6 +593,20 @@ function notarytoolArguments(opts) {
584
593
  "--wait"
585
594
  ];
586
595
  }
596
+ function copyMacOSAppBundle(source, destination) {
597
+ try {
598
+ const info = join2(source, "Contents", "Info.plist");
599
+ if (!existsSync4(info) || !existsSync4(join2(source, "Contents", "MacOS")))
600
+ throw new Error(`Invalid prebuilt macOS application: ${source}`);
601
+ if (resolve(source) !== resolve(destination)) {
602
+ rmSync2(destination, { recursive: true, force: true });
603
+ cpSync(source, destination, { recursive: true });
604
+ }
605
+ return { success: true };
606
+ } catch (error) {
607
+ return { success: false, error: error.message };
608
+ }
609
+ }
587
610
  function createMacOSAppBundle(opts) {
588
611
  try {
589
612
  const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
@@ -2842,7 +2865,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2842
2865
  `);
2843
2866
  }
2844
2867
  // package.json
2845
- var version = "0.0.272";
2868
+ var version = "0.0.278";
2846
2869
 
2847
2870
  // bin/cli.ts
2848
2871
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -118,6 +118,7 @@ __export(exports_src, {
118
118
  extractBundle: () => extractBundle,
119
119
  extractBundleFromDmg: () => extractBundleFromDmg,
120
120
  extractBundleFromZip: () => extractBundleFromZip,
121
+ files: () => files,
121
122
  focus: () => focus,
122
123
  focusFilters: () => focusFilters,
123
124
  foregroundService: () => foregroundService,
@@ -373,7 +374,7 @@ function craftBinaryNotFoundMessage(triedPath) {
373
374
  `);
374
375
  }
375
376
  // package.json
376
- var version = "0.0.272";
377
+ var version = "0.0.278";
377
378
 
378
379
  // src/sdk-version.ts
379
380
  var SDK_VERSION = version;
@@ -611,7 +612,7 @@ async function packageMacOS(config, outDir) {
611
612
  const opts = config.macos || {};
612
613
  const hardenedRuntime = !opts.appStore;
613
614
  const appBundlePath = import_path.join(outDir, `${name}.app`);
614
- const appBundle = createMacOSAppBundle({
615
+ const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
615
616
  name,
616
617
  version,
617
618
  bundleId,
@@ -694,19 +695,21 @@ async function packageMacOS(config, outDir) {
694
695
  async function notarizeIfRequested(opts, artifactPath, format) {
695
696
  if (!opts.notarize)
696
697
  return [];
697
- if (!opts.appleId || !opts.applePassword || !opts.teamId) {
698
+ if (!opts.notaryKeychainProfile && !opts.notaryApiKey && (!opts.appleId || !opts.applePassword || !opts.teamId)) {
698
699
  return [{
699
700
  success: false,
700
701
  platform: "macos",
701
702
  format: `${format} (notarize)`,
702
- error: "Notarization requires macos.appleId, macos.applePassword and macos.teamId"
703
+ error: "Notarization requires a keychain profile, API key, or Apple ID/password/team ID"
703
704
  }];
704
705
  }
705
706
  const submitted = await runTool("xcrun", notarytoolArguments({
706
707
  artifactPath,
707
708
  appleId: opts.appleId,
708
709
  applePassword: opts.applePassword,
709
- teamId: opts.teamId
710
+ teamId: opts.teamId,
711
+ keychainProfile: opts.notaryKeychainProfile,
712
+ apiKey: opts.notaryApiKey
710
713
  }));
711
714
  if (!submitted.success)
712
715
  return [{ success: false, platform: "macos", format: `${format} (notarize)`, error: submitted.error }];
@@ -939,6 +942,13 @@ function productbuildArguments(opts) {
939
942
  ];
940
943
  }
941
944
  function notarytoolArguments(opts) {
945
+ const base = ["notarytool", "submit", opts.artifactPath, "--wait"];
946
+ if (opts.keychainProfile)
947
+ return [...base, "--keychain-profile", opts.keychainProfile];
948
+ if (opts.apiKey)
949
+ return [...base, "--key", opts.apiKey.path, "--key-id", opts.apiKey.keyId, "--issuer", opts.apiKey.issuer];
950
+ if (!opts.appleId || !opts.applePassword || !opts.teamId)
951
+ throw new Error("Notarization credentials are incomplete");
942
952
  return [
943
953
  "notarytool",
944
954
  "submit",
@@ -952,6 +962,20 @@ function notarytoolArguments(opts) {
952
962
  "--wait"
953
963
  ];
954
964
  }
965
+ function copyMacOSAppBundle(source, destination) {
966
+ try {
967
+ const info = import_path.join(source, "Contents", "Info.plist");
968
+ if (!import_fs.existsSync(info) || !import_fs.existsSync(import_path.join(source, "Contents", "MacOS")))
969
+ throw new Error(`Invalid prebuilt macOS application: ${source}`);
970
+ if (import_path.resolve(source) !== import_path.resolve(destination)) {
971
+ import_fs.rmSync(destination, { recursive: true, force: true });
972
+ import_fs.cpSync(source, destination, { recursive: true });
973
+ }
974
+ return { success: true };
975
+ } catch (error) {
976
+ return { success: false, error: error.message };
977
+ }
978
+ }
955
979
  function createMacOSAppBundle(opts) {
956
980
  try {
957
981
  const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
@@ -5106,6 +5130,55 @@ var permissions = {
5106
5130
  }
5107
5131
  }
5108
5132
  };
5133
+ var files = {
5134
+ async pickFile(options = {}) {
5135
+ const craft = getCraftMobile();
5136
+ if (typeof window !== "undefined" && craft?.files) {
5137
+ return craft.files.pickFile(options);
5138
+ }
5139
+ if (typeof document === "undefined") {
5140
+ throw new Error("File picker not available. Must run in Craft or a browser.");
5141
+ }
5142
+ return new Promise((resolve, reject) => {
5143
+ const input = document.createElement("input");
5144
+ input.type = "file";
5145
+ if (options.contentTypes?.length)
5146
+ input.accept = options.contentTypes.join(",");
5147
+ const requested = Number(options.maxSelections);
5148
+ const maxSelections = Number.isFinite(requested) && requested > 0 ? Math.min(20, Math.floor(requested)) : 1;
5149
+ input.multiple = maxSelections > 1;
5150
+ input.style.display = "none";
5151
+ let settled = false;
5152
+ const finish = (result) => {
5153
+ if (settled)
5154
+ return;
5155
+ settled = true;
5156
+ input.remove();
5157
+ result();
5158
+ };
5159
+ input.addEventListener("cancel", () => finish(() => resolve([])), { once: true });
5160
+ input.onchange = () => {
5161
+ const chosen = Array.from(input.files ?? []).slice(0, maxSelections);
5162
+ const limit = Number(options.maxBytes);
5163
+ if (Number.isFinite(limit) && limit > 0) {
5164
+ const oversized = chosen.find((file) => file.size > limit);
5165
+ if (oversized) {
5166
+ finish(() => reject(Object.assign(new Error("The selected file is too large"), { code: "FILE_TOO_LARGE" })));
5167
+ return;
5168
+ }
5169
+ }
5170
+ finish(() => resolve(chosen.map((file) => ({
5171
+ path: URL.createObjectURL(file),
5172
+ name: file.name,
5173
+ type: file.type || "application/octet-stream",
5174
+ size: file.size
5175
+ }))));
5176
+ };
5177
+ document.body.append(input);
5178
+ input.click();
5179
+ });
5180
+ }
5181
+ };
5109
5182
  var camera = {
5110
5183
  async takePicture(options = {}) {
5111
5184
  const craft = getCraftMobile();
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.272";
120
+ var version = "0.0.278";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -252,7 +252,7 @@ import {
252
252
  writeFileSync as writeFileSync2
253
253
  } from "fs";
254
254
  import { homedir } from "os";
255
- import { basename, join as join2 } from "path";
255
+ import { basename, join as join2, resolve } from "path";
256
256
  import { deflateRawSync } from "zlib";
257
257
  var DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
258
258
  var CRC32_TABLE = (() => {
@@ -366,7 +366,7 @@ async function packageMacOS(config, outDir) {
366
366
  const opts = config.macos || {};
367
367
  const hardenedRuntime = !opts.appStore;
368
368
  const appBundlePath = join2(outDir, `${name}.app`);
369
- const appBundle = createMacOSAppBundle({
369
+ const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
370
370
  name,
371
371
  version,
372
372
  bundleId,
@@ -449,19 +449,21 @@ async function packageMacOS(config, outDir) {
449
449
  async function notarizeIfRequested(opts, artifactPath, format) {
450
450
  if (!opts.notarize)
451
451
  return [];
452
- if (!opts.appleId || !opts.applePassword || !opts.teamId) {
452
+ if (!opts.notaryKeychainProfile && !opts.notaryApiKey && (!opts.appleId || !opts.applePassword || !opts.teamId)) {
453
453
  return [{
454
454
  success: false,
455
455
  platform: "macos",
456
456
  format: `${format} (notarize)`,
457
- error: "Notarization requires macos.appleId, macos.applePassword and macos.teamId"
457
+ error: "Notarization requires a keychain profile, API key, or Apple ID/password/team ID"
458
458
  }];
459
459
  }
460
460
  const submitted = await runTool("xcrun", notarytoolArguments({
461
461
  artifactPath,
462
462
  appleId: opts.appleId,
463
463
  applePassword: opts.applePassword,
464
- teamId: opts.teamId
464
+ teamId: opts.teamId,
465
+ keychainProfile: opts.notaryKeychainProfile,
466
+ apiKey: opts.notaryApiKey
465
467
  }));
466
468
  if (!submitted.success)
467
469
  return [{ success: false, platform: "macos", format: `${format} (notarize)`, error: submitted.error }];
@@ -694,6 +696,13 @@ function productbuildArguments(opts) {
694
696
  ];
695
697
  }
696
698
  function notarytoolArguments(opts) {
699
+ const base = ["notarytool", "submit", opts.artifactPath, "--wait"];
700
+ if (opts.keychainProfile)
701
+ return [...base, "--keychain-profile", opts.keychainProfile];
702
+ if (opts.apiKey)
703
+ return [...base, "--key", opts.apiKey.path, "--key-id", opts.apiKey.keyId, "--issuer", opts.apiKey.issuer];
704
+ if (!opts.appleId || !opts.applePassword || !opts.teamId)
705
+ throw new Error("Notarization credentials are incomplete");
697
706
  return [
698
707
  "notarytool",
699
708
  "submit",
@@ -707,6 +716,20 @@ function notarytoolArguments(opts) {
707
716
  "--wait"
708
717
  ];
709
718
  }
719
+ function copyMacOSAppBundle(source, destination) {
720
+ try {
721
+ const info = join2(source, "Contents", "Info.plist");
722
+ if (!existsSync3(info) || !existsSync3(join2(source, "Contents", "MacOS")))
723
+ throw new Error(`Invalid prebuilt macOS application: ${source}`);
724
+ if (resolve(source) !== resolve(destination)) {
725
+ rmSync2(destination, { recursive: true, force: true });
726
+ cpSync(source, destination, { recursive: true });
727
+ }
728
+ return { success: true };
729
+ } catch (error) {
730
+ return { success: false, error: error.message };
731
+ }
732
+ }
710
733
  function createMacOSAppBundle(opts) {
711
734
  try {
712
735
  const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
@@ -4861,6 +4884,55 @@ var permissions = {
4861
4884
  }
4862
4885
  }
4863
4886
  };
4887
+ var files = {
4888
+ async pickFile(options = {}) {
4889
+ const craft = getCraftMobile();
4890
+ if (typeof window !== "undefined" && craft?.files) {
4891
+ return craft.files.pickFile(options);
4892
+ }
4893
+ if (typeof document === "undefined") {
4894
+ throw new Error("File picker not available. Must run in Craft or a browser.");
4895
+ }
4896
+ return new Promise((resolve, reject) => {
4897
+ const input = document.createElement("input");
4898
+ input.type = "file";
4899
+ if (options.contentTypes?.length)
4900
+ input.accept = options.contentTypes.join(",");
4901
+ const requested = Number(options.maxSelections);
4902
+ const maxSelections = Number.isFinite(requested) && requested > 0 ? Math.min(20, Math.floor(requested)) : 1;
4903
+ input.multiple = maxSelections > 1;
4904
+ input.style.display = "none";
4905
+ let settled = false;
4906
+ const finish = (result) => {
4907
+ if (settled)
4908
+ return;
4909
+ settled = true;
4910
+ input.remove();
4911
+ result();
4912
+ };
4913
+ input.addEventListener("cancel", () => finish(() => resolve([])), { once: true });
4914
+ input.onchange = () => {
4915
+ const chosen = Array.from(input.files ?? []).slice(0, maxSelections);
4916
+ const limit = Number(options.maxBytes);
4917
+ if (Number.isFinite(limit) && limit > 0) {
4918
+ const oversized = chosen.find((file) => file.size > limit);
4919
+ if (oversized) {
4920
+ finish(() => reject(Object.assign(new Error("The selected file is too large"), { code: "FILE_TOO_LARGE" })));
4921
+ return;
4922
+ }
4923
+ }
4924
+ finish(() => resolve(chosen.map((file) => ({
4925
+ path: URL.createObjectURL(file),
4926
+ name: file.name,
4927
+ type: file.type || "application/octet-stream",
4928
+ size: file.size
4929
+ }))));
4930
+ };
4931
+ document.body.append(input);
4932
+ input.click();
4933
+ });
4934
+ }
4935
+ };
4864
4936
  var camera = {
4865
4937
  async takePicture(options = {}) {
4866
4938
  const craft = getCraftMobile();
@@ -11403,6 +11475,7 @@ export {
11403
11475
  extractBundle,
11404
11476
  extractBundleFromDmg,
11405
11477
  extractBundleFromZip,
11478
+ files,
11406
11479
  focus,
11407
11480
  focusFilters,
11408
11481
  foregroundService,
@@ -776,12 +776,15 @@ function renderWatchEntitlements(config) {
776
776
  <array>
777
777
  ${plistArray(config.appGroups)}
778
778
  </array>
779
+ ` : "";
780
+ const healthKit = config.enableHealthKit ? ` <key>com.apple.developer.healthkit</key>
781
+ <true/>
779
782
  ` : "";
780
783
  return `<?xml version="1.0" encoding="UTF-8"?>
781
784
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
782
785
  <plist version="1.0">
783
786
  <dict>
784
- ${appGroups}</dict>
787
+ ${appGroups}${healthKit}</dict>
785
788
  </plist>
786
789
  `;
787
790
  }
@@ -1195,7 +1198,7 @@ function writeShell(output, config) {
1195
1198
  if (!enabled)
1196
1199
  continue;
1197
1200
  mkdirSync2(join3(output, folder), { recursive: true });
1198
- const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1201
+ const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
1199
1202
  writeFileSync2(join3(output, folder, filename), source);
1200
1203
  }
1201
1204
  }
@@ -1328,7 +1331,8 @@ ${nativeDependencies.join(`
1328
1331
  const watchInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1329
1332
  writeFileSync2(join3(output, "WatchApp", "Info.plist"), watchInfo);
1330
1333
  writeFileSync2(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1331
- const watchExtensionInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1334
+ const watchExtensionInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{HEALTH_USAGE_DESCRIPTIONS\}\}/g, config.enableHealthKit ? `${plistString("NSHealthShareUsageDescription", `${displayName} reads health data you choose to share.`)}
1335
+ ${plistString("NSHealthUpdateUsageDescription", `${displayName} writes health data only with your permission.`)}` : "");
1332
1336
  writeFileSync2(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1333
1337
  writeFileSync2(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1334
1338
  }
@@ -357,6 +357,7 @@ final class CraftHybrid: NSObject {
357
357
  isConfigured = true
358
358
  self.config = config
359
359
  CraftNativeScriptThread.isBackground = config.nativeScriptThread == "background"
360
+ if CraftNativeScriptThread.isBackground { CraftNativeBundle.prewarm() }
360
361
  hasWeb = config.renderer != "native"
361
362
  siteURL = config.devServerURL.flatMap(URL.init(string:)).flatMap { url in
362
363
  ["http", "https"].contains((url.scheme ?? "").lowercased()) ? url : nil
@@ -2295,13 +2296,15 @@ final class CraftFramePacing: NSObject {
2295
2296
  }
2296
2297
  }
2297
2298
 
2298
- /// One main-thread block, timed: a screen mount, a bundle evaluation.
2299
+ /// One block, timed: a screen mount, a bundle evaluation. Labelled by the
2300
+ /// thread it ran on, since a screen's script now runs off the main thread
2301
+ /// and a bundle's time there costs no frames.
2299
2302
  @discardableResult
2300
2303
  func measure<T>(_ name: String, _ work: () -> T) -> T {
2301
2304
  guard enabled else { return work() }
2302
2305
  let started = CACurrentMediaTime()
2303
2306
  let result = work()
2304
- write(String(format: "%@ main=%.1fms", name, (CACurrentMediaTime() - started) * 1000))
2307
+ write(String(format: "%@ %@=%.1fms", name, Thread.isMainThread ? "main" : "script", (CACurrentMediaTime() - started) * 1000))
2305
2308
  return result
2306
2309
  }
2307
2310
 
@@ -253,6 +253,12 @@ enum CraftNativeActions {
253
253
  CraftNativeFiles.pickVideo(options) { complete(requestToken, $0, completion) }
254
254
  }
255
255
  return
256
+ case ("Files", "pickFile"):
257
+ let options = args.first as? [String: Any] ?? [:]
258
+ DispatchQueue.main.async {
259
+ CraftNativeFiles.pickFile(options) { complete(requestToken, $0, completion) }
260
+ }
261
+ return
256
262
  case ("Files", "download"):
257
263
  download(requestToken: requestToken, args: args, completion: completion)
258
264
  return