@jasonshimmy/vite-plugin-cer-app 0.18.2 → 0.19.0
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/CHANGELOG.md +5 -0
- package/commits.txt +2 -1
- package/dist/cli/create/templates/spa/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssg/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssr/package.json.tpl +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +14 -0
- package/dist/plugin/index.js.map +1 -1
- package/dist/runtime/composables/use-auth.d.ts +6 -0
- package/dist/runtime/composables/use-auth.d.ts.map +1 -1
- package/dist/runtime/composables/use-auth.js.map +1 -1
- package/dist/runtime/composables/use-cookie.d.ts +2 -0
- package/dist/runtime/composables/use-cookie.d.ts.map +1 -1
- package/dist/runtime/composables/use-cookie.js.map +1 -1
- package/dist/runtime/composables/use-fetch.d.ts +1 -0
- package/dist/runtime/composables/use-fetch.d.ts.map +1 -1
- package/dist/runtime/composables/use-fetch.js.map +1 -1
- package/dist/runtime/composables/use-head.d.ts +4 -0
- package/dist/runtime/composables/use-head.d.ts.map +1 -1
- package/dist/runtime/composables/use-head.js.map +1 -1
- package/dist/runtime/composables/use-locale.d.ts +1 -0
- package/dist/runtime/composables/use-locale.d.ts.map +1 -1
- package/dist/runtime/composables/use-locale.js.map +1 -1
- package/dist/runtime/composables/use-runtime-config.d.ts +3 -0
- package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -1
- package/dist/runtime/composables/use-runtime-config.js +17 -4
- package/dist/runtime/composables/use-runtime-config.js.map +1 -1
- package/dist/runtime/composables/use-seo-meta.d.ts +5 -0
- package/dist/runtime/composables/use-seo-meta.d.ts.map +1 -1
- package/dist/runtime/composables/use-seo-meta.js.map +1 -1
- package/dist/runtime/composables/use-session.d.ts +5 -0
- package/dist/runtime/composables/use-session.d.ts.map +1 -1
- package/dist/runtime/composables/use-session.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +21 -1
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/runtime/isr-handler.d.ts +2 -0
- package/dist/runtime/isr-handler.d.ts.map +1 -1
- package/dist/runtime/isr-handler.js.map +1 -1
- package/dist/types/api.d.ts +21 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/config.d.ts +120 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +19 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/middleware.d.ts +16 -0
- package/dist/types/middleware.d.ts.map +1 -1
- package/dist/types/page.d.ts +56 -0
- package/dist/types/page.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +21 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/docs/authentication.md +18 -0
- package/docs/configuration.md +126 -1
- package/e2e/cypress/e2e/observability.cy.ts +77 -0
- package/e2e/kitchen-sink/app/pages/observability-test.ts +25 -0
- package/e2e/kitchen-sink/cer.config.ts +14 -0
- package/package.json +1 -1
- package/src/__tests__/plugin/entry-server-template.test.ts +50 -0
- package/src/__tests__/runtime/use-runtime-config.test.ts +40 -1
- package/src/cli/create/templates/spa/package.json.tpl +1 -1
- package/src/cli/create/templates/ssg/package.json.tpl +1 -1
- package/src/cli/create/templates/ssr/package.json.tpl +1 -1
- package/src/plugin/index.ts +13 -0
- package/src/runtime/composables/use-auth.ts +6 -0
- package/src/runtime/composables/use-cookie.ts +2 -0
- package/src/runtime/composables/use-fetch.ts +1 -0
- package/src/runtime/composables/use-head.ts +4 -0
- package/src/runtime/composables/use-locale.ts +1 -0
- package/src/runtime/composables/use-runtime-config.ts +23 -3
- package/src/runtime/composables/use-seo-meta.ts +5 -0
- package/src/runtime/composables/use-session.ts +5 -0
- package/src/runtime/entry-server-template.ts +21 -1
- package/src/runtime/isr-handler.ts +2 -0
- package/src/types/api.ts +21 -0
- package/src/types/config.ts +126 -1
- package/src/types/index.ts +1 -1
- package/src/types/middleware.ts +16 -0
- package/src/types/page.ts +58 -2
- package/src/types/plugin.ts +21 -0
- package/docs/plan-production-hardening.md +0 -1010
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http'
|
|
2
2
|
|
|
3
|
+
/** Return value of `useLocale()`. Exposes the active locale and helpers for locale-aware navigation. */
|
|
3
4
|
export interface LocaleComposable {
|
|
4
5
|
/** The active locale code for the current request / page. */
|
|
5
6
|
readonly locale: string
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/** Resolved public runtime config values. Available on both server and client. */
|
|
1
2
|
export interface RuntimeConfigPublic {
|
|
2
3
|
[key: string]: unknown
|
|
3
4
|
}
|
|
4
5
|
|
|
6
|
+
/** Resolved private runtime config values. Available on the server only — never present on the client. */
|
|
5
7
|
export interface RuntimeConfigPrivate {
|
|
6
8
|
[key: string]: string | string[]
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
/** Return value of `useRuntimeConfig()`. */
|
|
9
12
|
export interface RuntimeConfigResult {
|
|
10
13
|
public: RuntimeConfigPublic
|
|
11
14
|
private?: RuntimeConfigPrivate
|
|
@@ -36,10 +39,27 @@ export function useRuntimeConfig(): RuntimeConfigResult {
|
|
|
36
39
|
// between the composable and the virtual module.
|
|
37
40
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
41
|
const mod = (globalThis as any).__cerRuntimeConfig
|
|
39
|
-
|
|
42
|
+
const config: RuntimeConfigResult = mod ? (mod as RuntimeConfigResult) : { public: {} }
|
|
40
43
|
|
|
41
|
-
//
|
|
42
|
-
|
|
44
|
+
// In browser contexts, wrap the result in a Proxy that throws a clear,
|
|
45
|
+
// actionable error if any code attempts to read `runtimeConfig.private`.
|
|
46
|
+
// Private values are server-only secrets and are never serialized into the
|
|
47
|
+
// client bundle — accessing them client-side is always a bug.
|
|
48
|
+
if (typeof window !== 'undefined') {
|
|
49
|
+
return new Proxy(config, {
|
|
50
|
+
get(target, prop) {
|
|
51
|
+
if (prop === 'private') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'[cer-app] runtimeConfig.private is not available in the browser. ' +
|
|
54
|
+
'Move this access into a server-only loader, middleware, or API handler.',
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
return Reflect.get(target, prop)
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return config
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
/**
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useHead } from './use-head.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Convenience input for common SEO meta tags.
|
|
5
|
+
* Pass to `useSeoMeta()` — it maps each field to the correct `<meta>` or `<title>` tag automatically.
|
|
6
|
+
* All fields are optional.
|
|
7
|
+
*/
|
|
3
8
|
export interface SeoMetaInput {
|
|
4
9
|
/** Sets the document title. */
|
|
5
10
|
title?: string
|
|
@@ -3,6 +3,7 @@ import { useRuntimeConfig } from './use-runtime-config.js'
|
|
|
3
3
|
|
|
4
4
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
|
+
/** Options for `useSession()`. Controls the cookie name and expiry. */
|
|
6
7
|
export interface SessionOptions {
|
|
7
8
|
/**
|
|
8
9
|
* Cookie name for the session. Defaults to `'session'`.
|
|
@@ -14,6 +15,10 @@ export interface SessionOptions {
|
|
|
14
15
|
maxAge?: number
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* HMAC-signed cookie session returned by `useSession()`.
|
|
20
|
+
* All methods are async because signing and verification use the Web Crypto API.
|
|
21
|
+
*/
|
|
17
22
|
export interface SessionComposable<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
18
23
|
/**
|
|
19
24
|
* Reads and verifies the session cookie. Returns the session data or `null`
|
|
@@ -21,7 +21,7 @@ import layouts from 'virtual:cer-layouts'
|
|
|
21
21
|
import plugins from 'virtual:cer-plugins'
|
|
22
22
|
import apiRoutes from 'virtual:cer-server-api'
|
|
23
23
|
import serverMiddleware from 'virtual:cer-server-middleware'
|
|
24
|
-
import { runtimeConfig, _runtimePrivateDefaults, _authSessionKey } from 'virtual:cer-app-config'
|
|
24
|
+
import { runtimeConfig, _runtimePrivateDefaults, _authSessionKey, _hooks } from 'virtual:cer-app-config'
|
|
25
25
|
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
26
26
|
import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
27
27
|
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
@@ -121,6 +121,9 @@ export async function runServerMiddleware(req, res) {
|
|
|
121
121
|
})).catch(reject)
|
|
122
122
|
})
|
|
123
123
|
} catch (err) {
|
|
124
|
+
if (_hooks?.onError) {
|
|
125
|
+
try { await _hooks.onError(err, { type: 'middleware', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }
|
|
126
|
+
}
|
|
124
127
|
if (!res.writableEnded) {
|
|
125
128
|
const statusCode = (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number')
|
|
126
129
|
? (isNaN(err.status) ? 500 : err.status)
|
|
@@ -277,6 +280,9 @@ const _prepareRequest = async (req) => {
|
|
|
277
280
|
const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')
|
|
278
281
|
? err.status : 500
|
|
279
282
|
const message = (err instanceof Error) ? err.message : String(err)
|
|
283
|
+
if (_hooks?.onError) {
|
|
284
|
+
try { await _hooks.onError(err, { type: 'loader', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }
|
|
285
|
+
}
|
|
280
286
|
// P2-2: Prefer the route-level errorTag over the global one.
|
|
281
287
|
// routeErrorTag is not in scope here; use route?.meta?.errorTag from the matched route.
|
|
282
288
|
const effectiveErrorTag = route?.meta?.errorTag ?? errorTag
|
|
@@ -309,6 +315,11 @@ const _prepareRequest = async (req) => {
|
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
export const handler = async (req, res) => {
|
|
318
|
+
const _requestPath = new URL(req.url ?? '/', 'http://x').pathname
|
|
319
|
+
const _requestStart = Date.now()
|
|
320
|
+
if (_hooks?.onRequest) {
|
|
321
|
+
try { await _hooks.onRequest({ path: _requestPath, method: req.method ?? 'GET', req }) } catch { /* hooks must not crash the handler */ }
|
|
322
|
+
}
|
|
312
323
|
await _cerStateStore.run(new Map(), async () => {
|
|
313
324
|
await _cerReqStore.run({ req, res }, async () => {
|
|
314
325
|
await _cerDataStore.run(null, async () => {
|
|
@@ -406,7 +417,13 @@ export const handler = async (req, res) => {
|
|
|
406
417
|
|
|
407
418
|
// Inject DSD polyfill immediately before </body>, then close the document.
|
|
408
419
|
res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
|
|
420
|
+
if (_hooks?.onResponse) {
|
|
421
|
+
try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }
|
|
422
|
+
}
|
|
409
423
|
} catch (_renderErr) {
|
|
424
|
+
if (_hooks?.onError) {
|
|
425
|
+
try { await _hooks.onError(_renderErr, { type: 'render', path: _requestPath, req }) } catch { /* hooks must not crash the handler */ }
|
|
426
|
+
}
|
|
410
427
|
// Ensure the head collector is never left open on error.
|
|
411
428
|
if (_headCollectionOpen) { try { endHeadCollection() } catch { /* ignore */ } }
|
|
412
429
|
// If headers have not been flushed yet we can still send a proper 500 page.
|
|
@@ -418,6 +435,9 @@ export const handler = async (req, res) => {
|
|
|
418
435
|
} else {
|
|
419
436
|
res.end()
|
|
420
437
|
}
|
|
438
|
+
if (_hooks?.onResponse) {
|
|
439
|
+
try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }
|
|
440
|
+
}
|
|
421
441
|
}
|
|
422
442
|
}) // _cerAuthStore.run
|
|
423
443
|
}) // _cerFetchStore.run
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
19
19
|
|
|
20
|
+
/** A single cached SSR response stored by `createIsrHandler`. Includes the full rendered HTML, response headers, status code, and revalidation metadata. */
|
|
20
21
|
export interface IsrCacheEntry {
|
|
21
22
|
html: string
|
|
22
23
|
headers: Record<string, string>
|
|
@@ -25,6 +26,7 @@ export interface IsrCacheEntry {
|
|
|
25
26
|
revalidate: number
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** The SSR request handler signature produced by the server entry bundle (exported as `handler`). Compatible with Express, Hono, and any Node.js HTTP server. */
|
|
28
30
|
export type SsrHandlerFn = (req: IncomingMessage, res: ServerResponse) => unknown
|
|
29
31
|
|
|
30
32
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
package/src/types/api.ts
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Augmented Node.js `IncomingMessage` passed to API route handlers.
|
|
5
|
+
* Route params and query string are pre-parsed; `body` is parsed from JSON automatically.
|
|
6
|
+
*/
|
|
3
7
|
export interface ApiRequest extends IncomingMessage {
|
|
4
8
|
params: Record<string, string>
|
|
5
9
|
query: Record<string, string>
|
|
6
10
|
body: unknown
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Augmented Node.js `ServerResponse` passed to API route handlers.
|
|
15
|
+
* Adds convenience methods for JSON responses and fluent status setting.
|
|
16
|
+
*/
|
|
9
17
|
export interface ApiResponse extends ServerResponse {
|
|
10
18
|
json(data: unknown): void
|
|
11
19
|
status(code: number): ApiResponse
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Handler function for a file-based API route (`app/api/**\/*.ts`).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* // app/api/posts/[id].ts
|
|
28
|
+
* export const GET: ApiHandler = async (req, res) => {
|
|
29
|
+
* const post = await fetchPost(req.params.id)
|
|
30
|
+
* res.json(post)
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
14
34
|
export type ApiHandler = (req: ApiRequest, res: ApiResponse) => Promise<void> | void
|
|
15
35
|
|
|
36
|
+
/** Combined request/response context passed to API utility functions and composables inside API handlers. */
|
|
16
37
|
export interface ApiContext {
|
|
17
38
|
req: ApiRequest
|
|
18
39
|
res: ApiResponse
|
package/src/types/config.ts
CHANGED
|
@@ -1,7 +1,57 @@
|
|
|
1
1
|
import type { RouterConfig } from '@jasonshimmy/custom-elements-runtime/router'
|
|
2
|
+
import type { IncomingMessage } from 'node:http'
|
|
3
|
+
|
|
4
|
+
// ─── Observability hook context types ─────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context passed to the `onError` hook.
|
|
8
|
+
* `type` indicates which layer the error originated from so reporters can
|
|
9
|
+
* tag or route errors appropriately.
|
|
10
|
+
*/
|
|
11
|
+
export interface ErrorHookContext {
|
|
12
|
+
/** The layer that threw the error. */
|
|
13
|
+
type: 'loader' | 'render' | 'middleware'
|
|
14
|
+
/** The request URL pathname (e.g. `/about`). */
|
|
15
|
+
path: string
|
|
16
|
+
/** The raw Node.js incoming request. */
|
|
17
|
+
req: IncomingMessage
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Context passed to the `onRequest` hook.
|
|
22
|
+
* Fires at the start of every SSR request, before the route is matched
|
|
23
|
+
* or the loader runs.
|
|
24
|
+
*/
|
|
25
|
+
export interface RequestHookContext {
|
|
26
|
+
/** The request URL pathname (e.g. `/about`). */
|
|
27
|
+
path: string
|
|
28
|
+
/** HTTP method in upper-case (e.g. `'GET'`). */
|
|
29
|
+
method: string
|
|
30
|
+
/** The raw Node.js incoming request. */
|
|
31
|
+
req: IncomingMessage
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to the `onResponse` hook.
|
|
36
|
+
* Fires after the response has been sent (both success and error paths).
|
|
37
|
+
* Use this for latency tracking and request logging.
|
|
38
|
+
*/
|
|
39
|
+
export interface ResponseHookContext {
|
|
40
|
+
/** The request URL pathname (e.g. `/about`). */
|
|
41
|
+
path: string
|
|
42
|
+
/** HTTP method in upper-case (e.g. `'GET'`). */
|
|
43
|
+
method: string
|
|
44
|
+
/** Final HTTP status code written to the response. */
|
|
45
|
+
statusCode: number
|
|
46
|
+
/** Request duration in milliseconds from the first byte received to `res.end()`. */
|
|
47
|
+
duration: number
|
|
48
|
+
/** The raw Node.js incoming request. */
|
|
49
|
+
req: IncomingMessage
|
|
50
|
+
}
|
|
2
51
|
|
|
3
52
|
// ─── OAuth / Auth types ───────────────────────────────────────────────────────
|
|
4
53
|
|
|
54
|
+
/** Token payload returned by an OAuth provider after a successful authorization code exchange. */
|
|
5
55
|
export interface OAuthTokens {
|
|
6
56
|
accessToken: string
|
|
7
57
|
refreshToken?: string
|
|
@@ -9,6 +59,7 @@ export interface OAuthTokens {
|
|
|
9
59
|
tokenType: string
|
|
10
60
|
}
|
|
11
61
|
|
|
62
|
+
/** Configuration for a single OAuth 2.0 provider (Google, GitHub, Discord, or a custom provider). */
|
|
12
63
|
export interface OAuthProviderConfig {
|
|
13
64
|
/** OAuth application client ID. */
|
|
14
65
|
clientId: string
|
|
@@ -30,6 +81,7 @@ export interface OAuthProviderConfig {
|
|
|
30
81
|
mapUser?: (profile: Record<string, unknown>, tokens: OAuthTokens) => Record<string, unknown>
|
|
31
82
|
}
|
|
32
83
|
|
|
84
|
+
/** Authentication configuration. Enables OAuth login flows and auto-generates `/api/auth/*` routes. */
|
|
33
85
|
export interface AuthConfig {
|
|
34
86
|
/**
|
|
35
87
|
* OAuth provider configurations keyed by provider name.
|
|
@@ -53,6 +105,7 @@ export interface AuthConfig {
|
|
|
53
105
|
sessionKey?: string
|
|
54
106
|
}
|
|
55
107
|
|
|
108
|
+
/** Internationalisation routing configuration. When set, locale-aware URL routes are generated and `useLocale()` is enabled. */
|
|
56
109
|
export interface I18nConfig {
|
|
57
110
|
/**
|
|
58
111
|
* All supported locale codes. e.g. `['en', 'fr', 'de']`.
|
|
@@ -71,28 +124,46 @@ export interface I18nConfig {
|
|
|
71
124
|
strategy?: 'prefix' | 'prefix_except_default' | 'no_prefix'
|
|
72
125
|
}
|
|
73
126
|
|
|
127
|
+
/** Global Static Site Generation (SSG) configuration. Controls which routes are pre-rendered and at what concurrency. */
|
|
74
128
|
export interface SsgConfig {
|
|
129
|
+
/**
|
|
130
|
+
* Routes to pre-render.
|
|
131
|
+
* - `'auto'` (default) — pre-render every static route discovered in `app/pages/`.
|
|
132
|
+
* - `string[]` — explicit list of paths (e.g. `['/about', '/contact']`).
|
|
133
|
+
*/
|
|
75
134
|
routes?: 'auto' | string[]
|
|
135
|
+
/** Maximum number of pages rendered in parallel. Defaults to `1`. Increase for faster SSG builds at the cost of higher memory usage. */
|
|
76
136
|
concurrency?: number
|
|
77
|
-
|
|
137
|
+
/** When `true`, unenumerated routes fall back to SSR at runtime instead of returning 404. */
|
|
138
|
+
fallback?: boolean
|
|
78
139
|
}
|
|
79
140
|
|
|
141
|
+
/** JIT (Just-In-Time) CSS configuration for shadow-DOM style injection. */
|
|
80
142
|
export interface JitCssConfig {
|
|
143
|
+
/** Additional glob patterns for content files scanned by the JIT CSS engine. */
|
|
81
144
|
content?: string[]
|
|
145
|
+
/** Enable the extended color palette. Defaults to `false`. */
|
|
82
146
|
extendedColors?: boolean
|
|
83
147
|
}
|
|
84
148
|
|
|
149
|
+
/** Fine-grained control over which auto-import categories are injected into page/layout/component files. All categories are enabled by default. */
|
|
85
150
|
export interface AutoImportsConfig {
|
|
151
|
+
/** Auto-import components defined in `app/components/`. Defaults to `true`. */
|
|
86
152
|
components?: boolean
|
|
153
|
+
/** Auto-import framework composables (`useHead`, `useState`, etc.). Defaults to `true`. */
|
|
87
154
|
composables?: boolean
|
|
155
|
+
/** Auto-import template directives (`when`, `each`, `bind`). Defaults to `true`. */
|
|
88
156
|
directives?: boolean
|
|
157
|
+
/** Auto-import runtime primitives (`component`, `html`, `ref`, etc.). Defaults to `true`. */
|
|
89
158
|
runtime?: boolean
|
|
90
159
|
}
|
|
91
160
|
|
|
161
|
+
/** Arbitrary public runtime values serialized into the virtual module at build time. Available on both server and client via `useRuntimeConfig().public`. */
|
|
92
162
|
export interface RuntimePublicConfig {
|
|
93
163
|
[key: string]: unknown
|
|
94
164
|
}
|
|
95
165
|
|
|
166
|
+
/** Server-only secrets resolved from `process.env` at startup. Never serialized into the client bundle. Available via `useRuntimeConfig().private` in loaders and server middleware only. */
|
|
96
167
|
export interface RuntimePrivateConfig {
|
|
97
168
|
/**
|
|
98
169
|
* HMAC-SHA-256 signing secret(s) for `useSession()`.
|
|
@@ -119,6 +190,7 @@ export interface RuntimePrivateConfig {
|
|
|
119
190
|
[key: string]: string | string[] | undefined
|
|
120
191
|
}
|
|
121
192
|
|
|
193
|
+
/** Runtime configuration split into `public` (client + server) and `private` (server-only) sections. */
|
|
122
194
|
export interface RuntimeConfig {
|
|
123
195
|
/**
|
|
124
196
|
* Public runtime config — available on both server and client via
|
|
@@ -146,6 +218,7 @@ export interface RuntimeConfig {
|
|
|
146
218
|
private?: RuntimePrivateConfig
|
|
147
219
|
}
|
|
148
220
|
|
|
221
|
+
/** Root configuration object for `cer.config.ts`. Pass to `defineConfig()` for type-safe configuration. */
|
|
149
222
|
export interface CerAppConfig {
|
|
150
223
|
mode?: 'spa' | 'ssr' | 'ssg'
|
|
151
224
|
srcDir?: string // defaults to 'app'
|
|
@@ -218,8 +291,60 @@ export interface CerAppConfig {
|
|
|
218
291
|
* ```
|
|
219
292
|
*/
|
|
220
293
|
auth?: AuthConfig
|
|
294
|
+
/**
|
|
295
|
+
* Called when an error is caught by the framework's SSR error boundaries
|
|
296
|
+
* (loader throws, render crash, or server middleware throws). Use this to
|
|
297
|
+
* forward errors to Sentry, Datadog, or any other error-reporting service.
|
|
298
|
+
*
|
|
299
|
+
* Errors in this hook are silently swallowed so they cannot crash the request handler.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* import * as Sentry from '@sentry/node'
|
|
304
|
+
* export default defineConfig({
|
|
305
|
+
* onError(err, ctx) {
|
|
306
|
+
* Sentry.captureException(err, { tags: { type: ctx.type, path: ctx.path } })
|
|
307
|
+
* },
|
|
308
|
+
* })
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
onError?: (err: unknown, ctx: ErrorHookContext) => void | Promise<void>
|
|
312
|
+
/**
|
|
313
|
+
* Called at the start of every SSR request, before route matching and the loader.
|
|
314
|
+
* Use this for request logging or to initialise per-request APM transactions.
|
|
315
|
+
*
|
|
316
|
+
* Errors in this hook are silently swallowed.
|
|
317
|
+
*/
|
|
318
|
+
onRequest?: (ctx: RequestHookContext) => void | Promise<void>
|
|
319
|
+
/**
|
|
320
|
+
* Called after every SSR response is sent (both success and error paths).
|
|
321
|
+
* `ctx.duration` contains the elapsed milliseconds from first byte to `res.end()`.
|
|
322
|
+
* Use this for latency tracking and access logging.
|
|
323
|
+
*
|
|
324
|
+
* Errors in this hook are silently swallowed.
|
|
325
|
+
*/
|
|
326
|
+
onResponse?: (ctx: ResponseHookContext) => void | Promise<void>
|
|
221
327
|
}
|
|
222
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Define the framework configuration with full TypeScript intellisense.
|
|
331
|
+
* This is a pass-through helper — it returns `config` unchanged and exists
|
|
332
|
+
* solely to provide type checking and IDE autocompletion in `cer.config.ts`.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* // cer.config.ts
|
|
337
|
+
* import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'
|
|
338
|
+
*
|
|
339
|
+
* export default defineConfig({
|
|
340
|
+
* mode: 'ssr',
|
|
341
|
+
* runtimeConfig: {
|
|
342
|
+
* public: { apiBase: 'https://api.example.com' },
|
|
343
|
+
* private: { dbUrl: '' },
|
|
344
|
+
* },
|
|
345
|
+
* })
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
223
348
|
export function defineConfig(config: CerAppConfig): CerAppConfig {
|
|
224
349
|
return config
|
|
225
350
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig, RuntimePrivateConfig, I18nConfig } from './config.js'
|
|
1
|
+
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig, RuntimePrivateConfig, I18nConfig, ErrorHookContext, RequestHookContext, ResponseHookContext } from './config.js'
|
|
2
2
|
export { defineConfig } from './config.js'
|
|
3
3
|
export type { HydrateStrategy, SsgPathsContext, PageSsgConfig, PageMeta, PageLoaderContext, PageLoader } from './page.js'
|
|
4
4
|
export type { ApiRequest, ApiResponse, ApiHandler, ApiContext } from './api.js'
|
package/src/types/middleware.ts
CHANGED
|
@@ -40,6 +40,22 @@ export type MiddlewareFn = (
|
|
|
40
40
|
next: () => Promise<void>,
|
|
41
41
|
) => GuardResult | void
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Server middleware function. Receives the raw Node.js `req`/`res` pair and a `next`
|
|
45
|
+
* callback. Call `next()` to pass control to the next middleware in the chain,
|
|
46
|
+
* or call `next(err)` to signal an error (sets the response status from `err.status`,
|
|
47
|
+
* defaulting to 500).
|
|
48
|
+
*
|
|
49
|
+
* Defined with `defineServerMiddleware()` and placed in `app/middleware/`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* export default defineServerMiddleware((req, res, next) => {
|
|
54
|
+
* res.setHeader('X-Request-Id', crypto.randomUUID())
|
|
55
|
+
* next()
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
43
59
|
export type ServerMiddleware = (
|
|
44
60
|
req: IncomingMessage,
|
|
45
61
|
res: ServerResponse,
|
package/src/types/page.ts
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Controls when the client-side JS activates a pre-rendered page component.
|
|
5
|
+
*
|
|
6
|
+
* - `'load'` — activate immediately on page load (default).
|
|
7
|
+
* - `'idle'` — defer until `requestIdleCallback` fires (browser idle time).
|
|
8
|
+
* - `'visible'` — defer until `cer-layout-view` enters the viewport (IntersectionObserver).
|
|
9
|
+
* - `'none'` — never activate; SSR HTML is served as static markup with no JS.
|
|
10
|
+
*/
|
|
3
11
|
export type HydrateStrategy = 'load' | 'idle' | 'visible' | 'none'
|
|
4
12
|
|
|
13
|
+
/** Context object passed to a page's `ssg.paths()` function. Contains resolved route params for one path variant. */
|
|
5
14
|
export interface SsgPathsContext {
|
|
6
15
|
params: Record<string, string>
|
|
7
16
|
}
|
|
8
17
|
|
|
18
|
+
/** Per-page SSG / ISR configuration. Export as `export const meta = { ssg: { ... } }` from any page file. */
|
|
9
19
|
export interface PageSsgConfig {
|
|
20
|
+
/**
|
|
21
|
+
* Factory that returns the set of param combinations to pre-render for dynamic routes.
|
|
22
|
+
* Required for dynamic routes (e.g. `/posts/[slug]`) — without it the route is skipped
|
|
23
|
+
* during the static build.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* export const meta = {
|
|
28
|
+
* ssg: {
|
|
29
|
+
* paths: async () => {
|
|
30
|
+
* const slugs = await fetchAllSlugs()
|
|
31
|
+
* return slugs.map(slug => ({ params: { slug } }))
|
|
32
|
+
* },
|
|
33
|
+
* },
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
10
37
|
paths?: () => Promise<SsgPathsContext[]> | SsgPathsContext[]
|
|
11
38
|
/**
|
|
12
39
|
* Seconds before a cached SSR response is stale and should be re-rendered.
|
|
@@ -18,9 +45,15 @@ export interface PageSsgConfig {
|
|
|
18
45
|
revalidate?: number
|
|
19
46
|
}
|
|
20
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Per-page metadata. Export as `export const meta = { ... }` from any page file.
|
|
50
|
+
* Values are read at build time and embedded into the route manifest.
|
|
51
|
+
*/
|
|
21
52
|
export interface PageMeta {
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
/** Layout component to wrap this page. Must match a file in `app/layouts/`. Defaults to `'default'`. */
|
|
54
|
+
layout?: string
|
|
55
|
+
/** Named middleware files from `app/middleware/` to run before this route activates. */
|
|
56
|
+
middleware?: string[]
|
|
24
57
|
hydrate?: HydrateStrategy
|
|
25
58
|
ssg?: PageSsgConfig
|
|
26
59
|
/**
|
|
@@ -48,12 +81,35 @@ export interface PageMeta {
|
|
|
48
81
|
render?: 'static' | 'server' | 'spa'
|
|
49
82
|
}
|
|
50
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Context object passed to a page's `loader` function.
|
|
86
|
+
* Available on the server only — the loader runs before the page component renders.
|
|
87
|
+
*/
|
|
51
88
|
export interface PageLoaderContext<P extends Record<string, string> = Record<string, string>> {
|
|
52
89
|
params: P
|
|
53
90
|
query: Record<string, string>
|
|
54
91
|
req: IncomingMessage
|
|
55
92
|
}
|
|
56
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Server-side data loader for a page. Export as `export const loader` (or `export async function loader`)
|
|
96
|
+
* from any page file. The returned object is:
|
|
97
|
+
* - Made available via `usePageData()` inside the page component.
|
|
98
|
+
* - Primitive values are also forwarded as element attributes so `useProps()` works.
|
|
99
|
+
* - Serialized into `window.__CER_DATA__` for client-side hydration.
|
|
100
|
+
*
|
|
101
|
+
* Throwing an error (with an optional `.status` property) renders the page's error component
|
|
102
|
+
* and sets the HTTP response status code.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* export const loader: PageLoader<{ slug: string }, { post: Post }> = async ({ params }) => {
|
|
107
|
+
* const post = await fetchPost(params.slug)
|
|
108
|
+
* if (!post) throw Object.assign(new Error('Not found'), { status: 404 })
|
|
109
|
+
* return { post }
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
57
113
|
export type PageLoader<
|
|
58
114
|
P extends Record<string, string> = Record<string, string>,
|
|
59
115
|
D = Record<string, unknown>,
|
package/src/types/plugin.ts
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import type { Router } from '@jasonshimmy/custom-elements-runtime/router'
|
|
2
2
|
import type { CerAppConfig } from './config.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Application context passed to each plugin's `setup()` function.
|
|
6
|
+
* Use it to provide values to the component tree, access the router,
|
|
7
|
+
* or read the resolved config.
|
|
8
|
+
*/
|
|
4
9
|
export interface AppContext {
|
|
5
10
|
provide(key: PropertyKey, value: unknown): void
|
|
6
11
|
router: Router
|
|
7
12
|
config: CerAppConfig
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A framework plugin. Plugins run once at app startup (both server and client) before
|
|
17
|
+
* the first route renders. Use them for global setup: registering provide/inject values,
|
|
18
|
+
* subscribing to router events, or initialising third-party libraries.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // app/plugins/analytics.ts
|
|
23
|
+
* export default {
|
|
24
|
+
* name: 'analytics',
|
|
25
|
+
* async setup({ router }) {
|
|
26
|
+
* router.subscribe(({ path }) => trackPageView(path))
|
|
27
|
+
* },
|
|
28
|
+
* } satisfies AppPlugin
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
10
31
|
export interface AppPlugin {
|
|
11
32
|
name: string
|
|
12
33
|
setup(app: AppContext): void | Promise<void>
|