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

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 (41) hide show
  1. package/dist/adapter.d.ts +77 -7
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +131 -44
  4. package/dist/adapters/entry.d.ts +49 -0
  5. package/dist/adapters/entry.d.ts.map +1 -0
  6. package/dist/adapters/entry.js +122 -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 +207 -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 +73 -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 +30 -0
  23. package/dist/document.d.ts.map +1 -0
  24. package/dist/document.js +100 -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 +36 -0
  32. package/dist/handler-core.d.ts.map +1 -0
  33. package/dist/handler-core.js +40 -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/tsconfig.lib.tsbuildinfo +1 -1
  41. package/package.json +39 -6
package/dist/adapter.d.ts CHANGED
@@ -2,31 +2,101 @@ import type { FluixiConfig, ResolvedConfig } from './config.js';
2
2
  import { type FetchHandler } from './handler.js';
3
3
  /**
4
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.
5
+ * dist/client/index.html), reading both off disk. Runtime-neutral once assembled, but
6
+ * the assembly is not: a worker has no filesystem, so a deploy adapter generates an
7
+ * entry that imports the same pieces statically and calls `createHandlerFrom`.
7
8
  */
8
9
  export declare function createProdHandler(config?: FluixiConfig): Promise<FetchHandler>;
10
+ /**
11
+ * The shell the server renders into.
12
+ *
13
+ * `dist/client/index.html` is it, until the app prerenders `/` — then that path holds a
14
+ * rendered page and the build stashes the shell in `dist/server/template.html` first.
15
+ * Rendering into the page instead puts the home page's markup and head around every
16
+ * SSR response, and leaves hydration two mount elements to choose between.
17
+ */
18
+ export declare function readTemplate(clientDir: string, serverDir: string): Promise<string>;
9
19
  export interface AdapterContext {
10
20
  handler: FetchHandler;
11
21
  cfg: ResolvedConfig;
12
22
  /** Built client assets dir (dist/client). */
13
23
  clientDir: string;
14
24
  }
25
+ /** What an adapter is given after `fluixi build` has produced its output. */
26
+ export interface AdapterBuildContext {
27
+ cfg: ResolvedConfig;
28
+ /** App root. */
29
+ root: string;
30
+ /** Client assets and prerendered HTML (dist/client). */
31
+ clientDir: string;
32
+ /** The built server entry and middleware (dist/server). */
33
+ serverDir: string;
34
+ /** False for a fully static SPA, which ships no server entry to wrap. */
35
+ hasServer: boolean;
36
+ /**
37
+ * Bundle a generated entry into one self-contained file. Adapters emit a small
38
+ * platform entry — the worker's `fetch` export, the function's handler — and this
39
+ * pulls the server bundle and the template into it, because none of these
40
+ * platforms can resolve a module or read a file at runtime.
41
+ */
42
+ bundleEntry(source: string, outFile: string): Promise<void>;
43
+ }
15
44
  /**
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.
45
+ * A deploy target.
46
+ *
47
+ * The runtime-neutral fetch handler is the portable core; an adapter packages it for a
48
+ * platform — how the server is bundled (`bundle`), how it is served in development or
49
+ * on a long-running host (`serve`), and what a deploy actually needs on disk
50
+ * (`build`): a worker entry, a function manifest, a routing config.
21
51
  */
22
52
  export interface Adapter {
23
53
  name: string;
54
+ /**
55
+ * How `fluixi build` bundles the framework into the server output. The choice is a
56
+ * property of the deploy target, not of the app:
57
+ *
58
+ * - `'external'` — `@fluixi/*` stays as bare imports, resolved from `node_modules` at
59
+ * runtime. Smaller output, faster build; requires the dependencies to be installed
60
+ * next to it. Right for a long-running Node/Bun/Deno process.
61
+ * - `'inline'` — everything is bundled into one self-contained file that resolves
62
+ * nothing at runtime. Right for edge/serverless, where there is no `node_modules`.
63
+ *
64
+ * Unset leaves Vite's own heuristic alone (installed deps external, linked ones bundled).
65
+ */
66
+ bundle?: 'external' | 'inline';
24
67
  serve(ctx: AdapterContext): Promise<void> | {
25
68
  fetch: FetchHandler;
26
69
  };
70
+ /**
71
+ * Emit the platform's output layout. Runs after the client and server builds and
72
+ * after prerendering, so everything it copies or wraps already exists.
73
+ */
74
+ build?(ctx: AdapterBuildContext): Promise<void> | void;
27
75
  }
