@octanejs/vite-plugin 0.1.1
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 +10 -0
- package/LICENSE +21 -0
- package/package.json +45 -0
- package/src/bin/preview.js +46 -0
- package/src/constants.js +2 -0
- package/src/index.js +486 -0
- package/src/load-config.js +240 -0
- package/src/project-codegen.js +158 -0
- package/src/routes.js +127 -0
- package/src/server/component-wrappers.js +55 -0
- package/src/server/middleware.js +126 -0
- package/src/server/production.js +26 -0
- package/src/server/render-route.js +214 -0
- package/src/server/router.js +122 -0
- package/src/server/server-route.js +46 -0
- package/src/server/virtual-entry.js +15 -0
- package/tsconfig.typecheck.json +18 -0
- package/types/index.d.ts +189 -0
- package/types/production.d.ts +59 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production fetch-handler factory + config re-exports.
|
|
3
|
+
*
|
|
4
|
+
* PHASE 1 STUB. `createHandler` is the runtime entry the generated server
|
|
5
|
+
* bundle calls in production; it is wired in Phase 2 (async render, adapter
|
|
6
|
+
* `serve`, asset preload, RPC). Until then it throws a clear error. The
|
|
7
|
+
* `./production` export path stays resolvable, and `resolveOctaneConfig` is
|
|
8
|
+
* re-exported here because the generated server entry imports it from this
|
|
9
|
+
* module (mirrors @ripple-ts/vite-plugin).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { resolveOctaneConfig } from '../load-config.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create the production fetch handler (Phase 2).
|
|
16
|
+
*
|
|
17
|
+
* @param {unknown} _manifest
|
|
18
|
+
* @param {unknown} _deps
|
|
19
|
+
* @returns {never}
|
|
20
|
+
*/
|
|
21
|
+
export function createHandler(_manifest, _deps) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'[@octanejs/vite-plugin] Production build (createHandler) is Phase 2 — not yet implemented. ' +
|
|
24
|
+
'Dev SSR works via `vite dev`.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
|
|
4
|
+
import {
|
|
5
|
+
get_component_export,
|
|
6
|
+
get_route_entry_export_name,
|
|
7
|
+
get_route_entry_path,
|
|
8
|
+
} from '../routes.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('@octanejs/vite-plugin').Context} Context
|
|
12
|
+
* @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
|
|
13
|
+
* @typedef {import('@octanejs/vite-plugin').ResolvedOctaneConfig} ResolvedOctaneConfig
|
|
14
|
+
* @typedef {import('vite').ViteDevServer} ViteDevServer
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* octane RenderResult — `render()` is ASYNC and `css` is ALREADY a deduped
|
|
19
|
+
* `<style data-octane="hash">…</style>` string (NOT a Set<string> needing a
|
|
20
|
+
* `get_css_for_hashes` lookup like Ripple). So CSS handling here is identity.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} RenderResult
|
|
23
|
+
* @property {string} head
|
|
24
|
+
* @property {string} body
|
|
25
|
+
* @property {string} css
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle SSR rendering for a RenderRoute (dev).
|
|
30
|
+
*
|
|
31
|
+
* @param {RenderRoute} route
|
|
32
|
+
* @param {Context} context
|
|
33
|
+
* @param {ViteDevServer} vite
|
|
34
|
+
* @param {ResolvedOctaneConfig} [octaneConfig]
|
|
35
|
+
* @returns {Promise<Response>}
|
|
36
|
+
*/
|
|
37
|
+
export async function handleRenderRoute(route, context, vite, octaneConfig) {
|
|
38
|
+
try {
|
|
39
|
+
// Initialize so the server can register RPC functions from `module server`
|
|
40
|
+
// declarations during SSR module loading (renderer-agnostic; harmless when
|
|
41
|
+
// the app uses no RPC).
|
|
42
|
+
if (!(/** @type {any} */ (globalThis).rpc_modules)) {
|
|
43
|
+
/** @type {any} */ (globalThis).rpc_modules = new Map();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Load the octane server runtime. The wrappers call components directly
|
|
47
|
+
// (no ssrComponent injection — the root must NOT be marker-wrapped), so
|
|
48
|
+
// only `render` is needed here.
|
|
49
|
+
const { render } = await vite.ssrLoadModule('octane/server');
|
|
50
|
+
|
|
51
|
+
// Load the page component (compiled in server mode by octane()).
|
|
52
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
53
|
+
const pageModule = await vite.ssrLoadModule(/** @type {string} */ (entryPath));
|
|
54
|
+
const PageComponent = get_component_export(
|
|
55
|
+
pageModule,
|
|
56
|
+
get_route_entry_export_name(route.entry),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!PageComponent) {
|
|
60
|
+
throw new Error(`No component found for route ${route.path}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the component tree (with optional layout).
|
|
64
|
+
let RootComponent;
|
|
65
|
+
const pageProps = { params: context.params };
|
|
66
|
+
|
|
67
|
+
if (route.layout) {
|
|
68
|
+
const layoutModule = await vite.ssrLoadModule(route.layout);
|
|
69
|
+
const LayoutComponent = get_component_export(layoutModule, undefined);
|
|
70
|
+
|
|
71
|
+
if (!LayoutComponent) {
|
|
72
|
+
throw new Error(`No default export found in ${route.layout}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
RootComponent = createLayoutWrapper(
|
|
76
|
+
/** @type {any} */ (LayoutComponent),
|
|
77
|
+
/** @type {any} */ (PageComponent),
|
|
78
|
+
pageProps,
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
RootComponent = createPropsWrapper(/** @type {any} */ (PageComponent), pageProps);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Render to HTML. octane render() is async; head is '' in Phase 1
|
|
85
|
+
// (no document-head API yet). `body` already contains any inline
|
|
86
|
+
// <script data-octane-suspense> seed.
|
|
87
|
+
/** @type {RenderResult} */
|
|
88
|
+
const { head, body, css } = await render(RootComponent);
|
|
89
|
+
|
|
90
|
+
// CSS is already a ready <style> string (or '') — identity, no re-wrapping.
|
|
91
|
+
const cssContent = css || '';
|
|
92
|
+
|
|
93
|
+
// Build head content with hydration data. The client entry is CONFIG-FREE
|
|
94
|
+
// (importing octane.config.ts into the browser would drag the plugin + the
|
|
95
|
+
// server adapter — and their `node:fs` imports — into the client graph and
|
|
96
|
+
// break at module-eval). So everything the client needs to pick + import
|
|
97
|
+
// the page/layout is serialized HERE: entry path, export name, layout path,
|
|
98
|
+
// and params. routeIndex stays for debugging / Phase-2 static maps.
|
|
99
|
+
const routeData = JSON.stringify({
|
|
100
|
+
entry: entryPath,
|
|
101
|
+
exportName: get_route_entry_export_name(route.entry) ?? null,
|
|
102
|
+
layout: route.layout ?? null,
|
|
103
|
+
routeIndex: getRenderRouteIndex(octaneConfig, route),
|
|
104
|
+
params: context.params,
|
|
105
|
+
});
|
|
106
|
+
const headContent = [
|
|
107
|
+
head,
|
|
108
|
+
cssContent,
|
|
109
|
+
`<script id="__octane_data" type="application/json">${escapeScript(routeData)}</script>`,
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join('\n');
|
|
113
|
+
|
|
114
|
+
// Load and process index.html template.
|
|
115
|
+
const templatePath = join(vite.config.root, 'index.html');
|
|
116
|
+
let template = await readFile(templatePath, 'utf-8');
|
|
117
|
+
|
|
118
|
+
// Apply Vite's HTML transforms (HMR client, module resolution, etc.).
|
|
119
|
+
template = await vite.transformIndexHtml(context.url.pathname, template);
|
|
120
|
+
|
|
121
|
+
// Replace placeholders.
|
|
122
|
+
let html = template.replace('<!--ssr-head-->', headContent).replace('<!--ssr-body-->', body);
|
|
123
|
+
|
|
124
|
+
// Inject the hydration entry before </body>.
|
|
125
|
+
const hydrationScript = `<script type="module" src="/@id/virtual:octane-hydrate"></script>`;
|
|
126
|
+
html = html.replace('</body>', `${hydrationScript}\n</body>`);
|
|
127
|
+
|
|
128
|
+
return new Response(html, {
|
|
129
|
+
status: 200,
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('[octane] SSR render error:', error);
|
|
136
|
+
|
|
137
|
+
const errorHtml = generateErrorHtml(error, route);
|
|
138
|
+
return new Response(errorHtml, {
|
|
139
|
+
status: 500,
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {ResolvedOctaneConfig | undefined} config
|
|
149
|
+
* @param {RenderRoute} route
|
|
150
|
+
* @returns {number | undefined}
|
|
151
|
+
*/
|
|
152
|
+
function getRenderRouteIndex(config, route) {
|
|
153
|
+
if (!config) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const renderRoutes = config.router.routes.filter((r) => r.type === 'render');
|
|
157
|
+
const index = renderRoutes.indexOf(route);
|
|
158
|
+
return index === -1 ? undefined : index;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Escape script content to prevent XSS in the inline JSON data block.
|
|
163
|
+
* @param {string} str
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function escapeScript(str) {
|
|
167
|
+
return str.replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate an error HTML page for development.
|
|
172
|
+
*
|
|
173
|
+
* @param {unknown} error
|
|
174
|
+
* @param {RenderRoute} route
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
function generateErrorHtml(error, route) {
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
+
const stack = error instanceof Error ? error.stack : '';
|
|
180
|
+
|
|
181
|
+
return `<!DOCTYPE html>
|
|
182
|
+
<html lang="en">
|
|
183
|
+
<head>
|
|
184
|
+
<meta charset="UTF-8">
|
|
185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
186
|
+
<title>SSR Error</title>
|
|
187
|
+
<style>
|
|
188
|
+
body { font-family: system-ui, sans-serif; padding: 2rem; background: #1a1a1a; color: #fff; }
|
|
189
|
+
h1 { color: #ff6b6b; }
|
|
190
|
+
pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
|
191
|
+
.route { color: #888; }
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
<h1>SSR Render Error</h1>
|
|
196
|
+
<p class="route">Route: ${route.path} → ${route.entry}</p>
|
|
197
|
+
<pre>${escapeHtml(message)}</pre>
|
|
198
|
+
${stack ? `<pre>${escapeHtml(stack)}</pre>` : ''}
|
|
199
|
+
</body>
|
|
200
|
+
</html>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Escape HTML entities.
|
|
205
|
+
* @param {string} str
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function escapeHtml(str) {
|
|
209
|
+
return str
|
|
210
|
+
.replace(/&/g, '&')
|
|
211
|
+
.replace(/</g, '<')
|
|
212
|
+
.replace(/>/g, '>')
|
|
213
|
+
.replace(/"/g, '"');
|
|
214
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('@octanejs/vite-plugin').Route} Route
|
|
3
|
+
* @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
|
|
4
|
+
* @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} RouteMatch
|
|
9
|
+
* @property {Route} route
|
|
10
|
+
* @property {Record<string, string>} params
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} CompiledRoute
|
|
15
|
+
* @property {Route} route
|
|
16
|
+
* @property {RegExp} pattern
|
|
17
|
+
* @property {string[]} paramNames
|
|
18
|
+
* @property {number} specificity - Higher = more specific (static > param > catch-all)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert a route path pattern to a RegExp
|
|
23
|
+
* Supports:
|
|
24
|
+
* - Static segments: /about, /api/hello
|
|
25
|
+
* - Named params: /posts/:id, /users/:userId/posts/:postId
|
|
26
|
+
* - Catch-all: /docs/*slug
|
|
27
|
+
*
|
|
28
|
+
* @param {string} path
|
|
29
|
+
* @returns {{ pattern: RegExp, paramNames: string[], specificity: number }}
|
|
30
|
+
*/
|
|
31
|
+
function compilePath(path) {
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
const paramNames = [];
|
|
34
|
+
let specificity = 0;
|
|
35
|
+
|
|
36
|
+
// Escape special regex characters except our param syntax
|
|
37
|
+
const regexString = path
|
|
38
|
+
.split('/')
|
|
39
|
+
.map((segment) => {
|
|
40
|
+
if (!segment) return '';
|
|
41
|
+
|
|
42
|
+
// Catch-all param: *slug
|
|
43
|
+
if (segment.startsWith('*')) {
|
|
44
|
+
const paramName = segment.slice(1);
|
|
45
|
+
paramNames.push(paramName);
|
|
46
|
+
specificity += 1; // Lowest specificity
|
|
47
|
+
return '(.+)';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Named param: :id
|
|
51
|
+
if (segment.startsWith(':')) {
|
|
52
|
+
const paramName = segment.slice(1);
|
|
53
|
+
paramNames.push(paramName);
|
|
54
|
+
specificity += 10; // Medium specificity
|
|
55
|
+
return '([^/]+)';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Static segment
|
|
59
|
+
specificity += 100; // Highest specificity
|
|
60
|
+
return escapeRegex(segment);
|
|
61
|
+
})
|
|
62
|
+
.join('/');
|
|
63
|
+
|
|
64
|
+
const pattern = new RegExp(`^${regexString || '/'}$`);
|
|
65
|
+
return { pattern, paramNames, specificity };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Escape special regex characters
|
|
70
|
+
* @param {string} str
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function escapeRegex(str) {
|
|
74
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a router from a list of routes
|
|
79
|
+
* @param {Route[]} routes
|
|
80
|
+
* @returns {{ match: (method: string, pathname: string) => RouteMatch | null }}
|
|
81
|
+
*/
|
|
82
|
+
export function createRouter(routes) {
|
|
83
|
+
/** @type {CompiledRoute[]} */
|
|
84
|
+
const compiledRoutes = routes.map((route) => {
|
|
85
|
+
const { pattern, paramNames, specificity } = compilePath(route.path);
|
|
86
|
+
return { route, pattern, paramNames, specificity };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Sort by specificity (higher first) for correct matching order
|
|
90
|
+
compiledRoutes.sort((a, b) => b.specificity - a.specificity);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
/**
|
|
94
|
+
* Match a request to a route
|
|
95
|
+
* @param {string} method
|
|
96
|
+
* @param {string} pathname
|
|
97
|
+
* @returns {RouteMatch | null}
|
|
98
|
+
*/
|
|
99
|
+
match(method, pathname) {
|
|
100
|
+
for (const { route, pattern, paramNames } of compiledRoutes) {
|
|
101
|
+
// Check method for ServerRoute
|
|
102
|
+
if (route.type === 'server') {
|
|
103
|
+
const methods = /** @type {ServerRoute} */ (route).methods;
|
|
104
|
+
if (!methods.includes(method.toUpperCase())) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const match = pathname.match(pattern);
|
|
110
|
+
if (match) {
|
|
111
|
+
/** @type {Record<string, string>} */
|
|
112
|
+
const params = {};
|
|
113
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
114
|
+
params[paramNames[i]] = decodeURIComponent(match[i + 1]);
|
|
115
|
+
}
|
|
116
|
+
return { route, params };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('@octanejs/vite-plugin').Context} Context
|
|
3
|
+
* @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
|
|
4
|
+
* @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runMiddlewareChain } from './middleware.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handle a ServerRoute (API endpoint)
|
|
11
|
+
*
|
|
12
|
+
* @param {ServerRoute} route
|
|
13
|
+
* @param {Context} context
|
|
14
|
+
* @param {Middleware[]} globalMiddlewares
|
|
15
|
+
* @returns {Promise<Response>}
|
|
16
|
+
*/
|
|
17
|
+
export async function handleServerRoute(route, context, globalMiddlewares) {
|
|
18
|
+
try {
|
|
19
|
+
// The handler wrapped as a function returning Promise<Response>
|
|
20
|
+
const handler = async () => {
|
|
21
|
+
return route.handler(context);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Run the middleware chain: global → before → handler → after
|
|
25
|
+
const response = await runMiddlewareChain(
|
|
26
|
+
context,
|
|
27
|
+
globalMiddlewares,
|
|
28
|
+
route.before,
|
|
29
|
+
handler,
|
|
30
|
+
route.after,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return response;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('[octane] API route error:', error);
|
|
36
|
+
|
|
37
|
+
// Return error response
|
|
38
|
+
const message = error instanceof Error ? error.message : 'Internal Server Error';
|
|
39
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
40
|
+
status: 500,
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production server-entry generator.
|
|
3
|
+
*
|
|
4
|
+
* PHASE 1 STUB. The generated server entry (imported by the SSR bundle build
|
|
5
|
+
* in `closeBundle`) wires page/layout/rpc modules into `createHandler`. It is
|
|
6
|
+
* unused in dev (dev SSR uses `vite.ssrLoadModule`). Implemented in Phase 2.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {unknown} _options
|
|
11
|
+
* @returns {never}
|
|
12
|
+
*/
|
|
13
|
+
export function generateServerEntry(_options) {
|
|
14
|
+
throw new Error('[@octanejs/vite-plugin] Server entry generation is Phase 2.');
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"moduleResolution": "nodenext",
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"checkJs": false
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*", "types/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
18
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Plugin, BuildEnvironmentOptions, ViteDevServer } from 'vite';
|
|
2
|
+
import type { RuntimePrimitives } from '@ripple-ts/adapter';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Plugin exports
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export interface OctanePluginOptions {
|
|
9
|
+
/** Override the client HMR default (on in serve mode, off for SSR). */
|
|
10
|
+
hmr?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The octane metaframework plugin. Returns an array:
|
|
15
|
+
* `[octane(), metaPlugin]` — the first compiles `.tsrx`, the second owns
|
|
16
|
+
* config / routing / dev SSR / hydrate.
|
|
17
|
+
*/
|
|
18
|
+
export function octane(options?: OctanePluginOptions): Plugin[];
|
|
19
|
+
export function defineConfig(options: OctaneConfigOptions): OctaneConfigOptions;
|
|
20
|
+
export function resolveOctaneConfig(
|
|
21
|
+
raw: OctaneConfigOptions,
|
|
22
|
+
options?: { requireAdapter?: boolean },
|
|
23
|
+
): ResolvedOctaneConfig;
|
|
24
|
+
export function getOctaneConfigPath(projectRoot: string): string;
|
|
25
|
+
export function octaneConfigExists(projectRoot: string): boolean;
|
|
26
|
+
export function loadOctaneConfig(
|
|
27
|
+
projectRoot: string,
|
|
28
|
+
options?: { vite?: ViteDevServer; requireAdapter?: boolean },
|
|
29
|
+
): Promise<ResolvedOctaneConfig>;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Route classes
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export class RenderRoute {
|
|
36
|
+
readonly type: 'render';
|
|
37
|
+
path: string;
|
|
38
|
+
entry: RenderRouteEntry;
|
|
39
|
+
layout?: string;
|
|
40
|
+
before: Middleware[];
|
|
41
|
+
constructor(options: RenderRouteOptions);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ServerRoute {
|
|
45
|
+
readonly type: 'server';
|
|
46
|
+
path: string;
|
|
47
|
+
methods: string[];
|
|
48
|
+
handler: RouteHandler;
|
|
49
|
+
before: Middleware[];
|
|
50
|
+
after: Middleware[];
|
|
51
|
+
constructor(options: ServerRouteOptions);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Route = RenderRoute | ServerRoute;
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Route options
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export interface RenderRouteOptions {
|
|
61
|
+
/** URL path pattern (e.g., '/', '/posts/:id', '/docs/*slug') */
|
|
62
|
+
path: string;
|
|
63
|
+
/** Path to the component entry file, optionally with a preferred named export */
|
|
64
|
+
entry: RenderRouteEntry;
|
|
65
|
+
/** Path to the layout component (wraps the entry) */
|
|
66
|
+
layout?: string;
|
|
67
|
+
/** Middleware to run before rendering */
|
|
68
|
+
before?: Middleware[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ServerRouteOptions {
|
|
72
|
+
/** URL path pattern (e.g., '/api/hello', '/api/posts/:id') */
|
|
73
|
+
path: string;
|
|
74
|
+
/** HTTP methods to handle (default: ['GET']) */
|
|
75
|
+
methods?: string[];
|
|
76
|
+
/** Request handler that returns a Response */
|
|
77
|
+
handler: RouteHandler;
|
|
78
|
+
/** Middleware to run before the handler */
|
|
79
|
+
before?: Middleware[];
|
|
80
|
+
/** Middleware to run after the handler */
|
|
81
|
+
after?: Middleware[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Context and middleware
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export interface Context {
|
|
89
|
+
/** The incoming Request object */
|
|
90
|
+
request: Request;
|
|
91
|
+
/** URL parameters extracted from the route pattern */
|
|
92
|
+
params: Record<string, string>;
|
|
93
|
+
/** Parsed URL object */
|
|
94
|
+
url: URL;
|
|
95
|
+
/** Shared state for passing data between middlewares */
|
|
96
|
+
state: Map<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type NextFunction = () => Promise<Response>;
|
|
100
|
+
export type Middleware = (context: Context, next: NextFunction) => Response | Promise<Response>;
|
|
101
|
+
export type RouteHandler = (context: Context) => Response | Promise<Response>;
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Configuration
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
export type Component<T = Record<string, any>> = (
|
|
108
|
+
scope: any,
|
|
109
|
+
props: T,
|
|
110
|
+
extra?: any,
|
|
111
|
+
) => string | void;
|
|
112
|
+
|
|
113
|
+
export type RenderRouteEntry = string | readonly [exportName: string, path: string];
|
|
114
|
+
|
|
115
|
+
export interface RootBoundaryOptions {
|
|
116
|
+
pending?: Component<Record<string, never>>;
|
|
117
|
+
catch?: Component<{ error: unknown; reset: () => void }>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface OctaneConfigOptions {
|
|
121
|
+
build?: {
|
|
122
|
+
/** Output directory for the production build. @default 'dist' */
|
|
123
|
+
outDir?: string;
|
|
124
|
+
minify?: boolean;
|
|
125
|
+
target?: BuildEnvironmentOptions['target'];
|
|
126
|
+
};
|
|
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
|
+
};
|
|
136
|
+
router?: {
|
|
137
|
+
routes: Route[];
|
|
138
|
+
};
|
|
139
|
+
/** Global root pending/catch UI used by client and SSR render roots */
|
|
140
|
+
rootBoundary?: RootBoundaryOptions;
|
|
141
|
+
/** Global middlewares applied to all routes */
|
|
142
|
+
middlewares?: Middleware[];
|
|
143
|
+
platform?: {
|
|
144
|
+
env: Record<string, string>;
|
|
145
|
+
};
|
|
146
|
+
server?: {
|
|
147
|
+
/**
|
|
148
|
+
* Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when deriving the
|
|
149
|
+
* request origin. Enable only behind a trusted reverse proxy.
|
|
150
|
+
* @default false
|
|
151
|
+
*/
|
|
152
|
+
trustProxy?: boolean;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolved configuration with all defaults applied.
|
|
158
|
+
*/
|
|
159
|
+
export interface ResolvedOctaneConfig {
|
|
160
|
+
build: {
|
|
161
|
+
/** @default 'dist' */
|
|
162
|
+
outDir: string;
|
|
163
|
+
minify?: boolean;
|
|
164
|
+
target?: BuildEnvironmentOptions['target'];
|
|
165
|
+
};
|
|
166
|
+
adapter?: {
|
|
167
|
+
serve: AdapterServeFunction;
|
|
168
|
+
runtime: RuntimePrimitives;
|
|
169
|
+
};
|
|
170
|
+
router: {
|
|
171
|
+
routes: Route[];
|
|
172
|
+
};
|
|
173
|
+
rootBoundary: RootBoundaryOptions;
|
|
174
|
+
/** @default [] */
|
|
175
|
+
middlewares: Middleware[];
|
|
176
|
+
platform: {
|
|
177
|
+
/** @default {} */
|
|
178
|
+
env: Record<string, string>;
|
|
179
|
+
};
|
|
180
|
+
server: {
|
|
181
|
+
/** @default false */
|
|
182
|
+
trustProxy: boolean;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type AdapterServeFunction = (
|
|
187
|
+
handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
|
|
188
|
+
options?: Record<string, unknown>,
|
|
189
|
+
) => { listen: (port?: number) => unknown; close: () => void };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RuntimePrimitives } from '@ripple-ts/adapter';
|
|
2
|
+
import type {
|
|
3
|
+
Route,
|
|
4
|
+
Middleware,
|
|
5
|
+
ResolvedOctaneConfig,
|
|
6
|
+
OctaneConfigOptions,
|
|
7
|
+
RootBoundaryOptions,
|
|
8
|
+
} from '@octanejs/vite-plugin';
|
|
9
|
+
|
|
10
|
+
export function resolveOctaneConfig(
|
|
11
|
+
raw: OctaneConfigOptions,
|
|
12
|
+
options?: { requireAdapter?: boolean },
|
|
13
|
+
): ResolvedOctaneConfig;
|
|
14
|
+
|
|
15
|
+
export interface ClientAssetEntry {
|
|
16
|
+
/** Path to the built JS file (relative to client output dir) */
|
|
17
|
+
js: string;
|
|
18
|
+
/** Paths to the built CSS files (relative to client output dir) */
|
|
19
|
+
css: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ServerManifest {
|
|
23
|
+
routes: Route[];
|
|
24
|
+
components: Record<string, Function>;
|
|
25
|
+
layouts: Record<string, Function>;
|
|
26
|
+
middlewares: Middleware[];
|
|
27
|
+
/** Map of entry path → _$_server_$_ object for RPC support */
|
|
28
|
+
rpcModules?: Record<string, Record<string, Function>>;
|
|
29
|
+
/** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
|
|
30
|
+
trustProxy?: boolean;
|
|
31
|
+
rootBoundary?: RootBoundaryOptions;
|
|
32
|
+
/** Platform-specific runtime primitives from the adapter */
|
|
33
|
+
runtime: RuntimePrimitives;
|
|
34
|
+
/** Map of route entry paths to built client asset paths (preload tags). */
|
|
35
|
+
clientAssets?: Record<string, ClientAssetEntry>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* octane RenderResult — `render()` is async; `css` is ALREADY a ready,
|
|
40
|
+
* deduped `<style data-octane="hash">…</style>` string (NOT a Set<string>
|
|
41
|
+
* needing a `getCss` lookup like Ripple).
|
|
42
|
+
*/
|
|
43
|
+
export interface RenderResult {
|
|
44
|
+
head: string;
|
|
45
|
+
body: string;
|
|
46
|
+
css: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HandlerOptions {
|
|
50
|
+
render: (component: Function, props?: unknown) => Promise<RenderResult>;
|
|
51
|
+
htmlTemplate: string;
|
|
52
|
+
executeServerFunction: (fn: Function, body: string) => Promise<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Production fetch-handler factory. PHASE 2. */
|
|
56
|
+
export function createHandler(
|
|
57
|
+
manifest: ServerManifest,
|
|
58
|
+
options: HandlerOptions,
|
|
59
|
+
): (request: Request) => Promise<Response>;
|