catalyst-core 1.1.0 → 1.2.0-canary.2

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.
Files changed (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -0,0 +1,621 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RESOLVER_ONLY_KEYS = exports.MAX_BUILD_NUMBER = exports.BUILD_NUMBER_EPOCH_MS = void 0;
7
+ exports.appIdProblem = appIdProblem;
8
+ exports.assertReleaseHost = assertReleaseHost;
9
+ exports.computeBuildNumber = computeBuildNumber;
10
+ exports.computeStartUrl = computeStartUrl;
11
+ exports.describePrivateHost = describePrivateHost;
12
+ exports.isPlaceholderAppId = isPlaceholderAppId;
13
+ exports.isPrivateIPv4 = isPrivateIPv4;
14
+ exports.legacyPlatformVersion = legacyPlatformVersion;
15
+ exports.localMachineHosts = localMachineHosts;
16
+ exports.parseAppUrl = parseAppUrl;
17
+ exports.privateHostReason = privateHostReason;
18
+ exports.resolveAppVersion = resolveAppVersion;
19
+ exports.resolveNativeConfig = resolveNativeConfig;
20
+ var _fs = _interopRequireDefault(require("fs"));
21
+ var _os = _interopRequireDefault(require("os"));
22
+ var _path = _interopRequireDefault(require("path"));
23
+ var _utils = require("./utils.js");
24
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
25
+ // ---------------------------------------------------------------------------
26
+ // The one resolver between config/config.json and a native build.
27
+ //
28
+ // Apps write the new shape (WEBVIEW_CONFIG.url, appId, appName,
29
+ // allowPrivateHost) or the shipped legacy one (LOCAL_IP, port, useHttps,
30
+ // android.packageName, ios.appBundleId / bundleIdentifier, per-platform
31
+ // appName). The resolver turns either into the legacy shape the Android and
32
+ // iOS shells already read, and drops the new keys, so the generic Swift and
33
+ // properties emitters never see `url`, `appId`, `appName` or
34
+ // `allowPrivateHost`. Nothing here writes config.json.
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Keys of the new shape that never reach the native emitters. */
38
+ const RESOLVER_ONLY_KEYS = exports.RESOLVER_ONLY_KEYS = ["url", "appId", "appName", "allowPrivateHost"];
39
+
40
+ /** The ids the templates ship with: a release must never go out under one. */
41
+ const TEMPLATE_APP_IDS = ["io.yourname.androidproject", "com.catalyst.framework.app", "com.debug.webview"];
42
+
43
+ /** What each shell falls back to when no id is configured. */
44
+ const DEFAULT_APP_IDS = {
45
+ android: "io.yourname.androidproject",
46
+ ios: "com.catalyst.framework.app"
47
+ };
48
+ const DEFAULT_DEV_PORT = 3005;
49
+
50
+ // The build number is the whole minutes since this instant: always rising,
51
+ // no state to keep, and far below Google Play's 2100000000 ceiling.
52
+ const BUILD_NUMBER_EPOCH_MS = exports.BUILD_NUMBER_EPOCH_MS = Date.UTC(2024, 0, 1, 0, 0, 0);
53
+ const MAX_BUILD_NUMBER = exports.MAX_BUILD_NUMBER = 2100000000;
54
+ const isPlainObject = value => value !== null && typeof value === "object" && !Array.isArray(value);
55
+
56
+ /** True for an app id a store build must not ship with. */
57
+ function isPlaceholderAppId(appId) {
58
+ const id = String(appId || "").trim().toLowerCase();
59
+ return !id || id.startsWith("com.example.") || TEMPLATE_APP_IDS.includes(id);
60
+ }
61
+
62
+ /**
63
+ * Parse `WEBVIEW_CONFIG.url` (or `--url`): an origin with an http or https
64
+ * scheme. The port defaults from the scheme; HTTPS on a port other than 443
65
+ * is refused, because the iOS shell always loads HTTPS on 443.
66
+ *
67
+ * @param {string} value
68
+ * @param {object} [options]
69
+ * @param {string} [options.source] - where the value came from, for the error
70
+ * @returns {{ scheme: "http" | "https", host: string, port: number, origin: string }}
71
+ */
72
+ function parseAppUrl(value, {
73
+ source = "WEBVIEW_CONFIG.url"
74
+ } = {}) {
75
+ const text = String(value ?? "").trim();
76
+ const fail = why => (0, _utils.nativeError)("NATIVE-004", `${source} "${text}" ${why}`);
77
+ if (!text) throw fail("is empty");
78
+ let parsed;
79
+ try {
80
+ parsed = new URL(text);
81
+ } catch (error) {
82
+ throw fail("is not a URL (expected e.g. https://shop.acme.com)");
83
+ }
84
+ const scheme = parsed.protocol.replace(/:$/, "");
85
+ if (scheme !== "http" && scheme !== "https") throw fail("must start with https:// or http://");
86
+ if (parsed.username || parsed.password) throw fail("must not contain a user name or password");
87
+ if (parsed.pathname && parsed.pathname !== "/" || parsed.search || parsed.hash) {
88
+ throw fail("must be an origin only, without a path, query or fragment");
89
+ }
90
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
91
+ if (!host) throw fail("has no host");
92
+ const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
93
+ if (scheme === "https" && port !== 443) throw fail("uses HTTPS on a port other than 443");
94
+ const hostPart = host.includes(":") ? `[${host}]` : host;
95
+ const defaultPort = scheme === "https" ? 443 : 80;
96
+ return {
97
+ scheme,
98
+ host,
99
+ port,
100
+ origin: `${scheme}://${hostPart}${port === defaultPort ? "" : `:${port}`}`
101
+ };
102
+ }
103
+
104
+ /**
105
+ * The URL the app opens on launch, built the way each shell builds it from
106
+ * the legacy keys: Android uses the configured port, iOS forces 443 under
107
+ * HTTPS. With no LOCAL_IP, an Android release falls back to 127.0.0.1 and
108
+ * iOS to localhost.
109
+ */
110
+ function computeStartUrl(platform, wv = {}, {
111
+ release = false
112
+ } = {}) {
113
+ const protocol = wv.useHttps ? "https" : "http";
114
+ const fallbackHost = platform === "android" && release ? "127.0.0.1" : "localhost";
115
+ const host = String(wv.LOCAL_IP || fallbackHost);
116
+ let port;
117
+ if (platform === "ios") port = wv.port ? wv.useHttps ? 443 : wv.port : null;else port = wv.port || 3005;
118
+ const initial = platform === "android" ? String(wv.initial_url || "") : "";
119
+ const hostPart = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
120
+ return {
121
+ protocol,
122
+ host,
123
+ port,
124
+ url: `${protocol}://${hostPart}${port ? `:${port}` : ""}/${initial}`
125
+ };
126
+ }
127
+ function ipv4ToInt(ip) {
128
+ return ip.split(".").reduce((acc, part) => acc * 256 + Number(part), 0);
129
+ }
130
+ const PRIVATE_V4_RANGES = [["0.0.0.0", 8, "not a routable address"], ["127.0.0.0", 8, "this machine's loopback address"], ["10.0.0.0", 8, "a private LAN address"], ["172.16.0.0", 12, "a private LAN address"], ["192.168.0.0", 16, "a private LAN address"], ["169.254.0.0", 16, "a link-local address"]];
131
+
132
+ /**
133
+ * True for an RFC 1918 or link-local IPv4 literal: an address a LAN could
134
+ * hand this machine. Not 0.0.0.0/8 (a bind-all address) or 127.0.0.0/8.
135
+ */
136
+ function isPrivateIPv4(host) {
137
+ const text = String(host || "").trim();
138
+ if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(text)) return false;
139
+ if (text.split(".").some(part => Number(part) > 255)) return false;
140
+ const value = ipv4ToInt(text);
141
+ return PRIVATE_V4_RANGES.some(([start, bits]) => {
142
+ if (start === "0.0.0.0" || start === "127.0.0.0") return false;
143
+ const from = ipv4ToInt(start);
144
+ return value >= from && value < from + 2 ** (32 - bits);
145
+ });
146
+ }
147
+
148
+ /** This machine's own addresses and hostname: what a dev setup writes into LOCAL_IP. */
149
+ function localMachineHosts() {
150
+ const hosts = new Set();
151
+ try {
152
+ for (const entries of Object.values(_os.default.networkInterfaces())) {
153
+ for (const entry of entries || []) hosts.add(entry.address.toLowerCase());
154
+ }
155
+ } catch (error) {
156
+ // No interfaces to compare against.
157
+ }
158
+ try {
159
+ hosts.add(_os.default.hostname().toLowerCase());
160
+ } catch (error) {
161
+ // Hostname unavailable.
162
+ }
163
+ return hosts;
164
+ }
165
+
166
+ // Reserved for documentation and testing (RFC 2606 / RFC 6761): never a
167
+ // production host.
168
+ const PLACEHOLDER_SUFFIXES = [".example", ".invalid", ".test"];
169
+ const PLACEHOLDER_DOMAINS = ["example.com", "example.org", "example.net"];
170
+
171
+ /**
172
+ * Why a host can't serve a store build, or null when it can. Covers
173
+ * localhost, loopback, RFC 1918, link-local, mDNS (.local) names, this
174
+ * machine's own current addresses, and reserved placeholder domains. The
175
+ * reason reads after "<host> is", e.g. "192.168.1.5 is a private LAN
176
+ * address"; describePrivateHost puts the two together.
177
+ *
178
+ * @param {string} rawHost
179
+ * @param {object} [options]
180
+ * @param {Set<string>} [options.localHosts] - this machine's addresses; detected when omitted
181
+ * @returns {string | null}
182
+ */
183
+ function privateHostReason(rawHost, {
184
+ localHosts = localMachineHosts()
185
+ } = {}) {
186
+ const host = String(rawHost || "").trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
187
+ if (!host) return "empty";
188
+ if (host === "localhost" || host.endsWith(".localhost")) return "this machine";
189
+ if (host.endsWith(".local")) return "a .local name that only the local network resolves";
190
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) {
191
+ const value = ipv4ToInt(host);
192
+ for (const [start, bits, reason] of PRIVATE_V4_RANGES) {
193
+ const from = ipv4ToInt(start);
194
+ if (value >= from && value < from + 2 ** (32 - bits)) return reason;
195
+ }
196
+ } else if (host.includes(":")) {
197
+ if (host === "::") return "not a routable address";
198
+ if (host === "::1") return "this machine's loopback address";
199
+ if (/^fe[89ab]/.test(host)) return "a link-local address";
200
+ if (/^f[cd]/.test(host)) return "a private LAN address";
201
+ const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
202
+ if (mapped) return privateHostReason(mapped[1], {
203
+ localHosts
204
+ });
205
+ } else if (PLACEHOLDER_SUFFIXES.some(suffix => host.endsWith(suffix)) || PLACEHOLDER_DOMAINS.some(domain => host === domain || host.endsWith(`.${domain}`))) {
206
+ return "a placeholder domain reserved for examples";
207
+ }
208
+ if (localHosts.has(host)) return "this machine's own address";
209
+ return null;
210
+ }
211
+
212
+ /** "localhost is this machine": a host and its privateHostReason, for messages. */
213
+ function describePrivateHost(host, reason) {
214
+ const text = String(host || "").trim();
215
+ return text ? `${text} is ${reason}` : "the host is empty";
216
+ }
217
+
218
+ /**
219
+ * Fail a release whose start URL users' devices could never reach.
220
+ * `WEBVIEW_CONFIG.allowPrivateHost: true` downgrades the failure to a
221
+ * warning for internal apps. Plain HTTP is only ever a warning.
222
+ *
223
+ * @param {"android"|"ios"} platform
224
+ * @param {object} wv - the resolved (legacy-shape) WEBVIEW_CONFIG
225
+ * @param {object} [options]
226
+ * @param {boolean} [options.allowPrivateHost]
227
+ * @param {(message: string, type?: string) => void} [options.log]
228
+ * @param {Set<string>} [options.localHosts]
229
+ * @returns {{ protocol: string, host: string, port: number | null, url: string }}
230
+ */
231
+ function assertReleaseHost(platform, wv, {
232
+ allowPrivateHost = false,
233
+ log = () => {},
234
+ localHosts
235
+ } = {}) {
236
+ const start = computeStartUrl(platform, wv, {
237
+ release: true
238
+ });
239
+ const reason = privateHostReason(start.host, {
240
+ localHosts
241
+ });
242
+ if (reason) {
243
+ if (!allowPrivateHost) {
244
+ throw (0, _utils.nativeError)("NATIVE-001", `${start.url} (${describePrivateHost(start.host, reason)})`);
245
+ }
246
+ log(`Release start URL ${start.url}: ${describePrivateHost(start.host, reason)}. Allowed by WEBVIEW_CONFIG.allowPrivateHost`, "warning");
247
+ }
248
+ if (!wv.useHttps) {
249
+ log(`Release start URL ${start.url} uses plain HTTP. Use an https:// url for store builds.`, "warning");
250
+ }
251
+ return start;
252
+ }
253
+
254
+ /**
255
+ * The build number for this build: whole minutes since 2024-01-01T00:00:00Z,
256
+ * or `CATALYST_BUILD_NUMBER` when set (for an app that already uploaded a
257
+ * larger number, or a CI that wants its own).
258
+ *
259
+ * @param {object} [options]
260
+ * @param {Record<string, string | undefined>} [options.env]
261
+ * @param {number} [options.now] - ms since the Unix epoch
262
+ * @returns {{ buildNumber: number, buildNumberSource: "CATALYST_BUILD_NUMBER" | "clock" }}
263
+ */
264
+ function computeBuildNumber({
265
+ env = process.env,
266
+ now = Date.now()
267
+ } = {}) {
268
+ const override = env.CATALYST_BUILD_NUMBER;
269
+ if (override !== undefined && String(override).trim() !== "") {
270
+ const text = String(override).trim();
271
+ const number = Number(text);
272
+ if (!/^\d+$/.test(text) || number < 1 || number > MAX_BUILD_NUMBER) {
273
+ throw (0, _utils.nativeError)("NATIVE-002", `CATALYST_BUILD_NUMBER is "${text}"`);
274
+ }
275
+ return {
276
+ buildNumber: number,
277
+ buildNumberSource: "CATALYST_BUILD_NUMBER"
278
+ };
279
+ }
280
+ return {
281
+ buildNumber: Math.max(1, Math.floor((now - BUILD_NUMBER_EPOCH_MS) / 60000)),
282
+ buildNumberSource: "clock"
283
+ };
284
+ }
285
+
286
+ /**
287
+ * The version an app set in the shipped `WEBVIEW_CONFIG.ios.version`
288
+ * object ({ major, minor, patch?, buildNumber? }), read the way earlier
289
+ * releases read it. Null when the object is absent.
290
+ *
291
+ * @param {unknown} value - WEBVIEW_CONFIG.ios.version
292
+ * @returns {{ versionName: string, buildNumber: number | null } | null}
293
+ */
294
+ function legacyPlatformVersion(value) {
295
+ if (!isPlainObject(value)) return null;
296
+ const versionName = `${value.major || 1}.${value.minor || 0}.${value.patch || 0}`;
297
+ const text = String(value.buildNumber ?? "").trim();
298
+ const number = Number(text);
299
+ const buildNumber = /^\d+$/.test(text) && number >= 1 && number <= MAX_BUILD_NUMBER ? number : null;
300
+ return {
301
+ versionName,
302
+ buildNumber
303
+ };
304
+ }
305
+
306
+ /**
307
+ * The marketing version (package.json `version`) and the build number.
308
+ * `CATALYST_BUILD_NUMBER` wins over everything. A `legacy` version (the
309
+ * shipped ios.version object) wins over package.json and the clock, so an
310
+ * app already in the store never ships a lower version or build number.
311
+ *
312
+ * @param {object} [options]
313
+ * @param {string} [options.cwd] - the app root
314
+ * @param {Record<string, string | undefined>} [options.env]
315
+ * @param {number} [options.now]
316
+ * @param {{ versionName: string, buildNumber: number | null } | null} [options.legacy] - legacyPlatformVersion()
317
+ * @returns {{ versionName: string | undefined, buildNumber: number, buildNumberSource: string }}
318
+ */
319
+ function resolveAppVersion({
320
+ cwd = process.cwd(),
321
+ env = process.env,
322
+ now,
323
+ legacy = null
324
+ } = {}) {
325
+ let versionName;
326
+ try {
327
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - cwd is the local app directory; the file name is fixed.
328
+ const pkg = JSON.parse(_fs.default.readFileSync(_path.default.join(cwd, "package.json"), "utf8"));
329
+ if (typeof pkg.version === "string" && pkg.version.trim()) versionName = pkg.version.trim();
330
+ } catch (error) {
331
+ // No package.json version: the native project's default applies.
332
+ }
333
+ const build = computeBuildNumber({
334
+ env,
335
+ now
336
+ });
337
+ if (!legacy) return {
338
+ versionName,
339
+ ...build
340
+ };
341
+ const keepLegacyBuild = build.buildNumberSource !== "CATALYST_BUILD_NUMBER" && legacy.buildNumber;
342
+ return {
343
+ versionName: legacy.versionName,
344
+ ...(keepLegacyBuild ? {
345
+ buildNumber: legacy.buildNumber,
346
+ buildNumberSource: "ios.version"
347
+ } : build)
348
+ };
349
+ }
350
+
351
+ /**
352
+ * The app id: `appId`, then the platform keys as shipped (Android
353
+ * `packageName`; iOS `bundleIdentifier`, then `appBundleId`, the order
354
+ * Shared.xcconfig's PRODUCT_BUNDLE_IDENTIFIER has always used). Null when
355
+ * none is set.
356
+ */
357
+ function configuredAppId(platform, wv) {
358
+ if (typeof wv.appId === "string" && wv.appId.trim()) return wv.appId.trim();
359
+ const block = isPlainObject(wv[platform]) ? wv[platform] : {};
360
+ const candidates = platform === "android" ? [block.packageName] : [block.bundleIdentifier, block.appBundleId];
361
+ const legacy = candidates.find(value => typeof value === "string" && value.trim());
362
+ return legacy ? legacy.trim() : null;
363
+ }
364
+
365
+ // Java keywords and literals: none can be a segment of an Android package name.
366
+ const JAVA_RESERVED = new Set(("abstract assert boolean break byte case catch char class const continue default do double else " + "enum extends final finally float for goto if implements import instanceof int interface long " + "native new package private protected public return short static strictfp super switch " + "synchronized this throw throws transient try void volatile while true false null").split(" "));
367
+
368
+ /**
369
+ * Why an app id is not a valid id for the platform, or null when it is.
370
+ * Android (a Java package name): two or more dot-separated segments, each
371
+ * starting with a letter, then letters, digits or underscores, and no Java
372
+ * keyword. iOS (Apple's bundle id rule): two or more segments of letters,
373
+ * digits and hyphens.
374
+ *
375
+ * @param {"android"|"ios"} platform
376
+ * @param {string} appId
377
+ * @returns {string | null}
378
+ */
379
+ function appIdProblem(platform, appId) {
380
+ const segments = String(appId).split(".");
381
+ if (segments.length < 2) return "needs at least two dot-separated parts, like com.acme.shop";
382
+ for (const segment of segments) {
383
+ if (!segment) return "has an empty part";
384
+ if (platform === "android") {
385
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(segment)) {
386
+ return `part "${segment}" must start with a letter and use only letters, digits and _`;
387
+ }
388
+ if (JAVA_RESERVED.has(segment)) return `part "${segment}" is a Java keyword`;
389
+ } else if (!/^[A-Za-z0-9-]+$/.test(segment)) {
390
+ return `part "${segment}" may use only letters, digits and -`;
391
+ }
392
+ }
393
+ return null;
394
+ }
395
+
396
+ /** Point a localhost URL (or one on the production origin) at the dev server. */
397
+ function remapToDevOrigin(value, {
398
+ devOrigin,
399
+ devHost,
400
+ productionOrigin
401
+ }) {
402
+ if (typeof value !== "string" || !value) return value;
403
+ let parsed;
404
+ try {
405
+ parsed = new URL(value);
406
+ } catch (error) {
407
+ return value;
408
+ }
409
+ if (productionOrigin && parsed.origin === productionOrigin) {
410
+ return devOrigin + value.slice(productionOrigin.length);
411
+ }
412
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
413
+ if ((host === "localhost" || host === "127.0.0.1" || host === "::1") && devHost !== "localhost") {
414
+ parsed.hostname = devHost;
415
+ const text = parsed.toString();
416
+ // URL adds a trailing slash to a bare origin; keep the user's form.
417
+ return value.endsWith("/") || !text.endsWith("/") ? text : text.slice(0, -1);
418
+ }
419
+ return value;
420
+ }
421
+
422
+ /** allowedUrls with the dev server's origin added, in the form the shells read. */
423
+ function withDevOrigin(allowedUrls, devOrigin) {
424
+ const extra = [devOrigin, `${devOrigin}/*`];
425
+ if (Array.isArray(allowedUrls)) return [...allowedUrls, ...extra.filter(u => !allowedUrls.includes(u))];
426
+ if (typeof allowedUrls === "string") {
427
+ const list = allowedUrls.split(",").map(u => u.trim()).filter(Boolean);
428
+ return [...list, ...extra.filter(u => !list.includes(u))].join(",");
429
+ }
430
+ return allowedUrls;
431
+ }
432
+
433
+ /**
434
+ * Resolve config.json into what a native build uses.
435
+ *
436
+ * Release (`release: true`, or the deprecated `<platform>.buildType:
437
+ * "release"`): the start URL comes from `url` (the `--url` flag), then
438
+ * `WEBVIEW_CONFIG.url`, then the legacy LOCAL_IP/port/useHttps. It must be a
439
+ * public host (NATIVE-001 unless `allowPrivateHost: true`), and the app id
440
+ * must not be a placeholder (NATIVE-003).
441
+ *
442
+ * Debug: the app loads the dev server at `http://<devHost>:<port>`. `url` is
443
+ * ignored. The dev origin is added to `accessControl.allowedUrls`, and a
444
+ * localhost or production `apiBaseUrl` is pointed at it. A legacy config
445
+ * whose LOCAL_IP is a public host (and no `url`) keeps loading that host,
446
+ * with a warning, and `usesDevServer` is false.
447
+ *
448
+ * The result is in memory; `configPath` is a 0600 temp copy for tools that
449
+ * read a path (Gradle's -PconfigPath). Call `cleanup()` when done.
450
+ *
451
+ * @param {"android"|"ios"} platform
452
+ * @param {object} [options]
453
+ * @param {boolean} [options.release] - `--release`
454
+ * @param {string} [options.url] - `--url`, overrides WEBVIEW_CONFIG.url for a release
455
+ * @param {string} [options.devHost] - debug: the host the app reaches the dev server on ("localhost" for emulators, simulators and adb-reversed phones; `<mac>.local` or the LAN IP for a physical iPhone)
456
+ * @param {object} [options.config] - the parsed config.json; read from configPath when omitted
457
+ * @param {string} [options.configPath] - defaults to <cwd>/config/config.json
458
+ * @param {string} [options.cwd] - the app root, for config.json and package.json
459
+ * @param {Record<string, string | undefined>} [options.env]
460
+ * @param {number} [options.now] - for the build number
461
+ * @param {Set<string>} [options.localHosts] - this machine's addresses, for the host guard
462
+ * @param {(message: string, type?: string) => void} [options.log]
463
+ */
464
+ function resolveNativeConfig(platform, {
465
+ release: releaseFlag = false,
466
+ url: urlFlag,
467
+ devHost = "localhost",
468
+ config,
469
+ configPath,
470
+ cwd = process.cwd(),
471
+ env = process.env,
472
+ now,
473
+ localHosts,
474
+ log = () => {}
475
+ } = {}) {
476
+ if (platform !== "android" && platform !== "ios") throw new Error(`Unknown platform "${platform}"`);
477
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - cwd is the local app directory; the file name is fixed.
478
+ const sourcePath = configPath || _path.default.join(cwd, "config", "config.json");
479
+ const raw = config || JSON.parse(_fs.default.readFileSync(sourcePath, "utf8"));
480
+ const base = isPlainObject(raw.WEBVIEW_CONFIG) ? raw.WEBVIEW_CONFIG : {};
481
+ const platformBlock = isPlainObject(base[platform]) ? base[platform] : {};
482
+ const configuredRelease = String(platformBlock.buildType || "").toLowerCase() === "release";
483
+ if (configuredRelease && !releaseFlag) {
484
+ log(`WEBVIEW_CONFIG.${platform}.buildType "release" is deprecated. ` + `Run catalyst buildApp:${platform} --release instead, and set buildType back to debug or remove it.`, "warning");
485
+ }
486
+ const release = Boolean(releaseFlag || configuredRelease);
487
+ const allowPrivateHost = base.allowPrivateHost === true;
488
+
489
+ // The new keys go; the legacy shape is rebuilt from them below.
490
+ const WEBVIEW_CONFIG = Object.fromEntries(Object.entries(base).filter(([key]) => !RESOLVER_ONLY_KEYS.includes(key) && key !== "release"));
491
+ for (const name of ["android", "ios"]) {
492
+ WEBVIEW_CONFIG[name] = {
493
+ ...(isPlainObject(base[name]) ? base[name] : {})
494
+ };
495
+ }
496
+ const block = WEBVIEW_CONFIG[platform];
497
+ block.buildType = release ? "release" : "debug";
498
+
499
+ // App id and name. Every reader of the platform keys (Gradle, the
500
+ // xcconfig, simctl) gets the one resolved id, so the debug and release
501
+ // ids can't drift apart.
502
+ const explicitAppId = configuredAppId(platform, base);
503
+ const appId = explicitAppId || DEFAULT_APP_IDS[platform];
504
+ if (explicitAppId) {
505
+ const problem = appIdProblem(platform, explicitAppId);
506
+ if (problem) throw (0, _utils.nativeError)("NATIVE-010", `"${explicitAppId}" ${problem}`);
507
+ }
508
+ if (typeof base.appId === "string" && base.appId.trim()) {
509
+ WEBVIEW_CONFIG.android.packageName = appId;
510
+ WEBVIEW_CONFIG.ios.appBundleId = appId;
511
+ WEBVIEW_CONFIG.ios.bundleIdentifier = appId;
512
+ } else if (platform === "ios" && explicitAppId) {
513
+ WEBVIEW_CONFIG.ios.appBundleId = explicitAppId;
514
+ WEBVIEW_CONFIG.ios.bundleIdentifier = explicitAppId;
515
+ }
516
+ const appName = typeof base.appName === "string" && base.appName.trim() || block.appName || undefined;
517
+ if (typeof base.appName === "string" && base.appName.trim()) {
518
+ WEBVIEW_CONFIG.android.appName = base.appName.trim();
519
+ WEBVIEW_CONFIG.ios.appName = base.appName.trim();
520
+ }
521
+ let startUrl;
522
+ let usesDevServer = false;
523
+ let devOrigin = null;
524
+ if (release) {
525
+ const fromFlag = urlFlag !== undefined && urlFlag !== null;
526
+ const urlValue = fromFlag ? urlFlag : base.url;
527
+ if (urlValue !== undefined && urlValue !== null && urlValue !== "") {
528
+ const origin = parseAppUrl(urlValue, {
529
+ source: fromFlag ? "--url" : "WEBVIEW_CONFIG.url"
530
+ });
531
+ WEBVIEW_CONFIG.LOCAL_IP = origin.host;
532
+ WEBVIEW_CONFIG.port = String(origin.port);
533
+ WEBVIEW_CONFIG.useHttps = origin.scheme === "https";
534
+ } else if (!base.LOCAL_IP) {
535
+ log("No production URL: set WEBVIEW_CONFIG.url (e.g. https://shop.acme.com) or pass --url.", "warning");
536
+ }
537
+ startUrl = assertReleaseHost(platform, WEBVIEW_CONFIG, {
538
+ allowPrivateHost,
539
+ log,
540
+ localHosts
541
+ }).url;
542
+ if (isPlaceholderAppId(appId)) {
543
+ throw (0, _utils.nativeError)("NATIVE-003", explicitAppId ? `"${appId}"` : `none is set, so the template's "${appId}" would ship`);
544
+ }
545
+ } else {
546
+ const legacyPublicHost = !base.url && base.LOCAL_IP && !privateHostReason(base.LOCAL_IP, {
547
+ localHosts: new Set()
548
+ });
549
+ if (legacyPublicHost) {
550
+ log(`Debug build loads ${base.LOCAL_IP} from WEBVIEW_CONFIG.LOCAL_IP. ` + "Move your production host to WEBVIEW_CONFIG.url so debug builds use the dev server.", "warning");
551
+ startUrl = computeStartUrl(platform, WEBVIEW_CONFIG).url;
552
+ } else {
553
+ usesDevServer = true;
554
+ const port = Number(raw.NODE_SERVER_PORT || base.port || DEFAULT_DEV_PORT);
555
+ const hostPart = devHost.includes(":") ? `[${devHost}]` : devHost;
556
+ devOrigin = `http://${hostPart}:${port}`;
557
+ WEBVIEW_CONFIG.LOCAL_IP = devHost;
558
+ WEBVIEW_CONFIG.port = String(port);
559
+ WEBVIEW_CONFIG.useHttps = false;
560
+ startUrl = `${devOrigin}/`;
561
+ let productionOrigin = null;
562
+ try {
563
+ productionOrigin = base.url ? parseAppUrl(base.url).origin : null;
564
+ } catch (error) {
565
+ // A bad url only matters for a release.
566
+ }
567
+ if (isPlainObject(base.accessControl) && base.accessControl.allowedUrls !== undefined) {
568
+ WEBVIEW_CONFIG.accessControl = {
569
+ ...base.accessControl,
570
+ allowedUrls: withDevOrigin(base.accessControl.allowedUrls, devOrigin)
571
+ };
572
+ }
573
+ if (typeof base.apiBaseUrl === "string") {
574
+ WEBVIEW_CONFIG.apiBaseUrl = remapToDevOrigin(base.apiBaseUrl, {
575
+ devOrigin,
576
+ devHost,
577
+ productionOrigin
578
+ });
579
+ }
580
+ }
581
+ }
582
+
583
+ // The shipped ios.version object still decides the iOS version, so an
584
+ // app already in the App Store never ships a lower one.
585
+ const legacyVersion = platform === "ios" ? legacyPlatformVersion(platformBlock.version) : null;
586
+ if (legacyVersion) {
587
+ log("WEBVIEW_CONFIG.ios.version is deprecated: set the version in package.json and, for a " + "build number above the automatic one, CATALYST_BUILD_NUMBER. Then remove ios.version.", "warning");
588
+ }
589
+ const version = resolveAppVersion({
590
+ cwd,
591
+ env,
592
+ now,
593
+ legacy: legacyVersion
594
+ });
595
+ const resolved = {
596
+ ...raw,
597
+ WEBVIEW_CONFIG
598
+ };
599
+ const dir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), `catalyst-${platform}-config-`));
600
+ const resolvedPath = _path.default.join(dir, "config.json");
601
+ _fs.default.writeFileSync(resolvedPath, JSON.stringify(resolved, null, 2), {
602
+ mode: 0o600
603
+ });
604
+ return {
605
+ config: resolved,
606
+ WEBVIEW_CONFIG,
607
+ release,
608
+ appId,
609
+ appName,
610
+ startUrl,
611
+ usesDevServer,
612
+ devOrigin,
613
+ allowPrivateHost,
614
+ version,
615
+ configPath: resolvedPath,
616
+ cleanup: () => _fs.default.rmSync(dir, {
617
+ recursive: true,
618
+ force: true
619
+ })
620
+ };
621
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.DEFAULT_VERSION_NAME = void 0;
7
+ exports.artifactBaseName = artifactBaseName;
8
+ exports.assertReleaseFolderFree = assertReleaseFolderFree;
9
+ exports.releaseFolderName = releaseFolderName;
10
+ var _fs = _interopRequireDefault(require("fs"));
11
+ var _path = _interopRequireDefault(require("path"));
12
+ var _utils = require("./utils.js");
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ // What both platforms share about a release's output folder,
15
+ // build-output/<platform>/<version>-<build>/: the file-name-safe names and
16
+ // the guard against writing over an earlier release.
17
+
18
+ /** The version a release reports when package.json has none: what both shells ship then. */
19
+ const DEFAULT_VERSION_NAME = exports.DEFAULT_VERSION_NAME = "1.0.0";
20
+
21
+ /** A file-name-safe version of a name for the release artifacts. */
22
+ function artifactBaseName(name) {
23
+ const safe = String(name || "").trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
24
+ return safe || "app";
25
+ }
26
+
27
+ /**
28
+ * The folder name for one release: `<version>-<build>`.
29
+ *
30
+ * @param {{ versionName?: string, buildNumber: number | string }} version - resolveNativeConfig().version
31
+ */
32
+ function releaseFolderName(version) {
33
+ return `${artifactBaseName(version?.versionName || DEFAULT_VERSION_NAME)}-${version?.buildNumber}`;
34
+ }
35
+
36
+ /**
37
+ * Fail before building when the release folder already holds a store
38
+ * artifact (NATIVE-011): two releases in the same minute get the same build
39
+ * number, and the second would overwrite the first.
40
+ *
41
+ * @param {string} dir - the release folder
42
+ * @param {string[]} extensions - the artifact types, e.g. [".aab"] or [".ipa"]
43
+ */
44
+ function assertReleaseFolderFree(dir, extensions) {
45
+ let entries = [];
46
+ try {
47
+ entries = _fs.default.readdirSync(dir);
48
+ } catch (error) {
49
+ return;
50
+ }
51
+ const existing = entries.find(name => extensions.some(ext => name.toLowerCase().endsWith(ext)));
52
+ if (existing) throw (0, _utils.nativeError)("NATIVE-011", _path.default.join(dir, existing));
53
+ }