From 71692889b0d89b033c07ef87ee3dbf6d62d79235 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 23 May 2022 08:27:44 -0700 Subject: [PATCH] Cleanup Folders after codegen (#33891) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/33891 This diff adds a function to clean up folders and empty files (if any) after the codegen runs in the OSS. To align how iOS and Android behave in the codegen, we now have to preemptively create folders that we may not need later. Thanks to this function, we recursively the folders in the codegen and we delete them if they are empty. ## Changelog [iOS][Added] - Add function to cleanup codegen folders Reviewed By: cortinico Differential Revision: D36594999 fbshipit-source-id: 904aa2442c0d9621b7ec77c31f7be9daa4e7b7e1 --- .../generate-artifacts-executor-test.js | 225 ++++++++++++++++++ .../codegen/generate-artifacts-executor.js | 35 +++ 2 files changed, 260 insertions(+) diff --git a/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/scripts/codegen/__tests__/generate-artifacts-executor-test.js index b53806d26f778f..17a78e9d542186 100644 --- a/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -201,3 +201,228 @@ describe('extractLibrariesFromJSON', () => { }); }); }); + +describe('delete empty files and folders', () => { + beforeEach(() => { + jest.resetModules(); + }); + + it('when path is empty file, deletes it', () => { + const targetFilepath = 'my-file.txt'; + let statSyncInvocationCount = 0; + let rmSyncInvocationCount = 0; + let rmdirSyncInvocationCount = 0; + jest.mock('fs', () => ({ + statSync: filepath => { + statSyncInvocationCount += 1; + expect(filepath).toBe(targetFilepath); + return { + isFile: () => { + return true; + }, + size: 0, + }; + }, + rmSync: filepath => { + rmSyncInvocationCount += 1; + expect(filepath).toBe(targetFilepath); + }, + rmdirSync: filepath => { + rmdirSyncInvocationCount += 1; + }, + })); + + underTest._cleanupEmptyFilesAndFolders(targetFilepath); + expect(statSyncInvocationCount).toBe(1); + expect(rmSyncInvocationCount).toBe(1); + expect(rmdirSyncInvocationCount).toBe(0); + }); + + it('when path is not an empty file, does nothing', () => { + const targetFilepath = 'my-file.txt'; + const size = 128; + + let statSyncInvocationCount = 0; + let rmSyncInvocationCount = 0; + let rmdirSyncInvocationCount = 0; + + jest.mock('fs', () => ({ + statSync: filepath => { + statSyncInvocationCount += 1; + expect(filepath).toBe(targetFilepath); + return { + isFile: () => { + return true; + }, + size: size, + }; + }, + rmSync: filepath => { + rmSyncInvocationCount += 1; + }, + rmdirSync: filepath => { + rmdirSyncInvocationCount += 1; + }, + })); + + underTest._cleanupEmptyFilesAndFolders(targetFilepath); + expect(statSyncInvocationCount).toBe(1); + expect(rmSyncInvocationCount).toBe(0); + expect(rmdirSyncInvocationCount).toBe(0); + }); + + it("when path is folder and it's empty, removes it", () => { + const targetFolder = 'build/'; + const content = []; + + let statSyncInvocationCount = 0; + let readdirInvocationCount = 0; + let rmSyncInvocationCount = 0; + let rmdirSyncInvocationCount = 0; + + jest.mock('fs', () => ({ + statSync: filepath => { + statSyncInvocationCount += 1; + expect(filepath).toBe(targetFolder); + return { + isFile: () => { + return false; + }, + }; + }, + rmSync: filepath => { + rmSyncInvocationCount += 1; + }, + rmdirSync: filepath => { + rmdirSyncInvocationCount += 1; + expect(filepath).toBe(targetFolder); + }, + readdirSync: filepath => { + readdirInvocationCount += 1; + return content; + }, + })); + + underTest._cleanupEmptyFilesAndFolders(targetFolder); + expect(statSyncInvocationCount).toBe(1); + expect(readdirInvocationCount).toBe(2); + expect(rmSyncInvocationCount).toBe(0); + expect(rmdirSyncInvocationCount).toBe(1); + }); + + it("when path is folder and it's not empty, removes only empty folders and files", () => { + const targetFolder = 'build/'; + const content = ['emptyFolder', 'emptyFile', 'notEmptyFile']; + + const files = ['build/emptyFile', 'build/notEmptyFile']; + + const emptyContent = []; + const fileSizes = { + 'build/emptyFile': 0, + 'build/notEmptyFile': 32, + }; + + let statSyncInvocation = []; + let rmSyncInvocation = []; + let rmdirSyncInvocation = []; + let readdirInvocation = []; + + jest.mock('fs', () => ({ + statSync: filepath => { + statSyncInvocation.push(filepath); + + return { + isFile: () => { + return files.includes(filepath); + }, + size: fileSizes[filepath], + }; + }, + rmSync: filepath => { + rmSyncInvocation.push(filepath); + }, + rmdirSync: filepath => { + rmdirSyncInvocation.push(filepath); + }, + readdirSync: filepath => { + readdirInvocation.push(filepath); + return filepath === targetFolder ? content : emptyContent; + }, + })); + + underTest._cleanupEmptyFilesAndFolders(targetFolder); + expect(statSyncInvocation).toEqual([ + 'build/', + 'build/emptyFolder', + 'build/emptyFile', + 'build/notEmptyFile', + ]); + expect(readdirInvocation).toEqual([ + 'build/', + 'build/emptyFolder', + 'build/emptyFolder', + 'build/', + ]); + expect(rmSyncInvocation).toEqual(['build/emptyFile']); + expect(rmdirSyncInvocation).toEqual(['build/emptyFolder']); + }); + + it('when path is folder and it contains only empty folders, removes everything', () => { + const targetFolder = 'build/'; + const content = ['emptyFolder1', 'emptyFolder2']; + const emptyContent = []; + + let statSyncInvocation = []; + let rmSyncInvocation = []; + let rmdirSyncInvocation = []; + let readdirInvocation = []; + + jest.mock('fs', () => ({ + statSync: filepath => { + statSyncInvocation.push(filepath); + + return { + isFile: () => { + return false; + }, + }; + }, + rmSync: filepath => { + rmSyncInvocation.push(filepath); + }, + rmdirSync: filepath => { + rmdirSyncInvocation.push(filepath); + }, + readdirSync: filepath => { + readdirInvocation.push(filepath); + return filepath === targetFolder + ? content.filter( + element => + !rmdirSyncInvocation.includes(path.join(targetFolder, element)), + ) + : emptyContent; + }, + })); + + underTest._cleanupEmptyFilesAndFolders(targetFolder); + expect(statSyncInvocation).toEqual([ + 'build/', + 'build/emptyFolder1', + 'build/emptyFolder2', + ]); + expect(readdirInvocation).toEqual([ + 'build/', + 'build/emptyFolder1', + 'build/emptyFolder1', + 'build/emptyFolder2', + 'build/emptyFolder2', + 'build/', + ]); + expect(rmSyncInvocation).toEqual([]); + expect(rmdirSyncInvocation).toEqual([ + 'build/emptyFolder1', + 'build/emptyFolder2', + 'build/', + ]); + }); +}); diff --git a/scripts/codegen/generate-artifacts-executor.js b/scripts/codegen/generate-artifacts-executor.js index 784f44a2631afb..abae278a91110b 100644 --- a/scripts/codegen/generate-artifacts-executor.js +++ b/scripts/codegen/generate-artifacts-executor.js @@ -355,6 +355,39 @@ function createComponentProvider( } } +// It removes all the empty files and empty folders +// it finds, starting from `filepath`, recursively. +// +// This function is needed since, after aligning the codegen between +// iOS and Android, we have to create empty folders in advance and +// we don't know wheter they will be populated up until the end of the process. +// +// @parameter filepath: the root path from which we want to remove the empty files and folders. +function cleanupEmptyFilesAndFolders(filepath) { + const stats = fs.statSync(filepath); + + if (stats.isFile() && stats.size === 0) { + fs.rmSync(filepath); + return; + } else if (stats.isFile()) { + return; + } + + const dirContent = fs.readdirSync(filepath); + dirContent.forEach(contentPath => + cleanupEmptyFilesAndFolders(path.join(filepath, contentPath)), + ); + + // The original folder may be filled with empty folders + // if that the case, we would also like to remove the parent. + // Hence, we need to read the folder again. + const newContent = fs.readdirSync(filepath); + if (newContent.length === 0) { + fs.rmdirSync(filepath); + return; + } +} + // Execute /** @@ -428,6 +461,7 @@ function execute( ); createComponentProvider(fabricEnabled, schemaPaths, node, iosOutputDir); + cleanupEmptyFilesAndFolders(iosOutputDir); } catch (err) { console.error(err); process.exitCode = 1; @@ -443,4 +477,5 @@ module.exports = { _extractLibrariesFromJSON: extractLibrariesFromJSON, _executeNodeScript: executeNodeScript, _generateCode: generateCode, + _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders, };