rework/lightweight #12

Merged
GauthierWebDev merged 106 commits from rework/lightweight into main 2025-04-21 16:27:38 +00:00
38 changed files with 929 additions and 5 deletions
Showing only changes of commit 09c156b987 - Show all commits

49
README.md Normal file
View File

@ -0,0 +1,49 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 429](https://www.npmjs.com/package/create-vike/v/0.0.429)) using this command:
```sh
bun create vike@latest --solid --tailwindcss --authjs --telefunc --fastify --google-analytics --eslint --prettier --biome
```
## Contents
* [`/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)
This app is ready to start. It's powered by [Vike](https://vike.dev) and [SolidJS](https://www.solidjs.com/guides/getting-started).
### `/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.

4
app/.env Normal file
View File

@ -0,0 +1,4 @@
# Google Analytics
# See the documentation https://support.google.com/analytics/answer/9304153?hl=en#zippy=%2Cweb
PUBLIC_ENV__GOOGLE_ANALYTICS="G-XXXXXXXXXX"

3
app/.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

67
app/assets/logo.svg Normal file
View File

@ -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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

15
app/biome.json Normal file
View File

@ -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"]
}
}

14
app/components/Link.tsx Normal file
View File

@ -0,0 +1,14 @@
import { createMemo } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
export function Link(props: { href: string; children: string }) {
const pageContext = usePageContext();
const isActive = createMemo(() =>
props.href === "/" ? pageContext.urlPathname === props.href : pageContext.urlPathname.startsWith(props.href),
);
return (
<a href={props.href} class={isActive() ? "is-active" : undefined}>
{props.children}
</a>
);
}

17
app/database/todoItems.ts Normal file
View File

@ -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 };

56
app/eslint.config.ts Normal file
View File

@ -0,0 +1,56 @@
import eslint from "@eslint/js";
import prettier from "eslint-plugin-prettier/recommended";
import solid from "eslint-plugin-solid/configs/typescript";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: [
"dist/*",
// Temporary compiled files
"**/*.ts.build-*.mjs",
// JS files at the root of the project
"*.js",
"*.cjs",
"*.mjs",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false,
sourceType: "module",
ecmaVersion: "latest",
},
},
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
1,
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-namespace": 0,
},
},
{
files: ["**/*.{ts,tsx,js,jsx}"],
...solid,
languageOptions: {
parser: tseslint.parser,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
prettier,
);

76
app/fastify-entry.ts Normal file
View File

@ -0,0 +1,76 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { authjsHandler, authjsSessionMiddleware } from "./server/authjs-handler";
import { vikeHandler } from "./server/vike-handler";
import { telefuncHandler } from "./server/telefunc-handler";
import Fastify from "fastify";
import { createHandler, createMiddleware } from "@universal-middleware/fastify";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const root = __dirname;
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const hmrPort = process.env.HMR_PORT ? parseInt(process.env.HMR_PORT, 10) : 24678;
async function startServer() {
const app = Fastify();
// Avoid pre-parsing body, otherwise it will cause issue with universal handlers
// This will probably change in the future though, you can follow https://github.com/magne4000/universal-middleware for updates
app.removeAllContentTypeParsers();
app.addContentTypeParser("*", function (_request, _payload, done) {
done(null, "");
});
await app.register(await import("@fastify/middie"));
if (process.env.NODE_ENV === "production") {
await app.register(await import("@fastify/static"), {
root: `${root}/dist/client`,
wildcard: false,
});
} else {
// Instantiate Vite's development server and integrate its middleware to our server.
// ⚠️ We should instantiate it *only* in development. (It isn't needed in production
// and would unnecessarily bloat our server in production.)
const vite = await import("vite");
const viteDevMiddleware = (
await vite.createServer({
root,
server: { middlewareMode: true, hmr: { port: hmrPort } },
})
).middlewares;
app.use(viteDevMiddleware);
}
await app.register(createMiddleware(authjsSessionMiddleware)());
/**
* Auth.js route
* @link {@see https://authjs.dev/getting-started/installation}
**/
app.all("/api/auth/*", createHandler(authjsHandler)());
app.post<{ Body: string }>("/_telefunc", createHandler(telefuncHandler)());
/**
* Vike route
*
* @link {@see https://vike.dev}
**/
app.all("/*", createHandler(vikeHandler)());
return app;
}
const app = await startServer();
app.listen(
{
port: port,
},
() => {
console.log(`Server listening on http://localhost:${port}`);
},
);

12
app/global.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import { 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 {};

View File

@ -0,0 +1,50 @@
import "./style.css";
import "./tailwind.css";
import type { JSX } from "solid-js";
import logoUrl from "../assets/logo.svg";
import { Link } from "../components/Link.js";
export default function LayoutDefault(props: { children?: JSX.Element }) {
return (
<div class={"flex max-w-5xl m-auto"}>
<Sidebar>
<Logo />
<Link href="/">Welcome</Link>
<Link href="/todo">Todo</Link>
<Link href="/star-wars">Data Fetching</Link>
{""}
</Sidebar>
<Content>{props.children}</Content>
</div>
);
}
function Sidebar(props: { children: JSX.Element }) {
return (
<div id="sidebar" class={"p-5 flex flex-col shrink-0 border-r-2 border-r-gray-200"}>
{props.children}
</div>
);
}
function Content(props: { children: JSX.Element }) {
return (
<div id="page-container">
<div id="page-content" class={"p-5 pb-12 min-h-screen"}>
{props.children}
</div>
</div>
);
}
function Logo() {
return (
<div class={"p-5 mb-2"}>
<a href="/">
<img src={logoUrl} height={64} width={64} alt="logo" />
</a>
</div>
);
}

29
app/layouts/style.css Normal file
View File

@ -0,0 +1,29 @@
/* Links */
a {
text-decoration: none;
}
#sidebar a {
padding: 2px 10px;
margin-left: -10px;
}
#sidebar a.is-active {
background-color: #eee;
}
/* Reset */
body {
margin: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
/* Page Transition Animation */
#page-content {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
body.page-is-transitioning #page-content {
opacity: 0;
}

