vue-ssr-lite 0.0.3

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.
@@ -0,0 +1,600 @@
1
+ // SsrServer — the optional standalone Node server (default host of the engine).
2
+ //
3
+ // Ready-to-use production and development server: static assets, health and
4
+ // readiness endpoints, host classification across SSR and SPA entries,
5
+ // startup preflight, error shells, and graceful shutdown. The SSR rendering
6
+ // itself goes through the separable engine (../engine), so deployments with
7
+ // their own servers can bypass this file entirely.
8
+ //
9
+ // Development and production run one request pipeline; only module loading
10
+ // and HTML transformation differ (Vite middleware vs built assets).
11
+
12
+ import { createServer } from 'node:http'
13
+ import { readFile, stat } from 'node:fs/promises'
14
+ import { extname, resolve } from 'node:path'
15
+ import { pathToFileURL } from 'node:url'
16
+ import {
17
+ createSsrRequestHandler,
18
+ SSR_HTML_SECURITY_HEADERS,
19
+ } from '../engine/SsrEngine.js'
20
+ import {
21
+ SSR_MARKERS,
22
+ renderErrorMarkup,
23
+ escapeHtml,
24
+ } from '../engine/SsrEngineHtml.js'
25
+ import {
26
+ matchesSsrHostPatterns,
27
+ resolveSsrForwardedHost,
28
+ resolveSsrForwardedProtocol,
29
+ stripSsrHostPort,
30
+ } from '../engine/SsrEngineHostPolicy.js'
31
+
32
+ const LOCAL_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', '::1']
33
+
34
+ const MIME_TYPES = {
35
+ '.css': 'text/css; charset=utf-8',
36
+ '.gif': 'image/gif',
37
+ '.html': 'text/html; charset=utf-8',
38
+ '.ico': 'image/x-icon',
39
+ '.jpeg': 'image/jpeg',
40
+ '.jpg': 'image/jpeg',
41
+ '.js': 'text/javascript; charset=utf-8',
42
+ '.json': 'application/json; charset=utf-8',
43
+ '.png': 'image/png',
44
+ '.svg': 'image/svg+xml',
45
+ '.txt': 'text/plain; charset=utf-8',
46
+ '.webp': 'image/webp',
47
+ '.woff': 'font/woff',
48
+ '.woff2': 'font/woff2',
49
+ '.xml': 'application/xml; charset=utf-8',
50
+ }
51
+
52
+ const parseBoolean = (value) =>
53
+ ['1', 'true', 'yes', 'on'].includes(
54
+ String(value || '')
55
+ .trim()
56
+ .toLowerCase(),
57
+ )
58
+
59
+ const parsePort = (value, fallback) => {
60
+ const parsed = Number(value)
61
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) return fallback
62
+ return parsed
63
+ }
64
+
65
+ const resolveRootCause = (error) => {
66
+ let cause = error
67
+ while (cause instanceof Error && cause.cause) cause = cause.cause
68
+ return cause
69
+ }
70
+
71
+ export const SSR_RUNTIME_MANIFEST = '.vue-ssr-lite.json'
72
+
73
+ /** Load serialized server options emitted by the Vite integration at build time. */
74
+ export const loadSsrRuntimeManifest = async (clientDir) => {
75
+ try {
76
+ const raw = await readFile(resolve(clientDir, SSR_RUNTIME_MANIFEST), 'utf8')
77
+ return JSON.parse(raw)
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ export const createSsrServer = async (userOptions = {}) => {
84
+ const root = resolve(userOptions.root || process.cwd())
85
+ const production =
86
+ userOptions.mode != null
87
+ ? userOptions.mode === 'production'
88
+ : process.env.NODE_ENV === 'production'
89
+ const clientDir = resolve(root, userOptions.clientDir || 'dist/client')
90
+ const logger = userOptions.logger || console
91
+
92
+ // ---- Development: Vite in middleware mode -----------------------------
93
+ let vite = null
94
+ if (!production) {
95
+ const { createServer: createViteServer } = await import('vite')
96
+ vite = await createViteServer({
97
+ root,
98
+ server: { middlewareMode: true },
99
+ appType: 'custom',
100
+ ...(userOptions.vite || {}),
101
+ })
102
+ }
103
+
104
+ // ---- Options: programmatic > plugin/manifest > defaults ----------------
105
+ let integrationOptions = null
106
+ if (!production && vite) {
107
+ const plugin = vite.config.plugins.find((p) => p.name === 'vue-ssr-lite')
108
+ integrationOptions = plugin?.api?.getServerOptions?.() ?? null
109
+ } else if (production) {
110
+ integrationOptions = await loadSsrRuntimeManifest(clientDir)
111
+ }
112
+
113
+ const options = { ...(integrationOptions || {}), ...userOptions }
114
+
115
+ const port = parsePort(process.env.PORT ?? options.port, 4173)
116
+ const host = process.env.HOST || options.host || '0.0.0.0'
117
+ const trustProxy =
118
+ options.trustProxy ?? parseBoolean(process.env.SSR_TRUST_PROXY)
119
+ const globals = options.globals || {}
120
+ const headAttribute = options.headAttribute || 'data-ssr-head'
121
+ const cookieAllowlist = options.cookieAllowlist || []
122
+ const cookieDenylist = options.cookieDenylist || []
123
+ const serveRole = options.serveRole || 'unified'
124
+ const consumerRoutes = options.routes || []
125
+ const readinessProbes = options.readiness || []
126
+ const serviceName = options.serviceName || 'vue-ssr-lite'
127
+
128
+ const apps = (options.apps || []).map((app) => ({
129
+ type: 'ssr',
130
+ hosts: [],
131
+ ...app,
132
+ }))
133
+ if (apps.length === 0) {
134
+ apps.push({ name: 'ssr', type: 'ssr', template: 'index.html', hosts: [] })
135
+ }
136
+ const defaultApp =
137
+ apps.find((app) => app.default) || apps.find((app) => app.type === 'ssr') || apps[0]
138
+ const ssrApp = apps.find((app) => app.type === 'ssr')
139
+
140
+ const serverEntry = options.serverEntry || 'virtual:vue-ssr-lite/server'
141
+ const bundlePath = resolve(root, options.bundlePath || 'dist/server/entry.mjs')
142
+ const renderExport = options.renderExport || 'render'
143
+ const readyExport = options.readyExport || 'assertReady'
144
+
145
+ let productionModule = null
146
+ const loadRenderModule = async () => {
147
+ if (production) {
148
+ productionModule ??= await import(pathToFileURL(bundlePath).href)
149
+ return productionModule
150
+ }
151
+ return vite.ssrLoadModule(serverEntry)
152
+ }
153
+
154
+ const loadHtmlTemplate = async (templateName, requestUrl) => {
155
+ let template = await readFile(
156
+ resolve(production ? clientDir : root, templateName),
157
+ 'utf8',
158
+ )
159
+ if (!production) {
160
+ template = await vite.transformIndexHtml(requestUrl || '/', template)
161
+ }
162
+ return template
163
+ }
164
+
165
+ // ---- The separable engine ---------------------------------------------
166
+ const engine = ssrApp
167
+ ? createSsrRequestHandler({
168
+ loadTemplate: (requestUrl) => loadHtmlTemplate(ssrApp.template, requestUrl),
169
+ loadRenderModule,
170
+ renderExport,
171
+ config: options.config,
172
+ globals,
173
+ headAttribute,
174
+ defaultHead: options.defaultHead || '',
175
+ cookieAllowlist,
176
+ cookieDenylist,
177
+ renderTimeoutMs: options.renderTimeoutMs,
178
+ nonce: options.security?.nonce,
179
+ })
180
+ : null
181
+
182
+ // ---- Classification ----------------------------------------------------
183
+ const classify = (hostname, pathname, headers) => {
184
+ if (typeof options.classifyRequest === 'function') {
185
+ const result = options.classifyRequest({ hostname, pathname, headers })
186
+ if (result) return result
187
+ }
188
+ const bare = stripSsrHostPort(hostname)
189
+ if (!bare) return 'invalid'
190
+ for (const app of apps) {
191
+ if (app.hosts?.length && matchesSsrHostPatterns(bare, app.hosts)) {
192
+ return app.name
193
+ }
194
+ }
195
+ if (LOCAL_HOSTS.includes(bare)) {
196
+ const localApp = apps.find((app) => app.localhost) || defaultApp
197
+ return localApp.name
198
+ }
199
+ return defaultApp.name
200
+ }
201
+
202
+ const isAppAllowed = (appName) => serveRole === 'unified' || serveRole === appName
203
+
204
+ // ---- Helpers -----------------------------------------------------------
205
+ const endResponse = (request, response, body = '') => {
206
+ response.end(request.method === 'HEAD' ? '' : body)
207
+ }
208
+
209
+ const sendJson = (request, response, statusCode, payload) => {
210
+ response.writeHead(statusCode, {
211
+ 'content-type': 'application/json; charset=utf-8',
212
+ 'cache-control': 'no-store',
213
+ })
214
+ endResponse(request, response, JSON.stringify(payload))
215
+ }
216
+
217
+ const sendText = (
218
+ request,
219
+ response,
220
+ statusCode,
221
+ body,
222
+ contentType = 'text/plain; charset=utf-8',
223
+ headers = {},
224
+ ) => {
225
+ response.writeHead(statusCode, {
226
+ 'content-type': contentType,
227
+ 'cache-control': 'no-store',
228
+ ...headers,
229
+ })
230
+ endResponse(request, response, body)
231
+ }
232
+
233
+ const isHtmlNavigation = (request, pathname) => {
234
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false
235
+ if (request.headers['sec-fetch-mode'] === 'navigate') return true
236
+ const accept = String(request.headers.accept || '')
237
+ if (accept.includes('text/html')) return true
238
+ return (!accept || accept === '*/*') && !extname(pathname)
239
+ }
240
+
241
+ const serveProductionAsset = async (pathname, request, response) => {
242
+ try {
243
+ const relativePath = decodeURIComponent(pathname).replace(/^\/+/, '')
244
+ if (
245
+ relativePath === SSR_RUNTIME_MANIFEST ||
246
+ apps.some((app) => relativePath === app.template)
247
+ ) {
248
+ return false
249
+ }
250
+ const filePath = resolve(clientDir, relativePath)
251
+ if (!filePath.startsWith(`${clientDir}/`)) return false
252
+ const info = await stat(filePath)
253
+ if (!info.isFile()) return false
254
+ const body = await readFile(filePath)
255
+ response.writeHead(200, {
256
+ 'content-type': MIME_TYPES[extname(filePath)] || 'application/octet-stream',
257
+ 'cache-control': pathname.startsWith('/assets/')
258
+ ? 'public, max-age=31536000, immutable'
259
+ : 'public, max-age=3600',
260
+ })
261
+ endResponse(request, response, body)
262
+ return true
263
+ } catch {
264
+ return false
265
+ }
266
+ }
267
+
268
+ const runViteMiddleware = (request, response) =>
269
+ new Promise((resolveMiddleware, reject) => {
270
+ vite.middlewares(request, response, (error) => {
271
+ if (error) reject(error)
272
+ else resolveMiddleware()
273
+ })
274
+ })
275
+
276
+ const sendNavigationError = async (request, response, statusCode, appName, error) => {
277
+ const title = 'Application unavailable'
278
+ const message =
279
+ !production && error instanceof Error
280
+ ? error.message
281
+ : 'The application could not render this page. Please try again.'
282
+ const custom = options.errorPage?.({
283
+ statusCode,
284
+ error,
285
+ appName,
286
+ mode: production ? 'production' : 'development',
287
+ })
288
+ if (custom) {
289
+ return sendText(
290
+ request,
291
+ response,
292
+ statusCode,
293
+ custom,
294
+ 'text/html; charset=utf-8',
295
+ SSR_HTML_SECURITY_HEADERS,
296
+ )
297
+ }
298
+ const markup = renderErrorMarkup(title, message)
299
+ try {
300
+ const app = apps.find((candidate) => candidate.name === appName) || defaultApp
301
+ let template = await loadHtmlTemplate(app.template, request.url)
302
+ template = template
303
+ .replace(
304
+ SSR_MARKERS.head,
305
+ `<title ${headAttribute}>${escapeHtml(title)}</title><meta ${headAttribute} name="robots" content="noindex, nofollow">`,
306
+ )
307
+ .replace(SSR_MARKERS.state, '')
308
+ .replace(SSR_MARKERS.teleports, '')
309
+ // A broken bundle must not half-hydrate an error page.
310
+ .replace(/<script\b[^>]*type=["']module["'][^>]*>[\s\S]*?<\/script>/gi, '')
311
+ .replace(SSR_MARKERS.html, markup)
312
+ sendText(
313
+ request,
314
+ response,
315
+ statusCode,
316
+ template,
317
+ 'text/html; charset=utf-8',
318
+ SSR_HTML_SECURITY_HEADERS,
319
+ )
320
+ } catch {
321
+ sendText(
322
+ request,
323
+ response,
324
+ statusCode,
325
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="robots" content="noindex, nofollow"><title>${escapeHtml(title)}</title></head><body>${markup}</body></html>`,
326
+ 'text/html; charset=utf-8',
327
+ SSR_HTML_SECURITY_HEADERS,
328
+ )
329
+ }
330
+ }
331
+
332
+ // ---- Preflight ---------------------------------------------------------
333
+ const assertTemplatesReady = async () => {
334
+ await Promise.all(
335
+ apps
336
+ .filter((app) => isAppAllowed(app.name))
337
+ .map(async (app) => {
338
+ const templatePath = resolve(production ? clientDir : root, app.template)
339
+ const info = await stat(templatePath)
340
+ if (!info.isFile()) {
341
+ throw new Error(`Required client template is missing: ${app.template}`)
342
+ }
343
+ }),
344
+ )
345
+ }
346
+
347
+ const assertRuntimeReady = async () => {
348
+ await assertTemplatesReady()
349
+ if (ssrApp && isAppAllowed(ssrApp.name)) {
350
+ const module = await loadRenderModule()
351
+ if (typeof module[renderExport] !== 'function') {
352
+ throw new Error('The SSR render module could not be loaded.')
353
+ }
354
+ await module[readyExport]?.()
355
+ }
356
+ for (const probe of readinessProbes) await probe()
357
+ }
358
+
359
+ try {
360
+ await assertTemplatesReady()
361
+ if (ssrApp && isAppAllowed(ssrApp.name)) {
362
+ const module = await loadRenderModule()
363
+ if (typeof module[renderExport] !== 'function') {
364
+ throw new Error('The SSR render module could not be loaded.')
365
+ }
366
+ await module[readyExport]?.()
367
+ }
368
+ } catch (error) {
369
+ try {
370
+ await vite?.close()
371
+ } catch {
372
+ // Closing a half-started Vite server must not mask the preflight error.
373
+ }
374
+ logger.error(`\n[${serviceName}] runtime preflight failed; not starting.`)
375
+ logger.error(resolveRootCause(error))
376
+ process.exit(1)
377
+ }
378
+
379
+ // ---- Request pipeline --------------------------------------------------
380
+ let shuttingDown = false
381
+
382
+ const server = createServer(async (request, response) => {
383
+ const startedAt = new Date().toISOString()
384
+ let pathname = '/'
385
+ let appName = defaultApp.name
386
+
387
+ try {
388
+ pathname = new URL(request.url || '/', 'http://internal').pathname
389
+
390
+ if (pathname === '/healthz') {
391
+ return sendJson(request, response, 200, {
392
+ status: 'ok',
393
+ service: serviceName,
394
+ runtime: serveRole,
395
+ timestamp: startedAt,
396
+ })
397
+ }
398
+ if (pathname === '/readyz') {
399
+ if (shuttingDown) {
400
+ return sendJson(request, response, 503, {
401
+ status: 'error',
402
+ service: serviceName,
403
+ runtime: serveRole,
404
+ message: 'Server is shutting down.',
405
+ timestamp: startedAt,
406
+ })
407
+ }
408
+ try {
409
+ await assertRuntimeReady()
410
+ return sendJson(request, response, 200, {
411
+ status: 'ok',
412
+ service: serviceName,
413
+ runtime: serveRole,
414
+ timestamp: startedAt,
415
+ })
416
+ } catch {
417
+ return sendJson(request, response, 503, {
418
+ status: 'error',
419
+ service: serviceName,
420
+ runtime: serveRole,
421
+ timestamp: startedAt,
422
+ })
423
+ }
424
+ }
425
+
426
+ const incomingHost = resolveSsrForwardedHost(
427
+ request.headers['x-forwarded-host'],
428
+ request.headers.host,
429
+ trustProxy,
430
+ )
431
+ appName = classify(incomingHost, pathname, request.headers)
432
+ if (appName === 'invalid' || !incomingHost) {
433
+ if (isHtmlNavigation(request, pathname)) {
434
+ return sendNavigationError(
435
+ request,
436
+ response,
437
+ 400,
438
+ defaultApp.name,
439
+ new Error('The request Host header is invalid.'),
440
+ )
441
+ }
442
+ return sendJson(request, response, 400, {
443
+ status: 'error',
444
+ service: serviceName,
445
+ message: 'Invalid Host header.',
446
+ timestamp: startedAt,
447
+ })
448
+ }
449
+ if (!isAppAllowed(appName)) {
450
+ const message = `This ${serveRole} runtime does not serve ${appName} hosts.`
451
+ if (isHtmlNavigation(request, pathname)) {
452
+ return sendText(
453
+ request,
454
+ response,
455
+ 421,
456
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="robots" content="noindex, nofollow"><title>Misdirected request</title></head><body>${renderErrorMarkup('Misdirected request', message)}</body></html>`,
457
+ 'text/html; charset=utf-8',
458
+ SSR_HTML_SECURITY_HEADERS,
459
+ )
460
+ }
461
+ return sendJson(request, response, 421, {
462
+ status: 'error',
463
+ service: serviceName,
464
+ runtime: serveRole,
465
+ message,
466
+ timestamp: startedAt,
467
+ })
468
+ }
469
+
470
+ const protocol = resolveSsrForwardedProtocol(
471
+ request.headers['x-forwarded-proto'],
472
+ request.socket.encrypted ? 'https' : 'http',
473
+ trustProxy,
474
+ )
475
+
476
+ // Consumer endpoints (SEO resources, proxies, …) run after host
477
+ // classification and before asset handling, mirroring the audited
478
+ // ERP pipeline.
479
+ for (const route of consumerRoutes) {
480
+ if (!route.match(pathname, { appName, host: incomingHost, protocol })) {
481
+ continue
482
+ }
483
+ await route.handle(request, response, {
484
+ appName,
485
+ host: incomingHost,
486
+ protocol,
487
+ pathname,
488
+ config: options.config,
489
+ loadRenderModule,
490
+ sendJson: (statusCode, payload) =>
491
+ sendJson(request, response, statusCode, payload),
492
+ sendText: (statusCode, body, contentType, headers) =>
493
+ sendText(request, response, statusCode, body, contentType, headers),
494
+ })
495
+ if (response.writableEnded) return
496
+ }
497
+
498
+ if (production && (await serveProductionAsset(pathname, request, response))) {
499
+ return
500
+ }
501
+ if (
502
+ !production &&
503
+ (pathname.startsWith('/src/') ||
504
+ pathname.startsWith('/@') ||
505
+ pathname.includes('.') ||
506
+ pathname === '/__vite_ping')
507
+ ) {
508
+ await runViteMiddleware(request, response)
509
+ if (response.writableEnded) return
510
+ }
511
+
512
+ if (!isHtmlNavigation(request, pathname)) {
513
+ return sendJson(request, response, 404, {
514
+ status: 'error',
515
+ service: serviceName,
516
+ message: 'Resource not found.',
517
+ timestamp: startedAt,
518
+ })
519
+ }
520
+
521
+ const app = apps.find((candidate) => candidate.name === appName) || defaultApp
522
+
523
+ if (app.type === 'spa') {
524
+ const template = await loadHtmlTemplate(app.template, request.url)
525
+ response.writeHead(200, {
526
+ 'content-type': 'text/html; charset=utf-8',
527
+ 'cache-control': 'private, no-store',
528
+ vary: 'Host, X-Forwarded-Host',
529
+ ...SSR_HTML_SECURITY_HEADERS,
530
+ })
531
+ return endResponse(
532
+ request,
533
+ response,
534
+ template.replace(SSR_MARKERS.html, ''),
535
+ )
536
+ }
537
+
538
+ const result = await engine.handle({
539
+ url: request.url || '/',
540
+ host: incomingHost,
541
+ protocol,
542
+ cookieHeader: request.headers.cookie,
543
+ })
544
+ response.writeHead(result.statusCode, result.headers)
545
+ endResponse(request, response, result.html)
546
+ } catch (error) {
547
+ vite?.ssrFixStacktrace(error)
548
+ logger.error(error)
549
+ if (response.writableEnded) return
550
+ if (response.headersSent) {
551
+ response.destroy()
552
+ return
553
+ }
554
+ if (isHtmlNavigation(request, pathname)) {
555
+ return sendNavigationError(request, response, 500, appName, error)
556
+ }
557
+ sendJson(request, response, 500, {
558
+ status: 'error',
559
+ service: serviceName,
560
+ timestamp: startedAt,
561
+ })
562
+ }
563
+ })
564
+
565
+ // ---- Lifecycle ---------------------------------------------------------
566
+ const shutdown = async () => {
567
+ if (shuttingDown) return
568
+ shuttingDown = true
569
+ const forcedExit = setTimeout(() => process.exit(1), 10_000)
570
+ forcedExit.unref()
571
+ try {
572
+ await Promise.all([
573
+ new Promise((resolveClose, rejectClose) => {
574
+ server.close((error) => {
575
+ if (error) rejectClose(error)
576
+ else resolveClose()
577
+ })
578
+ }),
579
+ vite?.close(),
580
+ ])
581
+ clearTimeout(forcedExit)
582
+ process.exit(0)
583
+ } catch (error) {
584
+ logger.error(`[${serviceName}] Graceful shutdown failed.`, error)
585
+ process.exit(1)
586
+ }
587
+ }
588
+ process.once('SIGINT', () => void shutdown())
589
+ process.once('SIGTERM', () => void shutdown())
590
+
591
+ const listen = () =>
592
+ new Promise((resolveListen) => {
593
+ server.listen(port, host, () => {
594
+ logger.log(`${serviceName} (${serveRole}) listening on ${host}:${port}`)
595
+ resolveListen({ server, port, host })
596
+ })
597
+ })
598
+
599
+ return { server, vite, listen, shutdown, options }
600
+ }
@@ -0,0 +1,91 @@
1
+ import type { Server } from 'node:http'
2
+ import type { SsrEngineRequest } from '../engine/index'
3
+
4
+ export * from '../engine/index'
5
+
6
+ export interface SsrServerApp {
7
+ name: string
8
+ type?: 'ssr' | 'spa'
9
+ template: string
10
+ hosts?: string[]
11
+ default?: boolean
12
+ localhost?: boolean
13
+ }
14
+
15
+ export interface SsrServerRoute {
16
+ match: (
17
+ pathname: string,
18
+ info: { appName: string; host: string; protocol: string },
19
+ ) => boolean
20
+ handle: (
21
+ request: import('node:http').IncomingMessage,
22
+ response: import('node:http').ServerResponse,
23
+ tools: {
24
+ appName: string
25
+ host: string
26
+ protocol: string
27
+ pathname: string
28
+ config?: Record<string, unknown>
29
+ loadRenderModule: () => Promise<Record<string, unknown>>
30
+ sendJson: (statusCode: number, payload: unknown) => void
31
+ sendText: (
32
+ statusCode: number,
33
+ body: string,
34
+ contentType?: string,
35
+ headers?: Record<string, string>,
36
+ ) => void
37
+ },
38
+ ) => Promise<void> | void
39
+ }
40
+
41
+ export interface SsrServerOptions {
42
+ root?: string
43
+ mode?: 'development' | 'production'
44
+ port?: number
45
+ host?: string
46
+ trustProxy?: boolean
47
+ apps?: SsrServerApp[]
48
+ serverEntry?: string
49
+ bundlePath?: string
50
+ clientDir?: string
51
+ renderExport?: string
52
+ readyExport?: string
53
+ config?: Record<string, unknown>
54
+ globals?: { configKey?: string; stateKey?: string }
55
+ headAttribute?: string
56
+ defaultHead?: string
57
+ cookieAllowlist?: string[]
58
+ cookieDenylist?: string[]
59
+ classifyRequest?: (info: {
60
+ hostname: string
61
+ pathname: string
62
+ headers: Record<string, string | string[] | undefined>
63
+ }) => string | undefined
64
+ serveRole?: string
65
+ routes?: SsrServerRoute[]
66
+ readiness?: Array<() => Promise<void> | void>
67
+ errorPage?: (info: {
68
+ statusCode: number
69
+ error: unknown
70
+ appName: string
71
+ mode: 'development' | 'production'
72
+ }) => string | null | undefined
73
+ renderTimeoutMs?: number
74
+ security?: { nonce?: (request: SsrEngineRequest) => string | undefined }
75
+ serviceName?: string
76
+ logger?: Pick<Console, 'log' | 'warn' | 'error'>
77
+ vite?: Record<string, unknown>
78
+ }
79
+
80
+ export declare const SSR_RUNTIME_MANIFEST: string
81
+ export declare const loadSsrRuntimeManifest: (
82
+ clientDir: string,
83
+ ) => Promise<Record<string, unknown> | null>
84
+
85
+ export declare const createSsrServer: (options?: SsrServerOptions) => Promise<{
86
+ server: Server
87
+ vite: unknown
88
+ listen: () => Promise<{ server: Server; port: number; host: string }>
89
+ shutdown: () => Promise<void>
90
+ options: SsrServerOptions
91
+ }>
@@ -0,0 +1,14 @@
1
+ export {
2
+ createSsrServer,
3
+ loadSsrRuntimeManifest,
4
+ SSR_RUNTIME_MANIFEST,
5
+ } from './SsrServer.js'
6
+ // The engine is separable from the standalone server: custom Node servers,
7
+ // Express-style frameworks, tests, and serverless deployments consume it
8
+ // directly (also available from 'vue-ssr-lite/engine').
9
+ export {
10
+ createSsrRequestHandler,
11
+ SSR_HTML_SECURITY_HEADERS,
12
+ } from '../engine/SsrEngine.js'
13
+ export * from '../engine/SsrEngineHtml.js'
14
+ export * from '../engine/SsrEngineHostPolicy.js'