@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 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,4 @@
1
+ export function parseFileRedirects(redirectFile: any): Promise<{
2
+ redirects: any;
3
+ errors: any;
4
+ }>;
@@ -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
@@ -0,0 +1,7 @@
1
+ export declare const mergeRedirects: ({ fileRedirects, configRedirects }: {
2
+ fileRedirects: any;
3
+ configRedirects: any;
4
+ }) => {
5
+ redirects: unknown[];
6
+ errors: any;
7
+ };
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,4 @@
1
+ export function parseConfigRedirects(netlifyConfigPath: any): Promise<{
2
+ redirects: any;
3
+ errors: any;
4
+ }>;
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ export function normalizeRedirects(redirects: any, minimal: any): {
2
+ redirects: any;
3
+ errors: any;
4
+ };
@@ -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
+ };
@@ -0,0 +1,8 @@
1
+ export function splitResults(results: any): {
2
+ redirects: any;
3
+ errors: any;
4
+ };
5
+ export function concatResults(resultsArrays: any): {
6
+ redirects: any;
7
+ errors: any;
8
+ };
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
+ };
@@ -0,0 +1,3 @@
1
+ export function normalizeStatus(status: any): number | undefined;
2
+ export function transtypeStatusCode(status: any): number;
3
+ export function isValidStatusCode(status: any): boolean;
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
@@ -0,0 +1,5 @@
1
+ // Check if a field is valid redirect URL
2
+ export const isUrl = function (pathOrUrl) {
3
+ return SCHEMES.some((scheme) => pathOrUrl.startsWith(scheme));
4
+ };
5
+ const SCHEMES = ['http://', 'https://'];
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
+ }