From 2c2832afdd149b773f16d9b522364c56927f6709 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 12:26:24 +0300 Subject: [PATCH 1/3] Replace rimraf, temp, recursive-fs and slash with Node built-ins - rimraf: fs.promises.rm with the same retries; the react-* temp glob becomes an explicit prefix match - temp: fs.mkdtempSync - recursive-fs: a local listFiles with the same order and symlink handling - slash: normalizePath (yazl converts backslashes anyway) - Drop the Q deferreds around directory listing so sync errors reject - sign: return the rejection when copying a single file fails Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 78 -------------------------------------- package.json | 4 -- script/command-executor.ts | 28 +++++++------- script/hash-utils.ts | 36 ++++-------------- script/management-sdk.ts | 21 ++++------ script/sign.ts | 2 +- script/utils/file-utils.ts | 32 +++++++++++++--- test/file-utils.ts | 46 ++++++++++++++++++++++ 8 files changed, 103 insertions(+), 144 deletions(-) create mode 100644 test/file-utils.ts diff --git a/package-lock.json b/package-lock.json index c262934..c1e4929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,13 +26,9 @@ "properties": "^1.2.1", "protobufjs": "^7.6.3", "q": "~1.5.1", - "recursive-fs": "2.1.0", - "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "slash": "1.0.0", "superagent": "^8.0.9", - "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", "xcode": "^3.0.1", @@ -2306,14 +2302,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -2999,18 +2987,6 @@ "node": ">= 0.10" } }, - "node_modules/recursive-fs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/recursive-fs/-/recursive-fs-2.1.0.tgz", - "integrity": "sha512-oed3YruYsD52Mi16s/07eYblQOLi5dTtxpIJNdfCEJ7S5v8dDgVcycar0pRWf4IBuPMIkoctC8RTqGJzIKMNAQ==", - "bin": { - "recursive-copy": "bin/recursive-copy", - "recursive-delete": "bin/recursive-delete" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3062,17 +3038,6 @@ "node": ">= 0.4.0" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3289,14 +3254,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3443,41 +3400,6 @@ "node": ">= 0.4.0" } }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index dea9a8b..c09e6bb 100644 --- a/package.json +++ b/package.json @@ -43,13 +43,9 @@ "properties": "^1.2.1", "protobufjs": "^7.6.3", "q": "~1.5.1", - "recursive-fs": "2.1.0", - "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "slash": "1.0.0", "superagent": "^8.0.9", - "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", "xcode": "^3.0.1", diff --git a/script/command-executor.ts b/script/command-executor.ts index cd057ba..07465e5 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -63,8 +63,6 @@ const plist = require("plist"); const progress = require("progress"); const prompt = require("prompt"); -const rimraf = require("rimraf"); - const Table = require("cli-table"); const xcode = require("xcode"); @@ -293,15 +291,19 @@ function deleteConnectionInfoCache(printMessage: boolean = true): void { } function deleteFolder(folderPath: string): Promise { - return Promise((resolve, reject) => { - rimraf(folderPath, (err: any) => { - if (err) { - reject(err); - } else { - resolve(null); - } - }); - }); + return Q(fs.promises.rm(folderPath, { recursive: true, force: true, maxRetries: 3 })); +} + +// Deletes the react-* entries the React Native bundler leaves in the temp folder +function deleteReactTempFolders(): Promise { + const tempFolder: string = os.tmpdir(); + // Like the rimraf glob this replaces, an unreadable temp folder means there is nothing to delete + return Q(fs.promises.readdir(tempFolder).catch((): string[] => [])) + .then((names: string[]) => { + const reactFolders: string[] = names.filter((name) => name.startsWith("react-")); + return Q.all(reactFolders.map((name) => deleteFolder(path.join(tempFolder, name)))); + }) + .then(() => {}); } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { @@ -1397,7 +1399,7 @@ export const releaseExpo = (command: cli.IReleaseReactCommand): Promise => return createEmptyTempReleaseFolder(outputFolder); }) - .then(() => deleteFolder(`${os.tmpdir()}/react-*`)) + .then(() => deleteReactTempFolders()) .then(async () => { const { isHermes, sourceMaps, bundleOptions } = await planBundle(command, outputFolder, bundleName, sourcemapOutputFolder); await runExpoExportEmbedCommand(bundleOptions); @@ -1487,7 +1489,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = }) // This is needed to clear the react native bundler cache: // https://github.com/facebook/react-native/issues/4289 - .then(() => deleteFolder(`${os.tmpdir()}/react-*`)) + .then(() => deleteReactTempFolders()) .then(async () => { const { isHermes, sourceMaps, bundleOptions } = await planBundle(command, outputFolder, bundleName, sourcemapOutputFolder); await runReactNativeBundleCommand(bundleOptions, entryFile, isHermes); diff --git a/script/hash-utils.ts b/script/hash-utils.ts index 5b4d6b2..b8217e0 100644 --- a/script/hash-utils.ts +++ b/script/hash-utils.ts @@ -12,18 +12,9 @@ import * as path from "path"; import * as q from "q"; import * as stream from "stream"; -// Do not throw an exception if either of these modules are missing, as they may not be needed by the -// consumer of this file. -// - recursiveFs: Only required for hashing of directories -// - yauzl: Only required for in-memory hashing of zip files -let recursiveFs, yauzl; - -try { - recursiveFs = require("recursive-fs"); -} catch (e) {} -try { - yauzl = require("yauzl"); -} catch (e) {} +import * as yauzl from "yauzl"; + +import { listFiles } from "./utils/file-utils"; import Promise = q.Promise; const HASH_ALGORITHM = "sha256"; @@ -102,18 +93,11 @@ export function generatePackageManifestFromZip(filePath: string): Promise { - const deferred: q.Deferred = q.defer(); const fileHashesMap = new Map(); - recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]): void => { - if (error) { - deferred.reject(error); - return; - } - - if (!files || files.length === 0) { - deferred.reject("Error: Can't sign the release because no files were found."); - return; + 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 @@ -128,14 +112,8 @@ export function generatePackageManifestFromDirectory(directoryPath: string, base }); }, q(null)); - generateManifestPromise - .then(() => { - deferred.resolve(new PackageManifest(fileHashesMap)); - }, deferred.reject) - .done(); + return generateManifestPromise.then(() => new PackageManifest(fileHashesMap)); }); - - return deferred.promise; } export function hashFile(filePath: string): Promise { diff --git a/script/management-sdk.ts b/script/management-sdk.ts index a419b6b..85afc77 100644 --- a/script/management-sdk.ts +++ b/script/management-sdk.ts @@ -6,9 +6,7 @@ import * as os from "os"; import * as path from "path"; import Q = require("q"); import superagent = require("superagent"); -import * as recursiveFs from "recursive-fs"; import * as yazl from "yazl"; -import slash = require("slash"); import Promise = Q.Promise; @@ -29,6 +27,7 @@ import { BaseRelease, } from "./types"; import { ReactNativePackageInfo } from "./types/rest-definitions"; +import { listFiles, normalizePath } from "./utils/file-utils"; const packageJson = require("../../package.json"); @@ -465,15 +464,9 @@ class AccountManager { private packageFileFromPath(filePath: string) { let getPackageFilePromise: Promise; if (fs.lstatSync(filePath).isDirectory()) { - getPackageFilePromise = Promise((resolve: (file: PackageFile) => void, reject: (reason: Error) => void): void => { - const directoryPath: string = filePath; - - recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]) => { - if (error) { - reject(error); - return; - } - + 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(); @@ -493,14 +486,14 @@ class AccountManager { 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 = slash(path.relative(baseDirectoryPath, file)); + const relativePath: string = normalizePath(path.relative(baseDirectoryPath, file)); zipFile.addFile(file, relativePath); } zipFile.end(); - }); - }); + }) + ); } else { getPackageFilePromise = Q({ isTemporary: false, path: filePath }); } diff --git a/script/sign.ts b/script/sign.ts index fd3b8fb..e78743f 100644 --- a/script/sign.ts +++ b/script/sign.ts @@ -31,7 +31,7 @@ export default async function sign(privateKeyPath: string, updateContentsPath: s updateContentsPath = copyFileToTmpDir(updateContentsPath); } } catch (error) { - Promise.reject(error); + return Promise.reject(error); } const signatureFilePath: string = path.join(updateContentsPath, METADATA_FILE_NAME); diff --git a/script/utils/file-utils.ts b/script/utils/file-utils.ts index d2c22de..61200b4 100644 --- a/script/utils/file-utils.ts +++ b/script/utils/file-utils.ts @@ -1,7 +1,6 @@ import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; -import * as rimraf from "rimraf"; -import * as temp from "temp"; import * as yauzl from "yauzl"; import { pipeline } from "node:stream/promises"; @@ -25,9 +24,7 @@ export function fileExists(file: string): boolean { export function copyFileToTmpDir(filePath: string): string { if (!isDirectory(filePath)) { - const outputFolderPath: string = temp.mkdirSync("code-push"); - rimraf.sync(outputFolderPath); - fs.mkdirSync(outputFolderPath); + const outputFolderPath: string = fs.mkdtempSync(path.join(os.tmpdir(), "code-push")); const outputFilePath: string = path.join(outputFolderPath, path.basename(filePath)); fs.writeFileSync(outputFilePath, fs.readFileSync(filePath)); @@ -36,6 +33,31 @@ export function copyFileToTmpDir(filePath: string): string { } } +/** + * Lists the files under a folder, level by level, with paths joined by "/". + * Symlinks are listed as files, even when they point to a folder. + */ +export async function listFiles(directoryPath: string): Promise { + const files: string[] = []; + let folders: string[] = [directoryPath]; + while (folders.length > 0) { + const subfolders: string[] = []; + const levelEntries = await Promise.all(folders.map((folder) => fs.promises.readdir(folder, { withFileTypes: true }))); + levelEntries.forEach((entries, i) => { + for (const entry of entries) { + const entryPath = `${folders[i]}/${entry.name}`; + if (entry.isDirectory()) { + subfolders.push(entryPath); + } else { + files.push(entryPath); + } + } + }); + folders = subfolders; + } + return files; +} + export function fileDoesNotExistOrIsDirectory(path: string): boolean { try { return isDirectory(path); diff --git a/test/file-utils.ts b/test/file-utils.ts new file mode 100644 index 0000000..b621846 --- /dev/null +++ b/test/file-utils.ts @@ -0,0 +1,46 @@ +import { strict as assert } from "assert"; +import * as fs from "fs"; +import * as path from "path"; +import { copyFileToTmpDir, listFiles } from "../script/utils/file-utils"; +import { useTempDirectory } from "./helpers/temp-directory"; + +describe("file-utils", () => { + const dir = useTempDirectory("file-utils"); + + describe("listFiles", () => { + it("lists files level by level, joining paths with a slash", async () => { + dir.write("release/b/deep/c.txt"); + dir.write("release/a.txt"); + dir.write("release/b/b.txt"); + const root = path.join(dir.path, "release"); + + assert.deepEqual(await listFiles(root), [`${root}/a.txt`, `${root}/b/b.txt`, `${root}/b/deep/c.txt`]); + }); + + it("lists a symlink to a folder as a file", async () => { + dir.write("release/folder/a.txt"); + const root = path.join(dir.path, "release"); + fs.symlinkSync("folder", path.join(root, "link")); + + assert.deepEqual((await listFiles(root)).sort(), [`${root}/folder/a.txt`, `${root}/link`]); + }); + + it("rejects when the folder does not exist", async () => { + await assert.rejects(listFiles(path.join(dir.path, "missing")), { code: "ENOENT" }); + }); + }); + + describe("copyFileToTmpDir", () => { + it("copies the file into a new temp folder", () => { + const file = dir.write("main.jsbundle", "bundle"); + + const folder = copyFileToTmpDir(file); + try { + assert.deepEqual(fs.readdirSync(folder), ["main.jsbundle"]); + assert.equal(fs.readFileSync(path.join(folder, "main.jsbundle"), "utf8"), "bundle"); + } finally { + fs.rmSync(folder, { recursive: true, force: true }); + } + }); + }); +}); From ee23be72c49efac38f14c052d698455a88504090 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 12:33:27 +0300 Subject: [PATCH 2/3] Simplify listFiles Co-Authored-By: Claude Opus 5.5 --- script/utils/file-utils.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/script/utils/file-utils.ts b/script/utils/file-utils.ts index 61200b4..6a26e68 100644 --- a/script/utils/file-utils.ts +++ b/script/utils/file-utils.ts @@ -38,26 +38,21 @@ export function copyFileToTmpDir(filePath: string): string { * Symlinks are listed as files, even when they point to a folder. */ export async function listFiles(directoryPath: string): Promise { - const files: string[] = []; + let files: string[] = []; let folders: string[] = [directoryPath]; while (folders.length > 0) { - const subfolders: string[] = []; - const levelEntries = await Promise.all(folders.map((folder) => fs.promises.readdir(folder, { withFileTypes: true }))); - levelEntries.forEach((entries, i) => { - for (const entry of entries) { - const entryPath = `${folders[i]}/${entry.name}`; - if (entry.isDirectory()) { - subfolders.push(entryPath); - } else { - files.push(entryPath); - } - } - }); - folders = subfolders; + const entries = (await Promise.all(folders.map(readFolder))).flat(); + files = files.concat(entries.filter((entry) => !entry.isFolder).map((entry) => entry.path)); + folders = entries.filter((entry) => entry.isFolder).map((entry) => entry.path); } return files; } +async function readFolder(folder: string): Promise<{ path: string; isFolder: boolean }[]> { + const entries = await fs.promises.readdir(folder, { withFileTypes: true }); + return entries.map((entry) => ({ path: `${folder}/${entry.name}`, isFolder: entry.isDirectory() })); +} + export function fileDoesNotExistOrIsDirectory(path: string): boolean { try { return isDirectory(path); From dbc30ba283e0389e78057b0d76ecdd93459ef5e3 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 12:48:25 +0300 Subject: [PATCH 3/3] Simplify deleteReactTempFolders Co-Authored-By: Claude Opus 5.5 --- script/command-executor.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/script/command-executor.ts b/script/command-executor.ts index 07465e5..5b8b635 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -294,16 +294,12 @@ function deleteFolder(folderPath: string): Promise { return Q(fs.promises.rm(folderPath, { recursive: true, force: true, maxRetries: 3 })); } -// Deletes the react-* entries the React Native bundler leaves in the temp folder -function deleteReactTempFolders(): Promise { +async function deleteReactTempFolders(): globalThis.Promise { const tempFolder: string = os.tmpdir(); - // Like the rimraf glob this replaces, an unreadable temp folder means there is nothing to delete - return Q(fs.promises.readdir(tempFolder).catch((): string[] => [])) - .then((names: string[]) => { - const reactFolders: string[] = names.filter((name) => name.startsWith("react-")); - return Q.all(reactFolders.map((name) => deleteFolder(path.join(tempFolder, name)))); - }) - .then(() => {}); + // 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)))); } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise {