Skip to content

remove npm2 dependency #2209

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 6 commits into from
Nov 17, 2016
Merged

remove npm2 dependency #2209

merged 6 commits into from
Nov 17, 2016

Conversation

Plamen5kov
Copy link
Contributor

@Plamen5kov Plamen5kov commented Nov 9, 2016

What does this PR do?

PR removes the local npm2 dependency from CLI. Fix for: #1845

Description
Cache functionality is removed from lib/node-package-manager.ts and lib/npm-installation-manager. Other changes are made along the way to accommodate the lack of the cache add cache unpack functionality.

How did it work till now?

  • On npm install template: template is saved to tmp folder in some cases and to cache in others, and then are extracted to app folder. If Template is not default, the default template was downloaded because it's the only one that has App_Resources folder. The App_Resources folder is copied to new template.
  • On npm install core-framework (tns-ios, tns-android), tns-core-framework is saved to cache and extracted to platforms/ folder
  • On npm install node-inspector, package is saved and extracted to cache.
  • On tns platform update we always updated iOS

How will it work after this PR

  • On npm install, all packages are treated as normal node packages. That means all of them are installed in the node modules and in the case of the templates and the core-frameworks the packages are uninstalled after they are extracted to app/ and to platforms/ folders respectively. All templates will be expected to have an App_Resources folder with them.
  • We check the current iOS proj file and the new one and if they are different, there is a failure on update.
  • Currently the ios-node-inspector that is being installed through npm is in node_modules folder for each project. We might think of a way to make an exception for the inspector because of it's size.

The implementation works with npm cache internally.

Expected

Because not all templates have an App_Resource folder yet, there are tests that are expected to fail with: Project's app/App_Resources must contain Android and iOS directories.

Related PR

These PR add App_Resource folder to templates that don't have it. From now on all templates will have to carry with them an App_Resource folder.
https://github.com/NativeScript/template-hello-world-ts/pull/21
https://github.com/NativeScript/template-hello-world-ng/pull/34

ping: @NativeScript/android-runtime @rosen-vladimirov @hshristov @tzraikov @hdeshev @KristinaKoeva

@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch 2 times, most recently from 23c9293 to 6c50fb5 Compare November 9, 2016 08:58
}
return diff;
} catch (err) {
if (err.code === "EPEERINVALID") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not using npm as a library anymore, will we actually get an exception with an EPEERINVALID code from the child process? Seems like we should either delete this code or handle invalid peer dependencies in another way.

Copy link
Contributor Author

@Plamen5kov Plamen5kov Nov 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right I hadn't thought about that, will make adjustments
EDIT: @hdeshev after some research it turns out we throw exception on exec and this catch is executed, so we're safe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With new implementation we handle the EPEERINVALID error same as before.

let subDirsBefore:Array<string> = this.$fs.exists(path.join(pathToSave, "node_modules")).wait() ? this.$fs.readDirectory(path.join(pathToSave, "node_modules")).wait() : [];

let flags = this.getFlagsString(config);
this.$childProcess.exec(`npm install ${packageName} ${flags}`, { cwd: pathToSave }).wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exec invocation looks like it's eating the child process stdout. I think we should inherit stdio here so that npm-related errors are visible to the user. Hmm, yes, we need stdin too -- I've heard rumors on the internet there are packages that block the console asking for email addresses on installation.

Copy link
Contributor Author

@Plamen5kov Plamen5kov Nov 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdeshev inside childProcess.exec we do a throw which is caught by the catch, so we don't consume the error: https://github.com/telerik/mobile-cli-lib/blob/16a4c64e6f1ac2c58d21d92a5da11bc73854451c/child-process.ts#L14

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new version i use $childProcess.spawnFromEvent(...) so we can handle the stdout and stderr to keep previous behavior.

}).future<boolean>()();
}

// Is this used?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we kill this commented out code block..

assert.strictEqual(path.basename(actualPathToTemplate), expectedTemplatePath);
assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted.");
});
//there is no tmp directory anymore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete commented-out code.

@hdeshev
Copy link
Contributor

hdeshev commented Nov 9, 2016

Squash those commits, please.

@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch 2 times, most recently from 67619c9 to bc808aa Compare November 10, 2016 07:02
}
});
return future;
public install(packageName: string, pathToSave: string, config?: any): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method has async operation in it, so it should return IFuture. Also it's a good practice to avoid setting return type to any. As far as I can see, the return type here should be IFuture<string[]>

return;
let flags = this.getFlagsString(config, true);
let params = ["install", packageName];
for(let index in flags) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can directly concat the params and flags arrays:

let params = [ "install", packageName ].concat(flags);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried, didn't work :D

