UNPKG

eas-cli

Version:
325 lines (324 loc) 13.3 kB
"use strict"; 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;