1 | import filterObj from 'filter-obj'
|
2 | import isPlainObj from 'is-plain-obj'
|
3 |
|
4 | import { normalizeConditions } from './conditions.js'
|
5 | import { splitResults } from './results.js'
|
6 | import { isUrl } from './url.js'
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | export 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 |
|
21 | const 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 |
|
36 | const 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 |
|
73 |
|
74 | return removeUndefinedValues({
|
75 | from,
|
76 | query,
|
77 | to: finalTo,
|
78 | status,
|
79 | force,
|
80 | conditions: normalizedConditions,
|
81 | signed,
|
82 | headers,
|
83 |
|
84 |
|
85 | ...(!minimal && { scheme, host, path, proxy }),
|
86 | })
|
87 | }
|
88 |
|
89 |
|
90 | const 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 |
|
103 |
|
104 |
|
105 | const isSplatRule = function (from, status) {
|
106 | return from.endsWith('/*') && status >= 200 && status < 300
|
107 | }
|
108 |
|
109 | const SPLAT_REGEXP = /\/\*$/
|
110 |
|
111 |
|
112 | const 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 |
|
121 | const 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 |
|
135 | const isProxy = function (status, to) {
|
136 | return status === 200 && isUrl(to)
|
137 | }
|
138 |
|
139 | const removeUndefinedValues = function (object) {
|
140 | return filterObj(object, isDefined)
|
141 | }
|
142 |
|
143 | const isDefined = function (key, value) {
|
144 | return value !== undefined
|
145 | }
|