[gatsby-plugin-vercel-builder] Fix nested SSR routes (#10751)

Fixes SSR / DSG pages that are nested deeper than the root path for Gatsby projects.

Also introduces unit tests for the logic related to determining which page name to use.
This commit is contained in:
Nathan Rajlich
2023-10-24 13:15:46 -07:00
committed by GitHub
parent a732b64c02
commit efd3cc05dc
14 changed files with 175 additions and 26 deletions

View File

@@ -0,0 +1,5 @@
---
'@vercel/gatsby-plugin-vercel-builder': patch
---
Fix nested SSR routes

View File

@@ -0,0 +1,5 @@
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@@ -14,6 +14,8 @@
},
"scripts": {
"build": "node ../../utils/build-builder.mjs",
"test": "jest --reporters=default --reporters=jest-junit --env node --verbose --bail --runInBand",
"test-unit": "pnpm test test/unit.*test.*",
"type-check": "tsc --noEmit"
},
"dependencies": {
@@ -27,6 +29,7 @@
"devDependencies": {
"@types/etag": "1.8.0",
"@types/fs-extra": "11.0.1",
"@types/jest": "27.5.1",
"@types/node": "14.18.33",
"@types/react": "18.0.26",
"jest-junit": "16.0.0",

View File

@@ -17,9 +17,11 @@ import type {
export const writeHandler = async ({
outDir,
handlerFile,
prefix = '',
}: {
outDir: string;
handlerFile: string;
prefix?: string;
}) => {
const { major } = await getNodeVersion(process.cwd());
@@ -35,6 +37,7 @@ export const writeHandler = async ({
minify: true,
define: {
'process.env.NODE_ENV': "'production'",
vercel_pathPrefix: JSON.stringify(prefix),
},
});
} catch (e: any) {

View File

@@ -37,7 +37,7 @@ export async function createServerlessFunctions(
await ensureDir(functionDir);
await Promise.all([
writeHandler({ outDir: functionDir, handlerFile }),
writeHandler({ outDir: functionDir, handlerFile, prefix }),
copyFunctionLibs({ functionDir }),
copyHTMLFiles({ functionDir }),
writeVCConfig({ functionDir }),

View File

@@ -1,8 +1,8 @@
import os from 'os';
import etag from 'etag';
import { parse } from 'url';
import { join } from 'path';
import { copySync, existsSync } from 'fs-extra';
import { join, dirname, basename } from 'path';
import { getPageName } from './utils';
const TMP_DATA_PATH = join(os.tmpdir(), 'data/datastore');
const CUR_DATA_PATH = join(__dirname, '.cache/data/datastore');
@@ -25,34 +25,14 @@ async function getPageSSRHelpers() {
}
export default async function handler(req, res) {
let pageName;
const pathname = parse(req.url).pathname || '/';
const isPageData = pathname.startsWith('/page-data/');
if (isPageData) {
// /page-data/index/page-data.json
// /page-data/using-ssr/page-data.json
pageName = basename(dirname(pathname));
if (pageName === 'index') {
pageName = '/';
}
} else {
// /using-ssr
// /using-ssr/
// /using-ssr/index.html
pageName = basename(pathname);
if (pageName === 'index.html') {
pageName = basename(dirname(pathname));
}
if (!pageName) {
pageName = '/';
}
}
// eslint-disable-next-line no-undef
const { pathName, isPageData } = getPageName(req.url, vercel_pathPrefix);
const [graphqlEngine, { getData, renderHTML, renderPageData }] =
await Promise.all([getGraphQLEngine(), getPageSSRHelpers()]);
const data = await getData({
pathName: pageName,
pathName,
graphqlEngine,
req,
});

View File

@@ -0,0 +1,29 @@
import { parse } from 'url';
import { basename, dirname } from 'path';
export function getPageName(url: string, pathPrefix = '') {
let pathName = (parse(url).pathname || '/').slice(pathPrefix.length);
const isPageData = pathName.startsWith('/page-data/');
if (isPageData) {
// "/page-data/index/page-data.json" -> "/"
// "/page-data/using-ssr/page-data.json" -> "using-ssr"
// "/page-data/foo/bar/ssr/page-data.json" -> "foo/bar/ssr"
pathName = pathName.split('/').slice(2, -1).join('/');
if (pathName === 'index') {
pathName = '/';
}
} else {
// "/using-ssr" -> "using-ssr"
// "/using-ssr/" -> "using-ssr"
// "/using-ssr/index.html" -> "using-ssr"
// "/foo/bar/ssr" -> "foo/bar/ssr"
if (basename(pathName) === 'index.html') {
pathName = dirname(pathName);
}
if (pathName !== '/') {
// Remove leading and trailing "/"
pathName = pathName.replace(/(^\/|\/$)/g, '');
}
}
return { isPageData, pathName };
}

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"sourceMap": true
},
"extends": "../tsconfig.json",
"include": ["*.test.ts"]
}

View File

@@ -0,0 +1,64 @@
import { getPageName } from '../templates/utils';
describe('getPageName()', () => {
it.each([
{
input: '/page-data/index/page-data.json',
pageName: '/',
isPageData: true,
},
{
input: '/page-data/using-ssr/page-data.json',
pageName: 'using-ssr',
isPageData: true,
},
{ input: '/', pageName: '/', isPageData: false },
{ input: '/index.html', pageName: '/', isPageData: false },
{ input: '/using-ssr', pageName: 'using-ssr', isPageData: false },
{ input: '/using-ssr/', pageName: 'using-ssr', isPageData: false },
{
input: '/using-ssr/index.html',
pageName: 'using-ssr',
isPageData: false,
},
{ input: '/foo/bar/ssr', pageName: 'foo/bar/ssr', isPageData: false },
{
input: '/page-data/foo/bar/ssr/page-data.json',
pageName: 'foo/bar/ssr',
isPageData: true,
},
{ input: '/foo/', pathPrefix: '/foo', pageName: '/', isPageData: false },
{
input: '/foo/index.html',
pathPrefix: '/foo',
pageName: '/',
isPageData: false,
},
{
input: '/foo/bar/ssr',
pathPrefix: '/foo/',
pageName: 'bar/ssr',
isPageData: false,
},
{
input: '/foo/page-data/index/page-data.json',
pathPrefix: '/foo',
pageName: '/',
isPageData: true,
},
{
input: '/foo/page-data/bar/ssr/page-data.json',
pathPrefix: '/foo',
pageName: 'bar/ssr',
isPageData: true,
},
])(
'Should return "$pageName" for "$input"',
({ input, pathPrefix, pageName, isPageData }) => {
const actual = getPageName(input, pathPrefix);
expect(actual.pathName).toEqual(pageName);
expect(actual.isPageData).toEqual(isPageData);
}
);
});

View File

@@ -12,6 +12,10 @@
{
"path": "/foo/x/y/z",
"mustContain": "<h1>Page not found</h1>"
},
{
"path": "/foo/foo/bar/ssr/",
"mustContain": "<h1>This page is <!-- -->rendered server side (nested)</h1>"
}
]
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
const UsingSSR = ({ serverData }) => {
return (
<>
<h1>
This page is {serverData.message}
</h1>
</>
);
};
export const Head = () => <title>SSR Gatsby</title>;
export default UsingSSR;
export async function getServerData() {
return {
props: { message: 'rendered server side (nested)' },
}
}

View File

@@ -20,6 +20,10 @@
{
"path": "/x/y/z",
"mustContain": "<h1>Page not found</h1>"
},
{
"path": "/foo/bar/ssr/",
"mustContain": "<h1>This page is <!-- -->rendered server side (nested)</h1>"
}
]
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
const UsingSSR = ({ serverData }) => {
return (
<>
<h1>
This page is {serverData.message}
</h1>
</>
);
};
export const Head = () => <title>SSR Gatsby</title>;
export default UsingSSR;
export async function getServerData() {
return {
props: { message: 'rendered server side (nested)' },
}
}

3
pnpm-lock.yaml generated
View File

@@ -969,6 +969,9 @@ importers:
'@types/fs-extra':
specifier: 11.0.1
version: 11.0.1
'@types/jest':
specifier: 27.5.1
version: 27.5.1
'@types/node':
specifier: 14.18.33
version: 14.18.33