@adobe/aio-cli-lib-app-config 1.0.1-pre.2023-07-14.sha-628de670 → 1.1.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/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@adobe/aio-cli-lib-app-config",
3
- "version": "1.0.1-pre.2023-07-14.sha-628de670",
3
+ "version": "1.1.0",
4
4
  "description": "node lib to provide a consistent interface to various config files that make up Adobe Developer App Builder applications and extensions",
5
5
  "repository": "https://github.com/adobe/aio-cli-lib-app-config/",
6
6
  "license": "Apache-2.0",
7
7
  "main": "src/index.js",
8
8
  "files": [
9
- "src",
10
- "schema"
9
+ "src"
11
10
  ],
12
11
  "scripts": {
13
12
  "test": "npm run lint && npm run unit-tests",
@@ -19,10 +18,8 @@
19
18
  },
20
19
  "dependencies": {
21
20
  "@adobe/aio-lib-core-config": "^3.0.0",
22
- "@adobe/aio-lib-core-logging": "next",
21
+ "@adobe/aio-lib-core-logging": "^2.0.0",
23
22
  "@adobe/aio-lib-env": "^2.0.0",
24
- "ajv": "^8.12.0",
25
- "ajv-formats": "^2.1.1",
26
23
  "fs-extra": "^9.0.1",
27
24
  "js-yaml": "^3.14.0",
28
25
  "lodash.clonedeep": "^4.5.0"
@@ -34,9 +31,9 @@
34
31
  "eslint": "^8",
35
32
  "eslint-config-standard": "^17",
36
33
  "eslint-plugin-import": "^2.25.3",
37
- "eslint-plugin-jest": "^23",
38
- "eslint-plugin-jsdoc": "^37",
39
- "eslint-plugin-n": "^15",
34
+ "eslint-plugin-jest": "^27",
35
+ "eslint-plugin-jsdoc": "^42",
36
+ "eslint-plugin-n": "^16",
40
37
  "eslint-plugin-node": "^11.1.0",
41
38
  "eslint-plugin-promise": "^6",
42
39
  "eslint-plugin-standard": "^4.0.0",
@@ -74,6 +71,5 @@
74
71
  "setupFilesAfterEnv": [
75
72
  "./test/jest.setup.js"
76
73
  ]
77
- },
78
- "prereleaseSha": "628de6708a25f55af14bf772b3a6e4d16d353ac4"
79
- }
74
+ }
75
+ }
package/src/index.js CHANGED
@@ -15,11 +15,6 @@ const yaml = require('js-yaml')
15
15
  const fs = require('fs-extra')
16
16
  const aioConfigLoader = require('@adobe/aio-lib-core-config')
17
17
  const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-lib-app-config', { provider: 'debug' })
18
- const Ajv = require('ajv')
19
- const ajvAddFormats = require('ajv-formats')
20
-
21
- // eslint-disable-next-line node/no-unpublished-require
22
- const schema = require('../schema/app.config.yaml.schema.json')
23
18
 
24
19
  // give or take daylight savings, and leap seconds ...
25
20
  const AboutAWeekInSeconds = '604800'
@@ -57,19 +52,6 @@ const HookKeys = [
57
52
  'serve-static'
58
53
  ]
59
54
 
