catalyst-core 1.2.0-canary.3 → 1.2.0-canary.4
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/changelog.md +34 -0
- package/dist/cli/hints.js +5 -0
- package/dist/errors/registry.js +2 -2
- package/dist/errors-index.json +2 -2
- package/dist/native/buildAppIos.js +17 -5
- package/dist/native/buildIos/index.js +12 -3
- package/dist/native/buildIos/signing.js +108 -21
- package/dist/native/setupServer.js +18 -8
- package/dist/native/utils.js +1 -1
- package/dist/server/expressServer.js +21 -13
- package/mcp_v2/conversion-tasks.json +29 -24
- package/mcp_v2/knowledge-base.json +36 -8
- package/mcp_v2/test/build.test.ts +32 -0
- package/mcp_v2/test/config.test.ts +140 -1
- package/mcp_v2/test/native-scripts.test.ts +129 -0
- package/mcp_v2/tools/build.js +9 -2
- package/mcp_v2/tools/config.d.ts +3 -0
- package/mcp_v2/tools/config.js +100 -44
- package/mcp_v2/tools/conversion.js +123 -21
- package/mcp_v2/tools/tasks.js +7 -0
- package/package.json +2 -2
- package/test/cli/buildErrors.test.ts +12 -0
- package/test/native/iosInstalledSigning.test.ts +140 -1
- package/test/native/setupServer.test.ts +55 -1
package/changelog.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - Unreleased
|
|
4
|
+
|
|
5
|
+
This release builds, runs and releases the Android and iOS apps from the CLI, without opening Android Studio or Xcode. The web runtime has no breaking changes. Universal apps should read the breaking changes below and the 1.1.x to 1.2.0 section of the Version Upgrades guide. After upgrading, run `npm audit fix`: the `body-parser` floor is raised to `^1.20.9`.
|
|
6
|
+
|
|
7
|
+
### One command to run, one to release
|
|
8
|
+
|
|
9
|
+
New apps have four scripts. `npm run android` and `npm run ios` check the toolchain and install what is missing after asking, start the dev server when nothing answers on `NODE_SERVER_PORT`, and build, install and open the app on an emulator, simulator or phone. Android also saves the app as a debug APK in `build-output/android/debug/` that can be shared. `npm run android:release` writes a signed App Bundle, a signed APK and `upload_certificate.pem` to `build-output/android/<version>-<build>/`, and `npm run ios:release` writes a signed IPA and a zip of its dSYMs to `build-output/ios/<version>-<build>/`. The scripts run `catalyst buildApp:android` and `catalyst buildApp:ios`, with `--release` for the store builds, so older apps keep `buildApp:<platform>` and build a release with `npm run buildApp:<platform> -- --release`. `setupEmulator:android` and `setupEmulator:ios` still work and are no longer needed.
|
|
10
|
+
|
|
11
|
+
### App config
|
|
12
|
+
|
|
13
|
+
A native app needs `WEBVIEW_CONFIG.appId`, `appName` and `url`. `appId` is the Android application id and the iOS bundle id, and `android.packageName` and `ios.bundleIdentifier` (then `ios.appBundleId`) win over it. `url` is the deployed site a release opens; debug builds load the dev server. New apps get `appId` and `appName` from the project name. A release run in a terminal asks once for an app id or website that is missing or a placeholder, and saves the answer to `config/config.json`.
|
|
14
|
+
|
|
15
|
+
### Signing
|
|
16
|
+
|
|
17
|
+
Signing files live in `credentials/android/` and `credentials/ios/`, which new apps keep out of git. The first Android release in a terminal creates one upload key per app after asking; back it up, because Google Play only accepts updates signed with it. `npx catalyst credentials:export <platform>` prints the folder as one value for the `CATALYST_ANDROID_CREDENTIALS` or `CATALYST_IOS_CREDENTIALS` CI secret. On a Mac signed in to Xcode, an iOS release signs with the Apple account or an installed App Store profile and needs no files; the order the build tries is in the iOS release guide. The version comes from `package.json`, and the build number is `CATALYST_BUILD_NUMBER` or the minutes since 2024-01-01 UTC. Android apps target API 36 and align their native libraries for 16 KB memory pages.
|
|
18
|
+
|
|
19
|
+
### Breaking changes
|
|
20
|
+
|
|
21
|
+
- An iOS bundle id with an underscore fails with `NATIVE-010`. Set `ios.bundleIdentifier` to an id without one.
|
|
22
|
+
- A release with a placeholder app id (any `com.example.` id or a template id) fails with `NATIVE-003`. A release whose start URL is `localhost`, a private or link-local address, a `.local` name or this machine fails with `NATIVE-001`, unless `WEBVIEW_CONFIG.allowPrivateHost` is `true`. Run in a terminal, the build asks for the value instead.
|
|
23
|
+
- A debug build no longer installs on a connected phone by itself. A terminal run asks once where to run the app and remembers the answer in `.catalyst/local.json`; without a terminal the emulator or simulator is used. Pass `--device` for the phone.
|
|
24
|
+
- The first debug build rewrites `config/config.json` once: a value equal to this machine's LAN address, and a private `LOCAL_IP` that `NODE_SERVER_HOSTNAME` also holds, go back to `localhost`. Each change is printed. It never runs for a release, in CI or with `allowPrivateHost: true`.
|
|
25
|
+
- Keystore passwords in `android.keystoreConfig` and `android.keystore` are no longer used. Apps that uploaded a build signed with the keystore shipped in the template, or with a key generated by an earlier release build, need an upload key reset in Play Console.
|
|
26
|
+
|
|
27
|
+
### create-catalyst-app
|
|
28
|
+
|
|
29
|
+
New apps get a new starter page, an animated HTML splash screen that is on by default (`splashScreen.minDuration` `7000`), a restyled offline page, a placeholder `public/icon.png`, and a `server/index.js` with an empty stub for `preServerInit` and each per-request hook. Their `.gitignore` covers `credentials/`, `.catalyst/` and `build-output/`.
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- `MetaTag` waits for a `split()` route's chunk before it replaces the head tags, so the server's tags are no longer removed before the route's `setMetaData` is available.
|
|
34
|
+
- Startup warns when `server/index.js` puts a lifecycle hook inside `export default { ... }`, where Catalyst never calls it. Export each hook by name.
|
|
35
|
+
- `npm run start` prints a hint when `NODE_SERVER_HOSTNAME` is not an address of this machine.
|
|
36
|
+
|
|
3
37
|
## [1.1.0] - 2026-10-22
|
|
4
38
|
|
|
5
39
|
This release contains everything in 1.0.2, namely the HTML splash screen, experimental WebMCP support and the new CLI output, plus one fix. There are no breaking API changes.
|
package/dist/cli/hints.js
CHANGED
|
@@ -27,6 +27,11 @@ const HINTS = [
|
|
|
27
27
|
: `Something is already on ${where}. Stop it, or set NODE_SERVER_PORT in config/config.json.`
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
test: /listen EADDRNOTAVAIL\b(?:.*?address not available\s+(\S+):\d+)?/i,
|
|
32
|
+
hint: (match) =>
|
|
33
|
+
`NODE_SERVER_HOSTNAME ${match[1] ? `${match[1]} ` : ""}is not an address of this machine; set it to localhost (or this machine's address) in config/config.json.`,
|
|
34
|
+
},
|
|
30
35
|
{
|
|
31
36
|
test: /SDK location not found|ANDROID_HOME/i,
|
|
32
37
|
hint: () =>
|
package/dist/errors/registry.js
CHANGED
|
@@ -330,9 +330,9 @@ export const ERROR_DEFINITIONS = {
|
|
|
330
330
|
category: "IOS",
|
|
331
331
|
defaultMessage: "iOS code signing is not set up",
|
|
332
332
|
defaultDetails:
|
|
333
|
-
"A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for.",
|
|
333
|
+
"A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for. When an App Store release finds no App Store profile for its bundle id and has nothing else to sign with, the error says so first.",
|
|
334
334
|
suggestedAction:
|
|
335
|
-
"
|
|
335
|
+
"For an App Store release, install the App Store provisioning profile for your bundle id (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles) or set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name. Otherwise, and for a device build, sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team. In CI, put the signing files in credentials/ios/ and set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios.",
|
|
336
336
|
},
|
|
337
337
|
[ERROR_CODES.IOS_ASC_API_KEY_INVALID]: {
|
|
338
338
|
category: "IOS",
|
package/dist/errors-index.json
CHANGED
|
@@ -226,8 +226,8 @@
|
|
|
226
226
|
"IOS-010": {
|
|
227
227
|
"category": "IOS",
|
|
228
228
|
"message": "iOS code signing is not set up",
|
|
229
|
-
"details": "A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for.",
|
|
230
|
-
"suggestedAction": "
|
|
229
|
+
"details": "A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for. When an App Store release finds no App Store profile for its bundle id and has nothing else to sign with, the error says so first.",
|
|
230
|
+
"suggestedAction": "For an App Store release, install the App Store provisioning profile for your bundle id (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles) or set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name. Otherwise, and for a device build, sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team. In CI, put the signing files in credentials/ios/ and set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios.",
|
|
231
231
|
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-010.md"
|
|
232
232
|
},
|
|
233
233
|
"IOS-011": {
|
|
@@ -44,6 +44,7 @@ const {
|
|
|
44
44
|
planSigningForConfig,
|
|
45
45
|
readIosCredentials,
|
|
46
46
|
resolveExportMethod,
|
|
47
|
+
settleInstalledProfile,
|
|
47
48
|
settleXcodeTeam
|
|
48
49
|
} = require("./buildIos/signing.js");
|
|
49
50
|
const {
|
|
@@ -96,10 +97,11 @@ async function askAndSave(options) {
|
|
|
96
97
|
/**
|
|
97
98
|
* A release that will sign with the Apple account in Xcode needs
|
|
98
99
|
* ios.developmentTeam. When it is unset, use the one team Xcode has that can
|
|
99
|
-
* sign this export (or ask which, in a terminal).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* sign this export (or ask which, in a terminal). A release that auto-picked
|
|
101
|
+
* an installed App Store profile pins it as ios.provisioningProfile. In a
|
|
102
|
+
* terminal the team or profile is saved to config/config.json; without one
|
|
103
|
+
* it is used for this build only. Plans with planSigningForConfig, the same
|
|
104
|
+
* function createIosBuild signs with. Returns the appConfig to build with.
|
|
103
105
|
*
|
|
104
106
|
* Config and credential errors are left for createIosBuild, which reports
|
|
105
107
|
* them the usual way.
|
|
@@ -140,6 +142,16 @@ async function ensureSigningTeam({
|
|
|
140
142
|
found?.cleanup();
|
|
141
143
|
}
|
|
142
144
|
const file = path.join(cwd, "config", "config.json");
|
|
145
|
+
const profile = settleInstalledProfile({
|
|
146
|
+
plan,
|
|
147
|
+
log,
|
|
148
|
+
save: name => writeConfigKeys(file, {
|
|
149
|
+
"WEBVIEW_CONFIG.ios.provisioningProfile": name
|
|
150
|
+
}, {
|
|
151
|
+
log
|
|
152
|
+
})
|
|
153
|
+
});
|
|
154
|
+
if (profile?.saved) return loadAppConfig();
|
|
143
155
|
const team = await settleXcodeTeam({
|
|
144
156
|
plan,
|
|
145
157
|
release,
|
|
@@ -193,7 +205,7 @@ async function main() {
|
|
|
193
205
|
BUILD_OUTPUT_PATH
|
|
194
206
|
};
|
|
195
207
|
for (const change of migration.changes) {
|
|
196
|
-
log(`config.json ${change.path}: ${change.from} -> ${change.to}`, "
|
|
208
|
+
log(`config.json ${change.path}: ${change.from} -> ${change.to}`, "note");
|
|
197
209
|
}
|
|
198
210
|
const release = flags.release || isReleaseBuildType(appConfig.WEBVIEW_CONFIG?.ios?.buildType);
|
|
199
211
|
// A store build in a terminal asks once for a missing app id or
|
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
createProfileScanner,
|
|
36
36
|
pickInstalledProfile,
|
|
37
37
|
XCODE_TEAM_HINT,
|
|
38
|
+
missingProfileError,
|
|
38
39
|
buildExportOptions,
|
|
39
40
|
writeExportOptionsPlist,
|
|
40
41
|
isSigningFailure,
|
|
@@ -186,7 +187,11 @@ function createIosBuild(config, {
|
|
|
186
187
|
const apiKey = release ? resolveApiKey() : null;
|
|
187
188
|
if (release && signingPlan.style === "automatic" && !signingPlan.teamId) {
|
|
188
189
|
credentials?.cleanup();
|
|
189
|
-
|
|
190
|
+
const detail = "a signed release with automatic signing needs WEBVIEW_CONFIG.ios.developmentTeam";
|
|
191
|
+
const missing = missingProfileError(signingPlan.missingProfileFor, detail);
|
|
192
|
+
throw missing ? nativeError("IOS-010", missing.detail, {
|
|
193
|
+
hint: missing.hint
|
|
194
|
+
}) : nativeError("IOS-010", detail, signingPlan.source === "Xcode account" ? {
|
|
190
195
|
hint: XCODE_TEAM_HINT
|
|
191
196
|
} : {});
|
|
192
197
|
}
|
|
@@ -611,7 +616,9 @@ function createIosBuild(config, {
|
|
|
611
616
|
try {
|
|
612
617
|
await runCommandStreaming("xcodebuild", ["archive", "-project", `${PROJECT_NAME}.xcodeproj`, "-scheme", SCHEME_NAME, "-configuration", "Release", "-destination", "generic/platform=iOS", "-archivePath", archivePath, ...signingArgs]);
|
|
613
618
|
} catch (error) {
|
|
614
|
-
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the release archive", error
|
|
619
|
+
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the release archive", error, {
|
|
620
|
+
missingProfileFor: signing.missingProfileFor
|
|
621
|
+
}) : error;
|
|
615
622
|
failure.stage = "Archiving the release build";
|
|
616
623
|
throw failure;
|
|
617
624
|
}
|
|
@@ -629,7 +636,9 @@ function createIosBuild(config, {
|
|
|
629
636
|
try {
|
|
630
637
|
await runCommandStreaming("xcodebuild", ["-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", optionsPlist, ...(signing.style === "automatic" ? xcodeAuthArgs(ctx.apiKey) : [])]);
|
|
631
638
|
} catch (error) {
|
|
632
|
-
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the IPA for export", error
|
|
639
|
+
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the IPA for export", error, {
|
|
640
|
+
missingProfileFor: signing.missingProfileFor
|
|
641
|
+
}) : error;
|
|
633
642
|
failure.stage = "Exporting the IPA";
|
|
634
643
|
throw failure;
|
|
635
644
|
}
|
|
@@ -10,6 +10,7 @@ exports.createProfileScanner = createProfileScanner;
|
|
|
10
10
|
exports.findInstalledProfileIdentity = findInstalledProfileIdentity;
|
|
11
11
|
exports.isReleaseBuildType = isReleaseBuildType;
|
|
12
12
|
exports.isSigningFailure = isSigningFailure;
|
|
13
|
+
exports.missingProfileError = missingProfileError;
|
|
13
14
|
exports.needsXcodeTeam = needsXcodeTeam;
|
|
14
15
|
exports.parseIdentities = parseIdentities;
|
|
15
16
|
exports.parseIdentity = parseIdentity;
|
|
@@ -27,6 +28,7 @@ exports.readProvisioningProfile = readProvisioningProfile;
|
|
|
27
28
|
exports.readXcodeTeams = readXcodeTeams;
|
|
28
29
|
exports.resolveExportMethod = resolveExportMethod;
|
|
29
30
|
exports.scanInstalledProfiles = scanInstalledProfiles;
|
|
31
|
+
exports.settleInstalledProfile = settleInstalledProfile;
|
|
30
32
|
exports.settleXcodeTeam = settleXcodeTeam;
|
|
31
33
|
exports.setupP12Signing = setupP12Signing;
|
|
32
34
|
exports.signingFailureLine = signingFailureLine;
|
|
@@ -296,13 +298,19 @@ function planIosSigning({
|
|
|
296
298
|
if (release && exportMethod === "app-store-connect" && bundleId && scanProfiles) {
|
|
297
299
|
const scan = scanProfiles();
|
|
298
300
|
const {
|
|
299
|
-
identity
|
|
301
|
+
identity,
|
|
302
|
+
missing
|
|
300
303
|
} = pickInstalledProfile(scan, {
|
|
301
304
|
bundleId,
|
|
302
305
|
teamId,
|
|
303
306
|
needsPush: notifications,
|
|
304
307
|
now
|
|
305
308
|
});
|
|
309
|
+
// Nothing else to sign with: IOS-010 leads with the missing profile.
|
|
310
|
+
if (missing) return {
|
|
311
|
+
...automatic("Xcode account"),
|
|
312
|
+
missingProfileFor: bundleId
|
|
313
|
+
};
|
|
306
314
|
if (identity) {
|
|
307
315
|
// By name, as people know it, unless another installed profile shares it.
|
|
308
316
|
const sameName = scan.profiles.filter(profile => profile.name === identity.profileName).length;
|
|
@@ -580,7 +588,9 @@ function isAppStoreProfile(profile) {
|
|
|
580
588
|
* profile (.mobileprovision, no devices, get-task-allow false) for exactly
|
|
581
589
|
* this bundle id, for `teamId` when set, with aps-environment when
|
|
582
590
|
* `needsPush`. Expired profiles are skipped; the newest one whose
|
|
583
|
-
* certificate is in the keychain wins. Without a match, `reason` says why
|
|
591
|
+
* certificate is in the keychain wins. Without a match, `reason` says why,
|
|
592
|
+
* and `missing` is true when no App Store profile for the bundle id is
|
|
593
|
+
* installed at all.
|
|
584
594
|
*
|
|
585
595
|
* @param {ReturnType<typeof scanInstalledProfiles>} scan
|
|
586
596
|
* @param {object} options
|
|
@@ -589,7 +599,7 @@ function isAppStoreProfile(profile) {
|
|
|
589
599
|
* @param {string} [options.teamId]
|
|
590
600
|
* @param {boolean} [options.needsPush]
|
|
591
601
|
* @param {Date} [options.now]
|
|
592
|
-
* @returns {{ identity: { sha1: string, name: string, teamId?: string, uuid: string, profileName?: string } | null, reason?: string }}
|
|
602
|
+
* @returns {{ identity: { sha1: string, name: string, teamId?: string, uuid: string, profileName?: string } | null, reason?: string, missing?: boolean }}
|
|
593
603
|
*/
|
|
594
604
|
function pickInstalledProfile(scan, {
|
|
595
605
|
profile,
|
|
@@ -611,13 +621,19 @@ function pickInstalledProfile(scan, {
|
|
|
611
621
|
};
|
|
612
622
|
let candidates = scan?.profiles || [];
|
|
613
623
|
/** Keep the candidates that pass; when none does, that check is the reason. */
|
|
614
|
-
const steps = wanted ? [[c => c.name === wanted || String(c.uuid).toLowerCase() === wanted.toLowerCase(), () => "no installed profile has that name or UUID"], [c => !(c.expires && c.expires <= now.getTime()), left => `it expired on ${left[0].expirationDate}`], [c => profileMatchesBundleId(c.applicationIdentifier, bundleId), left => `it is for ${appIdOf(left[0])}, not ${bundleId}`]] : [[c => c.file.endsWith(".mobileprovision") && c.applicationIdentifier && appIdOf(c) === bundleId, () => `no installed profile is for exactly ${bundleId}
|
|
615
|
-
for (const [keep, reason] of steps) {
|
|
624
|
+
const steps = wanted ? [[c => c.name === wanted || String(c.uuid).toLowerCase() === wanted.toLowerCase(), () => "no installed profile has that name or UUID"], [c => !(c.expires && c.expires <= now.getTime()), left => `it expired on ${left[0].expirationDate}`], [c => profileMatchesBundleId(c.applicationIdentifier, bundleId), left => `it is for ${appIdOf(left[0])}, not ${bundleId}`]] : [[c => c.file.endsWith(".mobileprovision") && c.applicationIdentifier && appIdOf(c) === bundleId, () => `no installed profile is for exactly ${bundleId}`, true], [isAppStoreProfile, () => `no installed App Store profile is for ${bundleId}`, true], [c => !teamId || c.teamId === teamId, () => `no installed App Store profile for ${bundleId} is for team ${teamId}`], [c => !needsPush || Boolean(c.apsEnvironment), () => `no installed App Store profile for ${bundleId} has push notifications`], [c => !(c.expires && c.expires <= now.getTime()), () => `every installed App Store profile for ${bundleId} has expired`]];
|
|
625
|
+
for (const [keep, reason, missing] of steps) {
|
|
616
626
|
const left = candidates.filter(keep);
|
|
617
|
-
if (!left.length)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
627
|
+
if (!left.length) {
|
|
628
|
+
return missing ? {
|
|
629
|
+
identity: null,
|
|
630
|
+
reason: reason(candidates),
|
|
631
|
+
missing: true
|
|
632
|
+
} : {
|
|
633
|
+
identity: null,
|
|
634
|
+
reason: reason(candidates)
|
|
635
|
+
};
|
|
636
|
+
}
|
|
621
637
|
candidates = left;
|
|
622
638
|
}
|
|
623
639
|
const newestFirst = [...candidates].sort((a, b) => b.expires - a.expires);
|
|
@@ -685,6 +701,25 @@ function findInstalledProfileIdentity({
|
|
|
685
701
|
|
|
686
702
|
const XCODE_TEAM_HINT = exports.XCODE_TEAM_HINT = "Open Xcode > Settings > Accounts and sign in with the Apple account that has your company's team, or set WEBVIEW_CONFIG.ios.developmentTeam.";
|
|
687
703
|
|
|
704
|
+
/**
|
|
705
|
+
* IOS-010's detail and hint for an App Store release that found no installed
|
|
706
|
+
* App Store profile for its bundle id (plan.missingProfileFor) and has no
|
|
707
|
+
* other signing source: the missing profile first, then every way to sign,
|
|
708
|
+
* the Xcode account last. Null for any other plan.
|
|
709
|
+
*
|
|
710
|
+
* @param {string | undefined} bundleId - plan.missingProfileFor
|
|
711
|
+
* @param {string} [detail] - what failed after that, e.g. the Xcode account
|
|
712
|
+
* @returns {{ detail: string, hint: string } | null}
|
|
713
|
+
*/
|
|
714
|
+
function missingProfileError(bundleId, detail) {
|
|
715
|
+
if (!bundleId) return null;
|
|
716
|
+
const missing = `no App Store provisioning profile for ${bundleId} is installed on this Mac`;
|
|
717
|
+
return {
|
|
718
|
+
detail: detail ? `${missing}, and ${detail}` : missing,
|
|
719
|
+
hint: `Install the App Store profile for ${bundleId} (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles), set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name, put a .p12 and its profile or an App Store Connect API key in credentials/ios/, or sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team.`
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
688
723
|
/**
|
|
689
724
|
* The teams in IDEProvisioningTeamByIdentifier (account id -> teams), one
|
|
690
725
|
* per team id. `free` is isFreeProvisioningTeam, undefined when Xcode did
|
|
@@ -753,31 +788,35 @@ function needsXcodeTeam(plan, {
|
|
|
753
788
|
* Store, release-testing or enterprise export, so it is left out unless the
|
|
754
789
|
* export is for debugging; a team Xcode did not mark either way is kept.
|
|
755
790
|
* One team is used; several are a question, NATIVE-005 without a terminal;
|
|
756
|
-
* none is IOS-010.
|
|
791
|
+
* none is IOS-010, which leads with `missingProfileFor` when that is set.
|
|
757
792
|
*
|
|
758
793
|
* @param {object} options
|
|
759
794
|
* @param {ReturnType<typeof parseXcodeTeams>} options.teams - from readXcodeTeams
|
|
760
795
|
* @param {string} [options.exportMethod]
|
|
761
796
|
* @param {boolean} [options.isTTY]
|
|
762
797
|
* @param {typeof select} [options.choose]
|
|
798
|
+
* @param {string} [options.missingProfileFor] - plan.missingProfileFor
|
|
763
799
|
* @returns {Promise<{ teamId: string, teamName: string, free: boolean | undefined }>}
|
|
764
800
|
*/
|
|
765
801
|
async function chooseXcodeTeam({
|
|
766
802
|
teams,
|
|
767
803
|
exportMethod = "app-store-connect",
|
|
768
804
|
isTTY,
|
|
769
|
-
choose = _utils.select
|
|
805
|
+
choose = _utils.select,
|
|
806
|
+
missingProfileFor
|
|
770
807
|
}) {
|
|
771
|
-
|
|
772
|
-
|
|
808
|
+
const noTeam = detail => {
|
|
809
|
+
const missing = missingProfileError(missingProfileFor, detail);
|
|
810
|
+
return missing ? (0, _utils.nativeError)("IOS-010", missing.detail, {
|
|
811
|
+
hint: missing.hint
|
|
812
|
+
}) : (0, _utils.nativeError)("IOS-010", detail, {
|
|
773
813
|
hint: XCODE_TEAM_HINT
|
|
774
814
|
});
|
|
775
|
-
}
|
|
815
|
+
};
|
|
816
|
+
if (!teams) throw noTeam("could not read which teams are signed in to Xcode");
|
|
776
817
|
const usable = exportMethod === "debugging" ? teams : teams.filter(team => team.free !== true);
|
|
777
818
|
if (!usable.length) {
|
|
778
|
-
throw (
|
|
779
|
-
hint: XCODE_TEAM_HINT
|
|
780
|
-
});
|
|
819
|
+
throw noTeam(teams.length ? `Xcode is signed in with a personal team only, and a personal team cannot sign ${exportMethod} builds` : "no Apple account with a team is signed in to Xcode");
|
|
781
820
|
}
|
|
782
821
|
if (usable.length === 1) return usable[0];
|
|
783
822
|
const label = team => `${team.teamName || "Unnamed team"} (${team.teamId})`;
|
|
@@ -831,7 +870,8 @@ async function settleXcodeTeam({
|
|
|
831
870
|
teams: readTeams(),
|
|
832
871
|
exportMethod,
|
|
833
872
|
isTTY: interactive,
|
|
834
|
-
choose
|
|
873
|
+
choose,
|
|
874
|
+
missingProfileFor: plan.missingProfileFor
|
|
835
875
|
});
|
|
836
876
|
const label = `${team.teamName || "Unnamed team"} (${team.teamId})`;
|
|
837
877
|
if (!interactive) {
|
|
@@ -849,6 +889,44 @@ async function settleXcodeTeam({
|
|
|
849
889
|
};
|
|
850
890
|
}
|
|
851
891
|
|
|
892
|
+
/**
|
|
893
|
+
* The profile step for a release that auto-picked an installed App Store
|
|
894
|
+
* profile (planIosSigning's "installed profile"): in a terminal save it as
|
|
895
|
+
* ios.provisioningProfile with `save`, so later builds sign with the same
|
|
896
|
+
* one; without a terminal (CI, agents) only use it for this build, since
|
|
897
|
+
* config.json is never written there. Null for any other plan.
|
|
898
|
+
*
|
|
899
|
+
* @param {object} options
|
|
900
|
+
* @param {ReturnType<typeof planIosSigning>} options.plan - from planSigningForConfig
|
|
901
|
+
* @param {boolean} [options.isTTY] - defaults to isInteractive()
|
|
902
|
+
* @param {(profile: string) => void} options.save - writes WEBVIEW_CONFIG.ios.provisioningProfile
|
|
903
|
+
* @param {(message: string, type?: string) => void} [options.log]
|
|
904
|
+
* @returns {{ provisioningProfile: string, saved: boolean } | null}
|
|
905
|
+
*/
|
|
906
|
+
function settleInstalledProfile({
|
|
907
|
+
plan,
|
|
908
|
+
isTTY,
|
|
909
|
+
save,
|
|
910
|
+
log = () => {}
|
|
911
|
+
}) {
|
|
912
|
+
if (plan?.source !== "installed profile") return null;
|
|
913
|
+
// The name, or the UUID when another installed profile shares the name.
|
|
914
|
+
const profile = plan.provisioningProfile;
|
|
915
|
+
if (!(isTTY ?? (0, _utils.isInteractive)())) {
|
|
916
|
+
log(`Using the installed profile "${plan.profileName}". Set WEBVIEW_CONFIG.ios.provisioningProfile to keep it.`, "note");
|
|
917
|
+
return {
|
|
918
|
+
provisioningProfile: profile,
|
|
919
|
+
saved: false
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
log(`Signing with the installed profile "${plan.profileName}" (team ${plan.teamId})`, "info");
|
|
923
|
+
save(profile);
|
|
924
|
+
return {
|
|
925
|
+
provisioningProfile: profile,
|
|
926
|
+
saved: true
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
852
930
|
/**
|
|
853
931
|
* Local signing with credentials/ios/: import the .p12 into a temporary
|
|
854
932
|
* keychain, put that keychain on the user search list, and install the
|
|
@@ -1103,15 +1181,24 @@ function signingFailureLine(output) {
|
|
|
1103
1181
|
|
|
1104
1182
|
/**
|
|
1105
1183
|
* The coded error for a signing failure, keeping xcodebuild's transcript for
|
|
1106
|
-
* the diagnostic. The detail ends with the line that says what went wrong
|
|
1184
|
+
* the diagnostic. The detail ends with the line that says what went wrong,
|
|
1185
|
+
* and starts with the missing App Store profile when `missingProfileFor` is set.
|
|
1107
1186
|
*
|
|
1108
1187
|
* @param {string} detail
|
|
1109
1188
|
* @param {any} [cause]
|
|
1189
|
+
* @param {object} [options]
|
|
1190
|
+
* @param {string} [options.missingProfileFor] - plan.missingProfileFor
|
|
1110
1191
|
* @returns {any}
|
|
1111
1192
|
*/
|
|
1112
|
-
function signingSetupError(detail, cause
|
|
1193
|
+
function signingSetupError(detail, cause, {
|
|
1194
|
+
missingProfileFor
|
|
1195
|
+
} = {}) {
|
|
1113
1196
|
const said = signingFailureLine(cause?.output);
|
|
1114
|
-
const
|
|
1197
|
+
const failed = said ? `${detail}: ${said}` : detail;
|
|
1198
|
+
const missing = missingProfileError(missingProfileFor, failed);
|
|
1199
|
+
const error = missing ? (0, _utils.nativeError)("IOS-010", missing.detail, {
|
|
1200
|
+
hint: missing.hint
|
|
1201
|
+
}) : (0, _utils.nativeError)("IOS-010", failed);
|
|
1115
1202
|
if (cause) {
|
|
1116
1203
|
error.cause = cause;
|
|
1117
1204
|
if (cause.output) error.output = cause.output;
|
|
@@ -415,12 +415,14 @@ async function ensureDevServer({
|
|
|
415
415
|
// One-time migration: older setups wrote this machine's LAN address into
|
|
416
416
|
// config.json (every "localhost", and any http://<ip>), which makes Express
|
|
417
417
|
// bind only that address and breaks adb reverse. Put localhost back, but only
|
|
418
|
-
// for this machine's own addresses
|
|
419
|
-
// address is the app's own setting.
|
|
418
|
+
// for this machine's own addresses (or the one it had on another network):
|
|
419
|
+
// a container IP, 0.0.0.0 or a loopback address is the app's own setting.
|
|
420
420
|
// ---------------------------------------------------------------------------
|
|
421
421
|
|
|
422
422
|
const MIGRATION_KEY = "lanAddresses";
|
|
423
423
|
const IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
424
|
+
// 10/8, 172.16/12, 192.168/16 and link-local 169.254/16.
|
|
425
|
+
const PRIVATE_LAN_IPV4 = /^(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168|169\.254)\./;
|
|
424
426
|
const isCI = env => {
|
|
425
427
|
const value = env.CI;
|
|
426
428
|
return value !== undefined && value !== "" && value !== "0" && String(value).toLowerCase() !== "false";
|
|
@@ -429,10 +431,13 @@ const isCI = env => {
|
|
|
429
431
|
/**
|
|
430
432
|
* The changes the migration would make, without writing anything. Only a
|
|
431
433
|
* value equal to one of this machine's current IPv4 addresses, or an
|
|
432
|
-
* http:// URL on one of them, goes back to localhost.
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
434
|
+
* http:// URL on one of them, goes back to localhost. So does a private
|
|
435
|
+
* LOCAL_IP that NODE_SERVER_HOSTNAME also holds: an earlier setup wrote this
|
|
436
|
+
* machine's address on another network into both, and the dev server can't
|
|
437
|
+
* bind it here. Any other address, private ones included (a LOCAL_IP
|
|
438
|
+
* pointing at a VPN or LAN staging server), is the app's own setting.
|
|
439
|
+
* WEBVIEW_CONFIG.url is never touched, and nothing is when
|
|
440
|
+
* WEBVIEW_CONFIG.allowPrivateHost is true.
|
|
436
441
|
*
|
|
437
442
|
* @param {object} config - the parsed config.json (not modified)
|
|
438
443
|
* @param {object} [options]
|
|
@@ -451,6 +456,11 @@ function planLanAddressMigration(config, {
|
|
|
451
456
|
};
|
|
452
457
|
// This machine's IPv4 addresses, without loopback and 0.0.0.0/8.
|
|
453
458
|
const saved = new Set([...localHosts].filter(host => IPV4.test(host) && !/^(?:127|0)\./.test(host)));
|
|
459
|
+
// The LAN address this machine had when LOCAL_IP was written.
|
|
460
|
+
const leftover = typeof wv?.LOCAL_IP === "string" ? wv.LOCAL_IP.trim() : "";
|
|
461
|
+
if (IPV4.test(leftover) && PRIVATE_LAN_IPV4.test(leftover) && String(config.NODE_SERVER_HOSTNAME ?? "").trim() === leftover) {
|
|
462
|
+
saved.add(leftover);
|
|
463
|
+
}
|
|
454
464
|
if (!saved.size) return {
|
|
455
465
|
config,
|
|
456
466
|
changes
|
|
@@ -532,8 +542,8 @@ function migrateLanAddresses({
|
|
|
532
542
|
} : undefined).changes;
|
|
533
543
|
const changes = planned.length ? writeConfigKeys(file, Object.fromEntries(planned.map(change => [change.path, change.to]))) : [];
|
|
534
544
|
if (changes.length) {
|
|
535
|
-
log(`Put localhost back in config/config.json where an earlier setup saved this machine's LAN address:`, "
|
|
536
|
-
for (const change of changes) log(` ${change.path}: ${change.from} -> ${change.to}`, "
|
|
545
|
+
log(`Put localhost back in config/config.json where an earlier setup saved this machine's LAN address:`, "note");
|
|
546
|
+
for (const change of changes) log(` ${change.path}: ${change.from} -> ${change.to}`, "note");
|
|
537
547
|
}
|
|
538
548
|
const state = (0, _localState.readLocalState)({
|
|
539
549
|
cwd
|
package/dist/native/utils.js
CHANGED
|
@@ -508,7 +508,7 @@ const NATIVE_ERRORS = exports.NATIVE_ERRORS = {
|
|
|
508
508
|
},
|
|
509
509
|
"IOS-010": {
|
|
510
510
|
message: "iOS code signing is not set up",
|
|
511
|
-
hint: "
|
|
511
|
+
hint: "For an App Store release, install the App Store provisioning profile for your bundle id (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles) or set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name. Otherwise, and for a device build, sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team. In CI, put the signing files in credentials/ios/ and set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios."
|
|
512
512
|
},
|
|
513
513
|
"IOS-011": {
|
|
514
514
|
message: "App Store Connect API key settings are incomplete or wrong",
|
|
@@ -358,26 +358,33 @@ async function createServer() {
|
|
|
358
358
|
if (!res.headersSent) res.status(500).send("Internal Server Error");
|
|
359
359
|
}
|
|
360
360
|
});
|
|
361
|
-
|
|
361
|
+
|
|
362
|
+
// Express 4 never hands a bind failure to the listen callback: the
|
|
363
|
+
// http.Server emits it. Unhandled, an address this machine does not have
|
|
364
|
+
// (EADDRNOTAVAIL) left a raw uncaught exception and a server that never
|
|
365
|
+
// listened. Report it, then exit. Only until the server listens: a later
|
|
366
|
+
// error goes to the uncaughtException handler as before.
|
|
367
|
+
const onListenError = error => {
|
|
368
|
+
const known = hintFor(error.message);
|
|
369
|
+
process.stderr.write(diagnostic({
|
|
370
|
+
message: `Could not start the server: ${error.message}`,
|
|
371
|
+
scope: process.env.NODE_ENV === "development" ? "dev server" : "server",
|
|
372
|
+
hint: known?.hint,
|
|
373
|
+
docs: known?.docs
|
|
374
|
+
}));
|
|
375
|
+
safeCall(onServerError);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
};
|
|
378
|
+
const server = app.listen({
|
|
362
379
|
port,
|
|
363
380
|
host
|
|
364
|
-
},
|
|
381
|
+
}, () => {
|
|
382
|
+
server.removeListener("error", onListenError);
|
|
365
383
|
const {
|
|
366
384
|
NODE_SERVER_HOSTNAME,
|
|
367
385
|
NODE_SERVER_PORT
|
|
368
386
|
} = process.env;
|
|
369
387
|
const isDev = process.env.NODE_ENV === "development";
|
|
370
|
-
if (error) {
|
|
371
|
-
const known = hintFor(error.message);
|
|
372
|
-
process.stderr.write(diagnostic({
|
|
373
|
-
message: `Could not start the server: ${error.message}`,
|
|
374
|
-
scope: isDev ? "dev server" : "server",
|
|
375
|
-
hint: known?.hint,
|
|
376
|
-
docs: known?.docs
|
|
377
|
-
}));
|
|
378
|
-
safeCall(onServerError);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
388
|
|
|
382
389
|
// Reached from the listen callback, so this means a port was bound --
|
|
383
390
|
// it says nothing about whether anything compiled. Claiming a
|
|
@@ -409,6 +416,7 @@ async function createServer() {
|
|
|
409
416
|
process.stdout.write(`${wrapped ? NEXT_STEP : ""}${GUTTER}${t.dim(isDev ? "Watching for changes. " : "Serving ")}` + `${isDev ? "" : t.accent(`${process.env.BUILD_OUTPUT_PATH || "build"}/`)}` + `${t.dim(isDev ? "Press Ctrl+C to stop." : ". Press Ctrl+C to stop.")}\n`);
|
|
410
417
|
if (wrapped) process.stdout.write(`${BANNER_END}\n`);
|
|
411
418
|
});
|
|
419
|
+
server.once("error", onListenError);
|
|
412
420
|
}
|
|
413
421
|
createServer().catch(err => {
|
|
414
422
|
console.error(err?.stack || err);
|