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,139 @@
1
+ // SsrEngine — the framework-agnostic SSR request handler.
2
+ //
3
+ // The engine is deliberately independent of the standalone Node server: it
4
+ // accepts a plain request description and returns a plain response
5
+ // description, so it can be mounted into an existing Node server, Express or
6
+ // similar frameworks, tests, serverless functions, or any environment that
7
+ // can supply `loadTemplate` and `loadRenderModule` implementations. The
8
+ // standalone server in ../server is one (default) host for this engine.
9
+
10
+ import {
11
+ injectSsrDocument,
12
+ renderHeadTags,
13
+ renderStateScript,
14
+ } from './SsrEngineHtml.js'
15
+ import { selectSsrCookieHeader } from './SsrEngineHostPolicy.js'
16
+
17
+ export const SSR_HTML_SECURITY_HEADERS = Object.freeze({
18
+ 'referrer-policy': 'strict-origin-when-cross-origin',
19
+ 'x-content-type-options': 'nosniff',
20
+ 'x-frame-options': 'SAMEORIGIN',
21
+ })
22
+
23
+ /**
24
+ * Create an SSR request handler.
25
+ *
26
+ * options:
27
+ * - loadTemplate(requestUrl): Promise<string> — marker-carrying HTML template
28
+ * (already transformed for the current mode).
29
+ * - loadRenderModule(): Promise<module> — module exposing the render function.
30
+ * - renderExport: name of the render export (default 'render').
31
+ * - config: optional serializable public config forwarded to the render and
32
+ * serialized for the browser. Omit it entirely when the app needs none.
33
+ * - globals: { configKey, stateKey } — serialized global names.
34
+ * - headAttribute: attribute marking server-rendered head tags.
35
+ * - defaultHead: raw HTML string injected when the render yields no head
36
+ * payload (consumer-owned fallback; empty by default).
37
+ * - cookieAllowlist / cookieDenylist: cookie-name filtering before SSR.
38
+ * - renderTimeoutMs: hard budget for one render (default 10000).
39
+ * - nonce(request): optional per-request CSP nonce provider.
40
+ */
41
+ export const createSsrRequestHandler = (options) => {
42
+ const {
43
+ loadTemplate,
44
+ loadRenderModule,
45
+ renderExport = 'render',
46
+ config,
47
+ globals = {},
48
+ headAttribute = 'data-ssr-head',
49
+ defaultHead = '',
50
+ cookieAllowlist = [],
51
+ cookieDenylist = [],
52
+ renderTimeoutMs = 10_000,
53
+ nonce,
54
+ } = options
55
+
56
+ if (typeof loadTemplate !== 'function' || typeof loadRenderModule !== 'function') {
57
+ throw new Error(
58
+ 'createSsrRequestHandler requires loadTemplate and loadRenderModule functions.',
59
+ )
60
+ }
61
+
62
+ /**
63
+ * Handle one SSR request.
64
+ *
65
+ * request: { url (path + search + hash), host, protocol, cookieHeader }
66
+ * Returns: { statusCode, headers, html } — or throws; the host layer owns
67
+ * error presentation.
68
+ */
69
+ const handle = async (request) => {
70
+ const { url = '/', host, protocol = 'http', cookieHeader } = request
71
+ const module = await loadRenderModule()
72
+ const render = module[renderExport]
73
+ if (typeof render !== 'function') {
74
+ throw new Error(
75
+ `The SSR module does not export a "${renderExport}" function.`,
76
+ )
77
+ }
78
+
79
+ const cookie = selectSsrCookieHeader(cookieHeader, cookieAllowlist, cookieDenylist)
80
+ const renderRequest = {
81
+ url: `${protocol}://${host}${url}`,
82
+ host,
83
+ cookie,
84
+ config,
85
+ }
86
+
87
+ let timeoutHandle
88
+ const rendered = await Promise.race([
89
+ render(renderRequest),
90
+ new Promise((_resolve, reject) => {
91
+ timeoutHandle = setTimeout(
92
+ () => reject(new Error(`SSR render exceeded ${renderTimeoutMs}ms.`)),
93
+ renderTimeoutMs,
94
+ )
95
+ if (typeof timeoutHandle === 'object') timeoutHandle.unref?.()
96
+ }),
97
+ ]).finally(() => clearTimeout(timeoutHandle))
98
+
99
+ if (rendered.redirect) {
100
+ return {
101
+ statusCode: rendered.statusCode >= 300 && rendered.statusCode < 400
102
+ ? rendered.statusCode
103
+ : 302,
104
+ headers: { location: rendered.redirect, 'cache-control': 'no-store' },
105
+ html: '',
106
+ }
107
+ }
108
+
109
+ const template = await loadTemplate(url)
110
+ const requestNonce = typeof nonce === 'function' ? nonce(request) : undefined
111
+ const headHtml = renderHeadTags(rendered.head, headAttribute) || defaultHead
112
+ // Server-level config wins; otherwise the definition-resolved config
113
+ // reported by the render is serialized, so simple consumers configure
114
+ // everything in their single application definition.
115
+ const serializedConfig = config ?? rendered.config
116
+ const html = injectSsrDocument(template, {
117
+ head: headHtml,
118
+ html: rendered.html,
119
+ teleports: rendered.teleports ?? '',
120
+ state: renderStateScript(
121
+ { config: serializedConfig, state: rendered.state },
122
+ { ...globals, nonce: requestNonce },
123
+ ),
124
+ })
125
+
126
+ return {
127
+ statusCode: rendered.statusCode ?? 200,
128
+ headers: {
129
+ 'content-type': 'text/html; charset=utf-8',
130
+ 'cache-control': 'private, no-store',
131
+ vary: 'Host, X-Forwarded-Host',
132
+ ...SSR_HTML_SECURITY_HEADERS,
133
+ },
134
+ html,
135
+ }
136
+ }
137
+
138
+ return { handle }
139
+ }
@@ -0,0 +1,132 @@
1
+ // SsrEngineHostPolicy — host normalization and forwarded-header trust policy.
2
+ //
3
+ // Ported from the proven ERP host runtime. Forwarded headers are only ever
4
+ // honored under an explicit trust flag; hosts are validated for length,
5
+ // control characters, credentials, paths, ports, IPv6 brackets, case, and
6
+ // trailing dots. Product classification (which host serves which entry) is a
7
+ // consumer concern layered on top of these primitives.
8
+
9
+ const firstHeaderValue = (value) =>
10
+ String(Array.isArray(value) ? value[0] : value || '')
11
+ .split(',', 1)[0]
12
+ .trim()
13
+
14
+ export const normalizeSsrHost = (host) => {
15
+ const raw = firstHeaderValue(host)
16
+ if (
17
+ !raw ||
18
+ raw.length > 512 ||
19
+ /[\u0000-\u0020\u007f]/.test(raw) ||
20
+ /[\\/@?#]/.test(raw)
21
+ ) {
22
+ return ''
23
+ }
24
+
25
+ try {
26
+ const authority =
27
+ raw.includes(':') && raw.split(':').length > 2 && !raw.startsWith('[')
28
+ ? `[${raw}]`
29
+ : raw
30
+ const parsed = new URL(`http://${authority}`)
31
+ if (
32
+ parsed.username ||
33
+ parsed.password ||
34
+ parsed.pathname !== '/' ||
35
+ parsed.search ||
36
+ parsed.hash
37
+ ) {
38
+ return ''
39
+ }
40
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/, '')
41
+ if (!hostname) return ''
42
+ const bracketedHostname =
43
+ hostname.includes(':') && !hostname.startsWith('[')
44
+ ? `[${hostname}]`
45
+ : hostname
46
+ return `${bracketedHostname}${parsed.port ? `:${parsed.port}` : ''}`
47
+ } catch {
48
+ return ''
49
+ }
50
+ }
51
+
52
+ export const stripSsrHostPort = (host) => {
53
+ const value = normalizeSsrHost(host)
54
+ if (!value) return ''
55
+ if (value.startsWith('[')) {
56
+ const end = value.indexOf(']')
57
+ return end >= 0 ? value.slice(1, end) : ''
58
+ }
59
+ return value.replace(/:\d+$/, '').replace(/\.$/, '')
60
+ }
61
+
62
+ /** Normalize an exact host or a safe leading-wildcard pattern (`*.domain`). */
63
+ export const normalizeSsrHostPattern = (host) => {
64
+ const value = String(host || '').trim()
65
+ if (!value.startsWith('*.')) return normalizeSsrHost(value)
66
+ const baseDomain = stripSsrHostPort(value.slice(2))
67
+ if (!baseDomain || baseDomain.includes(':')) return ''
68
+ return `*.${baseDomain}`
69
+ }
70
+
71
+ /** True when `hostname` (no port) matches any exact or wildcard pattern. */
72
+ export const matchesSsrHostPatterns = (hostname, patterns) => {
73
+ const host = stripSsrHostPort(hostname)
74
+ if (!host) return false
75
+ for (const candidate of patterns || []) {
76
+ const pattern = normalizeSsrHostPattern(candidate)
77
+ if (!pattern) continue
78
+ if (pattern.startsWith('*.')) {
79
+ if (host.endsWith(`.${pattern.slice(2)}`)) return true
80
+ } else if (stripSsrHostPort(pattern) === host) {
81
+ return true
82
+ }
83
+ }
84
+ return false
85
+ }
86
+
87
+ export const resolveSsrForwardedHost = (
88
+ forwardedHost,
89
+ host,
90
+ trustForwardedHeaders = false,
91
+ ) =>
92
+ normalizeSsrHost(
93
+ trustForwardedHeaders ? firstHeaderValue(forwardedHost) || host : host,
94
+ )
95
+
96
+ export const resolveSsrForwardedProtocol = (
97
+ forwardedProtocol,
98
+ fallbackProtocol = 'http',
99
+ trustForwardedHeaders = false,
100
+ ) => {
101
+ const fallback = fallbackProtocol === 'https' ? 'https' : 'http'
102
+ if (!trustForwardedHeaders) return fallback
103
+ const protocol = firstHeaderValue(forwardedProtocol).toLowerCase()
104
+ return protocol === 'https' || protocol === 'http' ? protocol : fallback
105
+ }
106
+
107
+ /**
108
+ * Filter a Cookie header down to an allowlist of cookie names, with an
109
+ * optional denylist that always wins. Returns undefined when nothing remains,
110
+ * so downstream data layers never see unapproved cookies.
111
+ */
112
+ export const selectSsrCookieHeader = (cookieHeader, allowlist, denylist) => {
113
+ const allowed = new Set(
114
+ (allowlist || [])
115
+ .map((name) => String(name).trim())
116
+ .filter((name) => /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)),
117
+ )
118
+ const denied = new Set((denylist || []).map((name) => String(name).trim()))
119
+ for (const name of denied) allowed.delete(name)
120
+ if (!cookieHeader || allowed.size === 0) return undefined
121
+ const selected = String(cookieHeader)
122
+ .split(';')
123
+ .map((part) => part.trim())
124
+ .filter(Boolean)
125
+ .filter((part) => {
126
+ const separator = part.indexOf('=')
127
+ if (separator <= 0) return false
128
+ const name = part.slice(0, separator).trim()
129
+ return allowed.has(name) && !denied.has(name)
130
+ })
131
+ return selected.length > 0 ? selected.join('; ') : undefined
132
+ }
@@ -0,0 +1,166 @@
1
+ // SsrEngineHtml — package-owned HTML document primitives.
2
+ //
3
+ // These are invariants, not configuration: escaping rules, XSS-safe state
4
+ // serialization, marker names, and head-tag rendering order are identical in
5
+ // development and production and identical across consumers. The head payload
6
+ // contract and tag order are byte-compatible with the audited ERP renderer so
7
+ // the ERP migration cannot change its SEO output.
8
+
9
+ export const SSR_MARKERS = Object.freeze({
10
+ head: '<!--app-head-->',
11
+ html: '<!--app-html-->',
12
+ state: '<!--app-state-->',
13
+ teleports: '<!--app-teleports-->',
14
+ })
15
+
16
+ export const escapeHtml = (value) =>
17
+ String(value ?? '')
18
+ .replaceAll('&', '&amp;')
19
+ .replaceAll('<', '&lt;')
20
+ .replaceAll('>', '&gt;')
21
+ .replaceAll('"', '&quot;')
22
+ .replaceAll("'", '&#39;')
23
+
24
+ export const escapeXml = (value) =>
25
+ String(value ?? '')
26
+ .replaceAll('&', '&amp;')
27
+ .replaceAll('<', '&lt;')
28
+ .replaceAll('>', '&gt;')
29
+ .replaceAll('"', '&quot;')
30
+ .replaceAll("'", '&apos;')
31
+
32
+ /** XSS-safe JSON serialization for inline <script> payloads. */
33
+ export const serializeState = (value) =>
34
+ JSON.stringify(value)
35
+ .replaceAll('<', '\\u003c')
36
+ .replaceAll('\u2028', '\\u2028')
37
+ .replaceAll('\u2029', '\\u2029')
38
+
39
+ /**
40
+ * Render a head payload into escaped tags.
41
+ *
42
+ * Payload contract (all fields optional): title, description, keywords,
43
+ * robots, ogTitle, ogDescription, ogImage, ogImageAlt, ogUrl, ogType,
44
+ * ogSiteName, ogLocale, twitterCard, twitterTitle, twitterDescription,
45
+ * twitterImage, twitterImageAlt, twitterSite, twitterCreator, canonicalUrl,
46
+ * favicon, jsonLd (array of objects).
47
+ *
48
+ * Every rendered tag carries `attribute` so the browser runtime can remove
49
+ * server-rendered tags before the client SEO layer takes over.
50
+ */
51
+ export const renderHeadTags = (payload, attribute = 'data-ssr-head') => {
52
+ if (!payload) return ''
53
+ const tags = []
54
+ if (payload.title) {
55
+ tags.push(`<title ${attribute}>${escapeHtml(payload.title)}</title>`)
56
+ }
57
+ const meta = [
58
+ ['name', 'description', payload.description],
59
+ ['name', 'keywords', payload.keywords],
60
+ ['name', 'robots', payload.robots],
61
+ ['property', 'og:title', payload.ogTitle],
62
+ ['property', 'og:description', payload.ogDescription],
63
+ ['property', 'og:image', payload.ogImage],
64
+ ['property', 'og:image:alt', payload.ogImageAlt],
65
+ ['property', 'og:url', payload.ogUrl],
66
+ ['property', 'og:type', payload.ogType],
67
+ ['property', 'og:site_name', payload.ogSiteName],
68
+ ['property', 'og:locale', payload.ogLocale],
69
+ ['name', 'twitter:card', payload.twitterCard],
70
+ ['name', 'twitter:title', payload.twitterTitle],
71
+ ['name', 'twitter:description', payload.twitterDescription],
72
+ ['name', 'twitter:image', payload.twitterImage],
73
+ ['name', 'twitter:image:alt', payload.twitterImageAlt],
74
+ ['name', 'twitter:site', payload.twitterSite],
75
+ ['name', 'twitter:creator', payload.twitterCreator],
76
+ ]
77
+ for (const [attributeName, name, content] of meta) {
78
+ if (content != null && content !== '') {
79
+ tags.push(
80
+ `<meta ${attribute} ${attributeName}="${escapeHtml(name)}" content="${escapeHtml(content)}">`,
81
+ )
82
+ }
83
+ }
84
+ if (payload.canonicalUrl) {
85
+ tags.push(
86
+ `<link ${attribute} rel="canonical" href="${escapeHtml(payload.canonicalUrl)}">`,
87
+ )
88
+ }
89
+ if (payload.favicon) {
90
+ tags.push(`<link ${attribute} rel="icon" href="${escapeHtml(payload.favicon)}">`)
91
+ }
92
+ for (const block of payload.jsonLd || []) {
93
+ const json = JSON.stringify(block).replaceAll('<', '\\u003c')
94
+ tags.push(`<script ${attribute} type="application/ld+json">${json}</script>`)
95
+ }
96
+ return tags.join('')
97
+ }
98
+
99
+ /**
100
+ * Render the state script exposing serialized public config and hydration
101
+ * state on package-owned (or consumer-overridden) globals. Emits nothing when
102
+ * both parts are empty. `nonce` supports strict Content Security Policies.
103
+ */
104
+ export const renderStateScript = (
105
+ { config, state },
106
+ { configKey = '__SSR_CONFIG__', stateKey = '__SSR_STATE__', nonce } = {},
107
+ ) => {
108
+ const parts = []
109
+ if (config != null && Object.keys(config).length > 0) {
110
+ parts.push(`window.${configKey}=${serializeState(config)}`)
111
+ }
112
+ if (state != null) {
113
+ parts.push(`window.${stateKey}=${serializeState(state)}`)
114
+ }
115
+ if (parts.length === 0) return ''
116
+ const nonceAttribute = nonce ? ` nonce="${escapeHtml(nonce)}"` : ''
117
+ return `<script${nonceAttribute}>${parts.join(';')}</script>`
118
+ }
119
+
120
+ /** Inject rendered fragments into a marker-carrying HTML template. */
121
+ export const injectSsrDocument = (template, { head = '', html = '', state = '', teleports = '' }) =>
122
+ template
123
+ .replace(SSR_MARKERS.head, head)
124
+ .replace(SSR_MARKERS.teleports, teleports)
125
+ .replace(SSR_MARKERS.html, html)
126
+ .replace(SSR_MARKERS.state, state)
127
+
128
+ /**
129
+ * Guarantee the four SSR markers exist in a template so consumers never learn
130
+ * marker names. Existing markers are left untouched; missing ones are added:
131
+ * head before </head>, teleports after <body>, html inside the mount element,
132
+ * state before </body>.
133
+ */
134
+ export const ensureSsrMarkers = (template, { mountSelector = '#app' } = {}) => {
135
+ let html = String(template)
136
+ if (!html.includes(SSR_MARKERS.head)) {
137
+ html = html.replace(/<\/head>/i, `${SSR_MARKERS.head}\n</head>`)
138
+ }
139
+ if (!html.includes(SSR_MARKERS.teleports)) {
140
+ html = html.replace(/<body([^>]*)>/i, `<body$1>${SSR_MARKERS.teleports}`)
141
+ }
142
+ if (!html.includes(SSR_MARKERS.html)) {
143
+ const id = mountSelector.startsWith('#') ? mountSelector.slice(1) : mountSelector
144
+ const mountPattern = new RegExp(`(<div[^>]*id=["']${id}["'][^>]*>)(</div>)`, 'i')
145
+ html = html.replace(mountPattern, `$1${SSR_MARKERS.html}$2`)
146
+ }
147
+ if (!html.includes(SSR_MARKERS.state)) {
148
+ html = html.replace(/<\/body>/i, `${SSR_MARKERS.state}\n</body>`)
149
+ }
150
+ return html
151
+ }
152
+
153
+ /** Generic accessible error markup used by fallback shells (overridable upstream). */
154
+ export const renderErrorMarkup = (title, message) => `
155
+ <main id="main-content" style="min-height:70vh;display:grid;place-items:center;padding:2rem;text-align:center;font-family:system-ui,sans-serif" tabindex="-1">
156
+ <div><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p><p><a href="/">Return home</a></p></div>
157
+ </main>`
158
+
159
+ /** Sitemap primitives shared by consumers that serve SEO XML from the server. */
160
+ export const renderSitemapUrlSet = (urls) => {
161
+ const uniqueUrls = [...new Set(urls)]
162
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${uniqueUrls.map((url) => `\n <url><loc>${escapeXml(url)}</loc></url>`).join('')}\n</urlset>\n`
163
+ }
164
+
165
+ export const renderSitemapIndex = (urls) =>
166
+ `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.map((url) => `\n <sitemap><loc>${escapeXml(url)}</loc></sitemap>`).join('')}\n</sitemapindex>\n`
@@ -0,0 +1,86 @@
1
+ import type { SsrHeadPayload } from '../index'
2
+
3
+ export declare const SSR_MARKERS: Readonly<{
4
+ head: string
5
+ html: string
6
+ state: string
7
+ teleports: string
8
+ }>
9
+ export declare const SSR_HTML_SECURITY_HEADERS: Readonly<Record<string, string>>
10
+
11
+ export declare const escapeHtml: (value: unknown) => string
12
+ export declare const escapeXml: (value: unknown) => string
13
+ export declare const serializeState: (value: unknown) => string
14
+ export declare const renderHeadTags: (
15
+ payload: SsrHeadPayload | null | undefined,
16
+ attribute?: string,
17
+ ) => string
18
+ export declare const renderStateScript: (
19
+ parts: { config?: Record<string, unknown> | null; state?: unknown },
20
+ options?: { configKey?: string; stateKey?: string; nonce?: string },
21
+ ) => string
22
+ export declare const injectSsrDocument: (
23
+ template: string,
24
+ fragments: { head?: string; html?: string; state?: string; teleports?: string },
25
+ ) => string
26
+ export declare const ensureSsrMarkers: (
27
+ template: string,
28
+ options?: { mountSelector?: string },
29
+ ) => string
30
+ export declare const renderErrorMarkup: (title: string, message: string) => string
31
+ export declare const renderSitemapUrlSet: (urls: string[]) => string
32
+ export declare const renderSitemapIndex: (urls: string[]) => string
33
+
34
+ export declare const normalizeSsrHost: (host: unknown) => string
35
+ export declare const stripSsrHostPort: (host: unknown) => string
36
+ export declare const normalizeSsrHostPattern: (host: unknown) => string
37
+ export declare const matchesSsrHostPatterns: (
38
+ hostname: unknown,
39
+ patterns: ReadonlyArray<string> | undefined,
40
+ ) => boolean
41
+ export declare const resolveSsrForwardedHost: (
42
+ forwardedHost: unknown,
43
+ host: unknown,
44
+ trustForwardedHeaders?: boolean,
45
+ ) => string
46
+ export declare const resolveSsrForwardedProtocol: (
47
+ forwardedProtocol: unknown,
48
+ fallbackProtocol?: string,
49
+ trustForwardedHeaders?: boolean,
50
+ ) => 'http' | 'https'
51
+ export declare const selectSsrCookieHeader: (
52
+ cookieHeader: unknown,
53
+ allowlist?: ReadonlyArray<string>,
54
+ denylist?: ReadonlyArray<string>,
55
+ ) => string | undefined
56
+
57
+ export interface SsrEngineRequest {
58
+ url?: string
59
+ host: string
60
+ protocol?: 'http' | 'https'
61
+ cookieHeader?: string
62
+ }
63
+
64
+ export interface SsrEngineResponse {
65
+ statusCode: number
66
+ headers: Record<string, string>
67
+ html: string
68
+ }
69
+
70
+ export interface SsrEngineOptions {
71
+ loadTemplate: (requestUrl?: string) => Promise<string> | string
72
+ loadRenderModule: () => Promise<Record<string, unknown>>
73
+ renderExport?: string
74
+ config?: Record<string, unknown>
75
+ globals?: { configKey?: string; stateKey?: string }
76
+ headAttribute?: string
77
+ defaultHead?: string
78
+ cookieAllowlist?: string[]
79
+ cookieDenylist?: string[]
80
+ renderTimeoutMs?: number
81
+ nonce?: (request: SsrEngineRequest) => string | undefined
82
+ }
83
+
84
+ export declare const createSsrRequestHandler: (options: SsrEngineOptions) => {
85
+ handle: (request: SsrEngineRequest) => Promise<SsrEngineResponse>
86
+ }
@@ -0,0 +1,26 @@
1
+ export {
2
+ SSR_MARKERS,
3
+ escapeHtml,
4
+ escapeXml,
5
+ serializeState,
6
+ renderHeadTags,
7
+ renderStateScript,
8
+ injectSsrDocument,
9
+ ensureSsrMarkers,
10
+ renderErrorMarkup,
11
+ renderSitemapUrlSet,
12
+ renderSitemapIndex,
13
+ } from './SsrEngineHtml.js'
14
+ export {
15
+ normalizeSsrHost,
16
+ stripSsrHostPort,
17
+ normalizeSsrHostPattern,
18
+ matchesSsrHostPatterns,
19
+ resolveSsrForwardedHost,
20
+ resolveSsrForwardedProtocol,
21
+ selectSsrCookieHeader,
22
+ } from './SsrEngineHostPolicy.js'
23
+ export {
24
+ createSsrRequestHandler,
25
+ SSR_HTML_SECURITY_HEADERS,
26
+ } from './SsrEngine.js'
package/index.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { App, Component, InjectionKey, Ref } from 'vue'
2
+ import type { Router, RouteRecordRaw, RouterScrollBehavior } from 'vue-router'
3
+ import type { ApolloClient, ApolloLink, DefaultOptions, InMemoryCacheConfig, NormalizedCacheObject } from '@apollo/client/core'
4
+
5
+ export interface SsrRequestContext<
6
+ TConfig extends Record<string, unknown> = Record<string, unknown>,
7
+ TState extends Record<string, unknown> = Record<string, unknown>,
8
+ > {
9
+ url: URL
10
+ host: string
11
+ /** Allowlist-filtered Cookie header. Server only; undefined in the browser. */
12
+ cookie?: string
13
+ config: TConfig
14
+ state: TState
15
+ head: Ref<SsrHeadPayload | null>
16
+ response: { statusCode: number; redirect: string | null }
17
+ server: boolean
18
+ /** Present when the definition declares an Apollo integration. */
19
+ apollo?: ApolloClient<NormalizedCacheObject>
20
+ }
21
+
22
+ export interface SsrHeadPayload {
23
+ title?: string
24
+ description?: string
25
+ keywords?: string
26
+ robots?: string
27
+ ogTitle?: string
28
+ ogDescription?: string
29
+ ogImage?: string
30
+ ogImageAlt?: string
31
+ ogUrl?: string
32
+ ogType?: string
33
+ ogSiteName?: string
34
+ ogLocale?: string
35
+ twitterCard?: string
36
+ twitterTitle?: string
37
+ twitterDescription?: string
38
+ twitterImage?: string
39
+ twitterImageAlt?: string
40
+ twitterSite?: string
41
+ twitterCreator?: string
42
+ canonicalUrl?: string
43
+ favicon?: string
44
+ jsonLd?: Array<Record<string, unknown>>
45
+ }
46
+
47
+ export interface SsrApolloDefinition {
48
+ endpoint?: string | ((config: Record<string, unknown>) => string)
49
+ endpoints?: Record<string, string | ((config: Record<string, unknown>) => string)>
50
+ timeoutMs?: number | ((config: Record<string, unknown>) => number)
51
+ headers?:
52
+ | Record<string, string>
53
+ | ((environment: { server: boolean }) => Record<string, string>)
54
+ links?:
55
+ | ApolloLink[]
56
+ | ((environment: {
57
+ server: boolean
58
+ context: SsrRequestContext
59
+ }) => ApolloLink[])
60
+ cacheConfig?: InMemoryCacheConfig
61
+ ssrForceFetchDelay?: number
62
+ defaultOptions?: DefaultOptions
63
+ install?: (
64
+ app: App,
65
+ clients: Record<string, ApolloClient<NormalizedCacheObject>>,
66
+ context: SsrRequestContext,
67
+ ) => void
68
+ }
69
+
70
+ export interface SsrSetupEnvironment {
71
+ app: App
72
+ router: Router | null
73
+ context: SsrRequestContext
74
+ apollo: ApolloClient<NormalizedCacheObject> | null
75
+ apolloClients: Record<string, ApolloClient<NormalizedCacheObject>> | null
76
+ server: boolean
77
+ }
78
+
79
+ export interface SsrAppDefinition {
80
+ root: Component
81
+ routes?: RouteRecordRaw[]
82
+ router?: (environment: {
83
+ server: boolean
84
+ context: SsrRequestContext
85
+ }) => Router | Promise<Router>
86
+ scrollBehavior?: RouterScrollBehavior
87
+ apollo?: SsrApolloDefinition
88
+ config?: Record<string, unknown> | (() => Record<string, unknown>)
89
+ head?: SsrHeadPayload
90
+ setup?: (environment: SsrSetupEnvironment) => void | Promise<void>
91
+ hydration?: {
92
+ configKey?: string
93
+ stateKey?: string
94
+ headAttribute?: string
95
+ mountSelector?: string
96
+ }
97
+ }
98
+
99
+ export declare const SSR_CONTEXT_KEY: InjectionKey<SsrRequestContext>
100
+ export declare const useSsrContext: <
101
+ TConfig extends Record<string, unknown> = Record<string, unknown>,
102
+ TState extends Record<string, unknown> = Record<string, unknown>,
103
+ >() => SsrRequestContext<TConfig, TState>
104
+ export declare const defineSsrApp: (definition: SsrAppDefinition) => SsrAppDefinition
package/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // vue-ssr-lite — public application-definition entry (isomorphic, tiny).
2
+ //
3
+ // A consumer's entire SSR integration is one application definition created
4
+ // with defineSsrApp. The same definition drives the generated server entry
5
+ // (render) and the generated client entry (hydration), so server and browser
6
+ // can never drift.
7
+
8
+ export { SSR_CONTEXT_KEY, useSsrContext } from './runtime/SsrRuntimeContext.js'
9
+
10
+ /**
11
+ * Declare an SSR application.
12
+ *
13
+ * definition:
14
+ * - root (required): the root Vue component of the public application.
15
+ * - routes: optional route records; when present the package creates a
16
+ * router (memory history on the server, web history in the browser).
17
+ * - router: optional async factory ({ server, context }) => Router for apps
18
+ * that build their router dynamically (advanced alternative to routes).
19
+ * - scrollBehavior: optional router scroll behavior (used with routes).
20
+ * - apollo: optional Apollo integration — endpoint or endpoints (values may
21
+ * be functions of the public config), timeoutMs, headers, links,
22
+ * cacheConfig, ssrForceFetchDelay, defaultOptions, and an optional
23
+ * install(app, clients, context) hook for library-specific provides.
24
+ * - config: optional serializable public configuration (object or factory).
25
+ * Serialized for the browser only when non-empty.
26
+ * - head: optional default head payload used when no page sets one.
27
+ * - setup: optional ({ app, router, context, apollo, apolloClients, server })
28
+ * hook for plugin installation and application provides.
29
+ * - hydration: optional overrides for global names, head attribute, and
30
+ * mount selector (compatibility tier; normal consumers never set these).
31
+ */
32
+ export const defineSsrApp = (definition) => {
33
+ if (!definition || typeof definition !== 'object') {
34
+ throw new Error('defineSsrApp requires a definition object.')
35
+ }
36
+ if (!definition.root) {
37
+ throw new Error('defineSsrApp requires a root component.')
38
+ }
39
+ return definition
40
+ }