@mpgd/cli 0.59.0 → 0.61.1
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/capacitor-shell-starter.d.ts +24 -0
- package/dist/capacitor-shell-starter.js +100 -8
- package/dist/crash-reporting-starter.d.ts +99 -0
- package/dist/crash-reporting-starter.js +271 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +221 -14
- package/dist/ios-ipa-inspection.js +120 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +1617 -1185
- package/dist/native-deploy-run.d.ts +6 -0
- package/dist/native-deploy-run.js +19 -1
- package/dist/native-deploy-submission.d.ts +20 -1
- package/dist/native-deploy-submission.js +94 -7
- package/dist/native-shell-ios-push-files.d.ts +9 -0
- package/dist/native-shell-ios-push-files.js +65 -0
- package/dist/native-shell-store-policy.d.ts +109 -0
- package/dist/native-shell-store-policy.js +553 -7
- package/dist/play-internal-submission.d.ts +40 -4
- package/dist/play-internal-submission.js +215 -48
- package/dist/play-publisher-port.d.ts +26 -1
- package/dist/play-release-options.d.ts +97 -0
- package/dist/play-release-options.js +307 -0
- package/dist/play-release-promote.d.ts +59 -0
- package/dist/play-release-promote.js +235 -0
- package/dist/play-release-rollout.d.ts +57 -0
- package/dist/play-release-rollout.js +237 -0
- package/dist/play-sdk-adapter.js +26 -3
- package/dist/release-state.d.ts +6 -0
- package/dist/release-state.js +8 -0
- package/package.json +17 -17
- package/templates/phaser-game/apps/target-ait/package.json +2 -2
- package/templates/phaser-game/package.json +1 -1
- package/templates/phaser-game/vite.runtime-target-config.ts +4 -0
|
@@ -2,6 +2,7 @@ import type { PlatformVersionLedger } from '@mpgd/target-config';
|
|
|
2
2
|
import { type NativeDeploymentPlan, type NativeDeployTarget } from './deploy-planning.js';
|
|
3
3
|
import { type NativeStoreCredential } from './native-deploy-submission.js';
|
|
4
4
|
import { type NativeReleaseStatus } from './release-state.js';
|
|
5
|
+
import { type PlayReleaseRequest } from './play-release-options.js';
|
|
5
6
|
export interface RunNativeDeploymentInput {
|
|
6
7
|
readonly plan: NativeDeploymentPlan;
|
|
7
8
|
readonly releaseKey: string;
|
|
@@ -14,6 +15,11 @@ export interface RunNativeDeploymentInput {
|
|
|
14
15
|
/** Required only when no game ledger exists on release-state. */
|
|
15
16
|
readonly initialLedger?: PlatformVersionLedger;
|
|
16
17
|
readonly approved: boolean;
|
|
18
|
+
/** Google Play track, status, rollout and notes for the Android target. */
|
|
19
|
+
readonly androidRelease?: PlayReleaseRequest;
|
|
20
|
+
/** Confirms a production release or staged rollout (`--confirm-production`). */
|
|
21
|
+
readonly confirmProduction?: boolean;
|
|
22
|
+
readonly onDraftAppFallback?: (track: string) => void;
|
|
17
23
|
readonly environment?: NodeJS.ProcessEnv;
|
|
18
24
|
readonly signal?: AbortSignal;
|
|
19
25
|
/** Called only after a newly built target has an immutable, committed build record. */
|
|
@@ -4,16 +4,24 @@ import path from 'node:path';
|
|
|
4
4
|
import { doctorNativeDeployment, readNativeDeployCredentialNames, readNativeDeployTargetProfile, } from './deploy-planning.js';
|
|
5
5
|
import { withAndroidUploadSigningSession } from './android-signing-session.js';
|
|
6
6
|
import { withIosSigningSession } from './ios-signing-session.js';
|
|
7
|
-
import { submitRecordedNativeTarget, } from './native-deploy-submission.js';
|
|
7
|
+
import { assertSettledAndroidReleaseOptions, submitRecordedNativeTarget, } from './native-deploy-submission.js';
|
|
8
8
|
import { isNativeSubmissionSettled, readNativeReleaseStatus, recordNativeReleaseBuild, reserveNativeRelease, } from './release-state.js';
|
|
9
9
|
import { installPinnedReleaseDependencies, pinNativeDeploymentPlan, runPinnedNativeBuild, withPinnedReleaseWorkspace, } from './release-workspace.js';
|
|
10
10
|
import { inspectAndroidBundleSigner } from './android-bundle-signer.js';
|
|
11
|
+
import { assertPlayReleaseConfirmed, assertPlayReleaseRequest, } from './play-release-options.js';
|
|
11
12
|
const gitAuthEnvironmentNames = ['GITHUB_TOKEN', 'GH_TOKEN', 'GIT_ASKPASS'];
|
|
12
13
|
/** Reserve once, build only missing targets, then submit the recorded binaries. */
|
|
13
14
|
export async function runNativeDeployment(input) {
|
|
14
15
|
if (!input.approved) {
|
|
15
16
|
throw new Error('Native deployment requires explicit internal-test approval.');
|
|
16
17
|
}
|
|
18
|
+
if (input.androidRelease !== undefined) {
|
|
19
|
+
// Refuse an unconfirmed production release before reserving or building anything.
|
|
20
|
+
assertPlayReleaseRequest(input.androidRelease);
|
|
21
|
+
if (input.plan.targets.some((entry) => entry.target === 'android')) {
|
|
22
|
+
assertPlayReleaseConfirmed(input.androidRelease, input.confirmProduction === true);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
17
25
|
const environment = input.environment ?? process.env;
|
|
18
26
|
const pinned = await pinNativeDeploymentPlan(input.plan, input.kit, {
|
|
19
27
|
environment,
|
|
@@ -36,6 +44,8 @@ export async function runNativeDeployment(input) {
|
|
|
36
44
|
throw error;
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
// Refuse changed Play options for a committed release before any build work.
|
|
48
|
+
assertSettledAndroidReleaseOptions(existing?.submissions.android, input.androidRelease);
|
|
39
49
|
const pendingSteps = existing === undefined
|
|
40
50
|
? {
|
|
41
51
|
buildTargets: input.plan.targets.map((entry) => entry.target),
|
|
@@ -126,6 +136,7 @@ export async function runNativeDeployment(input) {
|
|
|
126
136
|
}
|
|
127
137
|
const submission = status.submissions[entry.target];
|
|
128
138
|
if (submission !== undefined && isNativeSubmissionSettled(submission.status)) {
|
|
139
|
+
assertSettledAndroidReleaseOptions(submission, input.androidRelease);
|
|
129
140
|
continue;
|
|
130
141
|
}
|
|
131
142
|
await submitRecordedNativeTarget({
|
|
@@ -134,6 +145,13 @@ export async function runNativeDeployment(input) {
|
|
|
134
145
|
releaseKey: input.releaseKey,
|
|
135
146
|
credential: readStoreCredential(input.plan, entry.target, environment),
|
|
136
147
|
approved: input.approved,
|
|
148
|
+
...(input.androidRelease === undefined ? {} : { androidRelease: input.androidRelease }),
|
|
149
|
+
...(input.confirmProduction === undefined ? {} : {
|
|
150
|
+
confirmProduction: input.confirmProduction,
|
|
151
|
+
}),
|
|
152
|
+
...(input.onDraftAppFallback === undefined ? {} : {
|
|
153
|
+
onDraftAppFallback: input.onDraftAppFallback,
|
|
154
|
+
}),
|
|
137
155
|
environment,
|
|
138
156
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
139
157
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NativeDeploymentPlan } from './deploy-planning.js';
|
|
2
2
|
import { checkpointNativeSubmission, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, type ImmutableNativeBuildRecord, type NativeSubmissionCheckpoint } from './release-state.js';
|
|
3
|
-
import { submitVerifiedAndroidBundle } from './play-internal-submission.js';
|
|
3
|
+
import { submitVerifiedAndroidBundle, type PlaySubmissionPlan } from './play-internal-submission.js';
|
|
4
|
+
import { type PlayReleaseRequest } from './play-release-options.js';
|
|
4
5
|
import { submitVerifiedIosBuild } from './testflight-submission.js';
|
|
5
6
|
export type NativeStoreCredential = {
|
|
6
7
|
readonly target: 'android';
|
|
@@ -19,6 +20,12 @@ export interface SubmitRecordedNativeTargetInput {
|
|
|
19
20
|
readonly releaseKey: string;
|
|
20
21
|
readonly credential: NativeStoreCredential;
|
|
21
22
|
readonly approved: boolean;
|
|
23
|
+
/** Google Play track, status, rollout, notes and mapping; defaults to internal/completed. */
|
|
24
|
+
readonly androidRelease?: PlayReleaseRequest;
|
|
25
|
+
/** Confirms a production release or staged rollout (`--confirm-production`). */
|
|
26
|
+
readonly confirmProduction?: boolean;
|
|
27
|
+
/** Called when Play accepts only a draft because the app was never published. */
|
|
28
|
+
readonly onDraftAppFallback?: (track: string) => void;
|
|
22
29
|
/** Allows the same caller to continue an in-process attempt without taking over its lease. */
|
|
23
30
|
readonly attemptId?: string;
|
|
24
31
|
readonly environment?: NodeJS.ProcessEnv;
|
|
@@ -33,8 +40,20 @@ export interface NativeSubmissionPorts {
|
|
|
33
40
|
readonly submitAndroid: typeof submitVerifiedAndroidBundle;
|
|
34
41
|
readonly submitIos: typeof submitVerifiedIosBuild;
|
|
35
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* `--approve`, or the profile's `preapproved-internal-test`. That preapproval
|
|
45
|
+
* covers only the internal track: any other Google Play track needs `--approve`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isNativeDeployApproved(plan: Pick<NativeDeploymentPlan, 'approval'>, approveFlag: boolean, androidRelease?: Pick<PlayReleaseRequest, 'track'>): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* A committed Android release is never re-sent, so a request with other
|
|
50
|
+
* Google Play options must fail instead of being skipped as done.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertSettledAndroidReleaseOptions(checkpoint: NativeSubmissionCheckpoint | undefined, androidRelease: PlayReleaseRequest | undefined): void;
|
|
36
53
|
/** Resume exactly one verified build; never rebuild or allocate another version here. */
|
|
37
54
|
export declare function submitRecordedNativeTarget(input: SubmitRecordedNativeTargetInput): Promise<NativeSubmissionCheckpoint>;
|
|
55
|
+
/** Verify one recorded Android build and print its Play edit without any store call. */
|
|
56
|
+
export declare function planRecordedAndroidSubmission(input: Omit<SubmitRecordedNativeTargetInput, 'credential' | 'approved'>, readStatus?: typeof readNativeReleaseStatus): Promise<PlaySubmissionPlan>;
|
|
38
57
|
export declare function submitRecordedNativeTargetWithPorts(input: SubmitRecordedNativeTargetInput, ports: NativeSubmissionPorts): Promise<NativeSubmissionCheckpoint>;
|
|
39
58
|
/** Validate the workflow-restored manifest against the immutable release-state hash. */
|
|
40
59
|
export declare function verifyRecordedReleaseManifest(gameRoot: string, record: Pick<ImmutableNativeBuildRecord, 'artifactLocation' | 'releaseManifestSha256' | 'target'>): void;
|
|
@@ -2,7 +2,8 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { checkpointNativeSubmission, freshSubmissionLeaseExpiresAt, isNativeSubmissionSettled, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, } from './release-state.js';
|
|
5
|
-
import { PlaySubmissionUncertainError, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
|
|
5
|
+
import { planVerifiedAndroidBundleSubmission, PlaySubmissionUncertainError, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
|
|
6
|
+
import { assertPlayReleaseConfirmed, assertPlayReleaseRequest, defaultPlayReleaseRequest, defaultPlayTrack, playReleaseRequestDigest, } from './play-release-options.js';
|
|
6
7
|
import { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
|
|
7
8
|
const productionPorts = {
|
|
8
9
|
readStatus: readNativeReleaseStatus,
|
|
@@ -12,23 +13,56 @@ const productionPorts = {
|
|
|
12
13
|
submitAndroid: submitVerifiedAndroidBundle,
|
|
13
14
|
submitIos: submitVerifiedIosBuild,
|
|
14
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* `--approve`, or the profile's `preapproved-internal-test`. That preapproval
|
|
18
|
+
* covers only the internal track: any other Google Play track needs `--approve`.
|
|
19
|
+
*/
|
|
20
|
+
export function isNativeDeployApproved(plan, approveFlag, androidRelease) {
|
|
21
|
+
const internalOnly = androidRelease === undefined || androidRelease.track === defaultPlayTrack;
|
|
22
|
+
return approveFlag || (plan.approval === 'preapproved-internal-test' && internalOnly);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A committed Android release is never re-sent, so a request with other
|
|
26
|
+
* Google Play options must fail instead of being skipped as done.
|
|
27
|
+
*/
|
|
28
|
+
export function assertSettledAndroidReleaseOptions(checkpoint, androidRelease) {
|
|
29
|
+
if (checkpoint?.target === 'android' && isNativeSubmissionSettled(checkpoint.status)
|
|
30
|
+
&& checkpoint.playReleaseSha256
|
|
31
|
+
!== playReleaseRequestDigest(androidRelease ?? defaultPlayReleaseRequest)) {
|
|
32
|
+
throw new Error('This release was already committed to Google Play with other release options; '
|
|
33
|
+
+ 'change it with "mpgd deploy rollout" or submit a new release.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
15
36
|
/** Resume exactly one verified build; never rebuild or allocate another version here. */
|
|
16
37
|
export async function submitRecordedNativeTarget(input) {
|
|
17
38
|
return submitRecordedNativeTargetWithPorts(input, productionPorts);
|
|
18
39
|
}
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
/** Verify one recorded Android build and print its Play edit without any store call. */
|
|
41
|
+
export async function planRecordedAndroidSubmission(input, readStatus = readNativeReleaseStatus) {
|
|
42
|
+
const recorded = await readRecordedSubmission(input, 'android', readStatus);
|
|
43
|
+
if (input.environment?.MPGD_VERIFY_RELEASE_MANIFEST === '1') {
|
|
44
|
+
verifyRecordedReleaseManifest(input.plan.gameRoot, recorded.record);
|
|
45
|
+
}
|
|
46
|
+
return planVerifiedAndroidBundleSubmission({
|
|
47
|
+
record: recorded.record,
|
|
48
|
+
aabFile: recorded.artifactFile,
|
|
49
|
+
packageName: recorded.planned.appId,
|
|
50
|
+
...(input.androidRelease === undefined ? {} : { release: input.androidRelease }),
|
|
51
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function readRecordedSubmission(input, target, readStatus) {
|
|
21
55
|
const planned = input.plan.targets.find((entry) => entry.target === target);
|
|
22
|
-
if (planned === undefined
|
|
56
|
+
if (planned === undefined) {
|
|
23
57
|
throw new Error('Native store submission requires a planned target and explicit approval.');
|
|
24
58
|
}
|
|
25
|
-
const
|
|
59
|
+
const status = await readStatus({
|
|
26
60
|
gameRoot: input.plan.gameRoot,
|
|
27
61
|
gameId: input.gameId,
|
|
62
|
+
releaseKey: input.releaseKey,
|
|
28
63
|
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
29
64
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
30
|
-
};
|
|
31
|
-
const status = await ports.readStatus({ ...stateInput, releaseKey: input.releaseKey });
|
|
65
|
+
});
|
|
32
66
|
if (status.plan.targetConfigDigest !== input.plan.targetConfigSha256
|
|
33
67
|
|| status.plan.gameId !== input.gameId
|
|
34
68
|
|| status.plan.targets[target] === undefined) {
|
|
@@ -44,7 +78,41 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
|
|
|
44
78
|
throw new Error('Store submission requires a matching immutable native build record.');
|
|
45
79
|
}
|
|
46
80
|
const artifactFile = resolveRecordedArtifact(input.plan.gameRoot, record.artifactLocation);
|
|
81
|
+
return { planned, record, artifactFile, status };
|
|
82
|
+
}
|
|
83
|
+
export async function submitRecordedNativeTargetWithPorts(input, ports) {
|
|
84
|
+
const target = input.credential.target;
|
|
85
|
+
const planned = input.plan.targets.find((entry) => entry.target === target);
|
|
86
|
+
if (planned === undefined || !input.approved) {
|
|
87
|
+
throw new Error('Native store submission requires a planned target and explicit approval.');
|
|
88
|
+
}
|
|
89
|
+
const androidRelease = input.androidRelease ?? defaultPlayReleaseRequest;
|
|
90
|
+
if (target === 'android') {
|
|
91
|
+
// Refuse an invalid or unconfirmed production/staged request before the
|
|
92
|
+
// release state is read or a checkpoint is written.
|
|
93
|
+
assertPlayReleaseRequest(androidRelease);
|
|
94
|
+
assertPlayReleaseConfirmed(androidRelease, input.confirmProduction === true);
|
|
95
|
+
}
|
|
96
|
+
const releaseDigest = target === 'android' ? playReleaseRequestDigest(androidRelease) : undefined;
|
|
97
|
+
const stateInput = {
|
|
98
|
+
gameRoot: input.plan.gameRoot,
|
|
99
|
+
gameId: input.gameId,
|
|
100
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
101
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
102
|
+
};
|
|
103
|
+
const { record, artifactFile, status } = await readRecordedSubmission(input, target, ports.readStatus);
|
|
47
104
|
let checkpoint = status.submissions[target];
|
|
105
|
+
assertSettledAndroidReleaseOptions(checkpoint, androidRelease);
|
|
106
|
+
// An attempt that never checkpointed a Play edit changed nothing remotely,
|
|
107
|
+
// so its release options may still be corrected.
|
|
108
|
+
const optionsReplaceable = checkpoint?.status === 'started'
|
|
109
|
+
&& checkpoint.remoteEditId === undefined && checkpoint.remoteUploadId === undefined
|
|
110
|
+
&& checkpoint.remoteBuildId === undefined;
|
|
111
|
+
if (checkpoint !== undefined && checkpoint.playReleaseSha256 !== releaseDigest
|
|
112
|
+
&& !optionsReplaceable) {
|
|
113
|
+
throw new Error('This submission was started with other Google Play release options (track, status, '
|
|
114
|
+
+ 'fraction, notes or mapping); re-run it with the same options before changing them.');
|
|
115
|
+
}
|
|
48
116
|
if (checkpoint !== undefined && isNativeSubmissionSettled(checkpoint.status)) {
|
|
49
117
|
return checkpoint;
|
|
50
118
|
}
|
|
@@ -70,6 +138,7 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
|
|
|
70
138
|
attemptId: randomBytes(16).toString('hex'),
|
|
71
139
|
leaseExpiresAt: freshSubmissionLeaseExpiresAt(),
|
|
72
140
|
status: 'started',
|
|
141
|
+
...(releaseDigest === undefined ? {} : { playReleaseSha256: releaseDigest }),
|
|
73
142
|
},
|
|
74
143
|
})).checkpoint;
|
|
75
144
|
}
|
|
@@ -90,6 +159,17 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
|
|
|
90
159
|
throw new Error('Submission checkpoint no longer matches its immutable build.');
|
|
91
160
|
}
|
|
92
161
|
let activeCheckpoint = checkpoint;
|
|
162
|
+
if (activeCheckpoint.playReleaseSha256 !== releaseDigest) {
|
|
163
|
+
const { playReleaseSha256: _replaced, ...withoutDigest } = activeCheckpoint;
|
|
164
|
+
activeCheckpoint = (await ports.checkpoint({
|
|
165
|
+
...stateInput,
|
|
166
|
+
checkpoint: {
|
|
167
|
+
...withoutDigest,
|
|
168
|
+
...(releaseDigest === undefined ? {} : { playReleaseSha256: releaseDigest }),
|
|
169
|
+
leaseExpiresAt: freshSubmissionLeaseExpiresAt(),
|
|
170
|
+
},
|
|
171
|
+
})).checkpoint;
|
|
172
|
+
}
|
|
93
173
|
const save = async (patch) => {
|
|
94
174
|
activeCheckpoint = (await ports.checkpoint({
|
|
95
175
|
...stateInput,
|
|
@@ -122,6 +202,13 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
|
|
|
122
202
|
onEditCreated: async (editId) => {
|
|
123
203
|
await save({ status: 'edit-open', remoteEditId: editId });
|
|
124
204
|
},
|
|
205
|
+
...(input.androidRelease === undefined ? {} : { release: input.androidRelease }),
|
|
206
|
+
...(input.confirmProduction === undefined ? {} : {
|
|
207
|
+
confirmProduction: input.confirmProduction,
|
|
208
|
+
}),
|
|
209
|
+
...(input.onDraftAppFallback === undefined ? {} : {
|
|
210
|
+
onDraftAppFallback: input.onDraftAppFallback,
|
|
211
|
+
}),
|
|
125
212
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
126
213
|
});
|
|
127
214
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IosPushWiringInput } from './native-shell-store-policy.js';
|
|
2
|
+
/**
|
|
3
|
+
* The iOS project files `inspectIosPushWiring` reads, from a shell's
|
|
4
|
+
* `ios/App` directory (the Xcode project directory): the project, a reader
|
|
5
|
+
* for files inside that directory, and the Swift sources of the App group.
|
|
6
|
+
* Symbolic links and paths that leave the directory are never followed.
|
|
7
|
+
* Undefined when the shell has no readable Xcode project.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readIosPushWiringInput(iosProjectDirectory: string): IosPushWiringInput | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Swift files read at most from the App group: a guard against a pathological tree. */
|
|
4
|
+
const maxSwiftSources = 2_000;
|
|
5
|
+
/**
|
|
6
|
+
* The iOS project files `inspectIosPushWiring` reads, from a shell's
|
|
7
|
+
* `ios/App` directory (the Xcode project directory): the project, a reader
|
|
8
|
+
* for files inside that directory, and the Swift sources of the App group.
|
|
9
|
+
* Symbolic links and paths that leave the directory are never followed.
|
|
10
|
+
* Undefined when the shell has no readable Xcode project.
|
|
11
|
+
*/
|
|
12
|
+
export function readIosPushWiringInput(iosProjectDirectory) {
|
|
13
|
+
const project = readContainedFile(iosProjectDirectory, 'App.xcodeproj/project.pbxproj');
|
|
14
|
+
if (project === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
project,
|
|
19
|
+
readProjectFile: (relative) => readContainedFile(iosProjectDirectory, relative),
|
|
20
|
+
swiftSources: listContainedSwiftFiles(iosProjectDirectory, 'App')
|
|
21
|
+
.map((relative) => readContainedFile(iosProjectDirectory, relative) ?? ''),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** A regular file inside `root` by its relative path, or undefined when missing or unsafe. */
|
|
25
|
+
function readContainedFile(root, relative) {
|
|
26
|
+
const file = path.resolve(root, relative);
|
|
27
|
+
const within = path.relative(root, file);
|
|
28
|
+
if (within === '' || within.startsWith('..') || path.isAbsolute(within)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
let current = root;
|
|
32
|
+
for (const segment of within.split(path.sep)) {
|
|
33
|
+
current = path.join(current, segment);
|
|
34
|
+
if (!existsSync(current) || lstatSync(current).isSymbolicLink()) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lstatSync(file).isFile() ? readFileSync(file, 'utf8') : undefined;
|
|
39
|
+
}
|
|
40
|
+
function listContainedSwiftFiles(root, relativeRoot) {
|
|
41
|
+
const pending = [relativeRoot];
|
|
42
|
+
const files = [];
|
|
43
|
+
while (pending.length > 0) {
|
|
44
|
+
const relative = pending.pop() ?? '';
|
|
45
|
+
const full = path.join(root, relative);
|
|
46
|
+
if (!existsSync(full) || lstatSync(full).isSymbolicLink() || !lstatSync(full).isDirectory()) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of readdirSync(full, { withFileTypes: true })) {
|
|
50
|
+
const child = path.join(relative, entry.name);
|
|
51
|
+
// The web bundle Capacitor copies into the App group holds no Swift.
|
|
52
|
+
if (entry.isDirectory() && child !== path.join(relativeRoot, 'public')) {
|
|
53
|
+
pending.push(child);
|
|
54
|
+
}
|
|
55
|
+
else if (entry.isFile() && entry.name.endsWith('.swift')) {
|
|
56
|
+
files.push(child);
|
|
57
|
+
if (files.length >= maxSwiftSources) {
|
|
58
|
+
// Enough to find the delegates; the rest is not read.
|
|
59
|
+
return files.sort();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files.sort();
|
|
65
|
+
}
|
|
@@ -121,3 +121,112 @@ export declare function applyAndroidManifestAdId(manifest: string, usesAds: bool
|
|
|
121
121
|
export declare function readGameAdvertisingIdPolicy(gameConfig: unknown): 'declared' | 'removed';
|
|
122
122
|
export declare function inspectAndroidManifestAds(manifest: string, usesAds: boolean, advertisingIdPolicy?: 'declared' | 'removed'): readonly NativeShellPolicyIssue[];
|
|
123
123
|
export declare function inspectIosInfoPlistAds(plist: string, usesAds: boolean): readonly NativeShellPolicyIssue[];
|
|
124
|
+
/** The entitlements file of a kit shell's App target, in the App group. */
|
|
125
|
+
export declare const iosPushEntitlementsFile = "App.entitlements";
|
|
126
|
+
/** Its `CODE_SIGN_ENTITLEMENTS` value, relative to the Xcode project directory (`ios/App`). */
|
|
127
|
+
export declare const iosPushEntitlementsSetting = "App/App.entitlements";
|
|
128
|
+
/**
|
|
129
|
+
* The Push Notifications entitlement the Xcode capability writes. Apple:
|
|
130
|
+
* "Xcode sets the value of the entitlement based on your app's current
|
|
131
|
+
* provisioning profile", so `development` here signs as `production` with
|
|
132
|
+
* an App Store profile
|
|
133
|
+
* (https://developer.apple.com/documentation/bundleresources/entitlements/aps-environment).
|
|
134
|
+
*/
|
|
135
|
+
export declare const iosPushEntitlementsXml: string;
|
|
136
|
+
/**
|
|
137
|
+
* The AppDelegate methods `@capacitor-firebase/messaging` documents for iOS:
|
|
138
|
+
* the plugin observes `.capacitorDidRegisterForRemoteNotifications` and hands
|
|
139
|
+
* the APNs token to Firebase, so without them it never gets an FCM token.
|
|
140
|
+
* The plugin README's third method (`didReceiveRemoteNotification`) is left
|
|
141
|
+
* out: the kit sends no silent pushes, and plugin 8.5.2 never calls the
|
|
142
|
+
* completion handler it is given.
|
|
143
|
+
*/
|
|
144
|
+
export declare const iosAppDelegatePushForwardingSwift: readonly string[];
|
|
145
|
+
/** The scene delegate methods that replay a cold-start tap (reference shell, #407). */
|
|
146
|
+
export declare const iosSceneDelegateColdStartSwift: readonly string[];
|
|
147
|
+
/** Whether the shell's package.json installs the Firebase messaging plugin (`--providers notifications`). */
|
|
148
|
+
export declare function shellUsesFirebaseMessaging(shellPackage: unknown): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Swift source with comments and string literals blanked to spaces, so
|
|
151
|
+
* offsets stay the same and only code is matched. Nested block comments,
|
|
152
|
+
* multi-line strings and raw strings (`#"..."#`) are handled; an
|
|
153
|
+
* interpolation's code inside a string is blanked with it, and a
|
|
154
|
+
* single-line string that runs into a newline ends there, so one the
|
|
155
|
+
* scanner misreads cannot blank the rest of the file.
|
|
156
|
+
*/
|
|
157
|
+
export declare function stripSwiftCommentsAndStrings(source: string): string;
|
|
158
|
+
/**
|
|
159
|
+
* Whether a Swift source's `didRegisterForRemoteNotificationsWithDeviceToken`
|
|
160
|
+
* hands the APNs token on: it posts `.capacitorDidRegisterForRemoteNotifications`
|
|
161
|
+
* (what the messaging plugin observes) or sets Firebase's `apnsToken` itself.
|
|
162
|
+
*/
|
|
163
|
+
export declare function swiftForwardsPushToken(source: string): boolean;
|
|
164
|
+
/** Whether a Swift source declares a scene delegate class. */
|
|
165
|
+
export declare function swiftDeclaresSceneDelegate(source: string): boolean;
|
|
166
|
+
/** Whether a Swift source forwards the scene's cold-start notification response. */
|
|
167
|
+
export declare function swiftForwardsColdStartNotification(source: string): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Adds the APNs registration forwarding (`iosAppDelegatePushForwardingSwift`)
|
|
170
|
+
* to the `AppDelegate` class. An AppDelegate that already forwards the token
|
|
171
|
+
* is returned unchanged; undefined when the class is not found or already
|
|
172
|
+
* implements a registration callback without the forwarding, which is left
|
|
173
|
+
* to the game.
|
|
174
|
+
*/
|
|
175
|
+
export declare function applyIosAppDelegatePushForwarding(source: string): string | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* Adds the cold-start notification forwarding of the kit reference shell
|
|
178
|
+
* (#407) to Capacitor's `SceneDelegate`: `import UserNotifications`, the
|
|
179
|
+
* call after `SceneDelegateProxy.shared.scene(_:willConnectTo:options:)`,
|
|
180
|
+
* and `iosSceneDelegateColdStartSwift`. A scene delegate that already reads
|
|
181
|
+
* `notificationResponse` is returned unchanged; undefined when it does not
|
|
182
|
+
* have Capacitor's layout, which is left to the game.
|
|
183
|
+
*/
|
|
184
|
+
export declare function applyIosSceneDelegateColdStartForwarding(source: string): string | undefined;
|
|
185
|
+
/** The App target's Release `CODE_SIGN_ENTITLEMENTS`, without quotes, or undefined. */
|
|
186
|
+
export declare function readIosAppReleaseEntitlements(project: string): string | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* The entitlements path a `CODE_SIGN_ENTITLEMENTS` value names, relative to
|
|
189
|
+
* the Xcode project directory, or undefined when it uses another build
|
|
190
|
+
* setting the kit cannot resolve.
|
|
191
|
+
*/
|
|
192
|
+
export declare function resolveIosEntitlementsSetting(value: string): string | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* Adds `App.entitlements` to the App group of a Capacitor-generated project
|
|
195
|
+
* and sets `CODE_SIGN_ENTITLEMENTS` in every App target build configuration.
|
|
196
|
+
* The file is not bundled (no build phase). A project that already points
|
|
197
|
+
* there is returned unchanged; undefined when the project has another layout
|
|
198
|
+
* or another entitlements file, which is left to the game.
|
|
199
|
+
*/
|
|
200
|
+
export declare function applyIosProjectPushEntitlements(project: string): string | undefined;
|
|
201
|
+
/** What `inspectIosPushWiring` reads from the shell's iOS project. */
|
|
202
|
+
export interface IosPushWiringInput {
|
|
203
|
+
/** `ios/App/App.xcodeproj/project.pbxproj`. */
|
|
204
|
+
readonly project: string;
|
|
205
|
+
/**
|
|
206
|
+
* A file of the iOS project by its path relative to `ios/App` (the Xcode
|
|
207
|
+
* project directory), or undefined when it is missing or unsafe to read.
|
|
208
|
+
*/
|
|
209
|
+
readonly readProjectFile: (relative: string) => string | undefined;
|
|
210
|
+
/** The Swift sources of the App group (`ios/App/App`). */
|
|
211
|
+
readonly swiftSources: readonly string[];
|
|
212
|
+
}
|
|
213
|
+
export declare const iosPushEntitlementStep: string;
|
|
214
|
+
export declare const iosPushTokenForwardingStep: string;
|
|
215
|
+
export declare const iosPushColdStartStep: string;
|
|
216
|
+
/**
|
|
217
|
+
* What a shell with `@capacitor-firebase/messaging` still lacks on iOS: the
|
|
218
|
+
* `aps-environment` entitlement on the App target's Release configuration,
|
|
219
|
+
* the AppDelegate's APNs token forwarding, and the scene delegate's
|
|
220
|
+
* cold-start tap forwarding (for a shell with a scene delegate). Each is an
|
|
221
|
+
* error: without the first two no push reaches the app, and without the
|
|
222
|
+
* third a tap on a notification while the app is not running opens it
|
|
223
|
+
* without the notification's route. An entitlement the kit cannot follow
|
|
224
|
+
* (`inspectIosPushEntitlement`) is a warning. The forwarding checks are
|
|
225
|
+
* textual: the token must be handed on inside
|
|
226
|
+
* `didRegisterForRemoteNotificationsWithDeviceToken`, and some App source
|
|
227
|
+
* must read `.notificationResponse`.
|
|
228
|
+
*/
|
|
229
|
+
export declare function inspectIosPushWiring(input: IosPushWiringInput): readonly NativeShellPolicyIssue[];
|
|
230
|
+
/** The Firebase config file the messaging plugin reads from the app bundle. */
|
|
231
|
+
export declare const iosFirebaseConfigFile = "GoogleService-Info.plist";
|
|
232
|
+
export declare function inspectIosFirebaseConfig(project: string): readonly NativeShellPolicyIssue[];
|