Update watcher function. Add tests
This commit is contained in:
parent
5c53e94a3e
commit
cf010ad4f5
1
dist/__tests__/functions/cache/grab-cache-names.test.d.ts
vendored
Normal file
1
dist/__tests__/functions/cache/grab-cache-names.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
37
dist/__tests__/functions/cache/grab-cache-names.test.js
vendored
Normal file
37
dist/__tests__/functions/cache/grab-cache-names.test.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabCacheNames from "../../../functions/cache/grab-cache-names";
|
||||
describe("grabCacheNames", () => {
|
||||
it("returns cache_name and cache_meta_name for a simple key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({ key: "home" });
|
||||
expect(cache_name).toBe("home.res.html");
|
||||
expect(cache_meta_name).toBe("home.meta.json");
|
||||
});
|
||||
it("defaults paradigm to html", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "page" });
|
||||
expect(cache_name).toEndWith(".res.html");
|
||||
});
|
||||
it("uses json paradigm when specified", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "api-data", paradigm: "json" });
|
||||
expect(cache_name).toBe("api-data.res.json");
|
||||
});
|
||||
it("URL-encodes the key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
||||
key: "/blog/hello world",
|
||||
});
|
||||
const encoded = encodeURIComponent("/blog/hello world");
|
||||
expect(cache_name).toBe(`${encoded}.res.html`);
|
||||
expect(cache_meta_name).toBe(`${encoded}.meta.json`);
|
||||
});
|
||||
it("handles keys with special characters", () => {
|
||||
const key = "page?id=1&sort=asc";
|
||||
const { cache_name } = grabCacheNames({ key });
|
||||
expect(cache_name).toBe(`${encodeURIComponent(key)}.res.html`);
|
||||
});
|
||||
it("cache_meta_name always uses .meta.json regardless of paradigm", () => {
|
||||
const { cache_meta_name } = grabCacheNames({
|
||||
key: "test",
|
||||
paradigm: "json",
|
||||
});
|
||||
expect(cache_meta_name).toBe("test.meta.json");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/functions/server/grab-web-meta-html.test.d.ts
vendored
Normal file
1
dist/__tests__/functions/server/grab-web-meta-html.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
97
dist/__tests__/functions/server/grab-web-meta-html.test.js
vendored
Normal file
97
dist/__tests__/functions/server/grab-web-meta-html.test.js
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
|
||||
describe("grabWebMetaHTML", () => {
|
||||
it("returns empty string for empty meta object", () => {
|
||||
expect(grabWebMetaHTML({ meta: {} })).toBe("");
|
||||
});
|
||||
it("generates a title tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
|
||||
expect(html).toContain("<title>My Page</title>");
|
||||
});
|
||||
it("generates a description meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { description: "A description" } });
|
||||
expect(html).toContain('<meta name="description" content="A description"');
|
||||
});
|
||||
it("joins array keywords with comma", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { keywords: ["react", "bun", "ssr"] },
|
||||
});
|
||||
expect(html).toContain('content="react, bun, ssr"');
|
||||
});
|
||||
it("uses string keywords directly", () => {
|
||||
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
|
||||
expect(html).toContain('content="react, bun"');
|
||||
});
|
||||
it("generates author meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
|
||||
expect(html).toContain('<meta name="author" content="Alice"');
|
||||
});
|
||||
it("generates robots meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
|
||||
expect(html).toContain('<meta name="robots" content="noindex"');
|
||||
});
|
||||
it("generates canonical link tag", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { canonical: "https://example.com/page" },
|
||||
});
|
||||
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
|
||||
});
|
||||
it("generates theme-color meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
|
||||
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
|
||||
});
|
||||
it("generates OG tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
og: {
|
||||
title: "OG Title",
|
||||
description: "OG Desc",
|
||||
image: "https://example.com/img.png",
|
||||
url: "https://example.com",
|
||||
type: "website",
|
||||
siteName: "Example",
|
||||
locale: "en_US",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta property="og:title" content="OG Title"');
|
||||
expect(html).toContain('<meta property="og:description" content="OG Desc"');
|
||||
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
|
||||
expect(html).toContain('<meta property="og:url" content="https://example.com"');
|
||||
expect(html).toContain('<meta property="og:type" content="website"');
|
||||
expect(html).toContain('<meta property="og:site_name" content="Example"');
|
||||
expect(html).toContain('<meta property="og:locale" content="en_US"');
|
||||
});
|
||||
it("generates Twitter card tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Tweet Title",
|
||||
description: "Tweet Desc",
|
||||
image: "https://example.com/tw.png",
|
||||
site: "@example",
|
||||
creator: "@alice",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
|
||||
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
|
||||
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
|
||||
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
|
||||
expect(html).toContain('<meta name="twitter:site" content="@example"');
|
||||
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
|
||||
});
|
||||
it("skips undefined OG fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
|
||||
expect(html).toContain("og:title");
|
||||
expect(html).not.toContain("og:description");
|
||||
expect(html).not.toContain("og:image");
|
||||
});
|
||||
it("does not emit tags for missing fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
|
||||
expect(html).not.toContain("description");
|
||||
expect(html).not.toContain("og:");
|
||||
expect(html).not.toContain("twitter:");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/deserialize-query.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/deserialize-query.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
33
dist/__tests__/utils/deserialize-query.test.js
vendored
Normal file
33
dist/__tests__/utils/deserialize-query.test.js
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import deserializeQuery from "../../utils/deserialize-query";
|
||||
describe("deserializeQuery", () => {
|
||||
it("passes through a plain object unchanged", () => {
|
||||
const input = { foo: "bar" };
|
||||
expect(deserializeQuery(input)).toEqual({ foo: "bar" });
|
||||
});
|
||||
it("parses a JSON string into an object", () => {
|
||||
const input = JSON.stringify({ a: 1, b: "hello" });
|
||||
expect(deserializeQuery(input)).toEqual({ a: 1, b: "hello" });
|
||||
});
|
||||
it("deep-parses string values that look like JSON objects", () => {
|
||||
const nested = { filter: JSON.stringify({ status: "active" }) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.filter).toEqual({ status: "active" });
|
||||
});
|
||||
it("deep-parses string values that look like JSON arrays", () => {
|
||||
const nested = { ids: JSON.stringify([1, 2, 3]) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.ids).toEqual([1, 2, 3]);
|
||||
});
|
||||
it("leaves plain string values alone", () => {
|
||||
const input = { name: "alice", age: "30" };
|
||||
expect(deserializeQuery(input)).toEqual({ name: "alice", age: "30" });
|
||||
});
|
||||
it("returns an empty object for an empty JSON string", () => {
|
||||
expect(deserializeQuery("{}")).toEqual({});
|
||||
});
|
||||
it("returns an empty object for an invalid JSON string", () => {
|
||||
// EJSON.parse returns undefined → Object(undefined) → {}
|
||||
expect(deserializeQuery("not-json")).toEqual({});
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/ejson.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/ejson.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
52
dist/__tests__/utils/ejson.test.js
vendored
Normal file
52
dist/__tests__/utils/ejson.test.js
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import EJSON from "../../utils/ejson";
|
||||
describe("EJSON.parse", () => {
|
||||
it("parses a valid JSON string", () => {
|
||||
expect(EJSON.parse('{"a":1}')).toEqual({ a: 1 });
|
||||
});
|
||||
it("parses a JSON array string", () => {
|
||||
expect(EJSON.parse('[1,2,3]')).toEqual([1, 2, 3]);
|
||||
});
|
||||
it("returns undefined for null input", () => {
|
||||
expect(EJSON.parse(null)).toBeUndefined();
|
||||
});
|
||||
it("returns undefined for empty string", () => {
|
||||
expect(EJSON.parse("")).toBeUndefined();
|
||||
});
|
||||
it("returns undefined for invalid JSON", () => {
|
||||
expect(EJSON.parse("{bad json")).toBeUndefined();
|
||||
});
|
||||
it("returns the object directly when passed an object (typeof object)", () => {
|
||||
const obj = { x: 1 };
|
||||
expect(EJSON.parse(obj)).toBe(obj);
|
||||
});
|
||||
it("returns undefined for a number input", () => {
|
||||
expect(EJSON.parse(42)).toBeUndefined();
|
||||
});
|
||||
it("applies a reviver function", () => {
|
||||
const result = EJSON.parse('{"a":"2"}', (key, value) => key === "a" ? Number(value) : value);
|
||||
expect(result).toEqual({ a: 2 });
|
||||
});
|
||||
});
|
||||
describe("EJSON.stringify", () => {
|
||||
it("stringifies an object", () => {
|
||||
expect(EJSON.stringify({ a: 1 })).toBe('{"a":1}');
|
||||
});
|
||||
it("stringifies an array", () => {
|
||||
expect(EJSON.stringify([1, 2, 3])).toBe("[1,2,3]");
|
||||
});
|
||||
it("applies spacing", () => {
|
||||
expect(EJSON.stringify({ a: 1 }, null, 2)).toBe('{\n "a": 1\n}');
|
||||
});
|
||||
it("returns undefined for circular references", () => {
|
||||
const obj = {};
|
||||
obj.self = obj;
|
||||
expect(EJSON.stringify(obj)).toBeUndefined();
|
||||
});
|
||||
it("stringifies null", () => {
|
||||
expect(EJSON.stringify(null)).toBe("null");
|
||||
});
|
||||
it("stringifies a number", () => {
|
||||
expect(EJSON.stringify(42)).toBe("42");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-app-names.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-app-names.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
23
dist/__tests__/utils/grab-app-names.test.js
vendored
Normal file
23
dist/__tests__/utils/grab-app-names.test.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
describe("AppNames", () => {
|
||||
it("has a defaultPort of 7000", () => {
|
||||
expect(AppNames.defaultPort).toBe(7000);
|
||||
});
|
||||
it("has the correct defaultAssetPrefix", () => {
|
||||
expect(AppNames.defaultAssetPrefix).toBe("_bunext/static");
|
||||
});
|
||||
it("has name Bunext", () => {
|
||||
expect(AppNames.name).toBe("Bunext");
|
||||
});
|
||||
it("has a version string", () => {
|
||||
expect(typeof AppNames.version).toBe("string");
|
||||
expect(AppNames.version.length).toBeGreaterThan(0);
|
||||
});
|
||||
it("has defaultDistDir as .bunext", () => {
|
||||
expect(AppNames.defaultDistDir).toBe(".bunext");
|
||||
});
|
||||
it("has RootPagesComponentName as __root", () => {
|
||||
expect(AppNames.RootPagesComponentName).toBe("__root");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-app-port.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-app-port.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
38
dist/__tests__/utils/grab-app-port.test.js
vendored
Normal file
38
dist/__tests__/utils/grab-app-port.test.js
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabAppPort from "../../utils/grab-app-port";
|
||||
const originalEnv = process.env.PORT;
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.PORT = originalEnv;
|
||||
}
|
||||
else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
describe("grabAppPort", () => {
|
||||
it("returns the default port (7000) when no config or env set", () => {
|
||||
expect(grabAppPort()).toBe(7000);
|
||||
});
|
||||
it("uses PORT env variable when set", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabAppPort()).toBe(8080);
|
||||
});
|
||||
it("uses config.port when PORT env is not set", () => {
|
||||
global.CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(3000);
|
||||
});
|
||||
it("PORT env takes precedence over config.port", () => {
|
||||
process.env.PORT = "9000";
|
||||
global.CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(9000);
|
||||
});
|
||||
it("handles non-numeric PORT env gracefully via numberfy", () => {
|
||||
process.env.PORT = "abc";
|
||||
// numberfy strips non-numeric chars, "abc" → "" → 0
|
||||
expect(grabAppPort()).toBe(0);
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-constants.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-constants.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
30
dist/__tests__/utils/grab-constants.test.js
vendored
Normal file
30
dist/__tests__/utils/grab-constants.test.js
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
beforeEach(() => {
|
||||
global.CONFIG = {};
|
||||
});
|
||||
describe("grabConstants", () => {
|
||||
it("has the correct ClientRootElementIDName", () => {
|
||||
expect(grabConstants().ClientRootElementIDName).toBe("__bunext");
|
||||
});
|
||||
it("has the correct ClientWindowPagePropsName", () => {
|
||||
expect(grabConstants().ClientWindowPagePropsName).toBe("__PAGE_PROPS__");
|
||||
});
|
||||
it("has the correct ClientRootComponentWindowName", () => {
|
||||
expect(grabConstants().ClientRootComponentWindowName).toBe("BUNEXT_ROOT");
|
||||
});
|
||||
it("calculates MBInBytes as 1024 * 1024", () => {
|
||||
expect(grabConstants().MBInBytes).toBe(1024 * 1024);
|
||||
});
|
||||
it("ServerDefaultRequestBodyLimitBytes is 10 MB", () => {
|
||||
expect(grabConstants().ServerDefaultRequestBodyLimitBytes).toBe(10 * 1024 * 1024);
|
||||
});
|
||||
it("MaxBundlerRebuilds is 5", () => {
|
||||
expect(grabConstants().MaxBundlerRebuilds).toBe(5);
|
||||
});
|
||||
it("returns the current global.CONFIG", () => {
|
||||
const cfg = { port: 9000 };
|
||||
global.CONFIG = cfg;
|
||||
expect(grabConstants().config).toBe(cfg);
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-dir-names.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-dir-names.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
55
dist/__tests__/utils/grab-dir-names.test.js
vendored
Normal file
55
dist/__tests__/utils/grab-dir-names.test.js
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
describe("grabDirNames", () => {
|
||||
it("derives all paths from process.cwd()", () => {
|
||||
const cwd = process.cwd();
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.ROOT_DIR).toBe(cwd);
|
||||
expect(dirs.SRC_DIR).toBe(path.join(cwd, "src"));
|
||||
expect(dirs.PAGES_DIR).toBe(path.join(cwd, "src", "pages"));
|
||||
expect(dirs.API_DIR).toBe(path.join(cwd, "src", "pages", "api"));
|
||||
expect(dirs.PUBLIC_DIR).toBe(path.join(cwd, "public"));
|
||||
});
|
||||
it("nests HYDRATION_DST_DIR under public/__bunext/pages", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "pages"));
|
||||
});
|
||||
it("nests BUNEXT_CACHE_DIR under public/__bunext/cache", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNEXT_CACHE_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "cache"));
|
||||
});
|
||||
it("places map JSON file inside HYDRATION_DST_DIR", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR_MAP_JSON_FILE).toBe(path.join(dirs.HYDRATION_DST_DIR, "map.json"));
|
||||
});
|
||||
it("places CONFIG_FILE at root", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.CONFIG_FILE).toBe(path.join(dirs.ROOT_DIR, "bunext.config.ts"));
|
||||
});
|
||||
it("places BUNX_TMP_DIR inside .bunext", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_TMP_DIR).toContain(".bunext");
|
||||
expect(dirs.BUNX_TMP_DIR).toEndWith(".tmp");
|
||||
});
|
||||
it("places BUNX_HYDRATION_SRC_DIR under client/hydration-src", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_HYDRATION_SRC_DIR).toContain(path.join("client", "hydration-src"));
|
||||
});
|
||||
it("sets 404 file name to not-found", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_FILE_NAME).toBe("not-found");
|
||||
});
|
||||
it("sets 500 file name to server-error", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_FILE_NAME).toBe("server-error");
|
||||
});
|
||||
it("preset 404 component path ends with not-found.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_PRESET_COMPONENT).toEndWith("not-found.tsx");
|
||||
});
|
||||
it("preset 500 component path ends with server-error.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_PRESET_COMPONENT).toEndWith("server-error.tsx");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-origin.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-origin.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
32
dist/__tests__/utils/grab-origin.test.js
vendored
Normal file
32
dist/__tests__/utils/grab-origin.test.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabOrigin from "../../utils/grab-origin";
|
||||
const originalPort = process.env.PORT;
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
if (originalPort !== undefined) {
|
||||
process.env.PORT = originalPort;
|
||||
}
|
||||
else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
describe("grabOrigin", () => {
|
||||
it("returns config.origin when set", () => {
|
||||
global.CONFIG = { origin: "https://example.com" };
|
||||
expect(grabOrigin()).toBe("https://example.com");
|
||||
});
|
||||
it("falls back to http://localhost:<port> using default port", () => {
|
||||
expect(grabOrigin()).toBe("http://localhost:7000");
|
||||
});
|
||||
it("falls back using PORT env variable", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabOrigin()).toBe("http://localhost:8080");
|
||||
});
|
||||
it("falls back using config.port", () => {
|
||||
global.CONFIG = { port: 3700 };
|
||||
expect(grabOrigin()).toBe("http://localhost:3700");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/grab-page-name.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/grab-page-name.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
46
dist/__tests__/utils/grab-page-name.test.js
vendored
Normal file
46
dist/__tests__/utils/grab-page-name.test.js
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabPageName from "../../utils/grab-page-name";
|
||||
describe("grabPageName", () => {
|
||||
it("returns the page name for a simple page path", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/about.tsx" }))
|
||||
.toBe("about");
|
||||
});
|
||||
it("returns 'index' for a root index file (no -index stripping at root)", () => {
|
||||
// -index suffix is only stripped when joined: e.g. "blog-index" → "blog"
|
||||
// A standalone "index" filename has no leading dash so stays as-is
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/index.tsx" }))
|
||||
.toBe("index");
|
||||
});
|
||||
it("handles nested page paths", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/blog/post.tsx" })).toBe("blog-post");
|
||||
});
|
||||
it("strips -index suffix from nested index files", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/blog/index.tsx" })).toBe("blog");
|
||||
});
|
||||
it("converts dynamic segments [slug] by replacing brackets", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/blog/[slug].tsx",
|
||||
});
|
||||
// [ → - and ] is dropped (not a-z or -), so [slug] → -slug
|
||||
expect(result).toBe("blog--slug");
|
||||
});
|
||||
it("converts spread [...params] segments", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/[...params].tsx",
|
||||
});
|
||||
// "[...params]" → remove ext → "[...params]"
|
||||
// [ → "-" → "-...params]"
|
||||
// "..." → "-" → "--params]"
|
||||
// strip non [a-z-] → "--params"
|
||||
expect(result).toBe("--params");
|
||||
});
|
||||
it("strips uppercase letters (only a-z and - are kept)", () => {
|
||||
// [^a-z\-] strips uppercase — 'A' is removed, 'bout' remains
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/About.tsx" })).toBe("bout");
|
||||
});
|
||||
it("handles deeply nested paths", () => {
|
||||
expect(grabPageName({
|
||||
path: "/home/user/project/src/pages/admin/users/list.tsx",
|
||||
})).toBe("admin-users-list");
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/is-development.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/is-development.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
32
dist/__tests__/utils/is-development.test.js
vendored
Normal file
32
dist/__tests__/utils/is-development.test.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
beforeEach(() => {
|
||||
// Reset global config before each test
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
describe("isDevelopment", () => {
|
||||
it("returns false when NODE_ENV is production", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
global.CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
it("returns true when config.development is true and NODE_ENV is not production", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(true);
|
||||
});
|
||||
it("returns false when config.development is false", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = { development: false };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
it("returns false when config.development is undefined", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = {};
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
});
|
||||
1
dist/__tests__/utils/numberfy.test.d.ts
vendored
Normal file
1
dist/__tests__/utils/numberfy.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
43
dist/__tests__/utils/numberfy.test.js
vendored
Normal file
43
dist/__tests__/utils/numberfy.test.js
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import numberfy, { _n } from "../../utils/numberfy";
|
||||
describe("numberfy", () => {
|
||||
it("converts a plain integer string", () => {
|
||||
expect(numberfy("42")).toBe(42);
|
||||
});
|
||||
it("converts a float string preserving decimals", () => {
|
||||
expect(numberfy("3.14")).toBe(3.14);
|
||||
});
|
||||
it("strips non-numeric characters", () => {
|
||||
expect(numberfy("$1,234.56")).toBe(1234.56);
|
||||
});
|
||||
it("returns 0 for an empty string", () => {
|
||||
expect(numberfy("")).toBe(0);
|
||||
});
|
||||
it("returns 0 for undefined", () => {
|
||||
expect(numberfy(undefined)).toBe(0);
|
||||
});
|
||||
it("returns 0 for null", () => {
|
||||
expect(numberfy(null)).toBe(0);
|
||||
});
|
||||
it("passes through a number directly", () => {
|
||||
expect(numberfy(7)).toBe(7);
|
||||
});
|
||||
it("rounds when no decimals specified and no decimal in input", () => {
|
||||
expect(numberfy("5.0")).toBe(5);
|
||||
});
|
||||
it("respects decimals=0 by rounding", () => {
|
||||
expect(numberfy("3.7", 0)).toBe(4);
|
||||
});
|
||||
it("respects explicit decimals parameter", () => {
|
||||
expect(numberfy("3.14159", 2)).toBe(3.14);
|
||||
});
|
||||
it("preserves existing decimal places when no decimals arg given", () => {
|
||||
expect(numberfy("1.500")).toBe(1.5);
|
||||
});
|
||||
it("strips a trailing dot", () => {
|
||||
expect(numberfy("5.")).toBe(5);
|
||||
});
|
||||
it("_n alias works identically", () => {
|
||||
expect(_n("10")).toBe(10);
|
||||
});
|
||||
});
|
||||
3
dist/commands/build/index.js
vendored
3
dist/commands/build/index.js
vendored
@ -1,12 +1,15 @@
|
||||
import { Command } from "commander";
|
||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import init from "../../functions/init";
|
||||
export default function () {
|
||||
return new Command("build")
|
||||
.description("Build Project")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.BUILD = "true";
|
||||
await init();
|
||||
log.banner();
|
||||
log.build("Building Project ...");
|
||||
allPagesBundler({
|
||||
exit_after_first_build: true,
|
||||
|
||||
2
dist/functions/bunext-init.js
vendored
2
dist/functions/bunext-init.js
vendored
@ -10,7 +10,6 @@ import EJSON from "../utils/ejson";
|
||||
import { log } from "../utils/log";
|
||||
import cron from "./server/cron";
|
||||
export default async function bunextInit() {
|
||||
log.banner();
|
||||
global.ORA_SPINNER = ora();
|
||||
global.ORA_SPINNER.clear();
|
||||
global.HMR_CONTROLLERS = [];
|
||||
@ -18,6 +17,7 @@ export default async function bunextInit() {
|
||||
global.BUNDLER_REBUILDS = 0;
|
||||
global.PAGE_FILES = [];
|
||||
await init();
|
||||
log.banner();
|
||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
const router = new Bun.FileSystemRouter({
|
||||
style: "nextjs",
|
||||
|
||||
3
dist/functions/init.js
vendored
3
dist/functions/init.js
vendored
@ -8,7 +8,8 @@ export default async function () {
|
||||
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
||||
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
||||
try {
|
||||
const current_version = (await Bun.file(path.resolve(__dirname, "../../package.json")).json()).version;
|
||||
const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json();
|
||||
const current_version = package_json.version;
|
||||
global.CURRENT_VERSION = current_version;
|
||||
}
|
||||
catch (error) { }
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
type Params = {
|
||||
req: Request;
|
||||
};
|
||||
export default function bunextRequestHandler({ req, }: Params): Promise<Response>;
|
||||
export default function bunextRequestHandler({ req: initial_req, }: Params): Promise<Response>;
|
||||
export {};
|
||||
|
||||
10
dist/functions/server/bunext-req-handler.js
vendored
10
dist/functions/server/bunext-req-handler.js
vendored
@ -7,20 +7,24 @@ import handleHmr from "./handle-hmr";
|
||||
import handleHmrUpdate from "./handle-hmr-update";
|
||||
import handlePublic from "./handle-public";
|
||||
import handleFiles from "./handle-files";
|
||||
export default async function bunextRequestHandler({ req, }) {
|
||||
export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
const is_dev = isDevelopment();
|
||||
let req = initial_req.clone();
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const { config } = grabConstants();
|
||||
let response = undefined;
|
||||
if (config?.middleware) {
|
||||
const middleware_res = await config.middleware({
|
||||
req,
|
||||
req: initial_req,
|
||||
url,
|
||||
});
|
||||
if (typeof middleware_res == "object") {
|
||||
if (middleware_res instanceof Response) {
|
||||
return middleware_res;
|
||||
}
|
||||
if (middleware_res instanceof Request) {
|
||||
req = middleware_res;
|
||||
}
|
||||
}
|
||||
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
|
||||
response = await handleHmrUpdate({ req });
|
||||
|
||||
30
dist/functions/server/watcher.js
vendored
30
dist/functions/server/watcher.js
vendored
@ -3,14 +3,23 @@ import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import rebuildBundler from "./rebuild-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
const { SRC_DIR } = grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default function watcher() {
|
||||
const pages_src_watcher = watch(SRC_DIR, {
|
||||
const pages_src_watcher = watch(ROOT_DIR, {
|
||||
recursive: true,
|
||||
persistent: true,
|
||||
}, async (event, filename) => {
|
||||
if (!filename)
|
||||
return;
|
||||
const excluded_match = /node_modules\/|^public\/|^\.bunext\/|^\.git\/|^dist\/|bun\.lockb$/;
|
||||
if (filename.match(excluded_match))
|
||||
return;
|
||||
if (filename.match(/bunext.config\.ts/)) {
|
||||
await fullRebuild({
|
||||
msg: `bunext.config.ts file changed. Rebuilding server ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event !== "rename") {
|
||||
if (filename.match(/\.(tsx?|jsx?|css)$/) &&
|
||||
global.BUNDLER_CTX) {
|
||||
@ -21,17 +30,26 @@ export default function watcher() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!filename.match(/^pages\//))
|
||||
if (!filename.match(/^src\/pages\//))
|
||||
return;
|
||||
if (filename.match(/\/(--|\()/))
|
||||
return;
|
||||
if (global.RECOMPILING)
|
||||
return;
|
||||
const fullPath = path.join(SRC_DIR, filename);
|
||||
const fullPath = path.join(ROOT_DIR, filename);
|
||||
const action = existsSync(fullPath) ? "created" : "deleted";
|
||||
await fullRebuild({
|
||||
msg: `Page ${action}: ${filename}. Rebuilding ...`,
|
||||
});
|
||||
});
|
||||
global.PAGES_SRC_WATCHER = pages_src_watcher;
|
||||
}
|
||||
async function fullRebuild({ msg }) {
|
||||
try {
|
||||
global.RECOMPILING = true;
|
||||
log.watch(`Page ${action}: ${filename}. Rebuilding ...`);
|
||||
if (msg) {
|
||||
log.watch(msg);
|
||||
}
|
||||
await rebuildBundler();
|
||||
}
|
||||
catch (error) {
|
||||
@ -44,6 +62,4 @@ export default function watcher() {
|
||||
global.PAGES_SRC_WATCHER.close();
|
||||
watcher();
|
||||
}
|
||||
});
|
||||
global.PAGES_SRC_WATCHER = pages_src_watcher;
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export default async function (params) {
|
||||
script += ` document.head.appendChild(newScript);\n\n`;
|
||||
script += ` } catch (err) {\n`;
|
||||
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
script += ` window.location.reload();\n`;
|
||||
script += ` }\n`;
|
||||
script += ` }\n`;
|
||||
script += `});\n`;
|
||||
|
||||
2
dist/types/index.d.ts
vendored
2
dist/types/index.d.ts
vendored
@ -44,7 +44,7 @@ export type BunextConfig = {
|
||||
};
|
||||
port?: number;
|
||||
development?: boolean;
|
||||
middleware?: (params: BunextConfigMiddlewareParams) => Promise<Response | undefined> | Response | undefined;
|
||||
middleware?: (params: BunextConfigMiddlewareParams) => Promise<Response | Request | undefined> | Response | Request | undefined;
|
||||
defaultCacheExpiry?: number;
|
||||
websocket?: WebSocketHandler<any>;
|
||||
serverOptions?: ServeOptions;
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "tsc --watch",
|
||||
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Bugfixes. Documentation update.' && git push",
|
||||
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Update watcher function. Add tests' && git push",
|
||||
"compile": "bun build ./src/commands/index.ts --compile --outfile bin/bunext",
|
||||
"build": "tsc"
|
||||
},
|
||||
|
||||
43
src/__tests__/functions/cache/grab-cache-names.test.ts
vendored
Normal file
43
src/__tests__/functions/cache/grab-cache-names.test.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabCacheNames from "../../../functions/cache/grab-cache-names";
|
||||
|
||||
describe("grabCacheNames", () => {
|
||||
it("returns cache_name and cache_meta_name for a simple key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({ key: "home" });
|
||||
expect(cache_name).toBe("home.res.html");
|
||||
expect(cache_meta_name).toBe("home.meta.json");
|
||||
});
|
||||
|
||||
it("defaults paradigm to html", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "page" });
|
||||
expect(cache_name).toEndWith(".res.html");
|
||||
});
|
||||
|
||||
it("uses json paradigm when specified", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "api-data", paradigm: "json" });
|
||||
expect(cache_name).toBe("api-data.res.json");
|
||||
});
|
||||
|
||||
it("URL-encodes the key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
||||
key: "/blog/hello world",
|
||||
});
|
||||
const encoded = encodeURIComponent("/blog/hello world");
|
||||
expect(cache_name).toBe(`${encoded}.res.html`);
|
||||
expect(cache_meta_name).toBe(`${encoded}.meta.json`);
|
||||
});
|
||||
|
||||
it("handles keys with special characters", () => {
|
||||
const key = "page?id=1&sort=asc";
|
||||
const { cache_name } = grabCacheNames({ key });
|
||||
expect(cache_name).toBe(`${encodeURIComponent(key)}.res.html`);
|
||||
});
|
||||
|
||||
it("cache_meta_name always uses .meta.json regardless of paradigm", () => {
|
||||
const { cache_meta_name } = grabCacheNames({
|
||||
key: "test",
|
||||
paradigm: "json",
|
||||
});
|
||||
expect(cache_meta_name).toBe("test.meta.json");
|
||||
});
|
||||
});
|
||||
110
src/__tests__/functions/server/grab-web-meta-html.test.ts
Normal file
110
src/__tests__/functions/server/grab-web-meta-html.test.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
|
||||
|
||||
describe("grabWebMetaHTML", () => {
|
||||
it("returns empty string for empty meta object", () => {
|
||||
expect(grabWebMetaHTML({ meta: {} })).toBe("");
|
||||
});
|
||||
|
||||
it("generates a title tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
|
||||
expect(html).toContain("<title>My Page</title>");
|
||||
});
|
||||
|
||||
it("generates a description meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { description: "A description" } });
|
||||
expect(html).toContain('<meta name="description" content="A description"');
|
||||
});
|
||||
|
||||
it("joins array keywords with comma", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { keywords: ["react", "bun", "ssr"] },
|
||||
});
|
||||
expect(html).toContain('content="react, bun, ssr"');
|
||||
});
|
||||
|
||||
it("uses string keywords directly", () => {
|
||||
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
|
||||
expect(html).toContain('content="react, bun"');
|
||||
});
|
||||
|
||||
it("generates author meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
|
||||
expect(html).toContain('<meta name="author" content="Alice"');
|
||||
});
|
||||
|
||||
it("generates robots meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
|
||||
expect(html).toContain('<meta name="robots" content="noindex"');
|
||||
});
|
||||
|
||||
it("generates canonical link tag", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { canonical: "https://example.com/page" },
|
||||
});
|
||||
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
|
||||
});
|
||||
|
||||
it("generates theme-color meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
|
||||
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
|
||||
});
|
||||
|
||||
it("generates OG tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
og: {
|
||||
title: "OG Title",
|
||||
description: "OG Desc",
|
||||
image: "https://example.com/img.png",
|
||||
url: "https://example.com",
|
||||
type: "website",
|
||||
siteName: "Example",
|
||||
locale: "en_US",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta property="og:title" content="OG Title"');
|
||||
expect(html).toContain('<meta property="og:description" content="OG Desc"');
|
||||
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
|
||||
expect(html).toContain('<meta property="og:url" content="https://example.com"');
|
||||
expect(html).toContain('<meta property="og:type" content="website"');
|
||||
expect(html).toContain('<meta property="og:site_name" content="Example"');
|
||||
expect(html).toContain('<meta property="og:locale" content="en_US"');
|
||||
});
|
||||
|
||||
it("generates Twitter card tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Tweet Title",
|
||||
description: "Tweet Desc",
|
||||
image: "https://example.com/tw.png",
|
||||
site: "@example",
|
||||
creator: "@alice",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
|
||||
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
|
||||
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
|
||||
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
|
||||
expect(html).toContain('<meta name="twitter:site" content="@example"');
|
||||
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
|
||||
});
|
||||
|
||||
it("skips undefined OG fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
|
||||
expect(html).toContain("og:title");
|
||||
expect(html).not.toContain("og:description");
|
||||
expect(html).not.toContain("og:image");
|
||||
});
|
||||
|
||||
it("does not emit tags for missing fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
|
||||
expect(html).not.toContain("description");
|
||||
expect(html).not.toContain("og:");
|
||||
expect(html).not.toContain("twitter:");
|
||||
});
|
||||
});
|
||||
40
src/__tests__/utils/deserialize-query.test.ts
Normal file
40
src/__tests__/utils/deserialize-query.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import deserializeQuery from "../../utils/deserialize-query";
|
||||
|
||||
describe("deserializeQuery", () => {
|
||||
it("passes through a plain object unchanged", () => {
|
||||
const input = { foo: "bar" };
|
||||
expect(deserializeQuery(input)).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("parses a JSON string into an object", () => {
|
||||
const input = JSON.stringify({ a: 1, b: "hello" });
|
||||
expect(deserializeQuery(input)).toEqual({ a: 1, b: "hello" });
|
||||
});
|
||||
|
||||
it("deep-parses string values that look like JSON objects", () => {
|
||||
const nested = { filter: JSON.stringify({ status: "active" }) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.filter).toEqual({ status: "active" });
|
||||
});
|
||||
|
||||
it("deep-parses string values that look like JSON arrays", () => {
|
||||
const nested = { ids: JSON.stringify([1, 2, 3]) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.ids).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("leaves plain string values alone", () => {
|
||||
const input = { name: "alice", age: "30" };
|
||||
expect(deserializeQuery(input)).toEqual({ name: "alice", age: "30" });
|
||||
});
|
||||
|
||||
it("returns an empty object for an empty JSON string", () => {
|
||||
expect(deserializeQuery("{}")).toEqual({});
|
||||
});
|
||||
|
||||
it("returns an empty object for an invalid JSON string", () => {
|
||||
// EJSON.parse returns undefined → Object(undefined) → {}
|
||||
expect(deserializeQuery("not-json")).toEqual({});
|
||||
});
|
||||
});
|
||||
68
src/__tests__/utils/ejson.test.ts
Normal file
68
src/__tests__/utils/ejson.test.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import EJSON from "../../utils/ejson";
|
||||
|
||||
describe("EJSON.parse", () => {
|
||||
it("parses a valid JSON string", () => {
|
||||
expect(EJSON.parse('{"a":1}')).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it("parses a JSON array string", () => {
|
||||
expect(EJSON.parse('[1,2,3]')).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("returns undefined for null input", () => {
|
||||
expect(EJSON.parse(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for empty string", () => {
|
||||
expect(EJSON.parse("")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for invalid JSON", () => {
|
||||
expect(EJSON.parse("{bad json")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns the object directly when passed an object (typeof object)", () => {
|
||||
const obj = { x: 1 };
|
||||
expect(EJSON.parse(obj as any)).toBe(obj);
|
||||
});
|
||||
|
||||
it("returns undefined for a number input", () => {
|
||||
expect(EJSON.parse(42)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies a reviver function", () => {
|
||||
const result = EJSON.parse('{"a":"2"}', (key, value) =>
|
||||
key === "a" ? Number(value) : value,
|
||||
);
|
||||
expect(result).toEqual({ a: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("EJSON.stringify", () => {
|
||||
it("stringifies an object", () => {
|
||||
expect(EJSON.stringify({ a: 1 })).toBe('{"a":1}');
|
||||
});
|
||||
|
||||
it("stringifies an array", () => {
|
||||
expect(EJSON.stringify([1, 2, 3])).toBe("[1,2,3]");
|
||||
});
|
||||
|
||||
it("applies spacing", () => {
|
||||
expect(EJSON.stringify({ a: 1 }, null, 2)).toBe('{\n "a": 1\n}');
|
||||
});
|
||||
|
||||
it("returns undefined for circular references", () => {
|
||||
const obj: any = {};
|
||||
obj.self = obj;
|
||||
expect(EJSON.stringify(obj)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stringifies null", () => {
|
||||
expect(EJSON.stringify(null)).toBe("null");
|
||||
});
|
||||
|
||||
it("stringifies a number", () => {
|
||||
expect(EJSON.stringify(42)).toBe("42");
|
||||
});
|
||||
});
|
||||
29
src/__tests__/utils/grab-app-names.test.ts
Normal file
29
src/__tests__/utils/grab-app-names.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
|
||||
describe("AppNames", () => {
|
||||
it("has a defaultPort of 7000", () => {
|
||||
expect(AppNames.defaultPort).toBe(7000);
|
||||
});
|
||||
|
||||
it("has the correct defaultAssetPrefix", () => {
|
||||
expect(AppNames.defaultAssetPrefix).toBe("_bunext/static");
|
||||
});
|
||||
|
||||
it("has name Bunext", () => {
|
||||
expect(AppNames.name).toBe("Bunext");
|
||||
});
|
||||
|
||||
it("has a version string", () => {
|
||||
expect(typeof AppNames.version).toBe("string");
|
||||
expect(AppNames.version.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("has defaultDistDir as .bunext", () => {
|
||||
expect(AppNames.defaultDistDir).toBe(".bunext");
|
||||
});
|
||||
|
||||
it("has RootPagesComponentName as __root", () => {
|
||||
expect(AppNames.RootPagesComponentName).toBe("__root");
|
||||
});
|
||||
});
|
||||
45
src/__tests__/utils/grab-app-port.test.ts
Normal file
45
src/__tests__/utils/grab-app-port.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabAppPort from "../../utils/grab-app-port";
|
||||
|
||||
const originalEnv = process.env.PORT;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
(global as any).CONFIG = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.PORT = originalEnv;
|
||||
} else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
|
||||
describe("grabAppPort", () => {
|
||||
it("returns the default port (7000) when no config or env set", () => {
|
||||
expect(grabAppPort()).toBe(7000);
|
||||
});
|
||||
|
||||
it("uses PORT env variable when set", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabAppPort()).toBe(8080);
|
||||
});
|
||||
|
||||
it("uses config.port when PORT env is not set", () => {
|
||||
(global as any).CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(3000);
|
||||
});
|
||||
|
||||
it("PORT env takes precedence over config.port", () => {
|
||||
process.env.PORT = "9000";
|
||||
(global as any).CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(9000);
|
||||
});
|
||||
|
||||
it("handles non-numeric PORT env gracefully via numberfy", () => {
|
||||
process.env.PORT = "abc";
|
||||
// numberfy strips non-numeric chars, "abc" → "" → 0
|
||||
expect(grabAppPort()).toBe(0);
|
||||
});
|
||||
});
|
||||
40
src/__tests__/utils/grab-constants.test.ts
Normal file
40
src/__tests__/utils/grab-constants.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).CONFIG = {};
|
||||
});
|
||||
|
||||
describe("grabConstants", () => {
|
||||
it("has the correct ClientRootElementIDName", () => {
|
||||
expect(grabConstants().ClientRootElementIDName).toBe("__bunext");
|
||||
});
|
||||
|
||||
it("has the correct ClientWindowPagePropsName", () => {
|
||||
expect(grabConstants().ClientWindowPagePropsName).toBe("__PAGE_PROPS__");
|
||||
});
|
||||
|
||||
it("has the correct ClientRootComponentWindowName", () => {
|
||||
expect(grabConstants().ClientRootComponentWindowName).toBe("BUNEXT_ROOT");
|
||||
});
|
||||
|
||||
it("calculates MBInBytes as 1024 * 1024", () => {
|
||||
expect(grabConstants().MBInBytes).toBe(1024 * 1024);
|
||||
});
|
||||
|
||||
it("ServerDefaultRequestBodyLimitBytes is 10 MB", () => {
|
||||
expect(grabConstants().ServerDefaultRequestBodyLimitBytes).toBe(
|
||||
10 * 1024 * 1024,
|
||||
);
|
||||
});
|
||||
|
||||
it("MaxBundlerRebuilds is 5", () => {
|
||||
expect(grabConstants().MaxBundlerRebuilds).toBe(5);
|
||||
});
|
||||
|
||||
it("returns the current global.CONFIG", () => {
|
||||
const cfg = { port: 9000 };
|
||||
(global as any).CONFIG = cfg;
|
||||
expect(grabConstants().config).toBe(cfg);
|
||||
});
|
||||
});
|
||||
75
src/__tests__/utils/grab-dir-names.test.ts
Normal file
75
src/__tests__/utils/grab-dir-names.test.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
|
||||
describe("grabDirNames", () => {
|
||||
it("derives all paths from process.cwd()", () => {
|
||||
const cwd = process.cwd();
|
||||
const dirs = grabDirNames();
|
||||
|
||||
expect(dirs.ROOT_DIR).toBe(cwd);
|
||||
expect(dirs.SRC_DIR).toBe(path.join(cwd, "src"));
|
||||
expect(dirs.PAGES_DIR).toBe(path.join(cwd, "src", "pages"));
|
||||
expect(dirs.API_DIR).toBe(path.join(cwd, "src", "pages", "api"));
|
||||
expect(dirs.PUBLIC_DIR).toBe(path.join(cwd, "public"));
|
||||
});
|
||||
|
||||
it("nests HYDRATION_DST_DIR under public/__bunext/pages", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR).toBe(
|
||||
path.join(dirs.PUBLIC_DIR, "__bunext", "pages"),
|
||||
);
|
||||
});
|
||||
|
||||
it("nests BUNEXT_CACHE_DIR under public/__bunext/cache", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNEXT_CACHE_DIR).toBe(
|
||||
path.join(dirs.PUBLIC_DIR, "__bunext", "cache"),
|
||||
);
|
||||
});
|
||||
|
||||
it("places map JSON file inside HYDRATION_DST_DIR", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR_MAP_JSON_FILE).toBe(
|
||||
path.join(dirs.HYDRATION_DST_DIR, "map.json"),
|
||||
);
|
||||
});
|
||||
|
||||
it("places CONFIG_FILE at root", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.CONFIG_FILE).toBe(path.join(dirs.ROOT_DIR, "bunext.config.ts"));
|
||||
});
|
||||
|
||||
it("places BUNX_TMP_DIR inside .bunext", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_TMP_DIR).toContain(".bunext");
|
||||
expect(dirs.BUNX_TMP_DIR).toEndWith(".tmp");
|
||||
});
|
||||
|
||||
it("places BUNX_HYDRATION_SRC_DIR under client/hydration-src", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_HYDRATION_SRC_DIR).toContain(
|
||||
path.join("client", "hydration-src"),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets 404 file name to not-found", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_FILE_NAME).toBe("not-found");
|
||||
});
|
||||
|
||||
it("sets 500 file name to server-error", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_FILE_NAME).toBe("server-error");
|
||||
});
|
||||
|
||||
it("preset 404 component path ends with not-found.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_PRESET_COMPONENT).toEndWith("not-found.tsx");
|
||||
});
|
||||
|
||||
it("preset 500 component path ends with server-error.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_PRESET_COMPONENT).toEndWith("server-error.tsx");
|
||||
});
|
||||
});
|
||||
38
src/__tests__/utils/grab-origin.test.ts
Normal file
38
src/__tests__/utils/grab-origin.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabOrigin from "../../utils/grab-origin";
|
||||
|
||||
const originalPort = process.env.PORT;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
(global as any).CONFIG = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPort !== undefined) {
|
||||
process.env.PORT = originalPort;
|
||||
} else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
|
||||
describe("grabOrigin", () => {
|
||||
it("returns config.origin when set", () => {
|
||||
(global as any).CONFIG = { origin: "https://example.com" };
|
||||
expect(grabOrigin()).toBe("https://example.com");
|
||||
});
|
||||
|
||||
it("falls back to http://localhost:<port> using default port", () => {
|
||||
expect(grabOrigin()).toBe("http://localhost:7000");
|
||||
});
|
||||
|
||||
it("falls back using PORT env variable", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabOrigin()).toBe("http://localhost:8080");
|
||||
});
|
||||
|
||||
it("falls back using config.port", () => {
|
||||
(global as any).CONFIG = { port: 3700 };
|
||||
expect(grabOrigin()).toBe("http://localhost:3700");
|
||||
});
|
||||
});
|
||||
62
src/__tests__/utils/grab-page-name.test.ts
Normal file
62
src/__tests__/utils/grab-page-name.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabPageName from "../../utils/grab-page-name";
|
||||
|
||||
describe("grabPageName", () => {
|
||||
it("returns the page name for a simple page path", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/about.tsx" }))
|
||||
.toBe("about");
|
||||
});
|
||||
|
||||
it("returns 'index' for a root index file (no -index stripping at root)", () => {
|
||||
// -index suffix is only stripped when joined: e.g. "blog-index" → "blog"
|
||||
// A standalone "index" filename has no leading dash so stays as-is
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/index.tsx" }))
|
||||
.toBe("index");
|
||||
});
|
||||
|
||||
it("handles nested page paths", () => {
|
||||
expect(
|
||||
grabPageName({ path: "/home/user/project/src/pages/blog/post.tsx" }),
|
||||
).toBe("blog-post");
|
||||
});
|
||||
|
||||
it("strips -index suffix from nested index files", () => {
|
||||
expect(
|
||||
grabPageName({ path: "/home/user/project/src/pages/blog/index.tsx" }),
|
||||
).toBe("blog");
|
||||
});
|
||||
|
||||
it("converts dynamic segments [slug] by replacing brackets", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/blog/[slug].tsx",
|
||||
});
|
||||
// [ → - and ] is dropped (not a-z or -), so [slug] → -slug
|
||||
expect(result).toBe("blog--slug");
|
||||
});
|
||||
|
||||
it("converts spread [...params] segments", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/[...params].tsx",
|
||||
});
|
||||
// "[...params]" → remove ext → "[...params]"
|
||||
// [ → "-" → "-...params]"
|
||||
// "..." → "-" → "--params]"
|
||||
// strip non [a-z-] → "--params"
|
||||
expect(result).toBe("--params");
|
||||
});
|
||||
|
||||
it("strips uppercase letters (only a-z and - are kept)", () => {
|
||||
// [^a-z\-] strips uppercase — 'A' is removed, 'bout' remains
|
||||
expect(
|
||||
grabPageName({ path: "/home/user/project/src/pages/About.tsx" }),
|
||||
).toBe("bout");
|
||||
});
|
||||
|
||||
it("handles deeply nested paths", () => {
|
||||
expect(
|
||||
grabPageName({
|
||||
path: "/home/user/project/src/pages/admin/users/list.tsx",
|
||||
}),
|
||||
).toBe("admin-users-list");
|
||||
});
|
||||
});
|
||||
39
src/__tests__/utils/is-development.test.ts
Normal file
39
src/__tests__/utils/is-development.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset global config before each test
|
||||
(global as any).CONFIG = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
describe("isDevelopment", () => {
|
||||
it("returns false when NODE_ENV is production", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
(global as any).CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when config.development is true and NODE_ENV is not production", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
(global as any).CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when config.development is false", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
(global as any).CONFIG = { development: false };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when config.development is undefined", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
(global as any).CONFIG = {};
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
});
|
||||
56
src/__tests__/utils/numberfy.test.ts
Normal file
56
src/__tests__/utils/numberfy.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import numberfy, { _n } from "../../utils/numberfy";
|
||||
|
||||
describe("numberfy", () => {
|
||||
it("converts a plain integer string", () => {
|
||||
expect(numberfy("42")).toBe(42);
|
||||
});
|
||||
|
||||
it("converts a float string preserving decimals", () => {
|
||||
expect(numberfy("3.14")).toBe(3.14);
|
||||
});
|
||||
|
||||
it("strips non-numeric characters", () => {
|
||||
expect(numberfy("$1,234.56")).toBe(1234.56);
|
||||
});
|
||||
|
||||
it("returns 0 for an empty string", () => {
|
||||
expect(numberfy("")).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 for undefined", () => {
|
||||
expect(numberfy(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 for null", () => {
|
||||
expect(numberfy(null)).toBe(0);
|
||||
});
|
||||
|
||||
it("passes through a number directly", () => {
|
||||
expect(numberfy(7)).toBe(7);
|
||||
});
|
||||
|
||||
it("rounds when no decimals specified and no decimal in input", () => {
|
||||
expect(numberfy("5.0")).toBe(5);
|
||||
});
|
||||
|
||||
it("respects decimals=0 by rounding", () => {
|
||||
expect(numberfy("3.7", 0)).toBe(4);
|
||||
});
|
||||
|
||||
it("respects explicit decimals parameter", () => {
|
||||
expect(numberfy("3.14159", 2)).toBe(3.14);
|
||||
});
|
||||
|
||||
it("preserves existing decimal places when no decimals arg given", () => {
|
||||
expect(numberfy("1.500")).toBe(1.5);
|
||||
});
|
||||
|
||||
it("strips a trailing dot", () => {
|
||||
expect(numberfy("5.")).toBe(5);
|
||||
});
|
||||
|
||||
it("_n alias works identically", () => {
|
||||
expect(_n("10")).toBe(10);
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import { Command } from "commander";
|
||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import init from "../../functions/init";
|
||||
|
||||
export default function () {
|
||||
return new Command("build")
|
||||
@ -9,6 +10,9 @@ export default function () {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.BUILD = "true";
|
||||
|
||||
await init();
|
||||
|
||||
log.banner();
|
||||
log.build("Building Project ...");
|
||||
|
||||
allPagesBundler({
|
||||
|
||||
@ -40,8 +40,6 @@ declare global {
|
||||
}
|
||||
|
||||
export default async function bunextInit() {
|
||||
log.banner();
|
||||
|
||||
global.ORA_SPINNER = ora();
|
||||
global.ORA_SPINNER.clear();
|
||||
global.HMR_CONTROLLERS = [];
|
||||
@ -50,6 +48,7 @@ export default async function bunextInit() {
|
||||
global.PAGE_FILES = [];
|
||||
|
||||
await init();
|
||||
log.banner();
|
||||
|
||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
|
||||
|
||||
@ -12,9 +12,11 @@ export default async function () {
|
||||
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
||||
|
||||
try {
|
||||
const current_version = (
|
||||
await Bun.file(path.resolve(__dirname, "../../package.json")).json()
|
||||
).version;
|
||||
const package_json = await Bun.file(
|
||||
path.resolve(__dirname, "../../package.json"),
|
||||
).json();
|
||||
|
||||
const current_version = package_json.version;
|
||||
|
||||
global.CURRENT_VERSION = current_version;
|
||||
} catch (error) {}
|
||||
|
||||
@ -7,15 +7,15 @@ import handleHmr from "./handle-hmr";
|
||||
import handleHmrUpdate from "./handle-hmr-update";
|
||||
import handlePublic from "./handle-public";
|
||||
import handleFiles from "./handle-files";
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
};
|
||||
|
||||
export default async function bunextRequestHandler({
|
||||
req,
|
||||
req: initial_req,
|
||||
}: Params): Promise<Response> {
|
||||
const is_dev = isDevelopment();
|
||||
let req = initial_req.clone();
|
||||
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
@ -26,13 +26,17 @@ export default async function bunextRequestHandler({
|
||||
|
||||
if (config?.middleware) {
|
||||
const middleware_res = await config.middleware({
|
||||
req,
|
||||
req: initial_req,
|
||||
url,
|
||||
});
|
||||
|
||||
if (typeof middleware_res == "object") {
|
||||
if (middleware_res instanceof Response) {
|
||||
return middleware_res;
|
||||
}
|
||||
|
||||
if (middleware_res instanceof Request) {
|
||||
req = middleware_res;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
|
||||
|
||||
78
src/functions/server/watcher.ts
Normal file
78
src/functions/server/watcher.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { watch, existsSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import rebuildBundler from "./rebuild-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
export default function watcher() {
|
||||
const pages_src_watcher = watch(
|
||||
ROOT_DIR,
|
||||
{
|
||||
recursive: true,
|
||||
persistent: true,
|
||||
},
|
||||
async (event, filename) => {
|
||||
if (!filename) return;
|
||||
const excluded_match =
|
||||
/node_modules\/|^public\/|^\.bunext\/|^\.git\/|^dist\/|bun\.lockb$/;
|
||||
|
||||
if (filename.match(excluded_match)) return;
|
||||
|
||||
if (filename.match(/bunext.config\.ts/)) {
|
||||
await fullRebuild({
|
||||
msg: `bunext.config.ts file changed. Rebuilding server ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (event !== "rename") {
|
||||
if (
|
||||
filename.match(/\.(tsx?|jsx?|css)$/) &&
|
||||
global.BUNDLER_CTX
|
||||
) {
|
||||
if (global.RECOMPILING) return;
|
||||
global.RECOMPILING = true;
|
||||
await global.BUNDLER_CTX.rebuild();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filename.match(/^src\/pages\//)) return;
|
||||
if (filename.match(/\/(--|\()/)) return;
|
||||
|
||||
if (global.RECOMPILING) return;
|
||||
|
||||
const fullPath = path.join(ROOT_DIR, filename);
|
||||
const action = existsSync(fullPath) ? "created" : "deleted";
|
||||
|
||||
await fullRebuild({
|
||||
msg: `Page ${action}: ${filename}. Rebuilding ...`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
global.PAGES_SRC_WATCHER = pages_src_watcher;
|
||||
}
|
||||
|
||||
async function fullRebuild({ msg }: { msg?: string }) {
|
||||
try {
|
||||
global.RECOMPILING = true;
|
||||
|
||||
if (msg) {
|
||||
log.watch(msg);
|
||||
}
|
||||
|
||||
await rebuildBundler();
|
||||
} catch (error: any) {
|
||||
log.error(error);
|
||||
} finally {
|
||||
global.RECOMPILING = false;
|
||||
}
|
||||
|
||||
if (global.PAGES_SRC_WATCHER) {
|
||||
global.PAGES_SRC_WATCHER.close();
|
||||
watcher();
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
import { watch, existsSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import rebuildBundler from "./rebuild-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
|
||||
const { SRC_DIR } = grabDirNames();
|
||||
|
||||
export default function watcher() {
|
||||
const pages_src_watcher = watch(
|
||||
SRC_DIR,
|
||||
{
|
||||
recursive: true,
|
||||
persistent: true,
|
||||
},
|
||||
async (event, filename) => {
|
||||
if (!filename) return;
|
||||
|
||||
if (event !== "rename") {
|
||||
if (
|
||||
filename.match(/\.(tsx?|jsx?|css)$/) &&
|
||||
global.BUNDLER_CTX
|
||||
) {
|
||||
if (global.RECOMPILING) return;
|
||||
global.RECOMPILING = true;
|
||||
await global.BUNDLER_CTX.rebuild();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!filename.match(/^pages\//)) return;
|
||||
if (filename.match(/\/(--|\()/)) return;
|
||||
|
||||
if (global.RECOMPILING) return;
|
||||
|
||||
const fullPath = path.join(SRC_DIR, filename);
|
||||
const action = existsSync(fullPath) ? "created" : "deleted";
|
||||
|
||||
try {
|
||||
global.RECOMPILING = true;
|
||||
|
||||
log.watch(`Page ${action}: ${filename}. Rebuilding ...`);
|
||||
|
||||
await rebuildBundler();
|
||||
} catch (error: any) {
|
||||
log.error(error);
|
||||
} finally {
|
||||
global.RECOMPILING = false;
|
||||
}
|
||||
|
||||
if (global.PAGES_SRC_WATCHER) {
|
||||
global.PAGES_SRC_WATCHER.close();
|
||||
watcher();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
global.PAGES_SRC_WATCHER = pages_src_watcher;
|
||||
}
|
||||
@ -59,7 +59,7 @@ export default async function (params?: Params) {
|
||||
script += ` document.head.appendChild(newScript);\n\n`;
|
||||
script += ` } catch (err) {\n`;
|
||||
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
script += ` window.location.reload();\n`;
|
||||
script += ` }\n`;
|
||||
script += ` }\n`;
|
||||
script += `});\n`;
|
||||
|
||||
@ -50,7 +50,11 @@ export type BunextConfig = {
|
||||
development?: boolean;
|
||||
middleware?: (
|
||||
params: BunextConfigMiddlewareParams,
|
||||
) => Promise<Response | undefined> | Response | undefined;
|
||||
) =>
|
||||
| Promise<Response | Request | undefined>
|
||||
| Response
|
||||
| Request
|
||||
| undefined;
|
||||
defaultCacheExpiry?: number;
|
||||
websocket?: WebSocketHandler<any>;
|
||||
serverOptions?: ServeOptions;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user