@jasonshimmy/vite-plugin-cer-app 0.20.3 → 0.20.4

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/commands/dev.d.ts.map +1 -1
  4. package/dist/cli/commands/dev.js +5 -0
  5. package/dist/cli/commands/dev.js.map +1 -1
  6. package/dist/plugin/build-ssg.d.ts.map +1 -1
  7. package/dist/plugin/build-ssg.js +0 -11
  8. package/dist/plugin/build-ssg.js.map +1 -1
  9. package/dist/plugin/content/index.d.ts +5 -4
  10. package/dist/plugin/content/index.d.ts.map +1 -1
  11. package/dist/plugin/content/index.js +9 -11
  12. package/dist/plugin/content/index.js.map +1 -1
  13. package/dist/plugin/dev-server.d.ts.map +1 -1
  14. package/dist/plugin/dev-server.js +40 -2
  15. package/dist/plugin/dev-server.js.map +1 -1
  16. package/dist/plugin/dts-generator.d.ts.map +1 -1
  17. package/dist/plugin/dts-generator.js +9 -1
  18. package/dist/plugin/dts-generator.js.map +1 -1
  19. package/dist/plugin/index.d.ts.map +1 -1
  20. package/dist/plugin/index.js +7 -0
  21. package/dist/plugin/index.js.map +1 -1
  22. package/dist/runtime/entry-server-template.d.ts +1 -1
  23. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  24. package/dist/runtime/entry-server-template.js +8 -2
  25. package/dist/runtime/entry-server-template.js.map +1 -1
  26. package/e2e/cypress/e2e/preview-hardening.cy.ts +42 -33
  27. package/e2e/cypress/e2e/use-page-data.cy.ts +122 -0
  28. package/e2e/kitchen-sink/app/pages/blog/[slug].ts +4 -0
  29. package/e2e/kitchen-sink/app/pages/blog/index.ts +5 -0
  30. package/package.json +5 -2
  31. package/src/__tests__/plugin/build-ssg.test.ts +2 -2
  32. package/src/__tests__/plugin/content/loader.test.ts +19 -27
  33. package/src/__tests__/plugin/entry-server-template.test.ts +4 -1
  34. package/src/cli/commands/dev.ts +5 -0
  35. package/src/plugin/build-ssg.ts +0 -12
  36. package/src/plugin/content/index.ts +8 -11
  37. package/src/plugin/dev-server.ts +37 -2
  38. package/src/plugin/dts-generator.ts +7 -1
  39. package/src/plugin/index.ts +7 -0
  40. package/src/runtime/entry-server-template.ts +8 -2
