Skip to content

Commit 7550d0f

Browse files
Merge branch 'main' into max-age
2 parents ce64747 + 138b19d commit 7550d0f

File tree

11 files changed

+378
-195
lines changed

11 files changed

+378
-195
lines changed

.github/workflows/release-please.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
release-please:
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: navikt/github-app-token-generator@a9cd374e271b8aef573b8c16ac46c44fb89b02db
10+
- uses: navikt/github-app-token-generator@a3831f44404199df32d8f39f7c0ad9bb8fa18b1c
1111
id: get-token
1212
with:
1313
private-key: ${{ secrets.TOKENS_PRIVATE_KEY }}

package-lock.json

+130-81
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@babel/preset-env": "^7.15.8",
5353
"@babel/preset-typescript": "^7.16.0",
5454
"@delucis/if-env": "^1.1.2",
55-
"@netlify/build": "^29.5.0",
55+
"@netlify/build": "^29.5.1",
5656
"@netlify/eslint-config-node": "^7.0.1",
5757
"@testing-library/cypress": "^8.0.1",
5858
"@types/fs-extra": "^9.0.13",

packages/runtime/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
"devDependencies": {
3838
"@delucis/if-env": "^1.1.2",
39-
"@netlify/build": "^29.5.0",
39+
"@netlify/build": "^29.5.1",
4040
"@types/fs-extra": "^9.0.13",
4141
"@types/jest": "^27.4.1",
4242
"@types/merge-stream": "^1.1.2",

packages/runtime/src/helpers/edge.ts

+89-53
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { RouteHas } from 'next/dist/lib/load-custom-routes'
1111
import { outdent } from 'outdent'
1212

1313
import { getRequiredServerFiles, NextConfig } from './config'
14-
import { makeLocaleOptional, stripLookahead } from './matchers'
14+
import { makeLocaleOptional, stripLookahead, transformCaptureGroups } from './matchers'
1515
import { RoutesManifest } from './types'
1616

1717
// This is the format as of [email protected]
@@ -202,27 +202,18 @@ const writeEdgeFunction = async ({
202202
edgeFunctionDefinition,
203203
edgeFunctionRoot,
204204
netlifyConfig,
205-
pageRegexMap,
206-
appPathRoutesManifest = {},
207-
nextConfig,
208-
cache,
205+
functionName,
206+
matchers = [],
207+
middleware = false,
209208
}: {
210209
edgeFunctionDefinition: EdgeFunctionDefinition
211210
edgeFunctionRoot: string
212211
netlifyConfig: NetlifyConfig
213-
pageRegexMap?: Map<string, string>
214-
appPathRoutesManifest?: Record<string, string>
215-
nextConfig: NextConfig
216-
cache?: 'manual'
217-
}): Promise<
218-
Array<{
219-
function: string
220-
name: string
221-
pattern: string
222-
}>
223-
> => {
224-
const name = sanitizeName(edgeFunctionDefinition.name)
225-
const edgeFunctionDir = join(edgeFunctionRoot, name)
212+
functionName: string
213+
matchers?: Array<MiddlewareMatcher>
214+
middleware?: boolean
215+
}) => {
216+
const edgeFunctionDir = join(edgeFunctionRoot, functionName)
226217

227218
const bundle = await getMiddlewareBundle({
228219
edgeFunctionDefinition,
@@ -234,43 +225,52 @@ const writeEdgeFunction = async ({
234225

235226
await copyEdgeSourceFile({
236227
edgeFunctionDir,
237-
file: 'runtime.ts',
228+
file: middleware ? 'middleware-runtime.ts' : 'function-runtime.ts',
238229
target: 'index.ts',
239230
})
240231

241-
const matchers: EdgeFunctionDefinitionV2['matchers'] = []
232+
if (middleware) {
233+
// Functions don't have complex matchers, so we can rely on the Netlify matcher
234+
await writeJson(join(edgeFunctionDir, 'matchers.json'), matchers)
235+
}
236+
}
242237

238+
const generateEdgeFunctionMiddlewareMatchers = ({
239+
edgeFunctionDefinition,
240+
nextConfig,
241+
}: {
242+
edgeFunctionDefinition: EdgeFunctionDefinition
243+
edgeFunctionRoot: string
244+
nextConfig: NextConfig
245+
cache?: 'manual'
246+
}): Array<MiddlewareMatcher> => {
243247
// The v1 middleware manifest has a single regexp, but the v2 has an array of matchers
244248
if ('regexp' in edgeFunctionDefinition) {
245-
matchers.push({ regexp: edgeFunctionDefinition.regexp })
246-
} else if (nextConfig.i18n) {
247-
matchers.push(
248-
...edgeFunctionDefinition.matchers.map((matcher) => ({
249-
...matcher,
250-
regexp: makeLocaleOptional(matcher.regexp),
251-
})),
252-
)
253-
} else {
254-
matchers.push(...edgeFunctionDefinition.matchers)
249+
return [{ regexp: edgeFunctionDefinition.regexp }]
255250
}
256-
257-
// If the EF matches a page, it's an app dir page so needs a matcher too
258-
// The object will be empty if appDir isn't enabled in the Next config
259-
if (pageRegexMap && edgeFunctionDefinition.page in appPathRoutesManifest) {
260-
const regexp = pageRegexMap.get(appPathRoutesManifest[edgeFunctionDefinition.page])
261-
if (regexp) {
262-
matchers.push({ regexp })
263-
}
251+
if (nextConfig.i18n) {
252+
return edgeFunctionDefinition.matchers.map((matcher) => ({
253+
...matcher,
254+
regexp: makeLocaleOptional(matcher.regexp),
255+
}))
264256
}
257+
return edgeFunctionDefinition.matchers
258+
}
265259

266-
await writeJson(join(edgeFunctionDir, 'matchers.json'), matchers)
267-
268-
// We add a defintion for each matching path
269-
return matchers.map((matcher) => {
270-
const pattern = stripLookahead(matcher.regexp)
271-
return { function: name, pattern, name: edgeFunctionDefinition.name, cache }
272-
})
260+
const middlewareMatcherToEdgeFunctionDefinition = (
261+
matcher: MiddlewareMatcher,
262+
name: string,
263+
cache?: 'manual',
264+
): {
265+
function: string
266+
name?: string
267+
pattern: string
268+
cache?: 'manual'
269+
} => {
270+
const pattern = transformCaptureGroups(stripLookahead(matcher.regexp))
271+
return { function: name, pattern, name, cache }
273272
}
273+
274274
export const cleanupEdgeFunctions = ({
275275
INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions',
276276
}: NetlifyPluginConstants) => emptyDir(INTERNAL_EDGE_FUNCTIONS_SRC)
@@ -348,9 +348,28 @@ export const writeRscDataEdgeFunction = async ({
348348
]
349349
}
350350

351+
const getEdgeFunctionPatternForPage = ({
352+
edgeFunctionDefinition,
353+
pageRegexMap,
354+
appPathRoutesManifest,
355+
}: {
356+
edgeFunctionDefinition: EdgeFunctionDefinitionV2
357+
pageRegexMap: Map<string, string>
358+
appPathRoutesManifest?: Record<string, string>
359+
}): string => {
360+
// We don't just use the matcher from the edge function definition, because it doesn't handle trailing slashes
361+
362+
// appDir functions have a name that _isn't_ the route name, but rather the route with `/page` appended
363+
const regexp = pageRegexMap.get(appPathRoutesManifest?.[edgeFunctionDefinition.page] ?? edgeFunctionDefinition.page)
364+
// If we need to fall back to the matcher, we need to add an optional trailing slash
365+
return regexp ?? edgeFunctionDefinition.matchers[0].regexp.replace(/([^/])\$$/, '$1/?$')
366+
}
367+
351368
/**
352369
* Writes Edge Functions for the Next middleware
353370
*/
371+
372+
// eslint-disable-next-line max-lines-per-function
354373
export const writeEdgeFunctions = async ({
355374
netlifyConfig,
356375
routesManifest,
@@ -415,16 +434,25 @@ export const writeEdgeFunctions = async ({
415434
for (const middleware of middlewareManifest.sortedMiddleware) {
416435
usesEdge = true
417436
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
418-
const functionDefinitions = await writeEdgeFunction({
437+
const functionName = sanitizeName(edgeFunctionDefinition.name)
438+
const matchers = generateEdgeFunctionMiddlewareMatchers({
419439
edgeFunctionDefinition,
420440
edgeFunctionRoot,
421-
netlifyConfig,
422441
nextConfig,
423442
})
424-
manifest.functions.push(...functionDefinitions)
443+
await writeEdgeFunction({
444+
edgeFunctionDefinition,
445+
edgeFunctionRoot,
446+
netlifyConfig,
447+
functionName,
448+
matchers,
449+
middleware: true,
450+
})
451+
452+
manifest.functions.push(
453+
...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)),
454+
)
425455
}
426-
// Older versions of the manifest format don't have the functions field
427-
// No, the version field was not incremented
428456
if (typeof middlewareManifest.functions === 'object') {
429457
// When using the app dir, we also need to check if the EF matches a page
430458
const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig)
@@ -438,17 +466,25 @@ export const writeEdgeFunctions = async ({
438466

439467
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
440468
usesEdge = true
441-
const functionDefinitions = await writeEdgeFunction({
469+
const functionName = sanitizeName(edgeFunctionDefinition.name)
470+
await writeEdgeFunction({
442471
edgeFunctionDefinition,
443472
edgeFunctionRoot,
444473
netlifyConfig,
474+
functionName,
475+
})
476+
const pattern = getEdgeFunctionPatternForPage({
477+
edgeFunctionDefinition,
445478
pageRegexMap,
446479
appPathRoutesManifest,
447-
nextConfig,
480+
})
481+
manifest.functions.push({
482+
function: functionName,
483+
name: edgeFunctionDefinition.name,
484+
pattern,
448485
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
449486
cache: usesAppDir ? 'manual' : undefined,
450487
})
451-
manifest.functions.push(...functionDefinitions)
452488
}
453489
}
454490
if (usesEdge) {

packages/runtime/src/helpers/matchers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export const stripLookahead = (regex: string) => {
2424
}
2525
}
2626

27+
// The Go regexp lib has alternative syntax for named capture groups
28+
export const transformCaptureGroups = (regex: string) => regex.replace(/\(\?<\w+>/, '(')
29+
2730
const LOCALIZED_REGEX_PREFIX = '(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))'
2831
const OPTIONAL_REGEX_PREFIX = '(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))?'
2932

packages/runtime/src/templates/edge-shared/utils.test.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assertEquals } from 'https://deno.land/[email protected]/testing/asserts.ts'
22
import { beforeEach, describe, it } from 'https://deno.land/[email protected]/testing/bdd.ts'
3-
import { updateModifiedHeaders, FetchEventResult } from './utils.ts'
3+
import { redirectTrailingSlash, updateModifiedHeaders } from './utils.ts'
44

55
describe('updateModifiedHeaders', () => {
66
it("does not modify the headers if 'x-middleware-override-headers' is not found", () => {
@@ -62,3 +62,53 @@ describe('updateModifiedHeaders', () => {
6262
})
6363
})
6464
})
65+
66+
describe('trailing slash redirects', () => {
67+
it('adds a trailing slash to the pathn if trailingSlash is enabled', () => {
68+
const url = new URL('https://example.com/foo')
69+
const result = redirectTrailingSlash(url, true)
70+
assertEquals(result?.status, 308)
71+
assertEquals(result?.headers.get('location'), 'https://example.com/foo/')
72+
})
73+
74+
it("doesn't add a trailing slash if trailingSlash is false", () => {
75+
const url = new URL('https://example.com/foo')
76+
const result = redirectTrailingSlash(url, false)
77+
assertEquals(result, undefined)
78+
})
79+
80+
it("doesn't add a trailing slash if the path is a file", () => {
81+
const url = new URL('https://example.com/foo.txt')
82+
const result = redirectTrailingSlash(url, true)
83+
assertEquals(result, undefined)
84+
})
85+
it('adds a trailing slash if there is a dot in the path', () => {
86+
const url = new URL('https://example.com/foo.bar/baz')
87+
const result = redirectTrailingSlash(url, true)
88+
assertEquals(result?.status, 308)
89+
assertEquals(result?.headers.get('location'), 'https://example.com/foo.bar/baz/')
90+
})
91+
it("doesn't add a trailing slash if the path is /", () => {
92+
const url = new URL('https://example.com/')
93+
const result = redirectTrailingSlash(url, true)
94+
assertEquals(result, undefined)
95+
})
96+
it('removes a trailing slash from the path if trailingSlash is false', () => {
97+
const url = new URL('https://example.com/foo/')
98+
const result = redirectTrailingSlash(url, false)
99+
assertEquals(result?.status, 308)
100+
assertEquals(result?.headers.get('location'), 'https://example.com/foo')
101+
})
102+
it("doesn't remove a trailing slash if trailingSlash is true", () => {
103+
const url = new URL('https://example.com/foo/')
104+
const result = redirectTrailingSlash(url, true)
105+
assertEquals(result, undefined)
106+
})
107+
108+
it('removes a trailing slash from the path if the path is a file', () => {
109+
const url = new URL('https://example.com/foo.txt/')
110+
const result = redirectTrailingSlash(url, false)
111+
assertEquals(result?.status, 308)
112+
assertEquals(result?.headers.get('location'), 'https://example.com/foo.txt')
113+
})
114+
})

0 commit comments

Comments
 (0)