Skip to content

Commit 77f86a3

Browse files
committed
add npm-independent traversal to get project's production dependencies
copy production node_modules as-is to the project's tns_modules
1 parent fdec58f commit 77f86a3

File tree

2 files changed

+155
-124
lines changed

2 files changed

+155
-124
lines changed

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

+137-29
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ 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";
77
import * as fiberBootstrap from "../../common/fiber-bootstrap";
8-
import {sleep} from "../../../lib/common/helpers";
8+
import { sleep } from "../../../lib/common/helpers";
99

1010
let glob = require("glob");
1111

@@ -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,57 +133,165 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
133133
// Force copying if the destination doesn't exist.
134134
lastModifiedTime = null;
135135
}
136-
137-
//TODO: plamen5kov: WIP
138-
let depJsonStr = this.$childProcess.exec("npm list --prod --json").wait();
139-
let prodDependenciesJson = JSON.parse(depJsonStr);
140-
let productionDepArray = this.getProductionDependencyNames(prodDependenciesJson);
141136

137+
let productionDependencies = this.getProductionDependencies(this.$projectData.projectDir);
142138

143-
let nodeModules = this.getChangedNodeModules(absoluteOutputPath, platform, lastModifiedTime).wait();
139+
// console.log(productionDependencies);
144140

145-
const resolver = new NpmDependencyResolver(this.$projectData.projectDir);
146-
const resolvedDependencies = resolver.resolveDependencies(_.keys(nodeModules), platform);
141+
// TODO: Pip3r4o - result is not used currently
142+
// let nodeModules = this.getChangedNodeModules(absoluteOutputPath, platform, lastModifiedTime).wait();
147143

148144
if (!this.$options.bundle) {
149145
const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, {
150146
outputRoot: absoluteOutputPath
151147
});
152-
tnsModulesCopy.copyModules(resolvedDependencies, platform);
148+
tnsModulesCopy.copyModules(productionDependencies, platform);
153149
} else {
154150
this.cleanNodeModules(absoluteOutputPath, platform);
155151
}
156152

157153
const npmPluginPrepare = this.$injector.resolve(NpmPluginPrepare, {});
158-
npmPluginPrepare.preparePlugins(resolvedDependencies, platform);
154+
npmPluginPrepare.preparePlugins(productionDependencies, platform);
159155
}).future<void>()();
160156
}
161157

