@fluixi/start 0.1.0-alpha.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/adapter.d.ts +32 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +78 -0
  6. package/dist/api.d.ts +35 -0
  7. package/dist/api.d.ts.map +1 -0
  8. package/dist/api.js +120 -0
  9. package/dist/commands/build.d.ts +8 -0
  10. package/dist/commands/build.d.ts.map +1 -0
  11. package/dist/commands/build.js +67 -0
  12. package/dist/commands/dev.d.ts +8 -0
  13. package/dist/commands/dev.d.ts.map +1 -0
  14. package/dist/commands/dev.js +91 -0
  15. package/dist/commands/prerender.d.ts +11 -0
  16. package/dist/commands/prerender.d.ts.map +1 -0
  17. package/dist/commands/prerender.js +78 -0
  18. package/dist/commands/start.d.ts +9 -0
  19. package/dist/commands/start.d.ts.map +1 -0
  20. package/dist/commands/start.js +18 -0
  21. package/dist/config.d.ts +82 -0
  22. package/dist/config.d.ts.map +1 -0
  23. package/dist/config.js +116 -0
  24. package/dist/di.d.ts +63 -0
  25. package/dist/di.d.ts.map +1 -0
  26. package/dist/di.js +210 -0
  27. package/dist/handler.d.ts +42 -0
  28. package/dist/handler.d.ts.map +1 -0
  29. package/dist/handler.js +152 -0
  30. package/dist/head.d.ts +14 -0
  31. package/dist/head.d.ts.map +1 -0
  32. package/dist/head.js +13 -0
  33. package/dist/index.d.ts +24 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +25 -0
  36. package/dist/interceptors.d.ts +15 -0
  37. package/dist/interceptors.d.ts.map +1 -0
  38. package/dist/interceptors.js +47 -0
  39. package/dist/internal.d.ts +87 -0
  40. package/dist/internal.d.ts.map +1 -0
  41. package/dist/internal.js +302 -0
  42. package/dist/middleware.d.ts +38 -0
  43. package/dist/middleware.d.ts.map +1 -0
  44. package/dist/middleware.js +39 -0
  45. package/dist/router.d.ts +13 -0
  46. package/dist/router.d.ts.map +1 -0
  47. package/dist/router.js +12 -0
  48. package/dist/server-fn-setup.d.ts +8 -0
  49. package/dist/server-fn-setup.d.ts.map +1 -0
  50. package/dist/server-fn-setup.js +17 -0
  51. package/dist/server-fn.d.ts +33 -0
  52. package/dist/server-fn.d.ts.map +1 -0
  53. package/dist/server-fn.js +96 -0
  54. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  55. package/dist/ui.d.ts +35 -0
  56. package/dist/ui.d.ts.map +1 -0
  57. package/dist/ui.js +99 -0
  58. package/package.json +86 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fluixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fluixi/assets/main/logos/128x128.png" alt="Fluixi" width="120" height="120" />
