From 54aae5673429170cbfa3375106b664803d907956 Mon Sep 17 00:00:00 2001 From: Shishant Biswas Date: Fri, 3 Apr 2026 13:48:50 +0530 Subject: [PATCH] add: agnostic web compliant adapter --- app/build.ts | 5 + app/e2e/inc/adapters.ts | 3 + app/package.json | 6 + app/src/adapter/web/index.ts | 1 + app/src/adapter/web/web.adapter.spec.ts | 103 +++++++ app/src/adapter/web/web.adapter.ts | 68 +++++ .../integration/(frameworks)/meta.json | 1 + .../integration/(frameworks)/web.mdx | 277 ++++++++++++++++++ .../integration/introduction.mdx | 6 + docs/content/docs/(documentation)/start.mdx | 6 + examples/qwik/.gitignore | 45 +++ examples/qwik/.prettierignore | 37 +++ examples/qwik/.vscode/launch.json | 24 ++ examples/qwik/.vscode/qwik-city.code-snippets | 36 +++ examples/qwik/.vscode/qwik.code-snippets | 78 +++++ examples/qwik/README.md | 65 ++++ examples/qwik/bknd.config.ts | 60 ++++ examples/qwik/eslint.config.js | 73 +++++ examples/qwik/package.json | 47 +++ examples/qwik/public/bknd.ico | Bin 0 -> 15086 bytes examples/qwik/public/bknd.svg | 14 + examples/qwik/public/favicon.svg | 1 + examples/qwik/public/file.svg | 1 + examples/qwik/public/globe.svg | 1 + examples/qwik/public/manifest.json | 9 + examples/qwik/public/robots.txt | 0 examples/qwik/public/window.svg | 1 + examples/qwik/qwik.env.d.ts | 4 + examples/qwik/src/components/footer.tsx | 36 +++ examples/qwik/src/components/list.tsx | 17 ++ examples/qwik/src/entry.dev.tsx | 17 ++ examples/qwik/src/entry.preview.tsx | 21 ++ examples/qwik/src/entry.ssr.tsx | 31 ++ examples/qwik/src/global.css | 23 ++ examples/qwik/src/lib/bknd.ts | 13 + examples/qwik/src/root.tsx | 77 +++++ examples/qwik/src/routes/index.tsx | 132 +++++++++ examples/qwik/src/routes/plugin@bknd.ts | 45 +++ examples/qwik/src/routes/user/index.tsx | 95 ++++++ examples/qwik/tsconfig.json | 26 ++ examples/qwik/vite.config.ts | 29 ++ 41 files changed, 1534 insertions(+) create mode 100644 app/src/adapter/web/index.ts create mode 100644 app/src/adapter/web/web.adapter.spec.ts create mode 100644 app/src/adapter/web/web.adapter.ts create mode 100644 docs/content/docs/(documentation)/integration/(frameworks)/web.mdx create mode 100644 examples/qwik/.gitignore create mode 100644 examples/qwik/.prettierignore create mode 100644 examples/qwik/.vscode/launch.json create mode 100644 examples/qwik/.vscode/qwik-city.code-snippets create mode 100644 examples/qwik/.vscode/qwik.code-snippets create mode 100644 examples/qwik/README.md create mode 100644 examples/qwik/bknd.config.ts create mode 100644 examples/qwik/eslint.config.js create mode 100644 examples/qwik/package.json create mode 100644 examples/qwik/public/bknd.ico create mode 100644 examples/qwik/public/bknd.svg create mode 100644 examples/qwik/public/favicon.svg create mode 100644 examples/qwik/public/file.svg create mode 100644 examples/qwik/public/globe.svg create mode 100644 examples/qwik/public/manifest.json create mode 100644 examples/qwik/public/robots.txt create mode 100644 examples/qwik/public/window.svg create mode 100644 examples/qwik/qwik.env.d.ts create mode 100644 examples/qwik/src/components/footer.tsx create mode 100644 examples/qwik/src/components/list.tsx create mode 100644 examples/qwik/src/entry.dev.tsx create mode 100644 examples/qwik/src/entry.preview.tsx create mode 100644 examples/qwik/src/entry.ssr.tsx create mode 100644 examples/qwik/src/global.css create mode 100644 examples/qwik/src/lib/bknd.ts create mode 100644 examples/qwik/src/root.tsx create mode 100644 examples/qwik/src/routes/index.tsx create mode 100644 examples/qwik/src/routes/plugin@bknd.ts create mode 100644 examples/qwik/src/routes/user/index.tsx create mode 100644 examples/qwik/tsconfig.json create mode 100644 examples/qwik/vite.config.ts diff --git a/app/build.ts b/app/build.ts index 92f63356..c5f3c182 100644 --- a/app/build.ts +++ b/app/build.ts @@ -330,6 +330,11 @@ async function buildAdapters() { platform: "node", }), + tsup.build({ + ...baseConfig("web"), + platform: "neutral", + }), + tsup.build({ ...baseConfig("sveltekit"), platform: "node", diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts index 66d691bb..0e6540d7 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -15,6 +15,9 @@ const configs = { nextjs: { base_path: "/admin", }, + meta: { + base_path: "/admin", + }, nuxt: { base_path: "/admin", }, diff --git a/app/package.json b/app/package.json index e9428f06..a79edb0e 100644 --- a/app/package.json +++ b/app/package.json @@ -233,6 +233,11 @@ "import": "./dist/adapter/nextjs/index.js", "require": "./dist/adapter/nextjs/index.js" }, + "./adapter/web": { + "types": "./dist/types/adapter/web/index.d.ts", + "import": "./dist/adapter/web/index.js", + "require": "./dist/adapter/web/index.js" + }, "./adapter/nuxt": { "types": "./dist/types/adapter/nuxt/index.d.ts", "import": "./dist/adapter/nuxt/index.js", @@ -292,6 +297,7 @@ "adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"], "adapter/vite": ["./dist/types/adapter/vite/index.d.ts"], "adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"], + "adapter/web": ["./dist/types/adapter/web/index.d.ts"], "adapter/nuxt": ["./dist/types/adapter/nuxt/index.d.ts"], "adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"], "adapter/bun": ["./dist/types/adapter/bun/index.d.ts"], diff --git a/app/src/adapter/web/index.ts b/app/src/adapter/web/index.ts new file mode 100644 index 00000000..c536188d --- /dev/null +++ b/app/src/adapter/web/index.ts @@ -0,0 +1 @@ +export * from "./web.adapter"; diff --git a/app/src/adapter/web/web.adapter.spec.ts b/app/src/adapter/web/web.adapter.spec.ts new file mode 100644 index 00000000..69a50a95 --- /dev/null +++ b/app/src/adapter/web/web.adapter.spec.ts @@ -0,0 +1,103 @@ +import { afterAll, beforeAll, describe, test, expect } from "bun:test"; +import { createBknd } from "./web.adapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; +import { adapterTestSuite } from "adapter/adapter-test-suite"; +import { bunTestRunner } from "adapter/bun/test"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +describe("web adapter via createBknd", () => { + adapterTestSuite(bunTestRunner, { + makeApp: (options, args) => createBknd({ mode: "api", options }, args).getApp(), + makeHandler: (options, args) => createBknd({ mode: "api", options: options ?? {} }, args).serve(), + }); + + // ------------------------ MODE API ------------------------ + test("caches app instance", async () => { + const bknd = createBknd({ mode: "api", options: { connection: { url: ":memory:" } } }); + const app1 = await bknd.getApp(); + const app2 = await bknd.getApp(); + expect(app1).toBe(app2); + }); + + test("getApi returns api", async () => { + const bknd = createBknd({ mode: "api", options: { connection: { url: ":memory:" } } }); + const api = await bknd.getApi(); + expect(api).toBeDefined(); + }); + + test("uses createFrameworkApp ", async () => { + const bknd = createBknd({ mode: "api", options: { connection: { url: ":memory:" } } }); + const app = await bknd.getApp(); + expect(app).toBeDefined(); + expect(app.isBuilt()).toBe(true); + }); + + test("serve returns a fetch handler", async () => { + const bknd = createBknd({ mode: "api", options: { connection: { url: ":memory:" } } }); + const handler = bknd.serve(); + const res = await handler(new Request("http://localhost:3000/api/system/config")); + expect(res.status).toBe(200); + }); +}); + + +// ------------------------ MODE STANDALONE ------------------------ +describe("web adapter via createBknd in standalone mode", () => { + adapterTestSuite(bunTestRunner, { + makeApp: (options, args) => createBknd({ mode: "standalone", options }, args).getApp(), + makeHandler: (options, args) => createBknd({ mode: "standalone", options: options ?? {} }, args).serve(), + }); + + test("caches app instance", async () => { + const bknd = createBknd({ mode: "standalone", options: { connection: { url: ":memory:" } } }); + const app1 = await bknd.getApp(); + const app2 = await bknd.getApp(); + expect(app1).toBe(app2); + }); + + test("getApi returns api", async () => { + const bknd = createBknd({ mode: "standalone", options: { connection: { url: ":memory:" } } }); + const api = await bknd.getApi(); + expect(api).toBeDefined(); + }); + + test("uses createRuntimeApp", async () => { + const bknd = createBknd({ + mode: "standalone", + options: { + connection: { url: ":memory:" }, + adminOptions: { adminBasepath: "/admin" }, + } + }); + const app = await bknd.getApp(); + expect(app).toBeDefined(); + expect(app.isBuilt()).toBe(true); + }); + + test("serve returns a fetch handler", async () => { + const bknd = createBknd({ + mode: "standalone", + options: { + connection: { url: ":memory:" }, + adminOptions: { adminBasepath: "/admin" }, + } + }); + const app = await bknd.getApp(); + expect(app.isBuilt()).toBe(true); + }); + + test("check admin route", async () => { + const bknd = createBknd({ + mode: "standalone", + options: { + connection: { url: ":memory:" }, + adminOptions: { adminBasepath: "/admin" }, + } + }); + const handler = bknd.serve(); + const res = await handler(new Request("http://localhost:3000/admin")); + expect(res.status).toBe(200); + }); +}); \ No newline at end of file diff --git a/app/src/adapter/web/web.adapter.ts b/app/src/adapter/web/web.adapter.ts new file mode 100644 index 00000000..5dc18971 --- /dev/null +++ b/app/src/adapter/web/web.adapter.ts @@ -0,0 +1,68 @@ +import { + createFrameworkApp, + createRuntimeApp, + type FrameworkBkndConfig, + type RuntimeBkndConfig, +} from "bknd/adapter"; +import { $console } from "core/utils"; +import type { App } from "App"; + +export type AdapterModeWithOptions> = + | { + mode: "standalone"; + options: RuntimeBkndConfig; + } + | { + mode: "api"; + options: FrameworkBkndConfig; + }; + +export function createBknd(config: AdapterModeWithOptions, env?: Env) { + let appPromise: Promise | undefined; + + const { mode, options } = config; + + async function getApp(): Promise { + if (!appPromise) { + if (mode === "standalone") { + if (!options.serveStatic && !options.adminOptions) { + $console.warn( + "adminOptions provided without serveStatic — admin UI assets may not be served. " + + "See `serveStatic`, `serveStaticViaImport`, or add a `package.json` script that runs `bknd copy-assets --out {relative_static_assets_directory_path}`.", + ); + } + appPromise = createRuntimeApp(options, env); + } else { + appPromise = createFrameworkApp(options, env); + } + } + return appPromise; + } + + async function getApi(opts?: { headers?: Headers; verify?: boolean }) { + const app = await getApp(); + if (opts?.verify) { + const api = app.getApi({ headers: opts.headers }); + await api.verifyAuth(); + return api; + } + return app.getApi(); + } + + function serve() { + return async (req: Request) => { + const app = await getApp(); + return app.fetch(req); + }; + } + + return { getApp, getApi, serve }; +} + +/** Utility type to determine the config type based on mode, + * Usage `Config<"standalone">` or `Config<"api">` + */ +export type Config = Extract< + Parameters[0], + { mode: T } +>['options']; \ No newline at end of file diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 39b253cd..fc6ca7fc 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -5,6 +5,7 @@ "astro", "sveltekit", "tanstack-start", + "web", "vite", "nuxt" ] diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/web.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/web.mdx new file mode 100644 index 00000000..95f9198c --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/web.mdx @@ -0,0 +1,277 @@ +--- +title: "Web Adapter" +description: "Bring bknd to any web framework or runtime" +tags: ["documentation"] +--- + +## What is the Web Adapter? + +The web adapter (`bknd/adapter/web`) is a framework/runtime agnostic adapter which can integrate `bknd` into **any** web framework or runtime — even ones without a dedicated adapter. + +**Use the web adapter when:** +- Your framework doesn't have a dedicated bknd adapter +- You want full control over your server setup +- You're building a custom server or edge function + +**Use a platform-specific adapter when:** +- You're using a supported framework (Next.js, SvelteKit, Nuxt, Astro, etc.) +- You prefer opinionated, zero-config setup + +## Installation + + + +```bash tab="npm" +npm install bknd +``` + +```bash tab="pnpm" +pnpm install bknd +``` + +```bash tab="yarn" +yarn add bknd +``` + +```bash tab="bun" +bun add bknd +``` + + + +## Basic Setup + +Start by creating a config file: + +```typescript title="bknd.config.ts" +import type { Config } from "bknd/adapter/web"; + +export default { + connection: { + url: "file:data.db", + }, +} satisfies Config<"api">; +``` + +The `Config` type is a helper type which maps the correct config type based on mode. + +Use `Config<"api">` when you are using React-based framework otherwise use `Config<"standalone">` to serve both the Admin UI and bknd api using the same handler + + +### Helper Singleton +```typescript title="lib/bknd.ts" +import { createBknd } from "bknd/adapter/web"; +import config from "../bknd.config"; + +export const bknd = createBknd({ mode: "standalone", options: config }, process.env); + // ^ use "api" if not using React based framework +``` + +`createBknd` returns three methods: + +| Method | Returns | Description | +|--------|---------|-------------| +| `getApp(env: Env)` | `Promise` | The built bknd app instance | +| `getApi({ headers, verify })` | `Promise` | Convenience wrapper around `app.getApi()` | +| `serve()` | `(req: Request) => Promise` | A fetch handler for your server | + +## Choosing an Admin UI Path + +The web adapter supports two approaches to serving the admin UI: + +| Path | Best For | How | +|------|----------|-----| +| **Client-side** (default) | React frameworks: Next.js, Astro, React Router, Tanstack Start, Waku | Import and render `` in your own React route | +| **Server-side** | Non-React frameworks: SvelteKit, Nuxt, , Bun, Node, Deno | The server serves the Admin UI assets and API | + +## Path 1: Client-Side Admin (Default) + +For React-capable frameworks, render the admin UI directly in a route: + +```tsx title="app/admin/[[...admin]]/page.tsx" +import { Admin } from "bknd/ui"; +import "bknd/dist/styles.css"; +import { bknd } from "@/bknd"; + +export default async function AdminPage() { + const api = await bknd.getApi({ verify: true }); + + return ( + + ); +} +``` + +No server-side admin configuration needed. The admin UI runs entirely in the browser. + +## Path 2: Server-Side Admin + +For non-React frameworks or standalone servers, enable the admin controller: + +```typescript title="src/bknd.ts" +import { createBknd } from "bknd/adapter/web"; +import config from "../bknd.config"; + +export const bknd = createBknd({ mode: "standalone", options: config }, env); + // ^ use "api" if not using React based framework +``` + +You'll also need to serve the admin's static assets (JS, CSS). Choose one of three strategies: + +### Strategy 1: serveStatic Middleware + +**Best for:** Bun, Node, standalone servers with filesystem access + +Use Hono's platform-specific `serveStatic` in `bknd.config.ts` to serve assets from `node_modules/bknd/dist/static/`: + +```typescript title="bknd.config.ts" +import type { Config } from "bknd/adapter/web"; +import { serveStatic } from "hono/bun"; // or "@hono/node-server/serve-static" + +export default { + connection: { + url: "file:data.db", + serveStatic: serveStatic({ root: "./node_modules/bknd/dist/static" }), + adminOptions: { + adminBasepath: "/admin", + }, + }, +} satisfies Config<"standalone">; +``` + +### Strategy 2: copy-assets Postinstall + +**Best for:** SvelteKit, Nuxt, any framework with a static directory + +Copy assets at install time and let your framework serve them: + +```json title="package.json" +{ + "scripts": { + "postinstall": "bknd copy-assets --out static" + } +} +``` + +Per-framework output paths: + +| Framework | `--out` flag | +|-----------|-------------| +| SvelteKit | `--out static` | +| Nuxt | `--out public` | +| Next.js | `--out public` | +| Astro | `--out public` | +| Qwik | `--out public` | + +### Strategy 3: serveStaticViaImport + +**Best for:** Edge/serverless runtimes (Deno Deploy, Cloudflare Workers) + +For environments without filesystem access: + +```typescript title="bknd.config.ts" +import type { Config } from "bknd/adapter/web"; +import { serveStaticViaImport } from "bknd/adapter"; + +export default { + connection: { + url: "file:data.db", + serveStatic: serveStaticViaImport(), + adminOptions: { + adminBasepath: "/admin", + }, + }, +} satisfies Config<"standalone">; + +``` + +## Serving Requests + +Exmaple use `bknd.serve()` as a fetch handler in your server: + +```typescript title="server.ts" +import { bknd } from "./bknd"; + +// Bun +Bun.serve({ fetch: bknd.serve(), port: 3000 }); + +// Node (with @hono/node-server) +import { serve } from "@hono/node-server"; +serve({ fetch: bknd.serve(), port: 3000 }); + +// Next.js +const handler = bknd.serve(); // here you'll use "api" for mode when setting up `bknd` instance + +export const GET = handler; +export const POST = handler; +export const PUT = handler; +export const PATCH = handler; +export const DELETE = handler; + +// Qwik City (Middleware) +export const onRequest: RequestHandler = async ({ + url, + next, + status, + headers, + request, + redirect, + getWritableStream, +}) => { + const pathname = url.pathname; + + if (pathname.startsWith("/api") || pathname !== "/") { + const response = await handler(request); + + // skips unknown paths + if (response.status === 404) { + await next(); + return; + } + + // adds the set-cookie header + response.headers.forEach((value, key) => { + headers.set(key, value); + }); + + // for redirect + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (location) { + throw redirect(response.status as any, location); + } + } + + // stream back the body + status(response.status); + if (response.body) { + await response.body?.pipeTo(getWritableStream()); + return; + } + } +}; +``` + +Or use `bknd.getApp()` to integrate with an existing Hono or other router setup. + +## Reference: Config\ + +```typescript +export type AdapterModeWithOptions> = + | { + mode: "standalone"; + options: RuntimeBkndConfig; + } + | { + mode: "api"; + options: FrameworkBkndConfig; + }; + +export type Config = Extract< + Parameters[0], + { mode: T } +>['options']; +``` \ No newline at end of file diff --git a/docs/content/docs/(documentation)/integration/introduction.mdx b/docs/content/docs/(documentation)/integration/introduction.mdx index a04cb12c..db42cd17 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -45,6 +45,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/nuxt" /> +} + title="Web Adapter" + href="/integration/web" +/> + Create a new issue to request a guide for your framework. diff --git a/docs/content/docs/(documentation)/start.mdx b/docs/content/docs/(documentation)/start.mdx index d0644ae2..495df0c8 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -168,6 +168,12 @@ Pick your framework or runtime to get started. href="/integration/aws" /> +} + title="Web Adapter" + href="/integration/web" +/> + } title="Vite" diff --git a/examples/qwik/.gitignore b/examples/qwik/.gitignore new file mode 100644 index 00000000..2fa17fe1 --- /dev/null +++ b/examples/qwik/.gitignore @@ -0,0 +1,45 @@ +# Build +/dist +/lib +/lib-types +/server + +# Development +node_modules +.env +*.local + +# Cache +.cache +.mf +.rollup.cache +tsconfig.tsbuildinfo + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Editor +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets + +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Yarn +.yarn/* +!.yarn/releases +data.db +public/admin +tmp diff --git a/examples/qwik/.prettierignore b/examples/qwik/.prettierignore new file mode 100644 index 00000000..b62a9681 --- /dev/null +++ b/examples/qwik/.prettierignore @@ -0,0 +1,37 @@ +**/*.log +**/.DS_Store +*. +.vscode/settings.json +.history +.yarn +bazel-* +bazel-bin +bazel-out +bazel-qwik +bazel-testlogs +dist +dist-dev +lib +lib-types +etc +external +node_modules +temp +tsc-out +tsdoc-metadata.json +target +output +rollup.config.js +build +.cache +.vscode +.rollup.cache +tsconfig.tsbuildinfo +vite.config.ts +*.spec.tsx +*.spec.ts +.netlify +pnpm-lock.yaml +package-lock.json +yarn.lock +server diff --git a/examples/qwik/.vscode/launch.json b/examples/qwik/.vscode/launch.json new file mode 100644 index 00000000..e684cc84 --- /dev/null +++ b/examples/qwik/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Chrome", + "request": "launch", + "type": "chrome", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}" + }, + { + "type": "node", + "name": "dev.debug", + "request": "launch", + "skipFiles": ["/**"], + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/node_modules/vite/bin/vite.js", + "args": ["--mode", "ssr", "--force"] + } + ] +} diff --git a/examples/qwik/.vscode/qwik-city.code-snippets b/examples/qwik/.vscode/qwik-city.code-snippets new file mode 100644 index 00000000..878fcf68 --- /dev/null +++ b/examples/qwik/.vscode/qwik-city.code-snippets @@ -0,0 +1,36 @@ +{ + "onRequest": { + "scope": "javascriptreact,typescriptreact", + "prefix": "qonRequest", + "description": "onRequest function for a route index", + "body": [ + "export const onRequest: RequestHandler = (request) => {", + " $0", + "};", + ], + }, + "loader$": { + "scope": "javascriptreact,typescriptreact", + "prefix": "qloader$", + "description": "loader$()", + "body": ["export const $1 = routeLoader$(() => {", " $0", "});"], + }, + "action$": { + "scope": "javascriptreact,typescriptreact", + "prefix": "qaction$", + "description": "action$()", + "body": ["export const $1 = routeAction$((data) => {", " $0", "});"], + }, + "Full Page": { + "scope": "javascriptreact,typescriptreact", + "prefix": "qpage", + "description": "Simple page component", + "body": [ + "import { component$ } from '@builder.io/qwik';", + "", + "export default component$(() => {", + " $0", + "});", + ], + }, +} diff --git a/examples/qwik/.vscode/qwik.code-snippets b/examples/qwik/.vscode/qwik.code-snippets new file mode 100644 index 00000000..62edc825 --- /dev/null +++ b/examples/qwik/.vscode/qwik.code-snippets @@ -0,0 +1,78 @@ +{ + "Qwik component (simple)": { + "scope": "javascriptreact,typescriptreact", + "prefix": "qcomponent$", + "description": "Simple Qwik component", + "body": [ + "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {", + " return <${2:div}>$4", + "});", + ], + }, + "Qwik component (props)": { + "scope": "typescriptreact", + "prefix": "qcomponent$ + props", + "description": "Qwik component w/ props", + "body": [ + "export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {", + " $2", + "}", + "", + "export const $1 = component$<$1Props>((props) => {", + " const ${2:count} = useSignal(0);", + " return (", + " <${3:div} on${4:Click}$={(ev) => {$5}}>", + " $6", + " ", + " );", + "});", + ], + }, + "Qwik signal": { + "scope": "javascriptreact,typescriptreact", + "prefix": "quseSignal", + "description": "useSignal() declaration", + "body": ["const ${1:foo} = useSignal($2);", "$0"], + }, + "Qwik store": { + "scope": "javascriptreact,typescriptreact", + "prefix": "quseStore", + "description": "useStore() declaration", + "body": ["const ${1:state} = useStore({", " $2", "});", "$0"], + }, + "$ hook": { + "scope": "javascriptreact,typescriptreact", + "prefix": "q$", + "description": "$() function hook", + "body": ["$(() => {", " $0", "});", ""], + }, + "useVisibleTask": { + "scope": "javascriptreact,typescriptreact", + "prefix": "quseVisibleTask", + "description": "useVisibleTask$() function hook", + "body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""], + }, + "useTask": { + "scope": "javascriptreact,typescriptreact", + "prefix": "quseTask$", + "description": "useTask$() function hook", + "body": [ + "useTask$(({ track }) => {", + " track(() => $1);", + " $0", + "});", + "", + ], + }, + "useResource": { + "scope": "javascriptreact,typescriptreact", + "prefix": "quseResource$", + "description": "useResource$() declaration", + "body": [ + "const $1 = useResource$(({ track, cleanup }) => {", + " $0", + "});", + "", + ], + }, +} diff --git a/examples/qwik/README.md b/examples/qwik/README.md new file mode 100644 index 00000000..482a769f --- /dev/null +++ b/examples/qwik/README.md @@ -0,0 +1,65 @@ +# Qwik City App ⚡️ + +- [Qwik Docs](https://qwik.dev/) +- [Discord](https://qwik.dev/chat) +- [Qwik GitHub](https://github.com/QwikDev/qwik) +- [@QwikDev](https://twitter.com/QwikDev) +- [Vite](https://vitejs.dev/) + +--- + +## Project Structure + +This project is using Qwik with [QwikCity](https://qwik.dev/qwikcity/overview/). QwikCity is just an extra set of tools on top of Qwik to make it easier to build a full site, including directory-based routing, layouts, and more. + +Inside your project, you'll see the following directory structure: + +``` +├── public/ +│ └── ... +└── src/ + ├── components/ + │ └── ... + └── routes/ + └── ... +``` + +- `src/routes`: Provides the directory-based routing, which can include a hierarchy of `layout.tsx` layout files, and an `index.tsx` file as the page. Additionally, `index.ts` files are endpoints. Please see the [routing docs](https://qwik.dev/qwikcity/routing/overview/) for more info. + +- `src/components`: Recommended directory for components. + +- `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info. + +## Add Integrations and deployment + +Use the `bun qwik add` command to add additional integrations. Some examples of integrations includes: Cloudflare, Netlify or Express Server, and the [Static Site Generator (SSG)](https://qwik.dev/qwikcity/guides/static-site-generation/). + +```shell +bun qwik add # or `bun qwik add` +``` + +## Development + +Development mode uses [Vite's development server](https://vitejs.dev/). The `dev` command will server-side render (SSR) the output during development. + +```shell +npm start # or `bun start` +``` + +> Note: during dev mode, Vite may request a significant number of `.js` files. This does not represent a Qwik production build. + +## Preview + +The preview command will create a production build of the client modules, a production build of `src/entry.preview.tsx`, and run a local server. The preview server is only for convenience to preview a production build locally and should not be used as a production server. + +```shell +bun preview # or `bun preview` +``` + +## Production + +The production build will generate client and server modules by running both client and server build commands. The build command will use Typescript to run a type check on the source code. + +```shell +bun build # or `bun build` +``` diff --git a/examples/qwik/bknd.config.ts b/examples/qwik/bknd.config.ts new file mode 100644 index 00000000..6f3d6809 --- /dev/null +++ b/examples/qwik/bknd.config.ts @@ -0,0 +1,60 @@ +import { em, entity, text, boolean } from "bknd"; +import { registerLocalMediaAdapter } from "bknd/adapter/node"; +import type { Config } from "bknd/adapter/web"; + +const local = registerLocalMediaAdapter(); + +const schema = em({ + todos: entity("todos", { + title: text(), + done: boolean(), + }), +}); + +// register your schema to get automatic type completion +type Database = (typeof schema)["DB"]; +declare module "bknd" { + interface DB extends Database {} +} + +export default { + connection: { + url: "file:data.db", + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: "random_gibberish_please_change_this", + }, + }, + media: { + enabled: true, + adapter: local({ + path: "./public/uploads", + }), + }, + }, + adminOptions: { + adminBasepath: "/admin", + assetsPath: "/admin/", + logoReturnPath: "../..", + }, +} satisfies Config<"standalone">; diff --git a/examples/qwik/eslint.config.js b/examples/qwik/eslint.config.js new file mode 100644 index 00000000..bcfd091f --- /dev/null +++ b/examples/qwik/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { globalIgnores } from "eslint/config"; +import { qwikEslint9Plugin } from "eslint-plugin-qwik"; + +const ignores = [ + "**/*.log", + "**/.DS_Store", + "**/*.", + ".vscode/settings.json", + "**/.history", + "**/.yarn", + "**/bazel-*", + "**/bazel-bin", + "**/bazel-out", + "**/bazel-qwik", + "**/bazel-testlogs", + "**/dist", + "**/dist-dev", + "**/lib", + "**/lib-types", + "**/etc", + "**/external", + "**/node_modules", + "**/temp", + "**/tsc-out", + "**/tsdoc-metadata.json", + "**/target", + "**/output", + "**/rollup.config.js", + "**/build", + "**/.cache", + "**/.vscode", + "**/.rollup.cache", + "**/dist", + "**/tsconfig.tsbuildinfo", + "**/vite.config.ts", + "**/*.spec.tsx", + "**/*.spec.ts", + "**/.netlify", + "**/pnpm-lock.yaml", + "**/package-lock.json", + "**/yarn.lock", + "**/server", + "eslint.config.js", +]; + +export default tseslint.config( + globalIgnores(ignores), + js.configs.recommended, + tseslint.configs.recommended, + qwikEslint9Plugin.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2021, + ...globals.serviceworker, + }, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, + }, +); diff --git a/examples/qwik/package.json b/examples/qwik/package.json new file mode 100644 index 00000000..1a9ea578 --- /dev/null +++ b/examples/qwik/package.json @@ -0,0 +1,47 @@ +{ + "name": "my-qwik-empty-starter", + "description": "Blank project with routing included", + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "engines-annotation": "Mostly required by sharp which needs a Node-API v9 compatible runtime", + "private": true, + "type": "module", + "scripts": { + "build": "qwik build", + "build.client": "vite build", + "build.preview": "vite build --ssr src/entry.preview.tsx", + "build.types": "tsc --incremental --noEmit", + "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", + "dev": "vite --mode ssr", + "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", + "fmt": "prettier --write .", + "fmt.check": "prettier --check .", + "lint": "eslint \"src/**/*.ts*\"", + "preview": "qwik build preview && vite preview --open", + "start": "vite --open --mode ssr", + "qwik": "qwik", + "typegen": "bunx tsx node_modules/.bin/bknd types --outfile bknd-types.d.ts", + "postinstall": "bun run bknd copy-assets --out public/admin" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.2", + "bknd": "file:../../app" + }, + "devDependencies": { + "@builder.io/qwik": "^1.19.2", + "@builder.io/qwik-city": "^1.19.2", + "@eslint/js": "^9", + "@types/node": "20.19.0", + "eslint": "9.32.0", + "eslint-plugin-qwik": "^1.19.2", + "globals": "16.4.0", + "prettier": "3.6.2", + "typescript": "5.4.5", + "typescript-eslint": "8.38.0", + "typescript-plugin-css-modules": "latest", + "undici": "*", + "vite": "7.3.1", + "vite-tsconfig-paths": "^4.2.1" + } +} diff --git a/examples/qwik/public/bknd.ico b/examples/qwik/public/bknd.ico new file mode 100644 index 0000000000000000000000000000000000000000..c1a946d533a71c15e51fc4cade87289ca467b57d GIT binary patch literal 15086 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 + + + \ No newline at end of file diff --git a/examples/qwik/public/favicon.svg b/examples/qwik/public/favicon.svg new file mode 100644 index 00000000..0ded7c13 --- /dev/null +++ b/examples/qwik/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/qwik/public/file.svg b/examples/qwik/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/examples/qwik/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/qwik/public/globe.svg b/examples/qwik/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/examples/qwik/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/qwik/public/manifest.json b/examples/qwik/public/manifest.json new file mode 100644 index 00000000..c18e75f7 --- /dev/null +++ b/examples/qwik/public/manifest.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/web-manifest-combined.json", + "name": "qwik-project-name", + "short_name": "Welcome to Qwik", + "start_url": ".", + "display": "standalone", + "background_color": "#fff", + "description": "A Qwik project app." +} diff --git a/examples/qwik/public/robots.txt b/examples/qwik/public/robots.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/qwik/public/window.svg b/examples/qwik/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/examples/qwik/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/qwik/qwik.env.d.ts b/examples/qwik/qwik.env.d.ts new file mode 100644 index 00000000..25af92b9 --- /dev/null +++ b/examples/qwik/qwik.env.d.ts @@ -0,0 +1,4 @@ +// This file can be used to add references for global types like `vite/client`. + +// Add global `vite/client` types. For more info, see: https://vitejs.dev/guide/features#client-types +/// diff --git a/examples/qwik/src/components/footer.tsx b/examples/qwik/src/components/footer.tsx new file mode 100644 index 00000000..54d5bfbe --- /dev/null +++ b/examples/qwik/src/components/footer.tsx @@ -0,0 +1,36 @@ +import { Link } from "@builder.io/qwik-city"; +import { component$ } from "@builder.io/qwik"; +import { useLocation } from "@builder.io/qwik-city"; + +export default component$(() => { + const loc = useLocation(); + return ( + + ); +}); diff --git a/examples/qwik/src/components/list.tsx b/examples/qwik/src/components/list.tsx new file mode 100644 index 00000000..9a87a290 --- /dev/null +++ b/examples/qwik/src/components/list.tsx @@ -0,0 +1,17 @@ +import { component$ } from "@builder.io/qwik"; + +interface Props { + items: string[]; +} + +export default component$(({ items }) => { + return ( +
    + {items.map((item, i) => ( +
  1. + {item} +
  2. + ))} +
