UNPKG

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