@adobe/helix-html-pipeline 2.0.2 → 2.1.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,25 @@
1
+ # [2.1.0](https://github.com/adobe/helix-html-pipeline/compare/v2.0.4...v2.1.0) (2022-06-02)
2
+
3
+
4
+ ### Features
5
+
6
+ * use /.helix/config-all.json ([#72](https://github.com/adobe/helix-html-pipeline/issues/72)) ([712046c](https://github.com/adobe/helix-html-pipeline/commit/712046c31d51eecc392bb2f0aabfd0e227ed595c))
7
+
8
+ ## [2.0.4](https://github.com/adobe/helix-html-pipeline/compare/v2.0.3...v2.0.4) (2022-05-31)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * fix glob matching for mapped pages ([c43e359](https://github.com/adobe/helix-html-pipeline/commit/c43e3590f9b564f988dfd35daeb5000e458dea71))
14
+ * rename mappedPath to unmappedPath ([12e0066](https://github.com/adobe/helix-html-pipeline/commit/12e0066c8da51ffcb16f8ac7e78306fae1a1cd93))
15
+
16
+ ## [2.0.3](https://github.com/adobe/helix-html-pipeline/compare/v2.0.2...v2.0.3) (2022-05-30)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **deps:** update dependency @adobe/helix-markdown-support to v3.1.6 ([1dd999a](https://github.com/adobe/helix-html-pipeline/commit/1dd999a2c4ef3caf06a6b66dd7ff3e091166ea72))
22
+
1
23
  ## [2.0.2](https://github.com/adobe/helix-html-pipeline/compare/v2.0.1...v2.0.2) (2022-05-28)
2
24
 
3
25
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "test": " c8 mocha",
9
+ "test": "c8 mocha",
10
10
  "lint": "eslint .",
11
11
  "docs": "npx jsdoc2md -c .jsdoc.json --files 'src/*.js' > docs/API.md",
12
12
  "semantic-release": "semantic-release",
@@ -29,8 +29,11 @@
29
29
  "reporter-options": "configFile=.mocha-multi.json",
30
30
  "loader": "esmock"
31
31
  },
32
+ "engines": {
33
+ "node": ">=16.x"
34
+ },
32
35
  "dependencies": {
33
- "@adobe/helix-markdown-support": "3.1.5",
36
+ "@adobe/helix-markdown-support": "3.1.6",
34
37
  "@adobe/helix-shared-utils": "2.0.10",
35
38
  "github-slugger": "1.4.0",
36
39
  "hast-util-raw": "7.2.1",
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer} from "./index";
13
13
  import {PipelineContent} from "./PipelineContent";
14
+ import {Modifiers} from './utils/modifiers';
14
15
 
15
16
  declare enum PipelineType {
16
17
  html = 'html',
@@ -18,6 +19,12 @@ declare enum PipelineType {
18
19
  form = 'form',
19
20
  }
20
21
 
22
+ declare interface HelixConfigAll {
23
+ host:string;
24
+ routes:RegExp[];
25
+ [string]:any;
26
+ }
27
+
21
28
  declare interface PipelineOptions {
22
29
  log: Console;
23
30
  s3Loader: S3Loader;
@@ -68,9 +75,19 @@ declare class PipelineState {
68
75
  helixConfig?: object;
69
76
 
70
77
  /**
71
- * metadata.json once loaded
78
+ * the /.helix/config.json in object form
79
+ */
80
+ config?: HelixConfigAll;
81
+
82
+ /**
83
+ * the metadata.json in modifier form.
84
+ */
85
+ metadata?: Modifiers;
86
+
87
+ /**
88
+ * the headers.json in modifier form.
72
89
  */
73
- metadata?: object;
90
+ headers?: Modifiers;
74
91
 
75
92
  /**
76
93
  * optional timer that is used to measure the timing
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { getPathInfo } from './utils/path.js';
14
14
  import { PipelineContent } from './PipelineContent.js';
15
+ import { Modifiers } from './utils/modifiers.js';
15
16
 
16
17
  /**
17
18
  * State of the pipeline
@@ -34,7 +35,9 @@ export class PipelineState {
34
35
  ref: opts.ref,
35
36
  partition: opts.partition,
36
37
  helixConfig: undefined,
37
- metadata: undefined,
38
+ metadata: Modifiers.EMPTY,
39
+ headers: Modifiers.EMPTY,
40
+ config: {},
38
41
  s3Loader: opts.s3Loader,
39
42
  messageDispatcher: opts.messageDispatcher,
40
43
  timer: opts.timer,
package/src/forms-pipe.js CHANGED
@@ -11,9 +11,8 @@
11
11
  */
12
12
  import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
13
  import { PipelineResponse } from './PipelineResponse.js';
14
- import fetchMetadata from './steps/fetch-metadata.js';
14
+ import fetchConfigAll from './steps/fetch-config-all.js';
15
15
  import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
16
- import { getOriginalHost } from './steps/utils.js';
17
16
 
18
17
  function error(log, msg, status, response) {
19
18
  log.error(msg);
@@ -96,7 +95,7 @@ export async function formsPipe(state, request) {
96
95
  'content-type': 'text/plain; charset=utf-8',
97
96
  },
98
97
  });
99
- await fetchMetadata(state, request, response);
98
+ await fetchConfigAll(state, request, response);
100
99
  await setCustomResponseHeaders(state, request, response);
101
100
 
102
101
  const {
@@ -138,7 +137,7 @@ export async function formsPipe(state, request) {
138
137
 
139
138
  // Send message to SQS if workbook contains and incoming
140
139
  // sheet and the source location is not null
141
- const host = getOriginalHost(request.headers);
140
+ const { host } = state.config;
142
141
 
143
142
  // Forms service expect owner and repo in the message body
144
143
  body.owner = owner;
package/src/html-pipe.js CHANGED
@@ -16,7 +16,7 @@ import createPictures from './steps/create-pictures.js';
16
16
  import extractMetaData from './steps/extract-metadata.js';
17
17
  import fetchConfig from './steps/fetch-config.js';
18
18
  import fetchContent from './steps/fetch-content.js';
19
- import fetchMetadata from './steps/fetch-metadata.js';
19
+ import fetchConfigAll from './steps/fetch-config-all.js';
20
20
  import fixSections from './steps/fix-sections.js';
21
21
  import folderMapping from './steps/folder-mapping.js';
22
22
  import getMetadata from './steps/get-metadata.js';
@@ -72,7 +72,7 @@ export async function htmlPipe(state, req) {
72
72
  // load metadata and content in parallel
73
73
  state.timer?.update('content-fetch');
74
74
  await Promise.all([
75
- fetchMetadata(state, req, res),
75
+ fetchConfigAll(state, req, res),
76
76
  fetchContent(state, req, res),
77
77
  ]);
78
78
 
package/src/index.d.ts CHANGED
@@ -65,6 +65,11 @@ declare interface PathInfo {
65
65
  * original extension as passed via request
66
66
  */
67
67
  originalExtension: string;
68
+
69
+ /**
70
+ * original path as passed before folder mapping
71
+ */
72
+ unmappedPath: string;
68
73
  }
69
74
 
70
75
  declare interface S3Loader {
package/src/index.js CHANGED
@@ -18,8 +18,3 @@ export * from './PipelineRequest.js';
18
18
  export * from './PipelineResponse.js';
19
19
  export * from './PipelineState.js';
20
20
  export * from './PipelineStatusError.js';
21
-
22
- export { default as fetchMetadata } from './steps/fetch-metadata.js';
23
- export { default as fetchConfig } from './steps/fetch-config.js';
24
- export { default as setCustomResponseHeaders } from './steps/set-custom-response-headers.js';
25
- export { getOriginalHost } from './steps/utils.js';
package/src/json-pipe.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import fetchMetadata from './steps/fetch-metadata.js';
12
+ import fetchConfigAll from './steps/fetch-config-all.js';
13
13
  import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
14
14
  import { PipelineResponse } from './PipelineResponse.js';
15
15
  import jsonFilter from './utils/json-filter.js';
@@ -78,8 +78,8 @@ export async function jsonPipe(state, req) {
78
78
  // set surrogate key
79
79
  response.headers.set('x-surrogate-key', `${contentBusId}${path}`.replace(/\//g, '_'));
80
80
 
81
- // Load metadata from metadata.json
82
- await fetchMetadata(state, req, response);
81
+ // Load config-all and set response headers
82
+ await fetchConfigAll(state, req, response);
83
83
  await setCustomResponseHeaders(state, req, response);
84
84
 
85
85
  return response;
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { PipelineResponse } from './PipelineResponse.js';
13
- import fetchMetadata from './steps/fetch-metadata.js';
13
+ import fetchConfigAll from './steps/fetch-config-all.js';
14
14
  import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
15
15
 
16
16
  /**
@@ -30,7 +30,7 @@ export async function optionsPipe(state, request) {
30
30
  'access-control-allow-headers': 'content-type',
31
31
  },
32
32
  });
33
- await fetchMetadata(state, request, response);
33
+ await fetchConfigAll(state, request, response);
34
34
  await setCustomResponseHeaders(state, request, response);
35
35
 
36
36
  return response;
@@ -16,7 +16,7 @@ import { visit, EXIT, CONTINUE } from 'unist-util-visit';
16
16
  import {
17
17
  getAbsoluteUrl, makeCanonicalHtmlUrl, optimizeImageURL, resolveUrl,
18
18
  } from './utils.js';
19
- import { filterGlobalMetadata, toMetaName, ALLOWED_RESPONSE_HEADERS } from '../utils/metadata.js';
19
+ import { toMetaName } from '../utils/modifiers.js';
20
20
  import { childNodes } from '../utils/hast-utils.js';
21
21
 
22
22
  /**
@@ -157,11 +157,11 @@ export default function extractMetaData(state, req) {
157
157
  // extract global metadata from spreadsheet, and overlay
158
158
  // with local metadata from document
159
159
  const metaConfig = Object.assign(
160
- filterGlobalMetadata(state.metadata, state.info.path),
160
+ state.metadata.getModifiers(state.info.unmappedPath || state.info.path),
161
161
  getLocalMetadata(hast),
162
162
  );
163
163
 
164
- const IGNORED_CUSTOM_META = [...ALLOWED_RESPONSE_HEADERS, 'twitter:card'];
164
+ const IGNORED_CUSTOM_META = ['twitter:card'];
165
165
 
166
166
  // first process supported metadata properties
167
167
  [
@@ -215,7 +215,7 @@ export default function extractMetaData(state, req) {
215
215
  }
216
216
 
217
217
  // use the req.url and not the state.info.path in case of folder mapping
218
- meta.url = makeCanonicalHtmlUrl(getAbsoluteUrl(req.headers, req.url.pathname));
218
+ meta.url = makeCanonicalHtmlUrl(getAbsoluteUrl(state, req.url.pathname));
219
219
  if (!meta.canonical) {
220
220
  meta.canonical = meta.url;
221
221
  }
@@ -231,7 +231,7 @@ export default function extractMetaData(state, req) {
231
231
  }
232
232
 
233
233
  meta.image = getAbsoluteUrl(
234
- req.headers,
234
+ state,
235
235
  optimizeMetaImage(state.info.path, meta.image || content.image || '/default-meta-image.png'),
236
236
  );
237
237
 
@@ -0,0 +1,132 @@
1
+ /*
2
+ * Copyright 2021 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
+ import { PipelineStatusError } from '../PipelineStatusError.js';
14
+ import { extractLastModified, updateLastModified } from '../utils/last-modified.js';
15
+ import { ALLOWED_RESPONSE_HEADERS, globToRegExp, Modifiers } from '../utils/modifiers.js';
16
+ import { getOriginalHost } from './utils.js';
17
+
18
+ /**
19
+ * Loads the metadata.json from the content-bus and stores it in `state.metadata` and
20
+ * `state.headers` in modifier form.
21
+ * this is to be backward compatible and can be removed in the future.
22
+ *
23
+ * @type PipelineStep
24
+ * @param {PipelineState} state
25
+ * @param {PipelineRequest} req
26
+ * @param {PipelineResponse} res
27
+ * @returns {Promise<void>}
28
+ */
29
+ async function fetchMetadata(state, req, res) {
30
+ const { contentBusId, partition } = state;
31
+ const key = `${contentBusId}/${partition}/metadata.json`;
32
+ const ret = await state.s3Loader.getObject('helix-content-bus', key);
33
+ if (ret.status === 200) {
34
+ let json;
35
+ try {
36
+ json = JSON.parse(ret.body);
37
+ } catch (e) {
38
+ throw new PipelineStatusError(400, `failed parsing of /metadata.json: ${e.message}`);
39
+ }
40
+
41
+ const { data } = json.default ?? json;
42
+ if (!Array.isArray(data)) {
43
+ throw new PipelineStatusError(400, 'failed loading of /metadata.json: data must be an array');
44
+ }
45
+
46
+ state.metadata = Modifiers.fromModifierSheet(
47
+ data,
48
+ (name) => !ALLOWED_RESPONSE_HEADERS.includes(name),
49
+ );
50
+ state.headers = Modifiers.fromModifierSheet(
51
+ data,
52
+ (name) => ALLOWED_RESPONSE_HEADERS.includes(name),
53
+ );
54
+
55
+ if (state.type === 'html' && state.info.selector !== 'plain') {
56
+ // also update last-modified (only for extensionless html pipeline)
57
+ updateLastModified(state, res, extractLastModified(ret.headers));
58
+ }
59
+ return;
60
+ }
61
+
62
+ if (ret.status !== 404) {
63
+ throw new PipelineStatusError(502, `failed to load /metadata.json: ${ret.status}`);
64
+ }
65
+
66
+ // ignore 404
67
+ }
68
+
69
+ /**
70
+ * Computes the routes from the given config value.
71
+ * @param {string|string[]|undefined} value
72
+ * @return {RegExp[]} and array of regexps for route matching
73
+ */
74
+ export function computeRoutes(value) {
75
+ if (!value) {
76
+ return [/.*/];
77
+ }
78
+ // eslint-disable-next-line no-param-reassign
79
+ return (Array.isArray(value) ? value : [value]).map((route) => {
80
+ if (route.indexOf('*') >= 0) {
81
+ return globToRegExp(route);
82
+ }
83
+ if (route.endsWith('/')) {
84
+ return new RegExp(`^${route}.*$`);
85
+ }
86
+ return new RegExp(`^${route}(/.*)?$`);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Loads the /.helix/config-all.json from the content-bus and stores it in the state. if no
92
+ * such config exists, it will load the metadata.json as fallback and separate out the
93
+ * `state.headers` and `state.metadata`.
94
+ *
95
+ * @type PipelineStep
96
+ * @param {PipelineState} state
97
+ * @param {PipelineRequest} req
98
+ * @param {PipelineResponse} res
99
+ * @returns {Promise<void>}
100
+ */
101
+ export default async function fetchConfigAll(state, req, res) {
102
+ const { contentBusId, partition } = state;
103
+ const key = `${contentBusId}/${partition}/.helix/config-all.json`;
104
+ const ret = await state.s3Loader.getObject('helix-content-bus', key);
105
+ if (ret.status === 200) {
106
+ let json;
107
+ try {
108
+ json = JSON.parse(ret.body);
109
+ } catch (e) {
110
+ throw new PipelineStatusError(400, `failed parsing of /.helix/config-all.json: ${e.message}`);
111
+ }
112
+ state.config = json.config?.data || {};
113
+ state.metadata = new Modifiers(json.metadata?.data || {});
114
+ state.headers = new Modifiers(json.headers?.data || {});
115
+
116
+ if (state.type === 'html' && state.info.selector !== 'plain') {
117
+ // also update last-modified (only for extensionless html pipeline)
118
+ updateLastModified(state, res, extractLastModified(ret.headers));
119
+ }
120
+ } else if (ret.status !== 404) {
121
+ throw new PipelineStatusError(502, `failed to load /.helix/config-all.json: ${ret.status}`);
122
+ } else {
123
+ // fallback to old metadata loading
124
+ await fetchMetadata(state, req, res);
125
+ }
126
+
127
+ // compute host and routes
128
+ if (!state.config.host) {
129
+ state.config.host = state.config.cdn?.prod?.host || getOriginalHost(req.headers);
130
+ }
131
+ state.config.routes = computeRoutes(state.config.cdn?.prod?.route);
132
+ }
@@ -48,6 +48,7 @@ export default function folderMapping(state) {
48
48
  const mapped = mapPath(folders, path);
49
49
  if (mapped) {
50
50
  state.info = getPathInfo(mapped);
51
+ state.info.unmappedPath = path;
51
52
  if (getExtension(mapped)) {
52
53
  // special case: use code-bus
53
54
  state.content.sourceBus = 'code';
@@ -10,7 +10,6 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
- import { filterGlobalMetadata, ALLOWED_RESPONSE_HEADERS } from '../utils/metadata.js';
14
13
 
15
14
  /**
16
15
  * Decorates the pipeline response object with the headers defined in metadata.json.
@@ -21,9 +20,9 @@ import { filterGlobalMetadata, ALLOWED_RESPONSE_HEADERS } from '../utils/metadat
21
20
  * @returns {Promise<void>}
22
21
  */
23
22
  export default function setCustomResponseHeaders(state, req, res) {
24
- const meta = filterGlobalMetadata(state.metadata, state.info.path);
25
- Object.entries(meta).forEach(([name, value]) => {
26
- if (ALLOWED_RESPONSE_HEADERS.includes(name)) {
23
+ Object.entries(state.headers.getModifiers(state.info.path)).forEach(([name, value]) => {
24
+ // don't use `link` header for non-html pipeline
25
+ if (name !== 'link' || state.type === 'html') {
27
26
  res.headers.set(name, cleanupHeaderValue(value));
28
27
  }
29
28
  });
@@ -163,16 +163,29 @@ export function resolveUrl(from, to) {
163
163
 
164
164
  /**
165
165
  * Turns a relative into an absolute URL.
166
- * @param {object} headers The request headers
166
+ * @param {PipelineState} state the request state
167
167
  * @param {string} url The relative or absolute URL
168
168
  * @returns {string} The absolute URL or <code>null</code>
169
169
  * if <code>url</code> is not a string
170
170
  */
171
- export function getAbsoluteUrl(headers, url) {
171
+ export function getAbsoluteUrl(state, url) {
172
172
  if (typeof url !== 'string') {
173
173
  return null;
174
174
  }
175
- return resolveUrl(`https://${getOriginalHost(headers)}/`, url);
175
+ return resolveUrl(`https://${state.config.host}/`, url);
176
+ }
177
+
178
+ /**
179
+ * Checks if the given `str` matches any of the given regs or if `regs` is empty.
180
+ * @param {RegExp[]} regs
181
+ * @param {string} str
182
+ * @returns {boolean} {@code true} if `regs` is empty or if `str` matches any of them.
183
+ */
184
+ function matchAny(regs, str) {
185
+ if (!regs || regs.length === 0) {
186
+ return true;
187
+ }
188
+ return regs.findIndex((r) => r.test(str)) >= 0;
176
189
  }
177
190
 
178
191
  /**
@@ -182,12 +195,14 @@ export function getAbsoluteUrl(headers, url) {
182
195
  * @returns {string|null}
183
196
  */
184
197
  export function rewriteUrl(state, url) {
185
- if (!url) {
198
+ if (!url || !url.startsWith('https://')) {
186
199
  return url;
187
200
  }
201
+ const {
202
+ host, pathname, search, hash,
203
+ } = new URL(url);
188
204
 
189
205
  if (AZURE_BLOB_REGEXP.test(url)) {
190
- const { pathname, hash } = new URL(url);
191
206
  const filename = pathname.split('/').pop();
192
207
  const [name, props] = hash.split('?');
193
208
  const extension = name.split('.').pop() || 'jpg';
@@ -196,29 +211,21 @@ export function rewriteUrl(state, url) {
196
211
  }
197
212
 
198
213
  if (MEDIA_BLOB_REGEXP.test(url)) {
199
- const { pathname, hash } = new URL(url);
200
214
  return `.${pathname}${hash}`;
201
215
  }
202
216
 
203
217
  if (HELIX_URL_REGEXP.test(url)) {
204
- const { pathname, hash, search } = new URL(url);
205
218
  if (hash && pathname === state.info?.path) {
206
219
  return hash;
207
220
  }
208
221
  return `${pathname}${search}${hash}`;
209
222
  }
210
223
 
211
- // todo: read host from contentbus config
212
- if (state.config?.host) {
213
- const {
214
- host, pathname, search, hash,
215
- } = new URL(url);
216
- if (host === state.config.host) {
217
- if (hash && pathname === state.info?.path) {
218
- return hash;
219
- }
220
- return `${pathname}${search}${hash}`;
224
+ if (host === state.config.host && matchAny(state.config.routes, pathname)) {
225
+ if (hash && pathname === state.info?.path) {
226
+ return hash;
221
227
  }
228
+ return `${pathname}${search}${hash}`;
222
229
  }
223
230
 
224
231
  return url;
@@ -0,0 +1,153 @@
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
+ * Converts all non-valid characters to `-`.
14
+ * @param {string} text input text
15
+ * @returns {string} the meta name
16
+ */
17
+ export function toMetaName(text) {
18
+ return text
19
+ .toLowerCase()
20
+ .replace(/[^0-9a-z:_]/gi, '-');
21
+ }
22
+
23
+ export function globToRegExp(glob) {
24
+ const reString = glob
25
+ .replaceAll('**', '|')
26
+ .replaceAll('*', '[0-9a-z-.]*')
27
+ .replaceAll('|', '.*');
28
+ return new RegExp(`^${reString}$`);
29
+ }
30
+
31
+ /**
32
+ * Array of headers allowed in the metadata.json file.
33
+ */
34
+ export const ALLOWED_RESPONSE_HEADERS = [
35
+ 'content-security-policy',
36
+ 'content-security-policy-report-only',
37
+ 'access-control-allow-origin',
38
+ 'access-control-allow-methods',
39
+ 'link',
40
+ ];
41
+
42
+ /**
43
+ * Converts all keys in a row object to lowercase
44
+ * @param {Object} obj A row of data from a sheet
45
+ * @returns {Object} A row with all keys converted to lowercase
46
+ */
47
+ function toLowerKeys(obj) {
48
+ return Object.keys(obj).reduce((prev, key) => {
49
+ prev[key.toLowerCase()] = obj[key];
50
+ return prev;
51
+ }, {});
52
+ }
53
+
54
+ /**
55
+ * The modifiers class help manage the metadata and headers modifiers.
56
+ */
57
+ export class Modifiers {
58
+ /**
59
+ * Empty modifiers
60
+ * @type {Modifiers}
61
+ */
62
+ static EMPTY = new Modifiers({});
63
+
64
+ /**
65
+ * Parses a sheet that is in a modifier format into a list of key/value pairs
66
+ *
67
+ * @example
68
+ *
69
+ * | url | key | value | Title | Description |
70
+ * |-------|-----|-------|---------|----------------|
71
+ * | "/*" | "A" | "B" | "" | "" |
72
+ * | "/*" | "C" | "D" | "" | "" |
73
+ * | "/f" | "" | "" | "Hero" | "Once upon..." |
74
+ *
75
+ * becomes:
76
+ *
77
+ * {
78
+ * "/*": [
79
+ * { "key": "A", "value": "B" },
80
+ * { "key": "C", "value": "D" },
81
+ * ],
82
+ * "/f": [
83
+ * { "key": "title", "value": "Hero" },
84
+ * { "key": "description", "value": "Once upon..." },
85
+ * ]
86
+ * }
87
+ *
88
+ *
89
+ * @param {object[]} sheet The sheet to parse
90
+ * @param {function} keyFilter filter to apply on keys
91
+ * @returns {object} An object containing an array of key/value pairs for every glob
92
+ */
93
+ static fromModifierSheet(sheet, keyFilter = () => true) {
94
+ const res = {};
95
+ for (let row of sheet) {
96
+ row = toLowerKeys(row);
97
+ const {
98
+ url, key, value, ...rest
99
+ } = row;
100
+ if (url) {
101
+ const put = (k, v) => {
102
+ if (keyFilter(k)) {
103
+ let entry = res[url];
104
+ if (!entry) {
105
+ entry = [];
106
+ res[url] = entry;
107
+ }
108
+ entry.push({ key: k, value: v });
109
+ }
110
+ };
111
+
112
+ // note that all values are strings, i.e. never another falsy value
113
+ if ('key' in row && 'value' in row && key && value) {
114
+ put(key, value);
115
+ } else {
116
+ Object.entries(rest).forEach(([k, v]) => {
117
+ if (k && v) {
118
+ put(k, v);
119
+ }
120
+ });
121
+ }
122
+ }
123
+ }
124
+ return new Modifiers(res);
125
+ }
126
+
127
+ constructor(config) {
128
+ this.modifiers = Object.entries(config).map(([url, mods]) => {
129
+ const pat = url.indexOf('*') >= 0 ? globToRegExp(url) : url;
130
+ return {
131
+ pat,
132
+ mods,
133
+ };
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Returns the modifier object for the given path.
139
+ * @param {string} path
140
+ * @return {object} the modifier
141
+ */
142
+ getModifiers(path) {
143
+ const modifiers = {};
144
+ for (const { pat, mods } of this.modifiers) {
145
+ if (pat === path || (pat instanceof RegExp && pat.test(path))) {
146
+ for (const { key, value } of mods) {
147
+ modifiers[toMetaName(key)] = value;
148
+ }
149
+ }
150
+ }
151
+ return modifiers;
152
+ }
153
+ }
package/src/utils/path.js CHANGED
@@ -37,6 +37,7 @@ export function getPathInfo(path) {
37
37
  originalExtension: '',
38
38
  originalPath: path,
39
39
  originalFilename: segs.pop(),
40
+ unmappedPath: '',
40
41
  };
41
42
 
42
43
  // path -> web path (no .html, no index)
@@ -1,55 +0,0 @@
1
- /*
2
- * Copyright 2021 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
- import { PipelineStatusError } from '../PipelineStatusError.js';
14
- import { extractLastModified, updateLastModified } from '../utils/last-modified.js';
15
-
16
- /**
17
- * Loads the metadata.json from the content-bus and stores it in `state.metadata`
18
- * @type PipelineStep
19
- * @param {PipelineState} state
20
- * @param {PipelineRequest} req
21
- * @param {PipelineResponse} res
22
- * @returns {Promise<void>}
23
- */
24
- export default async function fetchMetadata(state, req, res) {
25
- const { contentBusId, partition } = state;
26
- const key = `${contentBusId}/${partition}/metadata.json`;
27
- const ret = await state.s3Loader.getObject('helix-content-bus', key);
28
- if (ret.status === 200) {
29
- let json;
30
- try {
31
- json = JSON.parse(ret.body);
32
- } catch (e) {
33
- throw new PipelineStatusError(400, `failed parsing of /metadata.json: ${e.message}`);
34
- }
35
-
36
- const { data } = json.default ?? json;
37
- if (!Array.isArray(data)) {
38
- throw new PipelineStatusError(400, 'failed loading of /metadata.json: data must be an array');
39
- }
40
- state.metadata = data;
41
-
42
- if (state.type === 'html' && state.info.selector !== 'plain') {
43
- // also update last-modified (only for extensionless html pipeline)
44
- updateLastModified(state, res, extractLastModified(ret.headers));
45
- }
46
- return;
47
- }
48
-
49
- if (ret.status !== 404) {
50
- throw new PipelineStatusError(502, `failed to load /metadata.json: ${ret.status}`);
51
- }
52
-
53
- // ignore 404
54
- state.metadata = [];
55
- }
@@ -1,66 +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
- * Converts all non-valid characters to `-`.
14
- * @param {string} text input text
15
- * @returns {string} the meta name
16
- */
17
- export function toMetaName(text) {
18
- return text
19
- .toLowerCase()
20
- .replace(/[^0-9a-z:_]/gi, '-');
21
- }
22
-
23
- function applyMetaRule(target, obj) {
24
- Object.keys(obj).forEach((key) => {
25
- const metaKey = toMetaName(key);
26
- if (metaKey !== 'url' && obj[key]) {
27
- target[metaKey] = obj[key];
28
- }
29
- });
30
- }
31
-
32
- function globToRegExp(glob) {
33
- const reString = glob
34
- .replace(/\*\*/g, '_')
35
- .replace(/\*/g, '[0-9a-z-.]*')
36
- .replace(/_/g, '.*');
37
- return new RegExp(`^${reString}$`);
38
- }
39
-
40
- export function filterGlobalMetadata(metaRules, path) {
41
- const metaConfig = {};
42
- metaRules.forEach((rule) => {
43
- const glob = rule.url || rule.URL || rule.Url;
44
- if (glob && typeof glob === 'string' && /[0-9a-z-/*]/.test(glob)) {
45
- if (glob.indexOf('*') >= 0) {
46
- if (globToRegExp(glob).test(path)) {
47
- applyMetaRule(metaConfig, rule);
48
- }
49
- } else if (glob === path) {
50
- applyMetaRule(metaConfig, rule);
51
- }
52
- }
53
- });
54
- return metaConfig;
55
- }
56
-
57
- /**
58
- * Array of headers allowed in the metadata.json file.
59
- */
60
- export const ALLOWED_RESPONSE_HEADERS = [
61
- 'content-security-policy',
62
- 'content-security-policy-report-only',
63
- 'access-control-allow-origin',
64
- 'access-control-allow-methods',
65
- 'link',
66
- ];