@octanejs/app-core 0.0.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/LICENSE +21 -0
- package/README.md +34 -0
- package/package.json +88 -0
- package/src/codegen.js +297 -0
- package/src/config-loader.js +307 -0
- package/src/config.js +14 -0
- package/src/constants.js +5 -0
- package/src/html.js +10 -0
- package/src/index.js +18 -0
- package/src/middleware.js +3 -0
- package/src/resolve-config.js +177 -0
- package/src/routes.js +135 -0
- package/src/server/component-wrappers.js +99 -0
- package/src/server/html-stream.js +74 -0
- package/src/server/html-template.js +136 -0
- package/src/server/middleware.js +127 -0
- package/src/server/node-http.js +262 -0
- package/src/server/production.js +335 -0
- package/src/server/router.js +123 -0
- package/src/server/server-entry.js +436 -0
- package/src/server/server-route.js +47 -0
- package/types/codegen.d.ts +67 -0
- package/types/config-loader.d.ts +26 -0
- package/types/config.d.ts +26 -0
- package/types/html.d.ts +14 -0
- package/types/index.d.ts +319 -0
- package/types/middleware.d.ts +3 -0
- package/types/node.d.ts +31 -0
- package/types/production.d.ts +98 -0
- package/types/routes.d.ts +17 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Production server-entry generator.
|
|
4
|
+
*
|
|
5
|
+
* `generateServerEntry` emits the module an integration uses as its server
|
|
6
|
+
* bundle input. The generated module statically
|
|
7
|
+
* imports every RenderRoute entry/layout module (compiled in server mode by
|
|
8
|
+
* the active bundler integration) plus
|
|
9
|
+
* octane.config.ts itself, wires them into `createHandler`, and:
|
|
10
|
+
*
|
|
11
|
+
* - exports `handler` — the Web fetch handler `(Request) => Promise<Response>`
|
|
12
|
+
* - exports `nodeHandler` — a Node `(req, res)` wrapper for serverless
|
|
13
|
+
* platforms (e.g. a Vercel Node function does
|
|
14
|
+
* `export { nodeHandler as default } from '../dist/server/entry.js'`)
|
|
15
|
+
* - auto-boots when run directly (`node dist/server/entry.js`): the
|
|
16
|
+
* adapter's `serve()` when configured, else the built-in Node server
|
|
17
|
+
* (static dist/client assets + the handler).
|
|
18
|
+
*
|
|
19
|
+
* It is unused in dev, where the active integration loads modules directly.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @import { Route, RootBoundaryOptions } from '@octanejs/app-core' */
|
|
23
|
+
/** @import { ClientAssetEntry } from '../../types/production.d.ts' */
|
|
24
|
+
|
|
25
|
+
import { get_route_entry_export_name, get_route_entry_path } from '../routes.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} ServerEntryOptions
|
|
29
|
+
* @property {Route[]} routes - Route definitions from octane.config.ts
|
|
30
|
+
* @property {string} octaneConfigPath - Absolute path to octane.config.ts
|
|
31
|
+
* @property {RootBoundaryOptions} [rootBoundary] - Importable app-wide boundary entries
|
|
32
|
+
* @property {string[]} [rpcModulePaths] - Project-root module IDs containing `module server`
|
|
33
|
+
* @property {Record<string, ClientAssetEntry>} [clientAssetMap] - Route entry path → built client asset paths
|
|
34
|
+
* @property {string} [clientAssetMapFile] - JSON asset map, resolved beside the built server entry at runtime
|
|
35
|
+
* @property {Record<string, string>} [moduleImports] - Stable module ID → bundler import specifier
|
|
36
|
+
* @property {((id: string) => string)} [resolveImport] - Fallback module-specifier mapper
|
|
37
|
+
* @property {string} [configImportPath] - Bundler import specifier for octane.config.ts
|
|
38
|
+
* @property {'handler' | 'manifest'} [mode] - Emit a bootable handler or a template-free manifest module
|
|
39
|
+
* @property {string} [serverRuntimeModuleId] - Renderer server runtime module ID
|
|
40
|
+
* @property {string} [staticRuntimeModuleId] - Renderer static runtime module ID
|
|
41
|
+
* @property {string} [productionModuleId] - App-core production runtime module ID
|
|
42
|
+
* @property {string} [configModuleId] - App-core config runtime module ID
|
|
43
|
+
* @property {string} [nodeModuleId] - App-core Node bridge module ID
|
|
44
|
+
* @property {string} [generatedBy] - Integration name used in generated comments
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {ServerEntryOptions} options
|
|
49
|
+
* @returns {string} The generated JavaScript module source
|
|
50
|
+
*/
|
|
51
|
+
export function generateServerEntry(options) {
|
|
52
|
+
const {
|
|
53
|
+
routes,
|
|
54
|
+
octaneConfigPath,
|
|
55
|
+
rootBoundary = {},
|
|
56
|
+
rpcModulePaths = [],
|
|
57
|
+
clientAssetMap = {},
|
|
58
|
+
clientAssetMapFile,
|
|
59
|
+
moduleImports = {},
|
|
60
|
+
resolveImport,
|
|
61
|
+
configImportPath,
|
|
62
|
+
mode = 'handler',
|
|
63
|
+
serverRuntimeModuleId = 'octane/server',
|
|
64
|
+
staticRuntimeModuleId = 'octane/static',
|
|
65
|
+
productionModuleId = '@octanejs/app-core/production',
|
|
66
|
+
configModuleId = '@octanejs/app-core/config',
|
|
67
|
+
nodeModuleId = '@octanejs/app-core/node',
|
|
68
|
+
generatedBy = '@octanejs/app-core',
|
|
69
|
+
} = options;
|
|
70
|
+
const import_specifier = (/** @type {string} */ id) =>
|
|
71
|
+
moduleImports[id] ?? resolveImport?.(id) ?? id;
|
|
72
|
+
const resolvedConfigImport =
|
|
73
|
+
configImportPath ?? moduleImports[octaneConfigPath] ?? octaneConfigPath;
|
|
74
|
+
|
|
75
|
+
// Unique page-entry and layout module paths (multiple routes may share both).
|
|
76
|
+
/** @type {Map<string, string>} module path → import variable name */
|
|
77
|
+
const page_imports = new Map();
|
|
78
|
+
/** @type {Map<string, string>} */
|
|
79
|
+
const layout_imports = new Map();
|
|
80
|
+
/** @type {Map<string, string>} */
|
|
81
|
+
const rpc_imports = new Map();
|
|
82
|
+
/** @type {Map<string, string>} */
|
|
83
|
+
const boundary_imports = new Map();
|
|
84
|
+
|
|
85
|
+
for (const route of routes) {
|
|
86
|
+
if (route.type !== 'render') continue;
|
|
87
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
88
|
+
if (entryPath && !page_imports.has(entryPath)) {
|
|
89
|
+
page_imports.set(entryPath, `_page_${page_imports.size}`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof route.layout === 'string' && !layout_imports.has(route.layout)) {
|
|
92
|
+
layout_imports.set(route.layout, `_layout_${layout_imports.size}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const entry of [rootBoundary.pending, rootBoundary.catch]) {
|
|
97
|
+
const modulePath = get_route_entry_path(entry);
|
|
98
|
+
if (modulePath && !boundary_imports.has(modulePath)) {
|
|
99
|
+
boundary_imports.set(modulePath, `_boundary_${boundary_imports.size}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const modulePath of rpcModulePaths) {
|
|
104
|
+
if (!page_imports.has(modulePath)) {
|
|
105
|
+
rpc_imports.set(modulePath, `_rpc_${rpc_imports.size}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const import_lines = [];
|
|
110
|
+
for (const [modulePath, varName] of page_imports) {
|
|
111
|
+
import_lines.push(
|
|
112
|
+
`import * as ${varName} from ${JSON.stringify(import_specifier(modulePath))};`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
for (const [modulePath, varName] of layout_imports) {
|
|
116
|
+
import_lines.push(
|
|
117
|
+
`import * as ${varName} from ${JSON.stringify(import_specifier(modulePath))};`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
for (const [modulePath, varName] of boundary_imports) {
|
|
121
|
+
if (!page_imports.has(modulePath) && !layout_imports.has(modulePath)) {
|
|
122
|
+
import_lines.push(
|
|
123
|
+
`import * as ${varName} from ${JSON.stringify(import_specifier(modulePath))};`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [modulePath, varName] of rpc_imports) {
|
|
128
|
+
if (!boundary_imports.has(modulePath) && !layout_imports.has(modulePath)) {
|
|
129
|
+
import_lines.push(
|
|
130
|
+
`import * as ${varName} from ${JSON.stringify(import_specifier(modulePath))};`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The manifest maps MODULE PATHS to module namespaces; createHandler picks
|
|
136
|
+
// the export per-route with the same `get_component_export` dev uses.
|
|
137
|
+
const component_entries = [...page_imports]
|
|
138
|
+
.map(([modulePath, varName]) => `\t${JSON.stringify(modulePath)}: ${varName},`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
const layout_entries = [...layout_imports]
|
|
141
|
+
.map(([modulePath, varName]) => `\t${JSON.stringify(modulePath)}: ${varName},`)
|
|
142
|
+
.join('\n');
|
|
143
|
+
|
|
144
|
+
const import_var_for = (/** @type {string} */ modulePath) =>
|
|
145
|
+
page_imports.get(modulePath) ??
|
|
146
|
+
layout_imports.get(modulePath) ??
|
|
147
|
+
boundary_imports.get(modulePath) ??
|
|
148
|
+
rpc_imports.get(modulePath);
|
|
149
|
+
const boundary_value = (/** @type {unknown} */ entry) => {
|
|
150
|
+
const modulePath = get_route_entry_path(/** @type {any} */ (entry));
|
|
151
|
+
if (!modulePath) return 'null';
|
|
152
|
+
const varName = import_var_for(modulePath);
|
|
153
|
+
const exportName = get_route_entry_export_name(/** @type {any} */ (entry));
|
|
154
|
+
return `getComponentExport(${varName}, ${JSON.stringify(exportName ?? null)})`;
|
|
155
|
+
};
|
|
156
|
+
const rpc_entries = rpcModulePaths
|
|
157
|
+
.map((modulePath) => {
|
|
158
|
+
const varName = import_var_for(modulePath);
|
|
159
|
+
return `\t${JSON.stringify(modulePath)}: ${varName}._$_server_$_,`;
|
|
160
|
+
})
|
|
161
|
+
.join('\n');
|
|
162
|
+
|
|
163
|
+
if (mode === 'manifest') {
|
|
164
|
+
const assetFileImports = clientAssetMapFile
|
|
165
|
+
? `import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n`
|
|
166
|
+
: '';
|
|
167
|
+
const assetDirectory = clientAssetMapFile
|
|
168
|
+
? `const __dirname = dirname(fileURLToPath(import.meta.url));\n`
|
|
169
|
+
: '';
|
|
170
|
+
const clientAssets = clientAssetMapFile
|
|
171
|
+
? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
|
|
172
|
+
: JSON.stringify(clientAssetMap, null, '\t');
|
|
173
|
+
|
|
174
|
+
return `\
|
|
175
|
+
// Auto-generated by ${generatedBy} — the template-free server manifest entry.
|
|
176
|
+
// Do not edit; regenerated by the active app integration.
|
|
177
|
+
|
|
178
|
+
${assetFileImports}import { createHash } from 'node:crypto';
|
|
179
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
180
|
+
import {
|
|
181
|
+
renderToReadableStream,
|
|
182
|
+
executeServerFunction,
|
|
183
|
+
Suspense,
|
|
184
|
+
ErrorBoundary,
|
|
185
|
+
createElement,
|
|
186
|
+
} from ${JSON.stringify(serverRuntimeModuleId)};
|
|
187
|
+
import { prerender } from ${JSON.stringify(staticRuntimeModuleId)};
|
|
188
|
+
import { resolveOctaneConfig } from ${JSON.stringify(configModuleId)};
|
|
189
|
+
import _rawOctaneConfig from ${JSON.stringify(resolvedConfigImport)};
|
|
190
|
+
|
|
191
|
+
${import_lines.join('\n')}
|
|
192
|
+
|
|
193
|
+
export const octaneConfig = resolveOctaneConfig(_rawOctaneConfig);
|
|
194
|
+
|
|
195
|
+
const runtime = octaneConfig.adapter?.runtime ?? {
|
|
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
|
+
};
|
|
202
|
+
|
|
203
|
+
const components = {
|
|
204
|
+
${component_entries}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const layouts = {
|
|
208
|
+
${layout_entries}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
function getComponentExport(module, exportName) {
|
|
212
|
+
if (exportName) return typeof module[exportName] === 'function' ? module[exportName] : null;
|
|
213
|
+
if (typeof module.default === 'function') return module.default;
|
|
214
|
+
return Object.entries(module).find(([key, value]) =>
|
|
215
|
+
typeof value === 'function' && /^[A-Z]/.test(key))?.[1] ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const rootBoundary = {
|
|
219
|
+
pending: ${boundary_value(rootBoundary.pending)},
|
|
220
|
+
catch: ${boundary_value(rootBoundary.catch)},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const rootBoundaryEntries = ${JSON.stringify(
|
|
224
|
+
{
|
|
225
|
+
pending: serialize_entry(rootBoundary.pending),
|
|
226
|
+
catch: serialize_entry(rootBoundary.catch),
|
|
227
|
+
},
|
|
228
|
+
null,
|
|
229
|
+
'\t',
|
|
230
|
+
)};
|
|
231
|
+
|
|
232
|
+
const rpcModules = {
|
|
233
|
+
${rpc_entries}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
${assetDirectory}const clientAssets = ${clientAssets};
|
|
237
|
+
|
|
238
|
+
export const manifest = {
|
|
239
|
+
routes: octaneConfig.router.routes,
|
|
240
|
+
components,
|
|
241
|
+
layouts,
|
|
242
|
+
middlewares: octaneConfig.middlewares,
|
|
243
|
+
trustProxy: octaneConfig.server.trustProxy,
|
|
244
|
+
render: octaneConfig.server.render,
|
|
245
|
+
rootBoundary,
|
|
246
|
+
rootBoundaryEntries,
|
|
247
|
+
preHydrate: octaneConfig.router.preHydrate ?? null,
|
|
248
|
+
rpcModules,
|
|
249
|
+
runtime,
|
|
250
|
+
clientAssets,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const rendererDeps = {
|
|
254
|
+
renderToReadableStream,
|
|
255
|
+
prerender,
|
|
256
|
+
executeServerFunction,
|
|
257
|
+
Suspense,
|
|
258
|
+
ErrorBoundary,
|
|
259
|
+
createElement,
|
|
260
|
+
};
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return `\
|
|
265
|
+
// Auto-generated by ${generatedBy} — the production server entry.
|
|
266
|
+
// Do not edit; regenerated on every build.
|
|
267
|
+
|
|
268
|
+
import { readFileSync } from 'node:fs';
|
|
269
|
+
import { createHash } from 'node:crypto';
|
|
270
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
271
|
+
import { fileURLToPath } from 'node:url';
|
|
272
|
+
import { dirname, join, resolve } from 'node:path';
|
|
273
|
+
|
|
274
|
+
import {
|
|
275
|
+
renderToReadableStream,
|
|
276
|
+
executeServerFunction,
|
|
277
|
+
Suspense,
|
|
278
|
+
ErrorBoundary,
|
|
279
|
+
createElement,
|
|
280
|
+
} from ${JSON.stringify(serverRuntimeModuleId)};
|
|
281
|
+
import { prerender } from ${JSON.stringify(staticRuntimeModuleId)};
|
|
282
|
+
import { createHandler, resolveOctaneConfig } from ${JSON.stringify(productionModuleId)};
|
|
283
|
+
import { createNodeServer, nodeRequestToWebRequest, sendWebResponse } from ${JSON.stringify(nodeModuleId)};
|
|
284
|
+
|
|
285
|
+
// The app config is bundled by the active server integration.
|
|
286
|
+
import _rawOctaneConfig from ${JSON.stringify(resolvedConfigImport)};
|
|
287
|
+
|
|
288
|
+
${import_lines.join('\n')}
|
|
289
|
+
|
|
290
|
+
const octaneConfig = resolveOctaneConfig(_rawOctaneConfig);
|
|
291
|
+
|
|
292
|
+
// Platform primitives: the adapter's when configured, else Node defaults.
|
|
293
|
+
// (hash mirrors the compiler's module-server hashing: sha-256 hex, 8 chars.)
|
|
294
|
+
const runtime = octaneConfig.adapter?.runtime ?? {
|
|
295
|
+
hash: (str) => createHash('sha256').update(str).digest('hex').slice(0, 8),
|
|
296
|
+
createAsyncContext: () => {
|
|
297
|
+
const als = new AsyncLocalStorage();
|
|
298
|
+
return { run: (store, fn) => als.run(store, fn), getStore: () => als.getStore() };
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
303
|
+
// The HTML template is the BUILT client index.html (hashed hydrate script and
|
|
304
|
+
// asset links already in place), moved next to this entry by the build.
|
|
305
|
+
const htmlTemplate = readFileSync(join(__dirname, './index.html'), 'utf-8');
|
|
306
|
+
|
|
307
|
+
const components = {
|
|
308
|
+
${component_entries}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const layouts = {
|
|
312
|
+
${layout_entries}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
function getComponentExport(module, exportName) {
|
|
316
|
+
if (exportName) return typeof module[exportName] === 'function' ? module[exportName] : null;
|
|
317
|
+
if (typeof module.default === 'function') return module.default;
|
|
318
|
+
return Object.entries(module).find(([key, value]) =>
|
|
319
|
+
typeof value === 'function' && /^[A-Z]/.test(key))?.[1] ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const rootBoundary = {
|
|
323
|
+
pending: ${boundary_value(rootBoundary.pending)},
|
|
324
|
+
catch: ${boundary_value(rootBoundary.catch)},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const rootBoundaryEntries = ${JSON.stringify(
|
|
328
|
+
{
|
|
329
|
+
pending: serialize_entry(rootBoundary.pending),
|
|
330
|
+
catch: serialize_entry(rootBoundary.catch),
|
|
331
|
+
},
|
|
332
|
+
null,
|
|
333
|
+
'\t',
|
|
334
|
+
)};
|
|
335
|
+
|
|
336
|
+
const rpcModules = {
|
|
337
|
+
${rpc_entries}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const clientAssets = ${
|
|
341
|
+
clientAssetMapFile
|
|
342
|
+
? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
|
|
343
|
+
: JSON.stringify(clientAssetMap, null, '\t')
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export const handler = createHandler(
|
|
347
|
+
{
|
|
348
|
+
routes: octaneConfig.router.routes,
|
|
349
|
+
components,
|
|
350
|
+
layouts,
|
|
351
|
+
middlewares: octaneConfig.middlewares,
|
|
352
|
+
trustProxy: octaneConfig.server.trustProxy,
|
|
353
|
+
render: octaneConfig.server.render,
|
|
354
|
+
rootBoundary,
|
|
355
|
+
rootBoundaryEntries,
|
|
356
|
+
preHydrate: octaneConfig.router.preHydrate ?? null,
|
|
357
|
+
rpcModules,
|
|
358
|
+
runtime,
|
|
359
|
+
clientAssets,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
renderToReadableStream,
|
|
363
|
+
prerender,
|
|
364
|
+
htmlTemplate,
|
|
365
|
+
executeServerFunction,
|
|
366
|
+
Suspense,
|
|
367
|
+
ErrorBoundary,
|
|
368
|
+
createElement,
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Node-style (req, res) wrapper — for serverless platforms whose functions
|
|
374
|
+
* speak Node HTTP (e.g. Vercel's Node runtime).
|
|
375
|
+
*/
|
|
376
|
+
export async function nodeHandler(req, res) {
|
|
377
|
+
try {
|
|
378
|
+
const response = await handler(nodeRequestToWebRequest(req));
|
|
379
|
+
await sendWebResponse(res, response);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error('[octane] Request error:', error);
|
|
382
|
+
if (!res.headersSent) {
|
|
383
|
+
res.statusCode = 500;
|
|
384
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
385
|
+
}
|
|
386
|
+
res.end('Internal Server Error');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Auto-boot when run directly (node dist/server/entry.js); stay quiet when
|
|
391
|
+
// imported by a serverless wrapper.
|
|
392
|
+
const isMainModule =
|
|
393
|
+
typeof process !== 'undefined' &&
|
|
394
|
+
process.argv[1] &&
|
|
395
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
396
|
+
|
|
397
|
+
if (isMainModule) {
|
|
398
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
399
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
400
|
+
console.error('[octane] Invalid PORT value:', process.env.PORT);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const staticDir = join(__dirname, '../client');
|
|
404
|
+
const server = octaneConfig.adapter?.serve
|
|
405
|
+
? octaneConfig.adapter.serve(handler, { static: { dir: staticDir } })
|
|
406
|
+
: createNodeServer(handler, { staticDir });
|
|
407
|
+
server.listen(port);
|
|
408
|
+
console.log('[octane] Production server listening on port ' + port);
|
|
409
|
+
}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Generate the template-free module consumed by integration dev middleware.
|
|
415
|
+
* The loaded bundle exports `manifest` and `rendererDeps`; the integration
|
|
416
|
+
* supplies its current transformed HTML to `createHandler` per request/build.
|
|
417
|
+
*
|
|
418
|
+
* @param {ServerEntryOptions} options
|
|
419
|
+
* @returns {string}
|
|
420
|
+
*/
|
|
421
|
+
export function generateServerManifestEntry(options) {
|
|
422
|
+
return generateServerEntry({ ...options, mode: 'manifest' });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {unknown} entry
|
|
427
|
+
* @returns {{ path: string, exportName: string | null } | null}
|
|
428
|
+
*/
|
|
429
|
+
function serialize_entry(entry) {
|
|
430
|
+
const path = get_route_entry_path(/** @type {any} */ (entry));
|
|
431
|
+
if (!path) return null;
|
|
432
|
+
return {
|
|
433
|
+
path,
|
|
434
|
+
exportName: get_route_entry_export_name(/** @type {any} */ (entry)) ?? null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {import('@octanejs/app-core').Context} Context
|
|
4
|
+
* @typedef {import('@octanejs/app-core').ServerRoute} ServerRoute
|
|
5
|
+
* @typedef {import('@octanejs/app-core').Middleware} Middleware
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runMiddlewareChain } from './middleware.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle a ServerRoute (API endpoint)
|
|
12
|
+
*
|
|
13
|
+
* @param {ServerRoute} route
|
|
14
|
+
* @param {Context} context
|
|
15
|
+
* @param {Middleware[]} globalMiddlewares
|
|
16
|
+
* @returns {Promise<Response>}
|
|
17
|
+
*/
|
|
18
|
+
export async function handleServerRoute(route, context, globalMiddlewares) {
|
|
19
|
+
try {
|
|
20
|
+
// The handler wrapped as a function returning Promise<Response>
|
|
21
|
+
const handler = async () => {
|
|
22
|
+
return route.handler(context);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Run the middleware chain: global → before → handler → after
|
|
26
|
+
const response = await runMiddlewareChain(
|
|
27
|
+
context,
|
|
28
|
+
globalMiddlewares,
|
|
29
|
+
route.before,
|
|
30
|
+
handler,
|
|
31
|
+
route.after,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return response;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[octane] API route error:', error);
|
|
37
|
+
|
|
38
|
+
// Return error response
|
|
39
|
+
const message = error instanceof Error ? error.message : 'Internal Server Error';
|
|
40
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
41
|
+
status: 500,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RootBoundaryOptions, Route } from '@octanejs/app-core';
|
|
2
|
+
import type { ClientAssetEntry } from '@octanejs/app-core/production';
|
|
3
|
+
|
|
4
|
+
export const RESOLVED_ADAPTER_BROWSER_STUB_ID: '\0octane:adapter-browser-stub';
|
|
5
|
+
export const SERVER_ONLY_ADAPTER_IDS: Set<string>;
|
|
6
|
+
export function create_adapter_browser_stub_source(): string;
|
|
7
|
+
|
|
8
|
+
export interface GeneratedProjectOptions {
|
|
9
|
+
root: string;
|
|
10
|
+
cacheDir?: string;
|
|
11
|
+
generatedDir?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function get_project_generated_dir(options: GeneratedProjectOptions): string;
|
|
15
|
+
export function write_project_generated_file(
|
|
16
|
+
options: GeneratedProjectOptions,
|
|
17
|
+
name: string,
|
|
18
|
+
source: string,
|
|
19
|
+
): string;
|
|
20
|
+
|
|
21
|
+
export interface StaticClientEntry {
|
|
22
|
+
/** Stable ID serialized in hydration data. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Bundler-resolvable import specifier emitted in generated source. */
|
|
25
|
+
specifier: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClientEntryOptions {
|
|
29
|
+
configPath?: string;
|
|
30
|
+
staticEntries?: Array<string | StaticClientEntry>;
|
|
31
|
+
resolveImport?: (id: string) => string;
|
|
32
|
+
runtimeModuleId?: string;
|
|
33
|
+
generatedBy?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function create_client_entry_source(options?: ClientEntryOptions): string;
|
|
37
|
+
|
|
38
|
+
export interface ServerEntryOptions {
|
|
39
|
+
routes: Route[];
|
|
40
|
+
octaneConfigPath: string;
|
|
41
|
+
rootBoundary?: RootBoundaryOptions;
|
|
42
|
+
rpcModulePaths?: string[];
|
|
43
|
+
clientAssetMap?: Record<string, ClientAssetEntry>;
|
|
44
|
+
/** JSON file resolved beside the built server entry at module evaluation. */
|
|
45
|
+
clientAssetMapFile?: string;
|
|
46
|
+
/** Stable application module ID to emitted bundler import specifier. */
|
|
47
|
+
moduleImports?: Record<string, string>;
|
|
48
|
+
resolveImport?: (id: string) => string;
|
|
49
|
+
configImportPath?: string;
|
|
50
|
+
mode?: 'handler' | 'manifest';
|
|
51
|
+
serverRuntimeModuleId?: string;
|
|
52
|
+
staticRuntimeModuleId?: string;
|
|
53
|
+
productionModuleId?: string;
|
|
54
|
+
configModuleId?: string;
|
|
55
|
+
nodeModuleId?: string;
|
|
56
|
+
generatedBy?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Generate a production fetch-handler + optional Node auto-boot entry. */
|
|
60
|
+
export function generateServerEntry(options: ServerEntryOptions): string;
|
|
61
|
+
/** Generate a template-free bundle exporting `manifest` and `rendererDeps`. */
|
|
62
|
+
export function generateServerManifestEntry(options: ServerEntryOptions): string;
|
|
63
|
+
|
|
64
|
+
/** Convert an absolute file under `root` to a stable project-root module ID. */
|
|
65
|
+
export function normalize_module_reference(filename: string, root: string): string;
|
|
66
|
+
/** Compatibility alias retained for the Vite integration. */
|
|
67
|
+
export function to_vite_root_import(filename: string, root: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LoadedOctaneConfig,
|
|
3
|
+
LoadConfigOptions,
|
|
4
|
+
OctaneConfigOptions,
|
|
5
|
+
ResolvedOctaneConfig,
|
|
6
|
+
} from '@octanejs/app-core';
|
|
7
|
+
|
|
8
|
+
export { resolveOctaneConfig } from '@octanejs/app-core';
|
|
9
|
+
export function getOctaneConfigPath(projectRoot: string, configFile?: string): string;
|
|
10
|
+
export function octaneConfigExists(projectRoot: string, configFile?: string): boolean;
|
|
11
|
+
export function loadOctaneConfig(
|
|
12
|
+
projectRoot: string,
|
|
13
|
+
options?: LoadConfigOptions,
|
|
14
|
+
): Promise<ResolvedOctaneConfig>;
|
|
15
|
+
export function loadOctaneConfigWithMetadata(
|
|
16
|
+
projectRoot: string,
|
|
17
|
+
options?: LoadConfigOptions,
|
|
18
|
+
): Promise<LoadedOctaneConfig>;
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
ConfigModuleRunner,
|
|
22
|
+
LoadedOctaneConfig,
|
|
23
|
+
LoadConfigOptions,
|
|
24
|
+
OctaneConfigOptions,
|
|
25
|
+
ResolvedOctaneConfig,
|
|
26
|
+
} from '@octanejs/app-core';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_OUTDIR,
|
|
3
|
+
ENTRY_FILENAME,
|
|
4
|
+
OCTANE_NONCE_STATE_KEY,
|
|
5
|
+
RenderRoute,
|
|
6
|
+
ServerRoute,
|
|
7
|
+
defineConfig,
|
|
8
|
+
resolveOctaneConfig,
|
|
9
|
+
} from '@octanejs/app-core';
|
|
10
|
+
export type {
|
|
11
|
+
AdaptContext,
|
|
12
|
+
AdapterServeFunction,
|
|
13
|
+
BuildTarget,
|
|
14
|
+
Context,
|
|
15
|
+
Middleware,
|
|
16
|
+
OctaneAdapter,
|
|
17
|
+
OctaneConfigOptions,
|
|
18
|
+
PreHydrateHook,
|
|
19
|
+
RenderRouteEntry,
|
|
20
|
+
RenderRouteOptions,
|
|
21
|
+
RenderRouteProps,
|
|
22
|
+
ResolvedOctaneConfig,
|
|
23
|
+
RootBoundaryOptions,
|
|
24
|
+
Route,
|
|
25
|
+
ServerRouteOptions,
|
|
26
|
+
} from '@octanejs/app-core';
|
package/types/html.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from '@octanejs/app-core';
|
|
2
|
+
|
|
3
|
+
export const HYDRATION_NONCE_PLACEHOLDER: '__OCTANE_REQUEST_NONCE__';
|
|
4
|
+
export function composeHtmlStream(
|
|
5
|
+
prefix: string,
|
|
6
|
+
renderStream: ReadableStream<Uint8Array>,
|
|
7
|
+
suffix: string,
|
|
8
|
+
): ReadableStream<Uint8Array>;
|
|
9
|
+
export function validateSsrTemplate(html: string): void;
|
|
10
|
+
export function injectHydrationEntry(html: string, source: string, nonce?: string | null): string;
|
|
11
|
+
export function splitSsrTemplate(html: string): [prefix: string, suffix: string];
|
|
12
|
+
export function applyHydrationNonce(html: string, nonce?: string | null): string;
|
|
13
|
+
export function nonceAttribute(nonce?: string | null): string;
|
|
14
|
+
export function getContextNonce(context: Context): string | null;
|