60
- // please add any key that points to a path here
61
- // if they are defined in an included file, those need to be rewritten to be relative to the root folder
62
- const PATH_KEYS = [
63
- /^(application|extensions\.[^.]+)\.web$/,
64
- /^(application|extensions\.[^.]+)\.web\.src$/,
65
- /^(application|extensions\.[^.]+)\.actions$/,
66
- /^(application|extensions\.[^.]+)\.unitTest$/,
67
- /^(application|extensions\.[^.]+)\.e2eTest$/,
68
- /^(application|extensions\.[^.]+)\.dist$/,
69
- /^(application|extensions\.[^.]+)\.runtimeManifest\.packages\.[^.]+\.actions\.[^.]+\.function$/,
70
- /^(application|extensions\.[^.]+)\.runtimeManifest\.packages\.[^.]+\.actions\.[^.]+\.include\.\d+\.0$/
71
- ]
72
-
73
55
  const {
74
56
  getCliEnv, /* function */
75
57
  STAGE_ENV /* string */
@@ -77,15 +59,10 @@ const {
77
59
  const cloneDeep = require('lodash.clonedeep')
78
60
 
79
61
  /**
80
- * Loads app builder configuration in the current working directory.
81
- *
82
62
  * loading config returns following object (this config is internal, not user facing):
83
63
  * {
84
- * configSchema: { app.config.yaml configSchema field }
85
64
  * aio: {...aioConfig...},
86
65
  * packagejson: {...package.json...},
87
- * configSchema: {...app.config.yaml configSchema field as is},
88
- * productDependencies: {...app.config.yaml productDependencies field as is},
89
66
  * all: {
90
67
  * OPTIONAL:'application': {
91
68
  * app: {
@@ -124,8 +101,7 @@ const cloneDeep = require('lodash.clonedeep')
124
101
  * dist,
125
102
  * remote,
126
103
  * urls
127
- * },
128
- * events: {}
104
+ * }
129
105
  * }
130
106
  * },
131
107
  * OPTIONAL:'dx/asset-compute/worker/1': {
@@ -136,51 +112,30 @@ const cloneDeep = require('lodash.clonedeep')
136
112
  * },
137
113
  * }
138
114
  *
139
- * @param {object} options options to load Config
115
+ * @param {object} options options to loadConfig
140
116
  * @param {boolean} options.allowNoImpl do not throw if there is no implementation
141
- * @param {boolean} options.ignoreAioConfig do not load .aio config via aio-lib-core-config, which is loaded synchronously and blocks the main thread.
142
117
  * @returns {object} the config
143
118
  */
144
- async function load (options = {}) {
145
- const allowNoImpl = options.allowNoImpl === undefined ? false : options.allowNoImpl
146
- const ignoreAioConfig = options.ignoreAioConfig === undefined ? false : options.ignoreAioConfig
147
- // *NOTE* it would be nice to support an appFolder option to load config from a different folder.
148
- // However, this requires to update aio-lib-core-config to support loading
149
- // from a different folder aswell (or enforcing ignore).
150
-
151
- // I. load common config
119
+ function loadConfig (options = { allowNoImpl: false }) {
152
120
  // configuration that is shared for application and each extension config
153
121
  // holds things like ow credentials, packagejson and aioConfig
154
- const commonConfig = await loadCommonConfig({ ignoreAioConfig })
155
- // checkCommonConfig(commonConfig)
156
-
157
- // II. load app.config.yaml & validate + load/merge legacy configuration if any
158
- // support backward compatibility, include legacy application configuration
159
- const legacyAppConfigWithIndex = await legacyToAppConfig(commonConfig)
160
- let { config: appConfig, includeIndex } = legacyAppConfigWithIndex
161
- // no validation on the legacy configuration, which will be deprecated eventually
162
-
163
- if (await fs.exists(defaults.USER_CONFIG_FILE)) {
164
- // this will resolve $include directives and output the app config into a single object
165
- // paths config values in $included files will be rewritten
166
- const appConfigWithIndex = await coalesce(defaults.USER_CONFIG_FILE, { absolutePaths: true })
167
- await validate(appConfigWithIndex.config, { throws: true })
168
- const mergedAppConfig = await mergeLegacyAppConfig(appConfigWithIndex, legacyAppConfigWithIndex)
169
-
170
- appConfig = mergedAppConfig.config
171
- includeIndex = mergedAppConfig.includeIndex
172
- }
122
+ const commonConfig = loadCommonConfig()
123
+ checkCommonConfig(commonConfig)
124
+
125
+ // user configuration is specified in app.config.yaml and holds both standalone app and extension configuration
126
+ // note that `$includes` directive will be resolved here
127
+ // also this will load and merge the standalone legacy configuration system if any
128
+ const { config: userConfig, includeIndex } = loadUserConfig(commonConfig)
129
+
130
+ // load the full standalone application and extension configurations
131
+ const all = buildAllConfigs(userConfig, commonConfig, includeIndex)
173
132
 
174
- // III. build output object
175
- // full standalone application and extension configurations
176
- const all = await buildAllConfigs(appConfig, commonConfig, includeIndex)
177
133
  const impl = Object.keys(all).sort() // sort for predictable configuration
178
- if (!allowNoImpl && impl.length <= 0) {
134
+ if (!options.allowNoImpl && impl.length <= 0) {
179
135
  throw new Error(`Couldn't find configuration in '${process.cwd()}', make sure to add at least one extension or a standalone app`)
180
136
  }
137
+
181
138
  return {
182
- configSchema: appConfig?.configSchema || [],
183
- productDependencies: appConfig?.productDependencies || [],
184
139
  all,
185
140
  implements: impl, // e.g. 'dx/excshell/1', 'application'
186
141
  // includeIndex keeps a map from config keys to files that includes them and the relative key in the file.
@@ -192,43 +147,13 @@ async function load (options = {}) {
192
147
  }
193
148
  }
194
149
 
195
- /**
196
- * Validates the app configuration.
197
- * To validate an app.config.yaml file, use `await validate(await coalesce('app.config.yaml'))`
198
- *
199
- * @param {object} coalescedAppConfigObj the resolved app config object.
200
- * @param {object} options options
201
- * @param {boolean} options.throws defaults to false, if true throws on validation error instead of returning the error
202
- * @throws if not valid
203
- */
204
- async function validate (coalescedAppConfigObj, options = {}) {
205
- const throws = options.throws === undefined ? false : options.throws
206
- /* eslint-disable-next-line node/no-unpublished-require */
207
- const ajv = new Ajv({
208
- allErrors: true,
209
- allowUnionTypes: true
210
- })
211
- ajvAddFormats(ajv)
212
- const validate = ajv.compile(schema)
213
-
214
- const valid = validate(coalescedAppConfigObj)
215
- const errors = validate.errors
216
- if (!valid && throws) {
217
- throw new Error(`Missing or invalid keys in ${defaults.USER_CONFIG_FILE}: ${JSON.stringify(errors, null, 2)}`)
218
- }
219
- return { valid, errors }
220
- }
221
-
222
150
  /** @private */
223
- async function loadCommonConfig (/* istanbul ignore next */options = {}) {
224
- let aioConfig = {}
225
- if (!options.ignoreAioConfig) {
226
- // load aio config (mostly runtime and console config)
227
- aioConfigLoader.reload()
228
- aioConfig = aioConfigLoader.get() || {}
229
- }
151
+ function loadCommonConfig () {
152
+ // load aio config (mostly runtime and console config)
153
+ aioConfigLoader.reload()
154
+ const aioConfig = aioConfigLoader.get() || {}
230
155
 
231
- const packagejson = await fs.readJson('package.json', { throws: true })
156
+ const packagejson = fs.readJsonSync('package.json', { throws: true })
232
157
 
233
158
  // defaults
234
159
  // remove scoped name to use this for open whisk entities
@@ -243,7 +168,7 @@ async function loadCommonConfig (/* istanbul ignore next */options = {}) {
243
168
  owConfig.defaultApihost = defaults.defaultOwApihost
244
169
  owConfig.apihost = owConfig.apihost || defaults.defaultOwApihost // set by user
245
170
  owConfig.apiversion = owConfig.apiversion || 'v1'
246
- // default package name for replacing the legacy __APP_PACKAGE__ placeholder
171
+ // default package name replacing __APP_PACKAGE__ placeholder
247
172
  owConfig.package = `${packagejson.name}-${packagejson.version}`
248
173
 
249
174
  return {
@@ -256,57 +181,57 @@ async function loadCommonConfig (/* istanbul ignore next */options = {}) {
256
181
  }
257
182
 
258
183
  /** @private */
259
- // function checkCommonConfig (commonConfig) {
260
- // // todo this depends on the commands, expose a throwOnMissingConsoleInfo ?
261
- // // if (!commonConfig.aio.project || !commonConfig.ow.auth) {
262
- // // throw new Error('Missing project configuration, import a valid Console configuration first via \'aio app use\'')
263
- // // }
264
- // }
184
+ function checkCommonConfig (commonConfig) {
185
+ // todo this depends on the commands, expose a throwOnMissingConsoleInfo ?
186
+ // if (!commonConfig.aio.project || !commonConfig.ow.auth) {
187
+ // throw new Error('Missing project configuration, import a valid Console configuration first via \'aio app use\'')
188
+ // }
189
+ }
265
190
 
266
- /**
267
- * Resolve all includes, update relative paths and return a coalesced app
268
- * configuration object.
269
- *
270
- * Returns the appConfig along with an index of config keys to config file. The
271
- * config file paths in the index are absolute.
272
- *
273
- * @param {string} appConfigFile path to the app.config.yaml
274
- * @param {object} options options
275
- * @param {object} options.absolutePaths boolean, true for rewriting
276
- * configuration paths to absolute, false for relative to the appConfigFile
277
- * directory. Defaults to false. Note, that config values will never be
278
- * rewritten as relative to the cwd. But also note that
279
- * this option doesn't have any effect on the includeIndex paths which stay
280
- * relative to the cwd.
281
- * @returns {object} { config, includeIndex }
282
- */
283
- async function coalesce (appConfigFile, options = {}) {
284
- // this code is traversing app.config.yaml recursively to resolve all $includes directives
191
+ /** @private */
192
+ function loadUserConfig (commonConfig) {
193
+ const { config: legacyConfig, includeIndex: legacyIncludeIndex } = loadUserConfigLegacy(commonConfig)
194
+ const { config, includeIndex } = loadUserConfigAppYaml()
285
195
 
286
- const absolutePaths = options.absolutePaths === undefined ? false : options.absolutePaths
287
- const appRoot = path.dirname(appConfigFile)
196
+ const ret = {}
197
+ // include legacy application configuration
198
+ ret.config = mergeLegacyUserConfig(config, legacyConfig)
199
+ // merge includeIndexes, new config index takes precedence
200
+ ret.includeIndex = { ...legacyIncludeIndex, ...includeIndex }
288
201
 
289
- const config = yaml.safeLoad(await fs.readFile(appConfigFile, 'utf8'))
202
+ return ret
203
+ }
204
+
205
+ /** @private */
206
+ function loadUserConfigAppYaml () {
207
+ if (!fs.existsSync(defaults.USER_CONFIG_FILE)) {
208
+ // no error, support for legacy configuration
209
+ return { config: {}, includeIndex: {} }
210
+ }
211
+
212
+ // this code is traversing app.config.yaml recursively to resolve all $includes directives
213
+
214
+ // SETUP
215
+ // the config with $includes to be resolved
216
+ const config = yaml.safeLoad(fs.readFileSync(defaults.USER_CONFIG_FILE, 'utf8'))
290
217
  // keep an index that will map keys like 'extensions.abc.runtimeManifest' to the config file where there are defined
291
218
  const includeIndex = {}
292
219
  // keep a cache for common included files - avoid to read a same file twice
293
220
  const configCache = {}
294
-
295
- // stack entries to be iterated on
221
+ // stack entries to be added for new iterations
296
222
  /** @private */
297
223
  function buildStackEntries (obj, fullKeyParent, relativeFullKeyParent, includedFiles, filterKeys = null) {
298
224
  return Object.keys(obj || {})
299
225
  // include filtered keys only
300
226
  .filter(key => !filterKeys || filterKeys.includes(key))
301
- // `parentObj` stores the parent config object
302
- // `includedFiles` tracks already included files, is used for cycle detection and building the index key,
303
- // `fullKey` store parents and used for building the index,
304
- // `relativeFullKey` stores the key relative to the included file (e.g. actions are included actions.
227
+ // parentObj will be filled with $includes files
228
+ // includedFiles keep track of already included files, for cycle detection and building the index
229
+ // key, if its $includes will be loaded, if array or object will be recursively followed
230
+ // fullKey keeps track of all parents, used for building the index, relativeFullKey keeps track of the key in the included file
305
231
  .map(key => ({ parentObj: obj, includedFiles, key, fullKey: fullKeyParent.concat(`.${key}`), relativeFullKey: relativeFullKeyParent.concat(`.${key}`) }))
306
232
  }
307
-
308
- // initialize with top level config object, each key will be traversed and checked for $include directive
309
- const traverseStack = buildStackEntries(config, '', '', [appConfigFile])
233
+ // start with top level object
234
+ const traverseStack = buildStackEntries(config, '', '', [defaults.USER_CONFIG_FILE])
310
235
 
311
236
  // ITERATIONS
312
237
  // iterate until there are no entries
@@ -316,8 +241,7 @@ async function coalesce (appConfigFile, options = {}) {
316
241
  const currConfigFile = includedFiles[includedFiles.length - 1]
317
242
 
318
243
  // add full key to the index, slice(1) to remove initial dot
319
- const fullIndexKey = fullKey.slice(1)
320
- includeIndex[fullIndexKey] = {
244
+ includeIndex[fullKey.slice(1)] = {
321
245
  file: currConfigFile,
322
246
  key: relativeFullKey.slice(1)
323
247
  }
@@ -325,7 +249,7 @@ async function coalesce (appConfigFile, options = {}) {
325
249
  const value = parentObj[key]
326
250
 
327
251
  if (typeof value === 'object') {
328
- // if value is an object or an array, add new entries to be traversed
252
+ // if value is an object or an array, add entries for to stack
329
253
  traverseStack.push(...buildStackEntries(value, fullKey, relativeFullKey, includedFiles))
330
254
  continue
331
255
  }
@@ -333,24 +257,24 @@ async function coalesce (appConfigFile, options = {}) {
333
257
  if (key === defaults.INCLUDE_DIRECTIVE) {
334
258
  // $include: 'configFile', value is string pointing to config file
335
259
  // includes are relative to the current config file
336
-
337
260
  // config path in index always as unix path, it doesn't matter but makes it easier to generate testing mock data
338
261
  const incFile = path.join(path.dirname(currConfigFile), value)
339
262
  const configFile = incFile.split(path.sep).join(path.posix.sep)
263
+ // const configFile = upath.toUnix(path.join(path.dirname(currConfigFile), value))
340
264
 
341
265
  // 1. check for include cycles
342
266
  if (includedFiles.includes(configFile)) {
343
267
  throw new Error(`Detected '${defaults.INCLUDE_DIRECTIVE}' cycle: '${[...includedFiles, configFile].toString()}', please make sure that your configuration has no cycles.`)
344
268
  }
345
269
  // 2. check if file exists
346
- if (!configCache[configFile] && !(await fs.exists(configFile))) {
270
+ if (!configCache[configFile] && !fs.existsSync(configFile)) {
347
271
  throw new Error(`'${defaults.INCLUDE_DIRECTIVE}: ${configFile}' cannot be resolved, please make sure the file exists.`)
348
272
  }
349
273
  // 3. delete the $include directive to be replaced
350
274
  delete parentObj[key]
351
275
  // 4. load the included file
352
- // Note the included file can in turn also have includes, so we will have to traverse it as well
353
- const loadedConfig = configCache[configFile] || yaml.safeLoad(await fs.readFile(configFile, 'utf8'))
276
+ // Note the included file can in turn also have includes
277
+ const loadedConfig = configCache[configFile] || yaml.safeLoad(fs.readFileSync(configFile, 'utf8'))
354
278
  if (Array.isArray(loadedConfig) || typeof loadedConfig !== 'object') {
355
279
  throw new Error(`'${defaults.INCLUDE_DIRECTIVE}: ${configFile}' does not resolve to an object. Including an array or primitive type config is not supported.`)
356
280
  }
@@ -368,14 +292,13 @@ async function coalesce (appConfigFile, options = {}) {
368
292
  // else primitive types: do nothing
369
293
  }
370
294
 
371
- const appConfigWithIncludeIndex = { config, includeIndex }
372
- rewritePathsInPlace(appConfigWithIncludeIndex, { absolutePaths, appRoot })
373
-
374
- return appConfigWithIncludeIndex
295
+ // RETURN
296
+ // $includes are now resolved
297
+ return { config, includeIndex }
375
298
  }
376
299
 
377
300
  /** @private */
378
- async function legacyToAppConfig (commonConfig) {
301
+ function loadUserConfigLegacy (commonConfig) {
379
302
  // load legacy user app config from manifest.yml, package.json, .aio.app
380
303
  const includeIndex = {}
381
304
  const legacyAppConfig = {}
@@ -395,8 +318,8 @@ async function legacyToAppConfig (commonConfig) {
395
318
  }
396
319
 
397
320
  // 2. load legacy manifest.yaml
398
- if (await fs.exists(defaults.LEGACY_RUNTIME_MANIFEST)) {
399
- const runtimeManifest = yaml.safeLoad(await fs.readFile(defaults.LEGACY_RUNTIME_MANIFEST, 'utf8'))
321
+ if (fs.existsSync(defaults.LEGACY_RUNTIME_MANIFEST)) {
322
+ const runtimeManifest = yaml.safeLoad(fs.readFileSync(defaults.LEGACY_RUNTIME_MANIFEST, 'utf8'))
400
323
  legacyAppConfig.runtimeManifest = runtimeManifest
401
324
  // populate index
402
325
  const baseKey = `${defaults.APPLICATION_CONFIG_KEY}.runtimeManifest`
@@ -442,126 +365,83 @@ async function legacyToAppConfig (commonConfig) {
442
365
  }
443
366
  }
444
367
 
445
- const appConfigWithIncludeIndex = { includeIndex, config: { [defaults.APPLICATION_CONFIG_KEY]: legacyAppConfig } }
446
- if (Object.keys(includeIndex).length <= 0) {
447
- // no legacy configuration return now
448
- // todo return undefined here and for normal config, rewrite load and merge logic
449
- return appConfigWithIncludeIndex
368
+ if (Object.keys(includeIndex).length > 0) {
369
+ // add the top key
370
+ includeIndex[`${defaults.APPLICATION_CONFIG_KEY}`] = { file: '.aio', key: 'app' }
450
371
  }
451
372
 
452
- // add the top key
453
- includeIndex[`${defaults.APPLICATION_CONFIG_KEY}`] = { file: '.aio', key: 'app' }
454
-
455
- /* always absolute paths for now. Note that if we were interested in relative
456
- paths there would be no need to rewrite, as all paths are
457
- defined in the app root folder for legacy apps */
458
- rewritePathsInPlace(appConfigWithIncludeIndex, { absolutePaths: true })
459
-
460
- return appConfigWithIncludeIndex
373
+ return { includeIndex, config: { [defaults.APPLICATION_CONFIG_KEY]: legacyAppConfig } }
461
374
  }
462
375
 
463
376
  /** @private */
464
- function rewritePathsInPlace (appConfigWithIncludeIndex, options) {
465
- const { config: appConfig, includeIndex } = appConfigWithIncludeIndex
377
+ function mergeLegacyUserConfig (userConfig, legacyUserConfig) {
378
+ // NOTE: here we do a simplified merge, deep merge with copy might be wanted in future
466
379
 
467
- const buildStackEntries = (currObj, currFullKey) => Object.keys(currObj || {} /* cover for null */).map(k => {
468
- const fullKey = currFullKey ? currFullKey + '.' + k : k
469
- const includedFromConfigFile = includeIndex[fullKey].file
470
- return { fullKey, includedFromConfigFile, key: k, parentObj: currObj }
471
- })
472
- const stack = buildStackEntries(appConfig)
473
-
474
- while (stack.length > 0) {
475
- const { fullKey, includedFromConfigFile, key, parentObj } = stack.pop()
476
- const value = parentObj[key]
477
-
478
- if (typeof value === 'string' && PATH_KEYS.filter(reg => fullKey.match(reg)).length) {
479
- // rewrite path value to be relative to the root instead of being relative to the config file that includes it
480
- parentObj[key] = resolveToRoot(value, includedFromConfigFile, options)
481
- }
482
- if (typeof value === 'object') {
483
- // object or Array
484
- stack.push(...buildStackEntries(value, fullKey))
485
- }
486
- }
487
- }
488
-
489
- /** @private */
490
- async function mergeLegacyAppConfig (appConfigWithIncludeIndex, legacyAppConfigWithIncludeIndex) {
491
- // NOTE: here we do a simplified merge, deep merge with copy might be wanted
492
-
493
- // only need to merge application configs as legacy config system does not work with extensions
494
- const application = appConfigWithIncludeIndex.config[defaults.APPLICATION_CONFIG_KEY]
495
- const legacyApplication = legacyAppConfigWithIncludeIndex.config[defaults.APPLICATION_CONFIG_KEY]
380
+ // only need to merge application configs as legacy config system only works for standalone apps
381
+ const userConfigApp = userConfig[defaults.APPLICATION_CONFIG_KEY]
382
+ const legacyUserConfigApp = legacyUserConfig[defaults.APPLICATION_CONFIG_KEY]
496
383
 
497
384
  // merge 1 level config fields, such as 'actions': 'path/to/actions', precedence for new config
498
- const mergedApplication = { ...legacyApplication, ...application }
385
+ const mergedApp = { ...legacyUserConfigApp, ...userConfigApp }
499
386
 
500
387
  // special cases if both are defined
501
- if (application && legacyApplication) {
388
+ if (legacyUserConfigApp && userConfigApp) {
502
389
  // for simplicity runtimeManifest is not merged, it's one or the other
503
- if (legacyApplication.runtimeManifest && application.runtimeManifest) {
390
+ if (legacyUserConfigApp.runtimeManifest && userConfigApp.runtimeManifest) {
504
391
  aioLogger.warn('\'manifest.yml\' is ignored in favor of key \'runtimeManifest\' in \'app.config.yaml\'.')
505
392
  }
506
393
  // hooks are merged
507
- if (legacyApplication.hooks && application.hooks) {
508
- mergedApplication.hooks = { ...legacyApplication.hooks, ...application.hooks }
394
+ if (legacyUserConfigApp.hooks && userConfigApp.hooks) {
395
+ mergedApp.hooks = { ...legacyUserConfigApp.hooks, ...userConfigApp.hooks }
509
396
  }
510
397
  }
511
398
 
512
399
  return {
513
- config: {
514
- ...appConfigWithIncludeIndex.config,
515
- [defaults.APPLICATION_CONFIG_KEY]: mergedApplication
516
- },
517
- // new configuration index takes precedence
518
- includeIndex: { ...legacyAppConfigWithIncludeIndex.includeIndex, ...appConfigWithIncludeIndex.includeIndex }
400
+ ...userConfig,
401
+ [defaults.APPLICATION_CONFIG_KEY]: mergedApp
519
402
  }
520
403
  }
521
404
 
522
405
  /** @private */
523
- async function buildAllConfigs (userConfig, commonConfig, includeIndex) {
406
+ function buildAllConfigs (userConfig, commonConfig, includeIndex) {
524
407
  return {
525
- ...(await buildAppConfig(userConfig, commonConfig, includeIndex)),
526
- ...(await buildExtConfigs(userConfig, commonConfig, includeIndex))
408
+ ...buildAppConfig(userConfig, commonConfig, includeIndex),
409
+ ...buildExtConfigs(userConfig, commonConfig, includeIndex)
527
410
  }
528
411
  }
529
412
 
530
413
  /** @private */
531
- async function buildExtConfigs (userConfig, commonConfig, includeIndex) {
414
+ function buildExtConfigs (userConfig, commonConfig, includeIndex) {
532
415
  const configs = {}
533
416
  if (userConfig[defaults.EXTENSIONS_CONFIG_KEY]) {
534
- const entries = Object.entries(userConfig[defaults.EXTENSIONS_CONFIG_KEY])
535
- for (const [extName, singleUserConfig] of entries) {
536
- configs[extName] = await buildSingleConfig(extName, singleUserConfig, commonConfig, includeIndex)
417
+ Object.entries(userConfig[defaults.EXTENSIONS_CONFIG_KEY]).forEach(([extName, singleUserConfig]) => {
418
+ configs[extName] = buildSingleConfig(extName, singleUserConfig, commonConfig, includeIndex)
537
419
  // extensions have an extra operations field
538
420
  configs[extName].operations = singleUserConfig.operations
539
- // this is checked by the schema validation
540
- // if (!configs[extName].operations) {
541
- // throw new Error(`Missing 'operations' config field for extension point ${extName}`)
542
- // }
543
- }
421
+ if (!configs[extName].operations) {
422
+ throw new Error(`Missing 'operations' config field for extension point ${extName}`)
423
+ }
424
+ })
544
425
  }
545
426
  return configs
546
427
  }
547
428
 
548
429
  /** @private */
549
- async function buildAppConfig (userConfig, commonConfig, includeIndex) {
550
- const fullAppConfig = await buildSingleConfig(defaults.APPLICATION_CONFIG_KEY,
430
+ function buildAppConfig (userConfig, commonConfig, includeIndex) {
431
+ const fullAppConfig = buildSingleConfig(defaults.APPLICATION_CONFIG_KEY,
551
432
  userConfig[defaults.APPLICATION_CONFIG_KEY],
552
433
  commonConfig,
553
434
  includeIndex)
554
435
 
555
- // todo: this needs to be updated; an app doesn't exist if there is no config.
556
436
  if (!fullAppConfig.app.hasBackend && !fullAppConfig.app.hasFrontend) {
557
- // only set application config if there is an actual app, meaning either some backend or frontend
437
+ // only set application config if there is an actuall app, meaning either some backend or frontend
558
438
  return {}
559
439
  }
560
440
  return { [defaults.APPLICATION_CONFIG_KEY]: fullAppConfig }
561
441
  }
562
442
 
563
443
  /** @private */
564
- async function buildSingleConfig (configName, singleUserConfig, commonConfig, includeIndex) {
444
+ function buildSingleConfig (configName, singleUserConfig, commonConfig, includeIndex) {
565
445
  // used as subfolder folder in dist, converts to a single dir, e.g. dx/excshell/1 =>
566
446
  // dx-excshell-1 and dist/dx-excshell-1/actions/action-xyz.zip
567
447
  const subFolderName = configName.replace(/\//g, '-')
@@ -585,32 +465,28 @@ async function buildSingleConfig (configName, singleUserConfig, commonConfig, in
585
465
  return config
586
466
  }
587
467
 
588
- // Default paths are relative to the folder holding the config file.
468
+ const otherKeyInObject = Object.keys(singleUserConfig)[0]
469
+ // The default action and web path are relative to the folder holding the config file.
589
470
  // Let's search the config path that defines a key in the same config object level as 'web' or
590
471
  // 'action'
591
- const otherKeyInObject = Object.keys(singleUserConfig)[0]
592
- const configFilePath = includeIndex[`${fullKeyPrefix}.${otherKeyInObject}`].file
593
-
594
- const defaultActionPath = resolveToRoot('actions/', configFilePath)
595
- const defaultWebPath = resolveToRoot('web-src/', configFilePath)
596
- const defaultUnitTestPath = resolveToRoot('test/', configFilePath)
597
- const defaultE2eTestPath = resolveToRoot('e2e/', configFilePath)
472
+ const defaultActionPath = pathConfigValueToAbs('actions/', `${fullKeyPrefix}.${otherKeyInObject}`, includeIndex)
473
+ const defaultWebPath = pathConfigValueToAbs('web-src/', `${fullKeyPrefix}.${otherKeyInObject}`, includeIndex)
474
+ const defaultUnitTestPath = pathConfigValueToAbs('test/', `${fullKeyPrefix}.${otherKeyInObject}`, includeIndex)
475
+ const defaultE2eTestPath = pathConfigValueToAbs('e2e/', `${fullKeyPrefix}.${otherKeyInObject}`, includeIndex)
598
476
  const defaultDistPath = 'dist/' // relative to root
599
477
 
600
478
  // absolute paths
601
- const actions = singleUserConfig.actions || defaultActionPath
602
- const unitTest = singleUserConfig.unitTest || defaultUnitTestPath
603
- const e2eTest = singleUserConfig.e2eTest || defaultE2eTestPath
604
- const dist = singleUserConfig.dist || defaultDistPath
479
+ const actions = pathConfigValueToAbs(singleUserConfig.actions, fullKeyPrefix + '.actions', includeIndex) || defaultActionPath
480
+ const unitTest = pathConfigValueToAbs(singleUserConfig.unitTest, fullKeyPrefix + '.web', includeIndex) || defaultUnitTestPath
481
+ const e2eTest = pathConfigValueToAbs(singleUserConfig.e2eTest, fullKeyPrefix + '.web', includeIndex) || defaultE2eTestPath
482
+ const dist = pathConfigValueToAbs(singleUserConfig.dist, fullKeyPrefix + '.dist', includeIndex) || defaultDistPath
605
483
 
606
- // web src folder might be defined in 'web' key or 'web.src' key
607
484
  let web
608
- if (typeof singleUserConfig.web === 'string') {
609
- web = singleUserConfig.web
610
- } else if (typeof singleUserConfig.web === 'object') {
611
- web = singleUserConfig.web.src || defaultWebPath
485
+ if (!singleUserConfig.web || typeof singleUserConfig.web === 'string') {
486
+ // keep backward compatibility - web src is directly defined as string web: web-src
487
+ web = pathConfigValueToAbs(singleUserConfig.web, fullKeyPrefix + '.web', includeIndex) || defaultWebPath
612
488
  } else {
613
- web = defaultWebPath
489
+ web = pathConfigValueToAbs(singleUserConfig.web.src, fullKeyPrefix + '.web', includeIndex) || defaultWebPath
614
490
  }
615
491
 
616
492
  config.tests.unit = path.resolve(unitTest)
@@ -619,7 +495,7 @@ async function buildSingleConfig (configName, singleUserConfig, commonConfig, in
619
495
  const manifest = singleUserConfig.runtimeManifest
620
496
 
621
497
  config.app.hasBackend = !!manifest
622
- config.app.hasFrontend = await fs.exists(web)
498
+ config.app.hasFrontend = fs.existsSync(web)
623
499
  config.app.dist = path.resolve(dist, dist === defaultDistPath ? subFolderName : '')
624
500
 
625
501
  if (singleUserConfig.events) {
@@ -633,8 +509,8 @@ async function buildSingleConfig (configName, singleUserConfig, commonConfig, in
633
509
  config.actions.src = path.resolve(actions) // needed for app add first action
634
510
  if (config.app.hasBackend) {
635
511
  config.actions.dist = path.join(config.app.dist, 'actions')
636
- config.manifest = { src: 'manifest.yml' } // even for non legacy config paths, it is required for runtime sync
637
- config.manifest.full = cloneDeep(manifest)
512
+ config.manifest = { src: 'manifest.yml' } // even if a legacy config path, it is required for runtime sync
513
+ config.manifest.full = rewriteRuntimeManifestPathsToRelRoot(manifest, fullKeyPrefix + '.runtimeManifest', includeIndex)
638
514
  config.manifest.packagePlaceholder = '__APP_PACKAGE__'
639
515
  config.manifest.package = config.manifest.full.packages && config.manifest.full.packages[config.manifest.packagePlaceholder]
640
516
  if (config.manifest.package) {
@@ -689,27 +565,43 @@ async function buildSingleConfig (configName, singleUserConfig, commonConfig, in
689
565
  return config
690
566
  }
691
567
 
568
+ /** @private */
569
+ function rewriteRuntimeManifestPathsToRelRoot (manifestConfig, fullKeyToManifest, includeIndex) {
570
+ const manifestCopy = cloneDeep(manifestConfig)
571
+
572
+ Object.entries(manifestCopy.packages || {}).forEach(([pkgName, pkg]) => {
573
+ Object.entries(pkg.actions || {}).forEach(([actionName, action]) => {
574
+ const fullKeyToAction = `${fullKeyToManifest}.packages.${pkgName}.actions.${actionName}`
575
+ if (action.function) {
576
+ // absolut path
577
+ action.function = pathConfigValueToAbs(action.function, fullKeyToAction + '.function', includeIndex)
578
+ }
579
+ if (action.include) {
580
+ action.include.forEach((arr, i) => {
581
+ // absolut path
582
+ action.include[i][0] = pathConfigValueToAbs(action.include[i][0], fullKeyToAction + `.include.${i}.0`, includeIndex)
583
+ })
584
+ }
585
+ })
586
+ })
587
+
588
+ return manifestCopy
589
+ }
590
+
692
591
  // Because of the $include directives, config paths (e.g actions: './path/to/actions') can
693
592
  // be relative to config files in any subfolder. Config keys that define path values are
694
593
  // identified and their value is rewritten relative to the root folder.
695
594
  /** @private */
696
- function resolveToRoot (pathValue, includedFromConfigPath, options = {}) {
697
- // path.resolve => support both absolute pathValue and relative (relative joins with
698
- // config dir and process.cwd, absolute returns pathValue)
699
- if (options.absolutePaths) {
700
- return path.resolve(path.dirname(includedFromConfigPath), pathValue)
701
- }
702
-
703
- // relative paths
704
- if (options.appRoot) {
705
- // make sure path is relative to appRoot and not cwd
706
- includedFromConfigPath = path.relative(options.appRoot, includedFromConfigPath)
595
+ function pathConfigValueToAbs (pathValue, fullKeyToPathValue, includeIndex) {
596
+ const configData = includeIndex[fullKeyToPathValue]
597
+ if (!pathValue || !configData) {
598
+ return undefined
707
599
  }
708
- return path.join(path.dirname(includedFromConfigPath), pathValue).split(path.sep).join(path.posix.sep)
600
+ // if path value is defined and fullKeyToPathValyue is correct then index has an entry
601
+ const configPath = configData.file
602
+ // path.resolve => support both absolut pathValue and relative (relative joins with
603
+ // config dir and process.cwd, absolut returns pathValue)
604
+ return path.resolve(path.dirname(configPath), pathValue)
709
605
  }
710
606
 
711
- module.exports = {
712
- load,
713
- validate,
714
- coalesce
715
- }
607
+ module.exports = loadConfig
@@ -1,236 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://adobe.io/schemas/app-builder/app.config.yaml.json/v2",
4
- "type": "object",
5
- "properties": {
6
- "application": { "$ref": "#/definitions/application" },
7
- "extensions": { "$ref": "#/definitions/extensions" },
8
- "configSchema": { "$ref": "#/definitions/configSchema"},
9
- "productDependencies": { "$ref": "#/definitions/productDependencies"}
10
- },
11
- "anyOf": [
12
- {
13
- "required": ["application"]
14
- },
15
- {
16
- "required": ["extensions"]
17
- }
18
- ],
19
- "definitions": {
20
- "extensions": {
21
- "type": "object",
22
- "patternProperties": {
23
- "^[A-Za-z0-9-_/\\-]+$": {
24
- "$ref": "#/definitions/application",
25
- "type": "object",
26
- "properties": {
27
- "operations": {
28
- "type": "object",
29
- "patternProperties": {
30
- "^[^\n]+$": {
31
- "type":"array",
32
- "items": {
33
- "type": "object",
34
- "properties": {
35
- "type": { "type": "string" },
36
- "impl": { "type": "string" }
37
- }
38
- },
39
- "minItems": 1
40
- }
41
- },
42
- "minProperties": 1
43
- }
44
- },
45
- "required": ["operations"]
46
- }
47
- },
48
- "additionalProperties": false
49
- },
50
- "application": {
51
- "type": "object",
52
- "properties": {
53
- "runtimeManifest": { "$ref": "#/definitions/runtimeManifest" },
54
- "actions": { "type": "string" },
55
- "unitTest": { "type": "string" },
56
- "e2eTest": { "type": "string" },
57
- "dist": { "type": "string" },
58
- "tvmurl": { "type": "string" },
59
- "awsaccesskeyid": { "type": "string" },
60
- "awssecretaccesskey": { "type": "string" },
61
- "s3bucket": { "type": "string" },
62
- "events": { "$ref": "#/definitions/events" },
63
- "hostname": { "type": "string" },
64
- "htmlcacheduration": { "type": "number" },
65
- "jscacheduration": { "type": "number" },
66
- "csscacheduration": { "type": "number" },
67
- "imagecacheduration": { "type": "number" },
68
- "hooks": { "$ref": "#/definitions/hooks" },
69
- "web": { "$ref": "#/definitions/web" }
70
- },
71
- "required": []
72
- },
73
- "web": {
74
- "anyOf": [
75
- {
76
- "type": "string"
77
- },
78
- {
79
- "type": "object",
80
- "properties": {
81
- "src": { "type": "string" },
82
- "response-headers": {
83
- "type": "object",
84
- "patternProperties": {
85
- "^[^\n]+$": {
86
- "type":"object",
87
- "patternProperties": { "^[^\n]+$": { "type":"string" } }
88
- }
89
- }
90
- }
91
- },
92
- "additionalProperties": false
93
- }
94
- ]
95
- },
96
- "runtimeManifest": {
97
- "type": "object",
98
- "properties": {
99
- "packages": { "$ref": "#/definitions/packages" }
100
- },
101
- "required": ["packages"]
102
- },
103
- "packages": {
104
- "type": "object",
105
- "patternProperties": {
106
- "^[^\n]+$": {
107
- "$ref": "#/definitions/package"
108
- }
109
- },
110
- "additionalProperties": false
111
- },
112
- "package": {
113
- "type": "object",
114
- "properties": {
115
- "license": { "type": "string" },
116
- "actions": { "$ref": "#/definitions/actions" }
117
- }
118
- },
119
- "actions": {
120
- "type": "object",
121
- "patternProperties": {
122
- "^[^\n]+$": {
123
- "$ref": "#/definitions/action"
124
- }
125
- },
126
- "additionalProperties": false
127
- },
128
- "action": {
129
- "type": "object",
130
- "properties": {
131
- "function": { "type": "string" },
132
- "web": { "type": "string" },
133
- "runtime": { "type": "string" },
134
- "inputs": { "$ref": "#/definitions/inputs" },
135
- "annotations": { "$ref": "#/definitions/annotations" }
136
- },
137
- "required": []
138
- },
139
- "inputs": {
140
- "type": "object",
141
- "patternProperties": {
142
- "^[^\n]+$": {
143
- "type": ["string", "boolean"]
144
- }
145
- },
146
- "additionalProperties": false
147
- },
148
- "annotations": {
149
- "type": "object",
150
- "patternProperties": {
151
- "^[^\n]+$": {
152
- "type": ["string", "boolean"]
153
- }
154
- },
155
- "additionalProperties": false
156
- },
157
- "hooks": {
158
- "type": "object",
159
- "properties": {
160
- "pre-app-build": { "type": "string" },
161
- "post-app-build": { "type": "string" },
162
- "build-actions": { "type": "string" },
163
- "build-static": { "type": "string" },
164
- "pre-app-deploy": { "type": "string" },
165
- "post-app-deploy": { "type": "string" },
166
- "deploy-actions": { "type": "string" },
167
- "deploy-static": { "type": "string" },
168
- "pre-app-undeploy": { "type": "string" },
169
- "post-app-undeploy": { "type": "string" },
170
- "undeploy-actions": { "type": "string" },
171
- "undeploy-static": { "type": "string" },
172
- "pre-app-run": { "type": "string" },
173
- "post-app-run": { "type": "string" },
174
- "serve-static": { "type": "string" }
175
- }
176
- },
177
- "events": {
178
- "type": "object",
179
- "properties": {
180
- "registrations": {
181
- "type": "object",
182
- "patternProperties": {
183
- "^[^\n]+$": {
184
- "type": "object",
185
- "properties": {
186
- "description": { "type": "string" },
187
- "events_of_interest": {
188
- "type": "array",
189
- "items": {
190
- "type": "object",
191
- "properties": {
192
- "provider_metadata": { "type": "string" },
193
- "event_codes": { "type": "array" }
194
- }
195
- }
196
- },
197
- "runtime_action": {"type": "string" }
198
- }
199
- }
200
- }
201
- }
202
- }
203
- },
204
- "configSchema": {
205
- "type": "array",
206
- "maxItems": 50,
207
- "items": {
208
- "type": "object",
209
- "properties": {
210
- "type": { "type": "string", "enum": ["string", "boolean"] },
211
- "title": { "type": "string", "maxLength": 200 },
212
- "envKey": { "type": "string", "pattern": "[a-zA-Z_]{1,}[a-zA-Z0-9_]{0,}", "maxLength": 100 },
213
- "enum": { "type": "array", "items": { "$ref": "#/definitions/configSchemaValue" }, "minItems": 1, "maxItems": 100 },
214
- "default": { "$ref": "#/definitions/configSchemaValue" },
215
- "secret": { "type": "boolean" }
216
- },
217
- "required": ["type", "envKey"],
218
- "additionalProperties": false
219
- }
220
- },
221
- "configSchemaValue": { "type": "string", "maxLength": 1000 },
222
- "productDependencies": {
223
- "type": "array",
224
- "items": {
225
- "type": "object",
226
- "properties": {
227
- "code": { "type": "string" },
228
- "minVersion": { "type": "string", "pattern": "^[0-9]+.[0-9]+.[0-9]+$" },
229
- "maxVersion": { "type": "string", "pattern": "^[0-9]+.[0-9]+.[0-9]+$" }
230
- },
231
- "required": ["code", "minVersion", "maxVersion"],
232
- "additionalProperties": false
233
- }
234
- }
235
- }
236
- }