Skip to content

Commit 0bc09ec

Browse files
authored
Merge pull request #2152 from NativeScript/pete/copy-node_modules
Copy node_modules to platform on prepare (fix Node6/npm 3.x bug)
2 parents 1ae7c85 + b569b6e commit 0bc09ec

File tree

5 files changed

+169
-138
lines changed

5 files changed

+169
-138
lines changed

lib/definitions/platform.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ interface INodeModulesBuilder {
5757
prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date): IFuture<void>;
5858
cleanNodeModules(absoluteOutputPath: string, platform: string): void;
5959
}
60+
61+
interface INodeModulesDependenciesBuilder {
62+
getProductionDependencies(projectPath: string): void;
63+
}

lib/tools/node-modules/node-modules-builder.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import * as fs from "fs";
33
import * as path from "path";
44
import * as shelljs from "shelljs";
55
import Future = require("fibers/future");
6-
import {NpmDependencyResolver, TnsModulesCopy, NpmPluginPrepare} from "./node-modules-dest-copy";
6+
import { TnsModulesCopy, NpmPluginPrepare } from "./node-modules-dest-copy";
7+
import { NodeModulesDependenciesBuilder } from "./node-modules-dependencies-builder";
78
import * as fiberBootstrap from "../../common/fiber-bootstrap";
8-
import {sleep} from "../../../lib/common/helpers";
9+
import { sleep } from "../../../lib/common/helpers";
910

1011
let glob = require("glob");
1112

