craft-native 0.0.123 → 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) {
@@ -414,6 +441,102 @@ function renderLaunchScreen(config) {
414
441
  return ` <key>UIColorName</key>
415
442
  <string>LaunchBackground</string>${image}`;
416
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
+ `;
539
+ }
417
540
  function syncWebAssets(source, output) {
418
541
  const sourcePath = resolve(source);
419
542
  if (!existsSync(sourcePath))
@@ -522,14 +645,15 @@ async function init(options) {
522
645
  config.enableGeolocation = true;
523
646
  writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
524
647
  const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
525
- 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);
526
649
  writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
527
650
  cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
528
651
  cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
529
652
  cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
530
653
  cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
654
+ cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
531
655
  const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
532
- 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(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(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>
533
657
  <true/>
534
658
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
535
659
  <true/>` : "");
@@ -619,45 +743,7 @@ ${nativeDependencies.join(`
619
743
  writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
620
744
  writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
621
745
  }
622
- const placeholderHtml = `<!DOCTYPE html>
623
- <html>
624
- <head>
625
- <meta charset="UTF-8">
626
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
627
- <title>${xmlEscape(displayName)}</title>
628
- <style>
629
- * { margin: 0; padding: 0; box-sizing: border-box; }
630
- body {
631
- font-family: -apple-system, system-ui, sans-serif;
632
- background: ${config.backgroundColor};
633
- color: white;
634
- min-height: 100vh;
635
- display: flex;
636
- justify-content: center;
637
- align-items: center;
638
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
639
- }
640
- .container { text-align: center; padding: 2rem; }
641
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
642
- p { opacity: 0.7; }
643
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
644
- </style>
645
- </head>
646
- <body>
647
- <div class="container">
648
- <h1>\u26A1 ${xmlEscape(displayName)}</h1>
649
- <p>Built with Craft iOS</p>
650
- <p class="ready" id="status">Waiting for Craft bridge...</p>
651
- </div>
652
- <script>
653
- window.addEventListener('craftReady', (e) => {
654
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
655
- console.log('Craft capabilities:', e.detail.capabilities);
656
- });
657
- </script>
658
- </body>
659
- </html>`;
660
- writeFileSync(join(output, "dist", "index.html"), placeholderHtml);
746
+ writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
661
747
  console.log("\u2705 Project initialized");
662
748
  console.log("");
663
749
  console.log("Next steps:");
@@ -844,12 +930,15 @@ export {
844
930
  renderRuntimeSettings,
845
931
  renderPrivacyManifest,
846
932
  renderOrientations,
933
+ renderOfflinePage,
847
934
  renderLaunchScreen,
848
935
  renderEntitlements,
849
936
  renderDeviceFamilies,
937
+ renderBackgroundTaskIdentifiers,
850
938
  renderBackgroundModes,
851
939
  renderAppearance,
852
940
  renderAppBoundDomains,
941
+ readableTextOn,
853
942
  productName,
854
943
  pngHasAlpha,
855
944
  pickSimulator,
@@ -859,5 +948,7 @@ export {
859
948
  init,
860
949
  build,
861
950
  bootSimulator,
951
+ backgroundRefreshIdentifier,
952
+ associatedDomainEntries,
862
953
  LAUNCH_LOGO
863
954
  };