diff --git a/.changeset/calm-terms-dance.md b/.changeset/calm-terms-dance.md new file mode 100644 index 000000000..8e2bf31b9 --- /dev/null +++ b/.changeset/calm-terms-dance.md @@ -0,0 +1,5 @@ +--- +'@vercel/next': patch +--- + +Enable partial prerendering support for pre-generated pages diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index a4ea20e8d..d46ad6d32 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1142,6 +1142,10 @@ export const build: BuildV2 = async ({ appPathRoutesManifest, }); + /** + * This is a detection for preview mode that's required for the pages + * router. + */ const canUsePreviewMode = Object.keys(pages).some(page => isApiPage(pages[page].fsPath) ); @@ -1316,6 +1320,22 @@ export const build: BuildV2 = async ({ } } + /** + * All of the routes that have `experimentalPPR` enabled. + */ + const experimentalPPRRoutes = new Set(); + + for (const [route, { experimentalPPR }] of [ + ...Object.entries(prerenderManifest.staticRoutes), + ...Object.entries(prerenderManifest.blockingFallbackRoutes), + ...Object.entries(prerenderManifest.fallbackRoutes), + ...Object.entries(prerenderManifest.omittedRoutes), + ]) { + if (!experimentalPPR) continue; + + experimentalPPRRoutes.add(route); + } + if (requiredServerFilesManifest) { if (!routesManifest) { throw new Error( @@ -1371,6 +1391,7 @@ export const build: BuildV2 = async ({ hasIsr404Page, hasIsr500Page, variantsManifest, + experimentalPPRRoutes, }); } @@ -1883,17 +1904,18 @@ export const build: BuildV2 = async ({ ); } - dynamicRoutes = await getDynamicRoutes( + dynamicRoutes = await getDynamicRoutes({ entryPath, entryDirectory, dynamicPages, - false, + isDev: false, routesManifest, - omittedPrerenderRoutes, + omittedRoutes: omittedPrerenderRoutes, canUsePreviewMode, - prerenderManifest.bypassToken || '', - isServerMode - ).then(arr => + bypassToken: prerenderManifest.bypassToken || '', + isServerMode, + experimentalPPRRoutes, + }).then(arr => localizeDynamicRoutes( arr, dynamicPrefix, @@ -1912,17 +1934,18 @@ export const build: BuildV2 = async ({ // we need to include the prerenderManifest.omittedRoutes here // for the page to be able to be matched in the lambda for preview mode - const completeDynamicRoutes = await getDynamicRoutes( + const completeDynamicRoutes = await getDynamicRoutes({ entryPath, entryDirectory, dynamicPages, - false, + isDev: false, routesManifest, - undefined, + omittedRoutes: undefined, canUsePreviewMode, - prerenderManifest.bypassToken || '', - isServerMode - ).then(arr => + bypassToken: prerenderManifest.bypassToken || '', + isServerMode, + experimentalPPRRoutes, + }).then(arr => arr.map(route => { route.src = route.src.replace('^', `^${dynamicPrefix}`); return route; @@ -2119,22 +2142,33 @@ export const build: BuildV2 = async ({ appPathRoutesManifest, isSharedLambdas, canUsePreviewMode, + omittedPrerenderRoutes, }); - Object.keys(prerenderManifest.staticRoutes).forEach(route => - prerenderRoute(route, { isBlocking: false, isFallback: false }) + await Promise.all( + Object.keys(prerenderManifest.staticRoutes).map(route => + prerenderRoute(route, {}) + ) ); - Object.keys(prerenderManifest.fallbackRoutes).forEach(route => - prerenderRoute(route, { isBlocking: false, isFallback: true }) + + await Promise.all( + Object.keys(prerenderManifest.fallbackRoutes).map(route => + prerenderRoute(route, { isFallback: true }) + ) ); - Object.keys(prerenderManifest.blockingFallbackRoutes).forEach(route => - prerenderRoute(route, { isBlocking: true, isFallback: false }) + + await Promise.all( + Object.keys(prerenderManifest.blockingFallbackRoutes).map(route => + prerenderRoute(route, { isBlocking: true }) + ) ); if (static404Page && canUsePreviewMode) { - omittedPrerenderRoutes.forEach(route => { - prerenderRoute(route, { isOmitted: true }); - }); + await Promise.all( + Array.from(omittedPrerenderRoutes).map(route => + prerenderRoute(route, { isOmitted: true }) + ) + ); } // We still need to use lazyRoutes if the dataRoutes field diff --git a/packages/next/src/server-build.ts b/packages/next/src/server-build.ts index c1c38552a..5e1ea235c 100644 --- a/packages/next/src/server-build.ts +++ b/packages/next/src/server-build.ts @@ -52,6 +52,7 @@ import { RSC_PREFETCH_SUFFIX, normalizePrefetches, CreateLambdaFromPseudoLayersOptions, + getPostponeResumePathname, } from './utils'; import { nodeFileTrace, @@ -142,6 +143,7 @@ export async function serverBuild({ lambdaCompressedByteLimit, requiredServerFilesManifest, variantsManifest, + experimentalPPRRoutes, }: { appPathRoutesManifest?: Record; dynamicPages: string[]; @@ -183,6 +185,7 @@ export async function serverBuild({ prerenderManifest: NextPrerenderedRoutes; requiredServerFilesManifest: NextRequiredServerFilesManifest; variantsManifest: VariantsManifestLegacy | null; + experimentalPPRRoutes: ReadonlySet; }): Promise { lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths); @@ -353,18 +356,6 @@ export async function serverBuild({ internalPages.push('404.js'); } - const experimentalPPRRoutes = new Set(); - - for (const [route, { experimentalPPR }] of [ - ...Object.entries(prerenderManifest.staticRoutes), - ...Object.entries(prerenderManifest.blockingFallbackRoutes), - ...Object.entries(prerenderManifest.fallbackRoutes), - ]) { - if (!experimentalPPR) continue; - - experimentalPPRRoutes.add(route); - } - const prerenderRoutes: ReadonlySet = new Set([ ...(canUsePreviewMode ? omittedPrerenderRoutes : []), ...Object.keys(prerenderManifest.blockingFallbackRoutes), @@ -1185,7 +1176,7 @@ export async function serverBuild({ // lambda for the page for revalidation. let revalidate: NodejsLambda | undefined; if (isPPR) { - if (isPPR && !options.isStreaming) { + if (!options.isStreaming) { throw new Error("Invariant: PPR lambda isn't streaming"); } @@ -1197,24 +1188,28 @@ export async function serverBuild({ }); } - for (const page of group.pages) { - const pageNoExt = page.replace(/\.js$/, ''); - let isPrerender = prerenderRoutes.has( - path.join('/', pageNoExt === 'index' ? '' : pageNoExt) - ); + for (const pageFilename of group.pages) { + // This is the name of the page, where the root is `index`. + const pageName = pageFilename.replace(/\.js$/, ''); + + // This is the name of the page prefixed with a `/`, where the root is + // `/index`. + const pagePath = path.posix.join('/', pageName); + + // This is the routable pathname for the page, where the root is `/`. + const pagePathname = pagePath === '/index' ? '/' : pagePath; + + let isPrerender = prerenderRoutes.has(pagePathname); if (!isPrerender && routesManifest?.i18n) { isPrerender = routesManifest.i18n.locales.some(locale => { return prerenderRoutes.has( - path.join('/', locale, pageNoExt === 'index' ? '' : pageNoExt) + path.join('/', locale, pageName === 'index' ? '' : pageName) ); }); } - let outputName = path.posix.join(entryDirectory, pageNoExt); - if (!group.isAppRouter && !group.isAppRouteHandler) { - outputName = normalizeIndexOutput(outputName, true); - } + let outputName = path.posix.join(entryDirectory, pageName); // If this is a PPR page, then we should prefix the output name. if (isPPR) { @@ -1222,24 +1217,56 @@ export async function serverBuild({ throw new Error("Invariant: PPR lambda isn't set"); } - // Get the get the base path prefixed route, without the index - // normalization. - outputName = path.posix.join(entryDirectory, pageNoExt); + // Assign the revalidate lambda to the output name. That's used to + // perform the initial static shell render. lambdas[outputName] = revalidate; - const pprOutputName = path.posix.join( - entryDirectory, - '/_next/postponed/resume', - pageNoExt - ); - lambdas[pprOutputName] = lambda; + // If this isn't an omitted page, then we should add the link from the + // page to the postpone resume lambda. + if (!omittedPrerenderRoutes.has(pagePathname)) { + const key = getPostponeResumePathname(entryDirectory, pageName); + lambdas[key] = lambda; + + // We want to add the `experimentalStreamingLambdaPath` to this + // output. + experimentalStreamingLambdaPaths.set(outputName, key); + } else { + // As this is an omitted page, we should generate the experimental + // partial prerendering resume route for each of these routes that + // support partial prerendering. This is because the routes that + // haven't been omitted will have rewrite rules in place to rewrite + // the original request `/blog/my-slug` to the dynamic path + // `/blog/[slug]?nxtPslug=my-slug`. + for (const [ + routePathname, + { srcRoute, experimentalPPR }, + ] of Object.entries(prerenderManifest.staticRoutes)) { + // If the srcRoute doesn't match or this doesn't support + // experimental partial prerendering, then we can skip this route. + if (srcRoute !== pagePathname || !experimentalPPR) continue; + + // If this route is the same as the page route, then we can skip + // it, because we've already added the lambda to the output. + if (routePathname === pagePathname) continue; + + const key = getPostponeResumePathname( + entryDirectory, + routePathname + ); + lambdas[key] = lambda; + + outputName = path.posix.join(entryDirectory, routePathname); + experimentalStreamingLambdaPaths.set(outputName, key); + } + } - // We want to add the `experimentalStreamingLambdaPath` to this - // output. - experimentalStreamingLambdaPaths.set(outputName, pprOutputName); continue; } + if (!group.isAppRouter && !group.isAppRouteHandler) { + outputName = normalizeIndexOutput(outputName, true); + } + // we add locale prefixed outputs for SSR pages, // this is handled in onPrerenderRoute for SSG pages if ( @@ -1247,7 +1274,7 @@ export async function serverBuild({ !isPrerender && !group.isAppRouter && (!isCorrectLocaleAPIRoutes || - !(pageNoExt === 'api' || pageNoExt.startsWith('api/'))) + !(pageName === 'api' || pageName.startsWith('api/'))) ) { for (const locale of i18n.locales) { lambdas[ @@ -1255,7 +1282,7 @@ export async function serverBuild({ path.posix.join( entryDirectory, locale, - pageNoExt === 'index' ? '' : pageNoExt + pageName === 'index' ? '' : pageName ), true ) @@ -1288,6 +1315,7 @@ export async function serverBuild({ hasPages404: routesManifest.pages404, isCorrectNotFoundRoutes, isEmptyAllowQueryForPrendered, + omittedPrerenderRoutes, }); await Promise.all( @@ -1295,11 +1323,13 @@ export async function serverBuild({ prerenderRoute(route, {}) ) ); + await Promise.all( Object.keys(prerenderManifest.fallbackRoutes).map(route => prerenderRoute(route, { isFallback: true }) ) ); + await Promise.all( Object.keys(prerenderManifest.blockingFallbackRoutes).map(route => prerenderRoute(route, { isBlocking: true }) @@ -1308,9 +1338,9 @@ export async function serverBuild({ if (static404Page && canUsePreviewMode) { await Promise.all( - [...omittedPrerenderRoutes].map(route => { - return prerenderRoute(route, { isOmitted: true }); - }) + Array.from(omittedPrerenderRoutes).map(route => + prerenderRoute(route, { isOmitted: true }) + ) ); } @@ -1319,6 +1349,7 @@ export async function serverBuild({ if (routesManifest?.i18n) { route = normalizeLocalePath(route, routesManifest.i18n.locales).pathname; } + delete lambdas[ normalizeIndexOutput( path.posix.join('./', entryDirectory, route === '/' ? '/index' : route), @@ -1342,19 +1373,19 @@ export async function serverBuild({ middleware.staticRoutes.length > 0 && semver.gte(nextVersion, NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION); - const dynamicRoutes = await getDynamicRoutes( + const dynamicRoutes = await getDynamicRoutes({ entryPath, entryDirectory, dynamicPages, - false, + isDev: false, routesManifest, - omittedPrerenderRoutes, + omittedRoutes: omittedPrerenderRoutes, canUsePreviewMode, - prerenderManifest.bypassToken || '', - true, - middleware.dynamicRouteMap, - experimental.ppr - ).then(arr => + bypassToken: prerenderManifest.bypassToken || '', + isServerMode: true, + dynamicMiddlewareRouteMap: middleware.dynamicRouteMap, + experimentalPPRRoutes, + }).then(arr => localizeDynamicRoutes( arr, dynamicPrefix, @@ -1560,6 +1591,46 @@ export async function serverBuild({ throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'"); } + // If we're using the Experimental Partial Prerendering, we should ensure that + // all the routes that support it (and are listed) have configured lambdas. + // This only applies to routes that do not have fallbacks enabled (these are + // routes that have `dynamicParams = false` defined. + if (experimental.ppr) { + for (const { srcRoute, dataRoute, experimentalPPR } of Object.values( + prerenderManifest.staticRoutes + )) { + // Only apply this to the routes that support experimental PPR and + // that also have their `dataRoute` and `srcRoute` defined. + if (!experimentalPPR || !dataRoute || !srcRoute) continue; + + // If the srcRoute is not omitted, then we don't need to do anything. This + // is the indicator that a route should only have it's prerender defined + // and not a lambda. + if (!omittedPrerenderRoutes.has(srcRoute)) continue; + + // The lambda paths have their leading `/` stripped. + const srcPathname = srcRoute.substring(1); + const dataPathname = dataRoute.substring(1); + + // If we already have an associated lambda for the `.rsc` route, then + // we can skip this. + const dataPathnameExists = dataPathname in lambdas; + if (dataPathnameExists) continue; + + // We require that the source route has a lambda associated with it. If + // it doesn't this is an error. + const srcPathnameExists = srcPathname in lambdas; + if (!srcPathnameExists) { + throw new Error( + `Invariant: Expected to have a lambda for the source route: ${srcPathname}` + ); + } + + // Associate the data pathname with the source pathname's lambda. + lambdas[dataPathname] = lambdas[srcPathname]; + } + } + return { wildcard: wildcardConfig, images: getImagesConfig(imagesManifest), diff --git a/packages/next/src/utils.ts b/packages/next/src/utils.ts index f602c43da..e72abcf2a 100644 --- a/packages/next/src/utils.ts +++ b/packages/next/src/utils.ts @@ -304,19 +304,31 @@ export async function getRoutesManifest( return routesManifest; } -export async function getDynamicRoutes( - entryPath: string, - entryDirectory: string, - dynamicPages: ReadonlyArray, - isDev?: boolean, - routesManifest?: RoutesManifest, - omittedRoutes?: ReadonlySet, - canUsePreviewMode?: boolean, - bypassToken?: string, - isServerMode?: boolean, - dynamicMiddlewareRouteMap?: ReadonlyMap, - experimentalPPR?: boolean -): Promise { +export async function getDynamicRoutes({ + entryPath, + entryDirectory, + dynamicPages, + isDev, + routesManifest, + omittedRoutes, + canUsePreviewMode, + bypassToken, + isServerMode, + dynamicMiddlewareRouteMap, + experimentalPPRRoutes, +}: { + entryPath: string; + entryDirectory: string; + dynamicPages: string[]; + isDev?: boolean; + routesManifest?: RoutesManifest; + omittedRoutes?: ReadonlySet; + canUsePreviewMode?: boolean; + bypassToken?: string; + isServerMode?: boolean; + dynamicMiddlewareRouteMap?: ReadonlyMap; + experimentalPPRRoutes: ReadonlySet; +}): Promise { if (routesManifest) { switch (routesManifest.version) { case 1: @@ -389,7 +401,7 @@ export async function getDynamicRoutes( ]; } - if (experimentalPPR) { + if (experimentalPPRRoutes.has(page)) { let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1'); if (page === '/' || page === '/index') { @@ -919,6 +931,10 @@ export type NextPrerenderedRoutes = { }; }; + /** + * Routes that have their fallback behavior is disabled. All routes would've + * been provided in the top-level `routes` key (`staticRoutes`). + */ omittedRoutes: { [route: string]: { routeRegex: string; @@ -1298,8 +1314,6 @@ export async function getPrerenderManifest( prefetchDataRouteRegex, }; } else { - // Fallback behavior is disabled, all routes would've been provided - // in the top-level `routes` key (`staticRoutes`). ret.omittedRoutes[lazyRoute] = { experimentalBypassFor, experimentalPPR, @@ -1923,6 +1937,7 @@ type OnPrerenderRouteArgs = { routesManifest?: RoutesManifest; isCorrectNotFoundRoutes?: boolean; isEmptyAllowQueryForPrendered?: boolean; + omittedPrerenderRoutes: ReadonlySet; }; let prerenderGroup = 1; @@ -1959,6 +1974,7 @@ export const onPrerenderRoute = routesManifest, isCorrectNotFoundRoutes, isEmptyAllowQueryForPrendered, + omittedPrerenderRoutes, } = prerenderRouteArgs; if (isBlocking && isFallback) { @@ -2383,25 +2399,31 @@ export const onPrerenderRoute = sourcePath = srcRoute; } - // The `experimentalStreamingLambdaPaths` stores the page without the - // leading `/` and with the `/` rewritten to be `index`. We should - // normalize the key so that it matches that key in the map. - let key = srcRoute || routeKey; - if (key === '/') { - key = 'index'; - } else { - if (!key.startsWith('/')) { - throw new Error("Invariant: key doesn't start with /"); + let experimentalStreamingLambdaPath: string | undefined; + if (experimentalPPR) { + if (!experimentalStreamingLambdaPaths) { + throw new Error( + "Invariant: experimentalStreamingLambdaPaths doesn't exist" + ); } - key = key.substring(1); + // If a source route exists, and it's not listed as an omitted route, + // then use the src route as the basis for the experimental streaming + // lambda path. If the route doesn't have a source route or it's not + // omitted, then use the more specific `routeKey` as the basis. + if (srcRoute && !omittedPrerenderRoutes.has(srcRoute)) { + experimentalStreamingLambdaPath = + experimentalStreamingLambdaPaths.get( + pathnameToOutputName(entryDirectory, srcRoute) + ); + } else { + experimentalStreamingLambdaPath = + experimentalStreamingLambdaPaths.get( + pathnameToOutputName(entryDirectory, routeKey) + ); + } } - key = path.posix.join(entryDirectory, key); - - const experimentalStreamingLambdaPath = - experimentalStreamingLambdaPaths?.get(key); - prerenders[outputPathPage] = new Prerender({ expiration: initialRevalidate, lambda, @@ -2604,6 +2626,10 @@ export async function getStaticFiles( }; } +/** + * Strips the trailing `/index` from the output name if it's not the root if + * the server mode is enabled. + */ export function normalizeIndexOutput( outputName: string, isServerMode: boolean @@ -2624,6 +2650,19 @@ export function getNextServerPath(nextVersion: string) { : 'next/dist/next-server/server'; } +export function pathnameToOutputName(entryDirectory: string, pathname: string) { + if (pathname === '/') pathname = '/index'; + return path.posix.join(entryDirectory, pathname); +} + +export function getPostponeResumePathname( + entryDirectory: string, + pathname: string +): string { + if (pathname === '/') pathname = '/index'; + return path.posix.join(entryDirectory, '_next/postponed/resume', pathname); +} + // update to leverage export function updateRouteSrc( route: Route, diff --git a/packages/next/test/fixtures/00-app-dir-ppr-full/app/no-fallback/[slug]/page.jsx b/packages/next/test/fixtures/00-app-dir-ppr-full/app/no-fallback/[slug]/page.jsx new file mode 100644 index 000000000..d12ff7252 --- /dev/null +++ b/packages/next/test/fixtures/00-app-dir-ppr-full/app/no-fallback/[slug]/page.jsx @@ -0,0 +1,18 @@ +import React, { Suspense } from 'react' +import { Dynamic } from '../../../components/dynamic' + +export const dynamicParams = false; + +const slugs = ['a', 'b', 'c']; + +export function generateStaticParams() { + return slugs.map((slug) => ({ slug })); +} + +export default function NoFallbackPage({ params: { slug } }) { + return ( + }> + + + ) +} diff --git a/packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js b/packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js index 6ac4cfddb..e8af7f285 100644 --- a/packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js +++ b/packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js @@ -19,11 +19,23 @@ const pages = [ { pathname: '/no-suspense/nested/a', dynamic: true }, { pathname: '/no-suspense/nested/b', dynamic: true }, { pathname: '/no-suspense/nested/c', dynamic: true }, + { pathname: '/no-fallback/a', dynamic: true }, + { pathname: '/no-fallback/b', dynamic: true }, + { pathname: '/no-fallback/c', dynamic: true }, // TODO: uncomment when we've fixed the 404 case for force-dynamic pages // { pathname: '/dynamic/force-dynamic', dynamic: 'force-dynamic' }, { pathname: '/dynamic/force-static', dynamic: 'force-static' }, ]; +const cases = { + 404: [ + // For routes that do not support fallback (they had `dynamicParams` set to + // `false`), we shouldn't see any fallback behavior for routes not defined + // in `getStaticParams`. + { pathname: '/no-fallback/non-existent' }, + ], +}; + const ctx = {}; describe(`${__dirname.split(path.sep).pop()}`, () => { @@ -49,6 +61,14 @@ describe(`${__dirname.split(path.sep).pop()}`, () => { expect(html).toContain(''); } ); + + it.each(cases[404])( + 'should return 404 for $pathname', + async ({ pathname }) => { + const res = await fetch(`${ctx.deploymentUrl}${pathname}`); + expect(res.status).toEqual(404); + } + ); }); describe('prefetch RSC payloads should return', () => { @@ -88,6 +108,16 @@ describe(`${__dirname.split(path.sep).pop()}`, () => { } } ); + + it.each(cases[404])( + 'should return 404 for $pathname', + async ({ pathname }) => { + const res = await fetch(`${ctx.deploymentUrl}${pathname}`, { + headers: { RSC: 1, 'Next-Router-Prefetch': '1' }, + }); + expect(res.status).toEqual(404); + } + ); }); describe('dynamic RSC payloads should return', () => { @@ -122,5 +152,15 @@ describe(`${__dirname.split(path.sep).pop()}`, () => { expect(text).not.toContain(expected); } }); + + it.each(cases[404])( + 'should return 404 for $pathname', + async ({ pathname }) => { + const res = await fetch(`${ctx.deploymentUrl}${pathname}`, { + headers: { RSC: 1 }, + }); + expect(res.status).toEqual(404); + } + ); }); }); diff --git a/packages/next/test/fixtures/00-app-dir-ppr-full/package.json b/packages/next/test/fixtures/00-app-dir-ppr-full/package.json index 087749b0a..350584e84 100644 --- a/packages/next/test/fixtures/00-app-dir-ppr-full/package.json +++ b/packages/next/test/fixtures/00-app-dir-ppr-full/package.json @@ -1,8 +1,8 @@ { "dependencies": { "next": "canary", - "react": "experimental", - "react-dom": "experimental" + "react": "18.2.0", + "react-dom": "18.2.0" }, "ignoreNextjsUpdates": true } diff --git a/test/lib/deployment/now-deploy.js b/test/lib/deployment/now-deploy.js index 366abcaad..ddfe0089b 100644 --- a/test/lib/deployment/now-deploy.js +++ b/test/lib/deployment/now-deploy.js @@ -119,16 +119,28 @@ async function disableSSO(deploymentId, useTeam = true) { } ); + const text = await deployRes.text(); + if (!deployRes.ok) { throw new Error( - `Failed to get deployment info (status: ${ - deployRes.status - }, body: ${await deployRes.text()})` + `Failed to get deployment info (status: ${deployRes.status}, body: ${text})` ); } - const deploymentInfo = await deployRes.json(); - const { projectId, url: deploymentUrl } = deploymentInfo; + let info; + try { + info = JSON.parse(text); + } catch (err) { + throw new Error('Failed to parse deployment info JSON', { cause: err }); + } + + const { projectId, url: deploymentUrl } = info; + + if (!deploymentUrl || typeof deploymentUrl !== 'string') { + throw new Error( + `Failed to get deployment URL (status: ${deployRes.status}, body: ${text})` + ); + } const settingRes = await fetchWithAuth( `https://vercel.com/api/v5/projects/${encodeURIComponent(projectId)}`, @@ -160,7 +172,7 @@ async function disableSSO(deploymentId, useTeam = true) { `Disabled deployment protection for deploymentId: ${deploymentId} projectId: ${projectId}` ); } else { - console.error(settingRes.status, await settingRes.text(), deploymentInfo); + console.error(settingRes.status, await settingRes.text(), text); throw new Error( `Failed to disable deployment protection projectId: ${projectId} deploymentId ${deploymentId}` );