@fluixi/start 0.1.0-alpha.63 → 0.1.0-alpha.65

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 (44) hide show
  1. package/dist/adapter.d.ts +77 -7
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +150 -44
  4. package/dist/adapters/entry.d.ts +50 -0
  5. package/dist/adapters/entry.d.ts.map +1 -0
  6. package/dist/adapters/entry.js +124 -0
  7. package/dist/adapters/platforms.d.ts +30 -0
  8. package/dist/adapters/platforms.d.ts.map +1 -0
  9. package/dist/adapters/platforms.js +231 -0
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +7 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +114 -4
  14. package/dist/commands/index.d.ts +14 -0
  15. package/dist/commands/index.d.ts.map +1 -0
  16. package/dist/commands/index.js +13 -0
  17. package/dist/commands/start.d.ts.map +1 -1
  18. package/dist/commands/start.js +10 -1
  19. package/dist/config.d.ts +10 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +1 -0
  22. package/dist/document.d.ts +41 -0
  23. package/dist/document.d.ts.map +1 -0
  24. package/dist/document.js +125 -0
  25. package/dist/generated-api.d.ts +5 -0
  26. package/dist/generated-api.d.ts.map +1 -0
  27. package/dist/generated-api.js +26 -0
  28. package/dist/generated-server-fns.d.ts +5 -0
  29. package/dist/generated-server-fns.d.ts.map +1 -0
  30. package/dist/generated-server-fns.js +26 -0
  31. package/dist/handler-core.d.ts +41 -0
  32. package/dist/handler-core.d.ts.map +1 -0
  33. package/dist/handler-core.js +50 -0
  34. package/dist/index.d.ts +5 -7
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +9 -6
  37. package/dist/internal.d.ts +1 -29
  38. package/dist/internal.d.ts.map +1 -1
  39. package/dist/internal.js +10 -102
  40. package/dist/preload.d.ts +5 -0
  41. package/dist/preload.d.ts.map +1 -0
  42. package/dist/preload.js +27 -0
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/package.json +44 -6
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Turning a rendered app into a document, and keeping a failed render from taking
3
+ * the process with it.
4
+ *
5
+ * A leaf on purpose: this is imported by the request handler, which runs in a
6
+ * worker as often as on Node, and a runtime module that reaches into the build
7
+ * helpers drags a whole toolchain into the bundle with it.
8
+ */
9
+ import { extractHeadMarker } from '@fluixi/head';
10
+ /**
11
+ * Inject server-rendered app HTML into the template's mount element
12
+ * (`<div id="root"></div>`). Falls back to a `<!--ssr-outlet-->` marker, then to
13
+ * just before `</body>`.
14
+ */
15
+ export function injectApp(template, appHtml, mountId = 'root') {
16
+ const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
17
+ if (mount.test(template)) {
18
+ return template.replace(mount, `<div id="${mountId}">${appHtml}</div>`);
19
+ }
20
+ if (template.includes('<!--ssr-outlet-->')) {
21
+ return template.replace('<!--ssr-outlet-->', appHtml);
22
+ }
23
+ return template.replace('</body>', `<div id="${mountId}">${appHtml}</div></body>`);
24
+ }
25
+ /**
26
+ * Split the template at the mount point for streaming: `head` is everything up to and
27
+ * including the open mount tag (flushed first so the browser fetches assets while the
28
+ * server awaits data); `tail` is the close tag onward (client script + `</body>`).
29
+ * Mirrors injectApp's three cases (mount div, `<!--ssr-outlet-->`, before `</body>`).
30
+ */
31
+ export function splitTemplate(template, mountId = 'root') {
32
+ const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
33
+ const m = template.match(mount);
34
+ if (m && m.index !== undefined) {
35
+ return {
36
+ head: template.slice(0, m.index) + `<div id="${mountId}">`,
37
+ tail: `</div>` + template.slice(m.index + m[0].length),
38
+ };
39
+ }
40
+ const outlet = template.indexOf('<!--ssr-outlet-->');
41
+ if (outlet !== -1) {
42
+ return {
43
+ head: template.slice(0, outlet),
44
+ tail: template.slice(outlet + '<!--ssr-outlet-->'.length),
45
+ };
46
+ }
47
+ const body = template.indexOf('</body>');
48
+ if (body !== -1) {
49
+ return {
50
+ head: template.slice(0, body) + `<div id="${mountId}">`,
51
+ tail: `</div>` + template.slice(body),
52
+ };
53
+ }
54
+ return { head: template, tail: '' };
55
+ }
56
+ /**
57
+ * Reference the files this route needs in the document that renders it.
58
+ *
59
+ * A lazily-imported route is its own chunk with its own stylesheet, mentioned nowhere
60
+ * in the shell — without these the browser discovers them when hydration runs the
61
+ * dynamic import, so the markup paints before the CSS arrives.
62
+ *
63
+ * Files already named in the template are skipped: the entry chunk is in there from
64
+ * the client build, and preloading it again competes with the request in flight.
65
+ */
66
+ export function injectPreload(template, files) {
67
+ const tags = files
68
+ .filter((file) => !template.includes(file))
69
+ .map((file) => file.endsWith('.css')
70
+ ? `<link rel="stylesheet" href="${file}">`
71
+ : // `modulepreload` also fetches the chunk's own static imports.
72
+ `<link rel="modulepreload" href="${file}">`);
73
+ if (tags.length === 0)
74
+ return template;
75
+ // No `</head>` means a fragment, not a document.
76
+ const head = template.indexOf('</head>');
77
+ if (head === -1)
78
+ return template;
79
+ return template.slice(0, head) + tags.join('') + template.slice(head);
80
+ }
81
+ /** Set/replace an attribute on the template's <html> tag (e.g. lang). */
82
+ function setHtmlAttr(html, name, value) {
83
+ const existing = new RegExp(`(<html\\b[^>]*?)\\s${name}="[^"]*"`, 'i');
84
+ if (existing.test(html))
85
+ return html.replace(existing, `$1 ${name}="${value}"`);
86
+ return html.replace(/<html\b/i, `<html ${name}="${value}"`);
87
+ }
88
+ /**
89
+ * Inject the rendered app into the mount point AND lift any @fluixi/head metadata (carried back as
90
+ * a marker by renderRequestAsync) into <head> + <html>. If the page set a title, the template's own
91
+ * <title> is dropped so there's only one. No marker → identical to injectApp.
92
+ */
93
+ export function injectAppAndHead(template, rendered, mountId = 'root') {
94
+ const { head, body } = extractHeadMarker(rendered);
95
+ let html = injectApp(template, body, mountId);
96
+ if (head) {
97
+ if (head.headHtml) {
98
+ if (/<title[\s>]/i.test(head.headHtml))
99
+ html = html.replace(/<title>[\s\S]*?<\/title>/i, '');
100
+ html = html.replace('</head>', `${head.headHtml}</head>`);
101
+ }
102
+ for (const [k, v] of Object.entries(head.htmlAttrs))
103
+ html = setHtmlAttr(html, k, v);
104
+ }
105
+ return html;
106
+ }
107
+ /**
108
+ * Wrap a fetch handler so a thrown/rejected error becomes a 500 instead of an
109
+ * unhandled rejection. `createRequestHandler` already guards the renderer; this
110
+ * covers the middleware chain that wraps it. `onError` lets dev map the stack first.
111
+ */
112
+ export function guardHandler(handler, onError) {
113
+ return async (request) => {
114
+ try {
115
+ return await handler(request);
116
+ }
117
+ catch (e) {
118
+ onError?.(e);
119
+ return new Response(String(e?.stack || e), {
120
+ status: 500,
121
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
122
+ });
123
+ }
124
+ };
125
+ }
@@ -0,0 +1,5 @@
1
+ /** Whether this request addresses a file-based API route. */
2
+ export declare function isApiRequest(_request: Request): boolean;
3
+ /** Run the matched API handler and return its response. */
4
+ export declare function handleApiRequest(_request: Request): Promise<Response>;
5
+ //# sourceMappingURL=generated-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated-api.d.ts","sourceRoot":"","sources":["../src/generated-api.ts"],"names":[],"mappings":"AAqBA,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAEvD;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAErE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The file-based API routes found under `apiDir` (default `src/api`).
3
+ *
4
+ * ```ts
5
+ * import { isApiRequest, handleApiRequest } from '@fluixi/start/api-routes';
6
+ * ```
7
+ *
8
+ * Named `api-routes` because `@fluixi/start/api` is already the API plumbing you call
9
+ * directly (`createApiHandler`, `imageLoader`, …); this is the generated route table.
10
+ *
11
+ * A typed stand-in — the scan plugin serves the real one. `virtual:fluixi-api` still
12
+ * resolves.
13
+ */
14
+ function missingPlugin() {
15
+ throw new Error("[fluixi] '@fluixi/start/api-routes' was imported but the API scan plugin did not replace " +
16
+ 'it. Build with `fluixi build`/`fluixi dev`, or add the @fluixi/start plugins to your ' +
17
+ 'Vite config.');
18
+ }
19
+ /** Whether this request addresses a file-based API route. */
20
+ export function isApiRequest(_request) {
21
+ missingPlugin();
22
+ }
23
+ /** Run the matched API handler and return its response. */
24
+ export function handleApiRequest(_request) {
25
+ missingPlugin();
26
+ }
@@ -0,0 +1,5 @@
1
+ /** Whether this request is an RPC call to a `"use server"` function. */
2
+ export declare function isServerFnRequest(_request: Request): boolean;
3
+ /** Run the addressed server function and return its response. */
4
+ export declare function handleServerFn(_request: Request): Promise<Response>;
5
+ //# sourceMappingURL=generated-server-fns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated-server-fns.d.ts","sourceRoot":"","sources":["../src/generated-server-fns.ts"],"names":[],"mappings":"AAqBA,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEnE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The registry of `"use server"` functions found in this app.
3
+ *
4
+ * ```ts
5
+ * import { isServerFnRequest, handleServerFn } from '@fluixi/start/server-fns';
6
+ * ```
7
+ *
8
+ * A typed stand-in: the compiler's server-function plugin intercepts this specifier and
9
+ * serves the generated registry. It exists as a real module so the types ship with the
10
+ * package instead of an ambient `declare module` written into your `src/`.
11
+ *
12
+ * `virtual:fluixi-server-fns` still resolves.
13
+ */
14
+ function missingPlugin() {
15
+ throw new Error("[fluixi] '@fluixi/start/server-fns' was imported but the server-function plugin did not " +
16
+ 'replace it. Build with `fluixi build`/`fluixi dev`, or add the @fluixi/start plugins to ' +
17
+ 'your Vite config.');
18
+ }
19
+ /** Whether this request is an RPC call to a `"use server"` function. */
20
+ export function isServerFnRequest(_request) {
21
+ missingPlugin();
22
+ }
23
+ /** Run the addressed server function and return its response. */
24
+ export function handleServerFn(_request) {
25
+ missingPlugin();
26
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The request handler, assembled from things already in memory.
3
+ *
4
+ * `createProdHandler` reads the template off disk and imports the server entry by
5
+ * path — fine on Node, impossible in a worker, where there is no filesystem and
6
+ * every module has to be part of the bundle. So the assembly lives here and takes
7
+ * the template and the module as arguments; the platform entry a deploy adapter
8
+ * generates imports both statically and calls this.
9
+ *
10
+ * One assembly, so an edge deploy cannot quietly behave differently from `fluixi
11
+ * start` — the dispatch order (server functions, then API routes, then render,
12
+ * all under middleware) is a property of the framework, not of the host.
13
+ */
14
+ import type { ResolvedConfig } from './config.js';
15
+ import { type FetchHandler } from './handler.js';
16
+ /** The shape a built `entry-server.js` exposes. Every field is optional. */
17
+ export interface ServerModule {
18
+ render?: (url: string, request: Request) => string | Promise<string>;
19
+ default?: (url: string, request: Request) => string | Promise<string>;
20
+ renderStream?: (url: string, request: Request) => unknown;
21
+ isServerFnRequest?: (request: Request) => boolean;
22
+ handleServerFn?: (request: Request) => Response | Promise<Response>;
23
+ isApiRequest?: (request: Request) => boolean;
24
+ handleApiRequest?: (request: Request) => Response | Promise<Response>;
25
+ }
26
+ export interface HandlerParts {
27
+ /** `dist/client/index.html`, as a string. */
28
+ template: string;
29
+ /** The imported server entry. `{}` for an SPA that ships none. */
30
+ mod: ServerModule;
31
+ cfg: Pick<ResolvedConfig, 'ssr' | 'mountId'>;
32
+ /** The imported `middleware.js`, if the app has one. */
33
+ middleware?: unknown;
34
+ /**
35
+ * The built files this path needs, for the document to reference up front. Optional:
36
+ * without it a page is served exactly as before, just without preload hints.
37
+ */
38
+ preload?: (pathname: string) => string[];
39
+ }
40
+ export declare function createHandlerFrom({ template, mod, cfg, middleware, preload }: HandlerParts): FetchHandler;
41
+ //# sourceMappingURL=handler-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-core.d.ts","sourceRoot":"","sources":["../src/handler-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAwC,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGvF,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IAC1D,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IAClD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpE,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IAC7C,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC;IAC7C,wDAAwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CAC1C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,YAAY,CAmDzG"}
@@ -0,0 +1,50 @@
1
+ import { injectAppAndHead, injectPreload, splitTemplate, guardHandler } from './document.js';
2
+ import { createRequestHandler, streamDocument } from './handler.js';
3
+ import { composeMiddleware, normalizeMiddleware } from './middleware.js';
4
+ export function createHandlerFrom({ template, mod, cfg, middleware, preload }) {
5
+ const withPreload = (html, pathname) => preload ? injectPreload(html, preload(pathname)) : html;
6
+ // Streaming when the entry exports `renderStream` (a body stream): flush the split
7
+ // template head first, pipe the body, then the tail. Else inject the string render.
8
+ let core;
9
+ if (!cfg.ssr) {
10
+ // SPA: the page is the static shell; the client entry renders it. Server functions +
11
+ // middleware still run through this handler — there's just no SSR render pass. The
12
+ // path is still known, though, and the route it names still loads its own chunk, so
13
+ // the shell can say so and save the client a round trip it would otherwise start
14
+ // only after the router had booted.
15
+ core = createRequestHandler(async (url) => withPreload(template, url.split('?')[0]));
16
+ }
17
+ else if (typeof mod.renderStream === 'function') {
18
+ core = async (request) => {
19
+ const url = new URL(request.url);
20
+ // Split per request rather than once: the preload links belong in the head, and
21
+ // the head is the part that flushes before the body is known — which is the whole
22
+ // reason streaming wants them, since the browser can start fetching the route's
23
+ // chunk while the server is still awaiting data for it.
24
+ const { head, tail } = splitTemplate(withPreload(template, url.pathname), cfg.mountId);
25
+ const body = await mod.renderStream(url.pathname + url.search, request);
26
+ return streamDocument(head, body, tail);
27
+ };
28
+ }
29
+ else {
30
+ const render = mod.render ?? mod.default;
31
+ core = createRequestHandler(async (url, request) =>
32
+ // `url` here is `pathname + search`; matching only ever wants the path.
33
+ injectAppAndHead(withPreload(template, url.split('?')[0]), await render(url, request), cfg.mountId));
34
+ }
35
+ // Server-function RPC dispatch. The entry-server module re-exports the dispatcher, so
36
+ // it shares the registry that the app's "use server" modules populated (a separate
37
+ // import would have an empty registry).
38
+ if (typeof mod.handleServerFn === 'function' && typeof mod.isServerFnRequest === 'function') {
39
+ const render = core;
40
+ core = async (request) => mod.isServerFnRequest(request) ? mod.handleServerFn(request) : render(request);
41
+ }
42
+ // File-based API routes, re-exported by the entry from `@fluixi/start/api-routes`.
43
+ if (typeof mod.handleApiRequest === 'function' && typeof mod.isApiRequest === 'function') {
44
+ const next = core;
45
+ core = async (request) => mod.isApiRequest(request) ? mod.handleApiRequest(request) : next(request);
46
+ }
47
+ if (!middleware)
48
+ return guardHandler(core);
49
+ return guardHandler(composeMiddleware(normalizeMiddleware(middleware), core));
50
+ }
package/dist/index.d.ts CHANGED
@@ -8,15 +8,13 @@
8
8
  export { defineConfig, resolveConfig, loadConfig } from './config.js';
9
9
  export type { FluixiConfig, ResolvedConfig } from './config.js';
10
10
  export { html, svg } from '@fluixi/core';
11
- export { dev } from './commands/dev.js';
12
- export { build } from './commands/build.js';
13
- export { start } from './commands/start.js';
14
- export { prerender } from './commands/prerender.js';
15
- export { injectApp, injectAppAndHead, splitTemplate } from './internal.js';
11
+ export { injectApp, injectAppAndHead, splitTemplate } from './document.js';
16
12
  export { createRequestHandler, toNodeHandler, nodeToRequest, sendResponse, serveNode, streamDocument, withStaticFiles, } from './handler.js';
17
13
  export type { FetchHandler, RenderFn, StreamRenderFn, ServeNodeOptions } from './handler.js';
18
- export { createProdHandler, nodeAdapter, webAdapter } from './adapter.js';
19
- export type { Adapter, AdapterContext } from './adapter.js';
14
+ export { createProdHandler, nodeAdapter, webAdapter, cloudflareAdapter, netlifyAdapter, vercelAdapter, ssrBuildOptions, } from './adapter.js';
15
+ export type { Adapter, AdapterContext, AdapterBuildContext } from './adapter.js';
16
+ export { createHandlerFrom } from './handler-core.js';
17
+ export type { HandlerParts, ServerModule } from './handler-core.js';
20
18
  export { defineMiddleware, composeMiddleware, normalizeMiddleware } from './middleware.js';
21
19
  export type { Middleware, MiddlewareNext } from './middleware.js';
22
20
  export { addInterceptor, clearInterceptors, fetchWithInterceptors } from './interceptors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,YAAY,EACZ,SAAS,EACT,cAAc,EACd,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1E,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3F,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAI9G,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAOzC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,YAAY,EACZ,SAAS,EACT,cAAc,EACd,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE7F,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3F,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAI9G,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -9,15 +9,18 @@ export { defineConfig, resolveConfig, loadConfig } from './config.js';
9
9
  // The html``/svg`` template tags — so a meta-framework app can author without JSX:
10
10
  // `import { html } from '@fluixi/start'`. Inert markers; the compiler lowers them.
11
11
  export { html, svg } from '@fluixi/core';
12
- export { dev } from './commands/dev.js';
13
- export { build } from './commands/build.js';
14
- export { start } from './commands/start.js';
15
- export { prerender } from './commands/prerender.js';
16
- export { injectApp, injectAppAndHead, splitTemplate } from './internal.js';
12
+ // The commands live at `@fluixi/start/commands`. Re-exporting them here would put
13
+ // the dev server and the bundler behind every runtime import of this package —
14
+ // and a deploy adapter, which inlines everything its entry can reach, would carry
15
+ // vite into a worker.
16
+ // From the leaf, not internal.js: that one reaches the dev server's env loader,
17
+ // and with it vite, which has no business in a runtime import of this package.
18
+ export { injectApp, injectAppAndHead, splitTemplate } from './document.js';
17
19
  // The runtime-neutral request layer — the seam adapters (edge/worker/Bun/Deno) build on.
18
20
  export { createRequestHandler, toNodeHandler, nodeToRequest, sendResponse, serveNode, streamDocument, withStaticFiles, } from './handler.js';
19
21
  // Deploy adapters: the portable prod handler + the node/web reference adapters.
20
- export { createProdHandler, nodeAdapter, webAdapter } from './adapter.js';
22
+ export { createProdHandler, nodeAdapter, webAdapter, cloudflareAdapter, netlifyAdapter, vercelAdapter, ssrBuildOptions, } from './adapter.js';
23
+ export { createHandlerFrom } from './handler-core.js';
21
24
  // Request middleware — a `(request, next) => Response` chain run before render.
22
25
  export { defineMiddleware, composeMiddleware, normalizeMiddleware } from './middleware.js';
23
26
  // HTTP interceptors — request/response/error pipeline around fetch (server-fn RPC + app fetch).
@@ -1,5 +1,4 @@
1
1
  import type { ResolvedConfig } from './config.js';
2
- import type { FetchHandler } from './handler.js';
3
2
  /** The virtual module that registers every `"use server"` fn + re-exports the dispatcher. */
4
3
  export declare const SERVER_FNS_VMOD = "virtual:fluixi-server-fns";
5
4
  /** The virtual module exposing auto-discovered translations: `{ messages, locales, defaultLocale }`. */
@@ -50,34 +49,7 @@ export declare function startTypesPlugin(cfg: ResolvedConfig): any;
50
49
  * bits only (optimizeDeps, ssr.noExternal via config, etc.).
51
50
  */
52
51
  export declare function fluixiPlugins(cfg: ResolvedConfig): Promise<any[]>;
53
- /**
54
- * Inject server-rendered app HTML into the template's mount element
55
- * (`<div id="root"></div>`). Falls back to a `<!--ssr-outlet-->` marker, then to
56
- * just before `</body>`.
57
- */
58
- export declare function injectApp(template: string, appHtml: string, mountId?: string): string;
59
- /**
60
- * Split the template at the mount point for streaming: `head` is everything up to and
61
- * including the open mount tag (flushed first so the browser fetches assets while the
62
- * server awaits data); `tail` is the close tag onward (client script + `</body>`).
63
- * Mirrors injectApp's three cases (mount div, `<!--ssr-outlet-->`, before `</body>`).
64
- */
65
- export declare function splitTemplate(template: string, mountId?: string): {
66
- head: string;
67
- tail: string;
68
- };
69
- /**
70
- * Inject the rendered app into the mount point AND lift any @fluixi/head metadata (carried back as
71
- * a marker by renderRequestAsync) into <head> + <html>. If the page set a title, the template's own
72
- * <title> is dropped so there's only one. No marker → identical to injectApp.
73
- */
74
- export declare function injectAppAndHead(template: string, rendered: string, mountId?: string): string;
75
- /**
76
- * Wrap a fetch handler so a thrown/rejected error becomes a 500 instead of an
77
- * unhandled rejection. `createRequestHandler` already guards the renderer; this
78
- * covers the middleware chain that wraps it. `onError` lets dev map the stack first.
79
- */
80
- export declare function guardHandler(handler: FetchHandler, onError?: (e: unknown) => void): FetchHandler;
52
+ export { injectApp, splitTemplate, injectAppAndHead, guardHandler, } from './document.js';
81
53
  /**
82
54
  * A render fires the route's data fetches eagerly; if the backend is down those
83
55
  * promises can reject AFTER we've already responded, with nothing awaiting them —
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,8BAA8B,CAAC;AAE3D,wGAAwG;AACxG,eAAO,MAAM,SAAS,wBAAwB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,GAAG,CAoCrG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7E;AA6DD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CA4BzD;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAoBvE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CASrF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwBhG;AASD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAW7F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,GAC7B,YAAY,CAYd;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CASzC"}
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,8BAA8B,CAAC;AAE3D,wGAAwG;AACxG,eAAO,MAAM,SAAS,wBAAwB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,GAAG,CAoCrG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7E;AA6DD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CA0BzD;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAoBvE;AAED,OAAO,EACL,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,YAAY,GACb,MAAM,eAAe,CAAC;AAIvB;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CASzC"}
package/dist/internal.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { resolve as resolvePath, relative as relativePath } from 'node:path';
3
- import { extractHeadMarker } from '@fluixi/head';
4
3
  /** The virtual module that registers every `"use server"` fn + re-exports the dispatcher. */
5
4
  export const SERVER_FNS_VMOD = 'virtual:fluixi-server-fns';
6
5
  /** The virtual module exposing auto-discovered translations: `{ messages, locales, defaultLocale }`. */
@@ -142,18 +141,17 @@ export function startTypesPlugin(cfg) {
142
141
  root = config.root || root;
143
142
  },
144
143
  buildStart() {
145
- const blocks = [
146
- `declare module 'virtual:fluixi-server-fns' {`,
147
- ` export function isServerFnRequest(request: Request): boolean;`,
148
- ` export function handleServerFn(request: Request): Promise<Response>;`,
149
- `}`,
150
- `declare module 'virtual:fluixi-api' {`,
151
- ` export function isApiRequest(request: Request): boolean;`,
152
- ` export function handleApiRequest(request: Request): Promise<Response>;`,
153
- `}`,
154
- ];
144
+ // Only i18n is generated now. `virtual:fluixi-server-fns` and `virtual:fluixi-api`
145
+ // have fixed signatures, so they moved to real package entries
146
+ // (`@fluixi/start/server-fns`, `@fluixi/start/api-routes`) whose types ship with the
147
+ // package. i18n cannot follow: its `messages` type is derived from the app's own JSON
148
+ // (`typeof import('./i18n/en.json')`) and its `locales` is a literal tuple — that is
149
+ // what makes keys autocomplete, and no static entry can express it.
150
+ const blocks = [];
155
151
  if (cfg.i18n)
156
152
  blocks.push(...i18nTypeBlock(root, cfg.i18n));
153
+ if (blocks.length === 0)
154
+ return; // no i18n configured — nothing left to declare
157
155
  const content = `// Auto-generated by @fluixi/start — do not edit.\n${blocks.join('\n')}\n`;
158
156
  const file = resolvePath(root, 'src/fluixi-start-env.d.ts');
159
157
  try {
@@ -196,97 +194,7 @@ export async function fluixiPlugins(cfg) {
196
194
  plugins.push(startTypesPlugin(cfg));
197
195
  return plugins;
198
196
  }
199
- /**
200
- * Inject server-rendered app HTML into the template's mount element
201
- * (`<div id="root"></div>`). Falls back to a `<!--ssr-outlet-->` marker, then to
202
- * just before `</body>`.
203
- */
204
- export function injectApp(template, appHtml, mountId = 'root') {
205
- const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
206
- if (mount.test(template)) {
207
- return template.replace(mount, `<div id="${mountId}">${appHtml}</div>`);
208
- }
209
- if (template.includes('<!--ssr-outlet-->')) {
210
- return template.replace('<!--ssr-outlet-->', appHtml);
211
- }
212
- return template.replace('</body>', `<div id="${mountId}">${appHtml}</div></body>`);
213
- }
214
- /**
215
- * Split the template at the mount point for streaming: `head` is everything up to and
216
- * including the open mount tag (flushed first so the browser fetches assets while the
217
- * server awaits data); `tail` is the close tag onward (client script + `</body>`).
218
- * Mirrors injectApp's three cases (mount div, `<!--ssr-outlet-->`, before `</body>`).
219
- */
220
- export function splitTemplate(template, mountId = 'root') {
221
- const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
222
- const m = template.match(mount);
223
- if (m && m.index !== undefined) {
224
- return {
225
- head: template.slice(0, m.index) + `<div id="${mountId}">`,
226
- tail: `</div>` + template.slice(m.index + m[0].length),
227
- };
228
- }
229
- const outlet = template.indexOf('<!--ssr-outlet-->');
230
- if (outlet !== -1) {
231
- return {
232
- head: template.slice(0, outlet),
233
- tail: template.slice(outlet + '<!--ssr-outlet-->'.length),
234
- };
235
- }
236
- const body = template.indexOf('</body>');
237
- if (body !== -1) {
238
- return {
239
- head: template.slice(0, body) + `<div id="${mountId}">`,
240
- tail: `</div>` + template.slice(body),
241
- };
242
- }
243
- return { head: template, tail: '' };
244
- }
245
- /** Set/replace an attribute on the template's <html> tag (e.g. lang). */
246
- function setHtmlAttr(html, name, value) {
247
- const existing = new RegExp(`(<html\\b[^>]*?)\\s${name}="[^"]*"`, 'i');
248
- if (existing.test(html))
249
- return html.replace(existing, `$1 ${name}="${value}"`);
250
- return html.replace(/<html\b/i, `<html ${name}="${value}"`);
251
- }
252
- /**
253
- * Inject the rendered app into the mount point AND lift any @fluixi/head metadata (carried back as
254
- * a marker by renderRequestAsync) into <head> + <html>. If the page set a title, the template's own
255
- * <title> is dropped so there's only one. No marker → identical to injectApp.
256
- */
257
- export function injectAppAndHead(template, rendered, mountId = 'root') {
258
- const { head, body } = extractHeadMarker(rendered);
259
- let html = injectApp(template, body, mountId);
260
- if (head) {
261
- if (head.headHtml) {
262
- if (/<title[\s>]/i.test(head.headHtml))
263
- html = html.replace(/<title>[\s\S]*?<\/title>/i, '');
264
- html = html.replace('</head>', `${head.headHtml}</head>`);
265
- }
266
- for (const [k, v] of Object.entries(head.htmlAttrs))
267
- html = setHtmlAttr(html, k, v);
268
- }
269
- return html;
270
- }
271
- /**
272
- * Wrap a fetch handler so a thrown/rejected error becomes a 500 instead of an
273
- * unhandled rejection. `createRequestHandler` already guards the renderer; this
274
- * covers the middleware chain that wraps it. `onError` lets dev map the stack first.
275
- */
276
- export function guardHandler(handler, onError) {
277
- return async (request) => {
278
- try {
279
- return await handler(request);
280
- }
281
- catch (e) {
282
- onError?.(e);
283
- return new Response(String(e?.stack || e), {
284
- status: 500,
285
- headers: { 'content-type': 'text/plain; charset=utf-8' },
286
- });
287
- }
288
- };
289
- }
197
+ export { injectApp, splitTemplate, injectAppAndHead, guardHandler, } from './document.js';
290
198
  let _guardsInstalled = false;
291
199
  /**
292
200
  * A render fires the route's data fetches eagerly; if the backend is down those
@@ -0,0 +1,5 @@
1
+ /** URL pattern → the built files a request for it needs. */
2
+ export type RouteAssets = Record<string, string[]>;
3
+ /** Build the lookup a handler applies per request. */
4
+ export declare function createPreloader(assets: RouteAssets | undefined): (pathname: string) => string[];
5
+ //# sourceMappingURL=preload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../src/preload.ts"],"names":[],"mappings":"AAaA,4DAA4D;AAC5D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAEnD,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAAE,CAc/F"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Which built files a URL needs.
3
+ *
4
+ * Keyed by URL pattern, not module id: the server only has a path, and the route table
5
+ * that would map one to the other lives inside the bundle it is serving —
6
+ * `@fluixi/core/routes` resolves to a stub outside a build. `fluixi build` does the
7
+ * join, with each route's layouts folded in.
8
+ *
9
+ * A miss is silent: no file routes, no match, or an older build yields no files and an
10
+ * unchanged page.
11
+ */
12
+ import { matchRoutes } from '@fluixi/core/router-next';
13
+ /** Build the lookup a handler applies per request. */
14
+ export function createPreloader(assets) {
15
+ const patterns = Object.keys(assets ?? {});
16
+ if (!patterns.length)
17
+ return () => [];
18
+ // Through the router, not a string compare, so `/blog/:id` and wildcards resolve as
19
+ // they do when the app routes the request.
20
+ const routes = patterns.map((path) => ({ path, meta: { path } }));
21
+ return (pathname) => {
22
+ const matched = matchRoutes(routes, pathname)?.matched;
23
+ const leaf = matched?.[matched.length - 1];
24
+ const pattern = leaf?.route.meta?.path;
25
+ return pattern ? (assets[pattern] ?? []) : [];
26
+ };
27
+ }