@cloudflare/pages-shared 0.13.11 → 0.13.13

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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ generateRulesMatcher,
3
+ replacer,
4
+ } from "@cloudflare/workers-shared/asset-worker/src/utils/rules-engine";
1
5
  import {
2
6
  FoundResponse,
3
7
  InternalServerErrorResponse,
@@ -11,7 +15,6 @@ import {
11
15
  SeeOtherResponse,
12
16
  TemporaryRedirectResponse,
13
17
  } from "./responses";
14
- import { generateRulesMatcher, replacer } from "./rulesEngine";
15
18
  import type {
16
19
  Metadata,
17
20
  MetadataHeadersEntries,
@@ -1,20 +1,14 @@
1
- import { relative } from "node:path";
2
1
  import {
3
- ANALYTICS_VERSION,
4
- HEADERS_VERSION,
5
- PLACEHOLDER_REGEX,
6
- REDIRECTS_VERSION,
7
- SPLAT_REGEX,
8
- } from "./constants";
9
- import type { MetadataStaticRedirects } from "../asset-server/metadata";
2
+ constructHeaders,
3
+ constructRedirects,
4
+ } from "@cloudflare/workers-shared/utils/configuration/constructConfiguration";
5
+ import { ANALYTICS_VERSION } from "./constants";
6
+ import type { Metadata } from "./types";
10
7
  import type {
11
8
  Logger,
12
- Metadata,
13
- MetadataHeaders,
14
- MetadataRedirects,
15
9
  ParsedHeaders,
16
10
  ParsedRedirects,
17
- } from "./types";
11
+ } from "@cloudflare/workers-shared/utils/configuration/types";
18
12
 
