@adobe/helix-config 5.10.3 → 5.11.0

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,10 @@
1
+ # [5.11.0](https://github.com/adobe/helix-config/compare/v5.10.3...v5.11.0) (2026-06-09)
2
+
3
+
4
+ ### Features
5
+
6
+ * remove unsupported legacy config ([#385](https://github.com/adobe/helix-config/issues/385)) ([deb5e82](https://github.com/adobe/helix-config/commit/deb5e8237e2e36bed1739c9e6701c2b862b60de7))
7
+
1
8
  ## [5.10.3](https://github.com/adobe/helix-config/compare/v5.10.2...v5.10.3) (2026-05-19)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config",
3
- "version": "5.10.3",
3
+ "version": "5.11.0",
4
4
  "description": "Helix Config",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -35,8 +35,8 @@
35
35
  "reporter-options": "configFile=.mocha-multi.json"
36
36
  },
37
37
  "devDependencies": {
38
- "@adobe/eslint-config-helix": "3.0.27",
39
- "@eslint/config-helpers": "0.5.5",
38
+ "@adobe/eslint-config-helix": "3.0.28",
39
+ "@eslint/config-helpers": "0.6.0",
40
40
  "@semantic-release/changelog": "6.0.3",
41
41
  "@semantic-release/git": "10.0.1",
42
42
  "@semantic-release/npm": "13.1.5",
@@ -44,8 +44,8 @@
44
44
  "eslint": "9.4.0",
45
45
  "husky": "9.1.7",
46
46
  "junit-report-builder": "5.1.2",
47
- "lint-staged": "16.4.0",
48
- "mocha": "11.7.5",
47
+ "lint-staged": "17.0.5",
48
+ "mocha": "11.7.6",
49
49
  "mocha-multi-reporters": "1.5.1",
50
50
  "mocha-suppress-logs": "0.6.0",
51
51
  "semantic-release": "25.0.3"
@@ -30,7 +30,7 @@ export const SCOPE_PIPELINE = 'pipeline';
30
30
  export const SCOPE_ADMIN = 'admin';
31
31
 
32
32
  /**
33
- * Raw config service view, also for legacy sites.
33
+ * Raw config service view.
34
34
  * @type {string}
35
35
  */
36
36
  export const SCOPE_RAW = 'raw';
@@ -18,7 +18,6 @@ import {
18
18
  SCOPE_DELIVERY,
19
19
  SCOPE_RAW, SCOPE_PUBLIC,
20
20
  } from './ConfigContext.js';
21
- import { resolveLegacyConfig, fetchRobotsTxt } from './config-legacy.js';
22
21
  import {
23
22
  canonicalArrayString, deepGetOrCreate, prune, uniqueArray,
24
23
  } from './utils.js';
@@ -146,11 +145,11 @@ function resolveSecret(object, idProp, dstProp, siteConfig, orgConfig) {
146
145
  /**
147
146
  * Returns the normalized access configuration for the given partition.
148
147
  *
149
- * SecretIds (and legacy apiKeyIds) can be set on access.preview, access.live, and access.site.
150
- * The normalized config only has access.preview and access.live; access.site is not present in
151
- * the final object and is only used during resolution. For each partition (preview or live),
152
- * the effective secretId and allow lists are the merge of the partition's values and
153
- * access.site's values (union, deduplicated).
148
+ * SecretIds (apiKeyId is the deprecated alias of secretId) can be set on access.preview,
149
+ * access.live, and access.site. The normalized config only has access.preview and access.live;
150
+ * access.site is not present in the final object and is only used during resolution. For each
151
+ * partition (preview or live), the effective secretId and allow lists are the merge of the
152
+ * partition's values and access.site's values (union, deduplicated).
154
153
  *
155
154
  * @param {object} ctx - The context
156
155
  * @param {object} config - The config (with access.preview, access.live, access.site)
@@ -163,7 +162,8 @@ function resolveSecret(object, idProp, dstProp, siteConfig, orgConfig) {
163
162
  export async function getAccessConfig(ctx, config, orgConfig, partition, rso) {
164
163
  const { access = {} } = config;
165
164
  const pAccess = access[partition] ?? {};
166
- // merge partition and site secretIds (and legacy apiKeyIds), then allow
165
+
166
+ // union of the partition's and the site-wide secretIds (apiKeyId is the deprecated alias)
167
167
  const secretId = uniqueArray(
168
168
  pAccess.apiKeyId,
169
169
  pAccess.secretId,
@@ -171,37 +171,43 @@ export async function getAccessConfig(ctx, config, orgConfig, partition, rso) {
171
171
  access.site?.secretId,
172
172
  );
173
173
  const allow = uniqueArray(pAccess.allow, access.site?.allow);
174
- const cfg = {
175
- secretId,
176
- allow,
177
- tokenHash: secretId
178
- // token ids are always stored in base64url format, but legacy apiKeyIds are not
179
- .map((jti) => jti.replaceAll('/', '_').replaceAll('+', '-'))
180
- .map((id) => lookupSecret(config, orgConfig, id, true))
181
- .filter((hash) => !!hash),
182
- };
183
- // if an allow is defined but no secretId, create a fake one so that auth is still enforced.
184
- if (allow.length && !cfg.secretId.length) {
185
- cfg.secretId.push('dummy');
186
- }
187
174
 
188
- // if an secretId is defined but no tokenHash, create a fake one so that auth is still enforced.
189
- if (cfg.secretId.length) {
190
- // add global token hash if defined and needed
175
+ // resolve each secretId to its token hash (ids are stored base64url, apiKeyIds may not be)
176
+ const tokenHash = secretId
177
+ .map((id) => id.replaceAll('/', '_').replaceAll('+', '-'))
178
+ .map((id) => lookupSecret(config, orgConfig, id, true))
179
+ .filter((hash) => !!hash);
180
+
181
+ // when auth is required (an allow list or any secretId), always accept the global delivery
182
+ // token if configured; otherwise ensure at least one hash exists so auth stays enforced
183
+ if (allow.length || secretId.length) {
191
184
  const globalTokenHash = await getGlobalTokenHash(ctx, rso);
192
- if (cfg.tokenHash.length && globalTokenHash) {
193
- // augment the list of hashes with the global one if exists
194
- cfg.tokenHash.push(globalTokenHash);
195
- } else if (!cfg.tokenHash.length) {
196
- // add a dummy or global hash if no tokens match the secretIds.
197
- if (!config.legacy || allow.length) {
198
- // but only add for non-legacy sites or legacy with allows
199
- cfg.tokenHash.push(globalTokenHash || 'n/a');
200
- }
185
+ if (globalTokenHash) {
186
+ tokenHash.push(globalTokenHash);
187
+ } else if (!tokenHash.length) {
188
+ tokenHash.push('n/a');
201
189
  }
202
190
  }
203
191
 
204
- return cfg;
192
+ return { secretId, allow, tokenHash };
193
+ }
194
+
195
+ /**
196
+ * Retrieves the robots.txt from the code-bus
197
+ * @param {ConfigContext} ctx the context
198
+ * @param {string} owner
199
+ * @param {string} repo
200
+ * @returns {Promise<ConfigObject>} the robots.txt
201
+ */
202
+ async function fetchRobotsTxt(ctx, owner, repo) {
203
+ const key = `${owner}/${repo}/main/robots.txt`;
204
+ const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
205
+ const robots = new ConfigObject();
206
+ if (res.body) {
207
+ robots.txt = res.body;
208
+ robots.updateLastModified(res.headers);
209
+ }
210
+ return robots;
205
211
  }
206
212
 
207
213
  /**
@@ -318,14 +324,7 @@ async function resolveConfig(ctx, rso, scope) {
318
324
  ctx.log.error('error loading config %s: %d', key, res.status);
319
325
  throw new StatusCodeError('error loading config', res.status);
320
326
  }
321
- const config = await resolveLegacyConfig(ctx, rso, scope);
322
- if (config) {
323
- ctx.timer?.update('profile');
324
- const profile = await loadProfile(ctx, rso, 'default');
325
- config.merge(profile);
326
- config.data.legacy = true;
327
- }
328
- return config;
327
+ return null;
329
328
  }
330
329
  const site = new ConfigObject();
331
330
  site.data = res.json();
@@ -649,7 +648,7 @@ export async function getConfigResponse(ctx, opts) {
649
648
  },
650
649
  });
651
650
  }
652
- headers['x-hlx-version'] = siteConfig.data.legacy ? 4 : 5;
651
+ headers['x-hlx-version'] = 5;
653
652
 
654
653
  const config = siteConfig.data;
655
654
  if (scope === SCOPE_PIPELINE || config.extends?.profile === 'default') {
@@ -771,7 +770,6 @@ export async function getConfigResponse(ctx, opts) {
771
770
  public: config.public,
772
771
  robots: config.robots,
773
772
  access: config.access,
774
- legacy: config.legacy,
775
773
  lastModified: siteConfig.lastModified,
776
774
  created: config.created,
777
775
  trustedHosts: computeTrustedHosts(org, site, config),
@@ -1,228 +0,0 @@
1
- /*
2
- * Copyright 2024 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
- import {
13
- SCOPE_ADMIN, SCOPE_DELIVERY, SCOPE_PIPELINE, SCOPE_RAW,
14
- } from './ConfigContext.js';
15
- import { ConfigObject } from './config-object.js';
16
- import { contentConfigMerge } from './legacy-config-merge.js';
17
- import { prune, toArray } from './utils.js';
18
-
19
- const HELIX_CODE_BUS = 'helix-code-bus';
20
-
21
- /**
22
- * Retrieves the head.html from the code bus
23
- * @param ctx
24
- * @param rso
25
- * @returns {Promise<ConfigObject|{}>}
26
- */
27
- async function fetchHeadHtml(ctx, rso) {
28
- const key = `${rso.org}/${rso.site}/${rso.ref}/head.html`;
29
- const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
30
- if (res.body) {
31
- const head = new ConfigObject();
32
- head.html = res.body;
33
- head.updateLastModified(res.headers);
34
- return head;
35
- }
36
- return {};
37
- }
38
-
39
- /**
40
- * Retrieves the helix-config.json which is an aggregate from fstab.yaml and head.html.
41
- *
42
- * @param {ConfigContext} ctx the context
43
- * @param {RSO} rso
44
- * @returns {Promise<ConfigObject|null>} the helix-config or {@code null} if optional and not found.
45
- */
46
- async function fetchHelixConfig(ctx, rso) {
47
- const key = `${rso.org}/${rso.site}/main/helix-config.json`;
48
- const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
49
- if (!res.body) {
50
- return null;
51
- }
52
- const config = res.json();
53
- // set contentbus id, if present in header
54
- const cbid = res.headers.get('x-amz-meta-x-contentbus-id');
55
- if (cbid && !config.content) {
56
- // create the content section if not already present
57
- config.content = {
58
- data: Object.create(null),
59
- };
60
- const content = config.content.data;
61
- const nvps = cbid.split(/;\s+/);
62
- for (const nv of nvps) {
63
- const [path, id] = nv.split('=');
64
- if (!content[path]) {
65
- content[path] = {};
66
- }
67
- content[path].contentBusId = id;
68
- }
69
- }
70
- const cfg = new ConfigObject();
71
- cfg.updateLastModified(res.headers);
72
- cfg.data = config;
73
- return cfg;
74
- }
75
-
76
- /**
77
- * Retrieves the robots.txt from the code-bus
78
- * @param {ConfigContext} ctx the context
79
- * @param {string} owner
80
- * @param {string} repo
81
- * @returns {Promise<ConfigObject>} the robots.txt
82
- */
83
- export async function fetchRobotsTxt(ctx, owner, repo) {
84
- const key = `${owner}/${repo}/main/robots.txt`;
85
- const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
86
- const robots = new ConfigObject();
87
- if (res.body) {
88
- robots.txt = res.body;
89
- robots.updateLastModified(res.headers);
90
- }
91
- return robots;
92
- }
93
-
94
- async function resolveAdminAccess(ctx, admin) {
95
- const ret = {
96
- ...admin,
97
- };
98
- if (ret.apiKeyId) {
99
- ret.apiKeyId = toArray(ret.apiKeyId);
100
- }
101
- if (ret.defaultRole) {
102
- ret.defaultRole = toArray(ret.defaultRole);
103
- }
104
- for (const [role, users] of Object.entries(ret.role ?? [])) {
105
- // todo: load users.json
106
- ret.role[role] = toArray(users);
107
- }
108
- return ret;
109
- }
110
-
111
- /**
112
- * Loads the content from a helix 4 project.
113
- * @param {import('./ConfigContext.js)'.ConfigContext} ctx
114
- * @param {RSO} rso
115
- * @param {string} scope
116
- * @returns {Promise<ConfigObject|null>} the config object or {@code null} if not found.
117
- */
118
- export async function resolveLegacyConfig(ctx, rso, scope) {
119
- ctx.timer?.update('legacy-helix-config');
120
-
121
- // set owner==org and repo==site and fetch from helix-config for now
122
- const cfg = await fetchHelixConfig(ctx, rso);
123
- if (!cfg) {
124
- return null;
125
- }
126
- const helixConfig = cfg.data;
127
- const { contentBusId } = helixConfig.content.data['/'];
128
- if (!contentBusId) {
129
- return null;
130
- }
131
- const fstab = helixConfig.fstab?.data || helixConfig.fstab;
132
- if (!fstab) {
133
- return null;
134
- }
135
- let source = fstab.mountpoints['/'];
136
- if (!source) {
137
- return null;
138
- }
139
- if (typeof source === 'string') {
140
- source = {
141
- type: source.startsWith('https://drive.google.com/')
142
- ? 'google'
143
- : 'onedrive',
144
- url: source,
145
- };
146
- } else {
147
- delete source.contentBusId;
148
- }
149
-
150
- const head = await fetchHeadHtml(ctx, rso);
151
- const config = {
152
- version: 1,
153
- legacy: true,
154
- code: {
155
- owner: rso.org,
156
- repo: rso.site,
157
- source: {
158
- type: 'github',
159
- url: `https://github.com/${rso.org}/${rso.site}`,
160
- },
161
- },
162
- content: {
163
- contentBusId,
164
- source,
165
- },
166
- folders: fstab.folders ?? {},
167
- head,
168
- };
169
- cfg.data = config;
170
- ctx.timer?.update('legacy-config-all');
171
- const {
172
- preview: configAllPreview,
173
- live: configAllLive,
174
- } = await contentConfigMerge(ctx, config.content.contentBusId, rso);
175
- const { access, admin } = configAllPreview?.config?.data || {};
176
- if (access) {
177
- config.access = {
178
- preview: access.preview,
179
- live: access.live,
180
- };
181
- delete access.preview;
182
- delete access.live;
183
- config.access.site = access;
184
- }
185
- if (admin) {
186
- if (!config.access) {
187
- config.access = { };
188
- }
189
- config.access.admin = await resolveAdminAccess(ctx, admin);
190
- }
191
- if (configAllPreview) {
192
- config.cdn = configAllPreview.config?.data.cdn ?? {};
193
- if (!config.cdn.prod?.host && configAllPreview.config?.data.host) {
194
- config.cdn.prod = {
195
- host: configAllPreview.config.data.host,
196
- };
197
- }
198
- }
199
-
200
- if (scope !== SCOPE_DELIVERY && configAllPreview) {
201
- config.headers = configAllPreview.headers?.data ?? {};
202
- }
203
-
204
- if (scope === SCOPE_PIPELINE) {
205
- config.metadata = {};
206
- if (configAllPreview) {
207
- config.metadata.preview = configAllPreview.metadata;
208
- }
209
- if (configAllLive) {
210
- config.metadata.live = configAllLive.metadata;
211
- }
212
- ctx.timer?.update('robots-txt');
213
- config.robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
214
- if (!config.robots.txt) {
215
- delete config.robots;
216
- } else {
217
- cfg.updateLastModified(config.robots);
218
- }
219
- }
220
- if ((scope === SCOPE_ADMIN || scope === SCOPE_RAW) && configAllPreview?.config?.data?.metadata) {
221
- if (!config.metadata) {
222
- config.metadata = {};
223
- }
224
- config.metadata.source = configAllPreview?.config?.data?.metadata;
225
- }
226
-
227
- return prune(cfg);
228
- }
@@ -1,104 +0,0 @@
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
- }
@@ -1,236 +0,0 @@
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 { flatJson2object } from './flatJson2Object.js';
15
- import { ModifiersConfig } from './ModifiersConfig.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
- }