diff --git a/package.json b/package.json index d36af8aa..4026dd98 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,14 @@ "author": "Adobe Inc.", "bugs": "https://github.com/adobe/aio-cli-plugin-app/issues", "dependencies": { - "@adobe/aio-cli-lib-app-config": "^4", + "@adobe/aio-cli-lib-app-config": "^4.0.3", "@adobe/aio-cli-lib-console": "^5", "@adobe/aio-lib-core-config": "^5", "@adobe/aio-lib-core-logging": "^3", "@adobe/aio-lib-core-networking": "^5", "@adobe/aio-lib-env": "^3", "@adobe/aio-lib-ims": "^7", - "@adobe/aio-lib-runtime": "^7.0.0", + "@adobe/aio-lib-runtime": "^7.0.1", "@adobe/aio-lib-templates": "^3", "@adobe/aio-lib-web": "^7", "@adobe/generator-aio-app": "^7", diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 67ad8d45..c96fcbb6 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -18,10 +18,11 @@ const BaseCommand = require('../../BaseCommand') const BuildCommand = require('./build') const webLib = require('@adobe/aio-lib-web') const { Flags } = require('@oclif/core') -const { createWebExportFilter, runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo } = require('../../lib/app-helper') +const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExtensionMetadata, getCliInfo } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') const LogForwarding = require('../../lib/log-forwarding') const { sendAuditLogs, getAuditLogEvent, getFilesCountWithExtension } = require('../../lib/audit-logger') +const logActions = require('../../lib/log-actions') const PRE_DEPLOY_EVENT_REG = 'pre-deploy-event-reg' const POST_DEPLOY_EVENT_REG = 'post-deploy-event-reg' @@ -235,24 +236,9 @@ class Deploy extends BuildCommand { // log deployed resources if (deployedRuntimeEntities.actions && deployedRuntimeEntities.actions.length > 0) { - this.log(chalk.blue(chalk.bold('Your deployed actions:'))) - const web = deployedRuntimeEntities.actions.filter(createWebExportFilter(true)) - const nonWeb = deployedRuntimeEntities.actions.filter(createWebExportFilter(false)) - - if (web.length > 0) { - this.log('web actions:') - web.forEach(a => { - this.log(chalk.blue(chalk.bold(` -> ${a.url || a.name} `))) - }) - } - - if (nonWeb.length > 0) { - this.log('non-web actions:') - nonWeb.forEach(a => { - this.log(chalk.blue(chalk.bold(` -> ${a.url || a.name} `))) - }) - } + await logActions({ entities: deployedRuntimeEntities, log: (...rest) => this.log(chalk.bold(chalk.blue(...rest))) }) } + // TODO urls should depend on extension point, exc shell only for exc shell extension point - use a post-app-deploy hook ? if (deployedFrontendUrl) { this.log(chalk.blue(chalk.bold(`To view your deployed application:\n -> ${deployedFrontendUrl}`))) diff --git a/src/commands/app/undeploy.js b/src/commands/app/undeploy.js index 71ba3e66..feed4acc 100644 --- a/src/commands/app/undeploy.js +++ b/src/commands/app/undeploy.js @@ -112,9 +112,9 @@ class Undeploy extends BaseCommand { if (!script) { await rtLib.undeployActions(config) } - spinner.succeed(chalk.green(`Un-Deploying actions for ${extName}`)) + spinner.succeed(chalk.green(`Un-deploying actions for ${extName}`)) } catch (err) { - spinner.fail(chalk.green(`Un-Deploying actions for ${extName}`)) + spinner.fail(chalk.green(`Un-deploying actions for ${extName}`)) throw err } } else { diff --git a/src/lib/actions-watcher.js b/src/lib/actions-watcher.js index d92be398..e8b8202b 100644 --- a/src/lib/actions-watcher.js +++ b/src/lib/actions-watcher.js @@ -63,7 +63,13 @@ module.exports = async (watcherOptions) => { async function buildAndDeploy (watcherOptions, filterActions) { const { config, isLocal, log, inprocHook } = watcherOptions await buildActions(config, filterActions) - await deployActions(config, isLocal, log, filterActions, inprocHook) + const deployConfig = { + isLocalDev: isLocal, + filterEntities: { + actions: filterActions + } + } + await deployActions({ config, deployConfig, log, inprocHook }) } /** diff --git a/src/lib/app-helper.js b/src/lib/app-helper.js index 9b7c3c9e..ec58e01f 100644 --- a/src/lib/app-helper.js +++ b/src/lib/app-helper.js @@ -551,11 +551,13 @@ function deleteUserConfig (configData) { /** @private */ const createWebExportFilter = (filterValue) => { return (action) => { - if (!action || !action.annotations) { + if (!action) { return false } - return String(!!action.annotations['web-export']) === String(filterValue) + // if no annotations, its as if web-export = false + const webExportValue = action.annotations?.['web-export'] ?? false + return String(!!webExportValue) === String(filterValue) } } diff --git a/src/lib/deploy-actions.js b/src/lib/deploy-actions.js index 8bc1970d..fca533b5 100644 --- a/src/lib/deploy-actions.js +++ b/src/lib/deploy-actions.js @@ -1,5 +1,5 @@ /* -Copyright 2020 Adobe. All rights reserved. +Copyright 2024 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 @@ -10,60 +10,52 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { runInProcess, createWebExportFilter } = require('./app-helper') +const { runInProcess } = require('./app-helper') const { deployActions } = require('@adobe/aio-lib-runtime') +const logActions = require('./log-actions') /** * Deploys actions. * - * @param {object} config see src/lib/config-loader.js - * @param {boolean} isLocalDev default false, set to true if it's a local deploy - * @param {Function} [log] a log function - * @param {boolean} filter true if a filter by built actions is desired. + * @private + * @param {object} options + * @param {object} options.config see src/lib/config-loader.js + * @param {object} [options.deployConfig] see https://github.com/adobe/aio-lib-runtime/blob/master/README.md#typedefs + * @param {Function} [options.log] a log function + * @param {Function} [options.inprocHook] a hook function */ -/** @private */ -module.exports = async (config, isLocalDev = false, log = () => {}, filter = false, inprocHook) => { +module.exports = async ({ + config, + deployConfig = {}, + log = () => {}, + inprocHook +}) => { await runInProcess(config.hooks['pre-app-deploy'], config) - const script = await runInProcess(config.hooks['deploy-actions'], { config, options: { isLocalDev, filter } }) + + const hookFilterEntities = Array.isArray(deployConfig.filterEntities?.actions) ? deployConfig.filterEntities.actions : [] + const hookData = { + appConfig: config, + filterEntities: hookFilterEntities, + isLocalDev: deployConfig.isLocalDev + } + + let entities + const script = await runInProcess(config.hooks['deploy-actions'], hookData) if (!script) { - const deployConfig = { - isLocalDev, - filterEntities: { - byBuiltActions: filter - } - } if (inprocHook) { - const hookFilterEntities = Array.isArray(filter) ? filter : [] - const hookResults = await inprocHook('deploy-actions', { - appConfig: config, - filterEntities: hookFilterEntities, - isLocalDev - }) + const hookResults = await inprocHook('deploy-actions', hookData) if (hookResults?.failures?.length > 0) { // output should be "Error : : \n" for each failure log('Error: ' + hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: ')) throw new Error(`Hook 'deploy-actions' failed with ${hookResults.failures[0].error}`) } } - const entities = await deployActions(config, deployConfig, log) - if (entities.actions) { - const web = entities.actions.filter(createWebExportFilter(true)) - const nonWeb = entities.actions.filter(createWebExportFilter(false)) - if (web.length > 0) { - log('web actions:') - web.forEach(a => { - log(` -> ${a.url || a.name}`) - }) - } - - if (nonWeb.length > 0) { - log('non-web actions:') - nonWeb.forEach(a => { - log(` -> ${a.url || a.name}`) - }) - } - } + entities = await deployActions(config, deployConfig, log) + await logActions({ entities, log }) } + await runInProcess(config.hooks['post-app-deploy'], config) + + return { script, entities } } diff --git a/src/lib/log-actions.js b/src/lib/log-actions.js new file mode 100644 index 00000000..5620dd8b --- /dev/null +++ b/src/lib/log-actions.js @@ -0,0 +1,52 @@ +/* +Copyright 2024 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 REPRESENTATIONSTJ +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { createWebExportFilter } = require('./app-helper') + +/** + * Logs deployed action entities. + * + * @private + * @param {object} options + * @param {object} options.entities runtime entities that have been deployed + * @param {object} [options.deployConfig] see https://github.com/adobe/aio-lib-runtime?tab=readme-ov-file#typedefs + * @param {Function} [options.log] a log function + */ +module.exports = async ({ + entities, + log = console.log +}) => { + if (!entities.actions) { + return + } + + log('Your deployed actions:') + + const _web = entities.actions.filter(createWebExportFilter(true)) + const _webRaw = entities.actions.filter(createWebExportFilter('raw')) + const web = [..._web, ..._webRaw] + const nonWeb = entities.actions.filter(createWebExportFilter(false)) + + if (web.length > 0) { + log('web actions:') + web.forEach(a => { + log(` -> ${a.url || a.name}`) + }) + } + + if (nonWeb.length > 0) { + log('non-web actions:') + nonWeb.forEach(a => { + log(` -> ${a.url || a.name}`) + }) + } +} diff --git a/src/lib/run-dev.js b/src/lib/run-dev.js index 9c02130f..0d6771f0 100644 --- a/src/lib/run-dev.js +++ b/src/lib/run-dev.js @@ -122,7 +122,18 @@ async function runDev (config, dataDir, options = {}, log = () => {}, inprocHook // Deploy Phase - deploy actions if (withBackend) { log('redeploying actions..') - await deployActions(devConfig, isLocal, log, true, inprocHook) + const deployConfig = { + isLocalDev: isLocal, + filterEntities: { + byBuiltActions: true + } + } + await deployActions({ + config: devConfig, + deployConfig, + log, + inprocHook + }) } // Deploy Phase - serve the web UI diff --git a/test/commands/lib/app-helper.test.js b/test/commands/lib/app-helper.test.js index 3244fc88..8bd610bc 100644 --- a/test/commands/lib/app-helper.test.js +++ b/test/commands/lib/app-helper.test.js @@ -21,7 +21,6 @@ jest.mock('@adobe/aio-lib-core-networking', () => ({ jest.mock('@adobe/aio-lib-core-config') jest.mock('execa') -jest.mock('process') jest.mock('path') jest.mock('fs-extra') // do not touch the real fs jest.mock('@adobe/aio-lib-env') @@ -919,9 +918,25 @@ describe('createWebExportFilter', () => { const webFilter = appHelper.createWebExportFilter(true) const nonWebFilter = appHelper.createWebExportFilter(false) + test('null action', () => { + const action = null + + expect(webFilter(action)).toEqual(false) + expect(nonWebFilter(action)).toEqual(false) + }) + + test('no annotations', () => { + const action = { + name: 'abcde', url: 'https://fake.site' + } + + expect(webFilter(action)).toEqual(false) + expect(nonWebFilter(action)).toEqual(true) + }) + test('no web-export annotation', () => { const action = { - name: 'abcde', url: 'https://fake.site', annotations: [] + name: 'abcde', url: 'https://fake.site', annotations: {} } expect(webFilter(action)).toEqual(false) @@ -967,6 +982,17 @@ describe('createWebExportFilter', () => { expect(webFilter(action2)).toEqual(false) expect(nonWebFilter(action2)).toEqual(true) }) + + test('web-export: raw annotation', () => { + const action1 = { + name: 'abcde', + url: 'https://fake.site', + annotations: { 'web-export': 'raw' } + } + + expect(webFilter(action1)).toEqual(true) + expect(nonWebFilter(action1)).toEqual(false) + }) }) describe('object values', () => { diff --git a/test/commands/lib/deploy-actions.test.js b/test/commands/lib/deploy-actions.test.js index f3565b98..e1ad539c 100644 --- a/test/commands/lib/deploy-actions.test.js +++ b/test/commands/lib/deploy-actions.test.js @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ const deployActions = require('../../../src/lib/deploy-actions') -const utils = require('../../../src/lib/app-helper') const { deployActions: rtDeployActions } = require('@adobe/aio-lib-runtime') +const utils = require('../../../src/lib/app-helper') const appHelperActual = jest.requireActual('../../../src/lib/app-helper') jest.mock('../../../src/lib/app-helper') @@ -23,7 +23,6 @@ const createWebExportAnnotation = (value) => ({ beforeEach(() => { utils.runInProcess.mockReset() - // utils.runScript.mockReset() utils.createWebExportFilter.mockReset() rtDeployActions.mockReset() @@ -43,11 +42,12 @@ test('deploy-actions app hook available', async () => { } }) - await deployActions({ + const config = { hooks: { 'deploy-actions': 'deploy-actions' } - }) + } + await deployActions({ config }) expect(rtDeployActions).not.toHaveBeenCalled() expect(utils.runInProcess).toHaveBeenNthCalledWith(1, undefined, expect.any(Object)) // pre-app-deploy @@ -58,26 +58,35 @@ test('deploy-actions app hook available', async () => { test('it should deploy actions with filter param, (coverage)', async () => { utils.runInProcess.mockImplementation(() => false) - await deployActions({ + const config = { hooks: { 'deploy-actions': 'deploy-actions' - }, - filterByBuiltActions: true - }) + } + } + const deployConfig = { + filterEntities: { + byBuiltActions: true + } + } + await deployActions({ config, deployConfig }) expect(rtDeployActions).toHaveBeenCalled() expect(rtDeployActions).toHaveBeenCalledWith( expect.objectContaining({ - hooks: { 'deploy-actions': 'deploy-actions' }, - filterByBuiltActions: true + hooks: { 'deploy-actions': 'deploy-actions' } + }), + expect.objectContaining({ + filterEntities: { + byBuiltActions: true + } }), - expect.any(Object), expect.any(Function) ) }) test('no deploy-actions app hook available (use inbuilt)', async () => { - await deployActions({ hooks: {} }) + const config = { hooks: {} } + await deployActions({ config }) expect(rtDeployActions).toHaveBeenCalled() expect(utils.runInProcess).toHaveBeenNthCalledWith(1, undefined, expect.any(Object)) // pre-app-deploy expect(utils.runInProcess).toHaveBeenNthCalledWith(2, undefined, expect.any(Object)) // deploy-actions @@ -86,7 +95,12 @@ test('no deploy-actions app hook available (use inbuilt)', async () => { test('call inprocHook no filter', async () => { const mockHook = jest.fn() - await deployActions({ hooks: {} }, false, null, false, mockHook) + const config = { hooks: {} } + const deployConfig = { + isLocalDev: false, + filterEntities: {} + } + await deployActions({ config, deployConfig, log: null, inprocHook: mockHook }) expect(mockHook).toHaveBeenCalledWith('deploy-actions', expect.objectContaining({ appConfig: { hooks: {} }, filterEntities: [], @@ -100,7 +114,14 @@ test('call inprocHook no filter', async () => { test('call inprocHook with filter : isLocalDev false', async () => { const mockHook = jest.fn() - await deployActions({ hooks: {} }, false, null, ['boomer'], mockHook) + const config = { hooks: {} } + const deployConfig = { + isLocalDev: false, + filterEntities: { + actions: ['boomer'] + } + } + await deployActions({ config, deployConfig, log: null, inprocHook: mockHook }) expect(mockHook).toHaveBeenCalledWith('deploy-actions', expect.objectContaining({ appConfig: { hooks: {} }, filterEntities: ['boomer'], @@ -114,7 +135,14 @@ test('call inprocHook with filter : isLocalDev false', async () => { test('call inprocHook with filter : isLocalDev true', async () => { const mockHook = jest.fn() - await deployActions({ hooks: {} }, true, null, ['action-1', 'action-2'], mockHook) + const config = { hooks: {} } + const deployConfig = { + isLocalDev: true, + filterEntities: { + actions: ['action-1', 'action-2'] + } + } + await deployActions({ config, deployConfig, log: null, inprocHook: mockHook }) expect(mockHook).toHaveBeenCalledWith('deploy-actions', expect.objectContaining({ appConfig: { hooks: {} }, filterEntities: ['action-1', 'action-2'], @@ -132,7 +160,15 @@ test('throws if hook returns failures', async () => { failures: [{ plugin: { name: 'ifailedu' }, error: 'some error' }] }) const mockLog = jest.fn() - await expect(deployActions({ hooks: {} }, true, mockLog, ['action-1', 'action-2'], mockHook)).rejects.toThrow('Hook \'deploy-actions\' failed with some error') + const config = { hooks: {} } + const deployConfig = { + isLocalDev: true, + filterEntities: { + actions: ['action-1', 'action-2'] + } + } + await expect(deployActions({ config, deployConfig, log: mockLog, inprocHook: mockHook })) + .rejects.toThrow('Hook \'deploy-actions\' failed with some error') expect(mockHook).toHaveBeenCalledWith('deploy-actions', expect.objectContaining({ appConfig: { hooks: {} }, filterEntities: ['action-1', 'action-2'], @@ -151,7 +187,8 @@ test('use default parameters (coverage)', async () => { ] }) - await deployActions({ hooks: {} }) + const config = { hooks: {} } + await deployActions({ config }) expect(rtDeployActions).toHaveBeenCalled() expect(utils.runInProcess).toHaveBeenNthCalledWith(1, undefined, expect.any(Object)) // pre-app-deploy @@ -167,7 +204,12 @@ test('should log actions url or name when actions are deployed (web-export: true ] }) const log = jest.fn() - await deployActions({ hooks: {} }, false, log) + const config = { hooks: {} } + const deployConfig = { + isLocalDev: false, + filterEntities: {} + } + await deployActions({ config, deployConfig, log }) expect(log).toHaveBeenCalledWith(expect.stringContaining('web actions:')) expect(log).toHaveBeenCalledWith(expect.stringContaining('https://fake.com/action')) @@ -181,16 +223,29 @@ test('should log actions url or name when actions are deployed (web-export: fals { name: 'pkg/actionNoUrl', ...createWebExportAnnotation(false) } ] }) - const log = jest.fn() - await deployActions({ hooks: {} }, false, log) - - expect(log).toHaveBeenCalledWith(expect.stringContaining('non-web actions:')) - expect(log).toHaveBeenCalledWith(expect.stringContaining('https://fake.com/action')) - expect(log).toHaveBeenCalledWith(expect.stringContaining('pkg/actionNoUrl')) - - log.mockReset() - await deployActions({ hooks: {} }, false) // empty logger - expect(log).not.toHaveBeenCalledWith(expect.stringContaining('non-web actions:')) - expect(log).not.toHaveBeenCalledWith(expect.stringContaining('https://fake.com/action')) - expect(log).not.toHaveBeenCalledWith(expect.stringContaining('pkg/actionNoUrl')) + { + const log = jest.fn() + const config = { hooks: {} } + const deployConfig = { + isLocalDev: false, + filterEntities: {} + } + await deployActions({ config, deployConfig, log }) + + expect(log).toHaveBeenCalledWith(expect.stringContaining('non-web actions:')) + expect(log).toHaveBeenCalledWith(expect.stringContaining('https://fake.com/action')) + expect(log).toHaveBeenCalledWith(expect.stringContaining('pkg/actionNoUrl')) + } + { + const log = jest.fn() + const config = { hooks: {} } + const deployConfig = { + isLocalDev: false, + filterEntities: {} + } + await deployActions({ config, deployConfig }) // empty logger + expect(log).not.toHaveBeenCalledWith(expect.stringContaining('non-web actions:')) + expect(log).not.toHaveBeenCalledWith(expect.stringContaining('https://fake.com/action')) + expect(log).not.toHaveBeenCalledWith(expect.stringContaining('pkg/actionNoUrl')) + } }) diff --git a/test/commands/lib/log-actions.test.js b/test/commands/lib/log-actions.test.js new file mode 100644 index 00000000..9612d2aa --- /dev/null +++ b/test/commands/lib/log-actions.test.js @@ -0,0 +1,82 @@ +/* +Copyright 2024 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. +*/ + +const logActions = require('../../../src/lib/log-actions') + +beforeEach(() => { +}) + +test('exports', () => { + expect(typeof logActions).toEqual('function') +}) + +test('default log', async () => { + const log = jest.fn() + const entities = { + actions: [ + {} // non-web action + ] + } + + await logActions({ entities }) + expect(log).not.toHaveBeenCalled() +}) + +test('no actions', async () => { + const log = jest.fn() + const entities = {} // no actions + + await logActions({ entities, log }) + expect(log).not.toHaveBeenCalled() +}) + +test('one web action (truthy), one web action (raw)', async () => { + const log = jest.fn() + const entities = { + actions: [ + { annotations: { 'web-export': true } }, // web action + { annotations: { 'web-export': 'raw' } } // web action + ] + } + + await logActions({ entities, log }) + expect(log).toHaveBeenCalledWith('web actions:') + expect(log).not.toHaveBeenCalledWith('non-web actions:') +}) + +test('one non-web action', async () => { + const log = jest.fn() + const entities = { + actions: [ + {} // non-web action + ] + } + + await logActions({ entities, log }) + expect(log).not.toHaveBeenCalledWith('web actions:') + expect(log).toHaveBeenCalledWith('non-web actions:') +}) + +test('two web actions, one non-web action', async () => { + const log = jest.fn() + const entities = { + actions: [ + { annotations: { 'web-export': true } }, // web action + {}, // non-web action + { annotations: { 'web-export': 'raw' } } // web action + ] + } + + await logActions({ entities, log }) + expect(log).toHaveBeenCalledWith('web actions:') + expect(log).toHaveBeenCalledWith('non-web actions:') +})