19
13
  const noopLogger = {
20
14
  debug: (_message: string) => {},
@@ -52,154 +46,6 @@ export function createMetadataObject({
52
46
  };
53
47
  }
54
48
 
55
- function constructRedirects({
56
- redirects,
57
- redirectsFile,
58
- logger,
59
- }: {
60
- redirects?: ParsedRedirects;
61
- redirectsFile?: string;
62
- logger: Logger;
63
- }): Metadata {
64
- if (!redirects) {
65
- return {};
66
- }
67
-
68
- const num_valid = redirects.rules.length;
69
- const num_invalid = redirects.invalid.length;
70
-
71
- // exhaustive check, since we could not have parsed `redirects` out of
72
- // a non-existing redirects file
73
- const redirectsRelativePath = redirectsFile
74
- ? relative(process.cwd(), redirectsFile)
75
- : "";
76
-
77
- logger.log(
78
- `✨ Parsed ${num_valid} valid redirect rule${num_valid === 1 ? "" : "s"}.`
79
- );
80
-
81
- if (num_invalid > 0) {
82
- let invalidRedirectRulesList = ``;
83
-
84
- for (const { line, lineNumber, message } of redirects.invalid) {
85
- invalidRedirectRulesList += `▶︎ ${message}\n`;
86
-
87
- if (line) {
88
- invalidRedirectRulesList += ` at ${redirectsRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
89
- }
90
- }
91
-
92
- logger.warn(
93
- `Found ${num_invalid} invalid redirect rule${num_invalid === 1 ? "" : "s"}:\n` +
94
- `${invalidRedirectRulesList}`
95
- );
96
- }
97
-
98
- /* Better to return no Redirects object at all than one with empty rules */
99
- if (num_valid === 0) {
100
- return {};
101
- }
102
-
103
- const staticRedirects: MetadataStaticRedirects = {};
104
- const dynamicRedirects: MetadataRedirects = {};
105
- let canCreateStaticRule = true;
106
- for (const rule of redirects.rules) {
107
- if (!rule.from.match(SPLAT_REGEX) && !rule.from.match(PLACEHOLDER_REGEX)) {
108
- if (canCreateStaticRule) {
109
- staticRedirects[rule.from] = {
110
- status: rule.status,
111
- to: rule.to,
112
- lineNumber: rule.lineNumber,
113
- };
114
- continue;
115
- } else {
116
- logger.info(
117
- `The redirect rule ${rule.from} → ${rule.status} ${rule.to} could be made more performant by bringing it above any lines with splats or placeholders.`
118
- );
119
- }
120
- }
121
-
122
- dynamicRedirects[rule.from] = { status: rule.status, to: rule.to };
123
- canCreateStaticRule = false;
124
- }
125
-
126
- return {
127
- redirects: {
128
- version: REDIRECTS_VERSION,
129
- staticRules: staticRedirects,
130
- rules: dynamicRedirects,
131
- },
132
- };
133
- }
134
-
135
- function constructHeaders({
136
- headers,
137
- headersFile,
138
- logger,
139
- }: {
140
- headers?: ParsedHeaders;
141
- headersFile?: string;
142
- logger: Logger;
143
- }): Metadata {
144
- if (!headers) {
145
- return {};
146
- }
147
-
148
- const num_valid = headers.rules.length;
149
- const num_invalid = headers.invalid.length;
150
-
151
- // exhaustive check, since we could not have parsed `headers` out of
152
- // a non-existing headers file
153
- const headersRelativePath = headersFile
154
- ? relative(process.cwd(), headersFile)
155
- : "";
156
-
157
- logger.log(
158
- `✨ Parsed ${num_valid} valid header rule${num_valid === 1 ? "" : "s"}.`
159
- );
160
-
161
- if (num_invalid > 0) {
162
- let invalidHeaderRulesList = ``;
163
-
164
- for (const { line, lineNumber, message } of headers.invalid) {
165
- invalidHeaderRulesList += `▶︎ ${message}\n`;
166
-
167
- if (line) {
168
- invalidHeaderRulesList += ` at ${headersRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
169
- }
170
- }
171
-
172
- logger.warn(
173
- `Found ${num_invalid} invalid header rule${num_invalid === 1 ? "" : "s"}:\n` +
174
- `${invalidHeaderRulesList}`
175
- );
176
- }
177
-
178
- /* Better to return no Headers object at all than one with empty rules */
179
- if (num_valid === 0) {
180
- return {};
181
- }
182
-
183
- const rules: MetadataHeaders = {};
184
- for (const rule of headers.rules) {
185
- rules[rule.path] = {};
186
-
187
- if (Object.keys(rule.headers).length) {
188
- rules[rule.path].set = rule.headers;
189
- }
190
- if (rule.unsetHeaders.length) {
191
- rules[rule.path].unset = rule.unsetHeaders;
192
- }
193
- }
194
-
195
- return {
196
- headers: {
197
- version: HEADERS_VERSION,
198
- rules,
199
- },
200
- };
201
- }
202
-
203
49
  function constructWebAnalytics({
204
50
  webAnalyticsToken,
205
51
  }: {
@@ -1,48 +1,13 @@
1
- /* REDIRECT PARSING TYPES */
1
+ import type {
2
+ ParsedHeaders,
3
+ ParsedRedirects,
4
+ } from "@cloudflare/workers-shared/utils/configuration/types";
2
5
 
3
- export type RedirectLine = [from: string, to: string, status?: number];
4
- export type RedirectRule = {
5
- from: string;
6
- to: string;
7
- status: number;
8
- lineNumber: number;
9
- };
10
-
11
- export type Headers = Record<string, string>;
12
- export type HeadersRule = {
13
- path: string;
14
- headers: Headers;
15
- unsetHeaders: string[];
16
- };
17
-
18
- export type InvalidRedirectRule = {
19
- line?: string;
20
- lineNumber?: number;
21
- message: string;
22
- };
23
-
24
- export type InvalidHeadersRule = {
25
- line?: string;
26
- lineNumber?: number;
27
- message: string;
28
- };
29
-
30
- export type ParsedRedirects = {
31
- invalid: InvalidRedirectRule[];
32
- rules: RedirectRule[];
33
- };
34
-
35
- /** Parsed redirects and input file */
36
6
  export type ParsedRedirectsWithFile = {
37
7
  parsedRedirects?: ParsedRedirects;
38
8
  file?: string;
39
9
  };
40
10
 
41
- export type ParsedHeaders = {
42
- invalid: InvalidHeadersRule[];
43
- rules: HeadersRule[];
44
- };
45
-
46
11
  /** Parsed headers and input file */
47
12
  export type ParsedHeadersWithFile = {
48
13
  parsedHeaders?: ParsedHeaders;
@@ -86,11 +51,3 @@ export type Metadata = {
86
51
  deploymentId?: string;
87
52
  failOpen?: boolean;
88
53
  };
89
-
90
- export interface Logger {
91
- debug: (message: string) => void;
92
- log: (message: string) => void;
93
- info: (message: string) => void;
94
- warn: (message: string) => void;
95
- error: (error: Error) => void;
96
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/pages-shared",
3
- "version": "0.13.11",
3
+ "version": "0.13.13",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/workers-sdk.git",
@@ -13,11 +13,12 @@
13
13
  "metadata-generator/**/*"
14
14
  ],
15
15
  "dependencies": {
16
- "miniflare": "3.20250214.2"
16
+ "@cloudflare/workers-shared": "0.15.0",
17
+ "miniflare": "3.20250310.0"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@cloudflare/vitest-pool-workers": "0.7.0",
20
- "@cloudflare/workers-types": "^4.20250214.0",
21
+ "@cloudflare/workers-types": "^4.20250310.0",
21
22
  "concurrently": "^8.2.2",
22
23
  "glob": "^10.4.5",
23
24
  "html-rewriter-wasm": "^0.4.1",
@@ -1,96 +0,0 @@
1
- // Taken from https://stackoverflow.com/a/3561711
2
- // which is everything from the tc39 proposal, plus the following two characters: ^/
3
- // It's also everything included in the URLPattern escape (https://wicg.github.io/urlpattern/#escape-a-regexp-string), plus the following: -
4
- // As the answer says, there's no downside to escaping these extra characters, so better safe than sorry
5
- const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
6
- const escapeRegex = (str: string) => {
7
- return str.replace(ESCAPE_REGEX_CHARACTERS, "\\$&");
8
- };
9
-
10
- // Placeholder names must begin with a colon then a letter, be alphanumeric and optionally contain underscores.
11
- // e.g. :place_123_holder
12
- const HOST_PLACEHOLDER_REGEX =
13
- /(?<=^https:\\\/\\\/[^/]*?):([A-Za-z]\w*)(?=\\)/g;
14
- const PLACEHOLDER_REGEX = /:([A-Za-z]\w*)/g;
15
-
16
- export type Replacements = Record<string, string>;
17
-
18
- export type Removals = string[];
19
-
20
- export const replacer = (str: string, replacements: Replacements) => {
21
- for (const [replacement, value] of Object.entries(replacements)) {
22
- str = str.replaceAll(`:${replacement}`, value);
23
- }
24
- return str;
25
- };
26
-
27
- export const generateRulesMatcher = <T>(
28
- rules?: Record<string, T>,
29
- replacerFn: (match: T, replacements: Replacements) => T = (match) => match
30
- ) => {
31
- if (!rules) {
32
- return () => [];
33
- }
34
-
35
- const compiledRules = Object.entries(rules)
36
- .map(([rule, match]) => {
37
- const crossHost = rule.startsWith("https://");
38
-
39
- // Create :splat capturer then escape.
40
- rule = rule.split("*").map(escapeRegex).join("(?<splat>.*)");
41
-
42
- // Create :placeholder capturers (already escaped).
43
- // For placeholders in the host, we separate at forward slashes and periods.
44
- // For placeholders in the path, we separate at forward slashes.
45
- // This matches the behavior of URLPattern.
46
- // e.g. https://:subdomain.domain/ -> https://(here).domain/
47
- // e.g. /static/:file -> /static/(image.jpg)
48
- // e.g. /blog/:post -> /blog/(an-exciting-post)
49
- const host_matches = rule.matchAll(HOST_PLACEHOLDER_REGEX);
50
- for (const host_match of host_matches) {
51
- rule = rule.split(host_match[0]).join(`(?<${host_match[1]}>[^/.]+)`);
52
- }
53
-
54
- const path_matches = rule.matchAll(PLACEHOLDER_REGEX);
55
- for (const path_match of path_matches) {
56
- rule = rule.split(path_match[0]).join(`(?<${path_match[1]}>[^/]+)`);
57
- }
58
-
59
- // Wrap in line terminators to be safe.
60
- rule = "^" + rule + "$";
61
-
62
- try {
63
- const regExp = new RegExp(rule);
64
- return [{ crossHost, regExp }, match];
65
- } catch {}
66
- })
67
- .filter((value) => value !== undefined) as [
68
- { crossHost: boolean; regExp: RegExp },
69
- T,
70
- ][];
71
-
72
- return ({ request }: { request: Request }) => {
73
- const { pathname, hostname } = new URL(request.url);
74
-
75
- return compiledRules
76
- .map(([{ crossHost, regExp }, match]) => {
77
- // This, rather confusingly, means that although we enforce `https://` protocols in
78
- // the rules of `_headers`/`_redirects`, we don't actually respect that at all at runtime.
79
- // When processing a request against an absolute URL rule, we rewrite the protocol to `https://`.
80
- // This has the benefit of ensuring attackers can't specify a different protocol
81
- // to circumvent a developer's security rules (e.g. CORS), but it isn't obvious behavior.
82
- // We should consider different syntax in the future for developers when they specify rules.
83
- // For example, `*://example.com/path`, `://example.com/path` or `//example.com/`.
84
- // Though we'd need to be careful with that last one
85
- // as that would currently be read as a relative URL.
86
- // Perhaps, if we ever move the `_headers`/`_redirects` files to acting ahead of Functions,
87
- // this might be a good time for this change.
88
- const test = crossHost ? `https://${hostname}${pathname}` : pathname;
89
- const result = regExp.exec(test);
90
- if (result) {
91
- return replacerFn(match, result.groups || {});
92
- }
93
- })
94
- .filter((value) => value !== undefined) as T[];
95
- };
96
- };
@@ -1,170 +0,0 @@
1
- import {
2
- HEADER_SEPARATOR,
3
- MAX_HEADER_RULES,
4
- MAX_LINE_LENGTH,
5
- UNSET_OPERATOR,
6
- } from "./constants";
7
- import { validateUrl } from "./validateURL";
8
- import type { HeadersRule, InvalidHeadersRule, ParsedHeaders } from "./types";
9
-
10
- // Not strictly necessary to check for all protocols-like beginnings, since _technically_ that could be a legit header (e.g. name=http, value=://I'm a value).
11
- // But we're checking here since some people might be caught out and it'll help 99.9% of people who get it wrong.
12
- // We do the proper validation in `validateUrl` anyway :)
13
- const LINE_IS_PROBABLY_A_PATH = new RegExp(/^([^\s]+:\/\/|^\/)/);
14
-
15
- export function parseHeaders(input: string): ParsedHeaders {
16
- const lines = input.split("\n");
17
- const rules: HeadersRule[] = [];
18
- const invalid: InvalidHeadersRule[] = [];
19
-
20
- let rule: (HeadersRule & { line: string }) | undefined = undefined;
21
-
22
- for (let i = 0; i < lines.length; i++) {
23
- const line = lines[i].trim();
24
- if (line.length === 0 || line.startsWith("#")) {
25
- continue;
26
- }
27
-
28
- if (line.length > MAX_LINE_LENGTH) {
29
- invalid.push({
30
- message: `Ignoring line ${
31
- i + 1
32
- } as it exceeds the maximum allowed length of ${MAX_LINE_LENGTH}.`,
33
- });
34
- continue;
35
- }
36
-
37
- if (LINE_IS_PROBABLY_A_PATH.test(line)) {
38
- if (rules.length >= MAX_HEADER_RULES) {
39
- invalid.push({
40
- message: `Maximum number of rules supported is ${MAX_HEADER_RULES}. Skipping remaining ${
41
- lines.length - i
42
- } lines of file.`,
43
- });
44
- break;
45
- }
46
-
47
- if (rule) {
48
- if (isValidRule(rule)) {
49
- rules.push({
50
- path: rule.path,
51
- headers: rule.headers,
52
- unsetHeaders: rule.unsetHeaders,
53
- });
54
- } else {
55
- invalid.push({
56
- line: rule.line,
57
- lineNumber: i + 1,
58
- message: "No headers specified",
59
- });
60
- }
61
- }
62
-
63
- const [path, pathError] = validateUrl(line, false, true);
64
- if (pathError) {
65
- invalid.push({
66
- line,
67
- lineNumber: i + 1,
68
- message: pathError,
69
- });
70
- rule = undefined;
71
- continue;
72
- }
73
-
74
- rule = {
75
- path: path as string,
76
- line,
77
- headers: {},
78
- unsetHeaders: [],
79
- };
80
- continue;
81
- }
82
-
83
- if (!line.includes(HEADER_SEPARATOR)) {
84
- if (!rule) {
85
- invalid.push({
86
- line,
87
- lineNumber: i + 1,
88
- message: "Expected a path beginning with at least one forward-slash",
89
- });
90
- } else {
91
- if (line.trim().startsWith(UNSET_OPERATOR)) {
92
- rule.unsetHeaders.push(line.trim().replace(UNSET_OPERATOR, ""));
93
- } else {
94
- invalid.push({
95
- line,
96
- lineNumber: i + 1,
97
- message:
98
- "Expected a colon-separated header pair (e.g. name: value)",
99
- });
100
- }
101
- }
102
- continue;
103
- }
104
-
105
- const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR);
106
- const name = rawName.trim().toLowerCase();
107
-
108
- if (name.includes(" ")) {
109
- invalid.push({
110
- line,
111
- lineNumber: i + 1,
112
- message: "Header name cannot include spaces",
113
- });
114
- continue;
115
- }
116
-
117
- const value = rawValue.join(HEADER_SEPARATOR).trim();
118
-
119
- if (name === "") {
120
- invalid.push({
121
- line,
122
- lineNumber: i + 1,
123
- message: "No header name specified",
124
- });
125
- continue;
126
- }
127
-
128
- if (value === "") {
129
- invalid.push({
130
- line,
131
- lineNumber: i + 1,
132
- message: "No header value specified",
133
- });
134
- continue;
135
- }
136
-
137
- if (!rule) {
138
- invalid.push({
139
- line,
140
- lineNumber: i + 1,
141
- message: `Path should come before header (${name}: ${value})`,
142
- });
143
- continue;
144
- }
145
-
146
- const existingValues = rule.headers[name];
147
- rule.headers[name] = existingValues ? `${existingValues}, ${value}` : value;
148
- }
149
-
150
- if (rule) {
151
- if (isValidRule(rule)) {
152
- rules.push({
153
- path: rule.path,
154
- headers: rule.headers,
155
- unsetHeaders: rule.unsetHeaders,
156
- });
157
- } else {
158
- invalid.push({ line: rule.line, message: "No headers specified" });
159
- }
160
- }
161
-
162
- return {
163
- rules,
164
- invalid,
165
- };
166
- }
167
-
168
- function isValidRule(rule: HeadersRule) {
169
- return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
170
- }
@@ -1,155 +0,0 @@
1
- import {
2
- MAX_DYNAMIC_REDIRECT_RULES,
3
- MAX_LINE_LENGTH,
4
- MAX_STATIC_REDIRECT_RULES,
5
- PERMITTED_STATUS_CODES,
6
- PLACEHOLDER_REGEX,
7
- SPLAT_REGEX,
8
- } from "./constants";
9
- import { urlHasHost, validateUrl } from "./validateURL";
10
- import type {
11
- InvalidRedirectRule,
12
- ParsedRedirects,
13
- RedirectLine,
14
- RedirectRule,
15
- } from "./types";
16
-
17
- export function parseRedirects(input: string): ParsedRedirects {
18
- const lines = input.split("\n");
19
- const rules: RedirectRule[] = [];
20
- const seen_paths = new Set<string>();
21
- const invalid: InvalidRedirectRule[] = [];
22
-
23
- let staticRules = 0;
24
- let dynamicRules = 0;
25
- let canCreateStaticRule = true;
26
-
27
- for (let i = 0; i < lines.length; i++) {
28
- const line = lines[i].trim();
29
- if (line.length === 0 || line.startsWith("#")) {
30
- continue;
31
- }
32
-
33
- if (line.length > MAX_LINE_LENGTH) {
34
- invalid.push({
35
- message: `Ignoring line ${
36
- i + 1
37
- } as it exceeds the maximum allowed length of ${MAX_LINE_LENGTH}.`,
38
- });
39
- continue;
40
- }
41
-
42
- const tokens = line.split(/\s+/);
43
-
44
- if (tokens.length < 2 || tokens.length > 3) {
45
- invalid.push({
46
- line,
47
- lineNumber: i + 1,
48
- message: `Expected exactly 2 or 3 whitespace-separated tokens. Got ${tokens.length}.`,
49
- });
50
- continue;
51
- }
52
-
53
- const [str_from, str_to, str_status = "302"] = tokens as RedirectLine;
54
-
55
- const fromResult = validateUrl(str_from, true, true, false, false);
56
- if (fromResult[0] === undefined) {
57
- invalid.push({
58
- line,
59
- lineNumber: i + 1,
60
- message: fromResult[1],
61
- });
62
- continue;
63
- }
64
- const from = fromResult[0];
65
-
66
- if (
67
- canCreateStaticRule &&
68
- !from.match(SPLAT_REGEX) &&
69
- !from.match(PLACEHOLDER_REGEX)
70
- ) {
71
- staticRules += 1;
72
-
73
- if (staticRules > MAX_STATIC_REDIRECT_RULES) {
74
- invalid.push({
75
- message: `Maximum number of static rules supported is ${MAX_STATIC_REDIRECT_RULES}. Skipping line.`,
76
- });
77
- continue;
78
- }
79
- } else {
80
- dynamicRules += 1;
81
- canCreateStaticRule = false;
82
-
83
- if (dynamicRules > MAX_DYNAMIC_REDIRECT_RULES) {
84
- invalid.push({
85
- message: `Maximum number of dynamic rules supported is ${MAX_DYNAMIC_REDIRECT_RULES}. Skipping remaining ${
86
- lines.length - i
87
- } lines of file.`,
88
- });
89
- break;
90
- }
91
- }
92
-
93
- const toResult = validateUrl(str_to, false, false, true, true);
94
- if (toResult[0] === undefined) {
95
- invalid.push({
96
- line,
97
- lineNumber: i + 1,
98
- message: toResult[1],
99
- });
100
- continue;
101
- }
102
- const to = toResult[0];
103
-
104
- const status = Number(str_status);
105
- if (isNaN(status) || !PERMITTED_STATUS_CODES.has(status)) {
106
- invalid.push({
107
- line,
108
- lineNumber: i + 1,
109
- message: `Valid status codes are 200, 301, 302 (default), 303, 307, or 308. Got ${str_status}.`,
110
- });
111
- continue;
112
- }
113
-
114
- // We want to always block the `/* /index.html` redirect - this will cause TOO_MANY_REDIRECTS errors as
115
- // the asset worker will redirect it back to `/`, removing the `/index.html`. This is the case for regular
116
- // redirects, as well as proxied (200) rewrites. We only want to run this on relative urls
117
- if (/\/\*?$/.test(from) && /\/index(.html)?$/.test(to) && !urlHasHost(to)) {
118
- invalid.push({
119
- line,
120
- lineNumber: i + 1,
121
- message:
122
- "Infinite loop detected in this rule and has been ignored. This will cause a redirect to strip `.html` or `/index` and end up triggering this rule again. Please fix or remove this rule to silence this warning.",
123
- });
124
- continue;
125
- }
126
-
127
- if (seen_paths.has(from)) {
128
- invalid.push({
129
- line,
130
- lineNumber: i + 1,
131
- message: `Ignoring duplicate rule for path ${from}.`,
132
- });
133
- continue;
134
- }
135
- seen_paths.add(from);
136
-
137
- if (status === 200) {
138
- if (urlHasHost(to)) {
139
- invalid.push({
140
- line,
141
- lineNumber: i + 1,
142
- message: `Proxy (200) redirects can only point to relative paths. Got ${to}`,
143
- });
144
- continue;
145
- }
146
- }
147
-
148
- rules.push({ from, to, status, lineNumber: i + 1 });
149
- }
150
-
151
- return {
152
- rules,
153
- invalid,
154
- };
155
- }
@@ -1,76 +0,0 @@
1
- export const extractPathname = (
2
- path = "/",
3
- includeSearch: boolean,
4
- includeHash: boolean
5
- ): string => {
6
- if (!path.startsWith("/")) {
7
- path = `/${path}`;
8
- }
9
- const url = new URL(`//${path}`, "relative://");
10
- return `${url.pathname}${includeSearch ? url.search : ""}${
11
- includeHash ? url.hash : ""
12
- }`;
13
- };
14
-
15
- const URL_REGEX = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/;
16
- const HOST_WITH_PORT_REGEX = /.*:\d+$/;
17
- const PATH_REGEX = /^\//;
18
-
19
- export const validateUrl = (
20
- token: string,
21
- onlyRelative = false,
22
- disallowPorts = false,
23
- includeSearch = false,
24
- includeHash = false
25
- ): [undefined, string] | [string, undefined] => {
26
- const host = URL_REGEX.exec(token);
27
- if (host && host.groups && host.groups.host) {
28
- if (onlyRelative) {
29
- return [
30
- undefined,
31
- `Only relative URLs are allowed. Skipping absolute URL ${token}.`,
32
- ];
33
- }
34
-
35
- if (disallowPorts && host.groups.host.match(HOST_WITH_PORT_REGEX)) {
36
- return [
37
- undefined,
38
- `Specifying ports is not supported. Skipping absolute URL ${token}.`,
39
- ];
40
- }
41
-
42
- return [
43
- `https://${host.groups.host}${extractPathname(
44
- host.groups.path,
45
- includeSearch,
46
- includeHash
47
- )}`,
48
- undefined,
49
- ];
50
- } else {
51
- if (!token.startsWith("/") && onlyRelative) {
52
- token = `/${token}`;
53
- }
54
-
55
- const path = PATH_REGEX.exec(token);
56
- if (path) {
57
- try {
58
- return [extractPathname(token, includeSearch, includeHash), undefined];
59
- } catch {
60
- return [undefined, `Error parsing URL segment ${token}. Skipping.`];
61
- }
62
- }
63
- }
64
-
65
- return [
66
- undefined,
67
- onlyRelative
68
- ? "URLs should begin with a forward-slash."
69
- : 'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").',
70
- ];
71
- };
72
-
73
- export function urlHasHost(token: string): boolean {
74
- const host = URL_REGEX.exec(token);
75
- return Boolean(host && host.groups && host.groups.host);
76
- }