Skip to content

feat: add enhanced middleware support #1479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Jul 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5d67f5f
fix: update patch syntax
ascorbic Jul 22, 2022
efdb03d
feat: add support for rewriting middleware responses
ascorbic Jul 22, 2022
e1c41e9
chore: format
ascorbic Jul 22, 2022
d9011d0
chore: add extra content
ascorbic Jul 23, 2022
a2c88b7
chore: use city in demo
ascorbic Jul 23, 2022
ba284d9
feat: add html rewriting
ascorbic Jul 23, 2022
d66e08e
feat: add request header support
ascorbic Jul 23, 2022
55b5282
feat: add rewriting
ascorbic Jul 23, 2022
426a116
chore: move NetlifyReponse into a subpackage
ascorbic Jul 24, 2022
733e352
feat: allow returning `NetlifyReponse` directly
ascorbic Jul 24, 2022
138c225
chore: remove inlined types from middleware demo
ascorbic Jul 24, 2022
0256f25
chore: remove modified toml
ascorbic Jul 24, 2022
f35a285
chore: add demo links
ascorbic Jul 24, 2022
e5df385
chore: add comments to example
ascorbic Jul 25, 2022
2e1a4c6
chore: don't lint generated types
ascorbic Jul 25, 2022
5f5e17d
Merge branch 'main' into mk/rewrite-props-middleware
ascorbic Jul 25, 2022
d2d16df
chore: rename class
ascorbic Jul 25, 2022
e0861cb
chore: add comment about source of htmlrewriter types
ascorbic Jul 26, 2022
9d0f004
refactor: use type guards
ascorbic Jul 26, 2022
8deaba4
chore: rename classes
ascorbic Jul 26, 2022
5215688
chore: rename again
ascorbic Jul 26, 2022
3854370
chore: rename again
ascorbic Jul 26, 2022
e54fec6
chore: update example
ascorbic Jul 26, 2022
468a0d3
chore: make req a subclass of Request
ascorbic Jul 26, 2022
d4c0dc0
chore: switch from hidden fields to global map
ascorbic Jul 27, 2022
d5f2b95
Merge branch 'main' into mk/rewrite-props-middleware
ascorbic Jul 27, 2022
71fa122
Merge branch 'main' into mk/rewrite-props-middleware
ascorbic Jul 29, 2022
0fd6c33
ci: add cypress middleware tests
ascorbic Jul 29, 2022
328a584
ci: add tests for middleware headers
ascorbic Jul 29, 2022
4f6a2aa
ci: add tests for enhanced middleware
ascorbic Jul 29, 2022
1cf3e1f
chore: fix test
ascorbic Jul 29, 2022
33ade90
fix: handle other HTTP verbs
ascorbic Jul 29, 2022
9f4044c
feat: add helper methods
ascorbic Jul 29, 2022
78c8225
fix: less flaky test
ascorbic Jul 29, 2022
6c5664e
Merge branch 'main' into mk/rewrite-props-middleware
nickytonline Jul 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ node_modules
test
lib
demos
plugin/src/templates/edge
plugin/src/templates/edge
plugin/lib
plugin/dist-types
73 changes: 73 additions & 0 deletions .github/workflows/cypress-middleware.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Run e2e (middleware demo)
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
push:
branches:
- main
paths:
- 'demos/middleware/**/*.{js,jsx,ts,tsx}'
- 'cypress/integration/middleware/**/*.{ts,js}'
- 'src/**/*.{ts,js}'
jobs:
cypress:
name: Cypress
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Generate Github token
uses: navikt/github-app-token-generator@v1
id: get-token
with:
private-key: ${{ secrets.TOKENS_PRIVATE_KEY }}
app-id: ${{ secrets.TOKENS_APP_ID }}

- name: Checkout @netlify/wait-for-deploy-action
uses: actions/checkout@v2
with:
repository: netlify/wait-for-deploy-action
token: ${{ steps.get-token.outputs.token }}
path: ./.github/actions/wait-for-netlify-deploy

