UNPKG

4.15 kBJavaScriptView Raw
1const { readFile } = require('fs')
2const { promisify } = require('util')
3
4const pathExists = require('path-exists')
5
6const { splitResults } = require('./results')
7const { isUrl } = require('./url')
8
9const pReadFile = promisify(readFile)
10
11// Parse `_redirects` file to an array of objects.
12// Each line in that file must be either:
13// - An empty line
14// - A comment starting with #
15// - A redirect line, optionally ended with a comment
16// Each redirect line has the following format:
17// from [query] [to] [status[!]] [conditions]
18// The parts are:
19// - "from": a path or a URL
20// - "query": a whitespace-separated list of "key=value"
21// - "to": a path or a URL
22// - "status": an HTTP status integer
23// - "!": an optional exclamation mark appended to "status" meant to indicate
24// "forced"
25// - "conditions": a whitespace-separated list of "key=value"
26// - "Sign" is a special condition
27// Unlike "redirects" in "netlify.toml", the "headers" and "edge_handlers"
28// cannot be specified.
29const parseFileRedirects = async function (redirectFile) {
30 if (!(await pathExists(redirectFile))) {
31 return splitResults([])
32 }
33
34 const text = await pReadFile(redirectFile, 'utf8')
35 const results = text.split('\n').map(normalizeLine).filter(hasRedirect).map(parseRedirect)
36 return splitResults(results)
37}
38
39const normalizeLine = function (line, index) {
40 return { line: line.trim(), index }
41}
42
43const hasRedirect = function ({ line }) {
44 return line !== '' && !isComment(line)
45}
46
47const parseRedirect = function ({ line, index }) {
48 try {
49 return parseRedirectLine(line)
50 } catch (error) {
51 return new Error(`Could not parse redirect line ${index + 1}:
52 ${line}
53${error.message}`)
54 }
55}
56
57// Parse a single redirect line
58const parseRedirectLine = function (line) {
59 const [from, ...parts] = trimComment(line.split(LINE_TOKENS_REGEXP))
60
61 if (parts.length === 0) {
62 throw new Error('Missing destination path/URL')
63 }
64
65 const {
66 queryParts,
67 to,
68 lastParts: [statusPart, ...conditionsParts],
69 } = parseParts(from, parts)
70
71 const query = parsePairs(queryParts)
72 const { status, force } = parseStatus(statusPart)
73 const { Sign, signed = Sign, ...conditions } = parsePairs(conditionsParts)
74 return { from, query, to, status, force, conditions, signed }
75}
76
77// Removes inline comments at the end of the line
78const trimComment = function (parts) {
79 const commentIndex = parts.findIndex(isComment)
80 return commentIndex === -1 ? parts : parts.slice(0, commentIndex)
81}
82
83const isComment = function (part) {
84 return part.startsWith('#')
85}
86
87const LINE_TOKENS_REGEXP = /\s+/g
88
89// Figure out the purpose of each whitelist-separated part, taking into account
90// the fact that some are optional.
91const parseParts = function (from, parts) {
92 // Optional `to` field when using a forward rule.
93 // The `to` field is added and validated later on, so we can leave it
94 // `undefined`
95 if (isStatusCode(parts[0])) {
96 return { queryParts: [], to: undefined, lastParts: parts }
97 }
98
99 const toIndex = parts.findIndex(isToPart)
100 if (toIndex === -1) {
101 throw new Error('The destination path/URL must start with "/", "http:" or "https:"')
102 }
103
104 const queryParts = parts.slice(0, toIndex)
105 const to = parts[toIndex]
106 const lastParts = parts.slice(toIndex + 1)
107 return { queryParts, to, lastParts }
108}
109
110const isToPart = function (part) {
111 return part.startsWith('/') || isUrl(part)
112}
113
114const isStatusCode = function (part) {
115 return Number.isInteger(getStatusCode(part))
116}
117
118// Parse the `status` part
119const parseStatus = function (statusPart) {
120 if (statusPart === undefined) {
121 return {}
122 }
123
124 const status = getStatusCode(statusPart)
125 const force = statusPart.endsWith('!')
126 return { status, force }
127}
128
129const getStatusCode = function (statusPart) {
130 return Number.parseInt(statusPart)
131}
132
133// Part key=value pairs used for both the `query` and `conditions` parts
134const parsePairs = function (conditions) {
135 return Object.assign({}, ...conditions.map(parsePair))
136}
137
138const parsePair = function (condition) {
139 const [key, value] = condition.split('=')
140 return { [key]: value }
141}
142
143module.exports = { parseFileRedirects }