Compare commits

..

No commits in common. "ceeb6fbdaf711c3d8e27b5c17a0b0a0fb1a00df7" and "cb5126c94740b2edf27699a844bc866270db4d68" have entirely different histories.

44 changed files with 105 additions and 353 deletions

1
.gitignore vendored
View File

@ -182,4 +182,3 @@ __fixtures__
/.dump /.dump
/.vscode /.vscode
/source.md /source.md
SECURITY.md

View File

@ -1,9 +1,6 @@
import { Command } from "commander"; import { Command } from "commander";
import path from "path"; import path from "path";
import writeErrorFile from "../../functions/write-error-file"; import writeErrorFile from "../../functions/write-error-file";
let retries = 0;
let timeout;
const MAX_RETRIES = 5;
export default function () { export default function () {
return new Command("start") return new Command("start")
.description("Start production server") .description("Start production server")
@ -12,11 +9,6 @@ export default function () {
}); });
} }
async function start() { async function start() {
clearTimeout(timeout);
if (retries >= MAX_RETRIES) {
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
}
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts"); const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
const spawn_options = { const spawn_options = {
cmd: ["bun", dev_spawn_file], cmd: ["bun", dev_spawn_file],
@ -30,10 +22,6 @@ async function start() {
}, },
}; };
let dev_process = Bun.spawn(spawn_options); let dev_process = Bun.spawn(spawn_options);
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
const exited = await dev_process.exited; const exited = await dev_process.exited;
if (exited) { if (exited) {
return await start(); return await start();

View File

@ -4,7 +4,7 @@ export default async function buildOnstartErrorHandler(params) {
global.BUNDLER_CTX_DISPOSED = true; global.BUNDLER_CTX_DISPOSED = true;
global.RECOMPILING = false; global.RECOMPILING = false;
global.IS_SERVER_COMPONENT = false; global.IS_SERVER_COMPONENT = false;
await Promise.all([ Promise.all([
global.SSR_BUNDLER_CTX?.dispose(), global.SSR_BUNDLER_CTX?.dispose(),
global.BUNDLER_CTX?.dispose(), global.BUNDLER_CTX?.dispose(),
]); ]);

View File

@ -1,6 +1,6 @@
import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, PageFiles } from "../types"; import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, PageFiles } from "../types";
import type { FileSystemRouter, Server } from "bun"; import type { FileSystemRouter, Server } from "bun";
import { type DirNames } from "../utils/grab-dir-names"; import grabDirNames from "../utils/grab-dir-names";
import { type FSWatcher } from "fs"; import { type FSWatcher } from "fs";
import type { BuildContext } from "esbuild"; import type { BuildContext } from "esbuild";
import grabConstants from "../utils/grab-constants"; import grabConstants from "../utils/grab-constants";
@ -31,7 +31,7 @@ declare global {
var SKIPPED_BROWSER_MODULES: Set<string>; var SKIPPED_BROWSER_MODULES: Set<string>;
var BUNDLER_CTX: BuildContext | undefined; var BUNDLER_CTX: BuildContext | undefined;
var SSR_BUNDLER_CTX: BuildContext | undefined; var SSR_BUNDLER_CTX: BuildContext | undefined;
var DIR_NAMES: DirNames; var DIR_NAMES: ReturnType<typeof grabDirNames>;
var REACT_IMPORTS_MAP: { var REACT_IMPORTS_MAP: {
imports: Record<string, string>; imports: Record<string, string>;
}; };

View File

@ -1,4 +1,4 @@
import grabDirNames, {} from "../utils/grab-dir-names"; import grabDirNames from "../utils/grab-dir-names";
import {} from "fs"; import {} from "fs";
import init from "./init"; import init from "./init";
import isDevelopment from "../utils/is-development"; import isDevelopment from "../utils/is-development";

View File

@ -13,10 +13,6 @@ export default async function trimAllCache() {
const trim_key = await trimCacheKey({ const trim_key = await trimCacheKey({
key: cache_key, key: cache_key,
}); });
if (trim_key.success) {
cached_items.splice(i, 1);
i--;
}
} }
} }
catch (error) { catch (error) {

View File

@ -8,8 +8,6 @@ import handleBunextPublicAssets from "./handle-bunext-public-assets";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
const HMR_RETRY_COOLDOWN_MS = 5000;
let lastHmrRetryTime = 0;
export default async function bunextRequestHandler({ req: initial_req, server, }) { export default async function bunextRequestHandler({ req: initial_req, server, }) {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
let req = initial_req.clone(); let req = initial_req.clone();
@ -32,11 +30,6 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
} }
} }
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) { if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
const now = Date.now();
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
return new Response("Too Many Requests", { status: 429 });
}
lastHmrRetryTime = now;
await fullRebuild({ msg: `HMR Retry Rebuild ...` }); await fullRebuild({ msg: `HMR Retry Rebuild ...` });
return new Response("Modules Rebuilt"); return new Response("Modules Rebuilt");
} }
@ -67,12 +60,8 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
return response; return response;
} }
catch (error) { catch (error) {
if (is_dev) {
return new Response(`Server Error: ${error.message}`, { return new Response(`Server Error: ${error.message}`, {
status: 500, status: 500,
}); });
} }
console.error(`Server Error: ${error.message}`, error);
return new Response("Internal Server Error", { status: 500 });
}
} }

