craft-native 0.0.104 → 0.0.107

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/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.104";
313
+ var version = "0.0.107";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
@@ -322,6 +322,7 @@ var import_fs = require("fs");
322
322
  var import_os = require("os");
323
323
  var import_path = require("path");
324
324
  var import_zlib = require("zlib");
325
+ var DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
325
326
  var CRC32_TABLE = (() => {
326
327
  const table = new Uint32Array(256);
327
328
  for (let n = 0;n < 256; n++) {
@@ -553,6 +554,9 @@ async function packageWindows(config, outDir) {
553
554
  outputPath: import_path.join(outDir, `${name}-${version2}.msi`),
554
555
  manufacturer: config.author || "Unknown",
555
556
  architecture: opts.architecture || windowsArchitecture(process.arch),
557
+ additionalFiles: opts.additionalFiles,
558
+ urlSchemes: opts.urlSchemes,
559
+ launchUrl: opts.launchUrl,
556
560
  certificatePath: opts.certificatePath,
557
561
  certificatePassword: opts.certificatePassword
558
562
  });
@@ -568,7 +572,9 @@ async function packageWindows(config, outDir) {
568
572
  const zipResult = await createZIP({
569
573
  name,
570
574
  version: version2,
575
+ manufacturer: config.author || "Unknown",
571
576
  binaryPath: config.binaryPath,
577
+ additionalFiles: opts.additionalFiles,
572
578
  outputPath: import_path.join(outDir, `${name}-${version2}-windows.zip`)
573
579
  });
574
580
  results.push({
@@ -593,7 +599,9 @@ async function packageLinux(config, outDir) {
593
599
  outputPath: import_path.join(outDir, `${name}_${version2}_amd64.deb`),
594
600
  description: config.description || "",
595
601
  maintainer: config.author || "Unknown",
596
- dependencies: opts.debDependencies || ["libgtk-3-0", "libwebkit2gtk-4.1-37"]
602
+ dependencies: opts.debDependencies || [...DEFAULT_DEB_DEPENDENCIES],
603
+ urlSchemes: opts.urlSchemes,
604
+ launchUrl: opts.launchUrl
597
605
  });
598
606
  results.push({
599
607
  success: debResult.success,
@@ -640,17 +648,7 @@ async function packageLinux(config, outDir) {
640
648
  }
641
649
  var URL_SCHEME = /^[a-z][a-z0-9+.-]*$/i;
642
650
  function urlTypesEntry(bundleId, schemes) {
643
- const seen = new Set;
644
- const unique = [];
645
- for (const scheme of schemes) {
646
- if (!URL_SCHEME.test(scheme))
647
- throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
648
- const key = scheme.toLowerCase();
649
- if (seen.has(key))
650
- continue;
651
- seen.add(key);
652
- unique.push(scheme);
653
- }
651
+ const unique = uniqueUrlSchemes(schemes);
654
652
  if (unique.length === 0)
655
653
  return null;
656
654
  const list = unique.map((scheme) => ` <string>${xml(scheme)}</string>`).join(`
@@ -669,6 +667,46 @@ ${list}
669
667
  </dict>
670
668
  </array>`;
671
669
  }
670
+ function uniqueUrlSchemes(schemes) {
671
+ const seen = new Set;
672
+ const unique = [];
673
+ for (const scheme of schemes) {
674
+ if (!URL_SCHEME.test(scheme))
675
+ throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
676
+ const key = scheme.toLowerCase();
677
+ if (seen.has(key))
678
+ continue;
679
+ seen.add(key);
680
+ unique.push(scheme);
681
+ }
682
+ return unique;
683
+ }
684
+ function launchPageUrl(value) {
685
+ let url;
686
+ try {
687
+ url = new URL(value);
688
+ } catch {
689
+ throw new Error(`Invalid packaged app launch URL: ${JSON.stringify(value)}`);
690
+ }
691
+ if (!["http:", "https:", "file:"].includes(url.protocol))
692
+ throw new Error(`Unsupported packaged app launch URL protocol: ${url.protocol}`);
693
+ return url.href;
694
+ }
695
+ function desktopExecArgument(value) {
696
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
697
+ }
698
+ function linuxDesktopEntry(name, binaryName, urlSchemes = [], launchUrl) {
699
+ const schemes = uniqueUrlSchemes(urlSchemes);
700
+ const page = launchUrl ? ` --url ${desktopExecArgument(launchPageUrl(launchUrl))}` : "";
701
+ return `[Desktop Entry]
702
+ Type=Application
703
+ Name=${name}
704
+ Exec=/usr/bin/${binaryName}${page}${schemes.length ? " --deep-link %u" : ""}
705
+ Terminal=false
706
+ Categories=Utility;
707
+ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${scheme.toLowerCase()};`).join("")}
708
+ ` : ""}`;
709
+ }
672
710
  function macOSInfoPlist(metadata) {
673
711
  const entries = [
674
712
  ["CFBundleExecutable", metadata.name],
@@ -706,7 +744,7 @@ function codesignArguments(opts) {
706
744
  "--force",
707
745
  "--sign",
708
746
  opts.identity,
709
- "--timestamp",
747
+ ...opts.identity === "-" ? [] : ["--timestamp"],
710
748
  ...opts.hardenedRuntime ? ["--options", "runtime"] : [],
711
749
  ...opts.entitlements ? ["--entitlements", opts.entitlements] : [],
712
750
  "--deep",
@@ -934,6 +972,10 @@ function deterministicGuid(value) {
934
972
  const hex = digest.join("").toUpperCase();
935
973
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
936
974
  }
975
+ var WINDOWS_NOTIFICATION_ID_FILE = "craft-notification-id.txt";
976
+ function windowsNotificationAppId(name, manufacturer) {
977
+ return `Craft.${deterministicGuid(`${manufacturer.trim() || "Unknown"}/${name}`)}`;
978
+ }
937
979
  function windowsArchitecture(architecture) {
938
980
  if (architecture === "ia32" || architecture === "x86")
939
981
  return "x86";
@@ -941,7 +983,7 @@ function windowsArchitecture(architecture) {
941
983
  return architecture;
942
984
  throw new Error(`Unsupported Windows package architecture: ${architecture}`);
943
985
  }
944
- function renderWixSource(opts, sourceName) {
986
+ function renderWixSource(opts, sourceName, additionalFileNames = []) {
945
987
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?$/.test(opts.version))
946
988
  throw new Error(`MSI version must have 3 or 4 numeric parts: ${opts.version}`);
947
989
  const id = wixIdentifier(opts.name);
@@ -949,6 +991,9 @@ function renderWixSource(opts, sourceName) {
949
991
  const upgradeCode = deterministicGuid(`${manufacturer}/${opts.name}`);
950
992
  const programFilesFolder = opts.architecture === "x86" ? "ProgramFilesFolder" : "ProgramFiles64Folder";
951
993
  const win64 = opts.architecture === "x86" ? "no" : "yes";
994
+ const notificationAppId = windowsNotificationAppId(opts.name, manufacturer);
995
+ const schemes = uniqueUrlSchemes(opts.urlSchemes || []);
996
+ const page = opts.launchUrl ? ` --url &quot;${xml(launchPageUrl(opts.launchUrl))}&quot;` : "";
952
997
  return `<?xml version="1.0" encoding="UTF-8"?>
953
998
  <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
954
999
  <Product Id="*" Name="${xml(opts.name)}" Language="1033" Version="${opts.version}" Manufacturer="${xml(manufacturer)}" UpgradeCode="${upgradeCode}">
@@ -959,18 +1004,59 @@ function renderWixSource(opts, sourceName) {
959
1004
  <Directory Id="${programFilesFolder}">
960
1005
  <Directory Id="INSTALLFOLDER" Name="${xml(opts.name)}">
961
1006
  <Component Id="${id}Executable" Guid="*" Win64="${win64}">
962
- <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes" />
1007
+ <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes">
1008
+ <Shortcut Id="${id}StartMenuShortcut" Directory="ProgramMenuFolder" Name="${xml(opts.name)}" WorkingDirectory="INSTALLFOLDER"${page ? ` Arguments="${page.trim()}"` : ""}>
1009
+ <ShortcutProperty Key="System.AppUserModel.ID" Value="${notificationAppId}" />
1010
+ </Shortcut>
1011
+ </File>
1012
+ </Component>
1013
+ <Component Id="${id}NotificationIdentity" Guid="*" Win64="${win64}">
1014
+ <File Id="${id}NotificationIdentityFile" Source="${WINDOWS_NOTIFICATION_ID_FILE}" KeyPath="yes" />
963
1015
  </Component>
1016
+ ${additionalFileNames.map((name, index) => ` <Component Id="${id}Extra${index}" Guid="*" Win64="${win64}">
1017
+ <File Id="${id}ExtraFile${index}" Source="${xml(name)}" KeyPath="yes" />
1018
+ </Component>`).join(`
1019
+ `)}
1020
+ ${schemes.map((scheme, index) => ` <Component Id="${id}Protocol${index}" Guid="${deterministicGuid(`${manufacturer}/${opts.name}/protocol/${scheme.toLowerCase()}`)}" Win64="${win64}">
1021
+ <RegistryKey Root="HKLM" Key="Software\\Classes\\${xml(scheme.toLowerCase())}" Action="createAndRemoveOnUninstall">
1022
+ <RegistryValue Type="string" Value="URL:${xml(opts.name)} Protocol" KeyPath="yes" />
1023
+ <RegistryValue Type="string" Name="URL Protocol" Value="" />
1024
+ <RegistryKey Key="shell\\open\\command">
1025
+ <RegistryValue Type="string" Value="&quot;[#${id}File]&quot;${page} --deep-link &quot;%1&quot;" />
1026
+ </RegistryKey>
1027
+ </RegistryKey>
1028
+ </Component>`).join(`
1029
+ `)}
964
1030
  </Directory>
965
1031
  </Directory>
1032
+ <Directory Id="ProgramMenuFolder" />
966
1033
  </Directory>
967
1034
  <Feature Id="ProductFeature" Title="${xml(opts.name)}" Level="1">
968
1035
  <ComponentRef Id="${id}Executable" />
1036
+ <ComponentRef Id="${id}NotificationIdentity" />
1037
+ ${additionalFileNames.map((_, index) => ` <ComponentRef Id="${id}Extra${index}" />`).join(`
1038
+ `)}
1039
+ ${schemes.map((_, index) => ` <ComponentRef Id="${id}Protocol${index}" />`).join(`
1040
+ `)}
969
1041
  </Feature>
970
1042
  </Product>
971
1043
  </Wix>
972
1044
  `;
973
1045
  }
1046
+ function windowsCompanionFiles(paths, executableName) {
1047
+ const seen = new Set([executableName.toLowerCase(), WINDOWS_NOTIFICATION_ID_FILE.toLowerCase()]);
1048
+ return (paths || []).map((path) => {
1049
+ const name = import_path.basename(path);
1050
+ if (!name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name) || /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(name))
1051
+ throw new Error(`Invalid Windows companion file name: ${JSON.stringify(name)}`);
1052
+ if (seen.has(name.toLowerCase()))
1053
+ throw new Error(`Duplicate Windows package file: ${name}`);
1054
+ seen.add(name.toLowerCase());
1055
+ if (!import_fs.existsSync(path) || import_fs.lstatSync(path).isDirectory())
1056
+ throw new Error(`Windows companion file not found: ${path}`);
1057
+ return { path, name };
1058
+ });
1059
+ }
974
1060
  function windowsExecutableName(name) {
975
1061
  if (name.length === 0 || name.trim() !== name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name))
976
1062
  throw new Error(`Invalid Windows application name: ${JSON.stringify(name)}`);
@@ -992,11 +1078,15 @@ async function createMSI(opts) {
992
1078
  const tempDir = import_fs.mkdtempSync(import_path.join(import_os.tmpdir(), "craft-msi-"));
993
1079
  try {
994
1080
  const binaryName = windowsExecutableName(opts.name);
1081
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
995
1082
  const sourcePath = import_path.join(tempDir, binaryName);
996
1083
  const wxsPath = import_path.join(tempDir, "installer.wxs");
997
1084
  const wixobjPath = import_path.join(tempDir, "installer.wixobj");
998
1085
  import_fs.copyFileSync(opts.binaryPath, sourcePath);
999
- import_fs.writeFileSync(wxsPath, renderWixSource(opts, binaryName));
1086
+ import_fs.writeFileSync(import_path.join(tempDir, WINDOWS_NOTIFICATION_ID_FILE), windowsNotificationAppId(opts.name, opts.manufacturer));
1087
+ for (const file of additionalFiles)
1088
+ import_fs.copyFileSync(file.path, import_path.join(tempDir, file.name));
1089
+ import_fs.writeFileSync(wxsPath, renderWixSource(opts, binaryName, additionalFiles.map((file) => file.name)));
1000
1090
  await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
1001
1091
  await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
1002
1092
  if (opts.certificatePath) {
@@ -1018,8 +1108,13 @@ async function createMSI(opts) {
1018
1108
  }
1019
1109
  async function createZIP(opts) {
1020
1110
  try {
1021
- const data = new Uint8Array(import_fs.readFileSync(opts.binaryPath));
1022
- const zip = buildZip([{ name: `${opts.name}.exe`, data }]);
1111
+ const binaryName = windowsExecutableName(opts.name);
1112
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
1113
+ const entries = [{ name: binaryName, data: new Uint8Array(import_fs.readFileSync(opts.binaryPath)) }];
1114
+ entries.push({ name: WINDOWS_NOTIFICATION_ID_FILE, data: new TextEncoder().encode(windowsNotificationAppId(opts.name, opts.manufacturer)) });
1115
+ for (const file of additionalFiles)
1116
+ entries.push({ name: file.name, data: new Uint8Array(import_fs.readFileSync(file.path)) });
1117
+ const zip = buildZip(entries);
1023
1118
  import_fs.writeFileSync(opts.outputPath, zip);
1024
1119
  return { success: true, outputPath: opts.outputPath };
1025
1120
  } catch (err) {
@@ -1049,6 +1144,7 @@ async function createDEB(opts) {
1049
1144
  return;
1050
1145
  }
1051
1146
  }
1147
+ const desktopContent = linuxDesktopEntry(opts.name, sanitizedName, opts.urlSchemes, opts.launchUrl);
1052
1148
  const tempDir = import_fs.mkdtempSync(import_path.join(import_os.tmpdir(), "craft-deb-"));
1053
1149
  const debianDir = import_path.join(tempDir, "DEBIAN");
1054
1150
  const binDir = import_path.join(tempDir, "usr", "bin");
@@ -1069,13 +1165,6 @@ Maintainer: ${maintainer}
1069
1165
  Description: ${description || opts.name}
1070
1166
  `;
1071
1167
  import_fs.writeFileSync(import_path.join(debianDir, "control"), controlContent);
1072
- const desktopContent = `[Desktop Entry]
1073
- Type=Application
1074
- Name=${opts.name}
1075
- Exec=/usr/bin/${binaryName}
1076
- Terminal=false
1077
- Categories=Utility;
1078
- `;
1079
1168
  import_fs.writeFileSync(import_path.join(applicationsDir, `${binaryName}.desktop`), desktopContent);
1080
1169
  const proc = import_child_process.spawn("dpkg-deb", ["--build", tempDir, opts.outputPath]);
1081
1170
  proc.on("close", (code) => {
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.104";
61
+ var version = "0.0.107";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -82,6 +82,7 @@ import {
82
82
  import { homedir, tmpdir } from "os";
83
83
  import { basename, join } from "path";
84
84
  import { deflateRawSync } from "zlib";
85
+ var DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
85
86
  var CRC32_TABLE = (() => {
86
87
  const table = new Uint32Array(256);
87
88
  for (let n = 0;n < 256; n++) {
@@ -313,6 +314,9 @@ async function packageWindows(config, outDir) {
313
314
  outputPath: join(outDir, `${name}-${version2}.msi`),
314
315
  manufacturer: config.author || "Unknown",
315
316
  architecture: opts.architecture || windowsArchitecture(process.arch),
317
+ additionalFiles: opts.additionalFiles,
318
+ urlSchemes: opts.urlSchemes,
319
+ launchUrl: opts.launchUrl,
316
320
  certificatePath: opts.certificatePath,
317
321
  certificatePassword: opts.certificatePassword
318
322
  });
@@ -328,7 +332,9 @@ async function packageWindows(config, outDir) {
328
332
  const zipResult = await createZIP({
329
333
  name,
330
334
  version: version2,
335
+ manufacturer: config.author || "Unknown",
331
336
  binaryPath: config.binaryPath,
337
+ additionalFiles: opts.additionalFiles,
332
338
  outputPath: join(outDir, `${name}-${version2}-windows.zip`)
333
339
  });
334
340
  results.push({
@@ -353,7 +359,9 @@ async function packageLinux(config, outDir) {
353
359
  outputPath: join(outDir, `${name}_${version2}_amd64.deb`),
354
360
  description: config.description || "",
355
361
  maintainer: config.author || "Unknown",
356
- dependencies: opts.debDependencies || ["libgtk-3-0", "libwebkit2gtk-4.1-37"]
362
+ dependencies: opts.debDependencies || [...DEFAULT_DEB_DEPENDENCIES],
363
+ urlSchemes: opts.urlSchemes,
364
+ launchUrl: opts.launchUrl
357
365
  });
358
366
  results.push({
359
367
  success: debResult.success,
@@ -400,17 +408,7 @@ async function packageLinux(config, outDir) {
400
408
  }
401
409
  var URL_SCHEME = /^[a-z][a-z0-9+.-]*$/i;
402
410
  function urlTypesEntry(bundleId, schemes) {
403
- const seen = new Set;
404
- const unique = [];
405
- for (const scheme of schemes) {
406
- if (!URL_SCHEME.test(scheme))
407
- throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
408
- const key = scheme.toLowerCase();
409
- if (seen.has(key))
410
- continue;
411
- seen.add(key);
412
- unique.push(scheme);
413
- }
411
+ const unique = uniqueUrlSchemes(schemes);
414
412
  if (unique.length === 0)
415
413
  return null;
416
414
  const list = unique.map((scheme) => ` <string>${xml(scheme)}</string>`).join(`
@@ -429,6 +427,46 @@ ${list}
429
427
  </dict>
430
428
  </array>`;
431
429
  }
430
+ function uniqueUrlSchemes(schemes) {
431
+ const seen = new Set;
432
+ const unique = [];
433
+ for (const scheme of schemes) {
434
+ if (!URL_SCHEME.test(scheme))
435
+ throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
436
+ const key = scheme.toLowerCase();
437
+ if (seen.has(key))
438
+ continue;
439
+ seen.add(key);
440
+ unique.push(scheme);
441
+ }
442
+ return unique;
443
+ }
444
+ function launchPageUrl(value) {
445
+ let url;
446
+ try {
447
+ url = new URL(value);
448
+ } catch {
449
+ throw new Error(`Invalid packaged app launch URL: ${JSON.stringify(value)}`);
450
+ }
451
+ if (!["http:", "https:", "file:"].includes(url.protocol))
452
+ throw new Error(`Unsupported packaged app launch URL protocol: ${url.protocol}`);
453
+ return url.href;
454
+ }
455
+ function desktopExecArgument(value) {
456
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
457
+ }
458
+ function linuxDesktopEntry(name, binaryName, urlSchemes = [], launchUrl) {
459
+ const schemes = uniqueUrlSchemes(urlSchemes);
460
+ const page = launchUrl ? ` --url ${desktopExecArgument(launchPageUrl(launchUrl))}` : "";
461
+ return `[Desktop Entry]
462
+ Type=Application
463
+ Name=${name}
464
+ Exec=/usr/bin/${binaryName}${page}${schemes.length ? " --deep-link %u" : ""}
465
+ Terminal=false
466
+ Categories=Utility;
467
+ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${scheme.toLowerCase()};`).join("")}
468
+ ` : ""}`;
469
+ }
432
470
  function macOSInfoPlist(metadata) {
433
471
  const entries = [
434
472
  ["CFBundleExecutable", metadata.name],
@@ -466,7 +504,7 @@ function codesignArguments(opts) {
466
504
  "--force",
467
505
  "--sign",
468
506
  opts.identity,
469
- "--timestamp",
507
+ ...opts.identity === "-" ? [] : ["--timestamp"],
470
508
  ...opts.hardenedRuntime ? ["--options", "runtime"] : [],
471
509
  ...opts.entitlements ? ["--entitlements", opts.entitlements] : [],
472
510
  "--deep",
@@ -694,6 +732,10 @@ function deterministicGuid(value) {
694
732
  const hex = digest.join("").toUpperCase();
695
733
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
696
734
  }
735
+ var WINDOWS_NOTIFICATION_ID_FILE = "craft-notification-id.txt";
736
+ function windowsNotificationAppId(name, manufacturer) {
737
+ return `Craft.${deterministicGuid(`${manufacturer.trim() || "Unknown"}/${name}`)}`;
738
+ }
697
739
  function windowsArchitecture(architecture) {
698
740
  if (architecture === "ia32" || architecture === "x86")
699
741
  return "x86";
@@ -701,7 +743,7 @@ function windowsArchitecture(architecture) {
701
743
  return architecture;
702
744
  throw new Error(`Unsupported Windows package architecture: ${architecture}`);
703
745
  }
704
- function renderWixSource(opts, sourceName) {
746
+ function renderWixSource(opts, sourceName, additionalFileNames = []) {
705
747
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?$/.test(opts.version))
706
748
  throw new Error(`MSI version must have 3 or 4 numeric parts: ${opts.version}`);
707
749
  const id = wixIdentifier(opts.name);
@@ -709,6 +751,9 @@ function renderWixSource(opts, sourceName) {
709
751
  const upgradeCode = deterministicGuid(`${manufacturer}/${opts.name}`);
710
752
  const programFilesFolder = opts.architecture === "x86" ? "ProgramFilesFolder" : "ProgramFiles64Folder";
711
753
  const win64 = opts.architecture === "x86" ? "no" : "yes";
754
+ const notificationAppId = windowsNotificationAppId(opts.name, manufacturer);
755
+ const schemes = uniqueUrlSchemes(opts.urlSchemes || []);
756
+ const page = opts.launchUrl ? ` --url &quot;${xml(launchPageUrl(opts.launchUrl))}&quot;` : "";
712
757
  return `<?xml version="1.0" encoding="UTF-8"?>
713
758
  <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
714
759
  <Product Id="*" Name="${xml(opts.name)}" Language="1033" Version="${opts.version}" Manufacturer="${xml(manufacturer)}" UpgradeCode="${upgradeCode}">
@@ -719,18 +764,59 @@ function renderWixSource(opts, sourceName) {
719
764
  <Directory Id="${programFilesFolder}">
720
765
  <Directory Id="INSTALLFOLDER" Name="${xml(opts.name)}">
721
766
  <Component Id="${id}Executable" Guid="*" Win64="${win64}">
722
- <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes" />
767
+ <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes">
768
+ <Shortcut Id="${id}StartMenuShortcut" Directory="ProgramMenuFolder" Name="${xml(opts.name)}" WorkingDirectory="INSTALLFOLDER"${page ? ` Arguments="${page.trim()}"` : ""}>
769
+ <ShortcutProperty Key="System.AppUserModel.ID" Value="${notificationAppId}" />
770
+ </Shortcut>
771
+ </File>
772
+ </Component>
773
+ <Component Id="${id}NotificationIdentity" Guid="*" Win64="${win64}">
774
+ <File Id="${id}NotificationIdentityFile" Source="${WINDOWS_NOTIFICATION_ID_FILE}" KeyPath="yes" />
723
775
  </Component>
776
+ ${additionalFileNames.map((name, index) => ` <Component Id="${id}Extra${index}" Guid="*" Win64="${win64}">
777
+ <File Id="${id}ExtraFile${index}" Source="${xml(name)}" KeyPath="yes" />
778
+ </Component>`).join(`
779
+ `)}
780
+ ${schemes.map((scheme, index) => ` <Component Id="${id}Protocol${index}" Guid="${deterministicGuid(`${manufacturer}/${opts.name}/protocol/${scheme.toLowerCase()}`)}" Win64="${win64}">
781
+ <RegistryKey Root="HKLM" Key="Software\\Classes\\${xml(scheme.toLowerCase())}" Action="createAndRemoveOnUninstall">
782
+ <RegistryValue Type="string" Value="URL:${xml(opts.name)} Protocol" KeyPath="yes" />
783
+ <RegistryValue Type="string" Name="URL Protocol" Value="" />
784
+ <RegistryKey Key="shell\\open\\command">
785
+ <RegistryValue Type="string" Value="&quot;[#${id}File]&quot;${page} --deep-link &quot;%1&quot;" />
786
+ </RegistryKey>
787
+ </RegistryKey>
788
+ </Component>`).join(`
789
+ `)}
724
790
  </Directory>
725
791
  </Directory>
792
+ <Directory Id="ProgramMenuFolder" />
726
793
  </Directory>
727
794
  <Feature Id="ProductFeature" Title="${xml(opts.name)}" Level="1">
728
795
  <ComponentRef Id="${id}Executable" />
796
+ <ComponentRef Id="${id}NotificationIdentity" />
797
+ ${additionalFileNames.map((_, index) => ` <ComponentRef Id="${id}Extra${index}" />`).join(`
798
+ `)}
799
+ ${schemes.map((_, index) => ` <ComponentRef Id="${id}Protocol${index}" />`).join(`
800
+ `)}
729
801
  </Feature>
730
802
  </Product>
731
803
  </Wix>
732
804
  `;
733
805
  }
806
+ function windowsCompanionFiles(paths, executableName) {
807
+ const seen = new Set([executableName.toLowerCase(), WINDOWS_NOTIFICATION_ID_FILE.toLowerCase()]);
808
+ return (paths || []).map((path) => {
809
+ const name = basename(path);
810
+ if (!name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name) || /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(name))
811
+ throw new Error(`Invalid Windows companion file name: ${JSON.stringify(name)}`);
812
+ if (seen.has(name.toLowerCase()))
813
+ throw new Error(`Duplicate Windows package file: ${name}`);
814
+ seen.add(name.toLowerCase());
815
+ if (!existsSync2(path) || lstatSync(path).isDirectory())
816
+ throw new Error(`Windows companion file not found: ${path}`);
817
+ return { path, name };
818
+ });
819
+ }
734
820
  function windowsExecutableName(name) {
735
821
  if (name.length === 0 || name.trim() !== name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name))
736
822
  throw new Error(`Invalid Windows application name: ${JSON.stringify(name)}`);
@@ -752,11 +838,15 @@ async function createMSI(opts) {
752
838
  const tempDir = mkdtempSync(join(tmpdir(), "craft-msi-"));
753
839
  try {
754
840
  const binaryName = windowsExecutableName(opts.name);
841
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
755
842
  const sourcePath = join(tempDir, binaryName);
756
843
  const wxsPath = join(tempDir, "installer.wxs");
757
844
  const wixobjPath = join(tempDir, "installer.wixobj");
758
845
  copyFileSync(opts.binaryPath, sourcePath);
759
- writeFileSync(wxsPath, renderWixSource(opts, binaryName));
846
+ writeFileSync(join(tempDir, WINDOWS_NOTIFICATION_ID_FILE), windowsNotificationAppId(opts.name, opts.manufacturer));
847
+ for (const file of additionalFiles)
848
+ copyFileSync(file.path, join(tempDir, file.name));
849
+ writeFileSync(wxsPath, renderWixSource(opts, binaryName, additionalFiles.map((file) => file.name)));
760
850
  await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
761
851
  await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
762
852
  if (opts.certificatePath) {
@@ -778,8 +868,13 @@ async function createMSI(opts) {
778
868
  }
779
869
  async function createZIP(opts) {
780
870
  try {
781
- const data = new Uint8Array(readFileSync(opts.binaryPath));
782
- const zip = buildZip([{ name: `${opts.name}.exe`, data }]);
871
+ const binaryName = windowsExecutableName(opts.name);
872
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
873
+ const entries = [{ name: binaryName, data: new Uint8Array(readFileSync(opts.binaryPath)) }];
874
+ entries.push({ name: WINDOWS_NOTIFICATION_ID_FILE, data: new TextEncoder().encode(windowsNotificationAppId(opts.name, opts.manufacturer)) });
875
+ for (const file of additionalFiles)
876
+ entries.push({ name: file.name, data: new Uint8Array(readFileSync(file.path)) });
877
+ const zip = buildZip(entries);
783
878
  writeFileSync(opts.outputPath, zip);
784
879
  return { success: true, outputPath: opts.outputPath };
785
880
  } catch (err) {
@@ -809,6 +904,7 @@ async function createDEB(opts) {
809
904
  return;
810
905
  }
811
906
  }
907
+ const desktopContent = linuxDesktopEntry(opts.name, sanitizedName, opts.urlSchemes, opts.launchUrl);
812
908
  const tempDir = mkdtempSync(join(tmpdir(), "craft-deb-"));
813
909
  const debianDir = join(tempDir, "DEBIAN");
814
910
  const binDir = join(tempDir, "usr", "bin");
@@ -829,13 +925,6 @@ Maintainer: ${maintainer}
829
925
  Description: ${description || opts.name}
830
926
  `;
831
927
  writeFileSync(join(debianDir, "control"), controlContent);
832
- const desktopContent = `[Desktop Entry]
833
- Type=Application
834
- Name=${opts.name}
835
- Exec=/usr/bin/${binaryName}
836
- Terminal=false
837
- Categories=Utility;
838
- `;
839
928
  writeFileSync(join(applicationsDir, `${binaryName}.desktop`), desktopContent);
840
929
  const proc = spawn("dpkg-deb", ["--build", tempDir, opts.outputPath]);
841
930
  proc.on("close", (code) => {
@@ -7,6 +7,8 @@ export type CraftAppearance = 'light' | 'dark' | 'system';
7
7
  export interface CraftConfig {
8
8
  appName: string;
9
9
  bundleId: string;
10
+ /** Opt in to UIKit + JavaScriptCore instead of the default WebView. */
11
+ renderer?: 'web' | 'native';
10
12
  version?: string;
11
13
  buildNumber?: string;
12
14
  darkMode?: boolean;
@@ -108,6 +110,8 @@ export interface InitOptions {
108
110
  }
109
111
  export interface BuildOptions {
110
112
  htmlPath?: string;
113
+ /** JavaScript bundle emitted by `stx-native compile Screen.stx --format bundle`. */
114
+ nativeBundlePath?: string;
111
115
  devServer?: string;
112
116
  output: string;
113
117
  generateProject?: boolean;
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
  var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
7
7
  var DEFAULT_CONFIG = {
8
+ renderer: "web",
8
9
  version: "1.0.0",
9
10
  buildNumber: "1",
10
11
  darkMode: true,
@@ -393,12 +394,17 @@ async function init(options) {
393
394
  teamId: teamId || "",
394
395
  ...given
395
396
  };
397
+ if (config.renderer !== "web" && config.renderer !== "native") {
398
+ throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
399
+ }
396
400
  if (config.enableBackgroundLocation)
397
401
  config.enableGeolocation = true;
398
402
  writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
399
403
  const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
400
404
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
401
405
  writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
406
+ cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
407
+ cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
402
408
  const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
403
409
  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(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
404
410
  <true/>
@@ -533,13 +539,18 @@ ${nativeDependencies.join(`
533
539
  console.log("");
534
540
  console.log("Next steps:");
535
541
  console.log(` 1. cd ${output}`);
536
- console.log(" 2. Add your web content to dist/index.html");
537
- console.log(" 3. Run: craft ios build");
542
+ if (config.renderer === "native") {
543
+ console.log(" 2. Compile your screen: stx-native compile Screen.stx --format bundle --output screen.js");
544
+ console.log(" 3. Run: craft ios build --native-bundle screen.js");
545
+ } else {
546
+ console.log(" 2. Add your web content to dist/index.html");
547
+ console.log(" 3. Run: craft ios build");
548
+ }
538
549
  console.log(" 4. Run: craft ios open");
539
550
  console.log("");
540
551
  }
541
552
  async function build(options) {
542
- const { htmlPath, devServer, output, generateProject = true } = options;
553
+ const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
543
554
  console.log(`
544
555
  \uD83D\uDCE6 Building Craft iOS project...`);
545
556
  const configPath = join(output, "craft.config.json");
@@ -547,6 +558,24 @@ async function build(options) {
547
558
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
548
559
  }
549
560
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
561
+ if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
562
+ throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
563
+ }
564
+ if (config.renderer === "native") {
565
+ if (htmlPath || devServer) {
566
+ throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
567
+ }
568
+ if (nativeBundlePath) {
569
+ const destination = join(output, "dist", "native-screen.js");
570
+ if (resolve(nativeBundlePath) !== resolve(destination))
571
+ cpSync(nativeBundlePath, destination);
572
+ }
573
+ if (!existsSync(join(output, "dist", "native-screen.js"))) {
574
+ throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with stx-native first.");
575
+ }
576
+ } else if (nativeBundlePath) {
577
+ throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
578
+ }
550
579
  if (devServer) {
551
580
  config.devServerURL = devServer;
552
581
  const origin = new URL(devServer).origin;