├── README.md ├── .npmrc ├── examples ├── demo │ ├── tests │ │ ├── common │ │ │ ├── index.ts │ │ │ ├── index2.ts │ │ │ ├── index.html │ │ │ ├── teardown.ts │ │ │ ├── setup.ts │ │ │ ├── utils.ts │ │ │ └── helpers.ts │ │ ├── 05-vike │ │ │ ├── vitest.config.ts │ │ │ ├── vite.config._test_.js │ │ │ ├── utils.ts │ │ │ ├── vc-config.test.ts │ │ │ ├── prerender.test.ts │ │ │ ├── fs.test.ts │ │ │ └── config.test.ts │ │ ├── 04-isr │ │ │ ├── vitest.config.ts │ │ │ ├── fs.test.ts │ │ │ ├── globalSetup.ts │ │ │ ├── prerender.test.ts │ │ │ ├── vc-config.test.ts │ │ │ └── config.test.ts │ │ ├── 01-minimal │ │ │ ├── vitest.config.ts │ │ │ ├── globalSetup.ts │ │ │ ├── fs.test.ts │ │ │ ├── vc-config.test.ts │ │ │ └── config.test.ts │ │ ├── 03-prerender │ │ │ ├── vitest.config.ts │ │ │ ├── fs.test.ts │ │ │ ├── globalSetup.ts │ │ │ ├── vc-config.test.ts │ │ │ └── config.test.ts │ │ └── 02-additional-endpoints │ │ │ ├── vitest.config.ts │ │ │ ├── fs.test.ts │ │ │ ├── globalSetup.ts │ │ │ ├── vc-config.test.ts │ │ │ └── config.test.ts │ ├── pages │ │ ├── named │ │ │ ├── +route.ts │ │ │ ├── +onBeforePrerenderStart.ts │ │ │ ├── +config.ts │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── catch-all │ │ │ ├── +route.ts │ │ │ ├── +onBeforePrerenderStart.ts │ │ │ ├── +config.ts │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── dynamic │ │ │ ├── +config.ts │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── function │ │ │ ├── +onBeforePrerenderStart.ts │ │ │ ├── +config.ts │ │ │ ├── +route.ts │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── isr │ │ │ ├── +config.ts │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── vike-edge │ │ │ ├── +config.ts │ │ │ ├── Counter.tsx │ │ │ └── +Page.tsx │ │ ├── static │ │ │ ├── +onBeforeRender.ts │ │ │ └── +Page.tsx │ │ ├── index │ │ │ ├── Counter.tsx │ │ │ └── +Page.tsx │ │ └── _error │ │ │ └── +Page.tsx │ ├── endpoints │ │ ├── Roboto-Regular.ttf │ │ ├── edge.ts │ │ ├── og-edge.tsx │ │ └── og-node.tsx │ ├── renderer │ │ ├── css │ │ │ ├── reset.css │ │ │ ├── code.css │ │ │ ├── index.css │ │ │ ├── links.css │ │ │ ├── loading.svg │ │ │ └── page-transition-loading-animation.css │ │ ├── +onHydrationEnd.ts │ │ ├── +onPageTransitionEnd.ts │ │ ├── +onPageTransitionStart.ts │ │ ├── Link.tsx │ │ ├── getPageTitle.ts │ │ ├── usePageContext.tsx │ │ ├── +onRenderClient.tsx │ │ ├── +onRenderHtml.tsx │ │ ├── types.ts │ │ ├── +config.ts │ │ ├── PageShell.tsx │ │ └── logo.svg │ ├── public │ │ └── test.html │ ├── _api │ │ ├── post.ts │ │ ├── name │ │ │ └── [name].ts │ │ └── page.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ └── CHANGELOG.md ├── simple │ ├── _api │ │ ├── endpoint.ts │ │ ├── [...catchAll].ts │ │ ├── [[...optionalCatchAll]].ts │ │ ├── edge.ts │ │ ├── isr.ts │ │ └── headers.ts │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── CHANGELOG.md └── express │ ├── pages │ ├── +config.ts │ └── index │ │ ├── Counter.tsx │ │ └── +Page.tsx │ ├── tsconfig.json │ ├── server │ └── vike-handler.ts │ ├── vite.config.ts │ ├── package.json │ ├── express-entry.ts │ └── CHANGELOG.md ├── packages ├── vike-integration │ ├── config.js │ ├── helpers.js │ ├── helpers.d.ts │ ├── tsconfig.json │ ├── +config.ts │ ├── config.d.ts │ ├── templates │ │ ├── utils.ts │ │ ├── ssr_edge_.template.ts │ │ ├── ssr_.template.ts │ │ ├── node-helpers.ts │ │ └── edge-helpers.ts │ ├── route-regex.ts │ ├── tsup.config.ts │ ├── package.json │ ├── README.md │ └── CHANGELOG.md └── vercel │ ├── index.d.ts │ ├── src │ ├── schemas │ │ ├── exports.ts │ │ └── config │ │ │ ├── prerender-config.ts │ │ │ ├── vc-config.ts │ │ │ └── config.ts │ ├── assert.ts │ ├── helpers.ts │ ├── utils.ts │ ├── prerender.ts │ ├── config.ts │ ├── index.ts │ ├── types.ts │ └── build.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── CHANGELOG.md │ └── README.md ├── pnpm-workspace.yaml ├── .vscode └── settings.json ├── vercel.json ├── tsconfig.json ├── .changeset ├── config.json └── README.md ├── biome.json ├── .github ├── renovate.json5 └── workflows │ └── node.js.yml ├── package.json └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | packages/vercel/README.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false 2 | 3 | -------------------------------------------------------------------------------- /examples/demo/tests/common/index.ts: -------------------------------------------------------------------------------- 1 | // SSR entrypoint 2 | -------------------------------------------------------------------------------- /examples/demo/pages/named/+route.ts: -------------------------------------------------------------------------------- 1 | export default "/named/@someId"; 2 | -------------------------------------------------------------------------------- /examples/demo/pages/catch-all/+route.ts: -------------------------------------------------------------------------------- 1 | export default "/catch-all/*"; 2 | -------------------------------------------------------------------------------- /packages/vike-integration/config.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/+config.js"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /examples/demo/tests/common/index2.ts: -------------------------------------------------------------------------------- 1 | // SSR entrypoint 2 | console.log("hello"); 3 | -------------------------------------------------------------------------------- /packages/vike-integration/helpers.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/templates/helpers.js"; 2 | -------------------------------------------------------------------------------- /examples/demo/pages/dynamic/+config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | prerender: false, 3 | }; 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "redirects": [{ "source": "/redirect", "destination": "/static" }] 3 | } 4 | -------------------------------------------------------------------------------- /packages/vike-integration/helpers.d.ts: -------------------------------------------------------------------------------- 1 | // Help TS's resolver for node10 target 2 | export * from "./dist/templates/helpers"; 3 | -------------------------------------------------------------------------------- /examples/demo/pages/function/+onBeforePrerenderStart.ts: -------------------------------------------------------------------------------- 1 | export default function prerender() { 2 | return ["/function/a"]; 3 | } 4 | -------------------------------------------------------------------------------- /examples/demo/endpoints/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magne4000/vite-plugin-vercel/HEAD/examples/demo/endpoints/Roboto-Regular.ttf -------------------------------------------------------------------------------- /examples/demo/pages/named/+onBeforePrerenderStart.ts: -------------------------------------------------------------------------------- 1 | export default function prerender() { 2 | return ["/named/id-1", "/named/id-2"]; 3 | } 4 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/pages/named/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | isr: true, 5 | } satisfies Config; 6 | -------------------------------------------------------------------------------- /examples/demo/pages/catch-all/+onBeforePrerenderStart.ts: -------------------------------------------------------------------------------- 1 | export default function prerender() { 2 | return ["/catch-all/a/b/c", "/catch-all/a/d"]; 3 | } 4 | -------------------------------------------------------------------------------- /examples/demo/pages/catch-all/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | isr: { expiration: 15 }, 5 | } satisfies Config; 6 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | background-color: #eaeaea; 4 | padding: 3px 5px; 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./links.css"; 3 | @import "./code.css"; 4 | @import "./page-transition-loading-animation.css"; 5 | -------------------------------------------------------------------------------- /examples/demo/pages/isr/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | isr: { expiration: 15 }, 5 | prerender: false, 6 | } satisfies Config; 7 | -------------------------------------------------------------------------------- /examples/demo/renderer/+onHydrationEnd.ts: -------------------------------------------------------------------------------- 1 | export default onHydrationEnd; 2 | 3 | function onHydrationEnd() { 4 | console.log("Hydration finished; page is now interactive."); 5 | } 6 | -------------------------------------------------------------------------------- /examples/demo/public/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/links.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | #sidebar a { 5 | padding: 2px 10px; 6 | margin-left: -10px; 7 | } 8 | #sidebar a.is-active { 9 | background-color: #eee; 10 | } 11 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | enabled: false, 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/demo/_api/post.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async function handler(request: VercelRequest, response: VercelResponse) { 4 | return response.send("OK"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/simple/_api/endpoint.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async function handler(request: VercelRequest, response: VercelResponse) { 4 | return response.send("OK"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/demo/pages/function/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | // Should warn when building because it's incompatible with route function 5 | isr: { expiration: 15 }, 6 | } satisfies Config; 7 | -------------------------------------------------------------------------------- /examples/simple/_api/[...catchAll].ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async function handler(request: VercelRequest, response: VercelResponse) { 4 | return response.send("OK"); 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "target": "ES2022", 7 | "lib": ["ESNext"], 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/demo/pages/vike-edge/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | prerender: false, 5 | edge: true, 6 | headers: { 7 | "X-VitePluginVercel-Test": "test", 8 | }, 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /examples/demo/renderer/+onPageTransitionEnd.ts: -------------------------------------------------------------------------------- 1 | export default onPageTransitionEnd; 2 | 3 | function onPageTransitionEnd() { 4 | console.log("Page transition end"); 5 | document.querySelector("body")?.classList.remove("page-is-transitioning"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple/_api/[[...optionalCatchAll]].ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async function handler(request: VercelRequest, response: VercelResponse) { 4 | return response.send("OK"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/demo/renderer/+onPageTransitionStart.ts: -------------------------------------------------------------------------------- 1 | export default onPageTransitionStart; 2 | 3 | function onPageTransitionStart() { 4 | console.log("Page transition start"); 5 | document.querySelector("body")?.classList.add("page-is-transitioning"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/demo/_api/name/[name].ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async function handler(request: VercelRequest, response: VercelResponse) { 4 | return response.send(`Name: ${request.query.name}`); 5 | } 6 | -------------------------------------------------------------------------------- /examples/simple/_api/edge.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export const edge = true; 4 | 5 | export default async function handler(request: VercelRequest, response: VercelResponse) { 6 | return response.send("OK"); 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/pages/isr/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onBeforeRender 2 | export default function onBeforeRender() { 3 | return { 4 | pageContext: { 5 | pageProps: { 6 | d: String(new Date()), 7 | }, 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /examples/demo/pages/static/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onBeforeRender 2 | export default function onBeforeRender() { 3 | return { 4 | pageContext: { 5 | pageProps: { 6 | d: String(new Date()), 7 | }, 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/vercel/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { ViteVercelConfig } from "./dist"; 2 | 3 | export * from "./dist"; 4 | export { default } from "./dist"; 5 | 6 | declare module "vite" { 7 | export interface UserConfig { 8 | vercel?: ViteVercelConfig; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globalSetup: "tests/04-isr/globalSetup.ts", 6 | coverage: { 7 | enabled: false, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/express/pages/+config.ts: -------------------------------------------------------------------------------- 1 | import vikeReact from "vike-react/config"; 2 | import type { Config } from "vike/types"; 3 | 4 | // Default config (can be overridden by pages) 5 | export default { 6 | title: "My Vike App", 7 | extends: vikeReact, 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /examples/simple/_api/isr.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export const isr = { expiration: 10 }; 4 | 5 | export default async function handler(request: VercelRequest, response: VercelResponse) { 6 | return response.send("OK"); 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/tests/01-minimal/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globalSetup: "tests/01-minimal/globalSetup.ts", 6 | coverage: { 7 | enabled: false, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/demo/endpoints/edge.ts: -------------------------------------------------------------------------------- 1 | import { get } from "@vercel/edge-config"; 2 | 3 | export const edge = true; 4 | 5 | export default async function handler() { 6 | await get("someKey"); 7 | 8 | return new Response("Edge Function: OK", { 9 | status: 200, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /examples/demo/tests/03-prerender/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globalSetup: "tests/03-prerender/globalSetup.ts", 6 | coverage: { 7 | enabled: false, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/demo/pages/function/+route.ts: -------------------------------------------------------------------------------- 1 | import type { PageContextBuiltInServer } from "vike/types"; 2 | 3 | export default function (pageContext: PageContextBuiltInServer) { 4 | if (!pageContext.urlPathname.startsWith("/function/")) return false; 5 | return { 6 | precedence: -1, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple/_api/headers.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export const headers = { "X-VitePluginVercel-Test": "test" }; 4 | 5 | export default async function handler(request: VercelRequest, response: VercelResponse) { 6 | return response.send("OK"); 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/tests/02-additional-endpoints/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globalSetup: "tests/02-additional-endpoints/globalSetup.ts", 6 | coverage: { 7 | enabled: false, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/demo/_api/page.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export const headers = { 4 | "X-VitePluginVercel-Test": "test", 5 | }; 6 | 7 | export default async function handler(request: VercelRequest, response: VercelResponse) { 8 | return response.send("OK"); 9 | } 10 | -------------------------------------------------------------------------------- /examples/demo/pages/catch-all/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | import { PageContextBuiltInServer } from "vike/types"; 2 | 3 | // https://vike.dev/onBeforeRender 4 | export default function onBeforeRender() { 5 | return { 6 | pageContext: { 7 | pageProps: { 8 | d: String(new Date()), 9 | }, 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/pages/dynamic/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | import { PageContextBuiltInServer } from "vike/types"; 2 | 3 | // https://vike.dev/onBeforeRender 4 | export default function onBeforeRender() { 5 | return { 6 | pageContext: { 7 | pageProps: { 8 | d: String(new Date()), 9 | }, 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/pages/function/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | import { PageContextBuiltInServer } from "vike/types"; 2 | 3 | // https://vike.dev/onBeforeRender 4 | export default function onBeforeRender() { 5 | return { 6 | pageContext: { 7 | pageProps: { 8 | d: String(new Date()), 9 | }, 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export { Counter }; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/pages/vike-edge/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export { Counter }; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/express/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export { Counter }; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My App 7 | 8 | 9 |

Created with Bâti

