@adobe/helix-config 5.6.5 → 5.6.6
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 +7 -0
- package/package.json +1 -1
- package/src/config-legacy.js +9 -24
- package/src/flatJson2Object.js +104 -0
- package/src/legacy-config-merge.js +236 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [5.6.6](https://github.com/adobe/helix-config/compare/v5.6.5...v5.6.6) (2025-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* legacy config w/o config-all ([#255](https://github.com/adobe/helix-config/issues/255)) ([0251471](https://github.com/adobe/helix-config/commit/0251471209439df447fb93bbe3cb327d8be98c12))
|
|
7
|
+
|
|
1
8
|
## [5.6.5](https://github.com/adobe/helix-config/compare/v5.6.4...v5.6.5) (2025-09-18)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/config-legacy.js
CHANGED
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
SCOPE_ADMIN, SCOPE_DELIVERY, SCOPE_PIPELINE, SCOPE_RAW,
|
|
14
14
|
} from './ConfigContext.js';
|
|
15
15
|
import { ConfigObject } from './config-object.js';
|
|
16
|
+
import { contentConfigMerge } from './legacy-config-merge.js';
|
|
17
|
+
import { prune } from './utils.js';
|
|
16
18
|
|
|
17
19
|
const HELIX_CODE_BUS = 'helix-code-bus';
|
|
18
20
|
|
|
19
|
-
const HELIX_CONTENT_BUS = 'helix-content-bus';
|
|
20
|
-
|
|
21
21
|
export function toArray(v) {
|
|
22
22
|
if (!v) {
|
|
23
23
|
return [];
|
|
@@ -62,23 +62,6 @@ async function fetchHelixConfig(ctx, rso) {
|
|
|
62
62
|
return cfg;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
* Retrieves the project config from the underlying storage
|
|
67
|
-
* and stores it in the context as projectConfig.
|
|
68
|
-
* @param {ConfigContext} ctx the context
|
|
69
|
-
* @param {string} contentBusId
|
|
70
|
-
* @param {string} partition
|
|
71
|
-
* @returns {Promise<ConfigAll|null>} the project configuration
|
|
72
|
-
*/
|
|
73
|
-
async function fetchConfigAll(ctx, contentBusId, partition) {
|
|
74
|
-
const key = `${contentBusId}/${partition}/.helix/config-all.json`;
|
|
75
|
-
const res = await ctx.loader.getObject(HELIX_CONTENT_BUS, key);
|
|
76
|
-
if (!res.body) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return res.json();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
65
|
/**
|
|
83
66
|
* Retrieves the robots.txt from the code-bus
|
|
84
67
|
* @param {ConfigContext} ctx the context
|
|
@@ -170,8 +153,10 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
|
170
153
|
};
|
|
171
154
|
cfg.data = config;
|
|
172
155
|
ctx.timer?.update('legacy-config-all');
|
|
173
|
-
const
|
|
174
|
-
|
|
156
|
+
const {
|
|
157
|
+
preview: configAllPreview,
|
|
158
|
+
live: configAllLive,
|
|
159
|
+
} = await contentConfigMerge(ctx, config.content.contentBusId, rso);
|
|
175
160
|
const { access, admin } = configAllPreview?.config?.data || {};
|
|
176
161
|
if (access) {
|
|
177
162
|
config.access = {
|
|
@@ -204,9 +189,9 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
|
204
189
|
if (scope === SCOPE_PIPELINE) {
|
|
205
190
|
config.metadata = {};
|
|
206
191
|
if (configAllPreview) {
|
|
207
|
-
config.metadata.preview = configAllPreview.metadata
|
|
192
|
+
config.metadata.preview = configAllPreview.metadata;
|
|
208
193
|
}
|
|
209
|
-
if (configAllLive
|
|
194
|
+
if (configAllLive) {
|
|
210
195
|
config.metadata.live = configAllLive.metadata;
|
|
211
196
|
}
|
|
212
197
|
ctx.timer?.update('robots-txt');
|
|
@@ -222,5 +207,5 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
|
222
207
|
config.metadata.source = configAllPreview?.config?.data?.metadata;
|
|
223
208
|
}
|
|
224
209
|
|
|
225
|
-
return cfg;
|
|
210
|
+
return prune(cfg);
|
|
226
211
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2022 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a config row-based name/value pair array with dotted key notation to an object.
|
|
15
|
+
*
|
|
16
|
+
* example:
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* host: www.adobe.com
|
|
20
|
+
* document.level: 42,
|
|
21
|
+
* document.title: hello
|
|
22
|
+
* document.body.status: 200
|
|
23
|
+
* acl.allow: /foo
|
|
24
|
+
* acl.allow: /bar
|
|
25
|
+
* cnd.0.type: fastly
|
|
26
|
+
* cnd.0.host: www.adobe.com
|
|
27
|
+
* cnd.1.type: cloudflare
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* becomes:
|
|
31
|
+
*
|
|
32
|
+
* ```
|
|
33
|
+
* {
|
|
34
|
+
* host: 'www.adobe.com',
|
|
35
|
+
* document: {
|
|
36
|
+
* level: 42,
|
|
37
|
+
* title: 'hello',
|
|
38
|
+
* body: {
|
|
39
|
+
* status: 200,
|
|
40
|
+
* }
|
|
41
|
+
* },
|
|
42
|
+
* acl: {
|
|
43
|
+
* allow: ['/foo', '/bar],
|
|
44
|
+
* }
|
|
45
|
+
* cnd: [
|
|
46
|
+
* { type: 'fastly', host: 'www.adobe.com' },
|
|
47
|
+
* { type: 'cloudflare' },
|
|
48
|
+
* ]
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @param {object[]} rows array of columns
|
|
53
|
+
* @param {string} [keyName = 'key'] name of the key column.
|
|
54
|
+
* @param {string} [valueName = 'value'] name of the value column.
|
|
55
|
+
*/
|
|
56
|
+
export function flatJson2object(rows, keyName = 'key', valueName = 'value') {
|
|
57
|
+
const arrays = [];
|
|
58
|
+
const update = (o, segs, value) => {
|
|
59
|
+
const seg = segs.shift();
|
|
60
|
+
if (segs.length) {
|
|
61
|
+
if (seg in o) {
|
|
62
|
+
update(o[seg], segs, value);
|
|
63
|
+
return o;
|
|
64
|
+
}
|
|
65
|
+
const n = Number.parseInt(segs[0], 10);
|
|
66
|
+
if (Number.isNaN(n)) {
|
|
67
|
+
// eslint-disable-next-line no-param-reassign
|
|
68
|
+
o[seg] = update(Object.create(null), segs, value);
|
|
69
|
+
} else {
|
|
70
|
+
// eslint-disable-next-line no-param-reassign
|
|
71
|
+
o[seg] = update([], segs, value);
|
|
72
|
+
arrays.push(o[seg]);
|
|
73
|
+
}
|
|
74
|
+
return o;
|
|
75
|
+
}
|
|
76
|
+
if (!(seg in o)) {
|
|
77
|
+
// eslint-disable-next-line no-param-reassign
|
|
78
|
+
o[seg] = value;
|
|
79
|
+
} else if (Array.isArray(o[seg])) {
|
|
80
|
+
// eslint-disable-next-line no-param-reassign
|
|
81
|
+
o[seg].push(value);
|
|
82
|
+
} else {
|
|
83
|
+
// eslint-disable-next-line no-param-reassign
|
|
84
|
+
o[seg] = [o[seg], value];
|
|
85
|
+
}
|
|
86
|
+
return o;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const obj = Object.create(null);
|
|
90
|
+
rows.forEach((row) => {
|
|
91
|
+
update(obj, String(row[keyName]).trim().split('.'), String(row[valueName]).trim());
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// compact arrays
|
|
95
|
+
arrays.forEach((a) => {
|
|
96
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
97
|
+
if (!(i in a)) {
|
|
98
|
+
a.splice(i, 1);
|
|
99
|
+
i -= 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return obj;
|
|
104
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2022 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
|
+
import { logLevelForStatusCode, propagateStatusCode } from '@adobe/helix-shared-utils';
|
|
14
|
+
import { ModifiersConfig } from '@adobe/helix-shared-config';
|
|
15
|
+
import { flatJson2object } from './flatJson2Object.js';
|
|
16
|
+
|
|
17
|
+
export const CONFIG_JSON_PATH = '/.helix/config.json';
|
|
18
|
+
|
|
19
|
+
export const HEADERS_JSON_PATH = '/.helix/headers.json';
|
|
20
|
+
|
|
21
|
+
export const METADATA_JSON_PATH = '/metadata.json';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Coerces the given value to an array. if the value is null or undefined, an empty array is
|
|
25
|
+
* returned.
|
|
26
|
+
* @param {*} value
|
|
27
|
+
* @param {boolean} [unique = false] if true, the resulting array will contain only unique values
|
|
28
|
+
* @return {[]}
|
|
29
|
+
*/
|
|
30
|
+
export function coerceArray(value, unique = false) {
|
|
31
|
+
if (value === null || value === undefined) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const array = Array.isArray(value) ? value : [value];
|
|
35
|
+
if (unique) {
|
|
36
|
+
return Array.from(new Set(array));
|
|
37
|
+
}
|
|
38
|
+
return array;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* From a JSON response, retrieves the `data` sheet if this is a single sheet,
|
|
43
|
+
* or it returns the first existing sheet given by a list of names, if it is a
|
|
44
|
+
* multisheet.
|
|
45
|
+
* Returns `null` if there is neither.
|
|
46
|
+
*
|
|
47
|
+
* @param {any} json JSON object
|
|
48
|
+
* @param {String[]} names names to check in a multi sheet
|
|
49
|
+
*/
|
|
50
|
+
export function getSheetData(json, names) {
|
|
51
|
+
/* c8 ignore next 3 */
|
|
52
|
+
if (Array.isArray(json.data)) {
|
|
53
|
+
return json.data;
|
|
54
|
+
}
|
|
55
|
+
let sheet;
|
|
56
|
+
|
|
57
|
+
const match = names.find((name) => !!json[name]);
|
|
58
|
+
if (match) {
|
|
59
|
+
sheet = json[match];
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(sheet?.data)) {
|
|
62
|
+
return sheet.data;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Helper class to load and generate the aggregate configs
|
|
69
|
+
*/
|
|
70
|
+
export class ConfigMerger {
|
|
71
|
+
constructor(log, contentBusId, partition) {
|
|
72
|
+
Object.assign(this, {
|
|
73
|
+
log,
|
|
74
|
+
contentBusId,
|
|
75
|
+
partition,
|
|
76
|
+
lastModifiedTime: null,
|
|
77
|
+
lastModified: null,
|
|
78
|
+
all: {
|
|
79
|
+
version: 1,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toJSON() {
|
|
85
|
+
return this.all;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Loads a file from the content bus
|
|
90
|
+
* @param {ConfigContext} ctx
|
|
91
|
+
* @param {string} opts.path
|
|
92
|
+
* @param {string} opts.property
|
|
93
|
+
* @param {function} opts.mapping
|
|
94
|
+
* @returns {Promise<>}
|
|
95
|
+
*/
|
|
96
|
+
async loadFile(ctx, opts) {
|
|
97
|
+
const key = `${this.contentBusId}/${this.partition}${opts.path}`;
|
|
98
|
+
const fileResponse = await ctx.loader.getObject('helix-content-bus', key);
|
|
99
|
+
const fileText = fileResponse.body;
|
|
100
|
+
|
|
101
|
+
let data;
|
|
102
|
+
if (fileResponse.status === 200) {
|
|
103
|
+
try {
|
|
104
|
+
data = getSheetData(JSON.parse(fileText), ['default']);
|
|
105
|
+
// eslint-disable-next-line no-empty
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
if (!data) {
|
|
109
|
+
this.log.info(`[config-all] Error loading ${key}: empty or invalid json.`);
|
|
110
|
+
} else {
|
|
111
|
+
this.log.info(`[config-all] loaded: ${key}`);
|
|
112
|
+
}
|
|
113
|
+
} else if (fileResponse.status === 404) {
|
|
114
|
+
this.log.info(`[config-all] Error loading ${key}: no such object`);
|
|
115
|
+
} else {
|
|
116
|
+
this.log[logLevelForStatusCode(fileResponse.status)](`Invalid response (${fileResponse.status}) when loading json from ${key}`);
|
|
117
|
+
const err = new Error(`Unable to fetch json: ${fileResponse.status} ${fileText}`);
|
|
118
|
+
err.status = propagateStatusCode(fileResponse.status);
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
if (data) {
|
|
122
|
+
// get the config property for this file
|
|
123
|
+
let config = this.all[opts.property];
|
|
124
|
+
if (!config) {
|
|
125
|
+
config = {};
|
|
126
|
+
this.all[opts.property] = config;
|
|
127
|
+
}
|
|
128
|
+
// update the data of the config
|
|
129
|
+
config.data = opts.mapping(data, config);
|
|
130
|
+
this.updateLastModified(config, fileResponse.headers);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Update the last modified in the config object and tracks the most recent one
|
|
136
|
+
* @param {object} obj
|
|
137
|
+
* @param {Map} headers
|
|
138
|
+
*/
|
|
139
|
+
updateLastModified(obj, headers) {
|
|
140
|
+
const httpDate = headers.get('x-source-last-modified') ?? headers.get('last-modified');
|
|
141
|
+
/* c8 ignore next 3 */
|
|
142
|
+
if (!httpDate) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const time = new Date(httpDate).getTime();
|
|
146
|
+
/* c8 ignore next 4 */
|
|
147
|
+
if (Number.isNaN(time)) {
|
|
148
|
+
this.log.warn(`[config-all] updateLastModified date is invalid: ${httpDate}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
obj.lastModified = httpDate;
|
|
152
|
+
if (time > (this.lastModifiedTime ?? 0)) {
|
|
153
|
+
this.lastModifiedTime = time;
|
|
154
|
+
this.lastModified = httpDate;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clone(partition) {
|
|
159
|
+
const c = new ConfigMerger(this.log, this.contentBusId, partition);
|
|
160
|
+
c.lastModified = this.lastModified;
|
|
161
|
+
c.lastModifiedTime = this.lastModifiedTime;
|
|
162
|
+
// copy .helix/config.json and .helix/headers.json since it's the same for both partitions
|
|
163
|
+
c.all.config = this.all.config;
|
|
164
|
+
if (this.all.headers) {
|
|
165
|
+
c.all.headers = this.all.headers;
|
|
166
|
+
}
|
|
167
|
+
return c;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handles merging content bus configuration files. The metadata and headers are specific to
|
|
173
|
+
* their partition, whereas the config is always loaded from preview.
|
|
174
|
+
*
|
|
175
|
+
* this means that config-all is always updated in both partitions
|
|
176
|
+
*
|
|
177
|
+
* @param {ConfigContext} ctx the universal context
|
|
178
|
+
* @param {string} contentBusId the content bus id
|
|
179
|
+
* @param {Response} res the response of the preceding content bus operation (preview/publish)
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
* @implements ResponseHook
|
|
182
|
+
*/
|
|
183
|
+
export async function contentConfigMerge(ctx, contentBusId) {
|
|
184
|
+
const { log } = ctx;
|
|
185
|
+
|
|
186
|
+
// load config always from preview
|
|
187
|
+
const cfgPreview = new ConfigMerger(log, contentBusId, 'preview');
|
|
188
|
+
await cfgPreview.loadFile(ctx, {
|
|
189
|
+
path: CONFIG_JSON_PATH,
|
|
190
|
+
property: 'config',
|
|
191
|
+
mapping: (data) => flatJson2object(data),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// load headers always from preview
|
|
195
|
+
await cfgPreview.loadFile(ctx, {
|
|
196
|
+
path: HEADERS_JSON_PATH,
|
|
197
|
+
property: 'headers',
|
|
198
|
+
mapping: (data) => ModifiersConfig.parseModifierSheet(data),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const cfgLive = cfgPreview.clone('live');
|
|
202
|
+
const updates = [cfgPreview, cfgLive];
|
|
203
|
+
|
|
204
|
+
// generate preview and live versions
|
|
205
|
+
await Promise.all(updates.map((async (cfg) => {
|
|
206
|
+
// generate the metadata-all.json first
|
|
207
|
+
const metadataPaths = coerceArray(cfg.all.config?.data?.metadata || [METADATA_JSON_PATH], true);
|
|
208
|
+
for (const path of metadataPaths) {
|
|
209
|
+
const loadOpts = {
|
|
210
|
+
path,
|
|
211
|
+
property: 'metadata',
|
|
212
|
+
mapping: (data, metadata) => {
|
|
213
|
+
if (!Array.isArray(metadata.data)) {
|
|
214
|
+
metadata.data = [];
|
|
215
|
+
}
|
|
216
|
+
metadata.data.push(...data);
|
|
217
|
+
return metadata.data;
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop
|
|
221
|
+
await cfg.loadFile(ctx, loadOpts);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// convert to modifiers map
|
|
225
|
+
if (cfg.all.metadata?.data) {
|
|
226
|
+
cfg.all.metadata.data = ModifiersConfig.parseModifierSheet(cfg.all.metadata.data);
|
|
227
|
+
} else {
|
|
228
|
+
cfg.all.metadata = {};
|
|
229
|
+
}
|
|
230
|
+
})));
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
preview: cfgPreview.toJSON(),
|
|
234
|
+
live: cfgLive.toJSON(),
|
|
235
|
+
};
|
|
236
|
+
}
|