diff --git a/packages/qwik-city/adaptors/cloudflare-pages/api.md b/packages/qwik-city/adaptors/cloudflare-pages/api.md index 0f68ca8d28c..1f37d28588c 100644 --- a/packages/qwik-city/adaptors/cloudflare-pages/api.md +++ b/packages/qwik-city/adaptors/cloudflare-pages/api.md @@ -4,9 +4,7 @@ ```ts -import type { QwikManifest } from '@builder.io/qwik/optimizer'; -import type { SymbolMapper } from '@builder.io/qwik/optimizer'; -import type { SymbolMapperFn } from '@builder.io/qwik/optimizer'; +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; // @alpha (undocumented) export function cloudflarePagesAdaptor(opts?: CloudflarePagesAdaptorOptions): any; @@ -17,19 +15,7 @@ export interface CloudflarePagesAdaptorOptions { staticGenerate?: StaticGenerateRenderOptions | true; } -// Warning: (ae-forgotten-export) The symbol "RenderOptions" needs to be exported by the entry point index.d.ts -// -// @alpha (undocumented) -export interface StaticGenerateRenderOptions extends RenderOptions { - emitData?: boolean; - emitHtml?: boolean; - log?: 'debug'; - maxTasksPerWorker?: number; - maxWorkers?: number; - origin: string; - outDir: string; - sitemapOutFile?: string; -} +export { StaticGenerateRenderOptions } // (No @packageDocumentation comment for this package) diff --git a/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts b/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts index 5659c20d375..303be3b461d 100644 --- a/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts +++ b/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts @@ -1,172 +1,84 @@ -import type { Plugin } from 'vite'; -import type { PluginContext } from 'rollup'; -import type { QwikCityPlugin } from '@builder.io/qwik-city/vite'; -import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; -import type { StaticGenerateOptions, StaticGenerateRenderOptions } from '../../../static'; -import { join } from 'node:path'; +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; +import { viteAdaptor } from '../../shared/vite'; import fs from 'node:fs'; +import { join } from 'node:path'; /** * @alpha */ export function cloudflarePagesAdaptor(opts: CloudflarePagesAdaptorOptions = {}): any { - 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; - - async function generateBundles(ctx: PluginContext) { - const qwikVitePluginApi = qwikVitePlugin!.api; - const clientOutDir = qwikVitePluginApi.getClientOutDir()!; - const files = await fs.promises.readdir(clientOutDir, { withFileTypes: true }); - const exclude = files - .map((file) => { - if (file.name.startsWith('.')) { - return null; - } - if (file.isDirectory()) { - return `/${file.name}/*`; - } else if (file.isFile()) { - return `/${file.name}`; - } - return null; - }) - .filter(isNotNullable); - const include: string[] = ['/*']; - - const serverPackageJsonPath = join(serverOutDir!, 'package.json'); - const serverPackageJsonCode = `{"type":"module"}`; - await fs.promises.mkdir(serverOutDir!, { recursive: true }); - await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); - - if (opts.staticGenerate && renderModulePath && qwikCityPlanModulePath) { - const staticGenerate = await import('../../../static'); - let generateOpts: StaticGenerateOptions = { - outDir: clientOutDir, - origin: process?.env?.CF_PAGES_URL || 'https://your.cloudflare.pages.dev', - renderModulePath: renderModulePath, - qwikCityPlanModulePath: qwikCityPlanModulePath, - basePathname: qwikCityPlugin!.api.getBasePathname(), - }; - - if (typeof opts.staticGenerate === 'object') { - generateOpts = { - ...generateOpts, - ...opts.staticGenerate, - }; - } - const results = await staticGenerate.generate(generateOpts); - results.staticPaths.sort(); - results.staticPaths.sort((a, b) => { - return a.length - b.length; - }); - if (results.errors > 0) { - ctx.error('Error while runnning SSG. At least one path failed to render.'); - } - exclude.push(...results.staticPaths); - } - - const hasRoutesJson = exclude.includes('/_routes.json'); - if (!hasRoutesJson && opts.functionRoutes !== false) { - const routesJsonPath = join(clientOutDir, '_routes.json'); - const total = include.length + exclude.length; - const maxRules = 100; - if (total > maxRules) { - const toRemove = total - maxRules; - const removed = exclude.splice(-toRemove, toRemove); - ctx.warn( - `Cloudflare pages does not support more than 100 static rules. Qwik SSG generated ${total}, the following rules were excluded: ${JSON.stringify( - removed, - undefined, - 2 - )}` - ); - ctx.warn('Create an configure a "_routes.json" manually in the public.'); - } - - const routesJson = { - version: 1, - include, - exclude, - }; - await fs.promises.writeFile(routesJsonPath, JSON.stringify(routesJson, undefined, 2)); - } - } - - const plugin: Plugin = { - name: 'vite-plugin-qwik-city-cloudflare-pages', - enforce: 'post', - apply: 'build', + return viteAdaptor({ + name: 'cloudflare-pages', + origin: process?.env?.CF_PAGES_URL || 'https://your.cloudflare.pages.dev', + staticGenerate: opts.staticGenerate, config() { return { + ssr: { + target: 'webworker', + noExternal: true, + }, build: { + ssr: true, rollupOptions: { output: { + format: 'es', hoistTransitiveImports: false, }, }, }, - ssr: { - target: 'webworker', - }, + publicDir: false, }; }, - configResolved({ build, plugins }) { - qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; - if (!qwikCityPlugin) { - throw new Error('Missing vite-plugin-qwik-city'); - } - qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; - if (!qwikVitePlugin) { - throw new Error('Missing vite-plugin-qwik'); - } - serverOutDir = build.outDir; - - if (build?.ssr !== true) { - throw new Error( - '"build.ssr" must be set to `true` in order to use the Cloudflare Pages adaptor.' - ); - } - - if (!build?.rollupOptions?.input) { - throw new Error( - '"build.rollupOptions.input" must be set in order to use the Cloudflare Pages adaptor.' - ); - } - }, - - generateBundle(_, bundles) { - for (const fileName in bundles) { - const chunk = bundles[fileName]; - if (chunk.type === 'chunk' && chunk.isEntry) { - if (chunk.name === 'entry.ssr') { - renderModulePath = join(serverOutDir!, fileName); - } else if (chunk.name === '@qwik-city-plan') { - qwikCityPlanModulePath = join(serverOutDir!, fileName); + async generateRoutes({ clientOutDir, staticPaths, warn }) { + const clientFiles = await fs.promises.readdir(clientOutDir, { withFileTypes: true }); + const exclude = clientFiles + .map((f) => { + if (f.name.startsWith('.')) { + return null; + } + if (f.isDirectory()) { + return `/${f.name}/*`; + } else if (f.isFile()) { + return `/${f.name}`; } + return null; + }) + .filter(isNotNullable); + const include: string[] = ['/*']; + + const hasRoutesJson = exclude.includes('/_routes.json'); + if (!hasRoutesJson && opts.functionRoutes !== false) { + staticPaths.sort(); + staticPaths.sort((a, b) => a.length - b.length); + exclude.push(...staticPaths); + + const routesJsonPath = join(clientOutDir, '_routes.json'); + const total = include.length + exclude.length; + const maxRules = 100; + if (total > maxRules) { + const toRemove = total - maxRules; + const removed = exclude.splice(-toRemove, toRemove); + warn( + `Cloudflare Pages does not support more than 100 static rules. Qwik SSG generated ${total}, the following rules were excluded: ${JSON.stringify( + removed, + undefined, + 2 + )}` + ); + warn('Please manually create a routes config in the "public/_routes.json" directory.'); } - } - 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"?' - ); + const routesJson = { + version: 1, + include, + exclude, + }; + await fs.promises.writeFile(routesJsonPath, JSON.stringify(routesJson, undefined, 2)); } }, - - async closeBundle() { - await generateBundles(this); - }, - }; - return plugin; + }); } /** @@ -187,7 +99,7 @@ export interface CloudflarePagesAdaptorOptions { staticGenerate?: StaticGenerateRenderOptions | true; } -export type { StaticGenerateRenderOptions } from '../../../static'; +export type { StaticGenerateRenderOptions }; const isNotNullable = (v: T): v is NonNullable => { return v != null; diff --git a/packages/qwik-city/adaptors/express/api.md b/packages/qwik-city/adaptors/express/api.md index 52325d33494..2547c91847a 100644 --- a/packages/qwik-city/adaptors/express/api.md +++ b/packages/qwik-city/adaptors/express/api.md @@ -9,10 +9,10 @@ import type { SymbolMapper } from '@builder.io/qwik/optimizer'; import type { SymbolMapperFn } from '@builder.io/qwik/optimizer'; // @alpha (undocumented) -export function expressAdaptor(opts?: NetlifyEdgeAdaptorOptions): any; +export function expressAdaptor(opts?: ExpressAdaptorOptions): any; // @alpha (undocumented) -export interface NetlifyEdgeAdaptorOptions { +export interface ExpressAdaptorOptions { // Warning: (ae-forgotten-export) The symbol "StaticGenerateRenderOptions" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/packages/qwik-city/adaptors/express/vite/index.ts b/packages/qwik-city/adaptors/express/vite/index.ts index 1f555ce37cd..092bd6d6077 100644 --- a/packages/qwik-city/adaptors/express/vite/index.ts +++ b/packages/qwik-city/adaptors/express/vite/index.ts @@ -1,112 +1,29 @@ -import type { Plugin } from 'vite'; -import type { QwikCityPlugin } from '@builder.io/qwik-city/vite'; -import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; -import type { StaticGenerateOptions, StaticGenerateRenderOptions } from '../../../static'; -import { join } from 'node:path'; -import fs from 'node:fs'; +import type { StaticGenerateRenderOptions } from '../../../static'; +import { viteAdaptor } from '../../shared/vite'; /** * @alpha */ -export function expressAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { - 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; - - async function generateBundles() { - const qwikVitePluginApi = qwikVitePlugin!.api; - const clientOutDir = qwikVitePluginApi.getClientOutDir()!; - - const serverPackageJsonPath = join(serverOutDir!, 'package.json'); - const serverPackageJsonCode = `{"type":"module"}`; - await fs.promises.mkdir(serverOutDir!, { recursive: true }); - await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); - - if (opts.staticGenerate) { - const staticGenerate = await import('../../../static'); - let generateOpts: StaticGenerateOptions = { - outDir: clientOutDir, - origin: process?.env?.URL || 'https://yoursitename.example.com', - renderModulePath: renderModulePath!, - qwikCityPlanModulePath: qwikCityPlanModulePath!, - basePathname: qwikCityPlugin!.api.getBasePathname(), +export function expressAdaptor(opts: ExpressAdaptorOptions = {}): any { + return viteAdaptor({ + name: 'express', + origin: process?.env?.URL || 'https://yoursitename.qwik.builder.io', + staticGenerate: opts.staticGenerate, + + config() { + return { + build: { + ssr: true, + }, + publicDir: false, }; - - if (typeof opts.staticGenerate === 'object') { - generateOpts = { - ...generateOpts, - ...opts.staticGenerate, - }; - } - - await staticGenerate.generate(generateOpts); - } - } - - const plugin: Plugin = { - name: 'vite-plugin-qwik-city-express', - enforce: 'post', - apply: 'build', - - configResolved({ build, plugins }) { - qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; - if (!qwikCityPlugin) { - throw new Error('Missing vite-plugin-qwik-city'); - } - qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; - if (!qwikVitePlugin) { - throw new Error('Missing vite-plugin-qwik'); - } - - serverOutDir = build.outDir; - - if (build?.ssr !== true) { - throw new Error('"build.ssr" must be set to `true` in order to use the Express adaptor.'); - } - - if (!build?.rollupOptions?.input) { - throw new Error( - '"build.rollupOptions.input" must be set in order to use the Express adaptor.' - ); - } - }, - - generateBundle(_, bundles) { - for (const fileName in bundles) { - const chunk = bundles[fileName]; - if (chunk.type === 'chunk' && chunk.isEntry) { - 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"?' - ); - } - }, - - async closeBundle() { - await generateBundles(); }, - }; - return plugin; + }); } /** * @alpha */ -export interface NetlifyEdgeAdaptorOptions { +export interface ExpressAdaptorOptions { staticGenerate?: StaticGenerateRenderOptions | true; } diff --git a/packages/qwik-city/adaptors/netlify-edge/api.md b/packages/qwik-city/adaptors/netlify-edge/api.md index 1b6995cd46b..9891dac14bf 100644 --- a/packages/qwik-city/adaptors/netlify-edge/api.md +++ b/packages/qwik-city/adaptors/netlify-edge/api.md @@ -4,9 +4,7 @@ ```ts -import type { QwikManifest } from '@builder.io/qwik/optimizer'; -import type { SymbolMapper } from '@builder.io/qwik/optimizer'; -import type { SymbolMapperFn } from '@builder.io/qwik/optimizer'; +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; // @alpha (undocumented) export function netifyEdgeAdaptor(opts?: NetlifyEdgeAdaptorOptions): any; @@ -17,19 +15,7 @@ export interface NetlifyEdgeAdaptorOptions { staticGenerate?: StaticGenerateRenderOptions | true; } -// Warning: (ae-forgotten-export) The symbol "RenderOptions" needs to be exported by the entry point index.d.ts -// -// @alpha (undocumented) -export interface StaticGenerateRenderOptions extends RenderOptions { - emitData?: boolean; - emitHtml?: boolean; - log?: 'debug'; - maxTasksPerWorker?: number; - maxWorkers?: number; - origin: string; - outDir: string; - sitemapOutFile?: string; -} +export { StaticGenerateRenderOptions } // (No @packageDocumentation comment for this package) diff --git a/packages/qwik-city/adaptors/netlify-edge/vite/index.ts b/packages/qwik-city/adaptors/netlify-edge/vite/index.ts index 5f317b136a3..508542ed010 100644 --- a/packages/qwik-city/adaptors/netlify-edge/vite/index.ts +++ b/packages/qwik-city/adaptors/netlify-edge/vite/index.ts @@ -1,77 +1,19 @@ -import type { Plugin } from 'vite'; -import type { BuildRoute } from '../../../buildtime/types'; -import type { QwikCityPlugin } from '@builder.io/qwik-city/vite'; -import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; -import type { PluginContext } from 'rollup'; -import type { StaticGenerateOptions, StaticGenerateRenderOptions } from '../../../static'; -import { basename, dirname, join, resolve } from 'node:path'; +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; +import { getParentDir, viteAdaptor } from '../../shared/vite'; import fs from 'node:fs'; +import { join } from 'node:path'; /** * @alpha */ export function netifyEdgeAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { - 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; - - async function generateBundles(ctx: PluginContext) { - const qwikVitePluginApi = qwikVitePlugin!.api; - const clientOutDir = qwikVitePluginApi.getClientOutDir()!; - - // create server package.json to ensure mjs is used - const serverPackageJsonPath = join(serverOutDir!, 'package.json'); - const serverPackageJsonCode = `{"type":"module"}`; - await fs.promises.mkdir(serverOutDir!, { recursive: true }); - await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); - - const staticPaths: string[] = []; - if (opts.staticGenerate && renderModulePath && qwikCityPlanModulePath) { - const staticGenerate = await import('../../../static'); - let generateOpts: StaticGenerateOptions = { - outDir: clientOutDir, - origin: process?.env?.URL || 'https://yoursitename.netlify.app', - renderModulePath: renderModulePath, - qwikCityPlanModulePath: qwikCityPlanModulePath, - basePathname: qwikCityPlugin!.api.getBasePathname(), - }; - - if (typeof opts.staticGenerate === 'object') { - generateOpts = { - ...generateOpts, - ...opts.staticGenerate, - }; - } - - const result = await staticGenerate.generate(generateOpts); - if (result.errors > 0) { - ctx.error('Error while runnning SSG. At least one path failed to render.'); - } - - staticPaths.push(...result.staticPaths); - } - - if (opts.functionRoutes !== false) { - // create the netlify edge function manifest - // https://docs.netlify.com/edge-functions/create-integration/#generate-declarations - const routes = qwikCityPlugin!.api.getRoutes(); - const netlifyManifest = generateNetlifyEdgeManifest(routes, staticPaths); - const edgeFnsDir = getEdgeFunctionsDir(serverOutDir!); - const netlifyManifestPath = join(edgeFnsDir, 'manifest.json'); - await fs.promises.writeFile(netlifyManifestPath, JSON.stringify(netlifyManifest, null, 2)); - } - } - - const plugin: Plugin = { - name: 'vite-plugin-qwik-city-netlify-edge', - enforce: 'post', - apply: 'build', + return viteAdaptor({ + name: 'netlify-edge', + origin: process?.env?.URL || 'https://yoursitename.netlify.app', + staticGenerate: opts.staticGenerate, config(config) { const outDir = config.build?.outDir || '.netlify/edge-functions/entry.netlify-edge'; - return { ssr: { target: 'webworker', @@ -83,6 +25,7 @@ export function netifyEdgeAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { rollupOptions: { output: { format: 'es', + hoistTransitiveImports: false, }, }, }, @@ -90,116 +33,36 @@ export function netifyEdgeAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { }; }, - configResolved({ build, plugins }) { - qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; - if (!qwikCityPlugin) { - throw new Error('Missing vite-plugin-qwik-city'); - } - qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; - if (!qwikVitePlugin) { - throw new Error('Missing vite-plugin-qwik'); - } - serverOutDir = build.outDir; - - if (build?.ssr !== true) { - throw new Error( - '"build.ssr" must be set to `true` in order to use the Netlify Edge adaptor.' - ); - } - - if (!build?.rollupOptions?.input) { - throw new Error( - '"build.rollupOptions.input" must be set in order to use the Netlify Edge adaptor.' - ); - } - }, - - generateBundle(_, bundles) { - for (const fileName in bundles) { - const chunk = bundles[fileName]; - if (chunk.type === 'chunk' && chunk.isEntry) { - if (chunk.name === 'entry.ssr') { - renderModulePath = join(serverOutDir!, fileName); - } else if (chunk.name === '@qwik-city-plan') { - qwikCityPlanModulePath = join(serverOutDir!, fileName); - } - } - } + async generateRoutes({ serverOutDir, routes, staticPaths }) { + if (opts.functionRoutes !== false) { + const ssrRoutes = routes.filter((r) => !staticPaths.includes(r.pathname)); + + // https://docs.netlify.com/edge-functions/create-integration/#generate-declarations + const netlifyEdgeManifest = { + functions: ssrRoutes.map((r) => { + if (r.paramNames.length > 0) { + return { + pattern: r.pattern.toString(), + function: 'entry.netlify-edge', + }; + } + + return { + path: r.pathname, + function: 'entry.netlify-edge', + }; + }), + version: 1, + }; - 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"?' + const netlifyEdgeFnsDir = getParentDir(serverOutDir, 'edge-functions'); + await fs.promises.writeFile( + join(netlifyEdgeFnsDir, 'manifest.json'), + JSON.stringify(netlifyEdgeManifest, null, 2) ); } }, - - async closeBundle() { - await generateBundles(this); - }, - }; - return plugin; -} - -function getEdgeFunctionsDir(serverOutDir: string) { - const root = resolve('/'); - let dir = serverOutDir; - for (let i = 0; i < 20; i++) { - dir = dirname(dir); - if (basename(dir) === 'edge-functions') { - return dir; - } - if (dir === root) { - break; - } - } - throw new Error(`Unable to find edge functions dir from: ${serverOutDir}`); -} - -function generateNetlifyEdgeManifest(routes: BuildRoute[], staticPaths: string[]) { - const ssrRoutes = routes.filter((r) => !staticPaths.includes(r.pathname)); - - // https://docs.netlify.com/edge-functions/create-integration/#generate-declarations - const m: NetlifyEdgeManifest = { - functions: ssrRoutes.map((r) => { - if (r.paramNames.length > 0) { - return { - pattern: r.pattern.toString(), - function: 'entry.netlify-edge', - }; - } - - return { - path: r.pathname, - function: 'entry.netlify-edge', - }; - }), - version: 1, - }; - - return m; -} - -interface NetlifyEdgeManifest { - functions: (NetlifyEdgePathFunction | NetlifyEdgePatternFunction)[]; - import_map?: string; - version: 1; -} - -interface NetlifyEdgePathFunction { - path: string; - function: string; - name?: string; -} - -interface NetlifyEdgePatternFunction { - pattern: string; - function: string; - name?: string; + }); } /** @@ -220,4 +83,4 @@ export interface NetlifyEdgeAdaptorOptions { staticGenerate?: StaticGenerateRenderOptions | true; } -export type { StaticGenerateRenderOptions } from '../../../static'; +export type { StaticGenerateRenderOptions }; diff --git a/packages/qwik-city/adaptors/shared/vite/index.ts b/packages/qwik-city/adaptors/shared/vite/index.ts new file mode 100644 index 00000000000..2245d100754 --- /dev/null +++ b/packages/qwik-city/adaptors/shared/vite/index.ts @@ -0,0 +1,177 @@ +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, + StaticGenerateResult, +} from '@builder.io/qwik-city/static'; +import fs from 'node:fs'; +import { basename, dirname, join, resolve } from 'node:path'; +import type { BuildRoute } from 'packages/qwik-city/buildtime/types'; + +export function viteAdaptor(opts: ViteAdaptorPluginOptions) { + 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; + + const plugin: Plugin = { + name: `vite-plugin-qwik-city-${opts.name}`, + enforce: 'post', + apply: 'build', + + config(config) { + if (typeof opts.config === 'function') { + return opts.config(config); + } + }, + + configResolved({ build, plugins }) { + isSsrBuild = !!build.ssr; + + if (isSsrBuild) { + qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; + if (!qwikCityPlugin) { + throw new Error('Missing vite-plugin-qwik-city'); + } + qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; + if (!qwikVitePlugin) { + throw new Error('Missing vite-plugin-qwik'); + } + serverOutDir = build.outDir; + + if (build?.ssr !== true) { + throw new Error( + `"build.ssr" must be set to "true" in order to use the "${opts.name}" adaptor.` + ); + } + + if (!build?.rollupOptions?.input) { + throw new Error( + `"build.rollupOptions.input" must be set in order to use the "${opts.name}" adaptor.` + ); + } + } + }, + + generateBundle(_, bundles) { + if (isSsrBuild) { + for (const fileName in bundles) { + const chunk = bundles[fileName]; + if (chunk.type === 'chunk' && chunk.isEntry) { + 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"?' + ); + } + } + }, + + async closeBundle() { + if (isSsrBuild && serverOutDir && qwikCityPlugin?.api && qwikVitePlugin?.api) { + // create server package.json to ensure mjs is used + const serverPackageJsonPath = join(serverOutDir, 'package.json'); + const serverPackageJsonCode = `{"type":"module"}`; + await fs.promises.mkdir(serverOutDir, { recursive: true }); + await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); + + let staticGenerateResult: StaticGenerateResult | null = null; + if (opts.staticGenerate && renderModulePath && qwikCityPlanModulePath) { + let origin = opts.origin; + if (!origin) { + origin = `https://yoursite.qwik.builder.io`; + } + if ( + origin.length > 0 && + !origin.startsWith('https://') && + !origin.startsWith('http://') + ) { + origin = `https://${origin}`; + } + + const staticGenerate = await import('../../../static'); + let generateOpts: StaticGenerateOptions = { + basePathname: qwikCityPlugin.api.getBasePathname(), + outDir: qwikVitePlugin.api.getClientOutDir()!, + origin, + renderModulePath, + qwikCityPlanModulePath, + }; + + if (opts.staticGenerate && typeof opts.staticGenerate === 'object') { + generateOpts = { + ...generateOpts, + ...opts.staticGenerate, + }; + } + + staticGenerateResult = await staticGenerate.generate(generateOpts); + if (staticGenerateResult.errors > 0) { + this.error( + `Error while runnning SSG from "${opts.name}" adaptor. At least one path failed to render.` + ); + } + } + + if (typeof opts.generateRoutes === 'function') { + await opts.generateRoutes({ + serverOutDir, + clientOutDir: qwikVitePlugin.api.getClientOutDir()!, + routes: qwikCityPlugin.api.getRoutes(), + staticPaths: staticGenerateResult?.staticPaths ?? [], + warn: (message) => this.warn(message), + error: (message) => this.error(message), + }); + } + } + }, + }; + + return plugin; +} + +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}"`); +} + +interface ViteAdaptorPluginOptions { + name: string; + origin: string; + staticGenerate: true | StaticGenerateRenderOptions | undefined; + config?: (config: UserConfig) => UserConfig; + generateRoutes?: (generateOpts: { + clientOutDir: string; + serverOutDir: string; + routes: BuildRoute[]; + staticPaths: string[]; + warn: (message: string) => void; + error: (message: string) => void; + }) => Promise; +} diff --git a/packages/qwik-city/adaptors/static/vite/index.ts b/packages/qwik-city/adaptors/static/vite/index.ts index e7f9bd19e4c..368cb76fe22 100644 --- a/packages/qwik-city/adaptors/static/vite/index.ts +++ b/packages/qwik-city/adaptors/static/vite/index.ts @@ -1,100 +1,15 @@ -import type { Plugin } from 'vite'; -import type { QwikCityPlugin } from '@builder.io/qwik-city/vite'; -import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; -import type { StaticGenerateOptions, StaticGenerateRenderOptions } from '../../../static'; -import { join } from 'node:path'; -import fs from 'node:fs'; +import type { StaticGenerateRenderOptions } from '../../../static'; +import { viteAdaptor } from '../../shared/vite'; /** * @alpha */ export function staticAdaptor(opts: StaticGenerateAdaptorOptions): any { - let qwikCityPlugin: QwikCityPlugin | null = null; - let qwikVitePlugin: QwikVitePlugin | null = null; - let serverOutDir: string | null = null; - let ssrOutputPath: string | null = null; - let qwikCityPlanOutputPath: string | null = null; - - async function generateBundles() { - const qwikVitePluginApi = qwikVitePlugin!.api; - const clientOutDir = qwikVitePluginApi.getClientOutDir()!; - - const serverPackageJsonPath = join(serverOutDir!, 'package.json'); - const serverPackageJsonCode = `{"type":"module"}`; - await fs.promises.mkdir(serverOutDir!, { recursive: true }); - await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); - - const staticGenerate = await import('../../../static'); - - const generateOpts: StaticGenerateOptions = { - outDir: clientOutDir, - renderModulePath: ssrOutputPath!, - qwikCityPlanModulePath: qwikCityPlanOutputPath!, - basePathname: qwikCityPlugin!.api.getBasePathname(), - ...opts, - }; - - await staticGenerate.generate(generateOpts); - } - - const plugin: Plugin = { - name: 'vite-plugin-qwik-city-static-generate', - enforce: 'post', - apply: 'build', - - configResolved({ build, plugins }) { - qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; - if (!qwikCityPlugin) { - throw new Error('Missing vite-plugin-qwik-city'); - } - qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; - if (!qwikVitePlugin) { - throw new Error('Missing vite-plugin-qwik'); - } - serverOutDir = build.outDir; - - if (build?.ssr !== true) { - throw new Error( - '"build.ssr" must be set to `true` in order to use the Static Generate adaptor.' - ); - } - - if (!build?.rollupOptions?.input) { - throw new Error( - '"build.rollupOptions.input" must be set in order to use the Static Generate adaptor.' - ); - } - }, - - generateBundle(_, bundles) { - for (const fileName in bundles) { - const chunk = bundles[fileName]; - if (chunk.type === 'chunk' && chunk.isEntry) { - if (chunk.name === 'entry.ssr') { - ssrOutputPath = join(serverOutDir!, fileName); - } else if (chunk.name === '@qwik-city-plan') { - qwikCityPlanOutputPath = join(serverOutDir!, fileName); - } - } - } - - if (!ssrOutputPath) { - throw new Error( - 'Unable to find "entry.ssr" entry point. Did you forget to add it to "build.rollupOptions.input"?' - ); - } - if (!qwikCityPlanOutputPath) { - throw new Error( - 'Unable to find "@qwik-city-plan" entry point. Did you forget to add it to "build.rollupOptions.input"?' - ); - } - }, - - async closeBundle() { - await generateBundles(); - }, - }; - return plugin; + return viteAdaptor({ + name: 'static-generate', + staticGenerate: true, + ...opts, + }); } /** diff --git a/packages/qwik-city/adaptors/vercel-edge/api.md b/packages/qwik-city/adaptors/vercel-edge/api.md new file mode 100644 index 00000000000..8ef44419ff2 --- /dev/null +++ b/packages/qwik-city/adaptors/vercel-edge/api.md @@ -0,0 +1,24 @@ +## API Report File for "@builder.io/qwik-city" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; + +export { StaticGenerateRenderOptions } + +// @alpha (undocumented) +export function vercelEdgeAdaptor(opts?: VercelEdgeAdaptorOptions): any; + +// @alpha (undocumented) +export interface VercelEdgeAdaptorOptions { + outputConfig?: boolean; + staticGenerate?: StaticGenerateRenderOptions | true; + vcConfigEntryPoint?: string; + vcConfigEnvVarsInUse?: string[]; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/qwik-city/adaptors/vercel-edge/vite/api-extractor.json b/packages/qwik-city/adaptors/vercel-edge/vite/api-extractor.json new file mode 100644 index 00000000000..97a38405b0b --- /dev/null +++ b/packages/qwik-city/adaptors/vercel-edge/vite/api-extractor.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../../api-extractor.json", + "mainEntryPointFilePath": "/dist-dev/dts-out/packages/qwik-city/adaptors/vercel-edge/vite/index.d.ts", + "apiReport": { + "enabled": true, + "reportFileName": "api.md", + "reportFolder": "/packages/qwik-city/adaptors/vercel-edge/", + "reportTempFolder": "/dist-dev/api-extractor/qwik-city/adaptors/vercel-edge" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/packages/qwik-city/lib/adaptors/vercel-edge/vite/index.d.ts" + } +} diff --git a/packages/qwik-city/adaptors/vercel-edge/vite/index.ts b/packages/qwik-city/adaptors/vercel-edge/vite/index.ts new file mode 100644 index 00000000000..d45302d2a25 --- /dev/null +++ b/packages/qwik-city/adaptors/vercel-edge/vite/index.ts @@ -0,0 +1,112 @@ +import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; +import { getParentDir, viteAdaptor } from '../../shared/vite'; +import fs from 'node:fs'; +import { join } from 'node:path'; + +/** + * @alpha + */ +export function vercelEdgeAdaptor(opts: VercelEdgeAdaptorOptions = {}): any { + return viteAdaptor({ + name: 'vercel-edge', + origin: process?.env?.VERCEL_URL || 'https://yoursitename.vercel.app', + staticGenerate: opts.staticGenerate, + + config(config) { + const outDir = config.build?.outDir || '.vercel/output/functions/_qwik-city.func'; + return { + ssr: { + target: 'webworker', + noExternal: true, + }, + build: { + ssr: true, + outDir, + rollupOptions: { + output: { + format: 'es', + hoistTransitiveImports: false, + }, + }, + }, + publicDir: false, + }; + }, + + async generateRoutes({ clientOutDir, serverOutDir, routes, staticPaths }) { + const vercelOutputDir = getParentDir(serverOutDir, 'output'); + + if (opts.outputConfig !== false) { + const ssrRoutes = routes.filter((r) => !staticPaths.includes(r.pathname)); + + // https://vercel.com/docs/build-output-api/v3#features/edge-middleware + const vercelOutputConfig = { + routes: ssrRoutes.map((r) => { + let src = r.pattern.toString().slice(1, -2).replace(/\\\//g, '/'); + if (src === '^/') { + src = '^/?'; + } + return { + src, + middlewarePath: '_qwik-city', + }; + }), + version: 3, + }; + + await fs.promises.writeFile( + join(vercelOutputDir, 'config.json'), + JSON.stringify(vercelOutputConfig, null, 2) + ); + } + + const vcConfigPath = join(serverOutDir, '.vc-config.json'); + const vcConfig = { + runtime: 'edge', + entrypoint: opts.vcConfigEntryPoint || 'entry.vercel-edge.js', + envVarsInUse: opts.vcConfigEnvVarsInUse, + }; + await fs.promises.writeFile(vcConfigPath, JSON.stringify(vcConfig, null, 2)); + + const staticDir = join(vercelOutputDir, 'static'); + + if (fs.existsSync(staticDir)) { + await fs.promises.rm(staticDir, { recursive: true }); + } + + await fs.promises.rename(clientOutDir, staticDir); + }, + }); +} + +/** + * @alpha + */ +export interface VercelEdgeAdaptorOptions { + /** + * 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; + /** + * The `envVarsInUse` property in the `.vc-config.json` file. + * List of environment variable names that will be available for the Edge Function to utilize. + * + * Defaults to `undefined`. + */ + vcConfigEnvVarsInUse?: string[]; + /** + * Determines if the adaptor should also run Static Site Generation (SSG). + */ + staticGenerate?: StaticGenerateRenderOptions | true; +} + +export type { StaticGenerateRenderOptions }; diff --git a/packages/qwik-city/middleware/cloudflare-pages/api.md b/packages/qwik-city/middleware/cloudflare-pages/api.md index d9a1b1e09b2..9cf6784e27a 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/api.md +++ b/packages/qwik-city/middleware/cloudflare-pages/api.md @@ -7,7 +7,7 @@ import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik/server'; import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik'; -import type { RequestHandler as RequestHandler_2 } from '~qwik-city-runtime'; +import type { RequestHandler as RequestHandler_2 } from '@builder.io/qwik-city'; // @alpha (undocumented) export function createQwikCity(opts: QwikCityCloudflarePagesOptions): ({ request, env, waitUntil }: EventPluginContext) => Promise; diff --git a/packages/qwik-city/middleware/cloudflare-pages/index.ts b/packages/qwik-city/middleware/cloudflare-pages/index.ts index 5be397f7f37..9b3f3441c8f 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/index.ts +++ b/packages/qwik-city/middleware/cloudflare-pages/index.ts @@ -2,8 +2,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request- import { notFoundHandler, requestHandler } from '../request-handler'; import type { RenderOptions } from '@builder.io/qwik'; import type { Render } from '@builder.io/qwik/server'; +import type { RequestHandler } from '@builder.io/qwik-city'; import qwikCityPlan from '@qwik-city-plan'; -import type { RequestHandler } from '~qwik-city-runtime'; // @builder.io/qwik-city/middleware/cloudflare-pages diff --git a/packages/qwik-city/middleware/netlify-edge/api.md b/packages/qwik-city/middleware/netlify-edge/api.md index f541950b92e..c90f97179c8 100644 --- a/packages/qwik-city/middleware/netlify-edge/api.md +++ b/packages/qwik-city/middleware/netlify-edge/api.md @@ -8,7 +8,7 @@ import type { Context } from '@netlify/edge-functions'; import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik/server'; import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik'; -import type { RequestHandler as RequestHandler_2 } from '~qwik-city-runtime'; +import type { RequestHandler as RequestHandler_2 } from '@builder.io/qwik-city'; // @alpha (undocumented) export function createQwikCity(opts: QwikCityNetlifyOptions): (request: Request, context: Context) => Promise; diff --git a/packages/qwik-city/middleware/netlify-edge/index.ts b/packages/qwik-city/middleware/netlify-edge/index.ts index 0c20cb1a87b..02bce34f334 100644 --- a/packages/qwik-city/middleware/netlify-edge/index.ts +++ b/packages/qwik-city/middleware/netlify-edge/index.ts @@ -3,8 +3,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request- import { notFoundHandler, requestHandler } from '../request-handler'; import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik'; +import type { RequestHandler } from '@builder.io/qwik-city'; import qwikCityPlan from '@qwik-city-plan'; -import type { RequestHandler } from '~qwik-city-runtime'; // @builder.io/qwik-city/middleware/netlify-edge diff --git a/packages/qwik-city/middleware/vercel-edge/api-extractor.json b/packages/qwik-city/middleware/vercel-edge/api-extractor.json new file mode 100644 index 00000000000..3cfb6144929 --- /dev/null +++ b/packages/qwik-city/middleware/vercel-edge/api-extractor.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "/dist-dev/dts-out/packages/qwik-city/middleware/vercel-edge/index.d.ts", + "apiReport": { + "enabled": true, + "reportFileName": "api.md", + "reportFolder": "/packages/qwik-city/middleware/vercel-edge/", + "reportTempFolder": "/dist-dev/api-extractor/qwik-city/middleware/vercel-edge" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/packages/qwik-city/lib/middleware/vercel-edge/index.d.ts" + } +} diff --git a/packages/qwik-city/middleware/vercel-edge/api.md b/packages/qwik-city/middleware/vercel-edge/api.md new file mode 100644 index 00000000000..e56ad97e466 --- /dev/null +++ b/packages/qwik-city/middleware/vercel-edge/api.md @@ -0,0 +1,21 @@ +## API Report File for "@builder.io/qwik-city" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Render } from '@builder.io/qwik/server'; +import type { RenderOptions } from '@builder.io/qwik/server'; + +// @alpha (undocumented) +export function createQwikCity(opts: QwikCityVercelEdgeOptions): (request: Request) => Promise; + +// Warning: (ae-forgotten-export) The symbol "QwikCityHandlerOptions" needs to be exported by the entry point index.d.ts +// +// @alpha (undocumented) +export interface QwikCityVercelEdgeOptions extends QwikCityHandlerOptions { +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/qwik-city/middleware/vercel-edge/index.ts b/packages/qwik-city/middleware/vercel-edge/index.ts new file mode 100644 index 00000000000..28c9d7c0813 --- /dev/null +++ b/packages/qwik-city/middleware/vercel-edge/index.ts @@ -0,0 +1,76 @@ +import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request-handler/types'; +import { notFoundHandler, requestHandler } from '../request-handler'; + +// @builder.io/qwik-city/middleware/vercel-edge + +/** + * @alpha + */ +export function createQwikCity(opts: QwikCityVercelEdgeOptions) { + async function onRequest(request: Request) { + try { + const url = new URL(request.url); + + const requestCtx: QwikCityRequestContext = { + locale: undefined, + url, + request, + response: (status, headers, body) => { + return new Promise((resolve) => { + let flushedHeaders = false; + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + + const response = new Response(readable, { status, headers }); + + body({ + write: (chunk) => { + if (!flushedHeaders) { + flushedHeaders = true; + resolve(response); + } + if (typeof chunk === 'string') { + const encoder = new TextEncoder(); + writer.write(encoder.encode(chunk)); + } else { + writer.write(chunk); + } + }, + }).finally(() => { + if (!flushedHeaders) { + flushedHeaders = true; + resolve(response); + } + writer.close(); + }); + }); + }, + platform: process.env, + }; + + // send request to qwik city request handler + const handledResponse = await requestHandler('server', requestCtx, opts); + if (handledResponse) { + return handledResponse; + } + + // qwik city did not have a route for this request + // respond with qwik city's 404 handler + const notFoundResponse = await notFoundHandler(requestCtx); + return notFoundResponse; + } catch (e: any) { + console.error(e); + return new Response(String(e || 'Error'), { + status: 500, + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); + } + } + + return onRequest; +} + +/** + * @alpha + */ +export interface QwikCityVercelEdgeOptions extends QwikCityHandlerOptions {} diff --git a/packages/qwik-city/package.json b/packages/qwik-city/package.json index 521217127e7..8ac1bd8afb8 100644 --- a/packages/qwik-city/package.json +++ b/packages/qwik-city/package.json @@ -28,6 +28,10 @@ "import": "./lib/adaptors/static/vite/index.mjs", "require": "./lib/adaptors/static/vite/index.cjs" }, + "./adaptors/vercel-edge/vite": { + "import": "./lib/adaptors/vercel-edge/vite/index.mjs", + "require": "./lib/adaptors/vercel-edge/vite/index.cjs" + }, "./middleware/cloudflare-pages": { "import": "./lib/middleware/cloudflare-pages/index.mjs" }, diff --git a/packages/qwik-city/static/main-thread.ts b/packages/qwik-city/static/main-thread.ts index 9ba7221a103..6044b4fbca0 100644 --- a/packages/qwik-city/static/main-thread.ts +++ b/packages/qwik-city/static/main-thread.ts @@ -205,7 +205,9 @@ function validateOptions(opts: StaticGenerateOptions) { } siteOrigin = siteOrigin.trim(); if (!siteOrigin.startsWith('https://') && !siteOrigin.startsWith('http://')) { - throw new Error(`"origin" must start with a valid protocol, such as "https://" or "http://"`); + throw new Error( + `"origin" must start with a valid protocol, such as "https://" or "http://", received "${siteOrigin}"` + ); } try { new URL(siteOrigin); diff --git a/scripts/api.ts b/scripts/api.ts index 17b1cc4ea53..f955fd47dd6 100644 --- a/scripts/api.ts +++ b/scripts/api.ts @@ -92,6 +92,11 @@ export function apiExtractor(config: BuildConfig) { join(config.packagesDir, 'qwik-city', 'adaptors', 'static', 'vite'), join(config.packagesDir, 'qwik-city', 'lib', 'adaptors', 'static', 'vite', 'index.d.ts') ); + createTypesApi( + config, + join(config.packagesDir, 'qwik-city', 'adaptors', 'vercel-edge', 'vite'), + join(config.packagesDir, 'qwik-city', 'lib', 'adaptors', 'vercel-edge', 'vite', 'index.d.ts') + ); createTypesApi( config, join(config.packagesDir, 'qwik-city', 'middleware', 'cloudflare-pages'), @@ -112,6 +117,11 @@ export function apiExtractor(config: BuildConfig) { join(config.packagesDir, 'qwik-city', 'static'), join(config.packagesDir, 'qwik-city', 'lib', 'static', 'index.d.ts') ); + createTypesApi( + config, + join(config.packagesDir, 'qwik-city', 'middleware', 'vercel-edge'), + join(config.packagesDir, 'qwik-city', 'lib', 'middleware', 'vercel-edge', 'index.d.ts') + ); generateQwikCityReferenceModules(config); console.log('🥶', 'submodule d.ts API files generated'); diff --git a/scripts/qwik-city.ts b/scripts/qwik-city.ts index 4fd631bba3f..a20fec88d85 100644 --- a/scripts/qwik-city.ts +++ b/scripts/qwik-city.ts @@ -28,9 +28,11 @@ export async function buildQwikCity(config: BuildConfig) { buildAdaptorExpressVite(config, inputDir, outputDir), buildAdaptorNetlifyEdgeVite(config, inputDir, outputDir), buildAdaptorStaticVite(config, inputDir, outputDir), + buildAdaptorVercelEdgeVite(config, inputDir, outputDir), buildMiddlewareCloudflarePages(config, inputDir, outputDir), buildMiddlewareNetlifyEdge(config, inputDir, outputDir), buildMiddlewareNode(config, inputDir, outputDir), + buildMiddlewareVercelEdge(config, inputDir, outputDir), buildStatic(config, inputDir, outputDir), buildStaticNode(config, inputDir, outputDir), buildStaticDeno(config, inputDir, outputDir), @@ -70,6 +72,11 @@ export async function buildQwikCity(config: BuildConfig) { import: './adaptors/static/vite/index.mjs', require: './adaptors/static/vite/index.cjs', }, + './adaptors/vercel-edge/vite': { + types: './adaptors/vercel-edge/vite/index.d.ts', + import: './adaptors/vercel-edge/vite/index.mjs', + require: './adaptors/vercel-edge/vite/index.cjs', + }, './middleware/cloudflare-pages': { types: './middleware/cloudflare-pages/index.d.ts', import: './middleware/cloudflare-pages/index.mjs', @@ -83,6 +90,10 @@ export async function buildQwikCity(config: BuildConfig) { import: './middleware/node/index.mjs', require: './middleware/node/index.cjs', }, + './middleware/vercel-edge': { + types: './middleware/vercel-edge/index.d.ts', + import: './middleware/vercel-edge/index.mjs', + }, './static': { types: './static/index.d.ts', import: './static/index.mjs', @@ -358,6 +369,40 @@ async function buildAdaptorStaticVite(config: BuildConfig, inputDir: string, out }); } +async function buildAdaptorVercelEdgeVite( + config: BuildConfig, + inputDir: string, + outputDir: string +) { + const entryPoints = [join(inputDir, 'adaptors', 'vercel-edge', 'vite', 'index.ts')]; + + const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; + + await build({ + entryPoints, + outfile: join(outputDir, 'adaptors', 'vercel-edge', 'vite', 'index.mjs'), + bundle: true, + platform: 'node', + target: nodeTarget, + format: 'esm', + watch: watcher(config), + external, + plugins: [importPath(/static$/, '../../../static/index.mjs')], + }); + + await build({ + entryPoints, + outfile: join(outputDir, 'adaptors', 'vercel-edge', 'vite', 'index.cjs'), + bundle: true, + platform: 'node', + target: nodeTarget, + format: 'cjs', + watch: watcher(config), + external, + plugins: [importPath(/static$/, '../../../static/index.cjs')], + }); +} + async function buildMiddlewareCloudflarePages( config: BuildConfig, inputDir: string, @@ -379,6 +424,27 @@ async function buildMiddlewareCloudflarePages( }); } +async function buildMiddlewareNetlifyEdge( + config: BuildConfig, + inputDir: string, + outputDir: string +) { + const entryPoints = [join(inputDir, 'middleware', 'netlify-edge', 'index.ts')]; + + const external = ['@qwik-city-plan']; + + await build({ + entryPoints, + outfile: join(outputDir, 'middleware', 'netlify-edge', 'index.mjs'), + bundle: true, + platform: 'node', + target: nodeTarget, + format: 'esm', + watch: watcher(config), + external, + }); +} + async function buildMiddlewareNode(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'middleware', 'node', 'index.ts')]; @@ -407,24 +473,17 @@ async function buildMiddlewareNode(config: BuildConfig, inputDir: string, output }); } -async function buildMiddlewareNetlifyEdge( - config: BuildConfig, - inputDir: string, - outputDir: string -) { - const entryPoints = [join(inputDir, 'middleware', 'netlify-edge', 'index.ts')]; - - const external = ['@qwik-city-plan']; +async function buildMiddlewareVercelEdge(config: BuildConfig, inputDir: string, outputDir: string) { + const entryPoints = [join(inputDir, 'middleware', 'vercel-edge', 'index.ts')]; await build({ entryPoints, - outfile: join(outputDir, 'middleware', 'netlify-edge', 'index.mjs'), + outfile: join(outputDir, 'middleware', 'vercel-edge', 'index.mjs'), bundle: true, platform: 'node', target: nodeTarget, format: 'esm', watch: watcher(config), - external, }); } diff --git a/starters/adaptors/cloudflare-pages/package.json b/starters/adaptors/cloudflare-pages/package.json index 77a2bbe62f2..19588959c0f 100644 --- a/starters/adaptors/cloudflare-pages/package.json +++ b/starters/adaptors/cloudflare-pages/package.json @@ -9,7 +9,7 @@ }, "__qwik__": { "priority": 40, - "displayName": "Adaptor: Cloudflare Pages (serverless)", + "displayName": "Adaptor: Cloudflare Pages", "docs": [ "https://qwik.builder.io/qwikcity/adaptors/cloudflare-pages/", "https://developers.cloudflare.com/pages" diff --git a/starters/adaptors/express/package.json b/starters/adaptors/express/package.json index 7f5c4b0aa37..54bc6e78d0d 100644 --- a/starters/adaptors/express/package.json +++ b/starters/adaptors/express/package.json @@ -12,7 +12,7 @@ }, "__qwik__": { "priority": 20, - "displayName": "Adaptor: Express (server)", + "displayName": "Adaptor: Nodejs Express Server", "docs": [ "https://qwik.builder.io/qwikcity/adaptors/node/", "https://expressjs.com/" diff --git a/starters/adaptors/netlify-edge/README.md b/starters/adaptors/netlify-edge/README.md index 5db5bc1a7f3..a70beff7093 100644 --- a/starters/adaptors/netlify-edge/README.md +++ b/starters/adaptors/netlify-edge/README.md @@ -1,13 +1,13 @@ ## Netlify -This starter site is configured to deploy to [Netlify Edge Functions](https://www.netlify.com/products/edge/), which means it will be rendered at an edge location near to your users. +This starter site is configured to deploy to [Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/), which means it will be rendered at an edge location near to your users. ### Local development The [Netlify CLI](https://docs.netlify.com/cli/get-started/) can be used to preview a production build locally. To do so: First build your site, then to start a local server, run: -1. install Netlify CLI globally `npm i -g netlify-cli` -2. Build your site both ssr and client `npm run build`. +1. Install Netlify CLI globally `npm i -g netlify-cli`. +2. Build your site with both ssr and static `npm run build`. 3. Start a local server with `npm run serve`. In this project, `npm run serve` uses the `netlify dev` command to spin up a server that can handle Netlify's Edge Functions locally. 4. Visit [http://localhost:8888/](http://localhost:8888/) to check out your site. diff --git a/starters/adaptors/netlify-edge/package.json b/starters/adaptors/netlify-edge/package.json index 3a4a28f73d2..ce2747c6472 100644 --- a/starters/adaptors/netlify-edge/package.json +++ b/starters/adaptors/netlify-edge/package.json @@ -9,9 +9,10 @@ }, "__qwik__": { "priority": 30, - "displayName": "Adaptor: Netlify (serverless)", + "displayName": "Adaptor: Netlify Edge", "docs": [ "https://qwik.builder.io/qwikcity/adaptors/netlify-edge/", + "https://docs.netlify.com/edge-functions/overview/", "https://docs.netlify.com/" ] } diff --git a/starters/adaptors/vercel-edge/README.md b/starters/adaptors/vercel-edge/README.md new file mode 100644 index 00000000000..83d4ebec13d --- /dev/null +++ b/starters/adaptors/vercel-edge/README.md @@ -0,0 +1,7 @@ +## Vercel Edge + +This starter site is configured to deploy to [Vercel Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions), which means it will be rendered at an edge location near to your users. + +### Deployments + +You can [deploy your site to Vercel](https://vercel.com/docs/concepts/deployments/overview) either via a Git provider integration or through the Vercel CLI. diff --git a/starters/adaptors/vercel-edge/adaptors/netlify-edge/vite.config.ts b/starters/adaptors/vercel-edge/adaptors/netlify-edge/vite.config.ts new file mode 100644 index 00000000000..10cb647f833 --- /dev/null +++ b/starters/adaptors/vercel-edge/adaptors/netlify-edge/vite.config.ts @@ -0,0 +1,20 @@ +import { vercelEdgeAdaptor } from '@builder.io/qwik-city/adaptors/vercel-edge/vite'; +import { extendConfig } from '@builder.io/qwik-city/vite'; +import baseConfig from '../../vite.config'; + +export default extendConfig(baseConfig, () => { + return { + build: { + ssr: true, + rollupOptions: { + input: ['src/entry.vercel-edge.tsx', '@qwik-city-plan'], + }, + outDir: '.vercel/output/functions/_qwik-city.func', + }, + plugins: [ + vercelEdgeAdaptor({ + staticGenerate: true, + }), + ], + }; +}); diff --git a/starters/adaptors/vercel-edge/gitignore b/starters/adaptors/vercel-edge/gitignore new file mode 100644 index 00000000000..a96cc86b858 --- /dev/null +++ b/starters/adaptors/vercel-edge/gitignore @@ -0,0 +1,2 @@ +# Vercel +.vercel diff --git a/starters/adaptors/vercel-edge/package.json b/starters/adaptors/vercel-edge/package.json new file mode 100644 index 00000000000..f7ea9eb6bb7 --- /dev/null +++ b/starters/adaptors/vercel-edge/package.json @@ -0,0 +1,19 @@ +{ + "description": "Vercel Edge Functions", + "scripts": { + "build.server": "vite build -c adaptors/vercel-edge/vite.config.ts", + "serve": "vercel deploy" + }, + "devDependencies": { + "vercel": "^28.4.17" + }, + "__qwik__": { + "priority": 30, + "displayName": "Adaptor: Vercel Edge", + "docs": [ + "https://qwik.builder.io/qwikcity/adaptors/vercel-edge/", + "https://vercel.com/docs/concepts/functions/edge-functions", + "https://vercel.com/docs" + ] + } +} diff --git a/starters/adaptors/vercel-edge/src/entry.vercel-edge.tsx b/starters/adaptors/vercel-edge/src/entry.vercel-edge.tsx new file mode 100644 index 00000000000..92c2ac149a5 --- /dev/null +++ b/starters/adaptors/vercel-edge/src/entry.vercel-edge.tsx @@ -0,0 +1,5 @@ +import { createQwikCity } from '@builder.io/qwik-city/middleware/vercel-edge'; +import qwikCityPlan from '@qwik-city-plan'; +import render from './entry.ssr'; + +export default createQwikCity({ render, qwikCityPlan }); diff --git a/starters/adaptors/vercel-edge/vercel.json b/starters/adaptors/vercel-edge/vercel.json new file mode 100644 index 00000000000..9e9a00bbc6b --- /dev/null +++ b/starters/adaptors/vercel-edge/vercel.json @@ -0,0 +1,13 @@ +{ + "headers": [ + { + "source": "/build/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=31536000, s-maxage=31536000, immutable" + } + ] + } + ] +}