@adobe/helix-html-pipeline 1.2.1 → 1.3.2
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/README.md +2 -2
- package/package.json +2 -2
- package/src/PipelineState.d.ts +3 -1
- package/src/PipelineState.js +1 -0
- package/src/forms-pipe.js +160 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +2 -0
- package/src/options-pipe.js +37 -0
- package/src/steps/stringify-response.js +20 -0
- package/src/steps/utils.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [1.3.2](https://github.com/adobe/helix-html-pipeline/compare/v1.3.1...v1.3.2) (2022-03-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* expect spec-compliant URL ([d65428a](https://github.com/adobe/helix-html-pipeline/commit/d65428a2f0e68471f8eed785706766744a7f168b))
|
|
7
|
+
|
|
8
|
+
## [1.3.1](https://github.com/adobe/helix-html-pipeline/compare/v1.3.0...v1.3.1) (2022-03-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** update dependency @adobe/helix-shared-utils to v2.0.6 ([#26](https://github.com/adobe/helix-html-pipeline/issues/26)) ([186c376](https://github.com/adobe/helix-html-pipeline/commit/186c376d0252b0c96ee461670cf45a711aa93f4f))
|
|
14
|
+
* preserve formatting of script tags ([#25](https://github.com/adobe/helix-html-pipeline/issues/25)) ([7009f20](https://github.com/adobe/helix-html-pipeline/commit/7009f20d37190f5704b7f9363c59912b4272c0bf)), closes [#23](https://github.com/adobe/helix-html-pipeline/issues/23)
|
|
15
|
+
|
|
16
|
+
# [1.3.0](https://github.com/adobe/helix-html-pipeline/compare/v1.2.1...v1.3.0) (2022-03-17)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* provide pipes for OPTIONS and POSTs ([#24](https://github.com/adobe/helix-html-pipeline/issues/24)) ([1dfc47e](https://github.com/adobe/helix-html-pipeline/commit/1dfc47e764a0b1d8acee80b51b845be2e54a16f5))
|
|
22
|
+
|
|
1
23
|
## [1.2.1](https://github.com/adobe/helix-html-pipeline/compare/v1.2.0...v1.2.1) (2022-03-16)
|
|
2
24
|
|
|
3
25
|
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
This package contains the common code for `helix-pipeline-service` and `helix-cloudflare-pipeline` for rendering the html response for helix3. it has the following design goals:
|
|
4
4
|
|
|
5
5
|
- be platform neutral, i.e. not using node or browser specific modules or dependencies.
|
|
6
|
-
- +/-0 runtime dependencies
|
|
7
|
-
- offer extension interfaces where platform abstraction is required (e.g. reading from
|
|
6
|
+
- +/-0 runtime dependencies (eg. node [crypto](https://nodejs.org/api/crypto.html))
|
|
7
|
+
- offer extension interfaces where platform abstraction is required (e.g. reading from S3, sending to SQS)
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
[](https://codecov.io/gh/adobe/helix-html-pipeline)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Helix HTML Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@adobe/helix-markdown-support": "3.1.2",
|
|
36
|
-
"@adobe/helix-shared-utils": "2.0.
|
|
36
|
+
"@adobe/helix-shared-utils": "2.0.6",
|
|
37
37
|
"github-slugger": "1.4.0",
|
|
38
38
|
"hast-util-raw": "7.2.1",
|
|
39
39
|
"hast-util-select": "5.0.1",
|
package/src/PipelineState.d.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
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 {PathInfo, S3Loader, PipelineTimer} from "./index";
|
|
12
|
+
import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer} from "./index";
|
|
13
13
|
import {PipelineContent} from "./PipelineContent";
|
|
14
14
|
|
|
15
15
|
declare interface PipelineOptions {
|
|
16
16
|
log: Console;
|
|
17
17
|
s3Loader: S3Loader;
|
|
18
|
+
messageDispatcher: FormsMessageDispatcher;
|
|
18
19
|
owner: string;
|
|
19
20
|
repo: string;
|
|
20
21
|
ref: string;
|
|
@@ -31,6 +32,7 @@ declare class PipelineState {
|
|
|
31
32
|
content: PipelineContent;
|
|
32
33
|
contentBusId: string;
|
|
33
34
|
s3Loader: S3Loader;
|
|
35
|
+
messageDispatcher: FormsMessageDispatcher;
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Content bus partition
|
package/src/PipelineState.js
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
13
|
+
import { PipelineResponse } from './PipelineResponse.js';
|
|
14
|
+
import fetchMetadata from './steps/fetch-metadata.js';
|
|
15
|
+
import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
|
|
16
|
+
import { getOriginalHost } from './steps/utils.js';
|
|
17
|
+
|
|
18
|
+
function error(log, msg, status, response) {
|
|
19
|
+
log.error(msg);
|
|
20
|
+
response.status = status;
|
|
21
|
+
response.headers.set('x-error', cleanupHeaderValue(msg));
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts URLSearchParams to an object
|
|
27
|
+
* @param {URLSearchParams} searchParams the search params object
|
|
28
|
+
* @returns {Object} The converted object
|
|
29
|
+
*/
|
|
30
|
+
function searchParamsToObject(searchParams) {
|
|
31
|
+
const result = {};
|
|
32
|
+
|
|
33
|
+
for (const key of searchParams.keys()) {
|
|
34
|
+
// get all values association with the key
|
|
35
|
+
const values = searchParams.getAll(key);
|
|
36
|
+
|
|
37
|
+
// if multiple values, convert to array
|
|
38
|
+
result[key] = (values.length === 1) ? values[0] : values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extracts and parses the body data from the request
|
|
46
|
+
* @param {PipelineRequest} request the request object (see fetch api)
|
|
47
|
+
* @returns {Object} The body data
|
|
48
|
+
* @throws {Error} If an error occurs parsing the body
|
|
49
|
+
*/
|
|
50
|
+
export async function extractBodyData(request) {
|
|
51
|
+
let { body } = request;
|
|
52
|
+
if (!body) {
|
|
53
|
+
throw Error('missing body');
|
|
54
|
+
}
|
|
55
|
+
const type = request.headers.get('content-type');
|
|
56
|
+
|
|
57
|
+
// if content is form-urlencoded we need place the object in the body
|
|
58
|
+
// in a "data" property in the body as this is what forms-service expects.
|
|
59
|
+
if (/^application\/x-www-form-urlencoded/.test(type)) {
|
|
60
|
+
// did they pass an object in the body when a form-urlencoded body was expected?
|
|
61
|
+
if (body === '[object Object]') {
|
|
62
|
+
throw Error('invalid form-urlencoded body');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
body = {
|
|
66
|
+
data: searchParamsToObject(new URLSearchParams(body)),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// else treat the body as json
|
|
70
|
+
} else if (/^application\/json/.test(type)) {
|
|
71
|
+
body = JSON.parse(body);
|
|
72
|
+
// verify the body data is as expected
|
|
73
|
+
if (!body.data) {
|
|
74
|
+
throw Error('missing body.data');
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
throw Error(`post body content-type not supported: ${type}`);
|
|
78
|
+
}
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle a pipeline POST request.
|
|
84
|
+
* At this point POST's only apply to json files that are backed by a workbook.
|
|
85
|
+
* @param {PipelineState} state pipeline options
|
|
86
|
+
* @param {PipelineRequest} request
|
|
87
|
+
* @returns {Promise<PipelineResponse>} a response
|
|
88
|
+
*/
|
|
89
|
+
export async function formsPipe(state, request) {
|
|
90
|
+
const { log } = state;
|
|
91
|
+
|
|
92
|
+
// todo: improve
|
|
93
|
+
const response = new PipelineResponse('', {
|
|
94
|
+
headers: {
|
|
95
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
await fetchMetadata(state, request, response);
|
|
99
|
+
await setCustomResponseHeaders(state, request, response);
|
|
100
|
+
|
|
101
|
+
const {
|
|
102
|
+
owner, repo, ref, contentBusId, partition, s3Loader,
|
|
103
|
+
} = state;
|
|
104
|
+
const { path } = state.info;
|
|
105
|
+
const resourcePath = `${path}.json`;
|
|
106
|
+
|
|
107
|
+
// block all POSTs to resources with extensions
|
|
108
|
+
if (state.info.originalExtension !== '') {
|
|
109
|
+
return error(log, 'POST to URL with extension not allowed', 405, response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// head workbook in content bus
|
|
113
|
+
const resourceFetchResponse = await s3Loader.headObject('helix-content-bus', `${contentBusId}/${partition}${resourcePath}`);
|
|
114
|
+
if (resourceFetchResponse.status !== 200) {
|
|
115
|
+
return resourceFetchResponse;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let body;
|
|
119
|
+
try {
|
|
120
|
+
body = await extractBodyData(request);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return error(log, err.message, 400, response);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sheets = resourceFetchResponse.headers.get('x-amz-meta-x-sheet-names');
|
|
126
|
+
if (!sheets) {
|
|
127
|
+
return error(log, `Target workbook at ${resourcePath} missing x-sheet-names header.`, 403, response);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sourceLocation = resourceFetchResponse.headers.get('x-amz-meta-x-source-location');
|
|
131
|
+
const referer = request.headers.get('referer') || 'unknown';
|
|
132
|
+
const sheetNames = sheets.split(',');
|
|
133
|
+
|
|
134
|
+
if (!sourceLocation || !sheetNames.includes('incoming')) {
|
|
135
|
+
return error(log, `Target workbook at ${resourcePath} is not setup to intake data.`, 403, response);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Send message to SQS if workbook contains and incoming
|
|
139
|
+
// sheet and the source location is not null
|
|
140
|
+
const host = getOriginalHost(request.headers);
|
|
141
|
+
|
|
142
|
+
const message = {
|
|
143
|
+
url: `https://${ref}--${repo}--${owner}.hlx.live${resourcePath}`,
|
|
144
|
+
body,
|
|
145
|
+
host,
|
|
146
|
+
sourceLocation,
|
|
147
|
+
referer,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Send message to forms queue
|
|
152
|
+
const { requestId, messageId } = await state.messageDispatcher.dispatch(message);
|
|
153
|
+
response.status = 201;
|
|
154
|
+
response.headers.set('x-request-id', requestId);
|
|
155
|
+
response.headers.set('x-message-id', messageId);
|
|
156
|
+
return response;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return error(log, `Failed to send message to forms queue: ${err}`, 500, response);
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -83,6 +83,19 @@ declare interface S3Loader {
|
|
|
83
83
|
headObject(bucketId, key): Promise<PipelineResponse>;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
declare interface DispatchMessageResponse {
|
|
87
|
+
messageId:string,
|
|
88
|
+
requestId:string,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
declare interface FormsMessageDispatcher {
|
|
92
|
+
/**
|
|
93
|
+
* Dispatches the message to the forms queue
|
|
94
|
+
* @param {object} message
|
|
95
|
+
*/
|
|
96
|
+
dispatch(message:object): Promise<DispatchMessageResponse>;
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
/**
|
|
87
100
|
* Timer
|
|
88
101
|
*/
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './html-pipe.js';
|
|
13
13
|
export * from './json-pipe.js';
|
|
14
|
+
export * from './options-pipe.js';
|
|
15
|
+
export * from './forms-pipe.js';
|
|
14
16
|
export * from './PipelineContent.js';
|
|
15
17
|
export * from './PipelineRequest.js';
|
|
16
18
|
export * from './PipelineResponse.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
import { PipelineResponse } from './PipelineResponse.js';
|
|
13
|
+
import fetchMetadata from './steps/fetch-metadata.js';
|
|
14
|
+
import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handles options requests
|
|
18
|
+
* @param {PipelineState} state pipeline options
|
|
19
|
+
* @param {PipelineRequest} request
|
|
20
|
+
* @returns {Response} a response
|
|
21
|
+
*/
|
|
22
|
+
export async function optionsPipe(state, request) {
|
|
23
|
+
// todo: improve
|
|
24
|
+
const response = new PipelineResponse('', {
|
|
25
|
+
status: 204,
|
|
26
|
+
headers: {
|
|
27
|
+
// Set preflight cache duration
|
|
28
|
+
'access-control-max-age': '86400',
|
|
29
|
+
// Allow content type header
|
|
30
|
+
'access-control-allow-headers': 'content-type',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
await fetchMetadata(state, request, response);
|
|
34
|
+
await setCustomResponseHeaders(state, request, response);
|
|
35
|
+
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { toHtml } from 'hast-util-to-html';
|
|
13
13
|
// import rehypeFormat from 'rehype-format';
|
|
14
14
|
import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
|
|
15
|
+
import { visit } from 'unist-util-visit';
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Serializes the response document to HTML
|
|
17
19
|
* @param {PipelineState} state
|
|
@@ -32,7 +34,25 @@ export default function stringify(state, req, res) {
|
|
|
32
34
|
// TODO: for the next breaking release, pretty print the HTML with rehypeFormat.
|
|
33
35
|
// TODO: but for backward compatibility, output all on 1 line.
|
|
34
36
|
// rehypeFormat()(doc);
|
|
37
|
+
|
|
38
|
+
// due to a bug in rehype-minify-whitespace, script content is also minified to 1 line, which
|
|
39
|
+
// can result in errors https://github.com/rehypejs/rehype-minify/issues/44
|
|
40
|
+
// so we 'save' all text first and revert it afterwards
|
|
41
|
+
visit(doc, (node) => {
|
|
42
|
+
if (node.tagName === 'script' && node.children[0]?.type === 'text') {
|
|
43
|
+
node.children[0].savedValue = node.children[0].value;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
35
47
|
rehypeMinifyWhitespace()(doc);
|
|
48
|
+
|
|
49
|
+
visit(doc, (node) => {
|
|
50
|
+
if (node.tagName === 'script' && node.children[0]?.type === 'text') {
|
|
51
|
+
node.children[0].value = node.children[0].savedValue;
|
|
52
|
+
delete node.children[0].savedValue;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
36
56
|
res.body = toHtml(doc, {
|
|
37
57
|
upperDoctype: true,
|
|
38
58
|
});
|
package/src/steps/utils.js
CHANGED
|
@@ -125,8 +125,9 @@ export function optimizeImageURL(src, width, format = 'webply', optimize = 'medi
|
|
|
125
125
|
* @returns {string} resolved url
|
|
126
126
|
*/
|
|
127
127
|
export function resolveUrl(from, to) {
|
|
128
|
-
const
|
|
129
|
-
|
|
128
|
+
const DUMMY_BASE = 'https://__dummmy__';
|
|
129
|
+
const resolvedUrl = new URL(to, new URL(from, DUMMY_BASE));
|
|
130
|
+
if (resolvedUrl.origin === DUMMY_BASE) {
|
|
130
131
|
// `from` is a relative URL.
|
|
131
132
|
const { pathname, search, hash } = resolvedUrl;
|
|
132
133
|
return pathname + search + hash;
|