diff --git a/README.md b/README.md index 4322e86..64df12c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bunext -A server-rendering framework for React, built on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using ESBuild to bundle client assets and `Bun.serve` as the HTTP server. +A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using `Bun.build` to bundle client assets and `Bun.serve` as the HTTP server. ## Philosophy @@ -8,7 +8,7 @@ Bunext is focused on **server-side rendering and processing**. Every page is ren The goal is a framework that is: -- Fast — Bun's runtime speed and ESBuild's bundling make the full dev loop snappy +- Fast — Bun's runtime speed and Bun.build's bundling make the full dev loop snappy - Transparent — the entire request pipeline is readable and debugable - Standard — server functions and API handlers use native Web APIs (`Request`, `Response`, `URL`) with no custom wrappers @@ -61,7 +61,8 @@ The goal is a framework that is: - [Bun](https://bun.sh) v1.0 or later - TypeScript 5.0+ -- React 19 and react-dom 19 (peer dependencies) + +> **React is managed by Bunext.** You do not need to install `react` or `react-dom` — Bunext enforces its own pinned React version and removes any user-installed copies at startup to prevent version conflicts. Installing this package is all you need. --- @@ -152,7 +153,7 @@ bun run dev | Command | Description | | -------------- | ---------------------------------------------------------------------- | | `bunext dev` | Start the development server with HMR and file watching. | -| `bunext build` | Bundle all pages for production. Outputs artifacts to `public/pages/`. | +| `bunext build` | Bundle all pages for production. Outputs artifacts to `.bunext/public/pages/`. | | `bunext start` | Start the production server using pre-built artifacts. | ### Running the CLI @@ -186,7 +187,7 @@ bunext build bunext start ``` -> **Note:** `bunext start` will exit with an error if `public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `bunext start`. +> **Note:** `bunext start` will exit with an error if `.bunext/public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `bunext start`. --- @@ -208,9 +209,10 @@ my-app/ │ │ └── [slug].tsx # Route: /blog/:slug (dynamic) │ └── api/ │ └── users.ts # API route: /api/users -├── public/ # Static files and bundler output -│ └── __bunext/ -│ ├── pages/ # Generated by bundler (do not edit manually) +├── public/ # Static files served at /public/* +├── .bunext/ # Internal build artifacts (do not edit manually) +│ └── public/ +│ ├── pages/ # Generated by bundler │ │ └── map.json # Artifact map used by production server │ └── cache/ # File-based HTML cache (production only) ├── bunext.config.ts # Optional configuration @@ -605,7 +607,7 @@ public/ Bunext includes a file-based HTML cache for production. Caching is **disabled in development** — every request renders fresh. In production, a cron job runs every 30 seconds to delete expired cache entries. -Cache files are stored in `public/__bunext/cache/`. Each cached page produces two files: +Cache files are stored in `.bunext/public/cache/`. Each cached page produces two files: | File | Contents | | ----------------- | ---------------------------------------------- | @@ -670,14 +672,14 @@ Expiry resolution order (first truthy value wins): 2. `defaultCacheExpiry` in `bunext.config.ts` (global default, in seconds) 3. Built-in default: **3600 seconds (1 hour)** -The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `public/__bunext/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache. +The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `.bunext/public/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache. ### Cache Behavior and Limitations - **Production only.** Caching never activates in development (`bunext dev`). - **Cold start required.** The cache is populated on the first request; there is no pre-warming step. - **Immutable within the expiry window.** Once a page is cached, `writeCache` skips all subsequent write attempts for that key until the cron job deletes the expired entry. There is no manual invalidation API. -- **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `public/__bunext/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed. +- **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `.bunext/public/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed. - **No key collision.** Cache keys are generated via `encodeURIComponent()` on the URL path. `/foo/bar` encodes to `%2Ffoo%2Fbar` and `/foo-bar` to `%2Ffoo-bar` — distinct filenames with no collision risk. --- @@ -880,7 +882,7 @@ Running `bunext dev`: 1. Loads `bunext.config.ts` and sets `development: true`. 2. Initializes directories (`.bunext/`, `public/pages/`). 3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`. -4. Starts the ESBuild bundler in **watch mode** — it will automatically rebuild when file content changes. +4. Starts `Bun.build` in **watch mode** — it will automatically rebuild when file content changes. 5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points. 6. Waits for the first successful bundle. 7. Starts `Bun.serve()`. @@ -890,26 +892,26 @@ Running `bunext dev`: Running `bunext build`: 1. Sets `NODE_ENV=production`. -2. Runs ESBuild once (not in watch mode) with minification enabled. -3. Writes all bundled artifacts to `public/pages/` and the artifact map to `public/pages/map.json`. +2. Runs `Bun.build` once with minification enabled. +3. Writes all bundled artifacts to `.bunext/public/pages/` and the artifact map to `.bunext/public/pages/map.json`. 4. Exits. ### Production Server Running `bunext start`: -1. Reads `public/pages/map.json` to load the pre-built artifact map. +1. Reads `.bunext/public/pages/map.json` to load the pre-built artifact map. 2. Starts `Bun.serve()` without any bundler or file watcher. ### Bundler -The bundler (`allPagesBundler`) uses ESBuild with three custom plugins: +The bundler uses `Bun.build` with the `bun-plugin-tailwind` plugin. For each page, a client hydration entry point is generated and written as a real temporary file under `.bunext/hydration-src/`. Each entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. -- **`tailwindcss` plugin** — Processes any `.css` files through PostCSS + Tailwind CSS before bundling. -- **`virtual-entrypoints` plugin** — Generates an in-memory client hydration entry point for each page. Each entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. -- **`artifact-tracker` plugin** — After each build, collects all output file paths, content hashes, and source entrypoints into a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `public/pages/map.json`. +React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the `Bun.build` config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `\n`; + const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(/<\//g, "<\\/"); + html += ` \n`; if (bundledMap?.path) { + const dev = isDevelopment(); + const devSuffix = dev ? "?dev" : ""; + 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}`, + }, + }); + html += ` \n`; html += ` \n`; } if (isDevelopment()) { diff --git a/dist/functions/server/web-pages/grab-page-component.js b/dist/functions/server/web-pages/grab-page-component.js index ba3d5ea..57089fd 100644 --- a/dist/functions/server/web-pages/grab-page-component.js +++ b/dist/functions/server/web-pages/grab-page-component.js @@ -33,7 +33,7 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa // log.error(errMsg); throw new Error(errMsg); } - const bundledMap = global.BUNDLER_CTX_MAP[file_path]; + const bundledMap = global.BUNDLER_CTX_MAP?.[file_path]; if (!bundledMap?.path) { const errMsg = `No Bundled File Path for this request path!`; log.error(errMsg); diff --git a/dist/functions/server/web-pages/grab-page-error-component.js b/dist/functions/server/web-pages/grab-page-error-component.js index 2881544..e9b5abf 100644 --- a/dist/functions/server/web-pages/grab-page-error-component.js +++ b/dist/functions/server/web-pages/grab-page-error-component.js @@ -11,8 +11,8 @@ export default async function grabPageErrorComponent({ error, routeParams, is404 const match = router.match(errorRoute); const filePath = match?.filePath || presetComponent; const bundledMap = match?.filePath - ? global.BUNDLER_CTX_MAP[match.filePath] - : {}; + ? global.BUNDLER_CTX_MAP?.[match.filePath] + : undefined; const module = await import(filePath); const Component = module.default; const component = _jsx(Component, { children: _jsx("span", { children: error.message }) }); @@ -41,7 +41,7 @@ export default async function grabPageErrorComponent({ error, routeParams, is404 component: _jsx(DefaultNotFound, {}), routeParams, module: { default: DefaultNotFound }, - bundledMap: {}, + bundledMap: undefined, serverRes: { responseOptions: { status: is404 ? 404 : 500, diff --git a/dist/functions/server/web-pages/grab-web-meta-html.js b/dist/functions/server/web-pages/grab-web-meta-html.js index 0ee566f..e49986c 100644 --- a/dist/functions/server/web-pages/grab-web-meta-html.js +++ b/dist/functions/server/web-pages/grab-web-meta-html.js @@ -1,60 +1,61 @@ +import { escape } from "lodash"; export default function grabWebMetaHTML({ meta }) { let html = ``; if (meta.title) { - html += ` ${meta.title}\n`; + html += ` ${escape(meta.title)}\n`; } if (meta.description) { - html += ` \n`; + html += ` \n`; } if (meta.keywords) { const keywords = Array.isArray(meta.keywords) ? meta.keywords.join(", ") : meta.keywords; - html += ` \n`; + html += ` \n`; } if (meta.author) { - html += ` \n`; + html += ` \n`; } if (meta.robots) { - html += ` \n`; + html += ` \n`; } if (meta.canonical) { - html += ` \n`; + html += ` \n`; } if (meta.themeColor) { - html += ` \n`; + html += ` \n`; } if (meta.og) { const { og } = meta; if (og.title) - html += ` \n`; + html += ` \n`; if (og.description) - html += ` \n`; + html += ` \n`; if (og.image) - html += ` \n`; + html += ` \n`; if (og.url) - html += ` \n`; + html += ` \n`; if (og.type) - html += ` \n`; + html += ` \n`; if (og.siteName) - html += ` \n`; + html += ` \n`; if (og.locale) - html += ` \n`; + html += ` \n`; } if (meta.twitter) { const { twitter } = meta; if (twitter.card) - html += ` \n`; + html += ` \n`; if (twitter.title) - html += ` \n`; + html += ` \n`; if (twitter.description) - html += ` \n`; + html += ` \n`; if (twitter.image) - html += ` \n`; + html += ` \n`; if (twitter.site) - html += ` \n`; + html += ` \n`; if (twitter.creator) - html += ` \n`; + html += ` \n`; } return html; } diff --git a/dist/utils/grab-all-pages.js b/dist/utils/grab-all-pages.js index 2425de1..cbbd83b 100644 --- a/dist/utils/grab-all-pages.js +++ b/dist/utils/grab-all-pages.js @@ -48,7 +48,13 @@ function grabPageDirRecursively({ page_dir }) { } } } - return pages_files; + return pages_files.sort((a, b) => { + if (a.url_path === "/index") + return -1; + if (b.url_path === "/index") + return 1; + return 0; + }); } function grabPageFileObject({ file_path, }) { let url_path = file_path diff --git a/dist/utils/grab-dir-names.d.ts b/dist/utils/grab-dir-names.d.ts index 09ac9df..86029c6 100644 --- a/dist/utils/grab-dir-names.d.ts +++ b/dist/utils/grab-dir-names.d.ts @@ -20,4 +20,5 @@ export default function grabDirNames(): { BUNEXT_CACHE_DIR: string; BUNX_CWD_MODULE_CACHE_DIR: string; BUNX_CWD_PAGES_REWRITE_DIR: string; + HYDRATION_DST_DIR_MAP_JSON_FILE_NAME: string; }; diff --git a/dist/utils/grab-dir-names.js b/dist/utils/grab-dir-names.js index 28cf1b6..3f02d0f 100644 --- a/dist/utils/grab-dir-names.js +++ b/dist/utils/grab-dir-names.js @@ -5,16 +5,17 @@ export default function grabDirNames() { const PAGES_DIR = path.join(SRC_DIR, "pages"); const API_DIR = path.join(PAGES_DIR, "api"); const PUBLIC_DIR = path.join(ROOT_DIR, "public"); - const BUNEXT_PUBLIC_DIR = path.join(PUBLIC_DIR, "__bunext"); - const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages"); - const BUNEXT_CACHE_DIR = path.join(BUNEXT_PUBLIC_DIR, "cache"); - const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join(HYDRATION_DST_DIR, "map.json"); const CONFIG_FILE = path.join(ROOT_DIR, "bunext.config.ts"); const BUNX_CWD_DIR = path.resolve(ROOT_DIR, ".bunext"); const BUNX_CWD_MODULE_CACHE_DIR = path.resolve(BUNX_CWD_DIR, "module-cache"); const BUNX_CWD_PAGES_REWRITE_DIR = path.resolve(BUNX_CWD_DIR, "pages"); const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp"); const BUNX_HYDRATION_SRC_DIR = path.resolve(BUNX_CWD_DIR, "client", "hydration-src"); + const BUNEXT_PUBLIC_DIR = path.join(BUNX_CWD_DIR, "public"); + const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages"); + const BUNEXT_CACHE_DIR = path.join(BUNEXT_PUBLIC_DIR, "cache"); + const HYDRATION_DST_DIR_MAP_JSON_FILE_NAME = "map.json"; + const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join(HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME); const BUNX_ROOT_DIR = path.resolve(__dirname, "../../"); const BUNX_ROOT_SRC_DIR = path.join(BUNX_ROOT_DIR, "src"); const BUNX_ROOT_PRESETS_DIR = path.join(BUNX_ROOT_SRC_DIR, "presets"); @@ -44,5 +45,6 @@ export default function grabDirNames() { BUNEXT_CACHE_DIR, BUNX_CWD_MODULE_CACHE_DIR, BUNX_CWD_PAGES_REWRITE_DIR, + HYDRATION_DST_DIR_MAP_JSON_FILE_NAME, }; } diff --git a/src/__tests__/e2e/e2e.test.ts b/src/__tests__/e2e/e2e.test.ts index c8f0969..ef90181 100644 --- a/src/__tests__/e2e/e2e.test.ts +++ b/src/__tests__/e2e/e2e.test.ts @@ -1,21 +1,60 @@ import { describe, expect, test, beforeAll, afterAll } from "bun:test"; import startServer from "../../../src/functions/server/start-server"; -import bunextInit from "../../../src/functions/bunext-init"; +import rewritePagesModule from "../../../src/utils/rewrite-pages-module"; +import pagePathTransform from "../../../src/utils/page-path-transform"; import path from "path"; import fs from "fs"; +// Fixture lives under test/ so the framework's directory guard allows it +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; describe("E2E Integration", () => { let server: any; - + beforeAll(async () => { - // Change to the fixture directory to simulate actual user repo - const fixtureDir = path.resolve(__dirname, "../__fixtures__/app"); + originalPort = process.env.PORT; + // Use port 0 so Bun.serve picks a random available port + process.env.PORT = "0"; + process.chdir(fixtureDir); - - // Mock grabAppPort to assign dynamically to avoid port conflicts + global.CONFIG = { development: true }; + global.HMR_CONTROLLERS = []; + global.BUNDLER_REBUILDS = 0; + global.PAGE_FILES = []; + + // Set up router pointing at the fixture's pages directory + global.ROUTER = new Bun.FileSystemRouter({ + style: "nextjs", + 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. + global.BUNDLER_CTX_MAP = { + [fixtureIndexPage]: { + path: ".bunext/public/pages/index.js", + hash: "index", + type: "text/javascript", + entrypoint: fixtureIndexPage, + local_path: fixtureIndexPage, + url_path: "/", + file_name: "index", + }, + }; }); afterAll(async () => { @@ -23,32 +62,35 @@ describe("E2E Integration", () => { server.stop(true); } process.chdir(originalCwd); - - // Ensure to remove the dummy generated .bunext folder - const dotBunext = path.resolve(__dirname, "../__fixtures__/app/.bunext"); + + // Restore PORT env variable + if (originalPort !== undefined) { + process.env.PORT = originalPort; + } else { + 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)) { fs.rmSync(dotBunext, { recursive: true, force: true }); } - const pubBunext = path.resolve(__dirname, "../__fixtures__/app/public/__bunext"); - if (fs.existsSync(pubBunext)) { - fs.rmSync(pubBunext, { recursive: true, force: true }); - } }); test("boots up the server and correctly routes to index.tsx page", async () => { - // Mock to randomize port - // Note: Bun test runs modules in isolation but startServer imports grab-app-port - // If we can't easily mock we can set PORT env - process.env.PORT = "0"; // Let Bun.serve pick port - - await bunextInit(); server = await startServer(); expect(server).toBeDefined(); - - // Fetch the index page + expect(server.port).toBeGreaterThan(0); + const response = await fetch(`http://localhost:${server.port}/`); expect(response.status).toBe(200); - + const html = await response.text(); expect(html).toContain("Hello E2E"); }); @@ -57,7 +99,7 @@ describe("E2E Integration", () => { const response = await fetch(`http://localhost:${server.port}/unknown-foo-bar123`); expect(response.status).toBe(404); const text = await response.text(); - // Assume default 404 preset component is rendered + // Default 404 component is rendered expect(text).toContain("404"); }); }); diff --git a/src/__tests__/functions/server/handle-hmr.test.ts b/src/__tests__/functions/server/handle-hmr.test.ts index a2ec590..9026b37 100644 --- a/src/__tests__/functions/server/handle-hmr.test.ts +++ b/src/__tests__/functions/server/handle-hmr.test.ts @@ -10,9 +10,9 @@ describe("handle-hmr", () => { } } as any; global.HMR_CONTROLLERS = []; - global.BUNDLER_CTX_MAP = [ - { local_path: "/test-file" } as any - ]; + global.BUNDLER_CTX_MAP = { + "/test-file": { local_path: "/test-file" } as any, + }; }); afterEach(() => { diff --git a/src/__tests__/functions/server/handle-routes.test.ts b/src/__tests__/functions/server/handle-routes.test.ts index c813c23..e76af50 100644 --- a/src/__tests__/functions/server/handle-routes.test.ts +++ b/src/__tests__/functions/server/handle-routes.test.ts @@ -40,11 +40,11 @@ describe("handle-routes", () => { mock.restore(); }); - test("returns 401 for unknown route", async () => { + test("returns 404 for unknown route", async () => { const req = new Request("http://localhost/api/unknown"); const res = await handleRoutes({ req }); - - expect(res.status).toBe(401); + + expect(res.status).toBe(404); const json = await res.json(); expect(json.success).toBe(false); expect(json.msg).toContain("not found"); diff --git a/src/__tests__/utils/grab-dir-names.test.ts b/src/__tests__/utils/grab-dir-names.test.ts index 2e18387..3e48e4d 100644 --- a/src/__tests__/utils/grab-dir-names.test.ts +++ b/src/__tests__/utils/grab-dir-names.test.ts @@ -14,17 +14,17 @@ describe("grabDirNames", () => { expect(dirs.PUBLIC_DIR).toBe(path.join(cwd, "public")); }); - it("nests HYDRATION_DST_DIR under public/__bunext/pages", () => { + it("nests HYDRATION_DST_DIR under .bunext/public/pages", () => { const dirs = grabDirNames(); expect(dirs.HYDRATION_DST_DIR).toBe( - path.join(dirs.PUBLIC_DIR, "__bunext", "pages"), + path.join(dirs.BUNX_CWD_DIR, "public", "pages"), ); }); - it("nests BUNEXT_CACHE_DIR under public/__bunext/cache", () => { + it("nests BUNEXT_CACHE_DIR under .bunext/public/cache", () => { const dirs = grabDirNames(); expect(dirs.BUNEXT_CACHE_DIR).toBe( - path.join(dirs.PUBLIC_DIR, "__bunext", "cache"), + path.join(dirs.BUNX_CWD_DIR, "public", "cache"), ); }); diff --git a/src/functions/bundler/all-pages-bun-bundler.ts b/src/functions/bundler/all-pages-bun-bundler.ts index 3078b18..3725b67 100644 --- a/src/functions/bundler/all-pages-bun-bundler.ts +++ b/src/functions/bundler/all-pages-bun-bundler.ts @@ -71,6 +71,7 @@ export default async function allPagesBunBundler(params?: Params) { chunk: "chunks/[hash].[ext]", }, plugins: [tailwindcss], + // plugins: [tailwindcss, BunSkipNonBrowserPlugin], splitting: true, target, metafile: true, diff --git a/src/functions/bundler/plugins/bun-skip-browser-plugin.ts b/src/functions/bundler/plugins/bun-skip-browser-plugin.ts new file mode 100644 index 0000000..23db2e7 --- /dev/null +++ b/src/functions/bundler/plugins/bun-skip-browser-plugin.ts @@ -0,0 +1,35 @@ +const BunSkipNonBrowserPlugin: Bun.BunPlugin = { + name: "skip-non-browser", + setup(build) { + build.onResolve({ filter: /^(bun:|node:)/ }, (args) => { + 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", + ]; + + 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 }; + } + }); + }, +}; + +export default BunSkipNonBrowserPlugin; diff --git a/src/functions/init.ts b/src/functions/init.ts index 6cb51f4..6a18c36 100644 --- a/src/functions/init.ts +++ b/src/functions/init.ts @@ -3,6 +3,7 @@ import grabDirNames from "../utils/grab-dir-names"; import path from "path"; import grabConfig from "./grab-config"; import type { BunextConfig } from "../types"; +import { log } from "../utils/log"; export default async function () { const dirNames = grabDirNames(); @@ -29,7 +30,13 @@ export default async function () { "react-dom", ); - if (dirNames.BUNX_ROOT_DIR !== dirNames.ROOT_DIR) { + if ( + dirNames.ROOT_DIR.startsWith(dirNames.BUNX_ROOT_DIR) && + !dirNames.ROOT_DIR.includes(`${dirNames.BUNX_ROOT_DIR}/test/`) + ) { + log.error(`Can't Run From this Directory => ${dirNames.ROOT_DIR}`); + process.exit(1); + } else { rmSync(react_package_dir, { recursive: true }); rmSync(react_dom_package_dir, { recursive: true }); }