eas-cli 4.1.1 → 5.0.0
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/README.md +55 -55
- package/build/build/context.d.ts +1 -0
- package/build/build/createContext.js +5 -0
- package/build/build/local.js +1 -1
- package/build/build/metadata.js +1 -0
- package/build/build/runBuildAndSubmit.js +33 -8
- package/build/channel/branch-mapping.d.ts +6 -5
- package/build/channel/branch-mapping.js +8 -1
- package/build/channel/queries.d.ts +18 -0
- package/build/channel/queries.js +85 -5
- package/build/channel/utils.d.ts +13 -2
- package/build/commands/build/version/get.js +1 -0
- package/build/commands/build/version/sync.js +1 -0
- package/build/commands/channel/create.js +5 -53
- package/build/commands/metadata/lint.js +1 -0
- package/build/commands/metadata/pull.js +1 -0
- package/build/commands/metadata/push.js +1 -0
- package/build/commands/submit.js +1 -0
- package/build/commands/update/index.js +9 -0
- package/build/commands/update/republish.js +55 -12
- package/build/commands/update/roll-back-to-embedded.js +5 -1
- package/build/commands/update/rollback.d.ts +9 -0
- package/build/commands/update/rollback.js +40 -0
- package/build/credentials/ios/actions/SetUpAdhocProvisioningProfile.js +10 -1
- package/build/log.d.ts +1 -0
- package/build/log.js +3 -0
- package/build/project/publish.d.ts +1 -0
- package/build/project/publish.js +9 -5
- package/build/rollout/actions/EndRollout.d.ts +4 -4
- package/build/rollout/branch-mapping.d.ts +12 -12
- package/build/rollout/branch-mapping.js +18 -6
- package/build/rollout/utils.d.ts +2 -2
- package/build/update/republish.d.ts +2 -2
- package/build/update/republish.js +23 -6
- package/build/utils/profiles.d.ts +2 -1
- package/build/utils/profiles.js +35 -3
- package/oclif.manifest.json +1 -1
- package/package.json +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EASUpdateAction, EASUpdateContext } from '../../eas-update/utils';
|
|
2
2
|
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
|
|
3
|
-
import { UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
|
|
3
|
+
import { UpdateBranchObject, UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
|
|
4
4
|
import { Rollout } from '../branch-mapping';
|
|
5
5
|
export declare enum EndOutcome {
|
|
6
6
|
REPUBLISH_AND_REVERT = "republish-and-revert",
|
|
@@ -21,7 +21,7 @@ export declare class EndRollout implements EASUpdateAction<UpdateChannelBasicInf
|
|
|
21
21
|
constructor(channelInfo: UpdateChannelBasicInfoFragment, options: Partial<NonInteractiveOptions> & GeneralOptions);
|
|
22
22
|
runAsync(ctx: EASUpdateContext): Promise<UpdateChannelBasicInfoFragment>;
|
|
23
23
|
getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject>;
|
|
24
|
-
selectOutcomeAsync(rollout: Rollout): Promise<EndOutcome>;
|
|
25
|
-
performOutcomeAsync(ctx: EASUpdateContext, rollout: Rollout
|
|
26
|
-
confirmOutcomeAsync(ctx: EASUpdateContext, selectedOutcome: EndOutcome, rollout: Rollout): Promise<boolean>;
|
|
24
|
+
selectOutcomeAsync(rollout: Rollout<UpdateBranchObject>): Promise<EndOutcome>;
|
|
25
|
+
performOutcomeAsync(ctx: EASUpdateContext, rollout: Rollout<UpdateBranchObject>, outcome: EndOutcome): Promise<UpdateChannelBasicInfoFragment>;
|
|
26
|
+
confirmOutcomeAsync(ctx: EASUpdateContext, selectedOutcome: EndOutcome, rollout: Rollout<UpdateBranchObject>): Promise<boolean>;
|
|
27
27
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { BranchMapping, BranchMappingAlwaysTrue } from '../channel/branch-mapping';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export type Rollout = LegacyRollout | ConstrainedRollout;
|
|
2
|
+
import { BranchBasicInfo, ChannelBasicInfo, UpdateChannelInfoWithBranches } from '../channel/utils';
|
|
3
|
+
export type Rollout<Branch extends BranchBasicInfo> = LegacyRollout<Branch> | ConstrainedRollout<Branch>;
|
|
5
4
|
export type RolloutInfo = LegacyRolloutInfo | ConstrainedRolloutInfo;
|
|
6
|
-
type ConstrainedRollout = LegacyRollout & {
|
|
5
|
+
type ConstrainedRollout<Branch extends BranchBasicInfo> = LegacyRollout<Branch> & {
|
|
7
6
|
runtimeVersion: string;
|
|
8
7
|
};
|
|
9
|
-
type LegacyRollout = {
|
|
10
|
-
rolledOutBranch:
|
|
11
|
-
defaultBranch:
|
|
8
|
+
type LegacyRollout<Branch extends BranchBasicInfo> = {
|
|
9
|
+
rolledOutBranch: Branch;
|
|
10
|
+
defaultBranch: Branch;
|
|
12
11
|
} & LegacyRolloutInfo;
|
|
13
12
|
type ConstrainedRolloutInfo = LegacyRolloutInfo & {
|
|
14
13
|
runtimeVersion: string;
|
|
@@ -58,11 +57,11 @@ export type ConstrainedRolloutBranchMapping = {
|
|
|
58
57
|
};
|
|
59
58
|
export declare function isLegacyRolloutInfo(rollout: RolloutInfo): rollout is LegacyRolloutInfo;
|
|
60
59
|
export declare function isConstrainedRolloutInfo(rollout: RolloutInfo): rollout is ConstrainedRolloutInfo;
|
|
61
|
-
export declare function isConstrainedRollout(rollout: Rollout): rollout is ConstrainedRollout
|
|
62
|
-
export declare function getRolloutInfo(basicChannelInfo:
|
|
60
|
+
export declare function isConstrainedRollout<Branch extends BranchBasicInfo>(rollout: Rollout<Branch>): rollout is ConstrainedRollout<Branch>;
|
|
61
|
+
export declare function getRolloutInfo(basicChannelInfo: ChannelBasicInfo): RolloutInfo;
|
|
63
62
|
export declare function getRolloutInfoFromBranchMapping(branchMapping: RolloutBranchMapping): RolloutInfo;
|
|
64
|
-
export declare function getRollout(channel:
|
|
65
|
-
export declare function composeRollout(rolloutInfo: RolloutInfo, defaultBranch:
|
|
63
|
+
export declare function getRollout<Branch extends BranchBasicInfo>(channel: UpdateChannelInfoWithBranches<Branch>): Rollout<Branch>;
|
|
64
|
+
export declare function composeRollout<Branch extends BranchBasicInfo>(rolloutInfo: RolloutInfo, defaultBranch: Branch, rolledOutBranch: Branch): Rollout<Branch>;
|
|
66
65
|
export declare function getRolloutBranchMapping(branchMappingString: string): RolloutBranchMapping;
|
|
67
66
|
/**
|
|
68
67
|
* Detect if a branch mapping is a rollout.
|
|
@@ -116,7 +115,8 @@ export declare function getRolloutBranchMapping(branchMappingString: string): Ro
|
|
|
116
115
|
}
|
|
117
116
|
*/
|
|
118
117
|
export declare function isRolloutBranchMapping(branchMapping: BranchMapping): branchMapping is RolloutBranchMapping;
|
|
119
|
-
export declare function isRollout(channelInfo:
|
|
118
|
+
export declare function isRollout(channelInfo: ChannelBasicInfo): boolean;
|
|
119
|
+
export declare function doesTargetRollout(branchMapping: RolloutBranchMapping, runtimeVersion: string): boolean;
|
|
120
120
|
export declare function createRolloutBranchMapping({ defaultBranchId, rolloutBranchId, percent, runtimeVersion, }: {
|
|
121
121
|
defaultBranchId: string;
|
|
122
122
|
rolloutBranchId: string;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.assertRolloutBranchMapping = exports.editRolloutBranchMapping = exports.createRolloutBranchMapping = exports.isRollout = exports.isRolloutBranchMapping = exports.getRolloutBranchMapping = exports.composeRollout = exports.getRollout = exports.getRolloutInfoFromBranchMapping = exports.getRolloutInfo = exports.isConstrainedRollout = exports.isConstrainedRolloutInfo = exports.isLegacyRolloutInfo = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
3
|
+
exports.assertRolloutBranchMapping = exports.editRolloutBranchMapping = exports.createRolloutBranchMapping = exports.doesTargetRollout = exports.isRollout = exports.isRolloutBranchMapping = exports.getRolloutBranchMapping = exports.composeRollout = exports.getRollout = exports.getRolloutInfoFromBranchMapping = exports.getRolloutInfo = exports.isConstrainedRollout = exports.isConstrainedRolloutInfo = exports.isLegacyRolloutInfo = void 0;
|
|
6
4
|
const branch_mapping_1 = require("../channel/branch-mapping");
|
|
7
5
|
const utils_1 = require("../channel/utils");
|
|
8
6
|
function isLegacyRolloutInfo(rollout) {
|
|
@@ -30,12 +28,16 @@ function getRolloutInfoFromBranchMapping(branchMapping) {
|
|
|
30
28
|
(0, branch_mapping_1.assertStatement)(statementNode);
|
|
31
29
|
const nodesFromStatement = (0, branch_mapping_1.getNodesFromStatement)(statementNode);
|
|
32
30
|
const runtimeVersionNode = nodesFromStatement.find(isRuntimeVersionNode);
|
|
33
|
-
|
|
31
|
+
if (!runtimeVersionNode) {
|
|
32
|
+
throw new branch_mapping_1.BranchMappingValidationError('Runtime version node must be defined.');
|
|
33
|
+
}
|
|
34
34
|
(0, branch_mapping_1.assertNodeObject)(runtimeVersionNode);
|
|
35
35
|
const runtimeVersion = runtimeVersionNode.operand;
|
|
36
36
|
(0, branch_mapping_1.assertString)(runtimeVersion);
|
|
37
37
|
const rolloutNode = nodesFromStatement.find(isRolloutNode);
|
|
38
|
-
|
|
38
|
+
if (!rolloutNode) {
|
|
39
|
+
throw new branch_mapping_1.BranchMappingValidationError('Rollout node must be defined.');
|
|
40
|
+
}
|
|
39
41
|
(0, branch_mapping_1.assertNodeObject)(rolloutNode);
|
|
40
42
|
const operand = rolloutNode.operand;
|
|
41
43
|
(0, branch_mapping_1.assertNumber)(operand);
|
|
@@ -149,6 +151,14 @@ function isRollout(channelInfo) {
|
|
|
149
151
|
return isRolloutBranchMapping(branchMapping);
|
|
150
152
|
}
|
|
151
153
|
exports.isRollout = isRollout;
|
|
154
|
+
function doesTargetRollout(branchMapping, runtimeVersion) {
|
|
155
|
+
const rolloutInfo = getRolloutInfoFromBranchMapping(branchMapping);
|
|
156
|
+
if (!isConstrainedRolloutInfo(rolloutInfo)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return rolloutInfo.runtimeVersion === runtimeVersion;
|
|
160
|
+
}
|
|
161
|
+
exports.doesTargetRollout = doesTargetRollout;
|
|
152
162
|
function createRolloutBranchMapping({ defaultBranchId, rolloutBranchId, percent, runtimeVersion, }) {
|
|
153
163
|
assertPercent(percent);
|
|
154
164
|
return {
|
|
@@ -190,7 +200,9 @@ function editRtvConstrainedRollout(branchMapping, percent) {
|
|
|
190
200
|
const statementNode = newBranchMapping.data[0].branchMappingLogic;
|
|
191
201
|
const nodesFromStatement = (0, branch_mapping_1.getNodesFromStatement)(statementNode);
|
|
192
202
|
const rolloutNode = nodesFromStatement.find(isRolloutNode);
|
|
193
|
-
|
|
203
|
+
if (!rolloutNode) {
|
|
204
|
+
throw new branch_mapping_1.BranchMappingValidationError('Rollout node must be defined.');
|
|
205
|
+
}
|
|
194
206
|
rolloutNode.operand = percent / 100;
|
|
195
207
|
return newBranchMapping;
|
|
196
208
|
}
|
package/build/rollout/utils.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { RuntimeFragment, UpdateFragment } from '../graphql/generated';
|
|
1
|
+
import { RuntimeFragment, UpdateBranchBasicInfoFragment, UpdateFragment } from '../graphql/generated';
|
|
2
2
|
import { UpdateBranchObject, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
|
3
3
|
import { Rollout } from './branch-mapping';
|
|
4
4
|
export declare function printRollout(channel: UpdateChannelObject): void;
|
|
5
|
-
export declare function displayRolloutDetails(channelName: string, rollout: Rollout): void;
|
|
5
|
+
export declare function displayRolloutDetails(channelName: string, rollout: Rollout<UpdateBranchBasicInfoFragment>): void;
|
|
6
6
|
export declare function formatBranchWithUpdateGroup(maybeUpdateGroup: UpdateFragment[] | undefined | null, branch: UpdateBranchObject, percentRolledOut: number): string;
|
|
7
7
|
export declare function formatRuntimeWithUpdateGroup(maybeUpdateGroup: UpdateFragment[] | undefined | null, runtime: RuntimeFragment, branchName: string): string;
|
|
8
8
|
export declare function promptForRolloutPercentAsync({ promptMessage, }: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ExpoConfig } from '@expo/config';
|
|
2
2
|
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
3
|
-
import {
|
|
3
|
+
import { UpdateFragment } from '../graphql/generated';
|
|
4
4
|
import { CodeSigningInfo } from '../utils/code-signing';
|
|
5
5
|
export type UpdateToRepublish = {
|
|
6
6
|
groupId: string;
|
|
7
7
|
branchId: string;
|
|
8
8
|
branchName: string;
|
|
9
|
-
} &
|
|
9
|
+
} & UpdateFragment;
|
|
10
10
|
/**
|
|
11
11
|
* @param updatesToPublish The update group to republish
|
|
12
12
|
* @param targetBranch The branch to repubish the update group on
|
|
@@ -27,6 +27,8 @@ async function republishAsync({ graphqlClient, app, updatesToPublish, targetBran
|
|
|
27
27
|
update.branchName === arbitraryUpdate.branchName &&
|
|
28
28
|
update.runtimeVersion === arbitraryUpdate.runtimeVersion;
|
|
29
29
|
(0, assert_1.default)(updatesToPublish.every(isSameGroup), 'All updates must belong to the same update group');
|
|
30
|
+
(0, assert_1.default)(updatesToPublish.every(u => u.isRollBackToEmbedded) ||
|
|
31
|
+
updatesToPublish.every(u => !u.isRollBackToEmbedded), 'All updates must either be roll back to embedded updates or not');
|
|
30
32
|
const { runtimeVersion } = arbitraryUpdate;
|
|
31
33
|
// If codesigning was created for the original update, we need to add it to the republish.
|
|
32
34
|
// If one wishes to not sign the republish or sign with a different key, a normal publish should
|
|
@@ -47,13 +49,20 @@ async function republishAsync({ graphqlClient, app, updatesToPublish, targetBran
|
|
|
47
49
|
const publishIndicator = (0, ora_1.ora)('Republishing...').start();
|
|
48
50
|
let updatesRepublished;
|
|
49
51
|
try {
|
|
50
|
-
const
|
|
52
|
+
const arbitraryUpdate = updatesToPublish[0];
|
|
53
|
+
const objectToMergeIn = arbitraryUpdate.isRollBackToEmbedded
|
|
54
|
+
? {
|
|
55
|
+
rollBackToEmbeddedInfoGroup: Object.fromEntries(updatesToPublish.map(update => [update.platform, true])),
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
updateInfoGroup: Object.fromEntries(updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])),
|
|
59
|
+
};
|
|
51
60
|
updatesRepublished = await PublishMutation_1.PublishMutation.publishUpdateGroupAsync(graphqlClient, [
|
|
52
61
|
{
|
|
53
62
|
branchId: targetBranchId,
|
|
54
63
|
runtimeVersion,
|
|
55
64
|
message: updateMessage,
|
|
56
|
-
|
|
65
|
+
...objectToMergeIn,
|
|
57
66
|
gitCommitHash: updatesToPublish[0].gitCommitHash,
|
|
58
67
|
awaitingCodeSigningInfo: !!codeSigningInfo,
|
|
59
68
|
},
|
|
@@ -65,13 +74,21 @@ async function republishAsync({ graphqlClient, app, updatesToPublish, targetBran
|
|
|
65
74
|
method: 'GET',
|
|
66
75
|
headers: { accept: 'multipart/mixed' },
|
|
67
76
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
let signature;
|
|
78
|
+
if (newUpdate.isRollBackToEmbedded) {
|
|
79
|
+
const directiveBody = (0, nullthrows_1.default)(await (0, code_signing_1.getDirectiveBodyAsync)(response));
|
|
80
|
+
(0, code_signing_1.checkDirectiveBodyAgainstUpdateInfoGroup)(directiveBody);
|
|
81
|
+
signature = (0, code_signing_1.signBody)(directiveBody, codeSigningInfo);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const manifestBody = (0, nullthrows_1.default)(await (0, code_signing_1.getManifestBodyAsync)(response));
|
|
85
|
+
(0, code_signing_1.checkManifestBodyAgainstUpdateInfoGroup)(manifestBody, (0, nullthrows_1.default)((0, nullthrows_1.default)(objectToMergeIn.updateInfoGroup)[newUpdate.platform]));
|
|
86
|
+
signature = (0, code_signing_1.signBody)(manifestBody, codeSigningInfo);
|
|
87
|
+
}
|
|
71
88
|
await PublishMutation_1.PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
|
|
72
89
|
alg: codeSigningInfo.codeSigningMetadata.alg,
|
|
73
90
|
keyid: codeSigningInfo.codeSigningMetadata.keyid,
|
|
74
|
-
sig:
|
|
91
|
+
sig: signature,
|
|
75
92
|
});
|
|
76
93
|
}));
|
|
77
94
|
}
|
|
@@ -6,10 +6,11 @@ export type ProfileData<T extends ProfileType> = {
|
|
|
6
6
|
platform: Platform;
|
|
7
7
|
profileName: string;
|
|
8
8
|
};
|
|
9
|
-
export declare function getProfilesAsync<T extends ProfileType>({ easJsonAccessor, platforms, profileName, type, }: {
|
|
9
|
+
export declare function getProfilesAsync<T extends ProfileType>({ easJsonAccessor, platforms, profileName, type, projectDir, }: {
|
|
10
10
|
easJsonAccessor: EasJsonAccessor;
|
|
11
11
|
platforms: Platform[];
|
|
12
12
|
profileName?: string;
|
|
13
|
+
projectDir: string;
|
|
13
14
|
type: T;
|
|
14
15
|
}): Promise<ProfileData<T>[]>;
|
|
15
16
|
/**
|
package/build/utils/profiles.js
CHANGED
|
@@ -3,14 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.maybePrintBuildProfileDeprecationWarningsAsync = exports.clearHasPrintedDeprecationWarnings = exports.getProfilesAsync = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const eas_json_1 = require("@expo/eas-json");
|
|
6
|
+
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
7
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
|
+
const semver_1 = tslib_1.__importDefault(require("semver"));
|
|
6
9
|
const log_1 = tslib_1.__importStar(require("../log"));
|
|
7
|
-
async function getProfilesAsync({ easJsonAccessor, platforms, profileName, type, }) {
|
|
10
|
+
async function getProfilesAsync({ easJsonAccessor, platforms, profileName, type, projectDir, }) {
|
|
8
11
|
const results = platforms.map(async function (platform) {
|
|
9
|
-
const profile = await
|
|
12
|
+
const profile = await readProfileWithOverridesAsync({
|
|
10
13
|
easJsonAccessor,
|
|
11
14
|
platform,
|
|
12
15
|
type,
|
|
13
16
|
profileName,
|
|
17
|
+
projectDir,
|
|
14
18
|
});
|
|
15
19
|
return {
|
|
16
20
|
profile,
|
|
@@ -21,10 +25,38 @@ async function getProfilesAsync({ easJsonAccessor, platforms, profileName, type,
|
|
|
21
25
|
return await Promise.all(results);
|
|
22
26
|
}
|
|
23
27
|
exports.getProfilesAsync = getProfilesAsync;
|
|
24
|
-
async function
|
|
28
|
+
async function maybeSetNodeVersionFromFileAsync(projectDir, profile) {
|
|
29
|
+
if (profile === null || profile === void 0 ? void 0 : profile.node) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const nodeVersion = await getNodeVersionFromFileAsync(projectDir);
|
|
33
|
+
if (nodeVersion) {
|
|
34
|
+
log_1.default.log(`The EAS build profile does not specify a Node.js version. Using the version specified in .nvmrc: ${nodeVersion} `);
|
|
35
|
+
profile.node = nodeVersion;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function getNodeVersionFromFileAsync(projectDir) {
|
|
39
|
+
const nvmrcPath = path_1.default.join(projectDir, '.nvmrc');
|
|
40
|
+
if (!(await fs_extra_1.default.pathExists(nvmrcPath))) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let nodeVersion;
|
|
44
|
+
try {
|
|
45
|
+
nodeVersion = (await fs_extra_1.default.readFile(nvmrcPath, 'utf8')).toString().trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (!semver_1.default.valid(semver_1.default.coerce(nodeVersion))) {
|
|
51
|
+
throw new Error(`Invalid node version in .nvmrc: ${nodeVersion}`);
|
|
52
|
+
}
|
|
53
|
+
return nodeVersion;
|
|
54
|
+
}
|
|
55
|
+
async function readProfileWithOverridesAsync({ easJsonAccessor, platform, type, profileName, projectDir, }) {
|
|
25
56
|
if (type === 'build') {
|
|
26
57
|
const buildProfile = await eas_json_1.EasJsonUtils.getBuildProfileAsync(easJsonAccessor, platform, profileName);
|
|
27
58
|
await maybePrintBuildProfileDeprecationWarningsAsync(easJsonAccessor, platform, profileName);
|
|
59
|
+
await maybeSetNodeVersionFromFileAsync(projectDir, buildProfile);
|
|
28
60
|
return buildProfile;
|
|
29
61
|
}
|
|
30
62
|
else {
|