[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": { "scripts": {
"build": "node ../../utils/build-builder.mjs", "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" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@@ -27,6 +29,7 @@
"devDependencies": { "devDependencies": {
"@types/etag": "1.8.0", "@types/etag": "1.8.0",
"@types/fs-extra": "11.0.1", "@types/fs-extra": "11.0.1",
"@types/jest": "27.5.1",
"@types/node": "14.18.33", "@types/node": "14.18.33",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import os from 'os'; import os from 'os';
import etag from 'etag'; import etag from 'etag';
import { parse } from 'url'; import { join } from 'path';
import { copySync, existsSync } from 'fs-extra'; 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 TMP_DATA_PATH = join(os.tmpdir(), 'data/datastore');
const CUR_DATA_PATH = join(__dirname, '.cache/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) { export default async function handler(req, res) {
let pageName; // eslint-disable-next-line no-undef
const pathname = parse(req.url).pathname || '/'; const { pathName, isPageData } = getPageName(req.url, vercel_pathPrefix);
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 = '/';
}
}
const [graphqlEngine, { getData, renderHTML, renderPageData }] = const [graphqlEngine, { getData, renderHTML, renderPageData }] =
await Promise.all([getGraphQLEngine(), getPageSSRHelpers()]); await Promise.all([getGraphQLEngine(), getPageSSRHelpers()]);
const data = await getData({ const data = await getData({
pathName: pageName, pathName,
graphqlEngine, graphqlEngine,
req, 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", "path": "/foo/x/y/z",
"mustContain": "<h1>Page not found</h1>" "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", "path": "/x/y/z",
"mustContain": "<h1>Page not found</h1>" "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': '@types/fs-extra':
specifier: 11.0.1 specifier: 11.0.1
version: 11.0.1 version: 11.0.1
'@types/jest':
specifier: 27.5.1
version: 27.5.1
'@types/node': '@types/node':
specifier: 14.18.33 specifier: 14.18.33
version: 14.18.33 version: 14.18.33