| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| const fs = require('node:fs'); |
| const path = require('node:path'); |
| const execa = require('execa'); |
| |
| const cordovaUtil = require('../cordova/util'); |
| const scriptsFinder = require('./scriptsFinder'); |
| const Context = require('./Context'); |
| const { CordovaError, events } = require('cordova-common'); |
| |
| /** |
| * Tries to create a HooksRunner for passed project root. |
| * @constructor |
| */ |
| function HooksRunner (projectRoot) { |
| const root = cordovaUtil.isCordova(projectRoot); |
| if (!root) throw new CordovaError('Not a Cordova project ("' + projectRoot + '"), can\'t use hooks.'); |
| else this.projectRoot = root; |
| } |
| |
| /** |
| * Fires all event handlers and scripts for a passed hook type. |
| * Returns a promise. |
| */ |
| HooksRunner.prototype.fire = function fire (hook, opts) { |
| if (isHookDisabled(opts, hook)) { |
| return Promise.resolve('hook ' + hook + ' is disabled.'); |
| } |
| |
| // args check |
| if (!hook) { |
| throw new Error('hook type is not specified'); |
| } |
| opts = this.prepareOptions(opts); |
| |
| // first run hook event listeners, then run hook script files |
| return runJobsSerially([ |
| ...getEventHandlerJobs(hook, opts), |
| ...getHookJobs(hook, opts) |
| ]); |
| }; |
| |
| /** |
| * Refines passed options so that all required parameters are set. |
| */ |
| HooksRunner.prototype.prepareOptions = function (opts) { |
| opts = opts || {}; |
| opts.projectRoot = this.projectRoot; |
| opts.cordova = opts.cordova || {}; |
| opts.cordova.platforms = opts.cordova.platforms || opts.platforms || cordovaUtil.listPlatforms(opts.projectRoot); |
| opts.cordova.platforms = opts.cordova.platforms.map(function (platform) { return platform.split('@')[0]; }); |
| opts.cordova.plugins = opts.cordova.plugins || opts.plugins || cordovaUtil.findPlugins(path.join(opts.projectRoot, 'plugins')); |
| |
| try { |
| opts.cordova.version = opts.cordova.version || require('../../package').version; |
| } catch (ex) { |
| events.emit('warn', 'HooksRunner could not load package.json: ' + ex.message); |
| } |
| |
| return opts; |
| }; |
| |
| module.exports = HooksRunner; |
| |
| /** |
| * Executes hook event handlers serially. Doesn't require a HooksRunner to be constructed. |
| * Returns a promise. |
| */ |
| module.exports.fire = globalFire; |
| function globalFire (hook, opts) { |
| if (isHookDisabled(opts, hook)) { |
| return Promise.resolve('hook ' + hook + ' is disabled.'); |
| } |
| |
| opts = opts || {}; |
| return runJobsSerially(getEventHandlerJobs(hook, opts)); |
| } |
| |
| function getEventHandlerJobs (hook, opts) { |
| return events.listeners(hook) |
| .map(handler => () => handler(opts)); |
| } |
| |
| function getHookJobs (hook, opts) { |
| const scripts = scriptsFinder.getHookScripts(hook, opts); |
| |
| if (scripts.length === 0) { |
| events.emit('verbose', `No scripts found for hook "${hook}".`); |
| } |
| |
| const context = new Context(hook, opts); |
| return scripts.map(script => () => runScript(script, context)); |
| } |
| |
| function runJobsSerially (jobs) { |
| return jobs.reduce((acc, job) => acc.then(job), Promise.resolve()); |
| } |
| |
| /** |
| * Async runs single script file. |
| */ |
| function runScript (script, context) { |
| let source; |
| let relativePath; |
| |
| if (script.plugin) { |
| source = 'plugin ' + script.plugin.id; |
| relativePath = path.join('plugins', script.plugin.id, script.path); |
| } else { |
| source = 'config.xml'; |
| relativePath = path.normalize(script.path); |
| } |
| |
| events.emit('verbose', 'Executing script found in ' + source + ' for hook "' + context.hook + '": ' + relativePath); |
| |
| const runScriptStrategy = path.extname(script.path).toLowerCase() === '.js' |
| ? runScriptViaModuleLoader |
| : runScriptViaChildProcessSpawn; |
| |
| return runScriptStrategy(script, context); |
| } |
| |
| /** |
| * Runs script using require. |
| * Returns a promise. */ |
| function runScriptViaModuleLoader (script, context) { |
| if (!fs.existsSync(script.fullPath)) { |
| events.emit('warn', 'Script file does\'t exist and will be skipped: ' + script.fullPath); |
| return Promise.resolve(); |
| } |
| const scriptFn = require(script.fullPath); |
| context.scriptLocation = script.fullPath; |
| if (script.plugin) { |
| context.opts.plugin = script.plugin; |
| } |
| |
| // We can't run script if it is a plain Node script - it will run its commands when we require it. |
| // This is not a desired case as we want to pass context, but added for compatibility. |
| if (scriptFn instanceof Function) { |
| // If hook is async it can return promise instance and we will handle it. |
| return Promise.resolve(scriptFn(context)); |
| } else { |
| return Promise.resolve(); |
| } |
| } |
| |
| /** |
| * Runs script using child_process spawn method. |
| * Returns a promise. */ |
| function runScriptViaChildProcessSpawn (script, context) { |
| const opts = context.opts; |
| const command = script.fullPath; |
| const args = [opts.projectRoot]; |
| |
| const execOpts = { |
| cwd: opts.projectRoot, |
| stdio: 'inherit', |
| env: { |
| CORDOVA_VERSION: require('../../package').version, |
| CORDOVA_PLATFORMS: opts.platforms ? opts.platforms.join() : '', |
| CORDOVA_PLUGINS: opts.plugins ? opts.plugins.join() : '', |
| CORDOVA_HOOK: script.fullPath, |
| CORDOVA_CMDLINE: process.argv.join(' ') |
| } |
| }; |
| |
| events.emit('log', `Running hook: ${command} ${args.join(' ')}`); |
| |
| return execa(command, args, execOpts) |
| .then(data => data.stdout); |
| } |
| |
| /** |
| * Checks if the given hook type is disabled at the command line option. |
| * @param {Object} opts - the option object that contains command line options |
| * @param {String} hook - the hook type |
| * @returns {Boolean} return true if the passed hook is disabled. |
| */ |
| function isHookDisabled (opts, hook) { |
| if (opts === undefined || opts.nohooks === undefined) { |
| return false; |
| } |
| return opts.nohooks.some(pattern => hook.match(pattern) !== null); |
| } |