Skip to content

feat: skip postinstall steps in case CLI is not installed globally #4317

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 2 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions lib/commands/post-install.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { PostInstallCommand } from "../common/commands/post-install";

export class PostInstallCliCommand extends PostInstallCommand {
constructor($fs: IFileSystem,
export class PostInstallCliCommand implements ICommand {
constructor(private $fs: IFileSystem,
private $subscriptionService: ISubscriptionService,
$staticConfig: Config.IStaticConfig,
$commandsService: ICommandsService,
$helpService: IHelpService,
$settingsService: ISettingsService,
$doctorService: IDoctorService,
$analyticsService: IAnalyticsService,
$logger: ILogger) {
super($fs, $staticConfig, $commandsService, $helpService, $settingsService, $analyticsService, $logger);
private $commandsService: ICommandsService,
private $helpService: IHelpService,
private $settingsService: ISettingsService,
private $analyticsService: IAnalyticsService,
private $logger: ILogger,
private $hostInfo: IHostInfo) {
}

public async execute(args: string[]): Promise<void> {
await super.execute(args);
public disableAnalytics = true;
public allowedParameters: ICommandParameter[] = [];

public async execute(args: string[]): Promise<void> {
if (!this.$hostInfo.isWindows) {
// when running under 'sudo' we create a working dir with wrong owner (root) and
// it is no longer accessible for the user initiating the installation
// patch the owner here
if (process.env.SUDO_USER) {
// TODO: Check if this is the correct place, probably we should set this at the end of the command.
await this.$fs.setCurrentUserAsOwner(this.$settingsService.getProfileDir(), process.env.SUDO_USER);
}
}

await this.$helpService.generateHtmlPages();
// Explicitly ask for confirmation of usage-reporting:
await this.$analyticsService.checkConsent();
await this.$commandsService.tryExecuteCommand("autocomplete", []);
// Make sure the success message is separated with at least one line from all other messages.
this.$logger.out();
this.$logger.printMarkdown("Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.");
await this.$subscriptionService.subscribeForNewsletter();
}

Expand Down
30 changes: 2 additions & 28 deletions lib/common/commands/post-install.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,12 @@
export class PostInstallCommand implements ICommand {
constructor(private $fs: IFileSystem,
private $staticConfig: Config.IStaticConfig,
private $commandsService: ICommandsService,
private $helpService: IHelpService,
private $settingsService: ISettingsService,
private $analyticsService: IAnalyticsService,
protected $logger: ILogger) {
constructor(protected $errors: IErrors) {
}

public disableAnalytics = true;
public allowedParameters: ICommandParameter[] = [];

public async execute(args: string[]): Promise<void> {
if (process.platform !== "win32") {
// when running under 'sudo' we create a working dir with wrong owner (root) and
// it is no longer accessible for the user initiating the installation
// patch the owner here
if (process.env.SUDO_USER) {
await this.$fs.setCurrentUserAsOwner(this.$settingsService.getProfileDir(), process.env.SUDO_USER);
}
}

await this.$helpService.generateHtmlPages();

// Explicitly ask for confirmation of usage-reporting:
await this.$analyticsService.checkConsent();

await this.$commandsService.tryExecuteCommand("autocomplete", []);

if (this.$staticConfig.INSTALLATION_SUCCESS_MESSAGE) {
// Make sure the success message is separated with at least one line from all other messages.
this.$logger.out();
this.$logger.printMarkdown(this.$staticConfig.INSTALLATION_SUCCESS_MESSAGE);
}
this.$errors.fail("This command is deprecated. Use `tns dev-post-install-cli` instead");
}
}
$injector.registerCommand("dev-post-install", PostInstallCommand);
22 changes: 3 additions & 19 deletions lib/common/commands/preuninstall.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as path from "path";
import { doesCurrentNpmCommandMatch } from "../helpers";

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

public async execute(args: string[]): Promise<void> {
if (this.isIntentionalUninstall()) {
const isIntentionalUninstall = doesCurrentNpmCommandMatch([/^uninstall$/, /^remove$/, /^rm$/, /^r$/, /^un$/, /^unlink$/]);
if (isIntentionalUninstall) {
this.handleIntentionalUninstall();
}

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

private isIntentionalUninstall(): boolean {
let isIntentionalUninstall = false;
if (process.env && process.env.npm_config_argv) {
try {
const npmConfigArgv = JSON.parse(process.env.npm_config_argv);
const uninstallAliases = ["uninstall", "remove", "rm", "r", "un", "unlink"];
if (_.intersection(npmConfigArgv.original, uninstallAliases).length > 0) {
isIntentionalUninstall = true;
}
} catch (error) {
// ignore
}

}

return isIntentionalUninstall;
}

private handleIntentionalUninstall(): void {
this.$extensibilityService.removeAllExtensions();
this.$packageInstallationManager.clearInspectorCache();
Expand Down
1 change: 0 additions & 1 deletion lib/common/definitions/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ declare module Config {
RESOURCE_DIR_PATH: string;
PATH_TO_BOOTSTRAP: string;
QR_SIZE: number;
INSTALLATION_SUCCESS_MESSAGE?: string;
PROFILE_DIR_NAME: string
}

Expand Down
57 changes: 57 additions & 0 deletions lib/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,66 @@ import { ReadStream } from "tty";
import { Configurations } from "./constants";
import { EventEmitter } from "events";
import * as crypto from "crypto";
import * as _ from "lodash";

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

export function doesCurrentNpmCommandMatch(patterns?: RegExp[]): boolean {
const currentNpmCommandArgv = getCurrentNpmCommandArgv();
let result = false;

if (currentNpmCommandArgv.length) {
result = someWithRegExps(currentNpmCommandArgv, patterns);
}

return result;
}

/**
* Equivalent of lodash's some, but instead of lambda, just pass array of Regular Expressions.
* If any of them matches any of the given elements, true is returned.
* @param {string[]} array Elements to be checked.
* @param {RegExp[]} patterns Regular expressions to be tested
* @returns {boolean} True in case any element of the array matches any of the patterns. False otherwise.
*/
export function someWithRegExps(array: string[], patterns: RegExp[]): boolean {
return _.some(array, item => _.some(patterns, pattern => !!item.match(pattern)));
}

export function getCurrentNpmCommandArgv(): string[] {
let result = [];
if (process.env && process.env.npm_config_argv) {
try {
const npmConfigArgv = JSON.parse(process.env.npm_config_argv);
result = npmConfigArgv.original || [];
} catch (error) {
// ignore
}
}

return result;
}

export function isInstallingNativeScriptGlobally(): boolean {
return isInstallingNativeScriptGloballyWithNpm() || isInstallingNativeScriptGloballyWithYarn();
}

function isInstallingNativeScriptGloballyWithNpm(): boolean {
const isInstallCommand = doesCurrentNpmCommandMatch([/^install$/, /^i$/]);
const isGlobalCommand = doesCurrentNpmCommandMatch([/^--global$/, /^-g$/]);
const hasNativeScriptPackage = doesCurrentNpmCommandMatch([/^nativescript(@.*)?$/]);

return isInstallCommand && isGlobalCommand && hasNativeScriptPackage;
}

function isInstallingNativeScriptGloballyWithYarn(): boolean {
// yarn populates the same env used by npm - npm_config_argv, so check it for yarn specific command
const isInstallCommand = doesCurrentNpmCommandMatch([/^add$/]);
const isGlobalCommand = doesCurrentNpmCommandMatch([/^global$/]);
const hasNativeScriptPackage = doesCurrentNpmCommandMatch([/^nativescript(@.*)?$/]);

return isInstallCommand && isGlobalCommand && hasNativeScriptPackage;
}
/**
* Creates regular expression from input string.
* The method replaces all occurences of RegExp special symbols in the input string with \<symbol>.
Expand Down
126 changes: 126 additions & 0 deletions lib/common/test/unit-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ interface ITestData {
}

describe("helpers", () => {
let originalProcessEnvNpmConfig: any = null;
beforeEach(() => {
originalProcessEnvNpmConfig = process.env.npm_config_argv;
});

afterEach(() => {
process.env.npm_config_argv = originalProcessEnvNpmConfig;
});

const assertTestData = (testData: ITestData, method: Function) => {
const actualResult = method(testData.input);
Expand Down Expand Up @@ -699,4 +707,122 @@ describe("helpers", () => {
});
});
});

const setNpmConfigArgv = (original: string[]): void => {
process.env.npm_config_argv = JSON.stringify({ original });
};

describe("doesCurrentNpmCommandMatch", () => {
describe("when searching for global flag (--global or -g)", () => {
[
{
name: "returns true when `--global` is passed on terminal",
input: ["install", "--global", "nativescript"],
expectedOutput: true
},
{
name: "returns true when `-g` is passed on terminal",
input: ["install", "-g", "nativescript"],
expectedOutput: true
},
{
name: "returns false neither -g/--global are passed on terminal",
input: ["install", "nativescript"],
expectedOutput: false
},
{
name: "returns false when neither -g/--global are passed on terminal, but similar flag is passed",
input: ["install", "nativescript", "--globalEnv"],
expectedOutput: false
},
{
name: "returns false when neither -g/--global are passed on terminal, but trying to install global package",
input: ["install", "global"],
expectedOutput: false
}
].forEach(testCase => {
it(testCase.name, () => {
setNpmConfigArgv(testCase.input);
const result = helpers.doesCurrentNpmCommandMatch([/^--global$/, /^-g$/]);
assert.equal(result, testCase.expectedOutput);
});
});
});
});

describe("isInstallingNativeScriptGlobally", () => {
const installationFlags = ["install", "i"];
const globalFlags = ["--global", "-g"];
const validNativeScriptPackageNames = ["nativescript", "[email protected]", "nativescript@next"];

it("returns true when installing nativescript globally with npm", () => {
validNativeScriptPackageNames.forEach(nativescript => {
installationFlags.forEach(install => {
globalFlags.forEach(globalFlag => {
const npmArgs = [install, nativescript, globalFlag];
setNpmConfigArgv(npmArgs);
const result = helpers.isInstallingNativeScriptGlobally();
assert.isTrue(result);
});
});
});
});

it("returns true when installing nativescript globally with yarn", () => {
validNativeScriptPackageNames.forEach(nativescript => {
const npmArgs = ["global", "add", nativescript];
setNpmConfigArgv(npmArgs);
const result = helpers.isInstallingNativeScriptGlobally();
assert.isTrue(result);
});
});

const invalidInstallationFlags = ["installpackage", "is"];
const invalidGlobalFlags = ["--globalEnv", ""];
const invalidNativeScriptPackageNames = ["nativescript", "nativescript-facebook", "[email protected]", "kinvey-nativescript-plugin"];

it(`returns false when command does not install nativescript globally`, () => {
invalidInstallationFlags.forEach(nativescript => {
invalidGlobalFlags.forEach(install => {
invalidNativeScriptPackageNames.forEach(globalFlag => {
const npmArgs = [install, nativescript, globalFlag];
setNpmConfigArgv(npmArgs);
const result = helpers.isInstallingNativeScriptGlobally();
assert.isFalse(result);
});
});
});
});
});

describe("getCurrentNpmCommandArgv", () => {
it("returns the value of process.env.npm_config_argv.original", () => {
const command = ["install", "nativescript"];
process.env.npm_config_argv = JSON.stringify({ someOtherProp: 1, original: command });
const actualCommand = helpers.getCurrentNpmCommandArgv();
assert.deepEqual(actualCommand, command);
});

describe("returns empty array", () => {
const assertResultIsEmptyArray = () => {
const actualCommand = helpers.getCurrentNpmCommandArgv();
assert.deepEqual(actualCommand, []);
};

it("when npm_config_argv is not populated", () => {
delete process.env.npm_config_argv;
assertResultIsEmptyArray();
});

it("when npm_config_argv is not a valid json", () => {
process.env.npm_config_argv = "invalid datas";
assertResultIsEmptyArray();
});

it("when npm_config_argv.original is null", () => {
process.env.npm_config_argv = JSON.stringify({ original: null });
assertResultIsEmptyArray();
});
});
});
});
1 change: 0 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export class StaticConfig implements IStaticConfig {
public TRACK_FEATURE_USAGE_SETTING_NAME = "TrackFeatureUsage";
public ERROR_REPORT_SETTING_NAME = "TrackExceptions";
public ANALYTICS_INSTALLATION_ID_SETTING_NAME = "AnalyticsInstallationID";
public INSTALLATION_SUCCESS_MESSAGE = "Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.";
public get PROFILE_DIR_NAME(): string {
return ".nativescript-cli";
}
Expand Down
6 changes: 4 additions & 2 deletions postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ var path = require("path");
var constants = require(path.join(__dirname, "lib", "constants"));
var commandArgs = [path.join(__dirname, "bin", "tns"), constants.POST_INSTALL_COMMAND_NAME];
var nodeArgs = require(path.join(__dirname, "lib", "common", "scripts", "node-args")).getNodeArgs();

child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" });
var helpers = require(path.join(__dirname, "lib", "common", "helpers"));
if (helpers.isInstallingNativeScriptGlobally()) {
child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" });
}
2 changes: 2 additions & 0 deletions test/commands/post-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const createTestInjector = (): IInjector => {

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

testInjector.register("hostInfo", {});

return testInjector;
};

Expand Down
Loading