Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d4a79a6dd | ||
|
|
ed788dff97 | ||
|
|
8a981c854a | ||
|
|
af12852fb9 | ||
|
|
1e3e9c4d69 | ||
|
|
c64501cae5 | ||
|
|
5dc783fe97 | ||
|
|
3d0771f2d0 | ||
|
|
7694e80c60 | ||
|
|
40c17f903e | ||
|
|
23a309005a | ||
|
|
3a75880ae4 | ||
|
|
be4bebaa22 | ||
|
|
1169729b06 | ||
|
|
632881e174 | ||
|
|
d9de858439 | ||
|
|
3025b1681d | ||
|
|
eefb2ea5d2 | ||
|
|
74103a19c9 | ||
|
|
4238d9a4ea | ||
|
|
7bab610b38 | ||
|
|
6e6cfef5e5 | ||
|
|
3a4ece5255 | ||
|
|
8fa5b64b48 | ||
|
|
17a5c3df01 | ||
|
|
7e353dbeb3 | ||
|
|
d8ea4f8e02 |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: 1.3.14
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|||||||
@@ -183,3 +183,4 @@ __fixtures__
|
|||||||
/.vscode
|
/.vscode
|
||||||
/source.md
|
/source.md
|
||||||
SECURITY.md
|
SECURITY.md
|
||||||
|
/.tmp
|
||||||
@@ -19,6 +19,7 @@ bun run dev # tsc --watch
|
|||||||
bunext dev # Start dev server with HMR and file watcher
|
bunext dev # Start dev server with HMR and file watcher
|
||||||
bunext build # Bundle all pages for production
|
bunext build # Bundle all pages for production
|
||||||
bunext start # Start production server from pre-built artifacts
|
bunext start # Start production server from pre-built artifacts
|
||||||
|
bunext sitemap # Write public/sitemap.xml and public/robots.txt using config.sitemap / config.robots
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -51,6 +52,7 @@ Pages live in `src/pages/`. Server logic is **separated from page files** into c
|
|||||||
|
|
||||||
**Server file** (`page.server.ts` or `page.server.tsx`) — server-only, never sent to the browser:
|
**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
|
- Default export or `export const server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||||
|
- `export const sitemap`: `BunextPageSitemapFn` — optional; returns the page's sitemap entries (how dynamic routes get into `bunext sitemap`)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ API routes live in `src/pages/api/` and follow standard Bun `Request → Respons
|
|||||||
|
|
||||||
### Bundler artifact tracking
|
### Bundler artifact tracking
|
||||||
`BundlerCTXMap` (stored in `global.BUNDLER_CTX_MAP`) maps each page to its bundled output path, content hash, and entrypoint. In production this is serialized to `public/pages/map.json` and loaded at startup.
|
`BundlerCTXMap` (stored in `global.BUNDLER_CTX_MAP`) maps each page to its bundled output path, content hash, and entrypoint. In production this is serialized to `public/pages/map.json` and loaded at startup.
|
||||||
|
`bunext build` also updates `.bunext/page-lastmod.json` (`src/utils/page-lastmod-record.ts`): per page, a fingerprint of the bundle hash + CSS path and the build date it last changed. The sitemap uses these dates as `lastmod`.
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
Consumer projects define `bunext.config.ts` at their project root. The `BunextConfig` type fields: `distDir`, `assetsPrefix`, `origin`, `globalVars`, `port`, `development`.
|
Consumer projects define `bunext.config.ts` at their project root. The `BunextConfig` type fields: `distDir`, `assetsPrefix`, `origin`, `globalVars`, `port`, `development`.
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ The goal is a framework that is:
|
|||||||
- [API Routes](#api-routes)
|
- [API Routes](#api-routes)
|
||||||
- [Route Config (Body Size Limit)](#route-config-body-size-limit)
|
- [Route Config (Body Size Limit)](#route-config-body-size-limit)
|
||||||
- [Error Pages](#error-pages)
|
- [Error Pages](#error-pages)
|
||||||
|
- [Sitemap](#sitemap)
|
||||||
|
- [Page `sitemap` Export](#page-sitemap-export)
|
||||||
|
- [`bunext sitemap` Command](#bunext-sitemap-command)
|
||||||
|
- [Last-Modified Dates](#last-modified-dates)
|
||||||
|
- [Large Sitemaps](#large-sitemaps)
|
||||||
|
- [Generating at Runtime](#generating-at-runtime)
|
||||||
|
- [robots.txt](#robotstxt)
|
||||||
- [Static Files](#static-files)
|
- [Static Files](#static-files)
|
||||||
- [Caching](#caching)
|
- [Caching](#caching)
|
||||||
- [Enabling Cache Per Page](#enabling-cache-per-page)
|
- [Enabling Cache Per Page](#enabling-cache-per-page)
|
||||||
@@ -155,6 +162,7 @@ bun run dev
|
|||||||
| `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 `.bunext/public/pages/` using paths relative to the project root. |
|
| `bunext build` | Bundle all pages for production. Outputs artifacts to `.bunext/public/pages/` using paths relative to the project root. |
|
||||||
| `bunext start` | Start the production server using pre-built artifacts. |
|
| `bunext start` | Start the production server using pre-built artifacts. |
|
||||||
|
| `bunext sitemap` | Generate `public/sitemap.xml` and `public/robots.txt` from your pages, using the `sitemap` and `robots` options in `bunext.config.ts`. See [Sitemap](#sitemap). |
|
||||||
|
|
||||||
### Running the CLI
|
### Running the CLI
|
||||||
|
|
||||||
@@ -325,6 +333,8 @@ export default function ProfilePage({ props, url }: Props) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Server files can also export a `sitemap` function to list a page's URLs in the sitemap — see [Page `sitemap` Export](#page-sitemap-export).
|
||||||
|
|
||||||
> **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.
|
> **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:
|
||||||
@@ -624,6 +634,220 @@ If no custom error pages exist, Bunext uses built-in preset components.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sitemap
|
||||||
|
|
||||||
|
Bunext builds a sitemap from every static page in `src/pages/`. API routes, `404`/`500` pages and ignored directories are left out. Dynamic routes (`[slug]`) cannot be discovered from the file system, so they are only included when their server file exports a `sitemap` function (see below), or when you pass their URLs through `additional_paths`.
|
||||||
|
|
||||||
|
### Page `sitemap` Export
|
||||||
|
|
||||||
|
A page's `.server.ts` file can export a `sitemap` function returning that page's entries. This is the way to list URLs for a dynamic route:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/pages/blog/[slug].server.ts
|
||||||
|
import type { BunextPageServerFn, BunextPageSitemapFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn = async (ctx) => {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
|
|
||||||
|
export const sitemap: BunextPageSitemapFn = async ({ route }) => {
|
||||||
|
// route === "/blog/[slug]"
|
||||||
|
const posts = await getPublishedPosts();
|
||||||
|
|
||||||
|
return posts.map((post) => ({
|
||||||
|
path: `/blog/${post.slug}`,
|
||||||
|
lastmod: post.updated_at,
|
||||||
|
priority: 0.7,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- It can be sync or async, and returns strings or `BunextSitemapEntry` objects, like `additional_paths`.
|
||||||
|
- On a static page, the returned entries **replace** the page's default entry, so you can set its `priority`/`changefreq`, or return `[]` to leave the page out.
|
||||||
|
- `exclude` and the `default_*` options still apply to the returned entries.
|
||||||
|
- Generating a sitemap imports every page's server file, so their top-level code runs. If a `sitemap` function throws, generation fails with the file name in the error.
|
||||||
|
|
||||||
|
### `bunext sitemap` Command
|
||||||
|
|
||||||
|
Add a `sitemap` key to `bunext.config.ts`, then run `bunext sitemap`. By default the file is written to `public/sitemap.xml`, which is served at `/sitemap.xml`. Run it after `bunext build` in your deploy step to keep it current.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
import type { BunextConfig } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const config: BunextConfig = {
|
||||||
|
origin: "https://example.com",
|
||||||
|
sitemap: {
|
||||||
|
exclude: ["/login", "/admin/*"],
|
||||||
|
default_changefreq: "weekly",
|
||||||
|
// A function (sync or async) lets you load dynamic routes
|
||||||
|
additional_paths: async () => {
|
||||||
|
const posts = await getPublishedPosts();
|
||||||
|
return posts.map((post) => ({
|
||||||
|
path: `/blog/${post.slug}`,
|
||||||
|
lastmod: post.updated_at,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bunext sitemap # writes public/sitemap.xml and public/robots.txt
|
||||||
|
bunext sitemap -o public/sitemaps/main.xml # custom sitemap output path
|
||||||
|
bunext sitemap --origin https://staging.example.com
|
||||||
|
bunext sitemap --no-robots # sitemap only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Last-Modified Dates
|
||||||
|
|
||||||
|
`lastmod` comes from the build pipeline. On every `bunext build`, Bunext compares each page's client bundle hash with the previous build and records the build date for pages whose bundle changed in `.bunext/page-lastmod.json`. The hash covers the page's whole import graph, so editing a shared component re-dates every page that uses it, while untouched pages keep their date. Editing `__root.tsx` changes every page.
|
||||||
|
|
||||||
|
Each URL's `lastmod` is the first of:
|
||||||
|
|
||||||
|
1. The entry's own `lastmod` (from `additional_paths` or a page's `sitemap` export)
|
||||||
|
2. The page's date from the build record (also used for entries returned by that page's `sitemap` export)
|
||||||
|
3. The page file's modification time, if `use_file_lastmod: true`
|
||||||
|
4. `default_lastmod`
|
||||||
|
|
||||||
|
With no date, `<lastmod>` is left out. Run `bunext build` before `bunext sitemap`; the command warns when there is no record.
|
||||||
|
|
||||||
|
- **The record must persist between builds.** Without the previous record, every page is dated with the current build. Building on the server you deploy to keeps `.bunext/` between builds. For CI builds from a fresh checkout, set `sitemap.lastmod_file` to a path you cache between runs or commit (it is sorted JSON for small diffs).
|
||||||
|
- **Server data is not covered.** Content loaded in a server function (e.g. from a database) does not change the bundle, so set `lastmod` on those entries in the page's `sitemap` export.
|
||||||
|
- Upgrading esbuild or changing build options can change every hash once, re-dating all pages.
|
||||||
|
|
||||||
|
#### Large Sitemaps
|
||||||
|
|
||||||
|
A sitemap file may hold at most 50,000 URLs and 50MB. When the sitemap has more than `chunk_size` URLs (default 10,000), `bunext sitemap` splits it into chunk files and makes `sitemap.xml` a sitemap index pointing to them:
|
||||||
|
|
||||||
|
```
|
||||||
|
public/
|
||||||
|
├── sitemap.xml ← <sitemapindex> referencing the chunks
|
||||||
|
└── sitemap-chunks/
|
||||||
|
├── sitemap-1.xml ← up to chunk_size URLs each
|
||||||
|
├── sitemap-2.xml
|
||||||
|
└── sitemap-3.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Each index entry's `lastmod` is the newest `lastmod` in its chunk. Chunk files left over from a previous, larger run are removed; other files in the chunks directory are left alone. If the sitemap shrinks back under `chunk_size`, `sitemap.xml` returns to a plain `<urlset>`.
|
||||||
|
|
||||||
|
| Config option | Type | Description |
|
||||||
|
| ------------- | -------- | --------------------------------------------------------------------------------------------------- |
|
||||||
|
| `chunk_size` | `number` | Maximum URLs per file. Default `10000`, capped at `50000`. Chunks are also split at 50MB. |
|
||||||
|
| `chunks_dir` | `string` | Chunk directory relative to the project root. Must be inside `public/`. Default `public/sitemap-chunks`. |
|
||||||
|
|
||||||
|
`--output` and `--origin` override `sitemap.output` and `sitemap.origin` from the config. `--origin` also applies to robots.txt. The command also writes a `robots.txt` — see [robots.txt](#robotstxt).
|
||||||
|
|
||||||
|
### Generating at Runtime
|
||||||
|
|
||||||
|
`bunextGenerateSitemap()` resolves to the same XML as a single `<urlset>` (it does not chunk, and logs a warning above 50,000 URLs — use `bunext sitemap` for large sites), so you can serve it from a route instead of a file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/pages/api/sitemap.ts
|
||||||
|
import bunext from "@moduletrace/bunext";
|
||||||
|
|
||||||
|
export default async function handler(req: Request) {
|
||||||
|
const xml = await bunext.bunextGenerateSitemap({
|
||||||
|
exclude: ["/login", "/admin/*", /^\/drafts/],
|
||||||
|
additional_paths: async () => {
|
||||||
|
const posts = await getPublishedPosts();
|
||||||
|
return posts.map((post) => ({
|
||||||
|
path: `/blog/${post.slug}`,
|
||||||
|
lastmod: post.updated_at,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
default_changefreq: "weekly",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(xml, {
|
||||||
|
headers: { "Content-Type": "application/xml" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
| -------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `origin` | `string` | Base URL for each `<loc>`. Defaults to `config.origin`, then `http://localhost:<port>`. |
|
||||||
|
| `exclude` | `(string \| RegExp)[]` | Exact paths, subtrees ending in `/*` (`/admin/*` also excludes `/admin`), or regexes tested on the path. |
|
||||||
|
| `additional_paths` | `entries \| () => entries \| Promise<entries>` | Extra paths or entries (`path`, `lastmod`, `changefreq`, `priority`), or a sync/async function returning them, e.g. for dynamic routes. Each entry is a `string` or `BunextSitemapEntry`. |
|
||||||
|
| `default_lastmod` | `Date \| string` | `lastmod` for entries with no other date. See [Last-Modified Dates](#last-modified-dates). |
|
||||||
|
| `lastmod_file` | `string` | Page date record written by `bunext build`. Default `.bunext/page-lastmod.json`. |
|
||||||
|
| `use_file_lastmod` | `boolean` | Fall back to each page file's modification time. Default `false`. |
|
||||||
|
| `trailing_slash` | `boolean` | Append `/` to every non-root path. Default `false`. |
|
||||||
|
| `default_changefreq` | `BunextSitemapChangeFreq` | `changefreq` for entries that do not set one. |
|
||||||
|
| `default_priority` | `number` | `priority` (0.0 to 1.0) for entries that do not set one. |
|
||||||
|
|
||||||
|
The `sitemap` config key takes the same options, plus `output` (the file path relative to the project root, default `public/sitemap.xml`) and the [chunking options](#large-sitemaps).
|
||||||
|
|
||||||
|
### robots.txt
|
||||||
|
|
||||||
|
`bunext sitemap` also writes `public/robots.txt`, served at `/robots.txt`. With no `robots` config it allows every crawler and points to the generated sitemap:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Generated by bunext
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://example.com/sitemap.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure it with the `robots` key:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
const config: BunextConfig = {
|
||||||
|
origin: "https://example.com",
|
||||||
|
robots: {
|
||||||
|
rules: [
|
||||||
|
{ user_agent: "*", disallow: ["/admin", "/api/"] },
|
||||||
|
{ user_agent: ["GPTBot", "CCBot"], disallow: ["/"] },
|
||||||
|
],
|
||||||
|
// Block everything on staging
|
||||||
|
disallow_all: process.env.STAGING === "true",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
| -------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `rules` | `BunextRobotsRule[]` | Groups of `user_agent` (string or array), `allow`, `disallow` and `crawl_delay`. Default: allow everything for `*`. |
|
||||||
|
| `sitemaps` | `string[]` | Extra `Sitemap:` URLs or paths. The generated sitemap is added automatically when it is inside `public/`. |
|
||||||
|
| `disallow_all` | `boolean` | Block every crawler from the whole site. `rules` and `sitemaps` are ignored. |
|
||||||
|
| `origin` | `string` | Base URL for relative `sitemaps` paths. Defaults to `--origin`, then `sitemap.origin`, then `config.origin`. |
|
||||||
|
| `output` | `string` | Output file relative to the project root. Default `public/robots.txt`. |
|
||||||
|
|
||||||
|
- **Existing files are protected.** Generated files start with `# Generated by bunext`. Without a `robots` config, a `robots.txt` that lacks this line (one you wrote yourself) is left alone and a warning is shown. Setting `robots` in the config overwrites it.
|
||||||
|
- **Opting out.** Set `robots: false`, or pass `--no-robots`, to skip robots.txt entirely.
|
||||||
|
- **Sitemap `exclude` does not add `Disallow` lines.** Leaving a page out of the sitemap and blocking crawlers from it are different things; add `disallow` rules explicitly. Remember robots.txt is public, so `Disallow` paths are visible to anyone.
|
||||||
|
|
||||||
|
To serve it from a route instead, `bunext.bunextGenerateRobotsTxt()` takes the same options except `output` and returns the text.
|
||||||
|
|
||||||
|
### Disabling SEO
|
||||||
|
|
||||||
|
For preview or test deployments that must never be indexed, set `disable_seo`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
const config: BunextConfig = {
|
||||||
|
disable_seo: process.env.PREVIEW === "true",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
With it on, the server:
|
||||||
|
|
||||||
|
- adds `X-Robots-Tag: noindex, nofollow` to every response (pages, API routes and files),
|
||||||
|
- renders `<meta name="robots" content="noindex, nofollow">` on every page, replacing any `meta.robots`,
|
||||||
|
- answers `/robots.txt` with `Disallow: /` for all crawlers, ignoring `public/robots.txt`.
|
||||||
|
|
||||||
|
Nothing is written to disk, so the same build can be deployed to production with the option off. Responses returned directly by [middleware](#middleware) are left untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Static Files
|
## Static Files
|
||||||
|
|
||||||
Place static files in the `public/` directory. They are served at the `/public/` URL path:
|
Place static files in the `public/` directory. They are served at the `/public/` URL path:
|
||||||
@@ -764,6 +988,10 @@ export default config;
|
|||||||
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||||
| `websocket` | `WebSocketHandler<any>` | — | Bun WebSocket handler — see [WebSocket](#websocket) |
|
| `websocket` | `WebSocketHandler<any>` | — | Bun WebSocket handler — see [WebSocket](#websocket) |
|
||||||
| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) |
|
| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) |
|
||||||
|
| `sitemap` | `BunextSitemapConfig` | — | Options for `bunext sitemap` — see [Sitemap](#sitemap) |
|
||||||
|
| `robots` | `BunextRobotsConfig \| false` | — | robots.txt options for `bunext sitemap`, or `false` to skip it — see [robots.txt](#robotstxt) |
|
||||||
|
| `disable_seo` | `boolean` | `false` | Keep the whole site out of search engines (e.g. preview deployments) — see [Disabling SEO](#disabling-seo) |
|
||||||
|
| `minify_ssr_bundle` | `boolean` | `true` | Minify the SSR bundle. SSR output is served from local disk, not downloaded by browsers, so set `false` to cut build memory on constrained hosts. Never minified in dev |
|
||||||
|
|
||||||
### Middleware
|
### Middleware
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,32 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "bun-next",
|
"name": "bun-next",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "1.3.14",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.3.0",
|
||||||
"bun-plugin-tailwind": "^0.1.2",
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.28.2",
|
||||||
"lightningcss-wasm": "^1.32.0",
|
"lightningcss-wasm": "^1.32.0",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.18.1",
|
||||||
"micromatch": "^4.0.8",
|
|
||||||
"ora": "^9.0.0",
|
"ora": "^9.0.0",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.30",
|
||||||
"react": "^19.2.4",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.3.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"uuid": "^14.0.2",
|
"uuid": "^14.0.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/chokidar": "^2.1.7",
|
"@types/chokidar": "^2.1.7",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.26",
|
||||||
"@types/micromatch": "^4.0.10",
|
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"happy-dom": "^20.8.4",
|
"happy-dom": "^20.14.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -45,57 +43,57 @@
|
|||||||
|
|
||||||
"@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
"@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]7.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
|
"@esbuild/aix-ppc64": ["@esbuild/[email protected]8.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
|
"@esbuild/android-arm": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
|
"@esbuild/android-arm64": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
|
"@esbuild/android-x64": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/[email protected]7.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
|
"@esbuild/darwin-arm64": ["@esbuild/[email protected]8.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/[email protected]7.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
|
"@esbuild/darwin-x64": ["@esbuild/[email protected]8.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
|
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
|
"@esbuild/freebsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
|
"@esbuild/linux-arm": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
|
"@esbuild/linux-arm64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
|
"@esbuild/linux-ia32": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
|
"@esbuild/linux-loong64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
|
"@esbuild/linux-mips64el": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
|
"@esbuild/linux-ppc64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
|
"@esbuild/linux-riscv64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
|
"@esbuild/linux-s390x": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
|
"@esbuild/linux-x64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
|
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
|
"@esbuild/netbsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
|
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
|
"@esbuild/openbsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
|
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/[email protected]7.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
|
"@esbuild/sunos-x64": ["@esbuild/[email protected]8.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
|
"@esbuild/win32-arm64": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
|
"@esbuild/win32-ia32": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
|
"@esbuild/win32-x64": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
@@ -131,55 +129,51 @@
|
|||||||
|
|
||||||
"@oven/bun-windows-x64-baseline": ["@oven/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-gh3UAHbUdDUG6fhLc1Csa4IGdtghue6U8oAIXWnUqawp6lwb3gOCRvp25IUnLF5vUHtgfMxuEUYV7YA2WxVutw=="],
|
"@oven/bun-windows-x64-baseline": ["@oven/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-gh3UAHbUdDUG6fhLc1Csa4IGdtghue6U8oAIXWnUqawp6lwb3gOCRvp25IUnLF5vUHtgfMxuEUYV7YA2WxVutw=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="],
|
||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
"@testing-library/dom": ["@testing-library/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||||
|
|
||||||
"@types/aria-query": ["@types/[email protected]", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
"@types/aria-query": ["@types/[email protected]", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||||
|
|
||||||
"@types/braces": ["@types/b[email protected]", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/bun": ["@types/b[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
|
||||||
|
|
||||||
"@types/chokidar": ["@types/[email protected]", "", { "dependencies": { "chokidar": "*" } }, "sha512-A7/MFHf6KF7peCzjEC1BBTF8jpmZTokb3vr/A0NxRGfwRLK3Ws+Hq6ugVn6cJIMfM6wkCak/aplWrxbTcu8oig=="],
|
"@types/chokidar": ["@types/[email protected]", "", { "dependencies": { "chokidar": "*" } }, "sha512-A7/MFHf6KF7peCzjEC1BBTF8jpmZTokb3vr/A0NxRGfwRLK3Ws+Hq6ugVn6cJIMfM6wkCak/aplWrxbTcu8oig=="],
|
||||||
|
|
||||||
"@types/lodash": ["@types/[email protected]4", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
"@types/lodash": ["@types/[email protected]6", "", {}, "sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA=="],
|
||||||
|
|
||||||
"@types/micromatch": ["@types/[email protected]", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
|
||||||
|
|
||||||
"@types/uuid": ["@types/[email protected]", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
"@types/uuid": ["@types/[email protected]", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
||||||
|
|
||||||
@@ -193,13 +187,13 @@
|
|||||||
|
|
||||||
"aria-query": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
"aria-query": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||||
|
|
||||||
"braces": ["[email protected]", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"buffer-image-size": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
||||||
|
|
||||||
"bun": ["[email protected]", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.10", "@oven/bun-darwin-x64": "1.3.10", "@oven/bun-darwin-x64-baseline": "1.3.10", "@oven/bun-linux-aarch64": "1.3.10", "@oven/bun-linux-aarch64-musl": "1.3.10", "@oven/bun-linux-x64": "1.3.10", "@oven/bun-linux-x64-baseline": "1.3.10", "@oven/bun-linux-x64-musl": "1.3.10", "@oven/bun-linux-x64-musl-baseline": "1.3.10", "@oven/bun-windows-aarch64": "1.3.10", "@oven/bun-windows-x64": "1.3.10", "@oven/bun-windows-x64-baseline": "1.3.10" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA=="],
|
"bun": ["[email protected]", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.10", "@oven/bun-darwin-x64": "1.3.10", "@oven/bun-darwin-x64-baseline": "1.3.10", "@oven/bun-linux-aarch64": "1.3.10", "@oven/bun-linux-aarch64-musl": "1.3.10", "@oven/bun-linux-x64": "1.3.10", "@oven/bun-linux-x64-baseline": "1.3.10", "@oven/bun-linux-x64-musl": "1.3.10", "@oven/bun-linux-x64-musl-baseline": "1.3.10", "@oven/bun-windows-aarch64": "1.3.10", "@oven/bun-windows-x64": "1.3.10", "@oven/bun-windows-x64-baseline": "1.3.10" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA=="],
|
||||||
|
|
||||||
"bun-plugin-tailwind": ["[email protected]", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
"bun-plugin-tailwind": ["[email protected]", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||||
|
|
||||||
"bun-types": ["[email protected]1", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
"bun-types": ["[email protected]4", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
"chalk": ["[email protected]", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["[email protected]", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
@@ -219,27 +213,23 @@
|
|||||||
|
|
||||||
"dom-accessibility-api": ["[email protected]", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
"dom-accessibility-api": ["[email protected]", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["[email protected]0.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
"enhanced-resolve": ["[email protected]6.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="],
|
||||||
|
|
||||||
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"esbuild": ["[email protected]7.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
"esbuild": ["[email protected]8.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||||
|
|
||||||
"fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
|
||||||
|
|
||||||
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||||
|
|
||||||
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.8.4", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ=="],
|
"happy-dom": ["happy-dom@20.14.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-5DDsf4QmEIlRL6NkCv/fxSjYHHkZceL/IaYQdJc2h3G7DRTEai++VTjbfL0LB9geW2VrA3+vrjF3Efx+OTtX0Q=="],
|
||||||
|
|
||||||
"is-interactive": ["[email protected]", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
"is-interactive": ["[email protected]", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||||
|
|
||||||
"is-number": ["[email protected]", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
|
||||||
|
|
||||||
"is-unicode-supported": ["[email protected]", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
"is-unicode-supported": ["[email protected]", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
@@ -269,7 +259,7 @@
|
|||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
"lodash": ["[email protected]7.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
"lodash": ["[email protected]8.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||||
|
|
||||||
"log-symbols": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
"log-symbols": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||||
|
|
||||||
@@ -277,11 +267,9 @@
|
|||||||
|
|
||||||
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
"micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
|
||||||
|
|
||||||
"mimic-function": ["[email protected]", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
"mimic-function": ["[email protected]", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
"nanoid": ["[email protected].11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["[email protected].20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="],
|
||||||
|
|
||||||
"onetime": ["[email protected]", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"onetime": ["[email protected]", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
@@ -289,15 +277,13 @@
|
|||||||
|
|
||||||
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA=="],
|
||||||
|
|
||||||
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
|
||||||
|
|
||||||
"pretty-format": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["[email protected]", "", { "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": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||||
|
|
||||||
"react-is": ["[email protected]", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"react-is": ["[email protected]", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
@@ -305,11 +291,11 @@
|
|||||||
|
|
||||||
"restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
"restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||||
|
|
||||||
"scheduler": ["[email protected]7.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["[email protected]8.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||||
|
|
||||||
"signal-exit": ["[email protected]", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["[email protected]", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"source-map-js": ["[email protected].1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["[email protected].2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
|
||||||
|
|
||||||
"stdin-discarder": ["[email protected]", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
"stdin-discarder": ["[email protected]", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||||
|
|
||||||
@@ -317,11 +303,9 @@
|
|||||||
|
|
||||||
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
||||||
"tapable": ["[email protected].0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["[email protected].3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
"to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
|
||||||
|
|
||||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||||
|
|
||||||
@@ -331,19 +315,19 @@
|
|||||||
|
|
||||||
"whatwg-mimetype": ["[email protected]", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"whatwg-mimetype": ["[email protected]", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||||
|
|
||||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
"ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="],
|
||||||
|
|
||||||
"yoctocolors": ["[email protected]", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
"yoctocolors": ["[email protected]", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected].0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected].3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected].1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected].4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
|||||||
Vendored
+21
-2
@@ -3,8 +3,9 @@ import { log } from "../../utils/log";
|
|||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import { rmSync, writeFileSync } from "fs";
|
import { rmSync, writeFileSync } from "fs";
|
||||||
import bunextInit from "../../functions/bunext-init";
|
import bunextInit from "../../functions/bunext-init";
|
||||||
import pagesSSRBundler from "../../functions/bundler/pages-ssr-bundler";
|
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
import { updatePageLastmodRecord } from "../../utils/page-lastmod-record";
|
||||||
|
import disposeBundlerContexts from "../../functions/bundler/dispose-bundler-contexts";
|
||||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_CWD_MODULE_CACHE_DIR, BUILD_INFO_JSON, } = grabDirNames();
|
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_CWD_MODULE_CACHE_DIR, BUILD_INFO_JSON, } = grabDirNames();
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("build")
|
return new Command("build")
|
||||||
@@ -21,7 +22,13 @@ export default function () {
|
|||||||
catch (error) { }
|
catch (error) { }
|
||||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||||
await bunextInit({ build_only: true });
|
await bunextInit({ build_only: true });
|
||||||
// await pagesSSRBundler();
|
await disposeBundlerContexts({ stop_service: true });
|
||||||
|
// A failed bundle must fail the command: CI and scripts rely on
|
||||||
|
// the exit code instead of grepping logs.
|
||||||
|
if (global.BUNEXT_BUILD_FAILED) {
|
||||||
|
log.error("Build failed with errors. Exiting.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
let bundler_ctx_json_str = JSON.stringify(global.BUNEXT_BUNDLER_CTX_MAP);
|
let bundler_ctx_json_str = JSON.stringify(global.BUNEXT_BUNDLER_CTX_MAP);
|
||||||
writeFileSync(BUNDLER_CTX_MAP_JSON, bundler_ctx_json_str, "utf-8");
|
writeFileSync(BUNDLER_CTX_MAP_JSON, bundler_ctx_json_str, "utf-8");
|
||||||
@@ -35,6 +42,18 @@ export default function () {
|
|||||||
build_date_unix: Date.now(),
|
build_date_unix: Date.now(),
|
||||||
build_date: Date(),
|
build_date: Date(),
|
||||||
};
|
};
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
|
try {
|
||||||
|
updatePageLastmodRecord({
|
||||||
|
ctx_map: global.BUNEXT_BUNDLER_CTX_MAP,
|
||||||
|
build_date: new Date(build_data.build_date_unix),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Only sitemap dates depend on this; never fail the build.
|
||||||
|
log.warn(`Could not update page lastmod record: ${error?.message || error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
writeFileSync(BUILD_INFO_JSON, JSON.stringify(build_data, null, 4), "utf-8");
|
writeFileSync(BUILD_INFO_JSON, JSON.stringify(build_data, null, 4), "utf-8");
|
||||||
log.success("Modules Built Successfully!");
|
log.success("Modules Built Successfully!");
|
||||||
process.exit();
|
process.exit();
|
||||||
|
|||||||
Vendored
+14
@@ -2,13 +2,23 @@ import startServer from "../../functions/server/start-server";
|
|||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
import bunextInit from "../../functions/bunext-init";
|
import bunextInit from "../../functions/bunext-init";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
import { rmSync } from "fs";
|
import { rmSync } from "fs";
|
||||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||||
|
// The supervisor only restarts the dev server when this process actually
|
||||||
|
// exits with a code. Registering a listener that merely logs (the previous
|
||||||
|
// behavior) keeps Bun from exiting: the crash is swallowed, no error file is
|
||||||
|
// written, and the supervisor never restarts the server. Log, record the real
|
||||||
|
// error, and exit non-zero so recovery actually runs.
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
log.error(`Uncaught exception: ${error}`);
|
log.error(`Uncaught exception: ${error}`);
|
||||||
|
writeErrorFile({ error, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
process.on("unhandledRejection", (reason) => {
|
process.on("unhandledRejection", (reason) => {
|
||||||
log.error(`Unhandled rejection: ${reason}`);
|
log.error(`Unhandled rejection: ${reason}`);
|
||||||
|
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
log.info("Running development server ...");
|
log.info("Running development server ...");
|
||||||
try {
|
try {
|
||||||
@@ -21,5 +31,9 @@ try {
|
|||||||
await startServer();
|
await startServer();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
// Startup failures (e.g. port in use, config errors) must exit non-zero;
|
||||||
|
// otherwise the process hangs alive with no server and no restart.
|
||||||
log.error(`Failed to start development server: ${error}`);
|
log.error(`Failed to start development server: ${error}`);
|
||||||
|
writeErrorFile({ error, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+51
-46
@@ -3,10 +3,9 @@ import path from "path";
|
|||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import writeErrorFile from "../../functions/write-error-file";
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
import { existsSync, rmSync } from "fs";
|
import { existsSync, rmSync } from "fs";
|
||||||
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = grabDirNames();
|
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
||||||
let retries = 0;
|
|
||||||
let timeout;
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
|
const RETRY_RESET_AFTER_MS = 10000;
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("dev")
|
return new Command("dev")
|
||||||
.description("Run development server")
|
.description("Run development server")
|
||||||
@@ -14,54 +13,60 @@ export default function () {
|
|||||||
try {
|
try {
|
||||||
rmSync(SSR_BUNDLER_CTX_MAP_JSON, { recursive: true });
|
rmSync(SSR_BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||||
rmSync(BUNDLER_CTX_MAP_JSON, { recursive: true });
|
rmSync(BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||||
|
// A stale error file from a previous session makes the bundler
|
||||||
|
// run in "silent" mode and pages fail with misleading
|
||||||
|
// "Application Error" messages until the first rebuild.
|
||||||
|
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||||
}
|
}
|
||||||
catch (error) { }
|
catch (error) { }
|
||||||
await dev();
|
await dev();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function dev() {
|
async function dev() {
|
||||||
clearTimeout(timeout);
|
// A loop, not recursion: each restart would otherwise keep the previous
|
||||||
if (retries >= MAX_RETRIES) {
|
// call (and its subprocess) alive for the supervisor's whole lifetime.
|
||||||
console.error(`Dev server crashed ${MAX_RETRIES} times. Exiting.`);
|
let retries = 0;
|
||||||
process.exit(1);
|
while (true) {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const started_at = Date.now();
|
||||||
|
let child_process;
|
||||||
|
try {
|
||||||
|
child_process = Bun.spawn(spawn_options);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Failed to start dev process:`, error);
|
||||||
|
retries++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const exited = await child_process.exited;
|
||||||
|
// A null exit means the child was killed by a signal (e.g. Ctrl+C
|
||||||
|
// in the shared terminal), which we must not restart.
|
||||||
|
if (exited == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Restart on any numeric exit (0 included: the server should never
|
||||||
|
// exit cleanly). Only consecutive quick crashes count toward the
|
||||||
|
// limit; a child that ran for a while starts the count over.
|
||||||
|
retries =
|
||||||
|
Date.now() - started_at >= RETRY_RESET_AFTER_MS ? 1 : retries + 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
+2
@@ -3,6 +3,7 @@ import { program } from "commander";
|
|||||||
import start from "./start";
|
import start from "./start";
|
||||||
import dev from "./dev";
|
import dev from "./dev";
|
||||||
import build from "./build";
|
import build from "./build";
|
||||||
|
import sitemap from "./sitemap";
|
||||||
import { log } from "../utils/log";
|
import { log } from "../utils/log";
|
||||||
/**
|
/**
|
||||||
* # Describe Program
|
* # Describe Program
|
||||||
@@ -17,6 +18,7 @@ program
|
|||||||
program.addCommand(dev());
|
program.addCommand(dev());
|
||||||
program.addCommand(start());
|
program.addCommand(start());
|
||||||
program.addCommand(build());
|
program.addCommand(build());
|
||||||
|
program.addCommand(sitemap());
|
||||||
/**
|
/**
|
||||||
* # Handle Unavailable Commands
|
* # Handle Unavailable Commands
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
Vendored
+149
@@ -0,0 +1,149 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import grabConfig from "../../functions/grab-config";
|
||||||
|
import grabConstants from "../../utils/grab-constants";
|
||||||
|
import { generateSitemapFiles, grabPendingSitemapTask, } from "../../utils/generate-sitemap";
|
||||||
|
import { grabPageLastmodFile } from "../../utils/page-lastmod-record";
|
||||||
|
import generateRobotsTxt, { RobotsTxtGeneratedMarker, } from "../../utils/generate-robots-txt";
|
||||||
|
const SitemapStillWaitingInterval = 30_000;
|
||||||
|
export default function () {
|
||||||
|
return new Command("sitemap")
|
||||||
|
.description("Generate sitemap.xml and robots.txt from the pages directory")
|
||||||
|
.option("-o, --output <path>", "Output file relative to the project root (default: public/sitemap.xml)")
|
||||||
|
.option("--origin <url>", "Base URL for sitemap links")
|
||||||
|
.option("--no-robots", "Skip generating robots.txt")
|
||||||
|
.action(async (options) => {
|
||||||
|
const { ROOT_DIR, PUBLIC_DIR } = grabDirNames();
|
||||||
|
global.BUNEXT_CONFIG = (await grabConfig()) || {};
|
||||||
|
global.BUNEXT_CONSTANTS = grabConstants();
|
||||||
|
if (global.BUNEXT_CONFIG.disable_seo) {
|
||||||
|
log.warn(`\`disable_seo\` is set: the server blocks all crawlers at ` +
|
||||||
|
`/robots.txt, so the generated robots.txt is not served.`);
|
||||||
|
}
|
||||||
|
const { output, chunk_size, chunks_dir, ...sitemap_config } = global.BUNEXT_CONFIG.sitemap || {};
|
||||||
|
if (!existsSync(grabPageLastmodFile(sitemap_config.lastmod_file))) {
|
||||||
|
log.warn(`No page lastmod record found. Run \`bunext build\` first ` +
|
||||||
|
`so pages get lastmod dates.`);
|
||||||
|
}
|
||||||
|
let sitemap_file;
|
||||||
|
try {
|
||||||
|
const output_path = options.output || output;
|
||||||
|
sitemap_file = output_path
|
||||||
|
? path.resolve(ROOT_DIR, output_path)
|
||||||
|
: path.join(PUBLIC_DIR, "sitemap.xml");
|
||||||
|
const chunks_dir_path = path.resolve(ROOT_DIR, chunks_dir || "public/sitemap-chunks");
|
||||||
|
const chunks_public_path = grabPublicUrlPath(chunks_dir_path);
|
||||||
|
if (chunks_public_path === undefined) {
|
||||||
|
throw new Error(`\`sitemap.chunks_dir\` must be inside public/ ` +
|
||||||
|
`so the chunks are served.`);
|
||||||
|
}
|
||||||
|
// Bun exits silently (code 0) when the event loop empties
|
||||||
|
// while a promise is pending. Bun's SQL client (mariadb /
|
||||||
|
// mysql adapters, Bun 1.3.14) doesn't keep the loop alive
|
||||||
|
// for a query on a connection that has gone idle, so a
|
||||||
|
// second `sitemap` export querying the DB would end the
|
||||||
|
// process mid-generation. Hold the loop open with a timer,
|
||||||
|
// which also reports what a genuinely stuck run waits on.
|
||||||
|
const keep_alive = setInterval(() => {
|
||||||
|
log.warn(`Still waiting on ` +
|
||||||
|
`${grabPendingSitemapTask() || "sitemap generation"}...`);
|
||||||
|
}, SitemapStillWaitingInterval);
|
||||||
|
const { sitemap, chunks, url_count } = await generateSitemapFiles({
|
||||||
|
...sitemap_config,
|
||||||
|
origin: options.origin || sitemap_config.origin,
|
||||||
|
chunk_size,
|
||||||
|
chunks_url_path: chunks_public_path,
|
||||||
|
}).finally(() => clearInterval(keep_alive));
|
||||||
|
// Remove chunks from a previous, larger sitemap
|
||||||
|
if (existsSync(chunks_dir_path)) {
|
||||||
|
for (const file of readdirSync(chunks_dir_path)) {
|
||||||
|
if (file.match(/^sitemap-\d+\.xml$/)) {
|
||||||
|
rmSync(path.join(chunks_dir_path, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunks.length) {
|
||||||
|
mkdirSync(chunks_dir_path, { recursive: true });
|
||||||
|
}
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
writeFileSync(path.join(chunks_dir_path, chunk.file_name), chunk.xml, "utf-8");
|
||||||
|
}
|
||||||
|
mkdirSync(path.dirname(sitemap_file), { recursive: true });
|
||||||
|
writeFileSync(sitemap_file, sitemap, "utf-8");
|
||||||
|
const sitemap_relative_path = path.relative(ROOT_DIR, sitemap_file);
|
||||||
|
if (chunks.length) {
|
||||||
|
log.success(`Sitemap index with ${url_count} URLs in ${chunks.length} chunks ` +
|
||||||
|
`written to ${sitemap_relative_path} ` +
|
||||||
|
`(chunks in ${path.relative(ROOT_DIR, chunks_dir_path)})`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.success(`Sitemap with ${url_count} URLs written to ${sitemap_relative_path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Failed to generate sitemap: ${error?.message || error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const robots_config = global.BUNEXT_CONFIG.robots;
|
||||||
|
if (options.robots && robots_config !== false) {
|
||||||
|
writeRobotsTxt({
|
||||||
|
sitemap_file,
|
||||||
|
cli_origin: options.origin,
|
||||||
|
sitemap_origin: sitemap_config.origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function writeRobotsTxt({ sitemap_file, cli_origin, sitemap_origin, }) {
|
||||||
|
const { ROOT_DIR, PUBLIC_DIR } = grabDirNames();
|
||||||
|
const robots_config = global.BUNEXT_CONFIG.robots || undefined;
|
||||||
|
const { output, ...robots_params } = robots_config || {};
|
||||||
|
const robots_file = output
|
||||||
|
? path.resolve(ROOT_DIR, output)
|
||||||
|
: path.join(PUBLIC_DIR, "robots.txt");
|
||||||
|
const robots_relative_path = path.relative(ROOT_DIR, robots_file);
|
||||||
|
// Without a robots config, never replace a hand-written robots.txt.
|
||||||
|
if (!robots_config &&
|
||||||
|
existsSync(robots_file) &&
|
||||||
|
!readFileSync(robots_file, "utf-8").startsWith(RobotsTxtGeneratedMarker)) {
|
||||||
|
log.warn(`Skipped ${robots_relative_path}: it was not generated by bunext. ` +
|
||||||
|
`Set \`robots\` in bunext.config.ts to overwrite it, ` +
|
||||||
|
`or \`robots: false\` to silence this.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sitemap_public_path = grabPublicUrlPath(sitemap_file);
|
||||||
|
if (sitemap_public_path === undefined && !robots_params.sitemaps?.length) {
|
||||||
|
log.warn(`Sitemap is outside public/, so robots.txt has no Sitemap line. ` +
|
||||||
|
`Add its URL to \`robots.sitemaps\`.`);
|
||||||
|
}
|
||||||
|
const robots_txt = generateRobotsTxt({
|
||||||
|
...robots_params,
|
||||||
|
origin: cli_origin || robots_params.origin || sitemap_origin,
|
||||||
|
sitemaps: [
|
||||||
|
...(sitemap_public_path !== undefined
|
||||||
|
? [sitemap_public_path]
|
||||||
|
: []),
|
||||||
|
...(robots_params.sitemaps || []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mkdirSync(path.dirname(robots_file), { recursive: true });
|
||||||
|
writeFileSync(robots_file, robots_txt, "utf-8");
|
||||||
|
log.success(`robots.txt written to ${robots_relative_path}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* # URL path a file in public/ is served at
|
||||||
|
* Public files are served from the site root, e.g. public/sitemap.xml
|
||||||
|
* at /sitemap.xml. Returns `undefined` for paths outside public/.
|
||||||
|
*/
|
||||||
|
function grabPublicUrlPath(file_path) {
|
||||||
|
const { PUBLIC_DIR } = grabDirNames();
|
||||||
|
const relative_path = path.relative(PUBLIC_DIR, file_path);
|
||||||
|
if (relative_path.startsWith("..") || path.isAbsolute(relative_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return `/${relative_path.split(path.sep).join("/")}`;
|
||||||
|
}
|
||||||
Vendored
+52
-46
@@ -1,60 +1,66 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import writeErrorFile from "../../functions/write-error-file";
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
import { existsSync } from "fs";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
let retries = 0;
|
import { existsSync, rmSync } from "fs";
|
||||||
let timeout;
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
|
const RETRY_RESET_AFTER_MS = 10000;
|
||||||
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 () => {
|
||||||
|
// A stale error file from a previous session makes the bundler
|
||||||
|
// run in "silent" mode and pages fail with misleading messages.
|
||||||
|
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||||
await start();
|
await start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function start() {
|
async function start() {
|
||||||
clearTimeout(timeout);
|
// A loop, not recursion: each restart would otherwise keep the previous
|
||||||
if (retries >= MAX_RETRIES) {
|
// call (and its subprocess) alive for the supervisor's whole lifetime.
|
||||||
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
|
let retries = 0;
|
||||||
process.exit(1);
|
while (true) {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const started_at = Date.now();
|
||||||
|
let child_process;
|
||||||
|
try {
|
||||||
|
child_process = Bun.spawn(spawn_options);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Failed to start production process:`, error);
|
||||||
|
retries++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const exited = await child_process.exited;
|
||||||
|
// A null exit means the child was killed by a signal (e.g. Ctrl+C
|
||||||
|
// in the shared terminal), which we must not restart.
|
||||||
|
if (exited == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Restart on any numeric exit (0 included: the server should never
|
||||||
|
// exit cleanly). Only consecutive quick crashes count toward the
|
||||||
|
// limit; a child that ran for a while starts the count over.
|
||||||
|
retries =
|
||||||
|
Date.now() - started_at >= RETRY_RESET_AFTER_MS ? 1 : retries + 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
+23
-2
@@ -1,6 +1,27 @@
|
|||||||
import bunextInit from "../../functions/bunext-init";
|
import bunextInit from "../../functions/bunext-init";
|
||||||
import startServer from "../../functions/server/start-server";
|
import startServer from "../../functions/server/start-server";
|
||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
|
import writeErrorFile from "../../functions/write-error-file";
|
||||||
|
// Same crash contract as the dev child: log the real error, record it in the
|
||||||
|
// error file, and exit non-zero so the supervisor can restart and so the
|
||||||
|
// bundler pipeline sees the failure.
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
log.error(`Uncaught exception: ${error}`);
|
||||||
|
writeErrorFile({ error, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
log.error(`Unhandled rejection: ${reason}`);
|
||||||
|
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
log.info("Starting production server ...");
|
log.info("Starting production server ...");
|
||||||
await bunextInit();
|
try {
|
||||||
await startServer();
|
await bunextInit();
|
||||||
|
await startServer();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Failed to start production server: ${error}`);
|
||||||
|
writeErrorFile({ error, exitCode: 1, force: true });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -1,5 +1,6 @@
|
|||||||
export declare const AppData: {
|
export declare const AppData: {
|
||||||
readonly DefaultCacheExpiryTimeSeconds: number;
|
readonly DefaultCacheExpiryTimeSeconds: number;
|
||||||
|
readonly DefaultMaxCacheEntries: 1000;
|
||||||
readonly DefaultCronInterval: 30000;
|
readonly DefaultCronInterval: 30000;
|
||||||
readonly BunextStaticFilesCacheExpiry: number;
|
readonly BunextStaticFilesCacheExpiry: number;
|
||||||
readonly ClientHMRPath: "__bunext_client_hmr__";
|
readonly ClientHMRPath: "__bunext_client_hmr__";
|
||||||
@@ -7,4 +8,5 @@ export declare const AppData: {
|
|||||||
readonly BunextTmpFileExt: ".bunext_tmp.tsx";
|
readonly BunextTmpFileExt: ".bunext_tmp.tsx";
|
||||||
readonly BunextHMRRetryRoute: "/.bunext/hmr-retry";
|
readonly BunextHMRRetryRoute: "/.bunext/hmr-retry";
|
||||||
readonly DefaultMaxLogs: 50;
|
readonly DefaultMaxLogs: 50;
|
||||||
|
readonly DefaultSSRRenderTimeoutMs: 30000;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -1,5 +1,6 @@
|
|||||||
export const AppData = {
|
export const AppData = {
|
||||||
DefaultCacheExpiryTimeSeconds: 60 * 60,
|
DefaultCacheExpiryTimeSeconds: 60 * 60,
|
||||||
|
DefaultMaxCacheEntries: 1000,
|
||||||
DefaultCronInterval: 30000,
|
DefaultCronInterval: 30000,
|
||||||
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
|
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
|
||||||
ClientHMRPath: "__bunext_client_hmr__",
|
ClientHMRPath: "__bunext_client_hmr__",
|
||||||
@@ -7,4 +8,5 @@ export const AppData = {
|
|||||||
BunextTmpFileExt: ".bunext_tmp.tsx",
|
BunextTmpFileExt: ".bunext_tmp.tsx",
|
||||||
BunextHMRRetryRoute: "/.bunext/hmr-retry",
|
BunextHMRRetryRoute: "/.bunext/hmr-retry",
|
||||||
DefaultMaxLogs: 50,
|
DefaultMaxLogs: 50,
|
||||||
|
DefaultSSRRenderTimeoutMs: 30000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ import type { BundlerCTXMap } from "../../types";
|
|||||||
type Params = {
|
type Params = {
|
||||||
post_build_fn?: (params: {
|
post_build_fn?: (params: {
|
||||||
artifacts: BundlerCTXMap[];
|
artifacts: BundlerCTXMap[];
|
||||||
|
changed_pages?: string[];
|
||||||
}) => Promise<void> | void;
|
}) => Promise<void> | void;
|
||||||
build_only?: boolean;
|
build_only?: boolean;
|
||||||
start?: boolean;
|
start?: boolean;
|
||||||
|
/**
|
||||||
|
* Re-fetch server props for every HMR client after the build, even if
|
||||||
|
* only client code changed. Only applies when the context is recreated.
|
||||||
|
*/
|
||||||
|
refresh_props?: boolean;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* True when `local_path` is a hydration entry of the current esbuild context.
|
||||||
|
*/
|
||||||
|
export declare function isHydrationEntryPage(local_path: string): boolean;
|
||||||
export default function allPagesESBuildContextBundler(params?: Params): Promise<void>;
|
export default function allPagesESBuildContextBundler(params?: Params): Promise<void>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+99
-45
@@ -7,11 +7,36 @@ import grabClientHydrationScript from "./grab-client-hydration-script";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
||||||
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||||
import { existsSync } from "fs";
|
import { logBuildErrorsOnce, } from "../../utils/build-error-log";
|
||||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
import { log } from "../../utils/log";
|
||||||
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
|
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||||
|
// Snapshot of the entry key set + root file presence of the last build.
|
||||||
|
// When it differs from the freshly computed set, the esbuild context must
|
||||||
|
// be recreated (entry points are fixed at context creation).
|
||||||
|
let last_entry_keys = undefined;
|
||||||
|
let last_root_file_path = undefined;
|
||||||
|
// Local paths of the pages that made it into the current entry set. Pages
|
||||||
|
// skipped at build time (e.g. created empty, no default export yet) are
|
||||||
|
// absent, so the watcher can treat their later edits as structural.
|
||||||
|
let last_entry_pages = new Set();
|
||||||
|
/**
|
||||||
|
* True when `local_path` is a hydration entry of the current esbuild context.
|
||||||
|
*/
|
||||||
|
export function isHydrationEntryPage(local_path) {
|
||||||
|
return last_entry_pages.has(local_path);
|
||||||
|
}
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
if (a.length !== b.length)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] !== b[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
export default async function allPagesESBuildContextBundler(params) {
|
export default async function allPagesESBuildContextBundler(params) {
|
||||||
try {
|
try {
|
||||||
const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
|
||||||
const pages = grabAllPages({ exclude_api: true });
|
const pages = grabAllPages({ exclude_api: true });
|
||||||
global.BUNEXT_PAGE_FILES = pages;
|
global.BUNEXT_PAGE_FILES = pages;
|
||||||
const dev = isDevelopment();
|
const dev = isDevelopment();
|
||||||
@@ -24,54 +49,83 @@ export default async function allPagesESBuildContextBundler(params) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`);
|
const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`);
|
||||||
// await Bun.write(entryFile, txt, { createPath: true });
|
|
||||||
entryToPage.set(entryFile, { ...page, tsx });
|
entryToPage.set(entryFile, { ...page, tsx });
|
||||||
}
|
}
|
||||||
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
|
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
|
||||||
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
const entry_keys = [...entryToPage.keys()].sort();
|
||||||
entryPoints,
|
const { root_file_path } = grabRootFilePath();
|
||||||
outdir: HYDRATION_DST_DIR,
|
const structure_changed = !global.BUNEXT_BUNDLER_CTX ||
|
||||||
bundle: true,
|
global.BUNEXT_BUNDLER_CTX_DISPOSED ||
|
||||||
minify: !dev,
|
!last_entry_keys ||
|
||||||
format: "esm",
|
!arraysEqual(last_entry_keys, entry_keys) ||
|
||||||
target: "es2020",
|
last_root_file_path !== root_file_path;
|
||||||
platform: "browser",
|
if (structure_changed) {
|
||||||
define: {
|
try {
|
||||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
await global.BUNEXT_BUNDLER_CTX?.dispose();
|
||||||
},
|
}
|
||||||
entryNames: "[dir]/[hash]",
|
catch (error) { }
|
||||||
metafile: true,
|
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||||
plugins: [
|
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
||||||
forceExternalReact(),
|
entryPoints,
|
||||||
tailwindEsbuildPlugin,
|
outdir: HYDRATION_DST_DIR,
|
||||||
virtualFilesPlugin({
|
bundle: true,
|
||||||
entryToPage,
|
minify: !dev,
|
||||||
}),
|
format: "esm",
|
||||||
esbuildCTXArtifactTracker({
|
target: "es2020",
|
||||||
entryToPage,
|
platform: "browser",
|
||||||
post_build_fn: params?.post_build_fn,
|
define: {
|
||||||
build_only: params?.build_only || params?.start,
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
}),
|
},
|
||||||
],
|
entryNames: "[dir]/[hash]",
|
||||||
jsx: "automatic",
|
metafile: true,
|
||||||
splitting: true,
|
plugins: [
|
||||||
treeShaking: true,
|
forceExternalReact(),
|
||||||
external: [
|
tailwindEsbuildPlugin,
|
||||||
"react",
|
virtualFilesPlugin({
|
||||||
"react-dom",
|
entryToPage,
|
||||||
"react-dom/client",
|
}),
|
||||||
"react/jsx-runtime",
|
esbuildCTXArtifactTracker({
|
||||||
"react/jsx-dev-runtime",
|
entryToPage,
|
||||||
...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
|
post_build_fn: params?.post_build_fn,
|
||||||
],
|
build_only: params?.build_only || params?.start,
|
||||||
logLevel: did_process_exit_because_of_bundler_error
|
refresh_props_on_first_build: params?.refresh_props,
|
||||||
? "silent"
|
}),
|
||||||
: undefined,
|
],
|
||||||
});
|
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 || []),
|
||||||
|
],
|
||||||
|
// esbuild's own error logging prints per attempt; the
|
||||||
|
// artifact-tracker plugin reports errors (deduped, once) itself.
|
||||||
|
logLevel: "silent",
|
||||||
|
});
|
||||||
|
last_entry_keys = entry_keys;
|
||||||
|
last_root_file_path = root_file_path;
|
||||||
|
last_entry_pages = new Set([...entryToPage.values()].map((p) => p.local_path));
|
||||||
|
}
|
||||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(`ESBUILD Error =>`, error);
|
global.BUNEXT_BUILD_FAILED = true;
|
||||||
|
// When the build fails with esbuild errors, the artifact-tracker's
|
||||||
|
// onEnd already reported them (deduped). "Build failed with N errors"
|
||||||
|
// is that same failure rethrown by esbuild's API — re-logging it here
|
||||||
|
// would double every incident.
|
||||||
|
if (String(error?.message || "").startsWith("Build failed with")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logBuildErrorsOnce([
|
||||||
|
{
|
||||||
|
text: `ESBUILD Error => ${error?.message || String(error)}`,
|
||||||
|
},
|
||||||
|
], (line) => log.error(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function forceExternalReact() {
|
function forceExternalReact() {
|
||||||
|
|||||||
+9
-10
@@ -1,18 +1,17 @@
|
|||||||
export default async function buildOnstartErrorHandler(params) {
|
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) {
|
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
return;
|
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_BUNDLER_CTX_DISPOSED = true;
|
||||||
global.BUNEXT_RECOMPILING = false;
|
global.BUNEXT_RECOMPILING = false;
|
||||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
await Promise.all([
|
// NOTE: must NOT dispose the currently-building context here. This
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX?.dispose(),
|
// handler runs from the artifact-tracker plugin's onStart, i.e. while the
|
||||||
global.BUNEXT_BUNDLER_CTX?.dispose(),
|
// context's own build is in flight. esbuild's `dispose()` waits for the
|
||||||
]);
|
// in-flight build to finish, so disposing self = deadlock, which left the
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
// dev server permanently stuck after a failed rebuild.
|
||||||
global.BUNEXT_BUNDLER_CTX = undefined;
|
//
|
||||||
|
// Marking the context as disposed is enough: the next rebuild attempt
|
||||||
|
// (watcher / fullRebuild) sees BUNEXT_BUNDLER_CTX_DISPOSED and recreates a
|
||||||
|
// fresh context, whose cleanup disposes the old (now idle) one.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type Params = {
|
||||||
|
/**
|
||||||
|
* Also stop esbuild's service process. Only safe when no other esbuild
|
||||||
|
* call can be in flight (e.g. before exiting, or right after the startup
|
||||||
|
* build). The service restarts on the next esbuild call.
|
||||||
|
*/
|
||||||
|
stop_service?: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Disposes the hydration and SSR esbuild contexts, releasing the incremental
|
||||||
|
* build state esbuild keeps in memory for them. The bundlers re-create the
|
||||||
|
* contexts on their next run.
|
||||||
|
*/
|
||||||
|
export default function disposeBundlerContexts(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
/**
|
||||||
|
* Disposes the hydration and SSR esbuild contexts, releasing the incremental
|
||||||
|
* build state esbuild keeps in memory for them. The bundlers re-create the
|
||||||
|
* contexts on their next run.
|
||||||
|
*/
|
||||||
|
export default async function disposeBundlerContexts(params) {
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_BUNDLER_CTX?.dispose();
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
if (params?.stop_service) {
|
||||||
|
// Exiting while the service is still running can leave it orphaned
|
||||||
|
// with an unanswered ping, which crashes it with "all goroutines are
|
||||||
|
// asleep - deadlock!".
|
||||||
|
try {
|
||||||
|
await esbuild.stop();
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-2
@@ -2,6 +2,8 @@ import { existsSync } from "fs";
|
|||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import grabConstants from "../../utils/grab-constants";
|
import grabConstants from "../../utils/grab-constants";
|
||||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
|
import grabRelativePath from "../../utils/grab-relative-path";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
const { ROOT_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();
|
||||||
@@ -18,12 +20,19 @@ export default async function grabClientHydrationScript({ page_local_path, }) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const root_content = await Bun.file(root_file_path).text();
|
const root_content = await Bun.file(root_file_path).text();
|
||||||
if (!root_content.match(/^export default/m)) {
|
if (!hasBundlableDefaultExport({
|
||||||
|
file_path: root_file_path,
|
||||||
|
content: root_content,
|
||||||
|
is_root: true,
|
||||||
|
})) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const page_content = await Bun.file(page_local_path).text();
|
const page_content = await Bun.file(page_local_path).text();
|
||||||
if (!page_content.match(/^export default/m)) {
|
if (!hasBundlableDefaultExport({
|
||||||
|
file_path: page_local_path,
|
||||||
|
content: page_content,
|
||||||
|
})) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let txt = ``;
|
let txt = ``;
|
||||||
@@ -54,3 +63,36 @@ export default async function grabClientHydrationScript({ page_local_path, }) {
|
|||||||
txt += `}\n`;
|
txt += `}\n`;
|
||||||
return txt;
|
return txt;
|
||||||
}
|
}
|
||||||
|
// File path -> content hash of the last warning, so rebuilds don't repeat
|
||||||
|
// a warning until the file changes.
|
||||||
|
const warned_files = new Map();
|
||||||
|
/**
|
||||||
|
* # Check a page or root file can be bundled
|
||||||
|
* Files are only bundled when `export default` starts a line. Files with
|
||||||
|
* no default export at all (e.g. a page still being written) are skipped
|
||||||
|
* silently; this warns about the confusing case where one exists
|
||||||
|
* elsewhere, like `import X from "x"; export default X` on one line, or
|
||||||
|
* `export { Page as default }`.
|
||||||
|
*/
|
||||||
|
function hasBundlableDefaultExport({ file_path, content, is_root, }) {
|
||||||
|
if (/^export default/m.test(content)) {
|
||||||
|
warned_files.delete(file_path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const has_default_export = /\bexport\s+default\b/.test(content) ||
|
||||||
|
/\bas\s+default\b/.test(content);
|
||||||
|
if (!has_default_export) {
|
||||||
|
warned_files.delete(file_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const content_hash = Bun.hash(content);
|
||||||
|
if (warned_files.get(file_path) === content_hash)
|
||||||
|
return false;
|
||||||
|
warned_files.set(file_path, content_hash);
|
||||||
|
log.warn(`${grabRelativePath(file_path)} was skipped: ` +
|
||||||
|
`\`export default\` must be at the start of a line. ` +
|
||||||
|
(is_root
|
||||||
|
? `No pages can be bundled until the root layout is fixed.`
|
||||||
|
: `This page will not be bundled.`));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
post_build_fn?: (params: {
|
|
||||||
artifacts: any[];
|
|
||||||
}) => Promise<void> | void;
|
|
||||||
};
|
|
||||||
export default function pagesSSRBundler(params?: Params): Promise<void>;
|
|
||||||
export {};
|
|
||||||
-94
@@ -1,94 +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 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+21
-5
@@ -1,7 +1,23 @@
|
|||||||
type Params = {
|
type RebuildParams = {
|
||||||
post_build_fn?: (params: {
|
/**
|
||||||
artifacts: any[];
|
* Force a context recreation even when the entry set is unchanged.
|
||||||
}) => Promise<void> | void;
|
* Used by `fullRebuild` after it disposed the contexts.
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
};
|
};
|
||||||
export default function pagesSSRContextBundler(params?: Params): Promise<void>;
|
/**
|
||||||
|
* Single-flight SSR rebuild. Concurrent callers (each successful hydration
|
||||||
|
* rebuild triggers this, plus watcher/structural paths) join the in-flight
|
||||||
|
* rebuild instead of spawning overlapping esbuild builds.
|
||||||
|
*
|
||||||
|
* A fresh content change that lands while a rebuild is in flight is picked
|
||||||
|
* up by the next hydration rebuild cycle (each watcher event re-runs the
|
||||||
|
* hydration context rebuild, which re-enters this queue).
|
||||||
|
*/
|
||||||
|
export default function enqueueSSRRebuild(params?: RebuildParams): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Resolves once the in-flight SSR rebuild (if any) has finished. Lets page
|
||||||
|
* requests avoid rendering stale SSR modules against a newer client bundle.
|
||||||
|
*/
|
||||||
|
export declare function awaitSSRRebuild(): Promise<void>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+177
-45
@@ -2,25 +2,56 @@ import * as esbuild from "esbuild";
|
|||||||
import grabAllPages from "../../utils/grab-all-pages";
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import isDevelopment from "../../utils/is-development";
|
import isDevelopment from "../../utils/is-development";
|
||||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
import { cssStubEsbuildPlugin } from "../server/web-pages/tailwind-esbuild-plugin";
|
||||||
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||||
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||||
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
import cleanupArtifacts from "../server/cleanup-artifacts";
|
||||||
export default async function pagesSSRContextBundler(params) {
|
import { evictDeletedModules } from "../../utils/import-fresh";
|
||||||
const pages = grabAllPages();
|
import { writeFileSync } from "fs";
|
||||||
const dev = isDevelopment();
|
import path from "path";
|
||||||
if (global.BUNEXT_SSR_BUNDLER_CTX) {
|
import { log } from "../../utils/log";
|
||||||
await global.BUNEXT_SSR_BUNDLER_CTX.dispose();
|
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
import shouldMinifySSR from "../../utils/should-minify-ssr";
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||||
|
// Current virtual entry contents. The SSR virtual plugin resolves its
|
||||||
|
// onLoad contents lazily from this map on every rebuild, so edits to
|
||||||
|
// `.server.ts`/api files (whose entries mirror their raw file content) are
|
||||||
|
// picked up without recreating the esbuild context.
|
||||||
|
let current_entry_map = new Map();
|
||||||
|
// Entry key set + root file presence of the last *successful* build. When
|
||||||
|
// this differs from the freshly computed set, the context must be
|
||||||
|
// recreated (esbuild cannot add/remove entry points on a live context).
|
||||||
|
let last_entry_keys = undefined;
|
||||||
|
let last_root_file_path = undefined;
|
||||||
|
let rebuild_in_flight;
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
if (a.length !== b.length)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] !== b[i])
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const entryToPage = new Map();
|
return true;
|
||||||
|
}
|
||||||
|
async function computeEntryToPage() {
|
||||||
|
const pages = grabAllPages({
|
||||||
|
include_server: true,
|
||||||
|
});
|
||||||
const { root_file_path } = grabRootFilePath();
|
const { root_file_path } = grabRootFilePath();
|
||||||
|
const entryToPage = new Map();
|
||||||
|
try {
|
||||||
|
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-pages.json"), JSON.stringify(pages, null, 4));
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
if (page.local_path.match(/\/pages\/api\//)) {
|
if (page.local_path.match(/\/pages\/api\//) ||
|
||||||
|
page.local_path.match(/\.server\.tsx?$/)) {
|
||||||
const ts = await Bun.file(page.local_path).text();
|
const ts = await Bun.file(page.local_path).text();
|
||||||
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
if (ts.match(/(export default)|(export \w+ handler)|(export \w+ server)/)) {
|
||||||
|
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const tsx = grabPageReactComponentString({
|
const tsx = grabPageReactComponentString({
|
||||||
@@ -29,41 +60,142 @@ export default async function pagesSSRContextBundler(params) {
|
|||||||
});
|
});
|
||||||
if (!tsx)
|
if (!tsx)
|
||||||
continue;
|
continue;
|
||||||
|
if (!tsx.match(/export default/))
|
||||||
|
continue;
|
||||||
entryToPage.set(page.local_path, { ...page, tsx });
|
entryToPage.set(page.local_path, { ...page, tsx });
|
||||||
}
|
}
|
||||||
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
|
return { entryToPage, root_file_path };
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
|
}
|
||||||
entryPoints,
|
/**
|
||||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
* Single-flight SSR rebuild. Concurrent callers (each successful hydration
|
||||||
bundle: true,
|
* rebuild triggers this, plus watcher/structural paths) join the in-flight
|
||||||
minify: !dev,
|
* rebuild instead of spawning overlapping esbuild builds.
|
||||||
format: "esm",
|
*
|
||||||
target: "esnext",
|
* A fresh content change that lands while a rebuild is in flight is picked
|
||||||
platform: "node",
|
* up by the next hydration rebuild cycle (each watcher event re-runs the
|
||||||
define: {
|
* hydration context rebuild, which re-enters this queue).
|
||||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
*/
|
||||||
},
|
export default async function enqueueSSRRebuild(params) {
|
||||||
entryNames: "[dir]/[hash]",
|
if (rebuild_in_flight) {
|
||||||
metafile: true,
|
return rebuild_in_flight;
|
||||||
plugins: [
|
}
|
||||||
tailwindEsbuildPlugin,
|
const run = doSSRRebuild(params).finally(() => {
|
||||||
ssrVirtualFilesPlugin({
|
if (rebuild_in_flight === run) {
|
||||||
entryToPage,
|
rebuild_in_flight = undefined;
|
||||||
}),
|
}
|
||||||
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();
|
rebuild_in_flight = run;
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resolves once the in-flight SSR rebuild (if any) has finished. Lets page
|
||||||
|
* requests avoid rendering stale SSR modules against a newer client bundle.
|
||||||
|
*/
|
||||||
|
export async function awaitSSRRebuild() {
|
||||||
|
try {
|
||||||
|
await rebuild_in_flight;
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
}
|
||||||
|
async function doSSRRebuild(params) {
|
||||||
|
const force = Boolean(params?.force);
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const config = global.BUNEXT_CONFIG;
|
||||||
|
try {
|
||||||
|
const { entryToPage, root_file_path } = await computeEntryToPage();
|
||||||
|
const entry_keys = [...entryToPage.keys()].sort();
|
||||||
|
if (entry_keys.length === 0) {
|
||||||
|
current_entry_map = entryToPage;
|
||||||
|
last_entry_keys = entry_keys;
|
||||||
|
last_root_file_path = root_file_path;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Refresh the virtual contents BEFORE rebuilding so the plugin's
|
||||||
|
// onLoad sees the latest `.server.ts`/api contents.
|
||||||
|
current_entry_map = entryToPage;
|
||||||
|
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) { }
|
||||||
|
const structure_changed = !global.BUNEXT_SSR_BUNDLER_CTX ||
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED ||
|
||||||
|
force ||
|
||||||
|
!last_entry_keys ||
|
||||||
|
!arraysEqual(last_entry_keys, entry_keys) ||
|
||||||
|
last_root_file_path !== root_file_path;
|
||||||
|
if (structure_changed) {
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
|
||||||
|
entryPoints,
|
||||||
|
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
bundle: true,
|
||||||
|
minify: shouldMinifySSR({
|
||||||
|
minify_ssr_bundle: config.minify_ssr_bundle,
|
||||||
|
}),
|
||||||
|
format: "esm",
|
||||||
|
target: "esnext",
|
||||||
|
platform: "node",
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||||
|
},
|
||||||
|
entryNames: "[dir]/[hash]",
|
||||||
|
metafile: true,
|
||||||
|
plugins: [
|
||||||
|
// SSR CSS output is never served (HTML links the
|
||||||
|
// hydration build's CSS), so skip Tailwind here.
|
||||||
|
cssStubEsbuildPlugin,
|
||||||
|
ssrVirtualFilesPlugin({
|
||||||
|
getEntryToPage: () => current_entry_map,
|
||||||
|
}),
|
||||||
|
ssrCTXArtifactTracker({
|
||||||
|
entryToPage,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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,
|
||||||
|
logLevel: "silent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await global.BUNEXT_SSR_BUNDLER_CTX.rebuild();
|
||||||
|
// Only record the entry set after a successful build so a failed
|
||||||
|
// build retries with a fresh context next time.
|
||||||
|
last_entry_keys = entry_keys;
|
||||||
|
last_root_file_path = root_file_path;
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||||
|
global.BUNEXT_BUILD_FAILED = false;
|
||||||
|
await cleanupArtifacts({ paradigm: "ssr" });
|
||||||
|
// esbuild has now replaced superseded outputs on disk; release the
|
||||||
|
// ones the server already imported.
|
||||||
|
evictDeletedModules(BUNX_CWD_MODULE_CACHE_DIR);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||||
|
global.BUNEXT_BUILD_FAILED = true;
|
||||||
|
// esbuild errors are reported (deduped) by the ssr artifact tracker's
|
||||||
|
// onEnd; only re-log unexpected failures.
|
||||||
|
if (!String(error?.message || "").startsWith("Build failed with")) {
|
||||||
|
logBuildErrorsOnce([
|
||||||
|
{
|
||||||
|
text: `SSR Bundler Error: ${error?.message || String(error)}`,
|
||||||
|
},
|
||||||
|
], (line) => log.error(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ type Params = {
|
|||||||
}>;
|
}>;
|
||||||
post_build_fn?: (params: {
|
post_build_fn?: (params: {
|
||||||
artifacts: any[];
|
artifacts: any[];
|
||||||
|
changed_pages?: string[];
|
||||||
}) => Promise<void> | void;
|
}) => Promise<void> | void;
|
||||||
build_only?: boolean;
|
build_only?: boolean;
|
||||||
|
/**
|
||||||
|
* Always re-fetch server props after this context's first build, even
|
||||||
|
* when only client code changed (set by `fullRebuild`, e.g. after a
|
||||||
|
* config change).
|
||||||
|
*/
|
||||||
|
refresh_props_on_first_build?: boolean;
|
||||||
};
|
};
|
||||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }: Params): Plugin;
|
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, refresh_props_on_first_build, }: Params): Plugin;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+95
-24
@@ -1,39 +1,35 @@
|
|||||||
import {} from "esbuild";
|
import {} from "esbuild";
|
||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
||||||
import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import pagesSSRBundler from "../pages-ssr-bundler";
|
import enqueueSSRRebuild from "../pages-ssr-context-bundler";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
import { logBuildErrorsOnce, resetBuildErrorLog, } from "../../../utils/build-error-log";
|
||||||
|
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||||
import fullRebuild from "../../server/full-rebuild";
|
import fullRebuild from "../../server/full-rebuild";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import cleanupLogsDirs from "../../cleanup-logs-dir";
|
import cleanupLogsDirs from "../../cleanup-logs-dir";
|
||||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR, ROOT_DIR } = grabDirNames();
|
import cleanupArtifacts from "../../server/cleanup-artifacts";
|
||||||
|
import { pushClientUpdate } from "../../server/server-post-build-fn";
|
||||||
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
||||||
let build_start = 0;
|
let build_start = 0;
|
||||||
const MAX_BUILD_STARTS = 2;
|
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, refresh_props_on_first_build, }) {
|
||||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }) {
|
let force_props_refresh = Boolean(refresh_props_on_first_build);
|
||||||
const artifactTracker = {
|
const artifactTracker = {
|
||||||
name: "artifact-tracker",
|
name: "artifact-tracker",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onStart(async () => {
|
build.onStart(async () => {
|
||||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS++;
|
|
||||||
build_start = performance.now();
|
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) => {
|
build.onEnd(async (result) => {
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
|
global.BUNEXT_BUILD_FAILED = true;
|
||||||
global.BUNEXT_RECOMPILING = false;
|
global.BUNEXT_RECOMPILING = false;
|
||||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
log.error(`Build errors:`);
|
// Rebuild attempts repeat for every file edit / request
|
||||||
for (const err of result.errors) {
|
// while the error persists; only log the error once.
|
||||||
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
logBuildErrorsOnce(result.errors, (line) => log.error(` ${line}`));
|
||||||
}
|
|
||||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
try {
|
try {
|
||||||
@@ -49,26 +45,52 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
|||||||
result,
|
result,
|
||||||
entryToPage,
|
entryToPage,
|
||||||
});
|
});
|
||||||
|
// Pages whose entry output hash changed since the previous
|
||||||
|
// build. With `splitting`, a change to any shared chunk also
|
||||||
|
// re-hashes every page entry that imports it, so this set
|
||||||
|
// covers all pages whose client graph actually changed.
|
||||||
|
const changed_pages = [];
|
||||||
if (artifacts?.[0] && artifacts.length > 0) {
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
for (let i = 0; i < artifacts.length; i++) {
|
for (let i = 0; i < artifacts.length; i++) {
|
||||||
const artifact = artifacts[i];
|
const artifact = artifacts[i];
|
||||||
if (artifact?.local_path &&
|
if (artifact?.local_path &&
|
||||||
global.BUNEXT_BUNDLER_CTX_MAP) {
|
global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
const relative_path = grabRelativePath(artifact.local_path);
|
const relative_path = grabRelativePath(artifact.local_path);
|
||||||
|
const prior = global.BUNEXT_BUNDLER_CTX_MAP[relative_path];
|
||||||
|
if (!prior?.path ||
|
||||||
|
prior.path !== artifact.path ||
|
||||||
|
prior.css_path !== artifact.css_path) {
|
||||||
|
changed_pages.push(relative_path);
|
||||||
|
}
|
||||||
global.BUNEXT_BUNDLER_CTX_MAP[relative_path] =
|
global.BUNEXT_BUNDLER_CTX_MAP[relative_path] =
|
||||||
_.merge(global.BUNEXT_BUNDLER_CTX_MAP[relative_path], { ...artifact, local_path: relative_path });
|
_.merge(prior, {
|
||||||
|
...artifact,
|
||||||
|
local_path: relative_path,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const elapsed = (performance.now() - build_start).toFixed(0);
|
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||||
log.success(`[Built] in ${elapsed}ms`);
|
log.success(`[Built] in ${elapsed}ms`);
|
||||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
// A successful build clears the "last error" so a NEW incident
|
||||||
|
// with the same signature is reported again.
|
||||||
|
resetBuildErrorLog();
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
logBuildErrorsOnce(result.warnings, (line) => log.warn(line));
|
||||||
|
}
|
||||||
|
global.BUNEXT_BUILD_FAILED = false;
|
||||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
||||||
|
const props_refresh_forced = force_props_refresh;
|
||||||
|
force_props_refresh = false;
|
||||||
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||||
// SSR must finish before HMR so server props are fresh
|
// Client-only changes are pushed before the SSR rebuild;
|
||||||
|
// anything that may change server props waits for SSR so
|
||||||
|
// props are fresh. The rebuild is single-flighted: concurrent hydration
|
||||||
|
// rebuilds join the in-flight SSR rebuild instead of
|
||||||
|
// starting overlapping ones.
|
||||||
if (build_only) {
|
if (build_only) {
|
||||||
try {
|
try {
|
||||||
await pagesSSRBundler();
|
await enqueueSSRRebuild();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(`SSR Bundler Error: ${error}`);
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
@@ -81,22 +103,56 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
|||||||
cleanupLogsDirs();
|
cleanupLogsDirs();
|
||||||
await fullRebuild();
|
await fullRebuild();
|
||||||
}
|
}
|
||||||
else {
|
else if (changed_pages.length > 0 &&
|
||||||
|
post_build_fn &&
|
||||||
|
!props_refresh_forced) {
|
||||||
|
// Client graph changed: push the new bundle right away.
|
||||||
|
// Server functions live in `.server.ts` files, outside
|
||||||
|
// the client graph, so the browser's current props are
|
||||||
|
// still valid and it needn't wait for the SSR rebuild.
|
||||||
|
pushClientUpdate(changed_pages);
|
||||||
|
const server_hashes_before = grabServerEntryPaths();
|
||||||
try {
|
try {
|
||||||
await pagesSSRBundler();
|
await enqueueSSRRebuild();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(`SSR Bundler Error: ${error}`);
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
}
|
}
|
||||||
if (artifacts?.[0] && artifacts.length > 0) {
|
// A module shared by a page and its server file changed:
|
||||||
|
// props may differ, so re-fetch them for every tab.
|
||||||
|
if (server_hashes_before !== grabServerEntryPaths()) {
|
||||||
try {
|
try {
|
||||||
await post_build_fn?.({ artifacts });
|
await post_build_fn({
|
||||||
|
artifacts: artifacts || [],
|
||||||
|
changed_pages: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(`Post-build Error: ${error}`);
|
log.error(`Post-build Error: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
await enqueueSSRRebuild();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`SSR Bundler Error: ${error}`);
|
||||||
|
}
|
||||||
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
|
try {
|
||||||
|
await post_build_fn?.({
|
||||||
|
artifacts,
|
||||||
|
changed_pages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Post-build Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prune orphaned entry outputs from previous rebuilds.
|
||||||
|
await cleanupArtifacts({ paradigm: "hydration" });
|
||||||
global.BUNEXT_RECOMPILING = false;
|
global.BUNEXT_RECOMPILING = false;
|
||||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
});
|
});
|
||||||
@@ -104,3 +160,18 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
|||||||
};
|
};
|
||||||
return artifactTracker;
|
return artifactTracker;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fingerprint of the SSR outputs of all `.server.ts(x)` entries. Outputs are
|
||||||
|
* content-hashed, so a changed fingerprint means some server function's code
|
||||||
|
* (or a module it imports) changed.
|
||||||
|
*/
|
||||||
|
function grabServerEntryPaths() {
|
||||||
|
const map = global.BUNEXT_SSR_BUNDLER_CTX_MAP;
|
||||||
|
if (!map)
|
||||||
|
return "";
|
||||||
|
return Object.keys(map)
|
||||||
|
.filter((key) => key.match(/\.server\.tsx?$/))
|
||||||
|
.sort()
|
||||||
|
.map((key) => `${key}:${map[key]?.path || ""}`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ type Params = {
|
|||||||
entryToPage: Map<string, PageFiles & {
|
entryToPage: Map<string, PageFiles & {
|
||||||
tsx: string;
|
tsx: string;
|
||||||
}>;
|
}>;
|
||||||
post_build_fn?: (params: {
|
|
||||||
artifacts: any[];
|
|
||||||
}) => Promise<void> | void;
|
|
||||||
};
|
};
|
||||||
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin;
|
export default function ssrCTXArtifactTracker({ entryToPage }: Params): Plugin;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+10
-24
@@ -4,35 +4,22 @@ import { writeFileSync } from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||||
|
import { logBuildErrorsOnce, resetBuildErrorLog, } from "../../../utils/build-error-log";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
let build_start = 0;
|
let build_start = 0;
|
||||||
let build_starts = 0;
|
const { BUNX_TMP_DIR } = grabDirNames();
|
||||||
const MAX_BUILD_STARTS = 2;
|
export default function ssrCTXArtifactTracker({ entryToPage }) {
|
||||||
const { BUNX_TMP_DIR, ROOT_DIR } = grabDirNames();
|
|
||||||
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
|
||||||
const artifactTracker = {
|
const artifactTracker = {
|
||||||
name: "ssr-artifact-tracker",
|
name: "ssr-artifact-tracker",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onStart(async () => {
|
build.onStart(async () => {
|
||||||
build_starts++;
|
|
||||||
build_start = performance.now();
|
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) => {
|
build.onEnd(async (result) => {
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
|
global.BUNEXT_BUILD_FAILED = true;
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||||
try {
|
logBuildErrorsOnce(result.errors, (line) => log.error(`SSR Build error: ${line}`));
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
const artifacts = grabArtifactsFromBundledResults({
|
const artifacts = grabArtifactsFromBundledResults({
|
||||||
@@ -52,17 +39,16 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// post_build_fn?.({ artifacts });
|
|
||||||
// const elapsed = (performance.now() - build_start).toFixed(
|
|
||||||
// 0,
|
|
||||||
// );
|
|
||||||
// log.success(`SSR [Built] in ${elapsed}ms`);
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.BUNEXT_SSR_BUNDLER_CTX_MAP, null, 4));
|
writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.BUNEXT_SSR_BUNDLER_CTX_MAP, null, 4));
|
||||||
}
|
}
|
||||||
catch (error) { }
|
catch (error) { }
|
||||||
|
// const elapsed = (performance.now() - build_start).toFixed(0);
|
||||||
|
// log.success(`SSR [Built] in ${elapsed}ms`);
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||||
|
global.BUNEXT_BUILD_FAILED = false;
|
||||||
|
resetBuildErrorLog();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import type { Plugin } from "esbuild";
|
import type { Plugin } from "esbuild";
|
||||||
import type { PageFiles } from "../../../types";
|
import type { PageFiles } from "../../../types";
|
||||||
type Params = {
|
type SSRVirtualEntry = PageFiles & {
|
||||||
entryToPage: Map<string, PageFiles & {
|
tsx: string;
|
||||||
tsx: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
export default function ssrVirtualFilesPlugin({ entryToPage }: Params): Plugin;
|
type Params = {
|
||||||
|
/**
|
||||||
|
* Lazily-resolved current virtual entry contents. The map is refreshed
|
||||||
|
* before every rebuild so edits to `.server.ts`/api files (whose virtual
|
||||||
|
* entries mirror their raw file content) are picked up by a plain
|
||||||
|
* `ctx.rebuild()` without recreating the esbuild context.
|
||||||
|
*/
|
||||||
|
getEntryToPage: () => Map<string, SSRVirtualEntry>;
|
||||||
|
};
|
||||||
|
export default function ssrVirtualFilesPlugin({ getEntryToPage }: Params): Plugin;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { log } from "../../../utils/log";
|
export default function ssrVirtualFilesPlugin({ getEntryToPage }) {
|
||||||
export default function ssrVirtualFilesPlugin({ entryToPage }) {
|
|
||||||
const virtualPlugin = {
|
const virtualPlugin = {
|
||||||
name: "ssr-virtual-hydration",
|
name: "ssr-virtual-hydration",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
@@ -12,7 +11,7 @@ export default function ssrVirtualFilesPlugin({ entryToPage }) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => {
|
build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => {
|
||||||
const target = entryToPage.get(args.path);
|
const target = getEntryToPage().get(args.path);
|
||||||
if (!target?.tsx)
|
if (!target?.tsx)
|
||||||
return null;
|
return null;
|
||||||
const contents = target.tsx;
|
const contents = target.tsx;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
txt_code: string;
|
|
||||||
file_path: string;
|
|
||||||
};
|
|
||||||
export default function stripServerSideLogic({ txt_code, file_path }: Params): string;
|
|
||||||
export {};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import path from "path";
|
|
||||||
import ts from "typescript";
|
|
||||||
export default function stripServerSideLogic({ txt_code, file_path }) {
|
|
||||||
const file_dir_name = path.dirname(file_path);
|
|
||||||
// 1. Initial Parse of the source
|
|
||||||
const sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
||||||
const printer = ts.createPrinter();
|
|
||||||
/**
|
|
||||||
* PASS 1: Remove 'server' export and resolve absolute paths
|
|
||||||
*/
|
|
||||||
const stripTransformer = (context) => (rootNode) => {
|
|
||||||
const visitor = (node) => {
|
|
||||||
// Remove 'export const server'
|
|
||||||
if (ts.isVariableStatement(node) &&
|
|
||||||
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
||||||
if (node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) &&
|
|
||||||
d.name.text === "server")) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Convert relative imports to absolute
|
|
||||||
if (ts.isImportDeclaration(node)) {
|
|
||||||
const specifier = node.moduleSpecifier;
|
|
||||||
if (ts.isStringLiteral(specifier) &&
|
|
||||||
specifier.text.startsWith(".")) {
|
|
||||||
const absolutePath = path.resolve(file_dir_name, specifier.text);
|
|
||||||
return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(absolutePath), node.attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ts.visitEachChild(node, visitor, context);
|
|
||||||
};
|
|
||||||
return ts.visitNode(rootNode, visitor);
|
|
||||||
};
|
|
||||||
const step1Result = ts.transform(sourceFile, [stripTransformer]);
|
|
||||||
const intermediateCode = printer.printFile(step1Result.transformed[0]);
|
|
||||||
// 2. Re-parse to get a "Clean Slate" AST
|
|
||||||
const cleanSource = ts.createSourceFile("clean.tsx", intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
||||||
/**
|
|
||||||
* PASS 2: Collect all used Identifiers and Prune Imports
|
|
||||||
*/
|
|
||||||
const usedIdentifiers = new Set();
|
|
||||||
function walkAndFindUsages(node) {
|
|
||||||
if (ts.isIdentifier(node)) {
|
|
||||||
// We only care about identifiers that AREN'T the names in the import statements themselves
|
|
||||||
const parent = node.parent;
|
|
||||||
const isImportName = ts.isImportSpecifier(parent) ||
|
|
||||||
ts.isImportClause(parent) ||
|
|
||||||
ts.isNamespaceImport(parent);
|
|
||||||
if (!isImportName) {
|
|
||||||
usedIdentifiers.add(node.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ts.forEachChild(node, walkAndFindUsages);
|
|
||||||
}
|
|
||||||
walkAndFindUsages(cleanSource);
|
|
||||||
const cleanupTransformer = (context) => (rootNode) => {
|
|
||||||
const visitor = (node) => {
|
|
||||||
if (ts.isImportDeclaration(node)) {
|
|
||||||
const clause = node.importClause;
|
|
||||||
if (!clause)
|
|
||||||
return node;
|
|
||||||
// 1. Clean Named Imports: { A, B }
|
|
||||||
if (clause.namedBindings &&
|
|
||||||
ts.isNamedImports(clause.namedBindings)) {
|
|
||||||
const activeElements = clause.namedBindings.elements.filter((el) => usedIdentifiers.has(el.name.text));
|
|
||||||
// If no named imports are used and there is no default import, nix the whole line
|
|
||||||
if (activeElements.length === 0 && !clause.name)
|
|
||||||
return undefined;
|
|
||||||
// If we have some named imports left, update the node
|
|
||||||
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly,
|
|
||||||
// Only keep default import if it's used
|
|
||||||
clause.name &&
|
|
||||||
usedIdentifiers.has(clause.name.text)
|
|
||||||
? clause.name
|
|
||||||
: undefined, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes);
|
|
||||||
}
|
|
||||||
// 2. Clean Default Imports: import X from '...'
|
|
||||||
if (clause.name && !usedIdentifiers.has(clause.name.text)) {
|
|
||||||
// If there are no named bindings attached, nix the whole line
|
|
||||||
if (!clause.namedBindings)
|
|
||||||
return undefined;
|
|
||||||
// Otherwise, just strip the default name and keep the named bindings
|
|
||||||
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, undefined, clause.namedBindings), node.moduleSpecifier, node.attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ts.visitEachChild(node, visitor, context);
|
|
||||||
};
|
|
||||||
return ts.visitNode(rootNode, visitor);
|
|
||||||
};
|
|
||||||
const finalResult = ts.transform(cleanSource, [cleanupTransformer]);
|
|
||||||
return printer.printFile(finalResult.transformed[0]);
|
|
||||||
}
|
|
||||||
Vendored
+9
-2
@@ -3,7 +3,6 @@ import type { FileSystemRouter, Server } from "bun";
|
|||||||
import { type DirNames } from "../utils/grab-dir-names";
|
import { type DirNames } from "../utils/grab-dir-names";
|
||||||
import type { BuildContext } from "esbuild";
|
import type { BuildContext } from "esbuild";
|
||||||
import grabConstants from "../utils/grab-constants";
|
import grabConstants from "../utils/grab-constants";
|
||||||
import { type FSWatcher } from "fs";
|
|
||||||
/**
|
/**
|
||||||
* # Declare Global Variables
|
* # Declare Global Variables
|
||||||
*/
|
*/
|
||||||
@@ -24,7 +23,7 @@ declare global {
|
|||||||
[k: string]: BundlerCTXMap;
|
[k: string]: BundlerCTXMap;
|
||||||
};
|
};
|
||||||
var BUNEXT_BUNDLER_REBUILDS: 0;
|
var BUNEXT_BUNDLER_REBUILDS: 0;
|
||||||
var BUNEXT_PAGES_SRC_WATCHER: FSWatcher | undefined;
|
var BUNEXT_PAGES_SRC_WATCHER: any;
|
||||||
var BUNEXT_CURRENT_VERSION: string | undefined;
|
var BUNEXT_CURRENT_VERSION: string | undefined;
|
||||||
var BUNEXT_PAGE_FILES: PageFiles[];
|
var BUNEXT_PAGE_FILES: PageFiles[];
|
||||||
var BUNEXT_ROOT_FILE_UPDATED: boolean;
|
var BUNEXT_ROOT_FILE_UPDATED: boolean;
|
||||||
@@ -44,8 +43,16 @@ declare global {
|
|||||||
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||||
var BUNEXT_REBUILD_RETRIES: number;
|
var BUNEXT_REBUILD_RETRIES: number;
|
||||||
var BUNEXT_IS_404_PAGE: boolean;
|
var BUNEXT_IS_404_PAGE: boolean;
|
||||||
|
var BUNEXT_BUILD_FAILED: boolean;
|
||||||
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
||||||
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
||||||
|
/**
|
||||||
|
* Resolves when the one-time dev startup build (hydration + SSR for all
|
||||||
|
* pages) has completed. The dev server starts listening before this
|
||||||
|
* resolves; page requests await it so they never render against
|
||||||
|
* half-built modules.
|
||||||
|
*/
|
||||||
|
var BUNEXT_INITIAL_BUILD_PROMISE: Promise<void> | undefined;
|
||||||
}
|
}
|
||||||
type Params = {
|
type Params = {
|
||||||
build_only?: boolean;
|
build_only?: boolean;
|
||||||
|
|||||||
Vendored
+18
-3
@@ -8,6 +8,7 @@ import serverPostBuildFn from "./server/server-post-build-fn";
|
|||||||
import reactModulesBundler from "./bundler/react-modules-bundler";
|
import reactModulesBundler from "./bundler/react-modules-bundler";
|
||||||
import grabConstants from "../utils/grab-constants";
|
import grabConstants from "../utils/grab-constants";
|
||||||
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
|
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
|
||||||
|
import disposeBundlerContexts from "./bundler/dispose-bundler-contexts";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
const dirNames = grabDirNames();
|
const dirNames = grabDirNames();
|
||||||
const { PAGES_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = dirNames;
|
const { PAGES_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = dirNames;
|
||||||
@@ -40,6 +41,7 @@ export default async function bunextInit(params) {
|
|||||||
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
||||||
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
||||||
global.BUNEXT_REBUILD_RETRIES = 0;
|
global.BUNEXT_REBUILD_RETRIES = 0;
|
||||||
|
global.BUNEXT_BUILD_FAILED = false;
|
||||||
global.BUNEXT_PAGE_FILES = [];
|
global.BUNEXT_PAGE_FILES = [];
|
||||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||||
global.BUNEXT_DIR_NAMES = dirNames;
|
global.BUNEXT_DIR_NAMES = dirNames;
|
||||||
@@ -61,11 +63,21 @@ export default async function bunextInit(params) {
|
|||||||
}
|
}
|
||||||
else if (is_dev) {
|
else if (is_dev) {
|
||||||
log.build(`Building Modules ...`);
|
log.build(`Building Modules ...`);
|
||||||
await allPagesESBuildContextBundler({
|
// Do NOT block on the full-site build: `Bun.serve` (or the start
|
||||||
post_build_fn: async () => {
|
// server) opens its port immediately and requests await
|
||||||
await serverPostBuildFn();
|
// BUNEXT_INITIAL_BUILD_PROMISE, so the site becomes reachable while
|
||||||
|
// the one-time compile still runs in the background.
|
||||||
|
const initial_build_promise = allPagesESBuildContextBundler({
|
||||||
|
post_build_fn: async (params) => {
|
||||||
|
await serverPostBuildFn(params);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
global.BUNEXT_INITIAL_BUILD_PROMISE = initial_build_promise;
|
||||||
|
// Errors are logged (deduped) by the build pipeline itself; swallow
|
||||||
|
// the rejection here so it isn't an unhandled rejection.
|
||||||
|
initial_build_promise.catch(() => { });
|
||||||
|
// Start watching immediately so edits made during the initial build
|
||||||
|
// are not missed (rebuilds coalesce with the in-flight build).
|
||||||
watcherEsbuildCTX();
|
watcherEsbuildCTX();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -75,6 +87,9 @@ export default async function bunextInit(params) {
|
|||||||
else {
|
else {
|
||||||
log.build(`Building Modules ...`);
|
log.build(`Building Modules ...`);
|
||||||
await allPagesESBuildContextBundler({ start: true });
|
await allPagesESBuildContextBundler({ start: true });
|
||||||
|
// The server only needs the artifact maps; don't keep
|
||||||
|
// esbuild's build state alive for the rest of the process.
|
||||||
|
await disposeBundlerContexts({ stop_service: true });
|
||||||
}
|
}
|
||||||
cron();
|
cron();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,8 +1,14 @@
|
|||||||
type Params = {
|
type Params = {
|
||||||
key: string;
|
key?: string;
|
||||||
|
/**
|
||||||
|
* Hashed key, as found in cache file names. Used when the original
|
||||||
|
* key is no longer known (e.g. when trimming from a directory listing).
|
||||||
|
*/
|
||||||
|
cache_id?: string;
|
||||||
paradigm?: "html" | "json";
|
paradigm?: "html" | "json";
|
||||||
};
|
};
|
||||||
export default function grabCacheNames({ key, paradigm }: Params): {
|
export default function grabCacheNames({ key, cache_id, paradigm, }: Params): {
|
||||||
|
cache_id: string;
|
||||||
cache_name: string;
|
cache_name: string;
|
||||||
cache_meta_name: string;
|
cache_meta_name: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,9 @@
|
|||||||
export default function grabCacheNames({ key, paradigm = "html" }) {
|
export default function grabCacheNames({ key, cache_id, paradigm = "html", }) {
|
||||||
const parsed_key = encodeURIComponent(key);
|
// Hashing keeps file names short and fixed-length no matter how long
|
||||||
|
// the request path or query string is.
|
||||||
|
const parsed_key = cache_id ||
|
||||||
|
new Bun.CryptoHasher("sha256").update(key || "").digest("hex");
|
||||||
const cache_name = `${parsed_key}.res.${paradigm}`;
|
const cache_name = `${parsed_key}.res.${paradigm}`;
|
||||||
const cache_meta_name = `${parsed_key}.meta.json`;
|
const cache_meta_name = `${parsed_key}.meta.json`;
|
||||||
return { cache_name, cache_meta_name };
|
return { cache_id: parsed_key, cache_name, cache_meta_name };
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -9,14 +9,8 @@ export default async function trimAllCache() {
|
|||||||
const cached_item = cached_items[i];
|
const cached_item = cached_items[i];
|
||||||
if (!cached_item.endsWith(`.meta.json`))
|
if (!cached_item.endsWith(`.meta.json`))
|
||||||
continue;
|
continue;
|
||||||
const cache_key = decodeURIComponent(cached_item.replace(/\.meta\.json/, ""));
|
const cache_id = cached_item.replace(/\.meta\.json$/, "");
|
||||||
const trim_key = await trimCacheKey({
|
await trimCacheKey({ cache_id });
|
||||||
key: cache_key,
|
|
||||||
});
|
|
||||||
if (trim_key.success) {
|
|
||||||
cached_items.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
import type { APIResponseObject } from "../../types";
|
import type { APIResponseObject } from "../../types";
|
||||||
type Params = {
|
type Params = {
|
||||||
key: string;
|
key?: string;
|
||||||
|
cache_id?: string;
|
||||||
};
|
};
|
||||||
export default function trimCacheKey({ key, }: Params): Promise<APIResponseObject>;
|
export default function trimCacheKey({ key, cache_id, }: Params): Promise<APIResponseObject>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+11
-8
@@ -1,21 +1,24 @@
|
|||||||
import { readFileSync, unlinkSync } from "fs";
|
import { readFileSync, rmSync } from "fs";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import grabCacheNames from "./grab-cache-names";
|
import grabCacheNames from "./grab-cache-names";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppData } from "../../data/app-data";
|
import { AppData } from "../../data/app-data";
|
||||||
export default async function trimCacheKey({ key, }) {
|
export default async function trimCacheKey({ key, cache_id, }) {
|
||||||
try {
|
try {
|
||||||
const { BUNEXT_CACHE_DIR } = grabDirNames();
|
const { BUNEXT_CACHE_DIR } = grabDirNames();
|
||||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
const { cache_meta_name } = grabCacheNames({ key, cache_id });
|
||||||
key,
|
|
||||||
});
|
|
||||||
const config = global.BUNEXT_CONFIG;
|
const config = global.BUNEXT_CONFIG;
|
||||||
const default_expiry_time_seconds = config.default_cache_expiry ||
|
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_meta_path = path.join(BUNEXT_CACHE_DIR, cache_meta_name);
|
const cache_meta_path = path.join(BUNEXT_CACHE_DIR, cache_meta_name);
|
||||||
const cache_meta = JSON.parse(readFileSync(cache_meta_path, "utf-8"));
|
const cache_meta = JSON.parse(readFileSync(cache_meta_path, "utf-8"));
|
||||||
|
const { cache_name } = grabCacheNames({
|
||||||
|
key,
|
||||||
|
cache_id,
|
||||||
|
paradigm: cache_meta.paradigm,
|
||||||
|
});
|
||||||
|
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
|
||||||
const expiry_milliseconds = cache_meta.expiry_seconds
|
const expiry_milliseconds = cache_meta.expiry_seconds
|
||||||
? cache_meta.expiry_seconds * 1000
|
? cache_meta.expiry_seconds * 1000
|
||||||
: default_expiry_time_milliseconds;
|
: default_expiry_time_milliseconds;
|
||||||
@@ -25,8 +28,8 @@ export default async function trimCacheKey({ key, }) {
|
|||||||
msg: `Cache has not expired yet`,
|
msg: `Cache has not expired yet`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
unlinkSync(cache_content_path);
|
rmSync(cache_content_path, { force: true });
|
||||||
unlinkSync(cache_meta_path);
|
rmSync(cache_meta_path, { force: true });
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+12
-2
@@ -1,7 +1,8 @@
|
|||||||
import { existsSync, writeFileSync } from "fs";
|
import { existsSync, readdirSync, writeFileSync } from "fs";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import grabCacheNames from "./grab-cache-names";
|
import grabCacheNames from "./grab-cache-names";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
export default async function writeCache({ key, value, paradigm = "html", expiry_seconds, }) {
|
export default async function writeCache({ key, value, paradigm = "html", expiry_seconds, }) {
|
||||||
try {
|
try {
|
||||||
const { BUNEXT_CACHE_DIR } = grabDirNames();
|
const { BUNEXT_CACHE_DIR } = grabDirNames();
|
||||||
@@ -16,7 +17,16 @@ export default async function writeCache({ key, value, paradigm = "html", expiry
|
|||||||
msg: `Cache entry already exists`,
|
msg: `Cache entry already exists`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
writeFileSync(path.join(target_path), value);
|
const max_entries = global.BUNEXT_CONFIG?.max_cache_entries ||
|
||||||
|
AppData["DefaultMaxCacheEntries"];
|
||||||
|
const entries_count = readdirSync(BUNEXT_CACHE_DIR).filter((f) => f.endsWith(".meta.json")).length;
|
||||||
|
if (entries_count >= max_entries) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Cache entries limit (${max_entries}) reached`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
writeFileSync(target_path, value);
|
||||||
const cache_file_meta = {
|
const cache_file_meta = {
|
||||||
date_created: Date.now(),
|
date_created: Date.now(),
|
||||||
paradigm,
|
paradigm,
|
||||||
|
|||||||
Vendored
+5
-1
@@ -14,6 +14,10 @@ export default async function grabConfig() {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return undefined;
|
// A config that fails to load (syntax error, bad export) must not be
|
||||||
|
// silently replaced by defaults — the app would run with the wrong
|
||||||
|
// configuration and nothing would explain why.
|
||||||
|
const { CONFIG_FILE } = grabDirNames();
|
||||||
|
exitWithError(`Failed to load config file \`${CONFIG_FILE}\`: ${error?.message || error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
Vendored
-46
@@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
+31
-8
@@ -7,20 +7,28 @@ import handleFiles from "./handle-files";
|
|||||||
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
||||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||||
import { AppData } from "../../data/app-data";
|
import { AppData } from "../../data/app-data";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
import fullRebuild from "./full-rebuild";
|
import fullRebuild from "./full-rebuild";
|
||||||
|
import grabRouter from "../../utils/grab-router";
|
||||||
|
import { NoSEORobotsValue } from "../../utils/disable-seo";
|
||||||
|
import generateRobotsTxt from "../../utils/generate-robots-txt";
|
||||||
const HMR_RETRY_COOLDOWN_MS = 5000;
|
const HMR_RETRY_COOLDOWN_MS = 5000;
|
||||||
let lastHmrRetryTime = 0;
|
let lastHmrRetryTime = 0;
|
||||||
export default async function bunextRequestHandler({ req: initial_req, server, }) {
|
export default async function bunextRequestHandler({ req: initial_req, server, }) {
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
let req = initial_req.clone();
|
const middleware = global.BUNEXT_CONSTANTS.config?.middleware;
|
||||||
|
// Middleware may consume `initial_req`'s body, so it gets its own copy.
|
||||||
|
// Cloning tees the body stream and buffers whatever one side doesn't
|
||||||
|
// read, so only pay for it when middleware is configured.
|
||||||
|
let req = middleware ? initial_req.clone() : initial_req;
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url);
|
let url = new URL(req.url);
|
||||||
if (checkExcludedPatterns({ path: url.pathname })) {
|
if (checkExcludedPatterns({ path: url.pathname })) {
|
||||||
return Response.json({ success: false, msg: `Invalid Path` });
|
return Response.json({ success: false, msg: `Invalid Path` }, { status: 404 });
|
||||||
}
|
}
|
||||||
let response = undefined;
|
let response = undefined;
|
||||||
if (global.BUNEXT_CONSTANTS.config?.middleware) {
|
if (middleware) {
|
||||||
const middleware_res = await global.BUNEXT_CONSTANTS.config.middleware({
|
const middleware_res = await middleware({
|
||||||
req: initial_req,
|
req: initial_req,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
@@ -29,6 +37,7 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
|
|||||||
}
|
}
|
||||||
if (middleware_res instanceof Request) {
|
if (middleware_res instanceof Request) {
|
||||||
req = middleware_res;
|
req = middleware_res;
|
||||||
|
url = new URL(req.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
|
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
|
||||||
@@ -40,9 +49,15 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
|
|||||||
await fullRebuild({ msg: `HMR Retry Rebuild ...` });
|
await fullRebuild({ msg: `HMR Retry Rebuild ...` });
|
||||||
return new Response("Modules Rebuilt");
|
return new Response("Modules Rebuilt");
|
||||||
}
|
}
|
||||||
|
const disable_seo = global.BUNEXT_CONSTANTS.config?.disable_seo;
|
||||||
if (url.pathname === "/__hmr" && is_dev) {
|
if (url.pathname === "/__hmr" && is_dev) {
|
||||||
return handleHmr({ req });
|
return handleHmr({ req });
|
||||||
}
|
}
|
||||||
|
else if (disable_seo && url.pathname == "/robots.txt") {
|
||||||
|
response = new Response(generateRobotsTxt({ disallow_all: true }), {
|
||||||
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||||
|
});
|
||||||
|
}
|
||||||
else if (url.pathname.startsWith("/.bunext")) {
|
else if (url.pathname.startsWith("/.bunext")) {
|
||||||
response = await handleBunextPublicAssets({ req });
|
response = await handleBunextPublicAssets({ req });
|
||||||
}
|
}
|
||||||
@@ -52,8 +67,13 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
|
|||||||
else if (url.pathname.startsWith("/public/")) {
|
else if (url.pathname.startsWith("/public/")) {
|
||||||
response = await handlePublic({ req });
|
response = await handlePublic({ req });
|
||||||
}
|
}
|
||||||
else if (url.pathname.match(/\..*$/)) {
|
else if (url.pathname.match(/\.[^/]+$/)) {
|
||||||
response = await handleFiles({ req });
|
response = await handleFiles({ req });
|
||||||
|
// Paths like `/users/john.doe` look like files but can be pages.
|
||||||
|
if (response.status == 404 &&
|
||||||
|
grabRouter()?.match(url.pathname)?.filePath) {
|
||||||
|
response = await handleWebPages({ req });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
response = await handleWebPages({ req });
|
response = await handleWebPages({ req });
|
||||||
@@ -61,18 +81,21 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
|
|||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error(`No Response generated`);
|
throw new Error(`No Response generated`);
|
||||||
}
|
}
|
||||||
|
if (disable_seo) {
|
||||||
|
response.headers.set("X-Robots-Tag", NoSEORobotsValue);
|
||||||
|
}
|
||||||
if (is_dev) {
|
if (is_dev) {
|
||||||
response.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
response.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
log.error(`Server Error: ${error?.message || error}\n${error?.stack || ""}`);
|
||||||
if (is_dev) {
|
if (is_dev) {
|
||||||
return new Response(`Server Error: ${error.message}`, {
|
return new Response(`Server Error: ${error?.message || error}`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.error(`Server Error: ${error.message}`, error);
|
|
||||||
return new Response("Internal Server Error", { status: 500 });
|
return new Response("Internal Server Error", { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export default function chokadirWatcherEsbuildCTX(): Promise<void>;
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-3
@@ -1,6 +1,14 @@
|
|||||||
import type { BundlerCTXMap } from "../../types";
|
|
||||||
type Params = {
|
type Params = {
|
||||||
new_artifacts: BundlerCTXMap[];
|
paradigm: "hydration" | "ssr";
|
||||||
};
|
};
|
||||||
export default function cleanupArtifacts({ new_artifacts }: Params): Promise<void>;
|
/**
|
||||||
|
* Deletes orphaned entry outputs left behind by previous rebuilds.
|
||||||
|
*
|
||||||
|
* After a rebuild the page/SSR artifact maps only reference the newest
|
||||||
|
* hashed entry files; every previously hashed entry for the same directory
|
||||||
|
* is provably unreferenced (generated HTML always uses the current map) and
|
||||||
|
* is removed. Chunks, non-hashed fallback module files and `map.json` are
|
||||||
|
* left untouched.
|
||||||
|
*/
|
||||||
|
export default function cleanupArtifacts({ paradigm }: Params): Promise<void>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+80
-22
@@ -2,32 +2,90 @@ import { log } from "../../utils/log";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
||||||
const { ROOT_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME } = grabDirNames();
|
const { ROOT_DIR, HYDRATION_DST_DIR, BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
export default async function cleanupArtifacts({ new_artifacts }) {
|
// esbuild's content hashes are 8-char base64-ish (e.g. `CMSI7VSQ`). Entry
|
||||||
|
// outputs are `<dir>/<hash>.js` / `<dir>/<hash>.css`. Shared chunks are
|
||||||
|
// `chunk-*.js` and MUST be kept: any current entry may still reference an
|
||||||
|
// older chunk (esbuild reuses unchanged chunks across rebuilds), and
|
||||||
|
// verifying references would require parsing every entry's imports.
|
||||||
|
const ENTRY_HASH_FILE = /^[A-Za-z0-9]{8}\.(js|css)$/;
|
||||||
|
/**
|
||||||
|
* Deletes orphaned entry outputs left behind by previous rebuilds.
|
||||||
|
*
|
||||||
|
* After a rebuild the page/SSR artifact maps only reference the newest
|
||||||
|
* hashed entry files; every previously hashed entry for the same directory
|
||||||
|
* is provably unreferenced (generated HTML always uses the current map) and
|
||||||
|
* is removed. Chunks, non-hashed fallback module files and `map.json` are
|
||||||
|
* left untouched.
|
||||||
|
*/
|
||||||
|
export default async function cleanupArtifacts({ paradigm }) {
|
||||||
|
const is_hydration = paradigm === "hydration";
|
||||||
|
const root_dir = is_hydration
|
||||||
|
? HYDRATION_DST_DIR
|
||||||
|
: BUNX_CWD_MODULE_CACHE_DIR;
|
||||||
|
const map = is_hydration
|
||||||
|
? global.BUNEXT_BUNDLER_CTX_MAP
|
||||||
|
: global.BUNEXT_SSR_BUNDLER_CTX_MAP;
|
||||||
|
if (!map || !existsSync(root_dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < new_artifacts.length; i++) {
|
const keep_by_dir = new Map();
|
||||||
const new_artifact = new_artifacts[i];
|
for (const entry of Object.values(map)) {
|
||||||
const artifact_public_dir = path.dirname(path.join(ROOT_DIR, new_artifact.path));
|
if (!entry?.path)
|
||||||
const dir_content = readdirSync(artifact_public_dir);
|
continue;
|
||||||
for (let d = 0; d < dir_content.length; d++) {
|
const full_path = path.join(ROOT_DIR, entry.path);
|
||||||
const dir_or_file = dir_content[d];
|
const dir_path = path.dirname(full_path);
|
||||||
const full_path = path.join(artifact_public_dir, dir_or_file);
|
let keep = keep_by_dir.get(dir_path);
|
||||||
const file_or_path_stats = statSync(full_path);
|
if (!keep) {
|
||||||
if (file_or_path_stats.isDirectory() ||
|
keep = new Set();
|
||||||
dir_or_file == HYDRATION_DST_DIR_MAP_JSON_FILE_NAME) {
|
keep_by_dir.set(dir_path, keep);
|
||||||
continue;
|
}
|
||||||
}
|
keep.add(path.basename(full_path));
|
||||||
if (new_artifact.path.includes(dir_or_file) ||
|
if (entry.css_path) {
|
||||||
new_artifact.css_path?.includes(dir_or_file)) {
|
keep.add(path.basename(entry.css_path));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (existsSync(full_path)) {
|
|
||||||
unlinkSync(full_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sweepDir(root_dir, keep_by_dir);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(error);
|
log.error(`Cleanup Artifacts Error: ${error?.message || error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sweepDir(dir_path, keep_by_dir) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir_path);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const name of entries) {
|
||||||
|
const full_path = path.join(dir_path, name);
|
||||||
|
let is_dir = false;
|
||||||
|
try {
|
||||||
|
is_dir = statSync(full_path).isDirectory();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_dir) {
|
||||||
|
sweepDir(full_path, keep_by_dir);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!name.match(ENTRY_HASH_FILE)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (name.startsWith("chunk-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const keep = keep_by_dir.get(path.dirname(full_path));
|
||||||
|
if (keep?.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unlinkSync(full_path);
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-15
@@ -1,8 +1,33 @@
|
|||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
||||||
import serverPostBuildFn from "./server-post-build-fn";
|
import serverPostBuildFn from "./server-post-build-fn";
|
||||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
import { restartWatcher } from "./watcher-esbuild-ctx";
|
||||||
|
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||||
|
import disposeBundlerContexts from "../bundler/dispose-bundler-contexts";
|
||||||
|
import isDevelopment from "../../utils/is-development";
|
||||||
|
// Single-flight: error recovery paths (grab-page-component retries, the
|
||||||
|
// watcher, post-build HMR pushes) all call fullRebuild, and each rebuild
|
||||||
|
// triggers serverPostBuildFn which can re-enter error handling. Without this
|
||||||
|
// guard one broken page could fan out into cascading, overlapping rebuilds
|
||||||
|
// that leave the dev server unresponsive. Concurrent callers await the same
|
||||||
|
// in-flight rebuild instead of starting a new one.
|
||||||
|
let rebuild_in_progress;
|
||||||
export default async function fullRebuild(params) {
|
export default async function fullRebuild(params) {
|
||||||
|
if (rebuild_in_progress) {
|
||||||
|
if (params?.msg) {
|
||||||
|
log.watch(`Rebuild already in progress, skipping: ${params.msg}`);
|
||||||
|
}
|
||||||
|
return rebuild_in_progress;
|
||||||
|
}
|
||||||
|
const run = doFullRebuild(params).finally(() => {
|
||||||
|
if (rebuild_in_progress === run) {
|
||||||
|
rebuild_in_progress = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rebuild_in_progress = run;
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
async function doFullRebuild(params) {
|
||||||
try {
|
try {
|
||||||
const { msg } = params || {};
|
const { msg } = params || {};
|
||||||
global.BUNEXT_RECOMPILING = true;
|
global.BUNEXT_RECOMPILING = true;
|
||||||
@@ -10,28 +35,25 @@ export default async function fullRebuild(params) {
|
|||||||
log.watch(msg);
|
log.watch(msg);
|
||||||
}
|
}
|
||||||
global.BUNEXT_ROUTER.reload();
|
global.BUNEXT_ROUTER.reload();
|
||||||
try {
|
await disposeBundlerContexts();
|
||||||
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({
|
await allPagesESBuildContextBundler({
|
||||||
post_build_fn: async () => {
|
refresh_props: true,
|
||||||
await serverPostBuildFn();
|
post_build_fn: async (params) => {
|
||||||
|
await serverPostBuildFn(params);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Production never rebuilds incrementally; don't keep esbuild's
|
||||||
|
// build state alive for the rest of the process.
|
||||||
|
if (!isDevelopment()) {
|
||||||
|
await disposeBundlerContexts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(error);
|
logBuildErrorsOnce([{ text: error?.message || String(error) }], (line) => log.error(line));
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
global.BUNEXT_RECOMPILING = false;
|
global.BUNEXT_RECOMPILING = false;
|
||||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
}
|
}
|
||||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
restartWatcher();
|
||||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
|
||||||
watcherEsbuildCTX();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ export default async function ({ req }) {
|
|||||||
try {
|
try {
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, ""));
|
const file_path = path.join(BUNEXT_PUBLIC_DIR, decodeURIComponent(url.pathname).replace(/\/\.bunext\/public\//, ""));
|
||||||
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
|
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
|
||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ 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, decodeURIComponent(url.pathname));
|
||||||
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
|
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
|
||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ 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, decodeURIComponent(url.pathname).replace(/^\/public/, ""));
|
||||||
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
|
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
|
||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-84
@@ -2,103 +2,141 @@ 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 { log } from "../../utils/log";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import grabRelativePath from "../../utils/grab-relative-path";
|
import grabRelativePath from "../../utils/grab-relative-path";
|
||||||
|
import { RequestBodyTooLargeError } from "../../utils/read-request-body";
|
||||||
|
import importFresh from "../../utils/import-fresh";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function ({ req }) {
|
export default async function ({ req }) {
|
||||||
const url = new URL(req.url);
|
try {
|
||||||
const is_dev = isDevelopment();
|
const url = new URL(req.url);
|
||||||
const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = grabConstants();
|
const is_dev = isDevelopment();
|
||||||
const router = grabRouter();
|
const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = grabConstants();
|
||||||
const match = router.match(url.pathname);
|
const router = grabRouter();
|
||||||
if (!match?.filePath) {
|
const match = router.match(url.pathname);
|
||||||
const errMsg = `Route ${url.pathname} not found`;
|
if (!match?.filePath) {
|
||||||
|
const errMsg = `Route ${url.pathname} not found`;
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
msg: errMsg,
|
||||||
|
}, {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let module;
|
||||||
|
const relative_match_path = grabRelativePath(match.filePath);
|
||||||
|
if (is_dev &&
|
||||||
|
global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[relative_match_path]?.path) {
|
||||||
|
const target_import = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_match_path].path);
|
||||||
|
module = await importFresh(target_import);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
module = await importFresh(match.filePath);
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
if (contentLength && parseInt(contentLength, 10) > maxBodyBytes) {
|
||||||
|
return bodyTooLargeResponse();
|
||||||
|
}
|
||||||
|
let routeParams;
|
||||||
|
try {
|
||||||
|
routeParams = await grabRouteParams({
|
||||||
|
req,
|
||||||
|
query: match.query,
|
||||||
|
max_body_bytes: maxBodyBytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof RequestBodyTooLargeError) {
|
||||||
|
return bodyTooLargeResponse();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const target_module = (module["default"] ||
|
||||||
|
module["handler"]);
|
||||||
|
if (typeof target_module !== "function") {
|
||||||
|
log.error(`API Route ${url.pathname} has no default or \`handler\` export`);
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
msg: is_dev
|
||||||
|
? `Route ${url.pathname} has no default or \`handler\` export`
|
||||||
|
: "Internal Server Error",
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await target_module({
|
||||||
|
...routeParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`);
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
msg: routeErrorMessage(error),
|
||||||
|
}, {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res instanceof Response) {
|
||||||
|
if (is_dev) {
|
||||||
|
res.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
}
|
||||||
|
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` });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
log.error(`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`);
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: errMsg,
|
msg: routeErrorMessage(error),
|
||||||
}, {
|
}, {
|
||||||
status: 404,
|
status: 500,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const routeParams = await grabRouteParams({
|
}
|
||||||
req,
|
function bodyTooLargeResponse() {
|
||||||
query: match.query,
|
return Response.json({
|
||||||
});
|
success: false,
|
||||||
let module;
|
msg: "Request Body Too Large!",
|
||||||
const now = Date.now();
|
}, { status: 413 });
|
||||||
const relative_match_path = grabRelativePath(match.filePath);
|
}
|
||||||
if (is_dev &&
|
/**
|
||||||
global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[relative_match_path]?.path) {
|
* Error details can leak internals, so they are only exposed in
|
||||||
const target_import = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_match_path].path);
|
* development. They are always logged server-side.
|
||||||
module = await import(`${target_import}?t=${now}`);
|
*/
|
||||||
|
function routeErrorMessage(error) {
|
||||||
|
if (!isDevelopment()) {
|
||||||
|
return "Internal Server Error";
|
||||||
}
|
}
|
||||||
else {
|
return `Route handler error: ${error?.message || "Internal Server Error"}`;
|
||||||
const import_path = is_dev
|
|
||||||
? `${match.filePath}?t=${now}`
|
|
||||||
: match.filePath;
|
|
||||||
module = await import(import_path);
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
if (contentLength) {
|
|
||||||
const size = parseInt(contentLength, 10);
|
|
||||||
if (size > maxBodyBytes) {
|
|
||||||
return Response.json({
|
|
||||||
success: false,
|
|
||||||
msg: "Request Body Too Large!",
|
|
||||||
}, {
|
|
||||||
status: 413,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
if (res instanceof Response) {
|
|
||||||
if (is_dev) {
|
|
||||||
res.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
||||||
}
|
|
||||||
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` });
|
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -1,5 +1,23 @@
|
|||||||
type Params = {
|
type Params = {
|
||||||
reload_all_controllers?: boolean;
|
reload_all_controllers?: boolean;
|
||||||
|
/**
|
||||||
|
* Relative page paths whose client entry outputs changed in the last
|
||||||
|
* build. When provided with at least one entry, only controllers
|
||||||
|
* targeting those pages are re-rendered/re-pushed. When empty or
|
||||||
|
* omitted, every controller is refreshed (covers server-file-only
|
||||||
|
* changes, where hydration outputs are untouched but server props must
|
||||||
|
* still be re-fetched).
|
||||||
|
*/
|
||||||
|
changed_pages?: string[];
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* # Push a client-only HMR update
|
||||||
|
* Sends the new client bundle to every controller whose page is in
|
||||||
|
* `changed_pages`, without re-running server functions. The payload carries
|
||||||
|
* no `page_props`, so the browser keeps its current props and only swaps the
|
||||||
|
* hydration script/CSS. Used right after the hydration build so the browser
|
||||||
|
* doesn't wait for the SSR rebuild.
|
||||||
|
*/
|
||||||
|
export declare function pushClientUpdate(changed_pages: string[]): void;
|
||||||
export default function serverPostBuildFn(params?: Params): Promise<void>;
|
export default function serverPostBuildFn(params?: Params): Promise<void>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+50
-4
@@ -1,9 +1,46 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import grabPageComponent from "./web-pages/grab-page-component";
|
import grabPageComponent from "./web-pages/grab-page-component";
|
||||||
|
/**
|
||||||
|
* # Push a client-only HMR update
|
||||||
|
* Sends the new client bundle to every controller whose page is in
|
||||||
|
* `changed_pages`, without re-running server functions. The payload carries
|
||||||
|
* no `page_props`, so the browser keeps its current props and only swaps the
|
||||||
|
* hydration script/CSS. Used right after the hydration build so the browser
|
||||||
|
* doesn't wait for the SSR rebuild.
|
||||||
|
*/
|
||||||
|
export function pushClientUpdate(changed_pages) {
|
||||||
|
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const changed_set = new Set(changed_pages);
|
||||||
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
|
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||||
|
const local_path = controller?.target_map?.local_path;
|
||||||
|
if (!controller || !local_path || !changed_set.has(local_path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[local_path];
|
||||||
|
const payload = target_artifact?.path
|
||||||
|
? { target_map: target_artifact }
|
||||||
|
: { reload: true };
|
||||||
|
try {
|
||||||
|
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(payload)}\n\n`);
|
||||||
|
if (target_artifact?.path) {
|
||||||
|
controller.target_map = target_artifact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
export default async function serverPostBuildFn(params) {
|
export default async function serverPostBuildFn(params) {
|
||||||
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const changed_set = params?.changed_pages && params.changed_pages.length > 0
|
||||||
|
? new Set(params.changed_pages)
|
||||||
|
: undefined;
|
||||||
const reload_payload = { reload: true };
|
const reload_payload = { reload: true };
|
||||||
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
|
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
|
||||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||||
@@ -14,6 +51,10 @@ export default async function serverPostBuildFn(params) {
|
|||||||
if (!controller.target_map?.local_path) {
|
if (!controller.target_map?.local_path) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Only touch controllers whose page actually changed.
|
||||||
|
if (changed_set && !changed_set.has(controller.target_map.local_path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (params?.reload_all_controllers) {
|
if (params?.reload_all_controllers) {
|
||||||
try {
|
try {
|
||||||
controller.controller.enqueue(reload_enqueue);
|
controller.controller.enqueue(reload_enqueue);
|
||||||
@@ -33,9 +74,9 @@ export default async function serverPostBuildFn(params) {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mock_req = target_artifact.req_url
|
// Re-render with this tab's own URL and cookie, never another
|
||||||
? new Request(target_artifact.req_url, {})
|
// client's request.
|
||||||
: new Request(controller.page_url);
|
const mock_req = new Request(controller.page_url);
|
||||||
if (controller.page_cookie) {
|
if (controller.page_cookie) {
|
||||||
mock_req.headers.set("cookie", controller.page_cookie);
|
mock_req.headers.set("cookie", controller.page_cookie);
|
||||||
}
|
}
|
||||||
@@ -43,13 +84,18 @@ export default async function serverPostBuildFn(params) {
|
|||||||
req: mock_req,
|
req: mock_req,
|
||||||
return_server_res_only: true,
|
return_server_res_only: true,
|
||||||
is_hydration: true,
|
is_hydration: true,
|
||||||
|
// This runs inside serverPostBuildFn (post-build of fullRebuild);
|
||||||
|
// letting it trigger another rebuild/retry would cascade.
|
||||||
|
no_retry: true,
|
||||||
});
|
});
|
||||||
if (page_component instanceof Response) {
|
if (page_component instanceof Response) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { serverRes } = page_component || {};
|
const { serverRes } = page_component || {};
|
||||||
const final_artifact = {
|
const final_artifact = {
|
||||||
..._.omit(controller, ["controller"]),
|
// `page_cookie` holds the raw Cookie header (incl. HttpOnly
|
||||||
|
// cookies), so it must never be sent back to page JS.
|
||||||
|
..._.omit(controller, ["controller", "page_cookie", "page_url"]),
|
||||||
target_map: target_artifact,
|
target_map: target_artifact,
|
||||||
};
|
};
|
||||||
if (!target_artifact) {
|
if (!target_artifact) {
|
||||||
|
|||||||
+4
-7
@@ -2,7 +2,7 @@ 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 isDevelopment from "../../utils/is-development";
|
||||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
import { watcherWatchdogTick } 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);
|
||||||
@@ -10,12 +10,9 @@ export default async function startServer() {
|
|||||||
global.BUNEXT_SERVER = server;
|
global.BUNEXT_SERVER = server;
|
||||||
log.server(`http://${server.hostname}:${server.port}`);
|
log.server(`http://${server.hostname}:${server.port}`);
|
||||||
if (is_dev) {
|
if (is_dev) {
|
||||||
setInterval(() => {
|
// Watchdog: re-create the FS watcher if it went stale and stopped
|
||||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
// receiving events.
|
||||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
setInterval(watcherWatchdogTick, 5000);
|
||||||
watcherEsbuildCTX();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -1 +1,16 @@
|
|||||||
export default function watcherEsbuildCTX(): Promise<void>;
|
/**
|
||||||
|
* Closes and re-creates the FS watcher. Deduped: callers that restart the
|
||||||
|
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
|
||||||
|
* trigger a single re-creation.
|
||||||
|
*/
|
||||||
|
export declare function restartWatcher(): void;
|
||||||
|
/**
|
||||||
|
* Called periodically by the dev server. Re-creates the watcher only when it
|
||||||
|
* failed to report the probe written on the previous tick, i.e. when it went
|
||||||
|
* stale. A healthy watcher is left alone: re-creating it rescans the whole
|
||||||
|
* project tree.
|
||||||
|
*/
|
||||||
|
export declare function watcherWatchdogTick(): void;
|
||||||
|
export default function watcherEsbuildCTX(params?: {
|
||||||
|
pages_snapshot?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
|||||||
+322
-132
@@ -1,148 +1,338 @@
|
|||||||
import { watch, existsSync, statSync, glob } from "fs";
|
import chokidar from "chokidar";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
import fullRebuild from "./full-rebuild";
|
import fullRebuild from "./full-rebuild";
|
||||||
import { AppData } from "../../data/app-data";
|
import { AppData } from "../../data/app-data";
|
||||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||||
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
import allPagesESBuildContextBundler, { isHydrationEntryPage, } from "../bundler/all-pages-esbuild-context-bundler";
|
||||||
|
import grabAllPages from "../../utils/grab-all-pages";
|
||||||
|
import serverPostBuildFn from "./server-post-build-fn";
|
||||||
import { log } from "../../utils/log";
|
import { log } from "../../utils/log";
|
||||||
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||||
export default async function watcherEsbuildCTX() {
|
const { ROOT_DIR, BUNX_CWD_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||||
const pages_src_watcher = watch(ROOT_DIR, {
|
// The watchdog writes this file and expects the watcher to report it. It
|
||||||
recursive: true,
|
// sits under `.bunext` (otherwise ignored), so only this path is watched.
|
||||||
persistent: true,
|
const WATCH_PROBE_DIR = path.join(BUNX_CWD_DIR, "watch-probe");
|
||||||
}, async (event, filename) => {
|
const WATCH_PROBE_FILE = path.join(WATCH_PROBE_DIR, "probe");
|
||||||
let owns_recompile = false;
|
// Dedupes near-simultaneous restarts (fullRebuild, watchdog).
|
||||||
|
let last_watcher_restart = 0;
|
||||||
|
// The current watcher finished its initial scan; probes written before
|
||||||
|
// that may not produce events.
|
||||||
|
let watcher_ready = false;
|
||||||
|
// A probe was written and the watcher hasn't reported it yet.
|
||||||
|
let probe_pending = false;
|
||||||
|
/**
|
||||||
|
* Closes and re-creates the FS watcher. Deduped: callers that restart the
|
||||||
|
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
|
||||||
|
* trigger a single re-creation.
|
||||||
|
*/
|
||||||
|
export function restartWatcher() {
|
||||||
|
if (!global.BUNEXT_PAGES_SRC_WATCHER)
|
||||||
|
return;
|
||||||
|
if (Date.now() - last_watcher_restart < 1000)
|
||||||
|
return;
|
||||||
|
// Files created between close() and the new watcher's initial scan
|
||||||
|
// completing emit no `add` (ignoreInitial treats them as pre-existing).
|
||||||
|
// Snapshot the page set so the new watcher can detect them on `ready`.
|
||||||
|
const pages_snapshot = grabPagesSnapshot();
|
||||||
|
try {
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
global.BUNEXT_PAGES_SRC_WATCHER = undefined;
|
||||||
|
last_watcher_restart = Date.now();
|
||||||
|
watcherEsbuildCTX({ pages_snapshot });
|
||||||
|
}
|
||||||
|
function grabPagesSnapshot() {
|
||||||
|
try {
|
||||||
|
return grabAllPages()
|
||||||
|
.map((p) => p.local_path)
|
||||||
|
.sort()
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Called periodically by the dev server. Re-creates the watcher only when it
|
||||||
|
* failed to report the probe written on the previous tick, i.e. when it went
|
||||||
|
* stale. A healthy watcher is left alone: re-creating it rescans the whole
|
||||||
|
* project tree.
|
||||||
|
*/
|
||||||
|
export function watcherWatchdogTick() {
|
||||||
|
if (!global.BUNEXT_PAGES_SRC_WATCHER || !watcher_ready)
|
||||||
|
return;
|
||||||
|
if (probe_pending) {
|
||||||
|
restartWatcher();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mkdirSync(WATCH_PROBE_DIR, { recursive: true });
|
||||||
|
writeFileSync(WATCH_PROBE_FILE, String(Date.now()));
|
||||||
|
probe_pending = true;
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
}
|
||||||
|
function isWatchProbePath(p) {
|
||||||
|
return (p === WATCH_PROBE_FILE || p === WATCH_PROBE_DIR || p === BUNX_CWD_DIR);
|
||||||
|
}
|
||||||
|
const TARGET_FILES_MATCH = /\.(tsx?|jsx?|css)$/;
|
||||||
|
// Rebuild coalescing: editors emit bursts of events per save (change +
|
||||||
|
// atomic-swap noise), and edits can land while a rebuild is running.
|
||||||
|
// Events are debounced into a single runner; the runner loops until the
|
||||||
|
// event queue drains, so a mid-build edit is never silently dropped.
|
||||||
|
const REBUILD_DEBOUNCE_MS = 50;
|
||||||
|
let pending_structural = false;
|
||||||
|
let pending_content_file = undefined;
|
||||||
|
let debounce_timer = undefined;
|
||||||
|
// Guards against overlapping runners: BUNEXT_RECOMPILING alone is not enough
|
||||||
|
// since the artifact tracker clears it mid-run.
|
||||||
|
let runner_active = false;
|
||||||
|
function scheduleRebuild() {
|
||||||
|
if (debounce_timer) {
|
||||||
|
clearTimeout(debounce_timer);
|
||||||
|
}
|
||||||
|
debounce_timer = setTimeout(async () => {
|
||||||
|
debounce_timer = undefined;
|
||||||
|
// Another rebuild (fullRebuild, or a runner already looping) owns the
|
||||||
|
// bundler. Retry later rather than dropping the pending events.
|
||||||
|
if (runner_active || global.BUNEXT_RECOMPILING) {
|
||||||
|
scheduleRebuild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!filename)
|
await runRebuilds();
|
||||||
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) {
|
catch (error) {
|
||||||
log.error(`Watcher rebuild failed: ${error}`);
|
const msg = String(error?.message || error || "");
|
||||||
}
|
if (!msg.startsWith("Build failed with")) {
|
||||||
finally {
|
logBuildErrorsOnce([{ text: `Watcher rebuild failed: ${msg}` }], (line) => log.error(line));
|
||||||
if (owns_recompile) {
|
|
||||||
global.BUNEXT_RECOMPILING = false;
|
|
||||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, REBUILD_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Processes the pending event batch. Structural changes go through the
|
||||||
|
* all-pages bundler (which re-creates esbuild contexts only when the entry
|
||||||
|
* set really changed, so atomic-swap saves resolve to incremental rebuilds).
|
||||||
|
* Content changes rebuild the hydration context incrementally; its artifact
|
||||||
|
* tracker then queues the single-flighted SSR rebuild and pushes HMR updates.
|
||||||
|
*/
|
||||||
|
async function runRebuilds() {
|
||||||
|
runner_active = true;
|
||||||
|
global.BUNEXT_RECOMPILING = true;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const structural = pending_structural;
|
||||||
|
const content_file = pending_content_file;
|
||||||
|
pending_structural = false;
|
||||||
|
pending_content_file = undefined;
|
||||||
|
if (!structural && !content_file) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (structural) {
|
||||||
|
global.BUNEXT_ROUTER.reload();
|
||||||
|
await allPagesESBuildContextBundler({
|
||||||
|
post_build_fn: async (params) => {
|
||||||
|
await serverPostBuildFn(params);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (content_file) {
|
||||||
|
if (global.BUNEXT_BUNDLER_CTX &&
|
||||||
|
!global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||||
|
}
|
||||||
|
else if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||||
|
await fullRebuild({
|
||||||
|
msg: `Restarting Bundler ...`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (content_file.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
runner_active = false;
|
||||||
|
global.BUNEXT_RECOMPILING = false;
|
||||||
|
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isPathExcluded(filename, full_file_path) {
|
||||||
|
const user_patterns = global.BUNEXT_CONFIG?.exclude_watch_patterns;
|
||||||
|
if (!user_patterns)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < user_patterns.length; i++) {
|
||||||
|
const watch_pattern = user_patterns[i];
|
||||||
|
if (watch_pattern instanceof RegExp) {
|
||||||
|
if (watch_pattern.test(filename))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const excluded_path = path.resolve(ROOT_DIR, watch_pattern);
|
||||||
|
if (excluded_path == full_file_path)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Heavy directories are filtered at the watcher level so inotify doesn't
|
||||||
|
// install watches (and deliver events) for trees the bundler never reads.
|
||||||
|
function buildIgnoredPatterns() {
|
||||||
|
const ignored = [
|
||||||
|
(p) => !isWatchProbePath(p) && /(^|[\/\\])\../.test(p),
|
||||||
|
/(^|[\/\\])node_modules([\/\\]|$)/,
|
||||||
|
/(^|[\/\\])public([\/\\]|$)/,
|
||||||
|
(p) => !isWatchProbePath(p) && /(^|[\/\\])\.bunext([\/\\]|$)/.test(p),
|
||||||
|
/(^|[\/\\])\.git([\/\\]|$)/,
|
||||||
|
/(^|[\/\\])dist([\/\\]|$)/,
|
||||||
|
/bun\.lockb$/,
|
||||||
|
(p) => p.endsWith(AppData["BunextTmpFileExt"]),
|
||||||
|
];
|
||||||
|
const user_patterns = global.BUNEXT_CONFIG?.exclude_watch_patterns;
|
||||||
|
if (user_patterns) {
|
||||||
|
for (let i = 0; i < user_patterns.length; i++) {
|
||||||
|
const pattern = user_patterns[i];
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
ignored.push((p) => pattern.test(path.relative(ROOT_DIR, p)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const excluded_path = path.resolve(ROOT_DIR, pattern);
|
||||||
|
ignored.push((p) => p === excluded_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ignored;
|
||||||
|
}
|
||||||
|
export default async function watcherEsbuildCTX(params) {
|
||||||
|
try {
|
||||||
|
mkdirSync(WATCH_PROBE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
watcher_ready = false;
|
||||||
|
probe_pending = false;
|
||||||
|
const pages_src_watcher = chokidar.watch(ROOT_DIR, {
|
||||||
|
ignored: buildIgnoredPatterns(),
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
});
|
||||||
|
watcherHandlers(pages_src_watcher, (event, filePath) => handleEvent(event, filePath));
|
||||||
|
const pages_snapshot = params?.pages_snapshot;
|
||||||
|
pages_src_watcher.on("ready", () => {
|
||||||
|
if (global.BUNEXT_PAGES_SRC_WATCHER === pages_src_watcher) {
|
||||||
|
watcher_ready = true;
|
||||||
|
}
|
||||||
|
if (pages_snapshot === undefined)
|
||||||
|
return;
|
||||||
|
const current = grabPagesSnapshot();
|
||||||
|
if (current !== undefined && current !== pages_snapshot) {
|
||||||
|
pending_structural = true;
|
||||||
|
scheduleRebuild();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
|
global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
|
||||||
}
|
}
|
||||||
function reloadWatcher() {
|
function handleEvent(event, filePath) {
|
||||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
if (isWatchProbePath(filePath)) {
|
||||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
if (filePath === WATCH_PROBE_FILE) {
|
||||||
watcherEsbuildCTX();
|
probe_pending = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const filename = path.relative(ROOT_DIR, filePath);
|
||||||
|
const full_file_path = filePath;
|
||||||
|
if (isPathExcluded(filename, full_file_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
|
||||||
|
fullRebuild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.endsWith(AppData["BunextTmpFileExt"])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (full_file_path.match(/\/styles$/)) {
|
||||||
|
pending_structural = false;
|
||||||
|
pending_content_file = undefined;
|
||||||
|
if (debounce_timer)
|
||||||
|
clearTimeout(debounce_timer);
|
||||||
|
setTimeout(() => {
|
||||||
|
fullRebuild({
|
||||||
|
msg: `Detected new \`styles\` directory. Rebuilding ...`,
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.match(/bunext\.config\.ts/)) {
|
||||||
|
pending_structural = false;
|
||||||
|
pending_content_file = undefined;
|
||||||
|
if (debounce_timer)
|
||||||
|
clearTimeout(debounce_timer);
|
||||||
|
fullRebuild({
|
||||||
|
msg: `bunext.config.ts file changed. Rebuilding server ...`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event === "change") {
|
||||||
|
if (!filename.match(TARGET_FILES_MATCH)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A page that isn't a hydration entry yet (e.g. created empty,
|
||||||
|
// then given its default export) needs the entry set recomputed;
|
||||||
|
// an incremental rebuild of the existing context would miss it.
|
||||||
|
if (filename.match(/^src\/pages\//) &&
|
||||||
|
!filename.match(/^src\/pages\/api\/|\.server\.tsx?$/) &&
|
||||||
|
!checkExcludedPatterns({ path: filename }) &&
|
||||||
|
!isHydrationEntryPage(full_file_path)) {
|
||||||
|
pending_structural = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pending_content_file = full_file_path;
|
||||||
|
}
|
||||||
|
scheduleRebuild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// add / unlink / addDir / unlinkDir
|
||||||
|
const is_dir_event = event === "addDir" || event === "unlinkDir";
|
||||||
|
const is_file_of_interest = Boolean(filename.match(TARGET_FILES_MATCH)) || is_dir_event;
|
||||||
|
if (!is_file_of_interest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only page routes and stylesheets are structural; anything else
|
||||||
|
// (e.g. a component being added) is covered by the next content
|
||||||
|
// rebuild and needs no context recreation.
|
||||||
|
if (!filename.match(/^src\/pages\/|\.css$/) ||
|
||||||
|
checkExcludedPatterns({ path: filename }) ||
|
||||||
|
filename.match(/ /)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending_structural = true;
|
||||||
|
scheduleRebuild();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logBuildErrorsOnce([{ text: `Watcher error: ${error?.message || error}` }], (line) => log.error(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function watcherHandlers(watcher, handleEvent) {
|
||||||
|
watcher
|
||||||
|
.on("add", (p) => handleEvent("add", p))
|
||||||
|
.on("change", (p) => handleEvent("change", p))
|
||||||
|
.on("unlink", (p) => handleEvent("unlink", p))
|
||||||
|
.on("addDir", (p) => handleEvent("addDir", p))
|
||||||
|
.on("unlinkDir", (p) => handleEvent("unlinkDir", p));
|
||||||
|
}
|
||||||
|
|||||||
+74
-22
@@ -1,6 +1,6 @@
|
|||||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
import grabContants from "../../../utils/grab-constants";
|
import grabContants from "../../../utils/grab-constants";
|
||||||
import EJSON from "../../../utils/ejson";
|
import serializeForScript from "../../../utils/serialize-for-script";
|
||||||
import isDevelopment from "../../../utils/is-development";
|
import isDevelopment from "../../../utils/is-development";
|
||||||
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
|
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
|
||||||
import grabWebMetaHTML from "./grab-web-meta-html";
|
import grabWebMetaHTML from "./grab-web-meta-html";
|
||||||
@@ -8,6 +8,7 @@ import { log } from "../../../utils/log";
|
|||||||
import { AppData } from "../../../data/app-data";
|
import { AppData } from "../../../data/app-data";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
import { NoSEORobotsValue } from "../../../utils/disable-seo";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function genWebHTML({ component: Main, pageProps, bundledMap, module, routeParams, debug, root_module, }) {
|
export default async function genWebHTML({ component: Main, pageProps, bundledMap, module, routeParams, debug, root_module, }) {
|
||||||
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
||||||
@@ -19,7 +20,11 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
|||||||
if (!Main) {
|
if (!Main) {
|
||||||
throw new Error(`Main Component not found!`);
|
throw new Error(`Main Component not found!`);
|
||||||
}
|
}
|
||||||
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(/<\//g, "<\\/");
|
let serializedProps = "{}";
|
||||||
|
try {
|
||||||
|
serializedProps = serializeForScript(pageProps || {});
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
const page_hydration_script = await grabWebPageHydrationScript();
|
const page_hydration_script = await grabWebPageHydrationScript();
|
||||||
const root_meta = root_module?.meta
|
const root_meta = root_module?.meta
|
||||||
? typeof root_module.meta == "function" && routeParams
|
? typeof root_module.meta == "function" && routeParams
|
||||||
@@ -42,7 +47,9 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
|||||||
const Head = module?.Head;
|
const Head = module?.Head;
|
||||||
const RootHead = root_module?.Head;
|
const RootHead = root_module?.Head;
|
||||||
const dev = isDevelopment();
|
const dev = isDevelopment();
|
||||||
const final_meta = _.merge(root_meta, page_meta);
|
const final_meta = global.BUNEXT_CONFIG.disable_seo
|
||||||
|
? { ..._.merge(root_meta, page_meta), robots: NoSEORobotsValue }
|
||||||
|
: _.merge(root_meta, page_meta);
|
||||||
const public_envs = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith("BUNEXT_PUBLIC_")));
|
const public_envs = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith("BUNEXT_PUBLIC_")));
|
||||||
const client_process = {
|
const client_process = {
|
||||||
env: {
|
env: {
|
||||||
@@ -52,9 +59,9 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
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: {
|
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)}`,
|
__html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${serializeForScript(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: {
|
}, "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),
|
__html: serializeForScript(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: {
|
}, 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,
|
__html: page_hydration_script,
|
||||||
}, "data-bunext-head": true })) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: _jsx(Main, { ...pageProps }) }) })] }));
|
}, "data-bunext-head": true })) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: _jsx(Main, { ...pageProps }) }) })] }));
|
||||||
@@ -66,7 +73,62 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
|||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
// const htmlBody = await new Response(stream).text();
|
// const htmlBody = await new Response(stream).text();
|
||||||
const originalConsole = {
|
silenceConsole();
|
||||||
|
let htmlBody;
|
||||||
|
let render_error;
|
||||||
|
let has_render_error = false;
|
||||||
|
const render_timeout_ms = global.BUNEXT_CONFIG.ssr_render_timeout_ms ||
|
||||||
|
AppData["DefaultSSRRenderTimeoutMs"];
|
||||||
|
// Without a deadline, a Suspense boundary that never settles keeps the
|
||||||
|
// request (and its whole render tree) alive forever. The timer is
|
||||||
|
// cleared once the render ends so finished requests don't linger.
|
||||||
|
const render_abort = new AbortController();
|
||||||
|
const render_timeout = setTimeout(() => render_abort.abort(new RenderTimeoutError(`Page render timed out after ${render_timeout_ms}ms`)), render_timeout_ms);
|
||||||
|
try {
|
||||||
|
const stream = await renderToReadableStream(final_component, {
|
||||||
|
signal: render_abort.signal,
|
||||||
|
onError(error) {
|
||||||
|
if (error?.message?.includes('unique "key" prop'))
|
||||||
|
return;
|
||||||
|
if (!has_render_error) {
|
||||||
|
has_render_error = true;
|
||||||
|
render_error = error;
|
||||||
|
}
|
||||||
|
// A timeout has no useful stack; it's logged upstream.
|
||||||
|
if (!(error instanceof RenderTimeoutError)) {
|
||||||
|
original_console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
htmlBody = await new Response(stream).text();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
clearTimeout(render_timeout);
|
||||||
|
restoreConsole();
|
||||||
|
}
|
||||||
|
// A component crash during SSR must surface as a proper error (handled
|
||||||
|
// upstream as a 500 page). React only reports these to `onError`; without
|
||||||
|
// rethrowing, the user gets a 200 with broken/blank markup.
|
||||||
|
if (has_render_error) {
|
||||||
|
throw render_error;
|
||||||
|
}
|
||||||
|
html += htmlBody;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
class RenderTimeoutError extends Error {
|
||||||
|
}
|
||||||
|
let original_console = console;
|
||||||
|
let silenced_renders = 0;
|
||||||
|
/**
|
||||||
|
* Renders can overlap (each one awaits the stream), so the console is
|
||||||
|
* swapped only by the first active render and restored by the last one.
|
||||||
|
* Otherwise a render could save another render's no-ops as the "original"
|
||||||
|
* console and silence the server for good.
|
||||||
|
*/
|
||||||
|
function silenceConsole() {
|
||||||
|
if (silenced_renders++ > 0)
|
||||||
|
return;
|
||||||
|
original_console = {
|
||||||
log: console.log,
|
log: console.log,
|
||||||
warn: console.warn,
|
warn: console.warn,
|
||||||
error: console.error,
|
error: console.error,
|
||||||
@@ -78,20 +140,10 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
|||||||
console.error = () => { };
|
console.error = () => { };
|
||||||
console.info = () => { };
|
console.info = () => { };
|
||||||
console.debug = () => { };
|
console.debug = () => { };
|
||||||
let htmlBody;
|
}
|
||||||
try {
|
function restoreConsole() {
|
||||||
const stream = await renderToReadableStream(final_component, {
|
if (--silenced_renders > 0)
|
||||||
onError(error) {
|
return;
|
||||||
if (error.message.includes('unique "key" prop'))
|
silenced_renders = 0;
|
||||||
return;
|
Object.assign(console, original_console);
|
||||||
originalConsole.error(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
htmlBody = await new Response(stream).text();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Object.assign(console, originalConsole);
|
|
||||||
}
|
|
||||||
html += htmlBody;
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,19 @@ export default async function generateWebPageGetCachePage({ module, routeParams,
|
|||||||
const expiry_seconds = config?.cacheExpiry || serverRes?.cache_expiry;
|
const expiry_seconds = config?.cacheExpiry || serverRes?.cache_expiry;
|
||||||
if (cache_page && routeParams?.url) {
|
if (cache_page && routeParams?.url) {
|
||||||
try {
|
try {
|
||||||
|
const status = serverRes?.response_options?.status;
|
||||||
|
if (status && (status < 200 || status >= 300)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const res_headers = new Headers(serverRes?.response_options?.headers);
|
||||||
|
if (res_headers.has("set-cookie")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const req_headers = routeParams.req.headers;
|
||||||
|
if (!global.BUNEXT_CONFIG?.cache_credentialed_requests &&
|
||||||
|
(req_headers.has("cookie") || req_headers.has("authorization"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const is_cache = typeof cache_page == "boolean"
|
const is_cache = typeof cache_page == "boolean"
|
||||||
? cache_page
|
? cache_page
|
||||||
: await cache_page({ ctx: routeParams, serverRes });
|
: await cache_page({ ctx: routeParams, serverRes });
|
||||||
|
|||||||
+5
-5
@@ -5,6 +5,11 @@ import genWebHTML from "./generate-web-html";
|
|||||||
import generateWebPageGetCachePage from "./generate-web-page-get-cache-page";
|
import generateWebPageGetCachePage from "./generate-web-page-get-cache-page";
|
||||||
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }) {
|
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }) {
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
|
if (serverRes?.redirect?.destination) {
|
||||||
|
return Response.redirect(serverRes.redirect.destination, serverRes.redirect.permanent
|
||||||
|
? 301
|
||||||
|
: serverRes.redirect.status_code || 302);
|
||||||
|
}
|
||||||
const html = await genWebHTML({
|
const html = await genWebHTML({
|
||||||
component,
|
component,
|
||||||
pageProps: serverRes,
|
pageProps: serverRes,
|
||||||
@@ -17,11 +22,6 @@ export default async function generateWebPageResponseFromComponentReturn({ compo
|
|||||||
if (debug) {
|
if (debug) {
|
||||||
log.info("html", html);
|
log.info("html", html);
|
||||||
}
|
}
|
||||||
if (serverRes?.redirect?.destination) {
|
|
||||||
return Response.redirect(serverRes.redirect.destination, serverRes.redirect.permanent
|
|
||||||
? 301
|
|
||||||
: serverRes.redirect.status_code || 302);
|
|
||||||
}
|
|
||||||
const res_opts = {
|
const res_opts = {
|
||||||
...serverRes?.response_options,
|
...serverRes?.response_options,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
file_path: string;
|
|
||||||
out_file?: string;
|
|
||||||
};
|
|
||||||
export default function grabFilePathModule<T extends any = any>({ file_path, out_file, }: Params): Promise<T>;
|
|
||||||
export {};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import isDevelopment from "../../../utils/is-development";
|
|
||||||
import * as esbuild from "esbuild";
|
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
|
||||||
import path from "path";
|
|
||||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
|
||||||
export default async function grabFilePathModule({ file_path, out_file, }) {
|
|
||||||
const dev = isDevelopment();
|
|
||||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
|
||||||
const target_cache_file_path = out_file ||
|
|
||||||
path.join(BUNX_CWD_MODULE_CACHE_DIR, `${path.basename(file_path)}.js`);
|
|
||||||
await esbuild.build({
|
|
||||||
entryPoints: [file_path],
|
|
||||||
bundle: true,
|
|
||||||
format: "esm",
|
|
||||||
target: "es2020",
|
|
||||||
platform: "node",
|
|
||||||
// external: ["react", "react-dom"],
|
|
||||||
minify: true,
|
|
||||||
define: {
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
|
||||||
},
|
|
||||||
metafile: true,
|
|
||||||
plugins: [tailwindEsbuildPlugin],
|
|
||||||
jsx: "automatic",
|
|
||||||
outfile: target_cache_file_path,
|
|
||||||
});
|
|
||||||
// Loader.registry.delete(target_cache_file_path);
|
|
||||||
const module = await import(`${target_cache_file_path}?t=${Date.now()}`);
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import grabRootFilePath from "./grab-root-file-path";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||||
|
import importFresh from "../../../utils/import-fresh";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) {
|
export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) {
|
||||||
try {
|
try {
|
||||||
@@ -12,9 +13,13 @@ export default async function grabPageBundledReactComponent({ file_path, return_
|
|||||||
if (global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[relative_path]) {
|
if (global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[relative_path]) {
|
||||||
const abs = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_path].path);
|
const abs = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_path].path);
|
||||||
// Loader.registry.delete(abs);
|
// Loader.registry.delete(abs);
|
||||||
const mod = await import(`${abs}?t=${Date.now()}`);
|
const mod = await importFresh(abs);
|
||||||
const Main = mod.default;
|
const Main = mod.default;
|
||||||
return { component: Main };
|
return {
|
||||||
|
component: Main,
|
||||||
|
page_module: mod.__bunext_page,
|
||||||
|
root_module: mod.__bunext_root,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const { root_file_path } = grabRootFilePath();
|
const { root_file_path } = grabRootFilePath();
|
||||||
let tsx = grabPageReactComponentString({
|
let tsx = grabPageReactComponentString({
|
||||||
@@ -35,6 +40,8 @@ export default async function grabPageBundledReactComponent({ file_path, return_
|
|||||||
return {
|
return {
|
||||||
component: Main,
|
component: Main,
|
||||||
tsx,
|
tsx,
|
||||||
|
page_module: mod.__bunext_page,
|
||||||
|
root_module: mod.__bunext_root,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import grabPageServerPath from "./grab-page-server-path";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||||
|
import importFresh from "../../../utils/import-fresh";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
export default async function grabPageCombinedServerRes({ file_path, debug, url, query, routeParams, }) {
|
export default async function grabPageCombinedServerRes({ file_path, debug, url, query, routeParams, }) {
|
||||||
const now = Date.now();
|
|
||||||
const { root_file_path } = grabRootFilePath();
|
const { root_file_path } = grabRootFilePath();
|
||||||
const { server_file_path: root_server_file_path } = root_file_path
|
const { server_file_path: root_server_file_path } = root_file_path
|
||||||
? grabPageServerPath({ file_path: root_file_path })
|
? grabPageServerPath({ file_path: root_file_path })
|
||||||
@@ -23,7 +23,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
|
|||||||
// Loader.registry.delete(final_root_server_path);
|
// Loader.registry.delete(final_root_server_path);
|
||||||
}
|
}
|
||||||
const root_server_module = final_root_server_path
|
const root_server_module = final_root_server_path
|
||||||
? await import(`${final_root_server_path}?t=${now}`)
|
? await importFresh(final_root_server_path)
|
||||||
: undefined;
|
: undefined;
|
||||||
const root_server_fn = root_server_module?.default || root_server_module?.server;
|
const root_server_fn = root_server_module?.default || root_server_module?.server;
|
||||||
const rootServerRes = await grabPageServerRes({
|
const rootServerRes = await grabPageServerRes({
|
||||||
@@ -46,7 +46,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
|
|||||||
// Loader.registry.delete(final_page_server_path);
|
// Loader.registry.delete(final_page_server_path);
|
||||||
}
|
}
|
||||||
const server_module = final_page_server_path
|
const server_module = final_page_server_path
|
||||||
? await import(`${final_page_server_path}?t=${now}`)
|
? await importFresh(final_page_server_path)
|
||||||
: undefined;
|
: undefined;
|
||||||
const server_fn = server_module?.default || server_module?.server;
|
const server_fn = server_module?.default || server_module?.server;
|
||||||
const serverRes = await grabPageServerRes({
|
const serverRes = await grabPageServerRes({
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ type Params = {
|
|||||||
file_path?: string;
|
file_path?: string;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
retry?: boolean;
|
retry?: boolean;
|
||||||
|
/**
|
||||||
|
* Skip the dev auto-rebuild/retry recovery path. Used by internal callers
|
||||||
|
* (e.g. HMR post-build) where triggering yet another fullRebuild on top
|
||||||
|
* of the rebuild that's already in progress would cascade.
|
||||||
|
*/
|
||||||
|
no_retry?: boolean;
|
||||||
return_server_res_only?: boolean;
|
return_server_res_only?: boolean;
|
||||||
skip_server_res?: boolean;
|
skip_server_res?: boolean;
|
||||||
is_hydration?: boolean;
|
is_hydration?: boolean;
|
||||||
|
|||||||
+24
-13
@@ -10,6 +10,8 @@ import isDevelopment from "../../../utils/is-development";
|
|||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import grabDirNames from "../../../utils/grab-dir-names";
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||||
|
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||||
|
import { RequestBodyTooLargeError } from "../../../utils/read-request-body";
|
||||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, ROOT_DIR } = grabDirNames();
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE, ROOT_DIR } = grabDirNames();
|
||||||
class NotFoundError extends Error {
|
class NotFoundError extends Error {
|
||||||
status = 404;
|
status = 404;
|
||||||
@@ -19,12 +21,14 @@ class NotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default async function grabPageComponent(params) {
|
export default async function grabPageComponent(params) {
|
||||||
const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, } = params;
|
const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, no_retry, } = params;
|
||||||
const url = req?.url ? new URL(req.url) : undefined;
|
const url = req?.url ? new URL(req.url) : undefined;
|
||||||
const router = global.BUNEXT_ROUTER;
|
const router = global.BUNEXT_ROUTER;
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
const forwarded_proto = req?.headers.get("x-forwarded-proto");
|
const forwarded_proto = global.BUNEXT_CONFIG?.trust_proxy
|
||||||
if (url && forwarded_proto) {
|
? req?.headers.get("x-forwarded-proto")?.split(",")[0]?.trim()
|
||||||
|
: undefined;
|
||||||
|
if (url && (forwarded_proto == "http" || forwarded_proto == "https")) {
|
||||||
url.protocol = forwarded_proto;
|
url.protocol = forwarded_proto;
|
||||||
}
|
}
|
||||||
let routeParams = undefined;
|
let routeParams = undefined;
|
||||||
@@ -59,7 +63,10 @@ export default async function grabPageComponent(params) {
|
|||||||
}
|
}
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const MAX_RETRIES = 2;
|
const MAX_RETRIES = 2;
|
||||||
while (retries < MAX_RETRIES) {
|
// Only retry transient failures: if the last rebuild failed
|
||||||
|
// (BUNEXT_BUILD_FAILED), the file is broken and more rebuilds
|
||||||
|
// will just repeat the same error.
|
||||||
|
while (retries < MAX_RETRIES && !global.BUNEXT_BUILD_FAILED) {
|
||||||
await fullRebuild({
|
await fullRebuild({
|
||||||
msg: `Retrying Bundle map for file \`${relative_file_path}\``,
|
msg: `Retrying Bundle map for file \`${relative_file_path}\``,
|
||||||
});
|
});
|
||||||
@@ -67,16 +74,13 @@ export default async function grabPageComponent(params) {
|
|||||||
bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
||||||
if (bundledMap?.path)
|
if (bundledMap?.path)
|
||||||
break;
|
break;
|
||||||
|
retries++;
|
||||||
}
|
}
|
||||||
if (!bundledMap?.path) {
|
if (!bundledMap?.path) {
|
||||||
const errMsg = `No Bundled File Path for this request path!`;
|
const errMsg = `No Bundled File Path for this request path!`;
|
||||||
log.error(errMsg);
|
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (req && !is_hydration) {
|
|
||||||
global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path].req_url = req.url;
|
|
||||||
}
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
log.info(`bundledMap:`, bundledMap);
|
log.info(`bundledMap:`, bundledMap);
|
||||||
}
|
}
|
||||||
@@ -113,11 +117,15 @@ export default async function grabPageComponent(params) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
if (error instanceof RequestBodyTooLargeError) {
|
||||||
|
return new Response("Request Body Too Large", { status: 413 });
|
||||||
|
}
|
||||||
const is404 = error instanceof NotFoundError ||
|
const is404 = error instanceof NotFoundError ||
|
||||||
error?.name === "NotFoundError" ||
|
error?.name === "NotFoundError" ||
|
||||||
error?.status === 404;
|
error?.status === 404;
|
||||||
if (!params.retry && is_dev) {
|
if (!params.retry && !params.no_retry && is_dev && !is404) {
|
||||||
while (global.BUNEXT_REBUILD_RETRIES < 2) {
|
while (!global.BUNEXT_BUILD_FAILED &&
|
||||||
|
global.BUNEXT_REBUILD_RETRIES < 2) {
|
||||||
global.BUNEXT_REBUILD_RETRIES =
|
global.BUNEXT_REBUILD_RETRIES =
|
||||||
global.BUNEXT_REBUILD_RETRIES + 1;
|
global.BUNEXT_REBUILD_RETRIES + 1;
|
||||||
await fullRebuild();
|
await fullRebuild();
|
||||||
@@ -139,9 +147,12 @@ export default async function grabPageComponent(params) {
|
|||||||
global.BUNEXT_IS_404_PAGE = true;
|
global.BUNEXT_IS_404_PAGE = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.error(`Error Grabbing Page Component: ${error.message}`);
|
logBuildErrorsOnce([
|
||||||
log.error(`Page: ${passed_file_path || url?.pathname}`);
|
{
|
||||||
if (is_dev) {
|
text: `Error Grabbing Page Component: ${error.message}; Page: ${passed_file_path || url?.pathname}`,
|
||||||
|
},
|
||||||
|
], (line) => log.error(line));
|
||||||
|
if (is_dev && !params.no_retry && !global.BUNEXT_BUILD_FAILED) {
|
||||||
fullRebuild();
|
fullRebuild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
|
|||||||
const presetComponent = is404
|
const presetComponent = is404
|
||||||
? BUNX_ROOT_404_PRESET_COMPONENT
|
? BUNX_ROOT_404_PRESET_COMPONENT
|
||||||
: BUNX_ROOT_500_PRESET_COMPONENT;
|
: BUNX_ROOT_500_PRESET_COMPONENT;
|
||||||
|
// 404 messages only contain the requested path. Other error messages can
|
||||||
|
// leak internals, so they are only shown in development.
|
||||||
|
const error_message = is404 || is_dev ? error?.message : "Internal Server Error";
|
||||||
const default_server_res = {
|
const default_server_res = {
|
||||||
response_options: {
|
response_options: {
|
||||||
status: is404 ? 404 : 500,
|
status: is404 ? 404 : 500,
|
||||||
@@ -24,7 +27,7 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
|
|||||||
const default_module = await import(presetComponent);
|
const default_module = await import(presetComponent);
|
||||||
const Component = default_module.default;
|
const Component = default_module.default;
|
||||||
const default_jsx = () => {
|
const default_jsx = () => {
|
||||||
return _jsx(Component, { children: _jsx("span", { children: error.message }) });
|
return _jsx(Component, { children: _jsx("span", { children: error_message }) });
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
component: default_jsx,
|
component: default_jsx,
|
||||||
@@ -65,7 +68,7 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}, children: [_jsx("h1", { children: is404 ? "404 Not Found" : "500 Internal Server Error" }), _jsx("span", { children: error.message })] }));
|
}, children: [_jsx("h1", { children: is404 ? "404 Not Found" : "500 Internal Server Error" }), _jsx("span", { children: error_message })] }));
|
||||||
return {
|
return {
|
||||||
component: DefaultNotFound,
|
component: DefaultNotFound,
|
||||||
routeParams,
|
routeParams,
|
||||||
|
|||||||
+15
-10
@@ -3,8 +3,8 @@ import _ from "lodash";
|
|||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
import grabRootFilePath from "./grab-root-file-path";
|
import grabRootFilePath from "./grab-root-file-path";
|
||||||
import grabPageCombinedServerRes from "./grab-page-combined-server-res";
|
import grabPageCombinedServerRes from "./grab-page-combined-server-res";
|
||||||
|
import importFresh from "../../../utils/import-fresh";
|
||||||
export default async function grabPageModules({ file_path, debug, url, query, routeParams, skip_server_res, }) {
|
export default async function grabPageModules({ file_path, debug, url, query, routeParams, skip_server_res, }) {
|
||||||
const now = Date.now();
|
|
||||||
const { serverRes } = skip_server_res
|
const { serverRes } = skip_server_res
|
||||||
? {}
|
? {}
|
||||||
: await grabPageCombinedServerRes({
|
: await grabPageCombinedServerRes({
|
||||||
@@ -19,20 +19,25 @@ export default async function grabPageModules({ file_path, debug, url, query, ro
|
|||||||
? 301
|
? 301
|
||||||
: serverRes.redirect.status_code || 302);
|
: serverRes.redirect.status_code || 302);
|
||||||
}
|
}
|
||||||
const { root_file_path } = grabRootFilePath();
|
const { component, page_module, root_module: bundled_root_module, } = (await grabPageBundledReactComponent({
|
||||||
const root_module = root_file_path
|
|
||||||
? await import(`${root_file_path}?t=${now}`)
|
|
||||||
: undefined;
|
|
||||||
const module = await import(`${file_path}?t=${now}`);
|
|
||||||
if (debug) {
|
|
||||||
log.info(`module:`, module);
|
|
||||||
}
|
|
||||||
const { component } = (await grabPageBundledReactComponent({
|
|
||||||
file_path,
|
file_path,
|
||||||
})) || {};
|
})) || {};
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error(`Couldn't grab page component`);
|
throw new Error(`Couldn't grab page component`);
|
||||||
}
|
}
|
||||||
|
// Prefer the page/root exports re-exported by the SSR bundle. Importing
|
||||||
|
// the raw source with Bun's loader only cache-busts the page file itself:
|
||||||
|
// its transitive imports stay in Bun's module registry, so a component
|
||||||
|
// that failed to resolve once (e.g. imported before it was created)
|
||||||
|
// keeps failing until the server restarts.
|
||||||
|
const { root_file_path } = grabRootFilePath();
|
||||||
|
const root_module = !root_file_path
|
||||||
|
? undefined
|
||||||
|
: bundled_root_module || (await importFresh(root_file_path));
|
||||||
|
const module = page_module || (await importFresh(file_path));
|
||||||
|
if (debug) {
|
||||||
|
log.info(`module:`, module);
|
||||||
|
}
|
||||||
if (debug) {
|
if (debug) {
|
||||||
log.info(`component:`, component);
|
log.info(`component:`, component);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ export default function grabPageReactComponentString({ file_path, root_file_path
|
|||||||
}
|
}
|
||||||
tsx += ` )\n`;
|
tsx += ` )\n`;
|
||||||
tsx += `}\n`;
|
tsx += `}\n`;
|
||||||
|
// Expose the page/root's own exports (meta, Head, config, ...) through
|
||||||
|
// the bundle so the server never has to import the raw source files.
|
||||||
|
tsx += `export * as __bunext_page from "${file_path}"\n`;
|
||||||
|
if (root_file_path) {
|
||||||
|
tsx += `export * as __bunext_root from "${root_file_path}"\n`;
|
||||||
|
}
|
||||||
return tsx;
|
return tsx;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
+6
-3
@@ -1,5 +1,6 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
|
import isDevelopment from "../../../utils/is-development";
|
||||||
export default async function grabPageServerRes({ url, query, routeParams, server_function, props, }) {
|
export default async function grabPageServerRes({ url, query, routeParams, server_function, props, }) {
|
||||||
const default_props = {
|
const default_props = {
|
||||||
url: url
|
url: url
|
||||||
@@ -14,9 +15,7 @@ export default async function grabPageServerRes({ url, query, routeParams, serve
|
|||||||
"searchParams",
|
"searchParams",
|
||||||
"hash",
|
"hash",
|
||||||
"href",
|
"href",
|
||||||
"password",
|
|
||||||
"protocol",
|
"protocol",
|
||||||
"username",
|
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -36,8 +35,12 @@ export default async function grabPageServerRes({ url, query, routeParams, serve
|
|||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(`Page ${url?.pathname} Server Error => ${error.message}\n`, error);
|
log.error(`Page ${url?.pathname} Server Error => ${error.message}\n`, error);
|
||||||
|
// Error details can leak internals, so only expose them in
|
||||||
|
// development. They are always logged above.
|
||||||
return _.merge(default_props, {
|
return _.merge(default_props, {
|
||||||
error: error.message,
|
error: isDevelopment()
|
||||||
|
? error.message
|
||||||
|
: "Internal Server Error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
type Params = {
|
|
||||||
tsx: string;
|
|
||||||
};
|
|
||||||
export default function grabTsxStringModule<T>({ tsx, }: Params): Promise<T>;
|
|
||||||
export {};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import isDevelopment from "../../../utils/is-development";
|
|
||||||
import * as esbuild from "esbuild";
|
|
||||||
export default async function grabTsxStringModule({ tsx, }) {
|
|
||||||
const dev = isDevelopment();
|
|
||||||
const now = Date.now();
|
|
||||||
const final_tsx = dev ? tsx + `\n// v_${now}` : tsx;
|
|
||||||
const result = await esbuild.transform(final_tsx, {
|
|
||||||
loader: "tsx",
|
|
||||||
format: "esm",
|
|
||||||
jsx: "automatic",
|
|
||||||
minify: !dev,
|
|
||||||
});
|
|
||||||
const blob = new Blob([result.code], { type: "text/javascript" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const mod = await import(url);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
// export default async function grabTsxStringModule<T extends any = any>({
|
|
||||||
// tsx,
|
|
||||||
// }: Params): Promise<T> {
|
|
||||||
// const dev = isDevelopment();
|
|
||||||
// const now = Date.now();
|
|
||||||
// const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
|
||||||
// const target_cache_file_path = path.join(
|
|
||||||
// BUNX_CWD_MODULE_CACHE_DIR,
|
|
||||||
// `server-render-${now}.js`,
|
|
||||||
// );
|
|
||||||
// await esbuild.build({
|
|
||||||
// stdin: {
|
|
||||||
// contents: dev ? tsx + `\n// v_${now}` : tsx,
|
|
||||||
// resolveDir: process.cwd(),
|
|
||||||
// loader: "tsx",
|
|
||||||
// },
|
|
||||||
// bundle: true,
|
|
||||||
// format: "esm",
|
|
||||||
// target: "es2020",
|
|
||||||
// platform: "node",
|
|
||||||
// external: [
|
|
||||||
// "react",
|
|
||||||
// "react-dom",
|
|
||||||
// "react/jsx-runtime",
|
|
||||||
// "react/jsx-dev-runtime",
|
|
||||||
// ],
|
|
||||||
// minify: !dev,
|
|
||||||
// define: {
|
|
||||||
// "process.env.NODE_ENV": JSON.stringify(
|
|
||||||
// dev ? "development" : "production",
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
// jsx: "automatic",
|
|
||||||
// outfile: target_cache_file_path,
|
|
||||||
// plugins: [tailwindEsbuildPlugin],
|
|
||||||
// });
|
|
||||||
// Loader.registry.delete(target_cache_file_path);
|
|
||||||
// const mod = await import(`${target_cache_file_path}?t=${now}`);
|
|
||||||
// return mod as T;
|
|
||||||
// }
|
|
||||||
// if (!dev) {
|
|
||||||
// const now = Date.now();
|
|
||||||
// const final_tsx = dev ? tsx + `\n// v_${now}` : tsx;
|
|
||||||
// const result = await esbuild.transform(final_tsx, {
|
|
||||||
// loader: "tsx",
|
|
||||||
// format: "esm",
|
|
||||||
// jsx: "automatic",
|
|
||||||
// minify: !dev,
|
|
||||||
// });
|
|
||||||
// const blob = new Blob([result.code], { type: "text/javascript" });
|
|
||||||
// const url = URL.createObjectURL(blob);
|
|
||||||
// const mod = await import(url);
|
|
||||||
// URL.revokeObjectURL(url);
|
|
||||||
// return mod as T;
|
|
||||||
// }
|
|
||||||
+1
-1
@@ -2,5 +2,5 @@ import type { BunextPageModuleMeta } from "../../../types";
|
|||||||
type Params = {
|
type Params = {
|
||||||
meta: BunextPageModuleMeta;
|
meta: BunextPageModuleMeta;
|
||||||
};
|
};
|
||||||
export default function grabWebMetaHTML({ meta }: Params): import("react/jsx-runtime").JSX.Element;
|
export default function grabWebMetaHTML({ meta }: Params): import("react").JSX.Element;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+21
-1
@@ -1,9 +1,11 @@
|
|||||||
import isDevelopment from "../../../utils/is-development";
|
import isDevelopment from "../../../utils/is-development";
|
||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
|
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||||
import getCache from "../../cache/get-cache";
|
import getCache from "../../cache/get-cache";
|
||||||
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
|
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
|
||||||
import grabPageComponent from "./grab-page-component";
|
import grabPageComponent from "./grab-page-component";
|
||||||
import grabPageErrorComponent from "./grab-page-error-component";
|
import grabPageErrorComponent from "./grab-page-error-component";
|
||||||
|
import { awaitSSRRebuild } from "../../bundler/pages-ssr-context-bundler";
|
||||||
export default async function handleWebPages({ req, }) {
|
export default async function handleWebPages({ req, }) {
|
||||||
try {
|
try {
|
||||||
const is_dev = isDevelopment();
|
const is_dev = isDevelopment();
|
||||||
@@ -21,6 +23,20 @@ export default async function handleWebPages({ req, }) {
|
|||||||
return new Response(existing_cache, res_opts);
|
return new Response(existing_cache, res_opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The dev server port opens before the one-time startup build
|
||||||
|
// finishes. First page render waits for it so we never serve HTML
|
||||||
|
// against half-built hydration/SSR modules.
|
||||||
|
if (is_dev && global.BUNEXT_INITIAL_BUILD_PROMISE) {
|
||||||
|
try {
|
||||||
|
await global.BUNEXT_INITIAL_BUILD_PROMISE;
|
||||||
|
}
|
||||||
|
catch (error) { }
|
||||||
|
}
|
||||||
|
// HMR pushes client updates before the SSR rebuild finishes; a page
|
||||||
|
// load in that window waits so SSR HTML matches the new bundle.
|
||||||
|
if (is_dev) {
|
||||||
|
await awaitSSRRebuild();
|
||||||
|
}
|
||||||
const componentRes = await grabPageComponent({
|
const componentRes = await grabPageComponent({
|
||||||
req,
|
req,
|
||||||
});
|
});
|
||||||
@@ -32,7 +48,11 @@ export default async function handleWebPages({ req, }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
log.error(`Error Handling Web Page: ${error.message}`, error);
|
logBuildErrorsOnce([
|
||||||
|
{
|
||||||
|
text: `Error Handling Web Page: ${error?.message || String(error)}`,
|
||||||
|
},
|
||||||
|
], (line) => log.error(line));
|
||||||
const componentRes = await grabPageErrorComponent({
|
const componentRes = await grabPageErrorComponent({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
|
type Params = {
|
||||||
|
/**
|
||||||
|
* Run `.css` files through Tailwind/PostCSS. When `false`, every `.css`
|
||||||
|
* import loads as an empty stylesheet (for builds whose CSS output is
|
||||||
|
* never used, e.g. SSR).
|
||||||
|
*/
|
||||||
|
process_css?: boolean;
|
||||||
|
};
|
||||||
|
export declare function createTailwindEsbuildPlugin({ process_css, }?: Params): esbuild.Plugin;
|
||||||
|
/**
|
||||||
|
* Resolves `.css` imports like the Tailwind plugin but loads them empty.
|
||||||
|
*/
|
||||||
|
export declare const cssStubEsbuildPlugin: esbuild.Plugin;
|
||||||
declare const tailwindEsbuildPlugin: esbuild.Plugin;
|
declare const tailwindEsbuildPlugin: esbuild.Plugin;
|
||||||
export default tailwindEsbuildPlugin;
|
export default tailwindEsbuildPlugin;
|
||||||
|
|||||||
+51
-38
@@ -8,42 +8,55 @@ import grabDirNames from "../../../utils/grab-dir-names";
|
|||||||
import { log } from "../../../utils/log";
|
import { log } from "../../../utils/log";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
let error_logged = false;
|
let error_logged = false;
|
||||||
const tailwindEsbuildPlugin = {
|
// One processor for the whole process, so the Tailwind plugin's internal
|
||||||
name: "tailwindcss",
|
// state survives across rebuilds instead of being recreated per file load.
|
||||||
setup(build) {
|
let css_processor;
|
||||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
function grabCSSProcessor() {
|
||||||
try {
|
if (!css_processor) {
|
||||||
const source = await readFile(args.path, "utf-8");
|
css_processor = postcss([tailwindcss()]);
|
||||||
const result = await postcss([tailwindcss()]).process(source, {
|
}
|
||||||
from: args.path,
|
return css_processor;
|
||||||
});
|
}
|
||||||
error_logged = false;
|
export function createTailwindEsbuildPlugin({ process_css = true, } = {}) {
|
||||||
return { contents: result.css, loader: "css" };
|
return {
|
||||||
}
|
name: process_css ? "tailwindcss" : "css-stub",
|
||||||
catch (error) {
|
setup(build) {
|
||||||
return { errors: [{ text: error.message }] };
|
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||||
}
|
if (!process_css) {
|
||||||
});
|
return { contents: "", loader: "css" };
|
||||||
build.onResolve({ filter: /\.css$/ }, async (args) => {
|
}
|
||||||
const css_path = path.resolve(args.resolveDir, args.path.replace(/\@\//g, ROOT_DIR + "/"));
|
try {
|
||||||
const does_path_exist = existsSync(css_path);
|
const source = await readFile(args.path, "utf-8");
|
||||||
if (!does_path_exist && !error_logged) {
|
const result = await grabCSSProcessor().process(source, {
|
||||||
const err_msg = `CSS Error: ${css_path} file does not exist.`;
|
from: args.path,
|
||||||
log.error(err_msg);
|
});
|
||||||
error_logged = true;
|
error_logged = false;
|
||||||
// return {
|
return { contents: result.css, loader: "css" };
|
||||||
// errors: [
|
}
|
||||||
// {
|
catch (error) {
|
||||||
// text: err_msg,
|
return { errors: [{ text: error.message }] };
|
||||||
// },
|
}
|
||||||
// ],
|
});
|
||||||
// pluginName: "tailwindcss",
|
build.onResolve({ filter: /\.css$/ }, async (args) => {
|
||||||
// };
|
const css_path = path.resolve(args.resolveDir, args.path.replace(/\@\//g, ROOT_DIR + "/"));
|
||||||
}
|
const does_path_exist = existsSync(css_path);
|
||||||
return {
|
if (!does_path_exist && !error_logged) {
|
||||||
path: css_path,
|
const err_msg = `CSS Error: ${css_path} file does not exist.`;
|
||||||
};
|
log.error(err_msg);
|
||||||
});
|
error_logged = true;
|
||||||
},
|
}
|
||||||
};
|
return {
|
||||||
|
path: css_path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resolves `.css` imports like the Tailwind plugin but loads them empty.
|
||||||
|
*/
|
||||||
|
export const cssStubEsbuildPlugin = createTailwindEsbuildPlugin({
|
||||||
|
process_css: false,
|
||||||
|
});
|
||||||
|
const tailwindEsbuildPlugin = createTailwindEsbuildPlugin();
|
||||||
export default tailwindEsbuildPlugin;
|
export default tailwindEsbuildPlugin;
|
||||||
|
|||||||
Vendored
+22
-4
@@ -1,4 +1,22 @@
|
|||||||
export default function writeErrorFile({ exitCode, error, }: {
|
type Params = {
|
||||||
error?: Bun.ErrorLike;
|
/**
|
||||||
exitCode: number | null;
|
* Only include when the exiting process actually crashed with an error.
|
||||||
}): void;
|
* Bun's spawn `onExit` `error` is only set on spawn failures, so callers
|
||||||
|
* that DO have the error (crash handlers) should pass `force: true`.
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
|
error?: unknown;
|
||||||
|
exitCode?: number | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Writes the `BUNDLER_EXIT.txt` flag that the bundler pipeline watches to
|
||||||
|
* detect that the dev/prod child process previously crashed.
|
||||||
|
*
|
||||||
|
* Guardrails:
|
||||||
|
* - Never writes for clean exits (exitCode === 0) or signal deaths where no
|
||||||
|
* error is available (Ctrl+C must not leave a stale error file behind).
|
||||||
|
* - The supervisor writes only when the file does not already exist, so the
|
||||||
|
* crash handler's richer error/stack payload is not clobbered.
|
||||||
|
*/
|
||||||
|
export default function writeErrorFile({ force, error, exitCode, }: Params): void;
|
||||||
|
export {};
|
||||||
|
|||||||
Vendored
+35
-14
@@ -1,21 +1,42 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { mkdirSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||||
import grabDirNames from "../utils/grab-dir-names";
|
import grabDirNames from "../utils/grab-dir-names";
|
||||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||||
export default function writeErrorFile({ exitCode, error, }) {
|
/**
|
||||||
|
* Writes the `BUNDLER_EXIT.txt` flag that the bundler pipeline watches to
|
||||||
|
* detect that the dev/prod child process previously crashed.
|
||||||
|
*
|
||||||
|
* Guardrails:
|
||||||
|
* - Never writes for clean exits (exitCode === 0) or signal deaths where no
|
||||||
|
* error is available (Ctrl+C must not leave a stale error file behind).
|
||||||
|
* - The supervisor writes only when the file does not already exist, so the
|
||||||
|
* crash handler's richer error/stack payload is not clobbered.
|
||||||
|
*/
|
||||||
|
export default function writeErrorFile({ force, error, exitCode, }) {
|
||||||
|
const has_error = error != null;
|
||||||
|
const abnormal_exit = exitCode != null && exitCode !== 0;
|
||||||
|
if (!has_error && !abnormal_exit)
|
||||||
|
return;
|
||||||
|
if (!force && existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE))
|
||||||
|
return;
|
||||||
|
const err = error instanceof Error
|
||||||
|
? error
|
||||||
|
: typeof error === "string"
|
||||||
|
? new Error(error)
|
||||||
|
: undefined;
|
||||||
|
const message = err?.message || "Unknown error";
|
||||||
|
const stack = err?.stack || "N/A";
|
||||||
let txt = ``;
|
let txt = ``;
|
||||||
txt += `Bunext Error\n`;
|
txt += `Bunext Error\n`;
|
||||||
txt += `============================================\n`;
|
txt += `============================================\n`;
|
||||||
txt += `ERROR: ${error?.message}\n`;
|
txt += `ERROR: ${message}\n`;
|
||||||
txt += `EXIT_CODE: ${exitCode || 0}\n`;
|
txt += `EXIT_CODE: ${exitCode ?? "N/A"}\n`;
|
||||||
txt += `CALL_STACK: ${error?.stack}\n`;
|
txt += `CALL_STACK:\n${stack}\n`;
|
||||||
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), { recursive: true });
|
try {
|
||||||
writeFileSync(BUNX_BUNDLER_ERROR_EXIT_FILE, txt);
|
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
writeFileSync(BUNX_BUNDLER_ERROR_EXIT_FILE, txt);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
// 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
+4
@@ -1,5 +1,7 @@
|
|||||||
import bunextInit from "./functions/bunext-init";
|
import bunextInit from "./functions/bunext-init";
|
||||||
import bunextRequestHandler from "./functions/server/bunext-req-handler";
|
import bunextRequestHandler from "./functions/server/bunext-req-handler";
|
||||||
|
import generateRobotsTxt from "./utils/generate-robots-txt";
|
||||||
|
import generateSitemap from "./utils/generate-sitemap";
|
||||||
declare const bunext: {
|
declare const bunext: {
|
||||||
bunextRequestHandler: typeof bunextRequestHandler;
|
bunextRequestHandler: typeof bunextRequestHandler;
|
||||||
bunextLog: {
|
bunextLog: {
|
||||||
@@ -13,5 +15,7 @@ declare const bunext: {
|
|||||||
banner: () => void;
|
banner: () => void;
|
||||||
};
|
};
|
||||||
bunextInit: typeof bunextInit;
|
bunextInit: typeof bunextInit;
|
||||||
|
bunextGenerateSitemap: typeof generateSitemap;
|
||||||
|
bunextGenerateRobotsTxt: typeof generateRobotsTxt;
|
||||||
};
|
};
|
||||||
export default bunext;
|
export default bunext;
|
||||||
|
|||||||
Vendored
+4
@@ -1,9 +1,13 @@
|
|||||||
import bunextInit from "./functions/bunext-init";
|
import bunextInit from "./functions/bunext-init";
|
||||||
import bunextRequestHandler from "./functions/server/bunext-req-handler";
|
import bunextRequestHandler from "./functions/server/bunext-req-handler";
|
||||||
|
import generateRobotsTxt from "./utils/generate-robots-txt";
|
||||||
|
import generateSitemap from "./utils/generate-sitemap";
|
||||||
import { log } from "./utils/log";
|
import { log } from "./utils/log";
|
||||||
const bunext = {
|
const bunext = {
|
||||||
bunextRequestHandler,
|
bunextRequestHandler,
|
||||||
bunextLog: log,
|
bunextLog: log,
|
||||||
bunextInit,
|
bunextInit,
|
||||||
|
bunextGenerateSitemap: generateSitemap,
|
||||||
|
bunextGenerateRobotsTxt: generateRobotsTxt,
|
||||||
};
|
};
|
||||||
export default bunext;
|
export default bunext;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from "react";
|
import type { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from "react";
|
||||||
type Props = PropsWithChildren<DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>>;
|
type Props = PropsWithChildren<DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>>;
|
||||||
export default function Head({ children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
export default function Head({ children, ...props }: Props): import("react").JSX.Element;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
export default function DefaultNotFoundPage({ children }: PropsWithChildren): import("react/jsx-runtime").JSX.Element;
|
export default function DefaultNotFoundPage({ children }: PropsWithChildren): import("react").JSX.Element;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
export default function DefaultServerErrorPage({ children, }: PropsWithChildren): import("react/jsx-runtime").JSX.Element;
|
export default function DefaultServerErrorPage({ children, }: PropsWithChildren): import("react").JSX.Element;
|
||||||
|
|||||||
Vendored
+190
-1
@@ -46,6 +46,23 @@ export type BunextConfig = {
|
|||||||
development?: boolean;
|
development?: boolean;
|
||||||
middleware?: (params: BunextConfigMiddlewareParams) => Promise<Response | Request | undefined> | Response | Request | undefined;
|
middleware?: (params: BunextConfigMiddlewareParams) => Promise<Response | Request | undefined> | Response | Request | undefined;
|
||||||
default_cache_expiry?: number;
|
default_cache_expiry?: number;
|
||||||
|
/**
|
||||||
|
* Maximum number of cached pages kept on disk. New pages are not
|
||||||
|
* cached once this is reached. Defaults to 1000.
|
||||||
|
*/
|
||||||
|
max_cache_entries?: number;
|
||||||
|
/**
|
||||||
|
* By default, pages are not written to the cache when the request
|
||||||
|
* carries a `Cookie` or `Authorization` header, so one user's page is
|
||||||
|
* never served to everyone. Set this to `true` only if your cached
|
||||||
|
* pages never render per-user content.
|
||||||
|
*/
|
||||||
|
cache_credentialed_requests?: boolean;
|
||||||
|
/**
|
||||||
|
* Trust the `X-Forwarded-Proto` header (set this when running behind a
|
||||||
|
* reverse proxy that sets it). Defaults to `false`.
|
||||||
|
*/
|
||||||
|
trust_proxy?: boolean;
|
||||||
websocket?: WebSocketHandler<any>;
|
websocket?: WebSocketHandler<any>;
|
||||||
server_options?: Omit<Bun.Serve.Options<any>, "fetch" | "routes">;
|
server_options?: Omit<Bun.Serve.Options<any>, "fetch" | "routes">;
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +93,34 @@ export type BunextConfig = {
|
|||||||
* Public environment variables to be passed to the client
|
* Public environment variables to be passed to the client
|
||||||
*/
|
*/
|
||||||
public_envs?: Record<string, string>;
|
public_envs?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Options for the `bunext sitemap` command
|
||||||
|
*/
|
||||||
|
sitemap?: BunextSitemapConfig;
|
||||||
|
/**
|
||||||
|
* Options for the robots.txt written by `bunext sitemap`. Set to
|
||||||
|
* `false` to skip generating it.
|
||||||
|
*/
|
||||||
|
robots?: BunextRobotsConfig | false;
|
||||||
|
/**
|
||||||
|
* Keep the whole site out of search engines, e.g. for preview
|
||||||
|
* deployments. Every response gets `X-Robots-Tag: noindex, nofollow`,
|
||||||
|
* pages get a matching robots meta tag (overriding `meta.robots`), and
|
||||||
|
* `/robots.txt` disallows all crawlers regardless of `public/robots.txt`.
|
||||||
|
*/
|
||||||
|
disable_seo?: boolean;
|
||||||
|
/**
|
||||||
|
* Maximum time (ms) a page's server render may take before it is
|
||||||
|
* aborted and answered with a 500. Defaults to 30000.
|
||||||
|
*/
|
||||||
|
ssr_render_timeout_ms?: number;
|
||||||
|
/**
|
||||||
|
* Minify the SSR bundle. The SSR output is read off local disk by the
|
||||||
|
* server and is never downloaded by a browser, so minifying it only costs
|
||||||
|
* build memory. Set to `false` on memory-constrained hosts. Never minified
|
||||||
|
* in dev; defaults to `true` in production.
|
||||||
|
*/
|
||||||
|
minify_ssr_bundle?: boolean;
|
||||||
};
|
};
|
||||||
export type BunextConfigMiddlewareParams = {
|
export type BunextConfigMiddlewareParams = {
|
||||||
req: Request;
|
req: Request;
|
||||||
@@ -174,7 +219,19 @@ export type BunextPageModule = {
|
|||||||
export type BunextPageServerModule = {
|
export type BunextPageServerModule = {
|
||||||
default?: BunextPageServerFn;
|
default?: BunextPageServerFn;
|
||||||
server?: BunextPageServerFn;
|
server?: BunextPageServerFn;
|
||||||
|
sitemap?: BunextPageSitemapFn;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Returns the sitemap entries for a page. Required for dynamic routes
|
||||||
|
* (`[slug]`) to appear in the sitemap. On a static page, the returned
|
||||||
|
* entries replace the page's default entry.
|
||||||
|
*/
|
||||||
|
export type BunextPageSitemapFn = (params: {
|
||||||
|
/**
|
||||||
|
* The page's route pattern, e.g. `/blog/[slug]`
|
||||||
|
*/
|
||||||
|
route: string;
|
||||||
|
}) => BunextSitemapAdditionalPaths | Promise<BunextSitemapAdditionalPaths>;
|
||||||
export type BunextHTMLProps = DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>;
|
export type BunextHTMLProps = DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>;
|
||||||
export type BunextPageModuleMetaFn = (params: {
|
export type BunextPageModuleMetaFn = (params: {
|
||||||
ctx: BunxRouteParams;
|
ctx: BunxRouteParams;
|
||||||
@@ -301,6 +358,12 @@ export type GrabPageReactBundledComponentRes = {
|
|||||||
component?: FC;
|
component?: FC;
|
||||||
server_res?: BunextPageModuleServerReturn;
|
server_res?: BunextPageModuleServerReturn;
|
||||||
tsx?: string;
|
tsx?: string;
|
||||||
|
/**
|
||||||
|
* The page's own exports (`meta`, `Head`, `config`, ...), re-exported
|
||||||
|
* from the SSR bundle so they share its fresh module graph.
|
||||||
|
*/
|
||||||
|
page_module?: BunextPageModule;
|
||||||
|
root_module?: BunextPageModule;
|
||||||
};
|
};
|
||||||
export type PageFiles = {
|
export type PageFiles = {
|
||||||
local_path: string;
|
local_path: string;
|
||||||
@@ -316,7 +379,6 @@ export type BundlerCTXMap = {
|
|||||||
url_path: string;
|
url_path: string;
|
||||||
file_name: string;
|
file_name: string;
|
||||||
css_path?: string;
|
css_path?: string;
|
||||||
req_url?: string;
|
|
||||||
};
|
};
|
||||||
export type GlobalHMRControllerObject = {
|
export type GlobalHMRControllerObject = {
|
||||||
controller: ReadableStreamDefaultController<string>;
|
controller: ReadableStreamDefaultController<string>;
|
||||||
@@ -361,3 +423,130 @@ export type BuildDataType = {
|
|||||||
build_date_unix: number;
|
build_date_unix: number;
|
||||||
build_date: string;
|
build_date: string;
|
||||||
};
|
};
|
||||||
|
export type BunextSitemapChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
||||||
|
export type BunextSitemapEntry = {
|
||||||
|
/**
|
||||||
|
* URL path (e.g. `/blog/my-post`) or absolute URL.
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
lastmod?: Date | string;
|
||||||
|
changefreq?: BunextSitemapChangeFreq;
|
||||||
|
/**
|
||||||
|
* Between `0.0` and `1.0`.
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
};
|
||||||
|
export type BunextSitemapAdditionalPaths = (string | BunextSitemapEntry)[];
|
||||||
|
export type BunextGenerateSitemapParams = {
|
||||||
|
/**
|
||||||
|
* Base URL prepended to every path. Defaults to `config.origin`,
|
||||||
|
* falling back to `http://localhost:<port>`.
|
||||||
|
*/
|
||||||
|
origin?: string;
|
||||||
|
/**
|
||||||
|
* Paths to leave out. A string matches a path exactly, or a whole
|
||||||
|
* subtree when it ends with `/*` (e.g. `/admin/*`). A RegExp is
|
||||||
|
* tested against the path.
|
||||||
|
*/
|
||||||
|
exclude?: (string | RegExp)[];
|
||||||
|
/**
|
||||||
|
* Extra entries, typically concrete URLs for dynamic routes
|
||||||
|
* (`[slug]`), which cannot be discovered from the file system. Can
|
||||||
|
* also be a function (sync or async) returning them, e.g. to load
|
||||||
|
* them from a database.
|
||||||
|
*/
|
||||||
|
additional_paths?: BunextSitemapAdditionalPaths | (() => BunextSitemapAdditionalPaths | Promise<BunextSitemapAdditionalPaths>);
|
||||||
|
/**
|
||||||
|
* Fall back to each page file's modification time as `lastmod` when
|
||||||
|
* the build record has no date for it. Defaults to `false`: checkouts
|
||||||
|
* reset modification times, so they are rarely meaningful.
|
||||||
|
*/
|
||||||
|
use_file_lastmod?: boolean;
|
||||||
|
/**
|
||||||
|
* `lastmod` for entries that have no other date
|
||||||
|
*/
|
||||||
|
default_lastmod?: Date | string;
|
||||||
|
/**
|
||||||
|
* Page last-modified record written by `bunext build`, relative to the
|
||||||
|
* project root. Defaults to `.bunext/page-lastmod.json`. Point it at a
|
||||||
|
* cached or committed location when builds start from a fresh checkout.
|
||||||
|
*/
|
||||||
|
lastmod_file?: string;
|
||||||
|
/**
|
||||||
|
* Append a trailing slash to every non-root path. Defaults to `false`.
|
||||||
|
*/
|
||||||
|
trailing_slash?: boolean;
|
||||||
|
default_changefreq?: BunextSitemapChangeFreq;
|
||||||
|
default_priority?: number;
|
||||||
|
};
|
||||||
|
export type BunextSitemapConfig = BunextGenerateSitemapParams & {
|
||||||
|
/**
|
||||||
|
* Maximum URLs per sitemap file. Above this, `bunext sitemap` writes
|
||||||
|
* chunk files to `chunks_dir` and makes sitemap.xml a sitemap index
|
||||||
|
* referencing them. Defaults to 10,000; capped at 50,000, the protocol
|
||||||
|
* limit. Chunks are also split at 50MB.
|
||||||
|
*/
|
||||||
|
chunk_size?: number;
|
||||||
|
/**
|
||||||
|
* Directory for sitemap chunk files, relative to the project root.
|
||||||
|
* Must be inside `public/` so the files are served. Defaults to
|
||||||
|
* `public/sitemap-chunks`.
|
||||||
|
*/
|
||||||
|
chunks_dir?: string;
|
||||||
|
/**
|
||||||
|
* Output file, relative to the project root. Defaults to
|
||||||
|
* `public/sitemap.xml`, which is served at `/sitemap.xml`.
|
||||||
|
*/
|
||||||
|
output?: string;
|
||||||
|
};
|
||||||
|
export type BunextRobotsRule = {
|
||||||
|
/**
|
||||||
|
* One or more crawler names, e.g. `*` or `["GPTBot", "CCBot"]`
|
||||||
|
*/
|
||||||
|
user_agent: string | string[];
|
||||||
|
allow?: string[];
|
||||||
|
disallow?: string[];
|
||||||
|
/**
|
||||||
|
* Seconds between requests. Not supported by every crawler.
|
||||||
|
*/
|
||||||
|
crawl_delay?: number;
|
||||||
|
};
|
||||||
|
export type BunextGenerateRobotsTxtParams = {
|
||||||
|
/**
|
||||||
|
* Base URL used to turn relative `sitemaps` paths into absolute URLs.
|
||||||
|
* Defaults to `config.origin`, falling back to `http://localhost:<port>`.
|
||||||
|
*/
|
||||||
|
origin?: string;
|
||||||
|
/**
|
||||||
|
* Defaults to allowing every crawler everywhere.
|
||||||
|
*/
|
||||||
|
rules?: BunextRobotsRule[];
|
||||||
|
/**
|
||||||
|
* Sitemap URLs or paths, each written as a `Sitemap:` line.
|
||||||
|
*/
|
||||||
|
sitemaps?: string[];
|
||||||
|
/**
|
||||||
|
* Block every crawler from the whole site, ignoring `rules` and
|
||||||
|
* `sitemaps`. Useful for staging deployments.
|
||||||
|
*/
|
||||||
|
disallow_all?: boolean;
|
||||||
|
};
|
||||||
|
export type BunextRobotsConfig = BunextGenerateRobotsTxtParams & {
|
||||||
|
/**
|
||||||
|
* Output file, relative to the project root. Defaults to
|
||||||
|
* `public/robots.txt`, which is served at `/robots.txt`.
|
||||||
|
*/
|
||||||
|
output?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Written by `bunext build`. Keyed by page path relative to the project
|
||||||
|
* root. `lastmod` is the build date at which `fingerprint` last changed.
|
||||||
|
*/
|
||||||
|
export type BunextPageLastmodRecord = {
|
||||||
|
pages: {
|
||||||
|
[local_path: string]: {
|
||||||
|
fingerprint: string;
|
||||||
|
lastmod: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
export type BuildErrorLike = {
|
||||||
|
text?: string;
|
||||||
|
message?: string;
|
||||||
|
id?: string;
|
||||||
|
location?: {
|
||||||
|
file?: string;
|
||||||
|
line?: number;
|
||||||
|
column?: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Logs each error once — identical error sets are suppressed until the
|
||||||
|
* signature changes or `resetBuildErrorLog()` is called (after a successful
|
||||||
|
* build). Returns true when something was logged.
|
||||||
|
*/
|
||||||
|
export declare function logBuildErrorsOnce(errors: BuildErrorLike[], logLine?: (line: string) => void): boolean;
|
||||||
|
/**
|
||||||
|
* Clears the dedup state so a new incident with the same signature is logged
|
||||||
|
* again. Call after a successful build.
|
||||||
|
*/
|
||||||
|
export declare function resetBuildErrorLog(): void;
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
// Logs build/render errors exactly once while the same error state persists.
|
||||||
|
//
|
||||||
|
// Dev recovery paths (watcher, request retries, post-build HMR) re-attempt a
|
||||||
|
// rebuild for every file edit and request while a syntax error is still in the
|
||||||
|
// file. Without deduplication that one error is printed again on every
|
||||||
|
// attempt — an endless spiral of identical log lines. The signature is
|
||||||
|
// cleared after a successful build, so the same error later (after a fix and
|
||||||
|
// regression) is logged again as a new incident.
|
||||||
|
let last_build_error_signature;
|
||||||
|
function errorSignature(errors) {
|
||||||
|
return errors
|
||||||
|
.map((e) => {
|
||||||
|
const text = e.text || e.message || e.id || "";
|
||||||
|
const loc = e.location
|
||||||
|
? `${e.location.file}:${e.location.line}:${e.location.column}`
|
||||||
|
: "";
|
||||||
|
return `${text}${loc}`;
|
||||||
|
})
|
||||||
|
.join("\n---\n");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Logs each error once — identical error sets are suppressed until the
|
||||||
|
* signature changes or `resetBuildErrorLog()` is called (after a successful
|
||||||
|
* build). Returns true when something was logged.
|
||||||
|
*/
|
||||||
|
export function logBuildErrorsOnce(errors, logLine = (line) => console.error(line)) {
|
||||||
|
if (!errors || errors.length === 0)
|
||||||
|
return false;
|
||||||
|
const signature = errorSignature(errors);
|
||||||
|
if (signature === last_build_error_signature)
|
||||||
|
return false;
|
||||||
|
last_build_error_signature = signature;
|
||||||
|
for (const e of errors) {
|
||||||
|
const text = e.text || e.message || e.id || "Build Error";
|
||||||
|
const loc = e.location
|
||||||
|
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
||||||
|
: "";
|
||||||
|
logLine(`${text}${loc}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clears the dedup state so a new incident with the same signature is logged
|
||||||
|
* again. Call after a successful build.
|
||||||
|
*/
|
||||||
|
export function resetBuildErrorLog() {
|
||||||
|
last_build_error_signature = undefined;
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* `robots` meta / `X-Robots-Tag` value used when `config.disable_seo`
|
||||||
|
* is set.
|
||||||
|
*/
|
||||||
|
export declare const NoSEORobotsValue = "noindex, nofollow";
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* `robots` meta / `X-Robots-Tag` value used when `config.disable_seo`
|
||||||
|
* is set.
|
||||||
|
*/
|
||||||
|
export const NoSEORobotsValue = "noindex, nofollow";
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { BunextGenerateRobotsTxtParams } from "../types";
|
||||||
|
/**
|
||||||
|
* First line of every generated robots.txt. `bunext sitemap` only
|
||||||
|
* overwrites an existing robots.txt that starts with it, unless robots
|
||||||
|
* is configured explicitly.
|
||||||
|
*/
|
||||||
|
export declare const RobotsTxtGeneratedMarker = "# Generated by bunext";
|
||||||
|
/**
|
||||||
|
* # Generate a robots.txt string
|
||||||
|
*/
|
||||||
|
export default function generateRobotsTxt(params?: BunextGenerateRobotsTxtParams): string;
|
||||||
Vendored
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import grabOrigin from "./grab-origin";
|
||||||
|
/**
|
||||||
|
* First line of every generated robots.txt. `bunext sitemap` only
|
||||||
|
* overwrites an existing robots.txt that starts with it, unless robots
|
||||||
|
* is configured explicitly.
|
||||||
|
*/
|
||||||
|
export const RobotsTxtGeneratedMarker = "# Generated by bunext";
|
||||||
|
/**
|
||||||
|
* # Generate a robots.txt string
|
||||||
|
*/
|
||||||
|
export default function generateRobotsTxt(params) {
|
||||||
|
const lines = [RobotsTxtGeneratedMarker, ""];
|
||||||
|
if (params?.disallow_all) {
|
||||||
|
lines.push("User-agent: *", "Disallow: /", "");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
const rules = params?.rules?.length
|
||||||
|
? params.rules
|
||||||
|
: [{ user_agent: "*", allow: ["/"] }];
|
||||||
|
for (const rule of rules) {
|
||||||
|
const user_agents = [rule.user_agent].flat();
|
||||||
|
for (const user_agent of user_agents) {
|
||||||
|
lines.push(`User-agent: ${sanitizeValue(user_agent)}`);
|
||||||
|
}
|
||||||
|
for (const allow of rule.allow || []) {
|
||||||
|
lines.push(`Allow: ${sanitizeValue(allow)}`);
|
||||||
|
}
|
||||||
|
for (const disallow of rule.disallow || []) {
|
||||||
|
lines.push(`Disallow: ${sanitizeValue(disallow)}`);
|
||||||
|
}
|
||||||
|
// A group needs at least one rule; an empty Disallow allows all.
|
||||||
|
if (!rule.allow?.length && !rule.disallow?.length) {
|
||||||
|
lines.push("Disallow:");
|
||||||
|
}
|
||||||
|
if (typeof rule.crawl_delay == "number") {
|
||||||
|
lines.push(`Crawl-delay: ${rule.crawl_delay}`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
if (params?.sitemaps?.length) {
|
||||||
|
const origin = (params.origin || grabOrigin()).replace(/\/+$/, "");
|
||||||
|
for (const sitemap of new Set(params.sitemaps)) {
|
||||||
|
const url = sitemap.match(/^https?:\/\//)
|
||||||
|
? sitemap
|
||||||
|
: `${origin}/${sitemap.replace(/^\/+/, "")}`;
|
||||||
|
lines.push(`Sitemap: ${sanitizeValue(url)}`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Line breaks would let a value inject extra directives.
|
||||||
|
*/
|
||||||
|
function sanitizeValue(value) {
|
||||||
|
return value.replace(/[\r\n]+/g, " ").trim();
|
||||||
|
}
|
||||||
Vendored
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import type { BunextGenerateSitemapParams, BunextSitemapEntry } from "../types";
|
||||||
|
/**
|
||||||
|
* Protocol limits for a single sitemap file
|
||||||
|
* https://www.sitemaps.org/protocol.html#index
|
||||||
|
*/
|
||||||
|
export declare const SitemapMaxUrlsPerFile = 50000;
|
||||||
|
export declare const SitemapMaxBytesPerFile: number;
|
||||||
|
export declare const SitemapDefaultChunkSize = 10000;
|
||||||
|
export declare function grabPendingSitemapTask(): string | undefined;
|
||||||
|
export type SitemapChunkFile = {
|
||||||
|
file_name: string;
|
||||||
|
xml: string;
|
||||||
|
url_count: number;
|
||||||
|
};
|
||||||
|
export type SitemapFiles = {
|
||||||
|
/**
|
||||||
|
* A `<urlset>` when everything fits in one file, otherwise a
|
||||||
|
* `<sitemapindex>` referencing `chunks`.
|
||||||
|
*/
|
||||||
|
sitemap: string;
|
||||||
|
chunks: SitemapChunkFile[];
|
||||||
|
url_count: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* # Generate a sitemap.xml string for all pages
|
||||||
|
* Dynamic routes are only included through a `sitemap` export in their
|
||||||
|
* `.server.ts` file, or through `additional_paths`. Always returns a
|
||||||
|
* single `<urlset>`; `bunext sitemap` splits large sitemaps into chunks.
|
||||||
|
*/
|
||||||
|
export default function generateSitemap(params?: BunextGenerateSitemapParams): Promise<string>;
|
||||||
|
/**
|
||||||
|
* # Generate sitemap.xml, split into chunk files when needed
|
||||||
|
* Chunks hold at most `chunk_size` URLs and 50MB each. `chunks_url_path`
|
||||||
|
* is the public URL path the chunk files are served from.
|
||||||
|
*/
|
||||||
|
export declare function generateSitemapFiles({ chunk_size, chunks_url_path, ...params }: BunextGenerateSitemapParams & {
|
||||||
|
chunk_size?: number;
|
||||||
|
chunks_url_path: string;
|
||||||
|
}): Promise<SitemapFiles>;
|
||||||
|
export declare function grabSitemapEntries(params?: BunextGenerateSitemapParams): Promise<BunextSitemapEntry[]>;
|
||||||
Vendored
+290
@@ -0,0 +1,290 @@
|
|||||||
|
import { statSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import grabPageServerPath from "../functions/server/web-pages/grab-page-server-path";
|
||||||
|
import grabAllPages from "./grab-all-pages";
|
||||||
|
import grabDirNames from "./grab-dir-names";
|
||||||
|
import grabOrigin from "./grab-origin";
|
||||||
|
import grabRelativePath from "./grab-relative-path";
|
||||||
|
import isDevelopment from "./is-development";
|
||||||
|
import { log } from "./log";
|
||||||
|
import { readPageLastmodRecord } from "./page-lastmod-record";
|
||||||
|
const ErrorPagePaths = ["/404", "/500"];
|
||||||
|
/**
|
||||||
|
* Protocol limits for a single sitemap file
|
||||||
|
* https://www.sitemaps.org/protocol.html#index
|
||||||
|
*/
|
||||||
|
export const SitemapMaxUrlsPerFile = 50_000;
|
||||||
|
export const SitemapMaxBytesPerFile = 50 * 1024 * 1024;
|
||||||
|
export const SitemapDefaultChunkSize = 10_000;
|
||||||
|
const SitemapXmlHeader = `<?xml version="1.0" encoding="UTF-8"?>`;
|
||||||
|
const SitemapXmlns = `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"`;
|
||||||
|
/**
|
||||||
|
* What sitemap generation is currently waiting on, so `bunext sitemap`
|
||||||
|
* can name it when a `sitemap` export or `additional_paths` never settles.
|
||||||
|
*/
|
||||||
|
let pending_sitemap_task;
|
||||||
|
export function grabPendingSitemapTask() {
|
||||||
|
return pending_sitemap_task;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* # Generate a sitemap.xml string for all pages
|
||||||
|
* Dynamic routes are only included through a `sitemap` export in their
|
||||||
|
* `.server.ts` file, or through `additional_paths`. Always returns a
|
||||||
|
* single `<urlset>`; `bunext sitemap` splits large sitemaps into chunks.
|
||||||
|
*/
|
||||||
|
export default async function generateSitemap(params) {
|
||||||
|
const entries = await grabSitemapEntries(params);
|
||||||
|
const origin = grabSitemapOrigin(params?.origin);
|
||||||
|
if (entries.length > SitemapMaxUrlsPerFile) {
|
||||||
|
log.warn(`Sitemap has ${entries.length} URLs, above the ${SitemapMaxUrlsPerFile} ` +
|
||||||
|
`per-file limit. Use \`bunext sitemap\` to split it into chunks.`);
|
||||||
|
}
|
||||||
|
return renderUrlset(entries.map((entry) => renderUrl(entry, origin)));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* # Generate sitemap.xml, split into chunk files when needed
|
||||||
|
* Chunks hold at most `chunk_size` URLs and 50MB each. `chunks_url_path`
|
||||||
|
* is the public URL path the chunk files are served from.
|
||||||
|
*/
|
||||||
|
export async function generateSitemapFiles({ chunk_size = SitemapDefaultChunkSize, chunks_url_path, ...params }) {
|
||||||
|
const entries = await grabSitemapEntries(params);
|
||||||
|
const origin = grabSitemapOrigin(params.origin);
|
||||||
|
const max_urls = Math.min(Math.max(Math.floor(chunk_size) || 1, 1), SitemapMaxUrlsPerFile);
|
||||||
|
const urls = entries.map((entry) => renderUrl(entry, origin));
|
||||||
|
const empty_urlset_bytes = Buffer.byteLength(renderUrlset([]));
|
||||||
|
const chunks = [];
|
||||||
|
let chunk;
|
||||||
|
for (let i = 0; i < urls.length; i++) {
|
||||||
|
const url_bytes = Buffer.byteLength(urls[i]) + 1;
|
||||||
|
if (!chunk ||
|
||||||
|
chunk.urls.length >= max_urls ||
|
||||||
|
chunk.bytes + url_bytes > SitemapMaxBytesPerFile) {
|
||||||
|
chunk = { urls: [], bytes: empty_urlset_bytes };
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
chunk.urls.push(urls[i]);
|
||||||
|
chunk.bytes += url_bytes;
|
||||||
|
const lastmod = parseLastmod(entries[i].lastmod);
|
||||||
|
if (lastmod && (!chunk.lastmod || lastmod > chunk.lastmod)) {
|
||||||
|
chunk.lastmod = lastmod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunks.length <= 1) {
|
||||||
|
return {
|
||||||
|
sitemap: renderUrlset(chunks[0]?.urls || []),
|
||||||
|
chunks: [],
|
||||||
|
url_count: urls.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const chunks_dir_path = chunks_url_path.replace(/^\/+|\/+$/g, "");
|
||||||
|
const chunk_base_url = chunks_dir_path
|
||||||
|
? `${origin}/${chunks_dir_path}`
|
||||||
|
: origin;
|
||||||
|
const chunk_files = chunks.map((c, i) => ({
|
||||||
|
file_name: `sitemap-${i + 1}.xml`,
|
||||||
|
xml: renderUrlset(c.urls),
|
||||||
|
url_count: c.urls.length,
|
||||||
|
}));
|
||||||
|
const sitemap = [
|
||||||
|
SitemapXmlHeader,
|
||||||
|
`<sitemapindex ${SitemapXmlns}>`,
|
||||||
|
...chunk_files.map((file, i) => {
|
||||||
|
const loc = escapeXml(`${chunk_base_url}/${file.file_name}`);
|
||||||
|
const lines = [` <loc>${loc}</loc>`];
|
||||||
|
const lastmod = chunks[i].lastmod?.toISOString();
|
||||||
|
if (lastmod)
|
||||||
|
lines.push(` <lastmod>${lastmod}</lastmod>`);
|
||||||
|
return ` <sitemap>\n${lines.join("\n")}\n </sitemap>`;
|
||||||
|
}),
|
||||||
|
`</sitemapindex>`,
|
||||||
|
``,
|
||||||
|
].join("\n");
|
||||||
|
return { sitemap, chunks: chunk_files, url_count: urls.length };
|
||||||
|
}
|
||||||
|
function grabSitemapOrigin(origin) {
|
||||||
|
return (origin || grabOrigin()).replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
function renderUrlset(urls) {
|
||||||
|
return [
|
||||||
|
SitemapXmlHeader,
|
||||||
|
`<urlset ${SitemapXmlns}>`,
|
||||||
|
...urls,
|
||||||
|
`</urlset>`,
|
||||||
|
``,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
function renderUrl(entry, origin) {
|
||||||
|
const loc = entry.path.match(/^https?:\/\//)
|
||||||
|
? entry.path
|
||||||
|
: `${origin}${entry.path}`;
|
||||||
|
const lines = [` <loc>${escapeXml(loc)}</loc>`];
|
||||||
|
if (entry.lastmod) {
|
||||||
|
const lastmod = entry.lastmod instanceof Date
|
||||||
|
? entry.lastmod.toISOString()
|
||||||
|
: entry.lastmod;
|
||||||
|
lines.push(` <lastmod>${escapeXml(lastmod)}</lastmod>`);
|
||||||
|
}
|
||||||
|
if (entry.changefreq) {
|
||||||
|
lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
||||||
|
}
|
||||||
|
if (typeof entry.priority == "number") {
|
||||||
|
const priority = Math.min(Math.max(entry.priority, 0), 1);
|
||||||
|
lines.push(` <priority>${priority.toFixed(1)}</priority>`);
|
||||||
|
}
|
||||||
|
return ` <url>\n${lines.join("\n")}\n </url>`;
|
||||||
|
}
|
||||||
|
function parseLastmod(lastmod) {
|
||||||
|
if (!lastmod)
|
||||||
|
return;
|
||||||
|
const date = lastmod instanceof Date ? lastmod : new Date(lastmod);
|
||||||
|
return isNaN(date.getTime()) ? undefined : date;
|
||||||
|
}
|
||||||
|
export async function grabSitemapEntries(params) {
|
||||||
|
const use_file_lastmod = params?.use_file_lastmod ?? false;
|
||||||
|
const lastmod_record = readPageLastmodRecord(params?.lastmod_file);
|
||||||
|
const entries = [];
|
||||||
|
const pages = grabAllPages({ exclude_api: true });
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
const page = pages[i];
|
||||||
|
if (!page.local_path)
|
||||||
|
continue;
|
||||||
|
const page_path = page.url_path.replace(/\/index$/, "") || "/";
|
||||||
|
if (ErrorPagePaths.includes(page_path))
|
||||||
|
continue;
|
||||||
|
const page_sitemap_paths = await grabPageSitemapPaths({
|
||||||
|
file_path: page.local_path,
|
||||||
|
route: page_path,
|
||||||
|
});
|
||||||
|
// When the page's client bundle last changed, per `bunext build`
|
||||||
|
const build_lastmod = lastmod_record?.pages[grabRelativePath(page.local_path)]?.lastmod;
|
||||||
|
if (page_sitemap_paths) {
|
||||||
|
for (const page_sitemap_path of page_sitemap_paths) {
|
||||||
|
const entry = toSitemapEntry(page_sitemap_path);
|
||||||
|
entries.push({
|
||||||
|
...entry,
|
||||||
|
lastmod: entry.lastmod || build_lastmod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (page.url_path.match(/\[/))
|
||||||
|
continue;
|
||||||
|
entries.push({
|
||||||
|
path: page_path,
|
||||||
|
lastmod: build_lastmod ||
|
||||||
|
(use_file_lastmod
|
||||||
|
? statSync(page.local_path).mtime
|
||||||
|
: undefined),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let additional_paths;
|
||||||
|
if (typeof params?.additional_paths == "function") {
|
||||||
|
pending_sitemap_task = "`sitemap.additional_paths` in the config";
|
||||||
|
try {
|
||||||
|
additional_paths = await params.additional_paths();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
pending_sitemap_task = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
additional_paths = params?.additional_paths;
|
||||||
|
}
|
||||||
|
entries.push(...(additional_paths || []).map(toSitemapEntry));
|
||||||
|
const seen = new Set();
|
||||||
|
const final_entries = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const path = normalizePath(entry.path, params?.trailing_slash);
|
||||||
|
if (seen.has(path))
|
||||||
|
continue;
|
||||||
|
if (isExcluded(path, params?.exclude))
|
||||||
|
continue;
|
||||||
|
seen.add(path);
|
||||||
|
final_entries.push({
|
||||||
|
...entry,
|
||||||
|
path,
|
||||||
|
lastmod: entry.lastmod || params?.default_lastmod,
|
||||||
|
changefreq: entry.changefreq || params?.default_changefreq,
|
||||||
|
priority: entry.priority ?? params?.default_priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return final_entries;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* # Run the `sitemap` export of a page's server file, if any
|
||||||
|
*/
|
||||||
|
async function grabPageSitemapPaths({ file_path, route, }) {
|
||||||
|
const { server_file_path } = grabPageServerPath({ file_path });
|
||||||
|
if (!server_file_path)
|
||||||
|
return;
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
const server_ctx = global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[grabRelativePath(server_file_path)];
|
||||||
|
const final_server_path = server_ctx?.local_path
|
||||||
|
? path.join(ROOT_DIR, server_ctx.path)
|
||||||
|
: server_file_path;
|
||||||
|
const relative_server_path = grabRelativePath(server_file_path);
|
||||||
|
try {
|
||||||
|
pending_sitemap_task = `importing ${relative_server_path}`;
|
||||||
|
// Bust the import cache in development so edits to the server file
|
||||||
|
// are picked up without a restart.
|
||||||
|
const server_module = await import(isDevelopment()
|
||||||
|
? `${final_server_path}?t=${Date.now()}`
|
||||||
|
: final_server_path);
|
||||||
|
if (typeof server_module.sitemap != "function")
|
||||||
|
return;
|
||||||
|
pending_sitemap_task = `the \`sitemap\` export of ${relative_server_path}`;
|
||||||
|
try {
|
||||||
|
return await server_module.sitemap({ route });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error(`\`sitemap\` export of ${relative_server_path} failed: ${error?.message || error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
pending_sitemap_task = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toSitemapEntry(path) {
|
||||||
|
return typeof path == "string" ? { path } : path;
|
||||||
|
}
|
||||||
|
function normalizePath(path, trailing_slash) {
|
||||||
|
if (path.match(/^https?:\/\//))
|
||||||
|
return path;
|
||||||
|
let new_path = `/${path}`.replace(/\/{2,}/g, "/");
|
||||||
|
if (new_path != "/")
|
||||||
|
new_path = new_path.replace(/\/$/, "");
|
||||||
|
if (trailing_slash && new_path != "/")
|
||||||
|
new_path += "/";
|
||||||
|
return new_path;
|
||||||
|
}
|
||||||
|
function isExcluded(path, exclude) {
|
||||||
|
if (!exclude?.length)
|
||||||
|
return false;
|
||||||
|
const bare_path = path.replace(/^https?:\/\/[^/]+/, "").replace(/(.)\/$/, "$1") || "/";
|
||||||
|
for (const pattern of exclude) {
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
if (pattern.test(path) || pattern.test(bare_path))
|
||||||
|
return true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(pattern.replace(/\/\*$/, ""));
|
||||||
|
if (pattern.endsWith("/*")) {
|
||||||
|
if (bare_path == normalized ||
|
||||||
|
bare_path.startsWith(`${normalized.replace(/\/$/, "")}/`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bare_path == normalized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function escapeXml(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ export default function grabConstants() {
|
|||||||
const ServerDefaultRequestBodyLimitBytes = MB_IN_BYTES * 10;
|
const ServerDefaultRequestBodyLimitBytes = MB_IN_BYTES * 10;
|
||||||
const MaxBundlerRebuilds = 5;
|
const MaxBundlerRebuilds = 5;
|
||||||
const RouteIgnorePatterns = [/\/\(/];
|
const RouteIgnorePatterns = [/\/\(/];
|
||||||
if (config.pages_exclude_patterns) {
|
if (config?.pages_exclude_patterns) {
|
||||||
RouteIgnorePatterns.push(...config.pages_exclude_patterns);
|
RouteIgnorePatterns.push(...config.pages_exclude_patterns);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
Vendored
+1
@@ -29,5 +29,6 @@ export type DirNames = {
|
|||||||
BUNDLER_CTX_MAP_JSON: string;
|
BUNDLER_CTX_MAP_JSON: string;
|
||||||
SSR_BUNDLER_CTX_MAP_JSON: string;
|
SSR_BUNDLER_CTX_MAP_JSON: string;
|
||||||
BUILD_INFO_JSON: string;
|
BUILD_INFO_JSON: string;
|
||||||
|
PAGE_LASTMOD_JSON: string;
|
||||||
};
|
};
|
||||||
export default function grabDirNames(): DirNames;
|
export default function grabDirNames(): DirNames;
|
||||||
|
|||||||
Vendored
+4
-1
@@ -19,7 +19,8 @@ export default function grabDirNames() {
|
|||||||
const BUNEXT_PUBLIC_DIR = path.join(BUNX_CWD_DIR, "public");
|
const BUNEXT_PUBLIC_DIR = path.join(BUNX_CWD_DIR, "public");
|
||||||
const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages");
|
const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages");
|
||||||
const BUNEXT_VENDOR_DIR = path.join(BUNEXT_PUBLIC_DIR, "vendor");
|
const BUNEXT_VENDOR_DIR = path.join(BUNEXT_PUBLIC_DIR, "vendor");
|
||||||
const BUNEXT_CACHE_DIR = path.join(BUNEXT_PUBLIC_DIR, "cache");
|
// Kept outside BUNEXT_PUBLIC_DIR: that directory is served over HTTP.
|
||||||
|
const BUNEXT_CACHE_DIR = path.join(BUNX_CWD_DIR, "cache");
|
||||||
const HYDRATION_DST_DIR_MAP_JSON_FILE_NAME = "map.json";
|
const HYDRATION_DST_DIR_MAP_JSON_FILE_NAME = "map.json";
|
||||||
const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join(HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME);
|
const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join(HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME);
|
||||||
const BUNX_ROOT_DIR = path.resolve(__dirname, "../../");
|
const BUNX_ROOT_DIR = path.resolve(__dirname, "../../");
|
||||||
@@ -32,6 +33,7 @@ export default function grabDirNames() {
|
|||||||
const BUNDLER_CTX_MAP_JSON = path.join(BUNX_CWD_DIR, "bundler-ctx-map.json");
|
const BUNDLER_CTX_MAP_JSON = path.join(BUNX_CWD_DIR, "bundler-ctx-map.json");
|
||||||
const SSR_BUNDLER_CTX_MAP_JSON = path.join(BUNX_CWD_DIR, "ssr-bundler-ctx-map.json");
|
const SSR_BUNDLER_CTX_MAP_JSON = path.join(BUNX_CWD_DIR, "ssr-bundler-ctx-map.json");
|
||||||
const BUILD_INFO_JSON = path.join(BUNX_CWD_DIR, "build-info.json");
|
const BUILD_INFO_JSON = path.join(BUNX_CWD_DIR, "build-info.json");
|
||||||
|
const PAGE_LASTMOD_JSON = path.join(BUNX_CWD_DIR, "page-lastmod.json");
|
||||||
return {
|
return {
|
||||||
ROOT_DIR,
|
ROOT_DIR,
|
||||||
SRC_DIR,
|
SRC_DIR,
|
||||||
@@ -63,6 +65,7 @@ export default function grabDirNames() {
|
|||||||
BUNDLER_CTX_MAP_JSON,
|
BUNDLER_CTX_MAP_JSON,
|
||||||
SSR_BUNDLER_CTX_MAP_JSON,
|
SSR_BUNDLER_CTX_MAP_JSON,
|
||||||
BUILD_INFO_JSON,
|
BUILD_INFO_JSON,
|
||||||
|
PAGE_LASTMOD_JSON,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// const NODE_MODULES_DIR = path.resolve(
|
// const NODE_MODULES_DIR = path.resolve(
|
||||||
|
|||||||
Vendored
+9
-1
@@ -2,6 +2,14 @@ import type { BunxRouteParams } from "../types";
|
|||||||
type Params = {
|
type Params = {
|
||||||
req: Request;
|
req: Request;
|
||||||
query?: any;
|
query?: any;
|
||||||
|
/**
|
||||||
|
* Maximum request body size. Defaults to
|
||||||
|
* `ServerDefaultRequestBodyLimitBytes`.
|
||||||
|
*/
|
||||||
|
max_body_bytes?: number;
|
||||||
};
|
};
|
||||||
export default function grabRouteParams({ req, query: passed_query, }: Params): Promise<BunxRouteParams>;
|
/**
|
||||||
|
* Throws `RequestBodyTooLargeError` if the body exceeds `max_body_bytes`.
|
||||||
|
*/
|
||||||
|
export default function grabRouteParams({ req: initial_req, query: passed_query, max_body_bytes, }: Params): Promise<BunxRouteParams>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+28
-4
@@ -1,11 +1,35 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import deserializeQuery from "./deserialize-query";
|
import deserializeQuery from "./deserialize-query";
|
||||||
export default async function grabRouteParams({ req, query: passed_query, }) {
|
import grabConstants from "./grab-constants";
|
||||||
const url = new URL(req.url);
|
import readRequestBody from "./read-request-body";
|
||||||
|
/**
|
||||||
|
* Throws `RequestBodyTooLargeError` if the body exceeds `max_body_bytes`.
|
||||||
|
*/
|
||||||
|
export default async function grabRouteParams({ req: initial_req, query: passed_query, max_body_bytes, }) {
|
||||||
|
const url = new URL(initial_req.url);
|
||||||
const query = deserializeQuery(Object.fromEntries(url.searchParams));
|
const query = deserializeQuery(Object.fromEntries(url.searchParams));
|
||||||
const body = await (async () => {
|
const raw_body = await readRequestBody({
|
||||||
|
req: initial_req,
|
||||||
|
max_bytes: max_body_bytes || grabConstants().ServerDefaultRequestBodyLimitBytes,
|
||||||
|
});
|
||||||
|
// The original body stream is consumed above, so hand handlers a fresh
|
||||||
|
// request over the buffered bytes. This keeps `req.text()`,
|
||||||
|
// `req.formData()`, etc. usable (e.g. for webhook signatures/uploads).
|
||||||
|
const req = raw_body
|
||||||
|
? new Request(initial_req.url, {
|
||||||
|
method: initial_req.method,
|
||||||
|
headers: initial_req.headers,
|
||||||
|
body: raw_body,
|
||||||
|
signal: initial_req.signal,
|
||||||
|
})
|
||||||
|
: initial_req;
|
||||||
|
const body = (() => {
|
||||||
|
if (initial_req.method == "GET")
|
||||||
|
return undefined;
|
||||||
|
if (!raw_body)
|
||||||
|
return {};
|
||||||
try {
|
try {
|
||||||
return req.method == "GET" ? undefined : await req.json();
|
return JSON.parse(new TextDecoder().decode(raw_body) || "{}");
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Imports a module by its stable path.
|
||||||
|
*
|
||||||
|
* Dev: when the file changed since it was last imported, its registry
|
||||||
|
* entry is evicted first so the new version is evaluated and the old one
|
||||||
|
* can be garbage collected. A cache-busting `?t=` query would instead
|
||||||
|
* create a new, never-evicted module record on every call.
|
||||||
|
*
|
||||||
|
* Prod: plain import, so each module is loaded (and kept) once.
|
||||||
|
*/
|
||||||
|
export default function importFresh<T = any>(file_path: string): Promise<T>;
|
||||||
|
/**
|
||||||
|
* Drops a module from Bun's registry so its memory can be reclaimed and the
|
||||||
|
* next import re-evaluates it. Only the module itself is evicted, not the
|
||||||
|
* modules it imports.
|
||||||
|
*/
|
||||||
|
export declare function evictModule(file_path: string): void;
|
||||||
|
/**
|
||||||
|
* Evicts every registry entry under `dir` whose file no longer exists. Bundle
|
||||||
|
* outputs (entries and shared chunks) are content-hashed and esbuild deletes
|
||||||
|
* the superseded ones on rebuild, so a missing file means no current module
|
||||||
|
* imports it anymore.
|
||||||
|
*/
|
||||||
|
export declare function evictDeletedModules(dir: string): void;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user