mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: ditch hono
This commit is contained in:
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
// Globals replaced at build time by Vite (`define` in `vite.config.ts`).
|
||||
|
||||
declare const __PREFIX__: string;
|
||||
declare const __VERSION__: string;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { type ActionFunctionArgs, redirect } from "react-router";
|
||||
|
||||
import type { LoadContext } from "~/server";
|
||||
import type { AppContext } from "~/server/context";
|
||||
|
||||
export async function loader() {
|
||||
return redirect("/machines");
|
||||
}
|
||||
|
||||
export async function action({ request, context }: ActionFunctionArgs<LoadContext>) {
|
||||
export async function action({ request, context }: ActionFunctionArgs<AppContext>) {
|
||||
let principal: Awaited<ReturnType<typeof context.auth.require>> | undefined;
|
||||
try {
|
||||
principal = await context.auth.require(request);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLoaderData } from "react-router";
|
||||
import Code from "~/components/code";
|
||||
import Notice from "~/components/notice";
|
||||
import PageError from "~/components/page-error";
|
||||
import type { LoadContext } from "~/server";
|
||||
import type { AppContext } from "~/server/context";
|
||||
import { Capabilities } from "~/server/web/roles";
|
||||
|
||||
import ManageDomains from "./components/manage-domains";
|
||||
@@ -15,7 +15,7 @@ import ToggleMagic from "./components/toggle-magic";
|
||||
import { dnsAction } from "./dns-actions";
|
||||
|
||||
// We do not want to expose every config value
|
||||
export async function loader({ request, context }: LoaderFunctionArgs<LoadContext>) {
|
||||
export async function loader({ request, context }: LoaderFunctionArgs<AppContext>) {
|
||||
if (!context.hs.readable()) {
|
||||
throw new Error("No configuration is available");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# `app/server/`
|
||||
|
||||
Server-side application code for Headplane. Everything in this directory
|
||||
runs only on the Node process — never in the browser.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
app/server/
|
||||
├── app.ts ← The Headplane application (load context, RR listener)
|
||||
├── main.ts ← Production bootstrap (binds an http(s) server)
|
||||
├── context.ts ← createAppContext() — assembles the AppLoadContext
|
||||
├── result.ts ← Result<T, E> helper used across the server modules
|
||||
│
|
||||
├── config/ ← YAML config loading, schema, env-overrides, integrations
|
||||
├── db/ ← Drizzle SQLite client + schema
|
||||
├── headscale/ ← Headscale REST API client + headscale-config loader
|
||||
├── oidc/ ← OIDC provider abstraction
|
||||
├── web/ ← Authentication service, identity, RBAC capabilities
|
||||
└── hp-agent.ts ← Headplane agent process manager
|
||||
```
|
||||
|
||||
## Entry points
|
||||
|
||||
There are two SSR entries; both are picked up by Vite via `vite.config.ts`.
|
||||
|
||||
### `app.ts` — the application module
|
||||
|
||||
Loads config → builds the `AppLoadContext` (via [`context.ts`](./context.ts))
|
||||
→ exports the React Router `RequestListener` as `default`, plus the
|
||||
resolved `config` as a named export.
|
||||
|
||||
This module has no opinions about how the server is hosted. It does not
|
||||
listen on a socket, doesn't compose static-asset serving, and doesn't
|
||||
handle the basename redirect — that's the runtime's job (see
|
||||
[`runtime/`](../../runtime/)).
|
||||
|
||||
Consumed by:
|
||||
|
||||
- [`main.ts`](./main.ts) in production builds
|
||||
- [`runtime/vite-plugin.ts`](../../runtime/vite-plugin.ts) in `react-router dev`
|
||||
|
||||
### `main.ts` — the production bootstrap
|
||||
|
||||
The SSR build input. Rollup bundles this file into
|
||||
`build/server/index.js`. It:
|
||||
|
||||
1. imports the listener + config from [`app.ts`](./app.ts)
|
||||
2. wraps the listener with `composeListener` from
|
||||
[`runtime/http.ts`](../../runtime/http.ts) — adds `/admin → /admin/`
|
||||
redirect and serves `build/client/` as static assets with immutable
|
||||
caching for the `assets/` subdirectory
|
||||
3. binds an http(s) server with `startHttpServer`
|
||||
|
||||
Run with `node /app/build/server/index.js` (this is what the Dockerfile
|
||||
does). TLS is a one-line addition: pass `tls: { key, cert }` to
|
||||
`startHttpServer`.
|
||||
|
||||
## Application context
|
||||
|
||||
[`context.ts`](./context.ts) exposes `createAppContext(config)`, which
|
||||
constructs everything that needs to live for the lifetime of the
|
||||
process:
|
||||
|
||||
- the SQLite client (`db`)
|
||||
- the Headscale REST interface (`hsApi`)
|
||||
- the optional Headplane agent manager (`agents`)
|
||||
- the auth service (`auth`)
|
||||
- the optional OIDC service (`oidc`)
|
||||
- the live store (`hsLive`)
|
||||
- the (best-effort) parsed Headscale config (`hs`)
|
||||
- the integration adapter (`integration`)
|
||||
|
||||
The returned object is the `AppLoadContext` exposed to every React
|
||||
Router loader/action. The module also `declare module "react-router" { interface AppLoadContext extends AppContext {} }`
|
||||
so route handlers get full type inference on `context`.
|
||||
|
||||
When a route needs the type, import it from `~/server/context`:
|
||||
|
||||
```ts
|
||||
import type { AppContext } from "~/server/context";
|
||||
|
||||
export async function loader({ context }: LoaderFunctionArgs<AppContext>) {
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
## Dev vs. prod
|
||||
|
||||
| Concern | Dev (`react-router dev`) | Prod (`node build/server/index.js`) |
|
||||
| ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| HTTP server | Vite owns it (`vite.config.ts` `server.host/port`) | `runtime/http.ts` `startHttpServer` |
|
||||
| Static assets | `./public` (served by `runtime/vite-plugin.ts`) | `build/client/` (served by `runtime/http.ts` static handler) |
|
||||
| Basename redirect (`/admin` → `/admin/`) | `runtime/vite-plugin.ts` via `composeListener` | `main.ts` via `composeListener` |
|
||||
| App load (HMR) | `ssrLoadModule(app.ts)` per request | bundled into `build/server/index.js` |
|
||||
| Entry point | [`app.ts`](./app.ts) | [`main.ts`](./main.ts) |
|
||||
|
||||
There is **no** `if (import.meta.env.PROD)` branch in [`app.ts`](./app.ts)
|
||||
or [`main.ts`](./main.ts) — the dev/prod split is expressed by which
|
||||
file is loaded, not by runtime conditionals.
|
||||
|
||||
## Adding a new server-side module
|
||||
|
||||
1. Create the module under an existing subdirectory (or add a new one
|
||||
that names a coherent concern, e.g. `metrics/`, `ratelimit/`).
|
||||
2. If it owns process-lifetime state (a connection pool, a service
|
||||
client, …), construct it in [`context.ts`](./context.ts) and add it
|
||||
to the returned object — this gives every route automatic access via
|
||||
`context.<name>`.
|
||||
3. If it's purely a helper (pure functions, type definitions), import
|
||||
it directly from the module that needs it.
|
||||
@@ -0,0 +1,50 @@
|
||||
// MARK: Headplane Application
|
||||
//
|
||||
// Loads configuration, builds the per-process app context, and exports
|
||||
// the React Router request listener as the default export.
|
||||
//
|
||||
// This module is consumed in two places:
|
||||
// - `app/server/main.ts` — the production bootstrap; wraps the
|
||||
// listener with static-asset serving and binds an http(s) server.
|
||||
// - `runtime/vite-plugin.ts` — the dev-mode Vite middleware; loads
|
||||
// this module through `ssrLoadModule` and dispatches each request.
|
||||
|
||||
import { exit, versions } from "node:process";
|
||||
|
||||
import { createRequestListener } from "@react-router/node";
|
||||
import * as build from "virtual:react-router/server-build";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import type { HeadplaneConfig } from "./config/config-schema";
|
||||
import { ConfigError } from "./config/error";
|
||||
import { loadConfig } from "./config/load";
|
||||
import { createAppContext } from "./context";
|
||||
|
||||
log.info("server", "Running Node.js %s", versions.node);
|
||||
|
||||
let config: HeadplaneConfig;
|
||||
try {
|
||||
config = await loadConfig();
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigError) {
|
||||
log.error("server", "Unable to load configuration: %s", error.message);
|
||||
} else {
|
||||
log.error("server", "Failed to load configuration: %s", error);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const ctx = await createAppContext(config);
|
||||
ctx.auth.start();
|
||||
|
||||
export { config };
|
||||
|
||||
// TODO: `getLoadContext` is the right place to handle reverse proxy
|
||||
// translation — better than doing it in the OIDC client because it
|
||||
// applies to all requests, not just OIDC ones.
|
||||
export default createRequestListener({
|
||||
build,
|
||||
mode: import.meta.env.MODE,
|
||||
getLoadContext: () => ctx,
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import type { HeadplaneConfig } from "./config/config-schema";
|
||||
import { loadIntegration } from "./config/integration";
|
||||
import { createDbClient } from "./db/client.server";
|
||||
import { createHeadscaleInterface } from "./headscale/api";
|
||||
import { loadHeadscaleConfig } from "./headscale/config-loader";
|
||||
import { createLiveStore, nodesResource, usersResource } from "./headscale/live-store";
|
||||
import { createAgentManager } from "./hp-agent";
|
||||
import { createOidcService } from "./oidc/provider";
|
||||
import { createAuthService } from "./web/auth";
|
||||
|
||||
export type AppContext = Awaited<ReturnType<typeof createAppContext>>;
|
||||
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends AppContext {}
|
||||
}
|
||||
|
||||
export async function createAppContext(config: HeadplaneConfig) {
|
||||
const db = await createDbClient(join(config.server.data_path, "hp_persist.db"));
|
||||
const hsApi = await createHeadscaleInterface(
|
||||
config.headscale.url,
|
||||
config.headscale.tls_cert_path,
|
||||
);
|
||||
|
||||
// Resolve the Headscale API key: headscale.api_key takes precedence,
|
||||
// falling back to the deprecated oidc.headscale_api_key for compatibility.
|
||||
const headscaleApiKey = config.headscale.api_key ?? config.oidc?.headscale_api_key;
|
||||
|
||||
let agents;
|
||||
if (headscaleApiKey) {
|
||||
agents = await createAgentManager(
|
||||
config.integration?.agent,
|
||||
config.headscale.url,
|
||||
hsApi.getRuntimeClient(headscaleApiKey),
|
||||
hsApi.clientHelpers.isAtleast("0.28.0"),
|
||||
db,
|
||||
);
|
||||
} else if (config.integration?.agent?.enabled) {
|
||||
log.warn("agent", "Agent is enabled but no headscale.api_key is configured");
|
||||
}
|
||||
|
||||
const auth = createAuthService({
|
||||
secret: config.server.cookie_secret,
|
||||
headscaleApiKey,
|
||||
db,
|
||||
cookie: {
|
||||
name: "_hp_auth",
|
||||
secure: config.server.cookie_secure,
|
||||
maxAge: config.server.cookie_max_age,
|
||||
domain: config.server.cookie_domain,
|
||||
},
|
||||
});
|
||||
|
||||
const oidc =
|
||||
config.oidc && config.oidc.enabled !== false && headscaleApiKey
|
||||
? {
|
||||
service: createOidcService({
|
||||
issuer: config.oidc.issuer,
|
||||
clientId: config.oidc.client_id,
|
||||
clientSecret: config.oidc.client_secret,
|
||||
baseUrl: config.server.base_url ?? "",
|
||||
authorizationEndpoint: config.oidc.authorization_endpoint,
|
||||
tokenEndpoint: config.oidc.token_endpoint,
|
||||
userinfoEndpoint: config.oidc.userinfo_endpoint,
|
||||
endSessionEndpoint: config.oidc.end_session_endpoint,
|
||||
tokenEndpointAuthMethod:
|
||||
config.oidc.token_endpoint_auth_method === "client_secret_jwt"
|
||||
? undefined
|
||||
: config.oidc.token_endpoint_auth_method,
|
||||
usePkce: config.oidc.use_pkce,
|
||||
scope: config.oidc.scope,
|
||||
subjectClaims: config.oidc.subject_claims,
|
||||
allowWeakRsaKeys: config.oidc.allow_weak_rsa_keys,
|
||||
extraParams: config.oidc.extra_params,
|
||||
profilePictureSource: config.oidc.profile_picture_source,
|
||||
postLogoutRedirectUri: config.oidc.post_logout_redirect_uri,
|
||||
}),
|
||||
disableApiKeyLogin: config.oidc.disable_api_key_login,
|
||||
useEndSession: config.oidc.use_end_session,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
config,
|
||||
db,
|
||||
hsApi,
|
||||
headscaleApiKey,
|
||||
agents,
|
||||
auth,
|
||||
oidc,
|
||||
hsLive: createLiveStore([nodesResource, usersResource]),
|
||||
hs: await loadHeadscaleConfig(
|
||||
config.headscale.config_path,
|
||||
config.headscale.config_strict,
|
||||
config.headscale.dns_records_path,
|
||||
),
|
||||
integration: await loadIntegration(config.integration),
|
||||
};
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
import { exit, versions } from "node:process";
|
||||
|
||||
import { createHonoServer } from "react-router-hono-server/node";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import { loadIntegration } from "./config/integration";
|
||||
import { loadConfig } from "./config/load";
|
||||
import { createDbClient } from "./db/client.server";
|
||||
import { createHeadscaleInterface } from "./headscale/api";
|
||||
import { loadHeadscaleConfig } from "./headscale/config-loader";
|
||||
import { createLiveStore, nodesResource, usersResource } from "./headscale/live-store";
|
||||
import { createAgentManager } from "./hp-agent";
|
||||
import { createAuthService } from "./web/auth";
|
||||
|
||||
declare global {
|
||||
const __PREFIX__: string;
|
||||
const __VERSION__: string;
|
||||
}
|
||||
|
||||
// MARK: Side-Effects
|
||||
// This module contains a side-effect because everything running here
|
||||
// exists for the lifetime of the process, making it appropriate.
|
||||
log.info("server", "Running Node.js %s", versions.node);
|
||||
let config: HeadplaneConfig;
|
||||
|
||||
try {
|
||||
config = await loadConfig();
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigError) {
|
||||
log.error("server", "Unable to load configuration: %s", error.message);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const db = await createDbClient(join(config.server.data_path, "hp_persist.db"));
|
||||
const hsApi = await createHeadscaleInterface(config.headscale.url, config.headscale.tls_cert_path);
|
||||
|
||||
// Resolve the Headscale API key: headscale.api_key takes precedence,
|
||||
// falling back to the deprecated oidc.headscale_api_key for compatibility.
|
||||
const headscaleApiKey = config.headscale.api_key ?? config.oidc?.headscale_api_key;
|
||||
|
||||
const agents = headscaleApiKey
|
||||
? await createAgentManager(
|
||||
config.integration?.agent,
|
||||
config.headscale.url,
|
||||
hsApi.getRuntimeClient(headscaleApiKey),
|
||||
hsApi.clientHelpers.isAtleast("0.28.0"),
|
||||
db,
|
||||
)
|
||||
: (() => {
|
||||
if (config.integration?.agent?.enabled) {
|
||||
log.warn("agent", "Agent is enabled but no headscale.api_key is configured");
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
// We also use this file to load anything needed by the react router code.
|
||||
// These are usually per-request things that we need access to, like the
|
||||
// helper that can issue and revoke cookies.
|
||||
export type LoadContext = typeof appLoadContext;
|
||||
|
||||
import "react-router";
|
||||
import { HeadplaneConfig } from "./config/config-schema";
|
||||
import { ConfigError } from "./config/error";
|
||||
import { createOidcService } from "./oidc/provider";
|
||||
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
}
|
||||
|
||||
const hsLive = createLiveStore([nodesResource, usersResource]);
|
||||
|
||||
const appLoadContext = {
|
||||
config,
|
||||
hsLive,
|
||||
hs: await loadHeadscaleConfig(
|
||||
config.headscale.config_path,
|
||||
config.headscale.config_strict,
|
||||
config.headscale.dns_records_path,
|
||||
),
|
||||
|
||||
auth: createAuthService({
|
||||
secret: config.server.cookie_secret,
|
||||
headscaleApiKey,
|
||||
db,
|
||||
cookie: {
|
||||
name: "_hp_auth",
|
||||
secure: config.server.cookie_secure,
|
||||
maxAge: config.server.cookie_max_age,
|
||||
domain: config.server.cookie_domain,
|
||||
},
|
||||
}),
|
||||
|
||||
headscaleApiKey,
|
||||
hsApi,
|
||||
agents,
|
||||
integration: await loadIntegration(config.integration),
|
||||
oidc:
|
||||
config.oidc && config.oidc.enabled !== false && headscaleApiKey
|
||||
? {
|
||||
service: createOidcService({
|
||||
issuer: config.oidc.issuer,
|
||||
clientId: config.oidc.client_id,
|
||||
clientSecret: config.oidc.client_secret,
|
||||
baseUrl: config.server.base_url ?? "",
|
||||
authorizationEndpoint: config.oidc.authorization_endpoint,
|
||||
tokenEndpoint: config.oidc.token_endpoint,
|
||||
userinfoEndpoint: config.oidc.userinfo_endpoint,
|
||||
endSessionEndpoint: config.oidc.end_session_endpoint,
|
||||
tokenEndpointAuthMethod:
|
||||
config.oidc.token_endpoint_auth_method === "client_secret_jwt"
|
||||
? undefined
|
||||
: config.oidc.token_endpoint_auth_method,
|
||||
usePkce: config.oidc.use_pkce,
|
||||
scope: config.oidc.scope,
|
||||
subjectClaims: config.oidc.subject_claims,
|
||||
allowWeakRsaKeys: config.oidc.allow_weak_rsa_keys,
|
||||
extraParams: config.oidc.extra_params,
|
||||
profilePictureSource: config.oidc.profile_picture_source,
|
||||
postLogoutRedirectUri: config.oidc.post_logout_redirect_uri,
|
||||
}),
|
||||
disableApiKeyLogin: config.oidc.disable_api_key_login,
|
||||
useEndSession: config.oidc.use_end_session,
|
||||
}
|
||||
: undefined,
|
||||
db,
|
||||
};
|
||||
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
}
|
||||
|
||||
export default createHonoServer({
|
||||
overrideGlobalObjects: true,
|
||||
port: config.server.port,
|
||||
hostname: config.server.host,
|
||||
beforeAll: async (app) => {
|
||||
app.use(__PREFIX__, async (c) => {
|
||||
return c.redirect(`${__PREFIX__}/`);
|
||||
});
|
||||
},
|
||||
serveStaticOptions: {
|
||||
publicAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ""),
|
||||
},
|
||||
clientAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ""),
|
||||
},
|
||||
},
|
||||
|
||||
// Only log in development mode
|
||||
defaultLogger: import.meta.env.DEV,
|
||||
getLoadContext() {
|
||||
// TODO: This is the place where we can handle reverse proxy translation
|
||||
// This is better than doing it in the OIDC client, since we can do it
|
||||
// for all requests, not just OIDC ones.
|
||||
return appLoadContext;
|
||||
},
|
||||
|
||||
listeningListener(info) {
|
||||
log.info("server", "Running on %s:%s", info.address, info.port);
|
||||
},
|
||||
});
|
||||
|
||||
appLoadContext.auth.start();
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
log.info("server", "Received SIGINT, shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
log.info("server", "Received SIGTERM, shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// MARK: Production Bootstrap
|
||||
//
|
||||
// The production SSR build entry. Imports the React Router request
|
||||
// listener from `./app`, wraps it with static-asset serving (out of
|
||||
// `build/client`) and basename redirect, then binds an http(s) server.
|
||||
//
|
||||
// This file is NOT loaded in dev — `react-router dev` boots through
|
||||
// Vite, and the dev-only `runtime/vite-plugin.ts` dispatches requests
|
||||
// straight to `./app`'s default export.
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { composeListener, startHttpServer } from "../../runtime/http";
|
||||
import requestListener, { config } from "./app";
|
||||
|
||||
// `import.meta.url` resolves to `build/server/index.js`; the built
|
||||
// client lives next to it at `build/client/`.
|
||||
const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "../client");
|
||||
|
||||
startHttpServer({
|
||||
host: config.server.host,
|
||||
port: config.server.port,
|
||||
listener: composeListener({
|
||||
basename: __PREFIX__,
|
||||
staticRoot: clientDir,
|
||||
immutableAssets: true,
|
||||
requestListener,
|
||||
}),
|
||||
});
|
||||
@@ -49,7 +49,6 @@
|
||||
"react-error-boundary": "^6.1.1",
|
||||
"react-router": "^7.14.0",
|
||||
"react-router-dom": "^7.14.0",
|
||||
"react-router-hono-server": "2.25.2",
|
||||
"restty": "^0.1.35",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"ulidx": "2.4.1",
|
||||
@@ -105,9 +104,6 @@
|
||||
"ssh2",
|
||||
"utf-8-validate"
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"react-router-hono-server": "patches/react-router-hono-server.patch"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"better-sqlite3"
|
||||
],
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
diff --git a/dist/adapters/node.js b/dist/adapters/node.js
|
||||
index e081ab36a04ea100cf5da3fb5eb54c5174186f4d..35433810d05a829f2c824d63714e71bf5ed069a0 100644
|
||||
--- a/dist/adapters/node.js
|
||||
+++ b/dist/adapters/node.js
|
||||
@@ -49,13 +49,13 @@ async function createHonoServer(options) {
|
||||
}
|
||||
await mergedOptions.beforeAll?.(app);
|
||||
app.use(
|
||||
- `/${import.meta.env.REACT_ROUTER_HONO_SERVER_ASSETS_DIR}/*`,
|
||||
+ `${import.meta.env.REACT_ROUTER_HONO_SERVER_BASENAME}${import.meta.env.REACT_ROUTER_HONO_SERVER_ASSETS_DIR}/*`,
|
||||
cache(60 * 60 * 24 * 365),
|
||||
// 1 year
|
||||
serveStatic({ root: clientBuildPath, ...mergedOptions.serveStaticOptions?.clientAssets })
|
||||
);
|
||||
app.use(
|
||||
- "*",
|
||||
+ `${import.meta.env.REACT_ROUTER_HONO_SERVER_BASENAME}*`,
|
||||
cache(60 * 60),
|
||||
// 1 hour
|
||||
serveStatic({ root: PRODUCTION ? clientBuildPath : "./public", ...mergedOptions.serveStaticOptions?.publicAssets })
|
||||
Generated
+1
-96
@@ -7,11 +7,6 @@ settings:
|
||||
overrides:
|
||||
'@codemirror/state': 6.6.0
|
||||
|
||||
patchedDependencies:
|
||||
react-router-hono-server:
|
||||
hash: e94667f181837f2501959f5cf01fa0a9e6ea2883dcb1137be8b1c6178979bc77
|
||||
path: patches/react-router-hono-server.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -103,9 +98,6 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^7.14.0
|
||||
version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
react-router-hono-server:
|
||||
specifier: 2.25.2
|
||||
version: 2.25.2(patch_hash=e94667f181837f2501959f5cf01fa0a9e6ea2883dcb1137be8b1c6178979bc77)(@hono/node-server@1.19.13(hono@4.11.5))(@react-router/dev@7.14.0(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(@types/react@19.2.14)(bufferutil@4.0.9)(hono@4.11.5)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||
restty:
|
||||
specifier: ^0.1.35
|
||||
version: 0.1.35(typescript@6.0.2)
|
||||
@@ -1006,32 +998,6 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
'@hono/node-server@1.19.13':
|
||||
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@hono/node-ws@1.3.0':
|
||||
resolution: {integrity: sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
'@hono/node-server': ^1.19.2
|
||||
hono: ^4.6.0
|
||||
|
||||
'@hono/vite-dev-server@0.25.1':
|
||||
resolution: {integrity: sha512-H6EqoPtCV+uH1fu8nKyRy5Wh3LU012QxloEYMlUcyMjjEfVFiRXPjHx/0TQiiRi140Vu8San5A4XQWyElKFKaw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: '*'
|
||||
miniflare: '*'
|
||||
wrangler: '*'
|
||||
peerDependenciesMeta:
|
||||
miniflare:
|
||||
optional: true
|
||||
wrangler:
|
||||
optional: true
|
||||
|
||||
'@humanwhocodes/momoa@2.0.4':
|
||||
resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -2882,10 +2848,6 @@ packages:
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hono@4.11.5:
|
||||
resolution: {integrity: sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
@@ -3500,28 +3462,6 @@ packages:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
|
||||
react-router-hono-server@2.25.2:
|
||||
resolution: {integrity: sha512-hNgPx9lB8lTtlJAt7GWIB/S3LIStQTi4Y24ZcX3kCw05McDhUwEFxnN5WIK+F5j1iQxLgkX2vnkjsWdS7zc7Ng==}
|
||||
engines: {node: '>=22.20.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20250317.0
|
||||
'@hono/node-server': ^1.19.11
|
||||
'@react-router/dev': ^7.9.0
|
||||
'@types/react': ^19.0.0
|
||||
hono: ^4.12.8
|
||||
miniflare: ^3.20241205.0
|
||||
react-router: ^7.13.1
|
||||
vite: '>=7.3.1'
|
||||
wrangler: ^4.73.0
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
miniflare:
|
||||
optional: true
|
||||
wrangler:
|
||||
optional: true
|
||||
|
||||
react-router@7.14.0:
|
||||
resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -4948,25 +4888,6 @@ snapshots:
|
||||
protobufjs: 7.5.4
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hono/node-server@1.19.13(hono@4.11.5)':
|
||||
dependencies:
|
||||
hono: 4.11.5
|
||||
|
||||
'@hono/node-ws@1.3.0(@hono/node-server@1.19.13(hono@4.11.5))(bufferutil@4.0.9)(hono@4.11.5)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.13(hono@4.11.5)
|
||||
hono: 4.11.5
|
||||
ws: 8.20.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@hono/vite-dev-server@0.25.1(hono@4.11.5)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.13(hono@4.11.5)
|
||||
hono: 4.11.5
|
||||
minimatch: 9.0.9
|
||||
|
||||
'@humanwhocodes/momoa@2.0.4': {}
|
||||
|
||||
'@iconify-json/simple-icons@1.2.48':
|
||||
@@ -6662,8 +6583,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hono@4.11.5: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
hpagent@1.2.0: {}
|
||||
@@ -7285,21 +7204,6 @@ snapshots:
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
react-router: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
|
||||
react-router-hono-server@2.25.2(patch_hash=e94667f181837f2501959f5cf01fa0a9e6ea2883dcb1137be8b1c6178979bc77)(@hono/node-server@1.19.13(hono@4.11.5))(@react-router/dev@7.14.0(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(@types/react@19.2.14)(bufferutil@4.0.9)(hono@4.11.5)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@drizzle-team/brocli': 0.11.0
|
||||
'@hono/node-server': 1.19.13(hono@4.11.5)
|
||||
'@hono/node-ws': 1.3.0(@hono/node-server@1.19.13(hono@4.11.5))(bufferutil@4.0.9)(hono@4.11.5)(utf-8-validate@5.0.10)
|
||||
'@hono/vite-dev-server': 0.25.1(hono@4.11.5)
|
||||
'@react-router/dev': 7.14.0(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
|
||||
'@types/react': 19.2.14
|
||||
hono: 4.11.5
|
||||
react-router: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||
dependencies:
|
||||
cookie: 1.1.1
|
||||
@@ -8031,6 +7935,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
bufferutil: 4.0.9
|
||||
utf-8-validate: 5.0.10
|
||||
optional: true
|
||||
|
||||
wsl-utils@0.1.0:
|
||||
dependencies:
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// MARK: HTTP Runtime
|
||||
//
|
||||
// Bare Node http(s) runtime that hosts a `RequestListener` (typically the
|
||||
// React Router request listener from `@react-router/node`) and serves
|
||||
// static assets out of a directory.
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import {
|
||||
type IncomingMessage,
|
||||
type RequestListener,
|
||||
type Server,
|
||||
createServer as createHttpServer,
|
||||
} from "node:http";
|
||||
import type { ServerResponse } from "node:http";
|
||||
import {
|
||||
type ServerOptions as HttpsServerOptions,
|
||||
createServer as createHttpsServer,
|
||||
} from "node:https";
|
||||
import { extname, normalize, resolve, sep } from "node:path";
|
||||
|
||||
import mime from "mime";
|
||||
|
||||
export interface Logger {
|
||||
info: (msg: string, ...args: unknown[]) => void;
|
||||
error: (msg: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
// TODO: Replace with Pino!
|
||||
const defaultLogger: Logger = {
|
||||
info: (msg, ...args) => console.log(`[runtime] ${msg}`, ...args),
|
||||
error: (msg, ...args) => console.error(`[runtime] ${msg}`, ...args),
|
||||
};
|
||||
|
||||
export interface StaticOptions {
|
||||
root: string;
|
||||
basename: string;
|
||||
assetsDir: string;
|
||||
immutableAssets: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a static-file middleware. The callback resolves to `true` when
|
||||
* the request was served, `false` when the caller should fall through to
|
||||
* the next handler (e.g. the React Router request listener).
|
||||
*/
|
||||
function createStaticHandler(opts: StaticOptions) {
|
||||
const root = resolve(opts.root);
|
||||
const prefix = opts.basename.endsWith("/") ? opts.basename : `${opts.basename}/`;
|
||||
const assetsPrefix = `${prefix}${opts.assetsDir}/`;
|
||||
|
||||
return async function serveStatic(req: IncomingMessage, res: ServerResponse) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
||||
if (!req.url) return false;
|
||||
|
||||
let pathname: string;
|
||||
try {
|
||||
pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pathname.startsWith(prefix)) return false;
|
||||
const rel = pathname.slice(prefix.length);
|
||||
if (!rel || rel.endsWith("/")) return false;
|
||||
|
||||
// Resolve and confine to root to prevent path traversal.
|
||||
const file = resolve(root, normalize(rel));
|
||||
if (file !== root && !file.startsWith(root + sep)) return false;
|
||||
|
||||
let st;
|
||||
try {
|
||||
st = await stat(file);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!st.isFile()) return false;
|
||||
|
||||
const isAsset = pathname.startsWith(assetsPrefix);
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
isAsset && opts.immutableAssets
|
||||
? "public, max-age=31536000, immutable"
|
||||
: "public, max-age=3600",
|
||||
);
|
||||
|
||||
const mimeType = mime.getType(extname(file)) ?? "application/octet-stream";
|
||||
res.setHeader("Content-Type", mimeType);
|
||||
res.setHeader("Content-Length", String(st.size));
|
||||
res.setHeader("Last-Modified", st.mtime.toUTCString());
|
||||
res.statusCode = 200;
|
||||
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve_, reject) => {
|
||||
const stream = createReadStream(file);
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve_());
|
||||
stream.pipe(res);
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListenerOptions {
|
||||
basename: string;
|
||||
staticRoot?: string;
|
||||
assetsDir?: string;
|
||||
immutableAssets?: boolean;
|
||||
requestListener: RequestListener;
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes the full Node `RequestListener` chain:
|
||||
* 1. `${basename}` → 302 to `${basename}/`
|
||||
* 2. Static asset serving (optional)
|
||||
* 3. Delegate to the downstream request listener
|
||||
*/
|
||||
export function composeListener(opts: ListenerOptions): RequestListener {
|
||||
const log = opts.logger ?? defaultLogger;
|
||||
const basename = opts.basename;
|
||||
const serveStatic = opts.staticRoot
|
||||
? createStaticHandler({
|
||||
root: opts.staticRoot,
|
||||
basename,
|
||||
assetsDir: opts.assetsDir ?? "assets",
|
||||
immutableAssets: opts.immutableAssets ?? true,
|
||||
})
|
||||
: null;
|
||||
|
||||
return (req, res) => {
|
||||
if (req.url) {
|
||||
try {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
if (url.pathname === basename) {
|
||||
res.statusCode = 302;
|
||||
res.setHeader("Location", `${basename}/${url.search}`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!serveStatic) {
|
||||
opts.requestListener(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
serveStatic(req, res)
|
||||
.then((handled) => {
|
||||
if (!handled) opts.requestListener(req, res);
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("Static handler failed: %s", err);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end("Internal Server Error");
|
||||
} else {
|
||||
res.destroy(err as Error);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export interface StartOptions {
|
||||
host: string;
|
||||
port: number;
|
||||
listener: RequestListener;
|
||||
tls?: HttpsServerOptions;
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts an http(s) server. Wires up SIGINT/SIGTERM for
|
||||
* graceful shutdown.
|
||||
*/
|
||||
export function startHttpServer(opts: StartOptions): Server {
|
||||
const log = opts.logger ?? defaultLogger;
|
||||
const server = opts.tls
|
||||
? createHttpsServer(opts.tls, opts.listener)
|
||||
: createHttpServer(opts.listener);
|
||||
|
||||
server.listen(opts.port, opts.host, () => {
|
||||
const proto = opts.tls ? "https" : "http";
|
||||
log.info("Listening on %s://%s:%s", proto, opts.host, opts.port);
|
||||
});
|
||||
|
||||
const shutdown = (signal: string) => {
|
||||
log.info("Received %s, shutting down...", signal);
|
||||
server.close(() => process.exit(0));
|
||||
// Force exit if connections don't drain in time.
|
||||
setTimeout(() => process.exit(0), 5_000).unref();
|
||||
};
|
||||
|
||||
process.once("SIGINT", () => shutdown("SIGINT"));
|
||||
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
||||
|
||||
return server;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// MARK: Vite Plugin
|
||||
//
|
||||
// In development we want `react-router dev` (Vite) to host the entire
|
||||
// app, so we register a single connect-style middleware that:
|
||||
// 1. loads the SSR entry through Vite's `ssrLoadModule` (HMR-aware),
|
||||
// 2. dispatches every request through a `composeListener` chain
|
||||
// (basename redirect → static asset fallback → app handler).
|
||||
|
||||
import type { RequestListener } from "node:http";
|
||||
|
||||
import type { Plugin } from "vite";
|
||||
|
||||
import { composeListener } from "./http";
|
||||
|
||||
export interface DevServerOptions {
|
||||
entry: string;
|
||||
basename: string;
|
||||
publicDir: string;
|
||||
}
|
||||
|
||||
export function headplaneDevServer(options: DevServerOptions): Plugin {
|
||||
return {
|
||||
name: "headplane:dev-server",
|
||||
apply: "serve",
|
||||
configureServer(server) {
|
||||
// Lazy reference to the loaded entry; recomputed per request so
|
||||
// Vite's HMR picks up changes via `ssrLoadModule`.
|
||||
let appListener: RequestListener = (_req, res) => {
|
||||
res.statusCode = 503;
|
||||
res.end("Server entry not loaded yet");
|
||||
};
|
||||
|
||||
const composed = composeListener({
|
||||
basename: options.basename,
|
||||
staticRoot: options.publicDir,
|
||||
immutableAssets: false,
|
||||
requestListener: (req, res) => appListener(req, res),
|
||||
});
|
||||
|
||||
// Defer registration so our middleware runs AFTER Vite's
|
||||
// transform/asset middlewares (those serve `/@vite/...`,
|
||||
// `/node_modules/...`, module transforms, etc.).
|
||||
return () => {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
try {
|
||||
const mod = (await server.ssrLoadModule(options.entry)) as {
|
||||
default: RequestListener;
|
||||
};
|
||||
appListener = mod.default;
|
||||
composed(req, res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) server.ssrFixStacktrace(err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+33
-13
@@ -3,20 +3,24 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { reactRouterHonoServer } from "react-router-hono-server/dev";
|
||||
import { defineConfig } from "vite";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const prefix = process.env.__INTERNAL_PREFIX || "/admin";
|
||||
if (prefix.endsWith("/")) {
|
||||
import { headplaneDevServer } from "./runtime/vite-plugin";
|
||||
|
||||
const PROD_ENTRY = "./app/server/main.ts";
|
||||
const DEV_ENTRY = "./app/server/app.ts";
|
||||
|
||||
const PREFIX = process.env.__INTERNAL_PREFIX || "/admin";
|
||||
if (PREFIX.endsWith("/")) {
|
||||
throw new Error("Prefix must not end with a slash");
|
||||
}
|
||||
|
||||
// Derive version: HEADPLANE_VERSION env > git describe > package.json
|
||||
const isNext = process.env.IMAGE_TAG?.includes("next");
|
||||
let version: string;
|
||||
let VERSION: string;
|
||||
if (process.env.HEADPLANE_VERSION) {
|
||||
version = process.env.HEADPLANE_VERSION;
|
||||
VERSION = process.env.HEADPLANE_VERSION;
|
||||
} else {
|
||||
try {
|
||||
const describe = execSync("git describe --tags", { encoding: "utf-8" })
|
||||
@@ -25,13 +29,14 @@ if (process.env.HEADPLANE_VERSION) {
|
||||
const tag = execSync("git describe --tags --abbrev=0", { encoding: "utf-8" })
|
||||
.trim()
|
||||
.replace(/^v/, "");
|
||||
version = describe === tag ? tag : `${tag}-dev+${describe.split("-").pop()}`;
|
||||
VERSION = describe === tag ? tag : `${tag}-dev+${describe.split("-").pop()}`;
|
||||
} catch {
|
||||
const pkg = await readFile("package.json", "utf-8");
|
||||
version = JSON.parse(pkg).version;
|
||||
VERSION = JSON.parse(pkg).version;
|
||||
}
|
||||
}
|
||||
if (!version) {
|
||||
|
||||
if (!VERSION) {
|
||||
throw new Error("Unable to determine version");
|
||||
}
|
||||
|
||||
@@ -40,8 +45,16 @@ const config = await readFile("config.example.yaml", "utf-8");
|
||||
const { server } = parse(config);
|
||||
|
||||
export default defineConfig(({ command, isSsrBuild }) => ({
|
||||
base: command === "build" ? `${prefix}/` : undefined,
|
||||
plugins: [reactRouterHonoServer(), reactRouter(), tailwindcss()],
|
||||
base: command === "build" ? `${PREFIX}/` : undefined,
|
||||
plugins: [
|
||||
headplaneDevServer({
|
||||
entry: DEV_ENTRY,
|
||||
basename: PREFIX,
|
||||
publicDir: new URL("./public", import.meta.url).pathname,
|
||||
}),
|
||||
reactRouter(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
host: server.host,
|
||||
port: server.port,
|
||||
@@ -52,9 +65,16 @@ export default defineConfig(({ command, isSsrBuild }) => ({
|
||||
build: {
|
||||
target: "baseline-widely-available",
|
||||
sourcemap: true,
|
||||
rolldownOptions:
|
||||
rollupOptions:
|
||||
command === "build"
|
||||
? {
|
||||
// Override the SSR build entry so React Router emits the
|
||||
// production bootstrap (`app/server/main.ts`) as
|
||||
// `build/server/index.js`. It transitively imports the
|
||||
// SSR entry, which pulls in the React Router server build
|
||||
// via the virtual module `virtual:react-router/server-build`.
|
||||
input: isSsrBuild ? PROD_ENTRY : undefined,
|
||||
|
||||
// Exclude WASM from the client since it fetches from the server
|
||||
external: isSsrBuild ? [] : [/\.wasm(\?url)?$/],
|
||||
}
|
||||
@@ -64,7 +84,7 @@ export default defineConfig(({ command, isSsrBuild }) => ({
|
||||
noExternal: command === "build" ? true : undefined,
|
||||
},
|
||||
define: {
|
||||
__VERSION__: JSON.stringify(isNext ? `${version}-next` : version),
|
||||
__PREFIX__: JSON.stringify(prefix),
|
||||
__VERSION__: JSON.stringify(isNext ? `${VERSION}-next` : VERSION),
|
||||
__PREFIX__: JSON.stringify(PREFIX),
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user