@octanejs/vite-plugin 0.1.4 → 0.1.6
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/package.json +16 -4
- package/src/config-entry.js +15 -0
- package/src/constants.js +1 -3
- package/src/index.js +74 -24
- package/src/load-config.js +61 -81
- package/src/project-codegen.js +10 -218
- package/src/resolve-config.js +1 -171
- package/src/routes.js +1 -133
- package/src/server/component-wrappers.js +5 -56
- package/src/server/html-stream.js +1 -0
- package/src/server/html-template.js +9 -0
- package/src/server/middleware.js +1 -127
- package/src/server/node-http.js +1 -188
- package/src/server/production.js +1 -286
- package/src/server/render-route.js +66 -41
- package/src/server/router.js +1 -123
- package/src/server/server-route.js +1 -47
- package/src/server/virtual-entry.js +1 -184
- package/types/index.d.ts +30 -260
- package/types/node.d.ts +1 -31
- package/types/production.d.ts +1 -71
- package/CHANGELOG.md +0 -244
- package/tests/_fixtures/app/index.html +0 -11
- package/tests/_fixtures/app/octane.config.ts +0 -14
- package/tests/_fixtures/app/package.json +0 -7
- package/tests/_fixtures/app/src/Layout.tsrx +0 -6
- package/tests/_fixtures/app/src/Page.tsrx +0 -17
- package/tests/_fixtures/app/vite.config.ts +0 -12
- package/tests/handler.test.ts +0 -134
- package/tests/plugin.test.ts +0 -155
- package/tests/production.test.ts +0 -157
- package/tsconfig.typecheck.json +0 -18
package/src/server/middleware.js
CHANGED
|
@@ -1,127 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @typedef {import('@octanejs/vite-plugin').Context} Context
|
|
4
|
-
* @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
|
|
5
|
-
* @typedef {import('@octanejs/vite-plugin').NextFunction} NextFunction
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compose multiple middlewares into a single middleware
|
|
10
|
-
* Follows Koa-style execution: request flows down, response flows back up
|
|
11
|
-
*
|
|
12
|
-
* @param {Middleware[]} middlewares
|
|
13
|
-
* @returns {(context: Context, finalHandler: () => Promise<Response>) => Promise<Response>}
|
|
14
|
-
*/
|
|
15
|
-
export function compose(middlewares) {
|
|
16
|
-
return function composed(context, finalHandler) {
|
|
17
|
-
let index = -1;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {number} i
|
|
21
|
-
* @returns {Promise<Response>}
|
|
22
|
-
*/
|
|
23
|
-
function dispatch(i) {
|
|
24
|
-
if (i <= index) {
|
|
25
|
-
return Promise.reject(new Error('next() called multiple times'));
|
|
26
|
-
}
|
|
27
|
-
index = i;
|
|
28
|
-
|
|
29
|
-
/** @type {Middleware | (() => Promise<Response>) | undefined} */
|
|
30
|
-
let fn;
|
|
31
|
-
|
|
32
|
-
if (i < middlewares.length) {
|
|
33
|
-
fn = middlewares[i];
|
|
34
|
-
} else if (i === middlewares.length) {
|
|
35
|
-
fn = finalHandler;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!fn) {
|
|
39
|
-
return Promise.reject(new Error('No handler provided'));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
// For the final handler, we don't pass next
|
|
44
|
-
if (i === middlewares.length) {
|
|
45
|
-
return Promise.resolve(/** @type {() => Promise<Response>} */ (fn)());
|
|
46
|
-
}
|
|
47
|
-
// For middlewares, pass context and next
|
|
48
|
-
return Promise.resolve(/** @type {Middleware} */ (fn)(context, () => dispatch(i + 1)));
|
|
49
|
-
} catch (err) {
|
|
50
|
-
return Promise.reject(err);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return dispatch(0);
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Create a context object for the request
|
|
60
|
-
* @param {Request} request
|
|
61
|
-
* @param {Record<string, string>} params
|
|
62
|
-
* @returns {Context}
|
|
63
|
-
*/
|
|
64
|
-
export function createContext(request, params) {
|
|
65
|
-
return {
|
|
66
|
-
request,
|
|
67
|
-
params,
|
|
68
|
-
url: new URL(request.url),
|
|
69
|
-
state: new Map(),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Run middlewares with a final handler
|
|
75
|
-
* Combines global middlewares, route-level before/after, and the handler
|
|
76
|
-
*
|
|
77
|
-
* @param {Context} context
|
|
78
|
-
* @param {Middleware[]} globalMiddlewares
|
|
79
|
-
* @param {Middleware[]} beforeMiddlewares
|
|
80
|
-
* @param {() => Promise<Response>} handler
|
|
81
|
-
* @param {Middleware[]} afterMiddlewares
|
|
82
|
-
* @returns {Promise<Response>}
|
|
83
|
-
*/
|
|
84
|
-
export async function runMiddlewareChain(
|
|
85
|
-
context,
|
|
86
|
-
globalMiddlewares,
|
|
87
|
-
beforeMiddlewares,
|
|
88
|
-
handler,
|
|
89
|
-
afterMiddlewares = [],
|
|
90
|
-
) {
|
|
91
|
-
// Combine global + before middlewares
|
|
92
|
-
const allMiddlewares = [...globalMiddlewares, ...beforeMiddlewares];
|
|
93
|
-
|
|
94
|
-
// If there are after middlewares, wrap the handler to run them
|
|
95
|
-
const wrappedHandler =
|
|
96
|
-
afterMiddlewares.length > 0
|
|
97
|
-
? async () => {
|
|
98
|
-
const response = await handler();
|
|
99
|
-
// After middlewares can inspect/modify the response
|
|
100
|
-
// but have limited ability to change it in our model
|
|
101
|
-
// We run them for side-effects (logging, etc.)
|
|
102
|
-
return runAfterMiddlewares(context, afterMiddlewares, response);
|
|
103
|
-
}
|
|
104
|
-
: handler;
|
|
105
|
-
|
|
106
|
-
const composed = compose(allMiddlewares);
|
|
107
|
-
return composed(context, wrappedHandler);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Run after middlewares with the response
|
|
112
|
-
* After middlewares run in order and can intercept/modify the response
|
|
113
|
-
*
|
|
114
|
-
* @param {Context} context
|
|
115
|
-
* @param {Middleware[]} middlewares
|
|
116
|
-
* @param {Response} response
|
|
117
|
-
* @returns {Promise<Response>}
|
|
118
|
-
*/
|
|
119
|
-
async function runAfterMiddlewares(context, middlewares, response) {
|
|
120
|
-
let currentResponse = response;
|
|
121
|
-
|
|
122
|
-
for (const middleware of middlewares) {
|
|
123
|
-
currentResponse = await middleware(context, async () => currentResponse);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return currentResponse;
|
|
127
|
-
}
|
|
1
|
+
export * from '@octanejs/app-core/middleware';
|
package/src/server/node-http.js
CHANGED
|
@@ -1,188 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Node HTTP glue — shared by the dev middleware (src/index.js), the generated
|
|
4
|
-
* production server entry (its `nodeHandler` export for serverless wrappers),
|
|
5
|
-
* and the built-in production server (`createNodeServer`, the no-adapter
|
|
6
|
-
* default boot). Exported as '@octanejs/vite-plugin/node'.
|
|
7
|
-
*
|
|
8
|
-
* This module may import node builtins (unlike server/production.js, which is
|
|
9
|
-
* platform-agnostic) — it IS the Node platform layer.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import http from 'node:http';
|
|
13
|
-
import fs from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { Readable } from 'node:stream';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Convert a Node.js IncomingMessage to a Web Request.
|
|
19
|
-
* @param {import('node:http').IncomingMessage} nodeRequest
|
|
20
|
-
* @returns {Request}
|
|
21
|
-
*/
|
|
22
|
-
export function nodeRequestToWebRequest(nodeRequest) {
|
|
23
|
-
const host = nodeRequest.headers.host || 'localhost';
|
|
24
|
-
const url = new URL(nodeRequest.url || '/', `http://${host}`);
|
|
25
|
-
|
|
26
|
-
const headers = new Headers();
|
|
27
|
-
for (const [key, value] of Object.entries(nodeRequest.headers)) {
|
|
28
|
-
if (value == null) continue;
|
|
29
|
-
if (Array.isArray(value)) {
|
|
30
|
-
for (const v of value) headers.append(key, v);
|
|
31
|
-
} else {
|
|
32
|
-
headers.set(key, value);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const method = (nodeRequest.method || 'GET').toUpperCase();
|
|
37
|
-
/** @type {RequestInit & { duplex?: 'half' }} */
|
|
38
|
-
const init = { method, headers };
|
|
39
|
-
if (method !== 'GET' && method !== 'HEAD') {
|
|
40
|
-
// node:stream/web's ReadableStream and the DOM lib's are structurally the
|
|
41
|
-
// same at runtime; the lib types disagree on BYOB details.
|
|
42
|
-
init.body = /** @type {ReadableStream} */ (
|
|
43
|
-
/** @type {unknown} */ (Readable.toWeb(nodeRequest))
|
|
44
|
-
);
|
|
45
|
-
init.duplex = 'half';
|
|
46
|
-
}
|
|
47
|
-
return new Request(url, init);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Pipe a Web Response to a Node.js ServerResponse. Streams chunk-by-chunk so a
|
|
52
|
-
* streaming SSR body flushes as it renders (no buffering).
|
|
53
|
-
*
|
|
54
|
-
* @param {import('node:http').ServerResponse} nodeResponse
|
|
55
|
-
* @param {Response} webResponse
|
|
56
|
-
*/
|
|
57
|
-
export async function sendWebResponse(nodeResponse, webResponse) {
|
|
58
|
-
nodeResponse.statusCode = webResponse.status;
|
|
59
|
-
if (webResponse.statusText) nodeResponse.statusMessage = webResponse.statusText;
|
|
60
|
-
webResponse.headers.forEach((value, key) => {
|
|
61
|
-
nodeResponse.setHeader(key, value);
|
|
62
|
-
});
|
|
63
|
-
if (webResponse.body) {
|
|
64
|
-
const reader = webResponse.body.getReader();
|
|
65
|
-
try {
|
|
66
|
-
while (true) {
|
|
67
|
-
const { done, value } = await reader.read();
|
|
68
|
-
if (done) break;
|
|
69
|
-
nodeResponse.write(value);
|
|
70
|
-
}
|
|
71
|
-
} finally {
|
|
72
|
-
reader.releaseLock();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
nodeResponse.end();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Static-file MIME map (the common web set; anything else falls back to
|
|
79
|
-
// octet-stream, which is correct for downloads).
|
|
80
|
-
/** @type {Record<string, string>} */
|
|
81
|
-
const MIME_TYPES = {
|
|
82
|
-
'.html': 'text/html; charset=utf-8',
|
|
83
|
-
'.css': 'text/css; charset=utf-8',
|
|
84
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
85
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
86
|
-
'.json': 'application/json; charset=utf-8',
|
|
87
|
-
'.png': 'image/png',
|
|
88
|
-
'.jpg': 'image/jpeg',
|
|
89
|
-
'.jpeg': 'image/jpeg',
|
|
90
|
-
'.gif': 'image/gif',
|
|
91
|
-
'.svg': 'image/svg+xml',
|
|
92
|
-
'.ico': 'image/x-icon',
|
|
93
|
-
'.woff': 'font/woff',
|
|
94
|
-
'.woff2': 'font/woff2',
|
|
95
|
-
'.ttf': 'font/ttf',
|
|
96
|
-
'.otf': 'font/otf',
|
|
97
|
-
'.webp': 'image/webp',
|
|
98
|
-
'.avif': 'image/avif',
|
|
99
|
-
'.mp4': 'video/mp4',
|
|
100
|
-
'.webm': 'video/webm',
|
|
101
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
102
|
-
'.xml': 'application/xml',
|
|
103
|
-
'.wasm': 'application/wasm',
|
|
104
|
-
'.map': 'application/json',
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Serve a static file from `staticDir` if the request path maps to one.
|
|
109
|
-
* Hash-named build assets (everything under /assets/) get immutable caching;
|
|
110
|
-
* other files (favicon, robots.txt, …) revalidate.
|
|
111
|
-
*
|
|
112
|
-
* @param {import('node:http').IncomingMessage} req
|
|
113
|
-
* @param {import('node:http').ServerResponse} res
|
|
114
|
-
* @param {string} staticDir
|
|
115
|
-
* @returns {boolean} true when the request was handled as a static file
|
|
116
|
-
*/
|
|
117
|
-
export function serveStaticFile(req, res, staticDir) {
|
|
118
|
-
const method = (req.method || 'GET').toUpperCase();
|
|
119
|
-
if (method !== 'GET' && method !== 'HEAD') return false;
|
|
120
|
-
|
|
121
|
-
const pathname = decodeURIComponent(new URL(req.url || '/', 'http://localhost').pathname);
|
|
122
|
-
// Resolve inside staticDir only — a `..` escape must not leave the client dir.
|
|
123
|
-
const filePath = path.normalize(path.join(staticDir, pathname));
|
|
124
|
-
if (!filePath.startsWith(path.normalize(staticDir + path.sep))) return false;
|
|
125
|
-
|
|
126
|
-
/** @type {fs.Stats} */
|
|
127
|
-
let stat;
|
|
128
|
-
try {
|
|
129
|
-
stat = fs.statSync(filePath);
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
if (!stat.isFile()) return false;
|
|
134
|
-
|
|
135
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
136
|
-
res.statusCode = 200;
|
|
137
|
-
res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
|
|
138
|
-
res.setHeader('Content-Length', stat.size);
|
|
139
|
-
res.setHeader(
|
|
140
|
-
'Cache-Control',
|
|
141
|
-
pathname.startsWith('/assets/')
|
|
142
|
-
? 'public, max-age=31536000, immutable'
|
|
143
|
-
: 'public, max-age=0, must-revalidate',
|
|
144
|
-
);
|
|
145
|
-
if (method === 'HEAD') {
|
|
146
|
-
res.end();
|
|
147
|
-
} else {
|
|
148
|
-
fs.createReadStream(filePath).pipe(res);
|
|
149
|
-
}
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Minimal production HTTP server: static files from `staticDir` first (built
|
|
155
|
-
* client assets), then the fetch-style SSR handler. This is the DEFAULT boot
|
|
156
|
-
* when octane.config.ts has no adapter — an adapter's `serve()` replaces it.
|
|
157
|
-
*
|
|
158
|
-
* @param {(request: Request) => Response | Promise<Response>} handler
|
|
159
|
-
* @param {{ staticDir?: string }} [options]
|
|
160
|
-
* @returns {{ listen: (port?: number) => import('node:http').Server, close: () => void }}
|
|
161
|
-
*/
|
|
162
|
-
export function createNodeServer(handler, options = {}) {
|
|
163
|
-
const staticDir = options.staticDir;
|
|
164
|
-
|
|
165
|
-
const server = http.createServer((req, res) => {
|
|
166
|
-
(async () => {
|
|
167
|
-
if (staticDir && serveStaticFile(req, res, staticDir)) return;
|
|
168
|
-
const response = await handler(nodeRequestToWebRequest(req));
|
|
169
|
-
await sendWebResponse(res, response);
|
|
170
|
-
})().catch((error) => {
|
|
171
|
-
console.error('[@octanejs/vite-plugin] Request error:', error);
|
|
172
|
-
if (!res.headersSent) {
|
|
173
|
-
res.statusCode = 500;
|
|
174
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
175
|
-
}
|
|
176
|
-
res.end('Internal Server Error');
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
listen(port = 3000) {
|
|
182
|
-
return server.listen(port);
|
|
183
|
-
},
|
|
184
|
-
close() {
|
|
185
|
-
server.close();
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
1
|
+
export * from '@octanejs/app-core/node';
|
package/src/server/production.js
CHANGED
|
@@ -1,286 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Production fetch-handler factory + config re-exports.
|
|
4
|
-
*
|
|
5
|
-
* `createHandler(manifest, deps)` is the runtime entry the generated server
|
|
6
|
-
* bundle (dist/server/entry.js) calls in production. It is designed to be
|
|
7
|
-
* BUNDLED: platform-agnostic (no Node imports — platform capabilities come via
|
|
8
|
-
* `manifest.runtime`), and free of vite / octane-compiler imports (which is why
|
|
9
|
-
* `resolveOctaneConfig` is re-exported from resolve-config.js, not
|
|
10
|
-
* load-config.js).
|
|
11
|
-
*
|
|
12
|
-
* The render path mirrors the DEV middleware's `handleRenderRoute`
|
|
13
|
-
* (server/render-route.js) byte-for-byte in everything hydration can see —
|
|
14
|
-
* the same `renderToReadableStream` engine, the same `#__octane_data` payload
|
|
15
|
-
* (same keys, same order), and the same template-prefix → render-stream →
|
|
16
|
-
* template-suffix assembly — so `hydrateRoot()` adopts a production response
|
|
17
|
-
* exactly as it adopts a dev one. Deliberate differences: the template is the
|
|
18
|
-
* BUILT dist/client/index.html (hashed hydrate script already in place, so
|
|
19
|
-
* nothing is injected per-request), per-route `<link rel=stylesheet/modulepreload>`
|
|
20
|
-
* tags from the client manifest join the head, and render errors produce a
|
|
21
|
-
* plain 500 (no dev stack page). Keep the two files in sync when the shape
|
|
22
|
-
* changes.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { createRouter } from './router.js';
|
|
26
|
-
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
27
|
-
import { handleServerRoute } from './server-route.js';
|
|
28
|
-
import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
|
|
29
|
-
import {
|
|
30
|
-
get_component_export,
|
|
31
|
-
get_route_entry_export_name,
|
|
32
|
-
get_route_entry_path,
|
|
33
|
-
} from '../routes.js';
|
|
34
|
-
import {
|
|
35
|
-
patch_global_fetch,
|
|
36
|
-
build_rpc_lookup,
|
|
37
|
-
is_rpc_request,
|
|
38
|
-
handle_rpc_request,
|
|
39
|
-
} from '@ripple-ts/adapter/rpc';
|
|
40
|
-
|
|
41
|
-
export { resolveOctaneConfig } from '../resolve-config.js';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
|
|
45
|
-
* @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
|
|
46
|
-
* @typedef {import('@octanejs/vite-plugin').Context} Context
|
|
47
|
-
*/
|
|
48
|
-
/**
|
|
49
|
-
@import { ServerManifest, HandlerOptions, ClientAssetEntry } from '../../types/production.d.ts'
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create the production request handler from a manifest.
|
|
54
|
-
*
|
|
55
|
-
* The returned function is a standard Web `fetch`-style handler:
|
|
56
|
-
* `(request: Request) => Promise<Response>` — the generated server entry boots
|
|
57
|
-
* it behind the adapter's `serve()` (or the built-in Node server), and
|
|
58
|
-
* serverless wrappers import it directly.
|
|
59
|
-
*
|
|
60
|
-
* @param {ServerManifest} manifest
|
|
61
|
-
* @param {HandlerOptions} deps
|
|
62
|
-
* @returns {(request: Request) => Promise<Response>}
|
|
63
|
-
*/
|
|
64
|
-
export function createHandler(manifest, deps) {
|
|
65
|
-
const { renderToReadableStream, prerender, htmlTemplate, executeServerFunction } = deps;
|
|
66
|
-
const router = createRouter(manifest.routes);
|
|
67
|
-
const globalMiddlewares = manifest.middlewares ?? [];
|
|
68
|
-
const trustProxy = manifest.trustProxy ?? false;
|
|
69
|
-
const runtime = manifest.runtime;
|
|
70
|
-
|
|
71
|
-
// RPC lookup for `module server` functions (hash → server function). Empty
|
|
72
|
-
// today — the octane compiler does not emit `module server` modules yet —
|
|
73
|
-
// but the wiring matches the dev middleware so it lights up when it does.
|
|
74
|
-
const rpcLookup =
|
|
75
|
-
manifest.rpcModules && runtime ? build_rpc_lookup(manifest.rpcModules, runtime.hash) : null;
|
|
76
|
-
|
|
77
|
-
// Request-scoped async context + same-origin fetch short-circuit: fetch()
|
|
78
|
-
// during SSR that resolves to this origin routes through the handler
|
|
79
|
-
// in-process instead of a network round-trip.
|
|
80
|
-
const asyncContext = runtime?.createAsyncContext();
|
|
81
|
-
const fetchHandle = asyncContext ? patch_global_fetch(asyncContext) : null;
|
|
82
|
-
|
|
83
|
-
const handler = async function handler(/** @type {Request} */ request) {
|
|
84
|
-
const url = new URL(request.url);
|
|
85
|
-
const method = request.method;
|
|
86
|
-
|
|
87
|
-
if (is_rpc_request(url.pathname)) {
|
|
88
|
-
if (!rpcLookup || !asyncContext) {
|
|
89
|
-
return new Response(JSON.stringify({ error: 'RPC is not configured' }), {
|
|
90
|
-
status: 404,
|
|
91
|
-
headers: { 'Content-Type': 'application/json' },
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return handle_rpc_request(request, {
|
|
95
|
-
resolveFunction(/** @type {string} */ hash) {
|
|
96
|
-
const entry = rpcLookup.get(hash);
|
|
97
|
-
if (!entry) return null;
|
|
98
|
-
const fn = entry.serverObj[entry.funcName];
|
|
99
|
-
return typeof fn === 'function' ? fn : null;
|
|
100
|
-
},
|
|
101
|
-
executeServerFunction,
|
|
102
|
-
asyncContext,
|
|
103
|
-
trustProxy,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const match = router.match(method, url.pathname);
|
|
108
|
-
if (!match) {
|
|
109
|
-
// Static assets never reach here (the static layer — the built-in Node
|
|
110
|
-
// server, or the platform's file serving — runs first); an app with a
|
|
111
|
-
// catch-all RenderRoute matches everything else, so this is only hit
|
|
112
|
-
// when no catch-all exists.
|
|
113
|
-
return new Response('Not Found', { status: 404 });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const context = createContext(request, match.params);
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
if (match.route.type === 'render') {
|
|
120
|
-
return await runMiddlewareChain(
|
|
121
|
-
context,
|
|
122
|
-
globalMiddlewares,
|
|
123
|
-
match.route.before || [],
|
|
124
|
-
async () => renderRoute(/** @type {RenderRoute} */ (match.route), context),
|
|
125
|
-
[],
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
return await handleServerRoute(match.route, context, globalMiddlewares);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error('[@octanejs/vite-plugin] Request error:', error);
|
|
131
|
-
return new Response('Internal Server Error', { status: 500 });
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
fetchHandle?.set_handler(handler);
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Render a RenderRoute — the production twin of dev's `handleRenderRoute`.
|
|
139
|
-
*
|
|
140
|
-
* @param {RenderRoute} route
|
|
141
|
-
* @param {Context} context
|
|
142
|
-
* @returns {Promise<Response>}
|
|
143
|
-
*/
|
|
144
|
-
async function renderRoute(route, context) {
|
|
145
|
-
const entryPath = get_route_entry_path(route.entry);
|
|
146
|
-
const exportName = get_route_entry_export_name(route.entry);
|
|
147
|
-
const PageComponent = entryPath
|
|
148
|
-
? get_component_export(manifest.components[entryPath] ?? {}, exportName)
|
|
149
|
-
: null;
|
|
150
|
-
if (!PageComponent) {
|
|
151
|
-
throw new Error(`Component not found for route ${route.path}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Identical props to dev: `{ params, url }`, url origin-free so the client
|
|
155
|
-
// re-renders the exact string.
|
|
156
|
-
const requestUrl = context.url.pathname + context.url.search;
|
|
157
|
-
const pageProps = { params: context.params, url: requestUrl };
|
|
158
|
-
|
|
159
|
-
let RootComponent;
|
|
160
|
-
if (route.layout) {
|
|
161
|
-
const LayoutComponent = get_component_export(manifest.layouts[route.layout] ?? {}, undefined);
|
|
162
|
-
if (!LayoutComponent) {
|
|
163
|
-
throw new Error(`No layout component found for ${route.layout}`);
|
|
164
|
-
}
|
|
165
|
-
RootComponent = createLayoutWrapper(
|
|
166
|
-
/** @type {any} */ (LayoutComponent),
|
|
167
|
-
/** @type {any} */ (PageComponent),
|
|
168
|
-
pageProps,
|
|
169
|
-
);
|
|
170
|
-
} else {
|
|
171
|
-
RootComponent = createPropsWrapper(/** @type {any} */ (PageComponent), pageProps);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// The hydration payload — SAME keys, SAME order as dev render-route.js, so
|
|
175
|
-
// the data script is byte-identical between dev and production.
|
|
176
|
-
const routeData = JSON.stringify({
|
|
177
|
-
entry: entryPath,
|
|
178
|
-
exportName: exportName ?? null,
|
|
179
|
-
layout: route.layout ?? null,
|
|
180
|
-
routeIndex: getRenderRouteIndex(manifest.routes, route),
|
|
181
|
-
params: context.params,
|
|
182
|
-
url: requestUrl,
|
|
183
|
-
preHydrate: manifest.preHydrate ?? null,
|
|
184
|
-
});
|
|
185
|
-
const dataScript = `<script id="__octane_data" type="application/json">${escapeScript(routeData)}</script>`;
|
|
186
|
-
|
|
187
|
-
// Per-route asset hints from the client manifest: stylesheet links so
|
|
188
|
-
// page CSS applies before hydration, modulepreload so the page chunk
|
|
189
|
-
// downloads in parallel with the hydrate entry (which the template's own
|
|
190
|
-
// script tag already references).
|
|
191
|
-
/** @type {string[]} */
|
|
192
|
-
const preloadTags = [];
|
|
193
|
-
const entryAssets = entryPath ? manifest.clientAssets?.[entryPath] : undefined;
|
|
194
|
-
if (entryAssets) {
|
|
195
|
-
for (const cssFile of entryAssets.css) {
|
|
196
|
-
preloadTags.push(`<link rel="stylesheet" href="/${cssFile}">`);
|
|
197
|
-
}
|
|
198
|
-
if (entryAssets.js) {
|
|
199
|
-
preloadTags.push(`<link rel="modulepreload" href="/${entryAssets.js}">`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const headContent = [...preloadTags, dataScript].join('\n');
|
|
204
|
-
const html = htmlTemplate.replace('<!--ssr-head-->', headContent);
|
|
205
|
-
|
|
206
|
-
const status = route.status ?? 200;
|
|
207
|
-
const headers = { 'Content-Type': 'text/html; charset=utf-8' };
|
|
208
|
-
|
|
209
|
-
const splitAt = html.indexOf('<!--ssr-body-->');
|
|
210
|
-
if (splitAt === -1) {
|
|
211
|
-
return new Response(html, { status, headers });
|
|
212
|
-
}
|
|
213
|
-
const prefix = html.slice(0, splitAt);
|
|
214
|
-
const suffix = html.slice(splitAt + '<!--ssr-body-->'.length);
|
|
215
|
-
|
|
216
|
-
if (manifest.render === 'buffered') {
|
|
217
|
-
// Await-everything fallback (`prerender` from octane/static): no
|
|
218
|
-
// streaming, one document. The deduped scoped-style tags lead the body
|
|
219
|
-
// markup inside #root — the same position they hold in the streamed
|
|
220
|
-
// shell — so hydrateRoot's leading-style skip applies unchanged.
|
|
221
|
-
const { html: body, css } = await prerender(RootComponent, undefined, {
|
|
222
|
-
onError(/** @type {unknown} */ error) {
|
|
223
|
-
console.error('[octane] SSR render error:', error);
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
return new Response(prefix + css + body + suffix, { status, headers });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Streaming (default): shell flushes at first await, suspense segments
|
|
230
|
-
// stream out-of-order behind it — identical to dev.
|
|
231
|
-
/** @type {ReadableStream<Uint8Array>} */
|
|
232
|
-
const renderStream = await renderToReadableStream(RootComponent, undefined, {
|
|
233
|
-
onError(/** @type {unknown} */ error) {
|
|
234
|
-
console.error('[octane] SSR render error:', error);
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const encoder = new TextEncoder();
|
|
239
|
-
const body = new ReadableStream({
|
|
240
|
-
async start(controller) {
|
|
241
|
-
controller.enqueue(encoder.encode(prefix));
|
|
242
|
-
const reader = renderStream.getReader();
|
|
243
|
-
try {
|
|
244
|
-
while (true) {
|
|
245
|
-
const { done, value } = await reader.read();
|
|
246
|
-
if (done) break;
|
|
247
|
-
controller.enqueue(value);
|
|
248
|
-
}
|
|
249
|
-
controller.enqueue(encoder.encode(suffix));
|
|
250
|
-
controller.close();
|
|
251
|
-
} catch (error) {
|
|
252
|
-
controller.error(error);
|
|
253
|
-
} finally {
|
|
254
|
-
reader.releaseLock();
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
cancel(reason) {
|
|
258
|
-
return renderStream.cancel(reason);
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
return new Response(body, { status, headers });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return handler;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* @param {import('@octanejs/vite-plugin').Route[]} routes
|
|
270
|
-
* @param {RenderRoute} route
|
|
271
|
-
* @returns {number | undefined}
|
|
272
|
-
*/
|
|
273
|
-
function getRenderRouteIndex(routes, route) {
|
|
274
|
-
const renderRoutes = routes.filter((r) => r.type === 'render');
|
|
275
|
-
const index = renderRoutes.indexOf(route);
|
|
276
|
-
return index === -1 ? undefined : index;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Escape script content to prevent XSS in the inline JSON data block.
|
|
281
|
-
* @param {string} str
|
|
282
|
-
* @returns {string}
|
|
283
|
-
*/
|
|
284
|
-
function escapeScript(str) {
|
|
285
|
-
return str.replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
286
|
-
}
|
|
1
|
+
export * from '@octanejs/app-core/production';
|