+ ); +}); diff --git a/examples/qwik/src/entry.dev.tsx b/examples/qwik/src/entry.dev.tsx new file mode 100644 index 00000000..f421db70 --- /dev/null +++ b/examples/qwik/src/entry.dev.tsx @@ -0,0 +1,17 @@ +/* + * WHAT IS THIS FILE? + * + * Development entry point using only client-side modules: + * - Do not use this mode in production! + * - No SSR + * - No portion of the application is pre-rendered on the server. + * - All of the application is running eagerly in the browser. + * - More code is transferred to the browser than in SSR mode. + * - Optimizer/Serialization/Deserialization code is not exercised! + */ +import { render, type RenderOptions } from "@builder.io/qwik"; +import Root from "./root"; + +export default function (opts: RenderOptions) { + return render(document, , opts); +} diff --git a/examples/qwik/src/entry.preview.tsx b/examples/qwik/src/entry.preview.tsx new file mode 100644 index 00000000..ef0111a8 --- /dev/null +++ b/examples/qwik/src/entry.preview.tsx @@ -0,0 +1,21 @@ +/* + * WHAT IS THIS FILE? + * + * It's the bundle entry point for `npm run preview`. + * That is, serving your app built in production mode. + * + * Feel free to modify this file, but don't remove it! + * + * Learn more about Vite's preview command: + * - https://vitejs.dev/config/preview-options.html#preview-options + * + */ +import { createQwikCity } from "@builder.io/qwik-city/middleware/node"; +import qwikCityPlan from "@qwik-city-plan"; +// make sure qwikCityPlan is imported before entry +import render from "./entry.ssr"; + +/** + * The default export is the QwikCity adapter used by Vite preview. + */ +export default createQwikCity({ render, qwikCityPlan }); diff --git a/examples/qwik/src/entry.ssr.tsx b/examples/qwik/src/entry.ssr.tsx new file mode 100644 index 00000000..53210851 --- /dev/null +++ b/examples/qwik/src/entry.ssr.tsx @@ -0,0 +1,31 @@ +/** + * WHAT IS THIS FILE? + * + * SSR entry point, in all cases the application is rendered outside the browser, this + * entry point will be the common one. + * + * - Server (express, cloudflare...) + * - npm run start + * - npm run preview + * - npm run build + * + */ +import { + renderToStream, + type RenderToStreamOptions, +} from "@builder.io/qwik/server"; +import Root from "./root"; + +export default function (opts: RenderToStreamOptions) { + return renderToStream(, { + ...opts, + // Use container attributes to set attributes on the html tag. + containerAttributes: { + lang: "en-us", + ...opts.containerAttributes, + }, + serverData: { + ...opts.serverData, + }, + }); +} diff --git a/examples/qwik/src/global.css b/examples/qwik/src/global.css new file mode 100644 index 00000000..1d1ea1e1 --- /dev/null +++ b/examples/qwik/src/global.css @@ -0,0 +1,23 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +body { + @apply bg-background text-foreground; + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/qwik/src/lib/bknd.ts b/examples/qwik/src/lib/bknd.ts new file mode 100644 index 00000000..7b7b20b7 --- /dev/null +++ b/examples/qwik/src/lib/bknd.ts @@ -0,0 +1,13 @@ +import { createBknd } from "bknd/adapter/web"; +import bkndConfig from "../../bknd.config"; +import type { EnvGetter } from "@builder.io/qwik-city/middleware/request-handler"; + +export const getApp = async (env?: EnvGetter) => createBknd({ mode: "standalone", options: bkndConfig }, env); + +export const handler = async (req: Request, env?: EnvGetter) => { + return (await getApp(env)).serve()(req); +}; + +export const getApi = async (opts?: { headers?: Headers; verify?: boolean }) => { + return (await getApp()).getApi(opts); +}; diff --git a/examples/qwik/src/root.tsx b/examples/qwik/src/root.tsx new file mode 100644 index 00000000..a6f5c207 --- /dev/null +++ b/examples/qwik/src/root.tsx @@ -0,0 +1,77 @@ +import { component$, isDev } from "@builder.io/qwik"; +import { QwikCityProvider, RouterOutlet } from "@builder.io/qwik-city"; +import { useDocumentHead, useLocation } from "@builder.io/qwik-city"; + + +import "./global.css"; + +export default component$(() => { + /** + * The root of a QwikCity site always start with the component, + * immediately followed by the document's and . + * + * Don't remove the `` and `` elements. + */ + + return ( + + + + {!isDev && ( + + )} + + + + + + + ); +}); + + +export const RouterHead = component$(() => { + const head = useDocumentHead(); + const loc = useLocation(); + + return ( + <> + {head.title} + + + + + + {head.meta.map((m) => ( + + ))} + + {head.links.map((l) => ( + + ))} + + {head.styles.map((s) => ( +