@octanejs/vite-plugin 0.1.3 → 0.1.4
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/CHANGELOG.md +73 -1
- package/package.json +7 -2
- package/src/bin/preview.js +20 -5
- package/src/config-entry.js +31 -0
- package/src/constants.js +1 -0
- package/src/index.js +362 -73
- package/src/load-config.js +6 -126
- package/src/project-codegen.js +75 -15
- package/src/resolve-config.js +171 -0
- package/src/routes.js +7 -1
- package/src/server/component-wrappers.js +1 -0
- package/src/server/middleware.js +1 -0
- package/src/server/node-http.js +188 -0
- package/src/server/production.js +276 -16
- package/src/server/render-route.js +86 -37
- package/src/server/router.js +1 -0
- package/src/server/server-route.js +1 -0
- package/src/server/virtual-entry.js +176 -7
- package/tests/_fixtures/app/index.html +11 -0
- package/tests/_fixtures/app/octane.config.ts +14 -0
- package/tests/_fixtures/app/package.json +7 -0
- package/tests/_fixtures/app/src/Layout.tsrx +6 -0
- package/tests/_fixtures/app/src/Page.tsrx +17 -0
- package/tests/_fixtures/app/vite.config.ts +12 -0
- package/tests/handler.test.ts +134 -0
- package/tests/plugin.test.ts +155 -0
- package/tests/production.test.ts +157 -0
- package/tsconfig.typecheck.json +2 -2
- package/types/index.d.ts +102 -13
- package/types/node.d.ts +31 -0
- package/types/production.d.ts +34 -21
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Production SSR build — end-to-end over the fixture app (tests/_fixtures/app):
|
|
2
|
+
// `vite build` must produce BOTH bundles (dist/client assets + the
|
|
3
|
+
// self-contained dist/server/entry.js), and the server bundle's handler must
|
|
4
|
+
// render a route with hydratable output whose body region and #__octane_data
|
|
5
|
+
// payload BYTE-MATCH dev SSR for the same request — that is the contract that
|
|
6
|
+
// lets hydrateRoot adopt production responses exactly like dev ones.
|
|
7
|
+
//
|
|
8
|
+
// The fixture has no installed node_modules (it is not a workspace package);
|
|
9
|
+
// the setup symlinks the workspace's octane / @octanejs/vite-plugin / vite in,
|
|
10
|
+
// which is exactly what a pnpm install would produce.
|
|
11
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
15
|
+
import { build, createServer, type ViteDevServer } from 'vite';
|
|
16
|
+
|
|
17
|
+
const fixtureRoot = fileURLToPath(new URL('./_fixtures/app', import.meta.url));
|
|
18
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
19
|
+
const repoRoot = path.resolve(packageRoot, '../..');
|
|
20
|
+
const distDir = path.join(fixtureRoot, 'dist');
|
|
21
|
+
|
|
22
|
+
function linkPackage(name: string, target: string) {
|
|
23
|
+
const dest = path.join(fixtureRoot, 'node_modules', name);
|
|
24
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
25
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
26
|
+
fs.symlinkSync(target, dest, 'dir');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The rendered body region: everything streamed into `<div id="root">`. */
|
|
30
|
+
function bodyRegionOf(html: string): string {
|
|
31
|
+
const open = '<div id="root">';
|
|
32
|
+
const start = html.indexOf(open);
|
|
33
|
+
const end = html.lastIndexOf('</div>');
|
|
34
|
+
expect(start).toBeGreaterThan(-1);
|
|
35
|
+
expect(end).toBeGreaterThan(start);
|
|
36
|
+
return html.slice(start + open.length, end);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dataScriptOf(html: string): string {
|
|
40
|
+
const match = html.match(/<script id="__octane_data" type="application\/json">(.*?)<\/script>/s);
|
|
41
|
+
expect(match).not.toBeNull();
|
|
42
|
+
return match![1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let devServer: ViteDevServer | null = null;
|
|
46
|
+
let devOrigin = '';
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
linkPackage('octane', path.join(repoRoot, 'packages/octane'));
|
|
50
|
+
linkPackage('@octanejs/vite-plugin', packageRoot);
|
|
51
|
+
linkPackage('vite', path.join(packageRoot, 'node_modules/vite'));
|
|
52
|
+
|
|
53
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
54
|
+
|
|
55
|
+
// The production build: client bundle, then (closeBundle) the server bundle.
|
|
56
|
+
await build({ root: fixtureRoot, logLevel: 'silent' });
|
|
57
|
+
|
|
58
|
+
// A dev server on a random port — the byte-compat oracle.
|
|
59
|
+
devServer = await createServer({
|
|
60
|
+
root: fixtureRoot,
|
|
61
|
+
logLevel: 'silent',
|
|
62
|
+
server: { port: 0 },
|
|
63
|
+
});
|
|
64
|
+
await devServer.listen();
|
|
65
|
+
const address = devServer.httpServer?.address();
|
|
66
|
+
if (!address || typeof address !== 'object') throw new Error('dev server has no address');
|
|
67
|
+
devOrigin = `http://localhost:${address.port}`;
|
|
68
|
+
}, 180_000);
|
|
69
|
+
|
|
70
|
+
afterAll(async () => {
|
|
71
|
+
await devServer?.close();
|
|
72
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
73
|
+
fs.rmSync(path.join(fixtureRoot, 'node_modules'), { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('production SSR build', () => {
|
|
77
|
+
it('emits both bundles, moves the template to dist/server, and strips build metadata', () => {
|
|
78
|
+
expect(fs.existsSync(path.join(distDir, 'server/entry.js'))).toBe(true);
|
|
79
|
+
expect(fs.existsSync(path.join(distDir, 'server/index.html'))).toBe(true);
|
|
80
|
+
// The template must NOT stay in the static dir (it would shadow SSR at '/'
|
|
81
|
+
// on filesystem-first hosts) and the manifest must not ship.
|
|
82
|
+
expect(fs.existsSync(path.join(distDir, 'client/index.html'))).toBe(false);
|
|
83
|
+
expect(fs.existsSync(path.join(distDir, 'client/.vite'))).toBe(false);
|
|
84
|
+
// The client build produced hashed assets, including the hydrate entry
|
|
85
|
+
// referenced by the moved template.
|
|
86
|
+
const template = fs.readFileSync(path.join(distDir, 'server/index.html'), 'utf-8');
|
|
87
|
+
const scriptSrc = template.match(/<script type="module"[^>]*src="(\/assets\/[^"]+)"/)?.[1];
|
|
88
|
+
expect(scriptSrc).toBeTruthy();
|
|
89
|
+
expect(fs.existsSync(path.join(distDir, 'client', scriptSrc!))).toBe(true);
|
|
90
|
+
// The SSR placeholders survived the client build untouched.
|
|
91
|
+
expect(template).toContain('<!--ssr-head-->');
|
|
92
|
+
expect(template).toContain('<!--ssr-body-->');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('the server bundle is self-contained (imports only node builtins)', () => {
|
|
96
|
+
const entry = fs.readFileSync(path.join(distDir, 'server/entry.js'), 'utf-8');
|
|
97
|
+
const specifiers = [...entry.matchAll(/^import[^'"]*['"]([^'"]+)['"]/gm)].map((m) => m[1]);
|
|
98
|
+
expect(specifiers.length).toBeGreaterThan(0);
|
|
99
|
+
for (const spec of specifiers) {
|
|
100
|
+
expect(spec.startsWith('node:'), `unexpected external import: ${spec}`).toBe(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders a route through the built handler, byte-matching dev SSR', async () => {
|
|
105
|
+
const { handler } = await import(pathToFileURL(path.join(distDir, 'server/entry.js')).href);
|
|
106
|
+
|
|
107
|
+
for (const url of ['/', '/pages/hello']) {
|
|
108
|
+
const prodResponse = await handler(new Request(`http://localhost${url}`));
|
|
109
|
+
expect(prodResponse.status).toBe(200);
|
|
110
|
+
expect(prodResponse.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
|
111
|
+
const prodHtml = await prodResponse.text();
|
|
112
|
+
|
|
113
|
+
const devResponse = await fetch(`${devOrigin}${url}`);
|
|
114
|
+
expect(devResponse.status).toBe(200);
|
|
115
|
+
const devHtml = await devResponse.text();
|
|
116
|
+
|
|
117
|
+
// The hydratable body region and the hydration payload are the
|
|
118
|
+
// byte-compat contract between dev and production.
|
|
119
|
+
expect(bodyRegionOf(prodHtml)).toBe(bodyRegionOf(devHtml));
|
|
120
|
+
expect(dataScriptOf(prodHtml)).toBe(dataScriptOf(devHtml));
|
|
121
|
+
|
|
122
|
+
// Sanity: it actually rendered the page.
|
|
123
|
+
expect(prodHtml).toContain('fixture-nav');
|
|
124
|
+
expect(prodHtml).toContain(url === '/' ? 'Fixture page home' : 'Fixture page hello');
|
|
125
|
+
expect(prodHtml).toContain(`<p class="url">${url}</p>`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns 404 for unmatched routes (no catch-all in the fixture)', async () => {
|
|
130
|
+
const { handler } = await import(pathToFileURL(path.join(distDir, 'server/entry.js')).href);
|
|
131
|
+
const response = await handler(new Request('http://localhost/nope/nothing'));
|
|
132
|
+
expect(response.status).toBe(404);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('nodeHandler bridges the same handler for Node-style serverless wrappers', async () => {
|
|
136
|
+
const { nodeHandler } = await import(pathToFileURL(path.join(distDir, 'server/entry.js')).href);
|
|
137
|
+
const chunks: Buffer[] = [];
|
|
138
|
+
const headers: Record<string, unknown> = {};
|
|
139
|
+
const res = {
|
|
140
|
+
statusCode: 0,
|
|
141
|
+
headersSent: false,
|
|
142
|
+
setHeader(key: string, value: unknown) {
|
|
143
|
+
headers[key.toLowerCase()] = value;
|
|
144
|
+
},
|
|
145
|
+
write(chunk: Uint8Array) {
|
|
146
|
+
chunks.push(Buffer.from(chunk));
|
|
147
|
+
},
|
|
148
|
+
end(chunk?: Uint8Array) {
|
|
149
|
+
if (chunk) chunks.push(Buffer.from(chunk));
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
await nodeHandler({ method: 'GET', url: '/pages/node', headers: { host: 'localhost' } }, res);
|
|
153
|
+
expect(res.statusCode).toBe(200);
|
|
154
|
+
expect(headers['content-type']).toBe('text/html; charset=utf-8');
|
|
155
|
+
expect(Buffer.concat(chunks).toString('utf-8')).toContain('Fixture page node');
|
|
156
|
+
});
|
|
157
|
+
});
|
package/tsconfig.typecheck.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -8,6 +8,15 @@ import type { RuntimePrimitives } from '@ripple-ts/adapter';
|
|
|
8
8
|
export interface OctanePluginOptions {
|
|
9
9
|
/** Override the client HMR default (on in serve mode, off for SSR). */
|
|
10
10
|
hmr?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Path fragments the compiler's plain `.ts`/`.js` hook-slotting pass must
|
|
13
|
+
* skip — hand-slot-forwarding library sources that would otherwise be
|
|
14
|
+
* double-slotted. Published bindings live in node_modules and are skipped
|
|
15
|
+
* automatically; set this for monorepo / aliased-to-source setups where
|
|
16
|
+
* pnpm symlinks resolve `@octanejs/*` imports to `packages/*\/src` paths
|
|
17
|
+
* (e.g. `['/packages/tanstack-router/src/']`).
|
|
18
|
+
*/
|
|
19
|
+
exclude?: string[];
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
/**
|
|
@@ -16,6 +25,18 @@ export interface OctanePluginOptions {
|
|
|
16
25
|
* config / routing / dev SSR / hydrate.
|
|
17
26
|
*/
|
|
18
27
|
export function octane(options?: OctanePluginOptions): Plugin[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Is this a request the Vite dev server owns (module / asset / internal
|
|
31
|
+
* namespace / transform query), as opposed to a page navigation? The dev SSR
|
|
32
|
+
* middleware uses it so a catch-all RenderRoute never swallows Vite requests.
|
|
33
|
+
*
|
|
34
|
+
* `fileRoots` (the Vite root + publicDir) gate the file-extension heuristic:
|
|
35
|
+
* an extension-bearing path is only Vite's when it names a real file under
|
|
36
|
+
* one of them, so page URLs like `/docs/v2.0` still SSR. Without `fileRoots`
|
|
37
|
+
* any extension counts (conservative).
|
|
38
|
+
*/
|
|
39
|
+
export function isViteOwnedUrl(url: URL, fileRoots?: string[]): boolean;
|
|
19
40
|
export function defineConfig(options: OctaneConfigOptions): OctaneConfigOptions;
|
|
20
41
|
export function resolveOctaneConfig(
|
|
21
42
|
raw: OctaneConfigOptions,
|
|
@@ -38,6 +59,7 @@ export class RenderRoute {
|
|
|
38
59
|
entry: RenderRouteEntry;
|
|
39
60
|
layout?: string;
|
|
40
61
|
before: Middleware[];
|
|
62
|
+
status?: number;
|
|
41
63
|
constructor(options: RenderRouteOptions);
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -66,6 +88,11 @@ export interface RenderRouteOptions {
|
|
|
66
88
|
layout?: string;
|
|
67
89
|
/** Middleware to run before rendering */
|
|
68
90
|
before?: Middleware[];
|
|
91
|
+
/**
|
|
92
|
+
* HTTP status for the rendered response (default 200). Set 404 on a
|
|
93
|
+
* catch-all route so the SSR'd not-found page reports its real status.
|
|
94
|
+
*/
|
|
95
|
+
status?: number;
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
export interface ServerRouteOptions {
|
|
@@ -112,6 +139,27 @@ export type Component<T = Record<string, any>> = (
|
|
|
112
139
|
|
|
113
140
|
export type RenderRouteEntry = string | readonly [exportName: string, path: string];
|
|
114
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Props every RenderRoute component (and layout) receives: the route params
|
|
144
|
+
* and the request `url` (pathname + search, origin-free — the client hydrate
|
|
145
|
+
* entry re-renders with the identical string).
|
|
146
|
+
*/
|
|
147
|
+
export interface RenderRouteProps {
|
|
148
|
+
params: Record<string, string>;
|
|
149
|
+
url: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The app hook run by the client hydrate entry BEFORE `hydrateRoot` (config
|
|
154
|
+
* `router.preHydrate`): commit client-side state the server already resolved —
|
|
155
|
+
* typically a client router loading its match tree — so the first hydration
|
|
156
|
+
* pass adopts the same tree the server rendered.
|
|
157
|
+
*/
|
|
158
|
+
export type PreHydrateHook = (info: {
|
|
159
|
+
url: string;
|
|
160
|
+
params: Record<string, string>;
|
|
161
|
+
}) => void | Promise<void>;
|
|
162
|
+
|
|
115
163
|
export interface RootBoundaryOptions {
|
|
116
164
|
pending?: Component<Record<string, never>>;
|
|
117
165
|
catch?: Component<{ error: unknown; reset: () => void }>;
|
|
@@ -124,17 +172,15 @@ export interface OctaneConfigOptions {
|
|
|
124
172
|
minify?: boolean;
|
|
125
173
|
target?: BuildEnvironmentOptions['target'];
|
|
126
174
|
};
|
|
127
|
-
adapter?:
|
|
128
|
-
serve: AdapterServeFunction;
|
|
129
|
-
/**
|
|
130
|
-
* Platform-specific runtime primitives provided by the adapter.
|
|
131
|
-
* Required for production builds; in development the plugin falls back
|
|
132
|
-
* to Node.js defaults if not provided.
|
|
133
|
-
*/
|
|
134
|
-
runtime: RuntimePrimitives;
|
|
135
|
-
};
|
|
175
|
+
adapter?: OctaneAdapter;
|
|
136
176
|
router?: {
|
|
137
177
|
routes: Route[];
|
|
178
|
+
/**
|
|
179
|
+
* Vite-root path (e.g. '/src/pre-hydrate.ts') of a module whose default
|
|
180
|
+
* export is a {@link PreHydrateHook}. The client hydrate entry imports it
|
|
181
|
+
* and awaits the hook before calling `hydrateRoot`.
|
|
182
|
+
*/
|
|
183
|
+
preHydrate?: string;
|
|
138
184
|
};
|
|
139
185
|
/** Global root pending/catch UI used by client and SSR render roots */
|
|
140
186
|
rootBoundary?: RootBoundaryOptions;
|
|
@@ -150,6 +196,14 @@ export interface OctaneConfigOptions {
|
|
|
150
196
|
* @default false
|
|
151
197
|
*/
|
|
152
198
|
trustProxy?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Production SSR mode: 'streaming' (default) flushes the shell at
|
|
201
|
+
* first await and streams suspense segments out-of-order (same engine
|
|
202
|
+
* as dev SSR); 'buffered' awaits everything (`prerender`) and sends
|
|
203
|
+
* one document — for hosts that break streamed responses.
|
|
204
|
+
* @default 'streaming'
|
|
205
|
+
*/
|
|
206
|
+
render?: 'streaming' | 'buffered';
|
|
153
207
|
};
|
|
154
208
|
}
|
|
155
209
|
|
|
@@ -163,12 +217,10 @@ export interface ResolvedOctaneConfig {
|
|
|
163
217
|
minify?: boolean;
|
|
164
218
|
target?: BuildEnvironmentOptions['target'];
|
|
165
219
|
};
|
|
166
|
-
adapter?:
|
|
167
|
-
serve: AdapterServeFunction;
|
|
168
|
-
runtime: RuntimePrimitives;
|
|
169
|
-
};
|
|
220
|
+
adapter?: OctaneAdapter;
|
|
170
221
|
router: {
|
|
171
222
|
routes: Route[];
|
|
223
|
+
preHydrate?: string;
|
|
172
224
|
};
|
|
173
225
|
rootBoundary: RootBoundaryOptions;
|
|
174
226
|
/** @default [] */
|
|
@@ -180,9 +232,46 @@ export interface ResolvedOctaneConfig {
|
|
|
180
232
|
server: {
|
|
181
233
|
/** @default false */
|
|
182
234
|
trustProxy: boolean;
|
|
235
|
+
/** @default 'streaming' */
|
|
236
|
+
render: 'streaming' | 'buffered';
|
|
183
237
|
};
|
|
184
238
|
}
|
|
185
239
|
|
|
240
|
+
/**
|
|
241
|
+
* The build context @octanejs/vite-plugin passes to an adapter's `adapt()`
|
|
242
|
+
* after `vite build` produced both bundles.
|
|
243
|
+
*/
|
|
244
|
+
export interface AdaptContext {
|
|
245
|
+
/** Absolute project root (the Vite root). */
|
|
246
|
+
root: string;
|
|
247
|
+
/** The config `build.outDir` (relative to root, e.g. 'dist'). */
|
|
248
|
+
outDir: string;
|
|
249
|
+
/** Absolute path of the static client bundle ({outDir}/client). */
|
|
250
|
+
clientDir: string;
|
|
251
|
+
/** Absolute path of the server bundle ({outDir}/server, contains entry.js). */
|
|
252
|
+
serverDir: string;
|
|
253
|
+
/** Prefixed build logger. */
|
|
254
|
+
log: (message: string) => void;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The octane.config.ts `adapter` contract. All parts are optional and
|
|
259
|
+
* independent:
|
|
260
|
+
*
|
|
261
|
+
* - `adapt(ctx)` — post-build hook: restructure dist/client + dist/server for
|
|
262
|
+
* a deployment target (e.g. @octanejs/adapter-vercel emits `.vercel/output`).
|
|
263
|
+
* - `serve(handler, opts)` — replaces the generated server entry's built-in
|
|
264
|
+
* Node boot when running `node dist/server/entry.js` / `octane-preview`.
|
|
265
|
+
* - `runtime` — platform primitives (hashing, async context) replacing the
|
|
266
|
+
* entry's Node defaults; needed on non-Node runtimes.
|
|
267
|
+
*/
|
|
268
|
+
export interface OctaneAdapter {
|
|
269
|
+
name?: string;
|
|
270
|
+
adapt?: (ctx: AdaptContext) => void | Promise<void>;
|
|
271
|
+
serve?: AdapterServeFunction;
|
|
272
|
+
runtime?: RuntimePrimitives;
|
|
273
|
+
}
|
|
274
|
+
|
|
186
275
|
export type AdapterServeFunction = (
|
|
187
276
|
handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
|
|
188
277
|
options?: Record<string, unknown>,
|
package/types/node.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
2
|
+
|
|
3
|
+
/** Convert a Node.js IncomingMessage to a Web Request. */
|
|
4
|
+
export function nodeRequestToWebRequest(nodeRequest: IncomingMessage): Request;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pipe a Web Response to a Node.js ServerResponse, streaming chunk-by-chunk
|
|
8
|
+
* (a streaming SSR body flushes as it renders).
|
|
9
|
+
*/
|
|
10
|
+
export function sendWebResponse(nodeResponse: ServerResponse, webResponse: Response): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serve a static file from `staticDir` when the request path maps to one.
|
|
14
|
+
* `/assets/*` (hash-named build output) gets immutable caching; other files
|
|
15
|
+
* revalidate. Returns true when the request was handled.
|
|
16
|
+
*/
|
|
17
|
+
export function serveStaticFile(
|
|
18
|
+
req: IncomingMessage,
|
|
19
|
+
res: ServerResponse,
|
|
20
|
+
staticDir: string,
|
|
21
|
+
): boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal production HTTP server: static files from `staticDir` first (the
|
|
25
|
+
* built client assets), then the fetch-style SSR handler. The default boot for
|
|
26
|
+
* `node dist/server/entry.js` when octane.config.ts has no adapter.
|
|
27
|
+
*/
|
|
28
|
+
export function createNodeServer(
|
|
29
|
+
handler: (request: Request) => Response | Promise<Response>,
|
|
30
|
+
options?: { staticDir?: string },
|
|
31
|
+
): { listen: (port?: number) => Server; close: () => void };
|
package/types/production.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
OctaneConfigOptions,
|
|
7
7
|
RootBoundaryOptions,
|
|
8
8
|
} from '@octanejs/vite-plugin';
|
|
9
|
+
import type { RenderResult, StreamOptions, RenderOptions } from 'octane/server';
|
|
9
10
|
|
|
10
11
|
export function resolveOctaneConfig(
|
|
11
12
|
raw: OctaneConfigOptions,
|
|
@@ -13,45 +14,57 @@ export function resolveOctaneConfig(
|
|
|
13
14
|
): ResolvedOctaneConfig;
|
|
14
15
|
|
|
15
16
|
export interface ClientAssetEntry {
|
|
16
|
-
/** Path to the built JS file (relative to client output dir) */
|
|
17
|
+
/** Path to the built JS file (relative to the client output dir) */
|
|
17
18
|
js: string;
|
|
18
|
-
/** Paths to the built CSS files (relative to client output dir) */
|
|
19
|
+
/** Paths to the built CSS files (relative to the client output dir) */
|
|
19
20
|
css: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface ServerManifest {
|
|
23
24
|
routes: Route[];
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/** RenderRoute entry module path → module namespace (export picked per-route) */
|
|
26
|
+
components: Record<string, Record<string, unknown>>;
|
|
27
|
+
/** Layout module path → module namespace */
|
|
28
|
+
layouts: Record<string, Record<string, unknown>>;
|
|
26
29
|
middlewares: Middleware[];
|
|
27
|
-
/** Map of entry path → _$_server_$_ object for RPC support */
|
|
28
|
-
rpcModules?: Record<string, Record<string, Function>>;
|
|
29
30
|
/** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
|
|
30
31
|
trustProxy?: boolean;
|
|
32
|
+
/** 'streaming' (default) renders via renderToReadableStream; 'buffered' awaits everything via prerender */
|
|
33
|
+
render?: 'streaming' | 'buffered';
|
|
31
34
|
rootBoundary?: RootBoundaryOptions;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
/** Map of
|
|
35
|
+
/** config `router.preHydrate`, serialized into #__octane_data for the client entry */
|
|
36
|
+
preHydrate?: string | null;
|
|
37
|
+
/** Map of entry path → `module server` namespace for RPC support */
|
|
38
|
+
rpcModules?: Record<string, Record<string, Function>>;
|
|
39
|
+
/** Platform primitives (adapter's, or the generated entry's Node defaults) */
|
|
40
|
+
runtime?: RuntimePrimitives;
|
|
41
|
+
/** Route entry module path → built client asset paths (preload tags) */
|
|
35
42
|
clientAssets?: Record<string, ClientAssetEntry>;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
/**
|
|
39
|
-
* octane RenderResult — re-exported from 'octane/server' (the single source of
|
|
40
|
-
* truth) rather than re-declared, so the shape can't silently drift. Note
|
|
41
|
-
* `render()` is async and `css` is ALREADY a ready, deduped
|
|
42
|
-
* `<style data-octane="hash">…</style>` string (NOT a Set<string> needing a
|
|
43
|
-
* `getCss` lookup like Ripple).
|
|
44
|
-
*/
|
|
45
|
-
export type { RenderResult } from 'octane/server';
|
|
46
|
-
import type { RenderResult } from 'octane/server';
|
|
47
|
-
|
|
48
45
|
export interface HandlerOptions {
|
|
49
|
-
|
|
46
|
+
/** `renderToReadableStream` from 'octane/server' (the streaming engine dev SSR uses) */
|
|
47
|
+
renderToReadableStream: (
|
|
48
|
+
component: Function,
|
|
49
|
+
props?: unknown,
|
|
50
|
+
options?: StreamOptions,
|
|
51
|
+
) => Promise<ReadableStream<Uint8Array>>;
|
|
52
|
+
/** `prerender` from 'octane/static' (the buffered await-everything fallback) */
|
|
53
|
+
prerender: (
|
|
54
|
+
component: Function,
|
|
55
|
+
props?: unknown,
|
|
56
|
+
options?: RenderOptions,
|
|
57
|
+
) => Promise<RenderResult>;
|
|
58
|
+
/** The BUILT dist client index.html (moved to dist/server by the build) */
|
|
50
59
|
htmlTemplate: string;
|
|
60
|
+
/** RPC executor from 'octane/server' */
|
|
51
61
|
executeServerFunction: (fn: Function, body: string) => Promise<string>;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Production fetch-handler factory. Mirrors the dev middleware's render path
|
|
66
|
+
* byte-for-byte in everything hydration can see (see server/production.js).
|
|
67
|
+
*/
|
|
55
68
|
export function createHandler(
|
|
56
69
|
manifest: ServerManifest,
|
|
57
70
|
options: HandlerOptions,
|