diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 5484694547..2e48a3a310 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -60,6 +60,7 @@ $injector.require("broccoliBuilder", "./tools/broccoli/builder"); $injector.require("nodeModulesTree", "./tools/broccoli/trees/node-modules-tree"); $injector.require("broccoliPluginWrapper", "./tools/broccoli/broccoli-plugin-wrapper"); +$injector.require("pluginVariablesService", "./services/plugin-variables-service"); $injector.require("pluginsService", "./services/plugins-service"); $injector.requireCommand("plugin|add", "./commands/plugin/add-plugin"); $injector.requireCommand("plugin|remove", "./commands/plugin/remove-plugin"); diff --git a/lib/common b/lib/common index d6e390bb76..573ccd90d4 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit d6e390bb76bf8fe59cdc87ce619dfbafde623349 +Subproject commit 573ccd90d4d210ca81278d143022eb547bb7bade diff --git a/lib/constants.ts b/lib/constants.ts index da96453d31..cdda1c6d6a 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -6,6 +6,7 @@ export let APP_RESOURCES_FOLDER_NAME = "App_Resources"; export let PROJECT_FRAMEWORK_FOLDER_NAME = "framework"; export let NATIVESCRIPT_KEY_NAME = "nativescript"; export let NODE_MODULES_FOLDER_NAME = "node_modules"; +export let TNS_MODULES_FOLDER_NAME = "tns_modules"; export let TNS_CORE_MODULES_NAME = "tns-core-modules"; export let PACKAGE_JSON_FILE_NAME = "package.json"; export let NODE_MODULE_CACHE_PATH_KEY_NAME = "node-modules-cache-path"; diff --git a/lib/definitions/plugins.d.ts b/lib/definitions/plugins.d.ts index 876e5a0d07..9e21e98821 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -9,7 +9,9 @@ interface IPluginsService { interface IPluginData extends INodeModuleData { platformsData: IPluginPlatformsData; - pluginPlatformsFolderPath(platform: string): string; + /* Gets all plugin variables from plugin */ + pluginVariables: IDictionary; + pluginPlatformsFolderPath(platform: string): string; } interface INodeModuleData { @@ -23,4 +25,38 @@ interface INodeModuleData { interface IPluginPlatformsData { ios: string; android: string; +} + +interface IPluginVariablesService { + /** + * Saves plugin variables in project package.json file. + * @param {IPluginData} pluginData for the plugin. + * @return {IFuture} + */ + savePluginVariablesInProjectFile(pluginData: IPluginData): IFuture; + /** + * Removes plugin variables from project package.json file. + * @param {IPluginData} pluginData for the plugin. + * @return {IFuture} + */ + removePluginVariablesFromProjectFile(pluginData: IPluginData): IFuture; + /** + * Replaces all plugin variables with their corresponding values. + * @param {IPluginData} pluginData for the plugin. + * @param {pluginConfigurationFileContent} pluginConfigurationFileContent for the plugin. + * @return {IFuture} returns the changed plugin configuration file content. + */ + interpolatePluginVariables(pluginData: IPluginData, pluginConfigurationFileContent: string): IFuture; + /** + * Returns the + * @param {IPluginData} pluginData for the plugin. + * @return {IFuture} returns the changed plugin configuration file content. + */ + getPluginVariablePropertyName(pluginData: IPluginData): string; +} + +interface IPluginVariableData { + defaultValue?: string; + name?: string; + value?: string; } \ No newline at end of file diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 29108c0432..843b8c1094 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -16,6 +16,7 @@ interface IProjectDataService { getValue(propertyName: string): IFuture; setValue(key: string, value: any): IFuture; removeProperty(propertyName: string): IFuture; + removeDependency(dependencyName: string): IFuture; } interface IProjectTemplatesService { diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 258801bc53..e6e426db83 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -186,7 +186,7 @@ export class PlatformService implements IPlatformService { try { this.$pluginsService.ensureAllDependenciesAreInstalled().wait(); let tnsModulesDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, PlatformService.TNS_MODULES_FOLDER_NAME); - this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, this.$projectData.projectDir, platform, lastModifiedTime).wait(); + this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime).wait(); } catch(error) { this.$logger.debug(error); shell.rm("-rf", appResourcesDirectoryPath); diff --git a/lib/services/plugin-variables-service.ts b/lib/services/plugin-variables-service.ts new file mode 100644 index 0000000000..94895f46e8 --- /dev/null +++ b/lib/services/plugin-variables-service.ts @@ -0,0 +1,103 @@ +/// +"use strict"; + +import * as helpers from "./../common/helpers"; + +export class PluginVariablesService implements IPluginVariablesService { + private static PLUGIN_VARIABLES_KEY = "variables"; + + constructor(private $errors: IErrors, + private $pluginVariablesHelper: IPluginVariablesHelper, + private $projectData: IProjectData, + private $projectDataService: IProjectDataService, + private $prompter: IPrompter) { } + + public getPluginVariablePropertyName(pluginData: IPluginData): string { + return `${pluginData.name}-${PluginVariablesService.PLUGIN_VARIABLES_KEY}`; + } + + public savePluginVariablesInProjectFile(pluginData: IPluginData): IFuture { + return (() => { + let values = Object.create(null); + this.executeForAllPluginVariables(pluginData, (pluginVariableData: IPluginVariableData) => + (() => { + let pluginVariableValue = this.getPluginVariableValue(pluginVariableData).wait(); + this.ensurePluginVariableValue(pluginVariableValue, `Unable to find value for ${pluginVariableData.name} plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`); + values[pluginVariableData.name] = pluginVariableValue; + }).future()()).wait(); + + this.$projectDataService.initialize(this.$projectData.projectDir); + this.$projectDataService.setValue(this.getPluginVariablePropertyName(pluginData), values).wait(); + }).future()(); + } + + public removePluginVariablesFromProjectFile(pluginData: IPluginData): IFuture { + this.$projectDataService.initialize(this.$projectData.projectDir); + return this.$projectDataService.removeProperty(this.getPluginVariablePropertyName(pluginData)); + } + + public interpolatePluginVariables(pluginData: IPluginData, pluginConfigurationFileContent: string): IFuture { + return (() => { + this.executeForAllPluginVariables(pluginData, (pluginVariableData: IPluginVariableData) => + (() => { + this.ensurePluginVariableValue(pluginVariableData.value, `Unable to find the value for ${pluginVariableData.name} plugin variable into project package.json file. Verify that your package.json file is correct and try again.`); + pluginConfigurationFileContent = pluginConfigurationFileContent.replace(new RegExp(`{${pluginVariableData.name}}`, "gi"), pluginVariableData.value); + }).future()()).wait(); + return pluginConfigurationFileContent; + }).future()(); + } + + private ensurePluginVariableValue(pluginVariableValue: string, errorMessage: string): void { + if(!pluginVariableValue) { + this.$errors.failWithoutHelp(errorMessage); + } + } + + private getPluginVariableValue(pluginVariableData: IPluginVariableData): IFuture { + return (() => { + let pluginVariableName = pluginVariableData.name; + let value = this.$pluginVariablesHelper.getPluginVariableFromVarOption(pluginVariableName); + if(value) { + value = value[pluginVariableName]; + } else { + value = pluginVariableData.defaultValue; + if(!value && helpers.isInteractive() ) { + let promptSchema = { + name: pluginVariableName, + type: "input", + message: `Enter value for ${pluginVariableName} variable:`, + validate: (val: string) => !!val ? true : 'Please enter a value!' + }; + let promptData = this.$prompter.get([promptSchema]).wait(); + value = promptData[pluginVariableName]; + } + } + + return value; + }).future()(); + } + + private executeForAllPluginVariables(pluginData: IPluginData, action: (pluginVariableData: IPluginVariableData) => IFuture): IFuture { + return (() => { + let pluginVariables = pluginData.pluginVariables; + let pluginVariablesNames = _.keys(pluginVariables); + _.each(pluginVariablesNames, pluginVariableName => action(this.createPluginVariableData(pluginData, pluginVariableName).wait()).wait()); + }).future()(); + } + + private createPluginVariableData(pluginData: IPluginData, pluginVariableName: string): IFuture { + return (() => { + let variableData = pluginData.pluginVariables[pluginVariableName]; + + variableData.name = pluginVariableName; + + this.$projectDataService.initialize(this.$projectData.projectDir); + let pluginVariableValues = this.$projectDataService.getValue(this.getPluginVariablePropertyName(pluginData)).wait(); + variableData.value = pluginVariableValues ? pluginVariableValues[pluginVariableName] : undefined; + + return variableData; + }).future()(); + } +} +$injector.register("pluginVariablesService", PluginVariablesService); + diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 3258b76e02..3bf8998970 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -15,7 +15,8 @@ export class PluginsService implements IPluginsService { save: true }; - constructor(private $platformsData: IPlatformsData, + constructor(private $broccoliBuilder: IBroccoliBuilder, + private $platformsData: IPlatformsData, private $npm: INodePackageManager, private $fs: IFileSystem, private $projectData: IProjectData, @@ -24,15 +25,37 @@ export class PluginsService implements IPluginsService { private $options: IOptions, private $logger: ILogger, private $errors: IErrors, - private $projectFilesManager: IProjectFilesManager) { } + private $pluginVariablesService: IPluginVariablesService, + private $projectFilesManager: IProjectFilesManager, + private $injector: IInjector) { } public add(plugin: string): IFuture { return (() => { this.ensure().wait(); let dependencyData = this.$npm.cache(plugin, undefined, PluginsService.NPM_CONFIG).wait(); if(dependencyData.nativescript) { - this.executeNpmCommand(PluginsService.INSTALL_COMMAND_NAME, plugin).wait(); - this.prepare(dependencyData).wait(); + let pluginData = this.convertToPluginData(dependencyData); + + // Validate + let action = (pluginDestinationPath: string, platform: string, platformData: IPlatformData) => { + return (() => { + this.isPluginDataValidForPlatform(pluginData, platform).wait(); + }).future()(); + }; + this.executeForAllInstalledPlatforms(action).wait(); + + try { + this.$pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + this.executeNpmCommand(PluginsService.INSTALL_COMMAND_NAME, plugin).wait(); + } catch(err) { + // Revert package.json + this.$projectDataService.initialize(this.$projectData.projectDir); + this.$projectDataService.removeProperty(this.$pluginVariablesService.getPluginVariablePropertyName(pluginData)).wait(); + this.$projectDataService.removeDependency(pluginData.name).wait(); + + throw err; + } + this.$logger.out(`Successfully installed plugin ${dependencyData.name}.`); } else { this.$errors.failWithoutHelp(`${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`); @@ -43,14 +66,56 @@ export class PluginsService implements IPluginsService { public remove(pluginName: string): IFuture { return (() => { + let isUninstallCommandExecuted = false; + + let executeUninstallCommand = () => { + return (() => { + if(!isUninstallCommandExecuted) { + this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName).wait(); + isUninstallCommandExecuted = true; + } + }).future()(); + }; + let removePluginNativeCodeAction = (modulesDestinationPath: string, platform: string, platformData: IPlatformData) => { - let pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName).wait()); - pluginData.isPlugin = true; - return platformData.platformProjectService.removePluginNativeCode(pluginData); + return (() => { + let pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName).wait()); + + platformData.platformProjectService.removePluginNativeCode(pluginData).wait(); + + // Remove the plugin and call merge for another plugins that have configuration file + let pluginConfigurationFilePath = this.getPluginConfigurationFilePath(pluginData, platformData); + if(this.$fs.exists(pluginConfigurationFilePath).wait()) { + let tnsModulesDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, constants.TNS_MODULES_FOLDER_NAME); + let nodeModules = this.$broccoliBuilder.getChangedNodeModules(tnsModulesDestinationPath, platform).wait(); + + _(nodeModules) + .map(nodeModule => this.getNodeModuleData(pluginName).wait()) + .map(nodeModuleData => this.convertToPluginData(nodeModuleData)) + .filter(data => data.isPlugin && this.$fs.exists(this.getPluginConfigurationFilePath(data, platformData)).wait()) + .forEach((data, index) => { + executeUninstallCommand().wait(); + + if(index === 0) { + this.initializeConfigurationFileFromCache(platformData).wait(); + } + + if(data.name !== pluginName) { + this.merge(data, platformData).wait(); + } + }) + .value(); + } + + if(pluginData.pluginVariables) { + this.$pluginVariablesService.removePluginVariablesFromProjectFile(pluginData).wait(); + } + }).future()(); }; this.executeForAllInstalledPlatforms(removePluginNativeCodeAction).wait(); - this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName).wait(); + executeUninstallCommand().wait(); + let showMessage = true; let action = (modulesDestinationPath: string, platform: string, platformData: IPlatformData) => { return (() => { @@ -68,50 +133,40 @@ export class PluginsService implements IPluginsService { }).future()(); } + private initializeConfigurationFileFromCache(platformData: IPlatformData): IFuture { + return (() => { + this.$projectDataService.initialize(this.$projectData.projectDir); + let frameworkVersion = this.$projectDataService.getValue(platformData.frameworkPackageName).wait().version; + this.$npm.cache(platformData.frameworkPackageName, frameworkVersion).wait(); + + let relativeConfigurationFilePath = path.relative(platformData.projectRoot, platformData.configurationFilePath); + // We need to resolve this manager here due to some restrictions from npm api and in order to load PluginsService.NPM_CONFIG config + let npmInstallationManager: INpmInstallationManager = this.$injector.resolve("npmInstallationManager"); + let cachedPackagePath = npmInstallationManager.getCachedPackagePath(platformData.frameworkPackageName, frameworkVersion); + let cachedConfigurationFilePath = path.join(cachedPackagePath, constants.PROJECT_FRAMEWORK_FOLDER_NAME, relativeConfigurationFilePath); + + shelljs.cp("-f", cachedConfigurationFilePath, path.dirname(platformData.configurationFilePath)); + }).future()(); + } + public prepare(dependencyData: IDependencyData): IFuture { return (() => { let pluginData = this.convertToPluginData(dependencyData); let action = (pluginDestinationPath: string, platform: string, platformData: IPlatformData) => { return (() => { - // Process .js files - let installedFrameworkVersion = this.getInstalledFrameworkVersion(platform).wait(); - let pluginPlatformsData = pluginData.platformsData; - if(pluginPlatformsData) { - let pluginVersion = (pluginPlatformsData)[platform]; - if(!pluginVersion) { - this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`); - return; - } - - if(semver.gt(pluginVersion, installedFrameworkVersion)) { - this.$logger.warn(`${pluginData.name} ${pluginVersion} for ${platform} is not compatible with the currently installed framework version ${installedFrameworkVersion}.`); - return; - } + if(!this.isPluginDataValidForPlatform(pluginData, platform).wait()) { + return; } if(this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)).wait()) { - this.$fs.ensureDirectoryExists(pluginDestinationPath).wait(); shelljs.cp("-Rf", pluginData.fullPath, pluginDestinationPath); - let pluginPlatformsFolderPath = path.join(pluginDestinationPath, pluginData.name, "platforms", platform); - let pluginConfigurationFilePath = path.join(pluginPlatformsFolderPath, platformData.configurationFileName); - let configurationFilePath = platformData.configurationFilePath; + let pluginConfigurationFilePath = this.getPluginConfigurationFilePath(pluginData, platformData); if(this.$fs.exists(pluginConfigurationFilePath).wait()) { - // Validate plugin configuration file - let pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath).wait(); - this.validateXml(pluginConfigurationFileContent, pluginConfigurationFilePath); - - // Validate configuration file - let configurationFileContent = this.$fs.readText(configurationFilePath).wait(); - this.validateXml(configurationFileContent, configurationFilePath); - - // Merge xml - let resultXml = this.mergeXml(configurationFileContent, pluginConfigurationFileContent, platformData.mergeXmlConfig || []).wait(); - this.validateXml(resultXml); - this.$fs.writeFile(configurationFilePath, resultXml).wait(); + this.merge(pluginData, platformData).wait(); } this.$projectFilesManager.processPlatformSpecificFiles(pluginDestinationPath, platform).wait(); @@ -176,21 +231,20 @@ export class PluginsService implements IPluginsService { return _.keys(require(packageJsonFilePath).dependencies); } - private getNodeModuleData(moduleName: string): IFuture { + private getNodeModuleData(module: string): IFuture { // module can be modulePath or moduleName return (() => { - let packageJsonFilePath = this.getPackageJsonFilePathForModule(moduleName); - if(this.$fs.exists(packageJsonFilePath).wait()) { - let data = require(packageJsonFilePath); - return { - name: data.name, - version: data.version, - fullPath: path.dirname(packageJsonFilePath), - isPlugin: data.nativescript !== undefined, - moduleInfo: data.nativescript - }; + if(!this.$fs.exists(module).wait()) { + module = this.getPackageJsonFilePathForModule(module); } - return null; + let data = this.$fs.readJson(module).wait(); + return { + name: data.name, + version: data.version, + fullPath: path.dirname(module), + isPlugin: data.nativescript !== undefined, + moduleInfo: data.nativescript + }; }).future()(); } @@ -199,11 +253,13 @@ export class PluginsService implements IPluginsService { pluginData.name = cacheData.name; pluginData.version = cacheData.version; pluginData.fullPath = cacheData.directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name)); - pluginData.isPlugin = !!cacheData.nativescript; + pluginData.isPlugin = !!cacheData.nativescript || !!cacheData.moduleInfo; pluginData.pluginPlatformsFolderPath = (platform: string) => path.join(pluginData.fullPath, "platforms", platform); + let data = cacheData.nativescript || cacheData.moduleInfo; if(pluginData.isPlugin) { - pluginData.platformsData = cacheData.nativescript.platforms; + pluginData.platformsData = data.platforms; + pluginData.pluginVariables = data.variables; } return pluginData; @@ -290,5 +346,56 @@ export class PluginsService implements IPluginsService { }); doc.parseFromString(xml, 'text/xml'); } + + private merge(pluginData: IPluginData, platformData: IPlatformData): IFuture { + return (() => { + let pluginConfigurationFilePath = this.getPluginConfigurationFilePath(pluginData, platformData); + let configurationFilePath = platformData.configurationFilePath; + + // Validate plugin configuration file + let pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath).wait(); + pluginConfigurationFileContent = this.$pluginVariablesService.interpolatePluginVariables(pluginData, pluginConfigurationFileContent).wait(); + this.validateXml(pluginConfigurationFileContent, pluginConfigurationFilePath); + + // Validate configuration file + let configurationFileContent = this.$fs.readText(configurationFilePath).wait(); + this.validateXml(configurationFileContent, configurationFilePath); + + // Merge xml + let resultXml = this.mergeXml(configurationFileContent, pluginConfigurationFileContent, platformData.mergeXmlConfig || []).wait(); + this.validateXml(resultXml); + this.$fs.writeFile(configurationFilePath, resultXml).wait(); + }).future()(); + } + + private getPluginConfigurationFilePath(pluginData: IPluginData, platformData: IPlatformData): string { + let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platformData.normalizedPlatformName.toLowerCase()); + let pluginConfigurationFilePath = path.join(pluginPlatformsFolderPath, platformData.configurationFileName); + return pluginConfigurationFilePath; + } + + private isPluginDataValidForPlatform(pluginData: IPluginData, platform: string): IFuture { + return (() => { + let isValid = true; + + let installedFrameworkVersion = this.getInstalledFrameworkVersion(platform).wait(); + let pluginPlatformsData = pluginData.platformsData; + if(pluginPlatformsData) { + let pluginVersion = (pluginPlatformsData)[platform]; + if(!pluginVersion) { + this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`); + isValid = false; + } + + if(semver.gt(pluginVersion, installedFrameworkVersion)) { + this.$logger.warn(`${pluginData.name} ${pluginVersion} for ${platform} is not compatible with the currently installed framework version ${installedFrameworkVersion}.`); + isValid = false; + } + } + + return isValid; + + }).future()(); + } } $injector.register("pluginsService", PluginsService); diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 87d1370bb9..ff2f17aa98 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -5,6 +5,8 @@ import * as path from "path"; import * as assert from "assert"; export class ProjectDataService implements IProjectDataService { + private static DEPENDENCIES_KEY_NAME = "dependencies"; + private projectFilePath: string; private projectData: IDictionary; @@ -46,9 +48,17 @@ export class ProjectDataService implements IProjectDataService { }).future()(); } + public removeDependency(dependencyName: string): IFuture { + return (() => { + this.loadProjectFile().wait(); + delete this.projectData[ProjectDataService.DEPENDENCIES_KEY_NAME][dependencyName]; + this.$fs.writeJson(this.projectFilePath, this.projectData, "\t").wait(); + }).future()(); + } + private loadProjectFile(): IFuture { return (() => { - assert.ok(this.projectFilePath, "Initialize method of projectDataService is not called"); + assert.ok(this.projectFilePath, "Initialize method of projectDataService is not called."); if(!this.projectData) { if(!this.$fs.exists(this.projectFilePath).wait()) { diff --git a/lib/tools/broccoli/broccoli.d.ts b/lib/tools/broccoli/broccoli.d.ts index 7d4ba88820..df7b6d6fdb 100644 --- a/lib/tools/broccoli/broccoli.d.ts +++ b/lib/tools/broccoli/broccoli.d.ts @@ -152,7 +152,8 @@ interface BroccoliNode { } interface IBroccoliBuilder { - prepareNodeModules(outputPath: string, projectDir: string, platform: string, lastModifiedTime?: Date): IFuture; + getChangedNodeModules(outputPath: string, platform: string, lastModifiedTime?: Date): IFuture; + prepareNodeModules(outputPath: string, platform: string, lastModifiedTime?: Date): IFuture; } interface IDiffResult { diff --git a/lib/tools/broccoli/builder.ts b/lib/tools/broccoli/builder.ts index 595911df94..6a78eb89bf 100644 --- a/lib/tools/broccoli/builder.ts +++ b/lib/tools/broccoli/builder.ts @@ -1,6 +1,7 @@ /// "use strict"; +import * as constants from "../../../lib/constants"; import * as path from "path"; import Future = require("fibers/future"); import destCopyLib = require("./node-modules-dest-copy"); @@ -10,78 +11,86 @@ let vinylFilterSince = require("vinyl-filter-since"); let through = require("through2"); export class Builder implements IBroccoliBuilder { - private nodeModules: any = {}; - constructor(private $fs: IFileSystem, private $nodeModulesTree: INodeModulesTree, + private $projectData: IProjectData, private $projectDataService: IProjectDataService, private $injector: IInjector, private $logger: ILogger) { } - public prepareNodeModules(absoluteOutputPath: string, projectDir: string, platform: string, lastModifiedTime?: Date): IFuture { + public getChangedNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime?: Date): IFuture { + return (() => { + let projectDir = this.$projectData.projectDir; + let isNodeModulesModified = false; + let nodeModulesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME); + let nodeModules: any = {}; + + if(lastModifiedTime) { + let pipeline = gulp.src(path.join(projectDir, "node_modules/**")) + .pipe(vinylFilterSince(lastModifiedTime)) + .pipe(through.obj( (chunk: any, enc: any, cb: Function) => { + if(chunk.path === nodeModulesPath) { + isNodeModulesModified = true; + } + + if(!isNodeModulesModified) { + let rootModuleName = chunk.path.split(nodeModulesPath)[1].split(path.sep)[1]; + let rootModuleFullPath = path.join(nodeModulesPath, rootModuleName); + nodeModules[rootModuleFullPath] = rootModuleFullPath; + } + + cb(null); + })) + .pipe(gulp.dest(absoluteOutputPath)); + + let future = new Future(); + + pipeline.on('end', (err: Error, data: any) => { + if(err) { + future.throw(err); + } else { + future.return(); + } + }); + + future.wait(); + } + + if(isNodeModulesModified && this.$fs.exists(absoluteOutputPath).wait()) { + let currentPreparedTnsModules = this.$fs.readDirectory(absoluteOutputPath).wait(); + let tnsModulesPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.TNS_MODULES_FOLDER_NAME); + if(!this.$fs.exists(tnsModulesPath).wait()) { + tnsModulesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, constants.TNS_CORE_MODULES_NAME); + } + let tnsModulesInApp = this.$fs.readDirectory(tnsModulesPath).wait(); + let modulesToDelete = _.difference(currentPreparedTnsModules, tnsModulesInApp); + _.each(modulesToDelete, moduleName => this.$fs.deleteDirectory(path.join(absoluteOutputPath, moduleName)).wait()); + } + + if(!lastModifiedTime || isNodeModulesModified) { + let nodeModulesDirectories = this.$fs.exists(nodeModulesPath).wait() ? this.$fs.readDirectory(nodeModulesPath).wait() : []; + _.each(nodeModulesDirectories, nodeModuleDirectoryName => { + let nodeModuleFullPath = path.join(nodeModulesPath, nodeModuleDirectoryName); + nodeModules[nodeModuleFullPath] = nodeModuleFullPath; + }); + } + + return nodeModules; + }).future()(); + } + + public prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime?: Date): IFuture { return (() => { - let isNodeModulesModified = false; - let nodeModulesPath = path.join(projectDir, "node_modules"); - - if(lastModifiedTime) { - let pipeline = gulp.src(path.join(projectDir, "node_modules/**")) - .pipe(vinylFilterSince(lastModifiedTime)) - .pipe(through.obj( (chunk: any, enc: any, cb: Function) => { - if(chunk.path === nodeModulesPath) { - isNodeModulesModified = true; - } - - if(!isNodeModulesModified) { - let rootModuleName = chunk.path.split(nodeModulesPath)[1].split(path.sep)[1]; - let rootModuleFullPath = path.join(nodeModulesPath, rootModuleName); - this.nodeModules[rootModuleFullPath] = rootModuleFullPath; - } - - cb(null); - })) - .pipe(gulp.dest(absoluteOutputPath)); - - let future = new Future(); - - pipeline.on('end', (err: any, data: any) => { - if(err) { - future.throw(err); - } else { - future.return(); - } - }); - - future.wait(); - } - - if(isNodeModulesModified && this.$fs.exists(absoluteOutputPath).wait()) { - let currentPreparedTnsModules = this.$fs.readDirectory(absoluteOutputPath).wait(); - let tnsModulesPath = path.join(projectDir, "app", "tns_modules"); - if(!this.$fs.exists(tnsModulesPath).wait()) { - tnsModulesPath = path.join(projectDir, "node_modules", "tns-core-modules"); - } - let tnsModulesInApp = this.$fs.readDirectory(tnsModulesPath).wait(); - let modulesToDelete = _.difference(currentPreparedTnsModules, tnsModulesInApp); - _.each(modulesToDelete, moduleName => this.$fs.deleteDirectory(path.join(absoluteOutputPath, moduleName)).wait()); - } - - if(!lastModifiedTime || isNodeModulesModified) { - let nodeModulesDirectories = this.$fs.exists(nodeModulesPath).wait() ? this.$fs.readDirectory(nodeModulesPath).wait() : []; - _.each(nodeModulesDirectories, nodeModuleDirectoryName => { - let nodeModuleFullPath = path.join(nodeModulesPath, nodeModuleDirectoryName); - this.nodeModules[nodeModuleFullPath] = nodeModuleFullPath; - }); - } - - let destCopy = this.$injector.resolve(destCopyLib.DestCopy, { - inputPath: projectDir, - cachePath: "", - outputRoot: absoluteOutputPath, - projectDir: projectDir, - platform: platform - }); - - destCopy.rebuildChangedDirectories(_.keys(this.nodeModules)); + let nodeModules = this.getChangedNodeModules(absoluteOutputPath, platform, lastModifiedTime).wait(); + let destCopy = this.$injector.resolve(destCopyLib.DestCopy, { + inputPath: this.$projectData.projectDir, + cachePath: "", + outputRoot: absoluteOutputPath, + projectDir: this.$projectData.projectDir, + platform: platform + }); + + destCopy.rebuildChangedDirectories(_.keys(nodeModules)); }).future()(); } diff --git a/test/npm-support.ts b/test/npm-support.ts index 7c1f4723a5..f6edacf217 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -59,6 +59,7 @@ function createTestInjector(): IInjector { testInjector.register("commandsServiceProvider", { registerDynamicSubCommands: () => { /* intentionally left blank */ } }); + testInjector.register("pluginVariablesService", {}); return testInjector; } diff --git a/test/plugin-variables-service.ts b/test/plugin-variables-service.ts new file mode 100644 index 0000000000..800250d4f9 --- /dev/null +++ b/test/plugin-variables-service.ts @@ -0,0 +1,338 @@ +/// +"use strict"; + +import {assert} from "chai"; +import {Errors} from "../lib/common/errors"; +import {FileSystem} from "../lib/common/file-system"; +import Future = require("fibers/future"); +import {HostInfo} from "../lib/common/host-info"; +import {Options} from "../lib/options"; +import {PluginVariablesHelper} from "../lib/common/plugin-variables-helper"; +import {PluginVariablesService} from "../lib/services/plugin-variables-service"; +import {ProjectData} from "../lib/project-data"; +import {ProjectDataService} from "../lib/services/project-data-service"; +import {ProjectHelper} from "../lib/common/project-helper"; +import {StaticConfig} from "../lib/config"; +import {Yok} from '../lib/common/yok'; +import * as stubs from './stubs'; +import * as path from "path"; +import * as temp from "temp"; +temp.track(); + +function createTestInjector(): IInjector { + let testInjector = new Yok(); + + testInjector.register("errors", Errors); + testInjector.register("fs", FileSystem); + testInjector.register("hostInfo", HostInfo); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("options", Options); + testInjector.register("pluginVariablesHelper", PluginVariablesHelper); + testInjector.register("pluginVariablesService", PluginVariablesService); + testInjector.register("projectData", ProjectData); + testInjector.register("projectDataService", ProjectDataService); + testInjector.register("projectHelper", ProjectHelper); + testInjector.register("prompter", { + get: () => { + let errors: IErrors = testInjector.resolve("errors"); + errors.fail("$prompter.get function shouldn't be called!"); + } + }); + testInjector.register("staticConfig", StaticConfig); + + return testInjector; +} + +function createProjectFile(testInjector: IInjector): IFuture { + return (() => { + let tempFolder = temp.mkdirSync("pluginVariablesService"); + + let options = testInjector.resolve("options"); + options.path = tempFolder; + + let projectData = { + "name": "myProject", + "nativescript": { } + }; + testInjector.resolve("fs").writeJson(path.join(tempFolder, "package.json"), projectData).wait(); + }).future()(); +} + +function createPluginData(pluginVariables: any): IPluginData { + let pluginData = { + name: "myTestPlugin", + version: "", + fullPath: "", + isPlugin: true, + moduleInfo: "", + platformsData: { + ios: "", + android: "" + }, + pluginVariables: pluginVariables, + pluginPlatformsFolderPath: (platform: string) => "" + }; + + return pluginData; +} + +describe("Plugin Variables service", () => { + let testInjector: IInjector; + beforeEach(() => { + testInjector = createTestInjector(); + }); + + describe("plugin add when the console is non interactive", () => { + beforeEach(() => { + let helpers = require("./../lib/common/helpers"); + helpers.isInteractive = () => false; + }); + it("fails when no --var option and no default value are specified", () => { + createProjectFile(testInjector).wait(); + + let pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + + let expectedError = `Unable to find value for MY_TEST_PLUGIN_VARIABLE plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`; + let actualError: string = null; + + try { + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + } catch(err) { + actualError = err.message; + } + + assert.equal(expectedError, actualError); + }); + it("does not fail when --var option is specified", () => { + createProjectFile(testInjector).wait(); + + let pluginVariableValue = "myAppId"; + testInjector.resolve("options").var = { + "MY_APP_ID": pluginVariableValue + }; + + let pluginVariables = { "MY_APP_ID": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + + let fs = testInjector.resolve("fs"); + let projectData = testInjector.resolve("projectData"); + let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + + let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")).wait(); + assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_APP_ID"]); + }); + it("does not fail when default value is specified", () => { + createProjectFile(testInjector).wait(); + + let defaultPluginValue = "myDefaultValue"; + let pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": { defaultValue: defaultPluginValue } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + + let fs = testInjector.resolve("fs"); + let projectData = testInjector.resolve("projectData"); + let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + + let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")).wait(); + assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_TEST_PLUGIN_VARIABLE"]); + }); + }); + + describe("plugin add when the console is interactive", () => { + beforeEach(() => { + let helpers = require("./../lib/common/helpers"); + helpers.isInteractive = () => true; + }); + it("prompt for plugin variable value when no --var option and no default value are specified", () => { + createProjectFile(testInjector).wait(); + + let pluginVariableValue = "testAppURL"; + let prompter = testInjector.resolve("prompter"); + prompter.get = () => { + return Future.fromResult({ "APP_URL": pluginVariableValue }); + }; + + let pluginVariables = { "APP_URL": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + + let fs = testInjector.resolve("fs"); + let projectData = testInjector.resolve("projectData"); + let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + + let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")).wait(); + assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_URL"]); + }); + it("does not prompt for plugin variable value when default value is specified", () => { + createProjectFile(testInjector).wait(); + + let defaultPluginValue = "myAppNAme"; + let pluginVariables = { "APP_NAME": { defaultValue: defaultPluginValue } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + + let fs = testInjector.resolve("fs"); + let projectData = testInjector.resolve("projectData"); + let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + + let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")).wait(); + assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_NAME"]); + }); + it("does not prompt for plugin variable value when --var option is specified", () => { + createProjectFile(testInjector).wait(); + + let pluginVariableValue = "pencho.goshko"; + testInjector.resolve("options").var = { + "USERNAME": pluginVariableValue + }; + + let pluginVariables = { "USERNAME": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + pluginVariablesService.savePluginVariablesInProjectFile(pluginData).wait(); + + let fs = testInjector.resolve("fs"); + let projectData = testInjector.resolve("projectData"); + let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + + let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")).wait(); + assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["USERNAME"]); + }); + }); + + describe("plugin interpolation", () => { + it("fails when the plugin value is undefined", () => { + createProjectFile(testInjector).wait(); + + let pluginVariables = { "MY_VAR": { } }; + let pluginData = createPluginData(pluginVariables); + + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + + let expectedError = "Unable to find the value for MY_VAR plugin variable into project package.json file. Verify that your package.json file is correct and try again."; + let error: string = null; + try { + pluginVariablesService.interpolatePluginVariables(pluginData, "").wait(); + } catch(err) { + error = err.message; + } + + assert.equal(error, expectedError); + }); + + it("interpolates correctly plugin variable value", () => { + createProjectFile(testInjector).wait(); + + let projectData: IProjectData = testInjector.resolve("projectData"); + let fs: IFileSystem = testInjector.resolve("fs"); + + // Write plugin variables values to package.json file + let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + let data = fs.readJson(packageJsonFilePath).wait(); + data["nativescript"]["myTestPlugin-variables"] = { + "FB_APP_NAME": "myFacebookAppName" + }; + fs.writeJson(packageJsonFilePath, data).wait(); + + let pluginVariables = { "FB_APP_NAME": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + let pluginConfigurationFileContent = '' + + '' + + '' + + '' + + '' + + ''; + let result = pluginVariablesService.interpolatePluginVariables(pluginData, pluginConfigurationFileContent).wait(); + + let expectedResult = '' + + '' + + '' + + '' + + '' + + ''; + + assert.equal(result, expectedResult); + }); + + it("interpolates correctly case sensive plugin variable value", () => { + createProjectFile(testInjector).wait(); + + let projectData: IProjectData = testInjector.resolve("projectData"); + let fs: IFileSystem = testInjector.resolve("fs"); + + // Write plugin variables values to package.json file + let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + let data = fs.readJson(packageJsonFilePath).wait(); + data["nativescript"]["myTestPlugin-variables"] = { + "FB_APP_NAME": "myFacebookAppName" + }; + fs.writeJson(packageJsonFilePath, data).wait(); + + let pluginVariables = { "FB_APP_NAME": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + let pluginConfigurationFileContent = '' + + '' + + '' + + '' + + '' + + ''; + let result = pluginVariablesService.interpolatePluginVariables(pluginData, pluginConfigurationFileContent).wait(); + + let expectedResult = '' + + '' + + '' + + '' + + '' + + ''; + + assert.equal(result, expectedResult); + }); + + it("interpolates correctly more than one plugin variables values", () => { + createProjectFile(testInjector).wait(); + + let projectData: IProjectData = testInjector.resolve("projectData"); + let fs: IFileSystem = testInjector.resolve("fs"); + + let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + let data = fs.readJson(packageJsonFilePath).wait(); + data["nativescript"]["myTestPlugin-variables"] = { + "FB_APP_NAME": "myFacebookAppName", + "FB_APP_URL": "myFacebookAppURl" + }; + fs.writeJson(packageJsonFilePath, data).wait(); + + let pluginVariables = { "FB_APP_NAME": { }, "FB_APP_URL": { } }; + let pluginData = createPluginData(pluginVariables); + let pluginVariablesService = testInjector.resolve("pluginVariablesService"); + let pluginConfigurationFileContent = '' + + '' + + '' + + '' + + '' + + '' + + ''; + + let result = pluginVariablesService.interpolatePluginVariables(pluginData, pluginConfigurationFileContent).wait(); + + let expectedResult = '' + + '' + + '' + + '' + + '' + + '' + + ''; + + assert.equal(result, expectedResult); + }); + }); +}); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index ca88ba1f9c..9abd0d25b3 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -67,6 +67,11 @@ function createTestInjector() { trackFeature: () => { return future.fromResult(); } }); testInjector.register("projectFilesManager", ProjectFilesManager); + testInjector.register("pluginVariablesService", { + savePluginVariablesInProjectFile: (pluginData: IPluginData) => future.fromResult(), + interpolatePluginVariables: (pluginData: IPluginData, pluginConfigurationFileContent: string) => future.fromResult(pluginConfigurationFileContent) + }); + testInjector.register("npmInstallationManager", {}); return testInjector; } @@ -243,7 +248,8 @@ describe("Plugins service", () => { platformsData.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: path.join(projectFolder, "platforms", "android"), - frameworkPackageName: "tns-android" + frameworkPackageName: "tns-android", + normalizedPlatformName: "Android" }; }; @@ -489,12 +495,16 @@ describe("Plugins service", () => { return { appDestinationDirectoryPath: appDestinationDirectoryPath, frameworkPackageName: "tns-android", - configurationFileName: "AndroidManifest.xml" + configurationFileName: "AndroidManifest.xml", + normalizedPlatformName: "Android", + platformProjectService: { + preparePluginNativeCode: (pluginData: IPluginData) => future.fromResult() + } }; }; // Ensure the pluginDestinationPath folder exists - let pluginPlatformsDirPath = path.join(appDestinationDirectoryPath, "app", "tns_modules", pluginName, "platforms", "android"); + let pluginPlatformsDirPath = path.join(projectFolder, "node_modules", pluginName, "platforms", "android"); fs.ensureDirectoryExists(pluginPlatformsDirPath).wait(); // Creates invalid plugin's AndroidManifest.xml file @@ -510,9 +520,7 @@ describe("Plugins service", () => { `\n@#[line:1,col:39].` + `\n@#[line:1,col:39].`; mockBeginCommand(testInjector, expectedErrorMessage); - - let commandsService = testInjector.resolve(CommandsService); - commandsService.tryExecuteCommand("plugin|add", [pluginFolderPath]).wait(); + pluginsService.prepare(pluginJsonData).wait(); }); it("merges AndroidManifest.xml and produces correct xml", () => { let pluginName = "mySamplePlugin"; @@ -544,6 +552,7 @@ describe("Plugins service", () => { }; let appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android"); + fs.ensureDirectoryExists(path.join(appDestinationDirectoryPath, "app")).wait(); // Mock platformsData let platformsData = testInjector.resolve("platformsData"); @@ -556,12 +565,13 @@ describe("Plugins service", () => { mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }], platformProjectService: { preparePluginNativeCode: (pluginData: IPluginData) => future.fromResult() - } + }, + normalizedPlatformName: "Android" }; }; // Ensure the pluginDestinationPath folder exists - let pluginPlatformsDirPath = path.join(appDestinationDirectoryPath, "app", "tns_modules", pluginName, "platforms", "android"); + let pluginPlatformsDirPath = path.join(projectFolder, "node_modules", pluginName, "platforms", "android"); fs.ensureDirectoryExists(pluginPlatformsDirPath).wait(); // Creates valid plugin's AndroidManifest.xml file @@ -572,7 +582,7 @@ describe("Plugins service", () => { let pluginConfigurationFilePath = path.join(pluginPlatformsDirPath, "AndroidManifest.xml"); fs.writeFile(pluginConfigurationFilePath, xml).wait(); - pluginsService.add(pluginFolderPath).wait(); + pluginsService.prepare(pluginJsonData).wait(); let expectedXml = ''; expectedXml = helpers.stringReplaceAll(expectedXml, EOL, ""); diff --git a/test/stubs.ts b/test/stubs.ts index 70545e8233..4fffa1280b 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -338,6 +338,10 @@ export class ProjectDataService implements IProjectDataService { removeProperty(propertyName: string): IFuture { return Future.fromResult(); } + + removeDependency(dependencyName: string): IFuture { + return Future.fromResult(); + } } export class ProjectHelperStub implements IProjectHelper {