@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.
- package/dist/adapter.d.ts +77 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +150 -44
- package/dist/adapters/entry.d.ts +50 -0
- package/dist/adapters/entry.d.ts.map +1 -0
- package/dist/adapters/entry.js +124 -0
- package/dist/adapters/platforms.d.ts +30 -0
- package/dist/adapters/platforms.d.ts.map +1 -0
- package/dist/adapters/platforms.js +231 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +7 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +114 -4
- package/dist/commands/index.d.ts +14 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +10 -1
- package/dist/config.d.ts +10 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/document.d.ts +41 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +125 -0
- package/dist/generated-api.d.ts +5 -0
- package/dist/generated-api.d.ts.map +1 -0
- package/dist/generated-api.js +26 -0
- package/dist/generated-server-fns.d.ts +5 -0
- package/dist/generated-server-fns.d.ts.map +1 -0
- package/dist/generated-server-fns.js +26 -0
- package/dist/handler-core.d.ts +41 -0
- package/dist/handler-core.d.ts.map +1 -0
- package/dist/handler-core.js +50 -0
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/internal.d.ts +1 -29
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +10 -102
- package/dist/preload.d.ts +5 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +27 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- 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)
|
|
6
|
-
*
|
|
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.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* platform
|
|
20
|
-
*
|
|
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
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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)
|
|
12
|
-
*
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
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"}
|