@octanejs/app-core 0.0.7 → 0.0.8
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 +3 -3
- package/src/codegen.js +4 -0
- package/src/resolve-config.js +16 -0
- package/src/server/middleware.js +8 -3
- package/src/server/production.js +25 -10
- package/src/server/server-entry.js +66 -29
- package/types/codegen.d.ts +3 -2
- package/types/index.d.ts +11 -2
- package/types/production.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/app-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"esbuild": "^0.28.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"octane": "0.1.
|
|
82
|
+
"octane": "0.1.12"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "^24.3.0",
|
|
86
|
-
"octane": "0.1.
|
|
86
|
+
"octane": "0.1.12"
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/codegen.js
CHANGED
|
@@ -14,6 +14,7 @@ export const SERVER_ONLY_ADAPTER_IDS = new Set([
|
|
|
14
14
|
'@ripple-ts/adapter-bun',
|
|
15
15
|
'@ripple-ts/adapter-vercel',
|
|
16
16
|
'@octanejs/adapter-vercel',
|
|
17
|
+
'@octanejs/adapter-cloudflare',
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
/** @type {Map<string, string>} */
|
|
@@ -39,6 +40,9 @@ export function webResponseToNodeResponse() {
|
|
|
39
40
|
export function vercel() {
|
|
40
41
|
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
41
42
|
}
|
|
43
|
+
export function cloudflare() {
|
|
44
|
+
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
45
|
+
}
|
|
42
46
|
export function adapt() {
|
|
43
47
|
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
44
48
|
}
|
package/src/resolve-config.js
CHANGED
|
@@ -123,6 +123,22 @@ export function resolveOctaneConfig(raw, options = {}) {
|
|
|
123
123
|
if (raw.adapter.serve !== undefined && typeof raw.adapter.serve !== 'function') {
|
|
124
124
|
throw new Error('[octane] adapter.serve must be a function.');
|
|
125
125
|
}
|
|
126
|
+
if (
|
|
127
|
+
raw.adapter.serverTarget !== undefined &&
|
|
128
|
+
raw.adapter.serverTarget !== 'node' &&
|
|
129
|
+
raw.adapter.serverTarget !== 'webworker'
|
|
130
|
+
) {
|
|
131
|
+
throw new Error("[octane] adapter.serverTarget must be 'node' or 'webworker'.");
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
raw.adapter.serverTarget === 'webworker' &&
|
|
135
|
+
(typeof raw.adapter.runtime?.hash !== 'function' ||
|
|
136
|
+
typeof raw.adapter.runtime?.createAsyncContext !== 'function')
|
|
137
|
+
) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'[octane] A webworker adapter must provide runtime.hash and runtime.createAsyncContext functions.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
if (
|
package/src/server/middleware.js
CHANGED
|
@@ -59,15 +59,20 @@ export function compose(middlewares) {
|
|
|
59
59
|
* Create a context object for the request
|
|
60
60
|
* @param {Request} request
|
|
61
61
|
* @param {Record<string, string>} params
|
|
62
|
+
* @param {unknown} [platform]
|
|
62
63
|
* @returns {Context}
|
|
63
64
|
*/
|
|
64
|
-
export function createContext(request, params) {
|
|
65
|
-
|
|
65
|
+
export function createContext(request, params, platform) {
|
|
66
|
+
const context = /** @type {Context} */ ({
|
|
66
67
|
request,
|
|
67
68
|
params,
|
|
68
69
|
url: new URL(request.url),
|
|
69
70
|
state: new Map(),
|
|
70
|
-
};
|
|
71
|
+
});
|
|
72
|
+
// Preserve the existing context shape for Node and other integrations that
|
|
73
|
+
// do not supply request-scoped platform bindings.
|
|
74
|
+
if (platform !== undefined) context.platform = platform;
|
|
75
|
+
return context;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
package/src/server/production.js
CHANGED
|
@@ -68,8 +68,8 @@ const FETCH_COORDINATOR_KEY = Symbol.for('octane.app-core.fetch-coordinator');
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* @typedef {Object} FetchCoordinator
|
|
71
|
-
* @property {import('@ripple-ts/adapter/rpc').AsyncContext} asyncContext
|
|
72
|
-
* @property {((request: Request) => Promise<Response>) | null} handler
|
|
71
|
+
* @property {import('@ripple-ts/adapter/rpc').AsyncContext<{ origin?: string, platform?: unknown }>} asyncContext
|
|
72
|
+
* @property {((request: Request, platform?: unknown) => Promise<Response>) | null} handler
|
|
73
73
|
*/
|
|
74
74
|
|
|
75
75
|
/**
|
|
@@ -94,7 +94,7 @@ function getFetchCoordinator(runtime) {
|
|
|
94
94
|
if (!shared.handler) {
|
|
95
95
|
return Promise.resolve(new Response('Octane handler is not ready', { status: 503 }));
|
|
96
96
|
}
|
|
97
|
-
return shared.handler(request);
|
|
97
|
+
return shared.handler(request, shared.asyncContext.getStore()?.platform);
|
|
98
98
|
});
|
|
99
99
|
return coordinator;
|
|
100
100
|
}
|
|
@@ -112,13 +112,15 @@ function getFetchCoordinator(runtime) {
|
|
|
112
112
|
* Create the production request handler from a manifest.
|
|
113
113
|
*
|
|
114
114
|
* The returned function is a standard Web `fetch`-style handler:
|
|
115
|
-
* `(request: Request) => Promise<Response>` — the generated
|
|
116
|
-
* it behind the adapter's `serve()` (or the built-in Node
|
|
117
|
-
* serverless wrappers import it directly.
|
|
115
|
+
* `(request: Request, platform?: unknown) => Promise<Response>` — the generated
|
|
116
|
+
* server entry boots it behind the adapter's `serve()` (or the built-in Node
|
|
117
|
+
* server), and serverless wrappers import it directly. Integrations can expose
|
|
118
|
+
* request-scoped platform bindings to middleware and routes via the optional
|
|
119
|
+
* second argument.
|
|
118
120
|
*
|
|
119
121
|
* @param {ServerManifest} manifest
|
|
120
122
|
* @param {HandlerOptions} deps
|
|
121
|
-
* @returns {(request: Request) => Promise<Response>}
|
|
123
|
+
* @returns {(request: Request, platform?: unknown) => Promise<Response>}
|
|
122
124
|
*/
|
|
123
125
|
export function createHandler(manifest, deps) {
|
|
124
126
|
const { renderToReadableStream, prerender, htmlTemplate, executeServerFunction } = deps;
|
|
@@ -142,7 +144,10 @@ export function createHandler(manifest, deps) {
|
|
|
142
144
|
const fetchCoordinator = getFetchCoordinator(runtime);
|
|
143
145
|
const asyncContext = fetchCoordinator?.asyncContext;
|
|
144
146
|
|
|
145
|
-
const handler = async function handler(
|
|
147
|
+
const handler = async function handler(
|
|
148
|
+
/** @type {Request} */ request,
|
|
149
|
+
/** @type {unknown} */ platform = undefined,
|
|
150
|
+
) {
|
|
146
151
|
const url = new URL(request.url);
|
|
147
152
|
const method = request.method;
|
|
148
153
|
|
|
@@ -153,6 +158,16 @@ export function createHandler(manifest, deps) {
|
|
|
153
158
|
headers: { 'Content-Type': 'application/json' },
|
|
154
159
|
});
|
|
155
160
|
}
|
|
161
|
+
/** @type {import('@ripple-ts/adapter/rpc').AsyncContext<{ origin?: string, platform?: unknown }>} */
|
|
162
|
+
const requestAsyncContext =
|
|
163
|
+
platform === undefined
|
|
164
|
+
? asyncContext
|
|
165
|
+
: {
|
|
166
|
+
run(store, fn) {
|
|
167
|
+
return asyncContext.run({ ...store, platform }, fn);
|
|
168
|
+
},
|
|
169
|
+
getStore: () => asyncContext.getStore(),
|
|
170
|
+
};
|
|
156
171
|
return handle_rpc_request(request, {
|
|
157
172
|
resolveFunction(/** @type {string} */ hash) {
|
|
158
173
|
const entry = rpcLookup.get(hash);
|
|
@@ -161,7 +176,7 @@ export function createHandler(manifest, deps) {
|
|
|
161
176
|
return typeof fn === 'function' ? fn : null;
|
|
162
177
|
},
|
|
163
178
|
executeServerFunction,
|
|
164
|
-
asyncContext,
|
|
179
|
+
asyncContext: requestAsyncContext,
|
|
165
180
|
trustProxy,
|
|
166
181
|
});
|
|
167
182
|
}
|
|
@@ -175,7 +190,7 @@ export function createHandler(manifest, deps) {
|
|
|
175
190
|
return new Response('Not Found', { status: 404 });
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
const context = createContext(request, match.params);
|
|
193
|
+
const context = createContext(request, match.params, platform);
|
|
179
194
|
|
|
180
195
|
try {
|
|
181
196
|
if (match.route.type === 'render') {
|
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
* Production server-entry generator.
|
|
4
4
|
*
|
|
5
5
|
* `generateServerEntry` emits the module an integration uses as its server
|
|
6
|
-
* bundle input. The generated module statically
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* bundle input. The generated module statically imports every RenderRoute
|
|
7
|
+
* entry/layout module (compiled in server mode by the active bundler
|
|
8
|
+
* integration) plus octane.config.ts itself. Its mode selects one of three
|
|
9
|
+
* deployment surfaces:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* - `handler` exports the Web fetch handler and a Node `(req, res)` wrapper
|
|
12
|
+
* for serverless platforms, then auto-boots when run directly.
|
|
13
|
+
* - `manifest` exports the template-free manifest and renderer dependencies
|
|
14
|
+
* consumed by integrations that inject the current HTML themselves.
|
|
15
|
+
* - `webworker` exports those manifest values plus a
|
|
16
|
+
* `createWebWorkerHandler({ htmlTemplate, clientAssets? })` factory, without
|
|
17
|
+
* template filesystem access, a Node HTTP bridge, or automatic boot.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* The Node handler's wrapper supports platforms such as Vercel, where a
|
|
20
|
+
* function can do:
|
|
21
|
+
*
|
|
22
|
+
* - `export { nodeHandler as default } from '../dist/server/entry.js'`
|
|
23
|
+
*
|
|
24
|
+
* Its direct-execution path uses the adapter's `serve()` when configured,
|
|
25
|
+
* otherwise the built-in Node server (static dist/client assets + the handler).
|
|
26
|
+
*
|
|
27
|
+
* Dev integrations use the manifest shape while loading request modules
|
|
28
|
+
* directly; the deployment handler shapes are production-only.
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
/** @import { Route, RootBoundaryOptions } from '@octanejs/app-core' */
|
|
@@ -35,7 +44,7 @@ import { get_route_entry_export_name, get_route_entry_path } from '../routes.js'
|
|
|
35
44
|
* @property {Record<string, string>} [moduleImports] - Stable module ID → bundler import specifier
|
|
36
45
|
* @property {((id: string) => string)} [resolveImport] - Fallback module-specifier mapper
|
|
37
46
|
* @property {string} [configImportPath] - Bundler import specifier for octane.config.ts
|
|
38
|
-
* @property {'handler' | 'manifest'} [mode] - Emit a bootable handler
|
|
47
|
+
* @property {'handler' | 'manifest' | 'webworker'} [mode] - Emit a bootable handler, template-free manifest, or Web Worker factory module
|
|
39
48
|
* @property {string} [serverRuntimeModuleId] - Renderer server runtime module ID
|
|
40
49
|
* @property {string} [staticRuntimeModuleId] - Renderer static runtime module ID
|
|
41
50
|
* @property {string} [productionModuleId] - App-core production runtime module ID
|
|
@@ -160,24 +169,57 @@ export function generateServerEntry(options) {
|
|
|
160
169
|
})
|
|
161
170
|
.join('\n');
|
|
162
171
|
|
|
163
|
-
if (mode === 'manifest') {
|
|
164
|
-
const
|
|
172
|
+
if (mode === 'manifest' || mode === 'webworker') {
|
|
173
|
+
const isWebWorker = mode === 'webworker';
|
|
174
|
+
const readsAssetFile = !isWebWorker && Boolean(clientAssetMapFile);
|
|
175
|
+
const assetFileImports = readsAssetFile
|
|
165
176
|
? `import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n`
|
|
166
177
|
: '';
|
|
167
|
-
const
|
|
178
|
+
const platformImports = isWebWorker
|
|
179
|
+
? ''
|
|
180
|
+
: `import { createHash } from 'node:crypto';\nimport { AsyncLocalStorage } from 'node:async_hooks';\n`;
|
|
181
|
+
const handlerImport = isWebWorker
|
|
182
|
+
? `import { createHandler } from ${JSON.stringify(productionModuleId)};\n`
|
|
183
|
+
: '';
|
|
184
|
+
const assetDirectory = readsAssetFile
|
|
168
185
|
? `const __dirname = dirname(fileURLToPath(import.meta.url));\n`
|
|
169
186
|
: '';
|
|
170
|
-
const clientAssets =
|
|
187
|
+
const clientAssets = readsAssetFile
|
|
171
188
|
? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
|
|
172
189
|
: JSON.stringify(clientAssetMap, null, '\t');
|
|
190
|
+
const runtime = isWebWorker
|
|
191
|
+
? `const runtime = octaneConfig.adapter?.runtime;
|
|
192
|
+
if (!runtime) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"[octane] adapter.serverTarget 'webworker' requires adapter.runtime platform primitives.",
|
|
195
|
+
);
|
|
196
|
+
}`
|
|
197
|
+
: `const runtime = octaneConfig.adapter?.runtime ?? {
|
|
198
|
+
hash: (str) => createHash('sha256').update(str).digest('hex').slice(0, 8),
|
|
199
|
+
createAsyncContext: () => {
|
|
200
|
+
const als = new AsyncLocalStorage();
|
|
201
|
+
return { run: (store, fn) => als.run(store, fn), getStore: () => als.getStore() };
|
|
202
|
+
},
|
|
203
|
+
};`;
|
|
204
|
+
const workerFactory = isWebWorker
|
|
205
|
+
? `
|
|
206
|
+
export function createWebWorkerHandler({ htmlTemplate, clientAssets = manifest.clientAssets }) {
|
|
207
|
+
return createHandler(
|
|
208
|
+
{ ...manifest, clientAssets },
|
|
209
|
+
{ ...rendererDeps, htmlTemplate },
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
`
|
|
213
|
+
: '';
|
|
214
|
+
const entryDescription = isWebWorker
|
|
215
|
+
? 'the Web Worker server entry'
|
|
216
|
+
: 'the template-free server manifest entry';
|
|
173
217
|
|
|
174
218
|
return `\
|
|
175
|
-
// Auto-generated by ${generatedBy} —
|
|
219
|
+
// Auto-generated by ${generatedBy} — ${entryDescription}.
|
|
176
220
|
// Do not edit; regenerated by the active app integration.
|
|
177
221
|
|
|
178
|
-
${assetFileImports}import {
|
|
179
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
180
|
-
import {
|
|
222
|
+
${assetFileImports}${platformImports}import {
|
|
181
223
|
renderToReadableStream,
|
|
182
224
|
executeServerFunction,
|
|
183
225
|
Suspense,
|
|
@@ -186,19 +228,13 @@ import {
|
|
|
186
228
|
} from ${JSON.stringify(serverRuntimeModuleId)};
|
|
187
229
|
import { prerender } from ${JSON.stringify(staticRuntimeModuleId)};
|
|
188
230
|
import { resolveOctaneConfig } from ${JSON.stringify(configModuleId)};
|
|
189
|
-
import _rawOctaneConfig from ${JSON.stringify(resolvedConfigImport)};
|
|
231
|
+
${handlerImport}import _rawOctaneConfig from ${JSON.stringify(resolvedConfigImport)};
|
|
190
232
|
|
|
191
233
|
${import_lines.join('\n')}
|
|
192
234
|
|
|
193
235
|
export const octaneConfig = resolveOctaneConfig(_rawOctaneConfig);
|
|
194
236
|
|
|
195
|
-
|
|
196
|
-
hash: (str) => createHash('sha256').update(str).digest('hex').slice(0, 8),
|
|
197
|
-
createAsyncContext: () => {
|
|
198
|
-
const als = new AsyncLocalStorage();
|
|
199
|
-
return { run: (store, fn) => als.run(store, fn), getStore: () => als.getStore() };
|
|
200
|
-
},
|
|
201
|
-
};
|
|
237
|
+
${runtime}
|
|
202
238
|
|
|
203
239
|
const components = {
|
|
204
240
|
${component_entries}
|
|
@@ -258,6 +294,7 @@ export const rendererDeps = {
|
|
|
258
294
|
ErrorBoundary,
|
|
259
295
|
createElement,
|
|
260
296
|
};
|
|
297
|
+
${workerFactory}
|
|
261
298
|
`;
|
|
262
299
|
}
|
|
263
300
|
|
package/types/codegen.d.ts
CHANGED
|
@@ -47,7 +47,8 @@ export interface ServerEntryOptions {
|
|
|
47
47
|
moduleImports?: Record<string, string>;
|
|
48
48
|
resolveImport?: (id: string) => string;
|
|
49
49
|
configImportPath?: string;
|
|
50
|
-
|
|
50
|
+
/** Server module shape emitted for the active adapter target. @default 'handler' */
|
|
51
|
+
mode?: 'handler' | 'manifest' | 'webworker';
|
|
51
52
|
serverRuntimeModuleId?: string;
|
|
52
53
|
staticRuntimeModuleId?: string;
|
|
53
54
|
productionModuleId?: string;
|
|
@@ -56,7 +57,7 @@ export interface ServerEntryOptions {
|
|
|
56
57
|
generatedBy?: string;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/** Generate a production
|
|
60
|
+
/** Generate a production server entry in the requested module shape. */
|
|
60
61
|
export function generateServerEntry(options: ServerEntryOptions): string;
|
|
61
62
|
/** Generate a template-free bundle exporting `manifest` and `rendererDeps`. */
|
|
62
63
|
export function generateServerManifestEntry(options: ServerEntryOptions): string;
|
package/types/index.d.ts
CHANGED
|
@@ -108,6 +108,8 @@ export interface Context {
|
|
|
108
108
|
* renderer inline scripts, hydration data, and the hydrate module script.
|
|
109
109
|
*/
|
|
110
110
|
state: Map<string, unknown>;
|
|
111
|
+
/** Request-scoped bindings supplied by the active platform integration. */
|
|
112
|
+
platform?: unknown;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
export type NextFunction = () => Promise<Response>;
|
|
@@ -117,7 +119,11 @@ export type RouteHandler = (context: Context) => Response | Promise<Response>;
|
|
|
117
119
|
export function compose(
|
|
118
120
|
middlewares: Middleware[],
|
|
119
121
|
): (context: Context, finalHandler: () => Promise<Response>) => Promise<Response>;
|
|
120
|
-
export function createContext(
|
|
122
|
+
export function createContext(
|
|
123
|
+
request: Request,
|
|
124
|
+
params: Record<string, string>,
|
|
125
|
+
platform?: unknown,
|
|
126
|
+
): Context;
|
|
121
127
|
export function runMiddlewareChain(
|
|
122
128
|
context: Context,
|
|
123
129
|
globalMiddlewares: Middleware[],
|
|
@@ -402,11 +408,14 @@ export interface AdaptContext {
|
|
|
402
408
|
* a deployment target (e.g. @octanejs/adapter-vercel emits `.vercel/output`).
|
|
403
409
|
* - `serve(handler, opts)` — replaces the generated server entry's built-in
|
|
404
410
|
* Node boot when running `node dist/server/entry.js` / `octane-preview`.
|
|
411
|
+
* - `serverTarget` — selects the integration's Node or Web Worker server build.
|
|
405
412
|
* - `runtime` — platform primitives (hashing, async context) replacing the
|
|
406
|
-
* entry's Node defaults;
|
|
413
|
+
* entry's Node defaults; required for `serverTarget: 'webworker'`.
|
|
407
414
|
*/
|
|
408
415
|
export interface OctaneAdapter {
|
|
409
416
|
name?: string;
|
|
417
|
+
/** Server bundle runtime selected by the active app integration. @default 'node' */
|
|
418
|
+
serverTarget?: 'node' | 'webworker';
|
|
410
419
|
adapt?: (ctx: AdaptContext) => void | Promise<void>;
|
|
411
420
|
serve?: AdapterServeFunction;
|
|
412
421
|
runtime?: RuntimePrimitives;
|
package/types/production.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export interface HandlerOptions {
|
|
|
83
83
|
export function createHandler(
|
|
84
84
|
manifest: ServerManifest,
|
|
85
85
|
options: HandlerOptions,
|
|
86
|
-
): (request: Request) => Promise<Response>;
|
|
86
|
+
): (request: Request, platform?: unknown) => Promise<Response>;
|
|
87
87
|
|
|
88
88
|
export function createPropsWrapper(Page: Component, pageProps: Record<string, unknown>): Component;
|
|
89
89
|
export function createLayoutWrapper(
|