162-
private getProductionDependencyNames(inputJson: any): any {
163-
var finalDependencies:any = {};
164-
var queue:any = [];
158+
public getProductionDependencies(projectPath: string) {
159+
var deps: any = [];
160+
var seen: any = {};
165161

166-
inputJson.level = 0;
167-
queue.push(inputJson)
162+
var pJson = path.join(projectPath, "package.json");
163+
var nodeModulesDir = path.join(projectPath, "node_modules");
168164

169-
while(queue.length > 0) {
170-
var parent = queue.pop();
165+
var content = require(pJson);
171166

172-
if(parent.dependencies) {
173-
for(var dep in parent.dependencies) {
174-
var currentDependency = parent.dependencies[dep];
175-
currentDependency.level = parent.level + 1;
176-
queue.push(currentDependency);
177-
finalDependencies[dep] = currentDependency;
167+
Object.keys(content.dependencies).forEach((key) => {
168+
var depth = 0;
169+
var directory = path.join(nodeModulesDir, key);
170+
171+
// find and traverse child with name `key`, parent's directory -> dep.directory
172+
traverseChild(key, directory, depth);
173+
});
174+
175+
return filterUniqueDirectories(deps);
176+
177+
function traverseChild(name: string, currentModulePath: string, depth: number) {
178+
// check if key appears in a scoped module dependency
179+
var isScoped = name.indexOf('@') === 0;
180+
181+
if (!isScoped) {
182+
// Check if child has been extracted in the parent's node modules, AND THEN in `node_modules`
183+
// Slower, but prevents copying wrong versions if multiple of the same module are installed
184+
// Will also prevent copying project's devDependency's version if current module depends on another version
185+
var modulePath = path.join(currentModulePath, "node_modules", name); // /node_modules/parent/node_modules/<package>
186+
var exists = ensureModuleExists(modulePath);
187+
188+
if (exists) {
189+
var dep = addDependency(deps, name, modulePath, depth + 1);
190+
191+
traverseModule(modulePath, depth + 1, dep);
192+
} else {
193+
modulePath = path.join(nodeModulesDir, name); // /node_modules/<package>
194+
exists = ensureModuleExists(modulePath);
195+
196+
if(!exists) {
197+
return;
198+
}
199+
200+
var dep = addDependency(deps, name, modulePath, 0);
201+
202+
traverseModule(modulePath, 0, dep);
203+
}
204+
205+
}
206+
// module is scoped
207+
else {
208+
var scopeSeparatorIndex = name.indexOf('/');
209+
var scope = name.substring(0, scopeSeparatorIndex);
210+
var moduleName = name.substring(scopeSeparatorIndex + 1, name.length);
211+
var scopedModulePath = path.join(nodeModulesDir, scope, moduleName);
212+
213+
var exists = ensureModuleExists(scopedModulePath);
214+
215+
if (exists) {
216+
var dep = addDependency(deps, name, scopedModulePath, 0);
217+
traverseModule(scopedModulePath, depth, dep);
218+
}
219+
else {
220+
scopedModulePath = path.join(currentModulePath, "node_modules", scope, moduleName);
221+
222+
exists = ensureModuleExists(scopedModulePath);
223+
224+
if (!exists) {
225+
return;
226+
}
227+
228+
var dep = addDependency(deps, name, scopedModulePath, depth + 1);
229+
traverseModule(scopedModulePath, depth + 1, dep);
230+
}
231+
}
232+
233+
function traverseModule(modulePath: string, depth: number, currentDependency: any) {
234+
var packageJsonPath = path.join(modulePath, 'package.json');
235+
var packageJsonExists = fs.lstatSync(packageJsonPath).isFile();
236+
237+
if (packageJsonExists) {
238+
var packageJsonContents = require(packageJsonPath);
239+
240+
if (!!packageJsonContents.nativescript) {
241+
// add `nativescript` property, necessary for resolving plugins
242+
currentDependency.nativescript = packageJsonContents.nativescript;
243+
}
244+
245+
if (packageJsonContents.dependencies) {
246+
Object.keys(packageJsonContents.dependencies).forEach((key) => {
247+
248+
traverseChild(key, modulePath, depth);
249+
});
250+
}
251+
}
252+
}
253+
254+
function addDependency(deps: any[], name: string, directory: string, depth: number) {
255+
var dep: any = {};
256+
dep.name = name;
257+
dep.directory = directory;
258+
dep.depth = depth;
259+
260+
deps.push(dep);
261+
262+
return dep;
263+
}
264+
265+
function ensureModuleExists(modulePath: string): boolean {
266+
try {
267+
var exists = fs.lstatSync(modulePath);
268+
return exists.isDirectory();
269+
} catch (e) {
270+
return false;
178271
}
179272
}
180273
}
181274

182-
return finalDependencies;
275+
function filterUniqueDirectories(dependencies: any) {
276+
var unique: any = [];
277+
var distinct: any = [];
278+
for (var i in dependencies) {
279+
var dep = dependencies[i];
280+
if (distinct.indexOf(dep.directory) > -1) {
281+
continue;
282+
}
283+
284+
distinct.push(dep.directory);
285+
unique.push(dep);
286+
}
287+
288+
return unique;
289+
}
183290
}
184291

185292
public cleanNodeModules(absoluteOutputPath: string, platform: string): void {
186293
shelljs.rm("-rf", absoluteOutputPath);
187-
}
294+
}
188295
}
296+
189297
$injector.register("nodeModulesBuilder", NodeModulesBuilder);

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

+18-95
Original file line numberDiff line numberDiff line change
@@ -10,96 +10,17 @@ export interface ILocalDependencyData extends IDependencyData {
1010
directory: string;
1111
}
1212

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-
9413
export class TnsModulesCopy {
9514
constructor(
9615
private outputRoot: string,
9716
private $fs: IFileSystem
9817
) {
9918
}
10019

101-
public copyModules(dependencies: IDictionary<ILocalDependencyData>, platform: string): void {
102-
_.each(dependencies, dependency => {
20+
public copyModules(dependencies: any[], platform: string): void {
21+
for (var entry in dependencies) {
22+
var dependency = dependencies[entry];
23+
10324
this.copyDependencyDir(dependency);
10425

10526
if (dependency.name === constants.TNS_CORE_MODULES_NAME) {
@@ -110,22 +31,24 @@ export class TnsModulesCopy {
11031
let deleteFilesFutures = allFiles.filter(file => minimatch(file, "**/*.ts", { nocase: true })).map(file => this.$fs.deleteFile(file));
11132
Future.wait(deleteFilesFutures);
11233

113-
shelljs.cp("-Rf", path.join(tnsCoreModulesResourcePath, "*"), this.outputRoot);
114-
this.$fs.deleteDirectory(tnsCoreModulesResourcePath).wait();
34+
shelljs.rm("-rf", path.join(this.outputRoot, dependency.name, "node_modules"));
11535
}
116-
});
36+
}
11737
}
11838

119-
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);
39+
private copyDependencyDir(dependency: any) {
40+
if (dependency.depth === 0) {
41+
let isScoped = dependency.name.indexOf("@") === 0;
42+
let targetDir = this.outputRoot;
43+
44+
if (isScoped) {
45+
targetDir = path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/")));
46+
}
47+
48+
shelljs.mkdir("-p", targetDir);
49+
shelljs.cp("-Rf", dependency.directory, targetDir);
50+
// console.log("Copied dependency: " + dependency.name);
12551
}
126-
shelljs.mkdir("-p", targetDir);
127-
shelljs.cp("-Rf", dependency.directory, targetDir);
128-
shelljs.rm("-rf", path.join(targetDir, dependency.name, "node_modules"));
12952
}
13053
}
13154

0 commit comments

Comments
 (0)