craft-native 0.0.122 → 0.0.124

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.
@@ -4,6 +4,13 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statS
4
4
  import { dirname, join, resolve } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
  var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
7
+ var CRAFT_VERSION = (() => {
8
+ try {
9
+ return JSON.parse(readFileSync(join(dirname(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
10
+ } catch {
11
+ return "0.0.0";
12
+ }
13
+ })();
7
14
  var DEFAULT_CONFIG = {
8
15
  renderer: "web",
9
16
  version: "1.0.0",
@@ -171,6 +178,8 @@ function plistArray(values, indent = 2) {
171
178
  }
172
179
  function renderBackgroundModes(config) {
173
180
  const modes = new Set;
181
+ if (config.backgroundRefresh?.enabled)
182
+ modes.add("fetch");
174
183
  if (config.enableBackgroundLocation)
175
184
  modes.add("location");
176
185
  if (config.enableBackgroundTasks)
@@ -184,12 +193,30 @@ function renderBackgroundModes(config) {
184
193
  ${plistArray([...modes])}
185
194
  </array>`;
186
195
  }
196
+ function backgroundRefreshIdentifier(config) {
197
+ if (!config.backgroundRefresh?.enabled)
198
+ return null;
199
+ return config.backgroundRefresh.identifier?.trim() || `${config.bundleId}.refresh`;
200
+ }
201
+ function renderBackgroundTaskIdentifiers(config) {
202
+ const identifier = backgroundRefreshIdentifier(config);
203
+ if (!identifier)
204
+ return "";
205
+ return ` <key>BGTaskSchedulerPermittedIdentifiers</key>
206
+ <array>
207
+ ${plistArray([identifier])}
208
+ </array>`;
209
+ }
210
+ function associatedDomainEntries(domains) {
211
+ return [...new Set((domains ?? []).map((value) => value.trim()).filter(Boolean).map((value) => /^[a-z]+:/i.test(value) ? value : `applinks:${value}`))];
212
+ }
187
213
  function renderEntitlements(config) {
188
214
  const entries = [];
189
- if (config.associatedDomains?.length) {
215
+ const associatedDomains = associatedDomainEntries(config.associatedDomains);
216
+ if (associatedDomains.length) {
190
217
  entries.push(` <key>com.apple.developer.associated-domains</key>
191
218
  <array>
192
- ${plistArray(config.associatedDomains)}
219
+ ${plistArray(associatedDomains)}
193
220
  </array>`);
194
221
  }
195
222
  if (config.appGroups?.length) {
@@ -373,6 +400,142 @@ function renderAssetCatalog(output, config) {
373
400
  info: { author: "xcode", version: 1 }
374
401
  }, null, 2)}
375
402
  `);
403
+ renderLaunchLogo(catalog, config);
404
+ }
405
+ var LAUNCH_LOGO = "LaunchLogo";
406
+ function renderLaunchLogo(catalog, config) {
407
+ const imageset = join(catalog, `${LAUNCH_LOGO}.imageset`);
408
+ rmSync(imageset, { recursive: true, force: true });
409
+ if (!config.splashImagePath)
410
+ return;
411
+ mkdirSync(imageset, { recursive: true });
412
+ const images = [];
413
+ let vector = false;
414
+ for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
415
+ if (!source)
416
+ continue;
417
+ if (!existsSync(source))
418
+ throw new Error(`Splash image not found: ${source}`);
419
+ const extension = source.split(".").pop().toLowerCase();
420
+ if (extension === "svg" || extension === "pdf")
421
+ vector = true;
422
+ const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
423
+ cpSync(source, join(imageset, filename));
424
+ images.push({
425
+ ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
426
+ filename,
427
+ idiom: "universal"
428
+ });
429
+ }
430
+ writeFileSync(join(imageset, "Contents.json"), `${JSON.stringify({
431
+ images,
432
+ info: { author: "xcode", version: 1 },
433
+ ...vector ? { properties: { "preserves-vector-representation": true } } : {}
434
+ }, null, 2)}
435
+ `);
436
+ }
437
+ function renderLaunchScreen(config) {
438
+ const image = config.splashImagePath ? `
439
+ <key>UIImageName</key>
440
+ <string>${LAUNCH_LOGO}</string>` : "";
441
+ return ` <key>UIColorName</key>
442
+ <string>LaunchBackground</string>${image}`;
443
+ }
444
+ function hexChannels(hex) {
445
+ const value = hex?.trim().replace(/^#/, "") ?? "";
446
+ const full = value.length === 3 ? [...value].map((digit) => digit + digit).join("") : value.slice(0, 6);
447
+ if (!/^[0-9a-f]{6}$/i.test(full))
448
+ return null;
449
+ return [0, 2, 4].map((index) => Number.parseInt(full.slice(index, index + 2), 16));
450
+ }
451
+ function readableTextOn(background) {
452
+ const channels = hexChannels(background);
453
+ if (!channels)
454
+ return "#0f172a";
455
+ const [r, g, b] = channels.map((channel) => {
456
+ const c = channel / 255;
457
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
458
+ });
459
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
460
+ return luminance > 0.4 ? "#0f172a" : "#f8fafc";
461
+ }
462
+ function renderOfflinePage(config) {
463
+ const light = hexChannels(config.backgroundColor) ? config.backgroundColor : "#ffffff";
464
+ const dark = hexChannels(config.backgroundColorDark) ? config.backgroundColorDark : light;
465
+ const name = xmlEscape(config.appName);
466
+ return `<!DOCTYPE html>
467
+ <html lang="en">
468
+ <head>
469
+ <meta charset="UTF-8">
470
+ <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
471
+ <meta name="color-scheme" content="light dark">
472
+ <title>${name}</title>
473
+ <style>
474
+ :root { --background: ${light}; --text: ${readableTextOn(light)}; }
475
+ @media (prefers-color-scheme: dark) {
476
+ :root { --background: ${dark}; --text: ${readableTextOn(dark)}; }
477
+ }
478
+ * { margin: 0; padding: 0; box-sizing: border-box; }
479
+ html, body { height: 100%; }
480
+ body {
481
+ font: -apple-system-body;
482
+ font-family: -apple-system, system-ui, sans-serif;
483
+ background: var(--background);
484
+ color: var(--text);
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ text-align: center;
489
+ padding: calc(env(safe-area-inset-top) + 24px) calc(env(safe-area-inset-right) + 32px) calc(env(safe-area-inset-bottom) + 24px) calc(env(safe-area-inset-left) + 32px);
490
+ -webkit-user-select: none;
491
+ user-select: none;
492
+ -webkit-tap-highlight-color: transparent;
493
+ }
494
+ main { max-width: 22rem; }
495
+ .app { font: -apple-system-subheadline; font-weight: 600; opacity: 0.6; margin-bottom: 0.5rem; }
496
+ h1 { font: -apple-system-title1; font-weight: 700; margin-bottom: 0.75rem; }
497
+ p { opacity: 0.72; line-height: 1.4; }
498
+ button {
499
+ font: -apple-system-headline;
500
+ margin-top: 1.75rem;
501
+ min-width: 9rem;
502
+ min-height: 44px;
503
+ padding: 0.75rem 1.75rem;
504
+ border: 0;
505
+ border-radius: 999px;
506
+ background: var(--text);
507
+ color: var(--background);
508
+ cursor: pointer;
509
+ }
510
+ button:active { opacity: 0.7; }
511
+ button:disabled { opacity: 0.5; }
512
+ </style>
513
+ </head>
514
+ <body>
515
+ <main>
516
+ <div class="app">${name}</div>
517
+ <h1>You're offline</h1>
518
+ <p>${name} could not reach the internet. Check your connection, then try again.</p>
519
+ <button id="retry" type="button">Retry</button>
520
+ </main>
521
+ <script>
522
+ (function () {
523
+ var button = document.getElementById('retry');
524
+ function retry() {
525
+ button.disabled = true;
526
+ button.textContent = 'Retrying';
527
+ var handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.craft;
528
+ if (handler) handler.postMessage({ action: 'retryRemote' });
529
+ else location.reload();
530
+ // Still here a while later means the page is still out of reach.
531
+ setTimeout(function () { button.disabled = false; button.textContent = 'Retry'; }, 12000);
532
+ }
533
+ button.addEventListener('click', retry);
534
+ })();
535
+ </script>
536
+ </body>
537
+ </html>
538
+ `;
376
539
  }
377
540
  function syncWebAssets(source, output) {
378
541
  const sourcePath = resolve(source);
@@ -482,14 +645,15 @@ async function init(options) {
482
645
  config.enableGeolocation = true;
483
646
  writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
484
647
  const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
485
- const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
648
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
486
649
  writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
487
650
  cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
488
651
  cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
489
652
  cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
490
653
  cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
654
+ cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
491
655
  const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
492
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
656
+ const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
493
657
  <true/>
494
658
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
495
659
  <true/>` : "");
@@ -579,45 +743,7 @@ ${nativeDependencies.join(`
579
743
  writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
580
744
  writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
581
745
  }
582
- const placeholderHtml = `<!DOCTYPE html>
583
- <html>
584
- <head>
585
- <meta charset="UTF-8">
586
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
587
- <title>${xmlEscape(displayName)}</title>
588
- <style>
589
- * { margin: 0; padding: 0; box-sizing: border-box; }
590
- body {
591
- font-family: -apple-system, system-ui, sans-serif;
592
- background: ${config.backgroundColor};
593
- color: white;
594
- min-height: 100vh;
595
- display: flex;
596
- justify-content: center;
597
- align-items: center;
598
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
599
- }
600
- .container { text-align: center; padding: 2rem; }
601
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
602
- p { opacity: 0.7; }
603
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
604
- </style>
605
- </head>
606
- <body>
607
- <div class="container">
608
- <h1>\u26A1 ${xmlEscape(displayName)}</h1>
609
- <p>Built with Craft iOS</p>
610
- <p class="ready" id="status">Waiting for Craft bridge...</p>
611
- </div>
612
- <script>
613
- window.addEventListener('craftReady', (e) => {
614
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
615
- console.log('Craft capabilities:', e.detail.capabilities);
616
- });
617
- </script>
618
- </body>
619
- </html>`;
620
- writeFileSync(join(output, "dist", "index.html"), placeholderHtml);
746
+ writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
621
747
  console.log("\u2705 Project initialized");
622
748
  console.log("");
623
749
  console.log("Next steps:");
@@ -804,11 +930,15 @@ export {
804
930
  renderRuntimeSettings,
805
931
  renderPrivacyManifest,
806
932
  renderOrientations,
933
+ renderOfflinePage,
934
+ renderLaunchScreen,
807
935
  renderEntitlements,
808
936
  renderDeviceFamilies,
937
+ renderBackgroundTaskIdentifiers,
809
938
  renderBackgroundModes,
810
939
  renderAppearance,
811
940
  renderAppBoundDomains,
941
+ readableTextOn,
812
942
  productName,
813
943
  pngHasAlpha,
814
944
  pickSimulator,
@@ -817,5 +947,8 @@ export {
817
947
  installRuntime,
818
948
  init,
819
949
  build,
820
- bootSimulator
950
+ bootSimulator,
951
+ backgroundRefreshIdentifier,
952
+ associatedDomainEntries,
953
+ LAUNCH_LOGO
821
954
  };