diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 31daa127d6..95c558704c 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -56,3 +56,7 @@ interface INodeModulesBuilder { prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date): IFuture; cleanNodeModules(absoluteOutputPath: string, platform: string): void; } + +interface INodeModulesDependenciesBuilder { + getProductionDependencies(projectPath: string): void; +} \ No newline at end of file diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index 05840c9795..be0fcc564a 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -3,15 +3,15 @@ import * as fs from "fs"; import * as path from "path"; import * as shelljs from "shelljs"; import Future = require("fibers/future"); -import {NpmDependencyResolver, TnsModulesCopy, NpmPluginPrepare} from "./node-modules-dest-copy"; +import { TnsModulesCopy, NpmPluginPrepare } from "./node-modules-dest-copy"; +import { NodeModulesDependenciesBuilder } from "./node-modules-dependencies-builder"; import * as fiberBootstrap from "../../common/fiber-bootstrap"; -import {sleep} from "../../../lib/common/helpers"; +import { sleep } from "../../../lib/common/helpers"; let glob = require("glob"); export class NodeModulesBuilder implements INodeModulesBuilder { - constructor( - private $fs: IFileSystem, + constructor(private $fs: IFileSystem, private $projectData: IProjectData, private $projectDataService: IProjectDataService, private $injector: IInjector, @@ -37,7 +37,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder { }, (er: Error, files: string[]) => { fiberBootstrap.run(() => { - while(this.$lockfile.check().wait()) { + while (this.$lockfile.check().wait()) { sleep(10); } @@ -85,7 +85,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder { let intervalId = setInterval(() => { fiberBootstrap.run(() => { if (!this.$lockfile.check().wait() || future.isResolved()) { - if(!future.isResolved()) { + if (!future.isResolved()) { future.return(); } clearInterval(intervalId); @@ -133,27 +133,27 @@ export class NodeModulesBuilder implements INodeModulesBuilder { // Force copying if the destination doesn't exist. lastModifiedTime = null; } - let nodeModules = this.getChangedNodeModules(absoluteOutputPath, platform, lastModifiedTime).wait(); - const resolver = new NpmDependencyResolver(this.$projectData.projectDir); - const resolvedDependencies = resolver.resolveDependencies(_.keys(nodeModules), platform); + let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {}); + let productionDependencies = dependenciesBuilder.getProductionDependencies(this.$projectData.projectDir); if (!this.$options.bundle) { const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, { outputRoot: absoluteOutputPath }); - tnsModulesCopy.copyModules(resolvedDependencies, platform); + tnsModulesCopy.copyModules(productionDependencies, platform); } else { this.cleanNodeModules(absoluteOutputPath, platform); } const npmPluginPrepare = this.$injector.resolve(NpmPluginPrepare, {}); - npmPluginPrepare.preparePlugins(resolvedDependencies, platform); + npmPluginPrepare.preparePlugins(productionDependencies, platform); }).future()(); } public cleanNodeModules(absoluteOutputPath: string, platform: string): void { shelljs.rm("-rf", absoluteOutputPath); - } + } } + $injector.register("nodeModulesBuilder", NodeModulesBuilder); diff --git a/lib/tools/node-modules/node-modules-dependencies-builder.ts b/lib/tools/node-modules/node-modules-dependencies-builder.ts new file mode 100644 index 0000000000..0f0af7c5b9 --- /dev/null +++ b/lib/tools/node-modules/node-modules-dependencies-builder.ts @@ -0,0 +1,112 @@ +import * as path from "path"; +import * as fs from "fs"; + +export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder { + private projectPath: string; + private rootNodeModulesPath: string; + private resolvedDependencies: any[]; + private seen: any; + + public constructor(private $fs: IFileSystem) { + this.seen = {}; + this.resolvedDependencies = []; + } + + public getProductionDependencies(projectPath: string): any { + this.projectPath = projectPath; + this.rootNodeModulesPath = path.join(this.projectPath, "node_modules"); + + let projectPackageJsonpath = path.join(this.projectPath, "package.json"); + let packageJsonContent = this.$fs.readJson(projectPackageJsonpath).wait(); + + _.keys(packageJsonContent.dependencies).forEach(dependencyName => { + let depth = 0; + let directory = path.join(this.rootNodeModulesPath, dependencyName); + + // find and traverse child with name `key`, parent's directory -> dep.directory + this.traverseDependency(dependencyName, directory, depth); + }); + + return this.resolvedDependencies; + } + + private traverseDependency(name: string, currentModulePath: string, depth: number): void { + // Check if child has been extracted in the parent's node modules, AND THEN in `node_modules` + // Slower, but prevents copying wrong versions if multiple of the same module are installed + // Will also prevent copying project's devDependency's version if current module depends on another version + let modulePath = path.join(currentModulePath, "node_modules", name); // node_modules/parent/node_modules/ + let alternativeModulePath = path.join(this.rootNodeModulesPath, name); + + this.findModule(modulePath, alternativeModulePath, name, depth); + } + + private findModule(modulePath: string, alternativeModulePath: string, name: string, depth: number) { + let exists = this.moduleExists(modulePath); + + if (exists) { + if (this.seen[modulePath]) { + return; + } + + let dependency = this.addDependency(name, modulePath, depth + 1); + this.readModuleDependencies(modulePath, depth + 1, dependency); + } else { + modulePath = alternativeModulePath; // /node_modules/ + exists = this.moduleExists(modulePath); + + if (!exists) { + return; + } + + if (this.seen[modulePath]) { + return; + } + + let dependency = this.addDependency(name, modulePath, 0); + this.readModuleDependencies(modulePath, 0, dependency); + } + + this.seen[modulePath] = true; + } + + private readModuleDependencies(modulePath: string, depth: number, currentModule: any): void { + let packageJsonPath = path.join(modulePath, 'package.json'); + let packageJsonExists = fs.lstatSync(packageJsonPath).isFile(); + + if (packageJsonExists) { + let packageJsonContents = this.$fs.readJson(packageJsonPath).wait(); + + if (!!packageJsonContents.nativescript) { + // add `nativescript` property, necessary for resolving plugins + currentModule.nativescript = packageJsonContents.nativescript; + } + + _.keys(packageJsonContents.dependencies).forEach((dependencyName) => { + this.traverseDependency(dependencyName, modulePath, depth); + }); + } + } + + private addDependency(name: string, directory: string, depth: number): any { + let dependency: any = { + name, + directory, + depth + }; + + this.resolvedDependencies.push(dependency); + + return dependency; + } + + private moduleExists(modulePath: string): boolean { + try { + let exists = fs.lstatSync(modulePath); + return exists.isDirectory(); + } catch (e) { + return false; + } + } +} + +$injector.register("nodeModulesDependenciesBuilder", NodeModulesDependenciesBuilder); diff --git a/lib/tools/node-modules/node-modules-dest-copy.ts b/lib/tools/node-modules/node-modules-dest-copy.ts index 0b42395769..b6e2ec9615 100644 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ b/lib/tools/node-modules/node-modules-dest-copy.ts @@ -1,6 +1,4 @@ -import * as fs from "fs"; import * as path from "path"; -import * as semver from "semver"; import * as shelljs from "shelljs"; import * as constants from "../../constants"; import * as minimatch from "minimatch"; @@ -10,87 +8,6 @@ export interface ILocalDependencyData extends IDependencyData { directory: string; } -export class NpmDependencyResolver { - constructor( - private projectDir: string - ) { - } - - private getDevDependencies(projectDir: string): IDictionary { - let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); - let projectFileContent = require(projectFilePath); - return projectFileContent.devDependencies || {}; - } - - public resolveDependencies(changedDirectories: string[], platform: string): IDictionary { - const devDependencies = this.getDevDependencies(this.projectDir); - const dependencies: IDictionary = Object.create(null); - - _.each(changedDirectories, changedDirectoryAbsolutePath => { - if (!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 (!devDependencies[fileContent.name] && fileContent.name && fileContent.version) { // Don't flatten dev dependencies and flatten only dependencies with valid package.json - let currentDependency: ILocalDependencyData = { - name: fileContent.name, - version: fileContent.version, - directory: path.dirname(packageJsonFilePath), - nativescript: fileContent.nativescript - }; - - let addedDependency = 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 dependency 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); - - dependencies[currentDependency.name] = currentDependency; - } - } else { - dependencies[currentDependency.name] = currentDependency; - } - } - }); - } - }); - return dependencies; - } - - private enumeratePackageJsonFilesSync(nodeModulesDirectoryPath: string, foundFiles?: string[]): string[] { - foundFiles = foundFiles || []; - if (fs.existsSync(nodeModulesDirectoryPath)) { - let contents = fs.readdirSync(nodeModulesDirectoryPath); - for (let i = 0; i < contents.length; ++i) { - 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(moduleDirectoryInNodeModules, constants.NODE_MODULES_FOLDER_NAME); - if (fs.existsSync(directoryPath)) { - this.enumeratePackageJsonFilesSync(directoryPath, foundFiles); - } else if (fs.statSync(moduleDirectoryInNodeModules).isDirectory()) { - // Scoped modules (e.g. @angular) are grouped in a subfolder and we need to enumerate them too. - this.enumeratePackageJsonFilesSync(moduleDirectoryInNodeModules, foundFiles); - } - } - } - return foundFiles; - } -} - export class TnsModulesCopy { constructor( private outputRoot: string, @@ -98,8 +15,10 @@ export class TnsModulesCopy { ) { } - public copyModules(dependencies: IDictionary, platform: string): void { - _.each(dependencies, dependency => { + public copyModules(dependencies: any[], platform: string): void { + for (let entry in dependencies) { + let dependency = dependencies[entry]; + this.copyDependencyDir(dependency); if (dependency.name === constants.TNS_CORE_MODULES_NAME) { @@ -110,22 +29,23 @@ export class TnsModulesCopy { 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.rm("-rf", path.join(tnsCoreModulesResourcePath, "node_modules")); } - }); + } } private copyDependencyDir(dependency: any): void { - let dependencyDir = path.dirname(dependency.name || ""); - let insideNpmScope = /^@/.test(dependencyDir); - let targetDir = this.outputRoot; - if (insideNpmScope) { - targetDir = path.join(this.outputRoot, dependencyDir); + if (dependency.depth === 0) { + let isScoped = dependency.name.indexOf("@") === 0; + let targetDir = this.outputRoot; + + if (isScoped) { + targetDir = path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/"))); + } + + shelljs.mkdir("-p", targetDir); + shelljs.cp("-Rf", dependency.directory, targetDir); } - shelljs.mkdir("-p", targetDir); - shelljs.cp("-Rf", dependency.directory, targetDir); - shelljs.rm("-rf", path.join(targetDir, dependency.name, "node_modules")); } } diff --git a/test/npm-support.ts b/test/npm-support.ts index 5f6ab1eaa1..be69032469 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -16,20 +16,19 @@ import NodeModulesLib = require("../lib/tools/node-modules/node-modules-builder" import PluginsServiceLib = require("../lib/services/plugins-service"); import ChildProcessLib = require("../lib/common/child-process"); import ProjectFilesManagerLib = require("../lib/common/services/project-files-manager"); -import {DeviceAppDataFactory} from "../lib/common/mobile/device-app-data/device-app-data-factory"; -import {LocalToDevicePathDataFactory} from "../lib/common/mobile/local-to-device-path-data-factory"; -import {MobileHelper} from "../lib/common/mobile/mobile-helper"; -import {ProjectFilesProvider} from "../lib/providers/project-files-provider"; -import {DeviceAppDataProvider} from "../lib/providers/device-app-data-provider"; -import {MobilePlatformsCapabilities} from "../lib/mobile-platforms-capabilities"; -import {DevicePlatformsConstants} from "../lib/common/mobile/device-platforms-constants"; +import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/device-app-data-factory"; +import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; +import { MobileHelper } from "../lib/common/mobile/mobile-helper"; +import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; +import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; +import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; +import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; import { LockFile } from "../lib/lockfile"; import Future = require("fibers/future"); import path = require("path"); import temp = require("temp"); -import shelljs = require("shelljs"); temp.track(); let assert = require("chai").assert; @@ -191,6 +190,7 @@ describe("Npm support tests", () => { appDestinationFolderPath = projectSetup.appDestinationFolderPath; }); it("Ensures that the installed dependencies are prepared correctly", () => { + let fs: IFileSystem = testInjector.resolve("fs"); // Setup addDependencies(testInjector, projectFolder, { "bplist": "0.0.4" }).wait(); @@ -199,37 +199,28 @@ describe("Npm support tests", () => { // Assert let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let lodashFolderPath = path.join(tnsModulesFolderPath, "lodash"); - let bplistFolderPath = path.join(tnsModulesFolderPath, "bplist"); - let bplistCreatorFolderPath = path.join(tnsModulesFolderPath, "bplist-creator"); - let bplistParserFolderPath = path.join(tnsModulesFolderPath, "bplist-parser"); - let fs = testInjector.resolve("fs"); - assert.isTrue(fs.exists(lodashFolderPath).wait()); - assert.isTrue(fs.exists(bplistFolderPath).wait()); - assert.isTrue(fs.exists(bplistCreatorFolderPath).wait()); - assert.isTrue(fs.exists(bplistParserFolderPath).wait()); + let results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { + return true; + }, { enumerateDirectories: true }); + + assert.isTrue(results.filter((val) => _.endsWith(val, "lodash")).length === 1); + assert.isTrue(results.filter((val) => _.endsWith(val, path.join(tnsModulesFolderPath, "bplist"))).length === 1); + assert.isTrue(results.filter((val) => _.endsWith(val, "bplist-creator")).length === 1); + assert.isTrue(results.filter((val) => _.endsWith(val, "bplist-parser")).length === 1); }); it("Ensures that scoped dependencies are prepared correctly", () => { // Setup let fs = testInjector.resolve("fs"); let scopedName = "@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"; // Do not pass dependencies object as the sinopia cannot work with scoped dependencies. Instead move them manually. - addDependencies(testInjector, projectFolder, {}).wait(); - //create module dir, and add a package.json - shelljs.mkdir("-p", scopedModule); - fs.writeFile(scopedPackageJson, JSON.stringify({ name: scopedName, version: "1.0.0" })).wait(); - + addDependencies(testInjector, projectFolder, dependencies).wait(); // Act preparePlatform(testInjector).wait(); - // Assert let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let scopedDependencyPath = path.join(tnsModulesFolderPath, "@reactivex", "rxjs"); assert.isTrue(fs.exists(scopedDependencyPath).wait()); }); @@ -243,15 +234,19 @@ describe("Npm support tests", () => { 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 results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { + return true; + }, { enumerateDirectories: true }); - let scopedDependencyPath = path.join(tnsModulesFolderPath, "@scoped-plugin", "inner-plugin"); - assert.isTrue(fs.exists(scopedDependencyPath).wait()); + let filteredResults = results.filter((val) => { + return _.endsWith(val, path.join("@scoped-plugin", "inner-plugin")); + }); + + assert.isTrue(filteredResults.length === 1); }); it("Ensures that tns_modules absent when bundling", () => {