From efd3cc05dcf3993a68eb2ac41f39d42ed905b036 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 24 Oct 2023 13:15:46 -0700 Subject: [PATCH] [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. --- .changeset/happy-wolves-exist.md | 5 ++ .../jest.config.js | 5 ++ .../gatsby-plugin-vercel-builder/package.json | 3 + .../src/handlers/build.ts | 3 + .../src/helpers/functions.ts | 2 +- .../templates/ssr-handler.js | 30 ++------- .../templates/utils.ts | 29 +++++++++ .../test/tsconfig.json | 7 ++ .../test/unit.get-page-name.test.ts | 64 +++++++++++++++++++ .../fixtures/gatsby-v5-pathPrefix/probes.json | 4 ++ .../src/pages/foo/bar/ssr.js | 21 ++++++ .../test/fixtures/gatsby-v5/probes.json | 4 ++ .../gatsby-v5/src/pages/foo/bar/ssr.js | 21 ++++++ pnpm-lock.yaml | 3 + 14 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 .changeset/happy-wolves-exist.md create mode 100644 packages/gatsby-plugin-vercel-builder/jest.config.js create mode 100644 packages/gatsby-plugin-vercel-builder/templates/utils.ts create mode 100644 packages/gatsby-plugin-vercel-builder/test/tsconfig.json create mode 100644 packages/gatsby-plugin-vercel-builder/test/unit.get-page-name.test.ts create mode 100644 packages/static-build/test/fixtures/gatsby-v5-pathPrefix/src/pages/foo/bar/ssr.js create mode 100644 packages/static-build/test/fixtures/gatsby-v5/src/pages/foo/bar/ssr.js diff --git a/.changeset/happy-wolves-exist.md b/.changeset/happy-wolves-exist.md new file mode 100644 index 000000000..c6d297ffc --- /dev/null +++ b/.changeset/happy-wolves-exist.md @@ -0,0 +1,5 @@ +--- +'@vercel/gatsby-plugin-vercel-builder': patch +--- + +Fix nested SSR routes diff --git a/packages/gatsby-plugin-vercel-builder/jest.config.js b/packages/gatsby-plugin-vercel-builder/jest.config.js new file mode 100644 index 000000000..07978f56c --- /dev/null +++ b/packages/gatsby-plugin-vercel-builder/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/packages/gatsby-plugin-vercel-builder/package.json b/packages/gatsby-plugin-vercel-builder/package.json index 72837490b..8a3c6b101 100644 --- a/packages/gatsby-plugin-vercel-builder/package.json +++ b/packages/gatsby-plugin-vercel-builder/package.json @@ -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", diff --git a/packages/gatsby-plugin-vercel-builder/src/handlers/build.ts b/packages/gatsby-plugin-vercel-builder/src/handlers/build.ts index 8fa8c13fe..6d72151a7 100644 --- a/packages/gatsby-plugin-vercel-builder/src/handlers/build.ts +++ b/packages/gatsby-plugin-vercel-builder/src/handlers/build.ts @@ -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) { diff --git a/packages/gatsby-plugin-vercel-builder/src/helpers/functions.ts b/packages/gatsby-plugin-vercel-builder/src/helpers/functions.ts index 18bad2a77..305b7bd02 100644 --- a/packages/gatsby-plugin-vercel-builder/src/helpers/functions.ts +++ b/packages/gatsby-plugin-vercel-builder/src/helpers/functions.ts @@ -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 }), diff --git a/packages/gatsby-plugin-vercel-builder/templates/ssr-handler.js b/packages/gatsby-plugin-vercel-builder/templates/ssr-handler.js index f7d2edf0a..1e8320d29 100644 --- a/packages/gatsby-plugin-vercel-builder/templates/ssr-handler.js +++ b/packages/gatsby-plugin-vercel-builder/templates/ssr-handler.js @@ -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, }); diff --git a/packages/gatsby-plugin-vercel-builder/templates/utils.ts b/packages/gatsby-plugin-vercel-builder/templates/utils.ts new file mode 100644 index 000000000..7b6373700 --- /dev/null +++ b/packages/gatsby-plugin-vercel-builder/templates/utils.ts @@ -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 }; +} diff --git a/packages/gatsby-plugin-vercel-builder/test/tsconfig.json b/packages/gatsby-plugin-vercel-builder/test/tsconfig.json new file mode 100644 index 000000000..83d6f622d --- /dev/null +++ b/packages/gatsby-plugin-vercel-builder/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "sourceMap": true + }, + "extends": "../tsconfig.json", + "include": ["*.test.ts"] +} diff --git a/packages/gatsby-plugin-vercel-builder/test/unit.get-page-name.test.ts b/packages/gatsby-plugin-vercel-builder/test/unit.get-page-name.test.ts new file mode 100644 index 000000000..ce03b5939 --- /dev/null +++ b/packages/gatsby-plugin-vercel-builder/test/unit.get-page-name.test.ts @@ -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); + } + ); +}); diff --git a/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/probes.json b/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/probes.json index 0bd6ffa44..3133a0dfe 100644 --- a/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/probes.json +++ b/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/probes.json @@ -12,6 +12,10 @@ { "path": "/foo/x/y/z", "mustContain": "

Page not found

" + }, + { + "path": "/foo/foo/bar/ssr/", + "mustContain": "

This page is rendered server side (nested)

" } ] } diff --git a/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/src/pages/foo/bar/ssr.js b/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/src/pages/foo/bar/ssr.js new file mode 100644 index 000000000..e8812273e --- /dev/null +++ b/packages/static-build/test/fixtures/gatsby-v5-pathPrefix/src/pages/foo/bar/ssr.js @@ -0,0 +1,21 @@ +import * as React from 'react'; + +const UsingSSR = ({ serverData }) => { + return ( + <> +

+ This page is {serverData.message} +

+ + ); +}; + +export const Head = () => SSR Gatsby; + +export default UsingSSR; + +export async function getServerData() { + return { + props: { message: 'rendered server side (nested)' }, + } +} diff --git a/packages/static-build/test/fixtures/gatsby-v5/probes.json b/packages/static-build/test/fixtures/gatsby-v5/probes.json index 8b6c61e96..720ab16d0 100644 --- a/packages/static-build/test/fixtures/gatsby-v5/probes.json +++ b/packages/static-build/test/fixtures/gatsby-v5/probes.json @@ -20,6 +20,10 @@ { "path": "/x/y/z", "mustContain": "

Page not found

" + }, + { + "path": "/foo/bar/ssr/", + "mustContain": "

This page is rendered server side (nested)

" } ] } diff --git a/packages/static-build/test/fixtures/gatsby-v5/src/pages/foo/bar/ssr.js b/packages/static-build/test/fixtures/gatsby-v5/src/pages/foo/bar/ssr.js new file mode 100644 index 000000000..e8812273e --- /dev/null +++ b/packages/static-build/test/fixtures/gatsby-v5/src/pages/foo/bar/ssr.js @@ -0,0 +1,21 @@ +import * as React from 'react'; + +const UsingSSR = ({ serverData }) => { + return ( + <> +

+ This page is {serverData.message} +

+ + ); +}; + +export const Head = () => SSR Gatsby; + +export default UsingSSR; + +export async function getServerData() { + return { + props: { message: 'rendered server side (nested)' }, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 833ce7779..f32b0d913 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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