diff --git a/example/build-watch.ts b/example/build-watch.ts index ea94fbc..fcd8d1a 100644 --- a/example/build-watch.ts +++ b/example/build-watch.ts @@ -1,4 +1,4 @@ import { watchBuild } from "bun-react-ssr/watch"; import { doBuild } from "./build"; -watchBuild(doBuild, ["./hydrate.ts", "./pages"]); +watchBuild(doBuild, ["./hydrate.ts", "./pages", "./components"]); diff --git a/example/components/Clock.tsx b/example/components/Clock.tsx new file mode 100644 index 0000000..03c938b --- /dev/null +++ b/example/components/Clock.tsx @@ -0,0 +1,3 @@ +export function Clock({ time }: { time: Date }) { + return
Server time: {time.toISOString()}
; +} diff --git a/example/pages/index.tsx b/example/pages/index.tsx index 34844c6..d24740b 100644 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -1,5 +1,6 @@ import { Link, ReloadContext, useLoadingEffect } from "bun-react-ssr/router"; import { useContext } from "react"; +import { Clock } from "../components/Clock"; export default function Index({ time }: { time: Date }) { const reload = useContext(ReloadContext); @@ -8,7 +9,7 @@ export default function Index({ time }: { time: Date }) { }); return (
-
time {time.toISOString()}
+ index
reload()}>reload
diff --git a/example/routes.ts b/example/routes.ts index 8983dfb..46e7ecb 100644 --- a/example/routes.ts +++ b/example/routes.ts @@ -1,3 +1,12 @@ import { StaticRouters } from "bun-react-ssr"; +import { watch } from "node:fs"; export const router = new StaticRouters(import.meta.dir); + +if (Bun.env.NODE_ENV !== "production") { + const watcher = watch("./.build/.meta.json"); + watcher.on("change", () => { + console.log("reload"); + router.reload(); + }); +} diff --git a/index.tsx b/index.tsx index b5ca483..0f48441 100644 --- a/index.tsx +++ b/index.tsx @@ -1,25 +1,43 @@ import { FileSystemRouter } from "bun"; import { NJSON } from "next-json"; -import { statSync } from "node:fs"; +import { readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; -import { preloadModule } from "react-dom"; import { renderToReadableStream } from "react-dom/server"; import { ClientOnlyError } from "./client"; import { MetaContext, PreloadModule } from "./preload"; export class StaticRouters { - readonly server: FileSystemRouter; - readonly client: FileSystemRouter; - readonly #routes: Map; - readonly #routes_dump: string; - readonly #dependencies: Record; - readonly #hashed: Record; + server!: FileSystemRouter; + client!: FileSystemRouter; + #routes!: Map; + #routes_dump!: string; + #dependencies!: Record; + #hashed!: Record; + #cached = new Set(); constructor( public baseDir: string, public buildDir = ".build", public pageDir = "pages" ) { + this.reload(); + } + + reload(excludes: RegExp[] = []) { + const { baseDir, pageDir, buildDir } = this; + const metafile = Bun.fileURLToPath( + import.meta.resolve(join(baseDir, buildDir, ".meta.json")) + ); + delete require.cache[metafile]; + if (this.#cached.size) { + for (const cached of this.#cached) { + delete require.cache[cached]; + for (const dep of scanCacheDependencies(cached, excludes)) { + delete require.cache[dep]; + } + } + this.#cached.clear(); + } this.server = new FileSystemRouter({ dir: join(baseDir, pageDir), style: "nextjs", @@ -28,7 +46,7 @@ export class StaticRouters { dir: join(baseDir, buildDir, pageDir), style: "nextjs", }); - const parsed = require(join(baseDir, buildDir, ".meta.json")); + const parsed = require(metafile); this.#hashed = parsed.hashed; this.#dependencies = parsed.dependencies; this.#routes = new Map( @@ -83,7 +101,8 @@ export class StaticRouters { "No client-side script found for server-side component: " + serverSide.filePath ); - const module = await import(serverSide.filePath); + const module = require(serverSide.filePath); + this.#cached.add(serverSide.filePath); const result = await module.getServerSideProps?.({ params: serverSide.params, req: request, @@ -156,32 +175,36 @@ export class StaticRouters { } } -function DirectPreloadModule({ - target, - dependencies, -}: { - target: string; - dependencies: Record; -}) { - preloadModule(target, { as: "script" }); - preloadModule(target, { as: "script" }); - for (const dep of walkDependencies(target, dependencies)) { - preloadModule(dep, { as: "script" }); - preloadModule(dep, { as: "script" }); - } - return null; -} - -function* walkDependencies( +function* scanCacheDependencies( target: string, - dependencies: Record + excludes: RegExp[] = [] ): Generator { - if (dependencies[target]) { - for (const dep of dependencies[target]) { - yield dep; - yield* walkDependencies(dep, dependencies); + try { + const imports = new Bun.Transpiler({ + loader: target.endsWith(".tsx") + ? "tsx" + : target.endsWith(".ts") + ? "ts" + : "jsx", + }).scanImports(readFileSync(target)); + for (const imp of imports) { + if (imp.kind === "import-statement") { + const path = Bun.fileURLToPath(import.meta.resolve(imp.path, target)); + if ( + path.includes("/node_modules/") || + excludes.some((x) => path.match(x)) + ) + continue; + const resolved = Object.keys(require.cache).find((x) => + x.startsWith(path) + ); + if (resolved) { + yield resolved; + yield* scanCacheDependencies(resolved, excludes); + } + } } - } + } catch {} } export async function serveFromDir(config: {