- name: Wait for Netlify Deploy
id: deploy
uses: ./.github/actions/wait-for-netlify-deploy
with:
site-name: next-plugin-edge-middleware
timeout: 300

- name: Deploy successful
if: ${{ steps.deploy.outputs.origin-url }}
run: echo ${{ steps.deploy.outputs.origin-url }}

- name: Node
uses: actions/setup-node@v2
with:
node-version: '16'

- run: npm install

- name: Cypress run
if: ${{ steps.deploy.outputs.origin-url }}
id: cypress
uses: cypress-io/github-action@v2
with:
browser: chrome
headless: true
record: true
parallel: true
config-file: cypress/config/middleware.json
group: 'Next Plugin - Middleware'
spec: cypress/integration/middleware/*
env:
DEBUG: '@cypress/github-action'
CYPRESS_baseUrl: ${{ steps.deploy.outputs.origin-url }}
CYPRESS_NETLIFY_CONTEXT: ${{ steps.deploy.outputs.context }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_RECORD_KEY: ${{ secrets.MIDDLEWARE_CYPRESS_RECORD_KEY }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Temporary Items
demos/default/.next
.parcel-cache
plugin/lib
plugin/dist-types

# Cypress
cypress/screenshots
5 changes: 3 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ node_modules
lib
tsconfig.json
demos/nx-next-monorepo-demo
plugin/src/templates/edge

plugin/CHANGELOG.md
plugin/CHANGELOG.md
plugin/lib
plugin/dist-types
5 changes: 5 additions & 0 deletions cypress/config/middleware.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"baseUrl": "http://localhost:8888",
"integrationFolder": "cypress/integration/middleware",
"projectId": "yn8qwi"
}
28 changes: 28 additions & 0 deletions cypress/integration/middleware/enhanced.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
describe('Enhanced middleware', () => {
it('adds request headers', () => {
cy.request('/api/hello').then((response) => {
expect(response.body).to.have.nested.property('headers.x-hello', 'world')
})
})

it('adds request headers to a rewrite', () => {
cy.request('/headers').then((response) => {
expect(response.body).to.have.nested.property('headers.x-hello', 'world')
})
})

it('rewrites the response body', () => {
cy.visit('/static')
cy.get('#message').contains('This was static but has been transformed in')
cy.contains("This is an ad that isn't shown by default")
})

it('modifies the page props', () => {
cy.request('/_next/data/build-id/static.json').then((response) => {
expect(response.body).to.have.nested.property('pageProps.showAd', true)
expect(response.body)
.to.have.nested.property('pageProps.message')
.that.includes('This was static but has been transformed in')
})
})
})
22 changes: 22 additions & 0 deletions cypress/integration/middleware/standard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
describe('Standard middleware', () => {
it('rewrites to internal page', () => {
// preview mode is off by default
cy.visit('/shows/rewriteme')
cy.get('h1').should('contain', 'Show #100')
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewriteme`)
})

it('rewrites to external page', () => {
cy.visit('/shows/rewrite-external')
cy.get('h1').should('contain', 'Example Domain')
cy.url().should('eq', `${Cypress.config().baseUrl}/shows/rewrite-external`)
})

it('adds headers to static pages', () => {
cy.request('/shows/static/3').then((response) => {
expect(response.headers).to.have.property('x-middleware-date')
expect(response.headers).to.have.property('x-is-deno', 'true')
expect(response.headers).to.have.property('x-modified-edge', 'true')
})
})
})
39 changes: 34 additions & 5 deletions demos/middleware/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import { NextResponse } from 'next/server'
import { NextFetchEvent, NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest, ev: NextFetchEvent) {
import { MiddlewareRequest } from '@netlify/plugin-nextjs/middleware'

export async function middleware(req: NextRequest) {
let response
const {
nextUrl: { pathname },
} = request
} = req

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[dust] Use the full name request and response for variable names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request and response are already used for the MiddlewareRequest and MiddlewareResponse. I considered changing them to nextRequest and nextResponse but thought it made the code examples a bit verbose. Hard to say which is better tbh

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realise I've mixed the different styles, so yeah this should be tidied up in a follow-up.


const request = new MiddlewareRequest(req)

if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
const res = await request.next()
const message = `This was static but has been transformed in ${req.geo.city}`

// Transform the response HTML and props
res.replaceText('p[id=message]', message)
res.setPageProp('message', message)
res.setPageProp('showAd', true)

return res
}

if (pathname.startsWith('/api/hello')) {
// Add a header to the request
req.headers.set('x-hello', 'world')
return request.next()
}

if (pathname.startsWith('/headers')) {
// Add a header to the rewritten request
req.headers.set('x-hello', 'world')
return request.rewrite('/api/hello')
}

if (pathname.startsWith('/cookies')) {
response = NextResponse.next()
Expand All @@ -15,15 +44,15 @@ export function middleware(request: NextRequest, ev: NextFetchEvent) {

if (pathname.startsWith('/shows')) {
if (pathname.startsWith('/shows/rewrite-absolute')) {
response = NextResponse.rewrite(new URL('/shows/100', request.url))
response = NextResponse.rewrite(new URL('/shows/100', req.url))
response.headers.set('x-modified-in-rewrite', 'true')
}
if (pathname.startsWith('/shows/rewrite-external')) {
response = NextResponse.rewrite('http://example.com/')
response.headers.set('x-modified-in-rewrite', 'true')
}
if (pathname.startsWith('/shows/rewriteme')) {
const url = request.nextUrl.clone()
const url = req.nextUrl.clone()
url.pathname = '/shows/100'
response = NextResponse.rewrite(url)
response.headers.set('x-modified-in-rewrite', 'true')
Expand Down
10 changes: 10 additions & 0 deletions demos/middleware/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ included_files = [

[dev]
framework = "#static"

[[redirects]]
from = "/_next/static/*"
to = "/static/:splat"
status = 200

[[redirects]]
from = "/*"
to = "/.netlify/functions/___netlify-handler"
status = 200
1 change: 1 addition & 0 deletions demos/middleware/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const nextConfig = {
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
generateBuildId: () => 'build-id',
Copy link

@nickytonline nickytonline Jul 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

module.exports = nextConfig
4 changes: 2 additions & 2 deletions demos/middleware/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"ntl": "ntl-internal"
},
"dependencies": {
"@netlify/plugin-nextjs": "*",
"next": "^12.2.0",
"react": "18.0.0",
"react-dom": "18.0.0"
},
"devDependencies": {
"@netlify/plugin-nextjs": "*",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.25",
Expand All @@ -23,4 +23,4 @@
"npm-run-all": "^4.1.5",
"typescript": "^4.6.3"
}
}
}
2 changes: 1 addition & 1 deletion demos/middleware/pages/api/hello.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
res.status(200).json({ name: 'John Doe', headers: req.headers })
}
9 changes: 9 additions & 0 deletions demos/middleware/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export default function Home() {
Cookie API
</Link>
</p>
<p>
<Link href="/api/hello">Adds `x-hello` request header</Link>
</p>
<p>
<Link href="/static">Rewrite static page content</Link>
</p>
<p>
<Link href="/headers">Adds `x-hello` request header to a rewrite</Link>
</p>
</main>
</div>
)
Expand Down
37 changes: 37 additions & 0 deletions demos/middleware/pages/static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react'

const useHydrated = () => {
const [hydrated, setHydrated] = React.useState(false)
React.useEffect(() => {
setHydrated(true)
}, [])
return hydrated
}

const Page = ({ message, showAd }) => {
const hydrated = useHydrated()
return (
<div>
<p id="message">{message}</p>
{hydrated && showAd ? (
<div>
<p>This is an ad that isn't shown by default</p>
<img src="http://placekitten.com/400/300" />
</div>
) : (
<p>No ads for me</p>
)}
</div>
)
}

export async function getStaticProps() {
return {
props: {
message: 'This is a static page',
showAd: false,
},
}
}

export default Page
Loading