28
76
  /** Reference filesystem adapter — Node `http` + static files from dist/client. */
29
77
  export declare const nodeAdapter: Adapter;
78
+ export { cloudflareAdapter, netlifyAdapter, vercelAdapter } from './adapters/platforms.js';
30
79
  /** Reference edge/web adapter — export `{ fetch }`; the platform serves assets. */
31
80
  export declare const webAdapter: Adapter;
81
+ /**
82
+ * The Vite `ssr` options for a build, given the configured adapter's `bundle` mode.
83
+ * Shared by the server and middleware builds so the two cannot disagree.
84
+ *
85
+ * `'external'` lists the app's own `@fluixi/*` dependencies rather than externalizing
86
+ * everything: under a strict `node_modules` layout (pnpm) only what the app itself
87
+ * declares is resolvable from its root, so externalizing a merely transitive package
88
+ * would produce a bundle that imports something Node cannot find.
89
+ *
90
+ * Each package is listed with every subpath its `exports` map publishes, because Vite
91
+ * only honours an *exact* id in `ssr.external`. A bare package name falls through to a
92
+ * resolvability probe that does not externalize deep imports here, and the result is the
93
+ * worst of both worlds: `@fluixi/core` an import, `@fluixi/core/router-next` copied into
94
+ * the bundle — two routers, two sets of module state, in one server.
95
+ *
96
+ * `ssrNoExternal` always wins — it is the app saying "this one must be bundled".
97
+ */
98
+ export declare function ssrBuildOptions(cfg: ResolvedConfig): {
99
+ noExternal?: string[] | true;
100
+ external?: string[];
101
+ };
32
102
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAA8B,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAG7E;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiBxF;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,cAAc,CAAC;IACpB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,cAAc,CAAC;IACpB,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC/B,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG;QAAE,KAAK,EAAE,YAAY,CAAA;KAAE,CAAC;IACpE;;;OAGG;IACH,KAAK,CAAC,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED,kFAAkF;AAClF,eAAO,MAAM,WAAW,EAAE,OAWzB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE3F,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAAE,OAQxB,CAAC;AAUF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG;IACpD,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAoBA"}
package/dist/adapter.js CHANGED
@@ -1,67 +1,51 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { resolve, join } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { resolve, join, dirname } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import { createRequire } from 'node:module';
5
6
  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';
7
+ import { serveNode, withStaticFiles } from './handler.js';
8
+ import { createHandlerFrom } from './handler-core.js';
9
9
  /**
10
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.
11
+ * dist/client/index.html), reading both off disk. Runtime-neutral once assembled, but
12
+ * the assembly is not: a worker has no filesystem, so a deploy adapter generates an
13
+ * entry that imports the same pieces statically and calls `createHandlerFrom`.
13
14
  */
14
15
  export async function createProdHandler(config = {}) {
15
16
  const cfg = resolveConfig(config);
16
17
  const clientDir = resolve(cfg.root, 'dist/client');
17
- const template = await readFile(join(clientDir, 'index.html'), 'utf-8');
18
+ const template = await readTemplate(clientDir, resolve(cfg.root, 'dist/server'));
18
19
  const serverEntry = resolve(cfg.root, 'dist/server', 'entry-server.js');
19
20
  // SPA apps may ship no server entry (or one only for server functions) — import if present.
20
21
  const mod = existsSync(serverEntry)
21
22
  ? await import(/* @vite-ignore */ pathToFileURL(serverEntry).href)
22
23
  : {};
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
24
  // Optional request middleware (src/middleware.ts → dist/server/middleware.js).
56
25
  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));
