Skip to content

Copy node_modules to platform on prepare (fix Node6/npm 3.x bug) #2152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ interface INodeModulesBuilder {
prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date): IFuture<void>;
cleanNodeModules(absoluteOutputPath: string, platform: string): void;
}

interface INodeModulesDependenciesBuilder {
getProductionDependencies(projectPath: string): void;
}
24 changes: 12 additions & 12 deletions lib/tools/node-modules/node-modules-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<void>()();
}

public cleanNodeModules(absoluteOutputPath: string, platform: string): void {
shelljs.rm("-rf", absoluteOutputPath);
}
}
}

$injector.register("nodeModulesBuilder", NodeModulesBuilder);
112 changes: 112 additions & 0 deletions lib/tools/node-modules/node-modules-dependencies-builder.ts
Original file line number Diff line number Diff line change
@@ -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/<package>
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/<package>
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);
112 changes: 16 additions & 96 deletions lib/tools/node-modules/node-modules-dest-copy.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,96 +8,17 @@ export interface ILocalDependencyData extends IDependencyData {
directory: string;
}

export class NpmDependencyResolver {
constructor(
private projectDir: string
) {
}

private getDevDependencies(projectDir: string): IDictionary<any> {
let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME);
let projectFileContent = require(projectFilePath);
return projectFileContent.devDependencies || {};
}

public resolveDependencies(changedDirectories: string[], platform: string): IDictionary<ILocalDependencyData> {
const devDependencies = this.getDevDependencies(this.projectDir);
const dependencies: IDictionary<ILocalDependencyData> = 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,
private $fs: IFileSystem
) {
}

public copyModules(dependencies: IDictionary<ILocalDependencyData>, 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) {
Expand All @@ -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"));
}
}

Expand Down
Loading