if (this.$options.ignoreScripts) {
config = config || {};
config["ignore-scripts"] = true;
let res = this.$childProcess.spawnFromEvent("npm", params, "close", {}, { throwError: false }).wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure about this, but we've used to pass null for the fourth argument instead of empty object. This way the default childProcess options will be used.

config["ignore-scripts"] = true;
let res = this.$childProcess.spawnFromEvent("npm", params, "close", {}, { throwError: false }).wait();
if(res.stderr) {
throw {stderr: res.stderr};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way the error will not have message. Most of the cosumers which print errors use the message property of the error. You can use:

throw new Error(res.stderr); 

and later you can check if err.message.indexOf("EPEERINVALID") !== -1

* The solution is to traverse node_modules before and after install and get the name of the installed package,
* even if it's installed through local path or URL. If command installes more than one package, all package names are returned.
*/
let diff = subDirsAfter.filter(x => subDirsBefore.indexOf(x) === -1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case I want to install a single package, via .tgz, but it has dependencies, npm 3 will install all of them on the root level and current code will return list of all of them. But the method should have installed a single package and return its name. Is this behavior expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rosen-vladimirov, yes this behavior is expected

}

/**
* Reads package.json/nativescript/tns-(android/ios)/template if any and installes it.
* We need to remove this, because it may override the template already present in app if someone decides to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platform template is the template used for platforms/<platform>/ directory. It cannot override anything in app dir.

}
} else {
this.updatePlatformCore(platformData, currentVersion, newVersion, canUpdate).wait();
this.$errors.fail("Native Platfrom cannot be updated.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platfrom -> Platform
Using $errors.fail will print command's help after the error, is this required here or failWithoutHelp is more appropriate?

//npmInstallResultStream is not very usefull (doesn't contain all the package information), so we need to find the real one inside node_modules
let foundValidPlugin = false;
let realNpmPackageJson:any;
for(let index in installedModuleNames) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use of instead of in which will give you the name directly:

for(let name of installedModuleNames) {
...

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of

for(let index in installedModuleNames) {
let name = installedModuleNames[index];
let pathToRealNpmPackageJson = `${this.$projectData.projectDir}/node_modules/${name}/package.json`;
realNpmPackageJson = require(pathToRealNpmPackageJson);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's safer to use this.$fs.readJson(pathToRealNpmPackageJson).wait() as require caches results and also it does not work in case the file has BOM header

/**
* Provides a platform specific update logic for the specified runtime versions.
* @return true in cases when the update procedure should continue.
*/
updatePlatform(currentVersion: string, newVersion: string, canUpdate: boolean, addPlatform?: Function, removePlatform?: (platforms: string[]) => IFuture<void>): IFuture<boolean>;
// updatePlatform(currentVersion: string, newVersion: string, canUpdate: boolean, addPlatform?: Function, removePlatform?: (platforms: string[]) => IFuture<void>): IFuture<boolean>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case the method is commented, can you please delete it. Also the implementation should be removed.

}

public search(filter: string[], silent: boolean): IFuture<any> {
let args = (<any[]>([filter] || [])).concat(silent);
return this.loadAndExecute("search", args);
return this.$childProcess.exec(`npm search ${args}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As args is array, it's toString representation will be the elements, separated by ,. For example in case the args is array: [ "a", "b", "c" ], the execed process will be:

npm search a,b,c

In fact we would like the command to be:

npm search a b c

(note the results of the two search commands are totally different).

So I would advise you to use:

return this.$childProcess.exec(`npm search ${args.join(" ")}`);

@@ -81,66 +88,39 @@ export class NodePackageManager implements INodePackageManager {
}

public uninstall(packageName: string, config?: any, path?: string): IFuture<any> {
return this.loadAndExecute("uninstall", [[packageName]], { config, path });
let flags = this.getFlagsString(config, false);
return this.$childProcess.exec(`npm uninstall ${packageName} ${flags}`, { cwd: path });
}

public search(filter: string[], silent: boolean): IFuture<any> {
let args = (<any[]>([filter] || [])).concat(silent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

silent is boolean, so the args passed to npm search will contain the value true or false. Maybe you should add --silent only in case the silent argument is true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if silent is passed getFlagsString function will add it as a flag, so this case is covered.

let foundValidPlugin = false;
let realNpmPackageJson:any;
for(let name of installedModuleNames) {
let pathToRealNpmPackageJson = `${this.$projectData.projectDir}/node_modules/${name}/package.json`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use path.join when constructing file path instead of directly adding / as it may no work on all OSes

}

return this.parseNpmCommandResult(result);
return this.parseNpmCommandResult(npmCommandArguments);
}).future<string>()();
}

private parseNpmCommandResult(npmCommandResult: string): string { // [[name@version, node_modules/name]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the comment is no longer correct, can you please remove it.

* The solution is to traverse node_modules before and after install and get the name of the installed package,
* even if it's installed through local path or URL. If command installes more than one package, all package names are returned.
*/
let diff = dependenciesAfter.filter(x => dependenciesBefore.indexOf(x) === -1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found a case where this approach will not work. Consider the case where a plugin is already installed in package.json, but user wants to update it's version (or install it from .tgz this time). In this case the difference will be empty array and the plugin will not be installed. I would suggest to compare both the name of the plugin and the version. For example this code could do the job:

let diff = _(jsonContentAfter.dependencies)
    .omitBy((val: string, key: string) => jsonContentBefore && jsonContentBefore.dependencies[key] && jsonContentBefore.dependencies[key] === val)
    .keys()
    .value();

@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch 2 times, most recently from dd9e8dc to e777918 Compare November 14, 2016 07:03
@@ -48,30 +24,54 @@ export class NodePackageManager implements INodePackageManager {
}

try {
return this.loadAndExecute("install", [pathToSave, packageName], { config: config }).wait();
let jsonContentBefore = this.$fs.readJson(path.join(pathToSave, "package.json")).wait();
let dependenciesBefore: Array<string> = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you do not have to iterate over the dependencies in order to get the keys. You can use lodash here:

let dependenciesBefore = _.keys(jsonContentBefore.dependencies);

Lodash's keys method will return empty array in case there's no dependencies property in jsonContentBefore

this.$childProcess.spawnFromEvent("npm", params, "close", { cwd: pathToSave }).wait();

let jsonContentAfter = this.$fs.readJson(path.join(pathToSave, "package.json")).wait();
let dependenciesAfter: Array<string> = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above - you can skip the if and use _.keys(jsonContentAfter.dependencies);

} catch (err) {
if (err.code === "EPEERINVALID") {
if (err.message && err.message.indexOf("EPEERINVALID") !== -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case npm install fails with EPEERINVALID error, the method will show it as warning and will skip all of the checks regarding jsonContentBefore and jsonContentAfter. Also the method will not return anything. Is this expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, I'll fix it

// this package has not been published to npmjs.org yet - perhaps manually added via --framework-path
this.$logger.trace(`Checking shasum of package ${packageName}@${version}: skipped because the package was not found in npmjs.org`);
return true;
if(version === constants.PackageVersion.NEXT) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need special code for next version. In case we decide to use separate tag (for example nextNext) we shouldn't add it here. Npm supports installing of package directly by tag, for example npm install nativescript@next will work as expected. Is there a particular reason to have special methods for next and latest tags?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've missed removing, this if, i was testing something out, and I've seem to forgot to remove it. The idea behind the next and last tags is readability only.

}
let installedModuleNames = this.npmInstall(packageName, pathToSave, version, dependencyType).wait();
let installedPackageName = installedModuleNames[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why [0] - is it possible to return more than one packages? In case yes - why do we get only the first one. In case not - why does the method return array?

Copy link
Contributor Author

@Plamen5kov Plamen5kov Nov 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On all the places we use node-package-manager, we pass a single package to be installed, and expect a single package to be installed. My thinking was for a later refactoring, where we could be able to use the node-package-manager everywhere or removing it completely and using npm service instead. That means it could install one or multiple packages, hence saving the array result.

}
} else {
this.updatePlatformCore(platformData, currentVersion, newVersion, canUpdate).wait();
this.$errors.failWithoutHelp("Native Platfrom cannot be updated.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platfrom -> Platform

params.push(packageName); //because npm install ${pwd} on mac tries to install itself as a dependency (windows and linux have no such issues)
}
params = params.concat(flags);
this.$childProcess.spawnFromEvent("npm", params, "close", { cwd: pathToSave }).wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this code work on Windows? On Mac and Linux the executable name is npm, but on Windows it's npm.cmd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm not calling a direct path to executable, I expect npm to be found in path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no npm executable in PATH on Windows. There's npm.cmd

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more precise in the explanation:

  • on Windows, the default command prompt is able to resolve npm to npm.cmd when you execute npm install
  • childProcess.spawn method does not launch new shell, it just starts executable in PATH (or passed with full path)

In the current case, childProcess is trying to start a new process called npm but it cannot be resolved as the real name of the executable on Windows is npm.cmd.
We have such code, you can take a look at it here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, I'll fix it, thank you.

@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch from 6b73294 to 92de5c7 Compare November 15, 2016 09:29
@Plamen5kov
Copy link
Contributor Author

run CI

@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch from 10484fd to e384c4b Compare November 16, 2016 11:49
@Plamen5kov
Copy link
Contributor Author

@hdeshev @rosen-vladimirov, 5 tests concerning App_Resources folder are commented out so CI runs ok, but if you have any last notes for the PR, I would be happy to resolve them.

vchimev pushed a commit to NativeScript/nativescript-cli-tests that referenced this pull request Nov 16, 2016
@Plamen5kov Plamen5kov force-pushed the plamen5kov/remove_npm2_dependency branch from e384c4b to fd15624 Compare November 16, 2016 15:41
@Plamen5kov Plamen5kov merged commit 40566ce into master Nov 17, 2016
Plamen5kov added a commit that referenced this pull request Nov 17, 2016
…npm2_dependency"

This reverts commit 40566ce, reversing
changes made to 0be61c8.
Plamen5kov added a commit that referenced this pull request Nov 24, 2016
…/remove_npm2_dependency""

This reverts commit a60eabd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants