From 86ea86e7bdc2a28b9f5c78e8b8a334c2ce4fc65e Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sat, 1 Aug 2026 05:25:47 +0100 Subject: [PATCH] Updates --- dist/functions/server/watcher-esbuild-ctx.js | 21 ++- .../server/web-pages/grab-page-component.js | 1 + dist/types/index.d.ts | 6 + package.json | 2 +- .../server/server-post-build-fn.test.ts | 131 ++++++++++++++++++ src/functions/server/watcher-esbuild-ctx.ts | 32 ++++- .../server/web-pages/grab-page-component.tsx | 2 + src/types/index.ts | 6 + 8 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/functions/server/server-post-build-fn.test.ts diff --git a/dist/functions/server/watcher-esbuild-ctx.js b/dist/functions/server/watcher-esbuild-ctx.js index b228af7..af97036 100644 --- a/dist/functions/server/watcher-esbuild-ctx.js +++ b/dist/functions/server/watcher-esbuild-ctx.js @@ -1,4 +1,4 @@ -import { watch, existsSync, statSync } from "fs"; +import { watch, existsSync, statSync, glob } from "fs"; import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import fullRebuild from "./full-rebuild"; @@ -16,6 +16,24 @@ export default async function watcherEsbuildCTX() { try { if (!filename) return; + const full_file_path = path.join(ROOT_DIR, filename); + if (global.CONFIG.exclude_watch_patterns) { + for (let i = 0; i < global.CONFIG.exclude_watch_patterns.length; i++) { + const watch_pattern = global.CONFIG.exclude_watch_patterns[i]; + if (watch_pattern instanceof RegExp) { + const is_path_excluded = watch_pattern.test(filename); + if (is_path_excluded) { + return; + } + } + else { + const excluded_path = path.resolve(ROOT_DIR, watch_pattern); + if (excluded_path == full_file_path) { + return; + } + } + } + } if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) { await fullRebuild(); return; @@ -35,7 +53,6 @@ export default async function watcherEsbuildCTX() { if (filename.endsWith(AppData["BunextTmpFileExt"])) { return; } - const full_file_path = path.join(ROOT_DIR, filename); const does_file_exist = existsSync(full_file_path); const file_stat = does_file_exist ? statSync(full_file_path) diff --git a/dist/functions/server/web-pages/grab-page-component.js b/dist/functions/server/web-pages/grab-page-component.js index dcf02c9..6744127 100644 --- a/dist/functions/server/web-pages/grab-page-component.js +++ b/dist/functions/server/web-pages/grab-page-component.js @@ -138,6 +138,7 @@ export default async function grabPageComponent(params) { else { log.error(`Error Grabbing Page Component: ${error.message}`); log.error(`Page: ${passed_file_path || url?.pathname}`); + process.exit(1); } return await grabPageErrorComponent({ error, diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index de73d23..480b2ea 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -66,6 +66,12 @@ export type BunextConfig = { * bundler for the browser. Eg. `react/jsx-dev-runtime` */ page_compiler_excludes?: string[]; + /** + * Patterns to exclude from the watcher. Eg. `./src/server.ts` + * or `\*.test.ts\`. It should either be a file path (relative or + * absolute), or a RegEx pattern. + */ + exclude_watch_patterns?: (string | RegExp)[]; }; export type BunextConfigMiddlewareParams = { req: Request; diff --git a/package.json b/package.json index c48519e..5a1ce44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bunext", - "version": "1.0.102", + "version": "1.0.103", "main": "dist/index.js", "module": "index.ts", "dependencies": { diff --git a/src/__tests__/functions/server/server-post-build-fn.test.ts b/src/__tests__/functions/server/server-post-build-fn.test.ts new file mode 100644 index 0000000..8a1e9ab --- /dev/null +++ b/src/__tests__/functions/server/server-post-build-fn.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; + +const grabPageComponent = mock(async () => ({ + serverRes: { props: { ok: true } }, +})); + +mock.module( + "../../../functions/server/web-pages/grab-page-component", + () => ({ + default: grabPageComponent, + }), +); + +const { default: serverPostBuildFn } = await import( + "../../../functions/server/server-post-build-fn" +); + +function makeController(page_url: string, target_map?: { local_path: string }) { + const enqueued: string[] = []; + return { + controller: { + enqueue: (chunk: string) => { + enqueued.push(chunk); + }, + } as any, + page_url, + target_map: target_map as any, + enqueued, + }; +} + +describe("server-post-build-fn", () => { + beforeEach(() => { + grabPageComponent.mockClear(); + global.ROUTER = { + match: (path: string) => { + if (path === "/about") return { filePath: "/pages/about.tsx" }; + if (path === "/404") return { filePath: "/pages/404.tsx" }; + if (path === "/new-page") + return { filePath: "/pages/new-page.tsx" }; + return null; + }, + } as any; + global.BUNDLER_CTX_MAP = { + "/pages/about.tsx": { + local_path: "/pages/about.tsx", + path: "pages/about.js", + } as any, + "/pages/404.tsx": { + local_path: "/pages/404.tsx", + path: "pages/404.js", + } as any, + "/pages/new-page.tsx": { + local_path: "/pages/new-page.tsx", + path: "pages/new-page.js", + } as any, + }; + global.HMR_CONTROLLERS = []; + global.ROOT_FILE_UPDATED = false; + }); + + afterEach(() => { + global.ROUTER = undefined as any; + global.BUNDLER_CTX_MAP = undefined as any; + global.HMR_CONTROLLERS = []; + }); + + test("soft-updates a normal page controller", async () => { + const c = makeController("http://localhost/about", { + local_path: "/pages/about.tsx", + }); + global.HMR_CONTROLLERS = [c as any]; + + await serverPostBuildFn(); + + expect(c.enqueued.length).toBe(1); + expect(c.enqueued[0]).toContain("event: update"); + expect(c.enqueued[0]).not.toContain('"reload":true'); + expect(c.enqueued[0]).toContain("/pages/about.tsx"); + }); + + test("soft-updates unmatched URL via custom 404 artifact", async () => { + const c = makeController("http://localhost/missing", { + local_path: "/pages/404.tsx", + }); + global.HMR_CONTROLLERS = [c as any]; + + await serverPostBuildFn(); + + expect(c.enqueued.length).toBe(1); + expect(c.enqueued[0]).not.toContain('"reload":true'); + expect(c.enqueued[0]).toContain("/pages/404.tsx"); + expect(grabPageComponent).toHaveBeenCalled(); + }); + + test("full-reloads when a previously unmatched route now exists", async () => { + const c = makeController("http://localhost/new-page", { + local_path: "/pages/404.tsx", + }); + global.HMR_CONTROLLERS = [c as any]; + + await serverPostBuildFn(); + + expect(c.enqueued.length).toBe(1); + expect(c.enqueued[0]).toContain('"reload":true'); + expect(c.target_map?.local_path).toBe("/pages/new-page.tsx"); + }); + + test("full-reloads unmatched tab with no prior map when route appears", async () => { + const c = makeController("http://localhost/new-page"); + global.HMR_CONTROLLERS = [c as any]; + + await serverPostBuildFn(); + + expect(c.enqueued.length).toBe(1); + expect(c.enqueued[0]).toContain('"reload":true'); + }); + + test("full-reloads preset 404 tabs with no custom 404 page", async () => { + global.ROUTER = { + match: () => null, + } as any; + const c = makeController("http://localhost/missing"); + global.HMR_CONTROLLERS = [c as any]; + + await serverPostBuildFn(); + + expect(c.enqueued.length).toBe(1); + expect(c.enqueued[0]).toContain('"reload":true'); + }); +}); diff --git a/src/functions/server/watcher-esbuild-ctx.ts b/src/functions/server/watcher-esbuild-ctx.ts index 8d56e7a..2e9ad69 100644 --- a/src/functions/server/watcher-esbuild-ctx.ts +++ b/src/functions/server/watcher-esbuild-ctx.ts @@ -1,4 +1,4 @@ -import { watch, existsSync, statSync } from "fs"; +import { watch, existsSync, statSync, glob } from "fs"; import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import fullRebuild from "./full-rebuild"; @@ -21,6 +21,35 @@ export default async function watcherEsbuildCTX() { try { if (!filename) return; + const full_file_path = path.join(ROOT_DIR, filename); + + if (global.CONFIG.exclude_watch_patterns) { + for ( + let i = 0; + i < global.CONFIG.exclude_watch_patterns.length; + i++ + ) { + const watch_pattern = + global.CONFIG.exclude_watch_patterns[i]; + + if (watch_pattern instanceof RegExp) { + const is_path_excluded = + watch_pattern.test(filename); + if (is_path_excluded) { + return; + } + } else { + const excluded_path = path.resolve( + ROOT_DIR, + watch_pattern, + ); + + if (excluded_path == full_file_path) { + return; + } + } + } + } if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) { await fullRebuild(); @@ -46,7 +75,6 @@ export default async function watcherEsbuildCTX() { return; } - const full_file_path = path.join(ROOT_DIR, filename); const does_file_exist = existsSync(full_file_path); const file_stat = does_file_exist ? statSync(full_file_path) diff --git a/src/functions/server/web-pages/grab-page-component.tsx b/src/functions/server/web-pages/grab-page-component.tsx index 4d652ba..bb79cfb 100644 --- a/src/functions/server/web-pages/grab-page-component.tsx +++ b/src/functions/server/web-pages/grab-page-component.tsx @@ -196,6 +196,8 @@ export default async function grabPageComponent( } else { log.error(`Error Grabbing Page Component: ${error.message}`); log.error(`Page: ${passed_file_path || url?.pathname}`); + + process.exit(1); } return await grabPageErrorComponent({ diff --git a/src/types/index.ts b/src/types/index.ts index a996a46..0aad406 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -83,6 +83,12 @@ export type BunextConfig = { * bundler for the browser. Eg. `react/jsx-dev-runtime` */ page_compiler_excludes?: string[]; + /** + * Patterns to exclude from the watcher. Eg. `./src/server.ts` + * or `\*.test.ts\`. It should either be a file path (relative or + * absolute), or a RegEx pattern. + */ + exclude_watch_patterns?: (string | RegExp)[]; }; export type BunextConfigMiddlewareParams = {