From 9bea32c8f063acf56abdaf6d016a96c3504e7d0e Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 13:43:42 +0300 Subject: [PATCH 1/2] Replace Q with native promises - Q deferreds and Q.Promise(...) calls become new Promise(...) - Q(x), Q.all and .spread become Promise.resolve, Promise.all and array destructuring - Drop .done() in cli.ts; Node exits on unhandled rejections the same way - Remove the unused ReleaseHook type - Report errors that Q used to drop: an unreadable release folder no longer exits 0 silently, and yazl input errors no longer crash the process - Reject with an Error when signing an empty folder Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 18 ---- package.json | 2 - script/binary-utils.ts | 7 +- script/cli.ts | 11 +-- script/command-executor.ts | 47 +++++------ script/commands/debug.ts | 5 +- script/hash-utils.ts | 169 ++++++++++++++++--------------------- script/management-sdk.ts | 161 ++++++++++++++++------------------- script/types/cli.ts | 8 -- test/cli.ts | 2 +- test/management-sdk.ts | 3 +- 11 files changed, 180 insertions(+), 253 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd55e20..2a43d42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "prompt": "^1.3.0", "properties": "^1.2.1", "protobufjs": "^7.6.3", - "q": "~1.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", "superagent": "^8.0.9", @@ -42,7 +41,6 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^20.3.1", - "@types/q": "^1.5.8", "@types/sinon": "^10.0.15", "@types/yauzl": "^3.4.0", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -427,13 +425,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==" }, - "node_modules/@types/q": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", - "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -2907,15 +2898,6 @@ "node": ">=6" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", diff --git a/package.json b/package.json index c09e6bb..536b272 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "prompt": "^1.3.0", "properties": "^1.2.1", "protobufjs": "^7.6.3", - "q": "~1.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", "superagent": "^8.0.9", @@ -63,7 +62,6 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^20.3.1", - "@types/q": "^1.5.8", "@types/sinon": "^10.0.15", "@types/yauzl": "^3.4.0", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/script/binary-utils.ts b/script/binary-utils.ts index eebd2d3..b54290f 100644 --- a/script/binary-utils.ts +++ b/script/binary-utils.ts @@ -4,7 +4,6 @@ import * as crypto from "crypto"; import * as chalk from "chalk"; import { log } from "./command-executor"; import * as os from "os"; -import * as Q from "q"; import * as yazl from "yazl"; import * as yauzl from "yauzl"; import { readFile } from "node:fs/promises"; @@ -140,11 +139,13 @@ async function takeHashesFromMetadata(metadataPath: string): Promise item.split(":"))); } -function createZipArchive(sourceFolder: string, zipPath: string, filesToInclude: string[]): Q.Promise { - return Q.Promise((resolve, reject) => { +function createZipArchive(sourceFolder: string, zipPath: string, filesToInclude: string[]): Promise { + return new Promise((resolve, reject) => { const zipFile = new yazl.ZipFile(); const writeStream = fs.createWriteStream(zipPath); + // yazl reports unreadable input files on the ZipFile itself + zipFile.on("error", reject); zipFile.outputStream .pipe(writeStream) .on("error", (error: Error) => { diff --git a/script/cli.ts b/script/cli.ts index c143d57..3de0272 100644 --- a/script/cli.ts +++ b/script/cli.ts @@ -15,13 +15,10 @@ function run() { return; } - execute - .execute(command) - .catch((error: any): void => { - console.error(chalk.red(`[Error] ${error.message}`)); - process.exit(1); - }) - .done(); + execute.execute(command).catch((error: any): void => { + console.error(chalk.red(`[Error] ${error.message}`)); + process.exit(1); + }); } run(); diff --git a/script/command-executor.ts b/script/command-executor.ts index 5b8b635..5b094ca 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -10,14 +10,12 @@ import * as chalk from "chalk"; import * as moment from "moment"; import * as os from "os"; import * as path from "path"; -import * as Q from "q"; import * as semver from "semver"; import * as cli from "../script/types/cli"; import sign from "./sign"; const ApkReader = require("@devicefarmer/adbkit-apkreader"); import { AccessKey, - Account, App, CodePushError, CollaboratorMap, @@ -53,7 +51,6 @@ import { import AccountManager = require("./management-sdk"); import wordwrap = require("wordwrap"); -import Promise = Q.Promise; import { ReactNativePackageInfo } from "./types/rest-definitions"; import { getExpoCliPath } from "./expo-utils"; @@ -103,7 +100,7 @@ let connectionInfo: ILoginConnectionInfo; export const confirm = (message: string = "Are you sure?"): Promise => { message += " (y/N):"; - return Promise((resolve): void => { + return new Promise((resolve): void => { prompt.message = ""; prompt.delimiter = ""; @@ -291,15 +288,15 @@ function deleteConnectionInfoCache(printMessage: boolean = true): void { } function deleteFolder(folderPath: string): Promise { - return Q(fs.promises.rm(folderPath, { recursive: true, force: true, maxRetries: 3 })); + return fs.promises.rm(folderPath, { recursive: true, force: true, maxRetries: 3 }); } -async function deleteReactTempFolders(): globalThis.Promise { +async function deleteReactTempFolders(): Promise { const tempFolder: string = os.tmpdir(); // An unreadable temp folder means there is nothing to delete const names: string[] = await fs.promises.readdir(tempFolder).catch((): string[] => []); const reactFolders: string[] = names.filter((name) => name.startsWith("react-")); - await Q.all(reactFolders.map((name) => deleteFolder(path.join(tempFolder, name)))); + await Promise.all(reactFolders.map((name) => deleteFolder(path.join(tempFolder, name)))); } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { @@ -359,11 +356,11 @@ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage: } }); } else { - return Q(null); + return Promise.resolve(); } }); - return Q.all(metricsPromises); + return Promise.all(metricsPromises); } }) .then(() => { @@ -373,7 +370,7 @@ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage: function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise { const confirmation = command.isForce - ? Q.resolve(true) + ? Promise.resolve(true) : confirm("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable."); return confirmation.then((wasConfirmed: boolean): Promise => { @@ -404,11 +401,11 @@ function deploymentRename(command: cli.IDeploymentRenameCommand): Promise function deploymentHistory(command: cli.IDeploymentHistoryCommand): Promise { throwForInvalidOutputFormat(command.format); - return Q.all([ + return Promise.all([ sdk.getAccountInfo(), sdk.getDeploymentHistory(command.appName, command.deploymentName), sdk.getDeploymentMetrics(command.appName, command.deploymentName), - ]).spread((account: Account, deploymentHistory: Package[], metrics: DeploymentMetrics): void => { + ]).then(([account, deploymentHistory, metrics]): void => { const totalActive: number = getTotalActiveFromDeploymentMetrics(metrics); deploymentHistory.forEach((packageObject: Package) => { if (metrics[packageObject.label]) { @@ -450,7 +447,7 @@ function deserializeConnectionInfo(): ILoginConnectionInfo { export function execute(command: cli.ICommand) { connectionInfo = deserializeConnectionInfo(); - return Q(null).then(() => { + return Promise.resolve().then(() => { switch (command.type) { // Must not be logged in case cli.CommandType.login: @@ -604,7 +601,7 @@ function initiateExternalAuthenticationAsync(action: string, serverUrl?: string) function link(command: cli.ILinkCommand): Promise { initiateExternalAuthenticationAsync("link", command.serverUrl); - return Q(null); + return Promise.resolve(); } function login(command: cli.ILoginCommand): Promise { @@ -646,7 +643,7 @@ function loginWithExternalAuthentication(action: string, apiServerUrl?: string, } function logout(_command: cli.ICommand): Promise { - return Q(null) + return Promise.resolve() .then((): Promise => { if (!connectionInfo.preserveAccessKeyOnLogout) { const machineName: string = os.hostname(); @@ -940,9 +937,9 @@ function getReactNativeProjectVersionInfo(command: cli.IReleaseReactCommand, pro ); } - return Q({ appVersion: rawShortVersion, buildNumber: rawBundleVersion }); + return Promise.resolve({ appVersion: rawShortVersion, buildNumber: rawBundleVersion }); } else if (command.platform === "android") { - return Q(getAndroidVersionInfo(command.gradleFile)); + return getAndroidVersionInfo(command.gradleFile); } } @@ -997,7 +994,7 @@ function getAppVersionFromXcodeProject( throw new Error(`The "CURRENT_PROJECT_VERSION" key doesn't exist in the "${resolvedPbxprojFile}" file.`); } - return Q({ appVersion: marketingVersion, buildNumber: String(currentProjectVersion) }); + return Promise.resolve({ appVersion: marketingVersion, buildNumber: String(currentProjectVersion) }); } function printJson(object: any): void { @@ -1187,7 +1184,7 @@ export const runExpoExportEmbedCommand = async ({ const expoBundleProcess = spawn("node", expoBundleArgs); log(`node ${expoBundleArgs.join(" ")}`); - return Promise((resolve, reject) => { + return new Promise((resolve, reject) => { expoBundleProcess.stdout.on("data", (data: Buffer) => { log(data.toString().trim()); }); @@ -1287,7 +1284,7 @@ async function planBundle( outputFolder: string, bundleName: string, sourcemapOutput: string -): globalThis.Promise { +): Promise { const isHermes = command.useHermes || (await isHermesEnabled(command)); const sourceMaps = getSourceMapPaths(command, bundleName, sourcemapOutput, isHermes); return { @@ -1311,7 +1308,7 @@ async function compileHermesBundle( bundleName: string, sourceMaps: SourceMapPaths, baseReleaseTmpFolder: string -): globalThis.Promise { +): Promise { await createEmptyTempReleaseFolder(baseReleaseTmpFolder); const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName); @@ -1376,7 +1373,7 @@ export const releaseExpo = (command: cli.IReleaseReactCommand): Promise => // via --buildNumber. Auto-detection only applies to release-native where the binary // build number is the natural targeting key. const versionInfoPromise: Promise = command.appStoreVersion - ? Q({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) + ? Promise.resolve({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) : getReactNativeProjectVersionInfo(command, projectName).then((detected) => ({ appVersion: detected.appVersion, buildNumber: command.buildNumber, @@ -1463,7 +1460,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = // via --buildNumber. Auto-detection only applies to release-native where the binary // build number is the natural targeting key. const versionInfoPromise: Promise = command.appStoreVersion - ? Q({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) + ? Promise.resolve({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) : getReactNativeProjectVersionInfo(command, projectName).then((detected) => ({ appVersion: detected.appVersion, buildNumber: command.buildNumber, @@ -1781,7 +1778,7 @@ function rollback(command: cli.IRollbackCommand): Promise { } function requestAccessKey(): Promise { - return Promise((resolve): void => { + return new Promise((resolve): void => { prompt.message = ""; prompt.delimiter = ""; @@ -1850,7 +1847,7 @@ export const runReactNativeBundleCommand = async ( const reactNativeBundleProcess = spawn("node", reactNativeBundleArgs); log(`node ${reactNativeBundleArgs.join(" ")}`); - return Promise((resolve, reject) => { + return new Promise((resolve, reject) => { reactNativeBundleProcess.stdout.on("data", (data: Buffer) => { log(data.toString().trim()); }); diff --git a/script/commands/debug.ts b/script/commands/debug.ts index 787bf07..4c556c1 100644 --- a/script/commands/debug.ts +++ b/script/commands/debug.ts @@ -5,7 +5,6 @@ import * as childProcess from "child_process"; import * as cli from "../../script/types/cli"; import * as moment from "moment"; import * as path from "path"; -import * as Q from "q"; const simctl = require("simctl"); const which = require("which"); @@ -123,8 +122,8 @@ const debugPlatforms: any = { ios: new iOSDebugPlatform(), }; -export default function (command: cli.IDebugCommand): Q.Promise { - return Q.Promise((resolve, reject) => { +export default function (command: cli.IDebugCommand): Promise { + return new Promise((resolve, reject) => { const platform: string = command.platform.toLowerCase(); const debugPlatform: IDebugPlatform = debugPlatforms[platform]; diff --git a/script/hash-utils.ts b/script/hash-utils.ts index b8217e0..abc5778 100644 --- a/script/hash-utils.ts +++ b/script/hash-utils.ts @@ -9,14 +9,12 @@ import * as crypto from "crypto"; import * as fs from "fs"; import * as path from "path"; -import * as q from "q"; import * as stream from "stream"; import * as yauzl from "yauzl"; import { listFiles } from "./utils/file-utils"; -import Promise = q.Promise; const HASH_ALGORITHM = "sha256"; export function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise { @@ -30,90 +28,71 @@ export function generatePackageHashFromDirectory(directoryPath: string, basePath } export function generatePackageManifestFromZip(filePath: string): Promise { - const deferred: q.Deferred = q.defer(); - const reject = (error: Error) => { - if (deferred.promise.isPending()) { - deferred.reject(error); - } - }; - - const resolve = (manifest: PackageManifest) => { - if (deferred.promise.isPending()) { - deferred.resolve(manifest); - } - }; - let zipFile: any; - yauzl.open(filePath, { lazyEntries: true }, (error?: any, openedZipFile?: any): void => { - if (error) { - // This is the first time we try to read the package as a .zip file; - // however, it may not be a .zip file. Handle this gracefully. - resolve(null); - return; - } - - zipFile = openedZipFile; - const fileHashesMap = new Map(); - const hashFilePromises: q.Promise[] = []; - - // Read each entry in the archive sequentially and generate a hash for it. - zipFile.readEntry(); - zipFile - .on("error", (error: any): void => { - reject(error); - }) - .on("entry", (entry: any): void => { - const fileName: string = PackageManifest.normalizePath(entry.fileName); - if (PackageManifest.isIgnored(fileName)) { - zipFile.readEntry(); - return; - } + return new Promise((resolve, reject) => { + yauzl.open(filePath, { lazyEntries: true }, (error?: any, openedZipFile?: any): void => { + if (error) { + // This is the first time we try to read the package as a .zip file; + // however, it may not be a .zip file. Handle this gracefully. + resolve(null); + return; + } - zipFile.openReadStream(entry, (error?: any, readStream?: stream.Readable): void => { - if (error) { - reject(error); + zipFile = openedZipFile; + const fileHashesMap = new Map(); + const hashFilePromises: Promise[] = []; + + // Read each entry in the archive sequentially and generate a hash for it. + zipFile.readEntry(); + zipFile + .on("error", (error: any): void => { + reject(error); + }) + .on("entry", (entry: any): void => { + const fileName: string = PackageManifest.normalizePath(entry.fileName); + if (PackageManifest.isIgnored(fileName)) { + zipFile.readEntry(); return; } - hashFilePromises.push( - hashStream(readStream).then((hash: string) => { - fileHashesMap.set(fileName, hash); - zipFile.readEntry(); - }, reject) - ); + zipFile.openReadStream(entry, (error?: any, readStream?: stream.Readable): void => { + if (error) { + reject(error); + return; + } + + hashFilePromises.push( + hashStream(readStream).then((hash: string) => { + fileHashesMap.set(fileName, hash); + zipFile.readEntry(); + }, reject) + ); + }); + }) + .on("end", (): void => { + Promise.all(hashFilePromises).then(() => resolve(new PackageManifest(fileHashesMap)), reject); }); - }) - .on("end", (): void => { - q.all(hashFilePromises).then(() => resolve(new PackageManifest(fileHashesMap)), reject); - }); - }); - - return deferred.promise.finally(() => zipFile && zipFile.close()); + }); + }).finally(() => zipFile && zipFile.close()); } -export function generatePackageManifestFromDirectory(directoryPath: string, basePath: string): Promise { - const fileHashesMap = new Map(); +export async function generatePackageManifestFromDirectory(directoryPath: string, basePath: string): Promise { + const files: string[] = await listFiles(directoryPath); + if (files.length === 0) { + throw new Error("Can't sign the release because no files were found."); + } - return q(listFiles(directoryPath)).then((files: string[]): Promise => { - if (files.length === 0) { - return q.reject("Error: Can't sign the release because no files were found."); + // Hash the files sequentially, because streaming them in parallel is not necessarily faster + const fileHashesMap = new Map(); + for (const filePath of files) { + const relativePath: string = PackageManifest.normalizePath(path.relative(basePath, filePath)); + if (!PackageManifest.isIgnored(relativePath)) { + fileHashesMap.set(relativePath, await hashFile(filePath)); } + } - // Hash the files sequentially, because streaming them in parallel is not necessarily faster - const generateManifestPromise: Promise = files.reduce((soFar: Promise, filePath: string) => { - return soFar.then(() => { - const relativePath: string = PackageManifest.normalizePath(path.relative(basePath, filePath)); - if (!PackageManifest.isIgnored(relativePath)) { - return hashFile(filePath).then((hash: string) => { - fileHashesMap.set(relativePath, hash); - }); - } - }); - }, q(null)); - - return generateManifestPromise.then(() => new PackageManifest(fileHashesMap)); - }); + return new PackageManifest(fileHashesMap); } export function hashFile(filePath: string): Promise { @@ -123,29 +102,31 @@ export function hashFile(filePath: string): Promise { export function hashStream(readStream: stream.Readable): Promise { const hashStream = (crypto.createHash(HASH_ALGORITHM)); - const deferred: q.Deferred = q.defer(); - - readStream - .on("error", (error: any): void => { - if (deferred.promise.isPending()) { - hashStream.end(); - deferred.reject(error); - } - }) - .on("end", (): void => { - if (deferred.promise.isPending()) { - hashStream.end(); + let isSettled = false; - const buffer = hashStream.read(); - const hash: string = buffer.toString("hex"); + return new Promise((resolve, reject) => { + readStream + .on("error", (error: any): void => { + if (!isSettled) { + isSettled = true; + hashStream.end(); + reject(error); + } + }) + .on("end", (): void => { + if (!isSettled) { + isSettled = true; + hashStream.end(); - deferred.resolve(hash); - } - }); + const buffer = hashStream.read(); + const hash: string = buffer.toString("hex"); - readStream.pipe(hashStream); + resolve(hash); + } + }); - return deferred.promise; + readStream.pipe(hashStream); + }); } export class PackageManifest { @@ -172,7 +153,7 @@ export class PackageManifest { // can also compute this hash easily given the update contents. entries = entries.sort(); - return q(crypto.createHash(HASH_ALGORITHM).update(JSON.stringify(entries)).digest("hex")); + return Promise.resolve(crypto.createHash(HASH_ALGORITHM).update(JSON.stringify(entries)).digest("hex")); } public serialize(): string { diff --git a/script/management-sdk.ts b/script/management-sdk.ts index 85afc77..77e1d1a 100644 --- a/script/management-sdk.ts +++ b/script/management-sdk.ts @@ -4,12 +4,9 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import Q = require("q"); import superagent = require("superagent"); import * as yazl from "yazl"; -import Promise = Q.Promise; - import { AccessKey, AccessKeyRequest, @@ -87,7 +84,7 @@ class AccountManager { } public isAuthenticated(throwIfUnauthorized?: boolean): Promise { - return Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const request: superagent.Request = superagent.get(`${this._serverUrl}${urlEncode(["/authenticated"])}`); this.attachCredentials(request); @@ -304,73 +301,62 @@ class AccountManager { ); } - public release( + public async release( appName: string, deploymentName: string, filePath: string, updateMetadata: PackageInfo, uploadProgressCallback?: (progress: number) => void ): Promise { - return Promise((resolve, reject) => { + const packageFile: PackageFile = await this.packageFileFromPath(filePath); + return new Promise((resolve, reject) => { const request: superagent.Request = superagent.post( this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]) ); this.attachCredentials(request); - const getPackageFilePromise = Q.Promise((resolve, reject) => { - this.packageFileFromPath(filePath) - .then((result) => { - resolve(result); - }) - .catch((error) => { - reject(error); - }); - }); + const file: any = fs.createReadStream(packageFile.path); + request + .attach("package", file) + .field("packageInfo", JSON.stringify(updateMetadata)) + .on("progress", (event: any) => { + if (uploadProgressCallback && event && event.total > 0) { + const currentProgress: number = (event.loaded / event.total) * 100; + uploadProgressCallback(currentProgress); + } + }) + .end((err: any, res: superagent.Response) => { + if (packageFile.isTemporary) { + fs.unlinkSync(packageFile.path); + } - getPackageFilePromise.then((packageFile: PackageFile) => { - const file: any = fs.createReadStream(packageFile.path); - request - .attach("package", file) - .field("packageInfo", JSON.stringify(updateMetadata)) - .on("progress", (event: any) => { - if (uploadProgressCallback && event && event.total > 0) { - const currentProgress: number = (event.loaded / event.total) * 100; - uploadProgressCallback(currentProgress); - } - }) - .end((err: any, res: superagent.Response) => { - if (packageFile.isTemporary) { - fs.unlinkSync(packageFile.path); - } + if (err) { + reject(this.getCodePushError(err, res)); + return; + } - if (err) { - reject(this.getCodePushError(err, res)); - return; - } + if (res.ok) { + resolve(null); + } else { + let body; + try { + body = JSON.parse(res.text); + } catch (err) {} - if (res.ok) { - resolve(null); + if (body) { + reject({ + message: body.message, + statusCode: res && res.status, + }); } else { - let body; - try { - body = JSON.parse(res.text); - } catch (err) {} - - if (body) { - reject({ - message: body.message, - statusCode: res && res.status, - }); - } else { - reject({ - message: res.text, - statusCode: res && res.status, - }); - } + reject({ + message: res.text, + statusCode: res && res.status, + }); } - }); - }); + } + }); }); } @@ -381,7 +367,7 @@ class AccountManager { updateMetadata: ReactNativePackageInfo, uploadProgressCallback?: (progress: number) => void ): Promise { - return Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const request: superagent.Request = superagent.post( this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/nativerelease`]) ); @@ -461,43 +447,38 @@ class AccountManager { ).then(() => null); } - private packageFileFromPath(filePath: string) { - let getPackageFilePromise: Promise; - if (fs.lstatSync(filePath).isDirectory()) { - const directoryPath: string = filePath; - getPackageFilePromise = Q(listFiles(directoryPath)).then((files: string[]) => - Promise((resolve: (file: PackageFile) => void, reject: (reason: Error) => void): void => { - const baseDirectoryPath = path.dirname(directoryPath); - const fileName: string = this.generateRandomFilename(15) + ".zip"; - const zipFile = new yazl.ZipFile(); - const writeStream: fs.WriteStream = fs.createWriteStream(fileName); - - zipFile.outputStream - .pipe(writeStream) - .on("error", (error: Error): void => { - reject(error); - }) - .on("close", (): void => { - filePath = path.join(process.cwd(), fileName); - - resolve({ isTemporary: true, path: filePath }); - }); + private async packageFileFromPath(filePath: string): Promise { + if (!fs.lstatSync(filePath).isDirectory()) { + return { isTemporary: false, path: filePath }; + } - for (let i = 0; i < files.length; ++i) { - const file: string = files[i]; - // yazl does not like backslash (\) in the metadata path. - const relativePath: string = normalizePath(path.relative(baseDirectoryPath, file)); + const files: string[] = await listFiles(filePath); + return new Promise((resolve, reject) => { + const baseDirectoryPath = path.dirname(filePath); + const fileName: string = this.generateRandomFilename(15) + ".zip"; + const zipFile = new yazl.ZipFile(); + const writeStream: fs.WriteStream = fs.createWriteStream(fileName); + + // yazl reports unreadable input files on the ZipFile itself + zipFile.on("error", reject); + zipFile.outputStream + .pipe(writeStream) + .on("error", (error: Error): void => { + reject(error); + }) + .on("close", (): void => { + resolve({ isTemporary: true, path: path.join(process.cwd(), fileName) }); + }); - zipFile.addFile(file, relativePath); - } + for (const file of files) { + // yazl does not like backslash (\) in the metadata path. + const relativePath: string = normalizePath(path.relative(baseDirectoryPath, file)); - zipFile.end(); - }) - ); - } else { - getPackageFilePromise = Q({ isTemporary: false, path: filePath }); - } - return getPackageFilePromise; + zipFile.addFile(file, relativePath); + } + + zipFile.end(); + }); } private generateRandomFilename(length: number): string { @@ -544,7 +525,7 @@ class AccountManager { expectResponseBody: boolean, contentType: string ): Promise { - return Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let request: superagent.Request = (superagent)[method](this._serverUrl + endpoint); this.attachCredentials(request); diff --git a/script/types/cli.ts b/script/types/cli.ts index 25ce9ff..72cf73d 100644 --- a/script/types/cli.ts +++ b/script/types/cli.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import AccountManager = require("../management-sdk"); - export enum CommandType { accessKeyAdd, accessKeyPatch, @@ -242,12 +240,6 @@ export interface ISessionRemoveCommand extends ICommand { machineName: string; } -export type ReleaseHook = ( - currentCommand: IReleaseCommand, - originalCommand: IReleaseCommand, - sdk: AccountManager -) => Q.Promise; - export interface ReleaseFile { sourceLocation: string; // The current location of the file on disk targetLocation: string; // The desired location of the file within the zip diff --git a/test/cli.ts b/test/cli.ts index 221bed1..aa8b636 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -26,7 +26,7 @@ function fakeBundler(_command: string, args: string[]) { return fakeBundlerProcess(args); } -/** Runs the command, adapting the CLI's Q promise to a native one for `assert.rejects` */ +/** Runs the command; being async turns synchronous throws into rejections for `assert.rejects` */ async function execute(command: cli.ICommand): Promise { await cmdexec.execute(command); } diff --git a/test/management-sdk.ts b/test/management-sdk.ts index 3750a9f..d087f22 100644 --- a/test/management-sdk.ts +++ b/test/management-sdk.ts @@ -47,8 +47,7 @@ function mockServerResponse(status: number, body = "", header: Record ...` to assert.rejects to get a native promise -type SdkCall = (manager: AccountManager) => PromiseLike; +type SdkCall = (manager: AccountManager) => Promise; const callsThatPropagateServerErrors: Array<[name: string, call: SdkCall]> = [ ["addApp", (manager) => manager.addApp("appName")], From b6c3352c987040d056809c8b2810b27669b0b8d1 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 14:03:35 +0300 Subject: [PATCH 2/2] Wait for the app version before preparing release folders releaseReact and releaseExpo started version detection, awaited the source map folder, then returned the detection promise. A detection failure in between was an unhandled rejection and crashed Node instead of printing the CLI error. Co-Authored-By: Claude Opus 5.5 --- script/command-executor.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/script/command-executor.ts b/script/command-executor.ts index 5b094ca..d40a44c 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -1372,18 +1372,15 @@ export const releaseExpo = (command: cli.IReleaseReactCommand): Promise => // For release-expo, buildNumber is NOT auto-detected — it must be passed explicitly // via --buildNumber. Auto-detection only applies to release-native where the binary // build number is the natural targeting key. - const versionInfoPromise: Promise = command.appStoreVersion - ? Promise.resolve({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) - : getReactNativeProjectVersionInfo(command, projectName).then((detected) => ({ - appVersion: detected.appVersion, - buildNumber: command.buildNumber, - })); + const versionInfo: ProjectVersionInfo = command.appStoreVersion + ? { appVersion: command.appStoreVersion, buildNumber: command.buildNumber } + : { appVersion: (await getReactNativeProjectVersionInfo(command, projectName)).appVersion, buildNumber: command.buildNumber }; if (!sourcemapOutputFolder.endsWith(".map") && !command.sourcemapOutput) { await createEmptyTempReleaseFolder(sourcemapOutputFolder); } - return versionInfoPromise; + return versionInfo; }) .then(({ appVersion, buildNumber }: ProjectVersionInfo) => { throwForInvalidSemverRange(appVersion); @@ -1459,19 +1456,19 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = // For release-react, buildNumber is NOT auto-detected — it must be passed explicitly // via --buildNumber. Auto-detection only applies to release-native where the binary // build number is the natural targeting key. - const versionInfoPromise: Promise = command.appStoreVersion - ? Promise.resolve({ appVersion: command.appStoreVersion, buildNumber: command.buildNumber }) - : getReactNativeProjectVersionInfo(command, projectName).then((detected) => ({ - appVersion: detected.appVersion, + const versionInfo: ProjectVersionInfo = command.appStoreVersion + ? { appVersion: command.appStoreVersion, buildNumber: command.buildNumber } + : { + appVersion: (await getReactNativeProjectVersionInfo(command, projectName)).appVersion, buildNumber: command.buildNumber, - })); + }; if (!sourcemapOutputFolder.endsWith(".map") && !command.sourcemapOutput) { // create tmp dir only if no dir was given by user. User must crete a directory if --sourcemapOutput is passes await createEmptyTempReleaseFolder(sourcemapOutputFolder); } - return versionInfoPromise; + return versionInfo; }) .then(({ appVersion, buildNumber }: ProjectVersionInfo) => { throwForInvalidSemverRange(appVersion);