scaffold Vike app with Bati

master
Bati 4 months ago
commit 9916e95de0

142
.gitignore vendored

@ -0,0 +1,142 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Cloudflare
.wrangler/
# Vercel
.vercel/
# Sentry Vite Plugin
.env.sentry-build-plugin
# aws-cdk
.cdk.staging
cdk.out

@ -0,0 +1,68 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 450](https://www.npmjs.com/package/create-vike/v/0.0.450)) using this command:
```sh
pnpm create vike@latest --react --compiled-css --mantine --authjs --trpc --hono --cloudflare --biome
```
## Contents
* [React](#react)
* [`/pages/+config.ts`](#pagesconfigts)
* [Routing](#routing)
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
* [`/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`](#pagesonpagetransitionstartts-and-pagesonpagetransitionendts)
* [SSR](#ssr)
* [HTML Streaming](#html-streaming)
* [Mantine](#mantine)
## React
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
### `/pages/+config.ts`
Such `+` files are [the interface](https://vike.dev/config) between Vike and your code. It defines:
* A default [`<Layout>` component](https://vike.dev/Layout) (that wraps your [`<Page>` components](https://vike.dev/Page)).
* A default [`title`](https://vike.dev/title).
* Global [`<head>` tags](https://vike.dev/head-tags).
### Routing
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
* [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
* [Route Strings](https://vike.dev/route-string)
* [Route Functions](https://vike.dev/route-function)
### `/pages/_error/+Page.jsx`
The [error page](https://vike.dev/error-page) which is rendered when errors occur.
### `/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`
The [`onPageTransitionStart()` hook](https://vike.dev/onPageTransitionStart), together with [`onPageTransitionEnd()`](https://vike.dev/onPageTransitionEnd), enables you to implement page transition animations.
### SSR
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all your pages or only for some pages.
### HTML Streaming
You can enable/disable [HTML streaming](https://vike.dev/stream) for all your pages, or only for some pages while still using it for others.
## Mantine
This is a boilerplate for Mantine based on the [Getting Started](https://mantine.dev/docs/getting-started/) guide.
The following Packages are installed:
* `@mantine/hooks` Hooks for state and UI management
* `@mantine/core` Core components library: inputs, buttons, overlays, etc.
If you add more packages, make sure to update the `layouts/LayoutDefault.tsx` file to include the required CSSs.
The theme is defined in `layouts/theme.ts`.

@ -0,0 +1,67 @@
<svg class="hammer" width="41.217" height="41.217" version="1.1" viewBox="-50 -50 41.217 41.217" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mask111">
<rect x="-19.21" y="-25.7" width="46.217" height="41.217" fill="url(#linearGradient115)"/>
</mask>
<linearGradient id="linearGradient115" x1="-25.395" x2="-25.395" y1="-9.3005" y2="-18.03" gradientTransform="matrix(1.0589 0 0 .94436 30.79 24.3)" gradientUnits="userSpaceOnUse">
<stop offset="0"/>
<stop stop-color="#fff" offset="1"/>
</linearGradient>
</defs>
<g transform="translate(-33.29,-24.3)" mask="url(#mask111)">
<g stroke-linecap="round" stroke-linejoin="round">
<path d="m-8.511-10.449 1.126 4.064 2.707-2.765z" fill="#ababab"/>
<path d="m-2.273-24.496-6.238 14.047 3.833 1.299 6.238-14.048z" fill="#949494"/>
<path d="m-2.273-24.496 3.465-1.204.368 2.502z" fill="#ababab"/>
<path d="m17.511 4.674-2.707 2.766-22.189-13.825 2.707-2.765z" fill="#949494"/>
</g>
<g stroke="#878787">
<path d="m-9.045 20.369-1.169 2.634" stroke-width="9.6"/>
<path d="m-12.418 23.191c-1.85-1.153-2.326-2.132-1.086-2.238 1.239-.106 3.642.709 5.493 1.862s2.326 2.132 1.087 2.238c-1.24.106-3.643-.709-5.494-1.862" fill="#878787" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m-11.248 20.557c1.851 1.153 4.254 1.968 5.493 1.862 1.24-.106.764-1.085-1.086-2.238-1.851-1.153-4.254-1.968-5.494-1.862-1.239.106-.764 1.085 1.087 2.238" fill="#878787" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g stroke-linecap="round" stroke-linejoin="round">
<path d="m-16.71-9.748 8.199-.701 1.126 4.064-8.199.701z" fill="#949494"/>
<path d="m23.749-9.373-6.238 14.047-22.189-13.824 6.238-14.048z" fill="#757575"/>
<path d="m10.271-16.073 3.751 3.534c.062.058.083.156.052.238l-1.95 5.128c-.046.121-.18.153-.268.065l-1.024-1.03c-.095-.096-.242-.048-.275.091l-.516 2.152c-.034.145-.191.19-.284.082 0 0-.606-.696-.606-.696-.094-.108-.25-.063-.285.082l-.803 3.384c-.05.212-.317.178-.336-.043l-.014-.147s.058-9.892.058-9.892c.001-.165.165-.253.277-.148l1.077 1.009c.101.095.25.034.274-.111l.597-3.587c.025-.146.174-.206.275-.111z" fill="#fbbf28" stroke="#fbbf28" stroke-width=".6"/>
</g>
<g stroke="#808080">
<path d="m-8.362 18.833-.39.878" stroke-width="9.1"/>
<path d="m-10.956 19.899c-1.85-1.153-2.326-2.132-1.086-2.238 1.239-.106 3.642.708 5.493 1.861s2.326 2.132 1.087 2.238c-1.24.106-3.643-.708-5.494-1.861" fill="#808080" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
<path d="m-10.566 19.021c1.851 1.153 4.254 1.967 5.494 1.861 1.239-.106.764-1.085-1.087-2.238s-4.254-1.967-5.494-1.861c-1.239.106-.764 1.085 1.087 2.238" fill="#808080" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
</g>
<path d="m-16.71-9.748 8.199-.701 6.238-14.047-8.199.701z" fill="#757575" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m-1.754 3.951-6.511 14.662" stroke="#91512b" stroke-width="8.6"/>
<g stroke-linecap="round" stroke-linejoin="round">
<path d="m-10.468 18.801c-1.851-1.153-2.327-2.132-1.087-2.238 1.239-.106 3.643.709 5.493 1.862 1.851 1.153 2.327 2.132 1.087 2.238-1.239.106-3.643-.708-5.493-1.862" fill="#91512b"/>
<path d="m-3.958 4.139c1.851 1.153 4.254 1.968 5.494 1.862 1.239-.106.764-1.086-1.087-2.239s-4.254-1.967-5.493-1.861c-1.24.106-.764 1.085 1.086 2.238" fill="#91512b"/>
<path d="m1.192-25.7.368 2.502 22.189 13.825-.368-2.503z" fill="#949494"/>
<path d="m-10.472-23.795 8.199-.701 3.465-1.204-8.199.701z" fill="#949494"/>
</g>
<g stroke="#6e6e6e">
<path d="m-.487 1.097-1.17 2.634" stroke-width="9.1"/>
<path d="m-3.86 3.92c-1.851-1.153-2.326-2.132-1.087-2.238s3.643.708 5.493 1.861c1.851 1.153 2.327 2.132 1.087 2.238-1.239.106-3.643-.708-5.493-1.861" fill="#6e6e6e" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
<path d="m-2.691 1.286c1.851 1.153 4.254 1.967 5.494 1.861 1.239-.106.764-1.085-1.087-2.238s-4.254-1.967-5.493-1.861c-1.24.106-.764 1.085 1.086 2.238" fill="#6e6e6e" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
</g>
<g stroke-linecap="round" stroke-linejoin="round">
<path d="m18.269 6.236-3.465 1.204 2.707-2.766z" fill="#ababab"/>
<path d="m14.804 7.44-8.199.701-22.189-13.825 8.199-.701z" fill="#757575"/>
<path d="m-16.71-9.748 1.126 4.064-.367-2.502z" fill="#ababab"/>
<path d="m24.507-7.812-6.238 14.048-.758-1.562 6.238-14.047z" fill="#949494"/>
<path d="m-10.472-23.795-6.238 14.047.759 1.562 6.237-14.048z" fill="#949494"/>
<path d="m24.507-7.812-1.126-4.064.368 2.503z" fill="#ababab"/>
<path d="m23.381-11.876-8.199.701-22.189-13.824 8.199-.701z" fill="#757575"/>
<path d="m-10.472-23.795 3.465-1.204-2.707 2.765z" fill="#ababab"/>
<path d="m18.269 6.236-8.199.701-3.465 1.204 8.199-.701z" fill="#949494"/>
<path d="m-15.951-8.186.367 2.502 22.189 13.825-.367-2.503z" fill="#949494"/>
<path d="m18.269 6.236-8.199.701 6.238-14.048 8.199-.701z" fill="#757575"/>
<path d="m-9.714-22.234-6.237 14.048 22.189 13.824 6.237-14.047z" fill="#757575"/>
<path d="m2.545-12.79-4.583-1.659c-.076-.027-.156.008-.195.085 0 0-2.463 4.808-2.463 4.808-.058.114-.005.263.107.298l1.296.416c.122.039.171.21.093.321 0 0-1.205 1.722-1.205 1.722-.081.116-.024.294.105.325l.827.196c.128.031.186.209.104.325 0 0-1.899 2.701-1.899 2.701-.118.169.056.41.22.304l.11-.07 6.849-5.661c.115-.095.083-.304-.054-.354l-1.312-.48c-.123-.045-.165-.224-.078-.331 0 0 2.157-2.615 2.157-2.615.087-.106.045-.286-.079-.331z" fill="#fbbf28" stroke="#fbbf28" stroke-width=".6"/>
<path d="m24.507-7.812-8.199.701-1.126-4.064 8.199-.701z" fill="#949494"/>
<path d="m15.182-11.175-2.707 2.766-22.189-13.825 2.707-2.765z" fill="#949494"/>
<path d="m10.07 6.937-3.465 1.204-.367-2.503z" fill="#ababab"/>
<path d="m16.308-7.111-6.238 14.048-3.832-1.299 6.237-14.047z" fill="#949494"/>
<path d="m16.308-7.111-1.126-4.064-2.707 2.766z" fill="#ababab"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"files": {
"ignore": ["dist/**", "*.js", "*.cjs", "*.mjs", "*.spec.ts"]
}
}

@ -0,0 +1,9 @@
import { usePageContext } from "vike-react/usePageContext";
import { NavLink } from "@mantine/core";
export function Link({ href, label }: { href: string; label: string }) {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
return <NavLink href={href} label={label} active={isActive} />;
}

@ -0,0 +1,17 @@
interface TodoItem {
text: string;
}
const todosDefault = [{ text: "Buy milk" }, { text: "Buy strawberries" }];
const database =
// We create an in-memory database.
// - We use globalThis so that the database isn't reset upon HMR.
// - The database is reset when restarting the server, use a proper database (SQLite/PostgreSQL/...) if you want persistent data.
// biome-ignore lint:
((globalThis as unknown as { __database: { todos: TodoItem[] } }).__database ??= { todos: todosDefault });
const { todos } = database;
export { todos };
export type { TodoItem };

12
global.d.ts vendored

@ -0,0 +1,12 @@
import type { Session } from "@auth/core/types";
declare global {
namespace Vike {
interface PageContext {
session?: Session | null;
}
}
}
// biome-ignore lint/complexity/noUselessEmptyExport: ensure that the file is considered as a module
export {};

@ -0,0 +1,31 @@
import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { type Context, Hono } from "hono";
import { env } from "hono/adapter";
import { compress } from "hono/compress";
import app from "./hono-entry.js";
const envs = env<{ NODE_ENV?: string; PORT?: string }>({ env: {} } as unknown as Context<{
Bindings: { NODE_ENV?: string; PORT?: string };
}>);
const nodeApp = new Hono();
nodeApp.use(compress());
nodeApp.use(
"/*",
serveStatic({
root: `./dist/client/`,
}),
);
nodeApp.route("/", app!);
const port = envs.PORT ? parseInt(envs.PORT, 10) : 3000;
console.log(`Server listening on http://localhost:${port}`);
serve({
fetch: nodeApp.fetch,
port: port,
});

@ -0,0 +1,26 @@
import { authjsHandler, authjsSessionMiddleware } from "./server/authjs-handler";
import { vikeHandler } from "./server/vike-handler";
import { Hono } from "hono";
import { createHandler, createMiddleware } from "@universal-middleware/hono";
import { trpcHandler } from "./server/trpc-handler";
const app = new Hono();
app.use(createMiddleware(authjsSessionMiddleware)());
/**
* Auth.js route
* @link {@see https://authjs.dev/getting-started/installation}
**/
app.use("/api/auth/**", createHandler(authjsHandler)());
app.use("/api/trpc/*", createHandler(trpcHandler)("/api/trpc"));
/**
* Vike route
*
* @link {@see https://vike.dev}
**/
app.all("*", createHandler(vikeHandler)());
export default app;

@ -0,0 +1,36 @@
import "@mantine/core/styles.css";
import { AppShell, Burger, Group, Image, MantineProvider } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import theme from "./theme.js";
import logoUrl from "../assets/logo.svg";
import { Link } from "../components/Link";
export default function LayoutDefault({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure();
return (
<MantineProvider theme={theme}>
<AppShell
header={{ height: 60 }}
navbar={{ width: 300, breakpoint: "sm", collapsed: { mobile: !opened } }}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<a href="/">
{" "}
<Image h={50} fit="contain" src={logoUrl} />{" "}
</a>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<Link href="/" label="Welcome" />
<Link href="/todo" label="Todo" />
<Link href="/star-wars" label="Data Fetching" />
</AppShell.Navbar>
<AppShell.Main> {children} </AppShell.Main>
</AppShell>
</MantineProvider>
);
}

@ -0,0 +1,9 @@
import { createTheme } from "@mantine/core";
import type { MantineThemeOverride } from "@mantine/core";
const theme: MantineThemeOverride = createTheme({
/** Put your mantine theme override here */
primaryColor: "violet",
});
export default theme;

@ -0,0 +1,47 @@
{
"scripts": {
"dev": "vike dev",
"build": "vike build",
"preview": "run-s build preview:wrangler",
"lint": "biome lint --write .",
"format": "biome format --write .",
"preview:wrangler": "wrangler pages dev",
"deploy:wrangler": "wrangler pages deploy",
"deploy": "run-s build deploy:wrangler"
},
"dependencies": {
"vike": "^0.4.235",
"@auth/core": "^0.40.0",
"@universal-middleware/core": "^0.4.8",
"@compiled/react": "^0.18.6",
"@hono/node-server": "^1.14.4",
"@universal-middleware/hono": "^0.4.14",
"hono": "^4.8.2",
"@vitejs/plugin-react": "^4.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"vike-react": "^0.6.4",
"@trpc/server": "^11.4.2",
"@trpc/client": "^11.4.2",
"vike-cloudflare": "^0.1.7",
"@mantine/core": "^8.1.1",
"@mantine/hooks": "^8.1.1"
},
"devDependencies": {
"typescript": "^5.8.3",
"vite": "^6.3.5",
"@biomejs/biome": "1.9.4",
"vite-plugin-compiled-react": "^1.3.1",
"@hono/vite-dev-server": "^0.19.1",
"@types/node": "^20.19.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@cloudflare/workers-types": "^4.20250620.0",
"wrangler": "^4.20.5",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1"
},
"type": "module"
}

@ -0,0 +1,14 @@
// https://vike.dev/Head
import logoUrl from "../assets/logo.svg";
import { ColorSchemeScript } from "@mantine/core";
export default function HeadDefault() {
return (
<>
<link rel="icon" href={logoUrl} />
<ColorSchemeScript />
</>
);
}

@ -0,0 +1,18 @@
import vikeReact from "vike-react/config";
import type { Config } from "vike/types";
import Layout from "../layouts/LayoutDefault.js";
// Default config (can be overridden by pages)
// https://vike.dev/config
export default {
// https://vike.dev/Layout
Layout,
// https://vike.dev/head-tags
title: "My Vike App",
description: "Demo showcasing Vike",
passToClient: ["user"],
extends: vikeReact,
} satisfies Config;

@ -0,0 +1,6 @@
import type { OnPageTransitionEndAsync } from "vike/types";
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => {
console.log("Page transition end");
document.querySelector("body")?.classList.remove("page-is-transitioning");
};

@ -0,0 +1,6 @@
import type { OnPageTransitionStartAsync } from "vike/types";
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
console.log("Page transition start");
document.querySelector("body")?.classList.add("page-is-transitioning");
};

@ -0,0 +1,19 @@
import { usePageContext } from "vike-react/usePageContext";
export default function Page() {
const { is404 } = usePageContext();
if (is404) {
return (
<>
<h1>404 Page Not Found</h1>
<p>This page could not be found.</p>
</>
);
}
return (
<>
<h1>500 Internal Server Error</h1>
<p>Something went wrong.</p>
</>
);
}

@ -0,0 +1,16 @@
import { Counter } from "./Counter.js";
export default function Page() {
return (
<>
<h1 css={{ fontWeight: "700", fontSize: "1.875rem", paddingBottom: "1rem" }}>My Vike app</h1>
This page is:
<ul>
<li>Rendered to HTML.</li>
<li>
Interactive. <Counter />
</li>
</ul>
</>
);
}

@ -0,0 +1,25 @@
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button
type="button"
css={{
display: "inline-block",
border: "1px solid black",
borderRadius: "0.25rem",
backgroundColor: "#e5e7eb",
padding: "4px 8px 4px 8px",
fontSize: "0.75rem",
fontWeight: "500",
textTransform: "uppercase",
lineHeight: "1.5",
}}
onClick={() => setCount((count) => count + 1)}
>
Counter {count}
</button>
);
}

@ -0,0 +1,16 @@
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movie = useData<Data>();
return (
<>
<h1>{movie.title}</h1>
Release Date: {movie.release_date}
<br />
Director: {movie.director}
<br />
Producer: {movie.producer}
</>
);
}

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { PageContextServer } from "vike/types";
import type { MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async (pageContext: PageContextServer) => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
let movie = (await response.json()) as MovieDetails;
config({
// Set <title>
title: movie.title,
});
// We remove data we don't need because the data is passed to
// the client; we should minimize what is sent over the network.
movie = minimize(movie);
return movie;
};
function minimize(movie: MovieDetails): MovieDetails {
const { id, title, release_date, director, producer } = movie;
const minimizedMovie = { id, title, release_date, director, producer };
return minimizedMovie;
}

@ -0,0 +1,21 @@
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movies = useData<Data>();
return (
<>
<h1>Star Wars Movies</h1>
<ol>
{movies.map(({ id, title, release_date }) => (
<li key={id}>
<a href={`/star-wars/${id}`}>{title}</a> ({release_date})
</li>
))}
</ol>
<p>
Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>.
</p>
</>
);
}

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { Movie, MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async () => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
const moviesData = (await response.json()) as MovieDetails[];
config({
// Set <title>
title: `${moviesData.length} Star Wars Movies`,
});
// We remove data we don't need because the data is passed to the client; we should
// minimize what is sent over the network.
const movies = minimize(moviesData);
return movies;
};
function minimize(movies: MovieDetails[]): Movie[] {
return movies.map((movie) => {
const { title, release_date, id } = movie;
return { title, release_date, id };
});
}

@ -0,0 +1,10 @@
export type Movie = {
id: string;
title: string;
release_date: string;
};
export type MovieDetails = Movie & {
director: string;
producer: string;
};

@ -0,0 +1,13 @@
import type { Data } from "./+data";
import { useData } from "vike-react/useData";
import { TodoList } from "./TodoList.js";
export default function Page() {
const data = useData<Data>();
return (
<>
<h1>To-do List</h1>
<TodoList initialTodoItems={data.todo} />
</>
);
}

@ -0,0 +1,3 @@
export const config = {
prerender: false,
};

@ -0,0 +1,11 @@
// https://vike.dev/data
import { todos } from "../../database/todoItems";
import type { PageContextServer } from "vike/types";
export type Data = {
todo: { text: string }[];
};
export default async function data(_pageContext: PageContextServer): Promise<Data> {
return { todo: todos };
}

@ -0,0 +1,38 @@
import { trpc } from "../../trpc/client";
import { useState } from "react";
export function TodoList({ initialTodoItems }: { initialTodoItems: { text: string }[] }) {
const [todoItems, setTodoItems] = useState(initialTodoItems);
const [newTodo, setNewTodo] = useState("");
return (
<>
<ul>
{todoItems.map((todoItem, index) => (
// biome-ignore lint:
<li key={index}>{todoItem.text}</li>
))}
</ul>
<div>
<form
onSubmit={async (ev) => {
ev.preventDefault();
// Optimistic UI update
setTodoItems((prev) => [...prev, { text: newTodo }]);
try {
await trpc.onNewTodo.mutate(newTodo);
setNewTodo("");
} catch (e) {
console.error(e);
// rollback
setTodoItems((prev) => prev.slice(0, -1));
}
}}
>
<input type="text" onChange={(ev) => setNewTodo(ev.target.value)} value={newTodo} />
<button type="submit">Add to-do</button>
</form>
</div>
</>
);
}

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- "@biomejs/biome"

@ -0,0 +1,14 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

@ -0,0 +1,95 @@
import { Auth, type AuthConfig, createActionURL, setEnvDefaults } from "@auth/core";
import CredentialsProvider from "@auth/core/providers/credentials";
import type { Session } from "@auth/core/types";
// TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler, UniversalMiddleware } from "@universal-middleware/core";
const env: Record<string, string | undefined> =
typeof process?.env !== "undefined"
? process.env
: import.meta && "env" in import.meta
? (import.meta as ImportMeta & { env: Record<string, string | undefined> }).env
: {};
if (!globalThis.crypto) {
/**
* Polyfill needed if Auth.js code runs on node18
*/
Object.defineProperty(globalThis, "crypto", {
value: await import("node:crypto").then((crypto) => crypto.webcrypto as Crypto),
writable: false,
configurable: true,
});
}
const authjsConfig = {
basePath: "/api/auth",
trustHost: Boolean(env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production"),
// TODO: Replace secret {@see https://authjs.dev/reference/core#secret}
secret: "MY_SECRET",
providers: [
// TODO: Choose and implement providers
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
async authorize() {
// Add logic here to look up the user from the credentials supplied
const user = { id: "1", name: "J Smith", email: "jsmith@example.com" };
// Any object returned will be saved in `user` property of the JWT
// If you return null then an error will be displayed advising the user to check their details.
// You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
return user ?? null;
},
}),
],
} satisfies Omit<AuthConfig, "raw">;
/**
* Retrieve Auth.js session from Request
*/
export async function getSession(req: Request, config: Omit<AuthConfig, "raw">): Promise<Session | null> {
setEnvDefaults(process.env, config);
const requestURL = new URL(req.url);
const url = createActionURL("session", requestURL.protocol, req.headers, process.env, config);
const response = await Auth(new Request(url, { headers: { cookie: req.headers.get("cookie") ?? "" } }), config);
const { status = 200 } = response;
const data = await response.json();
if (!data || !Object.keys(data).length) return null;
if (status === 200) return data;
throw new Error(data.message);
}
/**
* Add Auth.js session to context
* @link {@see https://authjs.dev/getting-started/session-management/get-session}
**/
export const authjsSessionMiddleware: Get<[], UniversalMiddleware> = () => async (request, context) => {
try {
return {
...context,
session: await getSession(request, authjsConfig),
};
} catch (error) {
console.debug("authjsSessionMiddleware:", error);
return {
...context,
session: null,
};
}
};
/**
* Auth.js route
* @link {@see https://authjs.dev/getting-started/installation}
**/
export const authjsHandler = (() => async (request) => {
return Auth(request, authjsConfig);
}) satisfies Get<[], UniversalHandler>;

@ -0,0 +1,20 @@
import { appRouter } from "../trpc/server";
// TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler } from "@universal-middleware/core";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
export const trpcHandler = ((endpoint) => (request, context, runtime) => {
return fetchRequestHandler({
endpoint,
req: request,
router: appRouter,
createContext({ req, resHeaders }) {
return {
...context,
...runtime,
req,
resHeaders,
};
},
});
}) satisfies Get<[endpoint: string], UniversalHandler>;

@ -0,0 +1,18 @@
/// <reference lib="webworker" />
import { renderPage } from "vike/server";
// TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler } from "@universal-middleware/core";
export const vikeHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => {
const pageContextInit = { ...context, ...runtime, urlOriginal: request.url, headersOriginal: request.headers };
const pageContext = await renderPage(pageContextInit);
const response = pageContext.httpResponse;
const { readable, writable } = new TransformStream();
response.pipe(writable);
return new Response(readable, {
status: response.statusCode,
headers: response.headers,
});
};

@ -0,0 +1,10 @@
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server.js";
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "/api/trpc",
}),
],
});

@ -0,0 +1,32 @@
import { initTRPC } from "@trpc/server";
/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.context<object>().create();
/**
* Export reusable router and procedure helpers
* that can be used throughout the router
*/
export const router = t.router;
export const publicProcedure = t.procedure;
export const appRouter = router({
demo: publicProcedure.query(async () => {
return { demo: true };
}),
onNewTodo: publicProcedure
.input((value): string => {
if (typeof value === "string") {
return value;
}
throw new Error("Input is not a string");
})
.mutation(async (opts) => {
console.log("Received new todo", { text: opts.input });
}),
});
export type AppRouter = typeof appRouter;

@ -0,0 +1,29 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"module": "ESNext",
"noEmit": true,
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"types": [
"vite/client",
"vike-react",
"vike-cloudflare/types"
],
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"exclude": [
"dist"
]
}

@ -0,0 +1,40 @@
import { pages } from "vike-cloudflare";
import react from "@vitejs/plugin-react";
import devServer from "@hono/vite-dev-server";
import { compiled } from "vite-plugin-compiled-react";
import { defineConfig } from "vite";
import vike from "vike/plugin";
export default defineConfig({
plugins: [
vike(),
compiled({
extract: true,
}),
devServer({
entry: "hono-entry.ts",
exclude: [
/^\/@.+$/,
/.*\.(ts|tsx|vue)($|\?)/,
/.*\.(s?css|less)($|\?)/,
/^\/favicon\.ico$/,
/.*\.(svg|png)($|\?)/,
/^\/(public|assets|static)\/.+/,
/^\/node_modules\/.*/,
],
injectClientScript: false,
}),
react(),
pages({
server: {
kind: "hono",
entry: "hono-entry.ts",
},
}),
],
build: {
target: "es2022",
},
});

@ -0,0 +1,4 @@
name = "my-app"
compatibility_date = "2024-09-29"
pages_build_output_dir = "./dist/cloudflare"
compatibility_flags = [ "nodejs_compat" ]
Loading…
Cancel
Save