View File

@ -2,14 +2,13 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { readFileResponse } from "./handle-public"; import { readFileResponse } from "./handle-public";
import isSafePath from "../../utils/is-safe-path";
const { BUNEXT_PUBLIC_DIR } = grabDirNames(); const { BUNEXT_PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, "")); const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, ""));
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) { if (!file_path.startsWith(BUNEXT_PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
return readFileResponse({ return readFileResponse({

View File

@ -2,14 +2,13 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname); const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) { if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
if (!existsSync(file_path)) { if (!existsSync(file_path)) {
@ -18,11 +17,7 @@ export default async function ({ req }) {
}); });
} }
const file = Bun.file(file_path); const file = Bun.file(file_path);
const headers = new Headers(); return new Response(file);
if (!is_dev) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { headers });
} }
catch (error) { catch (error) {
return new Response(`File Not Found`, { return new Response(`File Not Found`, {

View File

@ -1,21 +1,5 @@
function removeController(controller) {
const idx = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof idx == "number" && idx >= 0) {
global.HMR_CONTROLLERS.splice(idx, 1);
}
}
export default async function ({ req }) { export default async function ({ req }) {
const referer = req.headers.get("referer"); const referer_url = new URL(req.headers.get("referer") || "");
if (!referer) {
return new Response("Missing Referer Header", { status: 400 });
}
let referer_url;
try {
referer_url = new URL(referer);
}
catch {
return new Response("Invalid Referer Header", { status: 400 });
}
const match = global.ROUTER.match(referer_url.pathname); const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.[match.filePath] ? global.BUNDLER_CTX_MAP?.[match.filePath]
@ -36,13 +20,16 @@ export default async function ({ req }) {
} }
catch { catch {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller);
} }
}, 5000); }, 5000);
}, },
cancel() { cancel() {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller); const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
}, },
}); });
return new Response(stream, { return new Response(stream, {

View File

@ -2,14 +2,13 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, "")); const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, ""));
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) { if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
return readFileResponse({ file_path }); return readFileResponse({ file_path });
@ -34,9 +33,6 @@ export function readFileResponse({ file_path, cache }) {
else if (cache?.duration) { else if (cache?.duration) {
headers.set("Cache-Control", `public, max-age=${cache.duration}`); headers.set("Cache-Control", `public, max-age=${cache.duration}`);
} }
else if (!isDevelopment()) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { return new Response(file, {
headers, headers,
}); });

View File

@ -41,13 +41,12 @@ export default async function ({ req }) {
module = await import(import_path); module = await import(import_path);
} }
const config = module.config; const config = module.config;
const maxBodyBytes = config?.max_request_body_mb
? config.max_request_body_mb * MBInBytes
: ServerDefaultRequestBodyLimitBytes;
const contentLength = req.headers.get("content-length"); const contentLength = req.headers.get("content-length");
if (contentLength) { if (contentLength) {
const size = parseInt(contentLength, 10); const size = parseInt(contentLength, 10);
if (size > maxBodyBytes) { if ((config?.max_request_body_mb &&
size > config.max_request_body_mb * MBInBytes) ||
size > ServerDefaultRequestBodyLimitBytes) {
return Response.json({ return Response.json({
success: false, success: false,
msg: "Request Body Too Large!", msg: "Request Body Too Large!",
@ -59,21 +58,6 @@ export default async function ({ req }) {
}); });
} }
} }
else if (req.method !== "GET" && req.method !== "HEAD") {
const body = await req.arrayBuffer();
if (body.byteLength > maxBodyBytes) {
return Response.json({
success: false,
msg: "Request Body Too Large!",
}, {
status: 413,
headers: {
"Content-Type": "application/json",
},
});
}
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
}
const target_module = (module["default"] || const target_module = (module["default"] ||
module["handler"]); module["handler"]);
const res = await target_module?.({ const res = await target_module?.({

View File

@ -30,8 +30,8 @@ export default async function serverPostBuildFn(params) {
controller.controller.enqueue(reload_enqueue); controller.controller.enqueue(reload_enqueue);
continue; continue;
} }
const mock_req = target_artifact.req_url const mock_req = target_artifact.req
? new Request(target_artifact.req_url) ? target_artifact.req.clone()
: new Request(controller.page_url); : new Request(controller.page_url);
const page_component = global.IS_SERVER_COMPONENT const page_component = global.IS_SERVER_COMPONENT
? await grabPageComponent({ ? await grabPageComponent({

View File

@ -76,8 +76,6 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
console.error = () => { }; console.error = () => { };
console.info = () => { }; console.info = () => { };
console.debug = () => { }; console.debug = () => { };
let htmlBody;
try {
const stream = await renderToReadableStream(final_component, { const stream = await renderToReadableStream(final_component, {
onError(error) { onError(error) {
if (error.message.includes('unique "key" prop')) if (error.message.includes('unique "key" prop'))
@ -85,11 +83,8 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
originalConsole.error(error); originalConsole.error(error);
}, },
}); });
htmlBody = await new Response(stream).text(); const htmlBody = await new Response(stream).text();
}
finally {
Object.assign(console, originalConsole); Object.assign(console, originalConsole);
}
html += htmlBody; html += htmlBody;
return html; return html;
} }

View File

@ -33,7 +33,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""]; const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""];
const final_page_server_path = page_server_ctx?.local_path const final_page_server_path = page_server_ctx?.local_path
? path.join(ROOT_DIR, page_server_ctx.path) ? path.join(ROOT_DIR, page_server_ctx.path)
: server_file_path; : root_server_file_path;
const server_module = final_page_server_path const server_module = final_page_server_path
? await import(`${final_page_server_path}?t=${now}`) ? await import(`${final_page_server_path}?t=${now}`)
: undefined; : undefined;

View File

@ -72,7 +72,7 @@ export default async function grabPageComponent(params) {
} }
} }
if (req && !is_hydration) { if (req && !is_hydration) {
global.BUNDLER_CTX_MAP[file_path].req_url = req.url; global.BUNDLER_CTX_MAP[file_path].req = req;
} }
if (debug) { if (debug) {
log.info(`bundledMap:`, bundledMap); log.info(`bundledMap:`, bundledMap);

View File

@ -306,7 +306,7 @@ export type BundlerCTXMap = {
url_path: string; url_path: string;
file_name: string; file_name: string;
css_path?: string; css_path?: string;
req_url?: string; req?: Request;
}; };
export type GlobalHMRControllerObject = { export type GlobalHMRControllerObject = {
controller: ReadableStreamDefaultController<string>; controller: ReadableStreamDefaultController<string>;

View File

@ -1,3 +1,6 @@
/**
* # Convert Serialized Query back to object
*/
export default function deserializeQuery(query: string | { export default function deserializeQuery(query: string | {
[s: string]: any; [s: string]: any;
}): { }): {

View File

@ -1,33 +1,18 @@
import EJSON from "./ejson"; import EJSON from "./ejson";
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]); /**
function sanitize(value) { * # Convert Serialized Query back to object
if (value === null || typeof value !== "object") */
return value;
if (Array.isArray(value))
return value.map(sanitize);
const clean = Object.create(null);
for (const key of Object.keys(value)) {
if (DANGEROUS_KEYS.has(key))
continue;
clean[key] = sanitize(value[key]);
}
return clean;
}
export default function deserializeQuery(query) { export default function deserializeQuery(query) {
let queryObject = typeof query == "object" ? query : Object(EJSON.parse(query)); let queryObject = typeof query == "object" ? query : Object(EJSON.parse(query));
const keys = Object.keys(queryObject); const keys = Object.keys(queryObject);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]; const key = keys[i];
const value = queryObject[key]; const value = queryObject[key];
if (DANGEROUS_KEYS.has(key)) {
delete queryObject[key];
continue;
}
if (typeof value == "string") { if (typeof value == "string") {
if (value.match(/^\{|^\[/)) { if (value.match(/^\{|^\[/)) {
queryObject[key] = sanitize(EJSON.parse(value)); queryObject[key] = EJSON.parse(value);
} }
} }
} }
return sanitize(queryObject); return queryObject;
} }

View File

@ -1,4 +1,4 @@
export type DirNames = { export default function grabDirNames(): {
ROOT_DIR: string; ROOT_DIR: string;
SRC_DIR: string; SRC_DIR: string;
PAGES_DIR: string; PAGES_DIR: string;
@ -27,4 +27,3 @@ export type DirNames = {
BUNX_ERROR_LOGS_DIR: string; BUNX_ERROR_LOGS_DIR: string;
BUNX_LOGS_DIR: string; BUNX_LOGS_DIR: string;
}; };
export default function grabDirNames(): DirNames;

View File

@ -1,7 +1,5 @@
import path from "path"; import path from "path";
export default function grabDirNames() { export default function grabDirNames() {
if (global.DIR_NAMES)
return global.DIR_NAMES;
const ROOT_DIR = process.cwd(); const ROOT_DIR = process.cwd();
const SRC_DIR = path.join(ROOT_DIR, "src"); const SRC_DIR = path.join(ROOT_DIR, "src");
const PAGES_DIR = path.join(SRC_DIR, "pages"); const PAGES_DIR = path.join(SRC_DIR, "pages");

View File

@ -1,6 +1,10 @@
export default function isDevelopment() { export default function isDevelopment() {
if (process.env.NODE_ENV === "production") { const config = global.CONFIG;
if (process.env.NODE_ENV == "production") {
return false; return false;
} }
return Boolean(global.CONFIG?.development); if (config.development) {
return true;
}
return false;
} }

View File

@ -1,4 +0,0 @@
export default function isSafePath({ filePath, allowedDir, }: {
filePath: string;
allowedDir: string;
}): boolean;

View File

@ -1,15 +0,0 @@
import { realpathSync } from "fs";
import path from "path";
export default function isSafePath({ filePath, allowedDir, }) {
const resolved = path.resolve(filePath);
if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
return false;
}
try {
const real = realpathSync(resolved);
return (real.startsWith(allowedDir + path.sep) || real === allowedDir);
}
catch {
return false;
}
}

View File

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

View File

@ -3,10 +3,6 @@ import path from "path";
import type { BunSpawnOptions } from "../../types"; import type { BunSpawnOptions } from "../../types";
import writeErrorFile from "../../functions/write-error-file"; import writeErrorFile from "../../functions/write-error-file";
let retries = 0;
let timeout: any;
const MAX_RETRIES = 5;
export default function () { export default function () {
return new Command("start") return new Command("start")
.description("Start production server") .description("Start production server")
@ -16,13 +12,6 @@ export default function () {
} }
async function start() { async function start() {
clearTimeout(timeout);
if (retries >= MAX_RETRIES) {
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
}
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts"); const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
const spawn_options: BunSpawnOptions = { const spawn_options: BunSpawnOptions = {
@ -39,12 +28,6 @@ async function start() {
let dev_process = Bun.spawn(spawn_options); let dev_process = Bun.spawn(spawn_options);
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
const exited = await dev_process.exited; const exited = await dev_process.exited;
if (exited) { if (exited) {

View File

@ -9,7 +9,7 @@ export default async function buildOnstartErrorHandler(params?: Params) {
global.RECOMPILING = false; global.RECOMPILING = false;
global.IS_SERVER_COMPONENT = false; global.IS_SERVER_COMPONENT = false;
await Promise.all([ Promise.all([
global.SSR_BUNDLER_CTX?.dispose(), global.SSR_BUNDLER_CTX?.dispose(),
global.BUNDLER_CTX?.dispose(), global.BUNDLER_CTX?.dispose(),
]); ]);

View File

@ -5,7 +5,7 @@ import type {
PageFiles, PageFiles,
} from "../types"; } from "../types";
import type { FileSystemRouter, Server } from "bun"; import type { FileSystemRouter, Server } from "bun";
import grabDirNames, { type DirNames } from "../utils/grab-dir-names"; import grabDirNames from "../utils/grab-dir-names";
import { type FSWatcher } from "fs"; import { type FSWatcher } from "fs";
import init from "./init"; import init from "./init";
import isDevelopment from "../utils/is-development"; import isDevelopment from "../utils/is-development";
@ -43,7 +43,7 @@ declare global {
var BUNDLER_CTX: BuildContext | undefined; var BUNDLER_CTX: BuildContext | undefined;
var SSR_BUNDLER_CTX: BuildContext | undefined; var SSR_BUNDLER_CTX: BuildContext | undefined;
// var API_ROUTES_BUNDLER_CTX: BuildContext | undefined; // var API_ROUTES_BUNDLER_CTX: BuildContext | undefined;
var DIR_NAMES: DirNames; var DIR_NAMES: ReturnType<typeof grabDirNames>;
var REACT_IMPORTS_MAP: { imports: Record<string, string> }; var REACT_IMPORTS_MAP: { imports: Record<string, string> };
var REACT_DOM_SERVER: any; var REACT_DOM_SERVER: any;
var REACT_DOM_MODULE_CACHE: Map<string, { main: any; css: string }>; var REACT_DOM_MODULE_CACHE: Map<string, { main: any; css: string }>;

View File

@ -17,11 +17,6 @@ export default async function trimAllCache() {
const trim_key = await trimCacheKey({ const trim_key = await trimCacheKey({
key: cache_key, key: cache_key,
}); });
if (trim_key.success) {
cached_items.splice(i, 1);
i--;
}
} }
} catch (error) { } catch (error) {
return undefined; return undefined;

View File

@ -8,10 +8,6 @@ import handleBunextPublicAssets from "./handle-bunext-public-assets";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
const HMR_RETRY_COOLDOWN_MS = 5000;
let lastHmrRetryTime = 0;
type Params = { type Params = {
req: Request; req: Request;
server: Bun.Server<any>; server: Bun.Server<any>;
@ -49,11 +45,6 @@ export default async function bunextRequestHandler({
} }
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) { if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
const now = Date.now();
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
return new Response("Too Many Requests", { status: 429 });
}
lastHmrRetryTime = now;
await fullRebuild({ msg: `HMR Retry Rebuild ...` }); await fullRebuild({ msg: `HMR Retry Rebuild ...` });
return new Response("Modules Rebuilt"); return new Response("Modules Rebuilt");
} }
@ -85,12 +76,8 @@ export default async function bunextRequestHandler({
return response; return response;
} catch (error: any) { } catch (error: any) {
if (is_dev) {
return new Response(`Server Error: ${error.message}`, { return new Response(`Server Error: ${error.message}`, {
status: 500, status: 500,
}); });
} }
console.error(`Server Error: ${error.message}`, error);
return new Response("Internal Server Error", { status: 500 });
}
} }

View File

@ -2,7 +2,6 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { readFileResponse } from "./handle-public"; import { readFileResponse } from "./handle-public";
import isSafePath from "../../utils/is-safe-path";
const { BUNEXT_PUBLIC_DIR } = grabDirNames(); const { BUNEXT_PUBLIC_DIR } = grabDirNames();
@ -20,7 +19,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/\/\.bunext\/public\//, ""), url.pathname.replace(/\/\.bunext\/public\//, ""),
); );
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) { if (!file_path.startsWith(BUNEXT_PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }

View File

@ -2,7 +2,6 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
@ -16,7 +15,7 @@ export default async function ({ req }: Params): Promise<Response> {
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname); const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) { if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
@ -27,13 +26,7 @@ export default async function ({ req }: Params): Promise<Response> {
} }
const file = Bun.file(file_path); const file = Bun.file(file_path);
const headers = new Headers(); return new Response(file);
if (!is_dev) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { headers });
} catch (error) { } catch (error) {
return new Response(`File Not Found`, { return new Response(`File Not Found`, {
status: 404, status: 404,

View File

@ -2,28 +2,8 @@ type Params = {
req: Request; req: Request;
}; };
function removeController(controller: ReadableStreamDefaultController<string>) {
const idx = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (typeof idx == "number" && idx >= 0) {
global.HMR_CONTROLLERS.splice(idx, 1);
}
}
export default async function ({ req }: Params): Promise<Response> { export default async function ({ req }: Params): Promise<Response> {
const referer = req.headers.get("referer"); const referer_url = new URL(req.headers.get("referer") || "");
if (!referer) {
return new Response("Missing Referer Header", { status: 400 });
}
let referer_url: URL;
try {
referer_url = new URL(referer);
} catch {
return new Response("Invalid Referer Header", { status: 400 });
}
const match = global.ROUTER.match(referer_url.pathname); const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath const target_map = match?.filePath
@ -45,13 +25,21 @@ export default async function ({ req }: Params): Promise<Response> {
c.enqueue(": keep-alive\n\n"); c.enqueue(": keep-alive\n\n");
} catch { } catch {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller);
} }
}, 5000); }, 5000);
}, },
cancel() { cancel() {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller); const targetControllerIndex = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (
typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0
) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
}, },
}); });

