From 8177df7dd3fe4695cd1944b51507f2baad6ea810 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Wed, 5 Nov 2025 07:12:15 +0100 Subject: [PATCH] First Commit --- .gitignore | 176 ++++++++++++++++++++++++ README.md | 23 ++++ bun.lock | 92 +++++++++++++ commands/build/index.ts | 13 ++ commands/dev/index.ts | 42 ++++++ commands/init/index.ts | 10 ++ commands/start/index.ts | 13 ++ functions/grab-config.ts | 22 +++ functions/router/get-route.ts | 47 +++++++ functions/server/grab-route-content.tsx | 31 +++++ functions/server/start-server.ts | 47 +++++++ index.ts | 54 ++++++++ package.json | 31 +++++ tsconfig.json | 27 ++++ types/index.ts | 59 ++++++++ utils/exit-with-error.ts | 4 + utils/grab-app-names.ts | 8 ++ utils/grab-app-port.ts | 16 +++ utils/grab-assets-prefix.ts | 11 ++ utils/grab-dir-names.ts | 37 +++++ utils/grab-origin.ts | 11 ++ utils/numberfy.ts | 33 +++++ 22 files changed, 807 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 commands/build/index.ts create mode 100644 commands/dev/index.ts create mode 100644 commands/init/index.ts create mode 100644 commands/start/index.ts create mode 100644 functions/grab-config.ts create mode 100644 functions/router/get-route.ts create mode 100644 functions/server/grab-route-content.tsx create mode 100644 functions/server/start-server.ts create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json create mode 100644 types/index.ts create mode 100644 utils/exit-with-error.ts create mode 100644 utils/grab-app-names.ts create mode 100644 utils/grab-app-port.ts create mode 100644 utils/grab-assets-prefix.ts create mode 100644 utils/grab-dir-names.ts create mode 100644 utils/grab-origin.ts create mode 100644 utils/numberfy.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84f3e9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +/test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..90b9436 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Bunext + +A Next JS replacement built with bun JS and docker + +## Running this application + +To run development: + +```bash +bun dev +``` + +To run production: + +```bash +bun start +``` + +## Requirements + +### Docker + +You need `docker` installed to run this project diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..e6a00ba --- /dev/null +++ b/bun.lock @@ -0,0 +1,92 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-next", + "dependencies": { + "commander": "^14.0.2", + "ora": "^9.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], + + "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="], + + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + } +} diff --git a/commands/build/index.ts b/commands/build/index.ts new file mode 100644 index 0000000..faf85e6 --- /dev/null +++ b/commands/build/index.ts @@ -0,0 +1,13 @@ +import { Command } from "commander"; +import grabConfig from "../../functions/grab-config"; + +export default function () { + return new Command("build") + .description("Build project") + .action(async () => { + console.log(`Building project ...`); + + const config = await grabConfig(); + global.CONFIG = config; + }); +} diff --git a/commands/dev/index.ts b/commands/dev/index.ts new file mode 100644 index 0000000..2228b96 --- /dev/null +++ b/commands/dev/index.ts @@ -0,0 +1,42 @@ +import { Command } from "commander"; +import grabConfig from "../../functions/grab-config"; +import grabDirNames from "../../utils/grab-dir-names"; +import AppNames from "../../utils/grab-app-names"; + +export default function () { + return new Command("dev") + .description("Run development server") + .action(async () => { + console.log(`Running development server ...`); + + const config = await grabConfig(); + global.CONFIG = config; + + const { entrypoint } = grabDirNames(); + const { defaultDistDir } = AppNames; + + let buildCmd = ["bun"]; + + buildCmd.push( + "build", + entrypoint, + "--outdir", + config.distDir || defaultDistDir, + "--watch" + ); + + const spawnedProcess = Bun.spawn({ + cmd: buildCmd, + }); + + const exitCode = await spawnedProcess.exited; + + // Bun.build({ + // entrypoints: [entrypoint], + // outdir: config.distDir || defaultDistDir, + // minify: true, + // }); + + // await startServer(); + }); +} diff --git a/commands/init/index.ts b/commands/init/index.ts new file mode 100644 index 0000000..430ae2a --- /dev/null +++ b/commands/init/index.ts @@ -0,0 +1,10 @@ +import { Command } from "commander"; +import AppNames from "../../utils/grab-app-names"; + +export default function () { + return new Command("init") + .description("Initialize project") + .action(async () => { + console.log(`Initializing ${AppNames.name} ...`); + }); +} diff --git a/commands/start/index.ts b/commands/start/index.ts new file mode 100644 index 0000000..dc36b6f --- /dev/null +++ b/commands/start/index.ts @@ -0,0 +1,13 @@ +import { Command } from "commander"; +import grabConfig from "../../functions/grab-config"; + +export default function () { + return new Command("start") + .description("Start production server") + .action(async () => { + console.log(`Starting production server ...`); + + const config = await grabConfig(); + global.CONFIG = config; + }); +} diff --git a/functions/grab-config.ts b/functions/grab-config.ts new file mode 100644 index 0000000..29943b9 --- /dev/null +++ b/functions/grab-config.ts @@ -0,0 +1,22 @@ +import { existsSync } from "fs"; +import type { BunextConfig } from "../types"; +import grabDirNames from "../utils/grab-dir-names"; +import exitWithError from "../utils/exit-with-error"; + +export default async function grabConfig(): Promise { + const { configFile } = grabDirNames(); + + if (!existsSync(configFile)) { + exitWithError(`Config file \`${configFile}\` doesn't exist!`); + } + + const config = (await import(configFile)).default as BunextConfig; + + if (!config) { + exitWithError( + `Config file \`${configFile}\` is invalid! Please provide a valid default export in your config file.` + ); + } + + return config; +} diff --git a/functions/router/get-route.ts b/functions/router/get-route.ts new file mode 100644 index 0000000..3510369 --- /dev/null +++ b/functions/router/get-route.ts @@ -0,0 +1,47 @@ +import grabDirNames from "../../utils/grab-dir-names"; +import type { GetRouteReturn } from "../../types"; +import grabAssetsPrefix from "../../utils/grab-assets-prefix"; +import grabOrigin from "../../utils/grab-origin"; + +type Params = { + route: string; +}; + +export default async function getRoute({ + route, +}: Params): Promise { + const { pagesDir } = grabDirNames(); + + if (route.match(/\(/)) { + return null; + } + + const assetPrefix = grabAssetsPrefix(); + const origin = grabOrigin(); + + const router = new Bun.FileSystemRouter({ + style: "nextjs", + dir: pagesDir, + origin, + assetPrefix, + }); + + const match = router.match(route); + + if (!match?.filePath) { + console.error(`Route ${route} not found`); + return null; + } + + const module = await import(match.filePath); + + return { + match, + module, + component: module.default, + serverProps: module.serverProps, + staticProps: module.staticProps, + staticPaths: module.staticPaths, + staticParams: module.staticParams, + }; +} diff --git a/functions/server/grab-route-content.tsx b/functions/server/grab-route-content.tsx new file mode 100644 index 0000000..fbda103 --- /dev/null +++ b/functions/server/grab-route-content.tsx @@ -0,0 +1,31 @@ +import type { GetRouteReturn } from "../../types"; +import AppNames from "../../utils/grab-app-names"; +import ReactDOMServer from "react-dom/server"; + +type Params = { + url: URL; + route: GetRouteReturn; + req: Request; +}; + +export default async function grabRouteContent({ + url, + route, + req, +}: Params): Promise { + const config = global.CONFIG; + const { name } = AppNames; + + let html = `Welcome to ${name} ...`; + + if (route.component) { + html = ReactDOMServer.renderToString(); + return new Response(html, { + headers: { + "Content-Type": "text/html; charset=utf-8", + }, + }); + } + + return new Response(html); +} diff --git a/functions/server/start-server.ts b/functions/server/start-server.ts new file mode 100644 index 0000000..0a1eefb --- /dev/null +++ b/functions/server/start-server.ts @@ -0,0 +1,47 @@ +import AppNames from "../../utils/grab-app-names"; +import grabAppPort from "../../utils/grab-app-port"; +import getRoute from "../router/get-route"; +import grabRouteContent from "./grab-route-content"; + +export default async function startServer() { + const config = global.CONFIG; + const port = grabAppPort(); + const { name } = AppNames; + + const server = Bun.serve({ + async fetch(req) { + try { + const url = new URL(req.url); + + const route = await getRoute({ route: url.pathname }); + + if (!route) { + return new Response(`Route ${url.pathname} not Found!`, { + status: 404, + }); + } + + const response = await grabRouteContent({ req, route, url }); + + if (response) { + return response; + } + + return new Response(`No Response!`, { + status: 404, + }); + } catch (error: any) { + return new Response(`Server Error: ${error.message}`, { + status: 500, + }); + } + }, + port, + development: true, + }); + + global.SERVER = server; + console.log(`${name} Server Running on Port ${server.port} ...`); + + return server; +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..9e5e2c4 --- /dev/null +++ b/index.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { program } from "commander"; +import start from "./commands/start"; +import dev from "./commands/dev"; +import build from "./commands/build"; +import init from "./commands/init"; +import ora, { type Ora } from "ora"; +import type { BunextConfig } from "./types"; +import type { Server } from "bun"; + +/** + * # Declare Global Variables + */ +declare global { + var ORA_SPINNER: Ora; + var CONFIG: BunextConfig; + var SERVER: Server | undefined; +} + +global.ORA_SPINNER = ora(); +global.ORA_SPINNER.clear(); + +/** + * # Describe Program + */ +program + .name(`bunext`) + .description(`A React Next JS replacement built with bun JS`) + .version(`1.0.0`); + +/** + * # Declare Commands + */ +program.addCommand(start()); +program.addCommand(dev()); +program.addCommand(build()); +program.addCommand(init()); + +/** + * # Handle Unavailable Commands + */ +program.on("command:*", () => { + console.error( + "Invalid command: %s\nSee --help for a list of available commands.", + program.args.join(" ") + ); + process.exit(1); +}); + +/** + * # Parse Arguments + */ +program.parse(Bun.argv); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3c33d58 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "bunext", + "module": "index.ts", + "type": "module", + "bin": { + "bunext": "index.ts" + }, + "files": [ + "index.ts" + ], + "scripts": { + "dev": "cd envs/development && docker compose down && docker compose up --build", + "start": "cd envs/production && docker compose down && docker compose up -d --build", + "preview": "cd envs/preview && docker compose down && docker compose up -d --build" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "commander": "^14.0.2", + "ora": "^9.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..d46ce4b --- /dev/null +++ b/types/index.ts @@ -0,0 +1,59 @@ +import type { MatchedRoute } from "bun"; + +export type ServerProps = { + params: Record; + searchParams: Record; + headers: Headers; + cookies: Record; + body: any; + method: string; + url: string; + pathname: string; + query: Record; + search: string; + hash: string; +}; + +export type StaticProps = { + params: Record; + searchParams: Record; + headers: Headers; + cookies: Record; + body: any; + method: string; + url: string; + pathname: string; + query: Record; + search: string; + hash: string; +}; + +export type StaticPaths = string[]; + +export type StaticParams = Record; + +export type PageModule = { + component: React.ComponentType; + serverProps: ServerProps; + staticProps: StaticProps; + staticPaths: StaticPaths; + staticParams: StaticParams; +}; + +export type BunextConfig = { + distDir?: string; + assetsPrefix?: string; + origin?: string; + globalVars?: { [k: string]: any }; + port?: number; +}; + +export type GetRouteReturn = { + match: MatchedRoute; + module: PageModule; + component: React.ComponentType; + serverProps: ServerProps; + staticProps: StaticProps; + staticPaths: StaticPaths; + staticParams: StaticParams; +}; diff --git a/utils/exit-with-error.ts b/utils/exit-with-error.ts new file mode 100644 index 0000000..b7ac9d9 --- /dev/null +++ b/utils/exit-with-error.ts @@ -0,0 +1,4 @@ +export default function exitWithError(msg: string, code?: number) { + console.error(msg); + process.exit(code || 1); +} diff --git a/utils/grab-app-names.ts b/utils/grab-app-names.ts new file mode 100644 index 0000000..2751472 --- /dev/null +++ b/utils/grab-app-names.ts @@ -0,0 +1,8 @@ +const AppNames = { + defaultPort: 7000, + defaultAssetPrefix: "_bunext/static", + name: "Bunext", + defaultDistDir: ".bunext", +} as const; + +export default AppNames; diff --git a/utils/grab-app-port.ts b/utils/grab-app-port.ts new file mode 100644 index 0000000..7f50510 --- /dev/null +++ b/utils/grab-app-port.ts @@ -0,0 +1,16 @@ +import AppNames from "./grab-app-names"; +import numberfy from "./numberfy"; + +export default function grabAppPort() { + if (process.env.PORT) { + return numberfy(process.env.PORT); + } + + if (global.CONFIG.port) { + return global.CONFIG.port; + } + + const { defaultPort } = AppNames; + + return numberfy(defaultPort); +} diff --git a/utils/grab-assets-prefix.ts b/utils/grab-assets-prefix.ts new file mode 100644 index 0000000..89ddc06 --- /dev/null +++ b/utils/grab-assets-prefix.ts @@ -0,0 +1,11 @@ +import AppNames from "./grab-app-names"; + +export default function grabAssetsPrefix() { + if (global.CONFIG.assetsPrefix) { + return global.CONFIG.assetsPrefix; + } + + const { defaultAssetPrefix } = AppNames; + + return defaultAssetPrefix; +} diff --git a/utils/grab-dir-names.ts b/utils/grab-dir-names.ts new file mode 100644 index 0000000..c61ec9f --- /dev/null +++ b/utils/grab-dir-names.ts @@ -0,0 +1,37 @@ +import path from "path"; + +type Params = { + dir?: string; +}; + +export default function grabDirNames(params?: Params) { + const rootDir = params?.dir || process.cwd(); + const appDir = path.resolve(__dirname, ".."); + const entrypoint = path.join( + appDir, + "functions", + "server", + "start-server.ts" + ); + + const pagesDir = path.join(rootDir, "pages"); + const componentsDir = path.join(rootDir, "components"); + const publicDir = path.join(rootDir, "public"); + const stylesDir = path.join(rootDir, "styles"); + const utilsDir = path.join(rootDir, "utils"); + const typesDir = path.join(rootDir, "types"); + const configFile = path.join(rootDir, "bunext.config.ts"); + + return { + rootDir, + pagesDir, + componentsDir, + publicDir, + stylesDir, + utilsDir, + typesDir, + configFile, + appDir, + entrypoint, + }; +} diff --git a/utils/grab-origin.ts b/utils/grab-origin.ts new file mode 100644 index 0000000..5626f4a --- /dev/null +++ b/utils/grab-origin.ts @@ -0,0 +1,11 @@ +import grabAppPort from "./grab-app-port"; + +export default function grabOrigin() { + if (global.CONFIG.origin) { + return global.CONFIG.origin; + } + + const port = grabAppPort(); + + return `http://localhost:${port}`; +} diff --git a/utils/numberfy.ts b/utils/numberfy.ts new file mode 100644 index 0000000..90aac17 --- /dev/null +++ b/utils/numberfy.ts @@ -0,0 +1,33 @@ +export default function numberfy(num: any, decimals?: number): number { + try { + const numberString = String(num) + .replace(/[^0-9\.]/g, "") + .replace(/\.$/, ""); + + if (!numberString.match(/./)) return 0; + + const existingDecimals = numberString.match(/\./) + ? numberString.split(".").pop()?.length + : undefined; + + const numberfiedNum = Number(numberString); + + if (typeof numberfiedNum !== "number") return 0; + if (isNaN(numberfiedNum)) return 0; + + if (decimals == 0) { + return Math.round(Number(numberfiedNum)); + } else if (decimals) { + return Number(numberfiedNum.toFixed(decimals)); + } + + if (existingDecimals) + return Number(numberfiedNum.toFixed(existingDecimals)); + return Math.round(numberfiedNum); + } catch (error: any) { + console.log(`Numberfy ERROR: ${error.message}`); + return 0; + } +} + +export const _n = numberfy;