From 4d77c1cba7e0899b1416ea301325dce8e55dc839 Mon Sep 17 00:00:00 2001 From: Okiki Date: Thu, 30 May 2024 05:11:40 +0000 Subject: [PATCH 1/3] chore: testing vercel serverless Signed-off-by: Okiki --- adapters/vercel-serverless/index.ts | 52 ++++ adapters/vercel-serverless/middleware.ts | 265 ++++++++++++++++++++ adapters/vercel-serverless/post-build.ts | 162 ++++++++++++ adapters/vercel-serverless/shared.ts | 288 ++++++++++++++++++++++ adapters/vercel-serverless/types.ts | 148 +++++++++++ adapters/vercel-serverless/vite.config.ts | 9 +- custom-src/entry.vercel-serverless.tsx | 24 +- 7 files changed, 931 insertions(+), 17 deletions(-) create mode 100644 adapters/vercel-serverless/index.ts create mode 100644 adapters/vercel-serverless/middleware.ts create mode 100644 adapters/vercel-serverless/post-build.ts create mode 100644 adapters/vercel-serverless/shared.ts create mode 100644 adapters/vercel-serverless/types.ts diff --git a/adapters/vercel-serverless/index.ts b/adapters/vercel-serverless/index.ts new file mode 100644 index 0000000..7fd2766 --- /dev/null +++ b/adapters/vercel-serverless/index.ts @@ -0,0 +1,52 @@ +import { createQwikCity as createQwikCityNode } from '@builder.io/qwik-city/middleware/node'; +import type { ServerRenderOptions } from '@builder.io/qwik-city/middleware/request-handler'; + +import type { Http2ServerRequest } from 'node:http2'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import * as process from 'node:process'; + +// @builder.io/qwik-city/middleware/vercel/serverless +const VERCEL_COOKIE = '__vdpl'; +const VERCEL_SKEW_PROTECTION_ENABLED = 'VERCEL_SKEW_PROTECTION_ENABLED'; +const VERCEL_DEPLOYMENT_ID = 'VERCEL_DEPLOYMENT_ID'; +const BASE_URL = 'BASE_URL'; + +/** @public */ +export function createQwikCity(opts: QwikCityVercelServerlessOptions) { + const { router } = createQwikCityNode(opts); + + return function onVercelServerlessRequest( + req: IncomingMessage | Http2ServerRequest, + res: ServerResponse, + next: (err?: any) => void + ) { + try { + if (process.env[VERCEL_SKEW_PROTECTION_ENABLED]) { + const deploymentId = process.env[VERCEL_DEPLOYMENT_ID] || ''; + const baseUrl = process.env[BASE_URL] || '/'; + + // Only on document request + if (req.headers['sec-fetch-dest']) { + // set cookie before creating response + const cookieName = VERCEL_COOKIE; + const cookieValue = deploymentId; + const cookieOptions = [`Path=${baseUrl}`, 'Secure', 'SameSite=Strict', 'HttpOnly']; + const cookieString = `${cookieName}=${cookieValue}; ${cookieOptions.join('; ')}`; + + // Set the cookie header + res.setHeader('Set-Cookie', cookieString); + } + } + + router(req, res, next); + } catch (err: any) { + throw new Error(err.message); + } + }; +} + +/** @public */ +export interface QwikCityVercelServerlessOptions extends ServerRenderOptions { } + +/** @public */ +export interface PlatformVercelServerless { } \ No newline at end of file diff --git a/adapters/vercel-serverless/middleware.ts b/adapters/vercel-serverless/middleware.ts new file mode 100644 index 0000000..441a522 --- /dev/null +++ b/adapters/vercel-serverless/middleware.ts @@ -0,0 +1,265 @@ +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; +import { getParentDir, type ServerAdapterOptions, viteAdapter } from './shared'; +import fs from 'node:fs'; +import { dirname, join } from 'node:path'; + +/** @public */ +export const FUNCTION_DIRECTORY = '_qwik-city-serverless'; + +/** @public */ +export function vercelServerlessAdapter(opts: VercelServerlessAdapterOptions = {}): any { + return viteAdapter({ + name: 'vercel-serverless', + origin: process?.env?.VERCEL_URL || 'https://yoursitename.vercel.app', + ssg: opts.ssg, + staticPaths: opts.staticPaths, + cleanStaticGenerated: true, + + config(config) { + const outDir = + config.build?.outDir || + join('.vercel', 'output', 'functions', `${FUNCTION_DIRECTORY}.func`); + return { + resolve: { + conditions: + opts.target === 'node' + ? ['node', 'import', 'module', 'browser', 'default'] + : ['edge-light', 'webworker', 'worker', 'browser', 'module', 'main'], + }, + ssr: { + target: 'node', + noExternal: true, + }, + build: { + ssr: true, + outDir, + rollupOptions: { + output: { + format: 'es', + hoistTransitiveImports: false, + }, + }, + }, + publicDir: false, + }; + }, + + async generate({ clientPublicOutDir, serverOutDir, basePathname, outputEntries }) { + const vercelOutputDir = getParentDir(serverOutDir, 'output'); + + if (opts.outputConfig !== false) { + // https://vercel.com/docs/build-output-api/v3#features/edge-middleware + const vercelOutputConfig = { + routes: [ + { handle: 'filesystem' }, + { + src: basePathname + '.*', + dest: `/${FUNCTION_DIRECTORY}`, + }, + ], + version: 3, + }; + + await fs.promises.writeFile( + join(vercelOutputDir, 'config.json'), + JSON.stringify(vercelOutputConfig, null, 2) + ); + } + + let entrypoint = opts.vcConfigEntryPoint; + if (!entrypoint) { + if (outputEntries.some((n) => n === 'entry.vercel-serverless.mjs')) { + entrypoint = 'entry.vercel-serverless.mjs'; + } else { + entrypoint = 'entry.vercel-serverless.js'; + } + } + + // https://vercel.com/docs/build-output-api/v3/primitives#serverless-functions + const vcConfigPath = join(serverOutDir, '.vc-config.json'); + const vcConfig = { + launcherType: 'Nodejs', + runtime: opts.runtime || 'nodejs20.x', + handler: entrypoint, + memory: opts.memory, + maxDuration: opts.maxDuration, + environment: opts.environment, + regions: opts.regions, + shouldAddHelpers: opts.shouldAddHelpers, + shouldAddSourcemapSupport: opts.shouldAddSourceMapSupport, + awsLambdaHandler: opts.awsLambdaHandler, + }; + await fs.promises.writeFile(vcConfigPath, JSON.stringify(vcConfig, null, 2)); + + // vercel places all of the static files into the .vercel/output/static directory + // move from the dist directory to vercel's output static directory + let vercelStaticDir = join(vercelOutputDir, 'static'); + + const basePathnameParts = basePathname.split('/').filter((p) => p.length > 0); + if (basePathnameParts.length > 0) { + // for vercel we need to add the base path to the static dir + vercelStaticDir = join(vercelStaticDir, ...basePathnameParts); + } + + // ensure we remove any existing static dir + await fs.promises.rm(vercelStaticDir, { recursive: true, force: true }); + + // ensure the containing directory exists we're moving the static dir to exists + await fs.promises.mkdir(dirname(vercelStaticDir), { recursive: true }); + + // move the dist directory to the vercel output static directory location + await fs.promises.rename(clientPublicOutDir, vercelStaticDir); + }, + }); +} + +/** @public */ +export interface ServerlessFunctionConfig { + /** + * Specifies which "runtime" will be used to execute the Serverless Function. + * + * Required: Yes + */ + runtime: string; + + /** + * Indicates the initial file where code will be executed for the Serverless Function. + * + * Required: Yes + */ + handler: string; + + /** + * Amount of memory (RAM in MB) that will be allocated to the Serverless Function. + * + * Required: No + */ + memory?: number; + + /** + * Maximum duration (in seconds) that will be allowed for the Serverless Function. + * + * Required: No + */ + maxDuration?: number; + + /** + * Map of additional environment variables that will be available to the Serverless Function, in + * addition to the env vars specified in the Project Settings. + * + * Required: No + */ + environment?: Record[]; + + /** + * List of Vercel Regions where the Serverless Function will be deployed to. + * + * Required: No + */ + regions?: string[]; + + /** + * True if a custom runtime has support for Lambda runtime wrappers. + * + * Required: No + */ + supportsWrapper?: boolean; + + /** + * When true, the Serverless Function will stream the response to the client. + * + * Required: No + */ + supportsResponseStreaming?: boolean; +} + +/** @public */ +export interface VercelServerlessAdapterOptions extends ServerAdapterOptions { + /** + * Determines if the build should auto-generate the `.vercel/output/config.json` config. + * + * Defaults to `true`. + */ + outputConfig?: boolean; + + /** + * The `entrypoint` property in the `.vc-config.json` file. Indicates the initial file where code + * will be executed for the Edge Function. + * + * Defaults to `entry.vercel-edge.js`. + */ + vcConfigEntryPoint?: string; + + /** + * Manually add pathnames that should be treated as static paths and not SSR. For example, when + * these pathnames are requested, their response should come from a static file, rather than a + * server-side rendered response. + */ + staticPaths?: string[]; + + /** + * Enables request and response helpers methods. + * + * Required: No Default: false + */ + shouldAddHelpers?: boolean; + + /** + * Enables source map generation. + * + * Required: No Default: false + */ + shouldAddSourceMapSupport?: boolean; + + /** + * AWS Handler Value for when the serverless function uses AWS Lambda syntax. + * + * Required: No + */ + awsLambdaHandler?: string; + + /** + * Specifies the target platform for the deployment, such as Vercel, AWS, etc. + * + * Required: No + */ + target?: string; + + /** + * Specifies the runtime environment for the function, for example, Node.js, Deno, etc. + * + * Required: No + */ + runtime?: string; + + /** + * Specifies the memory allocation for the serverless function. + * + * Required: No + */ + memory?: number; + + /** + * Specifies the maximum duration that the serverless function can run. + * + * Required: No + */ + maxDuration?: number; + + /** + * Specifies environment variables for the serverless function. + * + * Required: No + */ + environment?: { [key: string]: string }; + + /** + * Specifies the regions in which the serverless function should run. + * + * Required: No + */ + regions?: string[]; +} + +/** @public */ +export type { StaticGenerateRenderOptions }; \ No newline at end of file diff --git a/adapters/vercel-serverless/post-build.ts b/adapters/vercel-serverless/post-build.ts new file mode 100644 index 0000000..6b3af31 --- /dev/null +++ b/adapters/vercel-serverless/post-build.ts @@ -0,0 +1,162 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import { getErrorHtml } from '@builder.io/qwik-city/middleware/request-handler'; + +export async function postBuild( + clientOutDir: string, + basePathname: string, + userStaticPaths: string[], + format: string, + cleanStatic: boolean +) { + const ignorePathnames = new Set([basePathname + 'build/', basePathname + 'assets/']); + + const staticPaths = new Set(userStaticPaths.map(normalizeTrailingSlash)); + const notFounds: string[][] = []; + + const loadItem = async (fsDir: string, fsName: string, pathname: string) => { + pathname = normalizeTrailingSlash(pathname); + if (ignorePathnames.has(pathname)) { + return; + } + + const fsPath = join(fsDir, fsName); + + if (fsName === 'index.html' || fsName === 'q-data.json') { + // static index.html file + if (!staticPaths.has(pathname) && cleanStatic) { + await fs.promises.unlink(fsPath); + } + return; + } + + if (fsName === '404.html') { + // static 404.html file + const notFoundHtml = await fs.promises.readFile(fsPath, 'utf-8'); + notFounds.push([pathname, notFoundHtml]); + return; + } + + const stat = await fs.promises.stat(fsPath); + if (stat.isDirectory()) { + await loadDir(fsPath, pathname + fsName + '/'); + } else if (stat.isFile()) { + staticPaths.add(pathname + fsName); + } + }; + + const loadDir = async (fsDir: string, pathname: string) => { + const itemNames = await fs.promises.readdir(fsDir); + await Promise.all(itemNames.map((i) => loadItem(fsDir, i, pathname))); + }; + + if (fs.existsSync(clientOutDir)) { + await loadDir(clientOutDir, basePathname); + } + + const notFoundPathsCode = createNotFoundPathsModule(basePathname, notFounds, format); + const staticPathsCode = createStaticPathsModule(basePathname, staticPaths, format); + + return { + notFoundPathsCode, + staticPathsCode, + }; +} + +function normalizeTrailingSlash(pathname: string) { + if (!pathname.endsWith('/')) { + return pathname + '/'; + } + return pathname; +} + +function createNotFoundPathsModule(basePathname: string, notFounds: string[][], format: string) { + notFounds.sort((a, b) => { + if (a[0].length > b[0].length) { + return -1; + } + if (a[0].length < b[0].length) { + return 1; + } + if (a[0] < b[0]) { + return -1; + } + if (a[0] > b[0]) { + return 1; + } + return 0; + }); + + if (!notFounds.some((r) => r[0] === basePathname)) { + const html = getErrorHtml(404, 'Resource Not Found'); + notFounds.push([basePathname, html]); + } + + const c: string[] = []; + + c.push(`const notFounds = ${JSON.stringify(notFounds, null, 2)};`); + + c.push(`function getNotFound(p) {`); + c.push(` for (const r of notFounds) {`); + c.push(` if (p.startsWith(r[0])) {`); + c.push(` return r[1];`); + c.push(` }`); + c.push(` }`); + c.push(` return "Resource Not Found";`); + c.push(`}`); + + if (format === 'cjs') { + c.push('exports.getNotFound = getNotFound;'); + } else { + c.push('export { getNotFound };'); + } + + return c.join('\n'); +} + +function createStaticPathsModule(basePathname: string, staticPaths: Set, format: string) { + const assetsPath = basePathname + 'assets/'; + const baseBuildPath = basePathname + 'build/'; + + const c: string[] = []; + + c.push( + `const staticPaths = new Set(${JSON.stringify( + Array.from(new Set(staticPaths)).sort() + )});` + ); + + c.push(`function isStaticPath(method, url) {`); + c.push(` if (method.toUpperCase() !== 'GET') {`); + c.push(` return false;`); + c.push(` }`); + c.push(` const p = url.pathname;`); + c.push(` if (p.startsWith(${JSON.stringify(baseBuildPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (p.startsWith(${JSON.stringify(assetsPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (staticPaths.has(p)) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (p.endsWith('/q-data.json')) {`); + c.push(` const pWithoutQdata = p.replace(/\\/q-data.json$/, '');`); + c.push(` if (staticPaths.has(pWithoutQdata + '/')) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (staticPaths.has(pWithoutQdata)) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` }`); + c.push(` return false;`); + c.push(`}`); + + if (format === 'cjs') { + c.push('exports.isStaticPath = isStaticPath;'); + } else { + c.push('export { isStaticPath };'); + } + + return c.join('\n'); +} \ No newline at end of file diff --git a/adapters/vercel-serverless/shared.ts b/adapters/vercel-serverless/shared.ts new file mode 100644 index 0000000..60f8ee5 --- /dev/null +++ b/adapters/vercel-serverless/shared.ts @@ -0,0 +1,288 @@ +import type { Plugin, UserConfig } from 'vite'; +import type { QwikCityPlugin } from '@builder.io/qwik-city/vite'; +import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; +import type { + StaticGenerateOptions, + StaticGenerateRenderOptions, +} from '@builder.io/qwik-city/static'; +import type { BuildRoute } from './types'; +import fs from 'node:fs'; +import { basename, dirname, join, resolve } from 'node:path'; +import { postBuild } from './post-build'; + +/** @public */ +export function viteAdapter(opts: ViteAdapterPluginOptions) { + let qwikCityPlugin: QwikCityPlugin | null = null; + let qwikVitePlugin: QwikVitePlugin | null = null; + let serverOutDir: string | null = null; + let renderModulePath: string | null = null; + let qwikCityPlanModulePath: string | null = null; + let isSsrBuild = false; + let format = 'esm'; + const outputEntries: string[] = []; + + const plugin: Plugin = { + name: `vite-plugin-qwik-city-${opts.name}`, + enforce: 'post', + apply: 'build', + + config(config) { + if (typeof opts.config === 'function') { + config = opts.config(config); + } + config.define = { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + ...config.define, + }; + return config; + }, + + configResolved(config) { + isSsrBuild = !!config.build.ssr; + + if (isSsrBuild) { + qwikCityPlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik-city' + ) as QwikCityPlugin; + if (!qwikCityPlugin) { + throw new Error('Missing vite-plugin-qwik-city'); + } + qwikVitePlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik' + ) as QwikVitePlugin; + if (!qwikVitePlugin) { + throw new Error('Missing vite-plugin-qwik'); + } + serverOutDir = config.build.outDir; + + if (config.build?.ssr !== true) { + throw new Error( + `"build.ssr" must be set to "true" in order to use the "${opts.name}" adapter.` + ); + } + + if (!config.build?.rollupOptions?.input) { + throw new Error( + `"build.rollupOptions.input" must be set in order to use the "${opts.name}" adapter.` + ); + } + + // @ts-ignore `format` removed in Vite 5 + if (config.ssr?.format === 'cjs') { + format = 'cjs'; + } + } + }, + + generateBundle(_, bundles) { + if (isSsrBuild) { + outputEntries.length = 0; + + for (const fileName in bundles) { + const chunk = bundles[fileName]; + if (chunk.type === 'chunk' && chunk.isEntry) { + outputEntries.push(fileName); + + if (chunk.name === 'entry.ssr') { + renderModulePath = join(serverOutDir!, fileName); + } else if (chunk.name === '@qwik-city-plan') { + qwikCityPlanModulePath = join(serverOutDir!, fileName); + } + } + } + + if (!renderModulePath) { + throw new Error( + 'Unable to find "entry.ssr" entry point. Did you forget to add it to "build.rollupOptions.input"?' + ); + } + + if (!qwikCityPlanModulePath) { + throw new Error( + 'Unable to find "@qwik-city-plan" entry point. Did you forget to add it to "build.rollupOptions.input"?' + ); + } + } + }, + + closeBundle: { + sequential: true, + async handler() { + if ( + isSsrBuild && + opts.ssg !== null && + serverOutDir && + qwikCityPlugin?.api && + qwikVitePlugin?.api + ) { + const staticPaths: string[] = opts.staticPaths || []; + const routes = qwikCityPlugin.api.getRoutes(); + const basePathname = qwikCityPlugin.api.getBasePathname(); + const clientOutDir = qwikVitePlugin.api.getClientOutDir()!; + const clientPublicOutDir = qwikVitePlugin.api.getClientPublicOutDir()!; + + const rootDir = qwikVitePlugin.api.getRootDir() ?? undefined; + if (renderModulePath && qwikCityPlanModulePath && clientOutDir && clientPublicOutDir) { + let ssgOrigin = opts.ssg?.origin ?? opts.origin; + if (!ssgOrigin) { + ssgOrigin = `https://yoursite.qwik.dev`; + } + // allow for capacitor:// or http:// + if (ssgOrigin.length > 0 && !/:\/\//.test(ssgOrigin)) { + ssgOrigin = `https://${ssgOrigin}`; + } + if (ssgOrigin.startsWith('//')) { + ssgOrigin = `https:${ssgOrigin}`; + } + try { + ssgOrigin = new URL(ssgOrigin).origin; + } catch (e) { + this.warn( + `Invalid "origin" option: "${ssgOrigin}". Using default origin: "https://yoursite.qwik.dev"` + ); + ssgOrigin = `https://yoursite.qwik.dev`; + } + + const staticGenerate = await import('../../../static'); + const generateOpts: StaticGenerateOptions = { + maxWorkers: opts.maxWorkers, + basePathname, + outDir: clientPublicOutDir, + rootDir, + ...opts.ssg, + origin: ssgOrigin, + renderModulePath, + qwikCityPlanModulePath, + }; + + const staticGenerateResult = await staticGenerate.generate(generateOpts); + if (staticGenerateResult.errors > 0) { + const err = new Error( + `Error while running SSG from "${opts.name}" adapter. At least one path failed to render.` + ); + err.stack = undefined; + this.error(err); + } + + staticPaths.push(...staticGenerateResult.staticPaths); + + const { staticPathsCode, notFoundPathsCode } = await postBuild( + clientPublicOutDir, + basePathname, + staticPaths, + format, + !!opts.cleanStaticGenerated + ); + + await Promise.all([ + fs.promises.writeFile(join(serverOutDir, RESOLVED_STATIC_PATHS_ID), staticPathsCode), + fs.promises.writeFile( + join(serverOutDir, RESOLVED_NOT_FOUND_PATHS_ID), + notFoundPathsCode + ), + ]); + if (typeof opts.generate === 'function') { + await opts.generate({ + outputEntries, + serverOutDir, + clientOutDir, + clientPublicOutDir, + basePathname, + routes, + warn: (message) => this.warn(message), + error: (message) => this.error(message), + }); + } + } + } + }, + }, + }; + + return plugin; +} + +/** @public */ +export function getParentDir(startDir: string, dirName: string) { + const root = resolve('/'); + let dir = startDir; + for (let i = 0; i < 20; i++) { + dir = dirname(dir); + if (basename(dir) === dirName) { + return dir; + } + if (dir === root) { + break; + } + } + throw new Error(`Unable to find "${dirName}" directory from "${startDir}"`); +} + +/** @public */ +interface ViteAdapterPluginOptions { + name: string; + origin: string; + staticPaths?: string[]; + ssg?: AdapterSSGOptions | null; + cleanStaticGenerated?: boolean; + maxWorkers?: number; + config?: (config: UserConfig) => UserConfig; + generate?: (generateOpts: { + outputEntries: string[]; + clientOutDir: string; + clientPublicOutDir: string; + serverOutDir: string; + basePathname: string; + routes: BuildRoute[]; + warn: (message: string) => void; + error: (message: string) => void; + }) => Promise; +} + +/** @public */ +export interface ServerAdapterOptions { + /** + * Options the adapter should use when running Static Site Generation (SSG). Defaults the `filter` + * to "auto" which will attempt to automatically decides if a page can be statically generated and + * does not have dynamic data, or if it the page should instead be rendered on the server (SSR). + * Setting to `null` will prevent any pages from being statically generated. + */ + ssg?: AdapterSSGOptions | null; +} + +/** @public */ +export interface AdapterSSGOptions extends Omit { + /** Defines routes that should be static generated. Accepts wildcard behavior. */ + include: string[]; + /** + * Defines routes that should not be static generated. Accepts wildcard behavior. `exclude` always + * take priority over `include`. + */ + exclude?: string[]; + + /** + * The URL `origin`, which is a combination of the scheme (protocol) and hostname (domain). For + * example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`. However, the + * `origin` does not include a `pathname`. + * + * The `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate + * a complete URL rather than just the `pathname`. For example, in order to render a correct + * canonical tag URL or URLs within the `sitemap.xml`, the `origin` must be provided too. + * + * If the site also starts with a pathname other than `/`, please use the `basePathname` option in + * the Qwik City config options. + */ + origin?: string; +} + +/** @public */ +export const STATIC_PATHS_ID = '@qwik-city-static-paths'; + +/** @public */ +export const RESOLVED_STATIC_PATHS_ID = `${STATIC_PATHS_ID}.js`; + +/** @public */ +export const NOT_FOUND_PATHS_ID = '@qwik-city-not-found-paths'; + +/** @public */ +export const RESOLVED_NOT_FOUND_PATHS_ID = `${NOT_FOUND_PATHS_ID}.js`; \ No newline at end of file diff --git a/adapters/vercel-serverless/types.ts b/adapters/vercel-serverless/types.ts new file mode 100644 index 0000000..c869fca --- /dev/null +++ b/adapters/vercel-serverless/types.ts @@ -0,0 +1,148 @@ +export interface BuildContext { + rootDir: string; + opts: NormalizedPluginOptions; + routes: BuildRoute[]; + serverPlugins: BuildServerPlugin[]; + layouts: BuildLayout[]; + entries: BuildEntry[]; + serviceWorkers: BuildEntry[]; + menus: BuildMenu[]; + frontmatter: Map; + diagnostics: Diagnostic[]; + target: 'ssr' | 'client' | undefined; + isDevServer: boolean; + isDevServerClientOnly: boolean; + isDirty: boolean; + activeBuild: Promise | null; +} + +export type Yaml = string | number | boolean | null | { [attrName: string]: Yaml } | Yaml[]; + +export interface FrontmatterAttrs { + [attrName: string]: Yaml; +} + +export interface Diagnostic { + type: 'error' | 'warn'; + message: string; +} + +export interface RouteSourceFile extends RouteSourceFileName { + dirPath: string; + dirName: string; + filePath: string; + fileName: string; +} + +export interface RouteSourceFileName { + type: RouteSourceType; + /** Filename without the extension */ + extlessName: string; + /** Just the extension */ + ext: string; +} + +export type RouteSourceType = 'route' | 'layout' | 'entry' | 'menu' | 'service-worker'; + +export interface BuildRoute extends ParsedPathname { + /** Unique id built from its relative file system path */ + id: string; + /** Local file system path */ + filePath: string; + ext: string; + /** URL Pathname */ + pathname: string; + layouts: BuildLayout[]; +} + +export interface BuildServerPlugin { + /** Unique id built from its relative file system path */ + id: string; + /** Local file system path */ + filePath: string; + ext: string; +} + +export interface ParsedPathname { + routeName: string; + pattern: RegExp; // TODO(misko): duplicate information from `routeName` refactor to normalize + paramNames: string[]; // TODO(misko): duplicate information from `routeName` refactor to normalizehttps://github.com/QwikDev/qwik/pull/4954 + segments: PathnameSegment[]; +} + +export type PathnameSegment = PathnameSegmentPart[]; + +export interface PathnameSegmentPart { + content: string; + dynamic: boolean; + rest: boolean; +} + +export interface BuildLayout { + filePath: string; + dirPath: string; + id: string; + layoutType: 'top' | 'nested'; + layoutName: string; +} + +export interface BuildEntry extends ParsedPathname { + id: string; + chunkFileName: string; + filePath: string; +} + +export interface BuildMenu { + pathname: string; + filePath: string; +} + +export interface ParsedMenuItem { + text: string; + href?: string; + items?: ParsedMenuItem[]; +} + +/** @public */ +export interface RewriteRouteOption { + prefix?: string; + paths: Record; +} + +/** @public */ +export interface PluginOptions { + /** Directory of the `routes`. Defaults to `src/routes`. */ + routesDir?: string; + /** Directory of the `server plugins`. Defaults to `src/server-plugins`. */ + serverPluginsDir?: string; + /** + * The base pathname is used to create absolute URL paths up to the `hostname`, and must always + * start and end with a `/`. Defaults to `/`. + */ + basePathname?: string; + /** + * Ensure a trailing slash ends page urls. Defaults to `true`. (Note: Previous versions defaulted + * to `false`). + */ + trailingSlash?: boolean; + /** Enable or disable MDX plugins included by default in qwik-city. */ + mdxPlugins?: MdxPlugins; + /** MDX Options https://mdxjs.com/ */ + mdx?: any; + /** The platform object which can be used to mock the Cloudflare bindings. */ + platform?: Record; + /** Configuration to rewrite url paths */ + rewriteRoutes?: RewriteRouteOption[]; +} + +export interface MdxPlugins { + remarkGfm: boolean; + rehypeSyntaxHighlight: boolean; + rehypeAutolinkHeadings: boolean; +} + +export interface NormalizedPluginOptions extends Required { } + +export interface MarkdownAttributes { + [name: string]: string; +} \ No newline at end of file diff --git a/adapters/vercel-serverless/vite.config.ts b/adapters/vercel-serverless/vite.config.ts index 2445dd9..b06afb3 100644 --- a/adapters/vercel-serverless/vite.config.ts +++ b/adapters/vercel-serverless/vite.config.ts @@ -1,4 +1,7 @@ -import { vercelServerlessAdapter } from "@builder.io/qwik-city/adapters/vercel-serverless/vite"; +import { + vercelServerlessAdapter, + FUNCTION_DIRECTORY, +} from "./middleware"; import { extendConfig } from "@builder.io/qwik-city/vite"; import baseConfig from "../../vite.config"; @@ -9,7 +12,7 @@ export default extendConfig(baseConfig, () => { rollupOptions: { input: ["custom-src/entry.vercel-serverless.tsx", "@qwik-city-plan"], }, - outDir: ".vercel/output/functions/_qwik-city.func", + outDir: `.vercel/output/functions/${FUNCTION_DIRECTORY}.func`, minify: false, }, plugins: [ @@ -22,4 +25,4 @@ export default extendConfig(baseConfig, () => { }), ], }; -}); +}); \ No newline at end of file diff --git a/custom-src/entry.vercel-serverless.tsx b/custom-src/entry.vercel-serverless.tsx index 1b1dd31..0a5cac7 100644 --- a/custom-src/entry.vercel-serverless.tsx +++ b/custom-src/entry.vercel-serverless.tsx @@ -1,21 +1,17 @@ -import { createQwikCity } from "@builder.io/qwik-city/middleware/node"; +import { + createQwikCity, + type PlatformVercelServerless, +} from "../adapters/vercel-serverless/index"; import qwikCityPlan from "@qwik-city-plan"; import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; -import type { VercelRequest, VercelResponse } from "@vercel/node"; -const { router, notFound, staticFile } = createQwikCity({ +declare global { + interface QwikCityPlatform extends PlatformVercelServerless {} +} + +export default createQwikCity({ render, qwikCityPlan, manifest, -}); - -export default async function handler(req: VercelRequest, res: VercelResponse) { - staticFile(req, res, () => { - router(req, res, () => { - notFound(req, res, () => { - res.status(400); - }); - }); - }); -} +}); \ No newline at end of file From 0c3f36d7c58b3710f587d735a99a79d7e64ccbeb Mon Sep 17 00:00:00 2001 From: Okiki Date: Fri, 7 Jun 2024 05:52:52 +0000 Subject: [PATCH 2/3] chore: test on vercel Signed-off-by: Okiki --- playwright.vercel-serverless.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.vercel-serverless.config.ts b/playwright.vercel-serverless.config.ts index 849745c..b54fdd7 100644 --- a/playwright.vercel-serverless.config.ts +++ b/playwright.vercel-serverless.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...commonConfig, metadata: { - server: "vercel-serverless", + server: "vercel-serverless", }, use: { From 56a2793e5d7df6afb0fcaf49b2b022de637ad827 Mon Sep 17 00:00:00 2001 From: Okiki Date: Fri, 7 Jun 2024 05:56:53 +0000 Subject: [PATCH 3/3] fix: issue with static generation Signed-off-by: Okiki --- adapters/vercel-serverless/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/vercel-serverless/shared.ts b/adapters/vercel-serverless/shared.ts index 60f8ee5..c2eab69 100644 --- a/adapters/vercel-serverless/shared.ts +++ b/adapters/vercel-serverless/shared.ts @@ -143,7 +143,7 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) { ssgOrigin = `https://yoursite.qwik.dev`; } - const staticGenerate = await import('../../../static'); + const staticGenerate = await import('@builder.io/qwik-city/static'); const generateOpts: StaticGenerateOptions = { maxWorkers: opts.maxWorkers, basePathname,