@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 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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "CommonJS",
5
+ "types": ["@cloudflare/workers-types", "jest"]
6
+ }
7
+ }