1
app/layouts/tailwind.css Normal file
View File

@ -0,0 +1 @@
@import "tailwindcss";

40
app/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"scripts": {
"dev": "tsx ./fastify-entry.ts",
"build": "vike build",
"preview": "cross-env NODE_ENV=production tsx ./fastify-entry.ts",
"lint": "biome lint --write .",
"format": "biome format --write ."
},
"dependencies": {
"vike": "^0.4.228",
"@auth/core": "^0.38.0",
"@universal-middleware/core": "^0.4.7",
"@fastify/middie": "^9.0.3",
"@fastify/static": "^8.1.1",
"@universal-middleware/fastify": "^0.5.16",
"fastify": "^5.3.0",
"solid-js": "^1.9.5",
"vike-solid": "^0.7.9",
"telefunc": "^0.2.3"
},
"devDependencies": {
"typescript": "^5.8.3",
"vite": "^6.2.6",
"@biomejs/biome": "1.9.4",
"eslint": "^9.24.0",
"@eslint/js": "^9.24.0",
"typescript-eslint": "^8.29.1",
"globals": "^16.0.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-solid": "^0.14.5",
"@types/node": "^18.19.86",
"tsx": "^4.19.3",
"cross-env": "^7.0.3",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3",
"@tailwindcss/vite": "^4.1.3"
},
"type": "module"
}

21
app/pages/+Head.tsx Normal file
View File

@ -0,0 +1,21 @@
/* eslint-disable solid/no-innerhtml */
// https://vike.dev/Head
export default function HeadDefault() {
return (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${import.meta.env.PUBLIC_ENV__GOOGLE_ANALYTICS}`}
/>
<script
innerHTML={`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${import.meta.env.PUBLIC_ENV__GOOGLE_ANALYTICS}');`}
/>
</>
);
}

18
app/pages/+config.ts Normal file
View File

@ -0,0 +1,18 @@
import vikeSolid from "vike-solid/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: vikeSolid,
} satisfies Config;

View File

@ -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");
};

View File

@ -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");
};

View File

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

16
app/pages/index/+Page.tsx Normal file
View File

@ -0,0 +1,16 @@
import { Counter } from "./Counter.js";
export default function Page() {
return (
<>
<h1 class={"font-bold text-3xl pb-4"}>My Vike app</h1>
This page is:
<ul>
<li>Rendered to HTML.</li>
<li>
Interactive. <Counter />
</li>
</ul>
</>
);
}

