Skip to content

Commit 9397e32

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

File tree

2 files changed

+182
-124
lines changed

2 files changed

+182
-124
lines changed

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

+164-29
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,38 @@ 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

12+
export class DependencyNode {
13+
public constructor() {
14+
this.productionChildren = [];
15+
this.name = '';
16+
this.isRoot = false;
17+
this.parent = null;
18+
this.path = '';
19+
}
20+
21+
public static create(name: string, version: string, parent: DependencyNode): DependencyNode {
22+
var n = new DependencyNode();
23+
n.name = name;
24+
n.version = version;
25+
n.parent = parent;
26+
27+
return n;
28+
}
29+
30+
public isRoot: boolean;
31+
public name: string;
32+
public version: string;
33+
public parent: DependencyNode;
34+
public productionChildren: DependencyNode[];
35+
public path: string;
36+
}
37+
1238
export class NodeModulesBuilder implements INodeModulesBuilder {
1339
constructor(private $childProcess: IChildProcess,
1440
private $fs: IFileSystem,
@@ -37,7 +63,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
3763
}, (er: Error, files: string[]) => {
3864
fiberBootstrap.run(() => {
3965

40-
while(this.$lockfile.check().wait()) {
66+
while (this.$lockfile.check().wait()) {
4167
sleep(10);
4268
}
4369

@@ -85,7 +111,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
85111
let intervalId = setInterval(() => {
86112
fiberBootstrap.run(() => {
87113
if (!this.$lockfile.check().wait() || future.isResolved()) {
88-
if(!future.isResolved()) {
114+
if (!future.isResolved()) {
89115
future.return();
90116
}
91117
clearInterval(intervalId);
@@ -133,57 +159,166 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
133159
// Force copying if the destination doesn't exist.
134160
lastModifiedTime = null;
135161
}
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);
141162

163+
let productionDependencies = this.getProductionDependencies(this.$projectData.projectDir);
142164

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

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

148170
if (!this.$options.bundle) {
149171
const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, {
150172
outputRoot: absoluteOutputPath
151173
});
152-
tnsModulesCopy.copyModules(resolvedDependencies, platform);
174+
tnsModulesCopy.copyModules(productionDependencies, platform);
153175
} else {
154176
this.cleanNodeModules(absoluteOutputPath, platform);
155177
}
156178

157179
const npmPluginPrepare = this.$injector.resolve(NpmPluginPrepare, {});
158-
npmPluginPrepare.preparePlugins(resolvedDependencies, platform);
180+
npmPluginPrepare.preparePlugins(productionDependencies, platform);
159181
}).future<void>()();
160182
}
161183

162-
private getProductionDependencyNames(inputJson: any): any {
163-
var finalDependencies:any = {};
164-
var queue:any = [];
184+
public getProductionDependencies(projectPath: string) {
185+
var prodDependencies: any = [];
186+
var deps: any = [];
187+
var seen: any = {};
188+
189+
var pJson = path.join(projectPath, "package.json");
190+
var nodeModulesDir = path.join(projectPath, "node_modules");
191+
192+
var content = require(pJson);
193+
194+
Object.keys(content.dependencies).forEach((key) => {
195+
var depth = 0;
196+
var directory = path.join(nodeModulesDir, key);
197+
198+
// find and traverse child with name `key`, parent's directory -> dep.directory
199+
traverseChild(key, directory, depth);
200+
});
201+
202+
return filterUniqueDirectories(deps);
165203

166-
inputJson.level = 0;
167-
queue.push(inputJson)
204+
function traverseChild(name: string, currentModulePath: string, depth: number) {
205+
// check if key appears in a scoped module dependency
206+
var isScoped = name.indexOf('@') === 0;
168207

169-
while(queue.length > 0) {
170-
var parent = queue.pop();
208+
if (!isScoped) {
209+
// Check if child has been extracted in the parent's node modules, AND THEN in `node_modules`
210+
// Slower, but prevents copying wrong versions if multiple of the same module are installed
211+
// Will also prevent copying project's devDependency's version if current module depends on another version
212+
var modulePath = path.join(currentModulePath, "node_modules", name); // /node_modules/parent/node_modules/<package>
213+
var exists = ensureModuleExists(modulePath);
171214

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;
215+
if (exists) {
216+
var dep = addDependency(deps, name, modulePath, depth + 1);
217+
218+
traverseModule(modulePath, depth + 1, dep);
219+
} else {
220+
modulePath = path.join(nodeModulesDir, name); // /node_modules/<package>
221+
exists = ensureModuleExists(modulePath);
222+
223+
if(!exists) {
224+
return;
225+
}
226+
227+
var dep = addDependency(deps, name, modulePath, 0);
228+
229+
traverseModule(modulePath, 0, dep);
230+
}
231+
232+
}
233+
// module is scoped
234+
else {
235+
var scopeSeparatorIndex = name.indexOf('/');
236+
var scope = name.substring(0, scopeSeparatorIndex);
237+
var moduleName = name.substring(scopeSeparatorIndex + 1, name.length);
238+
var scopedModulePath = path.join(nodeModulesDir, scope, moduleName);
239+
240+
var exists = ensureModuleExists(scopedModulePath);
241+
242+
if (exists) {
243+
var dep = addDependency(deps, name, scopedModulePath, 0);
244+
traverseModule(scopedModulePath, depth, dep);
245+
}
246+
else {
247+
scopedModulePath = path.join(currentModulePath, "node_modules", scope, moduleName);
248+
249+
exists = ensureModuleExists(scopedModulePath);
250+
251+
if (!exists) {
252+
return;
253+
}
254+
255+
var dep = addDependency(deps, name, scopedModulePath, depth + 1);
256+
traverseModule(scopedModulePath, depth + 1, dep);
257+
}
258+
}
259+
260+
function traverseModule(modulePath: string, depth: number, currentDependency: any) {
261+
var packageJsonPath = path.join(modulePath, 'package.json');
262+
var packageJsonExists = fs.lstatSync(packageJsonPath).isFile();
263+
264+
if (packageJsonExists) {
265+
var packageJsonContents = require(packageJsonPath);
266+
267+
if (!!packageJsonContents.nativescript) {
268+
// add `nativescript` property, necessary for resolving plugins
269+
currentDependency.nativescript = packageJsonContents.nativescript;
270+
}
271+
272+
if (packageJsonContents.dependencies) {
273+
Object.keys(packageJsonContents.dependencies).forEach((key) => {
274+
275+
traverseChild(key, modulePath, depth);
276+
});
277+
}
278+
}
279+
}
280+
281+
function addDependency(deps: any[], name: string, directory: string, depth: number) {
282+
var dep: any = {};
283+
dep.name = name;
284+
dep.directory = directory;
285+
dep.depth = depth;
286+
287+
deps.push(dep);
288+
289+
return dep;
290+
}
291+
292+
function ensureModuleExists(modulePath: string): boolean {
293+
try {
294+
var exists = fs.lstatSync(modulePath);
295+
return exists.isDirectory();
296+
} catch (e) {
297+
return false;
178298
}
179299
}
180300
}
181301

182-
return finalDependencies;
302+
function filterUniqueDirectories(dependencies: any) {
303+
var unique: any = [];
304+
var distinct: any = [];
305+
for (var i in dependencies) {
306+
var dep = dependencies[i];
307+
if (distinct.indexOf(dep.directory) > -1) {
308+
continue;
309+
}
310+
311+
distinct.push(dep.directory);
312+
unique.push(dep);
313+
}
314+
315+
return unique;
316+
}
183317
}
184318

185319
public cleanNodeModules(absoluteOutputPath: string, platform: string): void {
186320
shelljs.rm("-rf", absoluteOutputPath);
187-
}
321+
}
188322
}
323+
189324
$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)