Compare commits
4 Commits
67ed8749e2
...
60ee353bf0
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ee353bf0 | |||
| 342f56f3f5 | |||
| 7dd8d87be8 | |||
| 6f1db7c01f |
18
CLAUDE.md
18
CLAUDE.md
@ -40,11 +40,21 @@ bunext start # Start production server from pre-built artifacts
|
||||
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
|
||||
|
||||
### Page module contract
|
||||
Pages live in `src/pages/`. A page file may export:
|
||||
- Default export: React component receiving `ServerProps | StaticProps`
|
||||
- `server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||
Pages live in `src/pages/`. Server logic is **separated from page files** into companion `.server.ts` / `.server.tsx` files to avoid bundling server-only code into the client.
|
||||
|
||||
**Page file** (`page.tsx`) — client-safe exports only (bundled to the browser):
|
||||
- Default export: React component receiving props from the server file
|
||||
- `meta`: `BunextPageModuleMeta` — SEO/OG metadata
|
||||
- `head`: ReactNode — extra `<head>` content
|
||||
- `Head`: FC — extra `<head>` content
|
||||
- `config`: `BunextRouteConfig` — cache settings
|
||||
- `html_props`: `BunextHTMLProps` — attributes on the `<html>` element
|
||||
|
||||
**Server file** (`page.server.ts` or `page.server.tsx`) — server-only, never sent to the browser:
|
||||
- Default export or `export const server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||
|
||||
The framework resolves the companion by replacing the page extension with `.server.ts` or `.server.tsx`. If neither exists, no server function runs and only the default `url` prop is injected.
|
||||
|
||||
`__root.tsx` follows the same contract; its server companion is `__root.server.ts`.
|
||||
|
||||
API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.
|
||||
|
||||
|
||||
86
README.md
86
README.md
@ -200,12 +200,15 @@ my-app/
|
||||
├── src/
|
||||
│ └── pages/ # File-system routes (pages and API handlers)
|
||||
│ ├── __root.tsx # Optional: root layout wrapper for all pages
|
||||
│ ├── __root.server.ts # Optional: root-level server logic
|
||||
│ ├── index.tsx # Route: /
|
||||
│ ├── index.server.ts # Optional: server logic for index route
|
||||
│ ├── about.tsx # Route: /about
|
||||
│ ├── 404.tsx # Optional: custom 404 page
|
||||
│ ├── 500.tsx # Optional: custom 500 page
|
||||
│ ├── blog/
|
||||
│ │ ├── index.tsx # Route: /blog
|
||||
│ │ ├── index.server.ts # Server logic for /blog
|
||||
│ │ └── [slug].tsx # Route: /blog/:slug (dynamic)
|
||||
│ └── api/
|
||||
│ └── users.ts # API route: /api/users
|
||||
@ -275,19 +278,15 @@ export default function HomePage() {
|
||||
|
||||
### Server Function
|
||||
|
||||
Export a `server` function to run server-side logic before rendering. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
|
||||
Server logic lives in a companion **`.server.ts`** (or `.server.tsx`) file alongside the page. The framework looks for `<page>.server.ts` or `<page>.server.tsx` next to the page file and loads it separately on the server — it is never bundled into the client JS.
|
||||
|
||||
```tsx
|
||||
// src/pages/profile.tsx
|
||||
The server file exports the server function as either `export default` or `export const server`. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
|
||||
|
||||
```ts
|
||||
// src/pages/profile.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
type Props = {
|
||||
props?: { username: string; bio: string };
|
||||
query?: Record<string, string>;
|
||||
url?: URL;
|
||||
};
|
||||
|
||||
export const server: BunextPageServerFn<{
|
||||
const server: BunextPageServerFn<{
|
||||
username: string;
|
||||
bio: string;
|
||||
}> = async (ctx) => {
|
||||
@ -304,6 +303,17 @@ export const server: BunextPageServerFn<{
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/pages/profile.tsx (client-only exports — bundled to the browser)
|
||||
type Props = {
|
||||
props?: { username: string; bio: string };
|
||||
query?: Record<string, string>;
|
||||
url?: URL;
|
||||
};
|
||||
|
||||
export default function ProfilePage({ props, url }: Props) {
|
||||
return (
|
||||
<div>
|
||||
@ -315,6 +325,8 @@ export default function ProfilePage({ props, url }: Props) {
|
||||
}
|
||||
```
|
||||
|
||||
> **Why separate files?** Bundling server code (Bun APIs, database clients, `fs`, secrets) into the same file as a React component causes TypeScript compilation errors because the bundler processes the page file for the browser. The `.server.ts` companion file is loaded only by the server at request time and is never included in the client bundle.
|
||||
|
||||
The server function receives a `ctx` object (type `BunxRouteParams`) with:
|
||||
|
||||
| Field | Type | Description |
|
||||
@ -366,10 +378,13 @@ The `url` prop exposes the following fields from the standard Web `URL` interfac
|
||||
|
||||
### Redirects from Server
|
||||
|
||||
Return a `redirect` object from the `server` function to redirect the client:
|
||||
Return a `redirect` object from the server function to redirect the client:
|
||||
|
||||
```tsx
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
```ts
|
||||
// src/pages/dashboard.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const isLoggedIn = false; // check auth
|
||||
|
||||
if (!isLoggedIn) {
|
||||
@ -384,6 +399,8 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
`permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided.
|
||||
@ -392,8 +409,11 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
|
||||
Control status codes, headers, and other response options from the server function:
|
||||
|
||||
```tsx
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
```ts
|
||||
// src/pages/submit.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
return {
|
||||
props: { message: "Created" },
|
||||
responseOptions: {
|
||||
@ -404,11 +424,13 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
### SEO Metadata
|
||||
|
||||
Export a `meta` object to inject SEO and Open Graph tags into the `<head>`:
|
||||
Export a `meta` object from the **page file** (not the server file) to inject SEO and Open Graph tags into the `<head>`:
|
||||
|
||||
```tsx
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
@ -447,7 +469,7 @@ export default function AboutPage() {
|
||||
|
||||
### Dynamic Metadata
|
||||
|
||||
`meta` can also be an async function that receives the request context and server response:
|
||||
`meta` can also be an async function that receives the request context and server response. Like `meta`, it is exported from the **page file**:
|
||||
|
||||
```tsx
|
||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
||||
@ -483,7 +505,21 @@ export default function Page() {
|
||||
|
||||
### Root Layout
|
||||
|
||||
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props:
|
||||
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props.
|
||||
|
||||
If the root layout also needs server-side logic, place it in `src/pages/__root.server.ts` (or `.server.tsx`) — the same `.server.*` convention used by regular pages:
|
||||
|
||||
```ts
|
||||
// src/pages/__root.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
// e.g. fetch navigation links, check auth
|
||||
return { props: { navLinks: ["/", "/about"] } };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/pages/__root.tsx
|
||||
@ -636,12 +672,13 @@ export default function ProductsPage() {
|
||||
|
||||
### Dynamic Cache Control from Server Function
|
||||
|
||||
Cache settings can also be returned from the `server` function, which lets you conditionally enable caching based on request data:
|
||||
Cache settings can also be returned from the server function, which lets you conditionally enable caching based on request data:
|
||||
|
||||
```tsx
|
||||
```ts
|
||||
// src/pages/products.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const data = await fetchProducts();
|
||||
|
||||
return {
|
||||
@ -651,6 +688,11 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/pages/products.tsx
|
||||
export default function ProductsPage({ props }: any) {
|
||||
return (
|
||||
<ul>
|
||||
@ -957,7 +999,7 @@ Request
|
||||
1. Match route via FileSystemRouter
|
||||
2. Find bundled artifact in BUNDLER_CTX_MAP
|
||||
3. Import page module (with cache-busting timestamp in dev)
|
||||
4. Run module.server(ctx) for server-side data
|
||||
4. Import companion server module (<page>.server.ts/tsx) if it exists; run its exported function for server-side data
|
||||
5. Resolve meta (static object or async function)
|
||||
6. renderToString(component) → inject into HTML template
|
||||
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>
|
||||
|
||||
@ -58,7 +58,7 @@ This report compares the two on their overlapping surface — server-side render
|
||||
| Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
|
||||
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
|
||||
| Component model | Classic SSR + hydration | React Server Components + Client Components |
|
||||
| Data fetching | Per-page `server` export | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
||||
| Data fetching | Per-page `.server.ts` companion file | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
||||
| State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
|
||||
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
|
||||
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` |
|
||||
@ -210,19 +210,25 @@ Streaming SSR's benefits — progressive flushing, Suspense-based partial render
|
||||
|
||||
### Data Fetching
|
||||
|
||||
**Bunext** exposes a single data-fetching primitive: the `server` export on each page module.
|
||||
**Bunext** exposes a single data-fetching primitive: a companion **`.server.ts`** file alongside each page.
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/products.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const data = await db.query(...);
|
||||
return { props: { data } };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server function.
|
||||
The server file is never bundled into client JS — it runs exclusively on the server at request time. The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server file at all.
|
||||
|
||||
**Design notes:**
|
||||
- One server function per page. Data fetching is centralised at the page level, not scattered across components.
|
||||
- One server file per page. Data fetching is centralised at the page level, not scattered across components.
|
||||
- The page file (`.tsx`) exports only client-safe code — the React component, `meta`, `Head`, `config`, and `html_props`. Server-only code (Bun APIs, database clients, `fs`) lives in the `.server.ts` companion and is never sent to the browser.
|
||||
- All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently.
|
||||
- Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size.
|
||||
|
||||
@ -289,7 +295,7 @@ Bunext's caching model is its answer to SSG. Rather than pre-building pages at d
|
||||
|
||||
**Bunext** implements a **file-based HTML cache**:
|
||||
|
||||
- Enabled per-page via `config.cachePage` or returned dynamically from the `server` function at runtime.
|
||||
- Enabled per-page via `config.cachePage` (exported from the page file) or returned dynamically from the server function in the `.server.ts` companion at runtime.
|
||||
- On a cache miss, the rendered HTML is written to `public/__bunext/cache/<key>.res.html` alongside a metadata file `<key>.meta.json` (creation timestamp, expiry, paradigm).
|
||||
- On a cache hit, the HTML file is read and returned with `X-Bunext-Cache: HIT`.
|
||||
- A cron job runs every 30 seconds to delete expired entries.
|
||||
@ -316,7 +322,7 @@ The key distinction from SSG: Bunext's cache is **demand-driven**. A site with 1
|
||||
|
||||
### Metadata and SEO
|
||||
|
||||
**Bunext** supports both static and dynamic metadata:
|
||||
**Bunext** supports both static and dynamic metadata. These exports live in the **page file** (not the `.server.ts` companion), since they are processed at the server HTML-generation step and may reference types from the client module:
|
||||
|
||||
- `export const meta: BunextPageModuleMeta` — static object with `title`, `description`, `keywords`, `author`, `robots`, `canonical`, `themeColor`, `og.*`, and `twitter.*` fields.
|
||||
- `export const meta: BunextPageModuleMetaFn` — async function receiving `ctx` and `serverRes` for dynamic metadata based on fetched data.
|
||||
@ -540,10 +546,13 @@ Next.js wraps these in `NextRequest` and `NextResponse`, which add convenience m
|
||||
|
||||
### Conditional Runtime Caching
|
||||
|
||||
Bunext's `server` function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
||||
Bunext's server function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/products.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const user = await getUser(ctx.req);
|
||||
return {
|
||||
props: { data },
|
||||
@ -551,16 +560,21 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
cacheExpiry: 300,
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
Next.js's ISR and full-route cache operate on fixed revalidation intervals set at build time. There is no mechanism to decide at runtime whether a specific request should be cached.
|
||||
|
||||
### Page Response Transform
|
||||
|
||||
The `resTransform` field in the page `server` function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
||||
The `resTransform` field in the server function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/page.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
ctx.resTransform = (res) => {
|
||||
res.headers.set("X-Custom-Header", "value");
|
||||
return res;
|
||||
@ -568,6 +582,8 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
This exists because page responses are generated entirely by the framework (`renderToString` → HTML template). Unlike API routes — where the developer returns a `Response` directly and already has full control — there is no other hook to modify the final page response at the route level without going through global middleware.
|
||||
@ -588,7 +604,7 @@ Next.js's codebase spans Turbopack (Rust), SWC (Rust), the App Router internals,
|
||||
|
||||
The RSC model requires developers to constantly reason about the `"use client"` / `"use server"` boundary: what can be async, what has access to browser APIs, what gets serialized into the RSC payload. Mistakes at this boundary produce runtime errors that are difficult to diagnose.
|
||||
|
||||
Bunext has one rule: the `server` function runs on the server, the component runs on both (SSR then hydration). There is no boundary to reason about.
|
||||
Bunext has one rule: the `.server.ts` companion file runs on the server, the page file runs on both (SSR then hydration). The separation is enforced by the file system, not by decorators or directives. There is no boundary to reason about inside a file.
|
||||
|
||||
### No Vendor Lock-In
|
||||
|
||||
|
||||
8
dist/commands/build/index.js
vendored
8
dist/commands/build/index.js
vendored
@ -1,23 +1,23 @@
|
||||
import { Command } from "commander";
|
||||
import { log } from "../../utils/log";
|
||||
import init from "../../functions/init";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
// import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
export default function () {
|
||||
return new Command("build")
|
||||
.description("Build Project")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.BUILD = "true";
|
||||
try {
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
}
|
||||
catch (error) { }
|
||||
await rewritePagesModule();
|
||||
global.SKIPPED_BROWSER_MODULES = new Set();
|
||||
// await rewritePagesModule();
|
||||
await init();
|
||||
log.banner();
|
||||
log.build("Building Project ...");
|
||||
|
||||
4
dist/commands/dev/index.js
vendored
4
dist/commands/dev/index.js
vendored
@ -2,9 +2,9 @@ import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
export default function () {
|
||||
return new Command("dev")
|
||||
@ -17,8 +17,8 @@ export default function () {
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
}
|
||||
catch (error) { }
|
||||
await rewritePagesModule();
|
||||
await bunextInit();
|
||||
await allPagesBunBundler();
|
||||
await startServer();
|
||||
});
|
||||
}
|
||||
|
||||
3
dist/commands/start/index.js
vendored
3
dist/commands/start/index.js
vendored
@ -2,13 +2,14 @@ import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
log.info("Starting production server ...");
|
||||
await bunextInit();
|
||||
await allPagesBunBundler();
|
||||
await startServer();
|
||||
});
|
||||
}
|
||||
|
||||
15
dist/functions/bundler/all-pages-bun-bundler.js
vendored
15
dist/functions/bundler/all-pages-bun-bundler.js
vendored
@ -7,6 +7,7 @@ import path from "path";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import { mkdirSync, rmSync } from "fs";
|
||||
import recordArtifacts from "./record-artifacts";
|
||||
import BunSkipNonBrowserPlugin from "./plugins/bun-skip-browser-plugin";
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
export default async function allPagesBunBundler(params) {
|
||||
const { target = "browser", page_file_paths } = params || {};
|
||||
@ -37,15 +38,16 @@ export default async function allPagesBunBundler(params) {
|
||||
if (entryToPage.size === 0)
|
||||
return;
|
||||
const buildStart = performance.now();
|
||||
const define = {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
};
|
||||
const result = await Bun.build({
|
||||
entrypoints: [...entryToPage.keys()],
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
root: BUNX_HYDRATION_SRC_DIR,
|
||||
minify: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
define,
|
||||
naming: {
|
||||
entry: "[dir]/[hash].[ext]",
|
||||
chunk: "chunks/[hash].[ext]",
|
||||
@ -94,7 +96,10 @@ export default async function allPagesBunBundler(params) {
|
||||
});
|
||||
}
|
||||
if (artifacts?.[0]) {
|
||||
await recordArtifacts({ artifacts });
|
||||
await recordArtifacts({
|
||||
artifacts,
|
||||
page_file_paths,
|
||||
});
|
||||
}
|
||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
|
||||
31
dist/functions/bundler/all-pages-bundler.js
vendored
31
dist/functions/bundler/all-pages-bundler.js
vendored
@ -8,6 +8,7 @@ import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
|
||||
import { writeFileSync } from "fs";
|
||||
import recordArtifacts from "./record-artifacts";
|
||||
import stripServerSideLogic from "./strip-server-side-logic";
|
||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
let build_starts = 0;
|
||||
const MAX_BUILD_STARTS = 10;
|
||||
@ -23,7 +24,7 @@ export default async function allPagesBundler(params) {
|
||||
const virtualEntries = {};
|
||||
const dev = isDevelopment();
|
||||
for (const page of target_pages) {
|
||||
const key = page.transformed_path;
|
||||
const key = page.local_path;
|
||||
const txt = await grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
@ -32,6 +33,11 @@ export default async function allPagesBundler(params) {
|
||||
// }
|
||||
if (!txt)
|
||||
continue;
|
||||
// const final_tsx = stripServerSideLogic({
|
||||
// txt_code: txt,
|
||||
// file_path: key,
|
||||
// });
|
||||
// console.log("final_tsx", final_tsx);
|
||||
virtualEntries[key] = txt;
|
||||
}
|
||||
const virtualPlugin = {
|
||||
@ -66,6 +72,28 @@ export default async function allPagesBundler(params) {
|
||||
},
|
||||
};
|
||||
const entryPoints = Object.keys(virtualEntries).map((k) => `virtual:${k}`);
|
||||
// let alias: any = {};
|
||||
// const excludes = [
|
||||
// "bun:sqlite",
|
||||
// "path",
|
||||
// "url",
|
||||
// "events",
|
||||
// "util",
|
||||
// "crypto",
|
||||
// "net",
|
||||
// "tls",
|
||||
// "fs",
|
||||
// "node:path",
|
||||
// "node:url",
|
||||
// "node:process",
|
||||
// "node:fs",
|
||||
// "node:timers/promises",
|
||||
// ];
|
||||
// for (let i = 0; i < excludes.length; i++) {
|
||||
// const exclude = excludes[i];
|
||||
// alias[exclude] = "./empty.js";
|
||||
// }
|
||||
// console.log("alias", alias);
|
||||
const result = await esbuild.build({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
@ -89,6 +117,7 @@ export default async function allPagesBundler(params) {
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
],
|
||||
// alias,
|
||||
});
|
||||
if (result.errors.length > 0) {
|
||||
for (const error of result.errors) {
|
||||
|
||||
@ -4,24 +4,27 @@ import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import pagePathTransform from "../../utils/page-path-transform";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
export default async function grabClientHydrationScript({ page_local_path, }) {
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
|
||||
const target_path = pagePathTransform({ page_path: page_local_path });
|
||||
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
// const target_path = pagePathTransform({ page_path: page_local_path });
|
||||
// const target_root_path = root_file_path
|
||||
// ? pagePathTransform({ page_path: root_file_path })
|
||||
// : undefined;
|
||||
let txt = ``;
|
||||
txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
if (root_file_path) {
|
||||
txt += `import Root from "${root_file_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${target_path}";\n\n`;
|
||||
txt += `import Page from "${page_local_path}";\n\n`;
|
||||
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
if (root_file_path) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
||||
|
||||
@ -1,31 +1,75 @@
|
||||
import { log } from "../../../utils/log";
|
||||
const BunSkipNonBrowserPlugin = {
|
||||
name: "skip-non-browser",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^(bun:|node:)/ }, (args) => {
|
||||
return { path: args.path, external: true };
|
||||
const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
|
||||
// const skipped_modules = new Set<string>();
|
||||
build.onResolve({ filter: skipFilter }, (args) => {
|
||||
global.SKIPPED_BROWSER_MODULES.add(args.path);
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "skipped",
|
||||
// external: true,
|
||||
};
|
||||
});
|
||||
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||
// If it's a built-in like 'fs' or 'path', skip it immediately
|
||||
const excludes = [
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"crypto",
|
||||
"net",
|
||||
"events",
|
||||
"util",
|
||||
];
|
||||
if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||
return { path: args.path, external: true };
|
||||
}
|
||||
try {
|
||||
Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||
return null;
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||
return { path: args.path, external: true };
|
||||
}
|
||||
// build.onEnd(() => {
|
||||
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [
|
||||
// ...global.SKIPPED_BROWSER_MODULES,
|
||||
// ]);
|
||||
// });
|
||||
// build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||
// // If it's a built-in like 'fs' or 'path', skip it immediately
|
||||
// const excludes = [
|
||||
// "fs",
|
||||
// "path",
|
||||
// "os",
|
||||
// "crypto",
|
||||
// "net",
|
||||
// "events",
|
||||
// "util",
|
||||
// "tls",
|
||||
// ];
|
||||
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
// try {
|
||||
// Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||
// return null;
|
||||
// } catch (e) {
|
||||
// console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
|
||||
return {
|
||||
contents: `
|
||||
const proxy = new Proxy(() => proxy, {
|
||||
get: () => proxy,
|
||||
construct: () => proxy,
|
||||
});
|
||||
|
||||
export const Database = proxy;
|
||||
export const join = proxy;
|
||||
export const fileURLToPath = proxy;
|
||||
export const arch = proxy;
|
||||
export const platform = proxy;
|
||||
export const statSync = proxy;
|
||||
|
||||
export const $H = proxy;
|
||||
export const _ = proxy;
|
||||
|
||||
export default proxy;
|
||||
`,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
3
dist/functions/bundler/record-artifacts.d.ts
vendored
3
dist/functions/bundler/record-artifacts.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
type Params = {
|
||||
artifacts: BundlerCTXMap[];
|
||||
page_file_paths?: string[];
|
||||
};
|
||||
export default function recordArtifacts({ artifacts }: Params): Promise<void>;
|
||||
export default function recordArtifacts({ artifacts, page_file_paths, }: Params): Promise<void>;
|
||||
export {};
|
||||
|
||||
10
dist/functions/bundler/record-artifacts.js
vendored
10
dist/functions/bundler/record-artifacts.js
vendored
@ -1,6 +1,7 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import _ from "lodash";
|
||||
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
export default async function recordArtifacts({ artifacts }) {
|
||||
export default async function recordArtifacts({ artifacts, page_file_paths, }) {
|
||||
const artifacts_map = {};
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact?.local_path) {
|
||||
@ -8,7 +9,10 @@ export default async function recordArtifacts({ artifacts }) {
|
||||
}
|
||||
}
|
||||
if (global.BUNDLER_CTX_MAP) {
|
||||
global.BUNDLER_CTX_MAP = artifacts_map;
|
||||
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map);
|
||||
}
|
||||
await Bun.write(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts_map, null, 4));
|
||||
// await Bun.write(
|
||||
// HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||
// JSON.stringify(artifacts_map, null, 4),
|
||||
// );
|
||||
}
|
||||
|
||||
1
dist/functions/bunext-init.d.ts
vendored
1
dist/functions/bunext-init.d.ts
vendored
@ -22,5 +22,6 @@ declare global {
|
||||
var CURRENT_VERSION: string | undefined;
|
||||
var PAGE_FILES: PageFiles[];
|
||||
var ROOT_FILE_UPDATED: boolean;
|
||||
var SKIPPED_BROWSER_MODULES: Set<string>;
|
||||
}
|
||||
export default function bunextInit(): Promise<void>;
|
||||
|
||||
13
dist/functions/bunext-init.js
vendored
13
dist/functions/bunext-init.js
vendored
@ -1,13 +1,11 @@
|
||||
import ora, {} from "ora";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { readFileSync } from "fs";
|
||||
import {} from "fs";
|
||||
import init from "./init";
|
||||
import isDevelopment from "../utils/is-development";
|
||||
import allPagesBundler from "./bundler/all-pages-bundler";
|
||||
import watcher from "./server/watcher";
|
||||
import { log } from "../utils/log";
|
||||
import cron from "./server/cron";
|
||||
import EJSON from "../utils/ejson";
|
||||
import allPagesBunBundler from "./bundler/all-pages-bun-bundler";
|
||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
export default async function bunextInit() {
|
||||
@ -17,6 +15,7 @@ export default async function bunextInit() {
|
||||
global.BUNDLER_CTX_MAP = {};
|
||||
global.BUNDLER_REBUILDS = 0;
|
||||
global.PAGE_FILES = [];
|
||||
global.SKIPPED_BROWSER_MODULES = new Set();
|
||||
await init();
|
||||
log.banner();
|
||||
const router = new Bun.FileSystemRouter({
|
||||
@ -26,17 +25,9 @@ export default async function bunextInit() {
|
||||
global.ROUTER = router;
|
||||
const is_dev = isDevelopment();
|
||||
if (is_dev) {
|
||||
// await allPagesBundler();
|
||||
await allPagesBunBundler();
|
||||
watcher();
|
||||
}
|
||||
else {
|
||||
const artifacts = EJSON.parse(readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"));
|
||||
if (!artifacts) {
|
||||
log.error("Please build first.");
|
||||
process.exit(1);
|
||||
}
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
cron();
|
||||
}
|
||||
}
|
||||
|
||||
13
dist/functions/server/watcher.js
vendored
13
dist/functions/server/watcher.js
vendored
@ -3,7 +3,7 @@ import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import rebuildBundler from "./rebuild-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
// import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default async function watcher() {
|
||||
const pages_src_watcher = watch(ROOT_DIR, {
|
||||
@ -62,14 +62,15 @@ async function fullRebuild(params) {
|
||||
try {
|
||||
const { msg } = params || {};
|
||||
global.RECOMPILING = true;
|
||||
const target_file_paths = global.HMR_CONTROLLERS.map((hmr) => hmr.target_map?.local_path).filter((f) => typeof f == "string");
|
||||
await rewritePagesModule({
|
||||
page_file_path: target_file_paths,
|
||||
});
|
||||
// const target_file_paths = global.HMR_CONTROLLERS.map(
|
||||
// (hmr) => hmr.target_map?.local_path,
|
||||
// ).filter((f) => typeof f == "string");
|
||||
// await rewritePagesModule();
|
||||
if (msg) {
|
||||
log.watch(msg);
|
||||
}
|
||||
await rebuildBundler({ target_file_paths });
|
||||
await rebuildBundler();
|
||||
// await rebuildBundler({ target_file_paths });
|
||||
}
|
||||
catch (error) {
|
||||
log.error(error);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
import type { LivePageDistGenParams } from "../../../types";
|
||||
export default function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }: LivePageDistGenParams): Promise<string>;
|
||||
export default function genWebHTML({ component, pageProps, bundledMap, module, routeParams, debug, root_module, }: LivePageDistGenParams): Promise<string>;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import grabContants from "../../../utils/grab-constants";
|
||||
import EJSON from "../../../utils/ejson";
|
||||
@ -14,56 +14,68 @@ try {
|
||||
_reactVersion = JSON.parse(readFileSync(path.join(process.cwd(), "node_modules/react/package.json"), "utf-8")).version;
|
||||
}
|
||||
catch { }
|
||||
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }) {
|
||||
export default async function genWebHTML({ component, pageProps, bundledMap, module, routeParams, debug, root_module, }) {
|
||||
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
||||
const is_dev = isDevelopment();
|
||||
if (debug) {
|
||||
log.info("component", component);
|
||||
}
|
||||
const componentHTML = renderToString(component);
|
||||
if (debug) {
|
||||
log.info("componentHTML", componentHTML);
|
||||
}
|
||||
const headHTML = Head
|
||||
? renderToString(_jsx(Head, { serverRes: pageProps, ctx: routeParams }))
|
||||
: "";
|
||||
let html = `<!DOCTYPE html>\n`;
|
||||
html += `<html>\n`;
|
||||
html += ` <head>\n`;
|
||||
html += ` <meta charset="utf-8" />\n`;
|
||||
html += ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
|
||||
if (meta) {
|
||||
html += ` ${grabWebMetaHTML({ meta })}\n`;
|
||||
}
|
||||
if (bundledMap?.css_path) {
|
||||
html += ` <link rel="stylesheet" href="/${bundledMap.css_path}" />\n`;
|
||||
}
|
||||
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(/<\//g, "<\\/");
|
||||
html += ` <script>window.${ClientWindowPagePropsName} = ${serializedProps}</script>\n`;
|
||||
if (bundledMap?.path) {
|
||||
const page_hydration_script = await grabWebPageHydrationScript();
|
||||
const root_meta = root_module?.meta
|
||||
? typeof root_module.meta == "function" && routeParams
|
||||
? await root_module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||
: typeof root_module.meta == "function"
|
||||
? undefined
|
||||
: root_module.meta
|
||||
: undefined;
|
||||
const page_meta = module?.meta
|
||||
? typeof module.meta == "function" && routeParams
|
||||
? await module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||
: typeof module.meta == "function"
|
||||
? undefined
|
||||
: module.meta
|
||||
: undefined;
|
||||
const html_props = {
|
||||
...module?.html_props,
|
||||
...root_module?.html_props,
|
||||
};
|
||||
const Head = module?.Head;
|
||||
const RootHead = root_module?.Head;
|
||||
const dev = isDevelopment();
|
||||
const devSuffix = dev ? "?dev" : "";
|
||||
const browser_imports = {
|
||||
react: `https://esm.sh/react@${_reactVersion}`,
|
||||
"react-dom": `https://esm.sh/react-dom@${_reactVersion}`,
|
||||
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client`,
|
||||
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime`,
|
||||
};
|
||||
if (dev) {
|
||||
browser_imports["react/jsx-dev-runtime"] =
|
||||
`https://esm.sh/react@${_reactVersion}/jsx-dev-runtime`;
|
||||
}
|
||||
const importMap = JSON.stringify({
|
||||
imports: {
|
||||
react: `https://esm.sh/react@${_reactVersion}${devSuffix}`,
|
||||
"react-dom": `https://esm.sh/react-dom@${_reactVersion}${devSuffix}`,
|
||||
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client${devSuffix}`,
|
||||
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime${devSuffix}`,
|
||||
"react/jsx-dev-runtime": `https://esm.sh/react@${_reactVersion}/jsx-dev-runtime${devSuffix}`,
|
||||
},
|
||||
imports: browser_imports,
|
||||
});
|
||||
html += ` <script type="importmap">${importMap}</script>\n`;
|
||||
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
|
||||
}
|
||||
if (isDevelopment()) {
|
||||
html += `<script defer>\n${await grabWebPageHydrationScript()}\n</script>\n`;
|
||||
}
|
||||
if (headHTML) {
|
||||
html += ` ${headHTML}\n`;
|
||||
}
|
||||
html += ` </head>\n`;
|
||||
html += ` <body>\n`;
|
||||
html += ` <div id="${ClientRootElementIDName}">${componentHTML}</div>\n`;
|
||||
html += ` </body>\n`;
|
||||
html += `</html>\n`;
|
||||
let final_component = (_jsxs("html", { ...html_props, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), root_meta ? grabWebMetaHTML({ meta: root_meta }) : null, page_meta ? grabWebMetaHTML({ meta: page_meta }) : null, bundledMap?.css_path ? (_jsx("link", { rel: "stylesheet", href: `/${bundledMap.css_path}` })) : null, _jsx("script", { dangerouslySetInnerHTML: {
|
||||
__html: `window.${ClientWindowPagePropsName} = ${serializedProps}`,
|
||||
} }), bundledMap?.path ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "importmap", dangerouslySetInnerHTML: {
|
||||
__html: importMap,
|
||||
}, fetchPriority: "high" }), _jsx("script", { src: `/${bundledMap.path}`, type: "module", id: AppData["BunextClientHydrationScriptID"], defer: true })] })) : null, is_dev ? (_jsx("script", { defer: true, dangerouslySetInnerHTML: {
|
||||
__html: page_hydration_script,
|
||||
} })) : null, RootHead ? (_jsx(RootHead, { serverRes: pageProps, ctx: routeParams })) : null, Head ? _jsx(Head, { serverRes: pageProps, ctx: routeParams }) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: component }) })] }));
|
||||
let html = `<!DOCTYPE html>\n`;
|
||||
// const stream = await renderToReadableStream(final_component, {
|
||||
// onError(error: any) {
|
||||
// // This is where you "omit" or handle the errors
|
||||
// // You can log it silently or ignore it
|
||||
// if (error.message.includes('unique "key" prop')) return;
|
||||
// console.error(error);
|
||||
// },
|
||||
// });
|
||||
// // 2. Convert the Web Stream to a String (Bun-optimized)
|
||||
// const htmlBody = await new Response(stream).text();
|
||||
// html += htmlBody;
|
||||
html += renderToString(final_component);
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
import type { GrabPageComponentRes } from "../../../types";
|
||||
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }: GrabPageComponentRes): Promise<Response>;
|
||||
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }: GrabPageComponentRes): Promise<Response>;
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import _ from "lodash";
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import { log } from "../../../utils/log";
|
||||
import writeCache from "../../cache/write-cache";
|
||||
import genWebHTML from "./generate-web-html";
|
||||
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }) {
|
||||
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }) {
|
||||
const html = await genWebHTML({
|
||||
component,
|
||||
pageProps: serverRes,
|
||||
bundledMap,
|
||||
module,
|
||||
meta,
|
||||
head,
|
||||
routeParams,
|
||||
debug,
|
||||
root_module,
|
||||
});
|
||||
if (debug) {
|
||||
log.info("html", html);
|
||||
@ -36,8 +36,9 @@ export default async function generateWebPageResponseFromComponentReturn({ compo
|
||||
Expires: "0",
|
||||
};
|
||||
}
|
||||
const cache_page = module.config?.cachePage || serverRes?.cachePage || false;
|
||||
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
|
||||
const config = _.merge(root_module?.config, module.config);
|
||||
const cache_page = config?.cachePage || serverRes?.cachePage || false;
|
||||
const expiry_seconds = config?.cacheExpiry || serverRes?.cacheExpiry;
|
||||
if (cache_page && routeParams?.url) {
|
||||
const key = routeParams.url.pathname + (routeParams.url.search || "");
|
||||
writeCache({
|
||||
|
||||
@ -4,6 +4,8 @@ import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
|
||||
import _ from "lodash";
|
||||
import { log } from "../../../utils/log";
|
||||
import grabRootFilePath from "./grab-root-file-path";
|
||||
import grabPageServerRes from "./grab-page-server-res";
|
||||
import grabPageServerPath from "./grab-page-server-path";
|
||||
class NotFoundError extends Error {
|
||||
}
|
||||
export default async function grabPageComponent({ req, file_path: passed_file_path, debug, }) {
|
||||
@ -35,6 +37,7 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
|
||||
}
|
||||
const bundledMap = global.BUNDLER_CTX_MAP?.[file_path];
|
||||
if (!bundledMap?.path) {
|
||||
console.log(global.BUNDLER_CTX_MAP);
|
||||
const errMsg = `No Bundled File Path for this request path!`;
|
||||
log.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
@ -43,72 +46,52 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
|
||||
log.info(`bundledMap:`, bundledMap);
|
||||
}
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
const root_module = root_file_path
|
||||
? await import(`${root_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
const { server_file_path: root_server_file_path } = root_file_path
|
||||
? grabPageServerPath({ file_path: root_file_path })
|
||||
: {};
|
||||
const root_server_module = root_server_file_path
|
||||
? await import(`${root_server_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
const root_server_fn = root_server_module?.default || root_server_module?.server;
|
||||
const rootServerRes = root_server_fn
|
||||
? await grabPageServerRes({
|
||||
server_function: root_server_fn,
|
||||
url,
|
||||
query: match?.query,
|
||||
routeParams,
|
||||
})
|
||||
: undefined;
|
||||
if (debug) {
|
||||
log.info(`rootServerRes:`, rootServerRes);
|
||||
}
|
||||
const module = await import(`${file_path}?t=${now}`);
|
||||
const { server_file_path } = grabPageServerPath({ file_path });
|
||||
const server_module = server_file_path
|
||||
? await import(`${server_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
if (debug) {
|
||||
log.info(`module:`, module);
|
||||
}
|
||||
const serverRes = await (async () => {
|
||||
const default_props = {
|
||||
url: {
|
||||
..._.pick(url, [
|
||||
"host",
|
||||
"hostname",
|
||||
"pathname",
|
||||
"origin",
|
||||
"port",
|
||||
"search",
|
||||
"searchParams",
|
||||
"hash",
|
||||
"href",
|
||||
"password",
|
||||
"protocol",
|
||||
"username",
|
||||
]),
|
||||
},
|
||||
const server_fn = server_module?.default || server_module?.server;
|
||||
const serverRes = server_fn
|
||||
? await grabPageServerRes({
|
||||
server_function: server_fn,
|
||||
url,
|
||||
query: match?.query,
|
||||
};
|
||||
try {
|
||||
if (routeParams) {
|
||||
const serverData = await module["server"]?.({
|
||||
...routeParams,
|
||||
query: { ...routeParams.query, ...match?.query },
|
||||
});
|
||||
return {
|
||||
...serverData,
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
})();
|
||||
routeParams,
|
||||
})
|
||||
: undefined;
|
||||
if (debug) {
|
||||
log.info(`serverRes:`, serverRes);
|
||||
}
|
||||
const meta = module.meta
|
||||
? typeof module.meta == "function" && routeParams
|
||||
? await module.meta({
|
||||
ctx: routeParams,
|
||||
serverRes,
|
||||
})
|
||||
: typeof module.meta == "object"
|
||||
? module.meta
|
||||
: undefined
|
||||
: undefined;
|
||||
if (debug) {
|
||||
log.info(`meta:`, meta);
|
||||
}
|
||||
const Head = module.Head;
|
||||
const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {});
|
||||
const { component } = (await grabPageBundledReactComponent({
|
||||
file_path,
|
||||
root_file_path,
|
||||
server_res: serverRes,
|
||||
server_res: mergedServerRes,
|
||||
})) || {};
|
||||
if (!component) {
|
||||
throw new Error(`Couldn't grab page component`);
|
||||
@ -118,12 +101,11 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
|
||||
}
|
||||
return {
|
||||
component,
|
||||
serverRes,
|
||||
serverRes: mergedServerRes,
|
||||
routeParams,
|
||||
module,
|
||||
bundledMap,
|
||||
meta,
|
||||
head: Head,
|
||||
root_module,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@ -2,21 +2,29 @@ import EJSON from "../../../utils/ejson";
|
||||
import pagePathTransform from "../../../utils/page-path-transform";
|
||||
export default function grabPageReactComponentString({ file_path, root_file_path, server_res, }) {
|
||||
try {
|
||||
const target_path = pagePathTransform({ page_path: file_path });
|
||||
// const target_path = pagePathTransform({ page_path: file_path });
|
||||
// const target_root_path = root_file_path
|
||||
// ? pagePathTransform({ page_path: root_file_path })
|
||||
// : undefined;
|
||||
let tsx = ``;
|
||||
const server_res_json = JSON.stringify(EJSON.stringify(server_res || {}) ?? "{}");
|
||||
// Import Root from its original source path so that all sub-components
|
||||
// that import __root (e.g. AppContext) resolve to the same module instance.
|
||||
// Using the rewritten .bunext/pages/__root would create a separate
|
||||
// createContext() call, breaking context for any sub-component that
|
||||
// imports AppContext via a relative path to the source __root.
|
||||
if (root_file_path) {
|
||||
tsx += `import Root from "${root_file_path}"\n`;
|
||||
}
|
||||
tsx += `import Page from "${target_path}"\n`;
|
||||
tsx += `import Page from "${file_path}"\n`;
|
||||
tsx += `export default function Main() {\n\n`;
|
||||
tsx += `const props = JSON.parse(${server_res_json})\n\n`;
|
||||
tsx += ` return (\n`;
|
||||
if (root_file_path) {
|
||||
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
|
||||
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
|
||||
tsx += ` <Page {...props} />\n`;
|
||||
}
|
||||
tsx += ` )\n`;
|
||||
tsx += `}\n`;
|
||||
|
||||
7
dist/functions/server/web-pages/grab-page-server-path.d.ts
vendored
Normal file
7
dist/functions/server/web-pages/grab-page-server-path.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
type Params = {
|
||||
file_path: string;
|
||||
};
|
||||
export default function grabPageServerPath({ file_path }: Params): {
|
||||
server_file_path: string | undefined;
|
||||
};
|
||||
export {};
|
||||
11
dist/functions/server/web-pages/grab-page-server-path.js
vendored
Normal file
11
dist/functions/server/web-pages/grab-page-server-path.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import { existsSync } from "fs";
|
||||
export default function grabPageServerPath({ file_path }) {
|
||||
const page_server_ts_file = file_path.replace(/\.tsx?$/, ".server.ts");
|
||||
const page_server_tsx_file = file_path.replace(/\.tsx?$/, ".server.tsx");
|
||||
const server_file_path = existsSync(page_server_ts_file)
|
||||
? page_server_ts_file
|
||||
: existsSync(page_server_tsx_file)
|
||||
? page_server_tsx_file
|
||||
: undefined;
|
||||
return { server_file_path };
|
||||
}
|
||||
9
dist/functions/server/web-pages/grab-page-server-res.d.ts
vendored
Normal file
9
dist/functions/server/web-pages/grab-page-server-res.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import type { BunextPageModuleServerReturn, BunextPageServerFn, BunxRouteParams } from "../../../types";
|
||||
type Params = {
|
||||
url?: URL;
|
||||
server_function: BunextPageServerFn;
|
||||
query?: Record<string, string>;
|
||||
routeParams?: BunxRouteParams;
|
||||
};
|
||||
export default function grabPageServerRes({ url, query, routeParams, server_function, }: Params): Promise<BunextPageModuleServerReturn>;
|
||||
export {};
|
||||
44
dist/functions/server/web-pages/grab-page-server-res.js
vendored
Normal file
44
dist/functions/server/web-pages/grab-page-server-res.js
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
import _ from "lodash";
|
||||
export default async function grabPageServerRes({ url, query, routeParams, server_function, }) {
|
||||
const default_props = {
|
||||
url: url
|
||||
? {
|
||||
..._.pick(url, [
|
||||
"host",
|
||||
"hostname",
|
||||
"pathname",
|
||||
"origin",
|
||||
"port",
|
||||
"search",
|
||||
"searchParams",
|
||||
"hash",
|
||||
"href",
|
||||
"password",
|
||||
"protocol",
|
||||
"username",
|
||||
]),
|
||||
}
|
||||
: null,
|
||||
query,
|
||||
};
|
||||
try {
|
||||
if (routeParams) {
|
||||
const serverData = await server_function({
|
||||
...routeParams,
|
||||
query: { ...routeParams.query, ...query },
|
||||
});
|
||||
return {
|
||||
...serverData,
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,58 @@
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import * as esbuild from "esbuild";
|
||||
import tailwindcss from "bun-plugin-tailwind";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
import BunSkipNonBrowserPlugin from "../../bundler/plugins/bun-skip-browser-plugin";
|
||||
export default async function grabTsxStringModule({ tsx, file_path, }) {
|
||||
const dev = isDevelopment();
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
const trimmed_file_path = file_path
|
||||
.replace(/.*\/src\/pages\//, "")
|
||||
.replace(/\.tsx$/, "");
|
||||
const src_file_path = path.join(BUNX_CWD_MODULE_CACHE_DIR, `${trimmed_file_path}.tsx`);
|
||||
const out_file_path = path.join(BUNX_CWD_MODULE_CACHE_DIR, `${trimmed_file_path}.js`);
|
||||
await esbuild.build({
|
||||
stdin: {
|
||||
contents: tsx,
|
||||
resolveDir: process.cwd(),
|
||||
loader: "tsx",
|
||||
},
|
||||
bundle: true,
|
||||
await Bun.write(src_file_path, tsx);
|
||||
const build = await Bun.build({
|
||||
entrypoints: [src_file_path],
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "node",
|
||||
target: "bun",
|
||||
external: ["react", "react-dom"],
|
||||
minify: true,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
metafile: true,
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
jsx: "automatic",
|
||||
write: true,
|
||||
outfile: out_file_path,
|
||||
plugins: [tailwindcss, BunSkipNonBrowserPlugin],
|
||||
jsx: {
|
||||
runtime: "automatic",
|
||||
development: dev,
|
||||
},
|
||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
});
|
||||
Loader.registry.delete(out_file_path);
|
||||
const module = await import(`${out_file_path}?t=${Date.now()}`);
|
||||
return module;
|
||||
}
|
||||
// await esbuild.build({
|
||||
// stdin: {
|
||||
// contents: tsx,
|
||||
// resolveDir: process.cwd(),
|
||||
// loader: "tsx",
|
||||
// },
|
||||
// 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",
|
||||
// write: true,
|
||||
// outfile: out_file_path,
|
||||
// });
|
||||
|
||||
@ -2,5 +2,5 @@ import type { BunextPageModuleMeta } from "../../../types";
|
||||
type Params = {
|
||||
meta: BunextPageModuleMeta;
|
||||
};
|
||||
export default function grabWebMetaHTML({ meta }: Params): string;
|
||||
export default function grabWebMetaHTML({ meta }: Params): import("react/jsx-runtime").JSX.Element;
|
||||
export {};
|
||||
|
||||
@ -1,61 +1,9 @@
|
||||
import { escape } from "lodash";
|
||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
export default function grabWebMetaHTML({ meta }) {
|
||||
let html = ``;
|
||||
if (meta.title) {
|
||||
html += ` <title>${escape(meta.title)}</title>\n`;
|
||||
}
|
||||
if (meta.description) {
|
||||
html += ` <meta name="description" content="${escape(meta.description)}" />\n`;
|
||||
}
|
||||
if (meta.keywords) {
|
||||
const keywords = Array.isArray(meta.keywords)
|
||||
const keywords = meta.keywords
|
||||
? Array.isArray(meta.keywords)
|
||||
? meta.keywords.join(", ")
|
||||
: meta.keywords;
|
||||
html += ` <meta name="keywords" content="${escape(keywords)}" />\n`;
|
||||
}
|
||||
if (meta.author) {
|
||||
html += ` <meta name="author" content="${escape(meta.author)}" />\n`;
|
||||
}
|
||||
if (meta.robots) {
|
||||
html += ` <meta name="robots" content="${escape(meta.robots)}" />\n`;
|
||||
}
|
||||
if (meta.canonical) {
|
||||
html += ` <link rel="canonical" href="${escape(meta.canonical)}" />\n`;
|
||||
}
|
||||
if (meta.themeColor) {
|
||||
html += ` <meta name="theme-color" content="${escape(meta.themeColor)}" />\n`;
|
||||
}
|
||||
if (meta.og) {
|
||||
const { og } = meta;
|
||||
if (og.title)
|
||||
html += ` <meta property="og:title" content="${escape(og.title)}" />\n`;
|
||||
if (og.description)
|
||||
html += ` <meta property="og:description" content="${escape(og.description)}" />\n`;
|
||||
if (og.image)
|
||||
html += ` <meta property="og:image" content="${escape(og.image)}" />\n`;
|
||||
if (og.url)
|
||||
html += ` <meta property="og:url" content="${escape(og.url)}" />\n`;
|
||||
if (og.type)
|
||||
html += ` <meta property="og:type" content="${escape(og.type)}" />\n`;
|
||||
if (og.siteName)
|
||||
html += ` <meta property="og:site_name" content="${escape(og.siteName)}" />\n`;
|
||||
if (og.locale)
|
||||
html += ` <meta property="og:locale" content="${escape(og.locale)}" />\n`;
|
||||
}
|
||||
if (meta.twitter) {
|
||||
const { twitter } = meta;
|
||||
if (twitter.card)
|
||||
html += ` <meta name="twitter:card" content="${escape(twitter.card)}" />\n`;
|
||||
if (twitter.title)
|
||||
html += ` <meta name="twitter:title" content="${escape(twitter.title)}" />\n`;
|
||||
if (twitter.description)
|
||||
html += ` <meta name="twitter:description" content="${escape(twitter.description)}" />\n`;
|
||||
if (twitter.image)
|
||||
html += ` <meta name="twitter:image" content="${escape(twitter.image)}" />\n`;
|
||||
if (twitter.site)
|
||||
html += ` <meta name="twitter:site" content="${escape(twitter.site)}" />\n`;
|
||||
if (twitter.creator)
|
||||
html += ` <meta name="twitter:creator" content="${escape(twitter.creator)}" />\n`;
|
||||
}
|
||||
return html;
|
||||
: meta.keywords
|
||||
: undefined;
|
||||
return (_jsxs(_Fragment, { children: [meta.title && _jsx("title", { children: meta.title }), meta.description && (_jsx("meta", { name: "description", content: meta.description })), keywords && _jsx("meta", { name: "keywords", content: keywords }), meta.author && _jsx("meta", { name: "author", content: meta.author }), meta.robots && _jsx("meta", { name: "robots", content: meta.robots }), meta.canonical && (_jsx("link", { rel: "canonical", href: meta.canonical })), meta.themeColor && (_jsx("meta", { name: "theme-color", content: meta.themeColor })), meta.og?.title && (_jsx("meta", { property: "og:title", content: meta.og.title })), meta.og?.description && (_jsx("meta", { property: "og:description", content: meta.og.description })), meta.og?.image && (_jsx("meta", { property: "og:image", content: meta.og.image })), meta.og?.url && (_jsx("meta", { property: "og:url", content: meta.og.url })), meta.og?.type && (_jsx("meta", { property: "og:type", content: meta.og.type })), meta.og?.siteName && (_jsx("meta", { property: "og:site_name", content: meta.og.siteName })), meta.og?.locale && (_jsx("meta", { property: "og:locale", content: meta.og.locale })), meta.twitter?.card && (_jsx("meta", { name: "twitter:card", content: meta.twitter.card })), meta.twitter?.title && (_jsx("meta", { name: "twitter:title", content: meta.twitter.title })), meta.twitter?.description && (_jsx("meta", { name: "twitter:description", content: meta.twitter.description })), meta.twitter?.image && (_jsx("meta", { name: "twitter:image", content: meta.twitter.image })), meta.twitter?.site && (_jsx("meta", { name: "twitter:site", content: meta.twitter.site })), meta.twitter?.creator && (_jsx("meta", { name: "twitter:creator", content: meta.twitter.creator }))] }));
|
||||
}
|
||||
|
||||
2
dist/index.d.ts
vendored
2
dist/index.d.ts
vendored
@ -6,7 +6,7 @@ declare const bunext: {
|
||||
info: (msg: string, log?: any) => void;
|
||||
success: (msg: string, log?: any) => void;
|
||||
error: (msg: string | Error, log?: any) => void;
|
||||
warn: (msg: string) => void;
|
||||
warn: (msg: string, log?: any) => void;
|
||||
build: (msg: string) => void;
|
||||
watch: (msg: string) => void;
|
||||
server: (url: string) => void;
|
||||
|
||||
4
dist/presets/components/head.d.ts
vendored
Normal file
4
dist/presets/components/head.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import type { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from "react";
|
||||
type Props = PropsWithChildren<DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>>;
|
||||
export default function Head({ children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
||||
export {};
|
||||
4
dist/presets/components/head.js
vendored
Normal file
4
dist/presets/components/head.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
export default function Head({ children, ...props }) {
|
||||
return _jsx("head", { ...props, children: children });
|
||||
}
|
||||
16
dist/types/index.d.ts
vendored
16
dist/types/index.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
import type { MatchedRoute, Server, WebSocketHandler } from "bun";
|
||||
import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
|
||||
import type { DetailedHTMLProps, FC, HtmlHTMLAttributes, JSX, PropsWithChildren, ReactNode } from "react";
|
||||
export type ServerProps = {
|
||||
params: Record<string, string>;
|
||||
searchParams: Record<string, string>;
|
||||
@ -129,11 +129,10 @@ export type PageDistGenParams = {
|
||||
};
|
||||
export type LivePageDistGenParams = {
|
||||
component: ReactNode;
|
||||
head?: FC<BunextPageHeadFCProps>;
|
||||
pageProps?: any;
|
||||
module?: BunextPageModule;
|
||||
root_module?: BunextRootModule;
|
||||
bundledMap?: BundlerCTXMap;
|
||||
meta?: BunextPageModuleMeta;
|
||||
routeParams?: BunxRouteParams;
|
||||
debug?: boolean;
|
||||
};
|
||||
@ -143,11 +142,16 @@ export type BunextPageHeadFCProps = {
|
||||
};
|
||||
export type BunextPageModule = {
|
||||
default: FC<any>;
|
||||
server?: BunextPageServerFn;
|
||||
meta?: BunextPageModuleMeta | BunextPageModuleMetaFn;
|
||||
Head?: FC<BunextPageHeadFCProps>;
|
||||
config?: BunextRouteConfig;
|
||||
html_props?: BunextHTMLProps;
|
||||
};
|
||||
export type BunextPageServerModule = {
|
||||
default?: BunextPageServerFn;
|
||||
server?: BunextPageServerFn;
|
||||
};
|
||||
export type BunextHTMLProps = DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>;
|
||||
export type BunextPageModuleMetaFn = (params: {
|
||||
ctx: BunxRouteParams;
|
||||
serverRes?: BunextPageModuleServerReturn;
|
||||
@ -235,10 +239,10 @@ export type GrabPageComponentRes = {
|
||||
routeParams?: BunxRouteParams;
|
||||
bundledMap?: BundlerCTXMap;
|
||||
module: BunextPageModule;
|
||||
meta?: BunextPageModuleMeta;
|
||||
head?: FC<BunextPageHeadFCProps>;
|
||||
root_module?: BunextRootModule;
|
||||
debug?: boolean;
|
||||
};
|
||||
export type BunextRootModule = BunextPageModule;
|
||||
export type GrabPageReactBundledComponentRes = {
|
||||
component: JSX.Element;
|
||||
server_res?: BunextPageModuleServerReturn;
|
||||
|
||||
6
dist/utils/grab-all-pages.js
vendored
6
dist/utils/grab-all-pages.js
vendored
@ -20,8 +20,9 @@ function grabPageDirRecursively({ page_dir }) {
|
||||
}
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const page_name = page.split("/").pop();
|
||||
const full_page_path = path.join(page_dir, page);
|
||||
if (!existsSync(full_page_path)) {
|
||||
if (!existsSync(full_page_path) || !page_name) {
|
||||
continue;
|
||||
}
|
||||
if (page.match(new RegExp(`${AppNames["RootPagesComponentName"]}`))) {
|
||||
@ -30,6 +31,9 @@ function grabPageDirRecursively({ page_dir }) {
|
||||
if (page.match(/\(|\)|--|\/api\//)) {
|
||||
continue;
|
||||
}
|
||||
if (page_name.split(".").length > 2) {
|
||||
continue;
|
||||
}
|
||||
const page_stat = statSync(full_page_path);
|
||||
if (page_stat.isDirectory()) {
|
||||
if (page.match(/\(|\)/))
|
||||
|
||||
2
dist/utils/log.d.ts
vendored
2
dist/utils/log.d.ts
vendored
@ -2,7 +2,7 @@ export declare const log: {
|
||||
info: (msg: string, log?: any) => void;
|
||||
success: (msg: string, log?: any) => void;
|
||||
error: (msg: string | Error, log?: any) => void;
|
||||
warn: (msg: string) => void;
|
||||
warn: (msg: string, log?: any) => void;
|
||||
build: (msg: string) => void;
|
||||
watch: (msg: string) => void;
|
||||
server: (url: string) => void;
|
||||
|
||||
3
dist/utils/log.js
vendored
3
dist/utils/log.js
vendored
@ -3,6 +3,7 @@ import AppNames from "./grab-app-names";
|
||||
const prefix = {
|
||||
info: chalk.bgCyan.bold(" ℹnfo "),
|
||||
success: chalk.green.bold("✓"),
|
||||
zap: chalk.green.bold("⚡"),
|
||||
error: chalk.red.bold("✗"),
|
||||
warn: chalk.yellow.bold("⚠"),
|
||||
build: chalk.magenta.bold("⚙"),
|
||||
@ -16,7 +17,7 @@ export const log = {
|
||||
console.log(`${prefix.success} ${chalk.green(msg)}`, log || "");
|
||||
},
|
||||
error: (msg, log) => console.error(`${prefix.error} ${chalk.red(String(msg))}`, log || ""),
|
||||
warn: (msg) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`),
|
||||
warn: (msg, log) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`, log || ""),
|
||||
build: (msg) => console.log(`${prefix.build} ${chalk.magenta(msg)}`),
|
||||
watch: (msg) => console.log(`${prefix.watch} ${chalk.blue(msg)}`),
|
||||
server: (url) => console.log(`${prefix.success} ${chalk.white("Server running on")} ${chalk.cyan.underline(url)}`),
|
||||
|
||||
14
dist/utils/rewrite-pages-module.js
vendored
14
dist/utils/rewrite-pages-module.js
vendored
@ -1,6 +1,8 @@
|
||||
import grabAllPages from "./grab-all-pages";
|
||||
import pagePathTransform from "./page-path-transform";
|
||||
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
|
||||
import grabRootFilePath from "../functions/server/web-pages/grab-root-file-path";
|
||||
import { existsSync } from "fs";
|
||||
export default async function rewritePagesModule(params) {
|
||||
const { page_file_path } = params || {};
|
||||
let target_pages;
|
||||
@ -15,10 +17,15 @@ export default async function rewritePagesModule(params) {
|
||||
}
|
||||
for (let i = 0; i < target_pages.length; i++) {
|
||||
const page_path = target_pages[i];
|
||||
const dst_path = pagePathTransform({ page_path });
|
||||
if (page_path.match(/__root\.tsx?/)) {
|
||||
continue;
|
||||
await transformFile(page_path);
|
||||
}
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
if (root_file_path && existsSync(root_file_path)) {
|
||||
await transformFile(root_file_path);
|
||||
}
|
||||
}
|
||||
async function transformFile(page_path) {
|
||||
const dst_path = pagePathTransform({ page_path });
|
||||
const origin_page_content = await Bun.file(page_path).text();
|
||||
const dst_page_content = stripServerSideLogic({
|
||||
txt_code: origin_page_content,
|
||||
@ -27,5 +34,4 @@ export default async function rewritePagesModule(params) {
|
||||
await Bun.write(dst_path, dst_page_content, {
|
||||
createPath: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "@moduletrace/bunext",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "1.0.20",
|
||||
"version": "1.0.3",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||
import startServer from "../../../src/functions/server/start-server";
|
||||
import rewritePagesModule from "../../../src/utils/rewrite-pages-module";
|
||||
import pagePathTransform from "../../../src/utils/page-path-transform";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
@ -10,9 +8,6 @@ const fixtureDir = path.resolve(__dirname, "../../../test/e2e-fixture");
|
||||
const fixturePagesDir = path.join(fixtureDir, "src", "pages");
|
||||
const fixtureIndexPage = path.join(fixturePagesDir, "index.tsx");
|
||||
|
||||
// The rewritten page path (inside .bunext/pages, stripped of server logic)
|
||||
const rewrittenIndexPage = pagePathTransform({ page_path: fixtureIndexPage });
|
||||
|
||||
let originalCwd = process.cwd();
|
||||
let originalPort: string | undefined;
|
||||
|
||||
@ -37,10 +32,6 @@ describe("E2E Integration", () => {
|
||||
dir: fixturePagesDir,
|
||||
});
|
||||
|
||||
// Rewrite the fixture page (strips server logic) into .bunext/pages
|
||||
// so that grab-page-react-component-string can resolve the import
|
||||
await rewritePagesModule({ page_file_path: fixtureIndexPage });
|
||||
|
||||
// Pre-populate the bundler context map so grab-page-component can
|
||||
// look up the compiled path. The `path` value only needs to be
|
||||
// present for the guard check; SSR does not require the file to exist.
|
||||
@ -70,12 +61,6 @@ describe("E2E Integration", () => {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
|
||||
// Remove the rewritten page created during setup
|
||||
const rewrittenDir = path.dirname(rewrittenIndexPage);
|
||||
if (fs.existsSync(rewrittenDir)) {
|
||||
fs.rmSync(rewrittenDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Remove any generated .bunext artifacts from the fixture
|
||||
const dotBunext = path.join(fixtureDir, ".bunext");
|
||||
if (fs.existsSync(dotBunext)) {
|
||||
@ -102,4 +87,41 @@ describe("E2E Integration", () => {
|
||||
// Default 404 component is rendered
|
||||
expect(text).toContain("404");
|
||||
});
|
||||
|
||||
test("server props injected from .server.ts companion file", async () => {
|
||||
const serverFilePath = path.join(fixturePagesDir, "index.server.ts");
|
||||
const pageFilePath = fixtureIndexPage;
|
||||
|
||||
// Write a temporary .server.ts companion that injects a prop
|
||||
await Bun.write(serverFilePath, `
|
||||
import type { BunextPageServerFn } from "../../../../../src/types";
|
||||
|
||||
const server: BunextPageServerFn<{ greeting: string }> = async () => {
|
||||
return { props: { greeting: "Hello from server" } };
|
||||
};
|
||||
|
||||
export default server;
|
||||
`);
|
||||
|
||||
// Add the fixture page to the BUNDLER_CTX_MAP
|
||||
global.BUNDLER_CTX_MAP[pageFilePath] = {
|
||||
path: ".bunext/public/pages/index.js",
|
||||
hash: "index",
|
||||
type: "text/javascript",
|
||||
entrypoint: pageFilePath,
|
||||
local_path: pageFilePath,
|
||||
url_path: "/",
|
||||
file_name: "index",
|
||||
};
|
||||
|
||||
const response = await fetch(`http://localhost:${server.port}/`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const html = await response.text();
|
||||
// __PAGE_PROPS__ should include the prop from the server companion
|
||||
expect(html).toContain("Hello from server");
|
||||
|
||||
// Clean up
|
||||
fs.unlinkSync(serverFilePath);
|
||||
});
|
||||
});
|
||||
|
||||
69
src/__tests__/functions/server/grab-page-server-path.test.ts
Normal file
69
src/__tests__/functions/server/grab-page-server-path.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import grabPageServerPath from "../../../../src/functions/server/web-pages/grab-page-server-path";
|
||||
|
||||
const tmpDir = path.join(import.meta.dir, "__tmp_server_path__");
|
||||
|
||||
beforeAll(() => {
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("grabPageServerPath", () => {
|
||||
it("returns undefined when no companion file exists", () => {
|
||||
const { server_file_path } = grabPageServerPath({
|
||||
file_path: path.join(tmpDir, "index.tsx"),
|
||||
});
|
||||
expect(server_file_path).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves .server.ts companion for a .tsx page", () => {
|
||||
const serverFile = path.join(tmpDir, "profile.server.ts");
|
||||
fs.writeFileSync(serverFile, "export default async () => ({})");
|
||||
|
||||
const { server_file_path } = grabPageServerPath({
|
||||
file_path: path.join(tmpDir, "profile.tsx"),
|
||||
});
|
||||
|
||||
expect(server_file_path).toBe(serverFile);
|
||||
});
|
||||
|
||||
it("resolves .server.tsx companion when only .server.tsx exists", () => {
|
||||
const serverFile = path.join(tmpDir, "about.server.tsx");
|
||||
fs.writeFileSync(serverFile, "export default async () => ({})");
|
||||
|
||||
const { server_file_path } = grabPageServerPath({
|
||||
file_path: path.join(tmpDir, "about.tsx"),
|
||||
});
|
||||
|
||||
expect(server_file_path).toBe(serverFile);
|
||||
});
|
||||
|
||||
it("prefers .server.ts over .server.tsx when both exist", () => {
|
||||
const tsFile = path.join(tmpDir, "blog.server.ts");
|
||||
const tsxFile = path.join(tmpDir, "blog.server.tsx");
|
||||
fs.writeFileSync(tsFile, "export default async () => ({})");
|
||||
fs.writeFileSync(tsxFile, "export default async () => ({})");
|
||||
|
||||
const { server_file_path } = grabPageServerPath({
|
||||
file_path: path.join(tmpDir, "blog.tsx"),
|
||||
});
|
||||
|
||||
expect(server_file_path).toBe(tsFile);
|
||||
});
|
||||
|
||||
it("resolves companion for a .ts page file", () => {
|
||||
const serverFile = path.join(tmpDir, "api-page.server.ts");
|
||||
fs.writeFileSync(serverFile, "export default async () => ({})");
|
||||
|
||||
const { server_file_path } = grabPageServerPath({
|
||||
file_path: path.join(tmpDir, "api-page.ts"),
|
||||
});
|
||||
|
||||
expect(server_file_path).toBe(serverFile);
|
||||
});
|
||||
});
|
||||
@ -1,58 +1,60 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
|
||||
|
||||
function render(meta: Parameters<typeof grabWebMetaHTML>[0]["meta"]) {
|
||||
return renderToString(grabWebMetaHTML({ meta }));
|
||||
}
|
||||
|
||||
describe("grabWebMetaHTML", () => {
|
||||
it("returns empty string for empty meta object", () => {
|
||||
expect(grabWebMetaHTML({ meta: {} })).toBe("");
|
||||
expect(render({})).toBe("");
|
||||
});
|
||||
|
||||
it("generates a title tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
|
||||
expect(html).toContain("<title>My Page</title>");
|
||||
expect(render({ title: "My Page" })).toContain("<title>My Page</title>");
|
||||
});
|
||||
|
||||
it("generates a description meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { description: "A description" } });
|
||||
expect(html).toContain('<meta name="description" content="A description"');
|
||||
expect(render({ description: "A description" })).toContain(
|
||||
'content="A description"',
|
||||
);
|
||||
});
|
||||
|
||||
it("joins array keywords with comma", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { keywords: ["react", "bun", "ssr"] },
|
||||
});
|
||||
expect(html).toContain('content="react, bun, ssr"');
|
||||
expect(render({ keywords: ["react", "bun", "ssr"] })).toContain(
|
||||
'content="react, bun, ssr"',
|
||||
);
|
||||
});
|
||||
|
||||
it("uses string keywords directly", () => {
|
||||
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
|
||||
expect(html).toContain('content="react, bun"');
|
||||
expect(render({ keywords: "react, bun" })).toContain(
|
||||
'content="react, bun"',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates author meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
|
||||
expect(html).toContain('<meta name="author" content="Alice"');
|
||||
expect(render({ author: "Alice" })).toContain('content="Alice"');
|
||||
});
|
||||
|
||||
it("generates robots meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
|
||||
expect(html).toContain('<meta name="robots" content="noindex"');
|
||||
expect(render({ robots: "noindex" })).toContain('content="noindex"');
|
||||
});
|
||||
|
||||
it("generates canonical link tag", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { canonical: "https://example.com/page" },
|
||||
});
|
||||
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
|
||||
expect(render({ canonical: "https://example.com/page" })).toContain(
|
||||
'href="https://example.com/page"',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates theme-color meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
|
||||
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
|
||||
expect(render({ themeColor: "#ff0000" })).toContain(
|
||||
'content="#ff0000"',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates OG tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
const html = render({
|
||||
og: {
|
||||
title: "OG Title",
|
||||
description: "OG Desc",
|
||||
@ -62,20 +64,19 @@ describe("grabWebMetaHTML", () => {
|
||||
siteName: "Example",
|
||||
locale: "en_US",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta property="og:title" content="OG Title"');
|
||||
expect(html).toContain('<meta property="og:description" content="OG Desc"');
|
||||
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
|
||||
expect(html).toContain('<meta property="og:url" content="https://example.com"');
|
||||
expect(html).toContain('<meta property="og:type" content="website"');
|
||||
expect(html).toContain('<meta property="og:site_name" content="Example"');
|
||||
expect(html).toContain('<meta property="og:locale" content="en_US"');
|
||||
expect(html).toContain('property="og:title"');
|
||||
expect(html).toContain('content="OG Title"');
|
||||
expect(html).toContain('property="og:description"');
|
||||
expect(html).toContain('property="og:image"');
|
||||
expect(html).toContain('property="og:url"');
|
||||
expect(html).toContain('property="og:type"');
|
||||
expect(html).toContain('property="og:site_name"');
|
||||
expect(html).toContain('property="og:locale"');
|
||||
});
|
||||
|
||||
it("generates Twitter card tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
const html = render({
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Tweet Title",
|
||||
@ -84,25 +85,25 @@ describe("grabWebMetaHTML", () => {
|
||||
site: "@example",
|
||||
creator: "@alice",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
|
||||
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
|
||||
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
|
||||
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
|
||||
expect(html).toContain('<meta name="twitter:site" content="@example"');
|
||||
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
|
||||
expect(html).toContain('name="twitter:card"');
|
||||
expect(html).toContain('content="summary_large_image"');
|
||||
expect(html).toContain('name="twitter:title"');
|
||||
expect(html).toContain('name="twitter:description"');
|
||||
expect(html).toContain('name="twitter:image"');
|
||||
expect(html).toContain('name="twitter:site"');
|
||||
expect(html).toContain('name="twitter:creator"');
|
||||
});
|
||||
|
||||
it("skips undefined OG fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
|
||||
const html = render({ og: { title: "Only Title" } });
|
||||
expect(html).toContain("og:title");
|
||||
expect(html).not.toContain("og:description");
|
||||
expect(html).not.toContain("og:image");
|
||||
});
|
||||
|
||||
it("does not emit tags for missing fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
|
||||
const html = render({ title: "Hello" });
|
||||
expect(html).not.toContain("description");
|
||||
expect(html).not.toContain("og:");
|
||||
expect(html).not.toContain("twitter:");
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Command } from "commander";
|
||||
import { log } from "../../utils/log";
|
||||
import init from "../../functions/init";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
// import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
@ -12,15 +13,14 @@ export default function () {
|
||||
return new Command("build")
|
||||
.description("Build Project")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.BUILD = "true";
|
||||
|
||||
try {
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
} catch (error) {}
|
||||
|
||||
await rewritePagesModule();
|
||||
global.SKIPPED_BROWSER_MODULES = new Set<string>();
|
||||
|
||||
// await rewritePagesModule();
|
||||
await init();
|
||||
|
||||
log.banner();
|
||||
|
||||
@ -2,9 +2,9 @@ import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
@ -21,8 +21,8 @@ export default function () {
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
} catch (error) {}
|
||||
|
||||
await rewritePagesModule();
|
||||
await bunextInit();
|
||||
await allPagesBunBundler();
|
||||
|
||||
await startServer();
|
||||
});
|
||||
|
||||
@ -2,17 +2,18 @@ import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
|
||||
|
||||
export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
log.info("Starting production server ...");
|
||||
|
||||
await bunextInit();
|
||||
|
||||
await allPagesBunBundler();
|
||||
|
||||
await startServer();
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import path from "path";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import { mkdirSync, rmSync } from "fs";
|
||||
import recordArtifacts from "./record-artifacts";
|
||||
import BunSkipNonBrowserPlugin from "./plugins/bun-skip-browser-plugin";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_TMP_DIR } =
|
||||
grabDirNames();
|
||||
@ -55,17 +56,19 @@ export default async function allPagesBunBundler(params?: Params) {
|
||||
|
||||
const buildStart = performance.now();
|
||||
|
||||
const define = {
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
dev ? "development" : "production",
|
||||
),
|
||||
};
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [...entryToPage.keys()],
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
root: BUNX_HYDRATION_SRC_DIR,
|
||||
minify: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
dev ? "development" : "production",
|
||||
),
|
||||
},
|
||||
define,
|
||||
naming: {
|
||||
entry: "[dir]/[hash].[ext]",
|
||||
chunk: "chunks/[hash].[ext]",
|
||||
@ -124,7 +127,10 @@ export default async function allPagesBunBundler(params?: Params) {
|
||||
}
|
||||
|
||||
if (artifacts?.[0]) {
|
||||
await recordArtifacts({ artifacts });
|
||||
await recordArtifacts({
|
||||
artifacts,
|
||||
page_file_paths,
|
||||
});
|
||||
}
|
||||
|
||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
||||
|
||||
@ -9,6 +9,7 @@ import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-resul
|
||||
import { writeFileSync } from "fs";
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
import recordArtifacts from "./record-artifacts";
|
||||
import stripServerSideLogic from "./strip-server-side-logic";
|
||||
|
||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
|
||||
@ -39,7 +40,7 @@ export default async function allPagesBundler(params?: Params) {
|
||||
const dev = isDevelopment();
|
||||
|
||||
for (const page of target_pages) {
|
||||
const key = page.transformed_path;
|
||||
const key = page.local_path;
|
||||
|
||||
const txt = await grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
@ -51,6 +52,13 @@ export default async function allPagesBundler(params?: Params) {
|
||||
|
||||
if (!txt) continue;
|
||||
|
||||
// const final_tsx = stripServerSideLogic({
|
||||
// txt_code: txt,
|
||||
// file_path: key,
|
||||
// });
|
||||
|
||||
// console.log("final_tsx", final_tsx);
|
||||
|
||||
virtualEntries[key] = txt;
|
||||
}
|
||||
|
||||
@ -94,6 +102,31 @@ export default async function allPagesBundler(params?: Params) {
|
||||
|
||||
const entryPoints = Object.keys(virtualEntries).map((k) => `virtual:${k}`);
|
||||
|
||||
// let alias: any = {};
|
||||
// const excludes = [
|
||||
// "bun:sqlite",
|
||||
// "path",
|
||||
// "url",
|
||||
// "events",
|
||||
// "util",
|
||||
// "crypto",
|
||||
// "net",
|
||||
// "tls",
|
||||
// "fs",
|
||||
// "node:path",
|
||||
// "node:url",
|
||||
// "node:process",
|
||||
// "node:fs",
|
||||
// "node:timers/promises",
|
||||
// ];
|
||||
|
||||
// for (let i = 0; i < excludes.length; i++) {
|
||||
// const exclude = excludes[i];
|
||||
// alias[exclude] = "./empty.js";
|
||||
// }
|
||||
|
||||
// console.log("alias", alias);
|
||||
|
||||
const result = await esbuild.build({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
@ -119,6 +152,7 @@ export default async function allPagesBundler(params?: Params) {
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
],
|
||||
// alias,
|
||||
});
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
|
||||
@ -4,6 +4,7 @@ import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import pagePathTransform from "../../utils/page-path-transform";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
|
||||
@ -20,28 +21,26 @@ export default async function grabClientHydrationScript({
|
||||
ClientWindowPagePropsName,
|
||||
} = grabConstants();
|
||||
|
||||
const target_path = pagePathTransform({ page_path: page_local_path });
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
|
||||
const root_component_path = path.join(
|
||||
PAGES_DIR,
|
||||
`${AppNames["RootPagesComponentName"]}.tsx`,
|
||||
);
|
||||
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
// const target_path = pagePathTransform({ page_path: page_local_path });
|
||||
// const target_root_path = root_file_path
|
||||
// ? pagePathTransform({ page_path: root_file_path })
|
||||
// : undefined;
|
||||
|
||||
let txt = ``;
|
||||
|
||||
txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
if (root_file_path) {
|
||||
txt += `import Root from "${root_file_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${target_path}";\n\n`;
|
||||
txt += `import Page from "${page_local_path}";\n\n`;
|
||||
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
||||
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
if (root_file_path) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
} else {
|
||||
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
|
||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||
|
||||
@ -1,33 +1,84 @@
|
||||
import { log } from "../../../utils/log";
|
||||
|
||||
const BunSkipNonBrowserPlugin: Bun.BunPlugin = {
|
||||
name: "skip-non-browser",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^(bun:|node:)/ }, (args) => {
|
||||
return { path: args.path, external: true };
|
||||
const skipFilter =
|
||||
/^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
|
||||
|
||||
// const skipped_modules = new Set<string>();
|
||||
|
||||
build.onResolve({ filter: skipFilter }, (args) => {
|
||||
global.SKIPPED_BROWSER_MODULES.add(args.path);
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "skipped",
|
||||
// external: true,
|
||||
};
|
||||
});
|
||||
|
||||
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||
// If it's a built-in like 'fs' or 'path', skip it immediately
|
||||
const excludes = [
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"crypto",
|
||||
"net",
|
||||
"events",
|
||||
"util",
|
||||
];
|
||||
// build.onEnd(() => {
|
||||
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [
|
||||
// ...global.SKIPPED_BROWSER_MODULES,
|
||||
// ]);
|
||||
// });
|
||||
|
||||
if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||
return { path: args.path, external: true };
|
||||
}
|
||||
// build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||
// // If it's a built-in like 'fs' or 'path', skip it immediately
|
||||
// const excludes = [
|
||||
// "fs",
|
||||
// "path",
|
||||
// "os",
|
||||
// "crypto",
|
||||
// "net",
|
||||
// "events",
|
||||
// "util",
|
||||
// "tls",
|
||||
// ];
|
||||
|
||||
try {
|
||||
Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||
return { path: args.path, external: true };
|
||||
}
|
||||
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
|
||||
// try {
|
||||
// Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||
// return null;
|
||||
// } catch (e) {
|
||||
// console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
|
||||
return {
|
||||
contents: `
|
||||
const proxy = new Proxy(() => proxy, {
|
||||
get: () => proxy,
|
||||
construct: () => proxy,
|
||||
});
|
||||
|
||||
export const Database = proxy;
|
||||
export const join = proxy;
|
||||
export const fileURLToPath = proxy;
|
||||
export const arch = proxy;
|
||||
export const platform = proxy;
|
||||
export const statSync = proxy;
|
||||
|
||||
export const $H = proxy;
|
||||
export const _ = proxy;
|
||||
|
||||
export default proxy;
|
||||
`,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
import _ from "lodash";
|
||||
|
||||
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
artifacts: BundlerCTXMap[];
|
||||
page_file_paths?: string[];
|
||||
};
|
||||
|
||||
export default async function recordArtifacts({ artifacts }: Params) {
|
||||
export default async function recordArtifacts({
|
||||
artifacts,
|
||||
page_file_paths,
|
||||
}: Params) {
|
||||
const artifacts_map: { [k: string]: BundlerCTXMap } = {};
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
@ -17,11 +22,11 @@ export default async function recordArtifacts({ artifacts }: Params) {
|
||||
}
|
||||
|
||||
if (global.BUNDLER_CTX_MAP) {
|
||||
global.BUNDLER_CTX_MAP = artifacts_map;
|
||||
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map);
|
||||
}
|
||||
|
||||
await Bun.write(
|
||||
HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||
JSON.stringify(artifacts_map, null, 4),
|
||||
);
|
||||
// await Bun.write(
|
||||
// HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||
// JSON.stringify(artifacts_map, null, 4),
|
||||
// );
|
||||
}
|
||||
|
||||
@ -7,14 +7,12 @@ import type {
|
||||
} from "../types";
|
||||
import type { FileSystemRouter, Server } from "bun";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { readFileSync, type FSWatcher } from "fs";
|
||||
import { type FSWatcher } from "fs";
|
||||
import init from "./init";
|
||||
import isDevelopment from "../utils/is-development";
|
||||
import allPagesBundler from "./bundler/all-pages-bundler";
|
||||
import watcher from "./server/watcher";
|
||||
import { log } from "../utils/log";
|
||||
import cron from "./server/cron";
|
||||
import EJSON from "../utils/ejson";
|
||||
import allPagesBunBundler from "./bundler/all-pages-bun-bundler";
|
||||
|
||||
/**
|
||||
@ -35,7 +33,7 @@ declare global {
|
||||
var CURRENT_VERSION: string | undefined;
|
||||
var PAGE_FILES: PageFiles[];
|
||||
var ROOT_FILE_UPDATED: boolean;
|
||||
// var BUNDLER_CTX: BuildContext | undefined;
|
||||
var SKIPPED_BROWSER_MODULES: Set<string>;
|
||||
}
|
||||
|
||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
@ -47,6 +45,7 @@ export default async function bunextInit() {
|
||||
global.BUNDLER_CTX_MAP = {};
|
||||
global.BUNDLER_REBUILDS = 0;
|
||||
global.PAGE_FILES = [];
|
||||
global.SKIPPED_BROWSER_MODULES = new Set<string>();
|
||||
|
||||
await init();
|
||||
log.banner();
|
||||
@ -61,18 +60,8 @@ export default async function bunextInit() {
|
||||
const is_dev = isDevelopment();
|
||||
|
||||
if (is_dev) {
|
||||
// await allPagesBundler();
|
||||
await allPagesBunBundler();
|
||||
watcher();
|
||||
} else {
|
||||
const artifacts = EJSON.parse(
|
||||
readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"),
|
||||
) as { [k: string]: BundlerCTXMap } | undefined;
|
||||
if (!artifacts) {
|
||||
log.error("Please build first.");
|
||||
process.exit(1);
|
||||
}
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
cron();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import rebuildBundler from "./rebuild-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
// import rewritePagesModule from "../../utils/rewrite-pages-module";
|
||||
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
@ -82,19 +82,18 @@ async function fullRebuild(params?: { msg?: string }) {
|
||||
|
||||
global.RECOMPILING = true;
|
||||
|
||||
const target_file_paths = global.HMR_CONTROLLERS.map(
|
||||
(hmr) => hmr.target_map?.local_path,
|
||||
).filter((f) => typeof f == "string");
|
||||
// const target_file_paths = global.HMR_CONTROLLERS.map(
|
||||
// (hmr) => hmr.target_map?.local_path,
|
||||
// ).filter((f) => typeof f == "string");
|
||||
|
||||
await rewritePagesModule({
|
||||
page_file_path: target_file_paths,
|
||||
});
|
||||
// await rewritePagesModule();
|
||||
|
||||
if (msg) {
|
||||
log.watch(msg);
|
||||
}
|
||||
|
||||
await rebuildBundler({ target_file_paths });
|
||||
await rebuildBundler();
|
||||
// await rebuildBundler({ target_file_paths });
|
||||
} catch (error: any) {
|
||||
log.error(error);
|
||||
} finally {
|
||||
|
||||
@ -24,78 +24,159 @@ export default async function genWebHTML({
|
||||
component,
|
||||
pageProps,
|
||||
bundledMap,
|
||||
head: Head,
|
||||
module,
|
||||
meta,
|
||||
routeParams,
|
||||
debug,
|
||||
root_module,
|
||||
}: LivePageDistGenParams) {
|
||||
const { ClientRootElementIDName, ClientWindowPagePropsName } =
|
||||
grabContants();
|
||||
|
||||
const is_dev = isDevelopment();
|
||||
|
||||
if (debug) {
|
||||
log.info("component", component);
|
||||
}
|
||||
|
||||
const componentHTML = renderToString(component);
|
||||
|
||||
if (debug) {
|
||||
log.info("componentHTML", componentHTML);
|
||||
}
|
||||
|
||||
const headHTML = Head
|
||||
? renderToString(<Head serverRes={pageProps} ctx={routeParams} />)
|
||||
: "";
|
||||
|
||||
let html = `<!DOCTYPE html>\n`;
|
||||
html += `<html>\n`;
|
||||
html += ` <head>\n`;
|
||||
html += ` <meta charset="utf-8" />\n`;
|
||||
html += ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
|
||||
|
||||
if (meta) {
|
||||
html += ` ${grabWebMetaHTML({ meta })}\n`;
|
||||
}
|
||||
|
||||
if (bundledMap?.css_path) {
|
||||
html += ` <link rel="stylesheet" href="/${bundledMap.css_path}" />\n`;
|
||||
}
|
||||
|
||||
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(
|
||||
/<\//g,
|
||||
"<\\/",
|
||||
);
|
||||
html += ` <script>window.${ClientWindowPagePropsName} = ${serializedProps}</script>\n`;
|
||||
|
||||
if (bundledMap?.path) {
|
||||
const page_hydration_script = await grabWebPageHydrationScript();
|
||||
const root_meta = root_module?.meta
|
||||
? typeof root_module.meta == "function" && routeParams
|
||||
? await root_module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||
: typeof root_module.meta == "function"
|
||||
? undefined
|
||||
: root_module.meta
|
||||
: undefined;
|
||||
const page_meta = module?.meta
|
||||
? typeof module.meta == "function" && routeParams
|
||||
? await module.meta({ ctx: routeParams, serverRes: pageProps })
|
||||
: typeof module.meta == "function"
|
||||
? undefined
|
||||
: module.meta
|
||||
: undefined;
|
||||
|
||||
const html_props = {
|
||||
...module?.html_props,
|
||||
...root_module?.html_props,
|
||||
};
|
||||
|
||||
const Head = module?.Head;
|
||||
const RootHead = root_module?.Head;
|
||||
|
||||
const dev = isDevelopment();
|
||||
const devSuffix = dev ? "?dev" : "";
|
||||
const browser_imports: Record<string, string> = {
|
||||
react: `https://esm.sh/react@${_reactVersion}`,
|
||||
"react-dom": `https://esm.sh/react-dom@${_reactVersion}`,
|
||||
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client`,
|
||||
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime`,
|
||||
};
|
||||
|
||||
if (dev) {
|
||||
browser_imports["react/jsx-dev-runtime"] =
|
||||
`https://esm.sh/react@${_reactVersion}/jsx-dev-runtime`;
|
||||
}
|
||||
|
||||
const importMap = JSON.stringify({
|
||||
imports: {
|
||||
react: `https://esm.sh/react@${_reactVersion}${devSuffix}`,
|
||||
"react-dom": `https://esm.sh/react-dom@${_reactVersion}${devSuffix}`,
|
||||
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client${devSuffix}`,
|
||||
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime${devSuffix}`,
|
||||
"react/jsx-dev-runtime": `https://esm.sh/react@${_reactVersion}/jsx-dev-runtime${devSuffix}`,
|
||||
},
|
||||
imports: browser_imports,
|
||||
});
|
||||
html += ` <script type="importmap">${importMap}</script>\n`;
|
||||
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
|
||||
}
|
||||
|
||||
if (isDevelopment()) {
|
||||
html += `<script defer>\n${await grabWebPageHydrationScript()}\n</script>\n`;
|
||||
}
|
||||
let final_component = (
|
||||
<html {...html_props}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
|
||||
if (headHTML) {
|
||||
html += ` ${headHTML}\n`;
|
||||
}
|
||||
{root_meta ? grabWebMetaHTML({ meta: root_meta }) : null}
|
||||
{page_meta ? grabWebMetaHTML({ meta: page_meta }) : null}
|
||||
|
||||
html += ` </head>\n`;
|
||||
html += ` <body>\n`;
|
||||
html += ` <div id="${ClientRootElementIDName}">${componentHTML}</div>\n`;
|
||||
html += ` </body>\n`;
|
||||
html += `</html>\n`;
|
||||
{bundledMap?.css_path ? (
|
||||
<link rel="stylesheet" href={`/${bundledMap.css_path}`} />
|
||||
) : null}
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.${ClientWindowPagePropsName} = ${serializedProps}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* {global.SKIPPED_BROWSER_MODULES ? (
|
||||
<script
|
||||
type="importmap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: importMap,
|
||||
}}
|
||||
fetchPriority="high"
|
||||
/>
|
||||
) : null} */}
|
||||
|
||||
{bundledMap?.path ? (
|
||||
<>
|
||||
<script
|
||||
type="importmap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: importMap,
|
||||
}}
|
||||
fetchPriority="high"
|
||||
/>
|
||||
<script
|
||||
src={`/${bundledMap.path}`}
|
||||
type="module"
|
||||
id={AppData["BunextClientHydrationScriptID"]}
|
||||
defer
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{is_dev ? (
|
||||
<script
|
||||
defer
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: page_hydration_script,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{RootHead ? (
|
||||
<RootHead serverRes={pageProps} ctx={routeParams} />
|
||||
) : null}
|
||||
{Head ? <Head serverRes={pageProps} ctx={routeParams} /> : null}
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id={ClientRootElementIDName}
|
||||
suppressHydrationWarning={!dev}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
let html = `<!DOCTYPE html>\n`;
|
||||
|
||||
// const stream = await renderToReadableStream(final_component, {
|
||||
// onError(error: any) {
|
||||
// // This is where you "omit" or handle the errors
|
||||
// // You can log it silently or ignore it
|
||||
// if (error.message.includes('unique "key" prop')) return;
|
||||
// console.error(error);
|
||||
// },
|
||||
// });
|
||||
|
||||
// // 2. Convert the Web Stream to a String (Bun-optimized)
|
||||
// const htmlBody = await new Response(stream).text();
|
||||
|
||||
// html += htmlBody;
|
||||
|
||||
html += renderToString(final_component);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import _ from "lodash";
|
||||
import type { GrabPageComponentRes } from "../../../types";
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import { log } from "../../../utils/log";
|
||||
@ -8,21 +9,19 @@ export default async function generateWebPageResponseFromComponentReturn({
|
||||
component,
|
||||
module,
|
||||
bundledMap,
|
||||
head,
|
||||
meta,
|
||||
routeParams,
|
||||
serverRes,
|
||||
debug,
|
||||
root_module,
|
||||
}: GrabPageComponentRes) {
|
||||
const html = await genWebHTML({
|
||||
component,
|
||||
pageProps: serverRes,
|
||||
bundledMap,
|
||||
module,
|
||||
meta,
|
||||
head,
|
||||
routeParams,
|
||||
debug,
|
||||
root_module,
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
@ -55,9 +54,10 @@ export default async function generateWebPageResponseFromComponentReturn({
|
||||
};
|
||||
}
|
||||
|
||||
const cache_page =
|
||||
module.config?.cachePage || serverRes?.cachePage || false;
|
||||
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
|
||||
const config = _.merge(root_module?.config, module.config);
|
||||
|
||||
const cache_page = config?.cachePage || serverRes?.cachePage || false;
|
||||
const expiry_seconds = config?.cacheExpiry || serverRes?.cacheExpiry;
|
||||
|
||||
if (cache_page && routeParams?.url) {
|
||||
const key = routeParams.url.pathname + (routeParams.url.search || "");
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
import grabRouteParams from "../../../utils/grab-route-params";
|
||||
import type {
|
||||
BunextPageModule,
|
||||
BunextPageModuleServerReturn,
|
||||
BunextPageServerModule,
|
||||
BunextRootModule,
|
||||
BunxRouteParams,
|
||||
GrabPageComponentRes,
|
||||
} from "../../../types";
|
||||
@ -11,6 +12,8 @@ import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
|
||||
import _ from "lodash";
|
||||
import { log } from "../../../utils/log";
|
||||
import grabRootFilePath from "./grab-root-file-path";
|
||||
import grabPageServerRes from "./grab-page-server-res";
|
||||
import grabPageServerPath from "./grab-page-server-path";
|
||||
|
||||
class NotFoundError extends Error {}
|
||||
|
||||
@ -65,6 +68,7 @@ export default async function grabPageComponent({
|
||||
const bundledMap = global.BUNDLER_CTX_MAP?.[file_path];
|
||||
|
||||
if (!bundledMap?.path) {
|
||||
console.log(global.BUNDLER_CTX_MAP);
|
||||
const errMsg = `No Bundled File Path for this request path!`;
|
||||
log.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
@ -75,81 +79,65 @@ export default async function grabPageComponent({
|
||||
}
|
||||
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
const root_module: BunextRootModule | undefined = root_file_path
|
||||
? await import(`${root_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
const { server_file_path: root_server_file_path } = root_file_path
|
||||
? grabPageServerPath({ file_path: root_file_path })
|
||||
: {};
|
||||
const root_server_module: BunextPageServerModule = root_server_file_path
|
||||
? await import(`${root_server_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
|
||||
const root_server_fn =
|
||||
root_server_module?.default || root_server_module?.server;
|
||||
|
||||
const rootServerRes: BunextPageModuleServerReturn | undefined =
|
||||
root_server_fn
|
||||
? await grabPageServerRes({
|
||||
server_function: root_server_fn,
|
||||
url,
|
||||
query: match?.query,
|
||||
routeParams,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (debug) {
|
||||
log.info(`rootServerRes:`, rootServerRes);
|
||||
}
|
||||
|
||||
const module: BunextPageModule = await import(`${file_path}?t=${now}`);
|
||||
const { server_file_path } = grabPageServerPath({ file_path });
|
||||
const server_module: BunextPageServerModule = server_file_path
|
||||
? await import(`${server_file_path}?t=${now}`)
|
||||
: undefined;
|
||||
|
||||
if (debug) {
|
||||
log.info(`module:`, module);
|
||||
}
|
||||
|
||||
const serverRes: BunextPageModuleServerReturn = await (async () => {
|
||||
const default_props: BunextPageModuleServerReturn = {
|
||||
url: {
|
||||
...(_.pick<URL, keyof URL>(url!, [
|
||||
"host",
|
||||
"hostname",
|
||||
"pathname",
|
||||
"origin",
|
||||
"port",
|
||||
"search",
|
||||
"searchParams",
|
||||
"hash",
|
||||
"href",
|
||||
"password",
|
||||
"protocol",
|
||||
"username",
|
||||
]) as any),
|
||||
},
|
||||
query: match?.query,
|
||||
};
|
||||
const server_fn = server_module?.default || server_module?.server;
|
||||
|
||||
try {
|
||||
if (routeParams) {
|
||||
const serverData = await module["server"]?.({
|
||||
...routeParams,
|
||||
query: { ...routeParams.query, ...match?.query },
|
||||
});
|
||||
return {
|
||||
...serverData,
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
})();
|
||||
const serverRes: BunextPageModuleServerReturn | undefined = server_fn
|
||||
? await grabPageServerRes({
|
||||
server_function: server_fn,
|
||||
url,
|
||||
query: match?.query,
|
||||
routeParams,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (debug) {
|
||||
log.info(`serverRes:`, serverRes);
|
||||
}
|
||||
|
||||
const meta = module.meta
|
||||
? typeof module.meta == "function" && routeParams
|
||||
? await module.meta({
|
||||
ctx: routeParams,
|
||||
serverRes,
|
||||
})
|
||||
: typeof module.meta == "object"
|
||||
? module.meta
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
if (debug) {
|
||||
log.info(`meta:`, meta);
|
||||
}
|
||||
|
||||
const Head = module.Head as FC<any>;
|
||||
const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {});
|
||||
|
||||
const { component } =
|
||||
(await grabPageBundledReactComponent({
|
||||
file_path,
|
||||
root_file_path,
|
||||
server_res: serverRes,
|
||||
server_res: mergedServerRes,
|
||||
})) || {};
|
||||
|
||||
if (!component) {
|
||||
@ -162,12 +150,11 @@ export default async function grabPageComponent({
|
||||
|
||||
return {
|
||||
component,
|
||||
serverRes,
|
||||
serverRes: mergedServerRes,
|
||||
routeParams,
|
||||
module,
|
||||
bundledMap,
|
||||
meta,
|
||||
head: Head,
|
||||
root_module,
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error Grabbing Page Component: ${error.message}`);
|
||||
|
||||
@ -13,25 +13,34 @@ export default function grabPageReactComponentString({
|
||||
server_res,
|
||||
}: Params): string | undefined {
|
||||
try {
|
||||
const target_path = pagePathTransform({ page_path: file_path });
|
||||
// const target_path = pagePathTransform({ page_path: file_path });
|
||||
// const target_root_path = root_file_path
|
||||
// ? pagePathTransform({ page_path: root_file_path })
|
||||
// : undefined;
|
||||
|
||||
let tsx = ``;
|
||||
|
||||
const server_res_json = JSON.stringify(
|
||||
EJSON.stringify(server_res || {}) ?? "{}",
|
||||
);
|
||||
|
||||
// Import Root from its original source path so that all sub-components
|
||||
// that import __root (e.g. AppContext) resolve to the same module instance.
|
||||
// Using the rewritten .bunext/pages/__root would create a separate
|
||||
// createContext() call, breaking context for any sub-component that
|
||||
// imports AppContext via a relative path to the source __root.
|
||||
if (root_file_path) {
|
||||
tsx += `import Root from "${root_file_path}"\n`;
|
||||
}
|
||||
|
||||
tsx += `import Page from "${target_path}"\n`;
|
||||
tsx += `import Page from "${file_path}"\n`;
|
||||
tsx += `export default function Main() {\n\n`;
|
||||
tsx += `const props = JSON.parse(${server_res_json})\n\n`;
|
||||
tsx += ` return (\n`;
|
||||
if (root_file_path) {
|
||||
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
|
||||
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
|
||||
} else {
|
||||
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
|
||||
tsx += ` <Page {...props} />\n`;
|
||||
}
|
||||
tsx += ` )\n`;
|
||||
tsx += `}\n`;
|
||||
|
||||
18
src/functions/server/web-pages/grab-page-server-path.tsx
Normal file
18
src/functions/server/web-pages/grab-page-server-path.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { existsSync } from "fs";
|
||||
|
||||
type Params = {
|
||||
file_path: string;
|
||||
};
|
||||
|
||||
export default function grabPageServerPath({ file_path }: Params) {
|
||||
const page_server_ts_file = file_path.replace(/\.tsx?$/, ".server.ts");
|
||||
const page_server_tsx_file = file_path.replace(/\.tsx?$/, ".server.tsx");
|
||||
|
||||
const server_file_path = existsSync(page_server_ts_file)
|
||||
? page_server_ts_file
|
||||
: existsSync(page_server_tsx_file)
|
||||
? page_server_tsx_file
|
||||
: undefined;
|
||||
|
||||
return { server_file_path };
|
||||
}
|
||||
64
src/functions/server/web-pages/grab-page-server-res.tsx
Normal file
64
src/functions/server/web-pages/grab-page-server-res.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import type {
|
||||
BunextPageModule,
|
||||
BunextPageModuleServerReturn,
|
||||
BunextPageServerFn,
|
||||
BunxRouteParams,
|
||||
GrabPageComponentRes,
|
||||
} from "../../../types";
|
||||
import _ from "lodash";
|
||||
|
||||
type Params = {
|
||||
url?: URL;
|
||||
server_function: BunextPageServerFn;
|
||||
query?: Record<string, string>;
|
||||
routeParams?: BunxRouteParams;
|
||||
};
|
||||
|
||||
export default async function grabPageServerRes({
|
||||
url,
|
||||
query,
|
||||
routeParams,
|
||||
server_function,
|
||||
}: Params): Promise<BunextPageModuleServerReturn> {
|
||||
const default_props: BunextPageModuleServerReturn = {
|
||||
url: url
|
||||
? {
|
||||
...(_.pick<URL, keyof URL>(url, [
|
||||
"host",
|
||||
"hostname",
|
||||
"pathname",
|
||||
"origin",
|
||||
"port",
|
||||
"search",
|
||||
"searchParams",
|
||||
"hash",
|
||||
"href",
|
||||
"password",
|
||||
"protocol",
|
||||
"username",
|
||||
]) as any),
|
||||
}
|
||||
: null,
|
||||
query,
|
||||
};
|
||||
|
||||
try {
|
||||
if (routeParams) {
|
||||
const serverData = await server_function({
|
||||
...routeParams,
|
||||
query: { ...routeParams.query, ...query },
|
||||
});
|
||||
return {
|
||||
...serverData,
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...default_props,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import * as esbuild from "esbuild";
|
||||
import tailwindcss from "bun-plugin-tailwind";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
import BunSkipNonBrowserPlugin from "../../bundler/plugins/bun-skip-browser-plugin";
|
||||
|
||||
type Params = {
|
||||
tsx: string;
|
||||
@ -20,21 +20,22 @@ export default async function grabTsxStringModule<T extends any = any>({
|
||||
.replace(/.*\/src\/pages\//, "")
|
||||
.replace(/\.tsx$/, "");
|
||||
|
||||
const src_file_path = path.join(
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
`${trimmed_file_path}.tsx`,
|
||||
);
|
||||
|
||||
const out_file_path = path.join(
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
`${trimmed_file_path}.js`,
|
||||
);
|
||||
|
||||
await esbuild.build({
|
||||
stdin: {
|
||||
contents: tsx,
|
||||
resolveDir: process.cwd(),
|
||||
loader: "tsx",
|
||||
},
|
||||
bundle: true,
|
||||
await Bun.write(src_file_path, tsx);
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [src_file_path],
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "node",
|
||||
target: "bun",
|
||||
external: ["react", "react-dom"],
|
||||
minify: true,
|
||||
define: {
|
||||
@ -43,10 +44,12 @@ export default async function grabTsxStringModule<T extends any = any>({
|
||||
),
|
||||
},
|
||||
metafile: true,
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
jsx: "automatic",
|
||||
write: true,
|
||||
outfile: out_file_path,
|
||||
plugins: [tailwindcss, BunSkipNonBrowserPlugin],
|
||||
jsx: {
|
||||
runtime: "automatic",
|
||||
development: dev,
|
||||
},
|
||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
});
|
||||
|
||||
Loader.registry.delete(out_file_path);
|
||||
@ -54,3 +57,27 @@ export default async function grabTsxStringModule<T extends any = any>({
|
||||
|
||||
return module as T;
|
||||
}
|
||||
|
||||
// await esbuild.build({
|
||||
// stdin: {
|
||||
// contents: tsx,
|
||||
// resolveDir: process.cwd(),
|
||||
// loader: "tsx",
|
||||
// },
|
||||
// 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",
|
||||
// write: true,
|
||||
// outfile: out_file_path,
|
||||
// });
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
import { escape } from "lodash";
|
||||
import type { BunextPageModuleMeta } from "../../../types";
|
||||
|
||||
type Params = {
|
||||
meta: BunextPageModuleMeta;
|
||||
};
|
||||
|
||||
export default function grabWebMetaHTML({ meta }: Params) {
|
||||
let html = ``;
|
||||
|
||||
if (meta.title) {
|
||||
html += ` <title>${escape(meta.title)}</title>\n`;
|
||||
}
|
||||
|
||||
if (meta.description) {
|
||||
html += ` <meta name="description" content="${escape(meta.description)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.keywords) {
|
||||
const keywords = Array.isArray(meta.keywords)
|
||||
? meta.keywords.join(", ")
|
||||
: meta.keywords;
|
||||
html += ` <meta name="keywords" content="${escape(keywords)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.author) {
|
||||
html += ` <meta name="author" content="${escape(meta.author)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.robots) {
|
||||
html += ` <meta name="robots" content="${escape(meta.robots)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.canonical) {
|
||||
html += ` <link rel="canonical" href="${escape(meta.canonical)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.themeColor) {
|
||||
html += ` <meta name="theme-color" content="${escape(meta.themeColor)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.og) {
|
||||
const { og } = meta;
|
||||
if (og.title)
|
||||
html += ` <meta property="og:title" content="${escape(og.title)}" />\n`;
|
||||
if (og.description)
|
||||
html += ` <meta property="og:description" content="${escape(og.description)}" />\n`;
|
||||
if (og.image)
|
||||
html += ` <meta property="og:image" content="${escape(og.image)}" />\n`;
|
||||
if (og.url)
|
||||
html += ` <meta property="og:url" content="${escape(og.url)}" />\n`;
|
||||
if (og.type)
|
||||
html += ` <meta property="og:type" content="${escape(og.type)}" />\n`;
|
||||
if (og.siteName)
|
||||
html += ` <meta property="og:site_name" content="${escape(og.siteName)}" />\n`;
|
||||
if (og.locale)
|
||||
html += ` <meta property="og:locale" content="${escape(og.locale)}" />\n`;
|
||||
}
|
||||
|
||||
if (meta.twitter) {
|
||||
const { twitter } = meta;
|
||||
if (twitter.card)
|
||||
html += ` <meta name="twitter:card" content="${escape(twitter.card)}" />\n`;
|
||||
if (twitter.title)
|
||||
html += ` <meta name="twitter:title" content="${escape(twitter.title)}" />\n`;
|
||||
if (twitter.description)
|
||||
html += ` <meta name="twitter:description" content="${escape(twitter.description)}" />\n`;
|
||||
if (twitter.image)
|
||||
html += ` <meta name="twitter:image" content="${escape(twitter.image)}" />\n`;
|
||||
if (twitter.site)
|
||||
html += ` <meta name="twitter:site" content="${escape(twitter.site)}" />\n`;
|
||||
if (twitter.creator)
|
||||
html += ` <meta name="twitter:creator" content="${escape(twitter.creator)}" />\n`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
76
src/functions/server/web-pages/grab-web-meta-html.tsx
Normal file
76
src/functions/server/web-pages/grab-web-meta-html.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import type { BunextPageModuleMeta } from "../../../types";
|
||||
|
||||
type Params = {
|
||||
meta: BunextPageModuleMeta;
|
||||
};
|
||||
|
||||
export default function grabWebMetaHTML({ meta }: Params) {
|
||||
const keywords = meta.keywords
|
||||
? Array.isArray(meta.keywords)
|
||||
? meta.keywords.join(", ")
|
||||
: meta.keywords
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{meta.title && <title>{meta.title}</title>}
|
||||
{meta.description && (
|
||||
<meta name="description" content={meta.description} />
|
||||
)}
|
||||
{keywords && <meta name="keywords" content={keywords} />}
|
||||
{meta.author && <meta name="author" content={meta.author} />}
|
||||
{meta.robots && <meta name="robots" content={meta.robots} />}
|
||||
{meta.canonical && (
|
||||
<link rel="canonical" href={meta.canonical} />
|
||||
)}
|
||||
{meta.themeColor && (
|
||||
<meta name="theme-color" content={meta.themeColor} />
|
||||
)}
|
||||
{meta.og?.title && (
|
||||
<meta property="og:title" content={meta.og.title} />
|
||||
)}
|
||||
{meta.og?.description && (
|
||||
<meta
|
||||
property="og:description"
|
||||
content={meta.og.description}
|
||||
/>
|
||||
)}
|
||||
{meta.og?.image && (
|
||||
<meta property="og:image" content={meta.og.image} />
|
||||
)}
|
||||
{meta.og?.url && (
|
||||
<meta property="og:url" content={meta.og.url} />
|
||||
)}
|
||||
{meta.og?.type && (
|
||||
<meta property="og:type" content={meta.og.type} />
|
||||
)}
|
||||
{meta.og?.siteName && (
|
||||
<meta property="og:site_name" content={meta.og.siteName} />
|
||||
)}
|
||||
{meta.og?.locale && (
|
||||
<meta property="og:locale" content={meta.og.locale} />
|
||||
)}
|
||||
{meta.twitter?.card && (
|
||||
<meta name="twitter:card" content={meta.twitter.card} />
|
||||
)}
|
||||
{meta.twitter?.title && (
|
||||
<meta name="twitter:title" content={meta.twitter.title} />
|
||||
)}
|
||||
{meta.twitter?.description && (
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={meta.twitter.description}
|
||||
/>
|
||||
)}
|
||||
{meta.twitter?.image && (
|
||||
<meta name="twitter:image" content={meta.twitter.image} />
|
||||
)}
|
||||
{meta.twitter?.site && (
|
||||
<meta name="twitter:site" content={meta.twitter.site} />
|
||||
)}
|
||||
{meta.twitter?.creator && (
|
||||
<meta name="twitter:creator" content={meta.twitter.creator} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/presets/components/head.tsx
Normal file
13
src/presets/components/head.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type {
|
||||
DetailedHTMLProps,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
|
||||
type Props = PropsWithChildren<
|
||||
DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>
|
||||
>;
|
||||
|
||||
export default function Head({ children, ...props }: Props) {
|
||||
return <head {...props}>{children}</head>;
|
||||
}
|
||||
@ -1,5 +1,12 @@
|
||||
import type { MatchedRoute, Server, WebSocketHandler } from "bun";
|
||||
import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
|
||||
import type {
|
||||
DetailedHTMLProps,
|
||||
FC,
|
||||
HtmlHTMLAttributes,
|
||||
JSX,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
export type ServerProps = {
|
||||
params: Record<string, string>;
|
||||
@ -145,11 +152,10 @@ export type PageDistGenParams = {
|
||||
|
||||
export type LivePageDistGenParams = {
|
||||
component: ReactNode;
|
||||
head?: FC<BunextPageHeadFCProps>;
|
||||
pageProps?: any;
|
||||
module?: BunextPageModule;
|
||||
root_module?: BunextRootModule;
|
||||
bundledMap?: BundlerCTXMap;
|
||||
meta?: BunextPageModuleMeta;
|
||||
routeParams?: BunxRouteParams;
|
||||
debug?: boolean;
|
||||
};
|
||||
@ -161,12 +167,22 @@ export type BunextPageHeadFCProps = {
|
||||
|
||||
export type BunextPageModule = {
|
||||
default: FC<any>;
|
||||
server?: BunextPageServerFn;
|
||||
meta?: BunextPageModuleMeta | BunextPageModuleMetaFn;
|
||||
Head?: FC<BunextPageHeadFCProps>;
|
||||
config?: BunextRouteConfig;
|
||||
html_props?: BunextHTMLProps;
|
||||
};
|
||||
|
||||
export type BunextPageServerModule = {
|
||||
default?: BunextPageServerFn;
|
||||
server?: BunextPageServerFn;
|
||||
};
|
||||
|
||||
export type BunextHTMLProps = DetailedHTMLProps<
|
||||
HtmlHTMLAttributes<HTMLHtmlElement>,
|
||||
HTMLHtmlElement
|
||||
>;
|
||||
|
||||
export type BunextPageModuleMetaFn = (params: {
|
||||
ctx: BunxRouteParams;
|
||||
serverRes?: BunextPageModuleServerReturn;
|
||||
@ -253,11 +269,12 @@ export type GrabPageComponentRes = {
|
||||
routeParams?: BunxRouteParams;
|
||||
bundledMap?: BundlerCTXMap;
|
||||
module: BunextPageModule;
|
||||
meta?: BunextPageModuleMeta;
|
||||
head?: FC<BunextPageHeadFCProps>;
|
||||
root_module?: BunextRootModule;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
export type BunextRootModule = BunextPageModule;
|
||||
|
||||
export type GrabPageReactBundledComponentRes = {
|
||||
component: JSX.Element;
|
||||
server_res?: BunextPageModuleServerReturn;
|
||||
|
||||
@ -33,9 +33,10 @@ function grabPageDirRecursively({ page_dir }: { page_dir: string }) {
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const page_name = page.split("/").pop();
|
||||
const full_page_path = path.join(page_dir, page);
|
||||
|
||||
if (!existsSync(full_page_path)) {
|
||||
if (!existsSync(full_page_path) || !page_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -47,6 +48,10 @@ function grabPageDirRecursively({ page_dir }: { page_dir: string }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (page_name.split(".").length > 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const page_stat = statSync(full_page_path);
|
||||
|
||||
if (page_stat.isDirectory()) {
|
||||
|
||||
@ -4,6 +4,7 @@ import AppNames from "./grab-app-names";
|
||||
const prefix = {
|
||||
info: chalk.bgCyan.bold(" ℹnfo "),
|
||||
success: chalk.green.bold("✓"),
|
||||
zap: chalk.green.bold("⚡"),
|
||||
error: chalk.red.bold("✗"),
|
||||
warn: chalk.yellow.bold("⚠"),
|
||||
build: chalk.magenta.bold("⚙"),
|
||||
@ -19,7 +20,8 @@ export const log = {
|
||||
},
|
||||
error: (msg: string | Error, log?: any) =>
|
||||
console.error(`${prefix.error} ${chalk.red(String(msg))}`, log || ""),
|
||||
warn: (msg: string) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`),
|
||||
warn: (msg: string, log?: any) =>
|
||||
console.warn(`${prefix.warn} ${chalk.yellow(msg)}`, log || ""),
|
||||
build: (msg: string) =>
|
||||
console.log(`${prefix.build} ${chalk.magenta(msg)}`),
|
||||
watch: (msg: string) => console.log(`${prefix.watch} ${chalk.blue(msg)}`),
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import grabAllPages from "./grab-all-pages";
|
||||
import pagePathTransform from "./page-path-transform";
|
||||
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
|
||||
import grabRootFilePath from "../functions/server/web-pages/grab-root-file-path";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
type Params = {
|
||||
page_file_path?: string | string[];
|
||||
@ -21,12 +23,19 @@ export default async function rewritePagesModule(params?: Params) {
|
||||
|
||||
for (let i = 0; i < target_pages.length; i++) {
|
||||
const page_path = target_pages[i];
|
||||
const dst_path = pagePathTransform({ page_path });
|
||||
|
||||
if (page_path.match(/__root\.tsx?/)) {
|
||||
continue;
|
||||
await transformFile(page_path);
|
||||
}
|
||||
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
|
||||
if (root_file_path && existsSync(root_file_path)) {
|
||||
await transformFile(root_file_path);
|
||||
}
|
||||
}
|
||||
|
||||
async function transformFile(page_path: string) {
|
||||
const dst_path = pagePathTransform({ page_path });
|
||||
|
||||
const origin_page_content = await Bun.file(page_path).text();
|
||||
const dst_page_content = stripServerSideLogic({
|
||||
txt_code: origin_page_content,
|
||||
@ -36,5 +45,4 @@ export default async function rewritePagesModule(params?: Params) {
|
||||
await Bun.write(dst_path, dst_page_content, {
|
||||
createPath: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user