Skip to content

Commit d70fe8b

Browse files
Merge pull request #4317 from NativeScript/vladimirov/skip-postinstall-lib
feat: skip postinstall steps in case CLI is not installed globally
2 parents 3d9efd3 + 3d13b99 commit d70fe8b

File tree

10 files changed

+246
-71
lines changed

10 files changed

+246
-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
@@ -23,7 +23,6 @@ declare module Config {
2323
RESOURCE_DIR_PATH: string;
2424
PATH_TO_BOOTSTRAP: string;
2525
QR_SIZE: number;
26-
INSTALLATION_SUCCESS_MESSAGE?: string;
2726
PROFILE_DIR_NAME: string
2827
}
2928

lib/common/helpers.ts

+57
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,66 @@ import { ReadStream } from "tty";
55
import { Configurations } from "./constants";
66
import { EventEmitter } from "events";
77
import * as crypto from "crypto";
8+
import * as _ from "lodash";
89

910
const Table = require("cli-table");
1011

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

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

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

lib/config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export class StaticConfig implements IStaticConfig {
3838
public TRACK_FEATURE_USAGE_SETTING_NAME = "TrackFeatureUsage";
3939
public ERROR_REPORT_SETTING_NAME = "TrackExceptions";
4040
public ANALYTICS_INSTALLATION_ID_SETTING_NAME = "AnalyticsInstallationID";
41-
public INSTALLATION_SUCCESS_MESSAGE = "Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.";
4241
public get PROFILE_DIR_NAME(): string {
4342
return ".nativescript-cli";
4443
}

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)