UNPKG

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