Compare commits
100
Commits
1634eeb213
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf1b651db | ||
|
|
247a64c873 | ||
|
|
f590deb11b | ||
|
|
3426c7b53b | ||
|
|
afd1af1827 | ||
|
|
22d3dedab2 | ||
|
|
8d12329f01 | ||
|
|
78e86b3999 | ||
|
|
87948340b0 | ||
|
|
86ea86e7bd | ||
|
|
596b9de047 | ||
|
|
9da1e16318 | ||
|
|
823c5bb1ca | ||
|
|
88ead3b3d6 | ||
|
|
45509deff8 | ||
|
|
a19863b3e9 | ||
|
|
a9cd51d71c | ||
|
|
e3a0f5fbeb | ||
|
|
1d0ac4aa80 | ||
|
|
817beacc7a | ||
|
|
61a9d8d612 | ||
|
|
cd9ac833dc | ||
|
|
e2b8b95a4b | ||
|
|
c06cb73181 | ||
|
|
f3bb972a20 | ||
|
|
40a987b983 | ||
|
|
ceeb6fbdaf | ||
|
|
4f5445e3df | ||
|
|
b702e26bf6 | ||
|
|
3b26292124 | ||
|
|
cb5126c947 | ||
|
|
f018b228a8 | ||
|
|
95fcee36b2 | ||
|
|
41e28d7a3e | ||
|
|
b2e92e5792 | ||
|
|
9a427412f3 | ||
|
|
a5f25d522e | ||
|
|
938411653d | ||
|
|
b597e1420e | ||
|
|
d6f0a7962e | ||
|
|
f0aae8a8fa | ||
|
|
84d490b189 | ||
|
|
532d0d6b56 | ||
|
|
e0c2ab5872 | ||
|
|
35f7a6fc85 | ||
|
|
814a289460 | ||
|
|
c4f7cf9164 | ||
|
|
af8c207ac1 | ||
|
|
5a0972beb8 | ||
|
|
257adfec39 | ||
|
|
349b99bacf | ||
|
|
972f6945c2 | ||
|
|
6477f446d1 | ||
|
|
7fb1784b95 | ||
|
|
40fc7778a8 | ||
|
|
f3b087a1f3 | ||
|
|
eb0721f94b | ||
|
|
ab6fc3be26 | ||
|
|
6b7d29bc53 | ||
|
|
a84ac10b24 | ||
|
|
4b3a4dbc77 | ||
|
|
c19b7c9607 | ||
|
|
57957b89a4 | ||
|
|
d441f9982e | ||
|
|
f49a17b24e | ||
|
|
fa8a108686 | ||
|
|
8d5b2a5c07 | ||
|
|
333c84281d | ||
|
|
5166ba037f | ||
|
|
1037bbb546 | ||
|
|
f34f8baa99 | ||
|
|
48c25d2fb4 | ||
|
|
7dbd1f7e12 | ||
|
|
68d88bce4a | ||
|
|
d6985b3335 | ||
|
|
86d4bb8d6c | ||
|
|
2b170d24e1 | ||
|
|
950bdc3dca | ||
|
|
572f739b5c | ||
|
|
ee07163ba5 | ||
|
|
ad51f7da0c | ||
|
|
6a024faece | ||
|
|
764e8f76f0 | ||
|
|
b8bae51b32 | ||
|
|
dad5cffaac | ||
|
|
321c8ebb89 | ||
|
|
336fa812a5 | ||
|
|
f6c7f6b78c | ||
|
|
99b319f5af | ||
|
|
47c262392c | ||
|
|
976ff5fec9 | ||
|
|
60ee353bf0 | ||
|
|
342f56f3f5 | ||
|
|
7dd8d87be8 | ||
|
|
6f1db7c01f | ||
|
|
67ed8749e2 | ||
|
|
146f51c590 | ||
|
|
4b8b610e32 | ||
|
|
567fb4f746 | ||
|
|
0308ea32ec |
@@ -181,3 +181,5 @@ __fixtures__
|
|||||||
/.data
|
/.data
|
||||||
/.dump
|
/.dump
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/source.md
|
||||||
|
SECURITY.md
|
||||||
@@ -40,11 +40,21 @@ bunext start # Start production server from pre-built artifacts
|
|||||||
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
|
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
|
||||||
|
|
||||||
### Page module contract
|
### Page module contract
|
||||||
Pages live in `src/pages/`. A page file may export:
|
Pages live in `src/pages/`. Server logic is **separated from page files** into companion `.server.ts` / `.server.tsx` files to avoid bundling server-only code into the client.
|
||||||
- Default export: React component receiving `ServerProps | StaticProps`
|
|
||||||
- `server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
**Page file** (`page.tsx`) — client-safe exports only (bundled to the browser):
|
||||||
|
- Default export: React component receiving props from the server file
|
||||||
- `meta`: `BunextPageModuleMeta` — SEO/OG metadata
|
- `meta`: `BunextPageModuleMeta` — SEO/OG metadata
|
||||||
- `head`: ReactNode — extra `<head>` content
|
- `Head`: FC — extra `<head>` content
|
||||||
|
- `config`: `BunextRouteConfig` — cache settings
|
||||||
|
- `html_props`: `BunextHTMLProps` — attributes on the `<html>` element
|
||||||
|
|
||||||
|
**Server file** (`page.server.ts` or `page.server.tsx`) — server-only, never sent to the browser:
|
||||||
|
- Default export or `export const server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||||
|
|
||||||
|
The framework resolves the companion by replacing the page extension with `.server.ts` or `.server.tsx`. If neither exists, no server function runs and only the default `url` prop is injected.
|
||||||
|
|
||||||
|
`__root.tsx` follows the same contract; its server companion is `__root.server.ts`.
|
||||||
|
|
||||||
API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.
|
API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Bunext
|
# Bunext
|
||||||
|
|
||||||
A server-rendering framework for React, built on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using ESBuild to bundle client assets and `Bun.serve` as the HTTP server.
|
A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using ESBuild to bundle client assets and `Bun.serve` as the HTTP server.
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
@@ -61,7 +61,8 @@ The goal is a framework that is:
|
|||||||
|
|
||||||
- [Bun](https://bun.sh) v1.0 or later
|
- [Bun](https://bun.sh) v1.0 or later
|
||||||
- TypeScript 5.0+
|
- TypeScript 5.0+
|
||||||
- React 19 and react-dom 19 (peer dependencies)
|
|
||||||
|
> **React is managed by Bunext.** You do not need to install `react` or `react-dom` — Bunext enforces its own pinned React version and removes any user-installed copies at startup to prevent version conflicts. Installing this package is all you need.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,9 +151,9 @@ bun run dev
|
|||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| -------------- | ---------------------------------------------------------------------- |
|
| -------------- | ------------------------------------------------------------------------------ |
|
||||||
| `bunext dev` | Start the development server with HMR and file watching. |
|
| `bunext dev` | Start the development server with HMR and file watching. |
|
||||||
| `bunext build` | Bundle all pages for production. Outputs artifacts to `public/pages/`. |
|
| `bunext build` | Bundle all pages for production. Outputs artifacts to `.bunext/public/pages/`. |
|
||||||
| `bunext start` | Start the production server using pre-built artifacts. |
|
| `bunext start` | Start the production server using pre-built artifacts. |
|
||||||
|
|
||||||
### Running the CLI
|
### Running the CLI
|
||||||
@@ -186,7 +187,7 @@ bunext build
|
|||||||
bunext start
|
bunext start
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** `bunext start` will exit with an error if `public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `bunext start`.
|
> **Note:** `bunext start` will exit with an error if `.bunext/public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `bunext start`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -199,18 +200,22 @@ my-app/
|
|||||||
├── src/
|
├── src/
|
||||||
│ └── pages/ # File-system routes (pages and API handlers)
|
│ └── pages/ # File-system routes (pages and API handlers)
|
||||||
│ ├── __root.tsx # Optional: root layout wrapper for all pages
|
│ ├── __root.tsx # Optional: root layout wrapper for all pages
|
||||||
|
│ ├── __root.server.ts # Optional: root-level server logic
|
||||||
│ ├── index.tsx # Route: /
|
│ ├── index.tsx # Route: /
|
||||||
|
│ ├── index.server.ts # Optional: server logic for index route
|
||||||
│ ├── about.tsx # Route: /about
|
│ ├── about.tsx # Route: /about
|
||||||
│ ├── 404.tsx # Optional: custom 404 page
|
│ ├── 404.tsx # Optional: custom 404 page
|
||||||
│ ├── 500.tsx # Optional: custom 500 page
|
│ ├── 500.tsx # Optional: custom 500 page
|
||||||
│ ├── blog/
|
│ ├── blog/
|
||||||
│ │ ├── index.tsx # Route: /blog
|
│ │ ├── index.tsx # Route: /blog
|
||||||
|
│ │ ├── index.server.ts # Server logic for /blog
|
||||||
│ │ └── [slug].tsx # Route: /blog/:slug (dynamic)
|
│ │ └── [slug].tsx # Route: /blog/:slug (dynamic)
|
||||||
│ └── api/
|
│ └── api/
|
||||||
│ └── users.ts # API route: /api/users
|
│ └── users.ts # API route: /api/users
|
||||||
├── public/ # Static files and bundler output
|
├── public/ # Static files served at /public/*
|
||||||
│ └── __bunext/
|
├── .bunext/ # Internal build artifacts (do not edit manually)
|
||||||
│ ├── pages/ # Generated by bundler (do not edit manually)
|
│ └── public/
|
||||||
|
│ ├── pages/ # Generated by bundler
|
||||||
│ │ └── map.json # Artifact map used by production server
|
│ │ └── map.json # Artifact map used by production server
|
||||||
│ └── cache/ # File-based HTML cache (production only)
|
│ └── cache/ # File-based HTML cache (production only)
|
||||||
├── bunext.config.ts # Optional configuration
|
├── bunext.config.ts # Optional configuration
|
||||||
@@ -273,19 +278,15 @@ export default function HomePage() {
|
|||||||
|
|
||||||
### Server Function
|
### Server Function
|
||||||
|
|
||||||
Export a `server` function to run server-side logic before rendering. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
|
Server logic lives in a companion **`.server.ts`** (or `.server.tsx`) file alongside the page. The framework looks for `<page>.server.ts` or `<page>.server.tsx` next to the page file and loads it separately on the server — it is never bundled into the client JS.
|
||||||
|
|
||||||
```tsx
|
The server file exports the server function as either `export default` or `export const server`. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
|
||||||
// src/pages/profile.tsx
|
|
||||||
|
```ts
|
||||||
|
// src/pages/profile.server.ts
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
type Props = {
|
const server: BunextPageServerFn<{
|
||||||
props?: { username: string; bio: string };
|
|
||||||
query?: Record<string, string>;
|
|
||||||
url?: URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const server: BunextPageServerFn<{
|
|
||||||
username: string;
|
username: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
}> = async (ctx) => {
|
}> = async (ctx) => {
|
||||||
@@ -302,6 +303,17 @@ export const server: BunextPageServerFn<{
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/pages/profile.tsx (client-only exports — bundled to the browser)
|
||||||
|
type Props = {
|
||||||
|
props?: { username: string; bio: string };
|
||||||
|
query?: Record<string, string>;
|
||||||
|
url?: URL;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ProfilePage({ props, url }: Props) {
|
export default function ProfilePage({ props, url }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -313,6 +325,8 @@ export default function ProfilePage({ props, url }: Props) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Why separate files?** Bundling server code (Bun APIs, database clients, `fs`, secrets) into the same file as a React component causes TypeScript compilation errors because the bundler processes the page file for the browser. The `.server.ts` companion file is loaded only by the server at request time and is never included in the client bundle.
|
||||||
|
|
||||||
The server function receives a `ctx` object (type `BunxRouteParams`) with:
|
The server function receives a `ctx` object (type `BunxRouteParams`) with:
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
@@ -364,10 +378,13 @@ The `url` prop exposes the following fields from the standard Web `URL` interfac
|
|||||||
|
|
||||||
### Redirects from Server
|
### Redirects from Server
|
||||||
|
|
||||||
Return a `redirect` object from the `server` function to redirect the client:
|
Return a `redirect` object from the server function to redirect the client:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
// src/pages/dashboard.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
const isLoggedIn = false; // check auth
|
const isLoggedIn = false; // check auth
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
@@ -382,6 +399,8 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
|
|
||||||
return { props: {} };
|
return { props: {} };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
```
|
```
|
||||||
|
|
||||||
`permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided.
|
`permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided.
|
||||||
@@ -390,8 +409,11 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
|
|
||||||
Control status codes, headers, and other response options from the server function:
|
Control status codes, headers, and other response options from the server function:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
// src/pages/submit.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
return {
|
return {
|
||||||
props: { message: "Created" },
|
props: { message: "Created" },
|
||||||
responseOptions: {
|
responseOptions: {
|
||||||
@@ -402,11 +424,13 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
```
|
```
|
||||||
|
|
||||||
### SEO Metadata
|
### SEO Metadata
|
||||||
|
|
||||||
Export a `meta` object to inject SEO and Open Graph tags into the `<head>`:
|
Export a `meta` object from the **page file** (not the server file) to inject SEO and Open Graph tags into the `<head>`:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||||
@@ -445,7 +469,7 @@ export default function AboutPage() {
|
|||||||
|
|
||||||
### Dynamic Metadata
|
### Dynamic Metadata
|
||||||
|
|
||||||
`meta` can also be an async function that receives the request context and server response:
|
`meta` can also be an async function that receives the request context and server response. Like `meta`, it is exported from the **page file**:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
||||||
@@ -481,7 +505,21 @@ export default function Page() {
|
|||||||
|
|
||||||
### Root Layout
|
### Root Layout
|
||||||
|
|
||||||
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props:
|
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props.
|
||||||
|
|
||||||
|
If the root layout also needs server-side logic, place it in `src/pages/__root.server.ts` (or `.server.tsx`) — the same `.server.*` convention used by regular pages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/pages/__root.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
|
// e.g. fetch navigation links, check auth
|
||||||
|
return { props: { navLinks: ["/", "/about"] } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
|
```
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/pages/__root.tsx
|
// src/pages/__root.tsx
|
||||||
@@ -605,7 +643,7 @@ public/
|
|||||||
|
|
||||||
Bunext includes a file-based HTML cache for production. Caching is **disabled in development** — every request renders fresh. In production, a cron job runs every 30 seconds to delete expired cache entries.
|
Bunext includes a file-based HTML cache for production. Caching is **disabled in development** — every request renders fresh. In production, a cron job runs every 30 seconds to delete expired cache entries.
|
||||||
|
|
||||||
Cache files are stored in `public/__bunext/cache/`. Each cached page produces two files:
|
Cache files are stored in `.bunext/public/cache/`. Each cached page produces two files:
|
||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
| ----------------- | ---------------------------------------------- |
|
| ----------------- | ---------------------------------------------- |
|
||||||
@@ -634,12 +672,13 @@ export default function ProductsPage() {
|
|||||||
|
|
||||||
### Dynamic Cache Control from Server Function
|
### Dynamic Cache Control from Server Function
|
||||||
|
|
||||||
Cache settings can also be returned from the `server` function, which lets you conditionally enable caching based on request data:
|
Cache settings can also be returned from the server function, which lets you conditionally enable caching based on request data:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
|
// src/pages/products.server.ts
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
const data = await fetchProducts();
|
const data = await fetchProducts();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -649,6 +688,11 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/pages/products.tsx
|
||||||
export default function ProductsPage({ props }: any) {
|
export default function ProductsPage({ props }: any) {
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
@@ -670,14 +714,14 @@ Expiry resolution order (first truthy value wins):
|
|||||||
2. `defaultCacheExpiry` in `bunext.config.ts` (global default, in seconds)
|
2. `defaultCacheExpiry` in `bunext.config.ts` (global default, in seconds)
|
||||||
3. Built-in default: **3600 seconds (1 hour)**
|
3. Built-in default: **3600 seconds (1 hour)**
|
||||||
|
|
||||||
The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `public/__bunext/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache.
|
The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `.bunext/public/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache.
|
||||||
|
|
||||||
### Cache Behavior and Limitations
|
### Cache Behavior and Limitations
|
||||||
|
|
||||||
- **Production only.** Caching never activates in development (`bunext dev`).
|
- **Production only.** Caching never activates in development (`bunext dev`).
|
||||||
- **Cold start required.** The cache is populated on the first request; there is no pre-warming step.
|
- **Cold start required.** The cache is populated on the first request; there is no pre-warming step.
|
||||||
- **Immutable within the expiry window.** Once a page is cached, `writeCache` skips all subsequent write attempts for that key until the cron job deletes the expired entry. There is no manual invalidation API.
|
- **Immutable within the expiry window.** Once a page is cached, `writeCache` skips all subsequent write attempts for that key until the cron job deletes the expired entry. There is no manual invalidation API.
|
||||||
- **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `public/__bunext/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed.
|
- **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `.bunext/public/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed.
|
||||||
- **No key collision.** Cache keys are generated via `encodeURIComponent()` on the URL path. `/foo/bar` encodes to `%2Ffoo%2Fbar` and `/foo-bar` to `%2Ffoo-bar` — distinct filenames with no collision risk.
|
- **No key collision.** Cache keys are generated via `encodeURIComponent()` on the URL path. `/foo/bar` encodes to `%2Ffoo%2Fbar` and `/foo-bar` to `%2Ffoo-bar` — distinct filenames with no collision risk.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -698,6 +742,9 @@ const config: BunextConfig = {
|
|||||||
globalVars: {
|
globalVars: {
|
||||||
MY_API_URL: "https://api.example.com",
|
MY_API_URL: "https://api.example.com",
|
||||||
},
|
},
|
||||||
|
public_envs: {
|
||||||
|
BUNEXT_PUBLIC_APP_NAME: "My App",
|
||||||
|
},
|
||||||
development: false, // forced by the CLI; set manually if needed
|
development: false, // forced by the CLI; set manually if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -711,6 +758,7 @@ export default config;
|
|||||||
| `distDir` | `string` | `.bunext` | Internal artifact directory |
|
| `distDir` | `string` | `.bunext` | Internal artifact directory |
|
||||||
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
|
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
|
||||||
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
|
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
|
||||||
|
| `public_envs` | `Record<string, string>` | — | Public env vars exposed to the client via `window.process.env` (see [Environment Variables](#environment-variables)) |
|
||||||
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
|
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
|
||||||
| `defaultCacheExpiry` | `number` | `3600` | Global page cache expiry in seconds |
|
| `defaultCacheExpiry` | `number` | `3600` | Global page cache expiry in seconds |
|
||||||
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||||
@@ -866,8 +914,34 @@ bun run server.ts
|
|||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| -------- | ------------------------------------------------------- |
|
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
||||||
| `PORT` | Override the server port (takes precedence over config) |
|
| `PORT` | Override the server port (takes precedence over config) |
|
||||||
|
| `BUNEXT_PUBLIC_*` | Any env var prefixed with `BUNEXT_PUBLIC_` is exposed to the client via `window.process.env` |
|
||||||
|
|
||||||
|
### Public Environment Variables
|
||||||
|
|
||||||
|
Variables prefixed with `BUNEXT_PUBLIC_` are automatically injected into every page as `window.process.env`. You can also define public envs in config via `public_envs` (config values override env vars of the same name):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
BUNEXT_PUBLIC_API_URL=https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
const config: BunextConfig = {
|
||||||
|
public_envs: {
|
||||||
|
BUNEXT_PUBLIC_APP_NAME: "My App",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Client component — available after hydration
|
||||||
|
const apiUrl = window.process.env.BUNEXT_PUBLIC_API_URL;
|
||||||
|
```
|
||||||
|
|
||||||
|
`window.process.env` always includes `NODE_ENV` (`"development"` or `"production"`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -880,7 +954,7 @@ Running `bunext dev`:
|
|||||||
1. Loads `bunext.config.ts` and sets `development: true`.
|
1. Loads `bunext.config.ts` and sets `development: true`.
|
||||||
2. Initializes directories (`.bunext/`, `public/pages/`).
|
2. Initializes directories (`.bunext/`, `public/pages/`).
|
||||||
3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`.
|
3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`.
|
||||||
4. Starts the ESBuild bundler in **watch mode** — it will automatically rebuild when file content changes.
|
4. Creates an ESBuild context and performs the initial build. File-change rebuilds are triggered manually by the FS watcher.
|
||||||
5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points.
|
5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points.
|
||||||
6. Waits for the first successful bundle.
|
6. Waits for the first successful bundle.
|
||||||
7. Starts `Bun.serve()`.
|
7. Starts `Bun.serve()`.
|
||||||
@@ -890,26 +964,28 @@ Running `bunext dev`:
|
|||||||
Running `bunext build`:
|
Running `bunext build`:
|
||||||
|
|
||||||
1. Sets `NODE_ENV=production`.
|
1. Sets `NODE_ENV=production`.
|
||||||
2. Runs ESBuild once (not in watch mode) with minification enabled.
|
2. Runs ESBuild once with minification enabled.
|
||||||
3. Writes all bundled artifacts to `public/pages/` and the artifact map to `public/pages/map.json`.
|
3. Writes all bundled artifacts to `.bunext/public/pages/` and the artifact map to `.bunext/public/pages/map.json`.
|
||||||
4. Exits.
|
4. Exits.
|
||||||
|
|
||||||
### Production Server
|
### Production Server
|
||||||
|
|
||||||
Running `bunext start`:
|
Running `bunext start`:
|
||||||
|
|
||||||
1. Reads `public/pages/map.json` to load the pre-built artifact map.
|
1. Reads `.bunext/public/pages/map.json` to load the pre-built artifact map.
|
||||||
2. Starts `Bun.serve()` without any bundler or file watcher.
|
2. Starts `Bun.serve()` without any bundler or file watcher.
|
||||||
|
|
||||||
### Bundler
|
### Bundler
|
||||||
|
|
||||||
The bundler (`allPagesBundler`) uses ESBuild with three custom plugins:
|
The bundler uses **ESBuild** with a virtual namespace plugin that generates in-memory hydration entry points for each page — no temporary files are written to disk. Each virtual entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. Tailwind CSS is processed via a dedicated ESBuild plugin.
|
||||||
|
|
||||||
- **`tailwindcss` plugin** — Processes any `.css` files through PostCSS + Tailwind CSS before bundling.
|
In development, an `esbuild.context()` is created once and rebuilt incrementally whenever the FS watcher detects a file change. In production, a single `esbuild.build()` call runs with minification enabled.
|
||||||
- **`virtual-entrypoints` plugin** — Generates an in-memory client hydration entry point for each page. Each entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout.
|
|
||||||
- **`artifact-tracker` plugin** — After each build, collects all output file paths, content hashes, and source entrypoints into a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `public/pages/map.json`.
|
|
||||||
|
|
||||||
Output files are named `[dir]/[name]/[hash]` so filenames change when content changes, enabling cache-busting.
|
React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the ESBuild config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `<script type="importmap">` pointing at `esm.sh`. This guarantees a single shared React instance across all page bundles and HMR updates regardless of project size.
|
||||||
|
|
||||||
|
After each build, ESBuild's metafile is used to map each output file back to its source page, producing a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `.bunext/public/pages/map.json`.
|
||||||
|
|
||||||
|
Output files are named `[hash].[ext]` so filenames change when content changes, enabling cache-busting.
|
||||||
|
|
||||||
### Hot Module Replacement
|
### Hot Module Replacement
|
||||||
|
|
||||||
@@ -949,13 +1025,13 @@ Request
|
|||||||
├── /favicon.* → Serve favicon from public/
|
├── /favicon.* → Serve favicon from public/
|
||||||
│
|
│
|
||||||
└── Everything else → Server-side render a page
|
└── Everything else → Server-side render a page
|
||||||
[Production only] Check public/__bunext/cache/ for key = pathname + search
|
[Production only] Check .bunext/public/cache/ for key = pathname + search
|
||||||
Cache HIT → return cached HTML with X-Bunext-Cache: HIT header
|
Cache HIT → return cached HTML with X-Bunext-Cache: HIT header
|
||||||
Cache MISS → continue ↓
|
Cache MISS → continue ↓
|
||||||
1. Match route via FileSystemRouter
|
1. Match route via FileSystemRouter
|
||||||
2. Find bundled artifact in BUNDLER_CTX_MAP
|
2. Find bundled artifact in BUNDLER_CTX_MAP
|
||||||
3. Import page module (with cache-busting timestamp in dev)
|
3. Import page module (with cache-busting timestamp in dev)
|
||||||
4. Run module.server(ctx) for server-side data
|
4. Import companion server module (<page>.server.ts/tsx) if it exists; run its exported function for server-side data
|
||||||
5. Resolve meta (static object or async function)
|
5. Resolve meta (static object or async function)
|
||||||
6. renderToString(component) → inject into HTML template
|
6. renderToString(component) → inject into HTML template
|
||||||
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>
|
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>
|
||||||
@@ -967,6 +1043,7 @@ Request
|
|||||||
Server-rendered HTML includes:
|
Server-rendered HTML includes:
|
||||||
|
|
||||||
- `window.__PAGE_PROPS__` — the serialized server function return value, read by `hydrateRoot` on the client.
|
- `window.__PAGE_PROPS__` — the serialized server function return value, read by `hydrateRoot` on the client.
|
||||||
|
- A `<script type="importmap">` mapping React package specifiers to the esm.sh CDN (uses the `?dev` build in development).
|
||||||
- A `<script type="module" async>` tag pointing to the page's bundled client script.
|
- A `<script type="module" async>` tag pointing to the page's bundled client script.
|
||||||
- A `<link rel="stylesheet">` tag if the bundler emitted a CSS file for the page.
|
- A `<link rel="stylesheet">` tag if the bundler emitted a CSS file for the page.
|
||||||
- In development: the HMR client script.
|
- In development: the HMR client script.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"bun-plugin-tailwind": "^0.1.2",
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"lightningcss-wasm": "^1.32.0",
|
"lightningcss-wasm": "^1.32.0",
|
||||||
@@ -19,17 +20,18 @@
|
|||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"ora": "^9.0.0",
|
"ora": "^9.0.0",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@types/chokidar": "^2.1.7",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/micromatch": "^4.0.10",
|
"@types/micromatch": "^4.0.10",
|
||||||
"happy-dom": "^20.8.4",
|
"happy-dom": "^20.8.4",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -165,6 +167,8 @@
|
|||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
|
|
||||||
|
"@types/chokidar": ["@types/chokidar@2.1.7", "", { "dependencies": { "chokidar": "*" } }, "sha512-A7/MFHf6KF7peCzjEC1BBTF8jpmZTokb3vr/A0NxRGfwRLK3Ws+Hq6ugVn6cJIMfM6wkCak/aplWrxbTcu8oig=="],
|
||||||
|
|
||||||
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
||||||
|
|
||||||
"@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="],
|
"@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="],
|
||||||
@@ -195,6 +199,8 @@
|
|||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
|
|
||||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||||
|
|
||||||
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
||||||
@@ -285,10 +291,18 @@
|
|||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|||||||
+28
-12
@@ -58,7 +58,7 @@ This report compares the two on their overlapping surface — server-side render
|
|||||||
| Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
|
| Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
|
||||||
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
|
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
|
||||||
| Component model | Classic SSR + hydration | React Server Components + Client Components |
|
| Component model | Classic SSR + hydration | React Server Components + Client Components |
|
||||||
| Data fetching | Per-page `server` export | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
| Data fetching | Per-page `.server.ts` companion file | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
||||||
| State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
|
| State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
|
||||||
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
|
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
|
||||||
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` |
|
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` |
|
||||||
@@ -210,19 +210,25 @@ Streaming SSR's benefits — progressive flushing, Suspense-based partial render
|
|||||||
|
|
||||||
### Data Fetching
|
### Data Fetching
|
||||||
|
|
||||||
**Bunext** exposes a single data-fetching primitive: the `server` export on each page module.
|
**Bunext** exposes a single data-fetching primitive: a companion **`.server.ts`** file alongside each page.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
// src/pages/products.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
const data = await db.query(...);
|
const data = await db.query(...);
|
||||||
return { props: { data } };
|
return { props: { data } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
```
|
```
|
||||||
|
|
||||||
The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server function.
|
The server file is never bundled into client JS — it runs exclusively on the server at request time. The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server file at all.
|
||||||
|
|
||||||
**Design notes:**
|
**Design notes:**
|
||||||
- One server function per page. Data fetching is centralised at the page level, not scattered across components.
|
- One server file per page. Data fetching is centralised at the page level, not scattered across components.
|
||||||
|
- The page file (`.tsx`) exports only client-safe code — the React component, `meta`, `Head`, `config`, and `html_props`. Server-only code (Bun APIs, database clients, `fs`) lives in the `.server.ts` companion and is never sent to the browser.
|
||||||
- All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently.
|
- All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently.
|
||||||
- Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size.
|
- Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size.
|
||||||
|
|
||||||
@@ -289,7 +295,7 @@ Bunext's caching model is its answer to SSG. Rather than pre-building pages at d
|
|||||||
|
|
||||||
**Bunext** implements a **file-based HTML cache**:
|
**Bunext** implements a **file-based HTML cache**:
|
||||||
|
|
||||||
- Enabled per-page via `config.cachePage` or returned dynamically from the `server` function at runtime.
|
- Enabled per-page via `config.cachePage` (exported from the page file) or returned dynamically from the server function in the `.server.ts` companion at runtime.
|
||||||
- On a cache miss, the rendered HTML is written to `public/__bunext/cache/<key>.res.html` alongside a metadata file `<key>.meta.json` (creation timestamp, expiry, paradigm).
|
- On a cache miss, the rendered HTML is written to `public/__bunext/cache/<key>.res.html` alongside a metadata file `<key>.meta.json` (creation timestamp, expiry, paradigm).
|
||||||
- On a cache hit, the HTML file is read and returned with `X-Bunext-Cache: HIT`.
|
- On a cache hit, the HTML file is read and returned with `X-Bunext-Cache: HIT`.
|
||||||
- A cron job runs every 30 seconds to delete expired entries.
|
- A cron job runs every 30 seconds to delete expired entries.
|
||||||
@@ -316,7 +322,7 @@ The key distinction from SSG: Bunext's cache is **demand-driven**. A site with 1
|
|||||||
|
|
||||||
### Metadata and SEO
|
### Metadata and SEO
|
||||||
|
|
||||||
**Bunext** supports both static and dynamic metadata:
|
**Bunext** supports both static and dynamic metadata. These exports live in the **page file** (not the `.server.ts` companion), since they are processed at the server HTML-generation step and may reference types from the client module:
|
||||||
|
|
||||||
- `export const meta: BunextPageModuleMeta` — static object with `title`, `description`, `keywords`, `author`, `robots`, `canonical`, `themeColor`, `og.*`, and `twitter.*` fields.
|
- `export const meta: BunextPageModuleMeta` — static object with `title`, `description`, `keywords`, `author`, `robots`, `canonical`, `themeColor`, `og.*`, and `twitter.*` fields.
|
||||||
- `export const meta: BunextPageModuleMetaFn` — async function receiving `ctx` and `serverRes` for dynamic metadata based on fetched data.
|
- `export const meta: BunextPageModuleMetaFn` — async function receiving `ctx` and `serverRes` for dynamic metadata based on fetched data.
|
||||||
@@ -540,10 +546,13 @@ Next.js wraps these in `NextRequest` and `NextResponse`, which add convenience m
|
|||||||
|
|
||||||
### Conditional Runtime Caching
|
### Conditional Runtime Caching
|
||||||
|
|
||||||
Bunext's `server` function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
Bunext's server function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
// src/pages/products.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
const user = await getUser(ctx.req);
|
const user = await getUser(ctx.req);
|
||||||
return {
|
return {
|
||||||
props: { data },
|
props: { data },
|
||||||
@@ -551,16 +560,21 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
cacheExpiry: 300,
|
cacheExpiry: 300,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
```
|
```
|
||||||
|
|
||||||
Next.js's ISR and full-route cache operate on fixed revalidation intervals set at build time. There is no mechanism to decide at runtime whether a specific request should be cached.
|
Next.js's ISR and full-route cache operate on fixed revalidation intervals set at build time. There is no mechanism to decide at runtime whether a specific request should be cached.
|
||||||
|
|
||||||
### Page Response Transform
|
### Page Response Transform
|
||||||
|
|
||||||
The `resTransform` field in the page `server` function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
The `resTransform` field in the server function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export const server: BunextPageServerFn = async (ctx) => {
|
// src/pages/page.server.ts
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
ctx.resTransform = (res) => {
|
ctx.resTransform = (res) => {
|
||||||
res.headers.set("X-Custom-Header", "value");
|
res.headers.set("X-Custom-Header", "value");
|
||||||
return res;
|
return res;
|
||||||
@@ -568,6 +582,8 @@ export const server: BunextPageServerFn = async (ctx) => {
|
|||||||
|
|
||||||
return { props: {} };
|
return { props: {} };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
```
|
```
|
||||||
|
|
||||||
This exists because page responses are generated entirely by the framework (`renderToString` → HTML template). Unlike API routes — where the developer returns a `Response` directly and already has full control — there is no other hook to modify the final page response at the route level without going through global middleware.
|
This exists because page responses are generated entirely by the framework (`renderToString` → HTML template). Unlike API routes — where the developer returns a `Response` directly and already has full control — there is no other hook to modify the final page response at the route level without going through global middleware.
|
||||||
@@ -588,7 +604,7 @@ Next.js's codebase spans Turbopack (Rust), SWC (Rust), the App Router internals,
|
|||||||
|
|
||||||
The RSC model requires developers to constantly reason about the `"use client"` / `"use server"` boundary: what can be async, what has access to browser APIs, what gets serialized into the RSC payload. Mistakes at this boundary produce runtime errors that are difficult to diagnose.
|
The RSC model requires developers to constantly reason about the `"use client"` / `"use server"` boundary: what can be async, what has access to browser APIs, what gets serialized into the RSC payload. Mistakes at this boundary produce runtime errors that are difficult to diagnose.
|
||||||
|
|
||||||
Bunext has one rule: the `server` function runs on the server, the component runs on both (SSR then hydration). There is no boundary to reason about.
|
Bunext has one rule: the `.server.ts` companion file runs on the server, the page file runs on both (SSR then hydration). The separation is enforced by the file system, not by decorators or directives. There is no boundary to reason about inside a file.
|
||||||
|
|
||||||
### No Vendor Lock-In
|
### No Vendor Lock-In
|
||||||
|
|
||||||
|
|||||||
Vendored
+8
-15
@@ -1,28 +1,21 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
|
||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
import init from "../../functions/init";
|
|
||||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
|
||||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import { rmSync } from "fs";
|
||||||
|
import bunextInit from "../../functions/bunext-init";
|
||||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("build")
|
return new Command("build")
|
||||||
.description("Build Project")
|
.description("Build Project")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
process.env.BUILD = "true";
|
|
||||||
try {
|
try {
|
||||||
execSync(`rm -rf ${HYDRATION_DST_DIR}`);
|
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||||
execSync(`rm -rf ${BUNX_CWD_PAGES_REWRITE_DIR}`);
|
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
catch (error) { }
|
catch (error) { }
|
||||||
await rewritePagesModule();
|
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||||
await init();
|
await bunextInit({ build_only: true });
|
||||||
log.banner();
|
log.success("Modules Built Successfully!");
|
||||||
log.build("Building Project ...");
|
process.exit();
|
||||||
// await allPagesBunBundler();
|
|
||||||
allPagesBundler();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import startServer from "../../functions/server/start-server";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
import bunextInit from "../../functions/bunext-init";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import { rmSync } from "fs";
|
||||||
|
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
log.error(`Uncaught exception: ${error}`);
|
||||||
|
});
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
log.error(`Unhandled rejection: ${reason}`);
|
||||||
|
});
|
||||||
|
log.info("Running development server ...");
|
||||||
|
try {
|
||||||
|
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||||
|
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
try {
|
||||||
|
await bunextInit();
|
||||||
|
await startServer();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Failed to start development server: ${error}`);
|
||||||
|
}
|
||||||
Vendored
+53
-16
@@ -1,24 +1,61 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import startServer from "../../functions/server/start-server";
|
import path from "path";
|
||||||
import { log } from "../../utils/log";
|
|
||||||
import bunextInit from "../../functions/bunext-init";
|
|
||||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
let retries = 0;
|
||||||
|
let timeout;
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("dev")
|
return new Command("dev")
|
||||||
.description("Run development server")
|
.description("Run development server")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
process.env.NODE_ENV == "development";
|
await dev();
|
||||||
log.info("Running development server ...");
|
|
||||||
try {
|
|
||||||
execSync(`rm -rf ${HYDRATION_DST_DIR}`);
|
|
||||||
execSync(`rm -rf ${BUNX_CWD_PAGES_REWRITE_DIR}`);
|
|
||||||
}
|
|
||||||
catch (error) { }
|
|
||||||
await rewritePagesModule();
|
|
||||||
await bunextInit();
|
|
||||||
await startServer();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function dev() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (retries >= MAX_RETRIES) {
|
||||||
|
console.error(`Dev server crashed ${MAX_RETRIES} times. Exiting.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts");
|
||||||
|
const dev_spawn_js_file = path.resolve(__dirname, "dev-spawn.js");
|
||||||
|
const final_spawn_file = existsSync(dev_spawn_js_file)
|
||||||
|
? dev_spawn_js_file
|
||||||
|
: dev_spawn_file;
|
||||||
|
const spawn_options = {
|
||||||
|
cmd: ["bun", final_spawn_file],
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
async onExit(subprocess, exitCode, signalCode, error) {
|
||||||
|
writeErrorFile({ exitCode, error });
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: "development",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let dev_process;
|
||||||
|
try {
|
||||||
|
dev_process = Bun.spawn(spawn_options);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Failed to start dev process:`, error);
|
||||||
|
retries++;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
return await dev();
|
||||||
|
}
|
||||||
|
const exited = await dev_process.exited;
|
||||||
|
if (exited) {
|
||||||
|
retries++;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
return await dev();
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
-3
@@ -4,21 +4,19 @@ import start from "./start";
|
|||||||
import dev from "./dev";
|
import dev from "./dev";
|
||||||
import build from "./build";
|
import build from "./build";
|
||||||
import { log } from "../utils/log";
|
import { log } from "../utils/log";
|
||||||
import rewritePages from "./rewrite-pages";
|
|
||||||
/**
|
/**
|
||||||
* # Describe Program
|
* # Describe Program
|
||||||
*/
|
*/
|
||||||
program
|
program
|
||||||
.name(`bunext`)
|
.name(`bunext`)
|
||||||
.description(`A React Next JS replacement built with bun JS`)
|
.description(`A React Next JS replacement built with bun JS`)
|
||||||
.version(`1.0.0`);
|
.version(`1.0.43`);
|
||||||
/**
|
/**
|
||||||
* # Declare Commands
|
* # Declare Commands
|
||||||
*/
|
*/
|
||||||
program.addCommand(dev());
|
program.addCommand(dev());
|
||||||
program.addCommand(start());
|
program.addCommand(start());
|
||||||
program.addCommand(build());
|
program.addCommand(build());
|
||||||
program.addCommand(rewritePages());
|
|
||||||
/**
|
/**
|
||||||
* # Handle Unavailable Commands
|
* # Handle Unavailable Commands
|
||||||
*/
|
*/
|
||||||
|
|||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
import { Command } from "commander";
|
|
||||||
export default function (): Command;
|
|
||||||
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
import { Command } from "commander";
|
|
||||||
import { log } from "../../utils/log";
|
|
||||||
import init from "../../functions/init";
|
|
||||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
|
||||||
export default function () {
|
|
||||||
return new Command("rewrite-pages")
|
|
||||||
.description("Rewrite pages from src to .bunext dir")
|
|
||||||
.action(async () => {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
process.env.BUILD = "true";
|
|
||||||
await init();
|
|
||||||
log.banner();
|
|
||||||
log.build("Rewriting Pages ...");
|
|
||||||
await rewritePagesModule();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Vendored
+53
-7
@@ -1,14 +1,60 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import startServer from "../../functions/server/start-server";
|
import path from "path";
|
||||||
import { log } from "../../utils/log";
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
import bunextInit from "../../functions/bunext-init";
|
import { existsSync } from "fs";
|
||||||
|
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")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
process.env.NODE_ENV = "production";
|
await start();
|
||||||
log.info("Starting production server ...");
|
|
||||||
await bunextInit();
|
|
||||||
await startServer();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function start() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (retries >= MAX_RETRIES) {
|
||||||
|
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const prod_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
|
||||||
|
const prod_spawn_js_file = path.resolve(__dirname, "prod-spawn.js");
|
||||||
|
const final_spawn_file = existsSync(prod_spawn_js_file)
|
||||||
|
? prod_spawn_js_file
|
||||||
|
: prod_spawn_file;
|
||||||
|
const spawn_options = {
|
||||||
|
cmd: ["bun", final_spawn_file],
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
onExit(subprocess, exitCode, signalCode, error) {
|
||||||
|
writeErrorFile({ exitCode, error });
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: "production",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let dev_process;
|
||||||
|
try {
|
||||||
|
dev_process = Bun.spawn(spawn_options);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Failed to start production process:`, error);
|
||||||
|
retries++;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
return await start();
|
||||||
|
}
|
||||||
|
const exited = await dev_process.exited;
|
||||||
|
if (exited) {
|
||||||
|
retries++;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
return await start();
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
retries = 0;
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import bunextInit from "../../functions/bunext-init";
|
||||||
|
import startServer from "../../functions/server/start-server";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
log.info("Starting production server ...");
|
||||||
|
await bunextInit();
|
||||||
|
await startServer();
|
||||||
Vendored
+3
@@ -4,4 +4,7 @@ export declare const AppData: {
|
|||||||
readonly BunextStaticFilesCacheExpiry: number;
|
readonly BunextStaticFilesCacheExpiry: number;
|
||||||
readonly ClientHMRPath: "__bunext_client_hmr__";
|
readonly ClientHMRPath: "__bunext_client_hmr__";
|
||||||
readonly BunextClientHydrationScriptID: "bunext-client-hydration-script";
|
readonly BunextClientHydrationScriptID: "bunext-client-hydration-script";
|
||||||
|
readonly BunextTmpFileExt: ".bunext_tmp.tsx";
|
||||||
|
readonly BunextHMRRetryRoute: "/.bunext/hmr-retry";
|
||||||
|
readonly DefaultMaxLogs: 50;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+3
@@ -4,4 +4,7 @@ export const AppData = {
|
|||||||
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
|
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
|
||||||
ClientHMRPath: "__bunext_client_hmr__",
|
ClientHMRPath: "__bunext_client_hmr__",
|
||||||
BunextClientHydrationScriptID: "bunext-client-hydration-script",
|
BunextClientHydrationScriptID: "bunext-client-hydration-script",
|
||||||
|
BunextTmpFileExt: ".bunext_tmp.tsx",
|
||||||
|
BunextHMRRetryRoute: "/.bunext/hmr-retry",
|
||||||
|
DefaultMaxLogs: 50,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
target?: "bun" | "browser";
|
|
||||||
};
|
|
||||||
export default function allPagesBunBundler(params?: Params): Promise<void>;
|
|
||||||
export {};
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
import grabAllPages from "../../utils/grab-all-pages";
|
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
|
||||||
import isDevelopment from "../../utils/is-development";
|
|
||||||
import { log } from "../../utils/log";
|
|
||||||
import tailwindcss from "bun-plugin-tailwind";
|
|
||||||
const { HYDRATION_DST_DIR } = grabDirNames();
|
|
||||||
export default async function allPagesBunBundler(params) {
|
|
||||||
const { target = "browser" } = params || {};
|
|
||||||
const pages = grabAllPages({ exclude_api: true });
|
|
||||||
const dev = isDevelopment();
|
|
||||||
let buildStart = 0;
|
|
||||||
buildStart = performance.now();
|
|
||||||
const build = await Bun.build({
|
|
||||||
entrypoints: pages.map((p) => p.transformed_path),
|
|
||||||
outdir: HYDRATION_DST_DIR,
|
|
||||||
minify: true,
|
|
||||||
format: "esm",
|
|
||||||
define: {
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
|
||||||
},
|
|
||||||
naming: {
|
|
||||||
entry: "[name]/[hash].[ext]",
|
|
||||||
chunk: "chunks/[name]-[hash].[ext]",
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
tailwindcss,
|
|
||||||
{
|
|
||||||
name: "post-build",
|
|
||||||
setup(build) {
|
|
||||||
build.onEnd((result) => {
|
|
||||||
console.log("result", result);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// plugins: [
|
|
||||||
// ],
|
|
||||||
splitting: true,
|
|
||||||
target,
|
|
||||||
external: ["bun"],
|
|
||||||
});
|
|
||||||
console.log("build", build);
|
|
||||||
if (build.success) {
|
|
||||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
|
||||||
log.success(`[Built] in ${elapsed}ms`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
/**
|
|
||||||
* Locations of the pages Files.
|
|
||||||
*/
|
|
||||||
page_file_paths?: string[];
|
|
||||||
};
|
|
||||||
export default function allPagesBundler(params?: Params): Promise<void>;
|
|
||||||
export {};
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
import * as esbuild from "esbuild";
|
|
||||||
import grabAllPages from "../../utils/grab-all-pages";
|
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
|
||||||
import isDevelopment from "../../utils/is-development";
|
|
||||||
import { log } from "../../utils/log";
|
|
||||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
|
||||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
|
||||||
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
|
|
||||||
import { writeFileSync } from "fs";
|
|
||||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
|
||||||
let build_starts = 0;
|
|
||||||
const MAX_BUILD_STARTS = 10;
|
|
||||||
export default async function allPagesBundler(params) {
|
|
||||||
const { page_file_paths } = params || {};
|
|
||||||
const pages = grabAllPages({ exclude_api: true });
|
|
||||||
const target_pages = page_file_paths?.[0]
|
|
||||||
? pages.filter((p) => page_file_paths.includes(p.local_path))
|
|
||||||
: pages;
|
|
||||||
if (!page_file_paths) {
|
|
||||||
global.PAGE_FILES = pages;
|
|
||||||
}
|
|
||||||
const virtualEntries = {};
|
|
||||||
const dev = isDevelopment();
|
|
||||||
for (const page of target_pages) {
|
|
||||||
const key = page.transformed_path;
|
|
||||||
const txt = await grabClientHydrationScript({
|
|
||||||
page_local_path: page.local_path,
|
|
||||||
});
|
|
||||||
if (!txt)
|
|
||||||
continue;
|
|
||||||
virtualEntries[key] = txt;
|
|
||||||
}
|
|
||||||
const virtualPlugin = {
|
|
||||||
name: "virtual-entrypoints",
|
|
||||||
setup(build) {
|
|
||||||
build.onResolve({ filter: /^virtual:/ }, (args) => ({
|
|
||||||
path: args.path.replace("virtual:", ""),
|
|
||||||
namespace: "virtual",
|
|
||||||
}));
|
|
||||||
build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => ({
|
|
||||||
contents: virtualEntries[args.path],
|
|
||||||
loader: "tsx",
|
|
||||||
resolveDir: process.cwd(),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const artifactTracker = {
|
|
||||||
name: "artifact-tracker",
|
|
||||||
setup(build) {
|
|
||||||
let buildStart = 0;
|
|
||||||
build.onStart(() => {
|
|
||||||
build_starts++;
|
|
||||||
buildStart = performance.now();
|
|
||||||
if (build_starts == MAX_BUILD_STARTS) {
|
|
||||||
const error_msg = `Build Failed. Please check all your components and imports.`;
|
|
||||||
log.error(error_msg);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
build.onEnd((result) => {
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
for (const error of result.errors) {
|
|
||||||
const loc = error.location;
|
|
||||||
const location = loc
|
|
||||||
? ` ${loc.file}:${loc.line}:${loc.column}`
|
|
||||||
: "";
|
|
||||||
log.error(`[Build]${location} ${error.text}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const artifacts = grabArtifactsFromBundledResults({
|
|
||||||
pages: target_pages,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
if (artifacts?.[0] && artifacts.length > 0) {
|
|
||||||
for (let i = 0; i < artifacts.length; i++) {
|
|
||||||
const artifact = artifacts[i];
|
|
||||||
global.BUNDLER_CTX_MAP[artifact.local_path] = artifact;
|
|
||||||
}
|
|
||||||
// params?.post_build_fn?.({ artifacts });
|
|
||||||
writeFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts));
|
|
||||||
}
|
|
||||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
|
||||||
log.success(`[Built] in ${elapsed}ms`);
|
|
||||||
global.RECOMPILING = false;
|
|
||||||
build_starts = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const entryPoints = Object.keys(virtualEntries).map((k) => `virtual:${k}`);
|
|
||||||
await esbuild.build({
|
|
||||||
entryPoints,
|
|
||||||
outdir: HYDRATION_DST_DIR,
|
|
||||||
bundle: true,
|
|
||||||
minify: true,
|
|
||||||
format: "esm",
|
|
||||||
target: "es2020",
|
|
||||||
platform: "browser",
|
|
||||||
define: {
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
|
||||||
},
|
|
||||||
entryNames: "[dir]/[name]/[hash]",
|
|
||||||
metafile: true,
|
|
||||||
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
|
|
||||||
jsx: "automatic",
|
|
||||||
splitting: true,
|
|
||||||
// logLevel: "silent",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { BundlerCTXMap } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
post_build_fn?: (params: {
|
||||||
|
artifacts: BundlerCTXMap[];
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
build_only?: boolean;
|
||||||
|
start?: boolean;
|
||||||
|
};
|
||||||
|
export default function allPagesESBuildContextBundler(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||||
|
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||||
|
import path from "path";
|
||||||
|
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
||||||
|
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
||||||
|
export default async function allPagesESBuildContextBundler(params) {
|
||||||
|
try {
|
||||||
|
const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||||
|
const pages = grabAllPages({ exclude_api: true });
|
||||||
|
global.BUNEXT_PAGE_FILES = pages;
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const entryToPage = new Map();
|
||||||
|
for (const page of pages) {
|
||||||
|
const tsx = await grabClientHydrationScript({
|
||||||
|
page_local_path: page.local_path,
|
||||||
|
});
|
||||||
|
if (!tsx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`);
|
||||||
|
// await Bun.write(entryFile, txt, { createPath: true });
|
||||||
|
entryToPage.set(entryFile, { ...page, tsx });
|
||||||
|
}
|
||||||
|
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
|
||||||
|
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
||||||
|
entryPoints,
|
||||||
|
outdir: HYDRATION_DST_DIR,
|
||||||
|
bundle: true,
|
||||||
|
minify: !dev,
|
||||||
|
format: "esm",
|
||||||
|
target: "es2020",
|
||||||
|
platform: "browser",
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
entryNames: "[dir]/[hash]",
|
||||||
|
metafile: true,
|
||||||
|
plugins: [
|
||||||
|
forceExternalReact(),
|
||||||
|
tailwindEsbuildPlugin,
|
||||||
|
virtualFilesPlugin({
|
||||||
|
entryToPage,
|
||||||
|
}),
|
||||||
|
esbuildCTXArtifactTracker({
|
||||||
|
entryToPage,
|
||||||
|
post_build_fn: params?.post_build_fn,
|
||||||
|
build_only: params?.build_only || params?.start,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
jsx: "automatic",
|
||||||
|
splitting: true,
|
||||||
|
treeShaking: true,
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react-dom/client",
|
||||||
|
"react/jsx-runtime",
|
||||||
|
"react/jsx-dev-runtime",
|
||||||
|
...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
|
||||||
|
],
|
||||||
|
logLevel: did_process_exit_because_of_bundler_error
|
||||||
|
? "silent"
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(`ESBUILD Error =>`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function forceExternalReact() {
|
||||||
|
return {
|
||||||
|
name: "force-external-react",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
|
||||||
|
if (args.pluginData?.externalReact)
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
path: args.path,
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function apiRoutesBundler(): Promise<void>;
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import tailwindcss from "bun-plugin-tailwind";
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
|
export default async function apiRoutesBundler() {
|
||||||
|
const api_routes = grabAllPages({ api_only: true });
|
||||||
|
const dev = isDevelopment();
|
||||||
|
try {
|
||||||
|
const build = await Bun.build({
|
||||||
|
entrypoints: api_routes.map((r) => r.local_path),
|
||||||
|
target: "bun",
|
||||||
|
format: "esm",
|
||||||
|
jsx: {
|
||||||
|
runtime: "automatic",
|
||||||
|
development: dev,
|
||||||
|
},
|
||||||
|
minify: !dev,
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
plugins: [tailwindcss],
|
||||||
|
naming: {
|
||||||
|
entry: "api/[dir]/[name].[ext]",
|
||||||
|
chunk: "api/[dir]/chunks/[hash].[ext]",
|
||||||
|
},
|
||||||
|
// external: [
|
||||||
|
// "react",
|
||||||
|
// "react-dom",
|
||||||
|
// "react-dom/client",
|
||||||
|
// "react/jsx-runtime",
|
||||||
|
// ],
|
||||||
|
splitting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(`API paths build ERROR:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function apiRoutesContextBundler(): Promise<void>;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||||
|
import apiRoutesCTXArtifactTracker from "./plugins/api-routes-ctx-artifact-tracker";
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
|
export default async function apiRoutesContextBundler() {
|
||||||
|
const pages = grabAllPages({ api_only: true });
|
||||||
|
const dev = isDevelopment();
|
||||||
|
// if (global.API_ROUTES_BUNDLER_CTX) {
|
||||||
|
// await global.API_ROUTES_BUNDLER_CTX.dispose();
|
||||||
|
// global.API_ROUTES_BUNDLER_CTX = undefined;
|
||||||
|
// }
|
||||||
|
// global.API_ROUTES_BUNDLER_CTX = await esbuild.context({
|
||||||
|
// entryPoints: pages.map((p) => p.local_path),
|
||||||
|
// outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
// bundle: true,
|
||||||
|
// minify: !dev,
|
||||||
|
// format: "esm",
|
||||||
|
// target: "esnext",
|
||||||
|
// platform: "node",
|
||||||
|
// define: {
|
||||||
|
// "process.env.NODE_ENV": JSON.stringify(
|
||||||
|
// dev ? "development" : "production",
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// entryNames: "api/[dir]/[hash]",
|
||||||
|
// metafile: true,
|
||||||
|
// plugins: [
|
||||||
|
// tailwindEsbuildPlugin,
|
||||||
|
// apiRoutesCTXArtifactTracker({ pages }),
|
||||||
|
// ],
|
||||||
|
// jsx: "automatic",
|
||||||
|
// external: [
|
||||||
|
// "react",
|
||||||
|
// "react-dom",
|
||||||
|
// "react/jsx-runtime",
|
||||||
|
// "react/jsx-dev-runtime",
|
||||||
|
// "bun:*",
|
||||||
|
// ],
|
||||||
|
// });
|
||||||
|
// await global.API_ROUTES_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
type Params = {};
|
||||||
|
export default function buildOnstartErrorHandler(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default async function buildOnstartErrorHandler(params) {
|
||||||
|
// const error_msg = `Build Failed. Please check all your components and imports.`;
|
||||||
|
// log.error(error_msg);
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log(`Killing Bundler ...`);
|
||||||
|
// console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
|
||||||
|
global.BUNEXT_BUNDLER_CTX_DISPOSED = true;
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
await Promise.all([
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX?.dispose(),
|
||||||
|
global.BUNEXT_BUNDLER_CTX?.dispose(),
|
||||||
|
]);
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function bunReactModulesBundler(): Promise<void>;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import path from "path";
|
||||||
|
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||||
|
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR } = grabDirNames();
|
||||||
|
const VENDOR_ENTRIES = {
|
||||||
|
react: `
|
||||||
|
import React from "react";
|
||||||
|
export const {
|
||||||
|
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||||
|
Suspense, cloneElement, createContext, createElement, createRef,
|
||||||
|
forwardRef, isValidElement, lazy, memo, startTransition,
|
||||||
|
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||||
|
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||||
|
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||||
|
useTransition, version, use, cache, act,
|
||||||
|
} = React;
|
||||||
|
export default React;
|
||||||
|
`,
|
||||||
|
"react-dom": `
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
export const {
|
||||||
|
createPortal, flushSync, version,
|
||||||
|
} = ReactDOM;
|
||||||
|
export default ReactDOM;
|
||||||
|
`,
|
||||||
|
"react-dom_client": `
|
||||||
|
import ReactDOMClient from "react-dom/client";
|
||||||
|
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||||
|
export default ReactDOMClient;
|
||||||
|
`,
|
||||||
|
"react_jsx-runtime": `
|
||||||
|
import JSXRuntime from "react/jsx-runtime";
|
||||||
|
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||||
|
`,
|
||||||
|
"react_jsx-dev-runtime": `
|
||||||
|
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||||
|
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
export default async function bunReactModulesBundler() {
|
||||||
|
const dev = isDevelopment();
|
||||||
|
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||||
|
const tmpDir = path.join(BUNEXT_VENDOR_DIR, "_tmp");
|
||||||
|
mkdirSync(tmpDir, { recursive: true });
|
||||||
|
const entrypoints = [];
|
||||||
|
for (const [name, contents] of Object.entries(VENDOR_ENTRIES)) {
|
||||||
|
const file = path.join(tmpDir, `${name}.mjs`);
|
||||||
|
writeFileSync(file, contents);
|
||||||
|
entrypoints.push(file);
|
||||||
|
}
|
||||||
|
await Bun.build({
|
||||||
|
entrypoints,
|
||||||
|
outdir: BUNEXT_VENDOR_DIR,
|
||||||
|
splitting: true,
|
||||||
|
format: "esm",
|
||||||
|
target: "browser",
|
||||||
|
minify: !dev,
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
rmSync(tmpDir, { force: true, recursive: true });
|
||||||
|
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||||
|
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||||
|
imports: {
|
||||||
|
react: `${PUBLIC_ROOT}/react.js`,
|
||||||
|
"react-dom": `${PUBLIC_ROOT}/react-dom.js`,
|
||||||
|
"react-dom/client": `${PUBLIC_ROOT}/react-dom_client.js`,
|
||||||
|
"react/jsx-runtime": `${PUBLIC_ROOT}/react_jsx-runtime.js`,
|
||||||
|
"react/jsx-dev-runtime": `${PUBLIC_ROOT}/react_jsx-dev-runtime.js`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ import * as esbuild from "esbuild";
|
|||||||
import type { BundlerCTXMap, PageFiles } from "../../types";
|
import type { BundlerCTXMap, PageFiles } from "../../types";
|
||||||
type Params = {
|
type Params = {
|
||||||
result: esbuild.BuildResult<esbuild.BuildOptions>;
|
result: esbuild.BuildResult<esbuild.BuildOptions>;
|
||||||
pages: PageFiles[];
|
entryToPage: Map<string, PageFiles & {
|
||||||
|
tsx: string;
|
||||||
|
}>;
|
||||||
|
virtual_match?: string;
|
||||||
};
|
};
|
||||||
export default function grabArtifactsFromBundledResults({ result, pages, }: Params): BundlerCTXMap[] | undefined;
|
export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match, }: Params): BundlerCTXMap[] | undefined;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
export default function grabArtifactsFromBundledResults({ result, pages, }) {
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match = "hydration-virtual", }) {
|
||||||
if (result.errors.length > 0)
|
if (result.errors.length > 0)
|
||||||
return;
|
return;
|
||||||
|
const virtual_regex = new RegExp(`^${virtual_match}:`);
|
||||||
const artifacts = Object.entries(result.metafile.outputs)
|
const artifacts = Object.entries(result.metafile.outputs)
|
||||||
.filter(([, meta]) => meta.entryPoint)
|
.filter(([, meta]) => meta.entryPoint)
|
||||||
.map(([outputPath, meta]) => {
|
.map(([outputPath, meta]) => {
|
||||||
const target_page = pages.find((p) => {
|
const entrypoint = meta.entryPoint?.match(virtual_regex)
|
||||||
return meta.entryPoint === `virtual:${p.transformed_path}`;
|
? meta.entryPoint?.replace(virtual_regex, "")
|
||||||
});
|
: meta.entryPoint
|
||||||
|
? path.join(ROOT_DIR, meta.entryPoint)
|
||||||
|
: "";
|
||||||
|
const target_page = entryToPage.get(entrypoint);
|
||||||
if (!target_page || !meta.entryPoint) {
|
if (!target_page || !meta.entryPoint) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const { file_name, local_path, url_path, transformed_path } = target_page;
|
const { file_name, local_path, url_path } = target_page;
|
||||||
const cssPath = meta.cssBundle || undefined;
|
|
||||||
return {
|
return {
|
||||||
path: outputPath,
|
path: outputPath,
|
||||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||||
@@ -21,11 +27,10 @@ export default function grabArtifactsFromBundledResults({ result, pages, }) {
|
|||||||
? "text/css"
|
? "text/css"
|
||||||
: "text/javascript",
|
: "text/javascript",
|
||||||
entrypoint: meta.entryPoint,
|
entrypoint: meta.entryPoint,
|
||||||
css_path: cssPath,
|
css_path: meta.cssBundle,
|
||||||
file_name,
|
file_name,
|
||||||
local_path,
|
local_path,
|
||||||
url_path,
|
url_path,
|
||||||
transformed_path,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (artifacts.length > 0) {
|
if (artifacts.length > 0) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
type Params = {
|
type Params = {
|
||||||
page_local_path: string;
|
page_local_path: string;
|
||||||
};
|
};
|
||||||
export default function grabClientHydrationScript({ page_local_path, }: Params): Promise<string>;
|
export default function grabClientHydrationScript({ page_local_path, }: Params): Promise<string | undefined>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+32
-15
@@ -1,27 +1,44 @@
|
|||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import path from "path";
|
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import AppNames from "../../utils/grab-app-names";
|
|
||||||
import grabConstants from "../../utils/grab-constants";
|
import grabConstants from "../../utils/grab-constants";
|
||||||
import pagePathTransform from "../../utils/page-path-transform";
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
const { PAGES_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function grabClientHydrationScript({ page_local_path, }) {
|
export default async function grabClientHydrationScript({ page_local_path, }) {
|
||||||
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
|
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
|
||||||
const target_path = pagePathTransform({ page_path: page_local_path });
|
const { root_file_path } = grabRootFilePath();
|
||||||
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
|
// const target_path = pagePathTransform({ page_path: page_local_path });
|
||||||
const does_root_exist = existsSync(root_component_path);
|
// const target_root_path = root_file_path
|
||||||
let txt = ``;
|
// ? pagePathTransform({ page_path: root_file_path })
|
||||||
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
|
// : undefined;
|
||||||
if (does_root_exist) {
|
if (!existsSync(page_local_path)) {
|
||||||
txt += `import Root from "${root_component_path}";\n`;
|
return undefined;
|
||||||
}
|
}
|
||||||
txt += `import Page from "${target_path}";\n\n`;
|
if (root_file_path) {
|
||||||
|
if (!existsSync(root_file_path)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const root_content = await Bun.file(root_file_path).text();
|
||||||
|
if (!root_content.match(/^export default/m)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const page_content = await Bun.file(page_local_path).text();
|
||||||
|
if (!page_content.match(/^export default/m)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let txt = ``;
|
||||||
|
txt += `import { hydrateRoot } from "${ROOT_DIR}/node_modules/react-dom/client.js";\n`;
|
||||||
|
// txt += `import react from "${ROOT_DIR}/node_modules/react/index.js";\n`;
|
||||||
|
if (root_file_path) {
|
||||||
|
txt += `import Root from "${root_file_path}";\n`;
|
||||||
|
}
|
||||||
|
txt += `import Page from "${page_local_path}";\n\n`;
|
||||||
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
||||||
if (does_root_exist) {
|
if (root_file_path) {
|
||||||
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
|
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
|
txt += `const component = <Page {...pageProps} />\n`;
|
||||||
}
|
}
|
||||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||||
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type Params = {
|
||||||
|
post_build_fn?: (params: {
|
||||||
|
artifacts: any[];
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
export default function pagesSSRBundler(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||||
|
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||||
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
|
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||||
|
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||||
|
export default async function pagesSSRBundler(params) {
|
||||||
|
const pages = grabAllPages({
|
||||||
|
include_server: true,
|
||||||
|
});
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const config = global.BUNEXT_CONFIG;
|
||||||
|
try {
|
||||||
|
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-pages.json"), JSON.stringify(pages, null, 4));
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
const entryToPage = new Map();
|
||||||
|
const { root_file_path } = grabRootFilePath();
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.local_path.match(/\/pages\/api\//) ||
|
||||||
|
page.local_path.match(/\.server\.tsx?$/)) {
|
||||||
|
const ts = await Bun.file(page.local_path).text();
|
||||||
|
if (ts.match(/(export default)|(export \w+ handler)|(export \w+ server)/)) {
|
||||||
|
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tsx = grabPageReactComponentString({
|
||||||
|
file_path: page.local_path,
|
||||||
|
root_file_path,
|
||||||
|
});
|
||||||
|
if (!tsx)
|
||||||
|
continue;
|
||||||
|
if (!tsx.match(/export default/))
|
||||||
|
continue;
|
||||||
|
entryToPage.set(page.local_path, { ...page, tsx });
|
||||||
|
}
|
||||||
|
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
|
||||||
|
try {
|
||||||
|
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entry-to-page.json"), JSON.stringify(Object(entryToPage), null, 4));
|
||||||
|
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entrypoints.json"), JSON.stringify(entryPoints, null, 4));
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
try {
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints,
|
||||||
|
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
bundle: true,
|
||||||
|
minify: !dev,
|
||||||
|
format: "esm",
|
||||||
|
target: "esnext",
|
||||||
|
platform: "node",
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
entryNames: "[dir]/[hash]",
|
||||||
|
metafile: true,
|
||||||
|
plugins: [
|
||||||
|
tailwindEsbuildPlugin,
|
||||||
|
ssrVirtualFilesPlugin({
|
||||||
|
entryToPage,
|
||||||
|
}),
|
||||||
|
ssrCTXArtifactTracker({
|
||||||
|
entryToPage,
|
||||||
|
post_build_fn: params?.post_build_fn,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
jsx: "automatic",
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react/jsx-runtime",
|
||||||
|
"react/jsx-dev-runtime",
|
||||||
|
"bun:*",
|
||||||
|
"bun",
|
||||||
|
"sqlite-vec",
|
||||||
|
"better-sqlite3",
|
||||||
|
...(config.ssr_compiler_excludes || []),
|
||||||
|
],
|
||||||
|
splitting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type Params = {
|
||||||
|
post_build_fn?: (params: {
|
||||||
|
artifacts: any[];
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
export default function pagesSSRContextBundler(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||||
|
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||||
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
|
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||||
|
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
|
export default async function pagesSSRContextBundler(params) {
|
||||||
|
const pages = grabAllPages();
|
||||||
|
const dev = isDevelopment();
|
||||||
|
if (global.BUNEXT_SSR_BUNDLER_CTX) {
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX.dispose();
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
}
|
||||||
|
const entryToPage = new Map();
|
||||||
|
const { root_file_path } = grabRootFilePath();
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.local_path.match(/\/pages\/api\//)) {
|
||||||
|
const ts = await Bun.file(page.local_path).text();
|
||||||
|
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tsx = grabPageReactComponentString({
|
||||||
|
file_path: page.local_path,
|
||||||
|
root_file_path,
|
||||||
|
});
|
||||||
|
if (!tsx)
|
||||||
|
continue;
|
||||||
|
entryToPage.set(page.local_path, { ...page, tsx });
|
||||||
|
}
|
||||||
|
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
|
||||||
|
entryPoints,
|
||||||
|
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
bundle: true,
|
||||||
|
minify: !dev,
|
||||||
|
format: "esm",
|
||||||
|
target: "esnext",
|
||||||
|
platform: "node",
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
entryNames: "[dir]/[hash]",
|
||||||
|
metafile: true,
|
||||||
|
plugins: [
|
||||||
|
tailwindEsbuildPlugin,
|
||||||
|
ssrVirtualFilesPlugin({
|
||||||
|
entryToPage,
|
||||||
|
}),
|
||||||
|
ssrCTXArtifactTracker({
|
||||||
|
entryToPage,
|
||||||
|
post_build_fn: params?.post_build_fn,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
jsx: "automatic",
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react/jsx-runtime",
|
||||||
|
"react/jsx-dev-runtime",
|
||||||
|
"bun:*",
|
||||||
|
],
|
||||||
|
// logLevel: "silent",
|
||||||
|
});
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { type Plugin } from "esbuild";
|
||||||
|
import type { PageFiles } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
pages: PageFiles[];
|
||||||
|
};
|
||||||
|
export default function apiRoutesCTXArtifactTracker({ pages }: Params): Plugin;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {} from "esbuild";
|
||||||
|
import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
|
let build_start = 0;
|
||||||
|
let build_starts = 0;
|
||||||
|
const MAX_BUILD_STARTS = 2;
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
export default function apiRoutesCTXArtifactTracker({ pages }) {
|
||||||
|
const artifactTracker = {
|
||||||
|
name: "ssr-artifact-tracker",
|
||||||
|
setup(build) {
|
||||||
|
build.onStart(async () => {
|
||||||
|
build_starts++;
|
||||||
|
build_start = performance.now();
|
||||||
|
if (build_starts == MAX_BUILD_STARTS) {
|
||||||
|
await buildOnstartErrorHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.log("result.errors", result.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifacts = Object.entries(result.metafile.outputs)
|
||||||
|
.filter(([, meta]) => meta.entryPoint)
|
||||||
|
.map(([outputPath, meta]) => {
|
||||||
|
const entrypoint = meta.entryPoint
|
||||||
|
? path.join(ROOT_DIR, meta.entryPoint)
|
||||||
|
: undefined;
|
||||||
|
const target_page = pages.find((p) => p.local_path == entrypoint);
|
||||||
|
if (!target_page || !meta.entryPoint) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { file_name, local_path, url_path } = target_page;
|
||||||
|
return {
|
||||||
|
path: outputPath,
|
||||||
|
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||||
|
type: "text/javascript",
|
||||||
|
entrypoint: meta.entryPoint,
|
||||||
|
file_name,
|
||||||
|
local_path,
|
||||||
|
url_path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
|
// for (let i = 0; i < artifacts.length; i++) {
|
||||||
|
// const artifact = artifacts[i];
|
||||||
|
// if (
|
||||||
|
// artifact?.local_path &&
|
||||||
|
// global.API_ROUTES_BUNDLER_CTX_MAP
|
||||||
|
// ) {
|
||||||
|
// global.API_ROUTES_BUNDLER_CTX_MAP[
|
||||||
|
// artifact.local_path
|
||||||
|
// ] = artifact;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||||
|
log.success(`API Routes [Built] in ${elapsed}ms`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return artifactTracker;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
declare const BunSkipNonBrowserPlugin: Bun.BunPlugin;
|
||||||
|
export default BunSkipNonBrowserPlugin;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { log } from "../../../utils/log";
|
||||||
|
const BunSkipNonBrowserPlugin = {
|
||||||
|
name: "skip-non-browser",
|
||||||
|
setup(build) {
|
||||||
|
const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
|
||||||
|
// const skipped_modules = new Set<string>();
|
||||||
|
build.onResolve({ filter: skipFilter }, (args) => {
|
||||||
|
global.BUNEXT_SKIPPED_BROWSER_MODULES.add(args.path);
|
||||||
|
return {
|
||||||
|
path: args.path,
|
||||||
|
namespace: "skipped",
|
||||||
|
// external: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// build.onEnd(() => {
|
||||||
|
// log.warn(`global.BUNEXT_SKIPPED_BROWSER_MODULES`, [
|
||||||
|
// ...global.BUNEXT_SKIPPED_BROWSER_MODULES,
|
||||||
|
// ]);
|
||||||
|
// });
|
||||||
|
// build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||||
|
// // If it's a built-in like 'fs' or 'path', skip it immediately
|
||||||
|
// const excludes = [
|
||||||
|
// "fs",
|
||||||
|
// "path",
|
||||||
|
// "os",
|
||||||
|
// "crypto",
|
||||||
|
// "net",
|
||||||
|
// "events",
|
||||||
|
// "util",
|
||||||
|
// "tls",
|
||||||
|
// ];
|
||||||
|
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||||
|
// return {
|
||||||
|
// path: args.path,
|
||||||
|
// // namespace: "skipped",
|
||||||
|
// external: true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// try {
|
||||||
|
// Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||||
|
// return null;
|
||||||
|
// } catch (e) {
|
||||||
|
// console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||||
|
// return {
|
||||||
|
// path: args.path,
|
||||||
|
// // namespace: "skipped",
|
||||||
|
// external: true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
|
||||||
|
return {
|
||||||
|
contents: `
|
||||||
|
const proxy = new Proxy(() => proxy, {
|
||||||
|
get: () => proxy,
|
||||||
|
construct: () => proxy,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Database = proxy;
|
||||||
|
export const join = proxy;
|
||||||
|
export const fileURLToPath = proxy;
|
||||||
|
export const arch = proxy;
|
||||||
|
export const platform = proxy;
|
||||||
|
export const statSync = proxy;
|
||||||
|
|
||||||
|
export const $H = proxy;
|
||||||
|
export const _ = proxy;
|
||||||
|
|
||||||
|
export default proxy;
|
||||||
|
`,
|
||||||
|
loader: "js",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default BunSkipNonBrowserPlugin;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
export default function reactVendorChunkPlugin(): esbuild.Plugin;
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
// plugins/react-vendor-chunk-plugin.ts
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||||
|
const REACT_MODULES = new Set([
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react-dom/client",
|
||||||
|
"react/jsx-runtime",
|
||||||
|
"react/jsx-dev-runtime",
|
||||||
|
]);
|
||||||
|
const VENDOR_BASE = "/.bunext/public/vendor";
|
||||||
|
const REACT_ENTRIES = {
|
||||||
|
react: `
|
||||||
|
import React from "react";
|
||||||
|
export const {
|
||||||
|
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||||
|
Suspense, cloneElement, createContext, createElement, createFactory,
|
||||||
|
createRef, forwardRef, isValidElement, lazy, memo, startTransition,
|
||||||
|
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||||
|
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||||
|
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||||
|
useTransition, version,
|
||||||
|
} = React;
|
||||||
|
export default React;
|
||||||
|
`,
|
||||||
|
"react-dom": `
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
export const {
|
||||||
|
createPortal, flushSync, findDOMNode, hydrate, render,
|
||||||
|
unmountComponentAtNode, version,
|
||||||
|
} = ReactDOM;
|
||||||
|
export default ReactDOM;
|
||||||
|
`,
|
||||||
|
"react-dom/client": `
|
||||||
|
import ReactDOMClient from "react-dom/client";
|
||||||
|
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||||
|
export default ReactDOMClient;
|
||||||
|
`,
|
||||||
|
"react/jsx-runtime": `
|
||||||
|
import JSXRuntime from "react/jsx-runtime";
|
||||||
|
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||||
|
export default JSXRuntime;
|
||||||
|
`,
|
||||||
|
"react/jsx-dev-runtime": `
|
||||||
|
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||||
|
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||||
|
export default JSXDevRuntime;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
// Map bare specifier -> browser path
|
||||||
|
function vendorPath(specifier) {
|
||||||
|
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||||
|
return `${VENDOR_BASE}/${filename}`;
|
||||||
|
}
|
||||||
|
function vendorOutfile(specifier) {
|
||||||
|
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||||
|
return path.join(BUNEXT_VENDOR_DIR, filename);
|
||||||
|
}
|
||||||
|
export default function reactVendorChunkPlugin() {
|
||||||
|
let vendorReady;
|
||||||
|
return {
|
||||||
|
name: "react-vendor-chunk",
|
||||||
|
setup(build) {
|
||||||
|
vendorReady ??= buildAllVendorChunks(build.initialOptions);
|
||||||
|
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, async (args) => {
|
||||||
|
const bare = args.path.replace(/\/index(\.m?js)?$/, "");
|
||||||
|
if (!(bare in REACT_ENTRIES))
|
||||||
|
return;
|
||||||
|
await vendorReady;
|
||||||
|
return {
|
||||||
|
path: vendorPath(bare),
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function buildAllVendorChunks(parentOptions) {
|
||||||
|
await Promise.all(Object.entries(REACT_ENTRIES).map(([specifier, contents]) => esbuild.build({
|
||||||
|
stdin: {
|
||||||
|
contents,
|
||||||
|
resolveDir: process.cwd(),
|
||||||
|
loader: "tsx",
|
||||||
|
},
|
||||||
|
outfile: vendorOutfile(specifier),
|
||||||
|
bundle: true,
|
||||||
|
minify: parentOptions.minify,
|
||||||
|
format: "esm",
|
||||||
|
target: parentOptions.target,
|
||||||
|
platform: "browser",
|
||||||
|
define: parentOptions.define,
|
||||||
|
mainFields: ["module", "main"],
|
||||||
|
conditions: ["import", "default"],
|
||||||
|
})));
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { type Plugin } from "esbuild";
|
||||||
|
import type { PageFiles } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
entryToPage: Map<string, PageFiles & {
|
||||||
|
tsx: string;
|
||||||
|
}>;
|
||||||
|
post_build_fn?: (params: {
|
||||||
|
artifacts: any[];
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
build_only?: boolean;
|
||||||
|
};
|
||||||
|
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }: Params): Plugin;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import {} from "esbuild";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
|
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
||||||
|
import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||||
|
import _ from "lodash";
|
||||||
|
import pagesSSRBundler from "../pages-ssr-bundler";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||||
|
import fullRebuild from "../../server/full-rebuild";
|
||||||
|
import path from "path";
|
||||||
|
import cleanupLogsDirs from "../../cleanup-logs-dir";
|
||||||
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
||||||
|
let build_start = 0;
|
||||||
|
const MAX_BUILD_STARTS = 2;
|
||||||
|
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }) {
|
||||||
|
const artifactTracker = {
|
||||||
|
name: "artifact-tracker",
|
||||||
|
setup(build) {
|
||||||
|
build.onStart(async () => {
|
||||||
|
global.BUNEXT_MAIN_CTX_BUILD_STARTS++;
|
||||||
|
build_start = performance.now();
|
||||||
|
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||||
|
if (global.BUNEXT_MAIN_CTX_BUILD_STARTS >= MAX_BUILD_STARTS &&
|
||||||
|
!does_error_file_exist) {
|
||||||
|
await buildOnstartErrorHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
build.onEnd(async (result) => {
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
log.error(`Build errors:`);
|
||||||
|
for (const err of result.errors) {
|
||||||
|
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||||
|
}
|
||||||
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
|
try {
|
||||||
|
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifacts = grabArtifactsFromBundledResults({
|
||||||
|
result,
|
||||||
|
entryToPage,
|
||||||
|
});
|
||||||
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
|
for (let i = 0; i < artifacts.length; i++) {
|
||||||
|
const artifact = artifacts[i];
|
||||||
|
if (artifact?.local_path &&
|
||||||
|
global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
|
global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path] =
|
||||||
|
_.merge(global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path], artifact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||||
|
log.success(`[Built] in ${elapsed}ms`);
|
||||||
|
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||||
|
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
||||||
|
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||||
|
// SSR must finish before HMR so server props are fresh
|
||||||
|
if (build_only) {
|
||||||
|
try {
|
||||||
|
await pagesSSRBundler();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (does_error_file_exist) {
|
||||||
|
mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
|
||||||
|
cpSync(BUNX_BUNDLER_ERROR_EXIT_FILE, path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`));
|
||||||
|
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||||
|
cleanupLogsDirs();
|
||||||
|
await fullRebuild();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
await pagesSSRBundler();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
}
|
||||||
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
|
try {
|
||||||
|
await post_build_fn?.({ artifacts });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Post-build Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return artifactTracker;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { Plugin } from "esbuild";
|
||||||
|
declare const reactAliasPlugin: Plugin;
|
||||||
|
export default reactAliasPlugin;
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
const reactAliasPlugin = {
|
||||||
|
name: "react-alias",
|
||||||
|
setup(build) {
|
||||||
|
const reactPath = path.join(ROOT_DIR, "node_modules");
|
||||||
|
build.onResolve({ filter: /^react$/ }, () => ({
|
||||||
|
path: path.join(reactPath, "react", "index.js"),
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: /^react-dom$/ }, () => ({
|
||||||
|
path: path.join(reactPath, "react-dom", "index.js"),
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: /^react\/jsx-runtime$/ }, () => ({
|
||||||
|
path: path.join(reactPath, "react", "jsx-runtime.js"),
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
||||||
|
path: path.join(reactPath, "react", "jsx-dev-runtime.js"),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default reactAliasPlugin;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { type Plugin } from "esbuild";
|
||||||
|
import type { PageFiles } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
entryToPage: Map<string, PageFiles & {
|
||||||
|
tsx: string;
|
||||||
|
}>;
|
||||||
|
post_build_fn?: (params: {
|
||||||
|
artifacts: any[];
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {} from "esbuild";
|
||||||
|
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
let build_start = 0;
|
||||||
|
let build_starts = 0;
|
||||||
|
const MAX_BUILD_STARTS = 2;
|
||||||
|
const { BUNX_TMP_DIR } = grabDirNames();
|
||||||
|
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
||||||
|
const artifactTracker = {
|
||||||
|
name: "ssr-artifact-tracker",
|
||||||
|
setup(build) {
|
||||||
|
build.onStart(async () => {
|
||||||
|
build_starts++;
|
||||||
|
build_start = performance.now();
|
||||||
|
if (build_starts == MAX_BUILD_STARTS) {
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
build.onEnd(async (result) => {
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
build_starts = 0;
|
||||||
|
for (const err of result.errors) {
|
||||||
|
console.error(`SSR Build error: ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifacts = grabArtifactsFromBundledResults({
|
||||||
|
result,
|
||||||
|
entryToPage,
|
||||||
|
virtual_match: `ssr-virtual`,
|
||||||
|
});
|
||||||
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
|
for (let i = 0; i < artifacts.length; i++) {
|
||||||
|
const artifact = artifacts[i];
|
||||||
|
if (artifact?.local_path &&
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_MAP) {
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_MAP[artifact.local_path] = artifact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// post_build_fn?.({ artifacts });
|
||||||
|
// const elapsed = (performance.now() - build_start).toFixed(
|
||||||
|
// 0,
|
||||||
|
// );
|
||||||
|
// log.success(`SSR [Built] in ${elapsed}ms`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.BUNEXT_SSR_BUNDLER_CTX_MAP, null, 4));
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return artifactTracker;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Plugin } from "esbuild";
|
||||||
|
import type { PageFiles } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
entryToPage: Map<string, PageFiles & {
|
||||||
|
tsx: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
export default function ssrVirtualFilesPlugin({ entryToPage }: Params): Plugin;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
|
export default function ssrVirtualFilesPlugin({ entryToPage }) {
|
||||||
|
const virtualPlugin = {
|
||||||
|
name: "ssr-virtual-hydration",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^ssr-virtual:/ }, (args) => {
|
||||||
|
const final_path = args.path.replace(/ssr-virtual:/, "");
|
||||||
|
return {
|
||||||
|
path: final_path,
|
||||||
|
namespace: "ssr-virtual",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => {
|
||||||
|
const target = entryToPage.get(args.path);
|
||||||
|
if (!target?.tsx)
|
||||||
|
return null;
|
||||||
|
const contents = target.tsx;
|
||||||
|
if (!contents.match(/export/)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contents: contents || "",
|
||||||
|
loader: "tsx",
|
||||||
|
resolveDir: path.dirname(target.local_path),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return virtualPlugin;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Plugin } from "esbuild";
|
||||||
|
import type { PageFiles } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
entryToPage: Map<string, PageFiles & {
|
||||||
|
tsx: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
export default function virtualFilesPlugin({ entryToPage }: Params): Plugin;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import path from "path";
|
||||||
|
export default function virtualFilesPlugin({ entryToPage }) {
|
||||||
|
const virtualPlugin = {
|
||||||
|
name: "virtual-hydration",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^hydration-virtual:/ }, (args) => {
|
||||||
|
const final_path = args.path.replace(/hydration-virtual:/, "");
|
||||||
|
return {
|
||||||
|
path: final_path,
|
||||||
|
namespace: "hydration-virtual",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
build.onResolve({ filter: /node_modules\/react(-dom)?/ }, (args) => ({
|
||||||
|
path: args.path.includes("react-dom")
|
||||||
|
? args.path.includes("client")
|
||||||
|
? "react-dom/client"
|
||||||
|
: "react-dom"
|
||||||
|
: args.path.includes("jsx-dev")
|
||||||
|
? "react/jsx-dev-runtime"
|
||||||
|
: args.path.includes("jsx")
|
||||||
|
? "react/jsx-runtime"
|
||||||
|
: "react",
|
||||||
|
external: true,
|
||||||
|
}));
|
||||||
|
build.onLoad({ filter: /.*/, namespace: "hydration-virtual" }, (args) => {
|
||||||
|
const target = entryToPage.get(args.path);
|
||||||
|
if (!target?.tsx)
|
||||||
|
return null;
|
||||||
|
const contents = target.tsx;
|
||||||
|
return {
|
||||||
|
contents: contents || "",
|
||||||
|
loader: "tsx",
|
||||||
|
resolveDir: path.dirname(target.local_path),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return virtualPlugin;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function reactModulesBundler(): Promise<void>;
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import path from "path";
|
||||||
|
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||||
|
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR, ROOT_DIR } = grabDirNames();
|
||||||
|
const VENDOR_ENTRIES = {
|
||||||
|
react: `
|
||||||
|
import React from "react";
|
||||||
|
export const {
|
||||||
|
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||||
|
Suspense, cloneElement, createContext, createElement, createRef,
|
||||||
|
forwardRef, isValidElement, lazy, memo, startTransition,
|
||||||
|
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||||
|
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||||
|
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||||
|
useTransition, version, use, cache, act,
|
||||||
|
} = React;
|
||||||
|
export default React;
|
||||||
|
`,
|
||||||
|
"react-dom": `
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
export const {
|
||||||
|
createPortal, flushSync, version,
|
||||||
|
} = ReactDOM;
|
||||||
|
export default ReactDOM;
|
||||||
|
`,
|
||||||
|
"react-dom_client": `
|
||||||
|
import ReactDOMClient from "react-dom/client";
|
||||||
|
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||||
|
export default ReactDOMClient;
|
||||||
|
`,
|
||||||
|
"react_jsx-runtime": `
|
||||||
|
import JSXRuntime from "react/jsx-runtime";
|
||||||
|
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||||
|
`,
|
||||||
|
"react_jsx-dev-runtime": `
|
||||||
|
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||||
|
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
export default async function reactModulesBundler() {
|
||||||
|
const dev = isDevelopment();
|
||||||
|
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||||
|
const tmpDir = path.join(BUNEXT_VENDOR_DIR, "_tmp");
|
||||||
|
mkdirSync(tmpDir, { recursive: true });
|
||||||
|
const entrypoints = {};
|
||||||
|
for (const [name, contents] of Object.entries(VENDOR_ENTRIES)) {
|
||||||
|
const file = path.join(tmpDir, `${name}.mjs`);
|
||||||
|
writeFileSync(file, contents);
|
||||||
|
entrypoints[name] = file;
|
||||||
|
}
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: entrypoints,
|
||||||
|
outdir: BUNEXT_VENDOR_DIR,
|
||||||
|
bundle: true,
|
||||||
|
splitting: true,
|
||||||
|
format: "esm",
|
||||||
|
platform: "browser",
|
||||||
|
target: "es2020",
|
||||||
|
minify: !dev,
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
rmSync(tmpDir, { force: true, recursive: true });
|
||||||
|
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||||
|
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||||
|
imports: {
|
||||||
|
react: `${PUBLIC_ROOT}/react.js`,
|
||||||
|
"react-dom": `${PUBLIC_ROOT}/react-dom.js`,
|
||||||
|
"react-dom/client": `${PUBLIC_ROOT}/react-dom_client.js`,
|
||||||
|
"react/jsx-runtime": `${PUBLIC_ROOT}/react_jsx-runtime.js`,
|
||||||
|
"react/jsx-dev-runtime": `${PUBLIC_ROOT}/react_jsx-dev-runtime.js`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { BundlerCTXMap } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
artifacts: BundlerCTXMap[];
|
||||||
|
page_file_paths?: string[];
|
||||||
|
};
|
||||||
|
export default function recordArtifacts({ artifacts, page_file_paths, }: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import _ from "lodash";
|
||||||
|
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||||
|
export default async function recordArtifacts({ artifacts, page_file_paths, }) {
|
||||||
|
const artifacts_map = {};
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
if (artifact?.local_path) {
|
||||||
|
artifacts_map[artifact.local_path] = artifact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
|
global.BUNEXT_BUNDLER_CTX_MAP = _.merge(global.BUNEXT_BUNDLER_CTX_MAP, artifacts_map);
|
||||||
|
}
|
||||||
|
// await Bun.write(
|
||||||
|
// HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||||
|
// JSON.stringify(artifacts_map, null, 4),
|
||||||
|
// );
|
||||||
|
}
|
||||||
Vendored
+45
-17
@@ -1,26 +1,54 @@
|
|||||||
import { type Ora } from "ora";
|
|
||||||
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 FSWatcher } from "fs";
|
import { type DirNames } from "../utils/grab-dir-names";
|
||||||
|
import type { BuildContext } from "esbuild";
|
||||||
|
import grabConstants from "../utils/grab-constants";
|
||||||
|
import type { FSWatcher } from "fs";
|
||||||
/**
|
/**
|
||||||
* # Declare Global Variables
|
* # Declare Global Variables
|
||||||
*/
|
*/
|
||||||
declare global {
|
declare global {
|
||||||
var ORA_SPINNER: Ora;
|
var BUNEXT_CONFIG: BunextConfig;
|
||||||
var CONFIG: BunextConfig;
|
var BUNEXT_SERVER: Server<any> | undefined;
|
||||||
var SERVER: Server<any> | undefined;
|
var BUNEXT_RECOMPILING: boolean;
|
||||||
var RECOMPILING: boolean;
|
var BUNEXT_BUILDING_SSR: boolean;
|
||||||
var WATCHER_TIMEOUT: any;
|
var BUNEXT_IS_SERVER_COMPONENT: boolean;
|
||||||
var ROUTER: FileSystemRouter;
|
var BUNEXT_WATCHER_TIMEOUT: any;
|
||||||
var HMR_CONTROLLERS: GlobalHMRControllerObject[];
|
var BUNEXT_ROUTER: FileSystemRouter;
|
||||||
var LAST_BUILD_TIME: number;
|
var BUNEXT_HMR_CONTROLLERS: GlobalHMRControllerObject[];
|
||||||
var BUNDLER_CTX_MAP: {
|
var BUNEXT_LAST_BUILD_TIME: number;
|
||||||
|
var BUNEXT_BUNDLER_CTX_MAP: {
|
||||||
[k: string]: BundlerCTXMap;
|
[k: string]: BundlerCTXMap;
|
||||||
};
|
};
|
||||||
var BUNDLER_REBUILDS: 0;
|
var BUNEXT_SSR_BUNDLER_CTX_MAP: {
|
||||||
var PAGES_SRC_WATCHER: FSWatcher | undefined;
|
[k: string]: BundlerCTXMap;
|
||||||
var CURRENT_VERSION: string | undefined;
|
};
|
||||||
var PAGE_FILES: PageFiles[];
|
var BUNEXT_BUNDLER_REBUILDS: 0;
|
||||||
var ROOT_FILE_UPDATED: boolean;
|
var BUNEXT_PAGES_SRC_WATCHER: FSWatcher | undefined;
|
||||||
|
var BUNEXT_CURRENT_VERSION: string | undefined;
|
||||||
|
var BUNEXT_PAGE_FILES: PageFiles[];
|
||||||
|
var BUNEXT_ROOT_FILE_UPDATED: boolean;
|
||||||
|
var BUNEXT_SKIPPED_BROWSER_MODULES: Set<string>;
|
||||||
|
var BUNEXT_BUNDLER_CTX: BuildContext | undefined;
|
||||||
|
var BUNEXT_SSR_BUNDLER_CTX: BuildContext | undefined;
|
||||||
|
var BUNEXT_DIR_NAMES: DirNames;
|
||||||
|
var BUNEXT_REACT_IMPORTS_MAP: {
|
||||||
|
imports: Record<string, string>;
|
||||||
|
};
|
||||||
|
var BUNEXT_REACT_DOM_SERVER: any;
|
||||||
|
var BUNEXT_REACT_DOM_MODULE_CACHE: Map<string, {
|
||||||
|
main: any;
|
||||||
|
css: string;
|
||||||
|
}>;
|
||||||
|
var BUNEXT_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||||
|
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||||
|
var BUNEXT_REBUILD_RETRIES: number;
|
||||||
|
var BUNEXT_IS_404_PAGE: boolean;
|
||||||
|
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
||||||
|
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
||||||
}
|
}
|
||||||
export default function bunextInit(): Promise<void>;
|
type Params = {
|
||||||
|
build_only?: boolean;
|
||||||
|
};
|
||||||
|
export default function bunextInit(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
|
|||||||
Vendored
+44
-24
@@ -1,40 +1,60 @@
|
|||||||
import ora, {} from "ora";
|
import grabDirNames, {} from "../utils/grab-dir-names";
|
||||||
import grabDirNames from "../utils/grab-dir-names";
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import init from "./init";
|
import init from "./init";
|
||||||
import isDevelopment from "../utils/is-development";
|
import isDevelopment from "../utils/is-development";
|
||||||
import allPagesBundler from "./bundler/all-pages-bundler";
|
|
||||||
import watcher from "./server/watcher";
|
|
||||||
import { log } from "../utils/log";
|
import { log } from "../utils/log";
|
||||||
import cron from "./server/cron";
|
import cron from "./server/cron";
|
||||||
import EJSON from "../utils/ejson";
|
import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler";
|
||||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
import serverPostBuildFn from "./server/server-post-build-fn";
|
||||||
export default async function bunextInit() {
|
import reactModulesBundler from "./bundler/react-modules-bundler";
|
||||||
global.ORA_SPINNER = ora();
|
import grabConstants from "../utils/grab-constants";
|
||||||
global.ORA_SPINNER.clear();
|
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
|
||||||
global.HMR_CONTROLLERS = [];
|
const dirNames = grabDirNames();
|
||||||
global.BUNDLER_CTX_MAP = {};
|
const { PAGES_DIR } = dirNames;
|
||||||
global.BUNDLER_REBUILDS = 0;
|
export default async function bunextInit(params) {
|
||||||
global.PAGE_FILES = [];
|
global.BUNEXT_HMR_CONTROLLERS = [];
|
||||||
|
global.BUNEXT_BUNDLER_CTX_MAP = {};
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_MAP = {};
|
||||||
|
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
||||||
|
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
||||||
|
global.BUNEXT_REBUILD_RETRIES = 0;
|
||||||
|
global.BUNEXT_PAGE_FILES = [];
|
||||||
|
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||||
|
global.BUNEXT_DIR_NAMES = dirNames;
|
||||||
|
global.BUNEXT_REACT_IMPORTS_MAP = { imports: {} };
|
||||||
|
global.BUNEXT_REACT_DOM_MODULE_CACHE = new Map();
|
||||||
|
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||||
await init();
|
await init();
|
||||||
log.banner();
|
log.banner();
|
||||||
|
global.BUNEXT_CONSTANTS = grabConstants();
|
||||||
|
await reactModulesBundler();
|
||||||
const router = new Bun.FileSystemRouter({
|
const router = new Bun.FileSystemRouter({
|
||||||
style: "nextjs",
|
style: "nextjs",
|
||||||
dir: PAGES_DIR,
|
dir: PAGES_DIR,
|
||||||
});
|
});
|
||||||
global.ROUTER = router;
|
global.BUNEXT_ROUTER = router;
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
if (is_dev) {
|
if (params?.build_only) {
|
||||||
await allPagesBundler();
|
log.build(`Building Modules ...`);
|
||||||
watcher();
|
await allPagesESBuildContextBundler();
|
||||||
|
}
|
||||||
|
else if (is_dev) {
|
||||||
|
log.build(`Building Modules ...`);
|
||||||
|
await allPagesESBuildContextBundler({
|
||||||
|
post_build_fn: async () => {
|
||||||
|
await serverPostBuildFn();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
watcherEsbuildCTX();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const artifacts = EJSON.parse(readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"));
|
log.build(`Building Modules ...`);
|
||||||
if (!artifacts?.[0]) {
|
await allPagesESBuildContextBundler({ start: true });
|
||||||
log.error("Please build first.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
global.BUNDLER_CTX_MAP = artifacts;
|
|
||||||
cron();
|
cron();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// process.on("exit", (code) => {
|
||||||
|
// Bun.spawn([process.execPath, ...process.argv.slice(1)], {
|
||||||
|
// stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
// env: process.env,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|||||||
+4
@@ -13,6 +13,10 @@ 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) {
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ export default async function trimCacheKey({ key, }) {
|
|||||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
const { cache_name, cache_meta_name } = grabCacheNames({
|
||||||
key,
|
key,
|
||||||
});
|
});
|
||||||
const config = global.CONFIG;
|
const config = global.BUNEXT_CONFIG;
|
||||||
const default_expiry_time_seconds = config.defaultCacheExpiry ||
|
const default_expiry_time_seconds = config.default_cache_expiry ||
|
||||||
AppData["DefaultCacheExpiryTimeSeconds"];
|
AppData["DefaultCacheExpiryTimeSeconds"];
|
||||||
const default_expiry_time_milliseconds = default_expiry_time_seconds * 1000;
|
const default_expiry_time_milliseconds = default_expiry_time_seconds * 1000;
|
||||||
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
|
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export default function cleanupLogsDirs(): void;
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
||||||
|
import grabDirNames from "../utils/grab-dir-names";
|
||||||
|
import grabConstants from "../utils/grab-constants";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||||
|
export default function cleanupLogsDirs() {
|
||||||
|
const logs_dirs = readdirSync(BUNX_LOGS_DIR);
|
||||||
|
const { config } = grabConstants();
|
||||||
|
const MAX_LOGS = config.max_logs || AppData["DefaultMaxLogs"];
|
||||||
|
for (let i = 0; i < logs_dirs.length; i++) {
|
||||||
|
const dir = logs_dirs[i];
|
||||||
|
const full_path = path.join(BUNX_LOGS_DIR, dir);
|
||||||
|
const path_stats = statSync(full_path);
|
||||||
|
if (!path_stats.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sub_dir_files = readdirSync(full_path).sort((a, b) => {
|
||||||
|
const timestamp_a = Number(a.split(".")[0]);
|
||||||
|
const timestamp_b = Number(b.split(".")[0]);
|
||||||
|
if (timestamp_a > timestamp_b)
|
||||||
|
return 1;
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
for (let j = 0; j < sub_dir_files.length; j++) {
|
||||||
|
const sub_dir_file = sub_dir_files[j];
|
||||||
|
const sub_dir_file_full_path = path.join(full_path, sub_dir_file);
|
||||||
|
const sub_dir_file_Stats = statSync(sub_dir_file_full_path);
|
||||||
|
if (!sub_dir_file_Stats.isFile()) {
|
||||||
|
rmSync(sub_dir_file_full_path, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j > MAX_LOGS - 1) {
|
||||||
|
rmSync(sub_dir_file_full_path, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.info("Running development server ...");
|
||||||
|
// try {
|
||||||
|
// rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||||
|
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||||
|
// } catch (error) {}
|
||||||
|
// await bunextInit();
|
||||||
|
// await startServer();
|
||||||
Vendored
+19
-6
@@ -1,17 +1,30 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||||
import grabDirNames from "../utils/grab-dir-names";
|
import grabDirNames from "../utils/grab-dir-names";
|
||||||
import { execSync } from "child_process";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import grabConfig from "./grab-config";
|
import grabConfig from "./grab-config";
|
||||||
|
import { log } from "../utils/log";
|
||||||
export default async function () {
|
export default async function () {
|
||||||
const dirNames = grabDirNames();
|
const dirNames = grabDirNames();
|
||||||
const is_dev = !Boolean(process.env.NODE_ENV == "production");
|
const is_dev = !Boolean(process.env.NODE_ENV == "production");
|
||||||
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
rmSync(dirNames.BUNEXT_CACHE_DIR, {
|
||||||
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
rmSync(dirNames.BUNX_CWD_MODULE_CACHE_DIR, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
if (dirNames.ROOT_DIR.startsWith(dirNames.BUNX_ROOT_DIR) &&
|
||||||
|
!dirNames.ROOT_DIR.includes(`${dirNames.BUNX_ROOT_DIR}/test/`)) {
|
||||||
|
log.error(`Can't Run From this Directory => ${dirNames.ROOT_DIR}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json();
|
const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json();
|
||||||
const current_version = package_json.version;
|
const current_version = package_json.version;
|
||||||
global.CURRENT_VERSION = current_version;
|
global.BUNEXT_CURRENT_VERSION = current_version;
|
||||||
}
|
}
|
||||||
catch (error) { }
|
catch (error) { }
|
||||||
const keys = Object.keys(dirNames);
|
const keys = Object.keys(dirNames);
|
||||||
@@ -32,7 +45,7 @@ export default async function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const config = (await grabConfig()) || {};
|
const config = (await grabConfig()) || {};
|
||||||
global.CONFIG = {
|
global.BUNEXT_CONFIG = {
|
||||||
...config,
|
...config,
|
||||||
development: is_dev,
|
development: is_dev,
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
Vendored
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { spawn } from "bun";
|
||||||
|
// Only the "supervisor" respawns. The child sets this env var so it won't respawn itself.
|
||||||
|
const IS_CHILD = process.env.__RESPAWN_CHILD === "1";
|
||||||
|
let shuttingDown = false;
|
||||||
|
async function cleanup() {
|
||||||
|
// Put real cleanup here: close DB handles, servers, file descriptors, timers, etc.
|
||||||
|
// Must be awaitable — do NOT rely on process.on("exit") for this.
|
||||||
|
}
|
||||||
|
function respawn(code) {
|
||||||
|
const child = spawn({
|
||||||
|
cmd: [process.execPath, ...process.argv.slice(1)],
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
env: { ...process.env, __RESPAWN_CHILD: "1" },
|
||||||
|
// Detach so the child survives independently and gets its own process group.
|
||||||
|
// Without this, killing the parent's group can take the child with it.
|
||||||
|
});
|
||||||
|
// Let the child live on its own.
|
||||||
|
child.unref?.();
|
||||||
|
}
|
||||||
|
async function shutdown(code) {
|
||||||
|
if (shuttingDown)
|
||||||
|
return;
|
||||||
|
shuttingDown = true;
|
||||||
|
try {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error("cleanup failed:", e);
|
||||||
|
}
|
||||||
|
// Only the supervisor respawns, and only on abnormal exit.
|
||||||
|
if (!IS_CHILD && code !== 0) {
|
||||||
|
respawn(code);
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
// Catch the things that actually fire *before* exit, where async works.
|
||||||
|
process.on("SIGINT", () => shutdown(130));
|
||||||
|
process.on("SIGTERM", () => shutdown(143));
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
console.error(err);
|
||||||
|
shutdown(1);
|
||||||
|
});
|
||||||
|
process.on("unhandledRejection", (err) => {
|
||||||
|
console.error(err);
|
||||||
|
shutdown(1);
|
||||||
|
});
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
type Params = {
|
type Params = {
|
||||||
req: Request;
|
req: Request;
|
||||||
|
server: Bun.Server<any>;
|
||||||
};
|
};
|
||||||
export default function bunextRequestHandler({ req: initial_req, }: Params): Promise<Response>;
|
export default function bunextRequestHandler({ req: initial_req, server, }: Params): Promise<Response>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+29
-6
@@ -1,19 +1,26 @@
|
|||||||
import handleWebPages from "./web-pages/handle-web-pages";
|
import handleWebPages from "./web-pages/handle-web-pages";
|
||||||
import handleRoutes from "./handle-routes";
|
import handleRoutes from "./handle-routes";
|
||||||
import isDevelopment from "../../utils/is-development";
|
import isDevelopment from "../../utils/is-development";
|
||||||
import grabConstants from "../../utils/grab-constants";
|
|
||||||
import handleHmr from "./handle-hmr";
|
import handleHmr from "./handle-hmr";
|
||||||
import handlePublic from "./handle-public";
|
import handlePublic from "./handle-public";
|
||||||
import handleFiles from "./handle-files";
|
import handleFiles from "./handle-files";
|
||||||
export default async function bunextRequestHandler({ req: initial_req, }) {
|
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
||||||
|
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import fullRebuild from "./full-rebuild";
|
||||||
|
const HMR_RETRY_COOLDOWN_MS = 5000;
|
||||||
|
let lastHmrRetryTime = 0;
|
||||||
|
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();
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const { config } = grabConstants();
|
if (checkExcludedPatterns({ path: url.pathname })) {
|
||||||
|
return Response.json({ success: false, msg: `Invalid Path` });
|
||||||
|
}
|
||||||
let response = undefined;
|
let response = undefined;
|
||||||
if (config?.middleware) {
|
if (global.BUNEXT_CONSTANTS.config?.middleware) {
|
||||||
const middleware_res = await config.middleware({
|
const middleware_res = await global.BUNEXT_CONSTANTS.config.middleware({
|
||||||
req: initial_req,
|
req: initial_req,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
@@ -24,8 +31,20 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
|
|||||||
req = middleware_res;
|
req = middleware_res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 ...` });
|
||||||
|
return new Response("Modules Rebuilt");
|
||||||
|
}
|
||||||
if (url.pathname === "/__hmr" && is_dev) {
|
if (url.pathname === "/__hmr" && is_dev) {
|
||||||
response = await handleHmr({ req });
|
return handleHmr({ req });
|
||||||
|
}
|
||||||
|
else if (url.pathname.startsWith("/.bunext")) {
|
||||||
|
response = await handleBunextPublicAssets({ req });
|
||||||
}
|
}
|
||||||
else if (url.pathname.startsWith("/api/")) {
|
else if (url.pathname.startsWith("/api/")) {
|
||||||
response = await handleRoutes({ req });
|
response = await handleRoutes({ req });
|
||||||
@@ -48,8 +67,12 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
|
|||||||
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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function chokadirWatcherEsbuildCTX(): Promise<void>;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import chokidar from "chokidar";
|
||||||
|
import path from "path";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import fullRebuild from "./full-rebuild";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||||
|
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||||
|
export default async function chokadirWatcherEsbuildCTX() {
|
||||||
|
const watcher = chokidar.watch(ROOT_DIR, {
|
||||||
|
ignored: [
|
||||||
|
/(^|[\/\\])\../,
|
||||||
|
/node_modules/,
|
||||||
|
/public/,
|
||||||
|
/\.bunext/,
|
||||||
|
/\.git/,
|
||||||
|
/dist/,
|
||||||
|
/bun\.lockb/,
|
||||||
|
(path) => path.endsWith(AppData["BunextTmpFileExt"]),
|
||||||
|
],
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
depth: 99,
|
||||||
|
});
|
||||||
|
const handleEvent = async (event, filePath) => {
|
||||||
|
let owns_recompile = false;
|
||||||
|
try {
|
||||||
|
const filename = path.relative(ROOT_DIR, filePath);
|
||||||
|
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
|
||||||
|
await fullRebuild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await fullRebuild({ msg: `Restarting Bundler ...` });
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await pagesSSRBundler().catch((error) => {
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (filename.match(/\/styles$/) || filename === "styles") {
|
||||||
|
owns_recompile = true;
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
await Bun.sleep(1000);
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `Detected new \`styles\` directory. Rebuilding ...`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.match(/bunext.config\.ts/)) {
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `bunext.config.ts file changed. Rebuilding server ...`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target_files_match = /\.(tsx?|jsx?|css)$/;
|
||||||
|
if (event === "change") {
|
||||||
|
if (filename.match(target_files_match)) {
|
||||||
|
if (global.BUNEXT_RECOMPILING)
|
||||||
|
return;
|
||||||
|
owns_recompile = true;
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
if (filename.match(/.*\.server\.tsx?/)) {
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = true;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX) {
|
||||||
|
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
|
if (filename.match(/(404|500)\.tsx?/)) {
|
||||||
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
|
try {
|
||||||
|
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (["add", "unlink", "addDir", "unlinkDir"].includes(event)) {
|
||||||
|
const is_file_of_interest = !!filename.match(target_files_match) ||
|
||||||
|
event.includes("Dir");
|
||||||
|
if (!is_file_of_interest)
|
||||||
|
return;
|
||||||
|
if (!filename.match(/^src\/pages\/|\.css$/) ||
|
||||||
|
checkExcludedPatterns({ path: filename }) ||
|
||||||
|
filename.includes(" ")) {
|
||||||
|
return reloadWatcher();
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_RECOMPILING)
|
||||||
|
return;
|
||||||
|
owns_recompile = true;
|
||||||
|
const action = event.startsWith("add") ? "created" : "deleted";
|
||||||
|
const type = filename.match(/\.css$/)
|
||||||
|
? "Stylesheet"
|
||||||
|
: event.includes("Dir")
|
||||||
|
? "Directory"
|
||||||
|
: filename.match(/\/pages\/api\//)
|
||||||
|
? "API Route"
|
||||||
|
: "Page";
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `${type} ${action}: ${filename}. Rebuilding ...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Watcher rebuild failed: ${error}`);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (owns_recompile) {
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watcher
|
||||||
|
.on("add", (path) => handleEvent("add", path))
|
||||||
|
.on("change", (path) => handleEvent("change", path))
|
||||||
|
.on("unlink", (path) => handleEvent("unlink", path))
|
||||||
|
.on("addDir", (path) => handleEvent("addDir", path))
|
||||||
|
.on("unlinkDir", (path) => handleEvent("unlinkDir", path));
|
||||||
|
}
|
||||||
|
function reloadWatcher() {
|
||||||
|
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||||
|
chokadirWatcherEsbuildCTX();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { BundlerCTXMap } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
new_artifacts: BundlerCTXMap[];
|
||||||
|
};
|
||||||
|
export default function cleanupArtifacts({ new_artifacts }: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { log } from "../../utils/log";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
||||||
|
const { ROOT_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME } = grabDirNames();
|
||||||
|
export default async function cleanupArtifacts({ new_artifacts }) {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < new_artifacts.length; i++) {
|
||||||
|
const new_artifact = new_artifacts[i];
|
||||||
|
const artifact_public_dir = path.dirname(path.join(ROOT_DIR, new_artifact.path));
|
||||||
|
const dir_content = readdirSync(artifact_public_dir);
|
||||||
|
for (let d = 0; d < dir_content.length; d++) {
|
||||||
|
const dir_or_file = dir_content[d];
|
||||||
|
const full_path = path.join(artifact_public_dir, dir_or_file);
|
||||||
|
const file_or_path_stats = statSync(full_path);
|
||||||
|
if (file_or_path_stats.isDirectory() ||
|
||||||
|
dir_or_file == HYDRATION_DST_DIR_MAP_JSON_FILE_NAME) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (new_artifact.path.includes(dir_or_file) ||
|
||||||
|
new_artifact.css_path?.includes(dir_or_file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existsSync(full_path)) {
|
||||||
|
unlinkSync(full_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function clearRequireCache(modulePath: string): void;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export default function clearRequireCache(modulePath) {
|
||||||
|
const resolved = require.resolve(modulePath);
|
||||||
|
const mod = require.cache[resolved];
|
||||||
|
if (mod) {
|
||||||
|
mod.children?.forEach((child) => {
|
||||||
|
clearRequireCache(child.id);
|
||||||
|
});
|
||||||
|
delete require.cache[resolved];
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export default function fullRebuild(params?: {
|
||||||
|
msg?: string;
|
||||||
|
}): Promise<void>;
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { log } from "../../utils/log";
|
||||||
|
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
||||||
|
import serverPostBuildFn from "./server-post-build-fn";
|
||||||
|
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||||
|
export default async function fullRebuild(params) {
|
||||||
|
try {
|
||||||
|
const { msg } = params || {};
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
if (msg) {
|
||||||
|
log.watch(msg);
|
||||||
|
}
|
||||||
|
global.BUNEXT_ROUTER.reload();
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_BUNDLER_CTX?.dispose();
|
||||||
|
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
await allPagesESBuildContextBundler({
|
||||||
|
post_build_fn: async () => {
|
||||||
|
await serverPostBuildFn();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||||
|
watcherEsbuildCTX();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type Params = {
|
||||||
|
req: Request;
|
||||||
|
};
|
||||||
|
export default function ({ req }: Params): Promise<Response>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import path from "path";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import { readFileResponse } from "./handle-public";
|
||||||
|
import isSafePath from "../../utils/is-safe-path";
|
||||||
|
const { BUNEXT_PUBLIC_DIR } = grabDirNames();
|
||||||
|
export default async function ({ req }) {
|
||||||
|
try {
|
||||||
|
const is_dev = isDevelopment();
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, ""));
|
||||||
|
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
|
||||||
|
return new Response("Forbidden", { status: 403 });
|
||||||
|
}
|
||||||
|
return readFileResponse({
|
||||||
|
file_path,
|
||||||
|
cache: url.pathname.includes("/vendor/")
|
||||||
|
? { duration: 3600 }
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return new Response(`File Not Found`, {
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-1
@@ -2,19 +2,27 @@ 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 })) {
|
||||||
|
return new Response("Forbidden", { status: 403 });
|
||||||
|
}
|
||||||
if (!existsSync(file_path)) {
|
if (!existsSync(file_path)) {
|
||||||
return new Response(`File Doesn't Exist`, {
|
return new Response(`File Doesn't Exist`, {
|
||||||
status: 404,
|
status: 404,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const file = Bun.file(file_path);
|
const file = Bun.file(file_path);
|
||||||
return new Response(file);
|
const headers = new Headers();
|
||||||
|
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`, {
|
||||||
|
|||||||
Vendored
+24
-9
@@ -1,18 +1,36 @@
|
|||||||
|
function removeController(controller) {
|
||||||
|
const idx = global.BUNEXT_HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
|
||||||
|
if (typeof idx == "number" && idx >= 0) {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
export default async function ({ req }) {
|
export default async function ({ req }) {
|
||||||
const referer_url = new URL(req.headers.get("referer") || "");
|
const referer = req.headers.get("referer");
|
||||||
const match = global.ROUTER.match(referer_url.pathname);
|
const page_cookie = req.headers.get("cookie");
|
||||||
|
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.BUNEXT_ROUTER.match(referer_url.pathname);
|
||||||
const target_map = match?.filePath
|
const target_map = match?.filePath
|
||||||
? global.BUNDLER_CTX_MAP[match.filePath]
|
? global.BUNEXT_BUNDLER_CTX_MAP?.[match.filePath]
|
||||||
: undefined;
|
: undefined;
|
||||||
let controller;
|
let controller;
|
||||||
let heartbeat;
|
let heartbeat;
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(c) {
|
start(c) {
|
||||||
controller = c;
|
controller = c;
|
||||||
global.HMR_CONTROLLERS.push({
|
global.BUNEXT_HMR_CONTROLLERS.push({
|
||||||
controller: c,
|
controller: c,
|
||||||
page_url: referer_url.href,
|
page_url: referer_url.href,
|
||||||
target_map,
|
target_map,
|
||||||
|
page_cookie,
|
||||||
});
|
});
|
||||||
heartbeat = setInterval(() => {
|
heartbeat = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
@@ -20,16 +38,13 @@ export default async function ({ req }) {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
|
removeController(controller);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
|
removeController(controller);
|
||||||
if (typeof targetControllerIndex == "number" &&
|
|
||||||
targetControllerIndex >= 0) {
|
|
||||||
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return new Response(stream, {
|
return new Response(stream, {
|
||||||
|
|||||||
+7
@@ -2,4 +2,11 @@ type Params = {
|
|||||||
req: Request;
|
req: Request;
|
||||||
};
|
};
|
||||||
export default function ({ req }: Params): Promise<Response>;
|
export default function ({ req }: Params): Promise<Response>;
|
||||||
|
type FileResponse = {
|
||||||
|
file_path: string;
|
||||||
|
cache?: {
|
||||||
|
duration?: "infinite" | number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export declare function readFileResponse({ file_path, cache }: FileResponse): Response;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+25
-7
@@ -2,20 +2,17 @@ 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 (!existsSync(file_path)) {
|
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
|
||||||
return new Response(`Public File Doesn't Exist`, {
|
return new Response("Forbidden", { status: 403 });
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const file = Bun.file(file_path);
|
return readFileResponse({ file_path });
|
||||||
let res_opts = {};
|
|
||||||
return new Response(file, res_opts);
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return new Response(`Public File Not Found`, {
|
return new Response(`Public File Not Found`, {
|
||||||
@@ -23,3 +20,24 @@ export default async function ({ req }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function readFileResponse({ file_path, cache }) {
|
||||||
|
if (!existsSync(file_path)) {
|
||||||
|
return new Response(`Public File Doesn't Exist`, {
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const file = Bun.file(file_path);
|
||||||
|
const headers = new Headers();
|
||||||
|
if (cache?.duration == "infinite" || (cache && !cache.duration)) {
|
||||||
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
|
}
|
||||||
|
else if (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, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+85
-8
@@ -2,6 +2,10 @@ import grabRouteParams from "../../utils/grab-route-params";
|
|||||||
import grabConstants from "../../utils/grab-constants";
|
import grabConstants from "../../utils/grab-constants";
|
||||||
import grabRouter from "../../utils/grab-router";
|
import grabRouter from "../../utils/grab-router";
|
||||||
import isDevelopment from "../../utils/is-development";
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import _ from "lodash";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function ({ req }) {
|
export default async function ({ req }) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
@@ -14,23 +18,36 @@ export default async function ({ req }) {
|
|||||||
success: false,
|
success: false,
|
||||||
msg: errMsg,
|
msg: errMsg,
|
||||||
}, {
|
}, {
|
||||||
status: 401,
|
status: 404,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const routeParams = await grabRouteParams({ req });
|
const routeParams = await grabRouteParams({
|
||||||
|
req,
|
||||||
|
query: match.query,
|
||||||
|
});
|
||||||
|
let module;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const import_path = is_dev ? `${match.filePath}?t=${now}` : match.filePath;
|
if (is_dev && global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[match.filePath]?.path) {
|
||||||
const module = await import(import_path);
|
const target_import = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[match.filePath].path);
|
||||||
|
module = await import(`${target_import}?t=${now}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const import_path = is_dev
|
||||||
|
? `${match.filePath}?t=${now}`
|
||||||
|
: match.filePath;
|
||||||
|
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 ((config?.maxRequestBodyMB &&
|
if (size > maxBodyBytes) {
|
||||||
size > config.maxRequestBodyMB * MBInBytes) ||
|
|
||||||
size > ServerDefaultRequestBodyLimitBytes) {
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: "Request Body Too Large!",
|
msg: "Request Body Too Large!",
|
||||||
@@ -42,11 +59,71 @@ export default async function ({ req }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await module["default"]({
|
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"] ||
|
||||||
|
module["handler"]);
|
||||||
|
const res = await target_module?.({
|
||||||
...routeParams,
|
...routeParams,
|
||||||
});
|
});
|
||||||
|
if (res instanceof Response) {
|
||||||
if (is_dev) {
|
if (is_dev) {
|
||||||
res.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
res.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
}
|
||||||
|
if (res) {
|
||||||
|
let final_res = Response.json(_.omit(res, [
|
||||||
|
"bunext_api_route_res_options",
|
||||||
|
"bunext_api_route_res_transform_fn",
|
||||||
|
]), {
|
||||||
|
...(res.bunext_api_route_res_options || undefined),
|
||||||
|
});
|
||||||
|
if (res.bunext_api_route_res_transform_fn) {
|
||||||
|
final_res = await res.bunext_api_route_res_transform_fn(final_res);
|
||||||
|
}
|
||||||
|
return final_res;
|
||||||
|
}
|
||||||
|
return Response.json({ err: `Route handler error` });
|
||||||
}
|
}
|
||||||
|
// const relative_path = match.filePath.replace(API_DIR, "");
|
||||||
|
// const relative_module_js_file = relative_path.replace(/\.tsx?$/, ".js");
|
||||||
|
// const bun_module_file = path.join(
|
||||||
|
// BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
// "api",
|
||||||
|
// relative_module_js_file,
|
||||||
|
// );
|
||||||
|
// if (existsSync(bun_module_file)) {
|
||||||
|
// module = await import(`${bun_module_file}?t=${now}`);
|
||||||
|
// } else {
|
||||||
|
// const import_path = is_dev
|
||||||
|
// ? `${match.filePath}?t=${now}`
|
||||||
|
// : match.filePath;
|
||||||
|
// module = await import(import_path);
|
||||||
|
// }
|
||||||
|
// if (is_dev) {
|
||||||
|
// const tmp_path = `${match.filePath}.${now}${AppData["BunextTmpFileExt"]}`;
|
||||||
|
// cpSync(match.filePath, tmp_path);
|
||||||
|
// module = await import(`${tmp_path}?t=${now}`);
|
||||||
|
// try {
|
||||||
|
// unlinkSync(tmp_path);
|
||||||
|
// } catch (error) {}
|
||||||
|
// } else {
|
||||||
|
// // const import_path = is_dev ? `${match.filePath}?t=${now}` : match.filePath;
|
||||||
|
// module = await import(match.filePath);
|
||||||
|
// }
|
||||||
|
// const import_path = is_dev ? `${match.filePath}?t=${now}` : match.filePath;
|
||||||
|
// module = await import(import_path);
|
||||||
|
|||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
target_file_paths?: string[];
|
|
||||||
};
|
|
||||||
export default function rebuildBundler(params?: Params): Promise<void>;
|
|
||||||
export {};
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
import allPagesBundler from "../bundler/all-pages-bundler";
|
|
||||||
import serverPostBuildFn from "./server-post-build-fn";
|
|
||||||
import { log } from "../../utils/log";
|
|
||||||
export default async function rebuildBundler(params) {
|
|
||||||
try {
|
|
||||||
global.ROUTER.reload();
|
|
||||||
// await global.BUNDLER_CTX?.dispose();
|
|
||||||
// global.BUNDLER_CTX = undefined;
|
|
||||||
await allPagesBundler({
|
|
||||||
page_file_paths: params?.target_file_paths,
|
|
||||||
});
|
|
||||||
await serverPostBuildFn();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -9,12 +9,12 @@ export default async function () {
|
|||||||
const config = await grabConfig();
|
const config = await grabConfig();
|
||||||
return {
|
return {
|
||||||
async fetch(req, server) {
|
async fetch(req, server) {
|
||||||
return await bunextRequestHandler({ req });
|
return await bunextRequestHandler({ req, server });
|
||||||
},
|
},
|
||||||
port,
|
port,
|
||||||
idleTimeout: development ? 0 : undefined,
|
idleTimeout: development ? 0 : undefined,
|
||||||
development,
|
development,
|
||||||
websocket: config?.websocket,
|
websocket: config?.websocket,
|
||||||
..._.omit(config?.serverOptions || {}, ["fetch"]),
|
..._.omit(config?.server_options || {}, ["fetch"]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -1 +1,5 @@
|
|||||||
export default function serverPostBuildFn(): Promise<void>;
|
type Params = {
|
||||||
|
reload_all_controllers?: boolean;
|
||||||
|
};
|
||||||
|
export default function serverPostBuildFn(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
|
|||||||
+47
-17
@@ -1,22 +1,53 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import grabPageComponent from "./web-pages/grab-page-component";
|
import grabPageComponent from "./web-pages/grab-page-component";
|
||||||
export default async function serverPostBuildFn() {
|
export default async function serverPostBuildFn(params) {
|
||||||
// if (!global.IS_FIRST_BUNDLE_READY) {
|
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
// global.IS_FIRST_BUNDLE_READY = true;
|
|
||||||
// }
|
|
||||||
if (!global.HMR_CONTROLLERS?.[0] || !global.BUNDLER_CTX_MAP) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < global.HMR_CONTROLLERS.length; i++) {
|
const reload_payload = { reload: true };
|
||||||
const controller = global.HMR_CONTROLLERS[i];
|
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
|
||||||
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
|
if (!controller) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!controller.target_map?.local_path) {
|
if (!controller.target_map?.local_path) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const target_artifact = global.BUNDLER_CTX_MAP[controller.target_map.local_path];
|
if (params?.reload_all_controllers) {
|
||||||
const mock_req = new Request(controller.page_url);
|
try {
|
||||||
const { serverRes } = await grabPageComponent({
|
controller.controller.enqueue(reload_enqueue);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[controller.target_map.local_path];
|
||||||
|
if (!target_artifact?.local_path) {
|
||||||
|
try {
|
||||||
|
controller.controller.enqueue(reload_enqueue);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mock_req = target_artifact.req_url
|
||||||
|
? new Request(target_artifact.req_url, {})
|
||||||
|
: new Request(controller.page_url);
|
||||||
|
if (controller.page_cookie) {
|
||||||
|
mock_req.headers.set("cookie", controller.page_cookie);
|
||||||
|
}
|
||||||
|
const page_component = await grabPageComponent({
|
||||||
req: mock_req,
|
req: mock_req,
|
||||||
|
return_server_res_only: true,
|
||||||
|
is_hydration: true,
|
||||||
});
|
});
|
||||||
|
if (page_component instanceof Response) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { serverRes } = page_component || {};
|
||||||
const final_artifact = {
|
const final_artifact = {
|
||||||
..._.omit(controller, ["controller"]),
|
..._.omit(controller, ["controller"]),
|
||||||
target_map: target_artifact,
|
target_map: target_artifact,
|
||||||
@@ -24,22 +55,21 @@ export default async function serverPostBuildFn() {
|
|||||||
if (!target_artifact) {
|
if (!target_artifact) {
|
||||||
delete final_artifact.target_map;
|
delete final_artifact.target_map;
|
||||||
}
|
}
|
||||||
if (serverRes) {
|
// Always replace so prior error props cannot linger
|
||||||
final_artifact.page_props = serverRes;
|
final_artifact.page_props = serverRes || {};
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
let final_data = {};
|
let final_data = {};
|
||||||
if (global.ROOT_FILE_UPDATED) {
|
if (global.BUNEXT_ROOT_FILE_UPDATED) {
|
||||||
final_data = { reload: true };
|
final_data = reload_payload;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final_data = final_artifact;
|
final_data = final_artifact;
|
||||||
}
|
}
|
||||||
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(final_data)}\n\n`);
|
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(final_data)}\n\n`);
|
||||||
global.ROOT_FILE_UPDATED = false;
|
global.BUNEXT_ROOT_FILE_UPDATED = false;
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
global.HMR_CONTROLLERS.splice(i, 1);
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -1,10 +1,21 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
import serverParamsGen from "./server-params-gen";
|
import serverParamsGen from "./server-params-gen";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||||
export default async function startServer() {
|
export default async function startServer() {
|
||||||
const serverParams = await serverParamsGen();
|
const serverParams = await serverParamsGen();
|
||||||
const server = Bun.serve(serverParams);
|
const server = Bun.serve(serverParams);
|
||||||
global.SERVER = server;
|
const is_dev = isDevelopment();
|
||||||
log.server(`http://localhost:${server.port}`);
|
global.BUNEXT_SERVER = server;
|
||||||
|
log.server(`http://${server.hostname}:${server.port}`);
|
||||||
|
if (is_dev) {
|
||||||
|
setInterval(() => {
|
||||||
|
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||||
|
watcherEsbuildCTX();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export default function watcherEsbuildCTX(): Promise<void>;
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
import { watch, existsSync, statSync, glob } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import fullRebuild from "./full-rebuild";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||||
|
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||||
|
export default async function watcherEsbuildCTX() {
|
||||||
|
const pages_src_watcher = watch(ROOT_DIR, {
|
||||||
|
recursive: true,
|
||||||
|
persistent: true,
|
||||||
|
}, async (event, filename) => {
|
||||||
|
let owns_recompile = false;
|
||||||
|
try {
|
||||||
|
if (!filename)
|
||||||
|
return;
|
||||||
|
const full_file_path = path.join(ROOT_DIR, filename);
|
||||||
|
if (global.BUNEXT_CONFIG.exclude_watch_patterns) {
|
||||||
|
for (let i = 0; i < global.BUNEXT_CONFIG.exclude_watch_patterns.length; i++) {
|
||||||
|
const watch_pattern = global.BUNEXT_CONFIG.exclude_watch_patterns[i];
|
||||||
|
if (watch_pattern instanceof RegExp) {
|
||||||
|
const is_path_excluded = watch_pattern.test(filename);
|
||||||
|
if (is_path_excluded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const excluded_path = path.resolve(ROOT_DIR, watch_pattern);
|
||||||
|
if (excluded_path == full_file_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
|
||||||
|
await fullRebuild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.match(/^\.\w+/)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await fullRebuild({ msg: `Restarting Bundler ...` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await pagesSSRBundler().catch((error) => {
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (filename.endsWith(AppData["BunextTmpFileExt"])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const does_file_exist = existsSync(full_file_path);
|
||||||
|
const file_stat = does_file_exist
|
||||||
|
? statSync(full_file_path)
|
||||||
|
: undefined;
|
||||||
|
if (full_file_path.match(/\/styles$/)) {
|
||||||
|
owns_recompile = true;
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
await Bun.sleep(1000);
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `Detected new \`styles\` directory. Rebuilding ...`,
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const target_files_match = /\.(tsx?|jsx?|css)$/;
|
||||||
|
if (event !== "rename") {
|
||||||
|
if (filename.match(target_files_match)) {
|
||||||
|
if (global.BUNEXT_RECOMPILING)
|
||||||
|
return;
|
||||||
|
owns_recompile = true;
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
if (filename.match(/.*\.server\.tsx?/)) {
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = true;
|
||||||
|
}
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX) {
|
||||||
|
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
|
if (filename.match(/(404|500)\.tsx?/)) {
|
||||||
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
|
try {
|
||||||
|
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const is_file_of_interest = Boolean(filename.match(target_files_match)) ||
|
||||||
|
file_stat?.isDirectory();
|
||||||
|
if (!is_file_of_interest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!filename.match(/^src\/pages\/|\.css$/))
|
||||||
|
return reloadWatcher();
|
||||||
|
if (checkExcludedPatterns({ path: filename }))
|
||||||
|
return reloadWatcher();
|
||||||
|
if (filename.match(/ /))
|
||||||
|
return reloadWatcher();
|
||||||
|
if (global.BUNEXT_RECOMPILING)
|
||||||
|
return;
|
||||||
|
owns_recompile = true;
|
||||||
|
const action = does_file_exist ? "created" : "deleted";
|
||||||
|
const type = filename.match(/\.css$/)
|
||||||
|
? "Sylesheet"
|
||||||
|
: file_stat?.isDirectory()
|
||||||
|
? "Directory"
|
||||||
|
: filename.match(/\/pages\/api\//)
|
||||||
|
? "API Route"
|
||||||
|
: "Page";
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `${type} ${action}: ${filename}. Rebuilding ...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Watcher rebuild failed: ${error}`);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (owns_recompile) {
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
|
||||||
|
}
|
||||||
|
function reloadWatcher() {
|
||||||
|
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||||
|
watcherEsbuildCTX();
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
export default function watcher(): Promise<void>;
|
|
||||||
Vendored
-83
@@ -1,83 +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";
|
|
||||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
|
||||||
const { ROOT_DIR } = grabDirNames();
|
|
||||||
export default async function watcher() {
|
|
||||||
await Bun.sleep(1000);
|
|
||||||
const pages_src_watcher = watch(ROOT_DIR, {
|
|
||||||
recursive: true,
|
|
||||||
persistent: true,
|
|
||||||
}, async (event, filename) => {
|
|
||||||
if (!filename)
|
|
||||||
return;
|
|
||||||
const full_file_path = path.join(ROOT_DIR, filename);
|
|
||||||
if (full_file_path.match(/\/styles$/)) {
|
|
||||||
global.RECOMPILING = true;
|
|
||||||
await Bun.sleep(1000);
|
|
||||||
await fullRebuild({
|
|
||||||
msg: `Detected new \`styles\` directory. Rebuilding ...`,
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const target_files_match = /\.(tsx?|jsx?|css)$/;
|
|
||||||
if (event !== "rename") {
|
|
||||||
if (filename.match(target_files_match)) {
|
|
||||||
if (global.RECOMPILING)
|
|
||||||
return;
|
|
||||||
global.RECOMPILING = true;
|
|
||||||
await fullRebuild();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const is_file_of_interest = Boolean(filename.match(target_files_match));
|
|
||||||
if (!is_file_of_interest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!filename.match(/^src\/pages\/|\.css$/))
|
|
||||||
return;
|
|
||||||
if (filename.match(/\/(--|\()/))
|
|
||||||
return;
|
|
||||||
if (global.RECOMPILING)
|
|
||||||
return;
|
|
||||||
const action = existsSync(full_file_path) ? "created" : "deleted";
|
|
||||||
const type = filename.match(/\.css$/) ? "Sylesheet" : "Page";
|
|
||||||
await fullRebuild({
|
|
||||||
msg: `${type} ${action}: ${filename}. Rebuilding ...`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
global.PAGES_SRC_WATCHER = pages_src_watcher;
|
|
||||||
}
|
|
||||||
async function fullRebuild(params) {
|
|
||||||
try {
|
|
||||||
const { msg } = params || {};
|
|
||||||
global.RECOMPILING = true;
|
|
||||||
const target_file_paths = global.HMR_CONTROLLERS.map((hmr) => hmr.target_map?.local_path).filter((f) => typeof f == "string");
|
|
||||||
await rewritePagesModule({ page_file_path: target_file_paths });
|
|
||||||
if (msg) {
|
|
||||||
log.watch(msg);
|
|
||||||
}
|
|
||||||
await rebuildBundler({ target_file_paths });
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
global.RECOMPILING = false;
|
|
||||||
}
|
|
||||||
if (global.PAGES_SRC_WATCHER) {
|
|
||||||
global.PAGES_SRC_WATCHER.close();
|
|
||||||
watcher();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
import type { LivePageDistGenParams } from "../../../types";
|
import type { LivePageDistGenParams } from "../../../types";
|
||||||
export default function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }: LivePageDistGenParams): Promise<string>;
|
export default function genWebHTML({ component: Main, pageProps, bundledMap, module, routeParams, debug, root_module, }: LivePageDistGenParams): Promise<string>;
|
||||||
|
|||||||
+81
-33
@@ -1,5 +1,4 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
import { renderToString } from "react-dom/server";
|
|
||||||
import grabContants from "../../../utils/grab-constants";
|
import grabContants from "../../../utils/grab-constants";
|
||||||
import EJSON from "../../../utils/ejson";
|
import EJSON from "../../../utils/ejson";
|
||||||
import isDevelopment from "../../../utils/is-development";
|
import isDevelopment from "../../../utils/is-development";
|
||||||
@@ -7,43 +6,92 @@ import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
|
|||||||
import grabWebMetaHTML from "./grab-web-meta-html";
|
import grabWebMetaHTML from "./grab-web-meta-html";
|
||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
import { AppData } from "../../../data/app-data";
|
import { AppData } from "../../../data/app-data";
|
||||||
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }) {
|
import _ from "lodash";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
export default async function genWebHTML({ component: Main, pageProps, bundledMap, module, routeParams, debug, root_module, }) {
|
||||||
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
||||||
|
const { renderToReadableStream } = await import(`${ROOT_DIR}/node_modules/react-dom/server.js`);
|
||||||
|
const is_dev = isDevelopment();
|
||||||
if (debug) {
|
if (debug) {
|
||||||
log.info("component", component);
|
log.info("component", Main);
|
||||||
}
|
}
|
||||||
const componentHTML = renderToString(component);
|
if (!Main) {
|
||||||
if (debug) {
|
throw new Error(`Main Component not found!`);
|
||||||
log.info("componentHTML", componentHTML);
|
|
||||||
}
|
}
|
||||||
const headHTML = Head
|
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(/<\//g, "<\\/");
|
||||||
? renderToString(_jsx(Head, { serverRes: pageProps, ctx: routeParams }))
|
const page_hydration_script = await grabWebPageHydrationScript();
|
||||||
: "";
|
const root_meta = root_module?.meta
|
||||||
|
? typeof root_module.meta == "function" && routeParams
|
||||||
|
? await root_module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||||
|
: typeof root_module.meta == "function"
|
||||||
|
? undefined
|
||||||
|
: root_module.meta
|
||||||
|
: undefined;
|
||||||
|
const page_meta = module?.meta
|
||||||
|
? typeof module.meta == "function" && routeParams
|
||||||
|
? await module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||||
|
: typeof module.meta == "function"
|
||||||
|
? undefined
|
||||||
|
: module.meta
|
||||||
|
: undefined;
|
||||||
|
const html_props = {
|
||||||
|
...module?.html_props,
|
||||||
|
...root_module?.html_props,
|
||||||
|
};
|
||||||
|
const Head = module?.Head;
|
||||||
|
const RootHead = root_module?.Head;
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const final_meta = _.merge(root_meta, page_meta);
|
||||||
|
const public_envs = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith("BUNEXT_PUBLIC_")));
|
||||||
|
const client_process = {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: dev ? "development" : "production",
|
||||||
|
...public_envs,
|
||||||
|
...global.BUNEXT_CONFIG.public_envs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let final_component = (_jsxs("html", { ...html_props, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8", "data-bunext-head": true }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0", "data-bunext-head": true }), final_meta ? grabWebMetaHTML({ meta: final_meta }) : null, bundledMap?.css_path ? (_jsx("link", { rel: "stylesheet", href: `/${bundledMap.css_path}`, "data-bunext-head": true })) : null, _jsx("script", { dangerouslySetInnerHTML: {
|
||||||
|
__html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${JSON.stringify(client_process)}`,
|
||||||
|
}, "data-bunext-head": true }), RootHead ? (_jsx(RootHead, { serverRes: pageProps, ctx: routeParams })) : null, Head ? _jsx(Head, { serverRes: pageProps, ctx: routeParams }) : null, bundledMap?.path ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "importmap", dangerouslySetInnerHTML: {
|
||||||
|
__html: JSON.stringify(global.BUNEXT_REACT_IMPORTS_MAP),
|
||||||
|
}, defer: true, "data-bunext-head": true }), _jsx("script", { src: `/${bundledMap.path}`, type: "module", id: AppData["BunextClientHydrationScriptID"], defer: true, "data-bunext-head": true })] })) : null, is_dev ? (_jsx("script", { defer: true, dangerouslySetInnerHTML: {
|
||||||
|
__html: page_hydration_script,
|
||||||
|
}, "data-bunext-head": true })) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: _jsx(Main, { ...pageProps }) }) })] }));
|
||||||
let html = `<!DOCTYPE html>\n`;
|
let html = `<!DOCTYPE html>\n`;
|
||||||
html += `<html>\n`;
|
// const stream = await renderToReadableStream(final_component, {
|
||||||
html += ` <head>\n`;
|
// onError(error: any) {
|
||||||
html += ` <meta charset="utf-8" />\n`;
|
// if (error.message.includes('unique "key" prop')) return;
|
||||||
html += ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
|
// console.error(error);
|
||||||
if (meta) {
|
// },
|
||||||
html += ` ${grabWebMetaHTML({ meta })}\n`;
|
// });
|
||||||
|
// const htmlBody = await new Response(stream).text();
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
info: console.info,
|
||||||
|
debug: console.debug,
|
||||||
|
};
|
||||||
|
console.log = () => { };
|
||||||
|
console.warn = () => { };
|
||||||
|
console.error = () => { };
|
||||||
|
console.info = () => { };
|
||||||
|
console.debug = () => { };
|
||||||
|
let htmlBody;
|
||||||
|
try {
|
||||||
|
const stream = await renderToReadableStream(final_component, {
|
||||||
|
onError(error) {
|
||||||
|
if (error.message.includes('unique "key" prop'))
|
||||||
|
return;
|
||||||
|
originalConsole.error(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
htmlBody = await new Response(stream).text();
|
||||||
}
|
}
|
||||||
if (bundledMap?.css_path) {
|
finally {
|
||||||
html += ` <link rel="stylesheet" href="/${bundledMap.css_path}" />\n`;
|
Object.assign(console, originalConsole);
|
||||||
}
|
}
|
||||||
html += ` <script>window.${ClientWindowPagePropsName} = ${EJSON.stringify(pageProps || {}) || "{}"}</script>\n`;
|
html += htmlBody;
|
||||||
if (bundledMap?.path) {
|
|
||||||
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
|
|
||||||
}
|
|
||||||
if (isDevelopment()) {
|
|
||||||
html += `<script defer>\n${await grabWebPageHydrationScript()}\n</script>\n`;
|
|
||||||
}
|
|
||||||
if (headHTML) {
|
|
||||||
html += ` ${headHTML}\n`;
|
|
||||||
}
|
|
||||||
html += ` </head>\n`;
|
|
||||||
html += ` <body>\n`;
|
|
||||||
html += ` <div id="${ClientRootElementIDName}">${componentHTML}</div>\n`;
|
|
||||||
html += ` </body>\n`;
|
|
||||||
html += `</html>\n`;
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { BunextPageModule, BunextPageModuleServerReturn, BunxRouteParams } from "../../../types";
|
||||||
|
type Params = {
|
||||||
|
html: string;
|
||||||
|
module?: BunextPageModule;
|
||||||
|
root_module?: BunextPageModule;
|
||||||
|
routeParams?: BunxRouteParams;
|
||||||
|
serverRes?: BunextPageModuleServerReturn<any, any>;
|
||||||
|
};
|
||||||
|
export default function generateWebPageGetCachePage({ module, routeParams, serverRes, root_module, html, }: Params): Promise<boolean>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
|
import writeCache from "../../cache/write-cache";
|
||||||
|
export default async function generateWebPageGetCachePage({ module, routeParams, serverRes, root_module, html, }) {
|
||||||
|
const config = _.merge(root_module?.config, module?.config);
|
||||||
|
const cache_page = config?.cachePage || serverRes?.cache_page || false;
|
||||||
|
const expiry_seconds = config?.cacheExpiry || serverRes?.cache_expiry;
|
||||||
|
if (cache_page && routeParams?.url) {
|
||||||
|
try {
|
||||||
|
const is_cache = typeof cache_page == "boolean"
|
||||||
|
? cache_page
|
||||||
|
: await cache_page({ ctx: routeParams, serverRes });
|
||||||
|
if (!is_cache) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const key = routeParams.url.pathname + (routeParams.url.search || "");
|
||||||
|
writeCache({
|
||||||
|
key,
|
||||||
|
value: html,
|
||||||
|
paradigm: "html",
|
||||||
|
expiry_seconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Error writing Cache => ${error.message}\n`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
import type { GrabPageComponentRes } from "../../../types";
|
import type { GrabPageComponentRes } from "../../../types";
|
||||||
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }: GrabPageComponentRes): Promise<Response>;
|
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }: GrabPageComponentRes): Promise<Response>;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user