3
+ </p>
4
+
5
+ # @fluixi/start
6
+
7
+ **The Fluixi meta-framework — zero-config dev/build/start with SSR, file routing, and hydration.**
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e.svg)](./LICENSE)
10
+ ![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)
11
+ ![Registry](https://img.shields.io/badge/registry-GitHub%20Packages-181717?logo=github)
12
+ ![Vite](https://img.shields.io/badge/built%20on-Vite-646cff?logo=vite&logoColor=white)
13
+
14
+ ---
15
+
16
+ ## ✨ Overview
17
+
18
+ `@fluixi/start` is `dev`, `build`, and production `start` over [`@fluixi/server`](../server)
19
+ + Vite, so apps stop hand-rolling a `server.mjs`. This package is the **runtime**
20
+ (programmatic commands + config); the `fluixi` CLI lives in [`@fluixi/cli`](../cli), and
21
+ you scaffold a new app with [`create-fluixi`](../create) (`npm create fluixi`).
22
+
23
+ ```bash
24
+ fluixi dev # SSR dev server (Vite middleware + HMR)
25
+ fluixi build # client + server bundles → dist/client, dist/server
26
+ fluixi start # production SSR server (after build)
27
+ ```
28
+
29
+ ## 📦 Installation
30
+
31
+ ```ini
32
+ # .npmrc
33
+ @fluixi:registry=https://npm.pkg.github.com
34
+ //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
35
+ ```
36
+
37
+ ```sh
38
+ pnpm add @fluixi/start @fluixi/cli
39
+ ```
40
+
41
+ ## 🗂️ Conventions
42
+
43
+ | Default | Config key |
44
+ | --- | --- |
45
+ | `src/entry-server.ts` exporting `render(url)` (and optionally `renderStream(url)`) | `serverEntry` |
46
+ | `src/entry-client.ts` (hydration) | `clientEntry` |
47
+ | `src/routes/` (file-based routes → `virtual:fluixi-routes`) | `routesDir` |
48
+ | `src/middleware.ts` (optional request middleware) | `middleware` |
49
+ | `index.html` with `<div id="root"></div>` | `template` / `mountId` |
50
+ | port `3000` | `port` |
51
+
52
+ ## ⚙️ Config
53
+
54
+ `fluixi.config.{ts,mts,mjs,js}` — TypeScript configs are bundled on the fly:
55
+
56
+ ```ts
57
+ import { defineConfig } from '@fluixi/start/config';
58
+
59
+ export default defineConfig({
60
+ port: 3010,
61
+ ssrNoExternal: ['@medusajs/js-sdk'], // SSR deps Vite must bundle
62
+ });
63
+ ```
64
+
65
+ ## 🚀 Features
66
+
67
+ - **Streaming SSR** — export `renderStream(url)` (via `@fluixi/server`'s
68
+ `renderToBodyStream`) and the `index.html` head is flushed first, then the app body is
69
+ streamed into the mount point.
70
+ - **Middleware / guards** — `src/middleware.ts` default-exports `defineMiddleware([...])`,
71
+ a `(request, next) => Response` chain run before SSR (auth redirects, headers, …).
72
+ - **Server functions** — a `"use server"` function runs only on the server and is callable
73
+ from the client over RPC (`/_server`, same-origin CSRF); `getRequestEvent()` works inside
74
+ it.
75
+ - **Routing** — `@fluixi/start/router` re-exports [`@fluixi/core/router-next`](../core):
76
+ `Router`, `Outlet`, `Link`, `Form`, the hooks, `createMemoryHistory`.
77
+ - **Deploy adapters** — `createProdHandler` builds a runtime-neutral fetch handler;
78
+ `nodeAdapter` (Node http + static files) and `webAdapter` (`export { fetch }` for
79
+ edge/worker) serve it. The same handler runs on Node, Bun, Deno, and edge runtimes.
80
+
81
+ ## 🧰 Programmatic
82
+
83
+ ```js
84
+ import { dev, build, start, createProdHandler } from '@fluixi/start';
85
+
86
+ await dev({ port: 3010 }); // or build() / start()
87
+ const handler = await createProdHandler(); // a (Request) => Promise<Response>
88
+ ```
89
+
90
+ ## 🔌 Subpaths
91
+
92
+ | Import | What |
93
+ | --- | --- |
94
+ | `@fluixi/start` | `dev`/`build`/`start`, the fetch-handler layer, adapters, middleware helpers |
95
+ | `@fluixi/start/config` | `defineConfig`, `loadConfig`, `resolveConfig` |
96
+ | `@fluixi/start/router` | the routing surface (re-export of `@fluixi/core/router-next`) |
97
+ | `@fluixi/start/server-fn` | server-function runtime (`isServerFnRequest`, `handleServerFn`) |
98
+
99
+ ## 📄 License
100
+
101
+ [MIT](./LICENSE) © [Fluixi](https://fluixi.dev)
@@ -0,0 +1,32 @@
1
+ import type { FluixiConfig, ResolvedConfig } from './config.js';
2
+ import { type FetchHandler } from './handler.js';
3
+ /**
4
+ * Build the production SSR fetch handler from a `fluixi build` output (dist/server +
5
+ * dist/client/index.html). It's runtime-neutral — the SAME handler runs on Node, Bun,
6
+ * Deno, and edge/worker runtimes. Adapters decide how to serve it + assets.
7
+ */
8
+ export declare function createProdHandler(config?: FluixiConfig): Promise<FetchHandler>;
9
+ export interface AdapterContext {
10
+ handler: FetchHandler;
11
+ cfg: ResolvedConfig;
12
+ /** Built client assets dir (dist/client). */
13
+ clientDir: string;
14
+ }
15
+ /**
16
+ * A deploy target. The runtime-neutral fetch handler is the portable core; an adapter
17
+ * packages it for a platform: filesystem runtimes (node) listen on a port and serve
18
+ * static files from disk; edge/serverless runtimes return a `{ fetch }` export the
19
+ * platform invokes (assets served by the platform/CDN). Build-time artifact emission
20
+ * (worker bundle, function manifest) is a later increment.
21
+ */
22
+ export interface Adapter {
23
+ name: string;
24
+ serve(ctx: AdapterContext): Promise<void> | {
25
+ fetch: FetchHandler;
26
+ };
27
+ }
28
+ /** Reference filesystem adapter — Node `http` + static files from dist/client. */
29
+ export declare const nodeAdapter: Adapter;
30
+ /** Reference edge/web adapter — export `{ fetch }`; the platform serves assets. */
31
+ export declare const webAdapter: Adapter;
32
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAGtB;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkDxF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,cAAc,CAAC;IACpB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG;QAAE,KAAK,EAAE,YAAY,CAAA;KAAE,CAAC;CACrE;AAED,kFAAkF;AAClF,eAAO,MAAM,WAAW,EAAE,OAQzB,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAAE,OAKxB,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve, join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { resolveConfig } from './config.js';
6
+ import { injectAppAndHead, splitTemplate, guardHandler } from './internal.js';
7
+ import { createRequestHandler, serveNode, streamDocument, withStaticFiles, } from './handler.js';
8
+ import { composeMiddleware, normalizeMiddleware } from './middleware.js';
9
+ /**
10
+ * Build the production SSR fetch handler from a `fluixi build` output (dist/server +
11
+ * dist/client/index.html). It's runtime-neutral — the SAME handler runs on Node, Bun,
12
+ * Deno, and edge/worker runtimes. Adapters decide how to serve it + assets.
13
+ */
14
+ export async function createProdHandler(config = {}) {
15
+ const cfg = resolveConfig(config);
16
+ const clientDir = resolve(cfg.root, 'dist/client');
17
+ const template = await readFile(join(clientDir, 'index.html'), 'utf-8');
18
+ const serverEntry = resolve(cfg.root, 'dist/server', 'entry-server.js');
19
+ // SPA apps may ship no server entry (or one only for server functions) — import if present.
20
+ const mod = existsSync(serverEntry)
21
+ ? await import(/* @vite-ignore */ pathToFileURL(serverEntry).href)
22
+ : {};
23
+ // Streaming when the entry exports `renderStream` (a body stream): flush the split
24
+ // template head first, pipe the body, then the tail. Else inject the string render.
25
+ let core;
26
+ if (!cfg.ssr) {
27
+ // SPA: the page is the static shell; the client entry renders it. Server functions +
28
+ // middleware still run through this handler — there's just no SSR render pass.
29
+ core = createRequestHandler(async () => template);
30
+ }
31
+ else if (typeof mod.renderStream === 'function') {
32
+ const { head, tail } = splitTemplate(template, cfg.mountId);
33
+ core = async (request) => {
34
+ const url = new URL(request.url);
35
+ const body = await mod.renderStream(url.pathname + url.search, request);
36
+ return streamDocument(head, body, tail);
37
+ };
38
+ }
39
+ else {
40
+ const render = mod.render ?? mod.default;
41
+ core = createRequestHandler(async (url, request) => injectAppAndHead(template, await render(url, request), cfg.mountId));
42
+ }
43
+ // Server-function RPC dispatch. The entry-server module re-exports the dispatcher, so
44
+ // it shares the registry that the app's "use server" modules populated (a separate
45
+ // node-imported copy would have an empty registry).
46
+ if (typeof mod.handleServerFn === 'function' && typeof mod.isServerFnRequest === 'function') {
47
+ const render = core;
48
+ core = (request) => (mod.isServerFnRequest(request) ? mod.handleServerFn(request) : render(request));
49
+ }
50
+ // File-based API routes — the entry-server re-exports them from virtual:fluixi-api.
51
+ if (typeof mod.handleApiRequest === 'function' && typeof mod.isApiRequest === 'function') {
52
+ const next = core;
53
+ core = (request) => (mod.isApiRequest(request) ? mod.handleApiRequest(request) : next(request));
54
+ }
55
+ // Optional request middleware (src/middleware.ts → dist/server/middleware.js).
56
+ const mwFile = resolve(cfg.root, 'dist/server', 'middleware.js');
57
+ if (!existsSync(mwFile))
58
+ return guardHandler(core);
59
+ const mwMod = await import(/* @vite-ignore */ pathToFileURL(mwFile).href);
60
+ return guardHandler(composeMiddleware(normalizeMiddleware(mwMod), core));
61
+ }
62
+ /** Reference filesystem adapter — Node `http` + static files from dist/client. */
63
+ export const nodeAdapter = {
64
+ name: 'node',
65
+ async serve({ handler, cfg, clientDir }) {
66
+ serveNode(withStaticFiles(handler, clientDir), {
67
+ port: cfg.port,
68
+ onListen: (p) => console.log(` fluixi start (node) → http://localhost:${p}`),
69
+ });
70
+ },
71
+ };
72
+ /** Reference edge/web adapter — export `{ fetch }`; the platform serves assets. */
73
+ export const webAdapter = {
74
+ name: 'web',
75
+ serve({ handler }) {
76
+ return { fetch: handler };
77
+ },
78
+ };
package/dist/api.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ export interface ApiRoute {
2
+ /** Regex source matching the pathname. */
3
+ source: string;
4
+ /** Param names, in capture-group order. */
5
+ keys: string[];
6
+ /** The route module (its method exports). */
7
+ mod: Record<string, unknown>;
8
+ /** Higher = more specific (static beats dynamic beats catch-all). */
9
+ rank: number;
10
+ }
11
+ /** Derive a route pattern from a file path relative to the api dir (no extension). */
12
+ export declare function derivePattern(rel: string, prefix?: string): {
13
+ source: string;
14
+ keys: string[];
15
+ rank: number;
16
+ };
17
+ /** Whether a pathname falls under the api prefix (so a miss 404s as API, not SSR). */
18
+ export declare function isApiPath(pathname: string, prefix?: string): boolean;
19
+ /** First matching route (by rank) for a pathname, with decoded params. */
20
+ export declare function matchApiRoutes(routes: ApiRoute[], pathname: string): {
21
+ mod: Record<string, unknown>;
22
+ params: Record<string, string>;
23
+ } | null;
24
+ /** Match + invoke the method handler. 404 outside any route, 405 when the method is missing. */
25
+ export declare function handleApi(routes: ApiRoute[], request: Request, prefix?: string): Promise<Response>;
26
+ /**
27
+ * Scan `dir` for API route files → `virtual:fluixi-api` exporting `isApiRequest(request)` +
28
+ * `handleApiRequest(request)`. Files are statically imported so they bundle for prod.
29
+ */
30
+ export declare function apiRoutesVitePlugin(opts: {
31
+ dir: string;
32
+ prefix?: string;
33
+ }): any;
34
+ export declare const API_VIRTUAL_MODULE = "virtual:fluixi-api";
35
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,QAAQ;IACvB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqB5G;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,OAAO,CAEpE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAS5I;AAED,gGAAgG;AAChG,wBAAsB,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYxG;AAkBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAiC/E;AAED,eAAO,MAAM,kBAAkB,uBAAW,CAAC"}
package/dist/api.js ADDED
@@ -0,0 +1,120 @@
1
+ // File-based API routes. Files under `apiDir` (default src/api) export HTTP-method handlers
2
+ // (GET/POST/PUT/PATCH/DELETE) that take (request, { params }) and return a Web Response. They map
3
+ // to `/api/<path>` ([id] → param, [...rest] → catch-all). A scan plugin emits `virtual:fluixi-api`
4
+ // (isApiRequest + handleApiRequest); dev auto-wires it, prod gets it via the entry-server re-export.
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import { resolve as resolvePath, relative as relativePath, join } from 'node:path';
7
+ const API_VMOD = 'virtual:fluixi-api';
8
+ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
9
+ /** Derive a route pattern from a file path relative to the api dir (no extension). */
10
+ export function derivePattern(rel, prefix = '/api') {
11
+ const segs = rel.split('/').filter(Boolean);
12
+ if (segs[segs.length - 1] === 'index')
13
+ segs.pop();
14
+ const keys = [];
15
+ let rank = 0;
16
+ const parts = segs.map((seg) => {
17
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
18
+ if (catchAll) {
19
+ keys.push(catchAll[1]);
20
+ return '(.+)';
21
+ }
22
+ const param = seg.match(/^\[(.+)\]$/);
23
+ if (param) {
24
+ keys.push(param[1]);
25
+ rank += 1;
26
+ return '([^/]+)';
27
+ }
28
+ rank += 10;
29
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ });
31
+ return { source: `^${prefix}${parts.length ? '/' + parts.join('/') : ''}/?$`, keys, rank };
32
+ }
33
+ /** Whether a pathname falls under the api prefix (so a miss 404s as API, not SSR). */
34
+ export function isApiPath(pathname, prefix = '/api') {
35
+ return pathname === prefix || pathname.startsWith(prefix + '/');
36
+ }
37
+ /** First matching route (by rank) for a pathname, with decoded params. */
38
+ export function matchApiRoutes(routes, pathname) {
39
+ for (const r of [...routes].sort((a, b) => b.rank - a.rank)) {
40
+ const m = new RegExp(r.source).exec(pathname);
41
+ if (!m)
42
+ continue;
43
+ const params = {};
44
+ r.keys.forEach((k, i) => (params[k] = decodeURIComponent(m[i + 1] ?? '')));
45
+ return { mod: r.mod, params };
46
+ }
47
+ return null;
48
+ }
49
+ /** Match + invoke the method handler. 404 outside any route, 405 when the method is missing. */
50
+ export async function handleApi(routes, request, prefix = '/api') {
51
+ const pathname = new URL(request.url).pathname;
52
+ const match = matchApiRoutes(routes, pathname);
53
+ if (!match)
54
+ return new Response('Not Found', { status: 404 });
55
+ const fn = match.mod[request.method];
56
+ if (typeof fn !== 'function') {
57
+ const allow = METHODS.filter((m) => typeof match.mod[m] === 'function').join(', ');
58
+ return new Response('Method Not Allowed', { status: 405, headers: allow ? { allow } : undefined });
59
+ }
60
+ const out = await fn(request, { params: match.params });
61
+ if (out instanceof Response)
62
+ return out;
63
+ return new Response(JSON.stringify(out ?? null), { headers: { 'content-type': 'application/json; charset=utf-8' } });
64
+ }
65
+ function scanFiles(dir, base = dir) {
66
+ let out = [];
67
+ let entries = [];
68
+ try {
69
+ entries = readdirSync(dir);
70
+ }
71
+ catch {
72
+ return out;
73
+ }
74
+ for (const name of entries) {
75
+ const full = join(dir, name);
76
+ if (statSync(full).isDirectory())
77
+ out = out.concat(scanFiles(full, base));
78
+ else if (/\.(ts|tsx|js|mjs)$/.test(name) && !/\.(spec|test)\./.test(name))
79
+ out.push(full);
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Scan `dir` for API route files → `virtual:fluixi-api` exporting `isApiRequest(request)` +
85
+ * `handleApiRequest(request)`. Files are statically imported so they bundle for prod.
86
+ */
87
+ export function apiRoutesVitePlugin(opts) {
88
+ let root = process.cwd();
89
+ const prefix = opts.prefix ?? '/api';
90
+ const id = '\0' + API_VMOD;
91
+ return {
92
+ name: 'fluixi-api',
93
+ configResolved(config) {
94
+ root = config.root || root;
95
+ },
96
+ resolveId(source) {
97
+ return source === API_VMOD ? id : null;
98
+ },
99
+ load(source) {
100
+ if (source !== id)
101
+ return null;
102
+ const dir = resolvePath(root, opts.dir);
103
+ const files = scanFiles(dir);
104
+ const imports = [];
105
+ const entries = [];
106
+ files.forEach((file, i) => {
107
+ const rel = relativePath(dir, file).replace(/\.(ts|tsx|js|mjs)$/, '').split(/[\\/]/).join('/');
108
+ const { source: src, keys, rank } = derivePattern(rel, prefix);
109
+ imports.push(`import * as _${i} from ${JSON.stringify(file)};`);
110
+ entries.push(`{ source: ${JSON.stringify(src)}, keys: ${JSON.stringify(keys)}, rank: ${rank}, mod: _${i} }`);
111
+ });
112
+ return (`import { isApiPath, handleApi } from '@fluixi/start/api';\n` +
113
+ `${imports.join('\n')}\n` +
114
+ `const routes = [${entries.join(',')}];\n` +
115
+ `export const isApiRequest = (request) => isApiPath(new URL(request.url).pathname, ${JSON.stringify(prefix)});\n` +
116
+ `export const handleApiRequest = (request) => handleApi(routes, request, ${JSON.stringify(prefix)});\n`);
117
+ },
118
+ };
119
+ }
120
+ export const API_VIRTUAL_MODULE = API_VMOD;
@@ -0,0 +1,8 @@
1
+ import type { FluixiConfig } from '../config.js';
2
+ /**
3
+ * Production build: a client bundle (browser, with an SSR manifest) and a server
4
+ * bundle (the entry that renders HTML). @fluixi/start provides the fluixi plugins
5
+ * (file routing + JSX/SSR transform), so the app's vite.config keeps only app bits.
6
+ */
7
+ export declare function build(config?: FluixiConfig): Promise<void>;
8
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DpE"}
@@ -0,0 +1,67 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { resolveConfig } from '../config.js';
4
+ import { fluixiPlugins, loadServerEnv } from '../internal.js';
5
+ import { prerender } from './prerender.js';
6
+ import { wordmark, step, done, c } from '../ui.js';
7
+ /**
8
+ * Production build: a client bundle (browser, with an SSR manifest) and a server
9
+ * bundle (the entry that renders HTML). @fluixi/start provides the fluixi plugins
10
+ * (file routing + JSX/SSR transform), so the app's vite.config keeps only app bits.
11
+ */
12
+ export async function build(config = {}) {
13
+ const startedAt = Date.now();
14
+ const cfg = resolveConfig(config);
15
+ await loadServerEnv(cfg.root, 'production'); // .env → process.env (server-only secrets)
16
+ const { build: viteBuild } = await import('vite');
17
+ const plugins = await fluixiPlugins(cfg);
18
+ process.stdout.write(`\n ${wordmark()} ${c.gray('building for production')}\n\n`);
19
+ step('client bundle');
20
+ await viteBuild({
21
+ root: cfg.root,
22
+ plugins,
23
+ logLevel: 'warn',
24
+ build: { outDir: 'dist/client', ssrManifest: true, emptyOutDir: true },
25
+ });
26
+ // SSR builds a server bundle that renders HTML. A SPA may still ship one — only for server
27
+ // functions / middleware — so build it when the entry exists; a pure static SPA skips it.
28
+ const serverEntrySrc = resolve(cfg.root, cfg.serverEntry);
29
+ if (cfg.ssr || existsSync(serverEntrySrc)) {
30
+ step('server bundle');
31
+ await viteBuild({
32
+ root: cfg.root,
33
+ plugins,
34
+ ssr: { noExternal: cfg.ssrNoExternal.length ? cfg.ssrNoExternal : undefined },
35
+ build: {
36
+ outDir: 'dist/server',
37
+ ssr: serverEntrySrc,
38
+ emptyOutDir: true,
39
+ },
40
+ });
41
+ }
42
+ // Optional request middleware → its own server entry (emptyOutDir:false keeps
43
+ // entry-server.js). createProdHandler picks up dist/server/middleware.js if present.
44
+ const middlewareSrc = resolve(cfg.root, cfg.middleware);
45
+ if (existsSync(middlewareSrc)) {
46
+ step('middleware');
47
+ await viteBuild({
48
+ root: cfg.root,
49
+ plugins,
50
+ logLevel: 'warn',
51
+ ssr: { noExternal: cfg.ssrNoExternal.length ? cfg.ssrNoExternal : undefined },
52
+ build: {
53
+ outDir: 'dist/server',
54
+ ssr: middlewareSrc,
55
+ emptyOutDir: false,
56
+ },
57
+ });
58
+ }
59
+ // SSG: prerender routes to static HTML in dist/client (config-driven).
60
+ if (cfg.prerender) {
61
+ process.stdout.write('\n');
62
+ await prerender(config);
63
+ }
64
+ const ms = Date.now() - startedAt;
65
+ process.stdout.write('\n');
66
+ done(`${c.bold('built')} in ${c.bold(String(ms))} ms ${c.gray('→ dist/client + dist/server')}\n`);
67
+ }
@@ -0,0 +1,8 @@
1
+ import type { FluixiConfig } from '../config.js';
2
+ /**
3
+ * Dev server: Vite in middleware mode compiles the app on the fly; the server
4
+ * entry renders the shell per request, injected into the template; the client
5
+ * entry hydrates. Generalizes the hand-rolled server.mjs pattern.
6
+ */
7
+ export declare function dev(config?: FluixiConfig): Promise<void>;
8
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQjD;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+ElE"}
@@ -0,0 +1,91 @@
1
+ import { createServer as createHttp } from 'node:http';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { resolveConfig } from '../config.js';
5
+ import { injectAppAndHead, splitTemplate, installCrashGuards, fluixiPlugins, guardHandler, loadServerEnv, SERVER_FNS_VMOD } from '../internal.js';
6
+ import { toNodeHandler, streamDocument } from '../handler.js';
7
+ import { composeMiddleware, normalizeMiddleware } from '../middleware.js';
8
+ import { isApiPath, API_VIRTUAL_MODULE } from '../api.js';
9
+ import { devReady } from '../ui.js';
10
+ /**
11
+ * Dev server: Vite in middleware mode compiles the app on the fly; the server
12
+ * entry renders the shell per request, injected into the template; the client
13
+ * entry hydrates. Generalizes the hand-rolled server.mjs pattern.
14
+ */
15
+ export async function dev(config = {}) {
16
+ const startedAt = Date.now();
17
+ const cfg = resolveConfig(config);
18
+ installCrashGuards();
19
+ await loadServerEnv(cfg.root, 'development'); // .env → process.env (server-only secrets)
20
+ const { createServer } = await import('vite');
21
+ const template = readFileSync(resolve(cfg.root, cfg.template), 'utf-8');
22
+ // Create the HTTP server FIRST so Vite's HMR WebSocket attaches to it instead of
23
+ // opening its own port (which, if held by a zombie, loops "[vite] server
24
+ // connection lost" → reload forever). Sharing one port avoids that.
25
+ let vite;
26
+ // App render → a runtime-neutral fetch handler. In dev, "render" is Vite
27
+ // ssrLoadModule + transformIndexHtml; the same handler shape powers prod + adapters.
28
+ // If the entry exports `renderStream`, stream: transform+split the template, flush
29
+ // the head, pipe the body stream, then the tail. guardHandler maps pre-flush errors
30
+ // (a streaming error after the head is out can only truncate the response).
31
+ const renderHandler = guardHandler(async (request) => {
32
+ const url = new URL(request.url);
33
+ const path = url.pathname + url.search;
34
+ // Server-function RPC: auto-wired. Load the registry vmod (registers every "use server"
35
+ // fn + wires the request-context runner + re-exports the dispatcher) and route /_server
36
+ // through it — no need for the app's entry-server to re-export anything.
37
+ if (url.pathname === '/_server') {
38
+ const sf = await vite.ssrLoadModule(SERVER_FNS_VMOD);
39
+ if (sf.isServerFnRequest(request))
40
+ return sf.handleServerFn(request);
41
+ }
42
+ // File-based API routes (src/api/** → /api/*) — auto-wired, no entry change needed.
43
+ if (isApiPath(url.pathname)) {
44
+ const api = await vite.ssrLoadModule(API_VIRTUAL_MODULE);
45
+ if (api.isApiRequest(request))
46
+ return api.handleApiRequest(request);
47
+ }
48
+ // SPA mode: serve the static shell; the client entry renders. The server still ran
49
+ // (server functions above + middleware below), there's just no SSR render pass.
50
+ if (!cfg.ssr) {
51
+ const html = await vite.transformIndexHtml(path, template);
52
+ return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } });
53
+ }
54
+ const mod = await vite.ssrLoadModule('/' + cfg.serverEntry);
55
+ if (typeof mod.renderStream === 'function') {
56
+ const shell = await vite.transformIndexHtml(path, template);
57
+ const { head, tail } = splitTemplate(shell, cfg.mountId);
58
+ return streamDocument(head, await mod.renderStream(path, request), tail);
59
+ }
60
+ const render = mod.render ?? mod.default;
61
+ const appHtml = await render(path, request); // sync or async; request enables the locals bridge
62
+ // injectAppAndHead (not injectApp): lift the @fluixi/head marker renderRequestAsync prepends to
63
+ // the body into <head> + <html>, exactly like the prod adapter/prerender. Plain injectApp left
64
+ // the `<!--fh:…-->` marker as the mount root's first child, which stranded the hydration cursor
65
+ // (whole tree rebuilt → duplicated) AND skipped dev SEO. No marker → identical to injectApp.
66
+ const html = await vite.transformIndexHtml(path, injectAppAndHead(template, appHtml, cfg.mountId));
67
+ return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } });
68
+ }, (e) => vite.ssrFixStacktrace?.(e));
69
+ // Optional request middleware. Loaded per request via ssrLoadModule so edits hot-
70
+ // reload (no server restart); guardHandler turns a middleware throw into a 500.
71
+ const hasMiddleware = existsSync(resolve(cfg.root, cfg.middleware));
72
+ const handler = hasMiddleware
73
+ ? guardHandler(async (request) => {
74
+ const mwMod = await vite.ssrLoadModule('/' + cfg.middleware);
75
+ return composeMiddleware(normalizeMiddleware(mwMod), renderHandler)(request);
76
+ }, (e) => vite.ssrFixStacktrace?.(e))
77
+ : renderHandler;
78
+ const nodeHandler = toNodeHandler(handler);
79
+ const http = createHttp((req, res) => {
80
+ // Vite middleware first (static assets + HMR), then our handler for app routes.
81
+ vite.middlewares(req, res, () => nodeHandler(req, res));
82
+ });
83
+ vite = await createServer({
84
+ root: cfg.root,
85
+ plugins: await fluixiPlugins(cfg), // file routing + JSX/SSR transform (fluixi)
86
+ server: { middlewareMode: true, hmr: { server: http } },
87
+ appType: 'custom',
88
+ ssr: cfg.ssrNoExternal.length ? { noExternal: cfg.ssrNoExternal } : undefined,
89
+ });
90
+ http.listen(cfg.port, () => devReady({ port: cfg.port, startedAt }));
91
+ }
@@ -0,0 +1,11 @@
1
+ import type { FluixiConfig } from '../config.js';
2
+ export declare function htmlPath(url: string): string;
3
+ export declare function extractLinks(html: string): string[];
4
+ /**
5
+ * Render routes to static HTML in dist/client, via the real prod handler — so each page is
6
+ * byte-identical to what `fluixi start` would serve (assets, __FX_DATA__, hydration), it just
7
+ * lives on disk. Seeds from `prerender.routes` (default `/`) and, when `crawl`, follows internal
8
+ * links. Returns the written route paths.
9
+ */
10
+ export declare function prerender(config?: FluixiConfig): Promise<string[]>;
11
+ //# sourceMappingURL=prerender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerender.d.ts","sourceRoot":"","sources":["../../src/commands/prerender.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAGD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnD;AAOD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA8C5E"}