mintiljs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/auth/index.ts +1 -0
- package/i18n/index.ts +1 -0
- package/index.ts +1 -0
- package/package.json +68 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/jwt.ts +101 -0
- package/src/auth/middleware.ts +150 -0
- package/src/auth/session.ts +46 -0
- package/src/auth/store.ts +39 -0
- package/src/auth/types.ts +55 -0
- package/src/cli/cli.ts +142 -0
- package/src/cli/generate.ts +142 -0
- package/src/core/client.ts +157 -0
- package/src/core/config.ts +16 -0
- package/src/core/css.ts +72 -0
- package/src/core/islands.ts +90 -0
- package/src/core/logger.ts +37 -0
- package/src/core/routes.ts +247 -0
- package/src/core/runtime.ts +131 -0
- package/src/core/watcher.ts +43 -0
- package/src/i18n/index.ts +57 -0
- package/src/i18n/loader.ts +125 -0
- package/src/i18n/translate.ts +89 -0
- package/src/i18n/types.ts +10 -0
- package/src/index.ts +10 -0
- package/src/render/island.tsx +78 -0
- package/src/render/ssr.tsx +198 -0
- package/src/render/stream.tsx +144 -0
- package/src/router/api.ts +111 -0
- package/src/router/index.ts +9 -0
- package/src/router/middleware.ts +16 -0
- package/src/router/pages.ts +97 -0
- package/src/router/shared.ts +3 -0
- package/src/types/api.ts +106 -0
- package/src/types/config.ts +35 -0
- package/src/types/index.ts +4 -0
- package/src/types/middleware.ts +21 -0
- package/src/types/page.ts +51 -0
- package/src/types/plugin.ts +29 -0
- package/src/utils/fs.ts +46 -0
- package/src/utils/logger.ts +21 -0
- package/src/utils/network.ts +14 -0
- package/templates/default/api/hello.ts +9 -0
- package/templates/default/components/card.tsx +10 -0
- package/templates/default/components/layouts/base.tsx +26 -0
- package/templates/default/lib/greeting.ts +3 -0
- package/templates/default/mintil.config.ts +10 -0
- package/templates/default/package.json +17 -0
- package/templates/default/pages/(*).tsx +11 -0
- package/templates/default/pages/about.tsx +13 -0
- package/templates/default/pages/blog/(:slug).tsx +12 -0
- package/templates/default/pages/counter.tsx +36 -0
- package/templates/default/pages/index.tsx +14 -0
- package/templates/default/public/readme.txt +1 -0
- package/templates/default/styles.css +1 -0
- package/templates/default/tsconfig.json +21 -0
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request object passed to API handlers and middlewares.
|
|
3
|
+
* Provides access to the incoming HTTP request data.
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiRequest {
|
|
6
|
+
/** HTTP method (GET, POST, PUT, etc.). */
|
|
7
|
+
readonly method: string;
|
|
8
|
+
/** Full request URL as a string. */
|
|
9
|
+
readonly url: string;
|
|
10
|
+
/** URL pathname. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** The raw `Request` object. */
|
|
13
|
+
readonly raw: Request;
|
|
14
|
+
/** Get a path parameter by name, or all params as an object. */
|
|
15
|
+
param(key?: string): any;
|
|
16
|
+
/** Get a query string value by name, or all params as an object. */
|
|
17
|
+
query(key?: string): any;
|
|
18
|
+
/** Get a request header by name, or all headers as an object. */
|
|
19
|
+
header(name?: string): any;
|
|
20
|
+
/** Parse the request body as JSON. */
|
|
21
|
+
json<T = any>(): Promise<T>;
|
|
22
|
+
/** Parse the request body as text. */
|
|
23
|
+
text(): Promise<string>;
|
|
24
|
+
/** Parse the request body as FormData. */
|
|
25
|
+
formData(): Promise<FormData>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Response object passed to API handlers and middlewares.
|
|
30
|
+
* Used to construct and send HTTP responses.
|
|
31
|
+
*/
|
|
32
|
+
export interface ApiResponse {
|
|
33
|
+
/** The incoming request. */
|
|
34
|
+
readonly req: ApiRequest;
|
|
35
|
+
/** The response object being built. */
|
|
36
|
+
readonly res: Response;
|
|
37
|
+
/** Set the HTTP response status code. */
|
|
38
|
+
status(code: number): void;
|
|
39
|
+
/** Set a response header. */
|
|
40
|
+
header(name: string, value: string): void;
|
|
41
|
+
/** Set a context-scoped variable (available to downstream handlers/middleware). */
|
|
42
|
+
set(key: string, value: any): void;
|
|
43
|
+
/** Get a context-scoped variable. */
|
|
44
|
+
get(key: string): any;
|
|
45
|
+
/** Map of context-scoped variables. */
|
|
46
|
+
var: Record<string, any>;
|
|
47
|
+
/** Send a JSON response. */
|
|
48
|
+
json(data: any, status?: number, headers?: Record<string, string | string[]>): Response;
|
|
49
|
+
/** Send a text response. */
|
|
50
|
+
text(data: string, status?: number, headers?: Record<string, string | string[]>): Response;
|
|
51
|
+
/** Send an HTML response. */
|
|
52
|
+
html(data: string, status?: number, headers?: Record<string, string | string[]>): Response;
|
|
53
|
+
/** Send a raw body response. */
|
|
54
|
+
body(data: any, status?: number, headers?: Record<string, string | string[]>): Response;
|
|
55
|
+
/** Redirect to another URL (default 302). */
|
|
56
|
+
redirect(location: string, status?: number): Response;
|
|
57
|
+
/** Create a new Response object. */
|
|
58
|
+
newResponse(data: any, status?: number, headers?: Record<string, string | string[]>): Response;
|
|
59
|
+
/** Return a 404 Not Found response. */
|
|
60
|
+
notFound(): Response;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generic API handler that receives the full `ApiResponse`.
|
|
65
|
+
* Useful for catch-all handlers or when you need direct access to `c`.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import type { ApiHandler } from "mintiljs";
|
|
70
|
+
*
|
|
71
|
+
* const handler: ApiHandler = (c) => {
|
|
72
|
+
* return c.json({ ok: true });
|
|
73
|
+
* };
|
|
74
|
+
* export default handler;
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export type ApiHandler = (c: ApiResponse) => Response | Promise<Response> | unknown | Promise<unknown>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Use `ApiRequest` instead.
|
|
81
|
+
*/
|
|
82
|
+
export type MintilRequest = ApiRequest;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Use `ApiResponse` instead.
|
|
86
|
+
*/
|
|
87
|
+
export type MintilResponse = ApiResponse;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Named-method API handler.
|
|
91
|
+
*
|
|
92
|
+
* Export a function with this type from a file in `api/` to handle a specific HTTP method.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import type { ApiMethodHandler } from "mintiljs";
|
|
97
|
+
*
|
|
98
|
+
* export const GET: ApiMethodHandler = (request, response) => {
|
|
99
|
+
* return response.json({ ok: true });
|
|
100
|
+
* };
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export type ApiMethodHandler = (
|
|
104
|
+
request: ApiRequest,
|
|
105
|
+
response: ApiResponse,
|
|
106
|
+
) => Response | Promise<Response> | unknown | Promise<unknown>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing configuration for a MintilJS project.
|
|
3
|
+
* Export a default object of this type from your project's entry file (`mintil.config.ts`).
|
|
4
|
+
*/
|
|
5
|
+
export interface MintilConfig {
|
|
6
|
+
/** Project root directory. Defaults to `process.cwd()`. */
|
|
7
|
+
root?: string;
|
|
8
|
+
/** Port to listen on. Defaults to `process.env.PORT` or `3456`. */
|
|
9
|
+
port?: number;
|
|
10
|
+
/** Hosts allowed/supported for the frontend. Stored for reference only. */
|
|
11
|
+
hosts?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Runtime mode.
|
|
14
|
+
* - `"development"` – enables file-watching auto-reload, dev-only logs, source maps.
|
|
15
|
+
* - `"production"` – minifies CSS/JS, disables watcher.
|
|
16
|
+
* Defaults to `process.env.NODE_ENV` or `"development"`.
|
|
17
|
+
*/
|
|
18
|
+
mode?: "development" | "production";
|
|
19
|
+
/** Whether to bind to all interfaces (`0.0.0.0`). Defaults to `false` (loopback only). */
|
|
20
|
+
host?: boolean;
|
|
21
|
+
/** Plugins to register. Each plugin's `setup` receives the Hono app and resolved config. */
|
|
22
|
+
plugins?: import("./plugin.ts").MintilPlugin[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Union type of the possible runtime modes. */
|
|
26
|
+
export type MintilMode = MintilConfig["mode"] & {};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fully resolved configuration with all defaults applied.
|
|
30
|
+
* Returned by {@link import("../core/config.ts").resolveConfig | `resolveConfig`}.
|
|
31
|
+
*/
|
|
32
|
+
export interface ResolvedConfig extends Required<Omit<MintilConfig, "hosts" | "plugins">> {
|
|
33
|
+
hosts: string[];
|
|
34
|
+
plugins: import("./plugin.ts").MintilPlugin[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { MintilConfig, ResolvedConfig, MintilMode } from "./config.ts";
|
|
2
|
+
export type { PageProps, PageComponent } from "./page.ts";
|
|
3
|
+
export type { ApiRequest, ApiResponse, ApiHandler, ApiMethodHandler, MintilRequest, MintilResponse } from "./api.ts";
|
|
4
|
+
export type { MintilMiddleware } from "./middleware.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ApiResponse } from "./api.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A middleware function that receives an `ApiResponse` and a `next` callback.
|
|
5
|
+
*
|
|
6
|
+
* Export one as the default export of a `middleware.ts` file (at the project root,
|
|
7
|
+
* inside `api/`, or inside a sub-directory of `api/`).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import type { MintilMiddleware } from "mintiljs";
|
|
12
|
+
*
|
|
13
|
+
* const mw: MintilMiddleware = async (c, next) => {
|
|
14
|
+
* console.log(c.req.method, c.req.path);
|
|
15
|
+
* await next();
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* export default mw;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type MintilMiddleware = (c: ApiResponse, next: () => Promise<void>) => Promise<Response | void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props passed to every page component during SSR and client hydration.
|
|
3
|
+
* @example
|
|
4
|
+
* ```tsx
|
|
5
|
+
* export default function Home({ params, searchParams }: PageProps) {
|
|
6
|
+
* return <div>{searchParams.get("q")}</div>;
|
|
7
|
+
* }
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export interface PageProps {
|
|
11
|
+
/** URL path parameters (e.g. `{ slug: "hello" }` for `blog/:slug`). */
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
/** Parsed query-string parameters. */
|
|
14
|
+
searchParams: URLSearchParams;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A page component. Must be the default export of a file inside `pages/`.
|
|
19
|
+
* Receives {@link PageProps} (plus any extra props from `getServerSideProps`).
|
|
20
|
+
*/
|
|
21
|
+
export type PageComponent = (props: any) => import("react").JSX.Element;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context object passed to `getServerSideProps`.
|
|
25
|
+
*/
|
|
26
|
+
export interface ServerSidePropsContext {
|
|
27
|
+
/** URL path parameters. */
|
|
28
|
+
params: Record<string, string>;
|
|
29
|
+
/** Parsed query-string parameters. */
|
|
30
|
+
searchParams: URLSearchParams;
|
|
31
|
+
/** The original Fetch API `Request` object. */
|
|
32
|
+
req: Request;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Server-side data-fetching function.
|
|
37
|
+
*
|
|
38
|
+
* Export a function with this type from any page file. It runs on every request
|
|
39
|
+
* before the page renders. Return `{ props }` to merge additional props into the page component.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* export const getServerSideProps: GetServerSideProps = async ({ params, searchParams }) => {
|
|
44
|
+
* const data = await db.find(params.id);
|
|
45
|
+
* return { props: { data } };
|
|
46
|
+
* };
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export type GetServerSideProps = (
|
|
50
|
+
ctx: ServerSidePropsContext,
|
|
51
|
+
) => Promise<{ props: Record<string, any> }>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A MintilJS plugin.
|
|
6
|
+
*
|
|
7
|
+
* Register plugins via the `plugins` field in {@link import("./config.ts").MintilConfig | `MintilConfig`}.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import type { MintilPlugin } from "mintiljs";
|
|
12
|
+
*
|
|
13
|
+
* const myPlugin: MintilPlugin = {
|
|
14
|
+
* name: "my-plugin",
|
|
15
|
+
* setup(app, config) {
|
|
16
|
+
* app.get("/plugin-route", (c) => c.text("Hello from plugin!"));
|
|
17
|
+
* },
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface MintilPlugin {
|
|
22
|
+
/** Display name used in logs. */
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Called during app startup with the Hono app and resolved config.
|
|
26
|
+
* Can add routes, middleware, or modify the app.
|
|
27
|
+
*/
|
|
28
|
+
setup: (app: Hono, config: ResolvedConfig) => void | Promise<void>;
|
|
29
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function* walkFiles(dir: string): AsyncGenerator<string> {
|
|
5
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (entry.name.startsWith("_")) continue;
|
|
8
|
+
const full = path.join(dir, entry.name);
|
|
9
|
+
if (entry.isDirectory()) {
|
|
10
|
+
yield* walkFiles(full);
|
|
11
|
+
} else if (entry.isFile()) {
|
|
12
|
+
yield full;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function pathExists(p: string): Promise<boolean> {
|
|
18
|
+
try {
|
|
19
|
+
await stat(p);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stripExt(file: string): string {
|
|
27
|
+
const ext = path.extname(file);
|
|
28
|
+
return ext ? file.slice(0, -ext.length) : file;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fileToRoutePath(filePath: string, baseDir: string): string {
|
|
32
|
+
const relative = path.relative(baseDir, filePath);
|
|
33
|
+
let noExt = stripExt(relative);
|
|
34
|
+
const parts = noExt.split(path.sep).map((part) => {
|
|
35
|
+
if (part === "(*)" || part === "*") return "*";
|
|
36
|
+
if (part.startsWith("(") && part.endsWith(")")) {
|
|
37
|
+
return part.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
return part;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let route = "/" + parts.join("/");
|
|
43
|
+
route = route.replace(/\/index$/i, "");
|
|
44
|
+
if (!route || route === "/index") return "/";
|
|
45
|
+
return route;
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const color = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
green: "\x1b[32m",
|
|
4
|
+
yellow: "\x1b[33m",
|
|
5
|
+
red: "\x1b[31m",
|
|
6
|
+
cyan: "\x1b[36m",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function formatDuration(ms: number): string {
|
|
10
|
+
if (ms >= 1000) {
|
|
11
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
12
|
+
}
|
|
13
|
+
return `${ms.toFixed(0)}ms`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function statusColorFor(status: number): string {
|
|
17
|
+
if (status >= 500) return color.red;
|
|
18
|
+
if (status >= 400) return color.yellow;
|
|
19
|
+
if (status >= 300) return color.cyan;
|
|
20
|
+
return color.green;
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export function getLocalIp(): string | null {
|
|
4
|
+
const interfaces = os.networkInterfaces();
|
|
5
|
+
for (const addrs of Object.values(interfaces)) {
|
|
6
|
+
if (!addrs) continue;
|
|
7
|
+
for (const addr of addrs) {
|
|
8
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
9
|
+
return addr.address;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { buildGreeting } from "@app/lib/greeting";
|
|
2
|
+
import type { ApiHandler } from "mintiljs";
|
|
3
|
+
|
|
4
|
+
const handler: ApiHandler = (c) => {
|
|
5
|
+
const name = c.req.query("name") ?? "world";
|
|
6
|
+
return c.json({ message: buildGreeting(name) });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default handler;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function Card({ title, children }: { title: string; children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mt-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
|
6
|
+
<h2 className="mb-2 text-lg font-semibold">{title}</h2>
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function BaseLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<html lang="pt">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charSet="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title>Mintil App</title>
|
|
10
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body className="min-h-screen bg-gray-50 text-gray-900 antialiased flex flex-col">
|
|
13
|
+
<header className="border-b border-gray-200 bg-white">
|
|
14
|
+
<nav className="mx-auto flex max-w-4xl items-center gap-6 px-4 py-4">
|
|
15
|
+
<a href="/" className="text-sm font-medium text-blue-600 hover:text-blue-800">Home</a>
|
|
16
|
+
<a href="/about" className="text-sm font-medium text-blue-600 hover:text-blue-800">About</a>
|
|
17
|
+
</nav>
|
|
18
|
+
</header>
|
|
19
|
+
<main className="mx-auto w-full max-w-4xl flex-1 px-4 py-10">{children}</main>
|
|
20
|
+
<footer className="border-t border-gray-200 py-6 text-center text-sm text-gray-500">
|
|
21
|
+
Built with MintilJs
|
|
22
|
+
</footer>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-mintil-app",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "mintil dev",
|
|
7
|
+
"start": "mintil start"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"typescript": "^5"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"mintiljs": "*",
|
|
14
|
+
"postcss": "^8",
|
|
15
|
+
"@tailwindcss/postcss": "^4"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function CatchAllPage({ params }: { params: Record<string, string> }) {
|
|
4
|
+
const path = params["*"] ?? "";
|
|
5
|
+
return (
|
|
6
|
+
<div className="text-center py-20">
|
|
7
|
+
<h1 className="text-6xl font-bold text-gray-300 mb-4">404</h1>
|
|
8
|
+
<p className="text-gray-500">No page matched <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">/{path}</code>.</p>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function AboutPage() {
|
|
4
|
+
return (
|
|
5
|
+
<section>
|
|
6
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">About</h1>
|
|
7
|
+
<p className="text-gray-600 leading-relaxed">
|
|
8
|
+
MintilJs is a minimal SSR framework that runs on Bun. Pages are rendered with React on the server
|
|
9
|
+
and streamed as HTML — no client-side JavaScript required.
|
|
10
|
+
</p>
|
|
11
|
+
</section>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function BlogPostPage({ params }: { params: Record<string, string> }) {
|
|
4
|
+
return (
|
|
5
|
+
<article className="prose prose-gray max-w-none">
|
|
6
|
+
<h1 className="mb-6 text-3xl font-bold capitalize tracking-tight">{params.slug}</h1>
|
|
7
|
+
<p className="text-gray-600">
|
|
8
|
+
This is a dynamic blog post page. The slug <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">{params.slug}</code> was extracted from the URL.
|
|
9
|
+
</p>
|
|
10
|
+
</article>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const useClient = true;
|
|
4
|
+
|
|
5
|
+
export default function CounterPage() {
|
|
6
|
+
const [count, setCount] = React.useState(0);
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex flex-col items-center gap-4 py-12">
|
|
9
|
+
<h1 className="text-3xl font-bold tracking-tight">Client Counter</h1>
|
|
10
|
+
<p className="text-gray-600">This page is interactive — useState works because it is hydrated on the client.</p>
|
|
11
|
+
<div className="flex items-center gap-4">
|
|
12
|
+
<button
|
|
13
|
+
onClick={() => setCount((c) => c - 1)}
|
|
14
|
+
className="rounded-lg bg-gray-200 px-4 py-2 text-lg font-semibold hover:bg-gray-300"
|
|
15
|
+
>
|
|
16
|
+
-1
|
|
17
|
+
</button>
|
|
18
|
+
<span className="min-w-[3rem] text-center text-2xl font-mono font-bold tabular-nums">
|
|
19
|
+
{count}
|
|
20
|
+
</span>
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => setCount((c) => c + 1)}
|
|
23
|
+
className="rounded-lg bg-blue-600 px-4 py-2 text-lg font-semibold text-white hover:bg-blue-700"
|
|
24
|
+
>
|
|
25
|
+
+1
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
<button
|
|
29
|
+
onClick={() => setCount(0)}
|
|
30
|
+
className="text-sm text-gray-500 underline hover:text-gray-700"
|
|
31
|
+
>
|
|
32
|
+
reset
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Card from "@app/components/card";
|
|
3
|
+
|
|
4
|
+
export default function HomePage() {
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">Welcome to MintilJs</h1>
|
|
8
|
+
<p className="mb-4 text-gray-600">A minimal SSR framework powered by Bun, Hono, React and Tailwind CSS.</p>
|
|
9
|
+
<Card title="Server rendered">
|
|
10
|
+
This component was imported via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">@app/components/card</code>.
|
|
11
|
+
</Card>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is a static asset served under /assets/readme.txt.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"baseUrl": ".",
|
|
16
|
+
"paths": {
|
|
17
|
+
"@app/*": ["./*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["**/*.ts", "**/*.tsx"]
|
|
21
|
+
}
|