@adobe/aio-cli-lib-app-config 0.2.2 → 1.0.1-pre.2023-07-10.sha-32c62844

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