This commit is contained in:
Benjamin Toby 2026-08-01 05:25:47 +01:00
parent 596b9de047
commit 86ea86e7bd
8 changed files with 196 additions and 5 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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;

View File

@ -1,6 +1,6 @@
{
"name": "@moduletrace/bunext",
"version": "1.0.102",
"version": "1.0.103",
"main": "dist/index.js",
"module": "index.ts",
"dependencies": {

View File

@ -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');
});
});

View File

@ -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)

View File

@ -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({

View File

@ -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 = {