@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
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;AAI7E;;;;;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;AAoBD,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,70 @@
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
+ import { createPreloader } from './preload.js';
9
10
  /**
10
11
  * 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.
12
+ * dist/client/index.html), reading both off disk. Runtime-neutral once assembled, but
13
+ * the assembly is not: a worker has no filesystem, so a deploy adapter generates an
14
+ * entry that imports the same pieces statically and calls `createHandlerFrom`.
13
15
  */
14
16
  export async function createProdHandler(config = {}) {
15
17
  const cfg = resolveConfig(config);
16
18
  const clientDir = resolve(cfg.root, 'dist/client');
17
- const template = await readFile(join(clientDir, 'index.html'), 'utf-8');
19
+ const template = await readTemplate(clientDir, resolve(cfg.root, 'dist/server'));
18
20
  const serverEntry = resolve(cfg.root, 'dist/server', 'entry-server.js');
19
21
  // SPA apps may ship no server entry (or one only for server functions) — import if present.
20
22
  const mod = existsSync(serverEntry)
21
23
  ? await import(/* @vite-ignore */ pathToFileURL(serverEntry).href)
22
24
  : {};
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
25
  // Optional request middleware (src/middleware.ts → dist/server/middleware.js).
56
26
  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));
27
+ const middleware = existsSync(mwFile)
28
+ ? await import(/* @vite-ignore */ pathToFileURL(mwFile).href)
29
+ : undefined;
30
+ return createHandlerFrom({ template, mod, cfg, middleware, preload: await loadPreloader(cfg) });
31
+ }
32
+ /**
33
+ * The shell the server renders into.
34
+ *
35
+ * `dist/client/index.html` is it, until the app prerenders `/` — then that path holds a
36
+ * rendered page and the build stashes the shell in `dist/server/template.html` first.
37
+ * Rendering into the page instead puts the home page's markup and head around every
38
+ * SSR response, and leaves hydration two mount elements to choose between.
39
+ */
40
+ export async function readTemplate(clientDir, serverDir) {
41
+ const stashed = join(serverDir, 'template.html');
42
+ return readFile(existsSync(stashed) ? stashed : join(clientDir, 'index.html'), 'utf-8');
43
+ }
44
+ /**
45
+ * The preloader for a filesystem host, from the route→files map `fluixi build` leaves
46
+ * in `dist/server`. Absent or unreadable means no hints and an unchanged page, never a
47
+ * failed request. A deploy adapter inlines the same map, because a worker cannot read
48
+ * it at runtime.
49
+ */
50
+ async function loadPreloader(cfg) {
51
+ const file = resolve(cfg.root, 'dist/server', 'route-assets.json');
52
+ if (!existsSync(file))
53
+ return undefined;
54
+ try {
55
+ return createPreloader(JSON.parse(await readFile(file, 'utf-8')));
56
+ }
57
+ catch {
58
+ // A map that cannot be read is a reason to skip preloading, not to refuse to serve.
59
+ return undefined;
60
+ }
61
61
  }
62
62
  /** Reference filesystem adapter — Node `http` + static files from dist/client. */