10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/vike-integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["ES2022"], 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Bundler", 6 | "target": "ES2022", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react-jsx", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/vercel/src/schemas/exports.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const vercelEndpointExports = z.object({ 4 | edge: z.boolean().optional(), 5 | headers: z.record(z.string()).optional(), 6 | streaming: z.boolean().optional(), 7 | isr: z 8 | .object({ 9 | expiration: z.number().or(z.literal(false)), 10 | }) 11 | .optional(), 12 | }); 13 | -------------------------------------------------------------------------------- /examples/demo/pages/isr/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Page(props: { d: string }) { 4 | return ( 5 | <> 6 |

Welcome

7 | This page is: 8 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/vike-integration/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | 3 | export default { 4 | name: "@vite-plugin-vercel/vike", 5 | meta: { 6 | isr: { 7 | env: { server: true }, 8 | }, 9 | edge: { 10 | env: { server: true }, 11 | }, 12 | headers: { 13 | env: { server: true }, 14 | }, 15 | }, 16 | } satisfies Config; 17 | -------------------------------------------------------------------------------- /examples/simple/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vercel from "vite-plugin-vercel"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | vercel({ 7 | // `smart` param only exist to circumvent a pnpm issue in this repo 8 | // You should not use this parameter outside this repository 9 | smart: false, 10 | }), 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /packages/vercel/src/assert.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { newError } from "@brillout/libassert"; 3 | 4 | const libName = "vite-plugin-vercel"; 5 | 6 | export function assert(condition: unknown, errorMessage: string): asserts condition { 7 | if (condition) { 8 | return; 9 | } 10 | 11 | const err = newError(`[${libName}][Wrong Usage] ${errorMessage}`, 2); 12 | throw err; 13 | } 14 | -------------------------------------------------------------------------------- /examples/demo/pages/static/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Page(props: { d: string }) { 4 | return ( 5 | <> 6 |

Welcome

7 | This page is: 8 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/demo/pages/dynamic/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Page(props: { d: string }) { 4 | return ( 5 | <> 6 |

Welcome

7 | This page is: 8 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/demo/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Counter } from "./Counter.js"; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 |

Welcome

8 | This page is: 9 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/demo/tests/common/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | Document 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/demo/pages/vike-edge/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Counter } from "./Counter.js"; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 |

Welcome

8 | This page is: 9 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/express/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Counter } from "./Counter.js"; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 |

Welcome

8 | This page is: 9 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/demo/tests/common/teardown.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import os from "node:os"; 3 | import fs from "node:fs/promises"; 4 | 5 | export function teardown(displayName: string) { 6 | return async () => { 7 | const tmpdir = path.join(os.tmpdir(), `vpv-demo-${displayName}`); 8 | 9 | await fs.rm(tmpdir, { 10 | recursive: true, 11 | force: true, 12 | }); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "lib": ["es2021", "dom"], 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "paths": { 12 | "vite": ["./node_modules/vite"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/demo/pages/named/+onBeforeRender.ts: -------------------------------------------------------------------------------- 1 | import type { PageContextBuiltInServer } from "vike/types"; 2 | 3 | // https://vike.dev/onBeforeRender 4 | export default function onBeforeRender(pageContext: PageContextBuiltInServer) { 5 | return { 6 | pageContext: { 7 | pageProps: { 8 | d: String(new Date()), 9 | someId: pageContext.routeParams.someId, 10 | }, 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/vercel/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | external: ["esbuild", "vike"], 7 | format: ["esm", "cjs"], 8 | platform: "node", 9 | target: "node18", 10 | dts: { 11 | entry: "./src/index.ts", 12 | compilerOptions: { 13 | paths: {}, 14 | }, 15 | }, 16 | }, 17 | ]); 18 | -------------------------------------------------------------------------------- /examples/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "module": "ESNext", 10 | "noEmit": true, 11 | "moduleResolution": "Bundler", 12 | "target": "ES2022", 13 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 14 | "types": ["vite/client"], 15 | "jsx": "react-jsx" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "bumpVersionsWithWorkspaceProtocolOnly": true, 11 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 12 | "onlyUpdatePeerDependentsWhenOutOfRange": true 13 | }, 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "module": "ESNext", 12 | "noEmit": true, 13 | "moduleResolution": "Bundler", 14 | "target": "ES2022", 15 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 16 | "types": ["vite/client"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/vike-integration/config.d.ts: -------------------------------------------------------------------------------- 1 | import "vike/types"; 2 | 3 | // When this is imported by a projet -> 4 | // import config from '@vite-plugin-vercel/vike/config' 5 | // the following override is applied to the whole project 6 | declare global { 7 | namespace Vike { 8 | export interface Config { 9 | isr?: boolean | { expiration: number }; 10 | edge?: boolean; 11 | headers?: Record; 12 | } 13 | } 14 | } 15 | 16 | // Help TS's resolver for node10 target 17 | export * from "./dist/+config.h"; 18 | -------------------------------------------------------------------------------- /packages/vercel/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | export async function copyDir(src: string, dest: string) { 5 | await fs.mkdir(dest, { recursive: true }); 6 | const entries = await fs.readdir(src, { withFileTypes: true }); 7 | 8 | for (const entry of entries) { 9 | const srcPath = path.join(src, entry.name); 10 | const destPath = path.join(dest, entry.name); 11 | 12 | entry.isDirectory() ? await copyDir(srcPath, destPath) : await fs.copyFile(srcPath, destPath); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/demo/renderer/Link.tsx: -------------------------------------------------------------------------------- 1 | export { Link }; 2 | 3 | import { usePageContext } from "./usePageContext"; 4 | import React from "react"; 5 | 6 | function Link({ href, children }: { href: string; children: string }) { 7 | const pageContext = usePageContext() as { urlPathname: string }; // TODO 8 | const { urlPathname } = pageContext; 9 | const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href); 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-vercel-simple", 3 | "private": true, 4 | "version": null, 5 | "description": "", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite build", 9 | "typecheck": "tsc -p tsconfig.json --noEmit" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "devDependencies": { 14 | "@types/node": "^18.19.130", 15 | "@vercel/node": "^3.2.29", 16 | "typescript": "^5.9.3", 17 | "vite": "^6.4.1" 18 | }, 19 | "dependencies": { 20 | "vite-plugin-vercel": "workspace:*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/demo/renderer/getPageTitle.ts: -------------------------------------------------------------------------------- 1 | export { getPageTitle }; 2 | 3 | import type { PageContext } from "./types"; 4 | 5 | function getPageTitle(pageContext: PageContext): string { 6 | const title = 7 | // Title defined dynamically by onBeforeRender() 8 | pageContext.title || 9 | // Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) 10 | // The config 'pageContext.config.title' is a custom config we defined at ./+config.ts 11 | pageContext.config.title || 12 | "Demo"; 13 | return title; 14 | } 15 | -------------------------------------------------------------------------------- /packages/vike-integration/templates/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "qs"; 2 | 3 | export function getOriginalUrl(xNowRouteMatchesHeader: unknown, originalPath: unknown, url?: string) { 4 | const matches = 5 | // FIXME x-now-route-matches is not definitive https://github.com/orgs/vercel/discussions/577#discussioncomment-2769478 6 | typeof xNowRouteMatchesHeader === "string" ? parse(xNowRouteMatchesHeader) : null; 7 | return typeof originalPath === "string" 8 | ? originalPath 9 | : matches && typeof matches?.["1"] === "string" 10 | ? matches["1"] 11 | : (url ?? ""); 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/tests/01-minimal/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { setup as _setup } from "../common/setup"; 3 | import { teardown as _teardown } from "../common/teardown"; 4 | import react from "@vitejs/plugin-react-swc"; 5 | import vercel from "vite-plugin-vercel"; 6 | 7 | export const setup = _setup(path.basename(__dirname), { 8 | configFile: false, 9 | mode: "production", 10 | root: process.cwd(), 11 | plugins: [ 12 | react(), 13 | vercel({ 14 | smart: false, 15 | }), 16 | ], 17 | }); 18 | 19 | export const teardown = _teardown(path.basename(__dirname)); 20 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["dist/", "package.json"] 5 | }, 6 | "formatter": { 7 | "indentWidth": 2, 8 | "indentStyle": "space" 9 | }, 10 | "javascript": { 11 | "formatter": { 12 | "lineWidth": 120 13 | } 14 | }, 15 | "vcs": { 16 | "enabled": true, 17 | "clientKind": "git" 18 | }, 19 | "overrides": [ 20 | { 21 | "include": ["**/*.test.ts"], 22 | "linter": { 23 | "rules": { 24 | "suspicious": { "noExplicitAny": "off" } 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/demo/tests/common/setup.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import os from "node:os"; 3 | import path from "node:path"; 4 | import type { InlineConfig } from "vite"; 5 | import { callBuild } from "./utils"; 6 | 7 | export function setup(displayName: string, inlineConfig: InlineConfig) { 8 | return async () => { 9 | const tmpdir = path.join(os.tmpdir(), `vpv-demo-${displayName}`); 10 | 11 | await fs.rm(tmpdir, { 12 | recursive: true, 13 | force: true, 14 | }); 15 | await fs.mkdir(tmpdir, { 16 | recursive: true, 17 | }); 18 | 19 | await callBuild(displayName, inlineConfig); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /examples/demo/renderer/usePageContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import type { PageContext } from "./types"; 3 | 4 | export { PageContextProvider }; 5 | export { usePageContext }; 6 | 7 | const Context = React.createContext(undefined as unknown as PageContext); 8 | 9 | function PageContextProvider({ 10 | pageContext, 11 | children, 12 | }: { 13 | pageContext: PageContext; 14 | children: React.ReactNode; 15 | }) { 16 | return {children}; 17 | } 18 | 19 | function usePageContext() { 20 | const pageContext = useContext(Context); 21 | return pageContext; 22 | } 23 | -------------------------------------------------------------------------------- /examples/demo/tests/03-prerender/fs.test.ts: -------------------------------------------------------------------------------- 1 | import { testFs } from "../common/helpers"; 2 | import { describe } from "vitest"; 3 | import path from "node:path"; 4 | 5 | describe("fs", () => { 6 | testFs(path.basename(__dirname), [ 7 | "/config.json", 8 | "/functions/api/name/[name].func/.vc-config.json", 9 | "/functions/api/name/[name].func/index.mjs", 10 | "/functions/api/page.func/index.mjs", 11 | "/functions/api/page.func/.vc-config.json", 12 | "/functions/api/post.func/index.mjs", 13 | "/functions/api/post.func/.vc-config.json", 14 | "/functions/edge.func/index.js", 15 | "/functions/edge.func/.vc-config.json", 16 | ]); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/vite.config._test_.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import ssr from "vike/plugin"; 3 | import vercel from "vite-plugin-vercel"; 4 | 5 | export default { 6 | mode: "production", 7 | root: process.cwd(), 8 | plugins: [ 9 | react(), 10 | ssr({ 11 | prerender: { 12 | disableAutoRun: true, 13 | }, 14 | }), 15 | vercel(), 16 | ], 17 | vercel: { 18 | rewrites: [], 19 | additionalEndpoints: [ 20 | { 21 | source: "endpoints/edge.ts", 22 | destination: "edge", 23 | edge: true, 24 | route: true, 25 | }, 26 | ], 27 | expiration: 25, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | // Inspired by https://github.com/vitejs/vite/blob/main/.github/renovate.json5 3 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 4 | extends: [ 5 | 'config:base', 6 | 'schedule:weekly', 7 | 'group:allNonMajor', 8 | // peerDependencies ranges should be left untouched/wide. 9 | ':disablePeerDependencies' 10 | ], 11 | labels: [ 12 | 'dependencies' 13 | ], 14 | rangeStrategy: 'bump', 15 | assignees: [ 16 | '@magne4000' 17 | ], 18 | ignoreDeps: [ 19 | // manually bumping 20 | 'node', 21 | '@types/node', 22 | 'pnpm', 23 | ], 24 | postUpdateOptions: [ 25 | 'pnpmDedupe' 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /examples/demo/renderer/css/page-transition-loading-animation.css: -------------------------------------------------------------------------------- 1 | #page-container { 2 | position: relative; 3 | width: 100%; 4 | } 5 | #page-container::before { 6 | content: ""; 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 999; 11 | background: no-repeat url("./loading.svg"); 12 | background-size: 100px; 13 | background-position: center center; 14 | pointer-events: none; 15 | opacity: 0; 16 | } 17 | body.page-is-transitioning #page-container::before { 18 | opacity: 1; 19 | } 20 | #page-content, 21 | #page-container::before { 22 | transition: opacity 0.5s ease-in-out; 23 | } 24 | body.page-is-transitioning #page-content { 25 | opacity: 0.17; 26 | } 27 | -------------------------------------------------------------------------------- /packages/vercel/src/schemas/config/prerender-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema definition for `.vercel/output/config.json` 3 | * @see {@link https://vercel.com/docs/build-output-api/v3#build-output-configuration} 4 | */ 5 | 6 | import { z } from "zod"; 7 | 8 | export const vercelOutputPrerenderConfigSchema = z 9 | .object({ 10 | expiration: z.union([z.number().int().positive(), z.literal(false)]), 11 | group: z.number().int().optional(), 12 | bypassToken: z.string().optional(), 13 | fallback: z.string().optional(), 14 | allowQuery: z.array(z.string()).optional(), 15 | }) 16 | .strict(); 17 | 18 | export type VercelOutputPrerenderConfig = z.infer; 19 | -------------------------------------------------------------------------------- /examples/express/server/vike-handler.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { renderPage } from "vike/server"; 3 | 4 | export async function vikeHandler>( 5 | request: Request, 6 | context?: Context, 7 | ): Promise { 8 | const pageContextInit = { ...context, urlOriginal: request.url }; 9 | const pageContext = await renderPage(pageContextInit); 10 | const response = pageContext.httpResponse; 11 | 12 | const { readable, writable } = new TransformStream(); 13 | 14 | response?.pipe(writable); 15 | 16 | return new Response(readable, { 17 | status: response?.statusCode, 18 | headers: response?.headers, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/vike-integration/templates/ssr_edge_.template.ts: -------------------------------------------------------------------------------- 1 | import { renderPage } from "vike/server"; 2 | import { getDefaultEmptyResponseHandler, getDefaultPageContextInit, getDefaultResponseHandler } from "./edge-helpers"; 3 | 4 | export default async function handler(request: Request): Promise { 5 | console.debug("url", request.url); 6 | console.debug("headers", request.headers); 7 | const pageContextInit = getDefaultPageContextInit(request); 8 | const pageContext = await renderPage(pageContextInit); 9 | const { httpResponse } = pageContext; 10 | 11 | if (!httpResponse) { 12 | return getDefaultEmptyResponseHandler(); 13 | } 14 | 15 | return getDefaultResponseHandler(httpResponse); 16 | } 17 | -------------------------------------------------------------------------------- /examples/demo/tests/01-minimal/fs.test.ts: -------------------------------------------------------------------------------- 1 | import { testFs } from "../common/helpers"; 2 | import { describe } from "vitest"; 3 | import path from "node:path"; 4 | 5 | describe("fs", () => { 6 | testFs(path.basename(__dirname), [ 7 | "/config.json", 8 | "/functions/api/name/[name].func/.vc-config.json", 9 | "/functions/api/name/[name].func/index.mjs", 10 | "/functions/api/page.func/index.mjs", 11 | "/functions/api/page.func/.vc-config.json", 12 | "/functions/api/post.func/index.mjs", 13 | "/functions/api/post.func/.vc-config.json", 14 | "/functions/edge.func/index.js", 15 | "/functions/edge.func/.vc-config.json", 16 | "/static/test.html", 17 | "/static/tests/common/index.html", 18 | ]); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/demo/tests/03-prerender/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { setup as _setup } from "../common/setup"; 3 | import { teardown as _teardown } from "../common/teardown"; 4 | import react from "@vitejs/plugin-react-swc"; 5 | import vercel from "vite-plugin-vercel"; 6 | 7 | export const setup = _setup(path.basename(__dirname), { 8 | configFile: false, 9 | mode: "production", 10 | root: process.cwd(), 11 | plugins: [ 12 | react(), 13 | vercel({ 14 | smart: false, 15 | }), 16 | ], 17 | vercel: { 18 | prerender() { 19 | return { 20 | ssr: { 21 | path: "ssr_", 22 | }, 23 | }; 24 | }, 25 | distContainsOnlyStatic: false, 26 | }, 27 | }); 28 | 29 | export const teardown = _teardown(path.basename(__dirname)); 30 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/fs.test.ts: -------------------------------------------------------------------------------- 1 | import { testFs } from "../common/helpers"; 2 | import { describe } from "vitest"; 3 | import path from "node:path"; 4 | 5 | describe("fs", () => { 6 | testFs(path.basename(__dirname), [ 7 | "/config.json", 8 | "/functions/api/name/[name].func/.vc-config.json", 9 | "/functions/api/name/[name].func/index.mjs", 10 | "/functions/api/page.func/index.mjs", 11 | "/functions/api/page.func/.vc-config.json", 12 | "/functions/api/post.func/index.mjs", 13 | "/functions/api/post.func/.vc-config.json", 14 | "/functions/edge.func/index.js", 15 | "/functions/edge.func/.vc-config.json", 16 | "/functions/page1.func/index.mjs", 17 | "/functions/page1.func/.vc-config.json", 18 | "/functions/page1.prerender-config.json", 19 | ]); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/express/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import vike from "vike/plugin"; 3 | import type { UserConfig } from "vite"; 4 | import vercel from "vite-plugin-vercel"; 5 | 6 | export default { 7 | plugins: [ 8 | react(), 9 | vike(), 10 | vercel({ 11 | // You usually want the server to handle all routes 12 | source: "/.*", 13 | }), 14 | ], 15 | vercel: { 16 | additionalEndpoints: [ 17 | { 18 | // entry file to the server. Default export must be a node server or a function 19 | source: "express-entry.ts", 20 | // replaces default Vike target 21 | destination: "ssr_", 22 | // already added by default Vike route 23 | route: false, 24 | }, 25 | ], 26 | }, 27 | } as UserConfig; 28 | -------------------------------------------------------------------------------- /examples/demo/endpoints/og-edge.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ImageResponse } from "@vercel/og"; 3 | 4 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 5 | 6 | export const edge = true; 7 | 8 | export default async function handler(request: VercelRequest, response: VercelResponse) { 9 | return new ImageResponse( 10 |
23 | 👋 Hello 24 |
, 25 | { 26 | width: 1200, 27 | height: 630, 28 | }, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { setup as _setup } from "../common/setup"; 3 | import { teardown as _teardown } from "../common/teardown"; 4 | import react from "@vitejs/plugin-react-swc"; 5 | import vercel from "vite-plugin-vercel"; 6 | 7 | export const setup = _setup(path.basename(__dirname), { 8 | configFile: false, 9 | mode: "production", 10 | root: process.cwd(), 11 | plugins: [ 12 | react(), 13 | vercel({ 14 | smart: false, 15 | }), 16 | ], 17 | vercel: { 18 | isr: { 19 | page1: { 20 | expiration: 42, 21 | route: "/page1", 22 | symlink: "api/page", 23 | }, 24 | }, 25 | distContainsOnlyStatic: false, 26 | }, 27 | }); 28 | 29 | export const teardown = _teardown(path.basename(__dirname)); 30 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/prerender.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputPrerenderConfigSchema } from "../../../../packages/vercel/src/schemas/config/prerender-config"; 4 | import { prepareTestJsonFileContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFileContent(path.basename(__dirname), "/functions/page1.prerender-config.json", (context) => { 7 | testSchema(context, vercelOutputPrerenderConfigSchema); 8 | 9 | it("should have only necessary properties", () => { 10 | expect(Object.keys(context.file as any)).toHaveLength(2); 11 | expect(context.file).toHaveProperty("expiration", 42); 12 | expect(context.file).toHaveProperty("group"); 13 | expect((context.file as any).group).toBeTypeOf("number"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/demo/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import { usePageContext } from "../../renderer/usePageContext"; 3 | 4 | export default function Page() { 5 | const ctx = usePageContext(); 6 | let { is404, abortReason } = ctx; 7 | if (!abortReason) { 8 | abortReason = is404 ? "Page not found." : "Something went wrong."; 9 | } 10 | return ( 11 |
12 |

{abortReason}

13 |
14 | ); 15 | } 16 | 17 | function Center({ style, ...props }: React.HTMLAttributes) { 18 | return ( 19 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/vike-integration/templates/ssr_.template.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { renderPage } from "vike/server"; 3 | import { getDefaultEmptyResponseHandler, getDefaultPageContextInit, getDefaultResponseHandler } from "./node-helpers"; 4 | 5 | export default async function handler(request: VercelRequest, response: VercelResponse) { 6 | console.debug("query", request.query); 7 | console.debug("url", request.url); 8 | console.debug("headers", request.headers); 9 | const pageContextInit = getDefaultPageContextInit(request); 10 | const pageContext = await renderPage(pageContextInit); 11 | const { httpResponse } = pageContext; 12 | 13 | if (!httpResponse) { 14 | return getDefaultEmptyResponseHandler(response); 15 | } 16 | 17 | return getDefaultResponseHandler(response, httpResponse); 18 | } 19 | -------------------------------------------------------------------------------- /examples/demo/tests/02-additional-endpoints/fs.test.ts: -------------------------------------------------------------------------------- 1 | import { testFs } from "../common/helpers"; 2 | import { describe } from "vitest"; 3 | import path from "node:path"; 4 | 5 | describe("fs", () => { 6 | testFs(path.basename(__dirname), [ 7 | "/config.json", 8 | "/functions/api/name/[name].func/.vc-config.json", 9 | "/functions/api/name/[name].func/index.mjs", 10 | "/functions/api/page.func/index.mjs", 11 | "/functions/api/page.func/.vc-config.json", 12 | "/functions/api/post.func/index.mjs", 13 | "/functions/api/post.func/.vc-config.json", 14 | "/functions/edge.func/index.js", 15 | "/functions/edge.func/.vc-config.json", 16 | "/functions/index2.func/index.mjs", 17 | "/functions/index2.func/.vc-config.json", 18 | "/functions/index3.func/index.mjs", 19 | "/functions/index3.func/.vc-config.json", 20 | "/static/test.html", 21 | "/static/tests/common/index.html", 22 | ]); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/demo/pages/function/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "../../renderer/Link"; 3 | 4 | export default function Page(props: { d: string }) { 5 | return ( 6 | <> 7 |

Welcome

8 | This page is: 9 |
    10 |
  • Using a route function
  • 11 |
  • 12 | Some pages are static: 13 |
      14 |
    • 15 | /function/a 16 |
    • 17 |
    18 |
  • 19 |
  • 20 | All other pages are dynamic, e.g.: 21 |
      22 |
    • 23 | /function/b 24 |
    • 25 |
    • 26 | /function/e/f/g/h/i 27 |
    • 28 |
    29 |
  • 30 |
  • {props.d}
  • 31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-vercel-simple", 3 | "private": true, 4 | "version": null, 5 | "description": "", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx ./express-entry.ts", 9 | "build": "vite build", 10 | "typecheck": "tsc -p tsconfig.json --noEmit" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "devDependencies": { 15 | "@types/express": "^4.17.25", 16 | "@types/node": "^18.19.130", 17 | "@types/react": "^18.3.27", 18 | "@types/react-dom": "^18.3.7", 19 | "@universal-middleware/express": "^0.2.10", 20 | "@vercel/node": "^3.2.29", 21 | "@vitejs/plugin-react-swc": "^3.11.0", 22 | "express": "^4.22.1", 23 | "react": "^18.3.1", 24 | "tsx": "^4.21.0", 25 | "typescript": "^5.9.3", 26 | "vike": "^0.4.249", 27 | "vike-react": "^0.5.13", 28 | "vite": "^6.4.1" 29 | }, 30 | "dependencies": { 31 | "vite-plugin-vercel": "workspace:*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vercel/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { normalizePath, type ResolvedConfig, type UserConfig } from "vite"; 2 | import path from "node:path"; 3 | 4 | export function getRoot(config: UserConfig | ResolvedConfig): string { 5 | return normalizePath(config.root || process.cwd()); 6 | } 7 | 8 | export function getOutput( 9 | config: ResolvedConfig, 10 | suffix?: "functions" | `functions/${string}.func` | "static", 11 | ): string { 12 | return path.join( 13 | config.vercel?.outDir ? "" : getRoot(config), 14 | config.vercel?.outDir ?? ".vercel/output", 15 | suffix ?? "", 16 | ); 17 | } 18 | 19 | export function getPublic(config: ResolvedConfig): string { 20 | return path.join(getRoot(config), config.publicDir || "public"); 21 | } 22 | 23 | export function pathRelativeTo(filePath: string, config: UserConfig | ResolvedConfig, rel: string): string { 24 | const root = getRoot(config); 25 | return normalizePath(path.relative(normalizePath(path.join(root, rel)), filePath)); 26 | } 27 | -------------------------------------------------------------------------------- /examples/demo/tests/common/utils.ts: -------------------------------------------------------------------------------- 1 | import os from "node:os"; 2 | import path from "node:path"; 3 | import { type InlineConfig, build } from "vite"; 4 | 5 | export function getTmpDir(displayName: string) { 6 | return path.join(os.tmpdir(), `vpv-demo-${displayName}`); 7 | } 8 | 9 | export async function callBuild(dirname: string, config: InlineConfig) { 10 | const tmpdir = getTmpDir(dirname); 11 | 12 | await build({ 13 | ...config, 14 | vercel: { 15 | ...config.vercel, 16 | additionalEndpoints: [ 17 | { 18 | source: "endpoints/edge.ts", 19 | destination: "edge", 20 | route: true, 21 | }, 22 | ...(config.vercel?.additionalEndpoints ?? []), 23 | ], 24 | outDir: tmpdir, 25 | }, 26 | build: { 27 | ssr: true, 28 | ...config.build, 29 | rollupOptions: { 30 | input: { 31 | "index.html": "tests/common/index.html", 32 | }, 33 | }, 34 | }, 35 | logLevel: "info", 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /examples/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import vike from "vike/plugin"; 3 | import type { UserConfig } from "vite"; 4 | import vercel from "vite-plugin-vercel"; 5 | 6 | export default { 7 | plugins: [react(), vike(), vercel()], 8 | vercel: { 9 | expiration: 25, 10 | additionalEndpoints: [ 11 | { 12 | source: "endpoints/edge.ts", 13 | destination: "edge", 14 | route: true, 15 | }, 16 | { 17 | source: "endpoints/og-node.tsx", 18 | destination: "og-node", 19 | route: true, 20 | }, 21 | { 22 | source: "endpoints/og-edge.tsx", 23 | destination: "og-edge", 24 | route: true, 25 | }, 26 | ], 27 | }, 28 | // We manually add a list of dependencies to be pre-bundled, in order to avoid a page reload at dev start which breaks vike's CI 29 | // (The 'react/jsx-runtime' entry is not needed in Vite 3 anymore.) 30 | optimizeDeps: { include: ["cross-fetch", "react/jsx-runtime"] }, 31 | } as UserConfig; 32 | -------------------------------------------------------------------------------- /examples/demo/renderer/+onRenderClient.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderClient 2 | export default onRenderClient; 3 | 4 | import "./css/index.css"; 5 | import React from "react"; 6 | import ReactDOM from "react-dom/client"; 7 | import { PageShell } from "./PageShell"; 8 | import { getPageTitle } from "./getPageTitle"; 9 | import type { PageContextClient } from "./types"; 10 | 11 | let root: ReactDOM.Root; 12 | async function onRenderClient(pageContext: PageContextClient) { 13 | const { Page, pageProps } = pageContext; 14 | const page = ( 15 | 16 | 17 | 18 | ); 19 | // biome-ignore lint/style/noNonNullAssertion: 20 | const container = document.getElementById("page-view")!; 21 | if (pageContext.isHydration) { 22 | root = ReactDOM.hydrateRoot(container, page); 23 | } else { 24 | if (!root) { 25 | root = ReactDOM.createRoot(container); 26 | } 27 | root.render(page); 28 | } 29 | document.title = getPageTitle(pageContext); 30 | } 31 | -------------------------------------------------------------------------------- /examples/demo/tests/02-additional-endpoints/globalSetup.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from "node:path"; 3 | import react from "@vitejs/plugin-react-swc"; 4 | import vercel from "vite-plugin-vercel"; 5 | import { setup as _setup } from "../common/setup"; 6 | import { teardown as _teardown } from "../common/teardown"; 7 | 8 | export const setup = _setup(path.basename(__dirname), { 9 | configFile: false, 10 | mode: "production", 11 | root: process.cwd(), 12 | plugins: [ 13 | react(), 14 | vercel({ 15 | smart: false, 16 | }), 17 | ], 18 | vercel: { 19 | additionalEndpoints: [ 20 | { 21 | source: "./tests/common/index2.ts", 22 | destination: "index2", 23 | }, 24 | { 25 | source: { 26 | contents: 'console.log("hi");', 27 | sourcefile: "hi.ts", 28 | loader: "ts", 29 | }, 30 | destination: "index3", 31 | }, 32 | ], 33 | }, 34 | }); 35 | 36 | export const teardown = _teardown(path.basename(__dirname)); 37 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import glob from "fast-glob"; 4 | import { beforeAll, describe } from "vitest"; 5 | import type { TestContext } from "../common/helpers"; 6 | 7 | export function prepareTestJsonFileContent(file: string, callback: (context: T) => void) { 8 | const context = { 9 | file: undefined, 10 | } as T; 11 | 12 | beforeAll(async () => { 13 | const dest = path.join(__dirname, "../../.vercel/output", file); 14 | const entries = await glob(dest); 15 | 16 | if (entries.length !== 1) { 17 | throw new Error(`Multiple or no file matches ${dest}`); 18 | } 19 | 20 | const fileContent = await fs.readFile(entries[0], { 21 | encoding: "utf-8", 22 | }); 23 | 24 | context.file = JSON.parse(fileContent); 25 | }); 26 | 27 | describe(file, () => { 28 | callback(context); 29 | }); 30 | } 31 | 32 | export function prepareTestJsonFilesContent(files: string[], callback: (context: T) => void) { 33 | for (const f of files) { 34 | prepareTestJsonFileContent(f, callback); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/vike-integration/route-regex.ts: -------------------------------------------------------------------------------- 1 | export function escapeStringRegexp(str: string) { 2 | return str.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); 3 | } 4 | 5 | function getSegmentRegex(segment: string): string { 6 | if (segment.startsWith("@")) { 7 | return "/[^/]+"; 8 | } 9 | if (segment === "*") { 10 | return "/.+?"; 11 | } 12 | return `/${segment}`; 13 | } 14 | 15 | export function getParametrizedRoute(route: string): string { 16 | const segments = (route.replace(/\/$/, "") || "/").slice(1).split("/"); 17 | return segments.map(getSegmentRegex).join(""); 18 | } 19 | 20 | export function getRoutesRegex(normalizedRoutes: string[]): string { 21 | const results = normalizedRoutes.map(getParametrizedRoute); 22 | return `^(${results.join("|")})(?:/)?$`; 23 | } 24 | 25 | export function getComplementaryRoutesRegex(normalizedRoutes: string[]): string { 26 | const results = normalizedRoutes.map(getParametrizedRoute); 27 | return results.map((r) => `(?!${r})`).join(""); 28 | } 29 | 30 | export function getVercelPattern(route: string): string { 31 | if (route.endsWith("/*")) { 32 | return route.replace(/\/\*/g, "/:any*"); 33 | } 34 | return route; 35 | } 36 | -------------------------------------------------------------------------------- /examples/demo/pages/catch-all/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "../../renderer/Link"; 3 | 4 | export default function Page(props: { d: string }) { 5 | return ( 6 | <> 7 |

Welcome

8 | This page is: 9 |
    10 |
  • Catch-all /catch-all routes
  • 11 |
  • 12 | Some pages are static: 13 |
      14 |
    • 15 | /catch-all/a/b/c 16 |
    • 17 |
    • 18 | /catch-all/a/d 19 |
    • 20 |
    21 |
  • 22 |
  • 23 | All other pages are ISR, e.g.: 24 |
      25 |
    • 26 | /catch-all/a 27 |
    • 28 |
    • 29 | /catch-all/d 30 |
    • 31 |
    • 32 | /catch-all/e/f/g/h/i 33 |
    • 34 |
    35 |
  • 36 |
  • {props.d}
  • 37 |
38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/demo/tests/01-minimal/vc-config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputVcConfigSchema } from "../../../../packages/vercel/src/schemas/config/vc-config"; 4 | import { prepareTestJsonFilesContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFilesContent( 7 | path.basename(__dirname), 8 | ["/functions/api/page.func/.vc-config.json", "/functions/api/post.func/.vc-config.json"], 9 | (context) => { 10 | testSchema(context, vercelOutputVcConfigSchema); 11 | 12 | it("should have only necessary properties", () => { 13 | expect(context.file).toStrictEqual({ 14 | handler: "index.mjs", 15 | launcherType: "Nodejs", 16 | runtime: "nodejs24.x", 17 | shouldAddHelpers: true, 18 | }); 19 | }); 20 | }, 21 | ); 22 | 23 | prepareTestJsonFilesContent(path.basename(__dirname), ["/functions/edge.func/.vc-config.json"], (context) => { 24 | testSchema(context, vercelOutputVcConfigSchema); 25 | 26 | it("should have only necessary properties", () => { 27 | expect(context.file).toStrictEqual({ 28 | runtime: "edge", 29 | entrypoint: "index.js", 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/demo/renderer/+onRenderHtml.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderHtml 2 | export default onRenderHtml; 3 | 4 | import ReactDOMServer from "react-dom/server"; 5 | import React from "react"; 6 | import { escapeInject } from "vike/server"; 7 | import { PageShell } from "./PageShell"; 8 | import { getPageTitle } from "./getPageTitle"; 9 | import type { PageContextServer } from "./types"; 10 | 11 | async function onRenderHtml(pageContext: PageContextServer) { 12 | const { Page, pageProps } = pageContext; 13 | 14 | const stream = ReactDOMServer.renderToString( 15 | 16 | 17 | , 18 | ); 19 | 20 | const title = getPageTitle(pageContext); 21 | 22 | const documentHtml = escapeInject` 23 | 24 | 25 | ${title} 26 | 27 | 28 |
${stream}
29 | 30 | `; 31 | 32 | return { 33 | documentHtml, 34 | // See https://vike.dev/stream#initial-data-after-stream-end 35 | pageContext: async () => { 36 | return { 37 | someAsyncProps: 42, 38 | }; 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/demo/tests/03-prerender/vc-config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputVcConfigSchema } from "../../../../packages/vercel/src/schemas/config/vc-config"; 4 | import { prepareTestJsonFilesContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFilesContent( 7 | path.basename(__dirname), 8 | ["/functions/api/page.func/.vc-config.json", "/functions/api/post.func/.vc-config.json"], 9 | (context) => { 10 | testSchema(context, vercelOutputVcConfigSchema); 11 | 12 | it("should have only necessary properties", () => { 13 | expect(context.file).toStrictEqual({ 14 | handler: "index.mjs", 15 | launcherType: "Nodejs", 16 | runtime: "nodejs24.x", 17 | shouldAddHelpers: true, 18 | }); 19 | }); 20 | }, 21 | ); 22 | 23 | prepareTestJsonFilesContent(path.basename(__dirname), ["/functions/edge.func/.vc-config.json"], (context) => { 24 | testSchema(context, vercelOutputVcConfigSchema); 25 | 26 | it("should have only necessary properties", () => { 27 | expect(context.file).toStrictEqual({ 28 | runtime: "edge", 29 | entrypoint: "index.js", 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x, 22.x, 24.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: pnpm/action-setup@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v6 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'pnpm' 29 | - name: Install dependencies 30 | run: pnpm install 31 | - name: Build 32 | run: pnpm run build:all 33 | - name: Typecheck 34 | run: pnpm run typecheck:all 35 | - name: Lint 36 | run: pnpm lint 37 | - name: Test 38 | run: pnpm test 39 | -------------------------------------------------------------------------------- /examples/demo/renderer/types.ts: -------------------------------------------------------------------------------- 1 | export type { PageContextServer }; 2 | export type { PageContextClient }; 3 | export type { PageContext }; 4 | export type { PageProps }; 5 | 6 | import type { 7 | PageContextBuiltInServer, 8 | //* 9 | // When using Client Routing https://vike.dev/clientRouting 10 | PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient, 11 | /*/ 12 | // When using Server Routing 13 | PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient 14 | //*/ 15 | } from "vike/types"; 16 | 17 | type Page = (pageProps: PageProps) => React.ReactElement; 18 | type PageProps = Record; 19 | 20 | export type PageContextCustom = { 21 | Page: Page; 22 | pageProps?: PageProps; 23 | config: { 24 | /** Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) */ 25 | title?: string; 26 | }; 27 | /** Title defined dynamically by onBeforeRender() */ 28 | title?: string; 29 | abortReason?: string; 30 | }; 31 | 32 | type PageContextServer = PageContextBuiltInServer & PageContextCustom; 33 | type PageContextClient = PageContextBuiltInClient & PageContextCustom; 34 | 35 | type PageContext = PageContextClient | PageContextServer; 36 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/vc-config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputVcConfigSchema } from "../../../../packages/vercel/src/schemas/config/vc-config"; 4 | import { prepareTestJsonFilesContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFilesContent( 7 | path.basename(__dirname), 8 | [ 9 | "/functions/api/page.func/.vc-config.json", 10 | "/functions/api/post.func/.vc-config.json", 11 | "/functions/page1.func/.vc-config.json", 12 | ], 13 | (context) => { 14 | testSchema(context, vercelOutputVcConfigSchema); 15 | 16 | it("should have only necessary properties", () => { 17 | expect(context.file).toStrictEqual({ 18 | handler: "index.mjs", 19 | launcherType: "Nodejs", 20 | runtime: "nodejs24.x", 21 | shouldAddHelpers: true, 22 | }); 23 | }); 24 | }, 25 | ); 26 | 27 | prepareTestJsonFilesContent(path.basename(__dirname), ["/functions/edge.func/.vc-config.json"], (context) => { 28 | testSchema(context, vercelOutputVcConfigSchema); 29 | 30 | it("should have only necessary properties", () => { 31 | expect(context.file).toStrictEqual({ 32 | runtime: "edge", 33 | entrypoint: "index.js", 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/vike-integration/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { rename } from "node:fs/promises"; 3 | import { defineConfig } from "tsup"; 4 | 5 | export default defineConfig([ 6 | { 7 | clean: true, 8 | entry: ["./vike.ts", "./templates/node-helpers.ts", "./templates/edge-helpers.ts", "./+config.ts"], 9 | external: ["esbuild", "rollup", "vite", "vike"], 10 | format: ["esm"], 11 | 12 | platform: "node", 13 | target: "node18", 14 | dts: { 15 | entry: ["./vike.ts", "./templates/node-helpers.ts", "./templates/edge-helpers.ts", "./+config.ts"], 16 | }, 17 | async onSuccess() { 18 | // rollup-plugin-dts chooses to rename things its way 19 | // and tsup doesn't wait for dts to be done to trigger this callback... 20 | // so https://github.com/egoist/tsup/issues/700#issuecomment-1499745933 21 | 22 | const start = Date.now(); 23 | 24 | // timeout after 5 seconds 25 | while (start + 5000 > Date.now()) { 26 | if (existsSync("./dist/_config.d.ts")) { 27 | break; 28 | } 29 | await new Promise((resolve) => { 30 | setTimeout(resolve, 250); 31 | }); 32 | } 33 | 34 | await rename("./dist/_config.d.ts", "./dist/+config.d.ts"); 35 | }, 36 | }, 37 | ]); 38 | -------------------------------------------------------------------------------- /examples/demo/tests/02-additional-endpoints/vc-config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputVcConfigSchema } from "../../../../packages/vercel/src/schemas/config/vc-config"; 4 | import { prepareTestJsonFilesContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFilesContent( 7 | path.basename(__dirname), 8 | [ 9 | "/functions/api/page.func/.vc-config.json", 10 | "/functions/api/post.func/.vc-config.json", 11 | "/functions/index2.func/.vc-config.json", 12 | "/functions/index3.func/.vc-config.json", 13 | ], 14 | (context) => { 15 | testSchema(context, vercelOutputVcConfigSchema); 16 | 17 | it("should have only necessary properties", () => { 18 | expect(context.file).toStrictEqual({ 19 | handler: "index.mjs", 20 | launcherType: "Nodejs", 21 | runtime: "nodejs24.x", 22 | shouldAddHelpers: true, 23 | }); 24 | }); 25 | }, 26 | ); 27 | 28 | prepareTestJsonFilesContent(path.basename(__dirname), ["/functions/edge.func/.vc-config.json"], (context) => { 29 | testSchema(context, vercelOutputVcConfigSchema); 30 | 31 | it("should have only necessary properties", () => { 32 | expect(context.file).toStrictEqual({ 33 | runtime: "edge", 34 | entrypoint: "index.js", 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-vercel-workspace-root", 3 | "packageManager": "pnpm@9.15.9", 4 | "private": "true", 5 | "type": "module", 6 | "scripts": { 7 | "dev:all": "pnpm -r --filter \"./packages/*\" --parallel dev", 8 | "dev:demo": "pnpm -r --filter \"./examples/demo\" --parallel dev", 9 | "vercel-build": "pnpm run build:all", 10 | "build:all": "pnpm -r --filter \"./packages/vercel\" build && pnpm -r --filter \"./packages/vike-integration\" build && pnpm -r --filter \"./examples/*\" build", 11 | "build:demo": "pnpm run build:all && cp -rf examples/demo/public examples/demo/.vercel .", 12 | "typecheck:all": "pnpm -r typecheck", 13 | "test": "pnpm run build:all && pnpm run -r test", 14 | "format": "biome format --write .", 15 | "lint": "biome lint .", 16 | "prepublishOnly": "pnpm run test && changeset version", 17 | "publish:beta": "changeset pre enter beta && pnpm run prepublishOnly && changeset publish -r --tag beta && changeset pre exit", 18 | "publish:stable": "pnpm run prepublishOnly && changeset publish -r" 19 | }, 20 | "devDependencies": { 21 | "@biomejs/biome": "^1.9.4", 22 | "@changesets/cli": "^2.29.8", 23 | "@types/node": "^18.19.130", 24 | "cross-env": "^7.0.3", 25 | "typescript": "^5.9.3" 26 | }, 27 | "pnpm": { 28 | "overrides": { 29 | "path-to-regexp@>=4.0.0 <6.3.0": "^6.3.0" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/demo/endpoints/og-node.tsx: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { Readable } from "node:stream"; 4 | import type { ReadableStream } from "node:stream/web"; 5 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 6 | import { ImageResponse } from "@vercel/og"; 7 | import React from "react"; 8 | 9 | const font = readFileSync(join(__dirname, "./Roboto-Regular.ttf")); 10 | 11 | export default async function handler(request: VercelRequest, response: VercelResponse) { 12 | const resp = new ImageResponse( 13 |
27 | 👋 Hello 28 |
, 29 | { 30 | width: 1200, 31 | height: 630, 32 | fonts: [ 33 | { 34 | name: "Roboto", 35 | // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. 36 | data: font, 37 | weight: 400, 38 | style: "normal", 39 | }, 40 | ], 41 | }, 42 | ); 43 | 44 | Readable.fromWeb(resp.body as ReadableStream).pipe(response); 45 | } 46 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/vc-config.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { vercelOutputVcConfigSchema } from "../../../../packages/vercel/src/schemas/config/vc-config"; 3 | import { testSchema } from "../common/helpers"; 4 | import { prepareTestJsonFilesContent } from "./utils"; 5 | 6 | prepareTestJsonFilesContent( 7 | [ 8 | "/functions/api/page.func/.vc-config.json", 9 | "/functions/api/post.func/.vc-config.json", 10 | "/functions/ssr_.func/.vc-config.json", 11 | "/functions/pages/catch-all-*.func/.vc-config.json", 12 | "/functions/pages/isr-*.func/.vc-config.json", 13 | "/functions/pages/named-*.func/.vc-config.json", 14 | ], 15 | (context) => { 16 | testSchema(context, vercelOutputVcConfigSchema); 17 | 18 | it("should have only necessary properties", () => { 19 | expect(context.file).toStrictEqual({ 20 | handler: "index.mjs", 21 | launcherType: "Nodejs", 22 | runtime: "nodejs24.x", 23 | shouldAddHelpers: true, 24 | supportsResponseStreaming: true, 25 | }); 26 | }); 27 | }, 28 | ); 29 | 30 | prepareTestJsonFilesContent(["/functions/edge.func/.vc-config.json"], (context) => { 31 | testSchema(context, vercelOutputVcConfigSchema); 32 | 33 | it("should have only necessary properties", () => { 34 | expect(context.file).toStrictEqual({ 35 | runtime: "edge", 36 | entrypoint: "index.js", 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/demo/renderer/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "vike/types"; 2 | import config from "@vite-plugin-vercel/vike/config"; 3 | 4 | // https://vike.dev/config 5 | export default { 6 | passToClient: ["pageProps", "title", "someAsyncProps"], 7 | clientRouting: true, 8 | hydrationCanBeAborted: true, 9 | // https://vike.dev/meta 10 | meta: { 11 | // Create new config 'title' 12 | title: { 13 | env: { server: true, client: true }, 14 | }, 15 | // Create new config 'onBeforeRenderIsomorph' 16 | onBeforeRenderIsomorph: { 17 | env: { config: true }, 18 | effect({ configDefinedAt, configValue }) { 19 | if (typeof configValue !== "boolean") { 20 | throw new Error(`${configDefinedAt} should be a boolean`); 21 | } 22 | if (configValue) { 23 | return { 24 | meta: { 25 | onBeforeRender: { 26 | // We override VPS's default behavior of always loading/executing onBeforeRender() on the server-side. 27 | // If we set onBeforeRenderIsomorph to true, then onBeforeRender() is loaded/executed in the browser as well, allowing us to fetch data direcly from the browser upon client-side navigation (without involving our Node.js/Edge server at all). 28 | env: { server: true, client: true }, 29 | }, 30 | }, 31 | }; 32 | } 33 | }, 34 | }, 35 | }, 36 | prerender: true, 37 | extends: config, 38 | } satisfies Config; 39 | -------------------------------------------------------------------------------- /examples/demo/pages/named/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "../../renderer/Link"; 3 | 4 | function isISR(someId: string) { 5 | return someId !== "id-1" && someId !== "id-2"; 6 | } 7 | 8 | export default function Page(props: { d: string; someId: string }) { 9 | return ( 10 | <> 11 |

Welcome

12 | This page is: 13 |
    14 |
  • Static with url parameter: {props.someId}
  • 15 |
  • 16 | Some pages are static: 17 |
      18 |
    • 19 | /named/id-1 20 |
    • 21 |
    • 22 | /named/id-2 23 |
    • 24 |
    25 |
  • 26 |
  • 27 | All other pages are ISR, e.g.: 28 |
      29 |
    • 30 | /named/id-3 31 |
    • 32 |
    • 33 | /named/something 34 |
    • 35 |
    36 |
  • 37 |
  • 38 | 404 on sub-pages, e.g.: 39 |
      40 |
    • 41 | /named/id-1/a 42 |
    • 43 |
    • 44 | /named/something/a 45 |
    • 46 |
    47 |
  • 48 |
  • {isISR(props.someId) ? "ISR" : "Static"}
  • 49 |
  • {props.d}
  • 50 |
51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/vercel/src/schemas/config/vc-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema definition for `.vercel/output/functions/.func/.vc-config.json` 3 | * @see {@link https://vercel.com/docs/build-output-api/v3/primitives#serverless-function-configuration} 4 | */ 5 | 6 | import { z } from "zod"; 7 | 8 | export const vercelOutputEdgeVcConfigSchema = z 9 | .object({ 10 | runtime: z.literal("edge"), 11 | entrypoint: z.string(), 12 | envVarsInUse: z.array(z.string()).optional(), 13 | }) 14 | .strict(); 15 | 16 | export const vercelOutputServerlessVcConfigSchema = z 17 | .object({ 18 | runtime: z.string(), 19 | handler: z.string(), 20 | memory: z.number().int().min(128).max(3008).optional(), 21 | maxDuration: z.number().int().positive().optional(), 22 | environment: z.record(z.string()).optional(), 23 | regions: z.array(z.string()).optional(), 24 | supportsWrapper: z.boolean().optional(), 25 | supportsResponseStreaming: z.boolean().optional(), 26 | }) 27 | .strict(); 28 | 29 | export const vercelOutputServerlessNodeVcConfigSchema = vercelOutputServerlessVcConfigSchema 30 | .extend({ 31 | launcherType: z.literal("Nodejs"), 32 | shouldAddHelpers: z.boolean().optional(), 33 | shouldAddSourcemapSupport: z.boolean().optional(), 34 | awsLambdaHandler: z.string().optional(), 35 | }) 36 | .strict(); 37 | 38 | export const vercelOutputVcConfigSchema = z.union([ 39 | vercelOutputEdgeVcConfigSchema, 40 | vercelOutputServerlessVcConfigSchema, 41 | vercelOutputServerlessNodeVcConfigSchema, 42 | ]); 43 | 44 | export type VercelOutputVcConfig = z.infer; 45 | -------------------------------------------------------------------------------- /packages/vike-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vite-plugin-vercel/vike", 3 | "version": "9.1.0", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "templates", 8 | "*.d.ts", 9 | "helpers.js", 10 | "config.js" 11 | ], 12 | "description": "Use vike with vite-plugin-vercel", 13 | "author": "Joël Charles ", 14 | "repository": "https://github.com/magne4000/vite-plugin-vercel", 15 | "main": "./dist/vike.js", 16 | "types": "./dist/vike.d.ts", 17 | "exports": { 18 | ".": "./dist/vike.js", 19 | "./helpers/node": "./dist/templates/node-helpers.js", 20 | "./helpers/edge": "./dist/templates/edge-helpers.js", 21 | "./config": { 22 | "types": "./config.d.ts", 23 | "default": "./dist/+config.js" 24 | } 25 | }, 26 | "scripts": { 27 | "build": "tsup", 28 | "dev": "tsup --watch", 29 | "prepack": "rm -rf dist && pnpm build", 30 | "typecheck": "tsc -p tsconfig.json --noEmit", 31 | "lint:ts": "eslint . --max-warnings 0 --ignore-pattern dist" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^18.19.130", 35 | "@types/qs": "^6.14.0", 36 | "@vercel/edge": "^1.2.2", 37 | "@vercel/node": "^3.2.29", 38 | "tsup": "^8.5.1", 39 | "typescript": "^5.9.3", 40 | "vike": "^0.4.249", 41 | "vite": "^6.4.1", 42 | "vite-plugin-vercel": "workspace:*" 43 | }, 44 | "dependencies": { 45 | "@brillout/libassert": "^0.5.8", 46 | "nanoid": "^5.1.6", 47 | "qs": "^6.14.0" 48 | }, 49 | "peerDependencies": { 50 | "vike": "^0.4.229", 51 | "vite": "^5.0.2 || ^6", 52 | "vite-plugin-vercel": "*" 53 | }, 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /examples/demo/tests/01-minimal/config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputConfigSchema } from "../../../../packages/vercel/src/schemas/config/config"; 4 | import { prepareTestJsonFileContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFileContent(path.basename(__dirname), "config.json", (context) => { 7 | testSchema(context, vercelOutputConfigSchema); 8 | 9 | it("should have defaults routes only", () => { 10 | expect(context.file).toHaveProperty("routes", [ 11 | { 12 | src: "^/api/page$", 13 | headers: { "X-VitePluginVercel-Test": "test" }, 14 | continue: true, 15 | }, 16 | { 17 | headers: { Location: "/$1" }, 18 | src: "^/(?:(.+)/)?index(?:\\.html)?/?$", 19 | status: 308, 20 | }, 21 | { 22 | headers: { Location: "/$1" }, 23 | src: "^/(.*)\\.html/?$", 24 | status: 308, 25 | }, 26 | { handle: "filesystem" }, 27 | { 28 | src: "^/edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 29 | dest: "/edge/$1", 30 | check: true, 31 | }, 32 | { 33 | check: true, 34 | src: "^/api/page$", 35 | dest: "/api/page", 36 | }, 37 | { 38 | check: true, 39 | src: "^/api/post$", 40 | dest: "/api/post", 41 | }, 42 | { 43 | check: true, 44 | src: "^/api/name(?:/([^/]+?))$", 45 | dest: "/api/name/[name]?name=$1", 46 | }, 47 | ]); 48 | expect(context.file).toHaveProperty("overrides", {}); 49 | expect(Object.keys(context.file as any).sort()).toMatchObject(["version", "overrides", "routes"].sort()); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/demo/tests/03-prerender/config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputConfigSchema } from "../../../../packages/vercel/src/schemas/config/config"; 4 | import { prepareTestJsonFileContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFileContent(path.basename(__dirname), "config.json", (context) => { 7 | testSchema(context, vercelOutputConfigSchema); 8 | 9 | it("should have defaults routes only", () => { 10 | expect(context.file).toHaveProperty("routes", [ 11 | { 12 | src: "^/api/page$", 13 | headers: { "X-VitePluginVercel-Test": "test" }, 14 | continue: true, 15 | }, 16 | { 17 | headers: { Location: "/$1" }, 18 | src: "^/(?:(.+)/)?index(?:\\.html)?/?$", 19 | status: 308, 20 | }, 21 | { 22 | headers: { Location: "/$1" }, 23 | src: "^/(.*)\\.html/?$", 24 | status: 308, 25 | }, 26 | { handle: "filesystem" }, 27 | { 28 | src: "^/edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 29 | dest: "/edge/$1", 30 | check: true, 31 | }, 32 | { 33 | check: true, 34 | src: "^/api/page$", 35 | dest: "/api/page", 36 | }, 37 | { 38 | check: true, 39 | src: "^/api/post$", 40 | dest: "/api/post", 41 | }, 42 | { 43 | check: true, 44 | src: "^/api/name(?:/([^/]+?))$", 45 | dest: "/api/name/[name]?name=$1", 46 | }, 47 | ]); 48 | expect(context.file).toHaveProperty("overrides", { 49 | ssr: { path: "ssr_" }, 50 | }); 51 | expect(Object.keys(context.file as any).sort()).toEqual(["version", "overrides", "routes"].sort()); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/prerender.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { vercelOutputPrerenderConfigSchema } from "../../../../packages/vercel/src/schemas/config/prerender-config"; 3 | import { testSchema } from "../common/helpers"; 4 | import { prepareTestJsonFileContent } from "./utils"; 5 | 6 | prepareTestJsonFileContent("/functions/pages/catch-all-*.prerender-config.json", (context) => { 7 | testSchema(context, vercelOutputPrerenderConfigSchema); 8 | 9 | it("should have only necessary properties", () => { 10 | expect(Object.keys(context.file as any)).toHaveLength(2); 11 | expect(context.file).toHaveProperty("expiration", 15); 12 | expect(context.file).toHaveProperty("group"); 13 | expect((context.file as any).group).toBeTypeOf("number"); 14 | }); 15 | }); 16 | 17 | prepareTestJsonFileContent("/functions/pages/isr-*.prerender-config.json", (context) => { 18 | testSchema(context, vercelOutputPrerenderConfigSchema); 19 | 20 | it("should have only necessary properties", () => { 21 | expect(Object.keys(context.file as any)).toHaveLength(2); 22 | expect(context.file).toHaveProperty("expiration", 15); 23 | expect(context.file).toHaveProperty("group"); 24 | expect((context.file as any).group).toBeTypeOf("number"); 25 | }); 26 | }); 27 | 28 | prepareTestJsonFileContent("/functions/pages/named-*.prerender-config.json", (context) => { 29 | testSchema(context, vercelOutputPrerenderConfigSchema); 30 | 31 | it("should have only necessary properties", () => { 32 | expect(Object.keys(context.file as any)).toHaveLength(2); 33 | expect(context.file).toHaveProperty("expiration", 25); 34 | expect(context.file).toHaveProperty("group"); 35 | expect((context.file as any).group).toBeTypeOf("number"); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/demo/tests/04-isr/config.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { expect, it } from "vitest"; 3 | import { vercelOutputConfigSchema } from "../../../../packages/vercel/src/schemas/config/config"; 4 | import { prepareTestJsonFileContent, testSchema } from "../common/helpers"; 5 | 6 | prepareTestJsonFileContent(path.basename(__dirname), "config.json", (context) => { 7 | testSchema(context, vercelOutputConfigSchema); 8 | 9 | it("should have defaults routes only", () => { 10 | expect(context.file).toHaveProperty("routes", [ 11 | { 12 | src: "^/api/page$", 13 | headers: { "X-VitePluginVercel-Test": "test" }, 14 | continue: true, 15 | }, 16 | { 17 | headers: { Location: "/$1" }, 18 | src: "^/(?:(.+)/)?index(?:\\.html)?/?$", 19 | status: 308, 20 | }, 21 | { 22 | headers: { Location: "/$1" }, 23 | src: "^/(.*)\\.html/?$", 24 | status: 308, 25 | }, 26 | { handle: "filesystem" }, 27 | { 28 | src: "^/edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 29 | dest: "/edge/$1", 30 | check: true, 31 | }, 32 | { 33 | check: true, 34 | src: "^/api/page$", 35 | dest: "/api/page", 36 | }, 37 | { 38 | check: true, 39 | src: "^/api/post$", 40 | dest: "/api/post", 41 | }, 42 | { 43 | check: true, 44 | src: "^/api/name(?:/([^/]+?))$", 45 | dest: "/api/name/[name]?name=$1", 46 | }, 47 | { check: true, dest: "page1/?__original_path=$1", src: "^(/page1)$" }, 48 | ]); 49 | expect(context.file).toHaveProperty("overrides", {}); 50 | expect(Object.keys(context.file as any).sort()).toEqual(["version", "overrides", "routes"].sort()); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/vike-integration/templates/node-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import type { renderPage } from "vike"; 3 | import { getOriginalUrl } from "./utils"; 4 | 5 | type HttpResponse = NonNullable>["httpResponse"]>; 6 | 7 | /** 8 | * Extract useful pageContext from request to give to renderPage(...). 9 | * It handles internals such as retrieving the original url from `x-now-route-matches` header or from `__original_path` query, 10 | * so it's highly recommended to use it instead of `request.url`. 11 | * @param request 12 | */ 13 | export function getDefaultPageContextInit(request: VercelRequest) { 14 | const query: Record = request.query ?? {}; 15 | const url = getOriginalUrl(request.headers["x-now-route-matches"], query.__original_path, request.url); 16 | return { 17 | urlOriginal: url, 18 | headersOriginal: request.headers, 19 | body: request.body, 20 | cookies: request.cookies, 21 | }; 22 | } 23 | 24 | /** 25 | * Send a default empty HTML response 26 | * @param response 27 | */ 28 | export function getDefaultEmptyResponseHandler(response: VercelResponse) { 29 | response.statusCode = 200; 30 | response.setHeader("content-type", "text/html; charset=UTF-8"); 31 | return response.end(""); 32 | } 33 | 34 | /** 35 | * Send `httpResponse` through `response` 36 | * @param response 37 | * @param httpResponse 38 | */ 39 | export function getDefaultResponseHandler(response: VercelResponse, httpResponse: HttpResponse) { 40 | const { statusCode, headers } = httpResponse; 41 | 42 | response.statusCode = statusCode; 43 | for (const [name, value] of headers) { 44 | response.setHeader(name, value); 45 | } 46 | 47 | return httpResponse.pipe(response); 48 | } 49 | -------------------------------------------------------------------------------- /packages/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-vercel", 3 | "version": "9.1.1", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "*.d.ts" 8 | ], 9 | "main": "./dist/index.cjs", 10 | "module": "./dist/index.js", 11 | "exports": { 12 | ".": { 13 | "types": "./index.d.ts", 14 | "import": "./dist/index.js", 15 | "require": "./dist/index.cjs" 16 | }, 17 | "./types": { 18 | "types": "./index.d.ts" 19 | } 20 | }, 21 | "types": "./index.d.ts", 22 | "description": "Vercel adapter for vite", 23 | "author": "Joël Charles ", 24 | "repository": "https://github.com/magne4000/vite-plugin-vercel", 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsup", 28 | "dev": "tsup --watch", 29 | "prepack": "rm -rf dist && pnpm build", 30 | "typecheck": "tsc -p tsconfig.json --noEmit" 31 | }, 32 | "peerDependencies": { 33 | "@vite-plugin-vercel/vike": "workspace:*", 34 | "vike": "*", 35 | "vite": "^4.4 || ^5.0.2 || ^6" 36 | }, 37 | "peerDependenciesMeta": { 38 | "@vite-plugin-vercel/vike": { 39 | "optional": true 40 | }, 41 | "vike": { 42 | "optional": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^18.19.130", 47 | "@vite-plugin-vercel/vike": "workspace:*", 48 | "tsup": "^8.5.1", 49 | "typescript": "^5.9.3", 50 | "vike": "^0.4.249", 51 | "vite": "^6.4.1" 52 | }, 53 | "dependencies": { 54 | "@brillout/libassert": "^0.5.8", 55 | "@manypkg/find-root": "^2.2.3", 56 | "@vercel/build-utils": "^13.2.3", 57 | "@vercel/nft": "^1.1.1", 58 | "@vercel/routing-utils": "^5.3.1", 59 | "esbuild": "^0.27.1", 60 | "fast-glob": "^3.3.3", 61 | "magicast": "^0.5.1", 62 | "zod": "^3.25.76" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-vercel-demo-v1", 3 | "private": "true", 4 | "scripts": { 5 | "dev": "vike dev", 6 | "build": "vike build", 7 | "preview": "vike build && vike preview", 8 | "typecheck": "tsc -p tsconfig.json --noEmit", 9 | "test": "pnpm run test:01 && pnpm run test:02 && pnpm run test:03 && pnpm run test:04 && pnpm run test:05", 10 | "test:01": "vitest run --dir tests/01-minimal --config tests/01-minimal/vitest.config.ts", 11 | "test:02": "vitest run --dir tests/02-additional-endpoints --config tests/02-additional-endpoints/vitest.config.ts", 12 | "test:03": "vitest run --dir tests/03-prerender --config tests/03-prerender/vitest.config.ts", 13 | "test:04": "vitest run --dir tests/04-isr --config tests/04-isr/vitest.config.ts", 14 | "test:05": "vite build && vitest run --dir tests/05-vike --config tests/05-vike/vitest.config.ts" 15 | }, 16 | "dependencies": { 17 | "@mdx-js/mdx": "^3.1.1", 18 | "@mdx-js/react": "^3.1.1", 19 | "@mdx-js/rollup": "^3.1.1", 20 | "@vercel/edge-config": "^1.4.3", 21 | "@vercel/og": "^0.6.8", 22 | "@vite-plugin-vercel/vike": "workspace:*", 23 | "@vitejs/plugin-react-swc": "^3.11.0", 24 | "cross-fetch": "^4.1.0", 25 | "node-fetch": "^3.3.2", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "typescript": "^5.9.3", 29 | "vike": "^0.4.249", 30 | "vite": "^6.4.1", 31 | "vite-plugin-vercel": "workspace:*" 32 | }, 33 | "type": "module", 34 | "version": null, 35 | "devDependencies": { 36 | "@types/node": "^18.19.130", 37 | "@types/node-fetch": "^2.6.13", 38 | "@types/react": "^18.3.27", 39 | "@types/react-dom": "^18.3.7", 40 | "@vercel/node": "^3.2.29", 41 | "fast-glob": "^3.3.3", 42 | "vitest": "^3.2.4", 43 | "zod": "^3.25.76" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/express/express-entry.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { createHandler, createMiddleware } from "@universal-middleware/express"; 5 | import express from "express"; 6 | import { vikeHandler } from "./server/vike-handler"; 7 | 8 | const __filename = globalThis.__filename ?? fileURLToPath(import.meta.url); 9 | const __dirname = globalThis.__dirname ?? dirname(__filename); 10 | const root = __dirname; 11 | const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000; 12 | const hmrPort = process.env.HMR_PORT ? Number.parseInt(process.env.HMR_PORT, 10) : 24678; 13 | 14 | export default await startServer(); 15 | 16 | async function startServer() { 17 | const app = express(); 18 | 19 | if (process.env.NODE_ENV === "production") { 20 | app.use(express.static(`${root}/dist/client`)); 21 | } else { 22 | // Instantiate Vite's development server and integrate its middleware to our server. 23 | // ! We should instantiate it *only* in development. (It isn't needed in production 24 | // and would unnecessarily bloat our server in production.) 25 | const vite = await import("vite"); 26 | const viteDevMiddleware = ( 27 | await vite.createServer({ 28 | root, 29 | server: { middlewareMode: true, hmr: { port: hmrPort } }, 30 | }) 31 | ).middlewares; 32 | app.use(viteDevMiddleware); 33 | } 34 | 35 | app.get( 36 | "/hello", 37 | createMiddleware(() => () => { 38 | console.log("HELLO"); 39 | return new Response("Hello"); 40 | })(), 41 | ); 42 | 43 | /** 44 | * Vike route 45 | * 46 | * @link {@see https://vike.dev} 47 | **/ 48 | app.all("*", createHandler(() => vikeHandler)()); 49 | 50 | app.listen(port, () => { 51 | console.log(`Server listening on http://localhost:${port}`); 52 | }); 53 | 54 | return app; 55 | } 56 | -------------------------------------------------------------------------------- /packages/vike-integration/templates/edge-helpers.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "qs"; 2 | import type { renderPage } from "vike"; 3 | import { getOriginalUrl } from "./utils"; 4 | 5 | type HttpResponse = NonNullable>["httpResponse"]>; 6 | 7 | /** 8 | * Extract useful pageContext from request to give to renderPage(...). 9 | * It handles internals such as retrieving the original url from `x-now-route-matches` header or from `__original_path` query, 10 | * so it's highly recommended to use it instead of `request.url`. 11 | * @param request 12 | */ 13 | export function getDefaultPageContextInit(request: Request) { 14 | const queryString = new URL(request.url).search.slice(1); // Remove the leading "?" with .slice(1) 15 | const query = parse(queryString); 16 | const url = getOriginalUrl(request.headers.get("x-now-route-matches"), query.__original_path, request.url); 17 | const cookieHeader = request.headers.get("Cookie") || ""; 18 | 19 | const cookies = cookieHeader.split("; ").reduce( 20 | (acc, cookie) => { 21 | const [name, value] = cookie.split("="); 22 | acc[name] = value; 23 | return acc; 24 | }, 25 | {} as Record, 26 | ); 27 | 28 | return { 29 | urlOriginal: url, 30 | headersOriginal: request.headers as Headers, 31 | body: request.body, 32 | cookies, 33 | }; 34 | } 35 | 36 | /** 37 | * Send a default empty HTML response 38 | */ 39 | export function getDefaultEmptyResponseHandler(): Response { 40 | return new Response(null, { 41 | status: 200, 42 | headers: { 43 | "content-type": "text/html; charset=UTF-8", 44 | }, 45 | }); 46 | } 47 | 48 | /** 49 | * Send `httpResponse` through `response` 50 | */ 51 | export function getDefaultResponseHandler(httpResponse: HttpResponse): Response { 52 | const { statusCode, headers } = httpResponse; 53 | 54 | return new Response(httpResponse.getReadableWebStream(), { 55 | status: statusCode, 56 | headers, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/demo/tests/02-additional-endpoints/config.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { vercelOutputConfigSchema } from "../../../../packages/vercel/src/schemas/config/config"; 3 | import { prepareTestJsonFileContent, testSchema } from "../common/helpers"; 4 | 5 | import path from "node:path"; 6 | 7 | prepareTestJsonFileContent(path.basename(__dirname), "config.json", (context) => { 8 | testSchema(context, vercelOutputConfigSchema); 9 | 10 | it("should have defaults routes only", () => { 11 | expect(context.file).toHaveProperty("routes", [ 12 | { 13 | src: "^/api/page$", 14 | headers: { "X-VitePluginVercel-Test": "test" }, 15 | continue: true, 16 | }, 17 | { 18 | headers: { Location: "/$1" }, 19 | src: "^/(?:(.+)/)?index(?:\\.html)?/?$", 20 | status: 308, 21 | }, 22 | { 23 | headers: { Location: "/$1" }, 24 | src: "^/(.*)\\.html/?$", 25 | status: 308, 26 | }, 27 | { handle: "filesystem" }, 28 | { 29 | src: "^/edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 30 | dest: "/edge/$1", 31 | check: true, 32 | }, 33 | { 34 | check: true, 35 | src: "^/index2(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 36 | dest: "/index2/$1", 37 | }, 38 | { 39 | check: true, 40 | src: "^/index3(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 41 | dest: "/index3/$1", 42 | }, 43 | { 44 | check: true, 45 | src: "^/api/page$", 46 | dest: "/api/page", 47 | }, 48 | { 49 | check: true, 50 | src: "^/api/post$", 51 | dest: "/api/post", 52 | }, 53 | { 54 | check: true, 55 | src: "^/api/name(?:/([^/]+?))$", 56 | dest: "/api/name/[name]?name=$1", 57 | }, 58 | ]); 59 | expect(context.file).toHaveProperty("overrides", {}); 60 | expect(Object.keys(context.file as any).sort()).toEqual(["version", "overrides", "routes"].sort()); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /examples/demo/tests/common/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import glob from "fast-glob"; 4 | import { beforeAll, describe, expect, it } from "vitest"; 5 | import type { ZodSchema } from "zod"; 6 | import { getTmpDir } from "./utils"; 7 | 8 | export interface TestContext { 9 | file: unknown; 10 | } 11 | 12 | export function testFs(dirname: string, filesOrCallback: Iterable | ((entries: string[]) => void)) { 13 | it("should generate the right files", async () => { 14 | const tmpdir = getTmpDir(dirname); 15 | const entries = await glob(`${tmpdir}/**`, { dot: true }); 16 | let mappedEntries = entries.map((e) => e.replace(tmpdir, "")).filter((e) => !e.startsWith("/_ignore")); 17 | 18 | mappedEntries = Array.from(new Set(mappedEntries)); 19 | 20 | if (typeof filesOrCallback === "function") { 21 | filesOrCallback(mappedEntries); 22 | } else { 23 | expect(mappedEntries.sort()).toMatchObject(Array.from(filesOrCallback).sort()); 24 | } 25 | }); 26 | } 27 | 28 | export function testSchema(context: TestContext, schema: ZodSchema) { 29 | it("should respect schema", () => { 30 | expect(schema.safeParse(context.file)).not.toHaveProperty("error"); 31 | }); 32 | } 33 | 34 | export function prepareTestJsonFilesContent( 35 | dirname: string, 36 | files: string[], 37 | callback: (context: T) => void, 38 | ) { 39 | for (const f of files) { 40 | prepareTestJsonFileContent(dirname, f, callback); 41 | } 42 | } 43 | 44 | export function prepareTestJsonFileContent( 45 | dirname: string, 46 | file: string, 47 | callback: (context: T) => void, 48 | ) { 49 | const context = { 50 | file: undefined, 51 | } as T; 52 | 53 | beforeAll(async () => { 54 | const dest = path.join(getTmpDir(dirname), file); 55 | const entries = await glob(dest); 56 | 57 | if (entries.length !== 1) { 58 | throw new Error(`Multiple or no file matches ${dest}`); 59 | } 60 | 61 | const fileContent = await fs.readFile(entries[0], { 62 | encoding: "utf-8", 63 | }); 64 | 65 | context.file = JSON.parse(fileContent); 66 | }); 67 | 68 | describe(file, () => { 69 | callback(context); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /examples/demo/renderer/PageShell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "./Link"; 3 | import logoUrl from "./logo.svg"; 4 | import type { PageContext } from "./types"; 5 | import { PageContextProvider } from "./usePageContext"; 6 | 7 | export { PageShell }; 8 | 9 | function PageShell({ 10 | pageContext, 11 | children, 12 | }: { 13 | pageContext: PageContext; 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | Home 23 | Dynamic 24 | Static 25 | ISR 26 | Named 27 | Catch-all 28 | Function 29 | Edge Function endpoint 30 | Vike Edge Function endpoint 31 | Edge OG endpoint 32 | Node OG endpoint 33 | 34 | {children} 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | function Layout({ children }: { children: React.ReactNode }) { 42 | return ( 43 |
50 | {children} 51 |
52 | ); 53 | } 54 | 55 | function Sidebar({ children }: { children: React.ReactNode }) { 56 | return ( 57 | 70 | ); 71 | } 72 | 73 | function Content({ children }: { children: React.ReactNode }) { 74 | return ( 75 |
76 |
84 | {children} 85 |
86 |
87 | ); 88 | } 89 | 90 | function Logo() { 91 | return ( 92 |
98 | 99 | Logo 100 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Vercel 95 | .output 96 | .vercel 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | .cache 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | # Local files 134 | *.local.* 135 | 136 | # can generated by Vite in case of error 137 | vite.config.ts.js 138 | 139 | .idea 140 | 141 | -------------------------------------------------------------------------------- /examples/express/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-vercel-simple 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - vite-plugin-vercel@6.0.1 9 | 10 | ## null 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - vite-plugin-vercel@6.0.0 16 | 17 | ## null 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies 22 | - vite-plugin-vercel@5.0.5 23 | 24 | ## null 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies 29 | - vite-plugin-vercel@5.0.4 30 | 31 | ## null 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies 36 | - vite-plugin-vercel@5.0.3 37 | 38 | ## null 39 | 40 | ### Patch Changes 41 | 42 | - vite-plugin-vercel@5.0.2 43 | 44 | ## null 45 | 46 | ### Patch Changes 47 | 48 | - vite-plugin-vercel@5.0.1 49 | 50 | ## null 51 | 52 | ### Patch Changes 53 | 54 | - Updated dependencies 55 | - vite-plugin-vercel@5.0.0 56 | 57 | ## null 58 | 59 | ### Patch Changes 60 | 61 | - Updated dependencies 62 | - vite-plugin-vercel@4.0.2 63 | 64 | ## null 65 | 66 | ### Patch Changes 67 | 68 | - Updated dependencies 69 | - vite-plugin-vercel@4.0.1 70 | 71 | ## null 72 | 73 | ### Patch Changes 74 | 75 | - Updated dependencies 76 | - vite-plugin-vercel@4.0.0 77 | 78 | ## null 79 | 80 | ### Patch Changes 81 | 82 | - Updated dependencies 83 | - vite-plugin-vercel@3.0.2 84 | 85 | ## null 86 | 87 | ### Patch Changes 88 | 89 | - Updated dependencies [8e7cad4] 90 | - vite-plugin-vercel@3.0.1 91 | 92 | ## null 93 | 94 | ### Patch Changes 95 | 96 | - Updated dependencies [ec8dcba] 97 | - vite-plugin-vercel@3.0.0 98 | 99 | ## null 100 | 101 | ### Patch Changes 102 | 103 | - Updated dependencies 104 | - vite-plugin-vercel@2.0.1 105 | 106 | ## null 107 | 108 | ### Patch Changes 109 | 110 | - Updated dependencies 111 | - vite-plugin-vercel@2.0.0 112 | 113 | ## null 114 | 115 | ### Patch Changes 116 | 117 | - Updated dependencies 118 | - vite-plugin-vercel@1.0.0 119 | 120 | ## null 121 | 122 | ### Patch Changes 123 | 124 | - Updated dependencies 125 | - vite-plugin-vercel@0.3.7 126 | 127 | ## null 128 | 129 | ### Patch Changes 130 | 131 | - Updated dependencies 132 | - vite-plugin-vercel@0.3.6 133 | 134 | ## null 135 | 136 | ### Patch Changes 137 | 138 | - Updated dependencies 139 | - vite-plugin-vercel@0.3.5 140 | 141 | ## null 142 | 143 | ### Patch Changes 144 | 145 | - Updated dependencies 146 | - vite-plugin-vercel@1.0.0 147 | 148 | ## null 149 | 150 | ### Patch Changes 151 | 152 | - Updated dependencies 153 | - vite-plugin-vercel@0.3.3 154 | 155 | ## null 156 | 157 | ### Patch Changes 158 | 159 | - Updated dependencies 160 | - vite-plugin-vercel@0.3.2 161 | 162 | ## null 163 | 164 | ### Patch Changes 165 | 166 | - Updated dependencies 167 | - vite-plugin-vercel@0.3.1 168 | 169 | ## null 170 | 171 | ### Patch Changes 172 | 173 | - Updated dependencies 174 | - vite-plugin-vercel@0.3.0 175 | 176 | ## null 177 | 178 | ### Patch Changes 179 | 180 | - Updated dependencies 181 | - vite-plugin-vercel@0.2.2 182 | 183 | ## null 184 | 185 | ### Patch Changes 186 | 187 | - Updated dependencies 188 | - vite-plugin-vercel@0.2.1 189 | -------------------------------------------------------------------------------- /examples/simple/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-vercel-simple 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - vite-plugin-vercel@7.0.0 8 | 9 | ## null 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies 14 | - vite-plugin-vercel@6.0.1 15 | 16 | ## null 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies 21 | - vite-plugin-vercel@6.0.0 22 | 23 | ## null 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies 28 | - vite-plugin-vercel@5.0.5 29 | 30 | ## null 31 | 32 | ### Patch Changes 33 | 34 | - Updated dependencies 35 | - vite-plugin-vercel@5.0.4 36 | 37 | ## null 38 | 39 | ### Patch Changes 40 | 41 | - Updated dependencies 42 | - vite-plugin-vercel@5.0.3 43 | 44 | ## null 45 | 46 | ### Patch Changes 47 | 48 | - vite-plugin-vercel@5.0.2 49 | 50 | ## null 51 | 52 | ### Patch Changes 53 | 54 | - vite-plugin-vercel@5.0.1 55 | 56 | ## null 57 | 58 | ### Patch Changes 59 | 60 | - Updated dependencies 61 | - vite-plugin-vercel@5.0.0 62 | 63 | ## null 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies 68 | - vite-plugin-vercel@4.0.2 69 | 70 | ## null 71 | 72 | ### Patch Changes 73 | 74 | - Updated dependencies 75 | - vite-plugin-vercel@4.0.1 76 | 77 | ## null 78 | 79 | ### Patch Changes 80 | 81 | - Updated dependencies 82 | - vite-plugin-vercel@4.0.0 83 | 84 | ## null 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies 89 | - vite-plugin-vercel@3.0.2 90 | 91 | ## null 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [8e7cad4] 96 | - vite-plugin-vercel@3.0.1 97 | 98 | ## null 99 | 100 | ### Patch Changes 101 | 102 | - Updated dependencies [ec8dcba] 103 | - vite-plugin-vercel@3.0.0 104 | 105 | ## null 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies 110 | - vite-plugin-vercel@2.0.1 111 | 112 | ## null 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies 117 | - vite-plugin-vercel@2.0.0 118 | 119 | ## null 120 | 121 | ### Patch Changes 122 | 123 | - Updated dependencies 124 | - vite-plugin-vercel@1.0.0 125 | 126 | ## null 127 | 128 | ### Patch Changes 129 | 130 | - Updated dependencies 131 | - vite-plugin-vercel@0.3.7 132 | 133 | ## null 134 | 135 | ### Patch Changes 136 | 137 | - Updated dependencies 138 | - vite-plugin-vercel@0.3.6 139 | 140 | ## null 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies 145 | - vite-plugin-vercel@0.3.5 146 | 147 | ## null 148 | 149 | ### Patch Changes 150 | 151 | - Updated dependencies 152 | - vite-plugin-vercel@1.0.0 153 | 154 | ## null 155 | 156 | ### Patch Changes 157 | 158 | - Updated dependencies 159 | - vite-plugin-vercel@0.3.3 160 | 161 | ## null 162 | 163 | ### Patch Changes 164 | 165 | - Updated dependencies 166 | - vite-plugin-vercel@0.3.2 167 | 168 | ## null 169 | 170 | ### Patch Changes 171 | 172 | - Updated dependencies 173 | - vite-plugin-vercel@0.3.1 174 | 175 | ## null 176 | 177 | ### Patch Changes 178 | 179 | - Updated dependencies 180 | - vite-plugin-vercel@0.3.0 181 | 182 | ## null 183 | 184 | ### Patch Changes 185 | 186 | - Updated dependencies 187 | - vite-plugin-vercel@0.2.2 188 | 189 | ## null 190 | 191 | ### Patch Changes 192 | 193 | - Updated dependencies 194 | - vite-plugin-vercel@0.2.1 195 | -------------------------------------------------------------------------------- /packages/vercel/src/prerender.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import type { Rewrite } from "@vercel/routing-utils"; 4 | import type { ResolvedConfig } from "vite"; 5 | import { copyDir } from "./helpers"; 6 | import { type VercelOutputPrerenderConfig, vercelOutputPrerenderConfigSchema } from "./schemas/config/prerender-config"; 7 | import type { VercelOutputIsr, ViteVercelPrerenderRoute } from "./types"; 8 | import { getOutput } from "./utils"; 9 | 10 | export function execPrerender( 11 | resolvedConfig: ResolvedConfig, 12 | ): ViteVercelPrerenderRoute | Promise { 13 | const prerender = resolvedConfig.vercel?.prerender; 14 | 15 | if (prerender === false) { 16 | return; 17 | } 18 | 19 | return prerender?.(resolvedConfig); 20 | } 21 | 22 | // FIXME { group } will be made optional https://github.com/orgs/vercel/discussions/577#discussioncomment-2759412 23 | let group = 1; 24 | 25 | export async function writePrerenderConfig( 26 | resolvedConfig: ResolvedConfig, 27 | destination: string, 28 | isr: VercelOutputPrerenderConfig, 29 | ): Promise { 30 | const parsed = path.parse(destination); 31 | 32 | const outfile = path.join(getOutput(resolvedConfig, "functions"), parsed.dir, `${parsed.name}.prerender-config.json`); 33 | 34 | await fs.mkdir(path.join(getOutput(resolvedConfig, "functions"), parsed.dir), { recursive: true }); 35 | 36 | await fs.writeFile( 37 | outfile, 38 | JSON.stringify( 39 | vercelOutputPrerenderConfigSchema.parse({ 40 | group: group++, 41 | ...isr, 42 | }), 43 | undefined, 44 | 2, 45 | ), 46 | "utf-8", 47 | ); 48 | } 49 | 50 | export function getPrerenderSymlinkInfo(resolvedConfig: ResolvedConfig, destination: string, target: string) { 51 | const parsed = path.parse(destination); 52 | const targetParsed = path.parse(target); 53 | 54 | return { 55 | target: path.join(getOutput(resolvedConfig, "functions"), targetParsed.dir, `${targetParsed.name}.func`), 56 | link: path.join(getOutput(resolvedConfig, "functions"), parsed.dir, `${parsed.name}.func`), 57 | }; 58 | } 59 | 60 | export async function buildPrerenderConfigs( 61 | resolvedConfig: ResolvedConfig, 62 | extractedIsr: Record, 63 | ): Promise> { 64 | const isr = Object.assign({}, extractedIsr, await getIsrConfig(resolvedConfig)); 65 | 66 | const entries = Object.entries(isr); 67 | const rewrites: Rewrite[] = []; 68 | 69 | for (const [destination, { symlink, route, ...isr }] of entries) { 70 | await writePrerenderConfig(resolvedConfig, destination, isr); 71 | if (symlink) { 72 | const info = getPrerenderSymlinkInfo(resolvedConfig, destination, symlink); 73 | // FIXME symlinks are currently broken https://github.com/orgs/vercel/discussions/577#discussioncomment-2767120 74 | // await fs.symlink( 75 | // path.relative(path.dirname(info.link), info.target), 76 | // info.link, 77 | // ); 78 | await copyDir(info.target, info.link); 79 | } 80 | if (route) { 81 | rewrites.push({ 82 | source: `(${route})`, 83 | destination: `${destination}/?__original_path=$1`, 84 | }); 85 | } 86 | } 87 | 88 | return rewrites; 89 | } 90 | 91 | async function getIsrConfig(resolvedConfig: ResolvedConfig): Promise> { 92 | const isr = resolvedConfig.vercel?.isr ?? {}; 93 | if (typeof isr === "function") { 94 | return await isr(); 95 | } 96 | return isr; 97 | } 98 | -------------------------------------------------------------------------------- /packages/vike-integration/README.md: -------------------------------------------------------------------------------- 1 | # @vite-plugin-vercel/vike 2 | 3 | [`vike`](https://github.com/vikejs/vike) integration for `vite-plugin-vercel`. 4 | 5 | - Versions `>=0.3.3` are compatible with vike@0.4.x and above 6 | - Versions `0.1.x` are compatible with vike@0.4.x 7 | - Versions `0.0.x` are compatible with vike@0.3.x 8 | 9 | ## Features 10 | 11 | - [Support for ISR/Prerender Functions](#isrprerender-functions) 12 | - [Route strings](https://vike.dev/route-string) and [filesystem routing](https://vike.dev/filesystem-routing) are compiled to [routes rules](https://vercel.com/docs/build-output-api/v3#build-output-configuration/supported-properties/routes) 13 | - A Serverless Function is created by default to handle SSR route. No need to [manually create it](https://github.com/vikejs/vike_vercel/blob/main/api/ssr.js) 14 | - If you need to customize the Function, [some helpers are available](#custom-serverless-function-for-vike) 15 | 16 | ## Usage 17 | 18 | Install `vite-plugin-vercel` and `@vite-plugin-vercel/vike` and make sure only `vite-plugin-vercel` is added as a vite plugin. 19 | 20 | `vite-plugin-vercel` will auto load `@vite-plugin-vercel/vike` when necessary. 21 | 22 | ```ts 23 | // vite.config.ts 24 | import { defineConfig } from 'vite'; 25 | import ssr from 'vike/plugin'; 26 | import vercel from 'vite-plugin-vercel'; 27 | 28 | export default defineConfig(async ({ command, mode }) => { 29 | return { 30 | plugins: [ssr(), vercel()], 31 | }; 32 | }); 33 | ``` 34 | 35 | ### ISR/Prerender Functions 36 | 37 | Official documentation: https://vercel.com/docs/build-output-api/v3/primitives#prerender-functions 38 | 39 | :warning: Pages with [route function](https://vike.dev/route-function) are not compatible with ISR. A warning will be shown if this occurs. 40 | 41 | #### vike 0.4.x 42 | 43 | Take any of your `.page` file (not `.page.server`) and add the following export: 44 | 45 | ```ts 46 | // Now this page is a Prerender Function, meaning that it will be cached on Edge network for 15 seconds. 47 | // Check official documentation for further details on how it works. 48 | export const isr = { expiration: 15 }; 49 | ``` 50 | 51 | #### vike V1 design 52 | 53 | Take any of your [page config file](https://vike.dev/config), and add the following configuration: 54 | 55 | ```ts 56 | import type { Config } from 'vike/types'; 57 | 58 | export default { 59 | // Now this page is a Prerender Function, meaning that it will be cached on Edge network for 15 seconds. 60 | // Check official documentation for further details on how it works. 61 | isr: { expiration: 15 }, 62 | } satisfies Config; 63 | ``` 64 | 65 | ### Custom Serverless Function for vike 66 | 67 | By default, a Serverless Function is created to handle all SSR routes. 68 | If for any reason you need to customize it, some tools are available: 69 | 70 | ```ts 71 | import type { VercelRequest, VercelResponse } from '@vercel/node'; 72 | import { renderPage } from 'vike/server'; 73 | import { 74 | getDefaultEmptyResponseHandler, 75 | // higly recommended to use at least this one, as it handles some internals 76 | // that overrides `request.url` 77 | getDefaultPageContextInit, 78 | getDefaultResponseHandler, 79 | } from '@vite-plugin-vercel/vike/helpers'; 80 | 81 | export default async function handler( 82 | request: VercelRequest, 83 | response: VercelResponse, 84 | ) { 85 | // pageContextInit.url is not necessarily equal to request.url. 86 | // It is required for `renderPage` to work properly. 87 | const pageContextInit = getDefaultPageContextInit(request); 88 | const pageContext = await renderPage(pageContextInit); 89 | const { httpResponse } = pageContext; 90 | 91 | if (!httpResponse) { 92 | return getDefaultEmptyResponseHandler(response); 93 | } 94 | 95 | return getDefaultResponseHandler(response, httpResponse); 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /examples/demo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-vercel-demo-v1 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - @vite-plugin-vercel/vike@7.0.0 9 | - vite-plugin-vercel@7.0.0 10 | 11 | ## null 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies 16 | - vite-plugin-vercel@6.0.1 17 | - @vite-plugin-vercel/vike@6.0.1 18 | 19 | ## null 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies 24 | - @vite-plugin-vercel/vike@6.0.0 25 | - vite-plugin-vercel@6.0.0 26 | 27 | ## null 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies 32 | - vite-plugin-vercel@5.0.5 33 | - @vite-plugin-vercel/vike@5.0.3 34 | 35 | ## null 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies 40 | - vite-plugin-vercel@5.0.4 41 | - @vite-plugin-vercel/vike@5.0.2 42 | 43 | ## null 44 | 45 | ### Patch Changes 46 | 47 | - Updated dependencies 48 | - vite-plugin-vercel@5.0.3 49 | - @vite-plugin-vercel/vike@5.0.2 50 | 51 | ## null 52 | 53 | ### Patch Changes 54 | 55 | - Updated dependencies 56 | - @vite-plugin-vercel/vike@5.0.2 57 | - vite-plugin-vercel@5.0.2 58 | 59 | ## null 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies 64 | - @vite-plugin-vercel/vike@5.0.1 65 | - vite-plugin-vercel@5.0.1 66 | 67 | ## null 68 | 69 | ### Patch Changes 70 | 71 | - Updated dependencies 72 | - vite-plugin-vercel@5.0.0 73 | - @vite-plugin-vercel/vike@5.0.0 74 | 75 | ## null 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies 80 | - vite-plugin-vercel@4.0.2 81 | - @vite-plugin-vercel/vike@4.0.2 82 | 83 | ## null 84 | 85 | ### Patch Changes 86 | 87 | - Updated dependencies 88 | - Updated dependencies 89 | - @vite-plugin-vercel/vike@4.0.1 90 | - vite-plugin-vercel@4.0.1 91 | 92 | ## null 93 | 94 | ### Patch Changes 95 | 96 | - Updated dependencies 97 | - vite-plugin-vercel@4.0.0 98 | - @vite-plugin-vercel/vike@4.0.0 99 | 100 | ## null 101 | 102 | ### Patch Changes 103 | 104 | - Updated dependencies 105 | - vite-plugin-vercel@3.0.2 106 | - @vite-plugin-vercel/vike@3.0.1 107 | 108 | ## null 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies [8e7cad4] 113 | - @vite-plugin-vercel/vike@3.0.1 114 | - vite-plugin-vercel@3.0.1 115 | 116 | ## null 117 | 118 | ### Patch Changes 119 | 120 | - Updated dependencies [ec8dcba] 121 | - @vite-plugin-vercel/vike@3.0.0 122 | - vite-plugin-vercel@3.0.0 123 | 124 | ## null 125 | 126 | ### Patch Changes 127 | 128 | - Updated dependencies 129 | - vite-plugin-vercel@2.0.1 130 | - @vite-plugin-vercel/vike@2.0.0 131 | 132 | ## null 133 | 134 | ### Patch Changes 135 | 136 | - Updated dependencies 137 | - vite-plugin-vercel@2.0.0 138 | - @vite-plugin-vercel/vike@2.0.0 139 | 140 | ## null 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies 145 | - vite-plugin-vercel@1.0.0 146 | - @vite-plugin-vercel/vike@1.0.0 147 | 148 | ## null 149 | 150 | ### Patch Changes 151 | 152 | - Updated dependencies 153 | - vite-plugin-vercel@0.3.7 154 | - @vite-plugin-vercel/vike@0.4.2 155 | 156 | ## null 157 | 158 | ### Patch Changes 159 | 160 | - Updated dependencies 161 | - vite-plugin-vercel@0.3.6 162 | - @vite-plugin-vercel/vike@0.4.1 163 | 164 | ## null 165 | 166 | ### Patch Changes 167 | 168 | - Updated dependencies 169 | - vite-plugin-vercel@0.3.5 170 | - @vite-plugin-vercel/vike@0.4.0 171 | 172 | ## null 173 | 174 | ### Patch Changes 175 | 176 | - Updated dependencies 177 | - @vite-plugin-vercel/vike@0.4.0 178 | - vite-plugin-vercel@1.0.0 179 | 180 | ## null 181 | 182 | ### Patch Changes 183 | 184 | - Updated dependencies 185 | - vite-plugin-vercel@0.3.3 186 | - @vite-plugin-vercel/vike@0.3.3 187 | 188 | ## null 189 | 190 | ### Patch Changes 191 | 192 | - Updated dependencies 193 | - vite-plugin-vercel@0.3.2 194 | - @vite-plugin-vercel/vike@0.3.3 195 | -------------------------------------------------------------------------------- /packages/vercel/src/schemas/config/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema definition for `.vercel/output/config.json` 3 | * @see {@link https://vercel.com/docs/build-output-api/v3#build-output-configuration} 4 | */ 5 | 6 | import { z } from "zod"; 7 | 8 | const HasOrMissing = z 9 | .array( 10 | z.union([ 11 | z 12 | .object({ 13 | type: z.literal("host"), 14 | value: z.string(), 15 | }) 16 | .strict(), 17 | z 18 | .object({ 19 | type: z.literal("header"), 20 | key: z.string(), 21 | value: z.string().optional(), 22 | }) 23 | .strict(), 24 | z 25 | .object({ 26 | type: z.literal("cookie"), 27 | key: z.string(), 28 | value: z.string().optional(), 29 | }) 30 | .strict(), 31 | z 32 | .object({ 33 | type: z.literal("query"), 34 | key: z.string(), 35 | value: z.string().optional(), 36 | }) 37 | .strict(), 38 | ]), 39 | ) 40 | .optional(); 41 | 42 | export const vercelOutputConfigSchema = z 43 | .object({ 44 | version: z.literal(3), 45 | routes: z 46 | .array( 47 | z.union([ 48 | z 49 | .object({ 50 | src: z.string(), 51 | dest: z.string().optional(), 52 | headers: z.record(z.string()).optional(), 53 | methods: z.array(z.string()).optional(), 54 | status: z.number().int().positive().optional(), 55 | continue: z.boolean().optional(), 56 | check: z.boolean().optional(), 57 | missing: HasOrMissing, 58 | has: HasOrMissing, 59 | locale: z 60 | .object({ 61 | redirect: z.record(z.string()).optional(), 62 | cookie: z.string().optional(), 63 | }) 64 | .strict() 65 | .optional(), 66 | middlewarePath: z.string().optional(), 67 | }) 68 | .strict(), 69 | z 70 | .object({ 71 | handle: z.union([ 72 | z.literal("rewrite"), 73 | z.literal("filesystem"), 74 | z.literal("resource"), 75 | z.literal("miss"), 76 | z.literal("hit"), 77 | z.literal("error"), 78 | ]), 79 | src: z.string().optional(), 80 | dest: z.string().optional(), 81 | status: z.number().optional(), 82 | }) 83 | .strict(), 84 | ]), 85 | ) 86 | .optional(), 87 | images: z 88 | .object({ 89 | sizes: z.tuple([z.number().int().positive(), z.number().int().positive()]), 90 | domains: z.array(z.string()).nonempty().optional(), 91 | minimumCacheTTL: z.number().int().positive().optional(), 92 | formats: z 93 | .union([z.literal("image/avif"), z.literal("image/webp")]) 94 | .array() 95 | .nonempty() 96 | .optional(), 97 | dangerouslyAllowSVG: z.boolean().optional(), 98 | contentSecurityPolicy: z.string().optional(), 99 | }) 100 | .strict() 101 | .optional(), 102 | wildcard: z 103 | .array( 104 | z 105 | .object({ 106 | domain: z.string(), 107 | value: z.string(), 108 | }) 109 | .strict(), 110 | ) 111 | .optional(), 112 | overrides: z 113 | .record( 114 | z 115 | .object({ 116 | path: z.string().optional(), 117 | contentType: z.string().optional(), 118 | }) 119 | .strict(), 120 | ) 121 | .optional(), 122 | cache: z.array(z.string()).optional(), 123 | }) 124 | .strict(); 125 | 126 | export type VercelOutputConfig = z.infer; 127 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/fs.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import glob from "fast-glob"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | describe("fs", () => { 6 | const buildManifest = require("../../dist/assets.json"); 7 | 8 | const generatedFiles = Array.from( 9 | new Set( 10 | Object.values(buildManifest) 11 | .filter((e: any): e is any => Boolean(e.file)) 12 | .flatMap((e) => [e.file, ...(e.assets ?? []), ...(e.css ?? [])]) 13 | .filter((f) => f.startsWith("assets/")), 14 | ), 15 | ); 16 | 17 | const expected = [ 18 | "/config.json", 19 | "/functions/api/name/[name].func/.vc-config.json", 20 | "/functions/api/name/[name].func/index.mjs", 21 | "/functions/api/page.func/index.mjs", 22 | "/functions/api/page.func/.vc-config.json", 23 | "/functions/api/post.func/index.mjs", 24 | "/functions/api/post.func/.vc-config.json", 25 | "/functions/edge.func/index.js", 26 | "/functions/edge.func/.vc-config.json", 27 | "/functions/og-node.func/index.mjs", 28 | "/functions/og-node.func/.vc-config.json", 29 | "/functions/og-node.func/noto-sans-v27-latin-regular.ttf", 30 | "/functions/og-node.func/Roboto-Regular.ttf", 31 | "/functions/og-node.func/resvg.wasm", 32 | "/functions/og-node.func/yoga.wasm", 33 | "/functions/og-edge.func/index.js", 34 | "/functions/og-edge.func/.vc-config.json", 35 | "/functions/og-edge.func/noto-sans-v27-latin-regular.ttf", 36 | "/functions/og-edge.func/resvg.wasm", 37 | "/functions/og-edge.func/yoga.wasm", 38 | // ISR + Static pages 39 | "/functions/ssr_.func/index.mjs", 40 | "/functions/ssr_.func/.vc-config.json", 41 | "/static/404.html", 42 | "/static/index.html", 43 | "/static/index.pageContext.json", 44 | "/static/static/index.html", 45 | "/static/static/index.pageContext.json", 46 | "/static/catch-all/a/b/c/index.html", 47 | "/static/catch-all/a/b/c/index.pageContext.json", 48 | "/static/catch-all/a/d/index.html", 49 | "/static/catch-all/a/d/index.pageContext.json", 50 | "/static/function/a/index.html", 51 | "/static/function/a/index.pageContext.json", 52 | "/static/named/id-1/index.html", 53 | "/static/named/id-1/index.pageContext.json", 54 | "/static/named/id-2/index.html", 55 | "/static/named/id-2/index.pageContext.json", 56 | "/static/test.html", 57 | /\/functions\/pages\/catch-all-([^\/]+?)\.prerender-config\.json/, 58 | /\/functions\/pages\/catch-all-([^\/]+?)\.func\/index\.mjs/, 59 | /\/functions\/pages\/catch-all-([^\/]+?)\.func\/\.vc-config\.json/, 60 | /\/functions\/pages\/isr-([^\/]+?)\.prerender-config\.json/, 61 | /\/functions\/pages\/isr-([^\/]+?)\.func\/index\.mjs/, 62 | /\/functions\/pages\/isr-([^\/]+?)\.func\/\.vc-config\.json/, 63 | /\/functions\/pages\/named-([^\/]+?)\.prerender-config\.json/, 64 | /\/functions\/pages\/named-([^\/]+?)\.func\/index\.mjs/, 65 | /\/functions\/pages\/named-([^\/]+?)\.func\/\.vc-config\.json/, 66 | // vike-edge 67 | /\/functions\/pages\/vike-edge-edge-([^\/]+?)\.func\/index\.js/, 68 | /\/functions\/pages\/vike-edge-edge-([^\/]+?)\.func\/\.vc-config\.json/, 69 | ...generatedFiles.map((f) => `/static/${f}`), 70 | ]; 71 | 72 | it("should generate the right files", async () => { 73 | const dir = path.join(__dirname, "../../.vercel/output"); 74 | const entries = await glob(`${dir}/**`, { dot: true }); 75 | let mappedEntries = entries.map((e) => e.replace(dir, "")).filter((e) => !e.startsWith("/_ignore")); 76 | 77 | mappedEntries = Array.from(new Set(mappedEntries)); 78 | 79 | expect(entries).toHaveLength(expected.length); 80 | for (const entry of mappedEntries) { 81 | expect(entry).toSatisfy((elt: string) => { 82 | for (const exp of expected) { 83 | if (typeof exp === "string") { 84 | if (exp === elt) return true; 85 | } else { 86 | const match = elt.match(exp); 87 | if (match) return true; 88 | } 89 | } 90 | console.error("no match found for", elt); 91 | return false; 92 | }); 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /examples/demo/tests/05-vike/config.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { vercelOutputConfigSchema } from "../../../../packages/vercel/src/schemas/config/config"; 3 | import { testSchema } from "../common/helpers"; 4 | import { prepareTestJsonFileContent } from "./utils"; 5 | 6 | prepareTestJsonFileContent("config.json", (context) => { 7 | testSchema(context, vercelOutputConfigSchema); 8 | 9 | it("should have defaults routes only", () => { 10 | const expected = [ 11 | { 12 | src: "^/vike-edge$", 13 | headers: { 14 | "X-VitePluginVercel-Test": "test", 15 | }, 16 | continue: true, 17 | }, 18 | { 19 | src: "^/vike-edge/index\\.pageContext\\.json$", 20 | headers: { 21 | "X-VitePluginVercel-Test": "test", 22 | }, 23 | continue: true, 24 | }, 25 | { 26 | src: "^/api/page$", 27 | headers: { "X-VitePluginVercel-Test": "test" }, 28 | continue: true, 29 | }, 30 | { 31 | headers: { Location: "/$1" }, 32 | src: "^/(?:(.+)/)?index(?:\\.html)?/?$", 33 | status: 308, 34 | }, 35 | { 36 | headers: { Location: "/$1" }, 37 | src: "^/(.*)\\.html/?$", 38 | status: 308, 39 | }, 40 | { handle: "filesystem" }, 41 | { 42 | src: "^/edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 43 | dest: "/edge/$1", 44 | check: true, 45 | }, 46 | { 47 | src: "^/og-node(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 48 | dest: "/og-node/$1", 49 | check: true, 50 | }, 51 | { 52 | src: "^/og-edge(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?$", 53 | dest: "/og-edge/$1", 54 | check: true, 55 | }, 56 | { 57 | src: "^(/vike-edge(?:/index\\.pageContext\\.json)?)$", 58 | dest: expect.stringMatching("/pages/vike-edge-edge-([^/]+?)/\\?__original_path=\\$1"), 59 | check: true, 60 | }, 61 | { 62 | check: true, 63 | src: "^/api/page$", 64 | dest: "/api/page", 65 | }, 66 | { 67 | check: true, 68 | src: "^/api/post$", 69 | dest: "/api/post", 70 | }, 71 | { 72 | check: true, 73 | src: "^/api/name(?:/([^/]+?))$", 74 | dest: "/api/name/[name]?name=$1", 75 | }, 76 | { 77 | check: true, 78 | src: "^(/catch-all/.+?(?:/index\\.pageContext\\.json)?)$", 79 | dest: expect.stringMatching("/pages/catch-all-([^/]+?)/\\?__original_path=\\$1"), 80 | }, 81 | { 82 | check: true, 83 | src: "^(/isr(?:/index\\.pageContext\\.json)?)$", 84 | dest: expect.stringMatching("/pages/isr-([^/]+?)/\\?__original_path=\\$1"), 85 | }, 86 | { 87 | check: true, 88 | src: "^(/named/[^/]+(?:/index\\.pageContext\\.json)?)$", 89 | dest: expect.stringMatching("/pages/named-([^/]+?)/\\?__original_path=\\$1"), 90 | }, 91 | { check: true, dest: "/ssr_/?__original_path=$1", src: "^((?!/api).*)$" }, 92 | ]; 93 | 94 | expect((context.file as any).routes).toHaveLength(expected.length); 95 | for (const route of expected) { 96 | expect((context.file as any).routes).toContainEqual(route); 97 | } 98 | expect((context.file as any).overrides).toMatchObject({ 99 | // '404.html': { 100 | // path: '404', 101 | // }, 102 | "catch-all/a/b/c/index.html": { 103 | path: "catch-all/a/b/c", 104 | }, 105 | "catch-all/a/d/index.html": { 106 | path: "catch-all/a/d", 107 | }, 108 | "function/a/index.html": { 109 | path: "function/a", 110 | }, 111 | "index.html": { 112 | path: "", 113 | }, 114 | "named/id-1/index.html": { 115 | path: "named/id-1", 116 | }, 117 | "named/id-2/index.html": { 118 | path: "named/id-2", 119 | }, 120 | "static/index.html": { 121 | path: "static", 122 | }, 123 | }); 124 | expect(Object.keys(context.file as any).sort()).toEqual(["version", "overrides", "routes"].sort()); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /packages/vercel/src/config.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedConfig } from "vite"; 2 | import path from "node:path"; 3 | import { getOutput } from "./utils"; 4 | import { type VercelOutputConfig, vercelOutputConfigSchema } from "./schemas/config/config"; 5 | import fs from "node:fs/promises"; 6 | import { 7 | getTransformedRoutes, 8 | type Header, 9 | mergeRoutes, 10 | normalizeRoutes, 11 | type Rewrite, 12 | type Route, 13 | } from "@vercel/routing-utils"; 14 | import type { ViteVercelRewrite } from "./types"; 15 | 16 | function reorderEnforce(arr: T[]) { 17 | return [ 18 | ...arr.filter((r) => r.enforce === "pre"), 19 | ...arr.filter((r) => !r.enforce), 20 | ...arr.filter((r) => r.enforce === "post"), 21 | ]; 22 | } 23 | 24 | export function getConfig( 25 | resolvedConfig: ResolvedConfig, 26 | rewrites?: ViteVercelRewrite[], 27 | overrides?: VercelOutputConfig["overrides"], 28 | headers?: Header[], 29 | ): VercelOutputConfig { 30 | const _rewrites: ViteVercelRewrite[] = [ 31 | // User provided config always comes first 32 | ...(resolvedConfig.vercel?.rewrites ?? []), 33 | ...(rewrites ?? []), 34 | ]; 35 | 36 | const _enforcedRewrites = reorderEnforce(_rewrites).map((r) => { 37 | // optional catch-all 38 | // :[...optionalCatchAll] -> :catchAll* (global) 39 | r.source = r.source.replaceAll(/:\[(\.\.\.)(.*)\]/g, ":$2*"); 40 | 41 | // catch-all 42 | // :...catchAll -> :catchAll+ (global) 43 | r.source = r.source.replaceAll(/:(\.\.\.)(.*)/g, ":$2+"); 44 | 45 | return r; 46 | }); 47 | 48 | const { routes, error } = getTransformedRoutes({ 49 | cleanUrls: resolvedConfig.vercel?.cleanUrls ?? true, 50 | trailingSlash: resolvedConfig.vercel?.trailingSlash, 51 | rewrites: _enforcedRewrites, 52 | redirects: resolvedConfig.vercel?.redirects ? reorderEnforce(resolvedConfig.vercel?.redirects) : undefined, 53 | headers, 54 | }); 55 | 56 | if (error) { 57 | throw error; 58 | } 59 | 60 | if ( 61 | resolvedConfig.vercel?.config?.routes && 62 | resolvedConfig.vercel.config.routes.length > 0 && 63 | !resolvedConfig.vercel.config.routes.every((r) => "continue" in r && r.continue) 64 | ) { 65 | console.warn( 66 | 'Did you forget to add `"continue": true` to your routes? See https://vercel.com/docs/build-output-api/v3/configuration#source-route\n' + 67 | "If not, it is discouraged to use `vercel.config.routes` to override routes. " + 68 | "Prefer using `vercel.rewrites` and `vercel.redirects`.", 69 | ); 70 | } 71 | 72 | let userRoutes: Route[] = []; 73 | let buildRoutes: Route[] = []; 74 | 75 | if (resolvedConfig.vercel?.config?.routes) { 76 | const norm = normalizeRoutes(resolvedConfig.vercel.config.routes); 77 | 78 | if (norm.error) { 79 | throw norm.error; 80 | } 81 | 82 | userRoutes = norm.routes ?? []; 83 | } 84 | 85 | if (routes) { 86 | const norm = normalizeRoutes(routes); 87 | 88 | if (norm.error) { 89 | throw norm.error; 90 | } 91 | 92 | buildRoutes = norm.routes ?? []; 93 | } 94 | 95 | const cleanRoutes = mergeRoutes({ 96 | userRoutes, 97 | builds: [ 98 | { 99 | use: "@vercel/node", 100 | entrypoint: "index.js", 101 | routes: buildRoutes, 102 | }, 103 | ], 104 | }); 105 | 106 | return vercelOutputConfigSchema.parse({ 107 | version: 3, 108 | ...resolvedConfig.vercel?.config, 109 | routes: cleanRoutes, 110 | overrides: { 111 | ...resolvedConfig.vercel?.config?.overrides, 112 | ...overrides, 113 | }, 114 | }); 115 | } 116 | 117 | export function getConfigDestination(resolvedConfig: ResolvedConfig) { 118 | return path.join(getOutput(resolvedConfig), "config.json"); 119 | } 120 | 121 | export async function writeConfig( 122 | resolvedConfig: ResolvedConfig, 123 | rewrites?: Rewrite[], 124 | overrides?: VercelOutputConfig["overrides"], 125 | headers?: Header[], 126 | ): Promise { 127 | await fs.writeFile( 128 | getConfigDestination(resolvedConfig), 129 | JSON.stringify(getConfig(resolvedConfig, rewrites, overrides, headers), undefined, 2), 130 | "utf-8", 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /examples/demo/renderer/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/vike-integration/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vite-plugin-vercel/vike 2 | 3 | ## 9.1.0 4 | 5 | ### Minor Changes 6 | 7 | - fix: upgrade dependencies. Dropping support for node@18 8 | 9 | ## 9.0.7 10 | 11 | ### Patch Changes 12 | 13 | - feat: adding support for node@24 14 | 15 | ## 9.0.6 16 | 17 | ### MINOR BREAKING CHANGES 18 | 19 | - Update Vike to `0.4.229` or above. 20 | 21 | ## 9.0.5 22 | 23 | ### Patch Changes 24 | 25 | - 3f3dd9c: fix(vike): do not show route function warnings for non route function 26 | - 3f3dd9c: fix(vike): streaming error in \_error pages 27 | 28 | ## 9.0.4 29 | 30 | ### Patch Changes 31 | 32 | - Improve compatibility with latest Vike versions 33 | 34 | ## 9.0.3 35 | 36 | ### Patch Changes 37 | 38 | - Update peerDependencies to include Vite6 39 | 40 | ## 9.0.2 41 | 42 | ### Patch Changes 43 | 44 | - Upgrade dependencies 45 | - fix CVE-2024-45296. See https://github.com/advisories/GHSA-9wv6-86v2-598j 46 | 47 | ## 9.0.1 48 | 49 | ### Patch Changes 50 | 51 | - feat: Vike now support headers in +config.ts 52 | 53 | ## 9.0.0 54 | 55 | ### Minor Changes 56 | 57 | - feat(vike): support edge config in +config.ts file 58 | 59 | ### Patch Changes 60 | 61 | - Updated dependencies 62 | - vite-plugin-vercel@9.0.0 63 | 64 | ## 8.0.1 65 | 66 | ### Patch Changes 67 | 68 | - avoid double build of additionnal endpoint 69 | - Updated dependencies 70 | - vite-plugin-vercel@8.0.1 71 | 72 | ## 8.0.0 73 | 74 | ### Minor Changes 75 | 76 | - Target node@18 by default 77 | 78 | ### Patch Changes 79 | 80 | - Updated dependencies 81 | - vite-plugin-vercel@8.0.0 82 | 83 | ## 7.0.1 84 | 85 | ### Patch Changes 86 | 87 | - Better support for servers like express 88 | - Updated dependencies 89 | - vite-plugin-vercel@7.0.1 90 | 91 | ## 7.0.0 92 | 93 | ### Minor Changes 94 | 95 | - Support for vike@0.4.172 96 | 97 | ### Patch Changes 98 | 99 | - vite-plugin-vercel@7.0.0 100 | 101 | ## 6.0.1 102 | 103 | ### Patch Changes 104 | 105 | - Update target to es2022 106 | - Updated dependencies 107 | - vite-plugin-vercel@6.0.1 108 | 109 | ## 6.0.0 110 | 111 | ### Minor Changes 112 | 113 | - add support for supportsResponseStreaming 114 | 115 | ### Patch Changes 116 | 117 | - Updated dependencies 118 | - vite-plugin-vercel@6.0.0 119 | 120 | ## 5.0.3 121 | 122 | ### Patch Changes 123 | 124 | - Fix warning when trying to read exports 125 | - Updated dependencies 126 | - vite-plugin-vercel@5.0.5 127 | 128 | ## 5.0.2 129 | 130 | ### Patch Changes 131 | 132 | - Update minimum peer dependency 133 | - vite-plugin-vercel@5.0.2 134 | 135 | ## 5.0.1 136 | 137 | ### Patch Changes 138 | 139 | - Migrate to last recommendations 140 | - vite-plugin-vercel@5.0.1 141 | 142 | ## 5.0.0 143 | 144 | ### Patch Changes 145 | 146 | - Updated dependencies 147 | - vite-plugin-vercel@5.0.0 148 | 149 | ## 4.0.2 150 | 151 | ### Patch Changes 152 | 153 | - Upgrade dependencies 154 | - Updated dependencies 155 | - vite-plugin-vercel@4.0.2 156 | 157 | ## 4.0.1 158 | 159 | ### Patch Changes 160 | 161 | - Fix **VITE_ASSETS_MAP** error 162 | - Updated dependencies 163 | - vite-plugin-vercel@4.0.1 164 | 165 | ## 4.0.0 166 | 167 | ### Patch Changes 168 | 169 | - Updated dependencies 170 | - vite-plugin-vercel@4.0.0 171 | 172 | ## 3.0.1 173 | 174 | ### Patch Changes 175 | 176 | - 8e7cad4: Add vite@5 in peerDependencies 177 | - Updated dependencies [8e7cad4] 178 | - vite-plugin-vercel@3.0.1 179 | 180 | ## 3.0.0 181 | 182 | ### Major Changes 183 | 184 | - ec8dcba: - Target node20.x 185 | - Upgrade all dependencies to their latest versions 186 | - Drops support for vike non-v1 design (still works with v2) 187 | 188 | ### Patch Changes 189 | 190 | - Updated dependencies [ec8dcba] 191 | - vite-plugin-vercel@3.0.0 192 | 193 | ## 2.0.0 194 | 195 | ### Patch Changes 196 | 197 | - Updated dependencies 198 | - vite-plugin-vercel@2.0.0 199 | 200 | ## 1.0.0 201 | 202 | ### Patch Changes 203 | 204 | - Updated dependencies 205 | - vite-plugin-vercel@1.0.0 206 | 207 | ## 0.4.2 208 | 209 | ### Patch Changes 210 | 211 | - Upgrade dependencies 212 | - Updated dependencies 213 | - vite-plugin-vercel@0.3.7 214 | 215 | ## 0.4.1 216 | 217 | ### Patch Changes 218 | 219 | - chore: remove esbuild verbose warnings 220 | - Updated dependencies 221 | - vite-plugin-vercel@0.3.6 222 | 223 | ## 0.4.0 224 | 225 | ### Minor Changes 226 | 227 | - Rename vite-plugin-vercel to Vike 228 | 229 | ## 0.3.3 230 | 231 | ### Patch Changes 232 | 233 | - Add support for vike V1 design 234 | 235 | ## 0.3.2 236 | 237 | ### Patch Changes 238 | 239 | - update dependencies 240 | 241 | ## 0.3.1 242 | 243 | ### Patch Changes 244 | 245 | - Rename Vike integration plugin to @vite-plugin-vercel/vike 246 | 247 | ## 0.2.2 248 | 249 | ### Patch Changes 250 | 251 | - Update documentation and upgrade dependencies 252 | 253 | ## 0.2.1 254 | 255 | ### Patch Changes 256 | 257 | - fix use case without vike 258 | 259 | ## 0.2.0 260 | 261 | ### Minor Changes 262 | 263 | - upgrade dependencies 264 | 265 | ### Patch Changes 266 | 267 | - support edge-light target 268 | 269 | ## 0.1.5 270 | 271 | ### Patch Changes 272 | 273 | - fix issue with win32 path 274 | 275 | ## 0.1.4 276 | 277 | ### Patch Changes 278 | 279 | - Upgrade dependencies. Fixes #9 and #10 280 | 281 | ## 0.1.3 282 | 283 | ### Patch Changes 284 | 285 | - Edge Functions support 286 | 287 | ## 0.1.2 288 | 289 | ### Patch Changes 290 | 291 | - Fix exports 292 | 293 | ## 0.1.1 294 | 295 | ### Patch Changes 296 | 297 | - Add missing helper file in bundle 298 | 299 | ## 0.1.0 300 | 301 | ### Minor Changes 302 | 303 | - Support for vike@0.4.x 304 | 305 | ### Patch Changes 306 | 307 | - Support for vite@3 308 | 309 | ## 0.0.9 310 | 311 | ### Patch Changes 312 | 313 | - Fix duplicate route entries 314 | 315 | ## 0.0.8 316 | 317 | ### Patch Changes 318 | 319 | - Support rewrites/redirects options (prefered over config.routes) 320 | 321 | ## 0.0.7 322 | 323 | ### Patch Changes 324 | 325 | - Change config syntax 326 | 327 | ## 0.0.6 328 | 329 | ### Patch Changes 330 | 331 | - Default SSR route excludes /api 332 | 333 | ## 0.0.5 334 | 335 | ### Patch Changes 336 | 337 | - isr config can now be overriden when using vike integration 338 | 339 | ## 0.0.4 340 | 341 | ### Patch Changes 342 | 343 | - Include templates folder 344 | 345 | ## 0.0.3 346 | 347 | ### Patch Changes 348 | 349 | - Update package.json metadata 350 | 351 | ## 0.0.2 352 | 353 | ### Patch Changes 354 | 355 | - Create a dedicated package for vike integration 356 | -------------------------------------------------------------------------------- /packages/vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import type { Plugin, PluginOption, ResolvedConfig } from "vite"; 4 | import { buildEndpoints } from "./build"; 5 | import { writeConfig } from "./config"; 6 | import { copyDir } from "./helpers"; 7 | import { buildPrerenderConfigs, execPrerender } from "./prerender"; 8 | import type { ViteVercelPrerenderRoute } from "./types"; 9 | import { getOutput, getPublic } from "./utils"; 10 | 11 | export * from "./types"; 12 | 13 | function vercelPluginCleanup(): Plugin { 14 | let resolvedConfig: ResolvedConfig; 15 | 16 | return { 17 | apply: "build", 18 | name: "vite-plugin-vercel:cleanup", 19 | enforce: "pre", 20 | 21 | configResolved(config) { 22 | resolvedConfig = config; 23 | }, 24 | writeBundle: { 25 | order: "pre", 26 | sequential: true, 27 | async handler() { 28 | if (!resolvedConfig.build?.ssr) { 29 | // step 1: Clean .vercel/ouput dir 30 | await cleanOutputDirectory(resolvedConfig); 31 | } 32 | }, 33 | }, 34 | }; 35 | } 36 | 37 | type Writeable = { -readonly [P in keyof T]: T[P] }; 38 | 39 | function vercelPlugin(): Plugin { 40 | let resolvedConfig: Writeable; 41 | let vikeFound = false; 42 | 43 | return { 44 | apply: "build", 45 | name: "vite-plugin-vercel", 46 | enforce: "post", 47 | 48 | configResolved(config) { 49 | resolvedConfig = config; 50 | vikeFound = resolvedConfig.plugins.some((p) => p.name.match("^vite-plugin-ssr:|^vike:")); 51 | 52 | if (typeof resolvedConfig.vercel?.distContainsOnlyStatic === "undefined") { 53 | resolvedConfig.vercel ??= {}; 54 | resolvedConfig.vercel.distContainsOnlyStatic = !vikeFound; 55 | } 56 | }, 57 | writeBundle: { 58 | order: "post", 59 | sequential: true, 60 | async handler() { 61 | if (!resolvedConfig.build?.ssr) { 62 | // special case: Vike triggers a second build with --ssr 63 | // TODO: find a way to fix that in a more generic way 64 | if (vikeFound) { 65 | return; 66 | } 67 | } 68 | 69 | // step 2: Execute prerender 70 | const overrides = await execPrerender(resolvedConfig); 71 | 72 | // step 3: Compute overrides for static HTML files 73 | const userOverrides = await computeStaticHtmlOverrides(resolvedConfig); 74 | 75 | // step 4: Compile serverless functions to ".vercel/output/functions" 76 | const { rewrites, isr, headers } = await buildEndpoints(resolvedConfig); 77 | 78 | // step 5: Generate prerender config files 79 | rewrites.push(...(await buildPrerenderConfigs(resolvedConfig, isr))); 80 | 81 | // step 6: Generate config file 82 | await writeConfig( 83 | resolvedConfig, 84 | rewrites, 85 | { 86 | ...userOverrides, 87 | ...overrides, 88 | }, 89 | headers, 90 | ); 91 | 92 | // step 7: Copy dist folder to static 93 | await copyDistToStatic(resolvedConfig); 94 | }, 95 | }, 96 | }; 97 | } 98 | 99 | async function cleanOutputDirectory(resolvedConfig: ResolvedConfig) { 100 | await fs.rm(getOutput(resolvedConfig), { 101 | recursive: true, 102 | force: true, 103 | }); 104 | 105 | await fs.mkdir(getOutput(resolvedConfig), { recursive: true }); 106 | } 107 | 108 | async function copyDistToStatic(resolvedConfig: ResolvedConfig) { 109 | if (resolvedConfig.vercel?.distContainsOnlyStatic) { 110 | await copyDir(resolvedConfig.build.outDir, getOutput(resolvedConfig, "static")); 111 | } 112 | } 113 | 114 | async function computeStaticHtmlOverrides( 115 | resolvedConfig: ResolvedConfig, 116 | ): Promise> { 117 | const staticAbsolutePath = getOutput(resolvedConfig, "static"); 118 | const files = await getStaticHtmlFiles(staticAbsolutePath); 119 | 120 | // public files copied by vite by default https://vitejs.dev/guide/assets.html#the-public-directory 121 | const publicDir = getPublic(resolvedConfig); 122 | const publicFiles = await getStaticHtmlFiles(publicDir); 123 | files.push(...publicFiles.map((f) => f.replace(publicDir, staticAbsolutePath))); 124 | 125 | return files.reduce( 126 | (acc, curr) => { 127 | const relPath = path.relative(staticAbsolutePath, curr); 128 | const parsed = path.parse(relPath); 129 | const pathJoined = path.join(parsed.dir, parsed.name); 130 | acc[relPath] = { 131 | path: pathJoined, 132 | }; 133 | return acc; 134 | }, 135 | {} as NonNullable, 136 | ); 137 | } 138 | 139 | async function getStaticHtmlFiles(src: string) { 140 | try { 141 | await fs.stat(src); 142 | } catch (e) { 143 | return []; 144 | } 145 | 146 | const entries = await fs.readdir(src, { withFileTypes: true }); 147 | const htmlFiles: string[] = []; 148 | 149 | for (const entry of entries) { 150 | const srcPath = path.join(src, entry.name); 151 | 152 | entry.isDirectory() 153 | ? htmlFiles.push(...(await getStaticHtmlFiles(srcPath))) 154 | : srcPath.endsWith(".html") 155 | ? htmlFiles.push(srcPath) 156 | : undefined; 157 | } 158 | 159 | return htmlFiles; 160 | } 161 | 162 | /** 163 | * Auto import `@vite-plugin-vercel/vike` if it is part of dependencies. 164 | * Ensures that `vike/plugin` is also present to ensure predictable behavior 165 | */ 166 | 167 | // biome-ignore lint/suspicious/noExplicitAny: 168 | async function tryImportVpvv(options: any) { 169 | try { 170 | // @ts-ignore 171 | await import("vike/plugin"); 172 | // @ts-ignore 173 | const vpvv = await import("@vite-plugin-vercel/vike"); 174 | return vpvv.default(options); 175 | } catch (e) { 176 | try { 177 | // @ts-ignore 178 | await import("vite-plugin-ssr/plugin"); 179 | // @ts-ignore 180 | const vpvv = await import("@vite-plugin-vercel/vike"); 181 | return vpvv.default(options); 182 | } catch (e) { 183 | return null; 184 | } 185 | } 186 | } 187 | 188 | // `smart` param only exist to circumvent a pnpm issue in dev 189 | // See https://github.com/pnpm/pnpm/issues/3697#issuecomment-1708687974 190 | // FIXME: Could be fixed by: 191 | // - shared-workspace-lockfile=false in .npmrc. See https://pnpm.io/npmrc#shared-workspace-lockfile 192 | // - Moving demo test in dedicated repo, with each a correct package.json 193 | // biome-ignore lint/suspicious/noExplicitAny: 194 | export default function allPlugins(options: any = {}): PluginOption[] { 195 | const { smart, ...rest } = options; 196 | return [vercelPluginCleanup(), vercelPlugin(), smart !== false ? tryImportVpvv(rest) : null]; 197 | } 198 | -------------------------------------------------------------------------------- /packages/vercel/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Header, Redirect, Rewrite } from "@vercel/routing-utils"; 2 | import type { BuildOptions, StdinOptions } from "esbuild"; 3 | import type { ResolvedConfig } from "vite"; 4 | import type { VercelOutputConfig } from "./schemas/config/config"; 5 | import type { VercelOutputPrerenderConfig } from "./schemas/config/prerender-config"; 6 | import type { VercelOutputVcConfig } from "./schemas/config/vc-config"; 7 | 8 | export type { VercelOutputConfig, VercelOutputVcConfig, VercelOutputPrerenderConfig }; 9 | 10 | export type ViteVercelRewrite = Rewrite & { enforce?: "pre" | "post" }; 11 | export type ViteVercelRedirect = Redirect & { enforce?: "pre" | "post" }; 12 | 13 | export type Awaitable = T | Promise; 14 | 15 | // Vite config for Vercel 16 | 17 | export interface ViteVercelConfig { 18 | /** 19 | * How long Functions should be allowed to run for every request, in seconds. 20 | * If left empty, default value for your plan will be used. 21 | */ 22 | defaultMaxDuration?: number; 23 | /** 24 | * Default expiration time (in seconds) for prerender functions. 25 | * Defaults to 86400 seconds (24h). 26 | * @see {@link https://vercel.com/docs/concepts/next.js/incremental-static-regeneration} 27 | * @see {@link https://vercel.com/docs/build-output-api/v3#vercel-primitives/prerender-functions/configuration} 28 | */ 29 | expiration?: number; 30 | /** 31 | * Also known as Server Side Generation, or SSG. 32 | * If present, this function is responsible to create static files in `.vercel/output/static`. 33 | * Defaults to `false`, which disables prerendering. 34 | */ 35 | prerender?: ViteVercelPrerenderFn | false; 36 | /** 37 | * @see {@link https://vercel.com/docs/projects/project-configuration#rewrites} 38 | */ 39 | rewrites?: ViteVercelRewrite[]; 40 | /** 41 | * @see {@link https://vercel.com/docs/projects/project-configuration#headers} 42 | * @beta 43 | */ 44 | headers?: Header[] | (() => Awaitable); 45 | /** 46 | * @see {@link https://vercel.com/docs/projects/project-configuration#redirects} 47 | */ 48 | redirects?: ViteVercelRedirect[]; 49 | /** 50 | * @see {@link https://vercel.com/docs/projects/project-configuration#cleanurls} 51 | */ 52 | cleanUrls?: boolean; 53 | /** 54 | * @see {@link https://vercel.com/docs/projects/project-configuration#trailingslash} 55 | */ 56 | trailingSlash?: boolean; 57 | /** 58 | * When true, the Serverless Function will stream the response to the client. 59 | * @see {@link https://vercel.com/docs/build-output-api/v3/primitives#serverless-function-configuration} 60 | */ 61 | defaultSupportsResponseStreaming?: boolean; 62 | /** 63 | * By default, all `api/*` endpoints are compiled under `.vercel/output/functions/api/*.func`. 64 | * If others serverless functions need to be compiled under `.vercel/output/functions`, they should be added here. 65 | * For instance, a framework can leverage this to have a generic ssr endpoint 66 | * without requiring the user to write any code. 67 | * 68 | * @example 69 | * ``` 70 | * { 71 | * additionalEndpoints: [ 72 | * { 73 | * // can also be an Object representing an esbuild StdinOptions 74 | * source: '/path/to/file.ts', 75 | * // URL path of the handler, will be generated to `.vercel/output/functions/api/file.func/index.js` 76 | * destination: '/api/file', 77 | * } 78 | * ] 79 | * } 80 | * ``` 81 | */ 82 | additionalEndpoints?: ( 83 | | ViteVercelApiEntry 84 | | (() => Awaitable) 85 | | (() => Awaitable) 86 | )[]; 87 | /** 88 | * Advanced configuration to override .vercel/output/config.json 89 | * @see {@link https://vercel.com/docs/build-output-api/v3/configuration#configuration} 90 | * @protected 91 | */ 92 | config?: Partial>; 93 | /** 94 | * ISR and SSG pages are mutually exclusive. If a page is found in both, ISR prevails. 95 | * Keys are path relative to .vercel/output/functions directory, either without extension, 96 | * or with `.prerender-config.json` extension. 97 | * If you have multiple isr configurations pointing to the same underlying function, you can leverage the `symlink` 98 | * property. See example below. 99 | * 100 | * @example 101 | * ``` 102 | * // Here `ssr_` means that a function is available under `.vercel/output/functions/ssr_.func` 103 | * isr: { 104 | * '/pages/a': { expiration: 15, symlink: 'ssr_', route: '^/a/.*$' }, 105 | * '/pages/b/c': { expiration: 15, symlink: 'ssr_', route: '^/b/c/.*$' }, 106 | * '/pages/d': { expiration: 15, symlink: 'ssr_', route: '^/d$' }, 107 | * '/pages/e': { expiration: 25 } 108 | * } 109 | * ``` 110 | * 111 | * @protected 112 | */ 113 | isr?: Record | (() => Awaitable>); 114 | /** 115 | * Defaults to `.vercel/output`. Mostly useful for testing purpose 116 | * @protected 117 | */ 118 | outDir?: string; 119 | /** 120 | * By default, Vite generates static files under `dist` folder. 121 | * But usually, when used through a Framework, such as Vike, 122 | * this folder can contain anything, requiring custom integration. 123 | * Set this to false if you create a plugin for a Framework. 124 | */ 125 | distContainsOnlyStatic?: boolean; 126 | } 127 | 128 | export interface VercelOutputIsr extends VercelOutputPrerenderConfig { 129 | symlink?: string; 130 | route?: string; 131 | } 132 | 133 | /** 134 | * Keys are path relative to .vercel/output/static directory 135 | */ 136 | export type ViteVercelPrerenderRoute = VercelOutputConfig["overrides"]; 137 | export type ViteVercelPrerenderFn = (resolvedConfig: ResolvedConfig) => Awaitable; 138 | 139 | export interface ViteVercelApiEntry { 140 | /** 141 | * Path to entry file, or stdin config 142 | */ 143 | source: string | StdinOptions; 144 | /** 145 | * Relative to `.vercel/output/functions`, without extension 146 | */ 147 | destination: string; 148 | /** 149 | * Override esbuild options 150 | */ 151 | buildOptions?: BuildOptions; 152 | /** 153 | * @deprecated use `route` instead 154 | */ 155 | addRoute?: boolean; 156 | /** 157 | * If `true`, guesses route for the function, and adds it to config.json (mimics defaults Vercel behavior). 158 | * If a string is provided, it will be equivalent to a `rewrites` rule. 159 | * Set to `false` to disable 160 | */ 161 | route?: string | boolean; 162 | /** 163 | * Set to `true` to mark this function as an Edge Function 164 | */ 165 | edge?: boolean; 166 | /** 167 | * Additional headers 168 | */ 169 | headers?: Record | null; 170 | /** 171 | * ISR config 172 | */ 173 | isr?: VercelOutputIsr; 174 | /** 175 | * When true, the Serverless Function will stream the response to the client 176 | */ 177 | streaming?: boolean; 178 | } 179 | -------------------------------------------------------------------------------- /packages/vercel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-vercel 2 | 3 | ## 9.1.1 4 | 5 | ### Patch Changes 6 | 7 | - fix: support recent node versions 8 | 9 | ## 9.1.0 10 | 11 | ### Minor Changes 12 | 13 | - fix: upgrade dependencies. Dropping support for node@18 14 | 15 | ### Patch Changes 16 | 17 | - Updated dependencies 18 | - @vite-plugin-vercel/vike@9.1.0 19 | 20 | ## 9.0.8 21 | 22 | ### Patch Changes 23 | 24 | - feat: adding support for node@24 25 | - Updated dependencies 26 | - @vite-plugin-vercel/vike@9.0.7 27 | 28 | ## 9.0.7 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies 33 | - @vite-plugin-vercel/vike@9.0.6 34 | 35 | ## 9.0.6 36 | 37 | ### Patch Changes 38 | 39 | - 3f3dd9c: fix(vike): do not show route function warnings for non route function 40 | - 3f3dd9c: fix(vike): streaming error in \_error pages 41 | - Updated dependencies 42 | - Updated dependencies [3f3dd9c] 43 | - Updated dependencies [3f3dd9c] 44 | - @vite-plugin-vercel/vike@9.0.5 45 | 46 | ## 9.0.5 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies 51 | - @vite-plugin-vercel/vike@9.0.4 52 | 53 | ## 9.0.4 54 | 55 | ### Patch Changes 56 | 57 | - Update peerDependencies to include Vite6 58 | - Updated dependencies 59 | - @vite-plugin-vercel/vike@9.0.3 60 | 61 | ## 9.0.3 62 | 63 | ### Patch Changes 64 | 65 | - fix: pass endpoint buildOptions to esbuild 66 | 67 | ## 9.0.2 68 | 69 | ### Patch Changes 70 | 71 | - Upgrade dependencies 72 | - fix CVE-2024-45296. See https://github.com/advisories/GHSA-9wv6-86v2-598j 73 | - Updated dependencies 74 | - Updated dependencies 75 | - @vite-plugin-vercel/vike@9.0.2 76 | 77 | ## 9.0.1 78 | 79 | ### Patch Changes 80 | 81 | - feat: Vike now support headers in +config.ts 82 | - Updated dependencies 83 | - @vite-plugin-vercel/vike@9.0.1 84 | 85 | ## 9.0.0 86 | 87 | ### Minor Changes 88 | 89 | - feat(vike): support edge config in +config.ts file 90 | 91 | ### Patch Changes 92 | 93 | - Updated dependencies 94 | - @vite-plugin-vercel/vike@9.0.0 95 | 96 | ## 8.0.1 97 | 98 | ### Patch Changes 99 | 100 | - avoid double build of additionnal endpoint 101 | - Updated dependencies 102 | - @vite-plugin-vercel/vike@8.0.1 103 | 104 | ## 8.0.0 105 | 106 | ### Minor Changes 107 | 108 | - Target node@18 by default 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies 113 | - @vite-plugin-vercel/vike@8.0.0 114 | 115 | ## 7.0.2 116 | 117 | ### Patch Changes 118 | 119 | - Explicitely export types 120 | - @vite-plugin-vercel/vike@7.0.1 121 | 122 | ## 7.0.1 123 | 124 | ### Patch Changes 125 | 126 | - Better support for servers like express 127 | - Updated dependencies 128 | - @vite-plugin-vercel/vike@7.0.1 129 | 130 | ## 7.0.0 131 | 132 | ### Patch Changes 133 | 134 | - Updated dependencies 135 | - @vite-plugin-vercel/vike@7.0.0 136 | 137 | ## 6.0.1 138 | 139 | ### Patch Changes 140 | 141 | - Update target to es2022 142 | - Updated dependencies 143 | - @vite-plugin-vercel/vike@6.0.1 144 | 145 | ## 6.0.0 146 | 147 | ### Patch Changes 148 | 149 | - add support for supportsResponseStreaming 150 | - Updated dependencies 151 | - @vite-plugin-vercel/vike@6.0.0 152 | 153 | ## 5.0.5 154 | 155 | ### Patch Changes 156 | 157 | - Fix warning when trying to read exports 158 | - Updated dependencies 159 | - @vite-plugin-vercel/vike@5.0.3 160 | 161 | ## 5.0.4 162 | 163 | ### Patch Changes 164 | 165 | - Replace workspace-root by @manypkg/find-root 166 | - @vite-plugin-vercel/vike@5.0.2 167 | 168 | ## 5.0.3 169 | 170 | ### Patch Changes 171 | 172 | - Avoid potential url and path module conflict 173 | - @vite-plugin-vercel/vike@5.0.2 174 | 175 | ## 5.0.2 176 | 177 | ### Patch Changes 178 | 179 | - Updated dependencies 180 | - @vite-plugin-vercel/vike@5.0.2 181 | 182 | ## 5.0.1 183 | 184 | ### Patch Changes 185 | 186 | - Updated dependencies 187 | - @vite-plugin-vercel/vike@5.0.1 188 | 189 | ## 5.0.0 190 | 191 | ### Major Changes 192 | 193 | - Always target ESM by default 194 | 195 | ### Patch Changes 196 | 197 | - @vite-plugin-vercel/vike@5.0.0 198 | 199 | ## 4.0.2 200 | 201 | ### Patch Changes 202 | 203 | - Upgrade dependencies 204 | - Updated dependencies 205 | - @vite-plugin-vercel/vike@4.0.2 206 | 207 | ## 4.0.1 208 | 209 | ### Patch Changes 210 | 211 | - Fix cleanup order 212 | - Updated dependencies 213 | - @vite-plugin-vercel/vike@4.0.1 214 | 215 | ## 4.0.0 216 | 217 | ### Minor Changes 218 | 219 | - Handle API endpoints with brackets 220 | 221 | ### Patch Changes 222 | 223 | - @vite-plugin-vercel/vike@4.0.0 224 | 225 | ## 3.0.2 226 | 227 | ### Patch Changes 228 | 229 | - Add support for static vite builds 230 | - @vite-plugin-vercel/vike@3.0.1 231 | 232 | ## 3.0.1 233 | 234 | ### Patch Changes 235 | 236 | - 8e7cad4: Add vite@5 in peerDependencies 237 | - Updated dependencies [8e7cad4] 238 | - @vite-plugin-vercel/vike@3.0.1 239 | 240 | ## 3.0.0 241 | 242 | ### Major Changes 243 | 244 | - ec8dcba: - Target node20.x 245 | - Upgrade all dependencies to their latest versions 246 | - Drops support for vike non-v1 design (still works with v2) 247 | 248 | ### Patch Changes 249 | 250 | - Updated dependencies [ec8dcba] 251 | - @vite-plugin-vercel/vike@3.0.0 252 | 253 | ## 2.0.1 254 | 255 | ### Patch Changes 256 | 257 | - fix: warn instead of crash in case parsing fails 258 | - @vite-plugin-vercel/vike@2.0.0 259 | 260 | ## 2.0.0 261 | 262 | ### Minor Changes 263 | 264 | - Respect configured node version 265 | 266 | ### Patch Changes 267 | 268 | - @vite-plugin-vercel/vike@2.0.0 269 | 270 | ## 1.0.0 271 | 272 | ### Minor Changes 273 | 274 | - Ability to exports configs directly from endpoints 275 | 276 | ### Patch Changes 277 | 278 | - @vite-plugin-vercel/vike@1.0.0 279 | 280 | ## 0.3.7 281 | 282 | ### Patch Changes 283 | 284 | - Upgrade dependencies 285 | - Updated dependencies 286 | - @vite-plugin-vercel/vike@0.4.2 287 | 288 | ## 0.3.6 289 | 290 | ### Patch Changes 291 | 292 | - chore: remove esbuild verbose warnings 293 | - Updated dependencies 294 | - @vite-plugin-vercel/vike@0.4.1 295 | 296 | ## 0.3.5 297 | 298 | ### Patch Changes 299 | 300 | - fix: ensure output folder is created 301 | - @vite-plugin-vercel/vike@0.4.0 302 | 303 | ## 0.3.4 304 | 305 | ### Patch Changes 306 | 307 | - Rename vite-plugin-vercel to Vike 308 | - Updated dependencies 309 | - @vite-plugin-vercel/vike@0.4.0 310 | 311 | ## 0.3.3 312 | 313 | ### Patch Changes 314 | 315 | - Adds support for @vercel/og 316 | - @vite-plugin-vercel/vike@0.3.3 317 | 318 | ## 0.3.2 319 | 320 | ### Patch Changes 321 | 322 | - Add support for vike V1 design 323 | - Updated dependencies 324 | - @vite-plugin-vercel/vike@0.3.3 325 | 326 | ## 0.3.1 327 | 328 | ### Patch Changes 329 | 330 | - update dependencies 331 | - Updated dependencies 332 | - @vite-plugin-vercel/vike@0.3.2 333 | 334 | ## 0.3.0 335 | 336 | ### Minor Changes 337 | 338 | - Rename Vike integration plugin to @vite-plugin-vercel/vike 339 | 340 | ### Patch Changes 341 | 342 | - Updated dependencies 343 | - @vite-plugin-vercel/vike@0.3.1 344 | 345 | ## 0.2.2 346 | 347 | ### Patch Changes 348 | 349 | - Update documentation and upgrade dependencies 350 | 351 | ## 0.2.1 352 | 353 | ### Patch Changes 354 | 355 | - fix use case without vike 356 | 357 | ## 0.2.0 358 | 359 | ### Minor Changes 360 | 361 | - upgrade dependencies 362 | 363 | ### Patch Changes 364 | 365 | - support edge-light target 366 | 367 | ## 0.1.7 368 | 369 | ### Patch Changes 370 | 371 | - fix issue with win32 path 372 | 373 | ## 0.1.6 374 | 375 | ### Patch Changes 376 | 377 | - Upgrade dependencies. Fixes #9 and #10 378 | 379 | ## 0.1.5 380 | 381 | ### Patch Changes 382 | 383 | - Edge Functions support 384 | 385 | ## 0.1.4 386 | 387 | ### Patch Changes 388 | 389 | - Support for vite@3 390 | 391 | ## 0.1.3 392 | 393 | ### Patch Changes 394 | 395 | - Support rewrites/redirects options (prefered over config.routes) 396 | 397 | ## 0.1.2 398 | 399 | ### Patch Changes 400 | 401 | - Truncate user provided static html files 402 | 403 | ## 0.1.1 404 | 405 | ### Patch Changes 406 | 407 | - Create a dedicated package for vike integration 408 | 409 | ## 0.1.0 410 | 411 | ### Minor Changes 412 | 413 | - Vercel API v3 beta release with ISR support fix 414 | 415 | ## 0.0.7 416 | 417 | ### Patch Changes 418 | 419 | - remove references to API v2 420 | 421 | ## 0.0.6 422 | 423 | ### Patch Changes 424 | 425 | - change target of serverless function back to cjs 426 | 427 | ## 0.0.5 428 | 429 | ### Patch Changes 430 | 431 | - initial beta release for v3 Vercel API 432 | -------------------------------------------------------------------------------- /packages/vercel/README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-vercel 2 | 3 | Vercel adapter for [Vite](https://vitejs.dev/). 4 | 5 | Bundle your Vite application as supported by [Vercel Output API (v3)](https://vercel.com/docs/build-output-api/v3). 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm i -D vite-plugin-vercel 11 | ``` 12 | 13 | ```bash 14 | yarn add -D vite-plugin-vercel 15 | ``` 16 | 17 | ```bash 18 | pnpm add -D vite-plugin-vercel 19 | ``` 20 | 21 | ```bash 22 | bun add -D vite-plugin-vercel 23 | ``` 24 | 25 | ## Features 26 | 27 | - [x] [SSG/Static files](https://vercel.com/docs/build-output-api/v3/primitives#static-files) 28 | - see [`prerender` config](/packages/vercel/src/types.ts#L37) 29 | - [x] [SSR/Serverless functions](https://vercel.com/docs/build-output-api/v3/primitives#serverless-functions) 30 | - `.[jt]s` files under the `/api` folder of your project are automatically bundled as Serverless functions under `.vercel/output/functions/api/*.func` 31 | - see [`additionalEndpoints` config](/packages/vercel/src/types.ts#L62) 32 | - [x] [ISR/Prerender functions](https://vercel.com/docs/build-output-api/v3/primitives#prerender-functions) 33 | - see [`isr` config](/packages/vercel/src/types.ts#L89). Also see implementation of [vike](/packages/vike-integration/vike.ts) for example 34 | - [x] [Edge functions](https://vercel.com/docs/build-output-api/v3/primitives#edge-functions) 35 | - [x] [Edge middleware](https://vercel.com/docs/functions/edge-middleware/middleware-api) 36 | - [ ] [Images optimization](https://vercel.com/docs/build-output-api/v3/configuration#images) 37 | - [ ] [Preview mode](https://vercel.com/docs/build-output-api/v3/features#preview-mode) 38 | - [x] [Advanced config](/packages/vercel/src/types.ts#L19) 39 | 40 | ## Simple usage 41 | 42 | Install this package as a dev dependency and add it to your Vite config: 43 | 44 | ```ts 45 | // vite.config.ts 46 | import { defineConfig } from 'vite'; 47 | import vercel from 'vite-plugin-vercel'; 48 | 49 | export default defineConfig({ 50 | plugins: [vercel()], 51 | vercel: { 52 | // optional configuration options, see "Advanced usage" below for details 53 | }, 54 | }); 55 | ``` 56 | 57 | > [!NOTE] 58 | > Files under `/api` or `/_api` directory will automatically be added under `/api/*` route 59 | > Prefer using `/_api` directory, as `@vercel/build` is currently force building `/api` files, 60 | > with no way to disable it, thus avoiding double compilation and unexpected behaviour. 61 | 62 | ### Configure endpoints 63 | 64 | Endpoints under `/api`, `/_api` or added through `additionalEndpoints` can be configured 65 | by exporting values from the endpoint file: 66 | 67 | ```ts 68 | // file: _api/endpoint.ts 69 | 70 | // Should run on edge runtime 71 | export const edge = true; 72 | 73 | // Always add those header to this endpoint 74 | export const headers = { 75 | 'Some-Header': 'some value', 76 | }; 77 | 78 | // Stream the response 79 | export const streaming = true; 80 | 81 | // Enable Incremental Static Regeneration for this endpoint 82 | export const isr = { 83 | expiration: 30, 84 | }; 85 | 86 | export default async function handler() { 87 | return new Response('Edge Function: OK', { 88 | status: 200, 89 | }); 90 | } 91 | ``` 92 | 93 | > [!NOTE] 94 | > Please create an issue if you need other per-endpoints configurations 95 | 96 | ### Edge middleware 97 | 98 | You can use [Edge middleware as describe in the official documentation](https://vercel.com/docs/functions/edge-middleware/middleware-api) (i.e. with a `middleware.ts` file at the root of your project). 99 | 100 | ## Usage with Vike 101 | 102 | [Vike](https://vike.dev/) is supported through [@vite-plugin-vercel/vike](/packages/vike-integration/README.md) plugin. 103 | 104 | You only need to install `@vite-plugin-vercel/vike`, the Vite config stays the same as above. 105 | 106 | You can then leverage [config files](https://vike.dev/config) to customize your endpoints: 107 | 108 | ```ts 109 | // /pages/product/+config.ts 110 | 111 | import Page from './Page'; 112 | import type { Config } from 'vike/types'; 113 | 114 | export default { 115 | // Customize ISR config for this page 116 | isr: { expiration: 15 }, 117 | // Target Edge instead of Serverless 118 | edge: true, 119 | // append headers to all responses 120 | headers: { 121 | 'X-Header': 'value' 122 | } 123 | } satisfies Config; 124 | ``` 125 | 126 | You will also need to extend the [renderer config](https://vike.dev/config#renderer) so that `vike` is aware of the new parameter: 127 | 128 | ```ts 129 | // /renderer/+config.ts 130 | 131 | import config from '@vite-plugin-vercel/vike/config'; 132 | import type { Config } from 'vike/types'; 133 | 134 | export default { 135 | extends: config, 136 | } satisfies Config; 137 | ``` 138 | 139 | ## Advanced usage 140 | 141 | ```ts 142 | // vite.config.ts 143 | import { defineConfig } from 'vite'; 144 | import vercel from 'vite-plugin-vercel'; 145 | 146 | export default defineConfig({ 147 | plugins: [vercel()], 148 | vercel: { 149 | // All the followings optional 150 | 151 | /** 152 | * How long Functions should be allowed to run for every request, in seconds. 153 | * If left empty, default value for your plan will be used. 154 | */ 155 | defaultMaxDuration: 30, 156 | /** 157 | * Enable streaming responses by default for all Serverless Functions 158 | */ 159 | defaultSupportsResponseStreaming: true, 160 | /** 161 | * Default expiration time (in seconds) for prerender functions. 162 | * Defaults to 86400 seconds (24h). 163 | */ 164 | expiration: 86400, 165 | /** 166 | * Also known as Server Side Generation, or SSG. 167 | * If present, this function is responsible to create static files in `.vercel/output/static`. 168 | * Defaults to `false`, which disables prerendering. 169 | */ 170 | prerender(resolvedConfig) { 171 | // Check `/packages/vike/vike.ts` `prerender` for an example 172 | }, 173 | /** 174 | * See https://vercel.com/docs/projects/project-configuration#rewrites 175 | */ 176 | rewrites: [{ source: '/about', destination: '/about-our-company.html' }], 177 | /** 178 | * @see {@link https://vercel.com/docs/projects/project-configuration#headers} 179 | */ 180 | headers: [ 181 | { 182 | "source": "/service-worker.js", 183 | "headers": [ 184 | { 185 | "key": "Cache-Control", 186 | "value": "public, max-age=0, must-revalidate" 187 | } 188 | ] 189 | } 190 | ], 191 | /** 192 | * See https://vercel.com/docs/projects/project-configuration#redirects 193 | */ 194 | redirects: [ 195 | { source: '/me', destination: '/profile.html', permanent: false }, 196 | ], 197 | /** 198 | * See https://vercel.com/docs/projects/project-configuration#cleanurls 199 | */ 200 | cleanUrls: true, 201 | /** 202 | * See https://vercel.com/docs/projects/project-configuration#trailingslash 203 | */ 204 | trailingSlash: true, 205 | /** 206 | * By default, all `api/*` and `_api/*` endpoints are compiled under `.vercel/output/functions/api/*.func`. 207 | * If others serverless functions need to be compiled under `.vercel/output/functions`, they should be added here. 208 | * For instance, a framework can leverage this to have a generic ssr endpoint 209 | * without requiring the user to write any code. 210 | */ 211 | additionalEndpoints: [ 212 | { 213 | // can also be an Object representing an esbuild StdinOptions 214 | source: '/path/to/file.ts', 215 | // URL path of the handler, will be generated to `.vercel/output/functions/api/file.func/index.js` 216 | destination: '/api/file', 217 | }, 218 | ], 219 | /** 220 | * Advanced configuration to override .vercel/output/config.json 221 | * See https://vercel.com/docs/build-output-api/v3/configuration#configuration 222 | */ 223 | config: { 224 | // routes?: Route[]; 225 | // images?: ImagesConfig; 226 | // wildcard?: WildcardConfig; 227 | // overrides?: OverrideConfig; 228 | // cache?: string[]; 229 | // crons?: CronsConfig; 230 | }, 231 | /** 232 | * ISR and SSG pages are mutually exclusive. If a page is found in both, ISR prevails. 233 | * Keys are path relative to .vercel/output/functions directory, either without extension, 234 | * or with `.prerender-config.json` extension. 235 | * If you have multiple isr configurations pointing to the same underlying function, you can leverage the `symlink` 236 | * property. 237 | * 238 | * Can be an object or a function returning an object (or a Promise of an object). 239 | * 240 | * Check `/packages/vike/vike.ts` `vitePluginVercelVpsIsrPlugin` for advanced usage. 241 | */ 242 | isr: { 243 | // `symlink: 'ssr_'` means that a function is available under `.vercel/output/functions/ssr_.func` 244 | '/pages/a': { expiration: 15, symlink: 'ssr_', route: '^/a/.*$' }, 245 | '/pages/b/c': { expiration: 15, symlink: 'ssr_', route: '^/b/c/.*$' }, 246 | '/pages/d': { expiration: 15, symlink: 'ssr_', route: '^/d$' }, 247 | '/pages/e': { expiration: 25 }, 248 | }, 249 | /** 250 | * Defaults to `.vercel/output`. Mostly useful for testing purpose 251 | */ 252 | outDir: '.vercel/output', 253 | }, 254 | }); 255 | ``` 256 | 257 | ## Demo 258 | 259 | https://vike-photon-demo.vercel.app/ 260 | -------------------------------------------------------------------------------- /packages/vercel/src/build.ts: -------------------------------------------------------------------------------- 1 | import fs, { copyFile } from "node:fs/promises"; 2 | import { builtinModules } from "node:module"; 3 | import path, { basename } from "node:path"; 4 | import { findRoot } from "@manypkg/find-root"; 5 | import { getNodeVersion } from "@vercel/build-utils"; 6 | import { nodeFileTrace } from "@vercel/nft"; 7 | import type { Header, Rewrite } from "@vercel/routing-utils"; 8 | import { type BuildOptions, type Plugin, build } from "esbuild"; 9 | import glob from "fast-glob"; 10 | import { type ASTNode, generateCode, loadFile } from "magicast"; 11 | import type { ResolvedConfig } from "vite"; 12 | import { assert } from "./assert"; 13 | import { vercelOutputVcConfigSchema } from "./schemas/config/vc-config"; 14 | import { vercelEndpointExports } from "./schemas/exports"; 15 | import type { VercelOutputIsr, ViteVercelApiEntry } from "./types"; 16 | import { getOutput, getRoot, pathRelativeTo } from "./utils"; 17 | 18 | export async function getAdditionalEndpoints(resolvedConfig: ResolvedConfig) { 19 | const userEndpoints: ViteVercelApiEntry[] = []; 20 | if (Array.isArray(resolvedConfig.vercel?.additionalEndpoints)) { 21 | for (const endpoint of resolvedConfig.vercel.additionalEndpoints) { 22 | if (typeof endpoint === "function") { 23 | const res = await endpoint(); 24 | if (Array.isArray(res)) { 25 | userEndpoints.push(...res); 26 | } else { 27 | userEndpoints.push(res); 28 | } 29 | } else { 30 | userEndpoints.push(endpoint); 31 | } 32 | } 33 | } 34 | 35 | return userEndpoints.map((e) => ({ 36 | ...e, 37 | route: e.route ?? true, 38 | // path.resolve removes the trailing slash if any 39 | destination: `${path.posix.resolve("/", e.destination)}.func`, 40 | })); 41 | } 42 | 43 | export async function getEntries(resolvedConfig: ResolvedConfig): Promise { 44 | const apiEntries = glob 45 | .sync(`${getRoot(resolvedConfig)}/api/**/*.*([a-zA-Z0-9])`) 46 | // from Vercel doc: Files with the underscore prefix are not turned into Serverless Functions. 47 | .filter((filepath) => !path.basename(filepath).startsWith("_")); 48 | 49 | if (apiEntries.length > 0) { 50 | console.warn( 51 | "@vercel/build is currently force building /api files itself, with no way to disable it. " + 52 | "In order to avoid double compilation, you should temporarily rename /api to /_api while using this plugin. " + 53 | "/_api functions are compiled under .vercel/output/functions/api/*.func as if they were in /api.", 54 | ); 55 | } 56 | 57 | const otherApiEntries = glob 58 | .sync(`${getRoot(resolvedConfig)}/_api/**/*.*([a-zA-Z0-9])`) 59 | // from Vercel doc: Files with the underscore prefix are not turned into Serverless Functions. 60 | .filter((filepath) => !path.basename(filepath).startsWith("_")); 61 | 62 | return [...apiEntries, ...otherApiEntries].reduce( 63 | (entryPoints, filePath) => { 64 | const outFilePath = pathRelativeTo(filePath, resolvedConfig, filePath.includes("/_api/") ? "_api" : "api"); 65 | const parsed = path.posix.parse(outFilePath); 66 | 67 | entryPoints.push({ 68 | source: filePath, 69 | destination: `api/${path.posix.join(parsed.dir, parsed.name)}.func`, 70 | route: true, 71 | }); 72 | 73 | return entryPoints; 74 | }, 75 | await getAdditionalEndpoints(resolvedConfig), 76 | ); 77 | } 78 | 79 | const edgeWasmPlugin: Plugin = { 80 | name: "edge-wasm-vercel", 81 | setup(build) { 82 | build.onResolve({ filter: /\.wasm/ }, (args) => { 83 | return { 84 | path: args.path.replace(/\.wasm\?module$/i, ".wasm"), 85 | external: true, 86 | }; 87 | }); 88 | }, 89 | }; 90 | 91 | const vercelOgPlugin = (ctx: { found: boolean; index: string }): Plugin => { 92 | return { 93 | name: "vercel-og", 94 | setup(build) { 95 | build.onResolve({ filter: /@vercel\/og/ }, () => { 96 | ctx.found = true; 97 | return undefined; 98 | }); 99 | 100 | build.onLoad({ filter: /@vercel\/og/ }, (args) => { 101 | ctx.index = args.path; 102 | return undefined; 103 | }); 104 | }, 105 | }; 106 | }; 107 | 108 | const standardBuildOptions: BuildOptions = { 109 | bundle: true, 110 | target: "es2022", 111 | format: "esm", 112 | platform: "node", 113 | logLevel: "info", 114 | logOverride: { 115 | "ignored-bare-import": "verbose", 116 | "require-resolve-not-external": "verbose", 117 | }, 118 | minify: false, 119 | plugins: [], 120 | define: { 121 | "process.env.NODE_ENV": '"production"', 122 | "import.meta.env.NODE_ENV": '"production"', 123 | }, 124 | }; 125 | 126 | export async function buildFn(resolvedConfig: ResolvedConfig, entry: ViteVercelApiEntry, buildOptions?: BuildOptions) { 127 | assert( 128 | entry.destination.length > 0, 129 | `Endpoint ${typeof entry.source === "string" ? entry.source : "-"} does not have build destination`, 130 | ); 131 | 132 | const options = Object.assign({}, standardBuildOptions); 133 | 134 | if (buildOptions) { 135 | Object.assign(options, buildOptions); 136 | } 137 | 138 | const filename = entry.edge || options.format === "cjs" ? "index.js" : "index.mjs"; 139 | const outfile = path.join(getOutput(resolvedConfig, "functions"), entry.destination, filename); 140 | 141 | Object.assign(options, { outfile }); 142 | 143 | if (!options.stdin) { 144 | if (typeof entry.source === "string") { 145 | options.entryPoints = [entry.source]; 146 | } else { 147 | assert(typeof entry.source === "object", "`{ source }` must be a string or an object"); 148 | assert(typeof entry.source.contents === "string", "`{ contents }` must be a string"); 149 | options.stdin = entry.source; 150 | } 151 | } 152 | 153 | if (entry.edge) { 154 | options.platform = undefined; 155 | options.external = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)]; 156 | options.conditions = ["edge-light", "worker", "browser", "module", "import", "require"]; 157 | options.plugins?.push(edgeWasmPlugin); 158 | options.format = "esm"; 159 | } else if (options.format === "esm") { 160 | options.banner = { 161 | js: `import { createRequire as VPV_createRequire } from "node:module"; 162 | import { fileURLToPath as VPV_fileURLToPath } from "node:url"; 163 | import { dirname as VPV_dirname } from "node:path"; 164 | const require = VPV_createRequire(import.meta.url); 165 | const __filename = VPV_fileURLToPath(import.meta.url); 166 | const __dirname = VPV_dirname(__filename); 167 | `, 168 | }; 169 | } 170 | 171 | const ctx = { found: false, index: "" }; 172 | options.plugins?.push(vercelOgPlugin(ctx)); 173 | 174 | const output = await build(options); 175 | 176 | // guess some assets dependencies 177 | if (typeof entry.source === "string") { 178 | let base = resolvedConfig.root; 179 | try { 180 | const dir = await findRoot(resolvedConfig.root); 181 | base = dir.rootDir; 182 | } catch (e) { 183 | // ignore error 184 | } 185 | const { fileList, reasons } = await nodeFileTrace([entry.source], { 186 | base, 187 | processCwd: resolvedConfig.root, 188 | mixedModules: true, 189 | ignore: [ 190 | "**/node_modules/react{,-dom,-dom-server-turbopack}/**/*.development.js", 191 | "**/*.d.ts", 192 | "**/*.map", 193 | "**/node_modules/webpack5/**/*", 194 | ], 195 | async readFile(filepath) { 196 | if (filepath.endsWith(".ts") || filepath.endsWith(".tsx")) { 197 | const result = await build({ 198 | ...standardBuildOptions, 199 | entryPoints: [entry.source as string], 200 | bundle: false, 201 | write: false, 202 | }); 203 | 204 | return result.outputFiles[0].text; 205 | } 206 | 207 | return fs.readFile(filepath, "utf-8"); 208 | }, 209 | }); 210 | 211 | for (const file of fileList) { 212 | if ( 213 | reasons.has(file) && 214 | reasons.get(file)?.type.includes("asset") && 215 | !file.endsWith(".js") && 216 | !file.endsWith(".cjs") && 217 | !file.endsWith(".mjs") && 218 | !file.endsWith("package.json") 219 | ) { 220 | await copyFile( 221 | path.join(base, file), 222 | path.join(getOutput(resolvedConfig, "functions"), entry.destination, basename(file)), 223 | ); 224 | } 225 | } 226 | } 227 | 228 | await writeVcConfig(resolvedConfig, entry.destination, filename, { 229 | edge: Boolean(entry.edge), 230 | streaming: entry.streaming, 231 | }); 232 | 233 | return output; 234 | } 235 | 236 | export async function writeVcConfig( 237 | resolvedConfig: ResolvedConfig, 238 | destination: string, 239 | filename: string, 240 | options: { 241 | edge: boolean; 242 | streaming?: boolean; 243 | }, 244 | ): Promise { 245 | const vcConfig = path.join(getOutput(resolvedConfig, "functions"), destination, ".vc-config.json"); 246 | 247 | const nodeVersion = await getNodeVersion(getOutput(resolvedConfig)); 248 | 249 | await fs.writeFile( 250 | vcConfig, 251 | JSON.stringify( 252 | vercelOutputVcConfigSchema.parse( 253 | options.edge 254 | ? { 255 | runtime: "edge", 256 | entrypoint: filename, 257 | } 258 | : { 259 | runtime: nodeVersion.runtime, 260 | handler: filename, 261 | maxDuration: resolvedConfig.vercel?.defaultMaxDuration, 262 | launcherType: "Nodejs", 263 | shouldAddHelpers: true, 264 | supportsResponseStreaming: options.streaming ?? resolvedConfig.vercel?.defaultSupportsResponseStreaming, 265 | }, 266 | ), 267 | undefined, 268 | 2, 269 | ), 270 | "utf-8", 271 | ); 272 | } 273 | 274 | function getSourceAndDestination(destination: string) { 275 | if (destination.startsWith("api/")) { 276 | return path.posix.resolve("/", destination); 277 | } 278 | return path.posix.resolve("/", destination, ":match*"); 279 | } 280 | 281 | const RE_BRACKETS = /^\[([^/]+)\]$/gm; 282 | function replaceBrackets(source: string) { 283 | return source 284 | .split("/") 285 | .map((segment) => segment.replace(RE_BRACKETS, ":$1")) 286 | .join("/"); 287 | } 288 | 289 | function isPrimitive(test: unknown) { 290 | return test !== Object(test); 291 | } 292 | 293 | export function _eval(code: unknown): boolean { 294 | const func = new Function(`{ return function(){ return ${code} } };`); 295 | return func.call(null).call(null); 296 | } 297 | 298 | function evalExport(exp: unknown) { 299 | if (!exp) return; 300 | 301 | const code = isPrimitive(exp) ? exp : generateCode(exp as ASTNode).code; 302 | 303 | return _eval(code); 304 | } 305 | 306 | async function extractExports(filepath: string) { 307 | try { 308 | const mod = await loadFile(filepath); 309 | 310 | const subject = { 311 | edge: evalExport(mod.exports.edge), 312 | headers: evalExport(mod.exports.headers), 313 | streaming: evalExport(mod.exports.streaming), 314 | isr: evalExport(mod.exports.isr), 315 | }; 316 | 317 | return vercelEndpointExports.parse(subject); 318 | } catch (e) { 319 | console.warn(`Warning: failed to read exports of '${filepath}'`, e); 320 | } 321 | } 322 | 323 | async function extractHeaders(resolvedConfig: ResolvedConfig) { 324 | let headers: Header[] = []; 325 | if (typeof resolvedConfig.vercel?.headers === "function") { 326 | headers = await resolvedConfig.vercel.headers(); 327 | } else if (Array.isArray(resolvedConfig.vercel?.headers)) { 328 | headers = resolvedConfig.vercel.headers; 329 | } 330 | 331 | return headers; 332 | } 333 | 334 | export async function buildEndpoints(resolvedConfig: ResolvedConfig): Promise<{ 335 | rewrites: Rewrite[]; 336 | isr: Record; 337 | headers: Header[]; 338 | }> { 339 | const entries = await getEntries(resolvedConfig); 340 | const headers = await extractHeaders(resolvedConfig); 341 | 342 | for (const entry of entries) { 343 | if (typeof entry.source === "string") { 344 | const exports = await extractExports(entry.source); 345 | 346 | if (exports) { 347 | if (entry.headers || exports.headers) { 348 | entry.headers = { 349 | ...exports.headers, 350 | ...entry.headers, 351 | }; 352 | } 353 | 354 | if (entry.edge !== undefined && exports.edge !== undefined) { 355 | throw new Error( 356 | `edge configuration should be defined either in the endpoint itself or through Vite config, not both ('${entry.source}')`, 357 | ); 358 | } 359 | 360 | if (exports.edge !== undefined) { 361 | entry.edge = exports.edge; 362 | } 363 | 364 | if (entry.isr !== undefined && exports.isr !== undefined) { 365 | throw new Error( 366 | `isr configuration should be defined either in the endpoint itself or through Vite config, not both ('${entry.source}')`, 367 | ); 368 | } 369 | 370 | if ( 371 | (entry.isr !== undefined || exports.isr !== undefined) && 372 | (entry.edge !== undefined || exports.edge !== undefined) 373 | ) { 374 | throw new Error(`isr cannot be enabled for edge functions ('${entry.source}')`); 375 | } 376 | 377 | if (exports.isr) { 378 | entry.isr = exports.isr; 379 | } 380 | 381 | if (typeof exports.streaming === "boolean") { 382 | entry.streaming = exports.streaming; 383 | } 384 | } 385 | } 386 | 387 | await buildFn(resolvedConfig, entry, entry.buildOptions); 388 | } 389 | 390 | const isrEntries = entries 391 | .filter((e) => e.isr) 392 | .map((e) => [e.destination.replace(/\.func$/, ""), { expiration: e.isr?.expiration }] as const); 393 | 394 | return { 395 | rewrites: entries 396 | .filter((e) => { 397 | if (e.addRoute === undefined && e.route !== undefined) { 398 | return e.route !== false; 399 | } 400 | if (e.addRoute !== undefined && e.route === undefined) { 401 | return e.addRoute !== false; 402 | } 403 | if (e.addRoute !== undefined && e.route !== undefined) { 404 | throw new Error("Cannot use both `route` and `addRoute` in `additionalEndpoints`"); 405 | } 406 | return true; 407 | }) 408 | .map((e) => { 409 | const destination = e.destination.replace(/\.func$/, ""); 410 | if (typeof e.route === "string") { 411 | return { 412 | source: `(${e.route})`, 413 | destination: `${destination}/?__original_path=$1`, 414 | }; 415 | } 416 | return { 417 | source: replaceBrackets(getSourceAndDestination(destination)), 418 | destination: getSourceAndDestination(destination), 419 | }; 420 | }), 421 | isr: Object.fromEntries(isrEntries) as Record, 422 | headers: [ 423 | ...entries 424 | .filter((e) => e.headers) 425 | .map((e) => ({ 426 | source: `/${e.destination.replace(/\.func$/, "")}`, 427 | headers: Object.entries(e.headers ?? {}).map(([key, value]) => ({ 428 | key, 429 | value, 430 | })), 431 | })), 432 | ...headers, 433 | ], 434 | }; 435 | } 436 | --------------------------------------------------------------------------------