eas-cli
Version:
EAS command line tool
325 lines (324 loc) • 13.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectBuildToCompareAsync = void 0;
const tslib_1 = require("tslib");
const eas_build_job_1 = require("@expo/eas-build-job");
const core_1 = require("@oclif/core");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
const builds_1 = require("../../commandUtils/builds");
const flags_1 = require("../../commandUtils/flags");
const generated_1 = require("../../graphql/generated");
const BuildQuery_1 = require("../../graphql/queries/BuildQuery");
const log_1 = tslib_1.__importDefault(require("../../log"));
const ora_1 = require("../../ora");
const projectUtils_1 = require("../../project/projectUtils");
const workflow_1 = require("../../project/workflow");
const prompts_1 = require("../../prompts");
const fingerprintCli_1 = require("../../utils/fingerprintCli");
const fingerprintDiff_1 = require("../../utils/fingerprintDiff");
const formatFields_1 = tslib_1.__importDefault(require("../../utils/formatFields"));
const json_1 = require("../../utils/json");
class FingerprintCompare extends EasCommand_1.default {
static description = 'compare fingerprints of the current project, builds and updates';
static hidden = true;
static flags = {
'build-id': core_1.Flags.string({
aliases: ['buildId'],
description: 'Compare the fingerprint with the build with the specified ID',
}),
...flags_1.EasNonInteractiveAndJsonFlags,
};
static contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync() {
const { flags } = await this.parse(FingerprintCompare);
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
const { projectId, privateProjectConfig: { projectDir }, loggedIn: { graphqlClient }, vcsClient, } = await this.getContextAsync(FingerprintCompare, {
nonInteractive,
withServerSideEnvironment: null,
});
if (jsonFlag) {
(0, json_1.enableJsonOutput)();
}
const displayName = await (0, projectUtils_1.getDisplayNameForProjectIdAsync)(graphqlClient, projectId);
let buildId = buildIdFromArg;
if (!buildId) {
if (nonInteractive) {
throw new Error('Build ID must be provided in non-interactive mode');
}
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
return;
}
}
log_1.default.log(`Comparing fingerprints of the current project and build ${buildId}…`);
const buildWithFingerprint = await BuildQuery_1.BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
const fingerprintDebugUrl = buildWithFingerprint.fingerprint?.debugInfoUrl;
if (!fingerprintDebugUrl) {
log_1.default.error('A fingerprint for the build could not be found.');
return;
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
const fingerprint = (await fingerprintResponse.json());
const workflows = await (0, workflow_1.resolveWorkflowPerPlatformAsync)(projectDir, vcsClient);
const buildPlatform = buildWithFingerprint.platform;
const workflow = workflows[appPlatformToPlatform(buildPlatform)];
const projectFingerprint = await (0, fingerprintCli_1.createFingerprintAsync)(projectDir, {
workflow,
platforms: [appPlatformToString(buildPlatform)],
debug: true,
env: undefined,
});
if (!projectFingerprint) {
log_1.default.error('Project fingerprints can only be computed for projects with SDK 52 or higher');
return;
}
if (fingerprint.hash === projectFingerprint.hash) {
log_1.default.log(`✅ Project fingerprint matches build`);
return;
}
else {
log_1.default.log(`🔄 Project fingerprint differs from build`);
}
const fingerprintDiffs = (0, fingerprintCli_1.diffFingerprint)(projectDir, fingerprint, projectFingerprint);
if (!fingerprintDiffs) {
log_1.default.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
return;
}
const filePathDiffs = fingerprintDiffs.filter(diff => {
let sourceType;
if (diff.op === 'added') {
sourceType = diff.addedSource.type;
}
else if (diff.op === 'removed') {
sourceType = diff.removedSource.type;
}
else if (diff.op === 'changed') {
sourceType = diff.beforeSource.type;
}
return sourceType === 'dir' || sourceType === 'file';
});
if (filePathDiffs.length > 0) {
log_1.default.newLine();
log_1.default.log('📁 Paths with native dependencies:');
}
const fields = [];
for (const diff of filePathDiffs) {
const field = getDiffFilePathFields(diff);
if (!field) {
throw new Error(`Unsupported diff: ${JSON.stringify(diff)}`);
}
fields.push(field);
}
log_1.default.log((0, formatFields_1.default)(fields, {
labelFormat: label => ` ${chalk_1.default.dim(label)}:`,
}));
const contentDiffs = fingerprintDiffs.filter(diff => {
let sourceType;
if (diff.op === 'added') {
sourceType = diff.addedSource.type;
}
else if (diff.op === 'removed') {
sourceType = diff.removedSource.type;
}
else if (diff.op === 'changed') {
sourceType = diff.beforeSource.type;
}
return sourceType === 'contents';
});
for (const diff of contentDiffs) {
printContentDiff(diff);
}
}
}
exports.default = FingerprintCompare;
function printContentDiff(diff) {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
if (sourceType === 'contents') {
printContentSource({
op: diff.op,
sourceType,
contentsId: diff.addedSource.id,
contentsAfter: diff.addedSource.contents,
});
}
}
else if (diff.op === 'removed') {
const sourceType = diff.removedSource.type;
if (sourceType === 'contents') {
printContentSource({
op: diff.op,
sourceType,
contentsId: diff.removedSource.id,
contentsBefore: diff.removedSource.contents,
});
}
}
else if (diff.op === 'changed') {
const sourceType = diff.beforeSource.type;
if (sourceType === 'contents') {
if (diff.afterSource.type !== 'contents') {
throw new Error(`Changed fingerprint source types must be the same, received ${diff.beforeSource.type}, ${diff.afterSource.type}`);
}
printContentSource({
op: diff.op,
sourceType: diff.beforeSource.type, // before and after source types should be the same
contentsId: diff.beforeSource.id, // before and after content ids should be the same
contentsBefore: diff.beforeSource.contents,
contentsAfter: diff.afterSource.contents,
});
}
}
}
function getDiffFilePathFields(diff) {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType,
filePath: diff.addedSource.filePath,
});
}
}
else if (diff.op === 'removed') {
const sourceType = diff.removedSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType,
filePath: diff.removedSource.filePath,
});
}
}
else if (diff.op === 'changed') {
const sourceType = diff.beforeSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType: diff.beforeSource.type, // before and after source types should be the same
filePath: diff.beforeSource.filePath, // before and after filePaths should be the same
});
}
}
return null;
}
function getFilePathSourceFields({ op, sourceType, filePath, }) {
if (sourceType === 'dir') {
if (op === 'added') {
return { label: 'new directory', value: filePath };
}
else if (op === 'removed') {
return { label: 'removed directory', value: filePath };
}
else if (op === 'changed') {
return { label: 'modified directory', value: filePath };
}
}
else if (sourceType === 'file') {
if (op === 'added') {
return { label: 'new file', value: filePath };
}
else if (op === 'removed') {
return { label: 'removed file', value: filePath };
}
else if (op === 'changed') {
return { label: 'modified file', value: filePath };
}
}
throw new Error(`Unsupported source and op: ${sourceType}, ${op}`);
}
const PRETTY_CONTENT_ID = {
'expoAutolinkingConfig:ios': 'Expo autolinking config (iOS)',
'expoAutolinkingConfig:android': 'Expo autolinking config (Android)',
'packageJson:scripts': 'package.json scripts',
expoConfig: 'Expo config',
};
function printContentSource({ op, contentsBefore, contentsAfter, contentsId, }) {
log_1.default.newLine();
const prettyContentId = PRETTY_CONTENT_ID[contentsId] ?? contentsId;
if (op === 'added') {
log_1.default.log(`${chalk_1.default.dim('📝 New content')}: ${prettyContentId}`);
}
else if (op === 'removed') {
log_1.default.log(`${chalk_1.default.dim('📝 Removed content')}: ${prettyContentId}`);
}
else if (op === 'changed') {
log_1.default.log(`${chalk_1.default.dim('📝 Modified content')}: ${prettyContentId}`);
}
printContentsDiff(contentsBefore ?? '', contentsAfter ?? '');
}
function printContentsDiff(contents1, contents2) {
const stringifiedContents1 = Buffer.isBuffer(contents1) ? contents1.toString() : contents1;
const stringifiedContents2 = Buffer.isBuffer(contents2) ? contents2.toString() : contents2;
const isStr1JSON = isJSON(stringifiedContents1);
const isStr2JSON = isJSON(stringifiedContents2);
const prettifiedContents1 = isStr1JSON
? JSON.stringify(JSON.parse(stringifiedContents1), null, 2)
: stringifiedContents1;
const prettifiedContents2 = isStr2JSON
? JSON.stringify(JSON.parse(stringifiedContents2), null, 2)
: stringifiedContents2;
(0, fingerprintDiff_1.abridgedDiff)(prettifiedContents1, prettifiedContents2, 0);
}
function isJSON(str) {
try {
JSON.parse(str);
return true;
}
catch {
return false;
}
}
function appPlatformToPlatform(platform) {
switch (platform) {
case generated_1.AppPlatform.Android:
return eas_build_job_1.Platform.ANDROID;
case generated_1.AppPlatform.Ios:
return eas_build_job_1.Platform.IOS;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
function appPlatformToString(platform) {
switch (platform) {
case generated_1.AppPlatform.Android:
return 'android';
case generated_1.AppPlatform.Ios:
return 'ios';
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
async function selectBuildToCompareAsync(graphqlClient, projectId, projectDisplayName, { filters, } = {}) {
const spinner = (0, ora_1.ora)().start('Fetching builds…');
let builds;
try {
builds = await (0, builds_1.fetchBuildsAsync)({ graphqlClient, projectId, filters });
spinner.stop();
}
catch (error) {
spinner.fail(`Something went wrong and we couldn't fetch the builds for the project ${projectDisplayName}.`);
throw error;
}
if (builds.length === 0) {
log_1.default.warn(`No fingerprints have been computed for builds of project ${projectDisplayName}.`);
return null;
}
else {
const build = await (0, prompts_1.selectAsync)('Which build do you want to compare?', builds.map(build => ({
title: (0, builds_1.formatBuild)(build),
value: build.id,
})));
return build;
}
}
exports.selectBuildToCompareAsync = selectBuildToCompareAsync;
;