View File

@ -0,0 +1,19 @@
import { createSignal } from "solid-js";
export { Counter };
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button
type="button"
class={
"inline-block border border-black rounded bg-gray-200 px-2 py-1 text-xs font-medium uppercase leading-normal"
}
onClick={() => setCount((count) => count + 1)}
>
Counter {count()}
</button>
);
}

View File

@ -0,0 +1,16 @@
import { useData } from "vike-solid/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}
</>
);
}

View File

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { PageContextServer } from "vike/types";
import type { MovieDetails } from "../types.js";
import { useConfig } from "vike-solid/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;
}

View File

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

View File

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { Movie, MovieDetails } from "../types.js";
import { useConfig } from "vike-solid/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 };
});
}

View File

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

13
app/pages/todo/+Page.tsx Normal file
View File

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

View File

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

11
app/pages/todo/+data.ts Normal file
View File

@ -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 };
}

View File

@ -0,0 +1,7 @@
// We use Telefunc (https://telefunc.com) for data mutations. Being able to use Telefunc for fetching initial data is work-in-progress (https://vike.dev/data-fetching#tools).
import { todos } from "../../database/todoItems";
export async function onNewTodo({ text }: { text: string }) {
todos.push({ text });
}

View File

@ -0,0 +1,49 @@
import { onNewTodo } from "./TodoList.telefunc";
import { createSignal, For, untrack } from "solid-js";
export function TodoList(props: { initialTodoItems: { text: string }[] }) {
const [todoItems, setTodoItems] = createSignal(props.initialTodoItems);
const [newTodo, setNewTodo] = createSignal("");
return (
<>
<ul>
<For each={todoItems()}>{(todoItem) => <li>{todoItem.text}</li>}</For>
</ul>
<div>
<form
onSubmit={async (ev) => {
ev.preventDefault();
// Optimistic UI update
setTodoItems((prev) => [...prev, { text: untrack(newTodo) }]);
try {
await onNewTodo({ text: untrack(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()}
class={
"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto p-2 mr-1 mb-1"
}
/>
<button
type="submit"
class={
"text-white bg-blue-700 hover:bg-blue-800 focus:ring-2 focus:outline-hidden focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto p-2"
}
>
Add to-do
</button>
</form>
</div>
</>
);
}

View File

@ -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>;

View File

@ -0,0 +1,22 @@
import { telefunc } from "telefunc";
// 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 telefuncHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => {
const httpResponse = await telefunc({
url: request.url.toString(),
method: request.method,
body: await request.text(),
context: {
...context,
...runtime,
},
});
const { body, statusCode, contentType } = httpResponse;
return new Response(body, {
status: statusCode,
headers: {
"content-type": contentType,
},
});
};

View File

@ -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,
});
};

28
app/tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"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-solid/client"
],
"jsx": "react-jsx",
"jsxImportSource": "solid-js"
},
"exclude": [
"dist"
]
}

12
app/vite.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { telefunc } from "telefunc/vite";
import tailwindcss from "@tailwindcss/vite";
import vikeSolid from "vike-solid/vite";
import { defineConfig } from "vite";
import vike from "vike/plugin";
export default defineConfig({
plugins: [vike(), vikeSolid(), tailwindcss(), telefunc()],
build: {
target: "es2022",
},
});

14
compose-old.yml Normal file
View File

@ -0,0 +1,14 @@
services:
memento-dev:
container_name: memento-dev
build:
context: .
dockerfile: pnpm.Dockerfile
env_file:
- .env
ports:
- "${PORT}:${PORT}"
- "${HMR_PORT}:${HMR_PORT}"
volumes:
- ./app:/app
restart: unless-stopped

View File

@ -1,9 +1,7 @@
services:
memento-dev:
app:
container_name: memento-dev
build:
context: .
dockerfile: pnpm.Dockerfile
image: oven/bun:alpine
env_file:
- .env
ports:
@ -11,4 +9,5 @@ services:
- "${HMR_PORT}:${HMR_PORT}"
volumes:
- ./app:/app
restart: unless-stopped
working_dir: /app
command: bun run dev