@octanejs/vite-plugin 0.1.5 → 0.1.9

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/src/routes.js CHANGED
@@ -1,133 +1 @@
1
- // @ts-check
2
- /**
3
- * @typedef {import('@octanejs/vite-plugin').Context} Context
4
- * @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
5
- * @typedef {import('@octanejs/vite-plugin').RenderRouteOptions} RenderRouteOptions
6
- * @typedef {import('@octanejs/vite-plugin').ServerRouteOptions} ServerRouteOptions
7
- */
8
-
9
- /**
10
- * @typedef {string | readonly [string, string]} RenderRouteEntry
11
- */
12
-
13
- /**
14
- * @param {RenderRouteEntry | undefined} entry
15
- * @returns {string | undefined}
16
- */
17
- export function get_route_entry_path(entry) {
18
- return typeof entry === 'string' ? entry : entry?.[1];
19
- }
20
-
21
- /**
22
- * @param {RenderRouteEntry | undefined} entry
23
- * @returns {string | undefined}
24
- */
25
- export function get_route_entry_export_name(entry) {
26
- return typeof entry === 'string' ? undefined : entry?.[0];
27
- }
28
-
29
- /**
30
- * @param {RenderRouteEntry | undefined} entry
31
- * @returns {string | undefined}
32
- */
33
- export function get_route_entry_id(entry) {
34
- const path = get_route_entry_path(entry);
35
- const export_name = get_route_entry_export_name(entry);
36
- return path && export_name ? `${path}#${export_name}` : path;
37
- }
38
-
39
- /**
40
- * @param {Record<string, unknown>} module
41
- * @param {string | undefined} export_name
42
- * @returns {Function | null}
43
- */
44
- export function get_component_export(module, export_name) {
45
- // When an explicit export name is given, require an exact match. Do NOT fall
46
- // back to default/first-PascalCase — a typo'd route tuple should fail loudly
47
- // rather than silently render the wrong component.
48
- if (export_name) {
49
- return typeof module[export_name] === 'function' ? module[export_name] : null;
50
- }
51
- if (typeof module.default === 'function') {
52
- return module.default;
53
- }
54
- for (const [key, value] of Object.entries(module)) {
55
- if (typeof value === 'function' && /^[A-Z]/.test(key)) {
56
- return value;
57
- }
58
- }
59
- return null;
60
- }
61
-
62
- /**
63
- * Route for rendering octane components with SSR
64
- */
65
- export class RenderRoute {
66
- /** @type {'render'} */
67
- type = 'render';
68
-
69
- /** @type {string} */
70
- path;
71
-
72
- // Non-optional: the constructor throws without one (matches types/index.d.ts).
73
- /** @type {RenderRouteEntry} */
74
- entry;
75
-
76
- /** @type {string | undefined} */
77
- layout;
78
-
79
- /** @type {Middleware[]} */
80
- before;
81
-
82
- /** @type {number | undefined} */
83
- status;
84
-
85
- /**
86
- * @param {RenderRouteOptions} options
87
- */
88
- constructor(options) {
89
- if (!options.entry) {
90
- throw new Error('RenderRoute requires an `entry`.');
91
- }
92
-
93
- this.path = options.path;
94
- this.entry = options.entry;
95
- this.layout = options.layout;
96
- this.before = options.before ?? [];
97
- this.status = options.status;
98
- }
99
- }
100
-
101
- /**
102
- * Route for API endpoints (returns Response directly)
103
- */
104
- export class ServerRoute {
105
- /** @type {'server'} */
106
- type = 'server';
107
-
108
- /** @type {string} */
109
- path;
110
-
111
- /** @type {string[]} */
112
- methods;
113
-
114
- /** @type {(context: Context) => Response | Promise<Response>} */
115
- handler;
116
-
117
- /** @type {Middleware[]} */
118
- before;
119
-
120
- /** @type {Middleware[]} */
121
- after;
122
-
123
- /**
124
- * @param {ServerRouteOptions} options
125
- */
126
- constructor(options) {
127
- this.path = options.path;
128
- this.methods = options.methods ?? ['GET'];
129
- this.handler = options.handler;
130
- this.before = options.before ?? [];
131
- this.after = options.after ?? [];
132
- }
133
- }
1
+ export * from '@octanejs/app-core/routes';
@@ -1,56 +1,5 @@
1
- // @ts-check
2
- /**
3
- * Server component composition for the octane renderer.
4
- *
5
- * octane's server ABI is PROPS-FIRST (matching the client): a component body is
6
- * `(props, scope, extra) => string`. `render(Component, props)` invokes the ROOT
7
- * directly as `Component(props, rootScope, undefined)` and does NOT wrap it in
8
- * block markers — and `hydrateRoot()` adopts the container's FIRST CHILD as the
9
- * root's own node. So the wrapper must call the top-level component DIRECTLY
10
- * (wrapping it in `ssrComponent` would add an extra `<!--[-->…<!--]-->` layer that
11
- * `clone()` then mis-adopts on hydrate).
12
- *
13
- * Only the layout's `{children}` is a nested hole: the compiled layout emits
14
- * `ssrChild(props.children, scope)`, and `ssrChild` invokes a FUNCTION child as
15
- * `children({}, scope, undefined)` wrapped in one `<!--[-->…<!--]-->` range. So
16
- * `children` is a ComponentBody that calls the page directly, and any page data
17
- * (params) rides its CLOSURE — `ssrChild` supplies only `{}`. The client
18
- * `childSlot` applies the identical rule (bare function = ComponentBody, `{}`
19
- * props, one marker range), so server markers and client adoption line up.
20
- *
21
- * @typedef {(props?: any, scope?: any, extra?: any) => string} ServerComponent
22
- */
23
-
24
- /**
25
- * Wrap a page component, baking in its route props.
26
- *
27
- * @param {ServerComponent} Page
28
- * @param {Record<string, unknown>} pageProps
29
- * @returns {ServerComponent}
30
- */
31
- export function createPropsWrapper(Page, pageProps) {
32
- return function Root(_props, scope) {
33
- return Page(pageProps, scope, undefined);
34
- };
35
- }
36
-
37
- /**
38
- * Compose a layout with a page: the layout's `{children}` renders the page.
39
- *
40
- * @param {ServerComponent} Layout
41
- * @param {ServerComponent} Page
42
- * @param {Record<string, unknown>} pageProps
43
- * @returns {ServerComponent}
44
- */
45
- export function createLayoutWrapper(Layout, Page, pageProps) {
46
- return function Root(_props, scope) {
47
- // `children` is a ComponentBody closing over pageProps; the layout's
48
- // `{children}` hole runs it via ssrChild (which supplies `{}` props and
49
- // wraps the output in one block range), so the page still gets its real
50
- // route props through the closure. PROPS-FIRST: childSlot/ssrChild call it
51
- // as `({}, scope, extra)`, so the page's real props are passed explicitly.
52
- const children = (/** @type {any} */ _cprops, /** @type {any} */ cscope) =>
53
- Page(pageProps, cscope, undefined);
54
- return Layout({ ...pageProps, children }, scope, undefined);
55
- };
56
- }
1
+ export {
2
+ createLayoutWrapper,
3
+ createPropsWrapper,
4
+ createRootBoundaryWrapper,
5
+ } from '@octanejs/app-core/production';
@@ -0,0 +1 @@
1
+ export { composeHtmlStream } from '@octanejs/app-core/html';
@@ -0,0 +1,9 @@
1
+ export {
2
+ HYDRATION_NONCE_PLACEHOLDER,
3
+ applyHydrationNonce,
4
+ getContextNonce,
5
+ injectHydrationEntry,
6
+ nonceAttribute,
7
+ splitSsrTemplate,
8
+ validateSsrTemplate,
9
+ } from '@octanejs/app-core/html';
@@ -1,127 +1 @@
1
- // @ts-check
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';
@@ -1,188 +1 @@
1
- // @ts-check
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';