Skip to content

Commit 73c6bb9

Browse files
committed
simplify redirect handling with sourceRegex shipped recently in adapters API
1 parent f3e466e commit 73c6bb9

File tree

1 file changed

+3
-132
lines changed

1 file changed

+3
-132
lines changed

src/adapter/build/routing.ts

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { cp, writeFile } from 'node:fs/promises'
22
import { join } from 'node:path/posix'
3-
import { format as formatUrl, parse as parseUrl } from 'node:url'
43

54
import { glob } from 'fast-glob'
6-
import {
7-
pathToRegexp,
8-
compile as pathToRegexpCompile,
9-
type Key as PathToRegexpKey,
10-
} from 'path-to-regexp'
115

126
import type { RoutingRule, RoutingRuleRedirect, RoutingRuleRewrite } from '../run/routing.js'
137

@@ -19,144 +13,21 @@ import {
1913
} from './constants.js'
2014
import type { NetlifyAdapterContext, OnBuildCompleteContext } from './types.js'
2115

22-
const UN_NAMED_SEGMENT = '__UN_NAMED_SEGMENT__'
23-
24-
// https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L273
25-
export function sourceToRegex(source: string) {
26-
const keys: PathToRegexpKey[] = []
27-
const regexp = pathToRegexp(source, keys, {
28-
strict: true,
29-
sensitive: true,
30-
delimiter: '/',
31-
})
32-
33-
return {
34-
sourceRegexString: regexp.source,
35-
segments: keys
36-
.map((key) => key.name)
37-
.map((keyName) => {
38-
if (typeof keyName !== 'string') {
39-
return UN_NAMED_SEGMENT
40-
}
41-
return keyName
42-
}),
43-
}
44-
}
45-
46-
// https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L345
47-
const escapeSegment = (str: string, segmentName: string) =>
48-
str.replace(new RegExp(`:${segmentName}`, 'g'), `__esc_colon_${segmentName}`)
49-
50-
// https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L348
51-
const unescapeSegments = (str: string) => str.replace(/__esc_colon_/gi, ':')
52-
53-
// https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L464
54-
function safelyCompile(
55-
val: string,
56-
indexes: { [k: string]: string },
57-
attemptDirectCompile?: boolean,
58-
): string {
59-
let value = val
60-
if (!value) {
61-
return value
62-
}
63-
64-
if (attemptDirectCompile) {
65-
try {
66-
// Attempt compiling normally with path-to-regexp first and fall back
67-
// to safely compiling to handle edge cases if path-to-regexp compile
68-
// fails
69-
return pathToRegexpCompile(value, { validate: false })(indexes)
70-
} catch {
71-
// non-fatal, we continue to safely compile
72-
}
73-
}
74-
75-
for (const key of Object.keys(indexes)) {
76-
if (value.includes(`:${key}`)) {
77-
value = value
78-
.replace(new RegExp(`:${key}\\*`, 'g'), `:${key}--ESCAPED_PARAM_ASTERISK`)
79-
.replace(new RegExp(`:${key}\\?`, 'g'), `:${key}--ESCAPED_PARAM_QUESTION`)
80-
.replace(new RegExp(`:${key}\\+`, 'g'), `:${key}--ESCAPED_PARAM_PLUS`)
81-
.replace(new RegExp(`:${key}(?!\\w)`, 'g'), `--ESCAPED_PARAM_COLON${key}`)
82-
}
83-
}
84-
value = value
85-
// eslint-disable-next-line unicorn/better-regex
86-
.replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1')
87-
.replace(/--ESCAPED_PARAM_PLUS/g, '+')
88-
.replace(/--ESCAPED_PARAM_COLON/g, ':')
89-
.replace(/--ESCAPED_PARAM_QUESTION/g, '?')
90-
.replace(/--ESCAPED_PARAM_ASTERISK/g, '*')
91-
92-
// the value needs to start with a forward-slash to be compiled
93-
// correctly
94-
return pathToRegexpCompile(`/${value}`, { validate: false })(indexes).slice(1)
95-
}
96-
97-
// https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L350
98-
export function destinationToReplacementString(destination: string, segments: string[]) {
99-
// convert /path/:id/route to /path/$1/route
100-
// convert /path/:id+ to /path/$1
101-
102-
let escapedDestination = destination
103-
104-
const indexes: { [k: string]: string } = {}
105-
106-
segments.forEach((name, index) => {
107-
indexes[name] = `$${index + 1}`
108-
escapedDestination = escapeSegment(escapedDestination, name)
109-
})
110-
111-
const parsedDestination = parseUrl(escapedDestination, true)
112-
delete (parsedDestination as any).href
113-
delete (parsedDestination as any).path
114-
delete (parsedDestination as any).search
115-
delete (parsedDestination as any).host
116-
let { pathname, ...rest } = parsedDestination
117-
pathname = unescapeSegments(pathname || '')
118-
119-
const pathnameKeys: PathToRegexpKey[] = []
120-
121-
try {
122-
pathToRegexp(pathname, pathnameKeys)
123-
} catch {
124-
// this is not fatal so don't error when failing to parse the
125-
// params from the destination
126-
}
127-
128-
pathname = safelyCompile(pathname, indexes, true)
129-
130-
const finalDestination = formatUrl({
131-
...rest,
132-
// hostname,
133-
pathname,
134-
// query,
135-
// hash,
136-
})
137-
// url.format() escapes the dollar sign but it must be preserved for now-proxy
138-
return finalDestination.replace(/%24/g, '$')
139-
}
140-
14116
export function convertRedirectToRoutingRule(
14217
redirect: Pick<
14318
OnBuildCompleteContext['routes']['redirects'][number],
144-
'source' | 'destination' | 'priority'
19+
'sourceRegex' | 'destination' | 'priority'
14520
>,
14621
description: string,
14722
): RoutingRuleRedirect {
148-
const { sourceRegexString, segments } = sourceToRegex(redirect.source)
149-
150-
const convertedDestination = destinationToReplacementString(redirect.destination, segments)
151-
15223
return {
15324
description,
15425
match: {
155-
path: sourceRegexString,
26+
path: redirect.sourceRegex,
15627
},
15728
apply: {
15829
type: 'redirect',
159-
destination: convertedDestination,
30+
destination: redirect.destination,
16031
},
16132
} satisfies RoutingRuleRedirect
16233
}

0 commit comments

Comments
 (0)