26
+ const middleware = existsSync(mwFile)
27
+ ? await import(/* @vite-ignore */ pathToFileURL(mwFile).href)
28
+ : undefined;
29
+ return createHandlerFrom({ template, mod, cfg, middleware });
30
+ }
31
+ /**
32
+ * The shell the server renders into.
33
+ *
34
+ * `dist/client/index.html` is it, until the app prerenders `/` — then that path holds a
35
+ * rendered page and the build stashes the shell in `dist/server/template.html` first.
36
+ * Rendering into the page instead puts the home page's markup and head around every
37
+ * SSR response, and leaves hydration two mount elements to choose between.
38
+ */
39
+ export async function readTemplate(clientDir, serverDir) {
40
+ const stashed = join(serverDir, 'template.html');
41
+ return readFile(existsSync(stashed) ? stashed : join(clientDir, 'index.html'), 'utf-8');
61
42
  }
62
43
  /** Reference filesystem adapter — Node `http` + static files from dist/client. */
63
44
  export const nodeAdapter = {
64
45
  name: 'node',
46
+ // A Node server runs from the app directory, so node_modules is right there: keep the
47
+ // framework as imports instead of copying it into the bundle.
48
+ bundle: 'external',
65
49
  async serve({ handler, cfg, clientDir }) {
66
50
  serveNode(withStaticFiles(handler, clientDir), {
67
51
  port: cfg.port,
@@ -69,10 +53,113 @@ export const nodeAdapter = {
69
53
  });
70
54
  },
71
55
  };
56
+ export { cloudflareAdapter, netlifyAdapter, vercelAdapter } from './adapters/platforms.js';
72
57
  /** Reference edge/web adapter — export `{ fetch }`; the platform serves assets. */
73
58
  export const webAdapter = {
74
59
  name: 'web',
60
+ // An edge/serverless runtime uploads a file, not an install: nothing may be left to
61
+ // resolve at runtime, so everything goes in the bundle.
62
+ bundle: 'inline',
75
63
  serve({ handler }) {
76
64
  return { fetch: handler };
77
65
  },
78
66
  };
67
+ /**
68
+ * Subpaths that only look like package entries: a Vite plugin replaces them with the
69
+ * app's generated route/server-fn/API tables, and the real file in the package is a stub
70
+ * that throws. Externalizing one would ship the stub. The plugins run with
71
+ * `enforce: 'pre'` so they win anyway — this list is the second lock on that door.
72
+ */
73
+ const GENERATED_SUBPATHS = new Set(['./routes', './server-fns', './api-routes']);
74
+ /**
75
+ * The Vite `ssr` options for a build, given the configured adapter's `bundle` mode.
76
+ * Shared by the server and middleware builds so the two cannot disagree.
77
+ *
78
+ * `'external'` lists the app's own `@fluixi/*` dependencies rather than externalizing
79
+ * everything: under a strict `node_modules` layout (pnpm) only what the app itself
80
+ * declares is resolvable from its root, so externalizing a merely transitive package
81
+ * would produce a bundle that imports something Node cannot find.
82
+ *
83
+ * Each package is listed with every subpath its `exports` map publishes, because Vite
84
+ * only honours an *exact* id in `ssr.external`. A bare package name falls through to a
85
+ * resolvability probe that does not externalize deep imports here, and the result is the
86
+ * worst of both worlds: `@fluixi/core` an import, `@fluixi/core/router-next` copied into
87
+ * the bundle — two routers, two sets of module state, in one server.
88
+ *
89
+ * `ssrNoExternal` always wins — it is the app saying "this one must be bundled".
90
+ */
91
+ export function ssrBuildOptions(cfg) {
92
+ const noExternal = cfg.ssrNoExternal.length ? cfg.ssrNoExternal : undefined;
93
+ const mode = cfg.adapter?.bundle;
94
+ if (mode === 'inline')
95
+ return { noExternal: true };
96
+ if (mode !== 'external')
97
+ return { noExternal };
98
+ const external = frameworkDeps(cfg.root)
99
+ .filter((name) => !cfg.ssrNoExternal.includes(name))
100
+ .flatMap((name) => withSubpaths(cfg.root, name));
101
+ if (!external.length) {
102
+ // Silently building an inlined bundle here would look identical to a working one
103
+ // until someone measures it, so say why nothing was externalized.
104
+ console.warn(` fluixi: adapter "${cfg.adapter?.name}" builds an externalized server, but no ` +
105
+ `@fluixi/* dependency was found in ${join(cfg.root, 'package.json')} — bundling everything.`);
106
+ }
107
+ return { noExternal, external: external.length ? external : undefined };
108
+ }
109
+ /** The app's own `@fluixi/*` dependencies, read from its package.json. */
110
+ function frameworkDeps(root) {
111
+ const pkg = readPackage(join(root, 'package.json'));
112
+ if (!pkg)
113
+ return [];
114
+ const declared = { ...pkg.dependencies, ...pkg.peerDependencies, ...pkg.optionalDependencies };
115
+ return Object.keys(declared).filter((id) => id.startsWith('@fluixi/'));
116
+ }
117
+ /**
118
+ * `['@fluixi/core', '@fluixi/core/rx', …]` — the package plus every static subpath it
119
+ * exports. Wildcard entries (`./lib/*`) cannot be enumerated and are left to Vite; an
120
+ * app importing through one gets it bundled, which is the safe direction.
121
+ */
122
+ function withSubpaths(root, name) {
123
+ const exports = resolvePackage(root, name)?.exports;
124
+ if (!exports || typeof exports !== 'object')
125
+ return [name];
126
+ const ids = [name];
127
+ for (const key of Object.keys(exports)) {
128
+ if (!key.startsWith('./') || key.includes('*'))
129
+ continue;
130
+ if (key === './package.json' || GENERATED_SUBPATHS.has(key))
131
+ continue;
132
+ ids.push(`${name}/${key.slice(2)}`);
133
+ }
134
+ return ids;
135
+ }
136
+ /**
137
+ * A dependency's package.json, read from where the app would resolve it. `require.resolve`
138
+ * covers the normal case; packages that do not export `./package.json` (a package may
139
+ * legitimately keep it private) need the node_modules walk, which also handles hoisting.
140
+ */
141
+ function resolvePackage(root, name) {
142
+ try {
143
+ return readPackage(createRequire(join(root, 'index.js')).resolve(`${name}/package.json`));
144
+ }
145
+ catch {
146
+ /* not exported — fall through */
147
+ }
148
+ for (let dir = root;; dir = dirname(dir)) {
149
+ const pkg = readPackage(join(dir, 'node_modules', name, 'package.json'));
150
+ if (pkg)
151
+ return pkg;
152
+ if (dirname(dir) === dir)
153
+ return undefined;
154
+ }
155
+ }
156
+ function readPackage(file) {
157
+ if (!existsSync(file))
158
+ return undefined;
159
+ try {
160
+ return JSON.parse(readFileSync(file, 'utf-8'));
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The source of the entry a deploy adapter bundles.
3
+ *
4
+ * Every one of these platforms runs the same handler; they differ only in what they
5
+ * expect the module to export and how static assets reach the request. So the entry
6
+ * is generated rather than shipped: it imports the built server and the template,
7
+ * hands both to `createHandlerFrom`, and wraps the result in whatever shape the
8
+ * platform invokes.
9
+ *
10
+ * The template is inlined as a string. Reading `index.html` at runtime is what
11
+ * `createProdHandler` does on Node and what none of these can do.
12
+ */
13
+ export interface EntrySource {
14
+ /** Path to the built server entry, relative to the generated file. */
15
+ serverEntry: string;
16
+ /** Path to the built middleware, or null when the app has none. */
17
+ middleware: string | null;
18
+ /** `dist/client/index.html`, inlined. */
19
+ template: string;
20
+ ssr: boolean;
21
+ mountId: string;
22
+ /** Paths that exist as prerendered HTML, so the platform can answer them itself. */
23
+ prerendered: string[];
24
+ }
25
+ /**
26
+ * Cloudflare Pages, advanced mode: a `_worker.js` at the root of the published
27
+ * directory takes every request. Pages stops serving static files itself in that
28
+ * mode, so assets go through the ASSETS binding, and `_routes.json` keeps the worker
29
+ * out of the hot path for anything under a hashed asset directory.
30
+ */
31
+ export declare function cloudflareEntry(source: EntrySource): string;
32
+ /**
33
+ * Netlify Functions v2: a default-exported fetch handler. `config.path` claims every
34
+ * route; Netlify still serves a matching static file first, so the function only sees
35
+ * what the CDN could not answer.
36
+ */
37
+ export declare function netlifyEntry(source: EntrySource): string;
38
+ /**
39
+ * Vercel Build Output API v3, Node runtime.
40
+ *
41
+ * The `Nodejs` launcher invokes the default export as `(req, res)` — Node's own
42
+ * signature, not a fetch handler — so the entry has to bridge the two itself.
43
+ * Exporting `(request) => Response` looks right and hangs: the launcher passes an
44
+ * `IncomingMessage`, nothing ever writes to `res`, and the request sits there until
45
+ * the platform times it out. Node stays the runtime rather than edge because a server
46
+ * function is allowed to use Node APIs here.
47
+ */
48
+ export declare function vercelEntry(source: EntrySource): string;
49
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/adapters/entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAqBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAsB3D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAOxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAuCvD"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The source of the entry a deploy adapter bundles.
3
+ *
4
+ * Every one of these platforms runs the same handler; they differ only in what they
5
+ * expect the module to export and how static assets reach the request. So the entry
6
+ * is generated rather than shipped: it imports the built server and the template,
7
+ * hands both to `createHandlerFrom`, and wraps the result in whatever shape the
8
+ * platform invokes.
9
+ *
10
+ * The template is inlined as a string. Reading `index.html` at runtime is what
11
+ * `createProdHandler` does on Node and what none of these can do.
12
+ */
13
+ /** Imports and the handler, shared by every platform entry. */
14
+ function preamble({ serverEntry, middleware, template, ssr, mountId }) {
15
+ return [
16
+ `import { createHandlerFrom } from '@fluixi/start/handler-core';`,
17
+ `import * as server from ${JSON.stringify(serverEntry)};`,
18
+ middleware ? `import * as middleware from ${JSON.stringify(middleware)};` : '',
19
+ ``,
20
+ `const template = ${JSON.stringify(template)};`,
21
+ `const handler = createHandlerFrom({`,
22
+ ` template,`,
23
+ ` mod: server,`,
24
+ ` cfg: { ssr: ${ssr}, mountId: ${JSON.stringify(mountId)} },`,
25
+ middleware ? ` middleware,` : '',
26
+ `});`,
27
+ ]
28
+ .filter(Boolean)
29
+ .join('\n');
30
+ }
31
+ /**
32
+ * Cloudflare Pages, advanced mode: a `_worker.js` at the root of the published
33
+ * directory takes every request. Pages stops serving static files itself in that
34
+ * mode, so assets go through the ASSETS binding, and `_routes.json` keeps the worker
35
+ * out of the hot path for anything under a hashed asset directory.
36
+ */
37
+ export function cloudflareEntry(source) {
38
+ return `${preamble(source)}
39
+
40
+ const prerendered = new Set(${JSON.stringify(source.prerendered)});
41
+
42
+ export default {
43
+ async fetch(request, env, ctx) {
44
+ // Static first, but only for what is actually static. The ASSETS binding answers
45
+ // "/" with index.html whether that file is a prerendered page or the empty shell
46
+ // the server renders into, so asking it about every path costs an SSR app its
47
+ // home page. A path with a file extension is a file; a path we prerendered is a
48
+ // page; everything else belongs to the handler — the rule the filesystem adapter
49
+ // already applies in \`withStaticFiles\`.
50
+ const { pathname } = new URL(request.url);
51
+ if (env && env.ASSETS && (prerendered.has(pathname) || /\\.[^/]+$/.test(pathname))) {
52
+ const asset = await env.ASSETS.fetch(request.clone());
53
+ if (asset.status !== 404) return asset;
54
+ }
55
+ return handler(request, { env, ctx });
56
+ },
57
+ };
58
+ `;
59
+ }
60
+ /**
61
+ * Netlify Functions v2: a default-exported fetch handler. `config.path` claims every
62
+ * route; Netlify still serves a matching static file first, so the function only sees
63
+ * what the CDN could not answer.
64
+ */
65
+ export function netlifyEntry(source) {
66
+ return `${preamble(source)}
67
+
68
+ export default async (request, context) => handler(request, { context });
69
+
70
+ export const config = { path: '/*', preferStatic: true };
71
+ `;
72
+ }
73
+ /**
74
+ * Vercel Build Output API v3, Node runtime.
75
+ *
76
+ * The `Nodejs` launcher invokes the default export as `(req, res)` — Node's own
77
+ * signature, not a fetch handler — so the entry has to bridge the two itself.
78
+ * Exporting `(request) => Response` looks right and hangs: the launcher passes an
79
+ * `IncomingMessage`, nothing ever writes to `res`, and the request sits there until
80
+ * the platform times it out. Node stays the runtime rather than edge because a server
81
+ * function is allowed to use Node APIs here.
82
+ */
83
+ export function vercelEntry(source) {
84
+ return `${preamble(source)}
85
+
86
+ export default async (req, res) => {
87
+ const proto = req.headers['x-forwarded-proto'] || 'https';
88
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
89
+
90
+ const headers = new Headers();
91
+ for (const [name, value] of Object.entries(req.headers)) {
92
+ if (Array.isArray(value)) for (const one of value) headers.append(name, one);
93
+ else if (value !== undefined) headers.set(name, value);
94
+ }
95
+
96
+ // A body has to be read off the stream before it can be handed to Request; only
97
+ // GET and HEAD are guaranteed not to have one.
98
+ let body;
99
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
100
+ const chunks = [];
101
+ for await (const chunk of req) chunks.push(chunk);
102
+ if (chunks.length) body = new Blob(chunks);
103
+ }
104
+
105
+ const response = await handler(
106
+ new Request(new URL(req.url, \`\${proto}://\${host}\`), { method: req.method, headers, body }),
107
+ );
108
+
109
+ res.statusCode = response.status;
110
+ // Every other header is single-valued by the time it gets here; set-cookie is the
111
+ // one that legitimately repeats, and folding it into one line breaks the cookies.
112
+ for (const [name, value] of response.headers) {
113
+ if (name.toLowerCase() !== 'set-cookie') res.setHeader(name, value);
114
+ }
115
+ const cookies = response.headers.getSetCookie?.() ?? [];
116
+ if (cookies.length) res.setHeader('set-cookie', cookies);
117
+
118
+ if (response.body) for await (const chunk of response.body) res.write(chunk);
119
+ res.end();
120
+ };
121
+ `;
122
+ }
@@ -0,0 +1,30 @@
1
+ import { type Adapter } from '../adapter.js';
2
+ /**
3
+ * Cloudflare Pages. `_worker.js` at the root of the published directory takes every
4
+ * request; `_routes.json` is what decides whether a request costs an invocation.
5
+ *
6
+ * In advanced mode the worker sees everything not excluded — so a prerendered page
7
+ * would wake it just to proxy the file back through the ASSETS binding. On a plan
8
+ * metered by request that is the whole page view budget spent on static files, so
9
+ * every prerendered path is excluded by name.
10
+ *
11
+ * Publish `dist/client`.
12
+ */
13
+ export declare const cloudflareAdapter: Adapter;
14
+ /**
15
+ * Netlify. The function is a v2 fetch handler under `.netlify/functions-internal/`,
16
+ * which is where a framework — as opposed to the user — puts one, so it cannot
17
+ * collide with the app's own `netlify/functions`.
18
+ *
19
+ * Publish `dist/client`.
20
+ */
21
+ export declare const netlifyAdapter: Adapter;
22
+ /**
23
+ * Vercel, through the Build Output API v3: `.vercel/output/` is read directly, with
24
+ * no framework detection and no `vercel.json` to keep in sync.
25
+ *
26
+ * `config.json` routes the filesystem first so a prerendered page is served as a
27
+ * file, then sends the rest to the function.
28
+ */
29
+ export declare const vercelAdapter: Adapter;
30
+ //# sourceMappingURL=platforms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platforms.d.ts","sourceRoot":"","sources":["../../src/adapters/platforms.ts"],"names":[],"mappings":"AAaA,OAAO,EAAgB,KAAK,OAAO,EAA4B,MAAM,eAAe,CAAC;AAgFrF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAgC/B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,EAAE,OAmC5B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,OAuD3B,CAAC"}