@adobe/helix-config 5.6.4 → 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 CHANGED
@@ -1,3 +1,17 @@
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
+
8
+ ## [5.6.5](https://github.com/adobe/helix-config/compare/v5.6.4...v5.6.5) (2025-09-18)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * correctly handle metadata with data sheets ([#319](https://github.com/adobe/helix-config/issues/319)) ([47d4bd9](https://github.com/adobe/helix-config/commit/47d4bd940a3aef72e9ca7385665f3889da665f5b)), closes [#318](https://github.com/adobe/helix-config/issues/318)
14
+
1
15
  ## [5.6.4](https://github.com/adobe/helix-config/compare/v5.6.3...v5.6.4) (2025-09-08)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config",
3
- "version": "5.6.4",
3
+ "version": "5.6.6",
4
4
  "description": "Helix Config",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  "reporter-options": "configFile=.mocha-multi.json"
36
36
  },
37
37
  "devDependencies": {
38
- "@adobe/eslint-config-helix": "3.0.9",
38
+ "@adobe/eslint-config-helix": "3.0.10",
39
39
  "@eslint/config-helpers": "0.3.1",
40
40
  "@semantic-release/changelog": "6.0.3",
41
41
  "@semantic-release/git": "10.0.1",
@@ -48,7 +48,7 @@
48
48
  "mocha": "11.7.2",
49
49
  "mocha-multi-reporters": "1.5.1",
50
50
  "mocha-suppress-logs": "0.6.0",
51
- "semantic-release": "24.2.7"
51
+ "semantic-release": "24.2.8"
52
52
  },
53
53
  "lint-staged": {
54
54
  "*.js": "eslint",
@@ -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 configAllPreview = await fetchConfigAll(ctx, config.content.contentBusId, 'preview');
174
- const configAllLive = await fetchConfigAll(ctx, config.content.contentBusId, 'live');
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?.metadata) {
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
  }
@@ -221,7 +221,7 @@ async function loadMetadata(ctx, config, partition) {
221
221
  const res = await ctx.loader.getObject(HELIX_CONTENT_BUS, key);
222
222
  if (res.body) {
223
223
  const json = res.json();
224
- const data = json.data || json.default?.data;
224
+ const data = Array.isArray(json.data) ? json.data : json.default?.data;
225
225
  if (data) {
226
226
  metadata.push(...data);
227
227
  }
@@ -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
+ }