@jasonshimmy/vite-plugin-cer-app 0.20.3 → 0.20.5
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 +8 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +5 -0
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +0 -11
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/content/index.d.ts +5 -4
- package/dist/plugin/content/index.d.ts.map +1 -1
- package/dist/plugin/content/index.js +9 -11
- package/dist/plugin/content/index.js.map +1 -1
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js +40 -2
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +9 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +7 -0
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +11 -0
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/runtime/app-template.d.ts +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +48 -21
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/use-page-data.d.ts +0 -41
- package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
- package/dist/runtime/composables/use-page-data.js +44 -20
- package/dist/runtime/composables/use-page-data.js.map +1 -1
- package/dist/runtime/entry-client-template.d.ts +1 -1
- package/dist/runtime/entry-client-template.d.ts.map +1 -1
- package/dist/runtime/entry-client-template.js +10 -0
- package/dist/runtime/entry-client-template.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 +30 -9
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/e2e/cypress/e2e/group-meta.cy.ts +5 -2
- package/e2e/cypress/e2e/preview-hardening.cy.ts +42 -33
- package/e2e/cypress/e2e/use-page-data.cy.ts +122 -0
- package/e2e/kitchen-sink/app/pages/blog/[slug].ts +4 -0
- package/e2e/kitchen-sink/app/pages/blog/index.ts +5 -0
- package/package.json +5 -2
- package/src/__tests__/plugin/build-ssg.test.ts +2 -2
- package/src/__tests__/plugin/content/loader.test.ts +19 -27
- package/src/__tests__/plugin/entry-server-template.test.ts +11 -3
- package/src/__tests__/plugin/virtual/routes.test.ts +26 -0
- package/src/__tests__/runtime/app-template.test.ts +25 -13
- package/src/__tests__/runtime/entry-client-template.test.ts +7 -1
- package/src/__tests__/runtime/use-page-data.test.ts +178 -1
- package/src/cli/commands/dev.ts +5 -0
- package/src/plugin/build-ssg.ts +0 -12
- package/src/plugin/content/index.ts +8 -11
- package/src/plugin/dev-server.ts +37 -2
- package/src/plugin/dts-generator.ts +7 -1
- package/src/plugin/index.ts +7 -0
- package/src/plugin/virtual/routes.ts +11 -0
- package/src/runtime/app-template.ts +48 -21
- package/src/runtime/composables/use-page-data.ts +50 -20
- package/src/runtime/entry-client-template.ts +10 -0
- package/src/runtime/entry-server-template.ts +30 -9
|
@@ -5,20 +5,22 @@
|
|
|
5
5
|
* and makes the result available in two ways:
|
|
6
6
|
*
|
|
7
7
|
* 1. **Server render pass** — the data is stored in a per-request
|
|
8
|
-
* `AsyncLocalStorage` context (set via `_cerDataStore.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* `AsyncLocalStorage` context (set via `_cerDataStore.run(data, ...)` in the
|
|
9
|
+
* entry-server template). `usePageData()` reads this store so the component
|
|
10
|
+
* renders with real data in the initial SSR/SSG HTML.
|
|
11
11
|
*
|
|
12
12
|
* 2. **Client hydration** — the server also serializes the data as
|
|
13
|
-
* `window.__CER_DATA__` in the page `<head>`. On first component
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* `window.__CER_DATA__` in the page `<head>`. On first component render
|
|
14
|
+
* `usePageData()` returns that value, caches it on the component context, and
|
|
15
|
+
* returns the same cached value on all subsequent re-renders of that component
|
|
16
|
+
* instance. The data remains available for the initial hydrated route until
|
|
17
|
+
* the next client-side navigation clears it before loading new page data.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* **Why caching is necessary**: the render function passed to `component()` is
|
|
20
|
+
* called on every re-render (it is the render function, not a setup function).
|
|
21
|
+
* Without caching, `usePageData()` could return `null` on later re-renders if a
|
|
22
|
+
* browser upgrades or re-renders the hydrated page after the initial read,
|
|
23
|
+
* flip the `ssrData` guard from truthy → null, and re-trigger client-side fetches.
|
|
22
24
|
*
|
|
23
25
|
* @returns The serialized loader result, or `null` if no SSR data is present.
|
|
24
26
|
*
|
|
@@ -39,6 +41,11 @@
|
|
|
39
41
|
* })
|
|
40
42
|
* ```
|
|
41
43
|
*/
|
|
44
|
+
import { getCurrentComponentContext } from '@jasonshimmy/custom-elements-runtime'
|
|
45
|
+
|
|
46
|
+
// Key used to cache the page data on the component context object across re-renders.
|
|
47
|
+
const _PAGE_DATA_KEY = '_cerPageData'
|
|
48
|
+
|
|
42
49
|
export function usePageData<T = unknown>(): T | null {
|
|
43
50
|
const g = globalThis as Record<string, unknown>
|
|
44
51
|
|
|
@@ -51,13 +58,36 @@ export function usePageData<T = unknown>(): T | null {
|
|
|
51
58
|
if (ssrData !== undefined && ssrData !== null) return ssrData
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
// Client-side:
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
61
|
+
// Client-side: check the component context cache first.
|
|
62
|
+
// getCurrentComponentContext() returns the context object for the component
|
|
63
|
+
// whose renderFn is currently executing (set by the component() runtime
|
|
64
|
+
// before calling renderFn). Caching on the context ensures the same value is
|
|
65
|
+
// returned on every re-render of the same element instance even if the
|
|
66
|
+
// browser performs a later hydration-time re-render.
|
|
67
|
+
const ctx = getCurrentComponentContext() as Record<string, unknown> | null
|
|
68
|
+
if (ctx) {
|
|
69
|
+
if (_PAGE_DATA_KEY in ctx) {
|
|
70
|
+
return ctx[_PAGE_DATA_KEY] as T | null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Read the raw value from the global set by the server-rendered <script> tag.
|
|
75
|
+
const data = g['__CER_DATA__'] as T | undefined | null
|
|
76
|
+
const result: T | null = (data === undefined || data === null) ? null : data
|
|
77
|
+
|
|
78
|
+
// Cache on the component context so subsequent re-renders of this element
|
|
79
|
+
// instance return the same value without reading __CER_DATA__ again.
|
|
80
|
+
// Use Object.defineProperty to bypass the reactive Proxy set-trap on context,
|
|
81
|
+
// which would otherwise schedule a spurious re-render (and risk an infinite loop)
|
|
82
|
+
// every time we first cache the value.
|
|
83
|
+
if (ctx) {
|
|
84
|
+
Object.defineProperty(ctx, _PAGE_DATA_KEY, {
|
|
85
|
+
value: result,
|
|
86
|
+
writable: false,
|
|
87
|
+
enumerable: false,
|
|
88
|
+
configurable: true,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
63
93
|
}
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const ENTRY_CLIENT_TEMPLATE = `// Client-side entry — hydrates SSR output
|
|
8
8
|
|
|
9
|
+
// Mark the already-rendered entry URL so plugin-generated route middleware can
|
|
10
|
+
// skip replaying guards once during hydration. This is cleared by the app
|
|
11
|
+
// bootstrap after the initial hydrated route commits.
|
|
12
|
+
if (typeof window !== 'undefined') {
|
|
13
|
+
(globalThis).__CER_HYDRATION_ENTRY__ = {
|
|
14
|
+
path: window.location.pathname,
|
|
15
|
+
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
// Capture SSR loader data before any scripts clear it.
|
|
10
20
|
// usePageData() reads from this global; it is cleared after the first read
|
|
11
21
|
// so subsequent client-side navigations don't reuse stale data.
|
|
@@ -214,8 +214,10 @@ function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
|
|
|
214
214
|
|
|
215
215
|
// Per-request async setup: initialize a fresh router, resolve the matched
|
|
216
216
|
// route and layout, pre-load the page module, and call the data loader.
|
|
217
|
-
// Loader data is
|
|
218
|
-
//
|
|
217
|
+
// Loader data is returned so the handler can scope it to _cerDataStore.run()
|
|
218
|
+
// during rendering. (AsyncLocalStorage.enterWith() inside an awaited child
|
|
219
|
+
// function does not propagate back to the parent continuation, so run() is
|
|
220
|
+
// the only reliable approach.)
|
|
219
221
|
const _prepareRequest = async (req) => {
|
|
220
222
|
await _pluginsReady
|
|
221
223
|
const router = initRouter({ routes, initialUrl: req.url ?? '/' })
|
|
@@ -236,6 +238,9 @@ const _prepareRequest = async (req) => {
|
|
|
236
238
|
// and breaks Declarative Shadow DOM on initial parse).
|
|
237
239
|
let pageVnode = { tag: 'div', props: {}, children: [] }
|
|
238
240
|
let head
|
|
241
|
+
// Loader data to pass to usePageData() during rendering. Declared here
|
|
242
|
+
// (outside try/catch) so it's visible in all return paths.
|
|
243
|
+
let loaderData = null
|
|
239
244
|
if (route?.load) {
|
|
240
245
|
try {
|
|
241
246
|
const mod = await route.load()
|
|
@@ -262,9 +267,11 @@ const _prepareRequest = async (req) => {
|
|
|
262
267
|
const query = current.query ?? {}
|
|
263
268
|
const data = await mod.loader({ params, query, req })
|
|
264
269
|
if (data !== undefined && data !== null) {
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
270
|
+
// Store loader data so the handler can pass it to _cerDataStore.run()
|
|
271
|
+
// below. Using enterWith() here doesn't work because it only modifies
|
|
272
|
+
// the async context *inside* this awaited function, not the outer
|
|
273
|
+
// handler's continuation where renderToStreamWithJITCSSDSD runs.
|
|
274
|
+
loaderData = data
|
|
268
275
|
head = \`<script>window.__CER_DATA__ = \${JSON.stringify(data)}</script>\`
|
|
269
276
|
// Expose primitive loader values as element attributes so useProps()
|
|
270
277
|
// can read them. Complex objects are only accessible via usePageData().
|
|
@@ -311,7 +318,7 @@ const _prepareRequest = async (req) => {
|
|
|
311
318
|
// If the request matched a catch-all route (user-defined 404.ts or [...all].ts),
|
|
312
319
|
// return HTTP 404 so browsers and crawlers treat it as a not-found response.
|
|
313
320
|
const isCatchAll = route?.path === '/:all*'
|
|
314
|
-
return { vnode, router, head, status: isCatchAll ? 404 : null }
|
|
321
|
+
return { vnode, router, head, status: isCatchAll ? 404 : null, loaderData }
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
export const handler = async (req, res) => {
|
|
@@ -332,10 +339,17 @@ export const handler = async (req, res) => {
|
|
|
332
339
|
try { _authUser = await useSession({ name: _authSessionKey }).get() } catch { /* no session secret */ }
|
|
333
340
|
}
|
|
334
341
|
await _cerAuthStore.run(_authUser, async () => {
|
|
335
|
-
const { vnode, router, head, status } = await _prepareRequest(req)
|
|
342
|
+
const { vnode, router, head, status, loaderData } = await _prepareRequest(req)
|
|
336
343
|
if (status != null) res.statusCode = status
|
|
337
344
|
|
|
338
345
|
let _headCollectionOpen = false
|
|
346
|
+
// Wrap the entire render pass in _cerDataStore.run(loaderData) so that
|
|
347
|
+
// usePageData() inside component renderFn calls sees the correct store
|
|
348
|
+
// value. AsyncLocalStorage.enterWith() inside _prepareRequest does NOT
|
|
349
|
+
// propagate back to this outer async continuation — it only affects the
|
|
350
|
+
// async chain inside _prepareRequest itself. Using run() here is the only
|
|
351
|
+
// reliable way to scope the data store to the synchronous render pass.
|
|
352
|
+
await _cerDataStore.run(loaderData ?? null, async () => {
|
|
339
353
|
try {
|
|
340
354
|
// Begin collecting useHead() calls made during the synchronous render pass.
|
|
341
355
|
// IMPORTANT: the stream's start() function runs synchronously on construction,
|
|
@@ -394,8 +408,14 @@ export const handler = async (req, res) => {
|
|
|
394
408
|
// (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
|
|
395
409
|
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${firstChunk}</body></html>\`
|
|
396
410
|
|
|
397
|
-
|
|
398
|
-
|
|
411
|
+
// In dev mode the module-level _clientTemplate is null (only the
|
|
412
|
+
// production dist/client/index.html path is searched at init time).
|
|
413
|
+
// The dev server sets (globalThis).__CER_CLIENT_TEMPLATE__ per-request
|
|
414
|
+
// after running server.transformIndexHtml so the Vite client scripts
|
|
415
|
+
// (/@vite/client, HMR) are included in every SSR response.
|
|
416
|
+
const _resolvedClientTemplate = (globalThis).__CER_CLIENT_TEMPLATE__ ?? _clientTemplate
|
|
417
|
+
const merged = _resolvedClientTemplate
|
|
418
|
+
? _mergeWithClientTemplate(ssrHtml, _resolvedClientTemplate)
|
|
399
419
|
: ssrHtml
|
|
400
420
|
|
|
401
421
|
// Split at </body> so async swap scripts and the DSD polyfill can be streamed
|
|
@@ -439,6 +459,7 @@ export const handler = async (req, res) => {
|
|
|
439
459
|
try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }
|
|
440
460
|
}
|
|
441
461
|
}
|
|
462
|
+
}) // _cerDataStore.run(loaderData)
|
|
442
463
|
}) // _cerAuthStore.run
|
|
443
464
|
}) // _cerFetchStore.run
|
|
444
465
|
}) // _cerDataStore.run
|