63
63
  export const nodeAdapter = {
64
64
  name: 'node',
65
+ // A Node server runs from the app directory, so node_modules is right there: keep the
66
+ // framework as imports instead of copying it into the bundle.
67
+ bundle: 'external',
65
68
  async serve({ handler, cfg, clientDir }) {
66
69
  serveNode(withStaticFiles(handler, clientDir), {
67
70
  port: cfg.port,
@@ -69,10 +72,113 @@ export const nodeAdapter = {
69
72
  });
70
73
  },
71
74
  };
75
+ export { cloudflareAdapter, netlifyAdapter, vercelAdapter } from './adapters/platforms.js';
72
76
  /** Reference edge/web adapter — export `{ fetch }`; the platform serves assets. */
73
77
  export const webAdapter = {
74
78
  name: 'web',
79
+ // An edge/serverless runtime uploads a file, not an install: nothing may be left to
80
+ // resolve at runtime, so everything goes in the bundle.
81
+ bundle: 'inline',
75
82
  serve({ handler }) {
76
83
  return { fetch: handler };
77
84
  },
78
85
  };
86
+ /**
87
+ * Subpaths that only look like package entries: a Vite plugin replaces them with the
88
+ * app's generated route/server-fn/API tables, and the real file in the package is a stub
89
+ * that throws. Externalizing one would ship the stub. The plugins run with
90
+ * `enforce: 'pre'` so they win anyway — this list is the second lock on that door.
91
+ */
92
+ const GENERATED_SUBPATHS = new Set(['./routes', './server-fns', './api-routes']);
93
+ /**
94
+ * The Vite `ssr` options for a build, given the configured adapter's `bundle` mode.
95
+ * Shared by the server and middleware builds so the two cannot disagree.
96
+ *
97
+ * `'external'` lists the app's own `@fluixi/*` dependencies rather than externalizing
98
+ * everything: under a strict `node_modules` layout (pnpm) only what the app itself
99
+ * declares is resolvable from its root, so externalizing a merely transitive package
100
+ * would produce a bundle that imports something Node cannot find.
101
+ *
102
+ * Each package is listed with every subpath its `exports` map publishes, because Vite
103
+ * only honours an *exact* id in `ssr.external`. A bare package name falls through to a
104
+ * resolvability probe that does not externalize deep imports here, and the result is the
105
+ * worst of both worlds: `@fluixi/core` an import, `@fluixi/core/router-next` copied into
106
+ * the bundle — two routers, two sets of module state, in one server.
107
+ *
108
+ * `ssrNoExternal` always wins — it is the app saying "this one must be bundled".
109
+ */
110
+ export function ssrBuildOptions(cfg) {
111
+ const noExternal = cfg.ssrNoExternal.length ? cfg.ssrNoExternal : undefined;
112
+ const mode = cfg.adapter?.bundle;
113
+ if (mode === 'inline')
114
+ return { noExternal: true };
115
+ if (mode !== 'external')
116
+ return { noExternal };
117
+ const external = frameworkDeps(cfg.root)
118
+ .filter((name) => !cfg.ssrNoExternal.includes(name))
119
+ .flatMap((name) => withSubpaths(cfg.root, name));
120
+ if (!external.length) {
121
+ // Silently building an inlined bundle here would look identical to a working one
122
+ // until someone measures it, so say why nothing was externalized.
123
+ console.warn(` fluixi: adapter "${cfg.adapter?.name}" builds an externalized server, but no ` +
124
+ `@fluixi/* dependency was found in ${join(cfg.root, 'package.json')} — bundling everything.`);
125
+ }
126
+ return { noExternal, external: external.length ? external : undefined };
127
+ }
128
+ /** The app's own `@fluixi/*` dependencies, read from its package.json. */
129
+ function frameworkDeps(root) {
130
+ const pkg = readPackage(join(root, 'package.json'));
131
+ if (!pkg)
132
+ return [];
133
+ const declared = { ...pkg.dependencies, ...pkg.peerDependencies, ...pkg.optionalDependencies };
134
+ return Object.keys(declared).filter((id) => id.startsWith('@fluixi/'));
135
+ }
136
+ /**
137
+ * `['@fluixi/core', '@fluixi/core/rx', …]` — the package plus every static subpath it
138
+ * exports. Wildcard entries (`./lib/*`) cannot be enumerated and are left to Vite; an
139
+ * app importing through one gets it bundled, which is the safe direction.
140
+ */
141
+ function withSubpaths(root, name) {
142
+ const exports = resolvePackage(root, name)?.exports;
143
+ if (!exports || typeof exports !== 'object')
144
+ return [name];
145
+ const ids = [name];
146
+ for (const key of Object.keys(exports)) {
147
+ if (!key.startsWith('./') || key.includes('*'))
148
+ continue;
149
+ if (key === './package.json' || GENERATED_SUBPATHS.has(key))
150
+ continue;
151
+ ids.push(`${name}/${key.slice(2)}`);
152
+ }
153
+ return ids;
154
+ }
155
+ /**
156
+ * A dependency's package.json, read from where the app would resolve it. `require.resolve`
157
+ * covers the normal case; packages that do not export `./package.json` (a package may
158
+ * legitimately keep it private) need the node_modules walk, which also handles hoisting.
159
+ */
160
+ function resolvePackage(root, name) {
161
+ try {
162
+ return readPackage(createRequire(join(root, 'index.js')).resolve(`${name}/package.json`));
163
+ }
164
+ catch {
165
+ /* not exported — fall through */
166
+ }
167
+ for (let dir = root;; dir = dirname(dir)) {
168
+ const pkg = readPackage(join(dir, 'node_modules', name, 'package.json'));
169
+ if (pkg)
170
+ return pkg;
171
+ if (dirname(dir) === dir)
172
+ return undefined;
173
+ }
174
+ }
175
+ function readPackage(file) {
176
+ if (!existsSync(file))
177
+ return undefined;
178
+ try {
179
+ return JSON.parse(readFileSync(file, 'utf-8'));
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
@@ -0,0 +1,50 @@
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
+ /** URL pattern → built files, inlined: a worker cannot read the map off disk. */
25
+ routeAssets: Record<string, string[]>;
26
+ }
27
+ /**
28
+ * Cloudflare Pages, advanced mode: a `_worker.js` at the root of the published
29
+ * directory takes every request. Pages stops serving static files itself in that
30
+ * mode, so assets go through the ASSETS binding, and `_routes.json` keeps the worker
31
+ * out of the hot path for anything under a hashed asset directory.
32
+ */
33
+ export declare function cloudflareEntry(source: EntrySource): string;
34
+ /**
35
+ * Netlify Functions v2: a default-exported fetch handler. `config.path` claims every
36
+ * route; Netlify still serves a matching static file first, so the function only sees
37
+ * what the CDN could not answer.
38
+ */
39
+ export declare function netlifyEntry(source: EntrySource): string;
40
+ /**
41
+ * Vercel Build Output API v3, Node runtime.
42
+ *
43
+ * The `Nodejs` launcher invokes the default export as `(req, res)`, so the entry
44
+ * bridges the two itself. Exporting `(request) => Response` hangs instead of failing:
45
+ * the launcher passes an `IncomingMessage`, nothing writes to `res`, and the request
46
+ * sits until the platform times it out. Node rather than edge, so a server function
47
+ * can use Node APIs.
48
+ */
49
+ export declare function vercelEntry(source: EntrySource): string;
50
+ //# 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;IACtB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AA0BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAoB3D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAOxD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAuCvD"}
@@ -0,0 +1,124 @@
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, routeAssets }) {
15
+ return [
16
+ `import { createHandlerFrom } from '@fluixi/start/handler-core';`,
17
+ `import { createPreloader } from '@fluixi/start/preload';`,
18
+ `import * as server from ${JSON.stringify(serverEntry)};`,
19
+ middleware ? `import * as middleware from ${JSON.stringify(middleware)};` : '',
20
+ ``,
21
+ `const template = ${JSON.stringify(template)};`,
22
+ // Inlined for the same reason as the template: this runs as an uploaded file, with
23
+ // no dist/server beside it to read.
24
+ `const routeAssets = ${JSON.stringify(routeAssets)};`,
25
+ `const handler = createHandlerFrom({`,
26
+ ` template,`,
27
+ ` mod: server,`,
28
+ ` cfg: { ssr: ${ssr}, mountId: ${JSON.stringify(mountId)} },`,
29
+ ` preload: createPreloader(routeAssets),`,
30
+ middleware ? ` middleware,` : '',
31
+ `});`,
32
+ ]
33
+ .filter(Boolean)
34
+ .join('\n');
35
+ }
36
+ /**
37
+ * Cloudflare Pages, advanced mode: a `_worker.js` at the root of the published
38
+ * directory takes every request. Pages stops serving static files itself in that
39
+ * mode, so assets go through the ASSETS binding, and `_routes.json` keeps the worker
40
+ * out of the hot path for anything under a hashed asset directory.
41
+ */
42
+ export function cloudflareEntry(source) {
43
+ return `${preamble(source)}
44
+
45
+ const prerendered = new Set(${JSON.stringify(source.prerendered)});
46
+
47
+ export default {
48
+ async fetch(request, env, ctx) {
49
+ // Static first, but only for what is static. ASSETS answers "/" with index.html
50
+ // whether that is a prerendered page or the shell, so asking about every path
51
+ // costs an SSR app its home page. Extension means a file, prerendered means a
52
+ // page, the rest is the handler's — the rule \`withStaticFiles\` already uses.
53
+ const { pathname } = new URL(request.url);
54
+ if (env && env.ASSETS && (prerendered.has(pathname) || /\\.[^/]+$/.test(pathname))) {
55
+ const asset = await env.ASSETS.fetch(request.clone());
56
+ if (asset.status !== 404) return asset;
57
+ }
58
+ return handler(request, { env, ctx });
59
+ },
60
+ };
61
+ `;
62
+ }
63
+ /**
64
+ * Netlify Functions v2: a default-exported fetch handler. `config.path` claims every
65
+ * route; Netlify still serves a matching static file first, so the function only sees
66
+ * what the CDN could not answer.
67
+ */
68
+ export function netlifyEntry(source) {
69
+ return `${preamble(source)}
70
+
71
+ export default async (request, context) => handler(request, { context });
72
+
73
+ export const config = { path: '/*', preferStatic: true };
74
+ `;
75
+ }
76
+ /**
77
+ * Vercel Build Output API v3, Node runtime.
78
+ *
79
+ * The `Nodejs` launcher invokes the default export as `(req, res)`, so the entry
80
+ * bridges the two itself. Exporting `(request) => Response` hangs instead of failing:
81
+ * the launcher passes an `IncomingMessage`, nothing writes to `res`, and the request
82
+ * sits until the platform times it out. Node rather than edge, so a server function
83
+ * can use Node APIs.
84
+ */
85
+ export function vercelEntry(source) {
86
+ return `${preamble(source)}
87
+
88
+ export default async (req, res) => {
89
+ const proto = req.headers['x-forwarded-proto'] || 'https';
90
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
91
+
92
+ const headers = new Headers();
93
+ for (const [name, value] of Object.entries(req.headers)) {
94
+ if (Array.isArray(value)) for (const one of value) headers.append(name, one);
95
+ else if (value !== undefined) headers.set(name, value);
96
+ }
97
+
98
+ // A body has to be read off the stream before it can be handed to Request; only
99
+ // GET and HEAD are guaranteed not to have one.
100
+ let body;
101
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
102
+ const chunks = [];
103
+ for await (const chunk of req) chunks.push(chunk);
104
+ if (chunks.length) body = new Blob(chunks);
105
+ }
106
+
107
+ const response = await handler(
108
+ new Request(new URL(req.url, \`\${proto}://\${host}\`), { method: req.method, headers, body }),
109
+ );
110
+
111
+ res.statusCode = response.status;
112
+ // set-cookie is the one header that legitimately repeats; folding it into one line
113
+ // breaks the cookies.
114
+ for (const [name, value] of response.headers) {
115
+ if (name.toLowerCase() !== 'set-cookie') res.setHeader(name, value);
116
+ }
117
+ const cookies = response.headers.getSetCookie?.() ?? [];
118
+ if (cookies.length) res.setHeader('set-cookie', cookies);
119
+
120
+ if (response.body) for await (const chunk of response.body) res.write(chunk);
121
+ res.end();
122
+ };
123
+ `;
124
+ }
@@ -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;AAwGrF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAgC/B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,EAAE,OA+B5B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,OAgE3B,CAAC"}