Skip to content

Commit da404fd

Browse files
feat: skip postinstall steps in case CLI is not installed globally
In case the CLI package is not installed globally, probably it is installed as a dependency of a project or as a dependency of other dependency. In this case the postinstall actions have no meaning for the user, as probably CLI will be used as a library. Skip all of the postinstall actions in such case. Remove the execution code of the old `post-install` command and move it to `post-install-cli` command. Add unit tests. Remove a property from staticConfig - it has no meaning to be there, just place it in the postinstall command directly.
1 parent 5bb46d1 commit da404fd

File tree

10 files changed

+243
-71
lines changed

10 files changed

+243
-71
lines changed

lib/commands/post-install.ts

+28-14
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1-
import { PostInstallCommand } from "../common/commands/post-install";
2-
3-
export class PostInstallCliCommand extends PostInstallCommand {
4-
constructor($fs: IFileSystem,
1+
export class PostInstallCliCommand implements ICommand {
2+
constructor(private $fs: IFileSystem,
53
private $subscriptionService: ISubscriptionService,
6-
$staticConfig: Config.IStaticConfig,
7-
$commandsService: ICommandsService,
8-
$helpService: IHelpService,
9-
$settingsService: ISettingsService,
10-
$doctorService: IDoctorService,
11-
$analyticsService: IAnalyticsService,
12-
$logger: ILogger) {
13-
super($fs, $staticConfig, $commandsService, $helpService, $settingsService, $analyticsService, $logger);
4+
private $commandsService: ICommandsService,
5+
private $helpService: IHelpService,
6+
private $settingsService: ISettingsService,
7+
private $analyticsService: IAnalyticsService,
8+
private $logger: ILogger,
9+
private $hostInfo: IHostInfo) {
1410
}
1511

16-
public async execute(args: string[]): Promise<void> {
17-
await super.execute(args);
12+
public disableAnalytics = true;
13+
public allowedParameters: ICommandParameter[] = [];
1814

15+
public async execute(args: string[]): Promise<void> {
16+
if (!this.$hostInfo.isWindows) {
17+
// when running under 'sudo' we create a working dir with wrong owner (root) and
18+
// it is no longer accessible for the user initiating the installation
19+
// patch the owner here
20+
if (process.env.SUDO_USER) {
21+
// TODO: Check if this is the correct place, probably we should set this at the end of the command.
22+
await this.$fs.setCurrentUserAsOwner(this.$settingsService.getProfileDir(), process.env.SUDO_USER);
23+
}
24+
}
25+
26+
await this.$helpService.generateHtmlPages();
27+
// Explicitly ask for confirmation of usage-reporting:
28+
await this.$analyticsService.checkConsent();
29+
await this.$commandsService.tryExecuteCommand("autocomplete", []);
30+
// Make sure the success message is separated with at least one line from all other messages.
31+
this.$logger.out();
32+
this.$logger.printMarkdown("Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.");
1933
await this.$subscriptionService.subscribeForNewsletter();
2034
}
2135

lib/common/commands/post-install.ts

+2-28
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,12 @@
11
export class PostInstallCommand implements ICommand {
2-
constructor(private $fs: IFileSystem,
3-
private $staticConfig: Config.IStaticConfig,
4-
private $commandsService: ICommandsService,
5-
private $helpService: IHelpService,
6-
private $settingsService: ISettingsService,
7-
private $analyticsService: IAnalyticsService,
8-
protected $logger: ILogger) {
2+
constructor(protected $errors: IErrors) {
93
}
104

115
public disableAnalytics = true;
126
public allowedParameters: ICommandParameter[] = [];
137

148
public async execute(args: string[]): Promise<void> {
15-
if (process.platform !== "win32") {
16-
// when running under 'sudo' we create a working dir with wrong owner (root) and
17-
// it is no longer accessible for the user initiating the installation
18-
// patch the owner here
19-
if (process.env.SUDO_USER) {
20-
await this.$fs.setCurrentUserAsOwner(this.$settingsService.getProfileDir(), process.env.SUDO_USER);
21-
}
22-
}
23-
24-
await this.$helpService.generateHtmlPages();
25-
26-
// Explicitly ask for confirmation of usage-reporting:
27-
await this.$analyticsService.checkConsent();
28-
29-
await this.$commandsService.tryExecuteCommand("autocomplete", []);
30-
31-
if (this.$staticConfig.INSTALLATION_SUCCESS_MESSAGE) {
32-
// Make sure the success message is separated with at least one line from all other messages.
33-
this.$logger.out();
34-
this.$logger.printMarkdown(this.$staticConfig.INSTALLATION_SUCCESS_MESSAGE);
35-
}
9+
this.$errors.fail("This command is deprecated. Use `tns dev-post-install-cli` instead");
3610
}
3711
}
3812
$injector.registerCommand("dev-post-install", PostInstallCommand);

lib/common/commands/preuninstall.ts

+3-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as path from "path";
2+
import { doesCurrentNpmCommandMatch } from "../helpers";
23

34
export class PreUninstallCommand implements ICommand {
45
public disableAnalytics = true;
@@ -11,31 +12,14 @@ export class PreUninstallCommand implements ICommand {
1112
private $settingsService: ISettingsService) { }
1213

1314
public async execute(args: string[]): Promise<void> {
14-
if (this.isIntentionalUninstall()) {
15+
const isIntentionalUninstall = doesCurrentNpmCommandMatch([/^uninstall$/, /^remove$/, /^rm$/, /^r$/, /^un$/, /^unlink$/]);
16+
if (isIntentionalUninstall) {
1517
this.handleIntentionalUninstall();
1618
}
1719

1820
this.$fs.deleteFile(path.join(this.$settingsService.getProfileDir(), "KillSwitches", "cli"));
1921
}
2022

21-
private isIntentionalUninstall(): boolean {
22-
let isIntentionalUninstall = false;
23-
if (process.env && process.env.npm_config_argv) {
24-
try {
25-
const npmConfigArgv = JSON.parse(process.env.npm_config_argv);
26-
const uninstallAliases = ["uninstall", "remove", "rm", "r", "un", "unlink"];
27-
if (_.intersection(npmConfigArgv.original, uninstallAliases).length > 0) {
28-
isIntentionalUninstall = true;
29-
}
30-
} catch (error) {
31-
// ignore
32-
}
33-
34-
}
35-
36-
return isIntentionalUninstall;
37-
}
38-
3923
private handleIntentionalUninstall(): void {
4024
this.$extensibilityService.removeAllExtensions();
4125
this.$packageInstallationManager.clearInspectorCache();

lib/common/definitions/config.d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ declare module Config {
2525
RESOURCE_DIR_PATH: string;
2626
PATH_TO_BOOTSTRAP: string;
2727
QR_SIZE: number;
28-
INSTALLATION_SUCCESS_MESSAGE?: string;
2928
PROFILE_DIR_NAME: string
3029
}
3130

lib/common/helpers.ts

+56
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,62 @@ import * as crypto from "crypto";
88

99
const Table = require("cli-table");
1010

11+
export function doesCurrentNpmCommandMatch(patterns?: RegExp[]): boolean {
12+
const currentNpmCommandArgv = getCurrentNpmCommandArgv();
13+
let result = false;
14+
15+
if (currentNpmCommandArgv.length) {
16+
result = someWithRegExps(currentNpmCommandArgv, patterns);
17+
}
18+
19+
return result;
20+
}
21+
22+
/**
23+
* Equivalent of lodash's some, but instead of lambda, just pass array of Regular Expressions.
24+
* If any of them matches any of the given elements, true is returned.
25+
* @param {string[]} array Elements to be checked.
26+
* @param {RegExp[]} patterns Regular expressions to be tested
27+
* @returns {boolean} True in case any element of the array matches any of the patterns. False otherwise.
28+
*/
29+
export function someWithRegExps(array: string[], patterns: RegExp[]): boolean {
30+
return _.some(array, item => _.some(patterns, pattern => !!item.match(pattern)));
31+
}
32+
33+
export function getCurrentNpmCommandArgv(): string[] {
34+
let result = [];
35+
if (process.env && process.env.npm_config_argv) {
36+
try {
37+
const npmConfigArgv = JSON.parse(process.env.npm_config_argv);
38+
result = npmConfigArgv.original || [];
39+
} catch (error) {
40+
// ignore
41+
}
42+
}
43+
44+
return result;
45+
}
46+
47+
export function isInstallingNativeScriptGlobally(): boolean {
48+
return isInstallingNativeScriptGloballyWithNpm() || isInstallingNativeScriptGloballyWithYarn();
49+
}
50+
51+
function isInstallingNativeScriptGloballyWithNpm(): boolean {
52+
const isInstallCommand = doesCurrentNpmCommandMatch([/^install$/, /^i$/]);
53+
const isGlobalCommand = doesCurrentNpmCommandMatch([/^--global$/, /^-g$/]);
54+
const hasNativeScriptPackage = doesCurrentNpmCommandMatch([/^nativescript(@.*)?$/]);
55+
56+
return isInstallCommand && isGlobalCommand && hasNativeScriptPackage;
57+
}
58+
59+
function isInstallingNativeScriptGloballyWithYarn(): boolean {
60+
// yarn populates the same env used by npm - npm_config_argv, so check it for yarn specific command
61+
const isInstallCommand = doesCurrentNpmCommandMatch([/^add$/]);
62+
const isGlobalCommand = doesCurrentNpmCommandMatch([/^global$/]);
63+
const hasNativeScriptPackage = doesCurrentNpmCommandMatch([/^nativescript(@.*)?$/]);
64+
65+
return isInstallCommand && isGlobalCommand && hasNativeScriptPackage;
66+
}
1167
/**
1268
* Creates regular expression from input string.
1369
* The method replaces all occurences of RegExp special symbols in the input string with \<symbol>.

lib/common/test/unit-tests/helpers.ts

+124
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ interface ITestData {
99
}
1010

1111
describe("helpers", () => {
12+
let originalProcessEnvNpmConfig: any = null;
13+
beforeEach(() => {
14+
originalProcessEnvNpmConfig = process.env.npm_config_argv;
15+
});
16+
17+
afterEach(() => {
18+
process.env.npm_config_argv = originalProcessEnvNpmConfig;
19+
});
1220

1321
const assertTestData = (testData: ITestData, method: Function) => {
1422
const actualResult = method(testData.input);
@@ -699,4 +707,120 @@ describe("helpers", () => {
699707
});
700708
});
701709
});
710+
711+
const setNpmConfigArgv = (original: string[]): void => {
712+
process.env.npm_config_argv = JSON.stringify({ original });
713+
};
714+
715+
describe("doesCurrentNpmCommandMatch", () => {
716+
[
717+
{
718+
name: "returns true when searching for --global flag and it is passed on terminal",
719+
input: ["install", "--global", "nativescript"],
720+
expectedOutput: true
721+
},
722+
{
723+
name: "returns true when searching for -g flag and --global is passed on terminal",
724+
input: ["install", "-g", "nativescript"],
725+
expectedOutput: true
726+
},
727+
{
728+
name: "returns false when searching for global flag and it is NOT passed on terminal",
729+
input: ["install", "nativescript"],
730+
expectedOutput: false
731+
},
732+
{
733+
name: "returns false when searching for global flag and it is NOT passed on terminal, but similar flag is passed",
734+
input: ["install", "nativescript", "--globalEnv"],
735+
expectedOutput: false
736+
},
737+
{
738+
name: "returns false when searching for global flag and it is NOT passed on terminal, but trying to install global package",
739+
input: ["install", "global"],
740+
expectedOutput: false
741+
}
742+
].forEach(testCase => {
743+
it(testCase.name, () => {
744+
setNpmConfigArgv(testCase.input);
745+
const result = helpers.doesCurrentNpmCommandMatch([/^--global$/, /^-g$/]);
746+
assert.equal(result, testCase.expectedOutput);
747+
});
748+
});
749+
});
750+
751+
describe("isInstallingNativeScriptGlobally", () => {
752+
const installationFlags = ["install", "i"];
753+
const globalFlags = ["--global", "-g"];
754+
const validNativeScriptPackageNames = ["nativescript", "[email protected]", "nativescript@next"];
755+
756+
it("returns true when installing nativescript globally with npm", () => {
757+
validNativeScriptPackageNames.forEach(nativescript => {
758+
installationFlags.forEach(install => {
759+
globalFlags.forEach(globalFlag => {
760+
const npmArgs = [install, nativescript, globalFlag];
761+
setNpmConfigArgv(npmArgs);
762+
const result = helpers.isInstallingNativeScriptGlobally();
763+
assert.isTrue(result);
764+
});
765+
});
766+
});
767+
});
768+
769+
it("returns true when installing nativescript globally with yarn", () => {
770+
validNativeScriptPackageNames.forEach(nativescript => {
771+
const npmArgs = ["global", "add", nativescript];
772+
setNpmConfigArgv(npmArgs);
773+
const result = helpers.isInstallingNativeScriptGlobally();
774+
assert.isTrue(result);
775+
});
776+
});
777+
778+
const invalidInstallationFlags = ["installpackage", "is"];
779+
const invalidGlobalFlags = ["--globalEnv", ""];
780+
const invalidNativeScriptPackageNames = ["nativescript", "nativescript-facebook", "[email protected]", "kinvey-nativescript-plugin"];
781+
782+
it(`returns false when command does not install nativescript globally`, () => {
783+
invalidInstallationFlags.forEach(nativescript => {
784+
invalidGlobalFlags.forEach(install => {
785+
invalidNativeScriptPackageNames.forEach(globalFlag => {
786+
const npmArgs = [install, nativescript, globalFlag];
787+
setNpmConfigArgv(npmArgs);
788+
const result = helpers.isInstallingNativeScriptGlobally();
789+
assert.isFalse(result);
790+
});
791+
});
792+
});
793+
});
794+
});
795+
796+
describe("getCurrentNpmCommandArgv", () => {
797+
it("returns the value of process.env.npm_config_argv.original", () => {
798+
const command = ["install", "nativescript"];
799+
process.env.npm_config_argv = JSON.stringify({ someOtherProp: 1, original: command });
800+
const actualCommand = helpers.getCurrentNpmCommandArgv();
801+
assert.deepEqual(actualCommand, command);
802+
});
803+
804+
describe("returns empty array", () => {
805+
const assertResultIsEmptyArray = () => {
806+
const actualCommand = helpers.getCurrentNpmCommandArgv();
807+
assert.deepEqual(actualCommand, []);
808+
};
809+
810+
it("when npm_config_argv is not populated", () => {
811+
delete process.env.npm_config_argv;
812+
assertResultIsEmptyArray();
813+
});
814+
815+
it("when npm_config_argv is not a valid json", () => {
816+
process.env.npm_config_argv = "invalid datas";
817+
assertResultIsEmptyArray();
818+
});
819+
820+
it("when npm_config_argv.original is null", () => {
821+
process.env.npm_config_argv = JSON.stringify({ original: null });
822+
assertResultIsEmptyArray();
823+
});
824+
});
825+
});
702826
});

lib/config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig {
2929
public TRACK_FEATURE_USAGE_SETTING_NAME = "TrackFeatureUsage";
3030
public ERROR_REPORT_SETTING_NAME = "TrackExceptions";
3131
public ANALYTICS_INSTALLATION_ID_SETTING_NAME = "AnalyticsInstallationID";
32-
public INSTALLATION_SUCCESS_MESSAGE = "Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.";
3332
public get PROFILE_DIR_NAME(): string {
3433
return ".nativescript-cli";
3534
}

postinstall.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ var path = require("path");
55
var constants = require(path.join(__dirname, "lib", "constants"));
66
var commandArgs = [path.join(__dirname, "bin", "tns"), constants.POST_INSTALL_COMMAND_NAME];
77
var nodeArgs = require(path.join(__dirname, "lib", "common", "scripts", "node-args")).getNodeArgs();
8-
9-
child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" });
8+
var helpers = require(path.join(__dirname, "lib", "common", "helpers"));
9+
if (helpers.isInstallingNativeScriptGlobally()) {
10+
child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" });
11+
}

test/commands/post-install.ts

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const createTestInjector = (): IInjector => {
4343

4444
testInjector.registerCommand("post-install-cli", PostInstallCliCommand);
4545

46+
testInjector.register("hostInfo", {});
47+
4648
return testInjector;
4749
};
4850

0 commit comments

Comments
 (0)