From 17aa4f42d2668bf07ede11ed31dd3695046f99b2 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 24 Jul 2018 01:51:24 +0300 Subject: [PATCH] fix: Project's Podfile is regenerated incorrectly In case multiple plugins have Podfile with post_install hook, CLI modifies the hook of each of them by renaming the hook's function and calling it in the single post_install that is allowed for Podfile (placed at the bottom of the project's Podfile). However, when some change in node_modules is detected, CLI processes all Podfiles again and tries to apply them to project's Podfile. The "apply" is check if the plugin's Podfile is already added to project's one and in case not - adding it again. Whenever all Podfiles are processed, the "afterPrepareAllPlugins" method checks the final project's Podfile and replaces the post_install hooks from all plugins with a new functions (by adding index to the name) and calls them inside a newly added post_install function. This is not working as when CLI checks if a plugin's Podfile is already added to the project's one, it uses the original content of the plugin's Podfile, where the post_install hook is called `post_install`, while in the project's one it is renamed. So CLI always thinks that plugin's Podfile, which has post_install hook is not added to the project. CLI tries to add it again and messes the whole structure. In order to fix the issue, apply the following logic: 1. When a plugin has a Podfile, during prepare of the plugin read the content of this file. After that apply modifications over the file by replacing the post_install with a well known name - `post_install_`. 2. Check if the replaced content is part of the project's Podfile. In case yes - do nothing. 3. In case the replaced content is not part of the project's Podfile, it means that either we have never added this Podfile or its content has been changed. In order to handle both cases first remove the plugin's Podfile from the project's one - this includes removing the whole content between `# Begin Podfile ` and `# End Podfile` for this Podfile to be removed from the project's one and all post_install hooks with name that would have been used for this plugin to be removed. After that add the replaced content in the project's Podfile and call all post_install replaced functions in the project's Podfile's post_install hook. 4. On `afterPrepareAllPlugins` do nothing with the Podfile, it is already processed. This solution ensures the Podfile is correct after processing each plugin. The previous one relyed on the `afterPrepareAllPlugins` to fix the project's Podfile. Also, by using well known pattern for generating the post_install hooks, we ensure we can remove them from the final Podfile whenever is required. --- lib/constants.ts | 7 + lib/definitions/project.d.ts | 32 +- lib/services/cocoapods-service.ts | 170 ++++++-- lib/services/ios-project-service.ts | 77 +--- test/cocoapods-service.ts | 583 ++++++++++++++++++++++++++-- test/ios-project-service.ts | 18 +- 6 files changed, 739 insertions(+), 148 deletions(-) diff --git a/lib/constants.ts b/lib/constants.ts index aa0e764f84..b0a9de8a4e 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -215,3 +215,10 @@ export class AddPlaformErrors { export const PLUGIN_BUILD_DATA_FILENAME = "plugin-data.json"; export const PLUGINS_BUILD_DATA_FILENAME = ".ns-plugins-build-data.json"; + +export class PluginNativeDirNames { + public static iOS = "ios"; + public static Android = "android"; +} + +export const PODFILE_NAME = "Podfile"; diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index add168c549..73c88f57ba 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -464,10 +464,32 @@ interface ICocoaPodsService { getPodfileFooter(): string; /** - * Merges the content of hooks with the provided name if there are more than one hooks with this name in the Podfile. - * @param {string} hookName The name of the hook. - * @param {string} pathToPodfile The path to the Podfile. - * @return {void} + * Prepares the Podfile content of a plugin and merges it in the project's Podfile. + * @param {IPluginData} pluginData Information about the plugin. + * @param {IProjectData} projectData Information about the project. + * @param {string} nativeProjectPath Path to the native Xcode project. + * @returns {Promise} + */ + applyPluginPodfileToProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): Promise; + + /** + * Removes plugins Podfile content from the project. + * @param {IPluginData} pluginData Information about the plugin. + * @param {IProjectData} projectData Information about the project. + * @param {string} nativeProjectPath Path to the native Xcode project. + * @returns {void} */ - mergePodfileHookContent(sectionName: string, pathToPodfile: string): void + removePluginPodfileFromProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): void; + + /** + * Gives the path to project's Podfile. + * @param {string} nativeProjectPath Path to the native Xcode project. + * @returns {string} Path to project's Podfile. + */ + getProjectPodfilePath(nativeProjectPath: string): string; +} + +interface IRubyFunction { + functionName: string; + functionParameters?: string; } diff --git a/lib/services/cocoapods-service.ts b/lib/services/cocoapods-service.ts index e9f8dfc117..b52dabaf6d 100644 --- a/lib/services/cocoapods-service.ts +++ b/lib/services/cocoapods-service.ts @@ -1,11 +1,11 @@ import { EOL } from "os"; - -interface IRubyFunction { - functionName: string; - functionParameters?: string; -} +import * as path from "path"; +import { PluginNativeDirNames, PODFILE_NAME } from "../constants"; export class CocoaPodsService implements ICocoaPodsService { + private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install"; + private static INSTALLER_BLOCK_PARAMETER_NAME = "installer"; + constructor(private $fs: IFileSystem) { } public getPodfileHeader(targetName: string): string { @@ -16,20 +16,133 @@ export class CocoaPodsService implements ICocoaPodsService { return `${EOL}end`; } - public mergePodfileHookContent(hookName: string, pathToPodfile: string): void { - if (!this.$fs.exists(pathToPodfile)) { - throw new Error(`The Podfile ${pathToPodfile} does not exist.`); + public getProjectPodfilePath(projectRoot: string): string { + return path.join(projectRoot, PODFILE_NAME); + } + + public async applyPluginPodfileToProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): Promise { + const pluginPodFilePath = this.getPluginPodfilePath(pluginData); + if (!this.$fs.exists(pluginPodFilePath)) { + return; + } + + const { pluginPodfileContent, replacedFunctions } = this.buildPodfileContent(pluginPodFilePath, pluginData.name); + const pathToProjectPodfile = this.getProjectPodfilePath(nativeProjectPath); + const projectPodfileContent = this.$fs.exists(pathToProjectPodfile) ? this.$fs.readText(pathToProjectPodfile).trim() : ""; + + if (projectPodfileContent.indexOf(pluginPodfileContent) === -1) { + // Remove old occurences of the plugin from the project's Podfile. + this.removePluginPodfileFromProject(pluginData, projectData, nativeProjectPath); + let finalPodfileContent = this.$fs.exists(pathToProjectPodfile) ? this.getPodfileContentWithoutTarget(projectData, this.$fs.readText(pathToProjectPodfile)) : ""; + + if (pluginPodfileContent.indexOf(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME) !== -1) { + finalPodfileContent = this.addPostInstallHook(replacedFunctions, finalPodfileContent, pluginPodfileContent); + } + + finalPodfileContent = `${pluginPodfileContent}${EOL}${finalPodfileContent}`; + this.saveProjectPodfile(projectData, finalPodfileContent, nativeProjectPath); + } + } + + public removePluginPodfileFromProject(pluginData: IPluginData, projectData: IProjectData, projectRoot: string): void { + const pluginPodfilePath = this.getPluginPodfilePath(pluginData); + + if (this.$fs.exists(pluginPodfilePath) && this.$fs.exists(this.getProjectPodfilePath(projectRoot))) { + let projectPodFileContent = this.$fs.readText(this.getProjectPodfilePath(projectRoot)); + // Remove the data between #Begin Podfile and #EndPodfile + const regExpToRemove = new RegExp(`${this.getPluginPodfileHeader(pluginPodfilePath)}[\\s\\S]*?${this.getPluginPodfileEnd()}`, "mg"); + projectPodFileContent = projectPodFileContent.replace(regExpToRemove, ""); + projectPodFileContent = this.removePostInstallHook(pluginData, projectPodFileContent); + + const defaultPodfileBeginning = this.getPodfileHeader(projectData.projectName); + const defaultContentWithPostInstallHook = `${defaultPodfileBeginning}${EOL}${this.getPostInstallHookHeader()}end${EOL}end`; + const defaultContentWithoutPostInstallHook = `${defaultPodfileBeginning}end`; + const trimmedProjectPodFileContent = projectPodFileContent.trim(); + if (!trimmedProjectPodFileContent || trimmedProjectPodFileContent === defaultContentWithPostInstallHook || trimmedProjectPodFileContent === defaultContentWithoutPostInstallHook) { + this.$fs.deleteFile(this.getProjectPodfilePath(projectRoot)); + } else { + this.$fs.writeFile(this.getProjectPodfilePath(projectRoot), projectPodFileContent); + } } + } + + private getPluginPodfilePath(pluginData: IPluginData): string { + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(PluginNativeDirNames.iOS); + const pluginPodFilePath = path.join(pluginPlatformsFolderPath, PODFILE_NAME); + return pluginPodFilePath; + } - const podfileContent = this.$fs.readText(pathToPodfile); + private addPostInstallHook(replacedFunctions: IRubyFunction[], finalPodfileContent: string, pluginPodfileContent: string): string { + const postInstallHookStart = this.getPostInstallHookHeader(); + let postInstallHookContent = ""; + _.each(replacedFunctions, rubyFunction => { + let functionExecution = rubyFunction.functionName; + if (rubyFunction.functionParameters && rubyFunction.functionParameters.length) { + functionExecution = `${functionExecution} ${CocoaPodsService.INSTALLER_BLOCK_PARAMETER_NAME}`; + } + + postInstallHookContent += ` ${functionExecution}${EOL}`; + }); + + if (postInstallHookContent) { + const index = finalPodfileContent.indexOf(postInstallHookStart); + if (index !== -1) { + finalPodfileContent = finalPodfileContent.replace(postInstallHookStart, `${postInstallHookStart}${postInstallHookContent}`); + } else { + const postInstallHook = `${postInstallHookStart}${postInstallHookContent}end`; + finalPodfileContent = `${finalPodfileContent}${postInstallHook}`; + } + } + + return finalPodfileContent; + } + + private getPodfileContentWithoutTarget(projectData: IProjectData, projectPodfileContent: string): string { + const podFileHeader = this.getPodfileHeader(projectData.projectName); + + if (_.startsWith(projectPodfileContent, podFileHeader)) { + projectPodfileContent = projectPodfileContent.substr(podFileHeader.length); + + const podFileFooter = this.getPodfileFooter(); + // Only remove the final end in case the file starts with the podFileHeader + if (_.endsWith(projectPodfileContent, podFileFooter)) { + projectPodfileContent = projectPodfileContent.substr(0, projectPodfileContent.length - podFileFooter.length); + } + } + + return projectPodfileContent.trim(); + } + + private saveProjectPodfile(projectData: IProjectData, projectPodfileContent: string, projectRoot: string): void { + projectPodfileContent = this.getPodfileContentWithoutTarget(projectData, projectPodfileContent); + const podFileHeader = this.getPodfileHeader(projectData.projectName); + const podFileFooter = this.getPodfileFooter(); + const contentToWrite = `${podFileHeader}${projectPodfileContent}${podFileFooter}`; + const projectPodfilePath = this.getProjectPodfilePath(projectRoot); + this.$fs.writeFile(projectPodfilePath, contentToWrite); + } + + private removePostInstallHook(pluginData: IPluginData, projectPodFileContent: string): string { + const regExp = new RegExp(`^.*?${this.getHookBasicFuncNameForPlugin(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME, pluginData.name)}.*?$\\r?\\n`, "gm"); + projectPodFileContent = projectPodFileContent.replace(regExp, ""); + return projectPodFileContent; + } + + private getHookBasicFuncNameForPlugin(hookName: string, pluginName: string): string { + // nativescript-hook and nativescript_hook should have different names, so replace all _ with ___ first and then replace all special symbols with _ + // This will lead to a clash in case plugins are called nativescript-hook and nativescript___hook + const replacedPluginName = pluginName.replace(/_/g, "___").replace(/[^A-Za-z0-9_]/g, "_"); + return `${hookName}${replacedPluginName}`; + } + + private replaceHookContent(hookName: string, podfileContent: string, pluginName: string): { replacedContent: string, newFunctions: IRubyFunction[] } { const hookStart = `${hookName} do`; const hookDefinitionRegExp = new RegExp(`${hookStart} *(\\|(\\w+)\\|)?`, "g"); - let newFunctionNameIndex = 1; const newFunctions: IRubyFunction[] = []; const replacedContent = podfileContent.replace(hookDefinitionRegExp, (substring: string, firstGroup: string, secondGroup: string, index: number): string => { - const newFunctionName = `${hookName}${newFunctionNameIndex++}`; + const newFunctionName = `${this.getHookBasicFuncNameForPlugin(hookName, pluginName)}_${newFunctions.length}`; let newDefinition = `def ${newFunctionName}`; const rubyFunction: IRubyFunction = { functionName: newFunctionName }; @@ -43,26 +156,31 @@ export class CocoaPodsService implements ICocoaPodsService { return newDefinition; }); - if (newFunctions.length > 1) { - // Execute all methods in the hook and pass the parameter to them. - const blokParameterName = "installer"; - let mergedHookContent = `${hookStart} |${blokParameterName}|${EOL}`; + return { replacedContent, newFunctions }; + } - _.each(newFunctions, (rubyFunction: IRubyFunction) => { - let functionExecution = rubyFunction.functionName; - if (rubyFunction.functionParameters && rubyFunction.functionParameters.length) { - functionExecution = `${functionExecution} ${blokParameterName}`; - } + private getPluginPodfileHeader(pluginPodFilePath: string): string { + return `# Begin Podfile - ${pluginPodFilePath}`; + } - mergedHookContent = `${mergedHookContent} ${functionExecution}${EOL}`; - }); + private getPluginPodfileEnd(): string { + return `# End Podfile${EOL}`; + } - mergedHookContent = `${mergedHookContent}end`; + private getPostInstallHookHeader() { + return `${CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME} do |${CocoaPodsService.INSTALLER_BLOCK_PARAMETER_NAME}|${EOL}`; + } - const newPodfileContent = `${replacedContent}${EOL}${mergedHookContent}`; - this.$fs.writeFile(pathToPodfile, newPodfileContent); - } + private buildPodfileContent(pluginPodFilePath: string, pluginName: string): { pluginPodfileContent: string, replacedFunctions: IRubyFunction[] } { + const pluginPodfileContent = this.$fs.readText(pluginPodFilePath); + const { replacedContent, newFunctions: replacedFunctions } = this.replaceHookContent(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME, pluginPodfileContent, pluginName); + + return { + pluginPodfileContent: `${this.getPluginPodfileHeader(pluginPodFilePath)}${EOL}${replacedContent}${EOL}${this.getPluginPodfileEnd()}`, + replacedFunctions + }; } + } $injector.register("cocoapodsService", CocoaPodsService); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 1e11cf10d2..5fcd6963bf 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as shell from "shelljs"; -import * as os from "os"; import * as semver from "semver"; import * as constants from "../constants"; import * as helpers from "../common/helpers"; @@ -28,11 +27,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private static XCODEBUILD_MIN_VERSION = "6.0"; private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; private static IOS_PLATFORM_NAME = "ios"; - private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install"; - - private get $npmInstallationManager(): INpmInstallationManager { - return this.$injector.resolve("npmInstallationManager"); - } constructor($fs: IFileSystem, private $childProcess: IChildProcess, @@ -900,10 +894,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName + IOSProjectService.XCODE_PROJECT_EXT_NAME); } - private getProjectPodFilePath(projectData: IProjectData): string { - return path.join(this.getPlatformData(projectData).projectRoot, "Podfile"); - } - private getPluginsDebugXcconfigFilePath(projectData: IProjectData): string { return path.join(this.getPlatformData(projectData).projectRoot, "plugins-debug.xcconfig"); } @@ -951,7 +941,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f await this.prepareResources(pluginPlatformsFolderPath, pluginData, projectData); await this.prepareFrameworks(pluginPlatformsFolderPath, pluginData, projectData); await this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); - await this.prepareCocoapods(pluginPlatformsFolderPath, projectData); + await this.prepareCocoapods(pluginPlatformsFolderPath, pluginData, projectData); } public async removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { @@ -960,20 +950,14 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.removeNativeSourceCode(pluginPlatformsFolderPath, pluginData, projectData); this.removeFrameworks(pluginPlatformsFolderPath, pluginData, projectData); this.removeStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); - this.removeCocoapods(pluginPlatformsFolderPath, projectData); + const projectRoot = this.getPlatformData(projectData).projectRoot; + + this.$cocoapodsService.removePluginPodfileFromProject(pluginData, projectData, projectRoot); } public async afterPrepareAllPlugins(projectData: IProjectData): Promise { - if (this.$fs.exists(this.getProjectPodFilePath(projectData))) { - const projectPodfileContent = this.$fs.readText(this.getProjectPodFilePath(projectData)); - this.$logger.trace("Project Podfile content"); - this.$logger.trace(projectPodfileContent); - - const firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME); - if (firstPostInstallIndex !== -1 && firstPostInstallIndex !== projectPodfileContent.lastIndexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME)) { - this.$cocoapodsService.mergePodfileHookContent(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME, this.getProjectPodFilePath(projectData)); - } - + const projectRoot = this.getPlatformData(projectData).projectRoot; + if (this.$fs.exists(this.$cocoapodsService.getProjectPodfilePath(projectRoot))) { const xcuserDataPath = path.join(this.getXcodeprojPath(projectData), "xcuserdata"); const sharedDataPath = path.join(this.getXcodeprojPath(projectData), "xcshareddata"); @@ -1169,38 +1153,15 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } } - private async prepareCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData, opts?: any): Promise { + private async prepareCocoapods(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData, opts?: any): Promise { + const projectRoot = this.getPlatformData(projectData).projectRoot; + await this.$cocoapodsService.applyPluginPodfileToProject(pluginData, projectData, projectRoot); const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); - if (this.$fs.exists(pluginPodFilePath)) { - const pluginPodFileContent = this.$fs.readText(pluginPodFilePath); - const pluginPodFilePreparedContent = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); - let projectPodFileContent = this.$fs.exists(this.getProjectPodFilePath(projectData)) ? this.$fs.readText(this.getProjectPodFilePath(projectData)) : ""; - - if (!~projectPodFileContent.indexOf(pluginPodFilePreparedContent)) { - const podFileHeader = this.$cocoapodsService.getPodfileHeader(projectData.projectName), - podFileFooter = this.$cocoapodsService.getPodfileFooter(); - - if (_.startsWith(projectPodFileContent, podFileHeader)) { - projectPodFileContent = projectPodFileContent.substr(podFileHeader.length); - } - - if (_.endsWith(projectPodFileContent, podFileFooter)) { - projectPodFileContent = projectPodFileContent.substr(0, projectPodFileContent.length - podFileFooter.length); - } - - const contentToWrite = `${podFileHeader}${projectPodFileContent}${pluginPodFilePreparedContent}${podFileFooter}`; - this.$fs.writeFile(this.getProjectPodFilePath(projectData), contentToWrite); - - const project = this.createPbxProj(projectData); - this.savePbxProj(project, projectData); - } - } if (opts && opts.executePodInstall && this.$fs.exists(pluginPodFilePath)) { await this.executePodInstall(projectData); } } - private removeNativeSourceCode(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): void { const project = this.createPbxProj(projectData); const group = this.getRootGroup(pluginData.name, pluginPlatformsFolderPath); @@ -1235,26 +1196,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.savePbxProj(project, projectData); } - private removeCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData): void { - const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); - - if (this.$fs.exists(pluginPodFilePath) && this.$fs.exists(this.getProjectPodFilePath(projectData))) { - const pluginPodFileContent = this.$fs.readText(pluginPodFilePath); - let projectPodFileContent = this.$fs.readText(this.getProjectPodFilePath(projectData)); - const contentToRemove = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); - projectPodFileContent = helpers.stringReplaceAll(projectPodFileContent, contentToRemove, ""); - if (projectPodFileContent.trim() === `use_frameworks!${os.EOL}${os.EOL}target "${projectData.projectName}" do${os.EOL}${os.EOL}end`) { - this.$fs.deleteFile(this.getProjectPodFilePath(projectData)); - } else { - this.$fs.writeFile(this.getProjectPodFilePath(projectData), projectPodFileContent); - } - } - } - - private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string { - return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`; - } - private generateModulemap(headersFolderPath: string, libraryName: string): void { const headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile()); const headersFolderContents = this.$fs.readDirectory(headersFolderPath); diff --git a/test/cocoapods-service.ts b/test/cocoapods-service.ts index f63770aeba..49602612d0 100644 --- a/test/cocoapods-service.ts +++ b/test/cocoapods-service.ts @@ -7,6 +7,8 @@ interface IMergePodfileHooksTestCase { input: string; output: string; testCaseDescription: string; + projectPodfileContent?: string; + pluginData?: IPluginData; } function createTestInjector(): IInjector { @@ -26,23 +28,114 @@ function changeNewLineCharacter(input: string): string { } describe("Cocoapods service", () => { - describe("merge Podfile hooks", () => { - let testInjector: IInjector; - let cocoapodsService: ICocoaPodsService; - let newPodfileContent: string; - - const mockFileSystem = (injector: IInjector, podfileContent: string): void => { - const fs: IFileSystem = injector.resolve("fs"); - - fs.exists = () => true; - fs.readText = () => podfileContent; - fs.writeFile = (pathToFile: string, content: any) => { - newPodfileContent = content; - }; + const nativeProjectPath = "nativeProjectPath"; + const mockPluginData: any = { + name: "plugin1", + pluginPlatformsFolderPath: () => "pluginPlatformsFolderPath" + }; + const mockProjectData: any = { + projectDir: "projectDir", + projectName: "projectName" + }; + + let testInjector: IInjector; + let cocoapodsService: ICocoaPodsService; + let newPodfileContent = ""; + + const mockFileSystem = (injector: IInjector, podfileContent: string, projectPodfileContent?: string): void => { + const fs: IFileSystem = injector.resolve("fs"); + + fs.exists = () => true; + fs.readText = (file: string) => { + if (file.indexOf("pluginPlatformsFolderPath") !== -1) { + return podfileContent; + } + + return newPodfileContent || projectPodfileContent || ""; }; - const testCaces: IMergePodfileHooksTestCase[] = [ + fs.writeFile = (pathToFile: string, content: any) => { + newPodfileContent = content; + }; + + fs.deleteFile = (path: string): void => { + newPodfileContent = null; + projectPodfileContent = null; + }; + }; + + beforeEach(() => { + testInjector = createTestInjector(); + cocoapodsService = testInjector.resolve("cocoapodsService"); + newPodfileContent = ""; + }); + + describe("merges Podfile files correctly", () => { + const testCases: IMergePodfileHooksTestCase[] = [ + { + testCaseDescription: "adds plugin's Podfile to project's one", + input: ` +pod 'GoogleAnalytics', '~> 3.1' +`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +pod 'GoogleAnalytics', '~> 3.1' + +# End Podfile +end`, + projectPodfileContent: "" + }, + { + testCaseDescription: "replaces correctly special chars from plugin's name", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_with_special_symbols_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| + post_installplugin1_with_special_symbols_0 installer +end +end`, + projectPodfileContent: "", + pluginData: { + name: "plugin1-with-special-symbols", + pluginPlatformsFolderPath: () => "pluginPlatformsFolderPath" + } + }, { + testCaseDescription: "replaces correctly special chars from plugin's name when plugin has _", input: ` target 'MyApp' do pod 'GoogleAnalytics', '~> 3.1' @@ -56,18 +149,97 @@ post_install do |installer| installer.pods_project.targets.each do |target| puts target.name end +end`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_with_special_symbols___and___underscore_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| + post_installplugin1_with_special_symbols___and___underscore_0 installer end +end`, + projectPodfileContent: "", + pluginData: { + name: "plugin1-with-special-symbols_and_underscore", + pluginPlatformsFolderPath: () => "pluginPlatformsFolderPath" + } + }, + { + testCaseDescription: "treats plugin1_plugin and plugin1___plugin as the same plugin (by design as we do not expect plugins to have names with three underscores)", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' +end + post_install do |installer| installer.pods_project.targets.each do |target| puts target.name end +end`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' +end + +def post_installplugin1___plugin_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end end +# End Podfile + post_install do |installer| + post_installplugin1___plugin_0 installer +end +end`, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'OCMock', '~> 2.0.1' +end + +def post_installplugin1___plugin_0 (installer) installer.pods_project.targets.each do |target| puts target.name end +end +# End Podfile + +post_install do |installer| + post_installplugin1___plugin_0 installer +end end`, - output: ` + pluginData: { + name: "plugin1_plugin", + pluginPlatformsFolderPath: () => "pluginPlatformsFolderPath" + } + }, + { + testCaseDescription: "replaces the plugin's old Podfile with the new one inside project's Podfile", + input: ` target 'MyApp' do pod 'GoogleAnalytics', '~> 3.1' target 'MyAppTests' do @@ -76,28 +248,125 @@ target 'MyApp' do end end -def post_install1 (installer) +post_install do |installer| installer.pods_project.targets.each do |target| puts target.name end +end`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end end -def post_install2 (installer) + +def post_installplugin1_0 (installer) installer.pods_project.targets.each do |target| puts target.name end end -def post_install3 (installer) +# End Podfile + +post_install do |installer| + post_installplugin1_0 installer +end +end`, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 2.1' # version changed here + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) installer.pods_project.targets.each do |target| puts target.name end end +# End Podfile + post_install do |installer| - post_install1 installer - post_install2 installer - post_install3 installer + post_installplugin1_0 installer +end end`, - testCaseDescription: "should merge more than one hooks with block parameter correctly." - }, { + }, + { + testCaseDescription: "merges more than one hooks with block parameter correctly.", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end`, + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +def post_installplugin1_1 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +def post_installplugin1_2 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| + post_installplugin1_0 installer + post_installplugin1_1 installer + post_installplugin1_2 installer +end +end`, + }, + { + testCaseDescription: "merges more than one hooks with and without block parameter correctly", input: ` target 'MyApp' do pod 'GoogleAnalytics', '~> 3.1' @@ -115,7 +384,11 @@ target 'MyApp' do puts "Hello World!" end end`, - output: ` + output: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + target 'MyApp' do pod 'GoogleAnalytics', '~> 3.1' target 'MyAppTests' do @@ -123,21 +396,25 @@ target 'MyApp' do pod 'OCMock', '~> 2.0.1' end - def post_install1 (installer_representation) + def post_installplugin1_0 (installer_representation) installer_representation.pods_project.targets.each do |target| puts target.name end end - def post_install2 + def post_installplugin1_1 puts "Hello World!" end end +# End Podfile + post_install do |installer| - post_install1 installer - post_install2 + post_installplugin1_0 installer + post_installplugin1_1 +end end`, - testCaseDescription: "should merge more than one hooks with and without block parameter correctly." - }, { + }, + { + testCaseDescription: "should not change the Podfile when the plugin content is already part of the project", input: ` target 'MyApp' do pod 'GoogleAnalytics', '~> 3.1' @@ -152,22 +429,248 @@ post_install do |installer| puts target.name end end`, - output: null, - testCaseDescription: "should not change the Podfile when there is only one hook." + output: "", + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| + post_installplugin1_0 installer +end +end`, } ]; - beforeEach(() => { - testInjector = createTestInjector(); - cocoapodsService = testInjector.resolve("cocoapodsService"); - newPodfileContent = null; + _.each(testCases, (testCase: IMergePodfileHooksTestCase) => { + it(testCase.testCaseDescription, async () => { + mockFileSystem(testInjector, testCase.input, testCase.projectPodfileContent); + + await cocoapodsService.applyPluginPodfileToProject(testCase.pluginData || mockPluginData, mockProjectData, nativeProjectPath); + + assert.deepEqual(changeNewLineCharacter(newPodfileContent), changeNewLineCharacter(testCase.output)); + }); }); + }); + + describe("removes plugin's Podfile correctly", () => { + const testCases: IMergePodfileHooksTestCase[] = [ + { + testCaseDescription: "removes plugin's Podfile from project's one and deletes project's Podfile as nothing is left there", + input: ` +pod 'GoogleAnalytics', '~> 3.1' +`, + output: null, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +pod 'GoogleAnalytics', '~> 3.1' + +# End Podfile +end` + }, + { + testCaseDescription: "removes plugin's Podfile (with hook) from project's one and deletes project's Podfile as nothing is left there", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end`, + output: null, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| +post_installplugin1_0 installer +end +end` + }, + { + testCaseDescription: "removes Podfile which has several postinstall hooks and deletes project's Podfile as nothing is left there", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end`, + output: null, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +def post_installplugin1_1 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +def post_installplugin1_2 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| + post_installplugin1_0 installer + post_installplugin1_1 installer + post_installplugin1_2 installer +end +end` + }, + { + testCaseDescription: "removes plugin's Podfile (with hook) from project's one when there are other plugins with hooks in the project Podfile", + input: ` +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + puts target.name + end +end`, + output: `use_frameworks! + +target "projectName" do + +# Begin Podfile - pluginPlatformsFolderPath1/Podfile + +pod 'Firebase', '~> 3.1' + +def post_installplugin2_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| +post_installplugin2_0 installer +end +end`, + projectPodfileContent: `use_frameworks! + +target "projectName" do +# Begin Podfile - pluginPlatformsFolderPath/Podfile + +target 'MyApp' do + pod 'GoogleAnalytics', '~> 3.1' + target 'MyAppTests' do + inherit! :search_paths + pod 'OCMock', '~> 2.0.1' + end +end + +def post_installplugin1_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +# Begin Podfile - pluginPlatformsFolderPath1/Podfile + +pod 'Firebase', '~> 3.1' + +def post_installplugin2_0 (installer) + installer.pods_project.targets.each do |target| + puts target.name + end +end +# End Podfile + +post_install do |installer| +post_installplugin1_0 installer +post_installplugin2_0 installer +end +end` + } + ]; - _.each(testCaces, (testCase: IMergePodfileHooksTestCase) => { - it(testCase.testCaseDescription, () => { - mockFileSystem(testInjector, testCase.input); + _.each(testCases, (testCase: IMergePodfileHooksTestCase) => { + it(testCase.testCaseDescription, async () => { + mockFileSystem(testInjector, testCase.input, testCase.projectPodfileContent); - cocoapodsService.mergePodfileHookContent("post_install", ""); + cocoapodsService.removePluginPodfileFromProject(mockPluginData, mockProjectData, nativeProjectPath); assert.deepEqual(changeNewLineCharacter(newPodfileContent), changeNewLineCharacter(testCase.output)); }); diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index e65ffd51bb..7b6fa20e5c 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -27,7 +27,6 @@ import { PluginsService } from "../lib/services/plugins-service"; import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper"; import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; -import { NpmInstallationManager } from "../lib/npm-installation-manager"; import { NodePackageManager } from "../lib/node-package-manager"; import { assert } from "chai"; @@ -114,7 +113,6 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX pbxGroupByName() { /* */ } } }); - testInjector.register("npmInstallationManager", NpmInstallationManager); testInjector.register("npm", NodePackageManager); testInjector.register("xCConfigService", XCConfigService); testInjector.register("settingsService", SettingsService); @@ -384,9 +382,9 @@ describe("Cocoapods support", () => { const actualProjectPodfileContent = fs.readText(projectPodfilePath); const expectedProjectPodfileContent = ["use_frameworks!\n", `target "${projectName}" do`, - `# Begin Podfile - ${pluginPodfilePath} `, - ` ${pluginPodfileContent} `, - " # End Podfile \n", + `# Begin Podfile - ${pluginPodfilePath}`, + `${pluginPodfileContent}`, + "# End Podfile", "end"] .join("\n"); assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent); @@ -445,7 +443,9 @@ describe("Cocoapods support", () => { const pluginData = { pluginPlatformsFolderPath(platform: string): string { return pluginPlatformsFolderPath; - } + }, + name: "pluginName", + fullPath: "fullPath" }; const projectData: IProjectData = testInjector.resolve("projectData"); @@ -457,9 +457,9 @@ describe("Cocoapods support", () => { const actualProjectPodfileContent = fs.readText(projectPodfilePath); const expectedProjectPodfileContent = ["use_frameworks!\n", `target "${projectName}" do`, - `# Begin Podfile - ${pluginPodfilePath} `, - ` ${pluginPodfileContent} `, - " # End Podfile \n", + `# Begin Podfile - ${pluginPodfilePath}`, + `${pluginPodfileContent}`, + "# End Podfile", "end"] .join("\n"); assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent);