@jasonshimmy/vite-plugin-cer-app 0.23.1 → 0.23.2

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 (48) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/commands/preview.d.ts.map +1 -1
  4. package/dist/cli/commands/preview.js +37 -4
  5. package/dist/cli/commands/preview.js.map +1 -1
  6. package/dist/plugin/build-ssg.d.ts +4 -2
  7. package/dist/plugin/build-ssg.d.ts.map +1 -1
  8. package/dist/plugin/build-ssg.js +55 -5
  9. package/dist/plugin/build-ssg.js.map +1 -1
  10. package/dist/plugin/dev-server.d.ts +2 -0
  11. package/dist/plugin/dev-server.d.ts.map +1 -1
  12. package/dist/plugin/dev-server.js.map +1 -1
  13. package/dist/plugin/generated-dir.js +1 -1
  14. package/dist/plugin/generated-dir.js.map +1 -1
  15. package/dist/plugin/html-post-process.d.ts +29 -0
  16. package/dist/plugin/html-post-process.d.ts.map +1 -0
  17. package/dist/plugin/html-post-process.js +88 -0
  18. package/dist/plugin/html-post-process.js.map +1 -0
  19. package/dist/plugin/index.d.ts +9 -0
  20. package/dist/plugin/index.d.ts.map +1 -1
  21. package/dist/plugin/index.js +30 -2
  22. package/dist/plugin/index.js.map +1 -1
  23. package/dist/runtime/app-template.d.ts +1 -4
  24. package/dist/runtime/app-template.d.ts.map +1 -1
  25. package/dist/runtime/app-template.js +14 -13
  26. package/dist/runtime/app-template.js.map +1 -1
  27. package/dist/runtime/composables/use-head.d.ts.map +1 -1
  28. package/dist/runtime/composables/use-head.js +30 -6
  29. package/dist/runtime/composables/use-head.js.map +1 -1
  30. package/dist/types/config.d.ts +8 -0
  31. package/dist/types/config.d.ts.map +1 -1
  32. package/dist/types/config.js.map +1 -1
  33. package/docs/configuration.md +29 -0
  34. package/package.json +1 -1
  35. package/src/__tests__/plugin/app-template.test.ts +72 -18
  36. package/src/__tests__/plugin/html-post-process.test.ts +146 -0
  37. package/src/__tests__/plugin/resolve-config.test.ts +33 -0
  38. package/src/__tests__/runtime/app-template.test.ts +10 -0
  39. package/src/__tests__/runtime/use-head.test.ts +45 -0
  40. package/src/cli/commands/preview.ts +38 -4
  41. package/src/plugin/build-ssg.ts +72 -5
  42. package/src/plugin/dev-server.ts +2 -0
  43. package/src/plugin/generated-dir.ts +1 -1
  44. package/src/plugin/html-post-process.ts +96 -0
  45. package/src/plugin/index.ts +33 -2
  46. package/src/runtime/app-template.ts +14 -17
  47. package/src/runtime/composables/use-head.ts +28 -6
  48. package/src/types/config.ts +8 -0
@@ -4,23 +4,18 @@
4
4
  * Always written to `.cer/app.ts` on every dev/build so consumers
5
5
  * automatically receive the latest bootstrap code on plugin update.
6
6
  * This file is gitignored and should never be edited directly.
7
- *
8
- * @param customColors - Project-specific color families to register in the JIT
9
- * CSS engine at runtime. Serialized as a JSON literal in the generated source.
10
7
  */
11
- export function generateAppEntryTemplate(
12
- customColors?: Record<string, Record<string, string>>,
13
- ): string {
14
- const enableJITCSSCall =
15
- customColors && Object.keys(customColors).length > 0
16
- ? `enableJITCSS({ customColors: ${JSON.stringify(customColors)} })`
17
- : `enableJITCSS()`
18
-
8
+ export function generateAppEntryTemplate(): string {
19
9
  return `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app — do not edit.
20
10
  // Regenerated automatically on every dev server start and build.
21
11
 
22
12
  import '@jasonshimmy/custom-elements-runtime/css'
23
13
  import 'virtual:cer-jit-css'
14
+ // virtual:cer-jit-init must run before virtual:cer-layouts and virtual:cer-plugins
15
+ // so JIT CSS is enabled before those modules upgrade custom elements via
16
+ // customElements.define(). Static imports execute depth-first in module order,
17
+ // so placing this import here guarantees enableJITCSS() fires first.
18
+ import 'virtual:cer-jit-init'
24
19
  import 'virtual:cer-content-components'
25
20
  import routes from 'virtual:cer-routes'
26
21
  import layouts from 'virtual:cer-layouts'
@@ -38,7 +33,6 @@ import {
38
33
  registerBuiltinComponents,
39
34
  } from '@jasonshimmy/custom-elements-runtime'
40
35
  import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
41
- import { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'
42
36
  import { initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
43
37
 
44
38
  const _cerProcess = (globalThis).process
@@ -57,7 +51,6 @@ const _cerRuntimeDev =
57
51
 
58
52
  registerBuiltinComponents()
59
53
  setDevMode(_cerRuntimeDev)
60
- ${enableJITCSSCall}
61
54
  initRuntimeConfig(runtimeConfig)
62
55
 
63
56
  const router = initRouter({ routes })
@@ -217,9 +210,13 @@ component('cer-layout-view', () => {
217
210
  // store.subscribe() synchronously pushes the current route once on
218
211
  // subscription. That initial callback is not a real navigation and must not
219
212
  // tear down the SSR slot before _doHydrate has pre-loaded the page.
220
- // Subsequent callbacks are real navigations (including the initial _replace
221
- // in _doHydrate), so if they arrive during the hydration gap we drop the
222
- // slot immediately and let the live render take over.
213
+ // Subsequent callbacks may be real navigations, but initRouter() also fires
214
+ // a queueMicrotask(() => navigate(...)) to run guards on the entry URL.
215
+ // That startup microtask resolves during the await route.load() gap
216
+ // before _currentPageTag is set — so we must not drop the slot until the
217
+ // page module has actually been loaded (_currentPageTag !== null).
218
+ // The explicit _cerHydrating.value = false in _doHydrate() fires after
219
+ // _loadPageForPath completes and is the authoritative hydration trigger.
223
220
  unsub = router.subscribe((s) => {
224
221
  const _isInitialSubscribePush = !_sawInitialRouteState
225
222
  _sawInitialRouteState = true
@@ -227,7 +224,7 @@ component('cer-layout-view', () => {
227
224
  current.value = s
228
225
  return
229
226
  }
230
- if (_cerHydrating.value) _cerHydrating.value = false
227
+ if (_cerHydrating.value && _currentPageTag !== null) _cerHydrating.value = false
231
228
  current.value = s
232
229
  })
233
230
  })
@@ -152,7 +152,14 @@ export function useHead(input: HeadInput): void {
152
152
  const rel = link.rel
153
153
  const href = link.href
154
154
  if (rel && href) {
155
- let el = document.querySelector(`link[rel="${rel}"][href="${href}"]`)
155
+ // Canonical and icon links are singletons — find by rel alone so that
156
+ // updating the URL overwrites the existing tag rather than adding a
157
+ // duplicate. All other link types are matched by rel+href (e.g.
158
+ // stylesheets, where multiple hrefs are valid).
159
+ const isSingleton = rel === 'canonical' || rel === 'icon' || rel === 'shortcut icon'
160
+ let el = isSingleton
161
+ ? document.querySelector(`link[rel="${rel}"]`)
162
+ : document.querySelector(`link[rel="${rel}"][href="${href}"]`)
156
163
  if (!el) {
157
164
  el = document.createElement('link')
158
165
  document.head.appendChild(el)
@@ -176,12 +183,27 @@ export function useHead(input: HeadInput): void {
176
183
  }
177
184
  document.head.appendChild(el)
178
185
  } else if (innerHTML && !src) {
179
- const el = document.createElement('script')
180
- el.textContent = innerHTML
181
- for (const [k, v] of Object.entries(rest)) {
182
- el.setAttribute(k, v)
186
+ const type = rest.type ?? ''
187
+ if (type === 'application/ld+json') {
188
+ // JSON-LD is a singleton update existing content rather than
189
+ // appending a duplicate on re-render or SPA navigation.
190
+ let el = document.querySelector('script[type="application/ld+json"]')
191
+ if (!el) {
192
+ el = document.createElement('script')
193
+ document.head.appendChild(el)
194
+ for (const [k, v] of Object.entries(rest)) {
195
+ el.setAttribute(k, v)
196
+ }
197
+ }
198
+ ;(el as HTMLScriptElement).textContent = innerHTML
199
+ } else {
200
+ const el = document.createElement('script')
201
+ el.textContent = innerHTML
202
+ for (const [k, v] of Object.entries(rest)) {
203
+ el.setAttribute(k, v)
204
+ }
205
+ document.head.appendChild(el)
183
206
  }
184
- document.head.appendChild(el)
185
207
  }
186
208
  }
187
209
  }
@@ -241,6 +241,14 @@ export interface RuntimeConfig {
241
241
  export interface CerAppConfig {
242
242
  mode?: 'spa' | 'ssr' | 'ssg'
243
243
  srcDir?: string // defaults to 'app'
244
+ /**
245
+ * The canonical origin of the site (e.g. `'https://example.com'`).
246
+ * No trailing slash. When set, the SSG build automatically:
247
+ * - Injects `<link rel="canonical" href="${siteUrl}${path}">` into every page.
248
+ * - Writes a `robots.txt` to `dist/` (skipped when a `public/robots.txt` already exists).
249
+ * Also available at runtime via `useRuntimeConfig().public.siteUrl`.
250
+ */
251
+ siteUrl?: string
244
252
  /** File-based content layer configuration. Reads from `content/` at the project root by default. */
245
253
  content?: import('./content.js').CerContentConfig
246
254
  /**