Skip to content

Commit dd8f866

Browse files
authoredMay 11, 2017
Fix getting production dependencies code (#2789)
* Fix getting production dependencies code CLI has logic to find which are the "production" dependencies, i.e. which should be copied from `node_modules` to `platforms` dir. However, when the project has a lot of dependencies (more than 15), on some machines the code leads to error: "Maximum callstack size exceeded". On other machines the code tooks significant time to execute. After investigation, it turned out the recursion inside `node-modules-dependencies-builder` is incorrect and it adds each package many times to the result array. Fix the recursion and change the class NodeModulesDependenciesBuilder to be stateless - instead of using properties in `this` object when calculating the production dependencies, the methods will persist the results through the passed args. This way the whole class can be safely added to `$injector` and used whenever we need the production dependencies. Each time the calculation is started from the beginning, which is the requirement for long living process, where the project may change. Fix the type of the result, which leads to fix in several other services, where the result has been expected as `IDictionary<smth>`. However it's never been dictionary, it's always been an array. The code, that expected dictionary has been working because the `_.values` method of lodash (used all over the places where the incorrect type of data has been expected), returns the same array when the passed argument is array. Fix the tests that incorrectly expected dictionary with keys "0", "1", "2", etc. Remove the usage of Node.js's `fs` module from `NodeModulesDependenciesBuilder` - replace it with `$fs` which allows easir writing of tests. Require the `nodeModulesDependenciesBuilder` in bootstrap, so it can be correctly resolved by `$injector`. Add unit tests for `nodeModulesDependenciesBuilder`. * Use breadth-first search for getting production dependencies Replace the recursion with breadth-first search algorithm in order to make the code easier for understanding and debugging. Fix some incorrect code in the tests. * Add checks before adding new elements to queue of production dependencies Add check before adding new elements to queue of production dependencies - do not add elements, which are already added and do not read package.json of elements that are already added to "resolvedModules".
1 parent 2124b88 commit dd8f866

15 files changed

+477
-125
lines changed
 

‎lib/bootstrap.ts

+2
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,5 @@ $injector.requireCommand("extension|*list", "./commands/extensibility/list-exten
135135
$injector.requireCommand("extension|install", "./commands/extensibility/install-extension");
136136
$injector.requireCommand("extension|uninstall", "./commands/extensibility/uninstall-extension");
137137
$injector.requirePublic("extensibilityService", "./services/extensibility-service");
138+
139+
$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder");

‎lib/common

‎lib/declarations.d.ts

+28-4
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,36 @@ interface INpmInstallOptions {
202202
dependencyType?: string;
203203
}
204204

205+
/**
206+
* Describes npm package installed in node_modules.
207+
*/
205208
interface IDependencyData {
209+
/**
210+
* The name of the package.
211+
*/
206212
name: string;
207-
version: string;
208-
nativescript: any;
209-
dependencies?: IStringDictionary;
210-
devDependencies?: IStringDictionary;
213+
214+
/**
215+
* The full path where the package is installed.
216+
*/
217+
directory: string;
218+
219+
/**
220+
* The depth inside node_modules dir, where the package is located.
221+
* The <project_dir>/node_modules/ is level 0.
222+
* Level 1 is <project dir>/node_modules/<package name>/node_modules, etc.
223+
*/
224+
depth: number;
225+
226+
/**
227+
* Describes the `nativescript` key in package.json of a dependency.
228+
*/
229+
nativescript?: any;
230+
231+
/**
232+
* Dependencies of the current module.
233+
*/
234+
dependencies?: string[];
211235
}
212236

213237
interface IStaticConfig extends Config.IStaticConfig { }

‎lib/definitions/platform.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ interface INodeModulesBuilder {
275275
}
276276

277277
interface INodeModulesDependenciesBuilder {
278-
getProductionDependencies(projectPath: string): void;
278+
getProductionDependencies(projectPath: string): IDependencyData[];
279279
}
280280

281281
interface IBuildInfo {

‎lib/definitions/project.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter {
225225
removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise<void>;
226226

227227
afterPrepareAllPlugins(projectData: IProjectData): Promise<void>;
228-
beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDictionary<IDependencyData>): Promise<void>;
228+
beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise<void>;
229229

230230
/**
231231
* Gets the path wheren App_Resources should be copied.

‎lib/services/android-project-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
400400
return;
401401
}
402402

403-
public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDictionary<IDependencyData>): Promise<void> {
403+
public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise<void> {
404404
if (!this.$config.debugLivesync) {
405405
if (dependencies) {
406406
let platformDir = path.join(projectData.platformsDir, "android");

‎lib/services/livesync/livesync-service.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as constants from "../../constants";
22
import * as helpers from "../../common/helpers";
33
import * as path from "path";
4-
import { NodeModulesDependenciesBuilder } from "../../tools/node-modules/node-modules-dependencies-builder";
54

65
let choki = require("chokidar");
76

@@ -17,7 +16,8 @@ class LiveSyncService implements ILiveSyncService {
1716
private $logger: ILogger,
1817
private $dispatcher: IFutureDispatcher,
1918
private $hooksService: IHooksService,
20-
private $processService: IProcessService) { }
19+
private $processService: IProcessService,
20+
private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder) { }
2121

2222
public get isInitialized(): boolean { // This function is used from https://github.com/NativeScript/nativescript-dev-typescript/blob/master/lib/before-prepare.js#L4
2323
return this._isInitialized;
@@ -94,8 +94,7 @@ class LiveSyncService implements ILiveSyncService {
9494

9595
private partialSync(syncWorkingDirectory: string, onChangedActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise<void>)[], projectData: IProjectData): void {
9696
let that = this;
97-
let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {});
98-
let productionDependencies = dependenciesBuilder.getProductionDependencies(projectData.projectDir);
97+
let productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);
9998
let pattern = ["app"];
10099

101100
if (this.$options.syncAllFiles) {

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as shelljs from "shelljs";
22
import { TnsModulesCopy, NpmPluginPrepare } from "./node-modules-dest-copy";
3-
import { NodeModulesDependenciesBuilder } from "./node-modules-dependencies-builder";
43

54
export class NodeModulesBuilder implements INodeModulesBuilder {
65
constructor(private $fs: IFileSystem,
76
private $injector: IInjector,
8-
private $options: IOptions
7+
private $options: IOptions,
8+
private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder
99
) { }
1010

1111
public async prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise<void> {
@@ -14,8 +14,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder {
1414
lastModifiedTime = null;
1515
}
1616

17-
let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {});
18-
let productionDependencies = dependenciesBuilder.getProductionDependencies(projectData.projectDir);
17+
let productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);
1918

2019
if (!this.$options.bundle) {
2120
const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, {

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

+77-78
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,110 @@
11
import * as path from "path";
2-
import * as fs from "fs";
2+
import { NODE_MODULES_FOLDER_NAME, PACKAGE_JSON_FILE_NAME } from "../../constants";
33

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);
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-
}
4+
interface IDependencyDescription {
5+
parentDir: string;
6+
name: string;
7+
depth: number;
8+
}
329

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);
10+
export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder {
11+
public constructor(private $fs: IFileSystem) { }
12+
13+
public getProductionDependencies(projectPath: string): IDependencyData[] {
14+
const rootNodeModulesPath = path.join(projectPath, NODE_MODULES_FOLDER_NAME);
15+
const projectPackageJsonPath = path.join(projectPath, PACKAGE_JSON_FILE_NAME);
16+
const packageJsonContent = this.$fs.readJson(projectPackageJsonPath);
17+
const dependencies = packageJsonContent && packageJsonContent.dependencies;
18+
19+
let resolvedDependencies: IDependencyData[] = [];
20+
21+
let queue: IDependencyDescription[] = _.keys(dependencies)
22+
.map(dependencyName => ({
23+
parentDir: projectPath,
24+
name: dependencyName,
25+
depth: 0
26+
}));
27+
28+
while (queue.length) {
29+
const currentModule = queue.shift();
30+
const resolvedDependency = this.findModule(rootNodeModulesPath, currentModule.parentDir, currentModule.name, currentModule.depth, resolvedDependencies);
31+
32+
if (resolvedDependency && !_.some(resolvedDependencies, r => r.directory === resolvedDependency.directory)) {
33+
_.each(resolvedDependency.dependencies, d => {
34+
const dependency: IDependencyDescription = { name: d, parentDir: resolvedDependency.directory, depth: resolvedDependency.depth + 1 };
35+
36+
const shouldAdd = !_.some(queue, element =>
37+
element.name === dependency.name &&
38+
element.parentDir === dependency.parentDir &&
39+
element.depth === dependency.depth);
40+
41+
if (shouldAdd) {
42+
queue.push(dependency);
43+
}
44+
});
45+
46+
resolvedDependencies.push(resolvedDependency);
47+
}
48+
}
3949

40-
this.findModule(modulePath, alternativeModulePath, name, depth);
50+
return resolvedDependencies;
4151
}
4252

43-
private findModule(modulePath: string, alternativeModulePath: string, name: string, depth: number) {
44-
let exists = this.moduleExists(modulePath);
53+
private findModule(rootNodeModulesPath: string, parentModulePath: string, name: string, depth: number, resolvedDependencies: IDependencyData[]): IDependencyData {
54+
let modulePath = path.join(parentModulePath, NODE_MODULES_FOLDER_NAME, name); // node_modules/parent/node_modules/<package>
55+
const rootModulesPath = path.join(rootNodeModulesPath, name);
56+
let depthInNodeModules = depth;
4557

46-
if (exists) {
47-
if (this.seen[modulePath]) {
48-
return;
58+
if (!this.moduleExists(modulePath)) {
59+
modulePath = rootModulesPath; // /node_modules/<package>
60+
if (!this.moduleExists(modulePath)) {
61+
return null;
4962
}
5063

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);
64+
depthInNodeModules = 0;
65+
}
5666

57-
if (!exists) {
58-
return;
59-
}
67+
if (_.some(resolvedDependencies, r => r.name === name && r.directory === modulePath)) {
68+
return null;
6069

61-
if (this.seen[modulePath]) {
62-
return;
63-
}
64-
65-
let dependency = this.addDependency(name, modulePath, 0);
66-
this.readModuleDependencies(modulePath, 0, dependency);
6770
}
6871

69-
this.seen[modulePath] = true;
72+
return this.getDependencyData(name, modulePath, depthInNodeModules);
7073
}
7174

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+
private getDependencyData(name: string, directory: string, depth: number): IDependencyData {
76+
const dependency: IDependencyData = {
77+
name,
78+
directory,
79+
depth
80+
};
81+
82+
const packageJsonPath = path.join(directory, PACKAGE_JSON_FILE_NAME);
83+
const packageJsonExists = this.$fs.getLsStats(packageJsonPath).isFile();
7584

7685
if (packageJsonExists) {
7786
let packageJsonContents = this.$fs.readJson(packageJsonPath);
7887

7988
if (!!packageJsonContents.nativescript) {
8089
// add `nativescript` property, necessary for resolving plugins
81-
currentModule.nativescript = packageJsonContents.nativescript;
90+
dependency.nativescript = packageJsonContents.nativescript;
8291
}
8392

84-
_.keys(packageJsonContents.dependencies).forEach((dependencyName) => {
85-
this.traverseDependency(dependencyName, modulePath, depth);
86-
});
93+
dependency.dependencies = _.keys(packageJsonContents.dependencies);
94+
return dependency;
8795
}
88-
}
8996

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;
97+
return null;
10098
}
10199

102100
private moduleExists(modulePath: string): boolean {
103101
try {
104-
let exists = fs.lstatSync(modulePath);
105-
if (exists.isSymbolicLink()) {
106-
exists = fs.lstatSync(fs.realpathSync(modulePath));
102+
let modulePathLsStat = this.$fs.getLsStats(modulePath);
103+
if (modulePathLsStat.isSymbolicLink()) {
104+
modulePathLsStat = this.$fs.getLsStats(this.$fs.realpath(modulePath));
107105
}
108-
return exists.isDirectory();
106+
107+
return modulePathLsStat.isDirectory();
109108
} catch (e) {
110109
return false;
111110
}

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

+9-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class TnsModulesCopy {
1515
) {
1616
}
1717

18-
public copyModules(dependencies: any[], platform: string): void {
18+
public copyModules(dependencies: IDependencyData[], platform: string): void {
1919
for (let entry in dependencies) {
2020
let dependency = dependencies[entry];
2121

@@ -34,7 +34,7 @@ export class TnsModulesCopy {
3434
}
3535
}
3636

37-
private copyDependencyDir(dependency: any): void {
37+
private copyDependencyDir(dependency: IDependencyData): void {
3838
if (dependency.depth === 0) {
3939
let isScoped = dependency.name.indexOf("@") === 0;
4040
let targetDir = this.outputRoot;
@@ -61,18 +61,18 @@ export class NpmPluginPrepare {
6161
) {
6262
}
6363

64-
protected async beforePrepare(dependencies: IDictionary<IDependencyData>, platform: string, projectData: IProjectData): Promise<void> {
64+
protected async beforePrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise<void> {
6565
await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.beforePrepareAllPlugins(projectData, dependencies);
6666
}
6767

68-
protected async afterPrepare(dependencies: IDictionary<IDependencyData>, platform: string, projectData: IProjectData): Promise<void> {
68+
protected async afterPrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise<void> {
6969
await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.afterPrepareAllPlugins(projectData);
7070
this.writePreparedDependencyInfo(dependencies, platform, projectData);
7171
}
7272

73-
private writePreparedDependencyInfo(dependencies: IDictionary<IDependencyData>, platform: string, projectData: IProjectData): void {
73+
private writePreparedDependencyInfo(dependencies: IDependencyData[], platform: string, projectData: IProjectData): void {
7474
let prepareData: IDictionary<boolean> = {};
75-
_.values(dependencies).forEach(d => {
75+
_.each(dependencies, d => {
7676
prepareData[d.name] = true;
7777
});
7878
this.$fs.createDirectory(this.preparedPlatformsDir(platform, projectData));
@@ -101,18 +101,18 @@ export class NpmPluginPrepare {
101101
return this.$fs.readJson(this.preparedPlatformsFile(platform, projectData), "utf8");
102102
}
103103

104-
private allPrepared(dependencies: IDictionary<IDependencyData>, platform: string, projectData: IProjectData): boolean {
104+
private allPrepared(dependencies: IDependencyData[], platform: string, projectData: IProjectData): boolean {
105105
let result = true;
106106
const previouslyPrepared = this.getPreviouslyPreparedDependencies(platform, projectData);
107-
_.values(dependencies).forEach(d => {
107+
_.each(dependencies, d => {
108108
if (!previouslyPrepared[d.name]) {
109109
result = false;
110110
}
111111
});
112112
return result;
113113
}
114114

115-
public async preparePlugins(dependencies: IDictionary<IDependencyData>, platform: string, projectData: IProjectData): Promise<void> {
115+
public async preparePlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise<void> {
116116
if (_.isEmpty(dependencies) || this.allPrepared(dependencies, platform, projectData)) {
117117
return;
118118
}

‎test/npm-support.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { XmlValidator } from "../lib/xml-validator";
2727
import { LockFile } from "../lib/lockfile";
2828
import ProjectChangesLib = require("../lib/services/project-changes-service");
2929
import { Messages } from "../lib/common/messages/messages";
30+
import { NodeModulesDependenciesBuilder } from "../lib/tools/node-modules/node-modules-dependencies-builder";
3031

3132
import path = require("path");
3233
import temp = require("temp");
@@ -81,9 +82,10 @@ function createTestInjector(): IInjector {
8182
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
8283
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
8384
testInjector.register("analyticsService", {
84-
track: async () => undefined
85+
track: async (): Promise<any> => undefined
8586
});
8687
testInjector.register("messages", Messages);
88+
testInjector.register("nodeModulesDependenciesBuilder", NodeModulesDependenciesBuilder);
8789

8890
return testInjector;
8991
}

‎test/plugin-prepare.ts

+17-14
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,54 @@ class TestNpmPluginPrepare extends NpmPluginPrepare {
1414
return this.previouslyPrepared;
1515
}
1616

17-
protected async beforePrepare(dependencies: IDictionary<IDependencyData>, platform: string): Promise<void> {
18-
_.values(dependencies).forEach(d => {
17+
protected async beforePrepare(dependencies: IDependencyData[], platform: string): Promise<void> {
18+
_.each(dependencies, d => {
1919
this.preparedDependencies[d.name] = true;
2020
});
2121
}
2222

23-
protected async afterPrepare(dependencies: IDictionary<IDependencyData>, platform: string): Promise<void> {
23+
protected async afterPrepare(dependencies: IDependencyData[], platform: string): Promise<void> {
2424
// DO NOTHING
2525
}
2626
}
2727

2828
describe("Plugin preparation", () => {
2929
it("skips prepare if no plugins", async () => {
3030
const pluginPrepare = new TestNpmPluginPrepare({});
31-
await pluginPrepare.preparePlugins({}, "android", null);
31+
await pluginPrepare.preparePlugins([], "android", null);
3232
assert.deepEqual({}, pluginPrepare.preparedDependencies);
3333
});
3434

3535
it("skips prepare if every plugin prepared", async () => {
3636
const pluginPrepare = new TestNpmPluginPrepare({ "tns-core-modules-widgets": true });
37-
const testDependencies: IDictionary<IDependencyData> = {
38-
"0": {
37+
const testDependencies: IDependencyData[] = [
38+
{
3939
name: "tns-core-modules-widgets",
40-
version: "1.0.0",
40+
depth: 0,
41+
directory: "some dir",
4142
nativescript: null,
4243
}
43-
};
44+
];
4445
await pluginPrepare.preparePlugins(testDependencies, "android", null);
4546
assert.deepEqual({}, pluginPrepare.preparedDependencies);
4647
});
4748

4849
it("saves prepared plugins after preparation", async () => {
4950
const pluginPrepare = new TestNpmPluginPrepare({ "tns-core-modules-widgets": true });
50-
const testDependencies: IDictionary<IDependencyData> = {
51-
"0": {
51+
const testDependencies: IDependencyData[] = [
52+
{
5253
name: "tns-core-modules-widgets",
53-
version: "1.0.0",
54+
depth: 0,
55+
directory: "some dir",
5456
nativescript: null,
5557
},
56-
"1": {
58+
{
5759
name: "nativescript-calendar",
58-
version: "1.0.0",
60+
depth: 0,
61+
directory: "some dir",
5962
nativescript: null,
6063
}
61-
};
64+
];
6265
await pluginPrepare.preparePlugins(testDependencies, "android", null);
6366
const prepareData = { "tns-core-modules-widgets": true, "nativescript-calendar": true };
6467
assert.deepEqual(prepareData, pluginPrepare.preparedDependencies);

‎test/plugins-service.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,15 @@ describe("Plugins service", () => {
477477
let pluginName = "mySamplePlugin";
478478
let projectFolder = createProjectFile(testInjector);
479479
let pluginFolderPath = path.join(projectFolder, pluginName);
480-
let pluginJsonData = {
481-
"name": pluginName,
482-
"version": "0.0.1",
483-
"nativescript": {
484-
"platforms": {
485-
"android": "0.10.0"
480+
let pluginJsonData: IDependencyData = {
481+
name: pluginName,
482+
nativescript: {
483+
platforms: {
484+
android: "0.10.0"
486485
}
487-
}
486+
},
487+
depth: 0,
488+
directory: "some dir"
488489
};
489490
let fs = testInjector.resolve("fs");
490491
fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData);

‎test/stubs.ts

+4
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ export class FileSystemStub implements IFileSystem {
171171
deleteEmptyParents(directory: string): void { }
172172

173173
utimes(path: string, atime: Date, mtime: Date): void { }
174+
175+
realpath(filePath: string): string {
176+
return null;
177+
}
174178
}
175179

176180
export class ErrorsStub implements IErrors {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { Yok } from "../../../lib/common/yok";
2+
import { assert } from "chai";
3+
import { NodeModulesDependenciesBuilder } from "../../../lib/tools/node-modules/node-modules-dependencies-builder";
4+
import * as path from "path";
5+
import * as constants from "../../../lib/constants";
6+
7+
interface IDependencyInfo {
8+
name: string;
9+
version: string;
10+
depth: number;
11+
dependencies?: IDependencyInfo[];
12+
nativescript?: any;
13+
};
14+
15+
// TODO: Add integration tests.
16+
// The tests assumes npm 3 or later is used, so all dependencies (and their dependencies) will be installed at the root node_modules
17+
describe("nodeModulesDependenciesBuilder", () => {
18+
const pathToProject = "some path";
19+
const getTestInjector = (): IInjector => {
20+
const testInjector = new Yok();
21+
testInjector.register("fs", {
22+
readJson: (pathToFile: string): any => undefined
23+
});
24+
25+
return testInjector;
26+
};
27+
28+
describe("getProductionDependencies", () => {
29+
describe("returns empty array", () => {
30+
const validateResultIsEmpty = async (resultOfReadJson: any) => {
31+
const testInjector = getTestInjector();
32+
const fs = testInjector.resolve<IFileSystem>("fs");
33+
fs.readJson = (filename: string, encoding?: string): any => {
34+
return resultOfReadJson;
35+
};
36+
37+
const nodeModulesDependenciesBuilder = testInjector.resolve<INodeModulesDependenciesBuilder>(NodeModulesDependenciesBuilder);
38+
const result = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject);
39+
40+
assert.deepEqual(result, []);
41+
};
42+
43+
it("when package.json does not have any data", async () => {
44+
await validateResultIsEmpty(null);
45+
});
46+
47+
it("when package.json does not have dependencies section", async () => {
48+
await validateResultIsEmpty({ name: "some name", devDependencies: { a: "1.0.0" } });
49+
});
50+
});
51+
52+
describe("returns correct dependencies", () => {
53+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
54+
* Helper functions for easier writing of consecutive tests in the suite. *
55+
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
56+
57+
const getPathToDependencyInNodeModules = (dependencyName: string, parentDir?: string): string => {
58+
return path.join(parentDir || pathToProject, constants.NODE_MODULES_FOLDER_NAME, dependencyName);
59+
};
60+
61+
const getNodeModuleInfoForExpecteDependency = (name: string, depth: number, nativescript?: any, dependencies?: string[]): IDependencyData => {
62+
let result: IDependencyData = {
63+
name: path.basename(name),
64+
directory: getPathToDependencyInNodeModules(name),
65+
depth,
66+
dependencies: dependencies || []
67+
};
68+
69+
if (nativescript) {
70+
result.nativescript = nativescript;
71+
}
72+
73+
return result;
74+
};
75+
76+
const getPathToPackageJsonOfDependency = (dependencyName: string, parentDir?: string): string => {
77+
return path.join(getPathToDependencyInNodeModules(dependencyName, parentDir), constants.PACKAGE_JSON_FILE_NAME);
78+
};
79+
80+
const getDependenciesObjectFromDependencyInfo = (depInfos: IDependencyInfo[], nativescript: any): { dependencies: any, nativescript?: any } => {
81+
const dependencies: any = {};
82+
_.each(depInfos, innerDependency => {
83+
dependencies[innerDependency.name] = innerDependency.version;
84+
});
85+
86+
let result: any = {
87+
dependencies
88+
};
89+
90+
if (nativescript) {
91+
result.nativescript = nativescript;
92+
}
93+
94+
return result;
95+
};
96+
97+
const getDependenciesObject = (filename: string, deps: IDependencyInfo[], parentDir: string): { dependencies: any } => {
98+
let result: { dependencies: any } = null;
99+
for (let dependencyInfo of deps) {
100+
const pathToPackageJson = getPathToPackageJsonOfDependency(dependencyInfo.name, parentDir);
101+
if (filename === pathToPackageJson) {
102+
return getDependenciesObjectFromDependencyInfo(dependencyInfo.dependencies, dependencyInfo.nativescript);
103+
}
104+
105+
if (dependencyInfo.dependencies) {
106+
result = getDependenciesObject(filename, dependencyInfo.dependencies, path.join(parentDir, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name));
107+
if (result) {
108+
break;
109+
}
110+
}
111+
}
112+
113+
return result;
114+
};
115+
116+
const generateTest = (rootDeps: IDependencyInfo[]): INodeModulesDependenciesBuilder => {
117+
const testInjector = getTestInjector();
118+
const nodeModulesDependenciesBuilder = testInjector.resolve<INodeModulesDependenciesBuilder>(NodeModulesDependenciesBuilder);
119+
const fs = testInjector.resolve<IFileSystem>("fs");
120+
121+
fs.readJson = (filename: string, encoding?: string): any => {
122+
const innerDependency = getDependenciesObject(filename, rootDeps, pathToProject);
123+
return innerDependency || getDependenciesObjectFromDependencyInfo(rootDeps, null);
124+
};
125+
126+
const isDirectory = (searchedPath: string, currentRootPath: string, deps: IDependencyInfo[], currentDepthLevel: number): boolean => {
127+
let result = false;
128+
129+
for (let dependencyInfo of deps) {
130+
const pathToDependency = path.join(currentRootPath, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name);
131+
132+
if (pathToDependency === searchedPath && currentDepthLevel === dependencyInfo.depth) {
133+
return true;
134+
}
135+
136+
if (dependencyInfo.dependencies) {
137+
result = isDirectory(searchedPath, pathToDependency, dependencyInfo.dependencies, currentDepthLevel + 1);
138+
if (result) {
139+
break;
140+
}
141+
}
142+
}
143+
144+
return result;
145+
};
146+
147+
const isPackageJsonOfDependency = (searchedPath: string, currentRootPath: string, deps: IDependencyInfo[], currentDepthLevel: number): boolean => {
148+
let result = false;
149+
for (let dependencyInfo of deps) {
150+
const pathToDependency = path.join(currentRootPath, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name);
151+
152+
const pathToPackageJson = path.join(pathToDependency, constants.PACKAGE_JSON_FILE_NAME);
153+
154+
if (pathToPackageJson === searchedPath && currentDepthLevel === dependencyInfo.depth) {
155+
return true;
156+
}
157+
158+
if (dependencyInfo.dependencies) {
159+
result = isPackageJsonOfDependency(searchedPath, pathToDependency, dependencyInfo.dependencies, currentDepthLevel + 1);
160+
if (result) {
161+
break;
162+
}
163+
}
164+
}
165+
166+
return result;
167+
};
168+
169+
fs.getLsStats = (pathToStat: string): any => {
170+
return {
171+
isDirectory: (): boolean => isDirectory(pathToStat, pathToProject, rootDeps, 0),
172+
isSymbolicLink: (): boolean => false,
173+
isFile: (): boolean => isPackageJsonOfDependency(pathToStat, pathToProject, rootDeps, 0)
174+
};
175+
};
176+
177+
return nodeModulesDependenciesBuilder;
178+
};
179+
180+
const generateDependency = (name: string, version: string, depth: number, dependencies: IDependencyInfo[], nativescript?: any): IDependencyInfo => {
181+
return {
182+
name,
183+
version,
184+
depth,
185+
dependencies,
186+
nativescript
187+
};
188+
};
189+
190+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
191+
* END of helper functions for easier writing of consecutive tests in the suite. *
192+
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
193+
194+
const firstPackage = "firstPackage";
195+
const secondPackage = "secondPackage";
196+
const thirdPackage = "thirdPackage";
197+
198+
it("when all dependencies are installed at the root level of the project", async () => {
199+
// The test validates the following dependency tree, when npm 3+ is used.
200+
// <project dir>
201+
// ├── firstPackage@1.0.0
202+
// ├── secondPackage@1.1.0
203+
// └── thirdPackage@1.2.0
204+
205+
const rootDeps: IDependencyInfo[] = [
206+
generateDependency(firstPackage, "1.0.0", 0, null),
207+
generateDependency(secondPackage, "1.1.0", 0, null),
208+
generateDependency(thirdPackage, "1.2.0", 0, null)
209+
];
210+
211+
const nodeModulesDependenciesBuilder = generateTest(rootDeps);
212+
const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject);
213+
214+
const expectedResult: IDependencyData[] = [
215+
getNodeModuleInfoForExpecteDependency(firstPackage, 0),
216+
getNodeModuleInfoForExpecteDependency(secondPackage, 0),
217+
getNodeModuleInfoForExpecteDependency(thirdPackage, 0)
218+
];
219+
220+
assert.deepEqual(actualResult, expectedResult);
221+
});
222+
223+
it("when the project has a dependency to a package and one of the other packages has dependency to other version of this package", async () => {
224+
// The test validates the following dependency tree, when npm 3+ is used.
225+
// <project dir>
226+
// ├─┬ firstPackage@1.0.0
227+
// │ └── secondPackage@1.2.0
228+
// └── secondPackage@1.1.0
229+
230+
const rootDeps: IDependencyInfo[] = [
231+
generateDependency(firstPackage, "1.0.0", 0, [generateDependency(secondPackage, "1.2.0", 1, null)]),
232+
generateDependency(secondPackage, "1.1.0", 0, null)
233+
];
234+
235+
const expectedResult: IDependencyData[] = [
236+
getNodeModuleInfoForExpecteDependency(firstPackage, 0, null, [secondPackage]),
237+
getNodeModuleInfoForExpecteDependency(secondPackage, 0),
238+
getNodeModuleInfoForExpecteDependency(path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, secondPackage), 1)
239+
];
240+
241+
const nodeModulesDependenciesBuilder = generateTest(rootDeps);
242+
const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject);
243+
assert.deepEqual(actualResult, expectedResult);
244+
});
245+
246+
it("when several package depend on different versions of other packages", async () => {
247+
// The test validates the following dependency tree, when npm 3+ is used.
248+
// <project dir>
249+
// ├─┬ firstPackage@1.0.0
250+
// │ ├─┬ secondPackage@1.1.0
251+
// │ │ └── thirdPackage@1.2.0
252+
// │ └── thirdPackage@1.1.0
253+
// ├── secondPackage@1.0.0
254+
// └── thirdPackage@1.0.0
255+
256+
const rootDeps: IDependencyInfo[] = [
257+
generateDependency(firstPackage, "1.0.0", 0, [
258+
generateDependency(secondPackage, "1.1.0", 1, [
259+
generateDependency(thirdPackage, "1.2.0", 2, null)
260+
]),
261+
generateDependency(thirdPackage, "1.1.0", 1, null)
262+
]),
263+
generateDependency(secondPackage, "1.0.0", 0, null),
264+
generateDependency(thirdPackage, "1.0.0", 0, null)
265+
];
266+
267+
const pathToSecondPackageInsideFirstPackage = path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, secondPackage);
268+
const expectedResult: IDependencyData[] = [
269+
getNodeModuleInfoForExpecteDependency(firstPackage, 0, null, [secondPackage, thirdPackage]),
270+
getNodeModuleInfoForExpecteDependency(secondPackage, 0),
271+
getNodeModuleInfoForExpecteDependency(thirdPackage, 0),
272+
getNodeModuleInfoForExpecteDependency(pathToSecondPackageInsideFirstPackage, 1, null, [thirdPackage]),
273+
getNodeModuleInfoForExpecteDependency(path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, thirdPackage), 1),
274+
getNodeModuleInfoForExpecteDependency(path.join(pathToSecondPackageInsideFirstPackage, constants.NODE_MODULES_FOLDER_NAME, thirdPackage), 2),
275+
];
276+
277+
const nodeModulesDependenciesBuilder = generateTest(rootDeps);
278+
const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject);
279+
assert.deepEqual(actualResult, expectedResult);
280+
});
281+
282+
it("when the installed packages have nativescript data in their package.json", async () => {
283+
// The test validates the following dependency tree, when npm 3+ is used.
284+
// <project dir>
285+
// ├── firstPackage@1.0.0
286+
// ├── secondPackage@1.1.0
287+
// └── thirdPackage@1.2.0
288+
289+
const getNativeScriptDataForPlugin = (pluginName: string): any => {
290+
return {
291+
platforms: {
292+
"tns-android": "x.x.x",
293+
"tns-ios": "x.x.x",
294+
},
295+
296+
customPropertyUsedForThisTestOnly: pluginName
297+
};
298+
};
299+
300+
const rootDeps: IDependencyInfo[] = [
301+
generateDependency(firstPackage, "1.0.0", 0, null, getNativeScriptDataForPlugin(firstPackage)),
302+
generateDependency(secondPackage, "1.1.0", 0, null, getNativeScriptDataForPlugin(secondPackage)),
303+
generateDependency(thirdPackage, "1.2.0", 0, null, getNativeScriptDataForPlugin(thirdPackage))
304+
];
305+
306+
const nodeModulesDependenciesBuilder = generateTest(rootDeps);
307+
const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject);
308+
309+
const expectedResult: IDependencyData[] = [
310+
getNodeModuleInfoForExpecteDependency(firstPackage, 0, getNativeScriptDataForPlugin(firstPackage)),
311+
getNodeModuleInfoForExpecteDependency(secondPackage, 0, getNativeScriptDataForPlugin(secondPackage)),
312+
getNodeModuleInfoForExpecteDependency(thirdPackage, 0, getNativeScriptDataForPlugin(thirdPackage))
313+
];
314+
315+
assert.deepEqual(actualResult, expectedResult);
316+
});
317+
});
318+
});
319+
});

0 commit comments

Comments
 (0)
Please sign in to comment.