@adobe/helix-html-pipeline 2.0.3 → 2.1.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 +22 -0
- package/package.json +5 -2
- package/src/PipelineState.d.ts +19 -2
- package/src/PipelineState.js +4 -1
- package/src/forms-pipe.js +3 -4
- package/src/html-pipe.js +2 -2
- package/src/index.d.ts +5 -0
- package/src/index.js +0 -5
- package/src/json-pipe.js +3 -3
- package/src/options-pipe.js +2 -2
- package/src/steps/create-pictures.js +3 -5
- package/src/steps/extract-metadata.js +5 -5
- package/src/steps/fetch-config-all.js +132 -0
- package/src/steps/folder-mapping.js +1 -0
- package/src/steps/set-custom-response-headers.js +3 -4
- package/src/steps/utils.js +24 -17
- package/src/utils/modifiers.js +153 -0
- package/src/utils/path.js +1 -0
- package/src/steps/fetch-metadata.js +0 -55
- package/src/utils/metadata.js +0 -66
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [2.1.1](https://github.com/adobe/helix-html-pipeline/compare/v2.1.0...v2.1.1) (2022-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Lazy images ([#73](https://github.com/adobe/helix-html-pipeline/issues/73)) ([7442a5e](https://github.com/adobe/helix-html-pipeline/commit/7442a5e0720699d974332a2f5659fdddd27b0dc6))
|
|
7
|
+
|
|
8
|
+
# [2.1.0](https://github.com/adobe/helix-html-pipeline/compare/v2.0.4...v2.1.0) (2022-06-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* 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))
|
|
14
|
+
|
|
15
|
+
## [2.0.4](https://github.com/adobe/helix-html-pipeline/compare/v2.0.3...v2.0.4) (2022-05-31)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* fix glob matching for mapped pages ([c43e359](https://github.com/adobe/helix-html-pipeline/commit/c43e3590f9b564f988dfd35daeb5000e458dea71))
|
|
21
|
+
* rename mappedPath to unmappedPath ([12e0066](https://github.com/adobe/helix-html-pipeline/commit/12e0066c8da51ffcb16f8ac7e78306fae1a1cd93))
|
|
22
|
+
|
|
1
23
|
## [2.0.3](https://github.com/adobe/helix-html-pipeline/compare/v2.0.2...v2.0.3) (2022-05-30)
|
|
2
24
|
|
|
3
25
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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": "
|
|
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,6 +29,9 @@
|
|
|
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
36
|
"@adobe/helix-markdown-support": "3.1.6",
|
|
34
37
|
"@adobe/helix-shared-utils": "2.0.10",
|
package/src/PipelineState.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
90
|
+
headers?: Modifiers;
|
|
74
91
|
|
|
75
92
|
/**
|
|
76
93
|
* optional timer that is used to measure the timing
|
package/src/PipelineState.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
75
|
+
fetchConfigAll(state, req, res),
|
|
76
76
|
fetchContent(state, req, res),
|
|
77
77
|
]);
|
|
78
78
|
|
package/src/index.d.ts
CHANGED
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
|
|
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
|
|
82
|
-
await
|
|
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;
|
package/src/options-pipe.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import { PipelineResponse } from './PipelineResponse.js';
|
|
13
|
-
import
|
|
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
|
|
33
|
+
await fetchConfigAll(state, request, response);
|
|
34
34
|
await setCustomResponseHeaders(state, request, response);
|
|
35
35
|
|
|
36
36
|
return response;
|
|
@@ -18,7 +18,7 @@ const BREAK_POINTS = [
|
|
|
18
18
|
{ width: '750' },
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
export function createOptimizedPicture(src, alt = ''
|
|
21
|
+
export function createOptimizedPicture(src, alt = '') {
|
|
22
22
|
const url = new URL(src, 'https://localhost/');
|
|
23
23
|
const { pathname, hash = '' } = url;
|
|
24
24
|
const props = new URLSearchParams(hash.substring(1));
|
|
@@ -53,7 +53,7 @@ export function createOptimizedPicture(src, alt = '', eager = false) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
return h('img', {
|
|
56
|
-
loading:
|
|
56
|
+
loading: 'lazy',
|
|
57
57
|
alt,
|
|
58
58
|
type: v.type,
|
|
59
59
|
src: srcset,
|
|
@@ -77,11 +77,9 @@ function isMediaImage(node) {
|
|
|
77
77
|
export default async function createPictures({ content }) {
|
|
78
78
|
const { hast } = content;
|
|
79
79
|
|
|
80
|
-
let first = true;
|
|
81
80
|
visitParents(hast, isMediaImage, (img, parents) => {
|
|
82
81
|
const { src, alt } = img.properties;
|
|
83
|
-
const picture = createOptimizedPicture(src, alt
|
|
84
|
-
first = false;
|
|
82
|
+
const picture = createOptimizedPicture(src, alt);
|
|
85
83
|
|
|
86
84
|
// check if parent has style and unwrap if needed
|
|
87
85
|
const parent = parents[parents.length - 1];
|
|
@@ -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 {
|
|
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
|
-
|
|
160
|
+
state.metadata.getModifiers(state.info.unmappedPath || state.info.path),
|
|
161
161
|
getLocalMetadata(hast),
|
|
162
162
|
);
|
|
163
163
|
|
|
164
|
-
const IGNORED_CUSTOM_META = [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
23
|
+
Object.entries(state.headers.getModifiers(state.info.path)).forEach(([name, value]) => {
|
|
24
|
+
// only use `link` header for extensionless pipeline
|
|
25
|
+
if (name !== 'link' || (state.type === 'html' && state.info.selector === '')) {
|
|
27
26
|
res.headers.set(name, cleanupHeaderValue(value));
|
|
28
27
|
}
|
|
29
28
|
});
|
package/src/steps/utils.js
CHANGED
|
@@ -163,16 +163,29 @@ export function resolveUrl(from, to) {
|
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
165
|
* Turns a relative into an absolute URL.
|
|
166
|
-
* @param {
|
|
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(
|
|
171
|
+
export function getAbsoluteUrl(state, url) {
|
|
172
172
|
if (typeof url !== 'string') {
|
|
173
173
|
return null;
|
|
174
174
|
}
|
|
175
|
-
return resolveUrl(`https://${
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
@@ -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
|
-
}
|
package/src/utils/metadata.js
DELETED
|
@@ -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
|
-
];
|