mintiljs 0.1.0 → 0.1.1
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 +54 -10
- package/package.json +1 -1
- package/src/cli/cli.ts +18 -9
- package/src/core/config.ts +3 -0
- package/src/core/routes.ts +22 -4
- package/src/core/runtime.ts +19 -10
- package/src/render/ssr.tsx +39 -0
- package/src/router/pages.ts +3 -1
- package/src/types/config.ts +2 -0
- package/src/types/page.ts +4 -1
- package/templates/default/api/counter.ts +10 -0
- package/templates/default/api/middleware.ts +8 -0
- package/templates/default/components/layouts/base.tsx +6 -0
- package/templates/default/i18n/messages/en.json +12 -0
- package/templates/default/islands/Counter.tsx +34 -0
- package/templates/default/middleware.ts +8 -0
- package/templates/default/package.json +4 -3
- package/templates/default/pages/about.tsx +12 -0
- package/templates/default/pages/blog/index.tsx +28 -0
- package/templates/default/pages/blog/layout.tsx +12 -0
- package/templates/default/pages/i18n-test.tsx +26 -0
- package/templates/default/pages/index.tsx +22 -0
- package/templates/default/pages/island-demo.tsx +14 -0
- package/templates/default/pages/server-data.tsx +36 -0
- package/templates/default/pages/ssr-counter.tsx +19 -0
- package/templates/default/public/readme.txt +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
MintilJS is a full-stack framework built on **Bun**, **Hono**, and **React 19**. Drop files in `pages/` and they become SSR routes. Add `api/` files for JSON endpoints. Throw in `islands/` for partial hydration. All with zero configuration.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
bun create
|
|
8
|
+
bun create mintil my-app
|
|
9
9
|
cd my-app
|
|
10
|
+
bun install
|
|
10
11
|
bun run mintil dev
|
|
11
12
|
```
|
|
12
13
|
|
|
@@ -28,6 +29,15 @@ bun run mintil dev
|
|
|
28
29
|
|
|
29
30
|
## Install
|
|
30
31
|
|
|
32
|
+
```sh
|
|
33
|
+
bun create mintil my-app
|
|
34
|
+
cd my-app
|
|
35
|
+
bun install
|
|
36
|
+
bun run mintil dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or add to an existing project:
|
|
40
|
+
|
|
31
41
|
```sh
|
|
32
42
|
bun add mintiljs
|
|
33
43
|
```
|
|
@@ -113,8 +123,9 @@ import type { MintilConfig } from "mintiljs";
|
|
|
113
123
|
|
|
114
124
|
export default {
|
|
115
125
|
port: 3456,
|
|
116
|
-
host: false,
|
|
117
|
-
mode: "development",
|
|
126
|
+
host: false, // true = bind to 0.0.0.0
|
|
127
|
+
mode: "development", // "development" | "production"
|
|
128
|
+
assetsPath: "/assets", // prefix for public/ files; "/" to serve at root
|
|
118
129
|
plugins: [],
|
|
119
130
|
} satisfies MintilConfig;
|
|
120
131
|
```
|
|
@@ -123,7 +134,7 @@ export default {
|
|
|
123
134
|
|
|
124
135
|
| Command | Description |
|
|
125
136
|
|---|---|
|
|
126
|
-
| `mintil init <name>` | Scaffold a new project |
|
|
137
|
+
| `mintil init <name>` | Scaffold a new project (use `.` for current dir) |
|
|
127
138
|
| `mintil dev` | Dev mode with auto-reload |
|
|
128
139
|
| `mintil start` | Production mode |
|
|
129
140
|
| `mintil g page <n>` | Scaffold a page |
|
|
@@ -135,16 +146,49 @@ export default {
|
|
|
135
146
|
### Auth (`mintiljs/auth`)
|
|
136
147
|
|
|
137
148
|
```ts
|
|
149
|
+
// api/login.ts
|
|
138
150
|
import { createAuthMiddleware, InMemorySessionStore } from "mintiljs/auth";
|
|
139
151
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
|
|
153
|
+
const store = new InMemorySessionStore();
|
|
154
|
+
|
|
155
|
+
export const auth = createAuthMiddleware({
|
|
156
|
+
jwt: { secret: JWT_SECRET, expiresIn: "1h" },
|
|
157
|
+
session: { store, maxAge: 86400, cookieName: "session", cookiePath: "/" },
|
|
143
158
|
});
|
|
144
159
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
// POST /api/login — sign JWT and return it
|
|
161
|
+
import type { ApiMethodHandler } from "mintiljs";
|
|
162
|
+
|
|
163
|
+
export const POST: ApiMethodHandler = async (request, response) => {
|
|
164
|
+
const { username, password } = await request.json();
|
|
165
|
+
if (username !== "admin" || password !== "123456") {
|
|
166
|
+
return response.json({ error: "Invalid credentials" }, 401);
|
|
167
|
+
}
|
|
168
|
+
const token = await auth.signJWT({ sub: username, userId: username, role: "admin" });
|
|
169
|
+
return response.json({ token });
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// api/admin/middleware.ts — protect all /api/admin/* routes
|
|
175
|
+
import { auth } from "../login";
|
|
176
|
+
|
|
177
|
+
export default auth.requireAuth;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// api/me.ts — use the verified token
|
|
182
|
+
import type { ApiMethodHandler } from "mintiljs";
|
|
183
|
+
import { auth } from "./login";
|
|
184
|
+
|
|
185
|
+
export const GET: ApiMethodHandler = async (request, response) => {
|
|
186
|
+
const token = request.header("Authorization")?.slice(7);
|
|
187
|
+
const payload = token ? await auth.verifyJWT(token) : null;
|
|
188
|
+
return payload
|
|
189
|
+
? response.json({ user: { id: payload.userId, name: payload.sub } })
|
|
190
|
+
: response.json({ error: "Unauthorized" }, 401);
|
|
191
|
+
};
|
|
148
192
|
```
|
|
149
193
|
|
|
150
194
|
### i18n (`mintiljs/i18n`)
|
package/package.json
CHANGED
package/src/cli/cli.ts
CHANGED
|
@@ -38,13 +38,17 @@ function showHelp() {
|
|
|
38
38
|
console.log(`Usage: mintil <command>
|
|
39
39
|
|
|
40
40
|
Commands:
|
|
41
|
-
init <name> Create a new Mintil project
|
|
42
|
-
dev Run the project in development mode
|
|
41
|
+
init <name> Create a new Mintil project (use "." for current dir)
|
|
42
|
+
dev Run the project in development mode
|
|
43
43
|
start Run the project (uses config mode or defaults to production)
|
|
44
44
|
generate <t> <n> Scaffold a page, api, component, or island (alias: g)
|
|
45
45
|
help Show this help message
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Quick start:
|
|
48
|
+
bun create mintil my-app
|
|
49
|
+
cd my-app
|
|
50
|
+
bun install
|
|
51
|
+
bun run mintil dev
|
|
48
52
|
`);
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -54,15 +58,18 @@ async function initProject(name?: string) {
|
|
|
54
58
|
process.exit(1);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
const
|
|
61
|
+
const useCwd = name === ".";
|
|
62
|
+
const target = useCwd ? process.cwd() : path.resolve(process.cwd(), name);
|
|
58
63
|
const template = path.resolve(import.meta.dir, "..", "..", "templates", "default");
|
|
59
64
|
|
|
60
65
|
try {
|
|
61
66
|
await fs.access(target);
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
if (!useCwd) {
|
|
68
|
+
console.error(`Directory already exists: ${target}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
64
71
|
} catch {
|
|
65
|
-
// expected: directory does not exist
|
|
72
|
+
// expected: directory does not exist (unless useCwd)
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
await fs.cp(template, target, { recursive: true });
|
|
@@ -71,14 +78,16 @@ async function initProject(name?: string) {
|
|
|
71
78
|
const pkgPath = path.join(target, "package.json");
|
|
72
79
|
try {
|
|
73
80
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
74
|
-
pkg.name =
|
|
81
|
+
pkg.name = path.basename(target);
|
|
75
82
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
76
83
|
} catch {
|
|
77
84
|
// ignore if no package.json
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
console.log(`Created Mintil project at ${target}`);
|
|
81
|
-
console.log(`\nNext steps
|
|
88
|
+
console.log(`\nNext steps:`);
|
|
89
|
+
if (!useCwd) console.log(` cd ${name}`);
|
|
90
|
+
console.log(` bun install\n mintil dev`);
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
async function runProject(command: "dev" | "start") {
|
package/src/core/config.ts
CHANGED
|
@@ -5,12 +5,15 @@ export function resolveConfig(root: string, userConfig?: MintilConfig): Resolved
|
|
|
5
5
|
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
6
6
|
const envMode = process.env.NODE_ENV === "production" ? "production" : "development";
|
|
7
7
|
|
|
8
|
+
const assetsPath = userConfig?.assetsPath ?? "/";
|
|
9
|
+
|
|
8
10
|
return {
|
|
9
11
|
root,
|
|
10
12
|
port: userConfig?.port ?? envPort ?? 3456,
|
|
11
13
|
hosts: userConfig?.hosts ?? [],
|
|
12
14
|
mode: userConfig?.mode ?? (envMode as ResolvedConfig["mode"]),
|
|
13
15
|
host: userConfig?.host ?? false,
|
|
16
|
+
assetsPath: assetsPath.startsWith("/") ? assetsPath : `/${assetsPath}`,
|
|
14
17
|
plugins: userConfig?.plugins ?? [],
|
|
15
18
|
};
|
|
16
19
|
}
|
package/src/core/routes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import type { Hono } from "hono";
|
|
4
4
|
import type { Context } from "hono";
|
|
5
|
-
import { renderPage } from "../render/ssr.tsx";
|
|
5
|
+
import { renderPage, renderClientShell } from "../render/ssr.tsx";
|
|
6
6
|
import { tryStreamPage } from "../render/stream.tsx";
|
|
7
7
|
import { resetIslands, getUsedIslands, getIslandDef } from "../render/island.tsx";
|
|
8
8
|
import {
|
|
@@ -125,7 +125,7 @@ export async function registerPageRoutes(
|
|
|
125
125
|
|
|
126
126
|
let clientBundle: string | null = null;
|
|
127
127
|
let clientBundleName: string | null = null;
|
|
128
|
-
if (page.useClient && frameworkJs) {
|
|
128
|
+
if (page.useClient && !page.useServer && frameworkJs) {
|
|
129
129
|
const js = await buildClientBundle(page.file, page.route, { mode: resolved.mode });
|
|
130
130
|
if (js) {
|
|
131
131
|
const bundleName = routeToBundleName(page.route);
|
|
@@ -152,7 +152,7 @@ export async function registerPageRoutes(
|
|
|
152
152
|
const props = pagePropsFromContext(c);
|
|
153
153
|
|
|
154
154
|
let extraProps: Record<string, any> | undefined;
|
|
155
|
-
if (page.getServerSideProps) {
|
|
155
|
+
if (page.getServerSideProps && !page.useServer) {
|
|
156
156
|
try {
|
|
157
157
|
const result = await page.getServerSideProps({
|
|
158
158
|
params: props.params,
|
|
@@ -180,9 +180,27 @@ export async function registerPageRoutes(
|
|
|
180
180
|
extraProps,
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
if (page.useClient && clientBundle) {
|
|
184
|
+
// useClient pages skip SSR of the React tree (hooks like useState
|
|
185
|
+
// don't work server-side). Render just the layout shell with an
|
|
186
|
+
// empty root div — the client bundle hydrates the full component.
|
|
187
|
+
const html = await renderClientShell(pageOpts);
|
|
188
|
+
return c.html(html);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (page.useServer) {
|
|
192
|
+
// useServer pages are async-first — skip island detection,
|
|
193
|
+
// skip getServerSideProps (component fetches its own data),
|
|
194
|
+
// go straight to streaming (supports Suspense/async components).
|
|
195
|
+
const streamResponse = await tryStreamPage(page.component, pageOpts);
|
|
196
|
+
if (streamResponse) return streamResponse;
|
|
197
|
+
// Fallback to sync render (will show error boundary if async)
|
|
198
|
+
const html = await renderPage(page.component, pageOpts);
|
|
199
|
+
return c.html(html);
|
|
200
|
+
}
|
|
201
|
+
|
|
183
202
|
// Quick sync pass to detect used islands
|
|
184
203
|
resetIslands();
|
|
185
|
-
const React = await import("react");
|
|
186
204
|
const { renderToString } = await import("react-dom/server");
|
|
187
205
|
const quickElement = buildQuickElement(page.component, pageOpts, layout);
|
|
188
206
|
try { renderToString(quickElement); } catch {}
|
package/src/core/runtime.ts
CHANGED
|
@@ -34,19 +34,24 @@ export async function createMintilApp(
|
|
|
34
34
|
return c.text(css, 200, { "Content-Type": "text/css" });
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Serve static files from public/ — works as a pass-through middleware so
|
|
38
|
+
// page/API routes still resolve when the file doesn't exist.
|
|
39
|
+
const prefix = resolved.assetsPath === "/" ? "" : resolved.assetsPath.replace(/\/+$/, "");
|
|
40
|
+
app.use("*", async (c, next) => {
|
|
41
|
+
if (c.req.method !== "GET") return next();
|
|
42
|
+
const reqPath = c.req.path;
|
|
43
|
+
// Skip internal routes
|
|
44
|
+
if (reqPath.startsWith("/_mintil") || reqPath === "/styles.css") return next();
|
|
45
|
+
// If a prefix is configured, only handle paths under it
|
|
46
|
+
if (prefix && !reqPath.startsWith(prefix)) return next();
|
|
47
|
+
const subpath = prefix ? reqPath.slice(prefix.length).replace(/^\//, "") : reqPath.replace(/^\//, "");
|
|
39
48
|
const filePath = path.join(resolved.root, "public", subpath);
|
|
40
|
-
if (!filePath.startsWith(path.join(resolved.root, "public")))
|
|
41
|
-
return c.notFound();
|
|
42
|
-
}
|
|
49
|
+
if (!filePath.startsWith(path.join(resolved.root, "public"))) return next();
|
|
43
50
|
try {
|
|
44
51
|
const file = Bun.file(filePath);
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return c.notFound();
|
|
49
|
-
}
|
|
52
|
+
if (await file.exists()) return new Response(file);
|
|
53
|
+
} catch { /* fall through */ }
|
|
54
|
+
return next();
|
|
50
55
|
});
|
|
51
56
|
|
|
52
57
|
// Auto-register i18n API if i18n/messages directory exists
|
|
@@ -129,3 +134,7 @@ export async function start(config?: MintilConfig): Promise<ReturnType<typeof Bu
|
|
|
129
134
|
export function resetStarted() {
|
|
130
135
|
started = false;
|
|
131
136
|
}
|
|
137
|
+
|
|
138
|
+
function escapeRegex(str: string): string {
|
|
139
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
140
|
+
}
|
package/src/render/ssr.tsx
CHANGED
|
@@ -196,3 +196,42 @@ function buildInnerPage(Page: PageComponent, pageProps: Record<string, any>) {
|
|
|
196
196
|
React.createElement(Page, pageProps),
|
|
197
197
|
);
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
function buildEmptyShell() {
|
|
201
|
+
return React.createElement("div", { id: "__mintil-root" });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Render a shell HTML for `useClient` pages — wraps the layout around an
|
|
206
|
+
* empty root div, then lets the client bundle hydrate the full component.
|
|
207
|
+
* Avoids SSR errors from hooks (useState, useEffect, etc.) that require
|
|
208
|
+
* a client-side React dispatcher.
|
|
209
|
+
*/
|
|
210
|
+
export async function renderClientShell(
|
|
211
|
+
opts: RenderOptions,
|
|
212
|
+
): Promise<string> {
|
|
213
|
+
const pageProps = { params: opts.params, searchParams: opts.searchParams, ...opts.extraProps };
|
|
214
|
+
|
|
215
|
+
const layout = opts.layout ?? (await loadLayout(opts.root, opts.nonce));
|
|
216
|
+
const css = await processCss(opts.root, opts.mode);
|
|
217
|
+
|
|
218
|
+
let content: string;
|
|
219
|
+
try {
|
|
220
|
+
const pageElement = layout
|
|
221
|
+
? React.createElement(layout, { children: buildEmptyShell() })
|
|
222
|
+
: buildEmptyShell();
|
|
223
|
+
content = renderToString(pageElement);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
226
|
+
console.error(`[mintil] client-shell render error for ${opts.route}:`, error.message);
|
|
227
|
+
const fallback = renderToString(
|
|
228
|
+
layout
|
|
229
|
+
? React.createElement(layout, { children: ErrorFallback({ error }) })
|
|
230
|
+
: ErrorFallback({ error }),
|
|
231
|
+
);
|
|
232
|
+
content = fallback;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const extra = buildExtraScripts(opts, pageProps);
|
|
236
|
+
return buildHtmlShell(content, extra, css, opts.route);
|
|
237
|
+
}
|
package/src/router/pages.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface PageRoute {
|
|
|
10
10
|
mod: any;
|
|
11
11
|
component: PageComponent;
|
|
12
12
|
useClient: boolean;
|
|
13
|
+
useServer: boolean;
|
|
13
14
|
getServerSideProps?: GetServerSideProps;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -36,8 +37,9 @@ export async function collectPageRoutes(root: string, nonce?: string): Promise<P
|
|
|
36
37
|
const component = mod.default ?? mod.Page ?? null;
|
|
37
38
|
if (typeof component !== "function") continue;
|
|
38
39
|
const useClient = mod.useClient === true;
|
|
40
|
+
const useServer = mod.useServer === true;
|
|
39
41
|
const getServerSideProps = typeof mod.getServerSideProps === "function" ? mod.getServerSideProps.bind(null) : undefined;
|
|
40
|
-
routes.push({ file, route, mod, component, useClient, getServerSideProps });
|
|
42
|
+
routes.push({ file, route, mod, component, useClient, useServer, getServerSideProps });
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
routes.sort((a, b) => sortRoute(a.route, b.route));
|
package/src/types/config.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface MintilConfig {
|
|
|
18
18
|
mode?: "development" | "production";
|
|
19
19
|
/** Whether to bind to all interfaces (`0.0.0.0`). Defaults to `false` (loopback only). */
|
|
20
20
|
host?: boolean;
|
|
21
|
+
/** URL path prefix for static assets from `public/`. Defaults to `/assets`. Set to `"/"` to serve at root. */
|
|
22
|
+
assetsPath?: string;
|
|
21
23
|
/** Plugins to register. Each plugin's `setup` receives the Hono app and resolved config. */
|
|
22
24
|
plugins?: import("./plugin.ts").MintilPlugin[];
|
|
23
25
|
}
|
package/src/types/page.ts
CHANGED
|
@@ -17,8 +17,11 @@ export interface PageProps {
|
|
|
17
17
|
/**
|
|
18
18
|
* A page component. Must be the default export of a file inside `pages/`.
|
|
19
19
|
* Receives {@link PageProps} (plus any extra props from `getServerSideProps`).
|
|
20
|
+
*
|
|
21
|
+
* When `useServer` is `true`, the component can be `async` and fetch data directly
|
|
22
|
+
* without `getServerSideProps`. React 19's streaming SSR handles Suspense natively.
|
|
20
23
|
*/
|
|
21
|
-
export type PageComponent = (props: any) => import("react").JSX.Element
|
|
24
|
+
export type PageComponent = (props: any) => import("react").JSX.Element | Promise<import("react").JSX.Element>;
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Context object passed to `getServerSideProps`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ApiHandler } from "mintiljs";
|
|
2
|
+
|
|
3
|
+
const counter: ApiHandler = (c) => {
|
|
4
|
+
const count = parseInt(c.req.query("count") ?? "0", 10);
|
|
5
|
+
const action = c.req.query("action") ?? "inc";
|
|
6
|
+
const next = action === "inc" ? count + 1 : Math.max(0, count - 1);
|
|
7
|
+
return c.json({ count: next });
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default counter;
|
|
@@ -14,6 +14,12 @@ export default function BaseLayout({ children }: { children: React.ReactNode })
|
|
|
14
14
|
<nav className="mx-auto flex max-w-4xl items-center gap-6 px-4 py-4">
|
|
15
15
|
<a href="/" className="text-sm font-medium text-blue-600 hover:text-blue-800">Home</a>
|
|
16
16
|
<a href="/about" className="text-sm font-medium text-blue-600 hover:text-blue-800">About</a>
|
|
17
|
+
<a href="/counter" className="text-sm font-medium text-blue-600 hover:text-blue-800">Counter</a>
|
|
18
|
+
<a href="/ssr-counter" className="text-sm font-medium text-blue-600 hover:text-blue-800">SSR</a>
|
|
19
|
+
<a href="/island-demo" className="text-sm font-medium text-blue-600 hover:text-blue-800">Islands</a>
|
|
20
|
+
<a href="/server-data" className="text-sm font-medium text-blue-600 hover:text-blue-800">Server</a>
|
|
21
|
+
<a href="/blog" className="text-sm font-medium text-blue-600 hover:text-blue-800">Blog</a>
|
|
22
|
+
<a href="/i18n-test" className="text-sm font-medium text-blue-600 hover:text-blue-800">i18n</a>
|
|
17
23
|
</nav>
|
|
18
24
|
</header>
|
|
19
25
|
<main className="mx-auto w-full max-w-4xl flex-1 px-4 py-10">{children}</main>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"greeting": "Hello",
|
|
3
|
+
"nav.home": "Home",
|
|
4
|
+
"nav.about": "About",
|
|
5
|
+
"nav.counter": "Counter",
|
|
6
|
+
"nav.ssr": "SSR Counter",
|
|
7
|
+
"nav.i18n": "i18n",
|
|
8
|
+
"nav.blog": "Blog",
|
|
9
|
+
"welcome": "Welcome to {site}!",
|
|
10
|
+
"farewell": "Goodbye",
|
|
11
|
+
"builtWith": "Built with MintilJs"
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface CounterProps {
|
|
4
|
+
initial?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function Counter({ initial = 0 }: CounterProps) {
|
|
8
|
+
const [count, setCount] = React.useState(initial);
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex items-center gap-4">
|
|
11
|
+
<button
|
|
12
|
+
onClick={() => setCount((c) => c - 1)}
|
|
13
|
+
className="rounded-lg bg-gray-200 px-4 py-2 text-lg font-semibold hover:bg-gray-300"
|
|
14
|
+
>
|
|
15
|
+
-1
|
|
16
|
+
</button>
|
|
17
|
+
<span className="min-w-[3rem] text-center text-2xl font-mono font-bold tabular-nums">
|
|
18
|
+
{count}
|
|
19
|
+
</span>
|
|
20
|
+
<button
|
|
21
|
+
onClick={() => setCount((c) => c + 1)}
|
|
22
|
+
className="rounded-lg bg-blue-600 px-4 py-2 text-lg font-semibold text-white hover:bg-blue-700"
|
|
23
|
+
>
|
|
24
|
+
+1
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setCount(0)}
|
|
28
|
+
className="text-sm text-gray-500 underline hover:text-gray-700"
|
|
29
|
+
>
|
|
30
|
+
reset
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-mintil-app",
|
|
3
|
-
"module": "index.ts",
|
|
4
3
|
"type": "module",
|
|
5
4
|
"scripts": {
|
|
6
5
|
"dev": "mintil dev",
|
|
@@ -9,9 +8,11 @@
|
|
|
9
8
|
"peerDependencies": {
|
|
10
9
|
"typescript": "^5"
|
|
11
10
|
},
|
|
12
|
-
"
|
|
13
|
-
"mintiljs": "*",
|
|
11
|
+
"optionalDependencies": {
|
|
14
12
|
"postcss": "^8",
|
|
15
13
|
"@tailwindcss/postcss": "^4"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"mintiljs": "*"
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -8,6 +8,18 @@ export default function AboutPage() {
|
|
|
8
8
|
MintilJs is a minimal SSR framework that runs on Bun. Pages are rendered with React on the server
|
|
9
9
|
and streamed as HTML — no client-side JavaScript required.
|
|
10
10
|
</p>
|
|
11
|
+
<div className="mt-8 space-y-4">
|
|
12
|
+
<h2 className="text-xl font-semibold">Features</h2>
|
|
13
|
+
<ul className="list-disc pl-6 space-y-2 text-gray-600">
|
|
14
|
+
<li>File-based routing — <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">pages/</code> for SSR, <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">api/</code> for JSON</li>
|
|
15
|
+
<li>Zero JS in browser unless you opt in with <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">useClient</code></li>
|
|
16
|
+
<li>Islands — independent interactive components <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">islands/</code></li>
|
|
17
|
+
<li>Internationalization via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">mintiljs/i18n</code></li>
|
|
18
|
+
<li>Auth module via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">mintiljs/auth</code></li>
|
|
19
|
+
<li>Hierarchical middleware — global, API, scoped</li>
|
|
20
|
+
<li>Tailwind v4 via PostCSS</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
11
23
|
</section>
|
|
12
24
|
);
|
|
13
25
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const posts = [
|
|
4
|
+
{ slug: "hello-world", title: "Hello World" },
|
|
5
|
+
{ slug: "getting-started", title: "Getting Started with MintilJS" },
|
|
6
|
+
{ slug: "ssr-vs-csr", title: "SSR vs CSR" },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default function BlogIndex() {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">Blog</h1>
|
|
13
|
+
<ul className="space-y-4">
|
|
14
|
+
{posts.map((post) => (
|
|
15
|
+
<li key={post.slug}>
|
|
16
|
+
<a
|
|
17
|
+
href={`/blog/${post.slug}`}
|
|
18
|
+
className="block rounded-lg border border-gray-200 bg-white p-4 transition hover:border-blue-300 hover:shadow-sm"
|
|
19
|
+
>
|
|
20
|
+
<h2 className="text-lg font-semibold text-blue-600">{post.title}</h2>
|
|
21
|
+
<p className="mt-1 text-sm text-gray-500">/{post.slug}</p>
|
|
22
|
+
</a>
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mx-auto max-w-2xl">
|
|
6
|
+
<nav className="mb-8 flex items-center gap-4 text-sm text-gray-500">
|
|
7
|
+
<a href="/blog" className="font-medium text-blue-600 hover:text-blue-800">All Posts</a>
|
|
8
|
+
</nav>
|
|
9
|
+
{children}
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { getMessages, t, format } from "mintiljs/i18n";
|
|
3
|
+
import type { GetServerSideProps } from "mintiljs";
|
|
4
|
+
|
|
5
|
+
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
6
|
+
const locale = (ctx.searchParams?.lang as string) || "en";
|
|
7
|
+
const messages = await getMessages(locale);
|
|
8
|
+
return { props: { locale, messages } };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function I18nPage({ locale, messages }: { locale: string; messages: Record<string, string> }) {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">i18n Example</h1>
|
|
15
|
+
<p className="mb-2 text-gray-600">Locale: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">{locale}</code></p>
|
|
16
|
+
<div className="mt-6 space-y-2 rounded-lg border border-gray-200 bg-white p-6">
|
|
17
|
+
<p><strong>greeting:</strong> {t(messages, "greeting")}</p>
|
|
18
|
+
<p><strong>nav.home:</strong> {t(messages, "nav.home")}</p>
|
|
19
|
+
<p><strong>welcome:</strong> {t(messages, "welcome", { site: "MintilJS" })}</p>
|
|
20
|
+
<p><strong>format() example:</strong> {format("Welcome to {site}, {user}!", { site: "MintilJS", user: "dev" })}</p>
|
|
21
|
+
<p className="text-sm text-gray-500">Total messages: {Object.keys(messages).length}</p>
|
|
22
|
+
</div>
|
|
23
|
+
<p className="mt-4 text-sm text-gray-400">Try: <code>?lang=pt-BR</code></p>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Card from "@app/components/card";
|
|
3
3
|
|
|
4
|
+
const features = [
|
|
5
|
+
{ href: "/about", title: "About", desc: "Learn more about MintilJS" },
|
|
6
|
+
{ href: "/counter", title: "Counter (useClient)", desc: "Interactive page with client-side JavaScript" },
|
|
7
|
+
{ href: "/ssr-counter", title: "SSR Counter", desc: "Server-rendered form-based interaction, zero JS" },
|
|
8
|
+
{ href: "/island-demo", title: "Islands", desc: "Hydratable interactive components" },
|
|
9
|
+
{ href: "/server-data", title: "Server (useServer)", desc: "Async component fetching data directly — zero JS" },
|
|
10
|
+
{ href: "/blog", title: "Blog", desc: "Dynamic routes with (:slug).tsx" },
|
|
11
|
+
{ href: "/i18n-test", title: "i18n", desc: "Internationalization with getMessages and t()" },
|
|
12
|
+
];
|
|
13
|
+
|
|
4
14
|
export default function HomePage() {
|
|
5
15
|
return (
|
|
6
16
|
<div>
|
|
@@ -9,6 +19,18 @@ export default function HomePage() {
|
|
|
9
19
|
<Card title="Server rendered">
|
|
10
20
|
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
21
|
</Card>
|
|
22
|
+
<div className="mt-8 grid gap-4 sm:grid-cols-2">
|
|
23
|
+
{features.map((f) => (
|
|
24
|
+
<a
|
|
25
|
+
key={f.href}
|
|
26
|
+
href={f.href}
|
|
27
|
+
className="rounded-lg border border-gray-200 bg-white p-4 transition hover:border-blue-300 hover:shadow-sm"
|
|
28
|
+
>
|
|
29
|
+
<h3 className="font-semibold text-blue-600">{f.title}</h3>
|
|
30
|
+
<p className="mt-1 text-sm text-gray-500">{f.desc}</p>
|
|
31
|
+
</a>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
12
34
|
</div>
|
|
13
35
|
);
|
|
14
36
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Island } from "mintiljs";
|
|
3
|
+
|
|
4
|
+
export default function IslandDemo() {
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">Island Demo</h1>
|
|
8
|
+
<p className="mb-4 text-gray-600">This counter is an Island — it hydrates independently on the client.</p>
|
|
9
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
10
|
+
<Island name="Counter" props={{ initial: 42 }} />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const useServer = true;
|
|
4
|
+
|
|
5
|
+
async function fetchPosts() {
|
|
6
|
+
// Simula fetching de dados — podia ser DB, FS, API externa
|
|
7
|
+
return [
|
|
8
|
+
{ id: 1, title: "MintilJS is fast" },
|
|
9
|
+
{ id: 2, title: "SSR sem JavaScript" },
|
|
10
|
+
{ id: 3, title: "React Server Components sem compilador" },
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function ServerDataPage() {
|
|
15
|
+
const posts = await fetchPosts();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">Server Data (useServer)</h1>
|
|
20
|
+
<p className="mb-4 text-gray-600">
|
|
21
|
+
Esta página usa <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">useServer = true</code>.
|
|
22
|
+
O componente é <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">async</code> e faz fetch
|
|
23
|
+
diretamente no servidor — sem <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">getServerSideProps</code>.
|
|
24
|
+
Zero JavaScript enviado ao cliente.
|
|
25
|
+
</p>
|
|
26
|
+
<div className="space-y-3">
|
|
27
|
+
{posts.map((post) => (
|
|
28
|
+
<div key={post.id} className="rounded-lg border border-gray-200 bg-white p-4">
|
|
29
|
+
<h2 className="font-semibold text-gray-900">{post.title}</h2>
|
|
30
|
+
<p className="text-sm text-gray-500">Post #{post.id}</p>
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function SsrCounterPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div>
|
|
6
|
+
<h1 className="mb-6 text-3xl font-bold tracking-tight">SSR Counter</h1>
|
|
7
|
+
<p className="mb-4 text-gray-600">This counter uses a form POST — no client JavaScript needed.</p>
|
|
8
|
+
<form method="POST" action="/api/counter" className="flex items-center gap-4">
|
|
9
|
+
<span className="text-2xl font-mono font-bold tabular-nums">0</span>
|
|
10
|
+
<button type="submit" name="action" value="inc" className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
|
|
11
|
+
+1
|
|
12
|
+
</button>
|
|
13
|
+
<button type="submit" name="action" value="dec" className="rounded-lg bg-gray-200 px-4 py-2 hover:bg-gray-300">
|
|
14
|
+
-1
|
|
15
|
+
</button>
|
|
16
|
+
</form>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
This is a static asset served
|
|
1
|
+
This is a static asset served from the public/ directory.
|