@craft-native/ios 0.0.451 → 0.0.467

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.js CHANGED
@@ -1,4 +1,212 @@
1
1
  // @bun
2
+ // ../typescript/src/native-notifications.ts
3
+ function nativeNotificationProblems(categories, nativeApp, renderer) {
4
+ if (categories === undefined)
5
+ return [];
6
+ if (!Array.isArray(categories))
7
+ return ["notificationCategories must be an array."];
8
+ const problems = [];
9
+ const categoryIds = new Set;
10
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
11
+ const text = (value) => typeof value === "string" && value.trim().length > 0;
12
+ for (const [index, category] of categories.entries()) {
13
+ const field = `notificationCategories[${index}]`;
14
+ if (!object(category) || !text(category.id)) {
15
+ problems.push(`${field} needs a nonempty id.`);
16
+ continue;
17
+ }
18
+ if (categoryIds.has(category.id))
19
+ problems.push(`${field}.id must be unique.`);
20
+ categoryIds.add(category.id);
21
+ if (!Array.isArray(category.actions) || category.actions.length < 1 || category.actions.length > 3) {
22
+ problems.push(`${field}.actions must contain 1\u20133 actions for both platforms.`);
23
+ continue;
24
+ }
25
+ const actionIds = new Set;
26
+ for (const [position, action] of category.actions.entries()) {
27
+ const path = `${field}.actions[${position}]`;
28
+ if (!object(action) || !text(action.id) || !text(action.title)) {
29
+ problems.push(`${path} needs a nonempty id and title.`);
30
+ continue;
31
+ }
32
+ if (action.id.startsWith("com.apple.") || actionIds.has(action.id))
33
+ problems.push(`${path}.id must be unique and must not use Apple's reserved prefix.`);
34
+ actionIds.add(action.id);
35
+ for (const flag of ["destructive", "foreground", "authenticationRequired"]) {
36
+ if (action[flag] !== undefined && typeof action[flag] !== "boolean")
37
+ problems.push(`${path}.${flag} must be a boolean.`);
38
+ }
39
+ if (!nativeApp?.trim() && (action.foreground !== true || renderer === "native"))
40
+ problems.push(`${path} requires nativeApp to receive native/background action events.`);
41
+ if (action.textInput !== undefined && (!object(action.textInput) || !text(action.textInput.buttonTitle) || action.textInput.placeholder !== undefined && typeof action.textInput.placeholder !== "string")) {
42
+ problems.push(`${path}.textInput needs a buttonTitle and an optional string placeholder.`);
43
+ }
44
+ }
45
+ }
46
+ return problems;
47
+ }
48
+
49
+ // ../typescript/src/native-routing.ts
50
+ function applyNativeParameterConstraints(params, rules) {
51
+ if (rules === undefined)
52
+ return true;
53
+ if (!rules || typeof rules !== "object" || Array.isArray(rules))
54
+ return false;
55
+ for (const [name, rule] of Object.entries(rules)) {
56
+ let value = params[name];
57
+ if (typeof value !== "string" || !rule || typeof rule !== "object" || Array.isArray(rule))
58
+ return false;
59
+ if (Object.keys(rule).some((key) => !["pattern", "exclude", "normalize"].includes(key)))
60
+ return false;
61
+ if (rule.normalize !== undefined && rule.normalize !== "lowercase")
62
+ return false;
63
+ if (rule.normalize === "lowercase")
64
+ value = value.toLowerCase();
65
+ if (rule.exclude !== undefined) {
66
+ if (!Array.isArray(rule.exclude) || rule.exclude.some((item) => typeof item !== "string"))
67
+ return false;
68
+ if (rule.exclude.some((item) => (rule.normalize === "lowercase" ? item.toLowerCase() : item) === value))
69
+ return false;
70
+ }
71
+ if (rule.pattern !== undefined) {
72
+ if (typeof rule.pattern !== "string")
73
+ return false;
74
+ try {
75
+ const matched = new RegExp(`^(?:${rule.pattern})$`).exec(value);
76
+ if (!matched || matched[0] !== value)
77
+ return false;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ params[name] = value;
83
+ }
84
+ return true;
85
+ }
86
+ function nativeRouteConstraintProblems(screens, constraints) {
87
+ const problems = [];
88
+ if (constraints === undefined)
89
+ return problems;
90
+ if (!constraints || typeof constraints !== "object" || Array.isArray(constraints))
91
+ return ["nativeScreenConstraints must name route parameter constraints."];
92
+ for (const [route, rules] of Object.entries(constraints)) {
93
+ const prefix = `nativeScreenConstraints[${JSON.stringify(route)}]`;
94
+ if (!Object.prototype.hasOwnProperty.call(screens ?? {}, route))
95
+ problems.push(`${prefix} names no native screen route.`);
96
+ if (!rules || typeof rules !== "object" || Array.isArray(rules)) {
97
+ problems.push(`${prefix} must name parameter constraints.`);
98
+ continue;
99
+ }
100
+ const names = route.split("/").filter((part) => part.startsWith(":")).map((part) => part.slice(1));
101
+ for (const [name, rule] of Object.entries(rules)) {
102
+ const field = `${prefix}[${JSON.stringify(name)}]`;
103
+ if (!names.includes(name))
104
+ problems.push(`${field} names no path parameter.`);
105
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
106
+ problems.push(`${field} must be a parameter constraint.`);
107
+ continue;
108
+ }
109
+ if (Object.keys(rule).some((key) => !["pattern", "exclude", "normalize"].includes(key)))
110
+ problems.push(`${field} has an unknown constraint field.`);
111
+ if (rule.exclude !== undefined && (!Array.isArray(rule.exclude) || rule.exclude.some((value) => typeof value !== "string")))
112
+ problems.push(`${field}.exclude must contain strings.`);
113
+ if (rule.normalize !== undefined && rule.normalize !== "lowercase")
114
+ problems.push(`${field}.normalize must be lowercase.`);
115
+ if (rule.pattern !== undefined) {
116
+ try {
117
+ if (typeof rule.pattern !== "string")
118
+ throw new Error("not a string");
119
+ new RegExp(`^(?:${rule.pattern})$`);
120
+ } catch {
121
+ problems.push(`${field}.pattern must be a valid regular expression.`);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ return problems;
127
+ }
128
+ function normalizeNativePath(raw) {
129
+ if (typeof raw !== "string")
130
+ return null;
131
+ let path = raw;
132
+ const cut = path.search(/[?#]/);
133
+ if (cut >= 0)
134
+ path = path.slice(0, cut);
135
+ if (!path.startsWith("/"))
136
+ return null;
137
+ path = path.replace(/\/{2,}/g, "/");
138
+ if (path.length > 1 && path.endsWith("/"))
139
+ path = path.slice(0, -1);
140
+ return path;
141
+ }
142
+ function routeSegments(path) {
143
+ return path === "/" ? [] : path.slice(1).split("/");
144
+ }
145
+ function matchNativePath(nativeScreens, raw, constraints) {
146
+ if (constraints !== undefined && (!constraints || typeof constraints !== "object" || Array.isArray(constraints)))
147
+ return null;
148
+ const path = normalizeNativePath(raw);
149
+ if (path === null)
150
+ return null;
151
+ const routes = Object.entries(nativeScreens ?? {}).map(([pattern, screen]) => ({ key: pattern, pattern: normalizeNativePath(pattern), screen })).filter((route) => route.pattern !== null && !!route.screen).map((route) => ({ ...route, segments: routeSegments(route.pattern) }));
152
+ const wild = (segments) => segments.at(-1) === "*" ? 1 : 0;
153
+ const fixed = (segments) => segments.filter((s) => s !== "*" && !s.startsWith(":")).length;
154
+ routes.sort((a, b) => wild(a.segments) - wild(b.segments) || fixed(b.segments) - fixed(a.segments) || b.segments.length - a.segments.length || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
155
+ const decode = (value) => {
156
+ try {
157
+ return decodeURIComponent(value);
158
+ } catch {
159
+ return value;
160
+ }
161
+ };
162
+ const parts = routeSegments(path);
163
+ for (const route of routes) {
164
+ const params = {};
165
+ let matched = true;
166
+ for (let i = 0;i < route.segments.length; i++) {
167
+ const segment = route.segments[i];
168
+ if (segment === "*") {
169
+ params["*"] = parts.slice(i).join("/");
170
+ break;
171
+ }
172
+ if (i >= parts.length) {
173
+ matched = false;
174
+ break;
175
+ }
176
+ if (segment.startsWith(":")) {
177
+ if (!parts[i]) {
178
+ matched = false;
179
+ break;
180
+ }
181
+ params[segment.slice(1)] = decode(parts[i]);
182
+ } else if (segment !== parts[i]) {
183
+ matched = false;
184
+ break;
185
+ }
186
+ }
187
+ if (!matched)
188
+ continue;
189
+ if (route.segments.at(-1) !== "*" && route.segments.length !== parts.length)
190
+ continue;
191
+ if (!applyNativeParameterConstraints(params, constraints?.[route.key]))
192
+ continue;
193
+ const query = raw.indexOf("?");
194
+ if (query >= 0) {
195
+ for (const item of raw.slice(query + 1).split("#")[0].split("&")) {
196
+ if (!item)
197
+ continue;
198
+ const [rawKey = "", ...rest] = item.split("=");
199
+ const key = decode(rawKey);
200
+ if (!key || key in params)
201
+ continue;
202
+ params[key] = decode(rest.join("=").replace(/\+/g, " "));
203
+ }
204
+ }
205
+ return { path, screen: route.screen, params };
206
+ }
207
+ return null;
208
+ }
209
+
2
210
  // src/index.ts
3
211
  import { gzipSync } from "zlib";
4
212
 
@@ -479,83 +687,6 @@ function resolveCraftVersion(startDir = import.meta.dir) {
479
687
  return "0.0.0";
480
688
  }
481
689
  var CRAFT_VERSION = resolveCraftVersion();
482
- function normalizeNativePath(raw) {
483
- if (typeof raw !== "string")
484
- return null;
485
- let path = raw;
486
- const cut = path.search(/[?#]/);
487
- if (cut >= 0)
488
- path = path.slice(0, cut);
489
- if (!path.startsWith("/"))
490
- return null;
491
- path = path.replace(/\/{2,}/g, "/");
492
- if (path.length > 1 && path.endsWith("/"))
493
- path = path.slice(0, -1);
494
- return path;
495
- }
496
- function routeSegments(path) {
497
- return path === "/" ? [] : path.slice(1).split("/");
498
- }
499
- function matchNativePath(nativeScreens, raw) {
500
- const path = normalizeNativePath(raw);
501
- if (path === null)
502
- return null;
503
- const routes = Object.entries(nativeScreens ?? {}).map(([pattern, screen]) => ({ pattern: normalizeNativePath(pattern), screen })).filter((route) => route.pattern !== null && !!route.screen).map((route) => ({ ...route, segments: routeSegments(route.pattern) }));
504
- const wild = (segments) => segments.at(-1) === "*" ? 1 : 0;
505
- const fixed = (segments) => segments.filter((s) => s !== "*" && !s.startsWith(":")).length;
506
- routes.sort((a, b) => wild(a.segments) - wild(b.segments) || fixed(b.segments) - fixed(a.segments) || b.segments.length - a.segments.length || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
507
- const decode = (value) => {
508
- try {
509
- return decodeURIComponent(value);
510
- } catch {
511
- return value;
512
- }
513
- };
514
- const parts = routeSegments(path);
515
- for (const route of routes) {
516
- const params = {};
517
- let matched = true;
518
- for (let i = 0;i < route.segments.length; i++) {
519
- const segment = route.segments[i];
520
- if (segment === "*") {
521
- params["*"] = parts.slice(i).join("/");
522
- break;
523
- }
524
- if (i >= parts.length) {
525
- matched = false;
526
- break;
527
- }
528
- if (segment.startsWith(":")) {
529
- if (!parts[i]) {
530
- matched = false;
531
- break;
532
- }
533
- params[segment.slice(1)] = decode(parts[i]);
534
- } else if (segment !== parts[i]) {
535
- matched = false;
536
- break;
537
- }
538
- }
539
- if (!matched)
540
- continue;
541
- if (route.segments.at(-1) !== "*" && route.segments.length !== parts.length)
542
- continue;
543
- const query = raw.indexOf("?");
544
- if (query >= 0) {
545
- for (const item of raw.slice(query + 1).split("#")[0].split("&")) {
546
- if (!item)
547
- continue;
548
- const [rawKey = "", ...rest] = item.split("=");
549
- const key = decode(rawKey);
550
- if (!key || key in params)
551
- continue;
552
- params[key] = decode(rest.join("=").replace(/\+/g, " "));
553
- }
554
- }
555
- return { path, screen: route.screen, params };
556
- }
557
- return null;
558
- }
559
690
  function isHybrid(config) {
560
691
  return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
561
692
  }
@@ -576,8 +707,9 @@ function nativeStartPath(config) {
576
707
  }
577
708
  }
578
709
  function hybridConfigProblems(config) {
579
- const problems = [];
710
+ const problems = nativeNotificationProblems(config.notificationCategories, config.nativeApp, config.renderer);
580
711
  const screens = Object.entries(config.nativeScreens ?? {});
712
+ problems.push(...nativeRouteConstraintProblems(config.nativeScreens, config.nativeScreenConstraints));
581
713
  for (const [pattern, screen] of screens) {
582
714
  if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
583
715
  problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
@@ -601,11 +733,11 @@ function hybridConfigProblems(config) {
601
733
  }
602
734
  if (hasNativeRoutes(config)) {
603
735
  for (const id of ids) {
604
- if (!matchNativePath(config.nativeScreens, id))
736
+ if (!matchNativePath(config.nativeScreens, id, config.nativeScreenConstraints))
605
737
  problems.push(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
606
738
  }
607
739
  const start = nativeStartPath(config);
608
- if (!matchNativePath(config.nativeScreens, start))
740
+ if (!matchNativePath(config.nativeScreens, start, config.nativeScreenConstraints))
609
741
  problems.push(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
610
742
  }
611
743
  if (config.nativeApp !== undefined) {
@@ -889,6 +1021,78 @@ function validateHealthKitBackground(config) {
889
1021
  if (!["immediate", "hourly", "daily", "weekly"].includes(config.healthKitBackgroundFrequency ?? "hourly"))
890
1022
  throw new Error("healthKitBackgroundFrequency must be immediate, hourly, daily or weekly");
891
1023
  }
1024
+ function validateNotificationAttachments(config) {
1025
+ if (config.enableNotificationAttachments !== undefined && typeof config.enableNotificationAttachments !== "boolean")
1026
+ throw new Error("enableNotificationAttachments must be a boolean");
1027
+ if (config.enableNotificationAttachments && config.enablePushNotifications !== true)
1028
+ throw new Error("enableNotificationAttachments requires enablePushNotifications");
1029
+ }
1030
+ var notificationServiceInclude = ` - path: CraftNotificationService.yml
1031
+ `;
1032
+ function syncNotificationService(output, config) {
1033
+ validateNotificationAttachments(config);
1034
+ const spec = join4(output, "CraftNotificationService.yml");
1035
+ if (!config.enableNotificationAttachments) {
1036
+ writeFileSync3(spec, `{}
1037
+ `);
1038
+ return;
1039
+ }
1040
+ const name = productName(config.appName);
1041
+ const folder = join4(output, "NotificationService");
1042
+ mkdirSync2(folder, { recursive: true });
1043
+ const source = readFileSync3(join4(TEMPLATES_DIR, "CraftNotificationService.swift"), "utf8");
1044
+ const destination = join4(folder, "CraftNotificationService.swift");
1045
+ if (!existsSync2(destination) || readFileSync3(destination, "utf8") !== source)
1046
+ writeFileSync3(destination, source);
1047
+ writeFileSync3(join4(folder, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
1048
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1049
+ <plist version="1.0"><dict>
1050
+ <key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
1051
+ <key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
1052
+ <key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
1053
+ <key>CFBundlePackageType</key><string>XPC!</string>
1054
+ <key>CFBundleShortVersionString</key><string>$(MARKETING_VERSION)</string>
1055
+ <key>CFBundleVersion</key><string>$(CURRENT_PROJECT_VERSION)</string>
1056
+ <key>NSExtension</key><dict>
1057
+ <key>NSExtensionPointIdentifier</key><string>com.apple.usernotifications.service</string>
1058
+ <key>NSExtensionPrincipalClass</key><string>$(PRODUCT_MODULE_NAME).CraftNotificationService</string>
1059
+ </dict>
1060
+ </dict></plist>
1061
+ `);
1062
+ writeFileSync3(spec, `targets:
1063
+ ${name}:
1064
+ dependencies:
1065
+ - target: ${name}NotificationService
1066
+ embed: true
1067
+ ${name}NotificationService:
1068
+ type: app-extension
1069
+ platform: iOS
1070
+ deploymentTarget: "${config.iosVersion || "15.0"}"
1071
+ sources:
1072
+ - NotificationService
1073
+ settings:
1074
+ INFOPLIST_FILE: NotificationService/Info.plist
1075
+ PRODUCT_NAME: "$(TARGET_NAME)"
1076
+ PRODUCT_BUNDLE_IDENTIFIER: ${config.bundleId}.notificationservice
1077
+ SWIFT_VERSION: "5.0"
1078
+ APPLICATION_EXTENSION_API_ONLY: YES
1079
+ SKIP_INSTALL: YES
1080
+ `);
1081
+ const project = join4(output, "project.yml");
1082
+ if (existsSync2(project)) {
1083
+ const text = readFileSync3(project, "utf8");
1084
+ const include = Bun.YAML.parse(text).include;
1085
+ const includes = Array.isArray(include) ? include : [include];
1086
+ const present = includes.some((entry) => entry === "CraftNotificationService.yml" || entry && typeof entry === "object" && ("path" in entry) && entry.path === "CraftNotificationService.yml");
1087
+ if (!present) {
1088
+ if (/^include:/m.test(text) && !/^include:[ \t]*\r?\n[ \t]+-/m.test(text))
1089
+ throw new Error("Add a CraftNotificationService.yml entry to the project.yml include list");
1090
+ writeFileSync3(project, /^include:/m.test(text) ? text.replace(/^include:[ \t]*\r?\n/m, `include:
1091
+ ${notificationServiceInclude}`) : `include:
1092
+ ${notificationServiceInclude}${text}`);
1093
+ }
1094
+ }
1095
+ }
892
1096
  function renderEntitlements(config) {
893
1097
  validateHealthKitBackground(config);
894
1098
  const entries = [];
@@ -1375,6 +1579,7 @@ function restoreShell(output) {
1375
1579
  writeFileSync3(file, JSON.stringify(config, null, 2));
1376
1580
  }
1377
1581
  function writeShell(output, config) {
1582
+ validateNotificationAttachments(config);
1378
1583
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
1379
1584
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
1380
1585
  const writeSource = (path, source) => {
@@ -1406,6 +1611,7 @@ function writeShell(output, config) {
1406
1611
  writeSource(join4(output, "Sources", "CraftNativeUI.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeUI.swift"), "utf8"));
1407
1612
  writeSource(join4(output, "Sources", "CraftNativeServices.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeServices.swift"), "utf8"));
1408
1613
  writeSource(join4(output, "Sources", "CraftNativeFiles.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeFiles.swift"), "utf8"));
1614
+ writeSource(join4(output, "Sources", "CraftNativeMediaOwnership.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeMediaOwnership.swift"), "utf8"));
1409
1615
  writeSource(join4(output, "Sources", "CraftVoiceRecording.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftVoiceRecording.swift"), "utf8"));
1410
1616
  writeSource(join4(output, "Sources", "CraftWebAudio.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftWebAudio.swift"), "utf8"));
1411
1617
  writeSource(join4(output, "Sources", "CraftHybrid.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftHybrid.swift"), "utf8"));
@@ -1422,6 +1628,7 @@ function writeShell(output, config) {
1422
1628
  const source = readFileSync3(join4(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
1423
1629
  writeSource(join4(output, folder, filename), source);
1424
1630
  }
1631
+ syncNotificationService(output, config);
1425
1632
  }
1426
1633
  function refreshUsageDescriptions(output, config) {
1427
1634
  const path = join4(output, "Info.plist");
@@ -1462,6 +1669,7 @@ async function init(options) {
1462
1669
  ...given
1463
1670
  };
1464
1671
  validateHealthKitBackground(config);
1672
+ validateNotificationAttachments(config);
1465
1673
  if (config.renderer !== "web" && config.renderer !== "native") {
1466
1674
  throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
1467
1675
  }
@@ -1563,7 +1771,8 @@ ${plistArray(nativeFontFiles, 2)}
1563
1771
  ${nativeDependencies.join(`
1564
1772
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1565
1773
  `));
1566
- writeFileSync3(join4(output, "project.yml"), projectYml);
1774
+ writeFileSync3(join4(output, "project.yml"), `include:
1775
+ ${notificationServiceInclude}${projectYml}`);
1567
1776
  writeFileSync3(join4(output, "Craft.entitlements"), renderEntitlements(config));
1568
1777
  writeFileSync3(join4(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1569
1778
  renderAssetCatalog(output, config);
@@ -1613,6 +1822,7 @@ async function build(options) {
1613
1822
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1614
1823
  }
1615
1824
  const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1825
+ validateNotificationAttachments(config);
1616
1826
  if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
1617
1827
  throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
1618
1828
  }
@@ -1802,6 +2012,7 @@ async function run(options) {
1802
2012
  console.log(" 4. Click Run (\u25B6\uFE0F)");
1803
2013
  }
1804
2014
  }
2015
+ var nativeRouteConstraints = true;
1805
2016
  export {
1806
2017
  LAUNCH_LOGO,
1807
2018
  assertNoApplicationSwiftSources,
@@ -1821,6 +2032,7 @@ export {
1821
2032
  matchNativePath,
1822
2033
  nativeMessages,
1823
2034
  nativeRouteBundles,
2035
+ nativeRouteConstraints,
1824
2036
  nativeRouteRuntime,
1825
2037
  nativeSensorCapabilities,
1826
2038
  nativeStartPath,