UNPKG

@adobe/aio-app-scripts

Version:

Utility tooling scripts to build, deploy and run an Adobe I/O App

412 lines (371 loc) 16.3 kB
#!/usr/bin/env node /* Copyright 2019 Adobe. All rights reserved. This file is licensed 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 REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* eslint-disable no-template-curly-in-string */ const BaseScript = require('../lib/abstract-script') const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-app-scripts:dev', { provider: 'debug' }) const path = require('path') const fs = require('fs-extra') const httpTerminator = require('http-terminator') const BuildActions = require('./build.actions') const DeployActions = require('./deploy.actions') const ActionLogs = require('./logs') const utils = require('../lib/utils') const EventPoller = require('../lib/poller') const { OW_JAR_FILE, OW_CONFIG_RUNTIMES_FILE, OW_JAR_URL, OW_LOCAL_APIHOST, OW_LOCAL_NAMESPACE, OW_LOCAL_AUTH } = require('../lib/owlocal') const execa = require('execa') const Bundler = require('parcel-bundler') const chokidar = require('chokidar') let running = false let changed = false let watcher const owWaitInitTime = 2000 const owWaitPeriodTime = 500 const owTimeout = 60000 const fetchLogInterval = 10000 const logOptions = {} const eventPoller = new EventPoller(fetchLogInterval) class ActionServer extends BaseScript { async run (args = [], options = {}) { // options /* parcel bundle options */ const bundleOptions = options.parcel || {} /* skip actions */ const skipActions = !!options.skipActions /* fetch logs for actions option */ const fetchLogs = options.fetchLogs || false const taskName = 'Local Dev Server' this.emit('start', taskName) // files // const OW_LOG_FILE = '.openwhisk-standalone.log' const DOTENV_SAVE = this._absApp('.env.app.save') const WSK_DEBUG_PROPS = this._absApp('.wskdebug.props.tmp') const CODE_DEBUG_SAVE = this._absApp('.vscode/launch.json.save') const CODE_DEBUG = this._absApp('.vscode/launch.json') // control variables const hasFrontend = this.config.app.hasFrontend const withBackend = this.config.app.hasBackend && !skipActions const isLocal = !this.config.actions.devRemote // applies only for backend // port for UI const uiPort = parseInt(args[0]) || parseInt(process.env.PORT) || 9080 let frontEndUrl = null // state const resources = {} let devConfig = this.config // config will be different if local or remote // bind cleanup function process.on('SIGINT', async () => { // in case app-scripts are eventually turned into a lib: // - don't exit the process, just make sure we get out of waiting // - unregister sigint and return properly (e.g. not waiting on stdin.resume anymore) try { await cleanup(resources) aioLogger.info('exiting!') process.exit(0) } catch (e) { aioLogger.error('unexpected error while cleaning up!') aioLogger.error(e) process.exit(1) } }) try { if (withBackend) { if (isLocal) { // take following steps only when we have a backend this.emit('progress', 'checking if java is installed...') if (!await utils.hasJavaCLI()) throw new Error('could not find java CLI, please make sure java is installed') this.emit('progress', 'checking if docker is installed...') if (!await utils.hasDockerCLI()) throw new Error('could not find docker CLI, please make sure docker is installed') this.emit('progress', 'checking if docker is running...') if (!await utils.isDockerRunning()) throw new Error('docker is not running, please make sure to start docker') if (!fs.existsSync(OW_JAR_FILE)) { this.emit('progress', `downloading OpenWhisk standalone jar from ${OW_JAR_URL} to ${OW_JAR_FILE}, this might take a while... (to be done only once!)`) await utils.downloadOWJar(OW_JAR_URL, OW_JAR_FILE) } this.emit('progress', 'starting local OpenWhisk stack..') const res = await utils.runOpenWhiskJar(OW_JAR_FILE, OW_CONFIG_RUNTIMES_FILE, OW_LOCAL_APIHOST, owWaitInitTime, owWaitPeriodTime, owTimeout, { stderr: 'inherit' }) resources.owProc = res.proc // case1: no dotenv file => expose local credentials in .env, delete on cleanup const dotenvFile = this._absApp('.env') if (!fs.existsSync(dotenvFile)) { this.emit('progress', 'writing temporary .env with local OpenWhisk guest credentials..') fs.writeFileSync(dotenvFile, `AIO_RUNTIME_NAMESPACE=${OW_LOCAL_NAMESPACE}\nAIO_RUNTIME_AUTH=${OW_LOCAL_AUTH}\nAIO_RUNTIME_APIHOST=${OW_LOCAL_APIHOST}`) resources.dotenv = dotenvFile } else { // case2: existing dotenv file => save .env & expose local credentials in .env, restore on cleanup this.emit('progress', `saving .env to ${DOTENV_SAVE} and writing new .env with local OpenWhisk guest credentials..`) utils.saveAndReplaceDotEnvCredentials(dotenvFile, DOTENV_SAVE, OW_LOCAL_APIHOST, OW_LOCAL_NAMESPACE, OW_LOCAL_AUTH) resources.dotenvSave = DOTENV_SAVE resources.dotenv = dotenvFile } // delete potentially conflicting env vars delete process.env.AIO_RUNTIME_APIHOST delete process.env.AIO_RUNTIME_NAMESPACE delete process.env.AIO_RUNTIME_AUTH devConfig = require('../lib/config-loader')() // reload config for local config } else { // check credentials utils.checkOpenWhiskCredentials(this.config) this.emit('progress', 'using remote actions') } // build and deploy actions this.emit('progress', 'redeploying actions..') await this._buildAndDeploy(devConfig, isLocal) watcher = chokidar.watch(devConfig.actions.src) watcher.on('change', this._getActionChangeHandler(devConfig, isLocal)) this.emit('progress', `writing credentials to tmp wskdebug config '${this._relApp(WSK_DEBUG_PROPS)}'..`) // prepare wskprops for wskdebug fs.writeFileSync(WSK_DEBUG_PROPS, `NAMESPACE=${devConfig.ow.namespace}\nAUTH=${devConfig.ow.auth}\nAPIHOST=${devConfig.ow.apihost}`) resources.wskdebugProps = WSK_DEBUG_PROPS } if (hasFrontend) { let urls = {} if (this.config.app.hasBackend) { // inject backend urls into ui // note the condition: we still write backend urls EVEN if skipActions is set // the urls will always point to remotely deployed actions if skipActions is set this.emit('progress', 'injecting backend urls into frontend config') urls = await utils.getActionUrls(devConfig, true, isLocal && !skipActions) } await utils.writeConfig(devConfig.web.injectedConfig, urls) this.emit('progress', 'starting local frontend server ..') const entryFile = path.join(devConfig.web.src, 'index.html') // our defaults here can be overridden by the bundleOptions passed in // bundleOptions.https are also passed to bundler.serve const parcelBundleOptions = { cache: false, outDir: devConfig.web.distDev, contentHash: false, watch: true, minify: false, logLevel: 1, ...bundleOptions } let actualPort = uiPort resources.uiBundler = new Bundler(entryFile, parcelBundleOptions) resources.uiServer = await resources.uiBundler.serve(uiPort, bundleOptions.https) actualPort = resources.uiServer.address().port resources.uiServerTerminator = httpTerminator.createHttpTerminator({ server: resources.uiServer }) if (actualPort !== uiPort) { this.emit('progress', `Could not use port:${uiPort}, using port:${actualPort} instead`) } frontEndUrl = `${bundleOptions.https ? 'https:' : 'http:'}//localhost:${actualPort}` this.emit('progress', `local frontend server running at ${frontEndUrl}`) } this.emit('progress', 'setting up vscode debug configuration files..') fs.ensureDirSync(path.dirname(CODE_DEBUG)) if (fs.existsSync(CODE_DEBUG)) { if (!fs.existsSync(CODE_DEBUG_SAVE)) { fs.moveSync(CODE_DEBUG, CODE_DEBUG_SAVE) resources.vscodeDebugConfigSave = CODE_DEBUG_SAVE } } fs.writeJSONSync(CODE_DEBUG, await this.generateVSCodeDebugConfig(devConfig, withBackend, hasFrontend, frontEndUrl, WSK_DEBUG_PROPS), { spaces: 2 }) resources.vscodeDebugConfig = CODE_DEBUG if (!resources.owProc && !resources.uiServer) { // not local + ow is not running => need to explicitely wait for CTRL+C // trick to avoid termination resources.dummyProc = execa('node') } this.emit('progress', 'press CTRL+C to terminate dev environment') if (this.config.app.hasBackend && fetchLogs) { // fetch action logs resources.stopFetchLogs = false eventPoller.onPoll(this.logListner) eventPoller.poll({ resources: resources, config: devConfig }) } } catch (e) { aioLogger.error('unexpected error, cleaning up...') await cleanup(resources) throw e } return frontEndUrl } async logListner (args) { const logScript = new ActionLogs(args.config) if (!args.resources.stopFetchLogs) { try { const ret = await logScript.run([], logOptions) logOptions.limit = 30 logOptions.startTime = ret.lastActivationTime } catch (e) { aioLogger.error('Error while fetching action logs ' + e) } finally { eventPoller.poll(args) } } } async generateVSCodeDebugConfig (devConfig, withBackend, hasFrontend, frontUrl, wskdebugProps) { const actionConfigNames = [] let actionConfigs = [] if (withBackend) { const packageName = devConfig.ow.package const manifestActions = devConfig.manifest.package.actions actionConfigs = Object.keys(manifestActions).map(an => { const name = `Action:${packageName}/${an}` actionConfigNames.push(name) const action = manifestActions[an] const actionPath = this._absApp(action.function) const config = { type: 'node', request: 'launch', name: name, runtimeExecutable: this._absApp('./node_modules/.bin/wskdebug'), env: { WSK_CONFIG_FILE: wskdebugProps }, timeout: 30000, // replaces remoteRoot with localRoot to get src files localRoot: this._absApp('.'), remoteRoot: '/code', outputCapture: 'std' } const actionFileStats = fs.lstatSync(actionPath) if (actionFileStats.isFile()) { // why is this condition here? } config.runtimeArgs = [ `${packageName}/${an}`, actionPath, '-v' ] if (actionFileStats.isDirectory()) { // take package.json.main or 'index.js' const zipMain = utils.getActionEntryFile(path.join(actionPath, 'package.json')) config.runtimeArgs[1] = path.join(actionPath, zipMain) } if (action.annotations && action.annotations['require-adobe-auth'] && devConfig.ow.apihost === 'https://adobeioruntime.net') { // NOTE: The require-adobe-auth annotation is a feature implemented in the // runtime plugin. The current implementation replaces the action by a sequence // and renames the action to __secured_<action>. The annotation will soon be // natively supported in Adobe I/O Runtime, at which point this condition won't // be needed anymore. /* instanbul ignore next */ config.runtimeArgs[0] = `${packageName}/__secured_${an}` } if (action.runtime) { config.runtimeArgs.push('--kind') config.runtimeArgs.push(action.runtime) } return config }) } const debugConfig = { configurations: actionConfigs, compounds: [{ name: 'Actions', configurations: actionConfigNames }] } if (hasFrontend) { debugConfig.configurations.push({ type: 'chrome', request: 'launch', name: 'Web', url: frontUrl, webRoot: devConfig.web.src, breakOnLoad: true, sourceMapPathOverrides: { '*': path.join(devConfig.web.distDev, '*') } }) debugConfig.compounds.push({ name: 'WebAndActions', configurations: ['Web'].concat(actionConfigNames) }) } return debugConfig } _getActionChangeHandler (devConfig, isLocalDev) { return async (filePath) => { if (running) { aioLogger.debug(`${filePath} has changed. Deploy in progress. This change will be deployed after completion of current deployment.`) changed = true return } running = true try { aioLogger.debug(`${filePath} has changed. Redeploying actions.`) await this._buildAndDeploy(devConfig, isLocalDev) aioLogger.debug('Deployment successfull.') } catch (err) { this.emit('progress', ' -> Error encountered while deploying actions. Stopping auto refresh.') aioLogger.debug(err) await watcher.close() } if (changed) { aioLogger.debug('Code changed during deployment. Triggering deploy again.') changed = running = false await this._getActionChangeHandler(devConfig)(devConfig.actions.src) } running = false } } async _buildAndDeploy (devConfig, isLocalDev) { await (new BuildActions(devConfig)).run() const entities = await (new DeployActions(devConfig)).run([], { isLocalDev }) if (entities.actions) { entities.actions.forEach(a => { this.emit('progress', ` -> ${a.url || a.name}`) }) } } } async function cleanup (resources) { if (watcher) { aioLogger.info('stopping action watcher...') await watcher.close() } if (resources.uiBundler) { aioLogger.info('stopping parcel watcher...') await resources.uiBundler.stop() } if (resources.uiServer && resources.uiServerTerminator) { aioLogger.info('stopping ui server...') // close server and kill any open connections await resources.uiServerTerminator.terminate() } if (resources.dotenv && resources.dotenvSave && fs.existsSync(resources.dotenvSave)) { aioLogger.info('restoring .env file...') fs.moveSync(resources.dotenvSave, resources.dotenv, { overwrite: true }) } else if (resources.dotenv && !resources.dotenvSave) { // if there was no save file it means .env was created aioLogger.info('deleting tmp .env file...') fs.removeSync(resources.dotenv) } if (resources.owProc) { aioLogger.info('stopping local OpenWhisk stack...') resources.owProc.kill() } if (resources.wskdebugProps) { aioLogger.info('removing wskdebug tmp credentials file...') fs.unlinkSync(resources.wskdebugProps) } if (resources.vscodeDebugConfig && !resources.vscodeDebugConfigSave) { aioLogger.info('removing .vscode/launch.json...') const vscodeDir = path.dirname(resources.vscodeDebugConfig) fs.unlinkSync(resources.vscodeDebugConfig) if (fs.readdirSync(vscodeDir).length === 0) { fs.rmdirSync(vscodeDir) } } if (resources.vscodeDebugConfigSave) { aioLogger.info('restoring previous .vscode/launch.json...') fs.moveSync(resources.vscodeDebugConfigSave, resources.vscodeDebugConfig, { overwrite: true }) } if (resources.dummyProc) { aioLogger.info('stopping sigint waiter...') resources.dummyProc.kill() } resources.stopFetchLogs = true } module.exports = ActionServer