@cloudflare/pages-shared 0.0.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/dist/index.js +1 -0
- package/dist/metadata-generator/constants.js +13 -0
- package/dist/metadata-generator/createMetadataObject.js +118 -0
- package/dist/metadata-generator/parseHeaders.js +140 -0
- package/dist/metadata-generator/parseRedirects.js +100 -0
- package/dist/metadata-generator/types.js +0 -0
- package/dist/metadata-generator/validateURL.js +41 -0
- package/package.json +42 -0
- package/tsconfig.json +7 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const REDIRECTS_VERSION = 1;
|
|
2
|
+
export const HEADERS_VERSION = 2;
|
|
3
|
+
export const ANALYTICS_VERSION = 1;
|
|
4
|
+
export const ROUTES_JSON_VERSION = 1;
|
|
5
|
+
export const PERMITTED_STATUS_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
6
|
+
export const HEADER_SEPARATOR = ":";
|
|
7
|
+
export const MAX_LINE_LENGTH = 2e3;
|
|
8
|
+
export const MAX_HEADER_RULES = 100;
|
|
9
|
+
export const MAX_DYNAMIC_REDIRECT_RULES = 100;
|
|
10
|
+
export const MAX_STATIC_REDIRECT_RULES = 2e3;
|
|
11
|
+
export const UNSET_OPERATOR = "! ";
|
|
12
|
+
export const SPLAT_REGEX = /\*/g;
|
|
13
|
+
export const PLACEHOLDER_REGEX = /:\w+/g;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANALYTICS_VERSION,
|
|
3
|
+
REDIRECTS_VERSION,
|
|
4
|
+
HEADERS_VERSION,
|
|
5
|
+
SPLAT_REGEX,
|
|
6
|
+
PLACEHOLDER_REGEX
|
|
7
|
+
} from "./constants";
|
|
8
|
+
export function createMetadataObject({
|
|
9
|
+
redirects,
|
|
10
|
+
headers,
|
|
11
|
+
webAnalyticsToken,
|
|
12
|
+
deploymentId,
|
|
13
|
+
logger = () => {
|
|
14
|
+
}
|
|
15
|
+
}) {
|
|
16
|
+
return {
|
|
17
|
+
...constructRedirects({ redirects, logger }),
|
|
18
|
+
...constructHeaders({ headers, logger }),
|
|
19
|
+
...constructWebAnalytics({ webAnalyticsToken, logger }),
|
|
20
|
+
deploymentId
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function constructRedirects({
|
|
24
|
+
redirects,
|
|
25
|
+
logger
|
|
26
|
+
}) {
|
|
27
|
+
if (!redirects)
|
|
28
|
+
return {};
|
|
29
|
+
const num_valid = redirects.rules.length;
|
|
30
|
+
const num_invalid = redirects.invalid.length;
|
|
31
|
+
logger(
|
|
32
|
+
`Parsed ${num_valid} valid redirect rule${num_valid === 1 ? "" : "s"}.`
|
|
33
|
+
);
|
|
34
|
+
if (num_invalid > 0) {
|
|
35
|
+
logger(`Found invalid redirect lines:`);
|
|
36
|
+
for (const { line, lineNumber, message } of redirects.invalid) {
|
|
37
|
+
if (line)
|
|
38
|
+
logger(` - ${lineNumber ? `#${lineNumber}: ` : ""}${line}`);
|
|
39
|
+
logger(` ${message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (num_valid === 0) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const staticRedirects = {};
|
|
46
|
+
const dynamicRedirects = {};
|
|
47
|
+
let canCreateStaticRule = true;
|
|
48
|
+
for (const rule of redirects.rules) {
|
|
49
|
+
if (!rule.from.match(SPLAT_REGEX) && !rule.from.match(PLACEHOLDER_REGEX)) {
|
|
50
|
+
if (canCreateStaticRule) {
|
|
51
|
+
staticRedirects[rule.from] = { status: rule.status, to: rule.to };
|
|
52
|
+
continue;
|
|
53
|
+
} else {
|
|
54
|
+
logger(
|
|
55
|
+
`Info: the redirect rule ${rule.from} \u2192 ${rule.status} ${rule.to} could be made more performant by bringing it above any lines with splats or placeholders.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
dynamicRedirects[rule.from] = { status: rule.status, to: rule.to };
|
|
60
|
+
canCreateStaticRule = false;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
redirects: {
|
|
64
|
+
version: REDIRECTS_VERSION,
|
|
65
|
+
staticRules: staticRedirects,
|
|
66
|
+
rules: dynamicRedirects
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function constructHeaders({
|
|
71
|
+
headers,
|
|
72
|
+
logger
|
|
73
|
+
}) {
|
|
74
|
+
if (!headers)
|
|
75
|
+
return {};
|
|
76
|
+
const num_valid = headers.rules.length;
|
|
77
|
+
const num_invalid = headers.invalid.length;
|
|
78
|
+
logger(`Parsed ${num_valid} valid header rule${num_valid === 1 ? "" : "s"}.`);
|
|
79
|
+
if (num_invalid > 0) {
|
|
80
|
+
logger(`Found invalid header lines:`);
|
|
81
|
+
for (const { line, lineNumber, message } of headers.invalid) {
|
|
82
|
+
if (line)
|
|
83
|
+
logger(` - ${lineNumber ? `#${lineNumber}: ` : ""} ${line}`);
|
|
84
|
+
logger(` ${message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (num_valid === 0) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
const rules = {};
|
|
91
|
+
for (const rule of headers.rules) {
|
|
92
|
+
rules[rule.path] = {};
|
|
93
|
+
if (Object.keys(rule.headers).length) {
|
|
94
|
+
rules[rule.path].set = rule.headers;
|
|
95
|
+
}
|
|
96
|
+
if (rule.unsetHeaders.length) {
|
|
97
|
+
rules[rule.path].unset = rule.unsetHeaders;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
headers: {
|
|
102
|
+
version: HEADERS_VERSION,
|
|
103
|
+
rules
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function constructWebAnalytics({
|
|
108
|
+
webAnalyticsToken
|
|
109
|
+
}) {
|
|
110
|
+
if (!webAnalyticsToken)
|
|
111
|
+
return {};
|
|
112
|
+
return {
|
|
113
|
+
analytics: {
|
|
114
|
+
version: ANALYTICS_VERSION,
|
|
115
|
+
token: webAnalyticsToken
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_LINE_LENGTH,
|
|
3
|
+
MAX_HEADER_RULES,
|
|
4
|
+
HEADER_SEPARATOR,
|
|
5
|
+
UNSET_OPERATOR
|
|
6
|
+
} from "./constants";
|
|
7
|
+
import { validateUrl } from "./validateURL";
|
|
8
|
+
const LINE_IS_PROBABLY_A_PATH = new RegExp(/^([^\s]+:\/\/|^\/)/);
|
|
9
|
+
export function parseHeaders(input) {
|
|
10
|
+
const lines = input.split("\n");
|
|
11
|
+
const rules = [];
|
|
12
|
+
const invalid = [];
|
|
13
|
+
let rule = void 0;
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i].trim();
|
|
16
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
17
|
+
continue;
|
|
18
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
19
|
+
invalid.push({
|
|
20
|
+
message: `Ignoring line ${i + 1} as it exceeds the maximum allowed length of ${MAX_LINE_LENGTH}.`
|
|
21
|
+
});
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (LINE_IS_PROBABLY_A_PATH.test(line)) {
|
|
25
|
+
if (rules.length >= MAX_HEADER_RULES) {
|
|
26
|
+
invalid.push({
|
|
27
|
+
message: `Maximum number of rules supported is ${MAX_HEADER_RULES}. Skipping remaining ${lines.length - i} lines of file.`
|
|
28
|
+
});
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
if (rule) {
|
|
32
|
+
if (isValidRule(rule)) {
|
|
33
|
+
rules.push({
|
|
34
|
+
path: rule.path,
|
|
35
|
+
headers: rule.headers,
|
|
36
|
+
unsetHeaders: rule.unsetHeaders
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
invalid.push({
|
|
40
|
+
line: rule.line,
|
|
41
|
+
lineNumber: i + 1,
|
|
42
|
+
message: "No headers specified"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const [path, pathError] = validateUrl(line);
|
|
47
|
+
if (pathError) {
|
|
48
|
+
invalid.push({
|
|
49
|
+
line,
|
|
50
|
+
lineNumber: i + 1,
|
|
51
|
+
message: pathError
|
|
52
|
+
});
|
|
53
|
+
rule = void 0;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
rule = {
|
|
57
|
+
path,
|
|
58
|
+
line,
|
|
59
|
+
headers: {},
|
|
60
|
+
unsetHeaders: []
|
|
61
|
+
};
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!line.includes(HEADER_SEPARATOR)) {
|
|
65
|
+
if (!rule) {
|
|
66
|
+
invalid.push({
|
|
67
|
+
line,
|
|
68
|
+
lineNumber: i + 1,
|
|
69
|
+
message: "Expected a path beginning with at least one forward-slash"
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
if (line.trim().startsWith(UNSET_OPERATOR)) {
|
|
73
|
+
rule.unsetHeaders.push(line.trim().replace(UNSET_OPERATOR, ""));
|
|
74
|
+
} else {
|
|
75
|
+
invalid.push({
|
|
76
|
+
line,
|
|
77
|
+
lineNumber: i + 1,
|
|
78
|
+
message: "Expected a colon-separated header pair (e.g. name: value)"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR);
|
|
85
|
+
const name = rawName.trim().toLowerCase();
|
|
86
|
+
if (name.includes(" ")) {
|
|
87
|
+
invalid.push({
|
|
88
|
+
line,
|
|
89
|
+
lineNumber: i + 1,
|
|
90
|
+
message: "Header name cannot include spaces"
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const value = rawValue.join(HEADER_SEPARATOR).trim();
|
|
95
|
+
if (name === "") {
|
|
96
|
+
invalid.push({
|
|
97
|
+
line,
|
|
98
|
+
lineNumber: i + 1,
|
|
99
|
+
message: "No header name specified"
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (value === "") {
|
|
104
|
+
invalid.push({
|
|
105
|
+
line,
|
|
106
|
+
lineNumber: i + 1,
|
|
107
|
+
message: "No header value specified"
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!rule) {
|
|
112
|
+
invalid.push({
|
|
113
|
+
line,
|
|
114
|
+
lineNumber: i + 1,
|
|
115
|
+
message: `Path should come before header (${name}: ${value})`
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const existingValues = rule.headers[name];
|
|
120
|
+
rule.headers[name] = existingValues ? `${existingValues}, ${value}` : value;
|
|
121
|
+
}
|
|
122
|
+
if (rule) {
|
|
123
|
+
if (isValidRule(rule)) {
|
|
124
|
+
rules.push({
|
|
125
|
+
path: rule.path,
|
|
126
|
+
headers: rule.headers,
|
|
127
|
+
unsetHeaders: rule.unsetHeaders
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
invalid.push({ line: rule.line, message: "No headers specified" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
rules,
|
|
135
|
+
invalid
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function isValidRule(rule) {
|
|
139
|
+
return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
|
|
140
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_LINE_LENGTH,
|
|
3
|
+
MAX_DYNAMIC_REDIRECT_RULES,
|
|
4
|
+
MAX_STATIC_REDIRECT_RULES,
|
|
5
|
+
PERMITTED_STATUS_CODES,
|
|
6
|
+
SPLAT_REGEX,
|
|
7
|
+
PLACEHOLDER_REGEX
|
|
8
|
+
} from "./constants";
|
|
9
|
+
import { validateUrl } from "./validateURL";
|
|
10
|
+
export function parseRedirects(input) {
|
|
11
|
+
const lines = input.split("\n");
|
|
12
|
+
const rules = [];
|
|
13
|
+
const seen_paths = /* @__PURE__ */ new Set();
|
|
14
|
+
const invalid = [];
|
|
15
|
+
let staticRules = 0;
|
|
16
|
+
let dynamicRules = 0;
|
|
17
|
+
let canCreateStaticRule = true;
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i].trim();
|
|
20
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
21
|
+
continue;
|
|
22
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
23
|
+
invalid.push({
|
|
24
|
+
message: `Ignoring line ${i + 1} as it exceeds the maximum allowed length of ${MAX_LINE_LENGTH}.`
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const tokens = line.split(/\s+/);
|
|
29
|
+
if (tokens.length < 2 || tokens.length > 3) {
|
|
30
|
+
invalid.push({
|
|
31
|
+
line,
|
|
32
|
+
lineNumber: i + 1,
|
|
33
|
+
message: `Expected exactly 2 or 3 whitespace-separated tokens. Got ${tokens.length}.`
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const [str_from, str_to, str_status = "302"] = tokens;
|
|
38
|
+
const fromResult = validateUrl(str_from, true, false, false);
|
|
39
|
+
if (fromResult[0] === void 0) {
|
|
40
|
+
invalid.push({
|
|
41
|
+
line,
|
|
42
|
+
lineNumber: i + 1,
|
|
43
|
+
message: fromResult[1]
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const from = fromResult[0];
|
|
48
|
+
if (canCreateStaticRule && !from.match(SPLAT_REGEX) && !from.match(PLACEHOLDER_REGEX)) {
|
|
49
|
+
staticRules += 1;
|
|
50
|
+
if (staticRules > MAX_STATIC_REDIRECT_RULES) {
|
|
51
|
+
invalid.push({
|
|
52
|
+
message: `Maximum number of static rules supported is ${MAX_STATIC_REDIRECT_RULES}. Skipping line.`
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
dynamicRules += 1;
|
|
58
|
+
canCreateStaticRule = false;
|
|
59
|
+
if (dynamicRules > MAX_DYNAMIC_REDIRECT_RULES) {
|
|
60
|
+
invalid.push({
|
|
61
|
+
message: `Maximum number of dynamic rules supported is ${MAX_DYNAMIC_REDIRECT_RULES}. Skipping remaining ${lines.length - i} lines of file.`
|
|
62
|
+
});
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const toResult = validateUrl(str_to, false, true, true);
|
|
67
|
+
if (toResult[0] === void 0) {
|
|
68
|
+
invalid.push({
|
|
69
|
+
line,
|
|
70
|
+
lineNumber: i + 1,
|
|
71
|
+
message: toResult[1]
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const to = toResult[0];
|
|
76
|
+
const status = Number(str_status);
|
|
77
|
+
if (isNaN(status) || !PERMITTED_STATUS_CODES.has(status)) {
|
|
78
|
+
invalid.push({
|
|
79
|
+
line,
|
|
80
|
+
lineNumber: i + 1,
|
|
81
|
+
message: `Valid status codes are 301, 302 (default), 303, 307, or 308. Got ${str_status}.`
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (seen_paths.has(from)) {
|
|
86
|
+
invalid.push({
|
|
87
|
+
line,
|
|
88
|
+
lineNumber: i + 1,
|
|
89
|
+
message: `Ignoring duplicate rule for path ${from}.`
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
seen_paths.add(from);
|
|
94
|
+
rules.push({ from, to, status, lineNumber: i + 1 });
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
rules,
|
|
98
|
+
invalid
|
|
99
|
+
};
|
|
100
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const extractPathname = (path = "/", includeSearch, includeHash) => {
|
|
2
|
+
if (!path.startsWith("/"))
|
|
3
|
+
path = `/${path}`;
|
|
4
|
+
const url = new URL(`//${path}`, "relative://");
|
|
5
|
+
return `${url.pathname}${includeSearch ? url.search : ""}${includeHash ? url.hash : ""}`;
|
|
6
|
+
};
|
|
7
|
+
const URL_REGEX = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/;
|
|
8
|
+
const PATH_REGEX = /^\//;
|
|
9
|
+
export const validateUrl = (token, onlyRelative = false, includeSearch = false, includeHash = false) => {
|
|
10
|
+
const host = URL_REGEX.exec(token);
|
|
11
|
+
if (host && host.groups && host.groups.host) {
|
|
12
|
+
if (onlyRelative)
|
|
13
|
+
return [
|
|
14
|
+
void 0,
|
|
15
|
+
`Only relative URLs are allowed. Skipping absolute URL ${token}.`
|
|
16
|
+
];
|
|
17
|
+
return [
|
|
18
|
+
`https://${host.groups.host}${extractPathname(
|
|
19
|
+
host.groups.path,
|
|
20
|
+
includeSearch,
|
|
21
|
+
includeHash
|
|
22
|
+
)}`,
|
|
23
|
+
void 0
|
|
24
|
+
];
|
|
25
|
+
} else {
|
|
26
|
+
if (!token.startsWith("/") && onlyRelative)
|
|
27
|
+
token = `/${token}`;
|
|
28
|
+
const path = PATH_REGEX.exec(token);
|
|
29
|
+
if (path) {
|
|
30
|
+
try {
|
|
31
|
+
return [extractPathname(token, includeSearch, includeHash), void 0];
|
|
32
|
+
} catch {
|
|
33
|
+
return [void 0, `Error parsing URL segment ${token}. Skipping.`];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
void 0,
|
|
39
|
+
onlyRelative ? "URLs should begin with a forward-slash." : 'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").'
|
|
40
|
+
];
|
|
41
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudflare/pages-shared",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"files": [
|
|
5
|
+
"tsconfig.json",
|
|
6
|
+
"dist/**/*"
|
|
7
|
+
],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node scripts/build.js",
|
|
10
|
+
"check:type": "tsc"
|
|
11
|
+
},
|
|
12
|
+
"jest": {
|
|
13
|
+
"coverageReporters": [
|
|
14
|
+
"json",
|
|
15
|
+
"html",
|
|
16
|
+
"text",
|
|
17
|
+
"cobertura"
|
|
18
|
+
],
|
|
19
|
+
"restoreMocks": true,
|
|
20
|
+
"setupFilesAfterEnv": [
|
|
21
|
+
"<rootDir>/__tests__/jest.setup.ts"
|
|
22
|
+
],
|
|
23
|
+
"testRegex": ".*.(test|spec)\\.[jt]sx?$",
|
|
24
|
+
"testTimeout": 30000,
|
|
25
|
+
"transform": {
|
|
26
|
+
"^.+\\.c?(t|j)sx?$": [
|
|
27
|
+
"esbuild-jest",
|
|
28
|
+
{
|
|
29
|
+
"sourcemap": true
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"transformIgnorePatterns": [
|
|
34
|
+
"node_modules/(?!find-up|locate-path|p-locate|p-limit|p-timeout|p-queue|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream|get-port|supports-color|pretty-bytes)"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@cloudflare/workers-types": "^3.16.0",
|
|
39
|
+
"concurrently": "^7.3.0",
|
|
40
|
+
"glob": "^8.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|