nativescript
Version:
Command-line interface for building NativeScript projects
316 lines • 14.6 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HooksService = void 0;
const path = require("path");
const util = require("util");
const _ = require("lodash");
const helpers_1 = require("../helpers");
const constants_1 = require("../../constants");
const yok_1 = require("../yok");
const color_1 = require("../../color");
const decorators_1 = require("../decorators");
class Hook {
constructor(name, fullPath) {
this.name = name;
this.fullPath = fullPath;
}
}
class HooksService {
constructor($childProcess, $fs, $logger, $errors, $config, $staticConfig, $injector, $projectHelper, $options, $performanceService, $projectConfigService) {
this.$childProcess = $childProcess;
this.$fs = $fs;
this.$logger = $logger;
this.$errors = $errors;
this.$config = $config;
this.$staticConfig = $staticConfig;
this.$injector = $injector;
this.$projectHelper = $projectHelper;
this.$options = $options;
this.$performanceService = $performanceService;
this.$projectConfigService = $projectConfigService;
}
get hookArgsName() {
return "hookArgs";
}
initialize(projectDir) {
this.cachedHooks = {};
this.hooksDirectories = [];
projectDir = projectDir || this.$projectHelper.projectDir;
if (projectDir) {
this.hooksDirectories.push(path.join(projectDir, HooksService.HOOKS_DIRECTORY_NAME));
}
this.$logger.trace("Hooks directories: " + util.inspect(this.hooksDirectories));
const customHooks = this.$projectConfigService.getValue("hooks", []);
if (customHooks.length) {
this.$logger.trace("Custom hooks: " + util.inspect(customHooks));
}
}
static formatHookName(commandName) {
// Remove everything after | (including the pipe)
return commandName.replace(/\|[\s\S]*$/, "");
}
executeBeforeHooks(commandName, hookArguments) {
const beforeHookName = `before-${HooksService.formatHookName(commandName)}`;
const traceMessage = `BeforeHookName for command ${commandName} is ${beforeHookName}`;
return this.executeHooks(beforeHookName, traceMessage, hookArguments);
}
executeAfterHooks(commandName, hookArguments) {
const afterHookName = `after-${HooksService.formatHookName(commandName)}`;
const traceMessage = `AfterHookName for command ${commandName} is ${afterHookName}`;
return this.executeHooks(afterHookName, traceMessage, hookArguments);
}
async executeHooks(hookName, traceMessage, hookArguments) {
if (this.$config.DISABLE_HOOKS || !this.$options.hooks) {
return;
}
const hookArgs = hookArguments && hookArguments[this.hookArgsName];
let projectDir = hookArgs && hookArgs.projectDir;
if (!projectDir && hookArgs) {
const candidate = (0, helpers_1.getValueFromNestedObject)(hookArgs, "projectDir");
projectDir = candidate && candidate.projectDir;
}
this.$logger.trace(`Project dir from hooksArgs is: ${projectDir}.`);
this.initialize(projectDir);
this.$logger.trace(traceMessage);
const results = [];
try {
for (const hooksDirectory of this.hooksDirectories) {
results.push(await this.executeHooksInDirectory(hooksDirectory, hookName, hookArguments));
}
const customHooks = this.getCustomHooksByName(hookName);
for (const hook of customHooks) {
results.push(await this.executeHook(this.$projectHelper.projectDir, hookName, hook, hookArguments));
}
}
catch (err) {
this.$logger.trace(`Failed during hook execution ${hookName}.`);
this.$errors.fail(err.message || err);
}
return _.flatten(results);
}
async executeHook(directoryPath, hookName, hook, hookArguments) {
hookArguments = hookArguments || {};
let result;
const relativePath = path.relative(directoryPath, hook.fullPath);
const trackId = relativePath.replace(new RegExp("\\" + path.sep, "g"), constants_1.AnalyticsEventLabelDelimiter);
let command = this.getSheBangInterpreter(hook);
let inProc = false;
if (!command) {
command = hook.fullPath;
if (path.extname(hook.fullPath).toLowerCase() === ".js") {
command = process.argv[0];
inProc = this.shouldExecuteInProcess(this.$fs.readText(hook.fullPath));
}
}
const startTime = this.$performanceService.now();
if (inProc) {
this.$logger.trace("Executing %s hook at location %s in-process", hookName, hook.fullPath);
const hookEntryPoint = require(hook.fullPath);
this.$logger.trace(`Validating ${hookName} arguments.`);
const invalidArguments = this.validateHookArguments(hookEntryPoint, hook.fullPath);
if (invalidArguments.length) {
this.$logger.warn(`${hook.fullPath} will NOT be executed because it has invalid arguments - ${color_1.color.grey(invalidArguments.join(", "))}.`);
return;
}
// HACK for backwards compatibility:
// In case $projectData wasn't resolved by the time we got here (most likely we got here without running a command but through a service directly)
// then it is probably passed as a hookArg
// if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector
// This helps make hooks stateless
const projectDataHookArg = hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"];
if (projectDataHookArg) {
hookArguments["projectData"] = hookArguments["$projectData"] = projectDataHookArg;
}
const maybePromise = this.$injector.resolve(hookEntryPoint, hookArguments);
if (maybePromise) {
this.$logger.trace("Hook promises to signal completion");
try {
result = await maybePromise;
}
catch (err) {
if (err &&
_.isBoolean(err.stopExecution) &&
err.errorAsWarning === true) {
this.$logger.warn(err.message || err);
}
else {
// Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles.
this.$logger.error(err);
throw err || new Error(`Failed to execute hook: ${hook.fullPath}.`);
}
}
this.$logger.trace("Hook completed");
}
}
else {
const environment = this.prepareEnvironment(hook.fullPath);
this.$logger.trace("Executing %s hook at location %s with environment ", hookName, hook.fullPath, environment);
const output = await this.$childProcess.spawnFromEvent(command, [hook.fullPath], "close", environment, { throwError: false });
result = output;
if (output.exitCode !== 0) {
throw new Error(output.stdout + output.stderr);
}
this.$logger.trace("Finished executing %s hook at location %s with environment ", hookName, hook.fullPath, environment);
}
const endTime = this.$performanceService.now();
this.$performanceService.processExecutionData(trackId, startTime, endTime, [
hookArguments,
]);
return result;
}
async executeHooksInDirectory(directoryPath, hookName, hookArguments) {
hookArguments = hookArguments || {};
const results = [];
const hooks = this.getHooksByName(directoryPath, hookName);
for (let i = 0; i < hooks.length; ++i) {
const hook = hooks[i];
const result = await this.executeHook(directoryPath, hookName, hook, hookArguments);
if (result) {
results.push(result);
}
}
return results;
}
getCustomHooksByName(hookName) {
const hooks = [];
const customHooks = this.$projectConfigService.getValue("hooks", []);
for (const cHook of customHooks) {
if (cHook.type === hookName) {
const fullPath = path.join(this.$projectHelper.projectDir, cHook.script);
const isFile = this.$fs.getFsStats(fullPath).isFile();
if (isFile) {
const fileNameParts = cHook.script.split("/");
hooks.push(new Hook(this.getBaseFilename(fileNameParts[fileNameParts.length - 1]), fullPath));
}
}
}
return hooks;
}
getHooksByName(directoryPath, hookName) {
const allBaseHooks = this.getHooksInDirectory(directoryPath);
const baseHooks = _.filter(allBaseHooks, (hook) => hook.name.toLowerCase() === hookName.toLowerCase());
const moreHooks = this.getHooksInDirectory(path.join(directoryPath, hookName));
return baseHooks.concat(moreHooks);
}
getHooksInDirectory(directoryPath) {
if (!this.cachedHooks[directoryPath]) {
let hooks = [];
if (directoryPath &&
this.$fs.exists(directoryPath) &&
this.$fs.getFsStats(directoryPath).isDirectory()) {
const directoryContent = this.$fs.readDirectory(directoryPath);
const files = _.filter(directoryContent, (entry) => {
const fullPath = path.join(directoryPath, entry);
const isFile = this.$fs.getFsStats(fullPath).isFile();
return isFile;
});
hooks = _.map(files, (file) => {
const fullPath = path.join(directoryPath, file);
return new Hook(this.getBaseFilename(file), fullPath);
});
}
this.cachedHooks[directoryPath] = hooks;
}
return this.cachedHooks[directoryPath];
}
prepareEnvironment(hookFullPath) {
const clientName = this.$staticConfig.CLIENT_NAME.toUpperCase();
const environment = {};
environment[util.format("%s-COMMANDLINE", clientName)] = process.argv.join(" ");
environment[util.format("%s-HOOK_FULL_PATH", clientName)] = hookFullPath;
environment[util.format("%s-VERSION", clientName)] = this.$staticConfig.version;
return {
cwd: this.$projectHelper.projectDir,
stdio: "inherit",
env: _.extend({}, process.env, environment),
};
}
getSheBangInterpreter(hook) {
let interpreter = null;
let shMatch = [];
const fileContent = this.$fs.readText(hook.fullPath);
if (fileContent) {
const sheBangMatch = fileContent
.split("\n")[0]
.match(/^#!(?:\/usr\/bin\/env )?([^\r\n]+)/m);
if (sheBangMatch) {
interpreter = sheBangMatch[1];
}
if (interpreter) {
// Likewise, make /usr/bin/bash work like "bash".
shMatch = interpreter.match(/bin\/((?:ba)?sh)$/);
}
if (shMatch) {
interpreter = shMatch[1];
}
}
return interpreter;
}
getBaseFilename(fileName) {
return fileName.substr(0, fileName.length - path.extname(fileName).length);
}
shouldExecuteInProcess(scriptSource) {
try {
const esprima = require("esprima");
const ast = esprima.parse(scriptSource);
let inproc = false;
ast.body.forEach((statement) => {
if (statement.type !== "ExpressionStatement" ||
statement.expression.type !== "AssignmentExpression") {
return;
}
const left = statement.expression.left;
if (left.type === "MemberExpression" &&
left.object &&
left.object.type === "Identifier" &&
left.object.name === "module" &&
left.property &&
left.property.type === "Identifier" &&
left.property.name === "exports") {
inproc = true;
}
});
return inproc;
}
catch (err) {
return false;
}
}
validateHookArguments(hookConstructor, hookFullPath) {
const invalidArguments = [];
// We need to annotate the hook in order to have the arguments of the constructor.
(0, helpers_1.annotate)(hookConstructor);
_.each(hookConstructor.$inject.args, (argument) => {
try {
if (argument !== this.hookArgsName) {
this.$injector.resolve(argument);
}
}
catch (err) {
this.$logger.trace(`Cannot resolve ${argument} of hook ${hookFullPath}, reason: ${err}`);
invalidArguments.push(argument);
}
});
return invalidArguments;
}
}
exports.HooksService = HooksService;
HooksService.HOOKS_DIRECTORY_NAME = "hooks";
__decorate([
(0, decorators_1.memoize)({
shouldCache() {
// only cache if we have hooks directories, the only case to
// not have hooks directories is when the project dir is
// not set yet, ie. when creating a project.
return !!this.hooksDirectories.length;
},
})
], HooksService.prototype, "initialize", null);
yok_1.injector.register("hooksService", HooksService);
//# sourceMappingURL=hooks-service.js.map