@adobe/aio-cli-plugin-app 10.1.1 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,7 @@ DESCRIPTION
71
71
  Create, run, test, and deploy Adobe I/O Apps
72
72
  ```
73
73
 
74
- _See code: [src/commands/app/index.js](https://github.com/adobe/aio-cli-plugin-app/blob/10.1.1/src/commands/app/index.js)_
74
+ _See code: [src/commands/app/index.js](https://github.com/adobe/aio-cli-plugin-app/blob/10.2.0/src/commands/app/index.js)_
75
75
 
76
76
  ## `aio app add`
77
77
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "10.1.1",
2
+ "version": "10.2.0",
3
3
  "commands": {
4
4
  "app:build": {
5
5
  "id": "app:build",
@@ -237,6 +237,13 @@
237
237
  "type": "boolean",
238
238
  "description": "[default: true] Update log forwarding configuration on server",
239
239
  "allowNo": true
240
+ },
241
+ "feature-event-hooks": {
242
+ "name": "feature-event-hooks",
243
+ "type": "boolean",
244
+ "description": "[default: false] Enable event hooks feature",
245
+ "hidden": true,
246
+ "allowNo": true
240
247
  }
241
248
  },
242
249
  "args": {}
@@ -492,6 +499,46 @@
492
499
  }
493
500
  }
494
501
  },
502
+ "app:install": {
503
+ "id": "app:install",
504
+ "description": "(Pre-release) This command will support installing apps packaged by '<%= config.bin %> app pack'.\n",
505
+ "strict": true,
506
+ "pluginName": "@adobe/aio-cli-plugin-app",
507
+ "pluginAlias": "@adobe/aio-cli-plugin-app",
508
+ "pluginType": "core",
509
+ "hidden": true,
510
+ "aliases": [],
511
+ "flags": {
512
+ "verbose": {
513
+ "name": "verbose",
514
+ "type": "boolean",
515
+ "char": "v",
516
+ "description": "Verbose output",
517
+ "allowNo": false
518
+ },
519
+ "version": {
520
+ "name": "version",
521
+ "type": "boolean",
522
+ "description": "Show version",
523
+ "allowNo": false
524
+ },
525
+ "output": {
526
+ "name": "output",
527
+ "type": "option",
528
+ "char": "o",
529
+ "description": "The packaged app output folder path",
530
+ "multiple": false,
531
+ "default": "."
532
+ }
533
+ },
534
+ "args": {
535
+ "path": {
536
+ "name": "path",
537
+ "description": "Path to the app package to install",
538
+ "required": true
539
+ }
540
+ }
541
+ },
495
542
  "app:logs": {
496
543
  "id": "app:logs",
497
544
  "description": "Fetch logs for an Adobe I/O App\n",
@@ -572,6 +619,46 @@
572
619
  },
573
620
  "args": {}
574
621
  },
622
+ "app:pack": {
623
+ "id": "app:pack",
624
+ "description": "(Pre-release) This command will support packaging apps for redistribution.\n",
625
+ "strict": true,
626
+ "pluginName": "@adobe/aio-cli-plugin-app",
627
+ "pluginAlias": "@adobe/aio-cli-plugin-app",
628
+ "pluginType": "core",
629
+ "hidden": true,
630
+ "aliases": [],
631
+ "flags": {
632
+ "verbose": {
633
+ "name": "verbose",
634
+ "type": "boolean",
635
+ "char": "v",
636
+ "description": "Verbose output",
637
+ "allowNo": false
638
+ },
639
+ "version": {
640
+ "name": "version",
641
+ "type": "boolean",
642
+ "description": "Show version",
643
+ "allowNo": false
644
+ },
645
+ "output": {
646
+ "name": "output",
647
+ "type": "option",
648
+ "char": "o",
649
+ "description": "The packaged app output file path",
650
+ "multiple": false,
651
+ "default": "app.zip"
652
+ }
653
+ },
654
+ "args": {
655
+ "path": {
656
+ "name": "path",
657
+ "description": "Path to the app directory to package",
658
+ "default": "."
659
+ }
660
+ }
661
+ },
575
662
  "app:run": {
576
663
  "id": "app:run",
577
664
  "description": "Run an Adobe I/O App",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adobe/aio-cli-plugin-app",
3
3
  "description": "Create, Build and Deploy Adobe I/O Applications",
4
- "version": "10.1.1",
4
+ "version": "10.2.0",
5
5
  "author": "Adobe Inc.",
6
6
  "bugs": "https://github.com/adobe/aio-cli-plugin-app/issues",
7
7
  "dependencies": {
@@ -14,35 +14,38 @@
14
14
  "@adobe/aio-lib-ims": "^6.0.0",
15
15
  "@adobe/aio-lib-runtime": "^5.0.0",
16
16
  "@adobe/aio-lib-templates": "^2.2.0",
17
- "@adobe/aio-lib-web": "^6.0.1",
17
+ "@adobe/aio-lib-web": "^6.1.0",
18
18
  "@adobe/generator-aio-app": "^5.1.0",
19
19
  "@adobe/generator-app-common-lib": "^0.3.3",
20
20
  "@adobe/inquirer-table-checkbox": "^1.2.0",
21
21
  "@oclif/core": "^1.15.0",
22
22
  "@parcel/core": "^2.7.0",
23
23
  "@parcel/reporter-cli": "^2.7.0",
24
- "ajv": "^6",
24
+ "ajv": "^8",
25
+ "ajv-formats": "^2.1.1",
26
+ "archiver": "^5.3.1",
25
27
  "chalk": "^4",
26
28
  "chokidar": "^3.5.2",
27
29
  "debug": "^4.1.1",
28
30
  "dedent-js": "^1.0.1",
29
31
  "dotenv": "^16",
30
32
  "execa": "^5.0.0",
31
- "fs-extra": "^10",
33
+ "fs-extra": "^11.1.1",
32
34
  "get-port": "^5",
33
35
  "hjson": "^3.2.1",
34
36
  "http-terminator": "^3",
35
37
  "hyperlinker": "^1.0.0",
36
38
  "inquirer": "^8",
37
- "js-yaml": "^4",
39
+ "js-yaml": "^4.1.0",
38
40
  "lodash.clonedeep": "^4.5.0",
39
41
  "node-fetch": "^2.6.7",
40
42
  "ora": "^5",
41
43
  "pure-http": "^3",
42
44
  "serve-static": "^1.14.1",
43
45
  "term-size": "^2.2.1",
46
+ "unzipper": "^0.10.11",
44
47
  "upath": "^2",
45
- "which": "^2.0.1",
48
+ "which": "^3.0.0",
46
49
  "yeoman-environment": "^3.2.0"
47
50
  },
48
51
  "devDependencies": {
@@ -61,7 +64,6 @@
61
64
  "eslint-plugin-node": "^11.1.0",
62
65
  "eslint-plugin-promise": "^6.0.0",
63
66
  "jest": "^29.5.0",
64
- "jest-plugin-fs": "^2.9.0",
65
67
  "nock": "^13.2.9",
66
68
  "oclif": "^3.2.0",
67
69
  "stdout-stderr": "^0.1.9"
@@ -102,7 +102,7 @@
102
102
  },
103
103
  "ims_org_id": {
104
104
  "type": "string",
105
- "format": "email"
105
+ "pattern": "^(\\w+)@(\\w+)"
106
106
  }
107
107
  },
108
108
  "required": [ "id", "name", "ims_org_id" ]
@@ -165,6 +165,10 @@ class BaseCommand extends Command {
165
165
  get appVersion () {
166
166
  return this.pjson.version
167
167
  }
168
+
169
+ preRelease () {
170
+ this.log(chalk.yellow('Pre-release warning: This command is in pre-release, and not suitable for production.'))
171
+ }
168
172
  }
169
173
 
170
174
  BaseCommand.flags = {
@@ -152,6 +152,8 @@ class TemplatesCommand extends AddCommand {
152
152
 
153
153
  if (templates.length <= 0) {
154
154
  aioLogger.debug('installTemplates: standalone-app')
155
+ // technically runHook can quietly fail, but we are choosing to ignore it as these are telemetry events
156
+ // and not mission critical
155
157
  await this.config.runHook('telemetry', { data: 'installTemplates:standalone-app' })
156
158
  } else {
157
159
  aioLogger.debug(`installTemplates: ${templates}`)
@@ -143,6 +143,15 @@ class Deploy extends BuildCommand {
143
143
 
144
144
  try {
145
145
  await runScript(config.hooks['pre-app-deploy'])
146
+
147
+ if (flags['feature-event-hooks']) {
148
+ this.log('feature-event-hooks is enabled, running pre-deploy-event-reg hook')
149
+ const hookResults = await this.config.runHook('pre-deploy-event-reg', { appConfig: config })
150
+ if (hookResults?.failures?.length > 0) {
151
+ // output should be "Error : <plugin-name> : <error-message>\n" for each failure
152
+ this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
153
+ }
154
+ }
146
155
  } catch (err) {
147
156
  this.log(err)
148
157
  }
@@ -158,11 +167,15 @@ class Deploy extends BuildCommand {
158
167
  try {
159
168
  const script = await runScript(config.hooks['deploy-actions'])
160
169
  if (!script) {
161
- await this.config.runHook('deploy-actions', {
170
+ const hookResults = await this.config.runHook('deploy-actions', {
162
171
  appConfig: config,
163
172
  filterEntities: filterActions || [],
164
173
  isLocalDev: false
165
174
  })
175
+ if (hookResults?.failures?.length > 0) {
176
+ // output should be "Error : <plugin-name> : <error-message>\n" for each failure
177
+ this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
178
+ }
166
179
  deployedRuntimeEntities = await rtLib.deployActions(config, { filterEntities }, onProgress)
167
180
  }
168
181
 
@@ -239,6 +252,14 @@ class Deploy extends BuildCommand {
239
252
 
240
253
  try {
241
254
  await runScript(config.hooks['post-app-deploy'])
255
+ if (flags['feature-event-hooks']) {
256
+ this.log('feature-event-hooks is enabled, running post-deploy-event-reg hook')
257
+ const hookResults = await this.config.runHook('post-deploy-event-reg', { appConfig: config })
258
+ if (hookResults?.failures?.length > 0) {
259
+ // output should be "Error : <plugin-name> : <error-message>\n" for each failure
260
+ this.error(hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '), { exit: 1 })
261
+ }
262
+ }
242
263
  } catch (err) {
243
264
  this.log(err)
244
265
  }
@@ -341,6 +362,12 @@ Deploy.flags = {
341
362
  description: '[default: true] Update log forwarding configuration on server',
342
363
  default: true,
343
364
  allowNo: true
365
+ }),
366
+ 'feature-event-hooks': Flags.boolean({
367
+ description: '[default: false] Enable event hooks feature',
368
+ default: false,
369
+ allowNo: true,
370
+ hidden: true
344
371
  })
345
372
  }
346
373
 
@@ -0,0 +1,166 @@
1
+ /*
2
+ Copyright 2023 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ const BaseCommand = require('../../BaseCommand')
14
+ const { Flags } = require('@oclif/core')
15
+ const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:install', { provider: 'debug' })
16
+ const path = require('node:path')
17
+ const fs = require('fs-extra')
18
+ const execa = require('execa')
19
+ const unzipper = require('unzipper')
20
+ const { validateJsonWithSchema } = require('../../lib/install-helper')
21
+ const jsYaml = require('js-yaml')
22
+ const { USER_CONFIG_FILE, DEPLOY_CONFIG_FILE } = require('../../lib/defaults')
23
+ const ora = require('ora')
24
+ const chalk = require('chalk')
25
+
26
+ class InstallCommand extends BaseCommand {
27
+ async run () {
28
+ const { args, flags } = await this.parse(InstallCommand)
29
+
30
+ this.preRelease()
31
+
32
+ aioLogger.debug(`flags: ${JSON.stringify(flags, null, 2)}`)
33
+ aioLogger.debug(`args: ${JSON.stringify(args, null, 2)}`)
34
+
35
+ // resolve to absolute path before any chdir
36
+ args.path = path.resolve(args.path)
37
+ aioLogger.debug(`args.path (resolved): ${args.path}`)
38
+
39
+ let outputPath = flags.output
40
+ // change the cwd if necessary
41
+ if (outputPath !== '.') {
42
+ outputPath = path.resolve(flags.output)
43
+ // TODO: confirm if dir exists for overwrite
44
+ await fs.ensureDir(outputPath)
45
+ process.chdir(outputPath)
46
+ aioLogger.debug(`changed current working directory to: ${outputPath}`)
47
+ }
48
+
49
+ try {
50
+ await this.validateZipDirectoryStructure(args.path)
51
+ await this.unzipFile(args.path, outputPath)
52
+ await this.validateConfig(outputPath, USER_CONFIG_FILE)
53
+ await this.validateConfig(outputPath, DEPLOY_CONFIG_FILE)
54
+ await this.npmInstall(flags.verbose)
55
+ await this.runTests()
56
+ this.spinner.succeed('Install done.')
57
+ } catch (e) {
58
+ this.spinner.fail(e.message)
59
+ this.error(flags.verbose ? e : e.message)
60
+ }
61
+ }
62
+
63
+ get spinner () {
64
+ if (!this._spinner) {
65
+ this._spinner = ora()
66
+ }
67
+ return this._spinner
68
+ }
69
+
70
+ diffArray (expected, actual) {
71
+ const _expected = expected ?? []
72
+ const _actual = actual ?? []
73
+ return _expected.filter(item => !_actual.includes(item))
74
+ }
75
+
76
+ async validateZipDirectoryStructure (zipFilePath) {
77
+ aioLogger.debug(`validateZipDirectoryStructure: ${zipFilePath}`)
78
+
79
+ const expectedFiles = [USER_CONFIG_FILE, DEPLOY_CONFIG_FILE, 'package.json']
80
+ const foundFiles = []
81
+
82
+ this.spinner.start(`Validating integrity of app package at ${zipFilePath}...`)
83
+
84
+ const zip = fs.createReadStream(zipFilePath).pipe(unzipper.Parse({ forceStream: true }))
85
+ for await (const entry of zip) {
86
+ const fileName = entry.path
87
+
88
+ if (expectedFiles.includes(fileName)) {
89
+ foundFiles.push(fileName)
90
+ }
91
+ entry.autodrain()
92
+ }
93
+
94
+ const diff = this.diffArray(expectedFiles, foundFiles)
95
+ if (diff.length > 0) {
96
+ throw new Error(`The app package ${zipFilePath} is missing these files: ${JSON.stringify(diff, null, 2)}`)
97
+ }
98
+ this.spinner.succeed(`Validated integrity of app package at ${zipFilePath}`)
99
+ }
100
+
101
+ async unzipFile (zipFilePath, destFolderPath) {
102
+ aioLogger.debug(`unzipFile: ${zipFilePath} to be extracted to ${destFolderPath}`)
103
+
104
+ this.spinner.start(`Extracting app package to ${destFolderPath}...`)
105
+ return unzipper.Open.file(zipFilePath)
106
+ .then((d) => d.extract({ path: destFolderPath }))
107
+ .then(() => this.spinner.succeed(`Extracted app package to ${destFolderPath}`))
108
+ }
109
+
110
+ async validateConfig (outputPath, configFileName, configFilePath = path.join(outputPath, configFileName)) {
111
+ this.spinner.start(`Validating ${configFileName}...`)
112
+ aioLogger.debug(`validateConfig: ${configFileName} at ${configFilePath}`)
113
+
114
+ const configFileJson = jsYaml.load(fs.readFileSync(configFilePath).toString())
115
+ const { valid, errors } = validateJsonWithSchema(configFileJson, configFileName)
116
+ if (!valid) {
117
+ throw new Error(`Missing or invalid keys in ${configFileName}: ${JSON.stringify(errors, null, 2)}`)
118
+ } else {
119
+ this.spinner.succeed(`Validated ${configFileName}`)
120
+ }
121
+ }
122
+
123
+ async npmInstall (isVerbose) {
124
+ this.spinner.start('Running npm install...')
125
+ const stdio = isVerbose ? 'inherit' : 'ignore'
126
+ return execa('npm', ['install'], { stdio })
127
+ .then(() => {
128
+ this.spinner.succeed('Ran npm install')
129
+ })
130
+ }
131
+
132
+ async runTests (isVerbose) {
133
+ this.spinner.start('Running app tests...')
134
+ return this.config.runCommand('app:test').then((result) => {
135
+ if (result === 0) { // success
136
+ this.spinner.succeed('App tests passed')
137
+ } else {
138
+ throw new Error('App tests failed')
139
+ }
140
+ })
141
+ }
142
+ }
143
+
144
+ InstallCommand.hidden = true // hide from help for pre-release
145
+
146
+ InstallCommand.description = chalk.yellow(`(Pre-release) This command will support installing apps packaged by '<%= config.bin %> app pack'.
147
+ `)
148
+
149
+ InstallCommand.flags = {
150
+ ...BaseCommand.flags,
151
+ output: Flags.string({
152
+ description: 'The packaged app output folder path',
153
+ char: 'o',
154
+ default: '.'
155
+ })
156
+ }
157
+
158
+ InstallCommand.args = [
159
+ {
160
+ name: 'path',
161
+ description: 'Path to the app package to install',
162
+ required: true
163
+ }
164
+ ]
165
+
166
+ module.exports = InstallCommand
@@ -0,0 +1,296 @@
1
+ /*
2
+ Copyright 2023 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ Unless required by applicable law or agreed to in writing, software distributed under
7
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8
+ OF ANY KIND, either express or implied. See the License for the specific language
9
+ governing permissions and limitations under the License.
10
+ */
11
+
12
+ const BaseCommand = require('../../BaseCommand')
13
+ const { Flags } = require('@oclif/core')
14
+ const path = require('node:path')
15
+ const fs = require('fs-extra')
16
+ const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:pack', { provider: 'debug' })
17
+ const archiver = require('archiver')
18
+ const yaml = require('js-yaml')
19
+ const execa = require('execa')
20
+ const { loadConfigFile, writeFile } = require('../../lib/import-helper')
21
+ const { getObjectValue } = require('../../lib/app-helper')
22
+ const ora = require('ora')
23
+ const chalk = require('chalk')
24
+
25
+ const DEFAULTS = {
26
+ OUTPUT_ZIP_FILE: 'app.zip',
27
+ ARTIFACTS_FOLDER: 'app-package',
28
+ DEPLOY_YAML_FILE: 'deploy.yaml'
29
+ }
30
+
31
+ class Pack extends BaseCommand {
32
+ async run () {
33
+ const { args, flags } = await this.parse(Pack)
34
+
35
+ this.preRelease()
36
+
37
+ aioLogger.debug(`flags: ${JSON.stringify(flags, null, 2)}`)
38
+ aioLogger.debug(`args: ${JSON.stringify(args, null, 2)}`)
39
+
40
+ const appConfig = this.getFullConfig()
41
+
42
+ // resolve to absolute path before any chdir
43
+ const outputZipFile = path.resolve(flags.output)
44
+
45
+ // change the cwd if necessary
46
+ if (args.path !== '.') {
47
+ const resolvedPath = path.resolve(args.path)
48
+ process.chdir(resolvedPath)
49
+ aioLogger.debug(`changed current working directory to: ${resolvedPath}`)
50
+ }
51
+
52
+ try {
53
+ // 1. create artifacts phase
54
+ this.spinner.start(`Creating package artifacts folder '${DEFAULTS.ARTIFACTS_FOLDER}'...`)
55
+ await fs.emptyDir(DEFAULTS.ARTIFACTS_FOLDER)
56
+ this.spinner.succeed(`Created package artifacts folder '${DEFAULTS.ARTIFACTS_FOLDER}'`)
57
+
58
+ // ACNA-2038
59
+ // not artifacts folder should exist before we fire the event
60
+ await this.config.runHook('pre-pack', { appConfig, artifactsFolder: DEFAULTS.ARTIFACTS_FOLDER })
61
+
62
+ // 2. copy files to package phase
63
+ this.spinner.start('Copying project files...')
64
+ const fileList = await this.filesToPack([flags.output])
65
+ await this.copyPackageFiles(DEFAULTS.ARTIFACTS_FOLDER, fileList)
66
+ this.spinner.succeed('Copied project files')
67
+
68
+ // 3. add/modify artifacts phase
69
+ this.spinner.start('Creating configuration files...')
70
+ await this.createDeployYamlFile(appConfig)
71
+ this.spinner.succeed('Created configuration files')
72
+
73
+ this.spinner.start('Adding code-download annotations...')
74
+ await this.addCodeDownloadAnnotation(appConfig)
75
+ this.spinner.succeed('Added code-download annotations')
76
+
77
+ // doing this before zip so other things can be added to the zip
78
+ await this.config.runHook('post-pack', { appConfig, artifactsFolder: DEFAULTS.ARTIFACTS_FOLDER })
79
+
80
+ // 4. zip package phase
81
+ this.spinner.start(`Zipping package artifacts folder '${DEFAULTS.ARTIFACTS_FOLDER}' to '${outputZipFile}'...`)
82
+ await fs.remove(outputZipFile)
83
+ await this.zipHelper(DEFAULTS.ARTIFACTS_FOLDER, outputZipFile)
84
+ this.spinner.succeed(`Zipped package artifacts folder '${DEFAULTS.ARTIFACTS_FOLDER}' to '${outputZipFile}'`)
85
+ } catch (e) {
86
+ this.spinner.fail(e.message)
87
+ this.error(flags.verbose ? e : e.message)
88
+ }
89
+
90
+ this.spinner.succeed('Packaging done.')
91
+ }
92
+
93
+ get spinner () {
94
+ if (!this._spinner) {
95
+ this._spinner = ora()
96
+ }
97
+ return this._spinner
98
+ }
99
+
100
+ /**
101
+ * Creates the deploy.yaml file
102
+ *
103
+ * @param {object} appConfig the app's configuration file
104
+ */
105
+ async createDeployYamlFile (appConfig) {
106
+ // get extensions
107
+ let extensions
108
+ if (appConfig.implements?.filter(item => item !== 'application').length > 0) {
109
+ extensions = appConfig.implements.map(ext => ({ extensionPointId: ext }))
110
+ }
111
+
112
+ // get workspaces
113
+ let workspaces
114
+ if (appConfig.aio?.project?.workspace?.name) {
115
+ workspaces = []
116
+ workspaces.push(appConfig.aio?.project?.workspace?.name)
117
+ }
118
+
119
+ // get apis
120
+ let apis
121
+ if (appConfig.aio?.project?.workspace?.details?.services?.length > 0) {
122
+ apis = appConfig.aio.project.workspace.details.services.map(service => ({ code: service.code }))
123
+ }
124
+
125
+ // read name and version from package.json
126
+ const application = {
127
+ id: appConfig.packagejson.name,
128
+ version: appConfig.packagejson.version
129
+ }
130
+
131
+ let meshConfig
132
+ // ACNA-2041
133
+ // get the mesh config by running the `aio api-mesh:get` command (if available)
134
+ // in the interim, we need to process the output to get the proper json config
135
+ // TODO: send a PR to their plugin to have a `--json` flag
136
+ const command = await this.config.findCommand('api-mesh:get')
137
+ if (command) {
138
+ this.spinner.start('Getting api-mesh config...')
139
+ const { stdout } = await execa('aio', ['api-mesh', 'get'], { cwd: process.cwd() })
140
+ // until we get the --json flag, we parse the output
141
+ const idx = stdout.indexOf('{')
142
+ meshConfig = JSON.parse(stdout.substring(idx))
143
+ aioLogger.debug(`api-mesh:get - ${JSON.stringify(meshConfig, null, 2)}`)
144
+ this.spinner.succeed('Got api-mesh config')
145
+ } else {
146
+ aioLogger.debug('api-mesh:get command was not found, meshConfig is not available for app:pack')
147
+ }
148
+
149
+ const deployJson = {
150
+ $schema: 'http://json-schema.org/draft-07/schema',
151
+ $id: 'https://adobe.io/schemas/app-builder-templates/1',
152
+ application,
153
+ extensions,
154
+ workspaces,
155
+ apis,
156
+ meshConfig,
157
+ runtime: true // always true for App Builder apps
158
+ }
159
+
160
+ await writeFile(
161
+ path.join(DEFAULTS.ARTIFACTS_FOLDER, DEFAULTS.DEPLOY_YAML_FILE),
162
+ yaml.dump(deployJson),
163
+ { overwrite: true })
164
+ }
165
+
166
+ /**
167
+ * Copies a list of files to a folder.
168
+ *
169
+ * @param {string} destinationFolder the destination folder for the files
170
+ * @param {Array<string>} filesList a list of files to copy
171
+ */
172
+ async copyPackageFiles (destinationFolder, filesList) {
173
+ for (const src of filesList) {
174
+ const dest = path.join(destinationFolder, src)
175
+ if (await fs.pathExists(src)) {
176
+ aioLogger.debug(`Copying ${src} to ${dest}`)
177
+ await fs.copy(src, dest)
178
+ } else {
179
+ aioLogger.debug(`Skipping copy for ${src} (path does not exist)`)
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Zip a file/folder using archiver
186
+ *
187
+ * @param {string} filePath path of file/folder to zip
188
+ * @param {string} out output path
189
+ * @param {boolean} pathInZip internal path in zip
190
+ * @returns {Promise} returns with a blank promise when done
191
+ */
192
+ zipHelper (filePath, out, pathInZip = false) {
193
+ aioLogger.debug(`Creating zip of file/folder '${filePath}'`)
194
+ const stream = fs.createWriteStream(out)
195
+ const archive = archiver('zip', { zlib: { level: 9 } })
196
+
197
+ return new Promise((resolve, reject) => {
198
+ stream.on('close', () => resolve())
199
+ archive.pipe(stream)
200
+ archive.on('error', err => reject(err))
201
+
202
+ let stats
203
+ try {
204
+ stats = fs.lstatSync(filePath) // throws if enoent
205
+ } catch (e) {
206
+ archive.destroy()
207
+ reject(e)
208
+ }
209
+
210
+ if (stats.isDirectory()) {
211
+ archive.directory(filePath, pathInZip)
212
+ } else { // if (stats.isFile()) {
213
+ archive.file(filePath, { name: pathInZip || path.basename(filePath) })
214
+ }
215
+ archive.finalize()
216
+ })
217
+ }
218
+
219
+ /**
220
+ * Gets a list of files that are to be packed.
221
+ *
222
+ * This runs `npm pack` to get the list.
223
+ *
224
+ * @param {Array<string>} filesToExclude a list of files to exclude
225
+ * @param {string} workingDirectory the working directory to run `npm pack` in
226
+ * @returns {Array<string>} a list of files that are to be packed
227
+ */
228
+ async filesToPack (filesToExclude = [], workingDirectory = process.cwd()) {
229
+ const { stdout } = await execa('npm', ['pack', '--dry-run', '--json'], { cwd: workingDirectory })
230
+
231
+ const { files } = JSON.parse(stdout)[0]
232
+ return files
233
+ .map(file => file.path)
234
+ .filter(file => !filesToExclude.includes(file))
235
+ }
236
+
237
+ /**
238
+ * An annotation called code-download will be added to all actions in app.config.yaml
239
+ * (and linked yaml configs for example in extensions). This value will be set to false.
240
+ * The annotation will by default be true if not set.
241
+ *
242
+ * @param {object} appConfig the app's configuration file
243
+ */
244
+ async addCodeDownloadAnnotation (appConfig) {
245
+ // get the configFiles that have runtime manifests
246
+ const configFiles = []
247
+ for (const [, value] of Object.entries(appConfig.includeIndex)) {
248
+ const { key } = value
249
+ if (key === 'runtimeManifest' || key === 'application.runtimeManifest') {
250
+ configFiles.push(value)
251
+ }
252
+ }
253
+
254
+ // for each configFile, we modify each action to have the "code-download: false" annotation
255
+ for (const configFile of configFiles) {
256
+ const configFilePath = path.join(DEFAULTS.ARTIFACTS_FOLDER, configFile.file)
257
+ const { values } = loadConfigFile(configFilePath)
258
+
259
+ const runtimeManifest = getObjectValue(values, configFile.key)
260
+ for (const [, pkgManifest] of Object.entries(runtimeManifest.packages)) {
261
+ // key is the package name (unused), value is the package manifest. we iterate through each package's "actions"
262
+ for (const [, actionManifest] of Object.entries(pkgManifest.actions)) {
263
+ // key is the action name (unused), value is the action manifest. we add the "code-download: false" annotation
264
+ actionManifest.annotations['code-download'] = false
265
+ }
266
+ }
267
+
268
+ // write back the modified manifest to disk
269
+ await writeFile(configFilePath, yaml.dump(values), { overwrite: true })
270
+ }
271
+ }
272
+ }
273
+
274
+ Pack.hidden = true // hide from help for pre-release
275
+
276
+ Pack.description = chalk.yellow(`(Pre-release) This command will support packaging apps for redistribution.
277
+ `)
278
+
279
+ Pack.flags = {
280
+ ...BaseCommand.flags,
281
+ output: Flags.string({
282
+ description: 'The packaged app output file path',
283
+ char: 'o',
284
+ default: DEFAULTS.OUTPUT_ZIP_FILE
285
+ })
286
+ }
287
+
288
+ Pack.args = [
289
+ {
290
+ name: 'path',
291
+ description: 'Path to the app directory to package',
292
+ default: '.'
293
+ }
294
+ ]
295
+
296
+ module.exports = Pack
@@ -48,6 +48,7 @@ class Test extends BaseCommand {
48
48
 
49
49
  this.printReport(totalResults)
50
50
  process.exitCode = exitCode
51
+ return exitCode
51
52
  }
52
53
 
53
54
  printReport (totalResults) {
@@ -505,7 +505,32 @@ const createWebExportFilter = (filterValue) => {
505
505
  }
506
506
  }
507
507
 
508
+ /**
509
+ * Get property from object with case insensitivity.
510
+ *
511
+ * @param {object} obj the object to wrap
512
+ * @param {string} key the key
513
+ * @private
514
+ */
515
+ function getObjectProp (obj, key) {
516
+ return obj[Object.keys(obj).find(k => k.toLowerCase() === key.toLowerCase())]
517
+ }
518
+
519
+ /**
520
+ * Get a value in an object by dot notation.
521
+ *
522
+ * @param {object} obj the object to wrap
523
+ * @param {string} key the key
524
+ * @returns {object} the value
525
+ */
526
+ function getObjectValue (obj, key) {
527
+ const keys = (key || '').toString().split('.')
528
+ return keys.filter(o => o.trim()).reduce((o, i) => o && getObjectProp(o, i), obj)
529
+ }
530
+
508
531
  module.exports = {
532
+ getObjectValue,
533
+ getObjectProp,
509
534
  createWebExportFilter,
510
535
  isNpmInstalled,
511
536
  isGitInstalled,
@@ -30,7 +30,9 @@ module.exports = {
30
30
  defaultHttpServerPort: 9080,
31
31
  AIO_CONFIG_WORKSPACE_SERVICES: 'project.workspace.details.services',
32
32
  AIO_CONFIG_ORG_SERVICES: 'project.org.details.services',
33
+ IMPORT_CONFIG_FILE: 'config.json',
33
34
  USER_CONFIG_FILE: 'app.config.yaml',
35
+ DEPLOY_CONFIG_FILE: 'deploy.yaml',
34
36
  LEGACY_RUNTIME_MANIFEST: 'manifest.yml',
35
37
  INCLUDE_DIRECTIVE: '$include',
36
38
  APPLICATION_CONFIG_KEY: 'application',
@@ -34,11 +34,16 @@ module.exports = async (config, isLocalDev = false, log = () => {}, filter = fal
34
34
  }
35
35
  if (inprocHook) {
36
36
  const hookFilterEntities = Array.isArray(filter) ? filter : []
37
- await inprocHook('deploy-actions', {
37
+ const hookResults = await inprocHook('deploy-actions', {
38
38
  appConfig: config,
39
39
  filterEntities: hookFilterEntities,
40
40
  isLocalDev
41
41
  })
42
+ if (hookResults?.failures?.length > 0) {
43
+ // output should be "Error : <plugin-name> : <error-message>\n" for each failure
44
+ log('Error: ' + hookResults.failures.map(f => `${f.plugin.name} : ${f.error.message}`).join('\nError: '))
45
+ throw new Error(`Hook 'deploy-actions' failed with ${hookResults.failures[0].error}`)
46
+ }
42
47
  }
43
48
  const entities = await deployActions(config, deployConfig, log)
44
49
  if (entities.actions) {
@@ -17,8 +17,8 @@ const fs = require('fs-extra')
17
17
  const inquirer = require('inquirer')
18
18
  const yaml = require('js-yaml')
19
19
  const hjson = require('hjson')
20
- const Ajv = require('ajv')
21
20
  const { EOL } = require('os')
21
+ const { validateJsonWithSchema } = require('./install-helper')
22
22
 
23
23
  const AIO_FILE = '.aio'
24
24
  const ENV_FILE = '.env'
@@ -33,21 +33,6 @@ const CONSOLE_CONFIG_KEY = 'console'
33
33
  // this into an init/constructor as it might create mocking issues in jest
34
34
  const prompt = inquirer.createPromptModule({ output: process.stderr })
35
35
 
36
- /**
37
- * Validate the config json
38
- *
39
- * @param {object} configJson the json to validate
40
- * @returns {object} with keys valid (boolean) and errors (object). errors is null if no errors
41
- */
42
- function validateConfig (configJson) {
43
- /* eslint-disable-next-line node/no-unpublished-require */
44
- const schema = require('../../schema/config.schema.json')
45
- const ajv = new Ajv({ allErrors: true })
46
- const validate = ajv.compile(schema)
47
-
48
- return { valid: validate(configJson), errors: validate.errors }
49
- }
50
-
51
36
  /**
52
37
  * Load a config file
53
38
  *
@@ -92,7 +77,7 @@ function loadConfigFile (fileOrBuffer) {
92
77
  */
93
78
  function loadAndValidateConfigFile (fileOrBuffer) {
94
79
  const res = loadConfigFile(fileOrBuffer)
95
- const { valid: configIsValid, errors: configErrors } = validateConfig(res.values)
80
+ const { valid: configIsValid, errors: configErrors } = validateJsonWithSchema(res.values, 'config.json')
96
81
  if (!configIsValid) {
97
82
  const message = `Missing or invalid keys in config: ${JSON.stringify(configErrors, null, 2)}`
98
83
  throw new Error(message)
@@ -616,7 +601,7 @@ async function importConfigJson (configFileOrBuffer, destinationFolder = process
616
601
  }
617
602
 
618
603
  module.exports = {
619
- validateConfig,
604
+ writeFile,
620
605
  loadConfigFile,
621
606
  loadAndValidateConfigFile,
622
607
  writeConsoleConfig,
@@ -0,0 +1,37 @@
1
+ /*
2
+ Copyright 2023 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ Unless required by applicable law or agreed to in writing, software distributed under
7
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8
+ OF ANY KIND, either express or implied. See the License for the specific language
9
+ governing permissions and limitations under the License.
10
+ */
11
+
12
+ const Ajv = require('ajv')
13
+ const ajvAddFormats = require('ajv-formats')
14
+
15
+ /**
16
+ * Validate the file json with one of our schemas.
17
+ *
18
+ * @param {object} fileJson the json to validate
19
+ * @param {string} schemaName one of config.json, app.config.yaml, deploy.yaml
20
+ * @returns {object} with keys valid (boolean) and errors (object). errors is null if no errors
21
+ */
22
+ function validateJsonWithSchema (fileJson, schemaName) {
23
+ /* eslint-disable-next-line node/no-unpublished-require */
24
+ const schemas = require('../../schema/index')
25
+ const ajv = new Ajv({
26
+ allErrors: true,
27
+ allowUnionTypes: true
28
+ })
29
+ ajvAddFormats(ajv)
30
+
31
+ const validate = ajv.compile(schemas[schemaName])
32
+ return { valid: validate(fileJson), errors: validate.errors }
33
+ }
34
+
35
+ module.exports = {
36
+ validateJsonWithSchema
37
+ }