diff --git a/src/index.ts b/src/index.ts index f02b8423..314d7210 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ import { runNpmInstall, runPackageJsonScript, } from '@now/build-utils'; -import { Route, Source } from '@now/routing-utils'; +import { Route } from '@now/routing-utils'; import { convertHeaders, convertRedirects, @@ -336,11 +336,17 @@ export const build = async ({ env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`; await runPackageJsonScript(entryPath, shouldRunScript, { ...spawnOpts, env }); + const appMountPrefixNoTrailingSlash = path.posix + .join('/', entryDirectory) + .replace(/\/+$/, ''); + const routesManifest = await getRoutesManifest(entryPath, realNextVersion); + const prerenderManifest = await getPrerenderManifest(entryPath); const headers: Route[] = []; const rewrites: Route[] = []; const redirects: Route[] = []; const nextBasePathRoute: Route[] = []; + const dataRoutes: Route[] = []; let nextBasePath: string | undefined; // whether they have enabled pages/404.js as the custom 404 page let hasPages404 = false; @@ -356,6 +362,35 @@ export const build = async ({ headers.push(...convertHeaders(routesManifest.headers)); } + if (routesManifest.dataRoutes) { + // Load the /_next/data routes for both dynamic SSG and SSP pages. + // These must be combined and sorted to prevent conflicts + for (const dataRoute of routesManifest.dataRoutes) { + const ssgDataRoute = prerenderManifest.lazyRoutes[dataRoute.page]; + + // we don't need to add routes for non-lazy SSG routes since + // they have outputs which would override the routes anyways + if (prerenderManifest.routes[dataRoute.page]) { + continue; + } + + dataRoutes.push({ + src: dataRoute.dataRouteRegex.replace( + /^\^/, + `^${appMountPrefixNoTrailingSlash}` + ), + dest: path.join( + '/', + entryDirectory, + // make sure to route SSG data route to the data prerender + // output, we don't do this for SSP routes since they don't + // have a separate data output + (ssgDataRoute && ssgDataRoute.dataRoute) || dataRoute.page + ), + }); + } + } + if (routesManifest.pages404) { hasPages404 = true; } @@ -521,13 +556,8 @@ export const build = async ({ const prerenders: { [key: string]: Prerender | FileFsRef } = {}; const staticPages: { [key: string]: FileFsRef } = {}; const dynamicPages: string[] = []; - const dynamicDataRoutes: Array = []; let static404Page: string | undefined; - const appMountPrefixNoTrailingSlash = path.posix - .join('/', entryDirectory) - .replace(/\/+$/, ''); - if (isLegacy) { const filesAfterBuild = await glob('**', entryPath); @@ -629,7 +659,6 @@ export const build = async ({ const pages = await glob('**/*.js', pagesDir); const staticPageFiles = await glob('**/*.html', pagesDir); - const prerenderManifest = await getPrerenderManifest(entryPath); Object.keys(staticPageFiles).forEach((page: string) => { const pathname = page.replace(/\.html$/, ''); @@ -976,18 +1005,25 @@ export const build = async ({ onPrerenderRoute(route, true) ); - // Dynamic pages for lazy routes should be handled by the lambda flow. - Object.keys(prerenderManifest.lazyRoutes).forEach(lazyRoute => { - const { dataRouteRegex, dataRoute } = prerenderManifest.lazyRoutes[ - lazyRoute - ]; - dynamicDataRoutes.push({ - // Next.js provided data route regex - src: dataRouteRegex.replace(/^\^/, `^${appMountPrefixNoTrailingSlash}`), - // Location of lambda in builder output - dest: path.posix.join(entryDirectory, dataRoute), + // We still need to use lazyRoutes if the dataRoutes field + // isn't available for backwards compatibility + if (!(routesManifest && routesManifest.dataRoutes)) { + // Dynamic pages for lazy routes should be handled by the lambda flow. + Object.keys(prerenderManifest.lazyRoutes).forEach(lazyRoute => { + const { dataRouteRegex, dataRoute } = prerenderManifest.lazyRoutes[ + lazyRoute + ]; + dataRoutes.push({ + // Next.js provided data route regex + src: dataRouteRegex.replace( + /^\^/, + `^${appMountPrefixNoTrailingSlash}` + ), + // Location of lambda in builder output + dest: path.posix.join(entryDirectory, dataRoute), + }); }); - }); + } } const nextStaticFiles = await glob( @@ -1086,6 +1122,7 @@ export const build = async ({ continue: true, }, { src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') }, + // Next.js page lambdas, `static/` folder, reserved assets, and `public/` // folder { handle: 'filesystem' }, @@ -1107,7 +1144,10 @@ export const build = async ({ ...rewrites, // Dynamic routes ...dynamicRoutes, - ...dynamicDataRoutes, + + // /_next/data routes for getServerProps/getStaticProps pages + ...dataRoutes, + // Custom Next.js 404 page (TODO: do we want to remove this?) ...(isLegacy ? [] diff --git a/src/utils.ts b/src/utils.ts index 589aea76..069cdec6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -316,6 +316,7 @@ export type RoutesManifest = { regex: string; }[]; version: number; + dataRoutes?: Array<{ page: string; dataRouteRegex: string }>; }; export async function getRoutesManifest( diff --git a/test/fixtures/05-spr-support/now.json b/test/fixtures/05-spr-support/now.json index ef9226ba..631aae8d 100644 --- a/test/fixtures/05-spr-support/now.json +++ b/test/fixtures/05-spr-support/now.json @@ -84,6 +84,21 @@ "x-now-cache": "/HIT|STALE/" } }, + { + "path": "/_next/data/testing-build-id/blog/post-4.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/_next/data/testing-build-id/blog/post-4.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "/HIT|STALE/" + } + }, { "path": "/blog/post-1/comment-1", "status": 200, diff --git a/test/fixtures/18-ssg-fallback-support/now.json b/test/fixtures/18-ssg-fallback-support/now.json index 4d9e0a05..f43e67b5 100644 --- a/test/fixtures/18-ssg-fallback-support/now.json +++ b/test/fixtures/18-ssg-fallback-support/now.json @@ -82,6 +82,21 @@ "x-now-cache": "/HIT|STALE/" } }, + { + "path": "/_next/data/testing-build-id/blog/post-4.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/_next/data/testing-build-id/blog/post-4.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "/HIT|STALE/" + } + }, { "path": "/blog/post-3", "status": 200, diff --git a/test/fixtures/18-ssg-fallback-support/package.json b/test/fixtures/18-ssg-fallback-support/package.json index 93e31bc4..17a2cb2a 100644 --- a/test/fixtures/18-ssg-fallback-support/package.json +++ b/test/fixtures/18-ssg-fallback-support/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "next": "9.2.2-canary.16", + "next": "9.2.3-canary.13", "react": "^16.8.6", "react-dom": "^16.8.6" } diff --git a/test/fixtures/21-server-props/next.config.js b/test/fixtures/21-server-props/next.config.js new file mode 100644 index 00000000..ac717504 --- /dev/null +++ b/test/fixtures/21-server-props/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + generateBuildId() { + return 'testing-build-id'; + }, +}; diff --git a/test/fixtures/21-server-props/now.json b/test/fixtures/21-server-props/now.json new file mode 100644 index 00000000..49a82db6 --- /dev/null +++ b/test/fixtures/21-server-props/now.json @@ -0,0 +1,164 @@ +{ + "version": 2, + "builds": [{ "src": "package.json", "use": "@now/next" }], + "probes": [ + { + "path": "/lambda", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/forever", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/forever", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/another", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/another", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/blog/post-1", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/blog/post-1", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/blog/post-2", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/blog/post-2", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/blog/post-3", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/blog/post-3", + "status": 200, + "responseHeaders": { + "x-now-cache": "/MISS/" + } + }, + { + "path": "/blog/post-1/comment-1", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/blog/post-2/comment-2", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/blog/post-3/comment-3", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/_next/data/testing-build-id/lambda.json", + "status": 404 + }, + { + "path": "/_next/data/testing-build-id/another.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "/MISS/" + } + }, + { + "path": "/_next/data/testing-build-id/another2.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { "delay": 2000 }, + { + "path": "/_next/data/testing-build-id/another2.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/_next/data/testing-build-id/blog/post-1.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "/MISS/" + } + }, + { + "path": "/_next/data/testing-build-id/blog/post-1.json", + "status": 200, + "mustContain": "post-1" + }, + { + "path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json", + "status": 200, + "responseHeaders": { + "x-now-cache": "MISS" + } + }, + { + "path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json", + "status": 200, + "mustContain": "comment-1337" + }, + { + "path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json", + "status": 200, + "mustContain": "post-1337" + } + ] +} diff --git a/test/fixtures/21-server-props/package.json b/test/fixtures/21-server-props/package.json new file mode 100644 index 00000000..17a2cb2a --- /dev/null +++ b/test/fixtures/21-server-props/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "next": "9.2.3-canary.13", + "react": "^16.8.6", + "react-dom": "^16.8.6" + } +} diff --git a/test/fixtures/21-server-props/pages/another.js b/test/fixtures/21-server-props/pages/another.js new file mode 100644 index 00000000..bc8ad36d --- /dev/null +++ b/test/fixtures/21-server-props/pages/another.js @@ -0,0 +1,20 @@ +import React from 'react'; + +// eslint-disable-next-line camelcase +export async function unstable_getServerProps() { + return { + props: { + world: 'world', + time: new Date().getTime(), + }, + }; +} + +export default ({ world, time }) => { + return ( + <> +

hello: {world}

+ time: {time} + + ); +}; diff --git a/test/fixtures/21-server-props/pages/another2.js b/test/fixtures/21-server-props/pages/another2.js new file mode 100644 index 00000000..bc8ad36d --- /dev/null +++ b/test/fixtures/21-server-props/pages/another2.js @@ -0,0 +1,20 @@ +import React from 'react'; + +// eslint-disable-next-line camelcase +export async function unstable_getServerProps() { + return { + props: { + world: 'world', + time: new Date().getTime(), + }, + }; +} + +export default ({ world, time }) => { + return ( + <> +

hello: {world}

+ time: {time} + + ); +}; diff --git a/test/fixtures/21-server-props/pages/blog/[post]/[comment].js b/test/fixtures/21-server-props/pages/blog/[post]/[comment].js new file mode 100644 index 00000000..bb823d68 --- /dev/null +++ b/test/fixtures/21-server-props/pages/blog/[post]/[comment].js @@ -0,0 +1,22 @@ +import React from 'react'; + +// eslint-disable-next-line camelcase +export async function unstable_getServerProps ({ params }) { + return { + props: { + post: params.post, + comment: params.comment, + time: new Date().getTime(), + }, + }; +} + +export default ({ post, comment, time }) => { + return ( + <> +

Post: {post}

+

Comment: {comment}

+ time: {time} + + ); +}; diff --git a/test/fixtures/21-server-props/pages/blog/[post]/index.js b/test/fixtures/21-server-props/pages/blog/[post]/index.js new file mode 100644 index 00000000..cf7b35c3 --- /dev/null +++ b/test/fixtures/21-server-props/pages/blog/[post]/index.js @@ -0,0 +1,26 @@ +import React from 'react' + +// eslint-disable-next-line camelcase +export async function unstable_getServerProps ({ params }) { + if (params.post === 'post-10') { + await new Promise(resolve => { + setTimeout(() => resolve(), 1000) + }) + } + + return { + props: { + post: params.post, + time: (await import('perf_hooks')).performance.now() + }, + } +} + +export default ({ post, time }) => { + return ( + <> +

Post: {post}

+ time: {time} + + ) +} diff --git a/test/fixtures/21-server-props/pages/forever.js b/test/fixtures/21-server-props/pages/forever.js new file mode 100644 index 00000000..bc8ad36d --- /dev/null +++ b/test/fixtures/21-server-props/pages/forever.js @@ -0,0 +1,20 @@ +import React from 'react'; + +// eslint-disable-next-line camelcase +export async function unstable_getServerProps() { + return { + props: { + world: 'world', + time: new Date().getTime(), + }, + }; +} + +export default ({ world, time }) => { + return ( + <> +

hello: {world}

+ time: {time} + + ); +}; diff --git a/test/fixtures/21-server-props/pages/index.js b/test/fixtures/21-server-props/pages/index.js new file mode 100644 index 00000000..cdeee3e6 --- /dev/null +++ b/test/fixtures/21-server-props/pages/index.js @@ -0,0 +1 @@ +export default () => 'Hi'; diff --git a/test/fixtures/21-server-props/pages/lambda.js b/test/fixtures/21-server-props/pages/lambda.js new file mode 100644 index 00000000..c2a3ce85 --- /dev/null +++ b/test/fixtures/21-server-props/pages/lambda.js @@ -0,0 +1,5 @@ +const Page = ({ data }) =>

{data} world

; + +Page.getInitialProps = () => ({ data: 'hello' }); + +export default Page;