From 08d022e06263f73071c592ccbddf49cae6f5cf71 Mon Sep 17 00:00:00 2001
From: ynwd <10122431+ynwd@users.noreply.github.com>
Date: Fri, 13 Mar 2026 07:32:39 +0700
Subject: [PATCH 1/2] init
---
core/loader.ts | 36 ++++++-
deno.json | 16 +++
manifest.ts | 17 +---
middlewares/render/render.ts | 13 ++-
middlewares/render/render_extra.test.ts | 127 ++++++++++++++++++------
modules/login/App.handler.tsx | 15 +++
modules/login/App.tsx | 44 ++++++++
modules/login/mod.ts | 10 ++
8 files changed, 227 insertions(+), 51 deletions(-)
create mode 100644 modules/login/App.handler.tsx
create mode 100644 modules/login/App.tsx
create mode 100644 modules/login/mod.ts
diff --git a/core/loader.ts b/core/loader.ts
index c2d1c3c6..44cbd5eb 100644
--- a/core/loader.ts
+++ b/core/loader.ts
@@ -288,7 +288,17 @@ export function registerFromNamespace(
(ns.default as (app: App) => void).length === 1
) {
try {
- (ns.default as (app: App) => void)(makeRegistrar(app) as unknown as App);
+ const wrappedApp = makeRegistrar(app) as unknown as App;
+ const originalUse = wrappedApp.use;
+ wrappedApp.use = (mw: Middleware) => {
+ const wrappedMw: Middleware = (req, ctx, next) => {
+ if (!ctx.state) ctx.state = {};
+ ctx.state.module = name;
+ return mw(req, ctx, next);
+ };
+ return originalUse.call(wrappedApp, wrappedMw);
+ };
+ (ns.default as (app: App) => void)(wrappedApp);
// Determine the recorded mount in a clearer, test-friendly way
let recordedMount: string;
if (typeof ns.mountPath === "string") {
@@ -319,7 +329,17 @@ export function registerFromNamespace(
// Support named `register` export: `export function register(app) {}`
if (typeof ns.register === "function") {
try {
- (ns.register as (app: App) => void)(makeRegistrar(app) as unknown as App);
+ const wrappedApp = makeRegistrar(app) as unknown as App;
+ const originalUse = wrappedApp.use;
+ wrappedApp.use = (mw: Middleware) => {
+ const wrappedMw: Middleware = (req, ctx, next) => {
+ if (!ctx.state) ctx.state = {};
+ ctx.state.module = name;
+ return mw(req, ctx, next);
+ };
+ return originalUse.call(wrappedApp, wrappedMw);
+ };
+ (ns.register as (app: App) => void)(wrappedApp);
{
let recordedMount: string;
if (typeof ns.mountPath === "string") {
@@ -351,8 +371,18 @@ export function registerFromNamespace(
"function"
) {
try {
+ const wrappedApp = makeRegistrar(app) as unknown as App;
+ const originalUse = wrappedApp.use;
+ wrappedApp.use = (mw: Middleware) => {
+ const wrappedMw: Middleware = (req, ctx, next) => {
+ if (!ctx.state) ctx.state = {};
+ ctx.state.module = name;
+ return mw(req, ctx, next);
+ };
+ return originalUse.call(wrappedApp, wrappedMw);
+ };
(ns.default as { register: (app: App) => void }).register(
- makeRegistrar(app) as unknown as App,
+ wrappedApp,
);
{
let recordedMount: string;
diff --git a/deno.json b/deno.json
index c5c898c4..c3e56a1e 100644
--- a/deno.json
+++ b/deno.json
@@ -18,8 +18,24 @@
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.19",
"@std/testing": "jsr:@std/testing@^1.0.17",
+ "@types/react": "npm:@types/react@^19.0.8",
+ "@types/react-dom": "npm:@types/react-dom@^19.0.3",
"react": "npm:react@^19.2.4",
"react-dom": "npm:react-dom@^19.2.4",
+ "react-dom/server": "npm:react-dom@^19.2.4/server",
+ "react/jsx-runtime": "npm:react@^19.2.4/jsx-runtime",
"tailwindcss": "npm:tailwindcss@^4.2.1"
+ },
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "jsxImportSourceTypes": "@types/react",
+ "lib": [
+ "deno.ns",
+ "deno.unstable",
+ "dom",
+ "dom.iterable",
+ "dom.asynciterable"
+ ]
}
}
diff --git a/manifest.ts b/manifest.ts
index f261a8c1..a54119af 100644
--- a/manifest.ts
+++ b/manifest.ts
@@ -1,18 +1,3 @@
-// Project manifest — export modules available for auto-registration.
-// Keep entries in this file in sync with the `modules/` folder. The
-// loader will import this manifest to auto-register middleware.
-
export * as index from "./modules/index/mod.ts";
export * as user from "./modules/user/mod.ts";
-
-// Uncomment or add exports as modules are created:
-// export * as dashboard from "./modules/dashboard/mod.ts";
-// export * as git from "./modules/git/mod.ts";
-// export * as media from "./modules/media/mod.ts";
-// export * as password from "./modules/password/mod.ts";
-// export * as posts from "./modules/posts/mod.ts";
-// export * as profile from "./modules/profile/mod.ts";
-// export * as signin from "./modules/signin/mod.ts";
-// export * as signup from "./modules/signup/mod.ts";
-// export * as ssr from "./modules/ssr/mod.ts";
-// export * as user from "./modules/user/mod.ts";
+export * as login from "./modules/login/mod.ts";
diff --git a/middlewares/render/render.ts b/middlewares/render/render.ts
index 204ebd25..5060d81c 100644
--- a/middlewares/render/render.ts
+++ b/middlewares/render/render.ts
@@ -286,11 +286,22 @@ const createRenderToString = (_context: Context) => {
// Prefer explicit option, otherwise fall back to the module name stored
// on the context by the loader (autoRegisterModules) or other middleware.
- const resolvedModule = moduleFromOpts ?? ((_context && _context.state &&
+ let resolvedModule = moduleFromOpts ?? ((_context && _context.state &&
typeof _context.state.module === "string")
? _context.state.module
: undefined);
+ if (!resolvedModule) {
+ const pathname = _context.url.pathname;
+ if (pathname === "/" || pathname === "" || pathname === "/index") {
+ resolvedModule = "index";
+ } else {
+ // Find the first segment that is not empty
+ const parts = pathname.split("/").filter(Boolean);
+ resolvedModule = parts[0] || "index";
+ }
+ }
+
const isProd = Deno.env.get("ENV") === "production";
const timestamp = !isProd ? `?t=${Date.now()}` : "";
const clientScript = resolvedModule
diff --git a/middlewares/render/render_extra.test.ts b/middlewares/render/render_extra.test.ts
index 281dfcb8..c2608656 100644
--- a/middlewares/render/render_extra.test.ts
+++ b/middlewares/render/render_extra.test.ts
@@ -22,7 +22,8 @@ Deno.test({
});
Deno.test({
- name: "render_extra: DA:185 - writeTextFileSync catch & DA:191 - second statSync throws",
+ name:
+ "render_extra: DA:185 - writeTextFileSync catch & DA:191 - second statSync throws",
sanitizeOps: false,
sanitizeResources: false,
fn: () => {
@@ -30,7 +31,7 @@ Deno.test({
let statCount = 0;
const originalStatSync = Deno.statSync;
const originalWriteTextFileSync = Deno.writeTextFileSync;
-
+
Deno.statSync = (p) => {
if (p === "./.build_done" || p.toString().endsWith(".build_done")) {
statCount++;
@@ -39,7 +40,7 @@ Deno.test({
}
return originalStatSync(p);
};
-
+
Deno.writeTextFileSync = (p, d) => {
if (p === "./.build_done" || p.toString().endsWith(".build_done")) {
throw new Error("force fail write");
@@ -68,12 +69,12 @@ Deno.test({
_resetWatcherForTests();
let statCount = 0;
const originalStatSync = Deno.statSync;
-
+
Deno.statSync = (p) => {
if (p === "./.build_done" || p.toString().endsWith(".build_done")) {
statCount++;
if (statCount === 2) {
- return { mtime: { getTime: () => 0 } } as unknown as Deno.FileInfo;
+ return { mtime: { getTime: () => 0 } } as unknown as Deno.FileInfo;
}
}
return originalStatSync(p);
@@ -139,7 +140,8 @@ Deno.test({
});
Deno.test({
- name: "render_extra: DA:222-224 - startComponentsWatcher initialization throws",
+ name:
+ "render_extra: DA:222-224 - startComponentsWatcher initialization throws",
sanitizeOps: false,
sanitizeResources: false,
fn: () => {
@@ -169,13 +171,28 @@ Deno.test({
_resetWatcherForTests();
const mw = createRenderMiddleware();
const req1 = { url: "" } as Request;
- mw(req1, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
-
+ mw(
+ req1,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
+
const req2 = {} as Request;
- mw(req2, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
-
- const req3 = { url: "invalid cat /home/dev/project/fw/fastro/middlewares/render/render_extra.test.ts" } as Request;
- mw(req3, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
+ mw(
+ req2,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
+
+ const req3 = {
+ url:
+ "invalid cat /home/dev/project/fw/fastro/middlewares/render/render_extra.test.ts",
+ } as Request;
+ mw(
+ req3,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
},
});
@@ -203,11 +220,19 @@ Deno.test({
};
const mw = createRenderMiddleware();
- const req = new Request("http://localhost/hmr", { headers: { upgrade: "websocket", connection: "Upgrade" } });
- mw(req, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
+ const req = new Request("http://localhost/hmr", {
+ headers: { upgrade: "websocket", connection: "Upgrade" },
+ });
+ mw(
+ req,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
_setLastMtimeForTests(0);
- await _watchTickForTestsWithStat(() => Promise.resolve({ mtime: new Date(Date.now() + 1000) } as Deno.FileInfo));
+ await _watchTickForTestsWithStat(() =>
+ Promise.resolve({ mtime: new Date(Date.now() + 1000) } as Deno.FileInfo)
+ );
if (mockSocket && mockSocket.onopen) {
mockSocket.readyState = WebSocket.OPEN;
@@ -230,6 +255,7 @@ Deno.test({
const originalSetInterval = globalThis.setInterval;
let heartbeatCb: (() => void) | undefined;
+ // @ts-ignore: mock setInterval
globalThis.setInterval = (cb: string | ((...args: unknown[]) => void)) => {
if (typeof cb === "function") heartbeatCb = cb as () => void;
return 999 as unknown as number;
@@ -254,8 +280,14 @@ Deno.test({
};
const mw = createRenderMiddleware();
- const req = new Request("http://localhost/hmr", { headers: { upgrade: "websocket", connection: "Upgrade" } });
- mw(req, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
+ const req = new Request("http://localhost/hmr", {
+ headers: { upgrade: "websocket", connection: "Upgrade" },
+ });
+ mw(
+ req,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
if (mockSocket && mockSocket.onopen) {
mockSocket.onopen();
@@ -263,7 +295,9 @@ Deno.test({
if (heartbeatCb) {
heartbeatCb();
- mockSocket.send = () => { throw new Error("Force catch"); };
+ mockSocket.send = () => {
+ throw new Error("Force catch");
+ };
heartbeatCb();
}
@@ -321,14 +355,19 @@ Deno.test({
const mw = createRenderMiddleware();
const req = { url: "invalid-url!" } as Request;
- mw(req, {} as import("../../core/types.ts").Context, () => new Response() as unknown as Response);
+ mw(
+ req,
+ {} as import("../../core/types.ts").Context,
+ () => new Response() as unknown as Response,
+ );
Deno.env.get = originalGet;
},
});
Deno.test({
- name: "render_extra: renderToString branches on includeHead and doctype false",
+ name:
+ "render_extra: renderToString branches on includeHead and doctype false",
sanitizeOps: false,
sanitizeResources: false,
fn: () => {
@@ -336,15 +375,25 @@ Deno.test({
const el = React.createElement("div", null, "Hello");
const ctx = {} as import("../../core/types.ts").Context;
const mw = createRenderMiddleware();
- mw(new Request("http://localhost/"), ctx, () => new Response() as unknown as Response);
+ mw(
+ new Request("http://localhost/"),
+ ctx,
+ () => new Response() as unknown as Response,
+ );
- const rts = (ctx as unknown as { renderToString: (...args: unknown[]) => string }).renderToString;
+ const rts =
+ (ctx as unknown as { renderToString: (...args: unknown[]) => string })
+ .renderToString;
if (rts) {
const html1 = rts(el, { includeHead: false, includeDoctype: true });
- if (!html1.startsWith("")) throw new Error("Missing doctype");
+ if (!html1.startsWith("")) {
+ throw new Error("Missing doctype");
+ }
const html2 = rts(el, { includeHead: false, includeDoctype: false });
- if (html2.startsWith("")) throw new Error("Unexpected doctype");
+ if (html2.startsWith("")) {
+ throw new Error("Unexpected doctype");
+ }
}
},
});
@@ -360,7 +409,9 @@ Deno.test({
if (k === "ENV") return "test";
return originalGet.call(Deno.env, k);
};
- await _watchTickForTestsWithStat(() => Promise.reject(new Error("Test catch branch")));
+ await _watchTickForTestsWithStat(() =>
+ Promise.reject(new Error("Test catch branch"))
+ );
Deno.env.get = originalGet;
},
});
@@ -374,13 +425,18 @@ Deno.test({
_setLastMtimeForTests(0);
const clients = _getHmrClientsForTests();
clients.clear();
- await _watchTickForTestsWithStat(() => Promise.resolve({ mtime: new Date(Date.now() + 1000) } as Deno.FileInfo));
- await _watchTickForTestsWithStat(() => Promise.resolve({ mtime: new Date(Date.now() + 2000) } as Deno.FileInfo));
+ await _watchTickForTestsWithStat(() =>
+ Promise.resolve({ mtime: new Date(Date.now() + 1000) } as Deno.FileInfo)
+ );
+ await _watchTickForTestsWithStat(() =>
+ Promise.resolve({ mtime: new Date(Date.now() + 2000) } as Deno.FileInfo)
+ );
},
});
Deno.test({
- name: "render_extra: startComponentsWatcher ignores errors if statSync fails on read but fallback works",
+ name:
+ "render_extra: startComponentsWatcher ignores errors if statSync fails on read but fallback works",
sanitizeOps: false,
sanitizeResources: false,
fn: () => {
@@ -403,6 +459,7 @@ Deno.test({
const originalSetInterval = globalThis.setInterval;
let intervalCb: () => void = () => {};
+ // @ts-ignore: mock setInterval
globalThis.setInterval = (cb: string | ((...args: unknown[]) => void)) => {
if (typeof cb === "function") intervalCb = cb as () => void;
return 123 as unknown as number;
@@ -425,10 +482,18 @@ Deno.test({
sanitizeResources: false,
fn: () => {
_resetWatcherForTests();
- const ctx = { state: { module: "test-module" } } as unknown as import("../../core/types.ts").Context;
+ const ctx = {
+ state: { module: "test-module" },
+ } as unknown as import("../../core/types.ts").Context;
const mw = createRenderMiddleware();
- mw(new Request("http://localhost/"), ctx, () => new Response() as unknown as Response);
- const rts = (ctx as unknown as { renderToString: (...args: unknown[]) => string }).renderToString;
+ mw(
+ new Request("http://localhost/"),
+ ctx,
+ () => new Response() as unknown as Response,
+ );
+ const rts =
+ (ctx as unknown as { renderToString: (...args: unknown[]) => string })
+ .renderToString;
if (rts) {
const el = React.createElement("div", null, "Hello");
const html = rts(el, { includeHead: true });
diff --git a/modules/login/App.handler.tsx b/modules/login/App.handler.tsx
new file mode 100644
index 00000000..6c8d2534
--- /dev/null
+++ b/modules/login/App.handler.tsx
@@ -0,0 +1,15 @@
+import { Context } from "../../mod.ts";
+import { App } from "./App.tsx";
+
+export function appHandler(_req: Request, ctx: Context) {
+ console.log("DEBUG: ctx.state.module =", ctx.state?.module);
+ const html = ctx.renderToString!(
+