@adobe/helix-config 4.3.0 → 4.3.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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [4.3.1](https://github.com/adobe/helix-config/compare/v4.3.0...v4.3.1) (2024-08-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * include and set last-modified ([#174](https://github.com/adobe/helix-config/issues/174)) ([b325569](https://github.com/adobe/helix-config/commit/b3255693807e1ee20609f4fc9411308a7174a50a)), closes [#171](https://github.com/adobe/helix-config/issues/171)
7
+
1
8
  # [4.3.0](https://github.com/adobe/helix-config/compare/v4.2.0...v4.3.0) (2024-08-20)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config",
3
- "version": "4.3.0",
3
+ "version": "4.3.1",
4
4
  "description": "Helix Config",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { SCOPE_PIPELINE } from './ConfigContext.js';
13
+ import { ConfigObject } from './config-object.js';
13
14
 
14
15
  const HELIX_CODE_BUS = 'helix-code-bus';
15
16
 
@@ -78,12 +79,17 @@ async function fetchConfigAll(ctx, contentBusId, partition) {
78
79
  * @param {ConfigContext} ctx the context
79
80
  * @param {string} owner
80
81
  * @param {string} repo
81
- * @returns {Promise<string|null>} the robots.txt
82
+ * @returns {Promise<ConfigObject>} the robots.txt
82
83
  */
83
84
  export async function fetchRobotsTxt(ctx, owner, repo) {
84
85
  const key = `${owner}/${repo}/main/robots.txt`;
85
86
  const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
86
- return res.body;
87
+ const robots = new ConfigObject();
88
+ if (res.body) {
89
+ robots.txt = res.body;
90
+ robots.updateLastModified(res.headers);
91
+ }
92
+ return robots;
87
93
  }
88
94
 
89
95
  async function resolveAdminAccess(ctx, admin) {
@@ -108,7 +114,7 @@ async function resolveAdminAccess(ctx, admin) {
108
114
  * @param {ConfigContext} ctx
109
115
  * @param {RSO} rso
110
116
  * @param {string} scope
111
- * @returns {Promise<Config>}
117
+ * @returns {Promise<ConfigObject|null>} the config object or {@code null} if not found.
112
118
  */
113
119
  export async function resolveLegacyConfig(ctx, rso, scope) {
114
120
  // set owner==org and repo==site and fetch from helix-config for now
@@ -178,12 +184,12 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
178
184
  if (configAllLive?.metadata) {
179
185
  config.metadata.live = configAllLive.metadata;
180
186
  }
181
- const robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
182
- if (robots) {
183
- config.robots = {
184
- txt: robots,
185
- };
187
+ config.robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
188
+ if (!config.robots.txt) {
189
+ delete config.robots;
186
190
  }
187
191
  }
188
- return config;
192
+ const obj = new ConfigObject();
193
+ obj.data = config;
194
+ return obj;
189
195
  }
@@ -133,10 +133,13 @@ function mergeConfig(dst, src) {
133
133
  });
134
134
  }
135
135
 
136
+ /**
137
+ * Returns the merge configuration of the site and the profile.
138
+ * @param {ConfigObject} site
139
+ * @param {ConfigObject} profile
140
+ * @returns {*}
141
+ */
136
142
  export function getMergedConfig(site, profile) {
137
- if (!profile) {
138
- return site;
139
- }
140
143
  const ret = Object.create(null);
141
144
  ret.version = site.version;
142
145
  mergeConfig(ret, profile);
@@ -0,0 +1,58 @@
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 { getMergedConfig } from './config-merge.js';
13
+
14
+ export class ConfigObject {
15
+ #lastModifiedTime = -Infinity;
16
+
17
+ lastModified;
18
+
19
+ /**
20
+ * Update the last modified if newer.
21
+ * @param {string|map|ConfigObject} [httpDate]
22
+ */
23
+ updateLastModified(httpDate) {
24
+ if (!httpDate) {
25
+ return;
26
+ }
27
+ if (httpDate instanceof ConfigObject) {
28
+ if (httpDate.#lastModifiedTime > this.#lastModifiedTime) {
29
+ this.#lastModifiedTime = httpDate.#lastModifiedTime;
30
+ this.lastModified = httpDate.lastModified;
31
+ }
32
+ return;
33
+ }
34
+ if (typeof httpDate.get === 'function') {
35
+ // eslint-disable-next-line no-param-reassign
36
+ httpDate = httpDate.get('x-source-last-modified') ?? httpDate.get('last-modified');
37
+ }
38
+ if (!httpDate || typeof httpDate !== 'string') {
39
+ return;
40
+ }
41
+ const time = new Date(httpDate).getTime();
42
+ if (Number.isNaN(time)) {
43
+ return;
44
+ }
45
+ if (time > this.#lastModifiedTime) {
46
+ this.#lastModifiedTime = time;
47
+ this.lastModified = httpDate;
48
+ }
49
+ }
50
+
51
+ merge(other) {
52
+ if (!other) {
53
+ return;
54
+ }
55
+ this.data = getMergedConfig(this.data, other.data);
56
+ this.updateLastModified(other);
57
+ }
58
+ }
@@ -20,8 +20,8 @@ import {
20
20
  SCOPE_RAW, SCOPE_PUBLIC,
21
21
  } from './ConfigContext.js';
22
22
  import { resolveLegacyConfig, fetchRobotsTxt, toArray } from './config-legacy.js';
23
- import { getMergedConfig } from './config-merge.js';
24
23
  import { deepGetOrCreate } from './utils.js';
24
+ import { ConfigObject } from './config-object.js';
25
25
 
26
26
  /**
27
27
  * @typedef Config
@@ -152,7 +152,7 @@ export async function getAccessConfig(ctx, config, partition, rso) {
152
152
  * @param ctx the context
153
153
  * @param config the config
154
154
  * @param partition the partition
155
- * @returns {Promise<{data: ModifierMap}|{}>} the metadata
155
+ * @returns {Promise<ConfigObject|{}>} the metadata
156
156
  */
157
157
  async function loadMetadata(ctx, config, partition) {
158
158
  const paths = config.metadata?.source ?? [];
@@ -161,6 +161,7 @@ async function loadMetadata(ctx, config, partition) {
161
161
  }
162
162
 
163
163
  // generate the metadata-all.json first
164
+ const metadataConfig = new ConfigObject();
164
165
  const metadata = [];
165
166
  for (const path of paths) {
166
167
  const key = `${config.content.contentBusId}/${partition}${path}`;
@@ -173,25 +174,25 @@ async function loadMetadata(ctx, config, partition) {
173
174
  metadata.push(...data);
174
175
  }
175
176
  }
177
+ metadataConfig.updateLastModified(res.headers);
176
178
  }
177
179
  if (!metadata.length) {
178
180
  return {};
179
181
  }
180
182
 
181
183
  // convert to modifiers map
182
- return {
183
- // todo: add lastModified
184
- data: ModifiersConfig.parseModifierSheet(metadata),
185
- };
184
+ metadataConfig.data = ModifiersConfig.parseModifierSheet(metadata);
185
+ return metadataConfig;
186
186
  }
187
187
 
188
188
  async function loadHeadHtml(ctx, config, ref) {
189
189
  const key = `${config.code.owner}/${config.code.repo}/${ref}/head.html`;
190
190
  const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
191
191
  if (res.body) {
192
- return {
193
- html: res.body,
194
- };
192
+ const head = new ConfigObject();
193
+ head.html = res.body;
194
+ head.updateLastModified(res.headers);
195
+ return head;
195
196
  }
196
197
  return {};
197
198
  }
@@ -214,7 +215,10 @@ async function loadProfile(ctx, rso, name) {
214
215
  const profileKey = `orgs/${rso.org}/profiles/${name}.json`;
215
216
  const res = await ctx.loader.getObject(HELIX_CONFIG_BUS, profileKey);
216
217
  if (res.body) {
217
- return res.json();
218
+ const profile = new ConfigObject();
219
+ profile.data = res.json();
220
+ profile.updateLastModified(res.headers);
221
+ return profile;
218
222
  }
219
223
  return null;
220
224
  }
@@ -224,7 +228,7 @@ async function loadProfile(ctx, rso, name) {
224
228
  * @param ctx
225
229
  * @param {RSO} rso
226
230
  * @param {string} scope
227
- * @return {Promise<Config|null>}
231
+ * @return {Promise<ConfigObject|null>}
228
232
  */
229
233
  async function resolveConfig(ctx, rso, scope) {
230
234
  // try to load site config from config-bus
@@ -235,39 +239,42 @@ async function resolveConfig(ctx, rso, scope) {
235
239
  const config = await resolveLegacyConfig(ctx, rso, scope);
236
240
  if (config) {
237
241
  const profile = await loadProfile(ctx, rso, 'default');
238
- config.tokens = profile?.tokens || {};
242
+ config.data.tokens = profile?.data.tokens || {};
239
243
  }
240
244
  return config;
241
245
  } else {
242
246
  return null;
243
247
  }
244
248
  }
245
- const site = res.json();
249
+ const site = new ConfigObject();
250
+ site.data = res.json();
251
+ site.updateLastModified(res.headers);
246
252
 
247
253
  // always load default profile if available
248
- if (!site.extends?.profile) {
249
- site.extends = {
254
+ if (!site.data.extends?.profile) {
255
+ site.data.extends = {
250
256
  profile: 'default',
251
257
  };
252
258
  }
253
- const profile = await loadProfile(ctx, rso, site.extends.profile);
254
- const config = getMergedConfig(site, profile);
259
+ const profile = await loadProfile(ctx, rso, site.data.extends.profile);
260
+ site.merge(profile);
261
+ const config = site.data;
255
262
  if (scope === SCOPE_PIPELINE) {
256
263
  config.metadata = {
257
264
  preview: await loadMetadata(ctx, config, 'preview'),
258
265
  live: await loadMetadata(ctx, config, 'live'),
259
266
  };
260
267
  if (!config.robots) {
261
- const txt = await fetchRobotsTxt(ctx, config.code.owner, config.code.repo);
262
- if (txt) {
263
- config.robots = { txt };
268
+ const robots = await fetchRobotsTxt(ctx, config.code.owner, config.code.repo);
269
+ if (robots.txt) {
270
+ config.robots = robots;
264
271
  }
265
272
  }
266
273
  }
267
274
  if (scope === SCOPE_PIPELINE || scope === SCOPE_DELIVERY) {
268
275
  config.head = await loadHeadHtml(ctx, config, rso.ref);
269
276
  }
270
- return config;
277
+ return site;
271
278
  }
272
279
 
273
280
  async function getSurrogateKey(opts, config) {
@@ -392,21 +399,22 @@ export async function getConfigResponse(ctx, opts) {
392
399
  }
393
400
 
394
401
  const rso = { ref, site, org };
395
- const config = await resolveConfig(ctx, rso, scope);
396
- const surrogateHeaders = {
397
- 'x-surrogate-key': await getSurrogateKey(opts, config),
402
+ const siteConfig = await resolveConfig(ctx, rso, scope);
403
+ const headers = {
404
+ 'x-surrogate-key': await getSurrogateKey(opts, siteConfig?.data),
398
405
  };
399
- if (!config) {
406
+ if (!siteConfig) {
400
407
  return new PipelineResponse('', {
401
408
  status: 404,
402
409
  headers: {
403
- ...surrogateHeaders,
410
+ ...headers,
404
411
  'x-error': 'config not found.',
405
412
  },
406
413
  });
407
414
  }
408
415
 
409
- if (config.extends && scope !== SCOPE_RAW) {
416
+ const config = siteConfig.data;
417
+ if (scope !== SCOPE_RAW) {
410
418
  delete config.extends;
411
419
  }
412
420
 
@@ -425,10 +433,22 @@ export async function getConfigResponse(ctx, opts) {
425
433
  }
426
434
  }
427
435
 
436
+ // todo: improve
437
+ siteConfig.updateLastModified(config.head);
438
+ siteConfig.updateLastModified(config.robots);
439
+ siteConfig.updateLastModified(config.metadata?.preview);
440
+ siteConfig.updateLastModified(config.metadata?.live);
441
+
442
+ if (siteConfig.lastModified) {
443
+ headers['last-modified'] = siteConfig.lastModified;
444
+ delete siteConfig.lastModified;
445
+ delete siteConfig.lastModifiedTime;
446
+ }
447
+
428
448
  if (opts.scope === SCOPE_DELIVERY) {
429
449
  return new PipelineResponse('', {
430
450
  headers: {
431
- ...surrogateHeaders,
451
+ ...headers,
432
452
  'x-hlx-contentbus-id': config.content.contentBusId,
433
453
  'x-hlx-owner': config.code.owner,
434
454
  'x-hlx-repo': config.code.repo,
@@ -464,7 +484,7 @@ export async function getConfigResponse(ctx, opts) {
464
484
  return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
465
485
  headers: {
466
486
  'content-type': 'application/json',
467
- ...surrogateHeaders,
487
+ ...headers,
468
488
  },
469
489
  });
470
490
  }
@@ -474,7 +494,7 @@ export async function getConfigResponse(ctx, opts) {
474
494
  return new PipelineResponse(JSON.stringify(config, null, 2), {
475
495
  headers: {
476
496
  'content-type': 'application/json',
477
- ...surrogateHeaders,
497
+ ...headers,
478
498
  },
479
499
  });
480
500
  }
@@ -501,7 +521,7 @@ export async function getConfigResponse(ctx, opts) {
501
521
  return new PipelineResponse(JSON.stringify(pipelineConfig, null, 2), {
502
522
  headers: {
503
523
  'content-type': 'application/json',
504
- ...surrogateHeaders,
524
+ ...headers,
505
525
  },
506
526
  });
507
527
  }
@@ -515,7 +535,7 @@ export async function getConfigResponse(ctx, opts) {
515
535
  return new PipelineResponse(JSON.stringify(publicConfig, null, 2), {
516
536
  headers: {
517
537
  'content-type': 'application/json',
518
- ...surrogateHeaders,
538
+ ...headers,
519
539
  },
520
540
  });
521
541
  }