1213
export class NodeModulesBuilder implements INodeModulesBuilder {
13-
constructor(
14-
private $fs: IFileSystem,
14+
constructor(private $fs: IFileSystem,
1515
private $projectData: IProjectData,
1616
private $projectDataService: IProjectDataService,
1717
private $injector: IInjector,
@@ -37,7 +37,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
3737
}, (er: Error, files: string[]) => {
3838
fiberBootstrap.run(() => {
3939

40-
while(this.$lockfile.check().wait()) {
40+
while (this.$lockfile.check().wait()) {
4141
sleep(10);
4242
}
4343

@@ -85,7 +85,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
8585
let intervalId = setInterval(() => {
8686
fiberBootstrap.run(() => {
8787
if (!this.$lockfile.check().wait() || future.isResolved()) {
88-
if(!future.isResolved()) {
88+
if (!future.isResolved()) {
8989
future.return();
9090
}
9191
clearInterval(intervalId);
@@ -133,27 +133,27 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
133133
// Force copying if the destination doesn't exist.
134134
lastModifiedTime = null;
135135
}
136-
let nodeModules = this.getChangedNodeModules(absoluteOutputPath, platform, lastModifiedTime).wait();
137136

138-
const resolver = new NpmDependencyResolver(this.$projectData.projectDir);
139-
const resolvedDependencies = resolver.resolveDependencies(_.keys(nodeModules), platform);
137+
let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {});
138+
let productionDependencies = dependenciesBuilder.getProductionDependencies(this.$projectData.projectDir);
140139

141140
if (!this.$options.bundle) {
142141
const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, {
143142
outputRoot: absoluteOutputPath
144143
});
145-
tnsModulesCopy.copyModules(resolvedDependencies, platform);
144+
tnsModulesCopy.copyModules(productionDependencies, platform);
146145
} else {
147146
this.cleanNodeModules(absoluteOutputPath, platform);
148147
}
149148

150149
const npmPluginPrepare = this.$injector.resolve(NpmPluginPrepare, {});
151-
npmPluginPrepare.preparePlugins(resolvedDependencies, platform);
150+
npmPluginPrepare.preparePlugins(productionDependencies, platform);
152151
}).future<void>()();
153152
}
154153

155154
public cleanNodeModules(absoluteOutputPath: string, platform: string): void {
156155
shelljs.rm("-rf", absoluteOutputPath);
157-
}
156+
}
158157
}
158+
159159
$injector.register("nodeModulesBuilder", NodeModulesBuilder);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as path from "path";
2+
import * as fs from "fs";
3+
4+
export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder {
5+
private projectPath: string;
6+
private rootNodeModulesPath: string;
7+
private resolvedDependencies: any[];
8+
private seen: any;
9+
10+
public constructor(private $fs: IFileSystem) {
11+
this.seen = {};
12+
this.resolvedDependencies = [];
13+
}
14+
15+
public getProductionDependencies(projectPath: string): any {
16+
this.projectPath = projectPath;
17+
this.rootNodeModulesPath = path.join(this.projectPath, "node_modules");
18+
19+
let projectPackageJsonpath = path.join(this.projectPath, "package.json");
20+
let packageJsonContent = this.$fs.readJson(projectPackageJsonpath).wait();
21+
22+
_.keys(packageJsonContent.dependencies).forEach(dependencyName => {
23+
let depth = 0;
24+
let directory = path.join(this.rootNodeModulesPath, dependencyName);
25+
26+
// find and traverse child with name `key`, parent's directory -> dep.directory
27+
this.traverseDependency(dependencyName, directory, depth);
28+
});
29+
30+
return this.resolvedDependencies;
31+
}
32+
33+
private traverseDependency(name: string, currentModulePath: string, depth: number): void {
34+
// Check if child has been extracted in the parent's node modules, AND THEN in `node_modules`
35+
// Slower, but prevents copying wrong versions if multiple of the same module are installed
36+
// Will also prevent copying project's devDependency's version if current module depends on another version
37+
let modulePath = path.join(currentModulePath, "node_modules", name); // node_modules/parent/node_modules/<package>
38+
let alternativeModulePath = path.join(this.rootNodeModulesPath, name);
39+
40+
this.findModule(modulePath, alternativeModulePath, name, depth);
41+
}
42+
43+
private findModule(modulePath: string, alternativeModulePath: string, name: string, depth: number) {
44+
let exists = this.moduleExists(modulePath);
45+
46+
if (exists) {
47+
if (this.seen[modulePath]) {
48+
return;
49+
}
50+
51+
let dependency = this.addDependency(name, modulePath, depth + 1);
52+
this.readModuleDependencies(modulePath, depth + 1, dependency);
53+
} else {
54+
modulePath = alternativeModulePath; // /node_modules/<package>
55+
exists = this.moduleExists(modulePath);
56+
57+
if (!exists) {
58+
return;
59+
}
60+
61+
if (this.seen[modulePath]) {
62+
return;
63+
}
64+
65+
let dependency = this.addDependency(name, modulePath, 0);
66+
this.readModuleDependencies(modulePath, 0, dependency);
67+
}
68+
69+
this.seen[modulePath] = true;
70+
}
71+
72+
private readModuleDependencies(modulePath: string, depth: number, currentModule: any): void {
73+
let packageJsonPath = path.join(modulePath, 'package.json');
74+
let packageJsonExists = fs.lstatSync(packageJsonPath).isFile();
75+
76+
if (packageJsonExists) {
77+
let packageJsonContents = this.$fs.readJson(packageJsonPath).wait();
78+
79+
if (!!packageJsonContents.nativescript) {
80+
// add `nativescript` property, necessary for resolving plugins
81+
currentModule.nativescript = packageJsonContents.nativescript;
82+
}
83+
84+
_.keys(packageJsonContents.dependencies).forEach((dependencyName) => {
85+
this.traverseDependency(dependencyName, modulePath, depth);
86+
});
87+
}
88+
}
89+
90+
private addDependency(name: string, directory: string, depth: number): any {
91+
let dependency: any = {
92+
name,
93+
directory,
94+
depth
95+
};
96+
97+
this.resolvedDependencies.push(dependency);
98+
99+
return dependency;
100+
}
101+
102+
private moduleExists(modulePath: string): boolean {
103+
try {
104+
let exists = fs.lstatSync(modulePath);
105+
return exists.isDirectory();
106+
} catch (e) {
107+
return false;
108+
}
109+
}
110+
}
111+
112+
$injector.register("nodeModulesDependenciesBuilder", NodeModulesDependenciesBuilder);

lib/tools/node-modules/node-modules-dest-copy.ts

+16-96
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import * as fs from "fs";
21
import * as path from "path";
3-
import * as semver from "semver";
42
import * as shelljs from "shelljs";
53
import * as constants from "../../constants";
64
import * as minimatch from "minimatch";
@@ -10,96 +8,17 @@ export interface ILocalDependencyData extends IDependencyData {
108
directory: string;
119
}
1210

13-
export class NpmDependencyResolver {
14-
constructor(
15-
private projectDir: string
16-
) {
17-
}
18-
19-
private getDevDependencies(projectDir: string): IDictionary<any> {
20-
let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME);
21-
let projectFileContent = require(projectFilePath);
22-
return projectFileContent.devDependencies || {};
23-
}
24-
25-
public resolveDependencies(changedDirectories: string[], platform: string): IDictionary<ILocalDependencyData> {
26-
const devDependencies = this.getDevDependencies(this.projectDir);
27-
const dependencies: IDictionary<ILocalDependencyData> = Object.create(null);
28-
29-
_.each(changedDirectories, changedDirectoryAbsolutePath => {
30-
if (!devDependencies[path.basename(changedDirectoryAbsolutePath)]) {
31-
let pathToPackageJson = path.join(changedDirectoryAbsolutePath, constants.PACKAGE_JSON_FILE_NAME);
32-
let packageJsonFiles = fs.existsSync(pathToPackageJson) ? [pathToPackageJson] : [];
33-
let nodeModulesFolderPath = path.join(changedDirectoryAbsolutePath, constants.NODE_MODULES_FOLDER_NAME);
34-
packageJsonFiles = packageJsonFiles.concat(this.enumeratePackageJsonFilesSync(nodeModulesFolderPath));
35-
36-
_.each(packageJsonFiles, packageJsonFilePath => {
37-
let fileContent = require(packageJsonFilePath);
38-
39-
if (!devDependencies[fileContent.name] && fileContent.name && fileContent.version) { // Don't flatten dev dependencies and flatten only dependencies with valid package.json
40-
let currentDependency: ILocalDependencyData = {
41-
name: fileContent.name,
42-
version: fileContent.version,
43-
directory: path.dirname(packageJsonFilePath),
44-
nativescript: fileContent.nativescript
45-
};
46-
47-
let addedDependency = dependencies[currentDependency.name];
48-
if (addedDependency) {
49-
if (semver.gt(currentDependency.version, addedDependency.version)) {
50-
let currentDependencyMajorVersion = semver.major(currentDependency.version);
51-
let addedDependencyMajorVersion = semver.major(addedDependency.version);
52-
53-
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}`;
54-
let logger = $injector.resolve("$logger");
55-
currentDependencyMajorVersion === addedDependencyMajorVersion ? logger.out(message) : logger.warn(message);
56-
57-
dependencies[currentDependency.name] = currentDependency;
58-
}
59-
} else {
60-
dependencies[currentDependency.name] = currentDependency;
61-
}
62-
}
63-
});
64-
}
65-
});
66-
return dependencies;
67-
}
68-
69-
private enumeratePackageJsonFilesSync(nodeModulesDirectoryPath: string, foundFiles?: string[]): string[] {
70-
foundFiles = foundFiles || [];
71-
if (fs.existsSync(nodeModulesDirectoryPath)) {
72-
let contents = fs.readdirSync(nodeModulesDirectoryPath);
73-
for (let i = 0; i < contents.length; ++i) {
74-
let moduleName = contents[i];
75-
let moduleDirectoryInNodeModules = path.join(nodeModulesDirectoryPath, moduleName);
76-
let packageJsonFilePath = path.join(moduleDirectoryInNodeModules, constants.PACKAGE_JSON_FILE_NAME);
77-
if (fs.existsSync(packageJsonFilePath)) {
78-
foundFiles.push(packageJsonFilePath);
79-
}
80-
81-
let directoryPath = path.join(moduleDirectoryInNodeModules, constants.NODE_MODULES_FOLDER_NAME);
82-
if (fs.existsSync(directoryPath)) {
83-
this.enumeratePackageJsonFilesSync(directoryPath, foundFiles);
84-
} else if (fs.statSync(moduleDirectoryInNodeModules).isDirectory()) {
85-
// Scoped modules (e.g. @angular) are grouped in a subfolder and we need to enumerate them too.
86-
this.enumeratePackageJsonFilesSync(moduleDirectoryInNodeModules, foundFiles);
87-
}
88-
}
89-
}
90-
return foundFiles;
91-
}
92-
}
93-
9411
export class TnsModulesCopy {
9512
constructor(
9613
private outputRoot: string,
9714
private $fs: IFileSystem
9815
) {
9916
}
10017

101-
public copyModules(dependencies: IDictionary<ILocalDependencyData>, platform: string): void {
102-
_.each(dependencies, dependency => {
18+
public copyModules(dependencies: any[], platform: string): void {
19+
for (let entry in dependencies) {
20+
let dependency = dependencies[entry];
21+
10322
this.copyDependencyDir(dependency);
10423

10524
if (dependency.name === constants.TNS_CORE_MODULES_NAME) {
@@ -110,22 +29,23 @@ export class TnsModulesCopy {
11029
let deleteFilesFutures = allFiles.filter(file => minimatch(file, "**/*.ts", { nocase: true })).map(file => this.$fs.deleteFile(file));
11130
Future.wait(deleteFilesFutures);
11231

113-
shelljs.cp("-Rf", path.join(tnsCoreModulesResourcePath, "*"), this.outputRoot);
114-
this.$fs.deleteDirectory(tnsCoreModulesResourcePath).wait();
32+
shelljs.rm("-rf", path.join(tnsCoreModulesResourcePath, "node_modules"));
11533
}
116-
});
34+
}
11735
}
11836

11937
private copyDependencyDir(dependency: any): void {
120-
let dependencyDir = path.dirname(dependency.name || "");
121-
let insideNpmScope = /^@/.test(dependencyDir);
122-
let targetDir = this.outputRoot;
123-
if (insideNpmScope) {
124-
targetDir = path.join(this.outputRoot, dependencyDir);
38+
if (dependency.depth === 0) {
39+
let isScoped = dependency.name.indexOf("@") === 0;
40+
let targetDir = this.outputRoot;
41+
42+
if (isScoped) {
43+
targetDir = path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/")));
44+
}
45+
46+
shelljs.mkdir("-p", targetDir);
47+
shelljs.cp("-Rf", dependency.directory, targetDir);
12548
}
126-
shelljs.mkdir("-p", targetDir);
127-
shelljs.cp("-Rf", dependency.directory, targetDir);
128-
shelljs.rm("-rf", path.join(targetDir, dependency.name, "node_modules"));
12949
}
13050
}
13151

0 commit comments

Comments
 (0)