diff --git a/lib/tools/broccoli/node-modules-dest-copy.ts b/lib/tools/broccoli/node-modules-dest-copy.ts index 0fa52e79d9..d8160ea792 100644 --- a/lib/tools/broccoli/node-modules-dest-copy.ts +++ b/lib/tools/broccoli/node-modules-dest-copy.ts @@ -15,8 +15,8 @@ import Future = require("fibers/future"); * and tees a copy to the given path outside the tmp dir. */ export class DestCopy implements IBroccoliPlugin { - private dependencies: IDictionary = null; - private devDependencies: IDictionary = null; + private dependencies: IDictionary = null; + private devDependencies: IDictionary = null; constructor( private inputPath: string, @@ -33,72 +33,72 @@ export class DestCopy implements IBroccoliPlugin { this.devDependencies = this.getDevDependencies(projectDir); } - public rebuildChangedDirectories(changedDirectories: string[], platform: string): void { - _.each(changedDirectories, changedDirectoryAbsolutePath => { - if(!this.devDependencies[path.basename(changedDirectoryAbsolutePath)]) { - let pathToPackageJson = path.join(changedDirectoryAbsolutePath, constants.PACKAGE_JSON_FILE_NAME); - let packageJsonFiles = fs.existsSync(pathToPackageJson) ? [pathToPackageJson] : []; - let nodeModulesFolderPath = path.join(changedDirectoryAbsolutePath, constants.NODE_MODULES_FOLDER_NAME); - packageJsonFiles = packageJsonFiles.concat(this.enumeratePackageJsonFilesSync(nodeModulesFolderPath)); - - _.each(packageJsonFiles, packageJsonFilePath => { - let fileContent = require(packageJsonFilePath); - - if(!this.devDependencies[fileContent.name]) { // Don't flatten dev dependencies - let currentDependency = { - name: fileContent.name, - version: fileContent.version, - directory: path.dirname(packageJsonFilePath), - nativescript: fileContent.nativescript - }; - - let addedDependency = this.dependencies[currentDependency.name]; - if (addedDependency) { - if (semver.gt(currentDependency.version, addedDependency.version)) { - let currentDependencyMajorVersion = semver.major(currentDependency.version); - let addedDependencyMajorVersion = semver.major(addedDependency.version); - - let message = `The depedency located at ${addedDependency.directory} with version ${addedDependency.version} will be replaced with dependency located at ${currentDependency.directory} with version ${currentDependency.version}`; - let logger = $injector.resolve("$logger"); - currentDependencyMajorVersion === addedDependencyMajorVersion ? logger.out(message) : logger.warn(message); - + public rebuildChangedDirectories(changedDirectories: string[], platform: string): void { + _.each(changedDirectories, changedDirectoryAbsolutePath => { + if (!this.devDependencies[path.basename(changedDirectoryAbsolutePath)]) { + let pathToPackageJson = path.join(changedDirectoryAbsolutePath, constants.PACKAGE_JSON_FILE_NAME); + let packageJsonFiles = fs.existsSync(pathToPackageJson) ? [pathToPackageJson] : []; + let nodeModulesFolderPath = path.join(changedDirectoryAbsolutePath, constants.NODE_MODULES_FOLDER_NAME); + packageJsonFiles = packageJsonFiles.concat(this.enumeratePackageJsonFilesSync(nodeModulesFolderPath)); + + _.each(packageJsonFiles, packageJsonFilePath => { + let fileContent = require(packageJsonFilePath); + + if (!this.devDependencies[fileContent.name]) { // Don't flatten dev dependencies + let currentDependency = { + name: fileContent.name, + version: fileContent.version, + directory: path.dirname(packageJsonFilePath), + nativescript: fileContent.nativescript + }; + + let addedDependency = this.dependencies[currentDependency.name]; + if (addedDependency) { + if (semver.gt(currentDependency.version, addedDependency.version)) { + let currentDependencyMajorVersion = semver.major(currentDependency.version); + let addedDependencyMajorVersion = semver.major(addedDependency.version); + + let message = `The depedency located at ${addedDependency.directory} with version ${addedDependency.version} will be replaced with dependency located at ${currentDependency.directory} with version ${currentDependency.version}`; + let logger = $injector.resolve("$logger"); + currentDependencyMajorVersion === addedDependencyMajorVersion ? logger.out(message) : logger.warn(message); + + this.dependencies[currentDependency.name] = currentDependency; + } + } else { this.dependencies[currentDependency.name] = currentDependency; } - } else { - this.dependencies[currentDependency.name] = currentDependency; } - } - }); - } - }); + }); + } + }); - _.each(this.dependencies, dependency => { - this.copyDependencyDir(dependency); + _.each(this.dependencies, dependency => { + this.copyDependencyDir(dependency); - let isPlugin = !!dependency.nativescript; - if(isPlugin) { - this.$pluginsService.prepare(dependency, platform).wait(); - } + let isPlugin = !!dependency.nativescript; + if (isPlugin) { + this.$pluginsService.prepare(dependency, platform).wait(); + } - if (dependency.name === constants.TNS_CORE_MODULES_NAME) { - let tnsCoreModulesResourcePath = path.join(this.outputRoot, constants.TNS_CORE_MODULES_NAME); + if (dependency.name === constants.TNS_CORE_MODULES_NAME) { + let tnsCoreModulesResourcePath = path.join(this.outputRoot, constants.TNS_CORE_MODULES_NAME); - // Remove .ts files - let allFiles = this.$fs.enumerateFilesInDirectorySync(tnsCoreModulesResourcePath); - let deleteFilesFutures = allFiles.filter(file => minimatch(file, "**/*.ts", {nocase: true})).map(file => this.$fs.deleteFile(file)); - Future.wait(deleteFilesFutures); + // Remove .ts files + let allFiles = this.$fs.enumerateFilesInDirectorySync(tnsCoreModulesResourcePath); + let deleteFilesFutures = allFiles.filter(file => minimatch(file, "**/*.ts", { nocase: true })).map(file => this.$fs.deleteFile(file)); + Future.wait(deleteFilesFutures); - shelljs.cp("-Rf", path.join(tnsCoreModulesResourcePath, "*"), this.outputRoot); - this.$fs.deleteDirectory(tnsCoreModulesResourcePath).wait(); - } - }); + shelljs.cp("-Rf", path.join(tnsCoreModulesResourcePath, "*"), this.outputRoot); + this.$fs.deleteDirectory(tnsCoreModulesResourcePath).wait(); + } + }); - if(!_.isEmpty(this.dependencies)) { - this.$platformsData.getPlatformData(platform).platformProjectService.afterPrepareAllPlugins().wait(); + if (!_.isEmpty(this.dependencies)) { + this.$platformsData.getPlatformData(platform).platformProjectService.afterPrepareAllPlugins().wait(); + } } - } - private copyDependencyDir(dependency: any): void { + private copyDependencyDir(dependency: any): void { let dependencyDir = path.dirname(dependency.name || ""); let insideNpmScope = /^@/.test(dependencyDir); let targetDir = this.outputRoot; @@ -108,42 +108,47 @@ export class DestCopy implements IBroccoliPlugin { shelljs.mkdir("-p", targetDir); shelljs.cp("-Rf", dependency.directory, targetDir); shelljs.rm("-rf", path.join(targetDir, dependency.name, "node_modules")); - } + } - public rebuild(treeDiff: IDiffResult): void { - this.rebuildChangedDirectories(treeDiff.changedDirectories, ""); + public rebuild(treeDiff: IDiffResult): void { + this.rebuildChangedDirectories(treeDiff.changedDirectories, ""); - // Cache input tree - let projectFilePath = path.join(this.projectDir, constants.PACKAGE_JSON_FILE_NAME); - let projectFileContent = require(projectFilePath); - projectFileContent[constants.NATIVESCRIPT_KEY_NAME][constants.NODE_MODULE_CACHE_PATH_KEY_NAME] = this.inputPath; - fs.writeFileSync(projectFilePath, JSON.stringify(projectFileContent, null, "\t"), { encoding: "utf8" }); - } + // Cache input tree + let projectFilePath = path.join(this.projectDir, constants.PACKAGE_JSON_FILE_NAME); + let projectFileContent = require(projectFilePath); + projectFileContent[constants.NATIVESCRIPT_KEY_NAME][constants.NODE_MODULE_CACHE_PATH_KEY_NAME] = this.inputPath; + fs.writeFileSync(projectFilePath, JSON.stringify(projectFileContent, null, "\t"), { encoding: "utf8" }); + } - private getDevDependencies(projectDir: string): IDictionary { - let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); - let projectFileContent = require(projectFilePath); - return projectFileContent.devDependencies || {}; - } + private getDevDependencies(projectDir: string): IDictionary { + let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); + let projectFileContent = require(projectFilePath); + return projectFileContent.devDependencies || {}; + } - private enumeratePackageJsonFilesSync(nodeModulesDirectoryPath: string, foundFiles?: string[]): string[] { + private enumeratePackageJsonFilesSync(nodeModulesDirectoryPath: string, foundFiles?: string[]): string[] { foundFiles = foundFiles || []; - if(fs.existsSync(nodeModulesDirectoryPath)) { + if (fs.existsSync(nodeModulesDirectoryPath)) { let contents = fs.readdirSync(nodeModulesDirectoryPath); for (let i = 0; i < contents.length; ++i) { - let packageJsonFilePath = path.join(nodeModulesDirectoryPath, contents[i], constants.PACKAGE_JSON_FILE_NAME); + let moduleName = contents[i]; + let moduleDirectoryInNodeModules = path.join(nodeModulesDirectoryPath, moduleName); + let packageJsonFilePath = path.join(moduleDirectoryInNodeModules, constants.PACKAGE_JSON_FILE_NAME); if (fs.existsSync(packageJsonFilePath)) { foundFiles.push(packageJsonFilePath); } - let directoryPath = path.join(nodeModulesDirectoryPath, contents[i], constants.NODE_MODULES_FOLDER_NAME); + let directoryPath = path.join(moduleDirectoryInNodeModules, constants.NODE_MODULES_FOLDER_NAME); if (fs.existsSync(directoryPath)) { this.enumeratePackageJsonFilesSync(directoryPath, foundFiles); + } else if (fs.statSync(moduleDirectoryInNodeModules).isDirectory()) { + // Some modules can be grouped in one folder and we need to enumerate them too (e.g. @angular). + this.enumeratePackageJsonFilesSync(moduleDirectoryInNodeModules, foundFiles); } } } return foundFiles; - } + } } export default wrapBroccoliPlugin(DestCopy); diff --git a/resources/test/plugin-with-scoped-dependency.zip b/resources/test/plugin-with-scoped-dependency.zip new file mode 100644 index 0000000000..c96dc0606d Binary files /dev/null and b/resources/test/plugin-with-scoped-dependency.zip differ diff --git a/test/npm-support.ts b/test/npm-support.ts index 4039e75bdd..7ebbc19288 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -37,6 +37,7 @@ import shelljs = require("shelljs"); temp.track(); let assert = require("chai").assert; +let nodeModulesFolderName = "node_modules"; function createTestInjector(): IInjector { let testInjector = new yok.Yok(); @@ -171,7 +172,7 @@ function addDependencies(testInjector: IInjector, projectFolder: string, depende let currentDependencies = packageJsonData.dependencies; _.extend(currentDependencies, dependencies); - if(devDependencies) { + if (devDependencies) { let currentDevDependencies = packageJsonData.devDependencies; _.extend(currentDevDependencies, devDependencies); } @@ -194,7 +195,7 @@ describe("Npm support tests", () => { }); it("Ensures that the installed dependencies are prepared correctly", () => { // Setup - addDependencies(testInjector, projectFolder, {"bplist": "0.0.4"}).wait(); + addDependencies(testInjector, projectFolder, { "bplist": "0.0.4" }).wait(); // Act preparePlatform(testInjector).wait(); @@ -216,7 +217,7 @@ describe("Npm support tests", () => { // Setup let fs = testInjector.resolve("fs"); let scopedName = "@reactivex/rxjs"; - let scopedModule = path.join(projectFolder, "node_modules", "@reactivex/rxjs"); + let scopedModule = path.join(projectFolder, nodeModulesFolderName, "@reactivex/rxjs"); let scopedPackageJson = path.join(scopedModule, "package.json"); let dependencies: any = {}; dependencies[scopedName] = "0.0.0-prealpha.3"; @@ -224,7 +225,7 @@ describe("Npm support tests", () => { addDependencies(testInjector, projectFolder, {}).wait(); //create module dir, and add a package.json shelljs.mkdir("-p", scopedModule); - fs.writeFile(scopedPackageJson, JSON.stringify({name: scopedName})).wait(); + fs.writeFile(scopedPackageJson, JSON.stringify({ name: scopedName })).wait(); // Act preparePlatform(testInjector).wait(); @@ -236,6 +237,26 @@ describe("Npm support tests", () => { assert.isTrue(fs.exists(scopedDependencyPath).wait()); }); + it("Ensures that scoped dependencies are prepared correctly when are not in root level", () => { + // Setup + let customPluginName = "plugin-with-scoped-dependency"; + let customPluginDirectory = temp.mkdirSync("custom-plugin-directory"); + + let fs: IFileSystem = testInjector.resolve("fs"); + fs.unzip(path.join("resources", "test", `${customPluginName}.zip`), customPluginDirectory).wait(); + + addDependencies(testInjector, projectFolder, { "plugin-with-scoped-dependency": `file:${path.join(customPluginDirectory, customPluginName)}` }).wait(); + + // Act + preparePlatform(testInjector).wait(); + + // Assert + let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + + let scopedDependencyPath = path.join(tnsModulesFolderPath, "@scoped-plugin", "inner-plugin"); + assert.isTrue(fs.exists(scopedDependencyPath).wait()); + }); + it("Ensures that tns_modules absent when bundling", () => { let fs = testInjector.resolve("fs"); let lockfile = testInjector.resolve("lockfile"); @@ -294,19 +315,19 @@ describe("Flatten npm modules tests", () => { assert.isFalse(fs.exists(gulpJshint).wait()); // Get all gulp dependencies - let gulpDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp", "node_modules")).wait(); + let gulpDependencies = fs.readDirectory(path.join(projectFolder, nodeModulesFolderName, "gulp", nodeModulesFolderName)).wait(); _.each(gulpDependencies, dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); }); // Get all gulp-jscs dependencies - let gulpJscsDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp-jscs", "node_modules")).wait(); + let gulpJscsDependencies = fs.readDirectory(path.join(projectFolder, nodeModulesFolderName, "gulp-jscs", nodeModulesFolderName)).wait(); _.each(gulpJscsDependencies, dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); }); // Get all gulp-jshint dependencies - let gulpJshintDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp-jshint", "node_modules")).wait(); + let gulpJshintDependencies = fs.readDirectory(path.join(projectFolder, nodeModulesFolderName, "gulp-jshint", nodeModulesFolderName)).wait(); _.each(gulpJshintDependencies, dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); });