From 3eb6d6bbfd97234efb23b3ef63a5c11dc9f94968 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 09:27:31 -0800 Subject: [PATCH 01/29] WIP: Just starting --- next.config.js | 5 +++++ server/index.js | 0 2 files changed, 5 insertions(+) create mode 100644 next.config.js create mode 100644 server/index.js diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..ba6f2a4 --- /dev/null +++ b/next.config.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + target: "serverless" +}; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..e69de29 From b017790bf76762ef56f71fcec2bb7b67f0a0919d Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 13:46:53 -0800 Subject: [PATCH 02/29] Add helper command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d32f970..7798f7b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "private": true, "scripts": { "dev": "next dev", + "clean": "rm -rf .next", "build": "next build", "start": "next start", "env": "echo export STAGE=${STAGE:-localdev}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", From 5290df7ca1b646e262c1d1232c5e89e81599bc8d Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 13:57:08 -0800 Subject: [PATCH 03/29] Start serving serverless target --- server/index.js | 35 +++++++++++++++++++++++++++++++++++ serverless.yml | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/server/index.js b/server/index.js index e69de29..8542d37 100644 --- a/server/index.js +++ b/server/index.js @@ -0,0 +1,35 @@ +"use strict"; + +const { createServer } = require("http"); + +const page = require("../.next/serverless/pages/index.js"); + +const DEFAULT_PORT = 3000; +const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_PORT, 10); +const HOST = process.env.SERVER_HOST || "0.0.0.0"; + +// Create the server app. +const app = createServer((req, res) => page.render(req, res)); + +// TODO: IMPLEMENT +// // LAMBDA: Export handler for lambda use. +// let handler; +// module.exports.handler = (event, context, callback) => { +// // Lazy require `serverless-http` to allow non-Lambda targets to omit. +// // eslint-disable-next-line global-require +// handler = handler || require("serverless-http")(app); +// return handler(event, context, callback); +// }; + +// DOCKER/DEV/ANYTHING: Start the server directly. +if (require.main === module) { + const server = app.listen({ + port: PORT, + host: HOST + }, () => { + const { address, port } = server.address(); + + // eslint-disable-next-line no-console + console.log(`Server started at http://${address}:${port}`); + }); +} diff --git a/serverless.yml b/serverless.yml index 4e6b75c..0c2471f 100644 --- a/serverless.yml +++ b/serverless.yml @@ -47,7 +47,7 @@ provider: functions: # SCENARIO - base: The simplest, vanilla Serverless app. blog: - handler: temp/index.handler # TODO: REPLACE WITH NEXT APP + handler: server/index.js events: # Use a generic proxy to allow Express app to route. - http: ANY /blog - http: 'ANY /blog/{proxy+}' From 939b99775a259e25cf2c126bd4e53c9a1852e433 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 14:52:47 -0800 Subject: [PATCH 04/29] Start the lambda side of things. --- package.json | 1 + server/index.js | 17 +++++++++-------- serverless.yml | 2 +- yarn.lock | 5 +++++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7798f7b..d54c181 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "date-fns": "^2.11.1", "gray-matter": "^4.0.2", "next": "^10.0.0", + "next-aws-lambda": "^2.5.1-alpha.0", "react": "17.0.1", "react-dom": "17.0.1", "remark": "^12.0.0", diff --git a/server/index.js b/server/index.js index 8542d37..cdb9abb 100644 --- a/server/index.js +++ b/server/index.js @@ -12,14 +12,15 @@ const HOST = process.env.SERVER_HOST || "0.0.0.0"; const app = createServer((req, res) => page.render(req, res)); // TODO: IMPLEMENT -// // LAMBDA: Export handler for lambda use. -// let handler; -// module.exports.handler = (event, context, callback) => { -// // Lazy require `serverless-http` to allow non-Lambda targets to omit. -// // eslint-disable-next-line global-require -// handler = handler || require("serverless-http")(app); -// return handler(event, context, callback); -// }; +// LAMBDA: Export handler for lambda use. +let handler; +module.exports.handler = async (event, context) => { + // Lazy require to allow non-Lambda targets to omit. + // eslint-disable-next-line global-require + const nextToLambda = require("next-aws-lambda"); + + return nextToLambda(page)(event, context); +}; // DOCKER/DEV/ANYTHING: Start the server directly. if (require.main === module) { diff --git a/serverless.yml b/serverless.yml index 0c2471f..6c5ed7d 100644 --- a/serverless.yml +++ b/serverless.yml @@ -47,7 +47,7 @@ provider: functions: # SCENARIO - base: The simplest, vanilla Serverless app. blog: - handler: server/index.js + handler: server/index.handler events: # Use a generic proxy to allow Express app to route. - http: ANY /blog - http: 'ANY /blog/{proxy+}' diff --git a/yarn.lock b/yarn.lock index 7b68e50..da0108b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5737,6 +5737,11 @@ nested-error-stacks@^2.0.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== +next-aws-lambda@^2.5.1-alpha.0: + version "2.5.1-alpha.0" + resolved "https://registry.yarnpkg.com/next-aws-lambda/-/next-aws-lambda-2.5.1-alpha.0.tgz#e9ea715c732e56d26727d9b41ba0492ac3d6a746" + integrity sha512-kQ7aBnG2LMbmlISvOwg6UiYX0l1yEH/bOwu5rH5BuXb99ar5nmX2LIX90LrrsYp46jlG8tkZgKGCWwiSJEWQVA== + next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" From 40127860156b7517c44211a46b6223c3a1a29a93 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 15:01:52 -0800 Subject: [PATCH 05/29] WIP: No routing, but see if we can package and ship. --- serverless.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/serverless.yml b/serverless.yml index 6c5ed7d..f5d80f4 100644 --- a/serverless.yml +++ b/serverless.yml @@ -12,6 +12,17 @@ custom: # Use trace mode for speed and ignore aws-sdk + all deps ignores: - "aws-sdk" + - "critters" + allowMissing: + # TODO: Add to trace-dep + # "./.next/serverless/pages/index.js": + # - "critters" # For CSS optimization + "node-fetch": + - "encoding" + # TODO: RESOLUTIONS + # dynamic: + # resolutions: + # - node_modules/@ampproject/toolbox-optimizer/lib/DomTransformer.js [169:22]: require(`./transformers/${Transformer}.js`) plugins: - serverless-jetpack From 0b761ced87b7231fb97aa7c726321f78ffd9a537 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 15:43:44 -0800 Subject: [PATCH 06/29] Make async add to config --- lib/posts.js | 19 +++++++++++-------- pages/index.js | 2 +- pages/posts/[id].js | 2 +- serverless.yml | 10 +++++++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/posts.js b/lib/posts.js index 53d8653..b4f7dd3 100644 --- a/lib/posts.js +++ b/lib/posts.js @@ -1,21 +1,24 @@ import fs from 'fs' +import { promisify } from 'util' import path from 'path' import matter from 'gray-matter' import remark from 'remark' import html from 'remark-html' +const readdir = promisify(fs.readdir); +const readFile = promisify(fs.readFile); const postsDirectory = path.join(process.cwd(), 'posts') -export function getSortedPostsData() { +export async function getSortedPostsData() { // Get file names under /posts - const fileNames = fs.readdirSync(postsDirectory) - const allPostsData = fileNames.map(fileName => { + const fileNames = await readdir(postsDirectory) + const allPostsData = await Promise.all(fileNames.map(async fileName => { // Remove ".md" from file name to get id const id = fileName.replace(/\.md$/, '') // Read markdown file as string const fullPath = path.join(postsDirectory, fileName) - const fileContents = fs.readFileSync(fullPath, 'utf8') + const fileContents = await readFile(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) @@ -25,7 +28,7 @@ export function getSortedPostsData() { id, ...matterResult.data } - }) + })) // Sort posts by date return allPostsData.sort((a, b) => { if (a.date < b.date) { @@ -36,8 +39,8 @@ export function getSortedPostsData() { }) } -export function getAllPostIds() { - const fileNames = fs.readdirSync(postsDirectory) +export async function getAllPostIds() { + const fileNames = await readdir(postsDirectory) return fileNames.map(fileName => { return { params: { @@ -49,7 +52,7 @@ export function getAllPostIds() { export async function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) - const fileContents = fs.readFileSync(fullPath, 'utf8') + const fileContents = await readFile(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) diff --git a/pages/index.js b/pages/index.js index f09aed3..2828c4d 100644 --- a/pages/index.js +++ b/pages/index.js @@ -39,7 +39,7 @@ export default function Home({ allPostsData }) { } export async function getStaticProps() { - const allPostsData = getSortedPostsData() + const allPostsData = await getSortedPostsData() return { props: { allPostsData diff --git a/pages/posts/[id].js b/pages/posts/[id].js index 28faaad..cebdf19 100644 --- a/pages/posts/[id].js +++ b/pages/posts/[id].js @@ -22,7 +22,7 @@ export default function Post({ postData }) { } export async function getStaticPaths() { - const paths = getAllPostIds() + const paths = await getAllPostIds() return { paths, fallback: false diff --git a/serverless.yml b/serverless.yml index f5d80f4..f7ff8a8 100644 --- a/serverless.yml +++ b/serverless.yml @@ -20,9 +20,10 @@ custom: "node-fetch": - "encoding" # TODO: RESOLUTIONS - # dynamic: - # resolutions: - # - node_modules/@ampproject/toolbox-optimizer/lib/DomTransformer.js [169:22]: require(`./transformers/${Transformer}.js`) + dynamic: + resolutions: + # [169:22]: require(`./transformers/${Transformer}.js`) + "@ampproject/toolbox-optimizer/lib/DomTransformer.js": [] plugins: - serverless-jetpack @@ -62,3 +63,6 @@ functions: events: # Use a generic proxy to allow Express app to route. - http: ANY /blog - http: 'ANY /blog/{proxy+}' + package: + include: + - "posts/**/*.md" From 7dfbee70405e596de64b70f6c2e174b226d19b8e Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 15:57:06 -0800 Subject: [PATCH 07/29] WIP: Get basic version working in Lambda --- serverless.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/serverless.yml b/serverless.yml index f7ff8a8..61de2d0 100644 --- a/serverless.yml +++ b/serverless.yml @@ -1,6 +1,9 @@ # CloudFormation output name: `sls-${SERVICE_NAME}-${STAGE}` service: sls-${self:custom.service} +package: + individually: true + custom: service: ${env:SERVICE_NAME} region: ${opt:region, env:AWS_REGION} @@ -65,4 +68,5 @@ functions: - http: 'ANY /blog/{proxy+}' package: include: + # Raw data for posts is read from disk outside `.next` build directory. - "posts/**/*.md" From 1f816ace119d8e0a7f2e7eea699e1c1d31308f58 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Wed, 27 Jan 2021 22:07:55 -0800 Subject: [PATCH 08/29] WIP: Try out express for static. --- next.config.js | 8 +- package.json | 4 +- server/index.js | 1 + serverless.yml | 3 + yarn.lock | 293 +++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 292 insertions(+), 17 deletions(-) diff --git a/next.config.js b/next.config.js index ba6f2a4..2ce8861 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,11 @@ "use strict"; module.exports = { - target: "serverless" + target: "serverless", + webpack: (config) => { + // Add more information in the bundle. + config.output.pathinfo = true; + + return config; + }, }; diff --git a/package.json b/package.json index d54c181..6551e36 100644 --- a/package.json +++ b/package.json @@ -41,13 +41,15 @@ }, "dependencies": { "date-fns": "^2.11.1", + "express": "^4.17.1", "gray-matter": "^4.0.2", "next": "^10.0.0", "next-aws-lambda": "^2.5.1-alpha.0", "react": "17.0.1", "react-dom": "17.0.1", "remark": "^12.0.0", - "remark-html": "^12.0.0" + "remark-html": "^12.0.0", + "serverless-http": "^2.7.0" }, "devDependencies": { "serverless": "^2.21.1", diff --git a/server/index.js b/server/index.js index cdb9abb..1f8d72f 100644 --- a/server/index.js +++ b/server/index.js @@ -1,5 +1,6 @@ "use strict"; +// TODO: HERE -- Refactor to express with static. const { createServer } = require("http"); const page = require("../.next/serverless/pages/index.js"); diff --git a/serverless.yml b/serverless.yml index 61de2d0..e309317 100644 --- a/serverless.yml +++ b/serverless.yml @@ -70,3 +70,6 @@ functions: include: # Raw data for posts is read from disk outside `.next` build directory. - "posts/**/*.md" + # Static assets. + # TODO(STATIC): Add a note about this. + - ".next/static/**" diff --git a/yarn.lock b/yarn.lock index da0108b..f138c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -825,6 +825,11 @@ request "^2.88.0" request-promise-native "^1.0.8" +"@types/aws-lambda@^8.10.56": + version "8.10.71" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.71.tgz#ab3084038411ce42f63b975e67aafb163f3aa353" + integrity sha512-l0Lag6qq06AlKllprAJ3pbgVUbXCjRGRb7VpHow8IMn2BMHTPR0t5OD97/w8CR1+wA5XZuWQoXLjYvdlk2kQrQ== + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -1098,6 +1103,14 @@ abort-controller@3.0.0: dependencies: event-target-shim "^5.0.0" +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-node@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-2.0.1.tgz#4a93ba32335950da9250175c654721f20f3375a7" @@ -1345,6 +1358,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array-flatten@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-3.0.0.tgz#6428ca2ee52c7b823192ec600fa3ed2f157cd541" @@ -1624,6 +1642,22 @@ bn.js@^5.0.0, bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + boxen@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" @@ -2400,13 +2434,18 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -content-disposition@^0.5.2: +content-disposition@0.5.3, content-disposition@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== dependencies: safe-buffer "5.1.2" +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2419,6 +2458,16 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + cookiejar@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" @@ -2659,6 +2708,13 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== +debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -2666,13 +2722,6 @@ debug@4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" -debug@^2.1.3, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.1.0, debug@^3.1.1: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2849,6 +2898,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -3033,6 +3087,11 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + electron-to-chromium@^1.3.585: version "1.3.645" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.645.tgz#c0b269ae2ecece5aedc02dd4586397d8096affb1" @@ -3078,6 +3137,11 @@ enabled@1.0.x: dependencies: env-variable "0.0.x" +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -3238,6 +3302,11 @@ escape-goat@^2.0.0: resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3291,7 +3360,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@1.8.1: +etag@1.8.1, etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= @@ -3370,6 +3439,42 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + ext-list@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" @@ -3593,6 +3698,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-cache-dir@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" @@ -3685,6 +3803,11 @@ formidable@^1.2.0: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3692,6 +3815,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + from2@^2.1.0, from2@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -4201,7 +4329,18 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@1.7.3: +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@1.7.3, http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -4419,6 +4558,11 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -5403,6 +5547,11 @@ mdurl@^1.0.0: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + mem@^6.0.1: version "6.1.1" resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.1.tgz#ea110c2ebc079eca3022e6b08c85a795e77f6318" @@ -5441,6 +5590,11 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5451,7 +5605,7 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1: +methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -5496,14 +5650,14 @@ mime-db@1.45.0, mime-db@1.x.x, mime-db@^1.28.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.28" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== dependencies: mime-db "1.45.0" -mime@^1.4.1: +mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -5645,6 +5799,11 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -5727,6 +5886,11 @@ ncjsm@^4.1.0: fs2 "^0.3.8" type "^2.0.0" +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -6011,6 +6175,13 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -6237,6 +6408,11 @@ parseuri@0.0.6: resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -6295,6 +6471,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6616,6 +6797,14 @@ protobufjs@^6.9.0: "@types/node" "^13.7.0" long "^4.0.0" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -6698,6 +6887,11 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@^6.5.1: version "6.9.6" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" @@ -6762,6 +6956,21 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + raw-body@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -7272,6 +7481,25 @@ semver@^7.1.3, semver@^7.3.2, semver@^7.3.4: dependencies: lru-cache "^6.0.0" +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -7279,6 +7507,23 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +serverless-http@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/serverless-http/-/serverless-http-2.7.0.tgz#352ca38cbbba58dc71dcfb11bb27c92c3c81fe69" + integrity sha512-iWq0z1X2Xkuvz6wL305uCux/SypbojHlYsB5bzmF5TqoLYsdvMNIoCsgtWjwqWoo3AR2cjw3zAmHN2+U6mF99Q== + optionalDependencies: + "@types/aws-lambda" "^8.10.56" + serverless-jetpack@^0.10.7: version "0.10.7" resolved "https://registry.yarnpkg.com/serverless-jetpack/-/serverless-jetpack-0.10.7.tgz#2eadba6b66807224bf05e447247bbdea6bc2a7c8" @@ -7757,7 +8002,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.5.0 < 2": +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -8356,6 +8601,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -8503,7 +8756,7 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= @@ -8628,6 +8881,11 @@ util@^0.11.0: dependencies: inherits "2.0.3" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -8643,6 +8901,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + velocityjs@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/velocityjs/-/velocityjs-2.0.3.tgz#cc772f687061997127b7d8a827dbef3af8a0bbe6" From cface69a895439b163ea13e1986aa30b9aed7563 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 13:38:06 -0800 Subject: [PATCH 09/29] Lots of work. Just starting to see if can work. --- next.config.js | 23 +++++++-- package.json | 6 +-- server/index.js | 25 +++++++--- server/util.js | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ serverless.yml | 20 ++++++-- yarn.lock | 10 ++-- 6 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 server/util.js diff --git a/next.config.js b/next.config.js index 2ce8861..ab0de98 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,27 @@ "use strict"; +const { nextExternals } = require("./server/util"); + module.exports = { target: "serverless", - webpack: (config) => { - // Add more information in the bundle. - config.output.pathinfo = true; + webpack: (config, { isServer }) => { + if (isServer) { + // Add more information in the bundle. + config.output.pathinfo = true; + + // Keep `node_modules` as runtime requires to help slim down page bundles. + config.externals = (config.externals || []).concat( + nextExternals() + // TODO: REMOVE? + // nodeExternals({ + // allowlist: [ + // "next-plugin-loader?middleware=document-head-tags-server!", + // /^next-plugin-loader.*/ + // ] + // }) + ) + } + return config; }, diff --git a/package.json b/package.json index 6551e36..2fdf3be 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dev": "next dev", "clean": "rm -rf .next", "build": "next build", - "start": "next start", + "start": "node server/index.js", "env": "echo export STAGE=${STAGE:-localdev}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", "cf:_params": "eval $(yarn -s env) && echo --parameters ParameterKey=Stage,ParameterValue=${STAGE} ParameterKey=ServiceName,ParameterValue=${SERVICE_NAME}", "cf:bootstrap:_stack": "eval $(yarn -s env) && echo --region ${AWS_REGION} --stack-name cf-${SERVICE_NAME}-${STAGE}-bootstrap", @@ -44,7 +44,6 @@ "express": "^4.17.1", "gray-matter": "^4.0.2", "next": "^10.0.0", - "next-aws-lambda": "^2.5.1-alpha.0", "react": "17.0.1", "react-dom": "17.0.1", "remark": "^12.0.0", @@ -54,6 +53,7 @@ "devDependencies": { "serverless": "^2.21.1", "serverless-jetpack": "^0.10.7", - "serverless-offline": "^6.8.0" + "serverless-offline": "^6.8.0", + "webpack-node-externals": "^2.5.2" } } diff --git a/server/index.js b/server/index.js index 1f8d72f..63a0c42 100644 --- a/server/index.js +++ b/server/index.js @@ -1,31 +1,42 @@ "use strict"; // TODO: HERE -- Refactor to express with static. -const { createServer } = require("http"); +const express = require("express"); const page = require("../.next/serverless/pages/index.js"); const DEFAULT_PORT = 3000; const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_PORT, 10); const HOST = process.env.SERVER_HOST || "0.0.0.0"; +const BASE_PATH = process.env.BASE_PATH || "/blog"; +const STAGE = process.env.STAGE || "localdev"; // Create the server app. -const app = createServer((req, res) => page.render(req, res)); +const getApp = ({ basePath = "" } = {}) => { + const app = express(); + app.all(`${basePath}`, (req, res) => { + console.log("TODO HERE REQ", req.url) + return page.render(req, res); + }); + + return app; +}; -// TODO: IMPLEMENT // LAMBDA: Export handler for lambda use. let handler; module.exports.handler = async (event, context) => { - // Lazy require to allow non-Lambda targets to omit. + // Lazy require `serverless-http` to allow non-Lambda targets to omit. // eslint-disable-next-line global-require - const nextToLambda = require("next-aws-lambda"); + handler = handler || require("serverless-http")(getApp({ + basePath: BASE_PATH + })); - return nextToLambda(page)(event, context); + return handler(event, context); }; // DOCKER/DEV/ANYTHING: Start the server directly. if (require.main === module) { - const server = app.listen({ + const server = getApp().listen({ port: PORT, host: HOST }, () => { diff --git a/server/util.js b/server/util.js new file mode 100644 index 0000000..381c3d2 --- /dev/null +++ b/server/util.js @@ -0,0 +1,125 @@ +"use strict"; + +const path = require("path"); + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- +// +// TODO: These are cribbed/tweaked from `trace-deps`. Should abstract / test / something. +// Simple conversion to produce Linux/Mac style forward slash-based paths. +const toPosix = (file) => !file ? file : file.replace(/\\/g, "/"); + +// Extract top-level package name + relative file path from full path. +// +// E.g., `["pkg2", "index.js"]` => +// { +// name: "pkg2", +// file: pkg2/index.js +// } +const getPackageFromParts = (parts = []) => { + // Get first part of package. + const firstPart = parts[0]; + if (!firstPart) { return null; } + + // Default to unscoped. + let name = firstPart; + if (firstPart[0] === "@") { + // Detect if scoped and adjust / short-circuit if no match. + const secondPart = parts[1]; // eslint-disable-line no-magic-numbers + if (!secondPart) { return null; } + + // Use posix path. + name = [firstPart, secondPart].join("/"); + } + + return { + name, + file: parts.join(path.sep) + }; +}; + +const getPackageFromContext = (context = "") => { + const parts = toPosix(path.normalize(context)).split("/"); + const nodeModulesIdx = parts.lastIndexOf("node_modules"); + if (nodeModulesIdx === -1) { return null; } + + return getPackageFromParts(parts.slice(nodeModulesIdx + 1)); +}; + +const REQUEST_SKIP_LIST = [ + // Loaders and synthetic files. + /^next-serverless-loader(\?|\/|$)/, + /^next-plugin-loader(\?|\/|$)/, + /^private-dot-next(\?|\/|$)/, + /^private-next-pages(\?|\/|$)/, + + // This doesn't have an export and just patches Node.js `global`. + "next/dist/next-server/server/node-polyfill-fetch" + // "next/dist/build/webpack/loaders/next-serverless-loader/page-handler" +]; +const getPackageFromRequest = (request = "") => { + // Must start with a valid package prefix. Notably, not: + // - Relative: `./foo.js` + // - Absolute: `/PATH/TO/foo.js` + if (!(/^[\@a-zA-Z]+/).test(request)) { + return null; + } + + // Skip Next.js loaders and things that need to be built in. + if (REQUEST_SKIP_LIST.some( + (strOrRe) => typeof strOrRe === "string" ? request.startsWith(strOrRe) : strOrRe.test(request) + )) { + return null; + } + + const parts = toPosix(path.normalize(request)).split("/"); + return getPackageFromParts(parts); +}; + +// ---------------------------------------------------------------------------- +// Exports +// ---------------------------------------------------------------------------- + +// Exclude all of node_modules with certain exceptions for Next.js +// common usage. +// +// Modeled after: https://github.com/liady/webpack-node-externals +// See: https://webpack.js.org/configuration/externals/#function +const nextExternals = () => (...args) => { + // Handle all versions of webpack externals function signature. + const isWebpack5 = !!(args[0] && args[0].context && args[0].request); + const context = isWebpack5 ? args[0].context : args[0]; + const request = isWebpack5 ? args[0].request : args[1]; + const callback = args[isWebpack5 ? 3 : 2]; + + // Somewhat different from `webpack-node-externals` we use `context` to + // find things in `node_modules` that we should exclude in addition to + // the module name itself. And we don't actually scan `node_modules`. + const requestPkg = getPackageFromRequest(request); + const contextPkg = getPackageFromContext(context); + const externalName = requestPkg ? requestPkg.file : null; + console.log("TODO HERE EXTERNAL", { + request, + requestPkg, + // TODO: IGNORE CONTEXT? + context, + contextPkg, + externalName + }); + if (externalName !== null) { + return void callback(null, `commonjs ${externalName}`); + } + + // if (pkgName && !["next", "@next/env"].includes(pkgName)) { + // console.log("TODO HERE EXTERNAL", { pkgName }) + // return void callback(null, `commonjs ${pkgName}`); + // } + + callback(); +}; + + +module.exports = { + nextExternals +}; diff --git a/serverless.yml b/serverless.yml index e309317..0e6f73b 100644 --- a/serverless.yml +++ b/serverless.yml @@ -12,16 +12,28 @@ custom: preInclude: - "!**" # Start with no files at all. trace: - # Use trace mode for speed and ignore aws-sdk + all deps ignores: + # Provided on Lambda - "aws-sdk" + + # TODO(trace-deps): Remove once in trace-deps - "critters" + + # Ignore real deps in Next.js that we shouldn't need at runtime. + - "webpack" + - "sass-loader" + - "pnp-webpack-plugin" + allowMissing: - # TODO: Add to trace-dep + # TODO(trace-deps): Add to trace-deps # "./.next/serverless/pages/index.js": # - "critters" # For CSS optimization "node-fetch": - "encoding" + "next": + - "critters" # for CSS optimization + - "pnpapi" # for PnP usage + # TODO: RESOLUTIONS dynamic: resolutions: @@ -70,6 +82,8 @@ functions: include: # Raw data for posts is read from disk outside `.next` build directory. - "posts/**/*.md" - # Static assets. + # Needed built Next.js assets and info. (Some of these are also traced). + - ".next/{BUILD_ID,*.json}" + - ".next/serverless/**" # TODO(STATIC): Add a note about this. - ".next/static/**" diff --git a/yarn.lock b/yarn.lock index f138c99..443e36b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5901,11 +5901,6 @@ nested-error-stacks@^2.0.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== -next-aws-lambda@^2.5.1-alpha.0: - version "2.5.1-alpha.0" - resolved "https://registry.yarnpkg.com/next-aws-lambda/-/next-aws-lambda-2.5.1-alpha.0.tgz#e9ea715c732e56d26727d9b41ba0492ac3d6a746" - integrity sha512-kQ7aBnG2LMbmlISvOwg6UiYX0l1yEH/bOwu5rH5BuXb99ar5nmX2LIX90LrrsYp46jlG8tkZgKGCWwiSJEWQVA== - next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -8979,6 +8974,11 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +webpack-node-externals@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz#178e017a24fec6015bc9e672c77958a6afac861d" + integrity sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w== + webpack-sources@1.4.3, webpack-sources@^1.4.0, webpack-sources@^1.4.1: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" From 3f0f9c33822df1b0b7ea0ac0c2e2b6bc4a6dff10 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:09:33 -0800 Subject: [PATCH 10/29] Data works --- server/index.js | 51 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/server/index.js b/server/index.js index 63a0c42..7cffa41 100644 --- a/server/index.js +++ b/server/index.js @@ -1,6 +1,7 @@ "use strict"; -// TODO: HERE -- Refactor to express with static. +const fs = require("fs").promises; +const path = require("path"); const express = require("express"); const page = require("../.next/serverless/pages/index.js"); @@ -11,14 +12,36 @@ const HOST = process.env.SERVER_HOST || "0.0.0.0"; const BASE_PATH = process.env.BASE_PATH || "/blog"; const STAGE = process.env.STAGE || "localdev"; +const NEXT_DIR = path.resolve(__dirname, "../.next"); +const NEXT_APP_ROOT = "/_next"; + // Create the server app. -const getApp = ({ basePath = "" } = {}) => { +const getApp = async ({ basePath = "" } = {}) => { + const buildId = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); const app = express(); + + // NOTE(STATIC): For this demo only, we just handle serve static content + // directly through the express app. For a real app, you'll want to deploy + // static contents to somewhere to be directly served by the CDN. + app.use(`${NEXT_APP_ROOT}/static`, express.static(path.join(NEXT_DIR, "static"))); + + // Proxy data requests. + // _next/data/y-BRZHyY6b_T25zMSRPY0/posts/ssg-ssr.json -> + // _next/serverless/posts/ssg-ssr.json + app.use( + `${NEXT_APP_ROOT}/data/${buildId}`, + express.static(path.join(NEXT_DIR, "serverless/pages")) + ); + + // Page handlers, app.all(`${basePath}`, (req, res) => { - console.log("TODO HERE REQ", req.url) + console.log("TODO HERE REQ", req.url); return page.render(req, res); }); + // TODO: 404. + // TODO: Hook up error (?) + return app; }; @@ -27,7 +50,7 @@ let handler; module.exports.handler = async (event, context) => { // Lazy require `serverless-http` to allow non-Lambda targets to omit. // eslint-disable-next-line global-require - handler = handler || require("serverless-http")(getApp({ + handler = handler || require("serverless-http")(await getApp({ basePath: BASE_PATH })); @@ -36,13 +59,15 @@ module.exports.handler = async (event, context) => { // DOCKER/DEV/ANYTHING: Start the server directly. if (require.main === module) { - const server = getApp().listen({ - port: PORT, - host: HOST - }, () => { - const { address, port } = server.address(); - - // eslint-disable-next-line no-console - console.log(`Server started at http://${address}:${port}`); - }); + (async () => { + const server = (await getApp()).listen({ + port: PORT, + host: HOST + }, () => { + const { address, port } = server.address(); + + // eslint-disable-next-line no-console + console.log(`Server started at http://${address}:${port}`); + }); + })(); } From e376d7598113f839d860f22572a8efe735c449d2 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:29:32 -0800 Subject: [PATCH 11/29] Handle data. Yay. --- server/index.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/server/index.js b/server/index.js index 7cffa41..60ca8e2 100644 --- a/server/index.js +++ b/server/index.js @@ -12,12 +12,15 @@ const HOST = process.env.SERVER_HOST || "0.0.0.0"; const BASE_PATH = process.env.BASE_PATH || "/blog"; const STAGE = process.env.STAGE || "localdev"; -const NEXT_DIR = path.resolve(__dirname, "../.next"); -const NEXT_APP_ROOT = "/_next"; - // Create the server app. const getApp = async ({ basePath = "" } = {}) => { - const buildId = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); + const NEXT_DIR = path.resolve(__dirname, "../.next"); + const NEXT_DATA_DIR = path.resolve(NEXT_DIR, "serverless/pages"); + const NEXT_APP_ROOT = "/_next"; + + const BUILD_ID = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); + const NEXT_DATA_ROOT = `${NEXT_APP_ROOT}/data/${BUILD_ID}`; + const app = express(); // NOTE(STATIC): For this demo only, we just handle serve static content @@ -25,13 +28,22 @@ const getApp = async ({ basePath = "" } = {}) => { // static contents to somewhere to be directly served by the CDN. app.use(`${NEXT_APP_ROOT}/static`, express.static(path.join(NEXT_DIR, "static"))); - // Proxy data requests. + // Manually proxy JSON data requests to file system. // _next/data/y-BRZHyY6b_T25zMSRPY0/posts/ssg-ssr.json -> // _next/serverless/posts/ssg-ssr.json - app.use( - `${NEXT_APP_ROOT}/data/${buildId}`, - express.static(path.join(NEXT_DIR, "serverless/pages")) - ); + // + // NOTE(STATIC): This _also_ could be uploaded to a real static serve. + // It technically _could_ change from data, so possibly disable SSG and + // make this always dynamically generated. + app.get(`${NEXT_DATA_ROOT}/*`, async (req, res, next) => { + // Only handle JSON. + if (req.url.endsWith(".json")) { + const filePath = req.url.replace(NEXT_DATA_ROOT, NEXT_DATA_DIR); + return res.sendFile(filePath); + } + + return next(); + }); // Page handlers, app.all(`${basePath}`, (req, res) => { From a9dae8e6c405f967cbeddee4ae9e2ccfc5d8d57f Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:35:30 -0800 Subject: [PATCH 12/29] Various cleanup. Client side render works now in dev server. --- server/index.js | 12 +++++++----- server/util.js | 11 ++--------- serverless.yml | 1 + 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/server/index.js b/server/index.js index 60ca8e2..c9d2517 100644 --- a/server/index.js +++ b/server/index.js @@ -16,6 +16,7 @@ const STAGE = process.env.STAGE || "localdev"; const getApp = async ({ basePath = "" } = {}) => { const NEXT_DIR = path.resolve(__dirname, "../.next"); const NEXT_DATA_DIR = path.resolve(NEXT_DIR, "serverless/pages"); + const NEXT_PUBLIC_DIR = path.resolve(__dirname, "../public"); const NEXT_APP_ROOT = "/_next"; const BUILD_ID = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); @@ -35,7 +36,7 @@ const getApp = async ({ basePath = "" } = {}) => { // NOTE(STATIC): This _also_ could be uploaded to a real static serve. // It technically _could_ change from data, so possibly disable SSG and // make this always dynamically generated. - app.get(`${NEXT_DATA_ROOT}/*`, async (req, res, next) => { + app.get(`${NEXT_DATA_ROOT}/*`, (req, res, next) => { // Only handle JSON. if (req.url.endsWith(".json")) { const filePath = req.url.replace(NEXT_DATA_ROOT, NEXT_DATA_DIR); @@ -46,10 +47,11 @@ const getApp = async ({ basePath = "" } = {}) => { }); // Page handlers, - app.all(`${basePath}`, (req, res) => { - console.log("TODO HERE REQ", req.url); - return page.render(req, res); - }); + // TODO(ROUTING): Need all the pages and routing. + app.all(`${basePath}`, (req, res) => page.render(req, res)); + + // NOTE(STATIC): User-added static assets. Should not be in Lambda. + app.use("/", express.static(NEXT_PUBLIC_DIR)); // TODO: 404. // TODO: Hook up error (?) diff --git a/server/util.js b/server/util.js index 381c3d2..a31173a 100644 --- a/server/util.js +++ b/server/util.js @@ -7,6 +7,7 @@ const path = require("path"); // ---------------------------------------------------------------------------- // // TODO: These are cribbed/tweaked from `trace-deps`. Should abstract / test / something. + // Simple conversion to produce Linux/Mac style forward slash-based paths. const toPosix = (file) => !file ? file : file.replace(/\\/g, "/"); @@ -39,6 +40,7 @@ const getPackageFromParts = (parts = []) => { }; }; +// NOTE: Unused for now. In case we want context parsing, this is useful. const getPackageFromContext = (context = "") => { const parts = toPosix(path.normalize(context)).split("/"); const nodeModulesIdx = parts.lastIndexOf("node_modules"); @@ -97,16 +99,7 @@ const nextExternals = () => (...args) => { // find things in `node_modules` that we should exclude in addition to // the module name itself. And we don't actually scan `node_modules`. const requestPkg = getPackageFromRequest(request); - const contextPkg = getPackageFromContext(context); const externalName = requestPkg ? requestPkg.file : null; - console.log("TODO HERE EXTERNAL", { - request, - requestPkg, - // TODO: IGNORE CONTEXT? - context, - contextPkg, - externalName - }); if (externalName !== null) { return void callback(null, `commonjs ${externalName}`); } diff --git a/serverless.yml b/serverless.yml index 0e6f73b..128b176 100644 --- a/serverless.yml +++ b/serverless.yml @@ -87,3 +87,4 @@ functions: - ".next/serverless/**" # TODO(STATIC): Add a note about this. - ".next/static/**" + - "public/**" From 22ea587141fcd12c45b6f4b4491fd56649782676 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:48:39 -0800 Subject: [PATCH 13/29] Add some notes --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2eb5b28..46d867a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ and is based on the following projects: - [nextjs-fargate-demo](https://github.com/FormidableLabs/nextjs-fargate-demo): We deploy the same Next.js application. - [aws-lambda-serverless-reference][]: The CloudFormation/Terraform infrastructure approach is basically identical to our reference Serverless project. +The main goals of this demo project are as follows: + +1. **Slim down a Next.js Lambda deployment**: The Next.js `target: "serverless"` Node.js outputs are huge. Like really, really big because **each page** contains **all the dependencies**. This project adds a custom externals handler to filter out almost all dependencies in `node_modules` and leave those as normal `require()` calls, thus dramatically decreasing the `pages` bundle sizes. The `node_modules` dependencies are included via `serverless-jetpack` trace mode to keep things tight. +2. **Single Lambda/APIGW proxy**: `TODO(ROUTING): INSERT_NOTES` + ## Local development Start with: From 798ac9d85ec58d99040b93a21bf0ef99f7510dba Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:50:29 -0800 Subject: [PATCH 14/29] Add more notes --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 46d867a..06ea3c0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ The main goals of this demo project are as follows: 1. **Slim down a Next.js Lambda deployment**: The Next.js `target: "serverless"` Node.js outputs are huge. Like really, really big because **each page** contains **all the dependencies**. This project adds a custom externals handler to filter out almost all dependencies in `node_modules` and leave those as normal `require()` calls, thus dramatically decreasing the `pages` bundle sizes. The `node_modules` dependencies are included via `serverless-jetpack` trace mode to keep things tight. 2. **Single Lambda/APIGW proxy**: `TODO(ROUTING): INSERT_NOTES` +Some caveats: + +1. **Static files**: To make this demo a whole lot easier to develop/deploy, we handle serve static assets _from_ the Lambda. This is not what you should do for a real application. Typically, you'll want to stick those assets in an S3 bucket behind a CDN or something. Look for the `TODO(STATIC)` comments variously throughout this repository to see all the shortcuts you should unwind to then reconfigure for static assets "the right way". + ## Local development Start with: From ef611636651d55502e8c75ca24c3a3aa92a3f4b7 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 14:59:12 -0800 Subject: [PATCH 15/29] Clean up --- next.config.js | 7 ------- package.json | 3 +-- server/util.js | 11 +++++------ yarn.lock | 5 ----- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/next.config.js b/next.config.js index ab0de98..9d5be30 100644 --- a/next.config.js +++ b/next.config.js @@ -12,13 +12,6 @@ module.exports = { // Keep `node_modules` as runtime requires to help slim down page bundles. config.externals = (config.externals || []).concat( nextExternals() - // TODO: REMOVE? - // nodeExternals({ - // allowlist: [ - // "next-plugin-loader?middleware=document-head-tags-server!", - // /^next-plugin-loader.*/ - // ] - // }) ) } diff --git a/package.json b/package.json index 2fdf3be..5368f41 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "devDependencies": { "serverless": "^2.21.1", "serverless-jetpack": "^0.10.7", - "serverless-offline": "^6.8.0", - "webpack-node-externals": "^2.5.2" + "serverless-offline": "^6.8.0" } } diff --git a/server/util.js b/server/util.js index a31173a..0d92509 100644 --- a/server/util.js +++ b/server/util.js @@ -58,7 +58,6 @@ const REQUEST_SKIP_LIST = [ // This doesn't have an export and just patches Node.js `global`. "next/dist/next-server/server/node-polyfill-fetch" - // "next/dist/build/webpack/loaders/next-serverless-loader/page-handler" ]; const getPackageFromRequest = (request = "") => { // Must start with a valid package prefix. Notably, not: @@ -88,6 +87,11 @@ const getPackageFromRequest = (request = "") => { // // Modeled after: https://github.com/liady/webpack-node-externals // See: https://webpack.js.org/configuration/externals/#function +// +// Notes: +// 1. **WARNING**: This is an incomplete solution for now. The list in +// REQUEST_SKIP_LIST need to be either generalized to "all webpack +// loaders" and/or configuration overrides need to be passed in as options. const nextExternals = () => (...args) => { // Handle all versions of webpack externals function signature. const isWebpack5 = !!(args[0] && args[0].context && args[0].request); @@ -104,11 +108,6 @@ const nextExternals = () => (...args) => { return void callback(null, `commonjs ${externalName}`); } - // if (pkgName && !["next", "@next/env"].includes(pkgName)) { - // console.log("TODO HERE EXTERNAL", { pkgName }) - // return void callback(null, `commonjs ${pkgName}`); - // } - callback(); }; diff --git a/yarn.lock b/yarn.lock index 443e36b..d942f81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8974,11 +8974,6 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-node-externals@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz#178e017a24fec6015bc9e672c77958a6afac861d" - integrity sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w== - webpack-sources@1.4.3, webpack-sources@^1.4.0, webpack-sources@^1.4.1: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" From d74b5b622da033def176dc7ae2fdd163c38f6dd8 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 15:14:38 -0800 Subject: [PATCH 16/29] Fix sls packaging. --- serverless.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/serverless.yml b/serverless.yml index 128b176..1d51250 100644 --- a/serverless.yml +++ b/serverless.yml @@ -24,6 +24,9 @@ custom: - "sass-loader" - "pnp-webpack-plugin" + # Ignore some build time webpack-y things. + - "next-plugin-loader?middleware=document-head-tags-server!" + allowMissing: # TODO(trace-deps): Add to trace-deps # "./.next/serverless/pages/index.js": @@ -34,12 +37,23 @@ custom: - "critters" # for CSS optimization - "pnpapi" # for PnP usage - # TODO: RESOLUTIONS dynamic: resolutions: # [169:22]: require(`./transformers/${Transformer}.js`) "@ampproject/toolbox-optimizer/lib/DomTransformer.js": [] + # express/lib/view.js [81:13]: require(mod) + "express/lib/view.js": [] + + # Next.js lazy-loading of manifests and server page bundles. + # next/dist/next-server/server/load-components.js [1:1034]: require((0,_path.join)(distDir,_constants.BUILD_MANIFEST)) + # next/dist/next-server/server/load-components.js [1:1093]: require((0,_path.join)(distDir,_constants.REACT_LOADABLE_MANIFEST)) + "next/dist/next-server/server/load-components.js": [] + # next/dist/next-server/server/require.js [1:656]: require((0,_path.join)(serverBuildPath,_constants.PAGES_MANIFEST)) + # next/dist/next-server/server/require.js [1:1183]: require(pagePath) + # next/dist/next-server/server/require.js [1:1387]: require((0,_path.join)(serverBuildPath,_constants.FONT_MANIFEST)) + "next/dist/next-server/server/require.js": [] + plugins: - serverless-jetpack - serverless-offline @@ -83,7 +97,8 @@ functions: # Raw data for posts is read from disk outside `.next` build directory. - "posts/**/*.md" # Needed built Next.js assets and info. (Some of these are also traced). - - ".next/{BUILD_ID,*.json}" + - ".next/BUILD_ID" + - ".next/*.json" - ".next/serverless/**" # TODO(STATIC): Add a note about this. - ".next/static/**" From 9697775c09a3b383bb63c29fbd3e44f9d55e3d16 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 16:23:07 -0800 Subject: [PATCH 17/29] Start wrangling BASE_PATH. dev and start work. On to lambda:localdev next. --- README.md | 5 +++++ components/layout.js | 5 +++-- next.config.js | 20 +++++++++++++++++--- package.json | 8 ++++---- pages/index.js | 9 ++++++++- server/index.js | 32 ++++++++++++++++++++++++-------- serverless.yml | 2 ++ 7 files changed, 63 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 06ea3c0..dcf7db8 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The main goals of this demo project are as follows: Some caveats: 1. **Static files**: To make this demo a whole lot easier to develop/deploy, we handle serve static assets _from_ the Lambda. This is not what you should do for a real application. Typically, you'll want to stick those assets in an S3 bucket behind a CDN or something. Look for the `TODO(STATIC)` comments variously throughout this repository to see all the shortcuts you should unwind to then reconfigure for static assets "the right way". +2. **Deployment URL**: We have the Next.js blog up at sub-path `/blog`. A consumer app may go instead for root and that would simplify some of the code we have in this repo to make all the dev + prod experience work the same. ## Local development @@ -181,6 +182,10 @@ We use AWS IAM users with different privileges for these commands. `FIRST.LAST-a is required (to effect the underlying CloudFormation changes). ```sh +# Build for production. +$ STAGE=sandbox yarn build + +# Deploy $ STAGE=sandbox aws-vault exec FIRST.LAST-admin -- \ yarn lambda:deploy diff --git a/components/layout.js b/components/layout.js index 7c0f470..a9bf99a 100644 --- a/components/layout.js +++ b/components/layout.js @@ -10,6 +10,7 @@ export default function Layout({ children, home }) { return (
+ {/* TODO: Favicon will need separately asset handling to be at root slot. */} {name} @@ -39,7 +40,7 @@ export default function Layout({ children, home }) { {name} diff --git a/next.config.js b/next.config.js index 9d5be30..04f3b48 100644 --- a/next.config.js +++ b/next.config.js @@ -2,8 +2,23 @@ const { nextExternals } = require("./server/util"); +// **NOTE**: We set a base path that assumes Lambda staging _and_ our +// APIGW proxy base path (of `blog` by default). Many real world apps will +// just have a root base path and it's probably easier than this. +// +// - For build and local node servers, it's typically `/blog`. +// - For Lambda (localdev or cloud), it's typically `/${STAGE}/blog`. +const { STAGE, APP_PATH } = process.env; +const BASE_PATH = `/${STAGE}${APP_PATH}`; + + module.exports = { target: "serverless", + basePath: BASE_PATH, + assetPrefix: BASE_PATH, + env: { + BASE_PATH + }, webpack: (config, { isServer }) => { if (isServer) { // Add more information in the bundle. @@ -12,10 +27,9 @@ module.exports = { // Keep `node_modules` as runtime requires to help slim down page bundles. config.externals = (config.externals || []).concat( nextExternals() - ) + ); } - return config; - }, + } }; diff --git a/package.json b/package.json index 5368f41..456bb44 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "license": "MIT", "private": true, "scripts": { - "dev": "next dev", + "dev": "eval $(yarn -s env) && next dev", "clean": "rm -rf .next", - "build": "next build", - "start": "node server/index.js", - "env": "echo export STAGE=${STAGE:-localdev}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", + "build": "eval $(yarn -s env) && next build", + "start": "eval $(yarn -s env) && node server/index.js", + "env": "echo export STAGE=${STAGE:-localdev}; echo export APP_PATH=${APP_PATH:-/blog}; export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", "cf:_params": "eval $(yarn -s env) && echo --parameters ParameterKey=Stage,ParameterValue=${STAGE} ParameterKey=ServiceName,ParameterValue=${SERVICE_NAME}", "cf:bootstrap:_stack": "eval $(yarn -s env) && echo --region ${AWS_REGION} --stack-name cf-${SERVICE_NAME}-${STAGE}-bootstrap", "cf:bootstrap:_tmpl": "echo --template-body file://aws/bootstrap.yml ", diff --git a/pages/index.js b/pages/index.js index 2828c4d..7c5762c 100644 --- a/pages/index.js +++ b/pages/index.js @@ -5,6 +5,12 @@ import { getSortedPostsData } from '../lib/posts' import Link from 'next/link' import Date from '../components/date' +// TODO: This _shouldn't need BASE_PATH as next/link is supposed to handle it. +const isServer = () => { + return typeof window === "undefined"; +}; +const LINK_BASE = isServer() ? process.env.BASE_PATH : ""; + export default function Home({ allPostsData }) { return ( @@ -23,7 +29,8 @@ export default function Home({ allPostsData }) {
    {allPostsData.map(({ id, date, title }) => (
  • - + {/* */} + {title}
    diff --git a/server/index.js b/server/index.js index c9d2517..cb079b1 100644 --- a/server/index.js +++ b/server/index.js @@ -9,19 +9,33 @@ const page = require("../.next/serverless/pages/index.js"); const DEFAULT_PORT = 3000; const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_PORT, 10); const HOST = process.env.SERVER_HOST || "0.0.0.0"; -const BASE_PATH = process.env.BASE_PATH || "/blog"; -const STAGE = process.env.STAGE || "localdev"; + +// Set up base path for both Node.js and Lambda. +const { STAGE, APP_PATH } = process.env; +if (typeof STAGE === "undefined") { + throw new Error("STAGE is required"); +} +if (typeof APP_PATH === "undefined") { + throw new Error("APP_PATH is required"); +} +const BASE_PATH = `/${STAGE}${APP_PATH}`; +process.env.BASE_PATH = BASE_PATH; // Create the server app. -const getApp = async ({ basePath = "" } = {}) => { +const getApp = async ({ appRoot = "" } = {}) => { + // Normalize appRoot. + appRoot = appRoot.replace(/\/*$/, ""); + + // Build stuff. const NEXT_DIR = path.resolve(__dirname, "../.next"); const NEXT_DATA_DIR = path.resolve(NEXT_DIR, "serverless/pages"); const NEXT_PUBLIC_DIR = path.resolve(__dirname, "../public"); - const NEXT_APP_ROOT = "/_next"; + const NEXT_APP_ROOT = `${appRoot}/_next`; const BUILD_ID = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); const NEXT_DATA_ROOT = `${NEXT_APP_ROOT}/data/${BUILD_ID}`; + // Stage, base path stuff. const app = express(); // NOTE(STATIC): For this demo only, we just handle serve static content @@ -48,10 +62,10 @@ const getApp = async ({ basePath = "" } = {}) => { // Page handlers, // TODO(ROUTING): Need all the pages and routing. - app.all(`${basePath}`, (req, res) => page.render(req, res)); + app.all(`${appRoot}/`, (req, res) => page.render(req, res)); // NOTE(STATIC): User-added static assets. Should not be in Lambda. - app.use("/", express.static(NEXT_PUBLIC_DIR)); + app.use(`${appRoot}/`, express.static(NEXT_PUBLIC_DIR)); // TODO: 404. // TODO: Hook up error (?) @@ -65,7 +79,7 @@ module.exports.handler = async (event, context) => { // Lazy require `serverless-http` to allow non-Lambda targets to omit. // eslint-disable-next-line global-require handler = handler || require("serverless-http")(await getApp({ - basePath: BASE_PATH + appRoot: APP_PATH })); return handler(event, context); @@ -74,7 +88,9 @@ module.exports.handler = async (event, context) => { // DOCKER/DEV/ANYTHING: Start the server directly. if (require.main === module) { (async () => { - const server = (await getApp()).listen({ + const server = (await getApp({ + appRoot: BASE_PATH + })).listen({ port: PORT, host: HOST }, () => { diff --git a/serverless.yml b/serverless.yml index 1d51250..391db14 100644 --- a/serverless.yml +++ b/serverless.yml @@ -89,6 +89,8 @@ functions: # SCENARIO - base: The simplest, vanilla Serverless app. blog: handler: server/index.handler + environment: + APP_PATH: /blog events: # Use a generic proxy to allow Express app to route. - http: ANY /blog - http: 'ANY /blog/{proxy+}' From 6989bede55aebf66ce9155eabd3e2a6e961ee94c Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 16:46:01 -0800 Subject: [PATCH 18/29] Deployed and somehwat working --- README.md | 5 +++-- package.json | 2 +- serverless.yml | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dcf7db8..e54a134 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,18 @@ and visit: http://127.0.0.1:3000/ This uses `serverless-offline` to simulate the application running on Lambda. ```sh +$ yarn build $ yarn lambda:localdev ``` and visit: http://127.0.0.1:4000/localdev/blog/ -### Next.js Production server +### Next.js production server This repo _doesn't_ use the prod server, but if you want to create it, here you go: ```sh -$ yarn build +$ yarn clean && yarn build $ yarn start ``` diff --git a/package.json b/package.json index 456bb44..f6861be 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf .next", "build": "eval $(yarn -s env) && next build", "start": "eval $(yarn -s env) && node server/index.js", - "env": "echo export STAGE=${STAGE:-localdev}; echo export APP_PATH=${APP_PATH:-/blog}; export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", + "env": "echo export STAGE=${STAGE:-localdev}; echo export APP_PATH=${APP_PATH:-/blog}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", "cf:_params": "eval $(yarn -s env) && echo --parameters ParameterKey=Stage,ParameterValue=${STAGE} ParameterKey=ServiceName,ParameterValue=${SERVICE_NAME}", "cf:bootstrap:_stack": "eval $(yarn -s env) && echo --region ${AWS_REGION} --stack-name cf-${SERVICE_NAME}-${STAGE}-bootstrap", "cf:bootstrap:_tmpl": "echo --template-body file://aws/bootstrap.yml ", diff --git a/serverless.yml b/serverless.yml index 391db14..14b06e1 100644 --- a/serverless.yml +++ b/serverless.yml @@ -85,6 +85,12 @@ provider: Stage: ${self:custom.stage} Service: ${self:custom.service} + # TODO(HERE): This doesn't work yet with serverless-offline. + # TODO(STATIC): Allows serving of binary media. Shouldn't use this for real. + apiGateway: + binaryMediaTypes: + - "*/*" + functions: # SCENARIO - base: The simplest, vanilla Serverless app. blog: From 2b8229194f8f20cb495962d6da9ff395f1020e0e Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 18:54:28 -0800 Subject: [PATCH 19/29] Get static working. --- server/index.js | 16 +++++++++++----- serverless.yml | 1 - 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/server/index.js b/server/index.js index cb079b1..b393c9e 100644 --- a/server/index.js +++ b/server/index.js @@ -38,7 +38,7 @@ const getApp = async ({ appRoot = "" } = {}) => { // Stage, base path stuff. const app = express(); - // NOTE(STATIC): For this demo only, we just handle serve static content + // TODO(STATIC): For this demo only, we just handle serve static content // directly through the express app. For a real app, you'll want to deploy // static contents to somewhere to be directly served by the CDN. app.use(`${NEXT_APP_ROOT}/static`, express.static(path.join(NEXT_DIR, "static"))); @@ -47,7 +47,7 @@ const getApp = async ({ appRoot = "" } = {}) => { // _next/data/y-BRZHyY6b_T25zMSRPY0/posts/ssg-ssr.json -> // _next/serverless/posts/ssg-ssr.json // - // NOTE(STATIC): This _also_ could be uploaded to a real static serve. + // TODO(STATIC): This _also_ could be uploaded to a real static serve. // It technically _could_ change from data, so possibly disable SSG and // make this always dynamically generated. app.get(`${NEXT_DATA_ROOT}/*`, (req, res, next) => { @@ -78,9 +78,15 @@ let handler; module.exports.handler = async (event, context) => { // Lazy require `serverless-http` to allow non-Lambda targets to omit. // eslint-disable-next-line global-require - handler = handler || require("serverless-http")(await getApp({ - appRoot: APP_PATH - })); + handler = handler || require("serverless-http")( + await getApp({ + appRoot: APP_PATH + }), + // TODO(STATIC): Again, shouldn't be serving images from the Lambda :) + { + binary: ["image/*"] + } + ); return handler(event, context); }; diff --git a/serverless.yml b/serverless.yml index 14b06e1..b8ef688 100644 --- a/serverless.yml +++ b/serverless.yml @@ -85,7 +85,6 @@ provider: Stage: ${self:custom.stage} Service: ${self:custom.service} - # TODO(HERE): This doesn't work yet with serverless-offline. # TODO(STATIC): Allows serving of binary media. Shouldn't use this for real. apiGateway: binaryMediaTypes: From 0f5fd82bd4a4912a676174bf73f2bb7ff35ec304 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 19:01:56 -0800 Subject: [PATCH 20/29] Small note --- server/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/index.js b/server/index.js index b393c9e..471484c 100644 --- a/server/index.js +++ b/server/index.js @@ -64,7 +64,7 @@ const getApp = async ({ appRoot = "" } = {}) => { // TODO(ROUTING): Need all the pages and routing. app.all(`${appRoot}/`, (req, res) => page.render(req, res)); - // NOTE(STATIC): User-added static assets. Should not be in Lambda. + // TODO(STATIC): User-added static assets. Should not be in Lambda. app.use(`${appRoot}/`, express.static(NEXT_PUBLIC_DIR)); // TODO: 404. From 14a3d422101a34733db0bc6f800d20bece7239ed Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 19:41:16 -0800 Subject: [PATCH 21/29] Refactor all of BASE_PATH --- README.md | 41 ++++++++++++++++++++++++++++++----------- next.config.js | 10 ++++------ package.json | 4 ++-- server/index.js | 25 ++++++++----------------- serverless.yml | 2 +- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e54a134..197ec87 100644 --- a/README.md +++ b/README.md @@ -35,34 +35,51 @@ Start with: $ yarn install ``` -### Next.js Development server +Then we provide a lot of different ways to develop the server. Here is a table of options with current working status: + +| Command | Status | URL | +| ----------------- | ------ | ---------------------------------------------- | +| `dev` | works | http://127.0.0.1:3000/blog/ | +| | works | http://127.0.0.1:3000/blog/posts/ssg-ssr | +| `start` | works | http://127.0.0.1:4000/blog/ | +| | fails | http://127.0.0.1:4000/blog/posts/ssg-ssr | +| `lambda:localdev` | works | http://127.0.0.1:4000/blog/ | +| | fails | http://127.0.0.1:4000/blog/posts/ssg-ssr | +| _deployed_ | works | https://nextjs-sls-sandbox.formidable.dev/blog/ | +| | fails | https://nextjs-sls-sandbox.formidable.dev/blog/posts/ssg-ssr | + +### Next.js Development server (3000) + +The built-in Next.js dev server, compilation and all. ```sh $ yarn dev ``` -and visit: http://127.0.0.1:3000/ +and visit: http://127.0.0.1:3000/blog/ -### Serverless development server +### Node.js production server (4000) -This uses `serverless-offline` to simulate the application running on Lambda. +We have a Node.js custom `express` server that uses _almost_ all of the Lambda code, which is sometimes an easier development experience that `serverless-offline`. This also could theoretically serve as a real production server on a bare metal or containerized compute instance outside of Lambda. ```sh -$ yarn build -$ yarn lambda:localdev +$ yarn clean && yarn build +$ yarn start ``` -and visit: http://127.0.0.1:4000/localdev/blog/ +and visit: http://127.0.0.1:4000/blog/ -### Next.js production server +### Lambda development server (5000) -This repo _doesn't_ use the prod server, but if you want to create it, here you go: +This uses `serverless-offline` to simulate the application running on Lambda. ```sh $ yarn clean && yarn build -$ yarn start +$ yarn lambda:localdev ``` +and visit: http://127.0.0.1:5000/blog/ + ## Deployment We target AWS via a simple command line deploy. For a real world application, you'd want to have this deployment come from your CI/CD pipeline with things like per-PR deployments, etc. However, this demo is just here to validate Next.js running on Lambda, so get yer laptop running and fire away! @@ -199,9 +216,11 @@ $ STAGE=sandbox aws-vault exec FIRST.LAST-admin -- \ See the [aws-lambda-serverless-reference][] docs for additional Serverless/Lambda (`yarn lambda:*`) tasks you can run. -`yarn lambda:info` gives the current APIGW endpoints. As a useful helper we've separately hooked up a custom domain for `STAGE=sandbox` at: +As a useful helper we've separately hooked up a custom domain for `STAGE=sandbox` at: https://nextjs-sls-sandbox.formidable.dev/blog/ +> ℹ️ **Note**: We set `BASE_PATH` to `/blog` and _not_ `/${STAGE}/blog` like API Gateway does for internal endpoints for our references to other static assets. It's kind of a moot point because frontend assets shouldn't be served via Lambda/APIGW like we do for this demo, but just worth noting that the internal endpoints will have incorrect asset paths. + [aws-lambda-serverless-reference]: https://github.com/FormidableLabs/aws-lambda-serverless-reference [aws-vault]: https://github.com/99designs/aws-vault diff --git a/next.config.js b/next.config.js index 04f3b48..994966c 100644 --- a/next.config.js +++ b/next.config.js @@ -5,12 +5,10 @@ const { nextExternals } = require("./server/util"); // **NOTE**: We set a base path that assumes Lambda staging _and_ our // APIGW proxy base path (of `blog` by default). Many real world apps will // just have a root base path and it's probably easier than this. -// -// - For build and local node servers, it's typically `/blog`. -// - For Lambda (localdev or cloud), it's typically `/${STAGE}/blog`. -const { STAGE, APP_PATH } = process.env; -const BASE_PATH = `/${STAGE}${APP_PATH}`; - +const { BASE_PATH } = process.env; +if (!BASE_PATH) { + throw new Error("BASE_PATH is required"); +} module.exports = { target: "serverless", diff --git a/package.json b/package.json index f6861be..c71402f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf .next", "build": "eval $(yarn -s env) && next build", "start": "eval $(yarn -s env) && node server/index.js", - "env": "echo export STAGE=${STAGE:-localdev}; echo export APP_PATH=${APP_PATH:-/blog}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", + "env": "echo export STAGE=${STAGE:-localdev}; echo export BASE_PATH=${BASE_PATH:-/blog}; echo export SERVICE_NAME=nextjs-serverless; echo export AWS_REGION=${AWS_REGION:-us-east-1}; echo export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR", "cf:_params": "eval $(yarn -s env) && echo --parameters ParameterKey=Stage,ParameterValue=${STAGE} ParameterKey=ServiceName,ParameterValue=${SERVICE_NAME}", "cf:bootstrap:_stack": "eval $(yarn -s env) && echo --region ${AWS_REGION} --stack-name cf-${SERVICE_NAME}-${STAGE}-bootstrap", "cf:bootstrap:_tmpl": "echo --template-body file://aws/bootstrap.yml ", @@ -31,7 +31,7 @@ "tf:service:apply": "yarn run tf:terraform apply $(yarn -s tf:service:_vars)", "tf:service:_delete": "yarn run tf:terraform destroy $(yarn -s tf:service:_vars)", "lambda:sls": "eval $(yarn -s env) && sls -s ${STAGE}", - "lambda:localdev": "yarn run lambda:sls offline start --httpPort ${SERVER_PORT:-4000} --host ${SERVER_HOST:-0.0.0.0}", + "lambda:localdev": "yarn run lambda:sls offline start --httpPort ${SERVER_PORT:-5000} --host ${SERVER_HOST:-0.0.0.0} --noPrependStageInUrl", "lambda:deploy": "yarn run lambda:sls deploy", "lambda:info": "yarn run lambda:sls info", "lambda:logs": "yarn run lambda:sls logs", diff --git a/server/index.js b/server/index.js index 471484c..53a5823 100644 --- a/server/index.js +++ b/server/index.js @@ -6,25 +6,20 @@ const express = require("express"); const page = require("../.next/serverless/pages/index.js"); -const DEFAULT_PORT = 3000; +const DEFAULT_PORT = 4000; const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_PORT, 10); const HOST = process.env.SERVER_HOST || "0.0.0.0"; // Set up base path for both Node.js and Lambda. -const { STAGE, APP_PATH } = process.env; -if (typeof STAGE === "undefined") { - throw new Error("STAGE is required"); +const { BASE_PATH } = process.env; +if (typeof BASE_PATH === "undefined") { + throw new Error("BASE_PATH is required"); } -if (typeof APP_PATH === "undefined") { - throw new Error("APP_PATH is required"); -} -const BASE_PATH = `/${STAGE}${APP_PATH}`; -process.env.BASE_PATH = BASE_PATH; // Create the server app. -const getApp = async ({ appRoot = "" } = {}) => { +const getApp = async () => { // Normalize appRoot. - appRoot = appRoot.replace(/\/*$/, ""); + const appRoot = BASE_PATH.replace(/\/*$/, ""); // Build stuff. const NEXT_DIR = path.resolve(__dirname, "../.next"); @@ -79,9 +74,7 @@ module.exports.handler = async (event, context) => { // Lazy require `serverless-http` to allow non-Lambda targets to omit. // eslint-disable-next-line global-require handler = handler || require("serverless-http")( - await getApp({ - appRoot: APP_PATH - }), + await getApp(), // TODO(STATIC): Again, shouldn't be serving images from the Lambda :) { binary: ["image/*"] @@ -94,9 +87,7 @@ module.exports.handler = async (event, context) => { // DOCKER/DEV/ANYTHING: Start the server directly. if (require.main === module) { (async () => { - const server = (await getApp({ - appRoot: BASE_PATH - })).listen({ + const server = (await getApp()).listen({ port: PORT, host: HOST }, () => { diff --git a/serverless.yml b/serverless.yml index b8ef688..2b6a54b 100644 --- a/serverless.yml +++ b/serverless.yml @@ -95,7 +95,7 @@ functions: blog: handler: server/index.handler environment: - APP_PATH: /blog + BASE_PATH: /blog events: # Use a generic proxy to allow Express app to route. - http: ANY /blog - http: 'ANY /blog/{proxy+}' From 32af96411df7d1cd114b44bd8b489778a74cb5d7 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 19:45:09 -0800 Subject: [PATCH 22/29] Update docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 197ec87..403e717 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ Then we provide a lot of different ways to develop the server. Here is a table o | | works | http://127.0.0.1:3000/blog/posts/ssg-ssr | | `start` | works | http://127.0.0.1:4000/blog/ | | | fails | http://127.0.0.1:4000/blog/posts/ssg-ssr | -| `lambda:localdev` | works | http://127.0.0.1:4000/blog/ | -| | fails | http://127.0.0.1:4000/blog/posts/ssg-ssr | +| `lambda:localdev` | works | http://127.0.0.1:5000/blog/ | +| | fails | http://127.0.0.1:5000/blog/posts/ssg-ssr | | _deployed_ | works | https://nextjs-sls-sandbox.formidable.dev/blog/ | | | fails | https://nextjs-sls-sandbox.formidable.dev/blog/posts/ssg-ssr | @@ -201,7 +201,7 @@ is required (to effect the underlying CloudFormation changes). ```sh # Build for production. -$ STAGE=sandbox yarn build +$ yarn clean && yarn build # Deploy $ STAGE=sandbox aws-vault exec FIRST.LAST-admin -- \ From a4ae4535f43522a2138e419a7465fcf13828cfe8 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 19:51:25 -0800 Subject: [PATCH 23/29] Update Readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 403e717..52c2a10 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ The main goals of this demo project are as follows: Some caveats: 1. **Static files**: To make this demo a whole lot easier to develop/deploy, we handle serve static assets _from_ the Lambda. This is not what you should do for a real application. Typically, you'll want to stick those assets in an S3 bucket behind a CDN or something. Look for the `TODO(STATIC)` comments variously throughout this repository to see all the shortcuts you should unwind to then reconfigure for static assets "the right way". -2. **Deployment URL**: We have the Next.js blog up at sub-path `/blog`. A consumer app may go instead for root and that would simplify some of the code we have in this repo to make all the dev + prod experience work the same. +2. **Deployment URL base path**: We have the Next.js blog up at sub-path `/blog`. A consumer app may go instead for root and that would simplify some of the code we have in this repo to make all the dev + prod experience work the same. +3. **Lambda SSR + CDN**: Our React SSR hasn't been tuned at all yet for caching in the CDN like a real world app would want to do. ## Local development From 62a01c96ad9a366e52d21c08b637123698389817 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 20:04:55 -0800 Subject: [PATCH 24/29] Add note for node externals and measurements. --- README.md | 28 ++++++++++++++++++++++++++++ next.config.js | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52c2a10..5449479 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,39 @@ and is based on the following projects: - [nextjs-fargate-demo](https://github.com/FormidableLabs/nextjs-fargate-demo): We deploy the same Next.js application. - [aws-lambda-serverless-reference][]: The CloudFormation/Terraform infrastructure approach is basically identical to our reference Serverless project. +### Goals + The main goals of this demo project are as follows: 1. **Slim down a Next.js Lambda deployment**: The Next.js `target: "serverless"` Node.js outputs are huge. Like really, really big because **each page** contains **all the dependencies**. This project adds a custom externals handler to filter out almost all dependencies in `node_modules` and leave those as normal `require()` calls, thus dramatically decreasing the `pages` bundle sizes. The `node_modules` dependencies are included via `serverless-jetpack` trace mode to keep things tight. + + + If you want to see the difference, we've got an environment variable to skip the Node.js package external excludes, producing default bundles with tons of code per page. Try out the following to see (1) the size of the zip bundle and number of individual files in the zip, and a separate command to see (2) the size of the unzipped index page bundle. + + ```sh + # Slimmer with packages in real node_modules and not bundle. + $ yarn clean && yarn build && yarn lambda:sls package --report + $ du -sh .serverless/blog.zip && zipinfo .serverless/blog.zip | wc -l + 2.1M .serverless/blog.zip + 1241 + $ du -sh .next/serverless/pages/index.js + 52K .next/serverless/pages/index.js + + # Bigger with packages in each page bundle + $ yarn clean && NEXT_SKIP_EXTERNALS=true yarn build && yarn lambda:sls package --report + $ du -sh .serverless/blog.zip && zipinfo .serverless/blog.zip | wc -l + 4.0M .serverless/blog.zip + 293 + $ du -sh .next/serverless/pages/index.js + 2.7M .next/serverless/pages/index.js + ``` + + > ℹ️ **Note**: For a full optimization we'd probably want to see if we could split out application code that is shared across pages as well. For now, we're avoiding a big chunk of `node_modules`, which has a good punch for just 3 actual pages (plus supporting boilerplate). + 2. **Single Lambda/APIGW proxy**: `TODO(ROUTING): INSERT_NOTES` +### Caveats + Some caveats: 1. **Static files**: To make this demo a whole lot easier to develop/deploy, we handle serve static assets _from_ the Lambda. This is not what you should do for a real application. Typically, you'll want to stick those assets in an S3 bucket behind a CDN or something. Look for the `TODO(STATIC)` comments variously throughout this repository to see all the shortcuts you should unwind to then reconfigure for static assets "the right way". diff --git a/next.config.js b/next.config.js index 994966c..37ce262 100644 --- a/next.config.js +++ b/next.config.js @@ -5,7 +5,7 @@ const { nextExternals } = require("./server/util"); // **NOTE**: We set a base path that assumes Lambda staging _and_ our // APIGW proxy base path (of `blog` by default). Many real world apps will // just have a root base path and it's probably easier than this. -const { BASE_PATH } = process.env; +const { BASE_PATH, NEXT_SKIP_EXTERNALS = "false" } = process.env; if (!BASE_PATH) { throw new Error("BASE_PATH is required"); } @@ -24,7 +24,7 @@ module.exports = { // Keep `node_modules` as runtime requires to help slim down page bundles. config.externals = (config.externals || []).concat( - nextExternals() + NEXT_SKIP_EXTERNALS === "true" ? [] : nextExternals() ); } From 708727004aa6e41422e194c3c731c9dd5f6366f5 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 28 Jan 2021 20:06:46 -0800 Subject: [PATCH 25/29] Add link to custom externals --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5449479..aae2949 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ and is based on the following projects: The main goals of this demo project are as follows: -1. **Slim down a Next.js Lambda deployment**: The Next.js `target: "serverless"` Node.js outputs are huge. Like really, really big because **each page** contains **all the dependencies**. This project adds a custom externals handler to filter out almost all dependencies in `node_modules` and leave those as normal `require()` calls, thus dramatically decreasing the `pages` bundle sizes. The `node_modules` dependencies are included via `serverless-jetpack` trace mode to keep things tight. +1. **Slim down a Next.js Lambda deployment**: The Next.js `target: "serverless"` Node.js outputs are huge. Like really, really big because **each page** contains **all the dependencies**. This project adds a [custom externals handler](./server/util.js) to filter out almost all dependencies in `node_modules` and leave those as normal `require()` calls, thus dramatically decreasing the `pages` bundle sizes. The `node_modules` dependencies are included via `serverless-jetpack` trace mode to keep things tight. If you want to see the difference, we've got an environment variable to skip the Node.js package external excludes, producing default bundles with tons of code per page. Try out the following to see (1) the size of the zip bundle and number of individual files in the zip, and a separate command to see (2) the size of the unzipped index page bundle. From 9c2e711e85b60eb4cc8478bbbd0720da2a544981 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Sat, 30 Jan 2021 14:51:49 -0800 Subject: [PATCH 26/29] Pkg updates and debugging --- package.json | 2 +- server/index.js | 12 ++++++++++++ serverless.yml | 6 ------ yarn.lock | 18 +++++++++--------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index c71402f..8ca134b 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "serverless": "^2.21.1", - "serverless-jetpack": "^0.10.7", + "serverless-jetpack": "^0.10.8", "serverless-offline": "^6.8.0" } } diff --git a/server/index.js b/server/index.js index 53a5823..82a8a59 100644 --- a/server/index.js +++ b/server/index.js @@ -5,11 +5,16 @@ const path = require("path"); const express = require("express"); const page = require("../.next/serverless/pages/index.js"); +const manifests = { + routes: require("../.next/routes-manifest.json"), + pages: require("../.next/serverless/pages-manifest.json") +}; const DEFAULT_PORT = 4000; const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_PORT, 10); const HOST = process.env.SERVER_HOST || "0.0.0.0"; +// TODO: REMOVE THIS AND USE MANIFEST??? // Set up base path for both Node.js and Lambda. const { BASE_PATH } = process.env; if (typeof BASE_PATH === "undefined") { @@ -30,6 +35,13 @@ const getApp = async () => { const BUILD_ID = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); const NEXT_DATA_ROOT = `${NEXT_APP_ROOT}/data/${BUILD_ID}`; + console.log("TOOD HERE", JSON.stringify({ + appRoot, + NEXT_APP_ROOT, + NEXT_DATA_ROOT, + manifests + }, null, 2)); + // Stage, base path stuff. const app = express(); diff --git a/serverless.yml b/serverless.yml index 2b6a54b..ce9cb5d 100644 --- a/serverless.yml +++ b/serverless.yml @@ -16,9 +16,6 @@ custom: # Provided on Lambda - "aws-sdk" - # TODO(trace-deps): Remove once in trace-deps - - "critters" - # Ignore real deps in Next.js that we shouldn't need at runtime. - "webpack" - "sass-loader" @@ -28,9 +25,6 @@ custom: - "next-plugin-loader?middleware=document-head-tags-server!" allowMissing: - # TODO(trace-deps): Add to trace-deps - # "./.next/serverless/pages/index.js": - # - "critters" # For CSS optimization "node-fetch": - "encoding" "next": diff --git a/yarn.lock b/yarn.lock index d942f81..fcbe5b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7519,10 +7519,10 @@ serverless-http@^2.7.0: optionalDependencies: "@types/aws-lambda" "^8.10.56" -serverless-jetpack@^0.10.7: - version "0.10.7" - resolved "https://registry.yarnpkg.com/serverless-jetpack/-/serverless-jetpack-0.10.7.tgz#2eadba6b66807224bf05e447247bbdea6bc2a7c8" - integrity sha512-Ra/6Swp86fGq7XxCyhQMVpCA+EYjdmJnClNBK2aQoSzDSc9c7iQ6J/5RG6RjcgDRp1K/oNbMkhgLfQjyJuKfHw== +serverless-jetpack@^0.10.8: + version "0.10.8" + resolved "https://registry.yarnpkg.com/serverless-jetpack/-/serverless-jetpack-0.10.8.tgz#61ddc35a9562b2ce287a079afa8ce8ca68fbedf9" + integrity sha512-QlDoC7RMD791KptkD1/mDJfH+x07iuy/qnWitEAIfPO/2SlDOxFknZqB/eG3bdIRCCjYCuqT/ffbXt9ysmWNuw== dependencies: archiver "^3.1.1" globby "^9.2.0" @@ -7531,7 +7531,7 @@ serverless-jetpack@^0.10.7: make-dir "^3.0.2" nanomatch "^1.2.13" p-limit "^2.3.0" - trace-deps "^0.3.3" + trace-deps "^0.3.8" serverless-offline@^6.8.0: version "6.8.0" @@ -8504,10 +8504,10 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trace-deps@^0.3.3: - version "0.3.7" - resolved "https://registry.yarnpkg.com/trace-deps/-/trace-deps-0.3.7.tgz#940a1c4358dfda2fdd3e3f2c51563d7b69628ee2" - integrity sha512-R4scNNMV/+/l+pbPHvF6mmh4/4UnAT4GwGr8ColzlaqJwroLtxbg0xHEC7fFoKcKXxKepAion+nry7fy9Nq0Tg== +trace-deps@^0.3.8: + version "0.3.8" + resolved "https://registry.yarnpkg.com/trace-deps/-/trace-deps-0.3.8.tgz#efb8d55087e96cfbae457c9176e9e2144b8acff2" + integrity sha512-VXRdVSmduxemn2y0HzPZiw2zMEMmiNytHb3xSdV/FHFYoO8LAJ4sY1cvP1lDyrPIJDb5wxp30eno8nbE32SjzA== dependencies: acorn-node "^2.0.1" resolve "^1.19.0" From 186503b894d6191614d53b408b332f50d0d3f053 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Mon, 1 Feb 2021 11:50:09 -0800 Subject: [PATCH 27/29] Add comment --- server/util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/util.js b/server/util.js index 0d92509..530f6a0 100644 --- a/server/util.js +++ b/server/util.js @@ -67,6 +67,7 @@ const getPackageFromRequest = (request = "") => { return null; } + // TODO: Can we just infer all loaders/plugins that are needed at build-time? // Skip Next.js loaders and things that need to be built in. if (REQUEST_SKIP_LIST.some( (strOrRe) => typeof strOrRe === "string" ? request.startsWith(strOrRe) : strOrRe.test(request) From 95854c7f5387ea8c3070cda48a57f7d0b01ffc9a Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 8 Apr 2021 20:55:09 -0700 Subject: [PATCH 28/29] Get all routing going --- lib/posts.js | 1 + server/index.js | 79 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/lib/posts.js b/lib/posts.js index b4f7dd3..28574c2 100644 --- a/lib/posts.js +++ b/lib/posts.js @@ -18,6 +18,7 @@ export async function getSortedPostsData() { // Read markdown file as string const fullPath = path.join(postsDirectory, fileName) + // TODO: On not found, wrap ENOENT and push back to a 404. const fileContents = await readFile(fullPath, 'utf8') // Use gray-matter to parse the post metadata section diff --git a/server/index.js b/server/index.js index 82a8a59..5cfea92 100644 --- a/server/index.js +++ b/server/index.js @@ -4,7 +4,6 @@ const fs = require("fs").promises; const path = require("path"); const express = require("express"); -const page = require("../.next/serverless/pages/index.js"); const manifests = { routes: require("../.next/routes-manifest.json"), pages: require("../.next/serverless/pages-manifest.json") @@ -21,30 +20,43 @@ if (typeof BASE_PATH === "undefined") { throw new Error("BASE_PATH is required"); } +// Next locations. +const NEXT_DIR = path.resolve(__dirname, "../.next"); +const NEXT_SLS_DIR = path.join(NEXT_DIR, "serverless"); +const NEXT_PAGES_DIR = path.join(NEXT_SLS_DIR, "pages"); +const NEXT_PUBLIC_DIR = path.resolve(__dirname, "../public"); + +// Send response. +const sendPage = ({ pagePath, req, res }) => { + const fullPath = path.join(NEXT_SLS_DIR, pagePath); + if (fullPath.endsWith(".html")) { + return res.sendFile(fullPath); + } + + if (fullPath.endsWith(".js")) { + const page = require(fullPath); + return page.render(req, res); + } + + throw new Error(`Unknown page format: ${pagePath}`); +}; + // Create the server app. const getApp = async () => { // Normalize appRoot. const appRoot = BASE_PATH.replace(/\/*$/, ""); // Build stuff. - const NEXT_DIR = path.resolve(__dirname, "../.next"); - const NEXT_DATA_DIR = path.resolve(NEXT_DIR, "serverless/pages"); - const NEXT_PUBLIC_DIR = path.resolve(__dirname, "../public"); const NEXT_APP_ROOT = `${appRoot}/_next`; - const BUILD_ID = (await fs.readFile(path.join(NEXT_DIR, "BUILD_ID"))).toString().trim(); const NEXT_DATA_ROOT = `${NEXT_APP_ROOT}/data/${BUILD_ID}`; - console.log("TOOD HERE", JSON.stringify({ - appRoot, - NEXT_APP_ROOT, - NEXT_DATA_ROOT, - manifests - }, null, 2)); - // Stage, base path stuff. const app = express(); + // Development tweaks. + app.set("json spaces", 2); + // TODO(STATIC): For this demo only, we just handle serve static content // directly through the express app. For a real app, you'll want to deploy // static contents to somewhere to be directly served by the CDN. @@ -60,7 +72,7 @@ const getApp = async () => { app.get(`${NEXT_DATA_ROOT}/*`, (req, res, next) => { // Only handle JSON. if (req.url.endsWith(".json")) { - const filePath = req.url.replace(NEXT_DATA_ROOT, NEXT_DATA_DIR); + const filePath = req.url.replace(NEXT_DATA_ROOT, NEXT_PAGES_DIR); return res.sendFile(filePath); } @@ -68,14 +80,47 @@ const getApp = async () => { }); // Page handlers, - // TODO(ROUTING): Need all the pages and routing. - app.all(`${appRoot}/`, (req, res) => page.render(req, res)); + app.all(`${appRoot}(/*)?`, (req, res, next) => { + // Remove app root or switch to root ("/"). + const relPath = req.url.replace(appRoot, "") || "/"; + + // Direct page match + let pagePath = manifests.pages[relPath]; + + // Dynamic routes + manifests.routes.dynamicRoutes.forEach(({ page, namedRegex }) => { + if (!pagePath && new RegExp(namedRegex).exec(relPath)) { + pagePath = manifests.pages[page]; + } + }); + + // Render + if (pagePath) { + return sendPage({ pagePath, req, res }); + } + + return next(); + }); // TODO(STATIC): User-added static assets. Should not be in Lambda. app.use(`${appRoot}/`, express.static(NEXT_PUBLIC_DIR)); - // TODO: 404. - // TODO: Hook up error (?) + // Not found handling. + app.use((req, res) => sendPage({ + pagePath: manifests.pages["/404"], + req, + res: res.status(404) + })); + + // Error handler. + app.use((err, req, res, next) => { + console.error(err.stack); + return sendPage({ + pagePath: manifests.pages["/_error"], + req, + res: res.status(500) + }); + }); return app; }; From e71ae1eb8c830bbfba3a30c3e956ab41094fb6f5 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Thu, 8 Apr 2021 21:41:24 -0700 Subject: [PATCH 29/29] More fixups --- next.config.js | 9 +++++---- serverless.yml | 7 +++++++ yarn.lock | 22 +++++++++++----------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/next.config.js b/next.config.js index 37ce262..5e22f47 100644 --- a/next.config.js +++ b/next.config.js @@ -22,10 +22,11 @@ module.exports = { // Add more information in the bundle. config.output.pathinfo = true; - // Keep `node_modules` as runtime requires to help slim down page bundles. - config.externals = (config.externals || []).concat( - NEXT_SKIP_EXTERNALS === "true" ? [] : nextExternals() - ); + // TODO: Replace with `webpack-next-externals` when ready. + // // Keep `node_modules` as runtime requires to help slim down page bundles. + // config.externals = (config.externals || []).concat( + // NEXT_SKIP_EXTERNALS === "true" ? [] : nextExternals() + // ); } return config; diff --git a/serverless.yml b/serverless.yml index ce9cb5d..43c550e 100644 --- a/serverless.yml +++ b/serverless.yml @@ -33,6 +33,9 @@ custom: dynamic: resolutions: + # Our server does lazy requires of pages. + "./server/index.js": [] + # [169:22]: require(`./transformers/${Transformer}.js`) "@ampproject/toolbox-optimizer/lib/DomTransformer.js": [] @@ -94,6 +97,10 @@ functions: - http: ANY /blog - http: 'ANY /blog/{proxy+}' package: + trace: + include: + # Next.js-generated files: gather dependencies + - ".next/serverless/**/*.js" include: # Raw data for posts is read from disk outside `.next` build directory. - "posts/**/*.md" diff --git a/yarn.lock b/yarn.lock index fcbe5b2..ffd1f2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -888,9 +888,9 @@ "@types/unist" "*" "@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" + integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== "@types/node@*": version "14.14.22" @@ -4636,7 +4636,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.1.0: +is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== @@ -7240,11 +7240,11 @@ resolve-url@^0.2.1: integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= resolve@^1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: - is-core-module "^2.1.0" + is-core-module "^2.2.0" path-parse "^1.0.6" responselike@1.0.2, responselike@^1.0.2: @@ -8505,9 +8505,9 @@ tr46@^1.0.1: punycode "^2.1.0" trace-deps@^0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/trace-deps/-/trace-deps-0.3.8.tgz#efb8d55087e96cfbae457c9176e9e2144b8acff2" - integrity sha512-VXRdVSmduxemn2y0HzPZiw2zMEMmiNytHb3xSdV/FHFYoO8LAJ4sY1cvP1lDyrPIJDb5wxp30eno8nbE32SjzA== + version "0.3.9" + resolved "https://registry.yarnpkg.com/trace-deps/-/trace-deps-0.3.9.tgz#85714d19444d67bd28bc9447919fa68655f744ef" + integrity sha512-85uDbKTm4ygcLCcnHO7FfchgrXi24s5bLloptSx565kblrv9lbaBi/bnWLEkLk2FHEC7m0BJb4QoOoshoSs9LQ== dependencies: acorn-node "^2.0.1" resolve "^1.19.0"