Skip to content

feat: use ODB (fallback pages + next/image) #295

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 1 commit into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,797 changes: 550 additions & 1,247 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
"dependencies": {
"@netlify/functions": "^0.6.0",
"@sls-next/lambda-at-edge": "1.8.0",
"adm-zip": "^0.5.4",
"chalk": "^4.1.0",
Expand All @@ -64,10 +65,12 @@
"find-cache-dir": "^3.3.1",
"find-up": "^5.0.0",
"fs-extra": "^9.1.0",
"jimp": "^0.16.1",
"make-dir": "^3.1.0",
"mime-types": "^2.1.30",
"moize": "^6.0.0",
"semver": "^7.3.2"
"node-fetch": "^2.6.1",
"semver": "^7.3.2",
"sharp": "^0.28.1"
},
"devDependencies": {
"@netlify/eslint-config-node": "^2.6.7",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const TEMPLATES_DIR = join(__dirname, 'templates')
// This is the Netlify Function template that wraps all SSR pages
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyFunction.js')

// This is the Netlify Builder template that wraps ISR pages
const BUILDER_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyOnDemandBuilder.js')

// This is the file where custom redirects can be configured
const CUSTOM_REDIRECTS_PATH = join('.', '_redirects')

Expand All @@ -45,6 +48,7 @@ module.exports = {
NEXT_CONFIG_PATH,
TEMPLATES_DIR,
FUNCTION_TEMPLATE_PATH,
BUILDER_TEMPLATE_PATH,
CUSTOM_REDIRECTS_PATH,
CUSTOM_HEADERS_PATH,
NEXT_IMAGE_FUNCTION_NAME,
Expand Down
9 changes: 5 additions & 4 deletions src/lib/helpers/setupNetlifyFunctionForPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ const { join } = require('path')

const { copySync } = require('fs-extra')

const { TEMPLATES_DIR, FUNCTION_TEMPLATE_PATH } = require('../config')
const { TEMPLATES_DIR, FUNCTION_TEMPLATE_PATH, BUILDER_TEMPLATE_PATH } = require('../config')

const copyDynamicImportChunks = require('./copyDynamicImportChunks')
const getNetlifyFunctionName = require('./getNetlifyFunctionName')
const getNextDistDir = require('./getNextDistDir')
const { logItem } = require('./logger')

// Create a Netlify Function for the page with the given file path
const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage }) => {
const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage, isISR }) => {
// Set function name based on file path
const functionName = getNetlifyFunctionName(filePath, isApiPage)
const functionDirectory = join(functionsPath, functionName)
Expand All @@ -21,13 +21,14 @@ const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage

// Copy function templates
const functionTemplateCopyPath = join(functionDirectory, `${functionName}.js`)
copySync(FUNCTION_TEMPLATE_PATH, functionTemplateCopyPath, {
const srcTemplatePath = isISR ? BUILDER_TEMPLATE_PATH : FUNCTION_TEMPLATE_PATH
copySync(srcTemplatePath, functionTemplateCopyPath, {
overwrite: false,
errorOnExist: true,
})

// Copy function helpers
const functionHelpers = ['renderNextPage.js', 'createRequestObject.js', 'createResponseObject.js']
const functionHelpers = ['functionBase.js', 'renderNextPage.js', 'createRequestObject.js', 'createResponseObject.js']
functionHelpers.forEach((helper) => {
copySync(join(TEMPLATES_DIR, helper), join(functionDirectory, helper), {
overwrite: false,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/pages/getStaticPropsWithFallback/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const setup = async (functionsPath) => {
const relativePath = getFilePathForRoute(route, 'js')
const filePath = join('pages', relativePath)
logItem(filePath)
await setupNetlifyFunctionForPage({ filePath, functionsPath })
await setupNetlifyFunctionForPage({ filePath, functionsPath, isISR: true })
})
}

Expand Down
10 changes: 8 additions & 2 deletions src/lib/steps/setupRedirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ const setupRedirects = async (publishPath) => {
const staticRedirects = nextRedirects.filter(({ route }) => !isDynamicRoute(removeFileExtension(route)))
const dynamicRedirects = nextRedirects.filter(({ route }) => isDynamicRoute(removeFileExtension(route)))

// Add next/image redirect to our image function
// Add necessary next/image redirects for our image function
dynamicRedirects.push({
route: '/_next/image* url=:url w=:width q=:quality',
target: `/.netlify/functions/${NEXT_IMAGE_FUNCTION_NAME}?url=:url&w=:width&q=:quality`,
target: `/nextimg/:url/:width/:quality`,
statusCode: '301',
force: true,
})
dynamicRedirects.push({
route: '/nextimg/*',
target: `/.netlify/functions/${NEXT_IMAGE_FUNCTION_NAME}`,
})

const sortedStaticRedirects = getSortedRedirects(staticRedirects)
Expand Down
32 changes: 32 additions & 0 deletions src/lib/templates/functionBase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// TEMPLATE: This file will be copied to the Netlify functions directory when
// running next-on-netlify

// Render function for the Next.js page
const renderNextPage = require('./renderNextPage')

const base = async (event, context, callback) => {
// x-forwarded-host is undefined on Netlify for proxied apps that need it
// fixes https://github.com/netlify/next-on-netlify/issues/46
if (!event.multiValueHeaders.hasOwnProperty('x-forwarded-host')) {
event.multiValueHeaders['x-forwarded-host'] = [event.headers['host']]
}

// Get the request URL
const { path } = event
console.log('[request]', path)

// Render the Next.js page
const response = await renderNextPage({ event, context })

// Convert header values to string. Netlify does not support integers as
// header values. See: https://github.com/netlify/cli/issues/451
Object.keys(response.multiValueHeaders).forEach((key) => {
response.multiValueHeaders[key] = response.multiValueHeaders[key].map((value) => String(value))
})

response.multiValueHeaders['Cache-Control'] = ['no-cache']

callback(null, response)
}

module.exports = base
48 changes: 40 additions & 8 deletions src/lib/templates/imageFunction.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
const jimp = require('jimp')
const path = require('path')
const { builder } = require('@netlify/functions')
const sharp = require('sharp')
const fetch = require('node-fetch')

// Function used to mimic next/image and sharp
exports.handler = async (event) => {
const { url, w = 500, q = 75 } = event.queryStringParameters
const handler = async (event) => {
const [, , url, w = 500, q = 75] = event.path.split('/')
const parsedUrl = decodeURIComponent(url)
const width = parseInt(w)
const quality = parseInt(q)

const imageUrl = url.startsWith('/') ? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${url}` : url
const image = await jimp.read(imageUrl)
const imageUrl = parsedUrl.startsWith('/')
? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${parsedUrl}`
: parsedUrl
const imageData = await fetch(imageUrl)
const bufferData = await imageData.buffer()
const ext = path.extname(imageUrl)
const mimeType = ext === 'jpg' ? `image/jpeg` : `image/${ext}`

image.resize(width, jimp.AUTO).quality(quality)
let image
let imageBuffer

const imageBuffer = await image.getBufferAsync(image.getMIME())
if (mimeType === 'image/gif') {
image = await sharp(bufferData, { animated: true })
// gif resizing in sharp seems unstable (https://github.com/lovell/sharp/issues/2275)
imageBuffer = await image.toBuffer()
} else {
image = await sharp(bufferData)
if (mimeType === 'image/webp') {
image = image.webp({ quality })
} else if (mimeType === 'image/jpeg') {
image = image.jpeg({ quality })
} else if (mimeType === 'image/png') {
image = image.png({ quality })
} else if (mimeType === 'image/avif') {
image = image.avif({ quality })
} else if (mimeType === 'image/tiff') {
image = image.tiff({ quality })
} else if (mimeType === 'image/heif') {
image = image.heif({ quality })
}
imageBuffer = await image.resize(width).toBuffer()
}

return {
statusCode: 200,
headers: {
'Content-Type': image.getMIME(),
'Content-Type': mimeType,
},
body: imageBuffer.toString('base64'),
isBase64Encoded: true,
}
}

exports.handler = builder(handler)
26 changes: 2 additions & 24 deletions src/lib/templates/netlifyFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,6 @@

// Render function for the Next.js page
const renderNextPage = require('./renderNextPage')
const functionBase = require('./functionBase')

exports.handler = async (event, context, callback) => {
// x-forwarded-host is undefined on Netlify for proxied apps that need it
// fixes https://github.com/netlify/next-on-netlify/issues/46
if (!event.multiValueHeaders.hasOwnProperty('x-forwarded-host')) {
event.multiValueHeaders['x-forwarded-host'] = [event.headers['host']]
}

// Get the request URL
const { path } = event
console.log('[request]', path)

// Render the Next.js page
const response = await renderNextPage({ event, context })

// Convert header values to string. Netlify does not support integers as
// header values. See: https://github.com/netlify/cli/issues/451
Object.keys(response.multiValueHeaders).forEach((key) => {
response.multiValueHeaders[key] = response.multiValueHeaders[key].map((value) => String(value))
})

response.multiValueHeaders['Cache-Control'] = ['no-cache']

callback(null, response)
}
exports.handler = functionBase
9 changes: 9 additions & 0 deletions src/lib/templates/netlifyOnDemandBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// TEMPLATE: This file will be copied to the Netlify functions directory when
// running next-on-netlify

// Render on demand builder for the Next.js page
const { builder } = require('@netlify/functions')
const renderNextPage = require('./renderNextPage')
const functionBase = require('./functionBase')

exports.handler = builder(functionBase)
3 changes: 2 additions & 1 deletion src/tests/__snapshots__/defaults.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ exports[`Routing creates Netlify redirects 1`] = `
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/:id.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:width/:quality 301!
/api/shows/:id /.netlify/functions/next_api_shows_id 200
/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
/getServerSideProps/all /.netlify/functions/next_getServerSideProps_all_slug 200
Expand All @@ -56,6 +56,7 @@ exports[`Routing creates Netlify redirects 1`] = `
/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
/getStaticProps/withFallbackBlocking/:id /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
/nextimg/* /.netlify/functions/next_image 200
/shows/:id /.netlify/functions/next_shows_id 200
/shows/:params/* /.netlify/functions/next_shows_params 200
/static/:id /static/[id].html 200"
Expand Down
3 changes: 2 additions & 1 deletion src/tests/__snapshots__/i18n.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ exports[`Routing creates Netlify redirects 1`] = `
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/:id.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:width/:quality 301!
/api/shows/:id /.netlify/functions/next_api_shows_id 200
/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
/en/getServerSideProps/all /.netlify/functions/next_getServerSideProps_all_slug 200
Expand Down Expand Up @@ -122,6 +122,7 @@ exports[`Routing creates Netlify redirects 1`] = `
/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
/getStaticProps/withFallbackBlocking/:id /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
/nextimg/* /.netlify/functions/next_image 200
/shows/:id /.netlify/functions/next_shows_id 200
/shows/:params/* /.netlify/functions/next_shows_params 200
/static/:id /en/static/[id].html 200"
Expand Down
3 changes: 2 additions & 1 deletion src/tests/__snapshots__/optionalCatchAll.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ exports[`Routing creates Netlify redirects 1`] = `
/page /.netlify/functions/next_page 200
/_next/data/%BUILD_ID%/index.json /.netlify/functions/next_all 200
/_next/data/%BUILD_ID%/* /.netlify/functions/next_all 200
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:width/:quality 301!
/nextimg/* /.netlify/functions/next_image 200
/ /.netlify/functions/next_all 200
/_next/* /_next/:splat 200
/* /.netlify/functions/next_all 200"
Expand Down
4 changes: 1 addition & 3 deletions src/tests/preRenderedIndexPages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ describe('Routing', () => {

// Check that no redirects are present
expect(redirects[0]).toEqual('# Next-on-Netlify Redirects')
expect(redirects[1]).toEqual(
'/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200',
)
expect(redirects[1]).toEqual('/_next/image* url=:url w=:width q=:quality /nextimg/:url/:width/:quality 301!')
})
})