UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

316 lines • 14.6 kB
"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