@adobe/aio-cli-lib-app-config 1.0.1 → 2.0.1

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": "1.0.1",
3
+ "version": "2.0.1",
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",
@@ -20,6 +21,8 @@
20
21
  "@adobe/aio-lib-core-config": "^3.0.0",
21
22
  "@adobe/aio-lib-core-logging": "^2.0.0",
22
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"
@@ -33,9 +36,9 @@
33
36
  "eslint-plugin-import": "^2.25.3",
34
37
  "eslint-plugin-jest": "^23",
35
38
  "eslint-plugin-jsdoc": "^37",
39
+ "eslint-plugin-n": "^15",
36
40
  "eslint-plugin-node": "^11.1.0",
37
41
  "eslint-plugin-promise": "^6",
38
- "eslint-plugin-n": "^15",
39
42
  "eslint-plugin-standard": "^4.0.0",
40
43
  "jest": "^27",
41
44
  "jest-junit": "^10.0.0",
@@ -0,0 +1,236 @@
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
+ }
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,62 @@ 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
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 = {}) {
194
284
  // this code is traversing app.config.yaml recursively to resolve all $includes directives
195
285
 
196
- // SETUP
197
- // the config with $includes to be resolved
198
- const config = yaml.safeLoad(fs.readFileSync(defaults.USER_CONFIG_FILE, 'utf8'))
286
+ const absolutePaths = options.absolutePaths === undefined ? false : options.absolutePaths
287
+ const appRoot = path.dirname(appConfigFile)
288
+
289
+ const config = yaml.safeLoad(await fs.readFile(appConfigFile, 'utf8'))
199
290
  // keep an index that will map keys like 'extensions.abc.runtimeManifest' to the config file where there are defined
200
291
  const includeIndex = {}
201
292
  // keep a cache for common included files - avoid to read a same file twice
202
293
  const configCache = {}
203
- // stack entries to be added for new iterations
294
+
295
+ // stack entries to be iterated on
204
296
  /** @private */
205
297
  function buildStackEntries (obj, fullKeyParent, relativeFullKeyParent, includedFiles, filterKeys = null) {
206
298
  return Object.keys(obj || {})
207
299
  // include filtered keys only
208
300
  .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
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.
213
305
  .map(key => ({ parentObj: obj, includedFiles, key, fullKey: fullKeyParent.concat(`.${key}`), relativeFullKey: relativeFullKeyParent.concat(`.${key}`) }))
214
306
  }
215
- // start with top level object
216
- const traverseStack = buildStackEntries(config, '', '', [defaults.USER_CONFIG_FILE])
307
+
308
+ // initialize with top level config object, each key will be traversed and checked for $include directive
309
+ const traverseStack = buildStackEntries(config, '', '', [appConfigFile])
217
310
 
218
311
  // ITERATIONS
219
312
  // iterate until there are no entries
@@ -223,7 +316,8 @@ function loadUserConfigAppYaml () {
223
316
  const currConfigFile = includedFiles[includedFiles.length - 1]
224
317
 
225
318
  // add full key to the index, slice(1) to remove initial dot
226
- includeIndex[fullKey.slice(1)] = {
319
+ const fullIndexKey = fullKey.slice(1)
320
+ includeIndex[fullIndexKey] = {
227
321
  file: currConfigFile,
228
322
  key: relativeFullKey.slice(1)
229
323
  }
@@ -231,7 +325,7 @@ function loadUserConfigAppYaml () {
231
325
  const value = parentObj[key]
232
326
 
233
327
  if (typeof value === 'object') {
234
- // if value is an object or an array, add entries for to stack
328
+ // if value is an object or an array, add new entries to be traversed
235
329
  traverseStack.push(...buildStackEntries(value, fullKey, relativeFullKey, includedFiles))
236
330
  continue
237
331
  }
@@ -239,24 +333,24 @@ function loadUserConfigAppYaml () {
239
333
  if (key === defaults.INCLUDE_DIRECTIVE) {
240
334
  // $include: 'configFile', value is string pointing to config file
241
335
  // includes are relative to the current config file
336
+
242
337
  // config path in index always as unix path, it doesn't matter but makes it easier to generate testing mock data
243
338
  const incFile = path.join(path.dirname(currConfigFile), value)
244
339
  const configFile = incFile.split(path.sep).join(path.posix.sep)
245
- // const configFile = upath.toUnix(path.join(path.dirname(currConfigFile), value))
246
340
 
247
341
  // 1. check for include cycles
248
342
  if (includedFiles.includes(configFile)) {
249
343
  throw new Error(`Detected '${defaults.INCLUDE_DIRECTIVE}' cycle: '${[...includedFiles, configFile].toString()}', please make sure that your configuration has no cycles.`)
250
344
  }
251
345
  // 2. check if file exists
252
- if (!configCache[configFile] && !fs.existsSync(configFile)) {
346
+ if (!configCache[configFile] && !(await fs.exists(configFile))) {
253
347
  throw new Error(`'${defaults.INCLUDE_DIRECTIVE}: ${configFile}' cannot be resolved, please make sure the file exists.`)
254
348
  }
255
349
  // 3. delete the $include directive to be replaced
256
350
  delete parentObj[key]
257
351
  // 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'))
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'))
260
354
  if (Array.isArray(loadedConfig) || typeof loadedConfig !== 'object') {
261
355
  throw new Error(`'${defaults.INCLUDE_DIRECTIVE}: ${configFile}' does not resolve to an object. Including an array or primitive type config is not supported.`)
262
356
  }
@@ -274,13 +368,14 @@ function loadUserConfigAppYaml () {
274
368
  // else primitive types: do nothing
275
369
  }
276
370
 
277
- // RETURN
278
- // $includes are now resolved
279
- return { config, includeIndex }
371
+ const appConfigWithIncludeIndex = { config, includeIndex }
372
+ rewritePathsInPlace(appConfigWithIncludeIndex, { absolutePaths, appRoot })
373
+
374
+ return appConfigWithIncludeIndex
280
375
  }
281
376
 
282
377
  /** @private */
283
- function loadUserConfigLegacy (commonConfig) {
378
+ async function legacyToAppConfig (commonConfig) {
284
379
  // load legacy user app config from manifest.yml, package.json, .aio.app
285
380
  const includeIndex = {}
286
381
  const legacyAppConfig = {}
@@ -289,7 +384,9 @@ function loadUserConfigLegacy (commonConfig) {
289
384
  // todo: new value usingLegacyConfig
290
385
  // this module should not console.log/warn ... or include chalk ...
291
386
  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\'.')
387
+ // this might have never have been seen in the wild as we don't know
388
+ // what log-level users have set
389
+ aioLogger.error('App config in \'.aio\' file is deprecated. Please move your \'.aio.app\' or \'.aio.cna\' to \'app.config.yaml\'.')
293
390
  const appConfig = { ...commonConfig.aio.app, ...commonConfig.aio.cna }
294
391
  Object.entries(appConfig).forEach(([k, v]) => {
295
392
  legacyAppConfig[k] = v
@@ -298,8 +395,8 @@ function loadUserConfigLegacy (commonConfig) {
298
395
  }
299
396
 
300
397
  // 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'))
398
+ if (await fs.exists(defaults.LEGACY_RUNTIME_MANIFEST)) {
399
+ const runtimeManifest = yaml.safeLoad(await fs.readFile(defaults.LEGACY_RUNTIME_MANIFEST, 'utf8'))
303
400
  legacyAppConfig.runtimeManifest = runtimeManifest
304
401
  // populate index
305
402
  const baseKey = `${defaults.APPLICATION_CONFIG_KEY}.runtimeManifest`
@@ -321,30 +418,17 @@ function loadUserConfigLegacy (commonConfig) {
321
418
  if (pkgjsonscripts) {
322
419
  const hooks = {}
323
420
  // 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]
421
+
422
+ HookKeys.forEach(hookKey => {
423
+ if (pkgjsonscripts[hookKey]) {
424
+ hooks[hookKey] = pkgjsonscripts[hookKey]
425
+ includeIndex[`${defaults.APPLICATION_CONFIG_KEY}.hooks.${hookKey}`] = { file: 'package.json', key: `scripts.${hookKey}` }
343
426
  }
344
427
  })
428
+
345
429
  // todo: new val usingLegacyHooks:Boolean
346
430
  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')
431
+ aioLogger.error('hooks in \'package.json\' are deprecated. Please move your hooks to \'app.config.yaml\' under the \'hooks\' key')
348
432
  legacyAppConfig.hooks = hooks
349
433
  // build index
350
434
  includeIndex[`${defaults.APPLICATION_CONFIG_KEY}.hooks`] = { file: 'package.json', key: 'scripts' }
@@ -358,80 +442,126 @@ function loadUserConfigLegacy (commonConfig) {
358
442
  }
359
443
  }
360
444
 
361
- if (Object.keys(includeIndex).length > 0) {
362
- // add the top key
363
- includeIndex[`${defaults.APPLICATION_CONFIG_KEY}`] = { file: '.aio', key: 'app' }
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
364
450
  }
365
451
 
366
- return { includeIndex, config: { [defaults.APPLICATION_CONFIG_KEY]: legacyAppConfig } }
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
461
+ }
462
+
463
+ /** @private */
464
+ function rewritePathsInPlace (appConfigWithIncludeIndex, options) {
465
+ const { config: appConfig, includeIndex } = appConfigWithIncludeIndex
466
+
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
+ }
367
487
  }
368
488
 
369
489
  /** @private */
370
- function mergeLegacyUserConfig (userConfig, legacyUserConfig) {
371
- // NOTE: here we do a simplified merge, deep merge with copy might be wanted in future
490
+ async function mergeLegacyAppConfig (appConfigWithIncludeIndex, legacyAppConfigWithIncludeIndex) {
491
+ // NOTE: here we do a simplified merge, deep merge with copy might be wanted
372
492
 
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]
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]
376
496
 
377
497
  // merge 1 level config fields, such as 'actions': 'path/to/actions', precedence for new config
378
- const mergedApp = { ...legacyUserConfigApp, ...userConfigApp }
498
+ const mergedApplication = { ...legacyApplication, ...application }
379
499
 
380
500
  // special cases if both are defined
381
- if (legacyUserConfigApp && userConfigApp) {
501
+ if (application && legacyApplication) {
382
502
  // for simplicity runtimeManifest is not merged, it's one or the other
383
- if (legacyUserConfigApp.runtimeManifest && userConfigApp.runtimeManifest) {
503
+ if (legacyApplication.runtimeManifest && application.runtimeManifest) {
384
504
  aioLogger.warn('\'manifest.yml\' is ignored in favor of key \'runtimeManifest\' in \'app.config.yaml\'.')
385
505
  }
386
506
  // hooks are merged
387
- if (legacyUserConfigApp.hooks && userConfigApp.hooks) {
388
- mergedApp.hooks = { ...legacyUserConfigApp.hooks, ...userConfigApp.hooks }
507
+ if (legacyApplication.hooks && application.hooks) {
508
+ mergedApplication.hooks = { ...legacyApplication.hooks, ...application.hooks }
389
509
  }
390
510
  }
391
511
 
392
512
  return {
393
- ...userConfig,
394
- [defaults.APPLICATION_CONFIG_KEY]: mergedApp
513
+ config: {
514
+ ...appConfigWithIncludeIndex.config,
515
+ [defaults.APPLICATION_CONFIG_KEY]: mergedApplication
516
+ },
517
+ // new configuration index takes precedence
518
+ includeIndex: { ...legacyAppConfigWithIncludeIndex.includeIndex, ...appConfigWithIncludeIndex.includeIndex }
395
519
  }
396
520
  }
397
521
 
398
522
  /** @private */
399
- function buildAllConfigs (userConfig, commonConfig, includeIndex) {
523
+ async function buildAllConfigs (userConfig, commonConfig, includeIndex) {
400
524
  return {
401
- ...buildAppConfig(userConfig, commonConfig, includeIndex),
402
- ...buildExtConfigs(userConfig, commonConfig, includeIndex)
525
+ ...(await buildAppConfig(userConfig, commonConfig, includeIndex)),
526
+ ...(await buildExtConfigs(userConfig, commonConfig, includeIndex))
403
527
  }
404
528
  }
405
529
 
406
530
  /** @private */
407
- function buildExtConfigs (userConfig, commonConfig, includeIndex) {
531
+ async function buildExtConfigs (userConfig, commonConfig, includeIndex) {
408
532
  const configs = {}
409
533
  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)
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)
412
537
  // extensions have an extra operations field
413
538
  configs[extName].operations = singleUserConfig.operations
414
- if (!configs[extName].operations) {
415
- throw new Error(`Missing 'operations' config field for extension point ${extName}`)
416
- }
417
- })
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
+ }
418
544
  }
419
545
  return configs
420
546
  }
421
547
 
422
548
  /** @private */
423
- function buildAppConfig (userConfig, commonConfig, includeIndex) {
424
- const fullAppConfig = buildSingleConfig(defaults.APPLICATION_CONFIG_KEY, userConfig[defaults.APPLICATION_CONFIG_KEY], commonConfig, includeIndex)
549
+ async function buildAppConfig (userConfig, commonConfig, includeIndex) {
550
+ const fullAppConfig = await buildSingleConfig(defaults.APPLICATION_CONFIG_KEY,
551
+ userConfig[defaults.APPLICATION_CONFIG_KEY],
552
+ commonConfig,
553
+ includeIndex)
425
554
 
555
+ // todo: this needs to be updated; an app doesn't exist if there is no config.
426
556
  if (!fullAppConfig.app.hasBackend && !fullAppConfig.app.hasFrontend) {
427
- // only set application config if there is an actuall app, meaning either some backend or frontend
557
+ // only set application config if there is an actual app, meaning either some backend or frontend
428
558
  return {}
429
559
  }
430
560
  return { [defaults.APPLICATION_CONFIG_KEY]: fullAppConfig }
431
561
  }
432
562
 
433
563
  /** @private */
434
- function buildSingleConfig (configName, singleUserConfig, commonConfig, includeIndex) {
564
+ async function buildSingleConfig (configName, singleUserConfig, commonConfig, includeIndex) {
435
565
  // used as subfolder folder in dist, converts to a single dir, e.g. dx/excshell/1 =>
436
566
  // dx-excshell-1 and dist/dx-excshell-1/actions/action-xyz.zip
437
567
  const subFolderName = configName.replace(/\//g, '-')
@@ -455,28 +585,32 @@ function buildSingleConfig (configName, singleUserConfig, commonConfig, includeI
455
585
  return config
456
586
  }
457
587
 
458
- const otherKeyInObject = Object.keys(singleUserConfig)[0]
459
- // The default action and web path are relative to the folder holding the config file.
588
+ // Default paths are relative to the folder holding the config file.
460
589
  // Let's search the config path that defines a key in the same config object level as 'web' or
461
590
  // '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)
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)
466
598
  const defaultDistPath = 'dist/' // relative to root
467
599
 
468
600
  // absolute paths
469
- const actions = pathConfigValueToAbs(singleUserConfig.actions, fullKeyPrefix + '.actions', includeIndex) || defaultActionPath
470
- const unitTest = pathConfigValueToAbs(singleUserConfig.unitTest, fullKeyPrefix + '.web', includeIndex) || defaultUnitTestPath
471
- const e2eTest = pathConfigValueToAbs(singleUserConfig.e2eTest, fullKeyPrefix + '.web', includeIndex) || defaultE2eTestPath
472
- const dist = pathConfigValueToAbs(singleUserConfig.dist, fullKeyPrefix + '.dist', includeIndex) || defaultDistPath
601
+ const actions = singleUserConfig.actions || defaultActionPath
602
+ const unitTest = singleUserConfig.unitTest || defaultUnitTestPath
603
+ const e2eTest = singleUserConfig.e2eTest || defaultE2eTestPath
604
+ const dist = singleUserConfig.dist || defaultDistPath
473
605
 
606
+ // web src folder might be defined in 'web' key or 'web.src' key
474
607
  let web
475
- if (!singleUserConfig.web || typeof singleUserConfig.web === 'string') {
476
- // keep backward compatibility - web src is directly defined as string web: web-src
477
- web = pathConfigValueToAbs(singleUserConfig.web, fullKeyPrefix + '.web', includeIndex) || defaultWebPath
608
+ if (typeof singleUserConfig.web === 'string') {
609
+ web = singleUserConfig.web
610
+ } else if (typeof singleUserConfig.web === 'object') {
611
+ web = singleUserConfig.web.src || defaultWebPath
478
612
  } else {
479
- web = pathConfigValueToAbs(singleUserConfig.web.src, fullKeyPrefix + '.web', includeIndex) || defaultWebPath
613
+ web = defaultWebPath
480
614
  }
481
615
 
482
616
  config.tests.unit = path.resolve(unitTest)
@@ -485,15 +619,22 @@ function buildSingleConfig (configName, singleUserConfig, commonConfig, includeI
485
619
  const manifest = singleUserConfig.runtimeManifest
486
620
 
487
621
  config.app.hasBackend = !!manifest
488
- config.app.hasFrontend = fs.existsSync(web)
622
+ config.app.hasFrontend = await fs.exists(web)
489
623
  config.app.dist = path.resolve(dist, dist === defaultDistPath ? subFolderName : '')
490
624
 
625
+ if (singleUserConfig.events) {
626
+ config.events = { ...singleUserConfig.events }
627
+ }
628
+ if (commonConfig?.aio?.project) {
629
+ config.project = commonConfig.aio.project
630
+ }
631
+
491
632
  // actions
492
633
  config.actions.src = path.resolve(actions) // needed for app add first action
493
634
  if (config.app.hasBackend) {
494
635
  config.actions.dist = path.join(config.app.dist, 'actions')
495
- config.manifest = { src: 'manifest.yml' } // even if a legacy config path, it is required for runtime sync
496
- config.manifest.full = rewriteRuntimeManifestPathsToRelRoot(manifest, fullKeyPrefix + '.runtimeManifest', includeIndex)
636
+ config.manifest = { src: 'manifest.yml' } // even for non legacy config paths, it is required for runtime sync
637
+ config.manifest.full = cloneDeep(manifest)
497
638
  config.manifest.packagePlaceholder = '__APP_PACKAGE__'
498
639
  config.manifest.package = config.manifest.full.packages && config.manifest.full.packages[config.manifest.packagePlaceholder]
499
640
  if (config.manifest.package) {
@@ -548,43 +689,27 @@ function buildSingleConfig (configName, singleUserConfig, commonConfig, includeI
548
689
  return config
549
690
  }
550
691
 
551
- /** @private */
552
- function rewriteRuntimeManifestPathsToRelRoot (manifestConfig, fullKeyToManifest, includeIndex) {
553
- const manifestCopy = cloneDeep(manifestConfig)
554
-
555
- Object.entries(manifestCopy.packages || {}).forEach(([pkgName, pkg]) => {
556
- Object.entries(pkg.actions || {}).forEach(([actionName, action]) => {
557
- const fullKeyToAction = `${fullKeyToManifest}.packages.${pkgName}.actions.${actionName}`
558
- if (action.function) {
559
- // absolut path
560
- action.function = pathConfigValueToAbs(action.function, fullKeyToAction + '.function', includeIndex)
561
- }
562
- if (action.include) {
563
- action.include.forEach((arr, i) => {
564
- // absolut path
565
- action.include[i][0] = pathConfigValueToAbs(action.include[i][0], fullKeyToAction + `.include.${i}.0`, includeIndex)
566
- })
567
- }
568
- })
569
- })
570
-
571
- return manifestCopy
572
- }
573
-
574
692
  // Because of the $include directives, config paths (e.g actions: './path/to/actions') can
575
693
  // be relative to config files in any subfolder. Config keys that define path values are
576
694
  // identified and their value is rewritten relative to the root folder.
577
695
  /** @private */
578
- function pathConfigValueToAbs (pathValue, fullKeyToPathValue, includeIndex) {
579
- const configData = includeIndex[fullKeyToPathValue]
580
- if (!pathValue || !configData) {
581
- return undefined
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)
582
707
  }
583
- // if path value is defined and fullKeyToPathValyue is correct then index has an entry
584
- const configPath = configData.file
585
- // path.resolve => support both absolut pathValue and relative (relative joins with
586
- // config dir and process.cwd, absolut returns pathValue)
587
- return path.resolve(path.dirname(configPath), pathValue)
708
+ return path.join(path.dirname(includedFromConfigPath), pathValue).split(path.sep).join(path.posix.sep)
588
709
  }
589
710
 
590
- module.exports = loadConfig
711
+ module.exports = {
712
+ load,
713
+ validate,
714
+ coalesce
715
+ }