Skip to content

Commit

Permalink
[now-next] Add support for using the fallback file for dynamic SSG pa…
Browse files Browse the repository at this point in the history
…ges (#3752)

This adds support for using the fallback file for dynamic (lazy) SSG pages to allow showing a loading state while the cache is populated with data. We can add tests for this behavior once the below referenced PR is shipped on canary

x-ref: vercel/next.js#10424

~Update: added tests now that fallback support has landed in Next.js although it appears `now-proxy` might need to be updated to revalidate the fallback correctly since the initial fallback is always being shown for lazy SSG pages with these changes~

Looks like this is working properly in `now-proxy` and we were overriding the prerender output with the static page output, thanks @juancampa for helping investigate! 🙏
  • Loading branch information
ijjk committed Feb 13, 2020
1 parent 69cb8cb commit bba9d4e
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 14 deletions.
29 changes: 17 additions & 12 deletions src/index.ts
Expand Up @@ -612,13 +612,12 @@ export const build = async ({

// Prerendered routes emit a `.html` file but should not be treated as a
// static page.
// Lazily prerendered routes do not have a `.html` file so we don't need
// to check/skip it here.
// Lazily prerendered routes have a fallback `.html` file on newer
// Next.js versions so we need to also not treat it as a static page here.
if (
Object.prototype.hasOwnProperty.call(
prerenderManifest.routes,
routeName
)
prerenderManifest.routes[routeName] ||
(prerenderManifest.lazyRoutes[routeName] &&
prerenderManifest.lazyRoutes[routeName].fallback)
) {
return;
}
Expand Down Expand Up @@ -848,12 +847,18 @@ export const build = async ({
const onPrerenderRoute = (routeKey: string, isLazy: boolean) => {
// Get the route file as it'd be mounted in the builder output
const routeFileNoExt = routeKey === '/' ? '/index' : routeKey;

const htmlFsRef = isLazy
? null
: new FileFsRef({
fsPath: path.join(pagesDir, `${routeFileNoExt}.html`),
});
const lazyHtmlFallback =
isLazy && prerenderManifest.lazyRoutes[routeKey].fallback;

const htmlFsRef =
isLazy && !lazyHtmlFallback
? null
: new FileFsRef({
fsPath: path.join(
pagesDir,
`${lazyHtmlFallback || routeFileNoExt + '.html'}`
),
});
const jsonFsRef = isLazy
? null
: new FileFsRef({
Expand Down
10 changes: 9 additions & 1 deletion src/utils.ts
Expand Up @@ -623,6 +623,7 @@ export type NextPrerenderedRoutes = {

lazyRoutes: {
[route: string]: {
fallback?: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
Expand Down Expand Up @@ -726,6 +727,7 @@ export async function getPrerenderManifest(
};
dynamicRoutes: {
[key: string]: {
fallback?: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
Expand Down Expand Up @@ -759,11 +761,17 @@ export async function getPrerenderManifest(
lazyRoutes.forEach(lazyRoute => {
const {
routeRegex,
fallback,
dataRoute,
dataRouteRegex,
} = manifest.dynamicRoutes[lazyRoute];

ret.lazyRoutes[lazyRoute] = { routeRegex, dataRoute, dataRouteRegex };
ret.lazyRoutes[lazyRoute] = {
routeRegex,
fallback,
dataRoute,
dataRouteRegex,
};
});

return ret;
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/18-ssg-fallback-support/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};
170 changes: 170 additions & 0 deletions test/fixtures/18-ssg-fallback-support/now.json
@@ -0,0 +1,170 @@
{
"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": "HIT"
}
},
{
"path": "/another",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/another",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "loading..."
},
{ "delay": 2000 },
{
"path": "/blog/post-3",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE/"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "post-3"
},
{
"path": "/blog/post-1/comment-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{
"path": "/blog/post-2/comment-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "loading..."
},
{
"path": "/_next/data/testing-build-id/lambda.json",
"status": 404
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "MISS"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "comment-3"
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/_next/data/testing-build-id/another.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE/"
}
},
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/_next/data/testing-build-id/blog/post-1.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE|PRERENDER/"
}
},
{
"path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
}
]
}
7 changes: 7 additions & 0 deletions test/fixtures/18-ssg-fallback-support/package.json
@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "9.2.2-canary.16",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}
21 changes: 21 additions & 0 deletions test/fixtures/18-ssg-fallback-support/pages/another.js
@@ -0,0 +1,21 @@
import React from 'react';

// eslint-disable-next-line camelcase
export async function unstable_getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
revalidate: 5,
};
}

export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};
21 changes: 21 additions & 0 deletions test/fixtures/18-ssg-fallback-support/pages/another2.js
@@ -0,0 +1,21 @@
import React from 'react';

// eslint-disable-next-line camelcase
export async function unstable_getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
revalidate: 5,
};
}

export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};
@@ -0,0 +1,36 @@
import React from 'react';

// eslint-disable-next-line camelcase
export async function unstable_getStaticPaths () {
return {
paths: [
'/blog/post-1/comment-1',
{ params: { post: 'post-2', comment: 'comment-2' } },
'/blog/post-1337/comment-1337',
]
}
}

// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ params }) {
return {
props: {
post: params.post,
comment: params.comment,
time: new Date().getTime(),
},
revalidate: 2,
};
}

export default ({ post, comment, time }) => {
if (!post) return <p>loading...</p>

return (
<>
<p>Post: {post}</p>
<p>Comment: {comment}</p>
<span>time: {time}</span>
</>
);
};
40 changes: 40 additions & 0 deletions test/fixtures/18-ssg-fallback-support/pages/blog/[post]/index.js
@@ -0,0 +1,40 @@
import React from 'react'

// eslint-disable-next-line camelcase
export async function unstable_getStaticPaths () {
return {
paths: [
'/blog/post-1',
{ params: { post: 'post-2' } },
]
}
}


// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ 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()
},
revalidate: 10
}
}

export default ({ post, time }) => {
if (!post) return <p>loading...</p>

return (
<>
<p>Post: {post}</p>
<span>time: {time}</span>
</>
)
}

0 comments on commit bba9d4e

Please sign in to comment.