@netlify/redirect-parser 14.4.0
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/LICENSE +22 -0
- package/README.md +6 -0
- package/lib/all.d.ts +13 -0
- package/lib/all.js +38 -0
- package/lib/conditions.d.ts +1 -0
- package/lib/conditions.js +31 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/line_parser.d.ts +4 -0
- package/lib/line_parser.js +127 -0
- package/lib/merge.d.ts +7 -0
- package/lib/merge.js +31 -0
- package/lib/netlify_config_parser.d.ts +4 -0
- package/lib/netlify_config_parser.js +30 -0
- package/lib/normalize.d.ts +4 -0
- package/lib/normalize.js +115 -0
- package/lib/results.d.ts +8 -0
- package/lib/results.js +21 -0
- package/lib/status.d.ts +3 -0
- package/lib/status.js +21 -0
- package/lib/url.d.ts +1 -0
- package/lib/url.js +5 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2021 Netlify <team@netlify.com>
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Netlify Redirect Parser
|
|
2
|
+
|
|
3
|
+
Parses redirect rules from both `_redirects` and `netlify.toml` and normalizes them to an array of objects.
|
|
4
|
+
|
|
5
|
+
For most users, you are not meant to use this directly, please refer to https://github.com/netlify/cli instead. However
|
|
6
|
+
if you are debugging issues with redirect parsing, issues and PRs are welcome.
|
package/lib/all.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse all redirects given programmatically via the `configRedirects` property, `netlify.toml` and `_redirects`
|
|
3
|
+
* files, then normalize and validate those.
|
|
4
|
+
*/
|
|
5
|
+
export declare const parseAllRedirects: ({ redirectsFiles, netlifyConfigPath, configRedirects, minimal, }: {
|
|
6
|
+
redirectsFiles: string[];
|
|
7
|
+
netlifyConfigPath?: string;
|
|
8
|
+
configRedirects: string[];
|
|
9
|
+
minimal: boolean;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
redirects: unknown[];
|
|
12
|
+
errors: any[];
|
|
13
|
+
}>;
|
package/lib/all.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseFileRedirects } from './line_parser.js';
|
|
2
|
+
import { mergeRedirects } from './merge.js';
|
|
3
|
+
import { parseConfigRedirects } from './netlify_config_parser.js';
|
|
4
|
+
import { normalizeRedirects } from './normalize.js';
|
|
5
|
+
import { splitResults, concatResults } from './results.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse all redirects given programmatically via the `configRedirects` property, `netlify.toml` and `_redirects`
|
|
8
|
+
* files, then normalize and validate those.
|
|
9
|
+
*/
|
|
10
|
+
export const parseAllRedirects = async function ({ redirectsFiles = [], netlifyConfigPath, configRedirects = [], minimal = false, }) {
|
|
11
|
+
const [{ redirects: fileRedirects, errors: fileParseErrors }, { redirects: parsedConfigRedirects, errors: configParseErrors },] = await Promise.all([getFileRedirects(redirectsFiles), getConfigRedirects(netlifyConfigPath)]);
|
|
12
|
+
const { redirects: normalizedFileRedirects, errors: fileNormalizeErrors } = normalizeRedirects(fileRedirects, minimal);
|
|
13
|
+
const { redirects: normalizedParsedConfigRedirects, errors: parsedConfigNormalizeErrors } = normalizeRedirects(parsedConfigRedirects, minimal);
|
|
14
|
+
const { redirects: normalizedConfigRedirects, errors: configNormalizeErrors } = normalizeRedirects(configRedirects, minimal);
|
|
15
|
+
const { redirects, errors: mergeErrors } = mergeRedirects({
|
|
16
|
+
fileRedirects: normalizedFileRedirects,
|
|
17
|
+
configRedirects: [...normalizedParsedConfigRedirects, ...normalizedConfigRedirects],
|
|
18
|
+
});
|
|
19
|
+
const errors = [
|
|
20
|
+
...fileParseErrors,
|
|
21
|
+
...fileNormalizeErrors,
|
|
22
|
+
...configParseErrors,
|
|
23
|
+
...parsedConfigNormalizeErrors,
|
|
24
|
+
...configNormalizeErrors,
|
|
25
|
+
...mergeErrors,
|
|
26
|
+
];
|
|
27
|
+
return { redirects, errors };
|
|
28
|
+
};
|
|
29
|
+
const getFileRedirects = async function (redirectsFiles) {
|
|
30
|
+
const resultsArrays = await Promise.all(redirectsFiles.map((redirectFile) => parseFileRedirects(redirectFile)));
|
|
31
|
+
return concatResults(resultsArrays);
|
|
32
|
+
};
|
|
33
|
+
const getConfigRedirects = async function (netlifyConfigPath) {
|
|
34
|
+
if (netlifyConfigPath === undefined) {
|
|
35
|
+
return splitResults([]);
|
|
36
|
+
}
|
|
37
|
+
return await parseConfigRedirects(netlifyConfigPath);
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function normalizeConditions(rawConditions: any): any;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Normalize conditions
|
|
2
|
+
export const normalizeConditions = function (rawConditions) {
|
|
3
|
+
const caseNormalizedConditions = normalizeConditionCases(rawConditions);
|
|
4
|
+
const listNormalizedConditions = normalizeConditionLists(caseNormalizedConditions);
|
|
5
|
+
return listNormalizedConditions;
|
|
6
|
+
};
|
|
7
|
+
// Conditions can optionally be capitalized
|
|
8
|
+
const normalizeConditionCases = function (conditions) {
|
|
9
|
+
return CONDITION_CAPITALIZED_PROPS.reduce(normalizeConditionCase, conditions);
|
|
10
|
+
};
|
|
11
|
+
const CONDITION_CAPITALIZED_PROPS = [
|
|
12
|
+
{ name: 'role', capitalizedName: 'Role' },
|
|
13
|
+
{ name: 'language', capitalizedName: 'Language' },
|
|
14
|
+
{ name: 'country', capitalizedName: 'Country' },
|
|
15
|
+
];
|
|
16
|
+
const normalizeConditionCase = function (conditions, { name, capitalizedName }) {
|
|
17
|
+
const { [capitalizedName]: capitalizedProp, [name]: prop = capitalizedProp, ...conditionsA } = conditions;
|
|
18
|
+
return prop === undefined ? conditionsA : { ...conditionsA, [capitalizedName]: prop };
|
|
19
|
+
};
|
|
20
|
+
// Some `conditions` are array of strings.
|
|
21
|
+
// In `_redirects`, they are comma-separated lists instead.
|
|
22
|
+
const normalizeConditionLists = function (conditions) {
|
|
23
|
+
return CONDITION_LIST_PROPS.reduce(normalizeConditionList, conditions);
|
|
24
|
+
};
|
|
25
|
+
const CONDITION_LIST_PROPS = ['Role', 'Language', 'Country'];
|
|
26
|
+
const normalizeConditionList = function (conditions, name) {
|
|
27
|
+
return typeof conditions[name] === 'string'
|
|
28
|
+
? { ...conditions, [name]: conditions[name].trim().split(CONDITION_LIST_REGEXP) }
|
|
29
|
+
: conditions;
|
|
30
|
+
};
|
|
31
|
+
const CONDITION_LIST_REGEXP = /\s*,\s*/gu;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseAllRedirects } from './all.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseAllRedirects } from './all.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { pathExists } from 'path-exists';
|
|
3
|
+
import { splitResults } from './results.js';
|
|
4
|
+
import { transtypeStatusCode, isValidStatusCode } from './status.js';
|
|
5
|
+
import { isUrl } from './url.js';
|
|
6
|
+
// Parse `_redirects` file to an array of objects.
|
|
7
|
+
// Each line in that file must be either:
|
|
8
|
+
// - An empty line
|
|
9
|
+
// - A comment starting with #
|
|
10
|
+
// - A redirect line, optionally ended with a comment
|
|
11
|
+
// Each redirect line has the following format:
|
|
12
|
+
// from [query] [to] [status[!]] [conditions]
|
|
13
|
+
// The parts are:
|
|
14
|
+
// - "from": a path or a URL
|
|
15
|
+
// - "query": a whitespace-separated list of "key=value"
|
|
16
|
+
// - "to": a path or a URL
|
|
17
|
+
// - "status": an HTTP status integer
|
|
18
|
+
// - "!": an optional exclamation mark appended to "status" meant to indicate
|
|
19
|
+
// "forced"
|
|
20
|
+
// - "conditions": a whitespace-separated list of "key=value"
|
|
21
|
+
// - "Sign" is a special condition
|
|
22
|
+
// Unlike "redirects" in "netlify.toml", the "headers" and "edge_handlers"
|
|
23
|
+
// cannot be specified.
|
|
24
|
+
export const parseFileRedirects = async function (redirectFile) {
|
|
25
|
+
const results = await parseRedirects(redirectFile);
|
|
26
|
+
return splitResults(results);
|
|
27
|
+
};
|
|
28
|
+
const parseRedirects = async function (redirectFile) {
|
|
29
|
+
if (!(await pathExists(redirectFile))) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const text = await readRedirectFile(redirectFile);
|
|
33
|
+
if (typeof text !== 'string') {
|
|
34
|
+
return [text];
|
|
35
|
+
}
|
|
36
|
+
return text
|
|
37
|
+
.split('\n')
|
|
38
|
+
.map(normalizeLine)
|
|
39
|
+
.filter(hasRedirect)
|
|
40
|
+
.map((redirectLine) => parseRedirect(redirectLine));
|
|
41
|
+
};
|
|
42
|
+
const readRedirectFile = async function (redirectFile) {
|
|
43
|
+
try {
|
|
44
|
+
return await fs.readFile(redirectFile, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Error(`Could not read redirects file: ${redirectFile}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const normalizeLine = function (line, index) {
|
|
51
|
+
return { line: line.trim(), index };
|
|
52
|
+
};
|
|
53
|
+
const hasRedirect = function ({ line }) {
|
|
54
|
+
return line !== '' && !isComment(line);
|
|
55
|
+
};
|
|
56
|
+
const parseRedirect = function ({ line, index }) {
|
|
57
|
+
try {
|
|
58
|
+
return parseRedirectLine(line);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return new Error(`Could not parse redirect line ${index + 1}:
|
|
62
|
+
${line}
|
|
63
|
+
${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Parse a single redirect line
|
|
67
|
+
const parseRedirectLine = function (line) {
|
|
68
|
+
const [from, ...parts] = trimComment(line.split(LINE_TOKENS_REGEXP));
|
|
69
|
+
if (parts.length === 0) {
|
|
70
|
+
throw new Error('Missing destination path/URL');
|
|
71
|
+
}
|
|
72
|
+
const { queryParts, to, lastParts: [statusPart, ...conditionsParts], } = parseParts(from, parts);
|
|
73
|
+
const query = parsePairs(queryParts);
|
|
74
|
+
const { status, force } = parseStatus(statusPart);
|
|
75
|
+
const { Sign, signed = Sign, ...conditions } = parsePairs(conditionsParts);
|
|
76
|
+
return { from, query, to, status, force, conditions, signed };
|
|
77
|
+
};
|
|
78
|
+
// Removes inline comments at the end of the line
|
|
79
|
+
const trimComment = function (parts) {
|
|
80
|
+
const commentIndex = parts.findIndex(isComment);
|
|
81
|
+
return commentIndex === -1 ? parts : parts.slice(0, commentIndex);
|
|
82
|
+
};
|
|
83
|
+
const isComment = function (part) {
|
|
84
|
+
return part.startsWith('#');
|
|
85
|
+
};
|
|
86
|
+
const LINE_TOKENS_REGEXP = /\s+/g;
|
|
87
|
+
// Figure out the purpose of each whitelist-separated part, taking into account
|
|
88
|
+
// the fact that some are optional.
|
|
89
|
+
const parseParts = function (from, parts) {
|
|
90
|
+
// Optional `to` field when using a forward rule.
|
|
91
|
+
// The `to` field is added and validated later on, so we can leave it
|
|
92
|
+
// `undefined`
|
|
93
|
+
if (isValidStatusCode(transtypeStatusCode(parts[0]))) {
|
|
94
|
+
return { queryParts: [], to: undefined, lastParts: parts };
|
|
95
|
+
}
|
|
96
|
+
const toIndex = parts.findIndex(isToPart);
|
|
97
|
+
if (toIndex === -1) {
|
|
98
|
+
throw new Error('The destination path/URL must start with "/", "http:" or "https:"');
|
|
99
|
+
}
|
|
100
|
+
const queryParts = parts.slice(0, toIndex);
|
|
101
|
+
const to = parts[toIndex];
|
|
102
|
+
const lastParts = parts.slice(toIndex + 1);
|
|
103
|
+
return { queryParts, to, lastParts };
|
|
104
|
+
};
|
|
105
|
+
const isToPart = function (part) {
|
|
106
|
+
return part.startsWith('/') || isUrl(part);
|
|
107
|
+
};
|
|
108
|
+
// Parse the `status` part
|
|
109
|
+
const parseStatus = function (statusPart) {
|
|
110
|
+
if (statusPart === undefined) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
const status = transtypeStatusCode(statusPart);
|
|
114
|
+
if (!isValidStatusCode(status)) {
|
|
115
|
+
return { status: statusPart, force: false };
|
|
116
|
+
}
|
|
117
|
+
const force = statusPart.endsWith('!');
|
|
118
|
+
return { status, force };
|
|
119
|
+
};
|
|
120
|
+
// Part key=value pairs used for both the `query` and `conditions` parts
|
|
121
|
+
const parsePairs = function (conditions) {
|
|
122
|
+
return Object.assign({}, ...conditions.map(parsePair));
|
|
123
|
+
};
|
|
124
|
+
const parsePair = function (condition) {
|
|
125
|
+
const [key, value] = condition.split('=');
|
|
126
|
+
return { [key]: value };
|
|
127
|
+
};
|
package/lib/merge.d.ts
ADDED
package/lib/merge.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import stringify from 'fast-safe-stringify';
|
|
2
|
+
import { splitResults } from './results.js';
|
|
3
|
+
// Merge redirects from `_redirects` with the ones from `netlify.toml`.
|
|
4
|
+
// When both are specified, both are used and `_redirects` has priority.
|
|
5
|
+
// Since in both `netlify.toml` and `_redirects`, only the first matching rule
|
|
6
|
+
// is used, it is possible to merge `_redirects` to `netlify.toml` by prepending
|
|
7
|
+
// its rules to `netlify.toml` `redirects` field.
|
|
8
|
+
export const mergeRedirects = function ({ fileRedirects, configRedirects }) {
|
|
9
|
+
const results = [...fileRedirects, ...configRedirects];
|
|
10
|
+
const { redirects, errors } = splitResults(results);
|
|
11
|
+
const mergedRedirects = removeDuplicates(redirects);
|
|
12
|
+
return { redirects: mergedRedirects, errors };
|
|
13
|
+
};
|
|
14
|
+
// Remove duplicates. This is especially likely considering `fileRedirects`
|
|
15
|
+
// might have been previously merged to `configRedirects`, which happens when
|
|
16
|
+
// `netlifyConfig.redirects` is modified by plugins.
|
|
17
|
+
// The latest duplicate value is the one kept, hence why we need to iterate the
|
|
18
|
+
// array backwards and reverse it at the end
|
|
19
|
+
const removeDuplicates = function (redirects) {
|
|
20
|
+
const uniqueRedirects = new Set();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (let i = redirects.length - 1; i >= 0; i--) {
|
|
23
|
+
const r = redirects[i];
|
|
24
|
+
const key = stringify.default.stableStringify(r);
|
|
25
|
+
if (uniqueRedirects.has(key))
|
|
26
|
+
continue;
|
|
27
|
+
uniqueRedirects.add(key);
|
|
28
|
+
result.push(r);
|
|
29
|
+
}
|
|
30
|
+
return result.reverse();
|
|
31
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { parse as loadToml } from '@iarna/toml';
|
|
3
|
+
import { pathExists } from 'path-exists';
|
|
4
|
+
import { splitResults } from './results.js';
|
|
5
|
+
// Parse `redirects` field in "netlify.toml" to an array of objects.
|
|
6
|
+
// This field is already an array of objects, so it only validates and
|
|
7
|
+
// normalizes it.
|
|
8
|
+
export const parseConfigRedirects = async function (netlifyConfigPath) {
|
|
9
|
+
if (!(await pathExists(netlifyConfigPath))) {
|
|
10
|
+
return splitResults([]);
|
|
11
|
+
}
|
|
12
|
+
const results = await parseConfig(netlifyConfigPath);
|
|
13
|
+
return splitResults(results);
|
|
14
|
+
};
|
|
15
|
+
// Load the configuration file and parse it (TOML)
|
|
16
|
+
const parseConfig = async function (configPath) {
|
|
17
|
+
try {
|
|
18
|
+
const configString = await fs.readFile(configPath, 'utf8');
|
|
19
|
+
const config = loadToml(configString);
|
|
20
|
+
// Convert `null` prototype objects to normal plain objects
|
|
21
|
+
const { redirects = [] } = JSON.parse(JSON.stringify(config));
|
|
22
|
+
if (!Array.isArray(redirects)) {
|
|
23
|
+
throw new TypeError(`"redirects" must be an array`);
|
|
24
|
+
}
|
|
25
|
+
return redirects;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return [new Error(`Could not parse configuration file: ${error}`)];
|
|
29
|
+
}
|
|
30
|
+
};
|
package/lib/normalize.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { includeKeys } from 'filter-obj';
|
|
2
|
+
import isPlainObj from 'is-plain-obj';
|
|
3
|
+
import { normalizeConditions } from './conditions.js';
|
|
4
|
+
import { splitResults } from './results.js';
|
|
5
|
+
import { normalizeStatus } from './status.js';
|
|
6
|
+
import { isUrl } from './url.js';
|
|
7
|
+
// Validate and normalize an array of `redirects` objects.
|
|
8
|
+
// This step is performed after `redirects` have been parsed from either
|
|
9
|
+
// `netlify.toml` or `_redirects`.
|
|
10
|
+
export const normalizeRedirects = function (redirects, minimal) {
|
|
11
|
+
if (!Array.isArray(redirects)) {
|
|
12
|
+
const error = new TypeError(`Redirects must be an array not: ${redirects}`);
|
|
13
|
+
return splitResults([error]);
|
|
14
|
+
}
|
|
15
|
+
const results = redirects.map((obj, index) => parseRedirect(obj, index, minimal));
|
|
16
|
+
return splitResults(results);
|
|
17
|
+
};
|
|
18
|
+
const parseRedirect = function (obj, index, minimal) {
|
|
19
|
+
if (!isPlainObj(obj)) {
|
|
20
|
+
return new TypeError(`Redirects must be objects not: ${obj}`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return parseRedirectObject(obj, minimal);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return new Error(`Could not parse redirect number ${index + 1}:
|
|
27
|
+
${JSON.stringify(obj)}
|
|
28
|
+
${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
// Parse a single `redirects` object
|
|
32
|
+
const parseRedirectObject = function ({
|
|
33
|
+
// `from` used to be named `origin`
|
|
34
|
+
origin, from = origin,
|
|
35
|
+
// `query` used to be named `params` and `parameters`
|
|
36
|
+
parameters = {}, params = parameters, query = params,
|
|
37
|
+
// `to` used to be named `destination`
|
|
38
|
+
destination, to = destination, status, force = false, conditions = {},
|
|
39
|
+
// `signed` used to be named `signing` and `sign`
|
|
40
|
+
sign, signing = sign, signed = signing, headers = {}, rate_limit, }, minimal) {
|
|
41
|
+
if (from === undefined) {
|
|
42
|
+
throw new Error('Missing "from" field');
|
|
43
|
+
}
|
|
44
|
+
if (!isPlainObj(headers)) {
|
|
45
|
+
throw new Error('"headers" field must be an object');
|
|
46
|
+
}
|
|
47
|
+
const statusA = normalizeStatus(status);
|
|
48
|
+
const finalTo = addForwardRule(from, statusA, to);
|
|
49
|
+
const { scheme, host, path } = parseFrom(from);
|
|
50
|
+
const proxy = isProxy(statusA, finalTo);
|
|
51
|
+
const normalizedConditions = normalizeConditions(conditions);
|
|
52
|
+
// We ensure the return value has the same shape as our `netlify-commons`
|
|
53
|
+
// backend
|
|
54
|
+
return removeUndefinedValues({
|
|
55
|
+
from,
|
|
56
|
+
query,
|
|
57
|
+
to: finalTo,
|
|
58
|
+
status: statusA,
|
|
59
|
+
force,
|
|
60
|
+
conditions: normalizedConditions,
|
|
61
|
+
signed,
|
|
62
|
+
headers,
|
|
63
|
+
rate_limit,
|
|
64
|
+
// If `minimal: true`, does not add additional properties that are not
|
|
65
|
+
// valid in `netlify.toml`
|
|
66
|
+
...(!minimal && { scheme, host, path, proxy }),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
// Add the optional `to` field when using a forward rule
|
|
70
|
+
const addForwardRule = function (from, status, to) {
|
|
71
|
+
if (to !== undefined) {
|
|
72
|
+
return to;
|
|
73
|
+
}
|
|
74
|
+
if (!isSplatRule(from, status)) {
|
|
75
|
+
throw new Error('Missing "to" field');
|
|
76
|
+
}
|
|
77
|
+
return from.replace(SPLAT_REGEXP, '/:splat');
|
|
78
|
+
};
|
|
79
|
+
// "to" can only be omitted when using forward rules:
|
|
80
|
+
// - This requires "from" to end with "/*" and "status" to be 2**
|
|
81
|
+
// - "to" will then default to "from" but with "/*" replaced to "/:splat"
|
|
82
|
+
const isSplatRule = function (from, status) {
|
|
83
|
+
return from.endsWith('/*') && status >= 200 && status < 300;
|
|
84
|
+
};
|
|
85
|
+
const SPLAT_REGEXP = /\/\*$/;
|
|
86
|
+
// Parses the `from` field which can be either a file path or a URL.
|
|
87
|
+
const parseFrom = function (from) {
|
|
88
|
+
const { scheme, host, path } = parseFromField(from);
|
|
89
|
+
if (path.startsWith('/.netlify')) {
|
|
90
|
+
throw new Error('"path" field must not start with "/.netlify"');
|
|
91
|
+
}
|
|
92
|
+
return { scheme, host, path };
|
|
93
|
+
};
|
|
94
|
+
const parseFromField = function (from) {
|
|
95
|
+
if (!isUrl(from)) {
|
|
96
|
+
return { path: from };
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const { host, protocol, pathname: path } = new URL(from);
|
|
100
|
+
const scheme = protocol.slice(0, -1);
|
|
101
|
+
return { scheme, host, path };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new Error(`Invalid URL: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const isProxy = function (status, to) {
|
|
108
|
+
return status === 200 && isUrl(to);
|
|
109
|
+
};
|
|
110
|
+
const removeUndefinedValues = function (object) {
|
|
111
|
+
return includeKeys(object, isDefined);
|
|
112
|
+
};
|
|
113
|
+
const isDefined = function (key, value) {
|
|
114
|
+
return value !== undefined;
|
|
115
|
+
};
|
package/lib/results.d.ts
ADDED
package/lib/results.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// If one redirect fails to parse, we still try to return the other ones
|
|
2
|
+
export const splitResults = function (results) {
|
|
3
|
+
const redirects = results.filter((result) => !isError(result));
|
|
4
|
+
const errors = results.filter(isError);
|
|
5
|
+
return { redirects, errors };
|
|
6
|
+
};
|
|
7
|
+
const isError = function (result) {
|
|
8
|
+
return result instanceof Error;
|
|
9
|
+
};
|
|
10
|
+
// Concatenate an array of `{ redirects, errors }`
|
|
11
|
+
export const concatResults = function (resultsArrays) {
|
|
12
|
+
const redirects = resultsArrays.flatMap(getRedirects);
|
|
13
|
+
const errors = resultsArrays.flatMap(getErrors);
|
|
14
|
+
return { redirects, errors };
|
|
15
|
+
};
|
|
16
|
+
const getRedirects = function ({ redirects }) {
|
|
17
|
+
return redirects;
|
|
18
|
+
};
|
|
19
|
+
const getErrors = function ({ errors }) {
|
|
20
|
+
return errors;
|
|
21
|
+
};
|
package/lib/status.d.ts
ADDED
package/lib/status.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Normalize `status` field
|
|
2
|
+
export const normalizeStatus = function (status) {
|
|
3
|
+
if (status === undefined) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const statusCode = transtypeStatusCode(status);
|
|
7
|
+
if (!isValidStatusCode(statusCode)) {
|
|
8
|
+
throw new Error(`Invalid status code: ${status}`);
|
|
9
|
+
}
|
|
10
|
+
return statusCode;
|
|
11
|
+
};
|
|
12
|
+
// Transtype `status` string to a number.
|
|
13
|
+
// `status` might be a string ending with `!`. If so, `Number.parseInt()` strips
|
|
14
|
+
// and ignores it.
|
|
15
|
+
export const transtypeStatusCode = function (status) {
|
|
16
|
+
return Number.parseInt(status);
|
|
17
|
+
};
|
|
18
|
+
// Check whether the field is a valid status code
|
|
19
|
+
export const isValidStatusCode = function (status) {
|
|
20
|
+
return Number.isInteger(status);
|
|
21
|
+
};
|
package/lib/url.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function isUrl(pathOrUrl: any): boolean;
|
package/lib/url.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netlify/redirect-parser",
|
|
3
|
+
"version": "14.4.0",
|
|
4
|
+
"description": "Parses netlify redirects into a js object representation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./lib/index.js",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib/**/*"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"prebuild": "rm -rf lib",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:bench": "vitest bench",
|
|
17
|
+
"test:dev": "vitest",
|
|
18
|
+
"test:ci": "vitest run --reporter=default && vitest bench --run --passWithNoTests"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"netlify"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^14.16.0 || >=16.0.0"
|
|
25
|
+
},
|
|
26
|
+
"author": "Netlify",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@iarna/toml": "^2.2.5",
|
|
30
|
+
"fast-safe-stringify": "^2.1.1",
|
|
31
|
+
"filter-obj": "^5.0.0",
|
|
32
|
+
"is-plain-obj": "^4.0.0",
|
|
33
|
+
"path-exists": "^5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^14.18.53",
|
|
37
|
+
"ts-node": "^10.9.1",
|
|
38
|
+
"typescript": "^5.0.0",
|
|
39
|
+
"vitest": "^0.34.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/netlify/build.git",
|
|
44
|
+
"directory": "packages/redirect-parser"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/netlify/build/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/netlify/build#readme",
|
|
50
|
+
"gitHead": "131a644bfde5205f730f3369b778d8914c7c0382"
|
|
51
|
+
}
|