UNPKG

3.88 kBJavaScriptView Raw
1import filterObj from 'filter-obj'
2import isPlainObj from 'is-plain-obj'
3
4import { normalizeConditions } from './conditions.js'
5import { splitResults } from './results.js'
6import { isUrl } from './url.js'
7
8// Validate and normalize an array of `redirects` objects.
9// This step is performed after `redirects` have been parsed from either
10// `netlify.toml` or `_redirects`.
11export const normalizeRedirects = function (redirects, opts) {
12 if (!Array.isArray(redirects)) {
13 const error = new TypeError(`Redirects must be an array not: ${redirects}`)
14 return splitResults([error])
15 }
16
17 const results = redirects.map((obj, index) => parseRedirect(obj, index, opts))
18 return splitResults(results)
19}
20
21const parseRedirect = function (obj, index, opts) {
22 if (!isPlainObj(obj)) {
23 return new TypeError(`Redirects must be objects not: ${obj}`)
24 }
25
26 try {
27 return parseRedirectObject(obj, opts)
28 } catch (error) {
29 return new Error(`Could not parse redirect number ${index + 1}:
30 ${JSON.stringify(obj)}
31${error.message}`)
32 }
33}
34
35// Parse a single `redirects` object
36const parseRedirectObject = function (
37 {
38 // `from` used to be named `origin`
39 origin,
40 from = origin,
41 // `query` used to be named `params` and `parameters`
42 parameters = {},
43 params = parameters,
44 query = params,
45 // `to` used to be named `destination`
46 destination,
47 to = destination,
48 status,
49 force = false,
50 conditions = {},
51 // `signed` used to be named `signing` and `sign`
52 sign,
53 signing = sign,
54 signed = signing,
55 headers = {},
56 },
57 { minimal = false },
58) {
59 if (from === undefined) {
60 throw new Error('Missing "from" field')
61 }
62
63 if (!isPlainObj(headers)) {
64 throw new Error('"headers" field must be an object')
65 }
66
67 const finalTo = addForwardRule(from, status, to)
68 const { scheme, host, path } = parseFrom(from)
69 const proxy = isProxy(status, finalTo)
70 const normalizedConditions = normalizeConditions(conditions)
71
72 // We ensure the return value has the same shape as our `netlify-commons`
73 // backend
74 return removeUndefinedValues({
75 from,
76 query,
77 to: finalTo,
78 status,
79 force,
80 conditions: normalizedConditions,
81 signed,
82 headers,
83 // If `minimal: true`, does not add additional properties that are not
84 // valid in `netlify.toml`
85 ...(!minimal && { scheme, host, path, proxy }),
86 })
87}
88
89// Add the optional `to` field when using a forward rule
90const addForwardRule = function (from, status, to) {
91 if (to !== undefined) {
92 return to
93 }
94
95 if (!isSplatRule(from, status)) {
96 throw new Error('Missing "to" field')
97 }
98
99 return from.replace(SPLAT_REGEXP, '/:splat')
100}
101
102// "to" can only be omitted when using forward rules:
103// - This requires "from" to end with "/*" and "status" to be 2**
104// - "to" will then default to "from" but with "/*" replaced to "/:splat"
105const isSplatRule = function (from, status) {
106 return from.endsWith('/*') && status >= 200 && status < 300
107}
108
109const SPLAT_REGEXP = /\/\*$/
110
111// Parses the `from` field which can be either a file path or a URL.
112const parseFrom = function (from) {
113 const { scheme, host, path } = parseFromField(from)
114 if (path.startsWith('/.netlify')) {
115 throw new Error('"path" field must not start with "/.netlify"')
116 }
117
118 return { scheme, host, path }
119}
120
121const parseFromField = function (from) {
122 if (!isUrl(from)) {
123 return { path: from }
124 }
125
126 try {
127 const { host, protocol, pathname: path } = new URL(from)
128 const scheme = protocol.slice(0, -1)
129 return { scheme, host, path }
130 } catch (error) {
131 throw new Error(`Invalid URL: ${error.message}`)
132 }
133}
134
135const isProxy = function (status, to) {
136 return status === 200 && isUrl(to)
137}
138
139const removeUndefinedValues = function (object) {
140 return filterObj(object, isDefined)
141}
142
143const isDefined = function (key, value) {
144 return value !== undefined
145}