diff --git a/package-lock.json b/package-lock.json index 826ed23..bd55e20 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", @@ -3000,18 +2988,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", @@ -3063,17 +3039,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", @@ -3290,14 +3255,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", @@ -3444,41 +3401,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..5b8b635 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,15 @@ 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 })); +} + +async function deleteReactTempFolders(): globalThis.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)))); } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { @@ -1397,7 +1395,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 +1485,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..6a26e68 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,26 @@ 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 { + let files: string[] = []; + let folders: string[] = [directoryPath]; + while (folders.length > 0) { + 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); 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 }); + } + }); + }); +});