@@ -24,14 +24,14 @@ export function resolveContentDir(root: string, contentConfig?: CerContentConfig
24
24
 
25
25
  /**
26
26
  * Loads all content files from `contentDir`, parses them concurrently, and
27
- * returns the full `ContentItem[]`. Excludes drafts in production unless
28
- * `drafts: true`. Uses async I/O + `Promise.all` for concurrent disk reads,
29
- * which is significantly faster than sequential `readFileSync` at 10k+ pages.
27
+ * returns the full `ContentItem[]`. Excludes drafts unless `includeDrafts: true`
28
+ * is set in the content config. Uses async I/O + `Promise.all` for concurrent
29
+ * disk reads, which is significantly faster than sequential `readFileSync` at
30
+ * 10k+ pages.
30
31
  */
31
32
  export async function loadContentStore(
32
33
  contentDir: string,
33
34
  isDraft: boolean,
34
- isProduction: boolean,
35
35
  ): Promise<ContentItem[]> {
36
36
  if (!existsSync(contentDir)) return []
37
37
 
@@ -41,8 +41,8 @@ export async function loadContentStore(
41
41
  files.map(async (file) => {
42
42
  try {
43
43
  const item = await parseContentFileAsync(file, contentDir)
44
- // Skip drafts in production (unless drafts flag is set)
45
- if (isProduction && !isDraft && item.draft === true) return null
44
+ // Skip drafts unless the user explicitly opted in via drafts: true
45
+ if (!isDraft && item.draft === true) return null
46
46
  return item
47
47
  } catch (err) {
48
48
  // Warn and skip unparseable / invalid files so one bad file does not
@@ -175,8 +175,7 @@ export function cerContent(
175
175
  },
176
176
 
177
177
  async buildStart() {
178
- const isProduction = this.meta.watchMode === false
179
- const items = await loadContentStore(_resolvedContentDir, includeDrafts, isProduction)
178
+ const items = await loadContentStore(_resolvedContentDir, includeDrafts)
180
179
  const g = globalThis as Record<string, unknown>
181
180
  g[CONTENT_STORE_KEY] = items
182
181
  },
@@ -225,9 +224,7 @@ export function cerContent(
225
224
  }
226
225
 
227
226
  async function refreshStore(contentDir: string, includeDrafts: boolean): Promise<void> {
228
- // HMR runs in dev (watchMode=true) — use isProduction=false so draft items
229
- // remain visible, matching the initial buildStart behaviour in dev mode.
230
- const items = await loadContentStore(contentDir, includeDrafts, false)
227
+ const items = await loadContentStore(contentDir, includeDrafts)
231
228
  const g = globalThis as Record<string, unknown>
232
229
  g[CONTENT_STORE_KEY] = items
233
230
  // Invalidate the dev middleware caches so the next request rebuilds manifest
@@ -1,5 +1,7 @@
1
1
  import type { ViteDevServer } from 'vite'
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
3
5
  import { join } from 'pathe'
4
6
  import { getGeneratedDir } from './generated-dir.js'
5
7
 
@@ -246,7 +248,7 @@ export function configureCerDevServer(
246
248
  const acceptsHtml =
247
249
  (req.headers['accept'] ?? '').includes('text/html') ||
248
250
  url === '/' ||
249
- (!url.includes('.') && !url.startsWith('/api/'))
251
+ (!url.includes('.') && !url.startsWith('/api/') && !url.startsWith('/@'))
250
252
 
251
253
  if (acceptsHtml) {
252
254
  // Check per-route render mode — skip SSR for 'spa' routes.
@@ -257,6 +259,20 @@ export function configureCerDevServer(
257
259
  for (const route of pageRoutes) {
258
260
  if (_matchDevRoute(route.path, urlPathOnly)) {
259
261
  if (route.meta?.render === 'spa') {
262
+ // In SSR/SSG dev mode, a route with render:'spa' should be served as
263
+ // the SPA shell (no server rendering). Serve .cer/index.html so the
264
+ // client bundle boots and handles the route client-side.
265
+ const _userHtml = resolve(config.root, 'index.html')
266
+ const _cerHtml = join(getGeneratedDir(config.root), 'index.html')
267
+ const _spaSrcPath = existsSync(_userHtml) ? _userHtml : _cerHtml
268
+ if (existsSync(_spaSrcPath)) {
269
+ const rawHtml = readFileSync(_spaSrcPath, 'utf-8')
270
+ const transformed = await server.transformIndexHtml(url, rawHtml)
271
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
272
+ res.statusCode = 200
273
+ res.end(transformed)
274
+ return
275
+ }
260
276
  next()
261
277
  return
262
278
  }
@@ -276,7 +292,26 @@ export function configureCerDevServer(
276
292
  ssrEntry.handler ?? ssrEntry.default?.handler
277
293
 
278
294
  if (typeof handler === 'function') {
279
- await handler(req, res)
295
+ // In dev mode _clientTemplate inside entry-server.ts is null because
296
+ // the dist/client/index.html path doesn't exist. Set the global that
297
+ // the handler reads per-request so the SSR response includes the
298
+ // Vite client scripts (/@vite/client, HMR, module imports for app.ts).
299
+ const _userIndexPath = resolve(config.root, 'index.html')
300
+ const _genIndexPath = join(getGeneratedDir(config.root), 'index.html')
301
+ const _rawHtml = existsSync(_userIndexPath)
302
+ ? readFileSync(_userIndexPath, 'utf-8')
303
+ : existsSync(_genIndexPath)
304
+ ? readFileSync(_genIndexPath, 'utf-8')
305
+ : null
306
+ if (_rawHtml) {
307
+ ;(globalThis as Record<string, unknown>).__CER_CLIENT_TEMPLATE__ =
308
+ await server.transformIndexHtml(url, _rawHtml)
309
+ }
310
+ try {
311
+ await handler(req, res)
312
+ } finally {
313
+ ;(globalThis as Record<string, unknown>).__CER_CLIENT_TEMPLATE__ = undefined
314
+ }
280
315
  return
281
316
  }
282
317
 
@@ -40,7 +40,13 @@ export function writeTsconfigPaths(root: string, srcDir: string): void {
40
40
  }
41
41
 
42
42
  const content = JSON.stringify(tsconfig, null, 2) + '\n'
43
- writeFileSync(join(cerDir, 'tsconfig.json'), content, 'utf-8')
43
+ const tsconfigPath = join(cerDir, 'tsconfig.json')
44
+ // Skip write when content is unchanged to avoid triggering Vite's tsconfig
45
+ // watcher, which would cause an unnecessary server restart on every dev start.
46
+ try {
47
+ if (existsSync(tsconfigPath) && readFileSync(tsconfigPath, 'utf-8') === content) return
48
+ } catch { /* proceed to write */ }
49
+ writeFileSync(tsconfigPath, content, 'utf-8')
44
50
  }
45
51
 
46
52
  const RUNTIME_GLOBALS = [
@@ -365,6 +365,13 @@ export function cerApp(userConfig: CerAppConfig = {}): Plugin[] {
365
365
  if (!existsSync(userHtml)) {
366
366
  const cerHtmlPath = join(getGeneratedDir(config.root), 'index.html')
367
367
  server.middlewares.use(async (req, res, next) => {
368
+ // In SSR/SSG mode, HTML requests must fall through to configureCerDevServer
369
+ // so the SSR handler can run loaders and inject __CER_DATA__ into the response.
370
+ // Only SPA mode (no server rendering) should serve the raw SPA shell here.
371
+ if (config.mode === 'ssr' || config.mode === 'ssg') {
372
+ next()
373
+ return
374
+ }
368
375
  const url = (req as { url?: string }).url ?? '/'
369
376
  const isHtmlRequest =
370
377
  url === '/' ||
@@ -394,8 +394,14 @@ export const handler = async (req, res) => {
394
394
  // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
395
395
  const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${firstChunk}</body></html>\`
396
396
 
397
- const merged = _clientTemplate
398
- ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
397
+ // In dev mode the module-level _clientTemplate is null (only the
398
+ // production dist/client/index.html path is searched at init time).
399
+ // The dev server sets (globalThis).__CER_CLIENT_TEMPLATE__ per-request
400
+ // after running server.transformIndexHtml so the Vite client scripts
401
+ // (/@vite/client, HMR) are included in every SSR response.
402
+ const _resolvedClientTemplate = (globalThis).__CER_CLIENT_TEMPLATE__ ?? _clientTemplate
403
+ const merged = _resolvedClientTemplate
404
+ ? _mergeWithClientTemplate(ssrHtml, _resolvedClientTemplate)
399
405
  : ssrHtml
400
406
 
401
407
  // Split at </body> so async swap scripts and the DSD polyfill can be streamed