View File

@ -2,7 +2,6 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
@ -20,7 +19,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/^\/public/, ""), url.pathname.replace(/^\/public/, ""),
); );
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) { if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
@ -54,8 +53,6 @@ export function readFileResponse({ file_path, cache }: FileResponse) {
headers.set("Cache-Control", "public, max-age=31536000, immutable"); headers.set("Cache-Control", "public, max-age=31536000, immutable");
} else if (cache?.duration) { } else if (cache?.duration) {
headers.set("Cache-Control", `public, max-age=${cache.duration}`); headers.set("Cache-Control", `public, max-age=${cache.duration}`);
} else if (!isDevelopment()) {
headers.set("Cache-Control", "public, max-age=3600");
} }
return new Response(file, { return new Response(file, {

View File

@ -68,16 +68,16 @@ export default async function ({ req }: Params): Promise<Response> {
const config = module.config as BunextServerRouteConfig | undefined; const config = module.config as BunextServerRouteConfig | undefined;
const maxBodyBytes = config?.max_request_body_mb
? config.max_request_body_mb * MBInBytes
: ServerDefaultRequestBodyLimitBytes;
const contentLength = req.headers.get("content-length"); const contentLength = req.headers.get("content-length");
if (contentLength) { if (contentLength) {
const size = parseInt(contentLength, 10); const size = parseInt(contentLength, 10);
if (size > maxBodyBytes) { if (
(config?.max_request_body_mb &&
size > config.max_request_body_mb * MBInBytes) ||
size > ServerDefaultRequestBodyLimitBytes
) {
return Response.json( return Response.json(
{ {
success: false, success: false,
@ -91,26 +91,6 @@ export default async function ({ req }: Params): Promise<Response> {
}, },
); );
} }
} else if (req.method !== "GET" && req.method !== "HEAD") {
const body = await req.arrayBuffer();
if (body.byteLength > maxBodyBytes) {
return Response.json(
{
success: false,
msg: "Request Body Too Large!",
},
{
status: 413,
headers: {
"Content-Type": "application/json",
},
},
);
}
routeParams.body = JSON.parse(
new TextDecoder().decode(body) || "{}",
);
} }
const target_module = (module["default"] || const target_module = (module["default"] ||

View File

@ -45,8 +45,8 @@ export default async function serverPostBuildFn(params?: Params) {
continue; continue;
} }
const mock_req = target_artifact.req_url const mock_req = target_artifact.req
? new Request(target_artifact.req_url) ? target_artifact.req.clone()
: new Request(controller.page_url); : new Request(controller.page_url);
const page_component = global.IS_SERVER_COMPONENT const page_component = global.IS_SERVER_COMPONENT

View File

@ -178,8 +178,6 @@ export default async function genWebHTML({
console.info = () => {}; console.info = () => {};
console.debug = () => {}; console.debug = () => {};
let htmlBody: string;
try {
const stream = await renderToReadableStream(final_component, { const stream = await renderToReadableStream(final_component, {
onError(error: any) { onError(error: any) {
if (error.message.includes('unique "key" prop')) return; if (error.message.includes('unique "key" prop')) return;
@ -187,10 +185,9 @@ export default async function genWebHTML({
}, },
}); });
htmlBody = await new Response(stream).text(); const htmlBody = await new Response(stream).text();
} finally {
Object.assign(console, originalConsole); Object.assign(console, originalConsole);
}
html += htmlBody; html += htmlBody;

View File

@ -63,7 +63,7 @@ export default async function grabPageCombinedServerRes({
const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""]; const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""];
const final_page_server_path = page_server_ctx?.local_path const final_page_server_path = page_server_ctx?.local_path
? path.join(ROOT_DIR, page_server_ctx.path) ? path.join(ROOT_DIR, page_server_ctx.path)
: server_file_path; : root_server_file_path;
const server_module: BunextPageServerModule = final_page_server_path const server_module: BunextPageServerModule = final_page_server_path
? await import(`${final_page_server_path}?t=${now}`) ? await import(`${final_page_server_path}?t=${now}`)

View File

@ -117,7 +117,7 @@ export default async function grabPageComponent(
} }
if (req && !is_hydration) { if (req && !is_hydration) {
global.BUNDLER_CTX_MAP[file_path].req_url = req.url; global.BUNDLER_CTX_MAP[file_path].req = req;
} }
if (debug) { if (debug) {

View File

@ -344,7 +344,7 @@ export type BundlerCTXMap = {
url_path: string; url_path: string;
file_name: string; file_name: string;
css_path?: string; css_path?: string;
req_url?: string; req?: Request;
}; };
export type GlobalHMRControllerObject = { export type GlobalHMRControllerObject = {

View File

@ -1,18 +1,8 @@
import EJSON from "./ejson"; import EJSON from "./ejson";
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]); /**
* # Convert Serialized Query back to object
function sanitize<T>(value: T): T { */
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(sanitize) as T;
const clean: Record<string, any> = Object.create(null);
for (const key of Object.keys(value as Record<string, any>)) {
if (DANGEROUS_KEYS.has(key)) continue;
clean[key] = sanitize((value as Record<string, any>)[key]);
}
return clean as T;
}
export default function deserializeQuery( export default function deserializeQuery(
query: string | { [s: string]: any } query: string | { [s: string]: any }
): { ): {
@ -27,17 +17,12 @@ export default function deserializeQuery(
const key = keys[i]; const key = keys[i];
const value = queryObject[key]; const value = queryObject[key];
if (DANGEROUS_KEYS.has(key)) {
delete queryObject[key];
continue;
}
if (typeof value == "string") { if (typeof value == "string") {
if (value.match(/^\{|^\[/)) { if (value.match(/^\{|^\[/)) {
queryObject[key] = sanitize(EJSON.parse(value)); queryObject[key] = EJSON.parse(value);
} }
} }
} }
return sanitize(queryObject); return queryObject;
} }

View File

@ -1,38 +1,6 @@
import path from "path"; import path from "path";
export type DirNames = { export default function grabDirNames() {
ROOT_DIR: string;
SRC_DIR: string;
PAGES_DIR: string;
API_DIR: string;
PUBLIC_DIR: string;
HYDRATION_DST_DIR: string;
BUNX_CWD_DIR: string;
BUNX_ROOT_DIR: string;
CONFIG_FILE: string;
BUNX_TMP_DIR: string;
BUNX_HYDRATION_SRC_DIR: string;
BUNX_ROOT_SRC_DIR: string;
BUNX_ROOT_PRESETS_DIR: string;
BUNX_ROOT_500_PRESET_COMPONENT: string;
BUNX_ROOT_500_FILE_NAME: string;
BUNX_ROOT_404_PRESET_COMPONENT: string;
BUNX_ROOT_404_FILE_NAME: string;
HYDRATION_DST_DIR_MAP_JSON_FILE: string;
BUNEXT_CACHE_DIR: string;
BUNX_CWD_MODULE_CACHE_DIR: string;
BUNX_CWD_PAGES_REWRITE_DIR: string;
HYDRATION_DST_DIR_MAP_JSON_FILE_NAME: string;
BUNEXT_VENDOR_DIR: string;
BUNEXT_PUBLIC_DIR: string;
BUNX_BUNDLER_ERROR_EXIT_FILE: string;
BUNX_ERROR_LOGS_DIR: string;
BUNX_LOGS_DIR: string;
};
export default function grabDirNames(): DirNames {
if (global.DIR_NAMES) return global.DIR_NAMES;
const ROOT_DIR = process.cwd(); const ROOT_DIR = process.cwd();
const SRC_DIR = path.join(ROOT_DIR, "src"); const SRC_DIR = path.join(ROOT_DIR, "src");
const PAGES_DIR = path.join(SRC_DIR, "pages"); const PAGES_DIR = path.join(SRC_DIR, "pages");

View File

@ -1,7 +1,13 @@
export default function isDevelopment() { export default function isDevelopment() {
if (process.env.NODE_ENV === "production") { const config = global.CONFIG;
if (process.env.NODE_ENV == "production") {
return false; return false;
} }
return Boolean(global.CONFIG?.development); if (config.development) {
return true;
}
return false;
} }

View File

@ -1,24 +0,0 @@
import { realpathSync } from "fs";
import path from "path";
export default function isSafePath({
filePath,
allowedDir,
}: {
filePath: string;
allowedDir: string;
}): boolean {
const resolved = path.resolve(filePath);
if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
return false;
}
try {
const real = realpathSync(resolved);
return (
real.startsWith(allowedDir + path.sep) || real === allowedDir
);
} catch {
return false;
}
}