@adobe/helix-config 2.14.0 → 2.15.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/config-merge.js +143 -0
- package/src/config-view.js +38 -18
- package/src/schemas/site.schema.json +6 -0
- package/src/storage/config-store.js +4 -1
- package/types/site-config.d.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.15.1](https://github.com/adobe/helix-config/compare/v2.15.0...v2.15.1) (2024-05-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* do not load org config in resolveConfig ([ccedbfd](https://github.com/adobe/helix-config/commit/ccedbfd4c7c465b842e25f9021fd5a06021e63d4))
|
|
7
|
+
|
|
8
|
+
# [2.15.0](https://github.com/adobe/helix-config/compare/v2.14.0...v2.15.0) (2024-05-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* support profile for site ([#67](https://github.com/adobe/helix-config/issues/67)) ([55c09c9](https://github.com/adobe/helix-config/commit/55c09c95919ec06f047393335bdbcbcc2d187eb2))
|
|
14
|
+
|
|
1
15
|
# [2.14.0](https://github.com/adobe/helix-config/compare/v2.13.1...v2.14.0) (2024-05-07)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
/* eslint-disable no-continue, no-param-reassign */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef RootProperty
|
|
16
|
+
* @property {boolean} atomic whether the property should be overwritten atomically
|
|
17
|
+
* @property {boolean} isModified whether the property need special merging
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const ROOT_PROPERTIES = {
|
|
21
|
+
name: { atomic: true },
|
|
22
|
+
title: { atomic: true },
|
|
23
|
+
description: { atomic: true },
|
|
24
|
+
content: { atomic: true },
|
|
25
|
+
code: { atomic: true },
|
|
26
|
+
folders: {},
|
|
27
|
+
headers: { isModifier: true },
|
|
28
|
+
cdn: {},
|
|
29
|
+
access: {},
|
|
30
|
+
sidekick: {},
|
|
31
|
+
metadata: { isModifier: true },
|
|
32
|
+
robots: {},
|
|
33
|
+
extends: {},
|
|
34
|
+
tokens: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const FORCED_TYPES = {
|
|
38
|
+
'.cdn.prod.route': 'array',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merges the `src` object into the `dst` object.
|
|
43
|
+
* - if the dst property is a scalar, it will be overwritten by the dst property
|
|
44
|
+
* - if the src property is an empty string, it will delete the dst property
|
|
45
|
+
* - if the dst property is an array, it will be expanded by the src property uniquely
|
|
46
|
+
* - some well known paths are forced to be arrays
|
|
47
|
+
* @param {object} dst
|
|
48
|
+
* @param {object} src
|
|
49
|
+
* @param {boolean} isModifier specifies that the objects are modifiers sheets and need special
|
|
50
|
+
* merging
|
|
51
|
+
*/
|
|
52
|
+
function merge(dst, src, path, isModifier) {
|
|
53
|
+
for (const [key, value] of Object.entries(src)) {
|
|
54
|
+
const itemPath = `${path}.${key}`;
|
|
55
|
+
if (value === '') {
|
|
56
|
+
// remove key if value is empty string (keep false, 0)
|
|
57
|
+
delete dst[key];
|
|
58
|
+
} else if (typeof value !== 'object') {
|
|
59
|
+
// handle non object
|
|
60
|
+
if (key in dst) {
|
|
61
|
+
let dstValue = dst[key];
|
|
62
|
+
if (!Array.isArray(dstValue) && FORCED_TYPES[itemPath] === 'array') {
|
|
63
|
+
dstValue = [dst[key]];
|
|
64
|
+
dst[key] = dstValue;
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(dstValue)) {
|
|
67
|
+
dst[key] = value;
|
|
68
|
+
} else if (!dstValue.includes(value)) {
|
|
69
|
+
// only add to array if not exist yet
|
|
70
|
+
dstValue.push(value);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
dst[key] = value;
|
|
74
|
+
}
|
|
75
|
+
} else if (Array.isArray(value)) {
|
|
76
|
+
// handle array
|
|
77
|
+
if (key in dst) {
|
|
78
|
+
if (!Array.isArray(dst[key])) {
|
|
79
|
+
dst[key] = [dst[key]];
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
dst[key] = [];
|
|
83
|
+
}
|
|
84
|
+
const dstArray = dst[key];
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
if (typeof item === 'object') {
|
|
87
|
+
// todo: handle arrays in arrays eventually
|
|
88
|
+
// special case for modifier sheets
|
|
89
|
+
if (isModifier && item.key) {
|
|
90
|
+
const idx = dstArray.findIndex((i) => i.key === item.key);
|
|
91
|
+
if (idx >= 0) {
|
|
92
|
+
dstArray.splice(idx, 1);
|
|
93
|
+
}
|
|
94
|
+
if (!item.value) {
|
|
95
|
+
// don't set empty modifier
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const dstObj = Object.create(null);
|
|
100
|
+
dstArray.push(dstObj);
|
|
101
|
+
merge(dstObj, item, itemPath, isModifier);
|
|
102
|
+
} else if (!dstArray.includes(item)) {
|
|
103
|
+
dstArray.push(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// handle plain object
|
|
108
|
+
// eslint-disable-next-line no-use-before-define
|
|
109
|
+
mergeObject(dst, key, value, `${path}.${key}`, isModifier);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeObject(dst, key, value, path, isModifier) {
|
|
115
|
+
const dstObj = dst[key] ?? Object.create(null);
|
|
116
|
+
if (!dst[key]) {
|
|
117
|
+
dst[key] = dstObj;
|
|
118
|
+
}
|
|
119
|
+
merge(dstObj, value, path, isModifier);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergeConfig(dst, src) {
|
|
123
|
+
Object.entries(ROOT_PROPERTIES).forEach(([key, { atomic, isModifier }]) => {
|
|
124
|
+
if (src[key]) {
|
|
125
|
+
if (atomic) {
|
|
126
|
+
dst[key] = src[key];
|
|
127
|
+
} else {
|
|
128
|
+
mergeObject(dst, key, src[key], `.${key}`, isModifier);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getMergedConfig(site, profile) {
|
|
135
|
+
if (!profile) {
|
|
136
|
+
return site;
|
|
137
|
+
}
|
|
138
|
+
const ret = Object.create(null);
|
|
139
|
+
ret.version = site.version;
|
|
140
|
+
mergeConfig(ret, profile);
|
|
141
|
+
mergeConfig(ret, site);
|
|
142
|
+
return ret;
|
|
143
|
+
}
|
package/src/config-view.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SCOPE_RAW,
|
|
20
20
|
} from './ConfigContext.js';
|
|
21
21
|
import { resolveLegacyConfig, fetchRobotsTxt, toArray } from './config-legacy.js';
|
|
22
|
+
import { getMergedConfig } from './config-merge.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* @typedef Config
|
|
@@ -81,7 +82,7 @@ export function getAccessConfig(config, partition) {
|
|
|
81
82
|
// if an allow is defined but no apiKeyId, create a fake one so that auth is still
|
|
82
83
|
// enforced. later we can remove the allow and the apiKeyId in favor of the tokenHash
|
|
83
84
|
if (allow.length && !cfg.apiKeyId.length) {
|
|
84
|
-
cfg.apiKeyId.push('
|
|
85
|
+
cfg.apiKeyId.push('dummy');
|
|
85
86
|
}
|
|
86
87
|
// if an apiKeyId is defined but no tokenHash, create a fake one so that auth is still
|
|
87
88
|
// enforced.
|
|
@@ -167,7 +168,7 @@ function retainProperty(obj, prop) {
|
|
|
167
168
|
*/
|
|
168
169
|
async function resolveConfig(ctx, rso, scope) {
|
|
169
170
|
// try to load site config from config-bus
|
|
170
|
-
|
|
171
|
+
const key = `orgs/${rso.org}/sites/${rso.site}.json`;
|
|
171
172
|
let res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
172
173
|
if (!res.body) {
|
|
173
174
|
if (scope !== SCOPE_ADMIN) {
|
|
@@ -176,7 +177,17 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
176
177
|
return null;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
|
-
const
|
|
180
|
+
const site = res.json();
|
|
181
|
+
let profile;
|
|
182
|
+
|
|
183
|
+
if (site.extends?.profile) {
|
|
184
|
+
const profileKey = `orgs/${rso.org}/profiles/${site.extends.profile}.json`;
|
|
185
|
+
res = await ctx.loader.getObject(HELIX_CONFIG_BUS, profileKey);
|
|
186
|
+
if (res.body) {
|
|
187
|
+
profile = res.json();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const config = getMergedConfig(site, profile);
|
|
180
191
|
if (scope === SCOPE_PIPELINE) {
|
|
181
192
|
config.metadata = {
|
|
182
193
|
preview: await loadMetadata(ctx, config, 'preview'),
|
|
@@ -192,24 +203,25 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
192
203
|
if (scope === SCOPE_PIPELINE || scope === SCOPE_DELIVERY) {
|
|
193
204
|
config.head = await loadHeadHtml(ctx, config, rso.ref);
|
|
194
205
|
}
|
|
195
|
-
|
|
196
|
-
// check for org config
|
|
197
|
-
key = `orgs/${rso.org}/config.json`;
|
|
198
|
-
res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
199
|
-
if (res.body) {
|
|
200
|
-
const orgConfig = res.json();
|
|
201
|
-
if (orgConfig.tokens) {
|
|
202
|
-
config.tokens = orgConfig.tokens;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
206
|
return config;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
async function getSurrogateKey(opts) {
|
|
209
|
+
async function getSurrogateKey(opts, profile) {
|
|
209
210
|
const { site, org } = opts;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
const keys = [
|
|
212
|
+
await computeSurrogateKey(`${org}_config.json`),
|
|
213
|
+
await computeSurrogateKey(`${site}--${org}_config.json`),
|
|
214
|
+
];
|
|
215
|
+
if (profile) {
|
|
216
|
+
keys.push(await computeSurrogateKey(`p:${profile}--${org}_config.json`));
|
|
217
|
+
}
|
|
218
|
+
return keys.join(' ');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function loadOrgConfig(ctx, org) {
|
|
222
|
+
const key = `orgs/${org}/config.json`;
|
|
223
|
+
const res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
224
|
+
return res.body ? res.json() : null;
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
export async function getConfigResponse(ctx, opts) {
|
|
@@ -219,7 +231,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
219
231
|
const rso = { ref, site, org };
|
|
220
232
|
const config = await resolveConfig(ctx, rso, scope);
|
|
221
233
|
const surrogateHeaders = {
|
|
222
|
-
'x-surrogate-key': await getSurrogateKey(opts),
|
|
234
|
+
'x-surrogate-key': await getSurrogateKey(opts, config?.extends?.profile),
|
|
223
235
|
};
|
|
224
236
|
if (!config) {
|
|
225
237
|
return new PipelineResponse('', {
|
|
@@ -230,6 +242,14 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
230
242
|
},
|
|
231
243
|
});
|
|
232
244
|
}
|
|
245
|
+
if (config.extends && scope !== SCOPE_RAW) {
|
|
246
|
+
delete config.extends;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const orgConfig = await loadOrgConfig(ctx, org);
|
|
250
|
+
if (orgConfig?.tokens) {
|
|
251
|
+
config.tokens = orgConfig.tokens;
|
|
252
|
+
}
|
|
233
253
|
|
|
234
254
|
if (config.access) {
|
|
235
255
|
// normalize access config
|