@jasonshimmy/vite-plugin-cer-app 0.23.2 → 0.24.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.
Files changed (392) hide show
  1. package/dist/cli/checks/lighthouse.d.ts +26 -0
  2. package/dist/cli/checks/lighthouse.d.ts.map +1 -0
  3. package/dist/cli/checks/lighthouse.js +121 -0
  4. package/dist/cli/checks/lighthouse.js.map +1 -0
  5. package/dist/cli/checks/links.d.ts +12 -0
  6. package/dist/cli/checks/links.d.ts.map +1 -0
  7. package/dist/cli/checks/links.js +89 -0
  8. package/dist/cli/checks/links.js.map +1 -0
  9. package/dist/cli/checks/performance.d.ts +17 -0
  10. package/dist/cli/checks/performance.d.ts.map +1 -0
  11. package/dist/cli/checks/performance.js +61 -0
  12. package/dist/cli/checks/performance.js.map +1 -0
  13. package/dist/cli/commands/check.d.ts +3 -0
  14. package/dist/cli/commands/check.d.ts.map +1 -0
  15. package/dist/cli/commands/check.js +85 -0
  16. package/dist/cli/commands/check.js.map +1 -0
  17. package/dist/cli/create/index.d.ts +11 -1
  18. package/dist/cli/create/index.d.ts.map +1 -1
  19. package/dist/cli/create/index.js +135 -65
  20. package/dist/cli/create/index.js.map +1 -1
  21. package/dist/cli/create/templates/spa/package.json.tpl +5 -3
  22. package/dist/cli/create/templates/ssg/cer.config.ts.tpl +2 -0
  23. package/dist/cli/create/templates/ssg/package.json.tpl +5 -3
  24. package/dist/cli/create/templates/ssr/package.json.tpl +5 -3
  25. package/dist/cli/index.js +2 -0
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/index.d.ts +3 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/plugin/build-ssg.d.ts +22 -6
  30. package/dist/plugin/build-ssg.d.ts.map +1 -1
  31. package/dist/plugin/build-ssg.js +227 -17
  32. package/dist/plugin/build-ssg.js.map +1 -1
  33. package/dist/plugin/content/parser.d.ts.map +1 -1
  34. package/dist/plugin/content/parser.js +54 -10
  35. package/dist/plugin/content/parser.js.map +1 -1
  36. package/dist/plugin/dev-server.d.ts +4 -1
  37. package/dist/plugin/dev-server.d.ts.map +1 -1
  38. package/dist/plugin/dev-server.js.map +1 -1
  39. package/dist/plugin/dts-generator.d.ts.map +1 -1
  40. package/dist/plugin/dts-generator.js +50 -8
  41. package/dist/plugin/dts-generator.js.map +1 -1
  42. package/dist/plugin/generated-dir.d.ts +2 -0
  43. package/dist/plugin/generated-dir.d.ts.map +1 -1
  44. package/dist/plugin/generated-dir.js +17 -6
  45. package/dist/plugin/generated-dir.js.map +1 -1
  46. package/dist/plugin/html-post-process.d.ts +6 -0
  47. package/dist/plugin/html-post-process.d.ts.map +1 -1
  48. package/dist/plugin/html-post-process.js +24 -0
  49. package/dist/plugin/html-post-process.js.map +1 -1
  50. package/dist/plugin/index.d.ts.map +1 -1
  51. package/dist/plugin/index.js +60 -13
  52. package/dist/plugin/index.js.map +1 -1
  53. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  54. package/dist/plugin/transforms/auto-import.js +7 -0
  55. package/dist/plugin/transforms/auto-import.js.map +1 -1
  56. package/dist/plugin/transforms/prune-runtime-css.d.ts +8 -0
  57. package/dist/plugin/transforms/prune-runtime-css.d.ts.map +1 -0
  58. package/dist/plugin/transforms/prune-runtime-css.js +40 -0
  59. package/dist/plugin/transforms/prune-runtime-css.js.map +1 -0
  60. package/dist/runtime/app-template.d.ts +1 -1
  61. package/dist/runtime/app-template.d.ts.map +1 -1
  62. package/dist/runtime/app-template.js +206 -36
  63. package/dist/runtime/app-template.js.map +1 -1
  64. package/dist/runtime/composables/index.d.ts +4 -0
  65. package/dist/runtime/composables/index.d.ts.map +1 -1
  66. package/dist/runtime/composables/index.js +2 -0
  67. package/dist/runtime/composables/index.js.map +1 -1
  68. package/dist/runtime/composables/use-active-headings.d.ts +24 -0
  69. package/dist/runtime/composables/use-active-headings.d.ts.map +1 -0
  70. package/dist/runtime/composables/use-active-headings.js +112 -0
  71. package/dist/runtime/composables/use-active-headings.js.map +1 -0
  72. package/dist/runtime/composables/use-content-page.d.ts +45 -0
  73. package/dist/runtime/composables/use-content-page.d.ts.map +1 -0
  74. package/dist/runtime/composables/use-content-page.js +179 -0
  75. package/dist/runtime/composables/use-content-page.js.map +1 -0
  76. package/dist/runtime/composables/use-content-search.d.ts.map +1 -1
  77. package/dist/runtime/composables/use-content-search.js +1 -5
  78. package/dist/runtime/composables/use-content-search.js.map +1 -1
  79. package/dist/runtime/composables/use-fetch.d.ts +2 -1
  80. package/dist/runtime/composables/use-fetch.d.ts.map +1 -1
  81. package/dist/runtime/composables/use-fetch.js +112 -43
  82. package/dist/runtime/composables/use-fetch.js.map +1 -1
  83. package/dist/runtime/composables/use-route.d.ts.map +1 -1
  84. package/dist/runtime/composables/use-route.js +39 -11
  85. package/dist/runtime/composables/use-route.js.map +1 -1
  86. package/dist/runtime/content/client.js +2 -2
  87. package/dist/runtime/content/client.js.map +1 -1
  88. package/dist/runtime/entry-server-template.d.ts +16 -1
  89. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  90. package/dist/runtime/entry-server-template.js +120 -13
  91. package/dist/runtime/entry-server-template.js.map +1 -1
  92. package/dist/types/config.d.ts +45 -4
  93. package/dist/types/config.d.ts.map +1 -1
  94. package/dist/types/config.js.map +1 -1
  95. package/dist/types/middleware.d.ts +2 -2
  96. package/dist/types/middleware.d.ts.map +1 -1
  97. package/package.json +22 -19
  98. package/.github/copilot-instructions.md +0 -169
  99. package/.github/workflows/publish.yml +0 -257
  100. package/.nvmrc +0 -1
  101. package/CHANGELOG.md +0 -236
  102. package/CLAUDE.md +0 -113
  103. package/commits.txt +0 -1
  104. package/cypress.config.ts +0 -16
  105. package/docs/authentication.md +0 -285
  106. package/docs/cli.md +0 -324
  107. package/docs/components.md +0 -237
  108. package/docs/composables.md +0 -907
  109. package/docs/configuration.md +0 -696
  110. package/docs/content.md +0 -570
  111. package/docs/data-loading.md +0 -336
  112. package/docs/getting-started.md +0 -237
  113. package/docs/head-management.md +0 -212
  114. package/docs/i18n.md +0 -159
  115. package/docs/layouts.md +0 -224
  116. package/docs/middleware.md +0 -234
  117. package/docs/plugins.md +0 -170
  118. package/docs/rendering-modes.md +0 -377
  119. package/docs/routing.md +0 -485
  120. package/docs/server-api.md +0 -253
  121. package/docs/testing.md +0 -455
  122. package/docs/use-fetch.md +0 -183
  123. package/e2e/cypress/e2e/api.cy.ts +0 -123
  124. package/e2e/cypress/e2e/async-component.cy.ts +0 -27
  125. package/e2e/cypress/e2e/component-splitting.cy.ts +0 -50
  126. package/e2e/cypress/e2e/content.cy.ts +0 -425
  127. package/e2e/cypress/e2e/cookie.cy.ts +0 -85
  128. package/e2e/cypress/e2e/data.cy.ts +0 -158
  129. package/e2e/cypress/e2e/error-boundary.cy.ts +0 -88
  130. package/e2e/cypress/e2e/fouc.cy.ts +0 -254
  131. package/e2e/cypress/e2e/group-meta.cy.ts +0 -36
  132. package/e2e/cypress/e2e/head.cy.ts +0 -89
  133. package/e2e/cypress/e2e/i18n.cy.ts +0 -79
  134. package/e2e/cypress/e2e/interactive.cy.ts +0 -137
  135. package/e2e/cypress/e2e/isr-nested-runtime.cy.ts +0 -102
  136. package/e2e/cypress/e2e/jit-css-custom-colors.cy.ts +0 -43
  137. package/e2e/cypress/e2e/middleware-chain.cy.ts +0 -89
  138. package/e2e/cypress/e2e/middleware-status.cy.ts +0 -30
  139. package/e2e/cypress/e2e/middleware.cy.ts +0 -45
  140. package/e2e/cypress/e2e/navigate.cy.ts +0 -39
  141. package/e2e/cypress/e2e/observability.cy.ts +0 -77
  142. package/e2e/cypress/e2e/per-route-error.cy.ts +0 -42
  143. package/e2e/cypress/e2e/per-route-render.cy.ts +0 -70
  144. package/e2e/cypress/e2e/preview-hardening.cy.ts +0 -88
  145. package/e2e/cypress/e2e/routes.cy.ts +0 -128
  146. package/e2e/cypress/e2e/seo-meta.cy.ts +0 -108
  147. package/e2e/cypress/e2e/server-middleware.cy.ts +0 -46
  148. package/e2e/cypress/e2e/session-rotation.cy.ts +0 -25
  149. package/e2e/cypress/e2e/session.cy.ts +0 -73
  150. package/e2e/cypress/e2e/ssr-render-error.cy.ts +0 -68
  151. package/e2e/cypress/e2e/synthetic-404.cy.ts +0 -45
  152. package/e2e/cypress/e2e/use-page-data.cy.ts +0 -122
  153. package/e2e/cypress/e2e/use-state.cy.ts +0 -138
  154. package/e2e/cypress/support/commands.ts +0 -120
  155. package/e2e/cypress/support/e2e.ts +0 -10
  156. package/e2e/cypress/tsconfig.json +0 -14
  157. package/e2e/kitchen-sink/app/components/ks-badge.ts +0 -8
  158. package/e2e/kitchen-sink/app/composables/useKsCounter.ts +0 -9
  159. package/e2e/kitchen-sink/app/error.ts +0 -18
  160. package/e2e/kitchen-sink/app/layouts/admin.ts +0 -13
  161. package/e2e/kitchen-sink/app/layouts/default.ts +0 -25
  162. package/e2e/kitchen-sink/app/layouts/group.ts +0 -9
  163. package/e2e/kitchen-sink/app/layouts/minimal.ts +0 -7
  164. package/e2e/kitchen-sink/app/loading.ts +0 -9
  165. package/e2e/kitchen-sink/app/middleware/auth.ts +0 -9
  166. package/e2e/kitchen-sink/app/middleware/custom-log.ts +0 -4
  167. package/e2e/kitchen-sink/app/middleware/group-auth.ts +0 -7
  168. package/e2e/kitchen-sink/app/middleware/logger.ts +0 -14
  169. package/e2e/kitchen-sink/app/pages/(auth)/login.ts +0 -13
  170. package/e2e/kitchen-sink/app/pages/(auth)/protected.ts +0 -16
  171. package/e2e/kitchen-sink/app/pages/404.ts +0 -9
  172. package/e2e/kitchen-sink/app/pages/about.ts +0 -17
  173. package/e2e/kitchen-sink/app/pages/admin/_layout.ts +0 -1
  174. package/e2e/kitchen-sink/app/pages/admin/dashboard.ts +0 -11
  175. package/e2e/kitchen-sink/app/pages/async-component-error-test.ts +0 -18
  176. package/e2e/kitchen-sink/app/pages/async-component-test.ts +0 -18
  177. package/e2e/kitchen-sink/app/pages/blog/[slug].ts +0 -59
  178. package/e2e/kitchen-sink/app/pages/blog/index.ts +0 -51
  179. package/e2e/kitchen-sink/app/pages/content-blog.ts +0 -37
  180. package/e2e/kitchen-sink/app/pages/content-doc.ts +0 -42
  181. package/e2e/kitchen-sink/app/pages/content-fallback.ts +0 -36
  182. package/e2e/kitchen-sink/app/pages/content-guides.ts +0 -35
  183. package/e2e/kitchen-sink/app/pages/content-index.ts +0 -39
  184. package/e2e/kitchen-sink/app/pages/content-search.ts +0 -36
  185. package/e2e/kitchen-sink/app/pages/cookie-test.ts +0 -22
  186. package/e2e/kitchen-sink/app/pages/counter.ts +0 -17
  187. package/e2e/kitchen-sink/app/pages/group-auth-blocked.ts +0 -9
  188. package/e2e/kitchen-sink/app/pages/group-meta-test/_layout.ts +0 -6
  189. package/e2e/kitchen-sink/app/pages/group-meta-test/custom-mw.ts +0 -14
  190. package/e2e/kitchen-sink/app/pages/group-meta-test/page.ts +0 -9
  191. package/e2e/kitchen-sink/app/pages/group-meta-test/protected.ts +0 -8
  192. package/e2e/kitchen-sink/app/pages/head.ts +0 -20
  193. package/e2e/kitchen-sink/app/pages/i18n-test.ts +0 -14
  194. package/e2e/kitchen-sink/app/pages/index.ts +0 -27
  195. package/e2e/kitchen-sink/app/pages/isr-test.ts +0 -17
  196. package/e2e/kitchen-sink/app/pages/items/[id].ts +0 -20
  197. package/e2e/kitchen-sink/app/pages/jit-css-custom-colors-test.ts +0 -14
  198. package/e2e/kitchen-sink/app/pages/loader-error-test.ts +0 -13
  199. package/e2e/kitchen-sink/app/pages/loader-props-test.ts +0 -19
  200. package/e2e/kitchen-sink/app/pages/loader-response-error-test.ts +0 -13
  201. package/e2e/kitchen-sink/app/pages/middleware-chain-test.ts +0 -12
  202. package/e2e/kitchen-sink/app/pages/middleware-status-test.ts +0 -9
  203. package/e2e/kitchen-sink/app/pages/navigate-test.ts +0 -16
  204. package/e2e/kitchen-sink/app/pages/observability-test.ts +0 -25
  205. package/e2e/kitchen-sink/app/pages/per-route-error-test.error.ts +0 -13
  206. package/e2e/kitchen-sink/app/pages/per-route-error-test.ts +0 -15
  207. package/e2e/kitchen-sink/app/pages/plugin-dsd-test.ts +0 -10
  208. package/e2e/kitchen-sink/app/pages/render-error-test.ts +0 -12
  209. package/e2e/kitchen-sink/app/pages/render-server-test.ts +0 -12
  210. package/e2e/kitchen-sink/app/pages/render-spa-test.ts +0 -12
  211. package/e2e/kitchen-sink/app/pages/route-info.ts +0 -14
  212. package/e2e/kitchen-sink/app/pages/seo-test.ts +0 -23
  213. package/e2e/kitchen-sink/app/pages/session-rotation-test.ts +0 -25
  214. package/e2e/kitchen-sink/app/pages/use-state-test.ts +0 -37
  215. package/e2e/kitchen-sink/app/plugins/01.setup.ts +0 -7
  216. package/e2e/kitchen-sink/app/plugins/02.ks-plugin-components.ts +0 -27
  217. package/e2e/kitchen-sink/cer-auto-imports.d.ts +0 -51
  218. package/e2e/kitchen-sink/cer-env.d.ts +0 -35
  219. package/e2e/kitchen-sink/cer-tsconfig.json +0 -30
  220. package/e2e/kitchen-sink/cer.config.ts +0 -43
  221. package/e2e/kitchen-sink/content/01.guides/01.index.md +0 -8
  222. package/e2e/kitchen-sink/content/01.guides/02.intro.md +0 -8
  223. package/e2e/kitchen-sink/content/01.guides/10.advanced.md +0 -8
  224. package/e2e/kitchen-sink/content/blog/2026-04-01-hello.md +0 -26
  225. package/e2e/kitchen-sink/content/blog/2026-04-02-draft.md +0 -10
  226. package/e2e/kitchen-sink/content/blog/index.md +0 -8
  227. package/e2e/kitchen-sink/content/blog/no-frontmatter.md +0 -7
  228. package/e2e/kitchen-sink/content/docs/getting-started.md +0 -48
  229. package/e2e/kitchen-sink/content/index.md +0 -16
  230. package/e2e/kitchen-sink/server/api/echo.ts +0 -12
  231. package/e2e/kitchen-sink/server/api/health.ts +0 -3
  232. package/e2e/kitchen-sink/server/api/posts/[slug].ts +0 -11
  233. package/e2e/kitchen-sink/server/api/posts/index.ts +0 -5
  234. package/e2e/kitchen-sink/server/api/session.ts +0 -23
  235. package/e2e/kitchen-sink/server/data/posts.ts +0 -21
  236. package/e2e/kitchen-sink/server/middleware/01-headers.ts +0 -6
  237. package/e2e/kitchen-sink/server/middleware/02-status-test.ts +0 -10
  238. package/e2e/kitchen-sink/tsconfig.json +0 -3
  239. package/e2e/scripts/clean.mjs +0 -14
  240. package/eslint.config.ts +0 -22
  241. package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +0 -132
  242. package/src/__tests__/cli/adapters/cloudflare.test.ts +0 -332
  243. package/src/__tests__/cli/adapters/netlify-bridge.integration.test.ts +0 -139
  244. package/src/__tests__/cli/adapters/netlify.test.ts +0 -286
  245. package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +0 -129
  246. package/src/__tests__/cli/adapters/vercel.test.ts +0 -263
  247. package/src/__tests__/cli/preview-hardening.test.ts +0 -265
  248. package/src/__tests__/cli/preview-isr.test.ts +0 -320
  249. package/src/__tests__/index.test.ts +0 -21
  250. package/src/__tests__/plugin/app-template.test.ts +0 -166
  251. package/src/__tests__/plugin/build-ssg-render.test.ts +0 -155
  252. package/src/__tests__/plugin/build-ssg.test.ts +0 -598
  253. package/src/__tests__/plugin/build-ssr.test.ts +0 -192
  254. package/src/__tests__/plugin/cer-app-plugin.test.ts +0 -552
  255. package/src/__tests__/plugin/content/emitter.test.ts +0 -117
  256. package/src/__tests__/plugin/content/loader.test.ts +0 -154
  257. package/src/__tests__/plugin/content/parser.test.ts +0 -381
  258. package/src/__tests__/plugin/content/path-utils.test.ts +0 -69
  259. package/src/__tests__/plugin/content/search.test.ts +0 -119
  260. package/src/__tests__/plugin/dev-server.test.ts +0 -813
  261. package/src/__tests__/plugin/dts-generator.test.ts +0 -394
  262. package/src/__tests__/plugin/entry-server-template.test.ts +0 -385
  263. package/src/__tests__/plugin/generated-dir.test.ts +0 -144
  264. package/src/__tests__/plugin/html-post-process.test.ts +0 -146
  265. package/src/__tests__/plugin/path-utils.test.ts +0 -411
  266. package/src/__tests__/plugin/resolve-config.test.ts +0 -233
  267. package/src/__tests__/plugin/scanner.test.ts +0 -186
  268. package/src/__tests__/plugin/transforms/auto-import.test.ts +0 -510
  269. package/src/__tests__/plugin/transforms/head-inject.test.ts +0 -178
  270. package/src/__tests__/plugin/virtual/composables.test.ts +0 -57
  271. package/src/__tests__/plugin/virtual/content-components.test.ts +0 -66
  272. package/src/__tests__/plugin/virtual/error.test.ts +0 -71
  273. package/src/__tests__/plugin/virtual/layouts.test.ts +0 -70
  274. package/src/__tests__/plugin/virtual/loading.test.ts +0 -72
  275. package/src/__tests__/plugin/virtual/middleware.test.ts +0 -83
  276. package/src/__tests__/plugin/virtual/plugins.test.ts +0 -129
  277. package/src/__tests__/plugin/virtual/routes.test.ts +0 -884
  278. package/src/__tests__/plugin/virtual/server-api.test.ts +0 -164
  279. package/src/__tests__/plugin/virtual/server-middleware.test.ts +0 -102
  280. package/src/__tests__/runtime/app-template.test.ts +0 -208
  281. package/src/__tests__/runtime/define-middleware.test.ts +0 -64
  282. package/src/__tests__/runtime/define-server-middleware.test.ts +0 -54
  283. package/src/__tests__/runtime/entry-client-template.test.ts +0 -61
  284. package/src/__tests__/runtime/isr-handler.test.ts +0 -338
  285. package/src/__tests__/runtime/oauth/handler.test.ts +0 -358
  286. package/src/__tests__/runtime/oauth/providers.test.ts +0 -47
  287. package/src/__tests__/runtime/oauth-index.test.ts +0 -43
  288. package/src/__tests__/runtime/use-auth.test.ts +0 -169
  289. package/src/__tests__/runtime/use-content-search-composable.test.ts +0 -405
  290. package/src/__tests__/runtime/use-content-search.test.ts +0 -163
  291. package/src/__tests__/runtime/use-content.test.ts +0 -226
  292. package/src/__tests__/runtime/use-cookie.test.ts +0 -277
  293. package/src/__tests__/runtime/use-fetch-component.test.ts +0 -214
  294. package/src/__tests__/runtime/use-fetch.test.ts +0 -377
  295. package/src/__tests__/runtime/use-head.test.ts +0 -311
  296. package/src/__tests__/runtime/use-inject-client.test.ts +0 -67
  297. package/src/__tests__/runtime/use-inject.test.ts +0 -66
  298. package/src/__tests__/runtime/use-locale.test.ts +0 -152
  299. package/src/__tests__/runtime/use-navigate.test.ts +0 -121
  300. package/src/__tests__/runtime/use-page-data.test.ts +0 -298
  301. package/src/__tests__/runtime/use-route.test.ts +0 -156
  302. package/src/__tests__/runtime/use-runtime-config.test.ts +0 -182
  303. package/src/__tests__/runtime/use-seo-meta.test.ts +0 -109
  304. package/src/__tests__/runtime/use-session.test.ts +0 -320
  305. package/src/__tests__/runtime/use-state.test.ts +0 -340
  306. package/src/__tests__/types/config.test.ts +0 -18
  307. package/src/cli/adapters/cloudflare.ts +0 -335
  308. package/src/cli/adapters/netlify.ts +0 -326
  309. package/src/cli/adapters/vercel.ts +0 -311
  310. package/src/cli/commands/adapt.ts +0 -84
  311. package/src/cli/commands/build.ts +0 -165
  312. package/src/cli/commands/dev.ts +0 -125
  313. package/src/cli/commands/generate.ts +0 -91
  314. package/src/cli/commands/preview-isr.ts +0 -172
  315. package/src/cli/commands/preview.ts +0 -523
  316. package/src/cli/create/index.ts +0 -275
  317. package/src/cli/create/templates/shared/.gitignore.tpl +0 -25
  318. package/src/cli/create/templates/shared/app/layouts/default.ts.tpl +0 -15
  319. package/src/cli/create/templates/shared/app/pages/index.ts.tpl +0 -8
  320. package/src/cli/create/templates/shared/index.html.tpl +0 -12
  321. package/src/cli/create/templates/shared/tsconfig.json.tpl +0 -6
  322. package/src/cli/create/templates/spa/cer.config.ts.tpl +0 -6
  323. package/src/cli/create/templates/spa/package.json.tpl +0 -18
  324. package/src/cli/create/templates/ssg/cer.config.ts.tpl +0 -10
  325. package/src/cli/create/templates/ssg/package.json.tpl +0 -19
  326. package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -6
  327. package/src/cli/create/templates/ssr/package.json.tpl +0 -18
  328. package/src/cli/index.ts +0 -22
  329. package/src/index.ts +0 -20
  330. package/src/plugin/build-ssg.ts +0 -403
  331. package/src/plugin/build-ssr.ts +0 -134
  332. package/src/plugin/content/emitter.ts +0 -50
  333. package/src/plugin/content/index.ts +0 -233
  334. package/src/plugin/content/parser.ts +0 -259
  335. package/src/plugin/content/path-utils.ts +0 -55
  336. package/src/plugin/content/scanner.ts +0 -26
  337. package/src/plugin/content/search.ts +0 -28
  338. package/src/plugin/dev-server.ts +0 -379
  339. package/src/plugin/dts-generator.ts +0 -273
  340. package/src/plugin/generated-dir.ts +0 -121
  341. package/src/plugin/html-post-process.ts +0 -96
  342. package/src/plugin/index.ts +0 -584
  343. package/src/plugin/path-utils.ts +0 -186
  344. package/src/plugin/scanner.ts +0 -65
  345. package/src/plugin/transforms/auto-import.ts +0 -206
  346. package/src/plugin/transforms/head-inject.ts +0 -161
  347. package/src/plugin/virtual/composables.ts +0 -28
  348. package/src/plugin/virtual/content-components.ts +0 -83
  349. package/src/plugin/virtual/error.ts +0 -34
  350. package/src/plugin/virtual/layouts.ts +0 -41
  351. package/src/plugin/virtual/loading.ts +0 -32
  352. package/src/plugin/virtual/middleware.ts +0 -45
  353. package/src/plugin/virtual/plugins.ts +0 -46
  354. package/src/plugin/virtual/routes.ts +0 -514
  355. package/src/plugin/virtual/server-api.ts +0 -85
  356. package/src/plugin/virtual/server-middleware.ts +0 -44
  357. package/src/runtime/app-template.ts +0 -377
  358. package/src/runtime/composables/define-middleware.ts +0 -17
  359. package/src/runtime/composables/define-server-middleware.ts +0 -18
  360. package/src/runtime/composables/index.ts +0 -27
  361. package/src/runtime/composables/use-auth.ts +0 -125
  362. package/src/runtime/composables/use-content-search.ts +0 -205
  363. package/src/runtime/composables/use-content.ts +0 -146
  364. package/src/runtime/composables/use-cookie.ts +0 -135
  365. package/src/runtime/composables/use-fetch.ts +0 -438
  366. package/src/runtime/composables/use-head.ts +0 -234
  367. package/src/runtime/composables/use-inject.ts +0 -49
  368. package/src/runtime/composables/use-locale.ts +0 -109
  369. package/src/runtime/composables/use-navigate.ts +0 -58
  370. package/src/runtime/composables/use-page-data.ts +0 -93
  371. package/src/runtime/composables/use-route.ts +0 -71
  372. package/src/runtime/composables/use-runtime-config.ts +0 -123
  373. package/src/runtime/composables/use-seo-meta.ts +0 -80
  374. package/src/runtime/composables/use-session.ts +0 -186
  375. package/src/runtime/composables/use-state.ts +0 -92
  376. package/src/runtime/content/client.ts +0 -168
  377. package/src/runtime/entry-client-template.ts +0 -39
  378. package/src/runtime/entry-server-template.ts +0 -493
  379. package/src/runtime/isr-handler.ts +0 -184
  380. package/src/runtime/oauth/handler.ts +0 -407
  381. package/src/runtime/oauth/index.ts +0 -49
  382. package/src/runtime/oauth/providers.ts +0 -38
  383. package/src/types/api.ts +0 -40
  384. package/src/types/config.ts +0 -379
  385. package/src/types/content.ts +0 -66
  386. package/src/types/index.ts +0 -7
  387. package/src/types/middleware.ts +0 -63
  388. package/src/types/page.ts +0 -117
  389. package/src/types/plugin.ts +0 -34
  390. package/tsconfig.build.json +0 -10
  391. package/tsconfig.json +0 -19
  392. package/vitest.config.ts +0 -33
@@ -1,438 +0,0 @@
1
- /**
2
- * useFetch — isomorphic data fetching composable.
3
- *
4
- * Behaves differently based on the calling context:
5
- *
6
- * **Component context** (inside a `component()` render function):
7
- * Uses `createComposable` internally so that `useOnConnected` is bound to the
8
- * calling component's lifecycle. Returns reactive `ReactiveState` refs for
9
- * `data`, `pending`, and `error` — the component re-renders automatically
10
- * when the fetch settles. The fetch is triggered by `useOnConnected` (i.e.
11
- * when the component mounts), unless `lazy: true` / `server: false`.
12
- *
13
- * **Loader / SSR context** (inside an async `load()` or middleware):
14
- * Returns a *thenable* `UseFetchResult` — you can `await useFetch(...)` to
15
- * block SSR rendering until data is ready. The result is serialised into
16
- * `window.__CER_FETCH_DATA__` for client hydration.
17
- *
18
- * @example
19
- * ```ts
20
- * // app/pages/posts.ts — loader context (SSR + client navigation)
21
- * export const loader = async () => {
22
- * const { data: posts } = await useFetch<Post[]>('/api/posts')
23
- * return { posts }
24
- * }
25
- * ```
26
- *
27
- * @example
28
- * ```ts
29
- * // Inside a component — reactive, auto-fetches on mount
30
- * component('post-list', () => {
31
- * const { data: posts, pending, error } = useFetch<Post[]>('/api/posts')
32
- *
33
- * return html`
34
- * ${pending.value ? html`<p>Loading…</p>` : ''}
35
- * ${error.value ? html`<p>Error: ${error.value.message}</p>` : ''}
36
- * ${posts.value?.map(p => html`<li>${p.title}</li>`)}
37
- * `
38
- * })
39
- * ```
40
- *
41
- * @example
42
- * ```ts
43
- * // Lazy — skip auto-fetch, call refresh() manually
44
- * component('post-list', () => {
45
- * const { data: posts, refresh } = useFetch<Post[]>('/api/posts', { lazy: true })
46
- *
47
- * useOnConnected(async () => {
48
- * await refresh()
49
- * })
50
- * })
51
- * ```
52
- */
53
-
54
- import { createComposable, getCurrentComponentContext, ref, useOnConnected } from '@jasonshimmy/custom-elements-runtime'
55
- import type { ReactiveState } from '@jasonshimmy/custom-elements-runtime'
56
-
57
- // ─── Types ────────────────────────────────────────────────────────────────────
58
-
59
- /**
60
- * Return value when `useFetch` is called inside a component render function.
61
- * All fields are reactive (`ReactiveState`) — the component re-renders
62
- * automatically when the fetch settles or `refresh()` is called.
63
- */
64
- export interface UseFetchReactiveReturn<T = unknown> {
65
- /** Reactive fetch result. Access via `.data.value`. */
66
- data: ReactiveState<T | null>
67
- /** `true` while the request is in-flight. */
68
- pending: ReactiveState<boolean>
69
- /** Set when the request fails; `null` on success. */
70
- error: ReactiveState<Error | null>
71
- /** Re-issues the fetch and updates `data`, `pending`, and `error`. */
72
- refresh(): Promise<void>
73
- }
74
-
75
- /** Options for `useFetch()`. Controls caching, SSR behaviour, data transformation, and HTTP request details. */
76
- export interface UseFetchOptions<T = unknown> {
77
- /**
78
- * Unique cache key. Defaults to the full URL string (including query params).
79
- *
80
- * On the server, calls sharing the same key within one request reuse the
81
- * first result without issuing a second network request. On the client,
82
- * the key is used to consume the hydrated `window.__CER_FETCH_DATA__` entry
83
- * exactly once — subsequent calls with the same key issue a fresh fetch.
84
- */
85
- key?: string
86
- /**
87
- * When `true`, skip the SSR fetch entirely and only fetch on the client.
88
- * Equivalent to `server: false`.
89
- *
90
- * In component context this also skips the `useOnConnected` auto-fetch —
91
- * call `refresh()` manually when you want the request to fire.
92
- */
93
- lazy?: boolean
94
- /**
95
- * When `false`, equivalent to `lazy: true` — skip SSR, fetch client-side.
96
- */
97
- server?: boolean
98
- /**
99
- * Factory that returns the initial value before the fetch completes.
100
- * Called once per `useFetch()` invocation. Defaults to `() => null`.
101
- *
102
- * @example `{ default: () => [] }` — start with an empty array
103
- */
104
- default?: () => T
105
- /**
106
- * Transform the raw JSON response before storing / returning it.
107
- * Applied after `pick` (if both are set).
108
- */
109
- transform?: (data: unknown) => T
110
- /**
111
- * Pick a subset of keys from an object response.
112
- * Applied before `transform`. Keys absent in the response become
113
- * `undefined` in the result.
114
- */
115
- pick?: string[]
116
- /** HTTP method. Defaults to `'GET'`. */
117
- method?: string
118
- /**
119
- * Request body for POST / PUT / PATCH. Serialised to JSON automatically;
120
- * sets `Content-Type: application/json` if not already provided.
121
- */
122
- body?: unknown
123
- /** Additional request headers merged with auto-generated ones. */
124
- headers?: Record<string, string>
125
- /**
126
- * Query parameters appended to the URL via `URLSearchParams`.
127
- * Included in the default cache `key`.
128
- */
129
- query?: Record<string, string>
130
- }
131
-
132
- export interface UseFetchReturn<T = unknown> {
133
- data: T | null
134
- pending: boolean
135
- error: Error | null
136
- /** Re-issues the fetch and updates `data`, `pending`, and `error` in place. */
137
- refresh(): Promise<UseFetchReturn<T>>
138
- }
139
-
140
- /** A UseFetchReturn that is also awaitable (resolves once the fetch completes). */
141
- export type UseFetchResult<T = unknown> = UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>
142
-
143
- // ─── Client-side in-flight deduplication ─────────────────────────────────────
144
- // P2-3: When two components (or two concurrent refresh() calls) fetch the same
145
- // key simultaneously, only one HTTP request is issued. The second caller awaits
146
- // the shared Promise and applies its own transform to the raw response.
147
- // The map is module-level so all component instances share it.
148
- // Keys are removed when the Promise settles (resolve or reject).
149
- const _inflight = new Map<string, Promise<unknown>>()
150
-
151
- // ─── Helpers ──────────────────────────────────────────────────────────────────
152
-
153
- // Access fetch via globalThis so that vi.stubGlobal('fetch', mock) works in tests.
154
- // A direct `fetch(...)` call in an ESM module may bind to the runtime's fetch
155
- // at module-evaluation time and bypass globalThis mutations.
156
- function _fetch(url: string, init?: RequestInit): Promise<Response> {
157
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
- return ((globalThis as any)['fetch'] as typeof fetch)(url, init)
159
- }
160
-
161
- function applyPick(data: unknown, pick: string[]): unknown {
162
- if (typeof data !== 'object' || data === null) return data
163
- return Object.fromEntries(pick.map((k) => [k, (data as Record<string, unknown>)[k]]))
164
- }
165
-
166
- function applyTransform<T>(raw: unknown, options?: UseFetchOptions<T>): T {
167
- let result = raw
168
- if (options?.pick) result = applyPick(result, options.pick)
169
- if (options?.transform) result = options.transform(result)
170
- return result as T
171
- }
172
-
173
- function buildInit(options?: UseFetchOptions): RequestInit {
174
- const init: RequestInit = { method: options?.method ?? 'GET' }
175
- const headers: Record<string, string> = { ...options?.headers }
176
- if (options?.body !== undefined) {
177
- init.body = JSON.stringify(options.body)
178
- headers['Content-Type'] = 'application/json'
179
- }
180
- if (Object.keys(headers).length > 0) init.headers = headers
181
- return init
182
- }
183
-
184
- function appendQuery(url: string, query?: Record<string, string>): string {
185
- if (!query || Object.keys(query).length === 0) return url
186
- const sep = url.includes('?') ? '&' : '?'
187
- return url + sep + new URLSearchParams(query).toString()
188
- }
189
-
190
- function makeResult<T>(
191
- state: UseFetchReturn<T>,
192
- settling: Promise<void> | null,
193
- ): UseFetchResult<T> {
194
- // Return a wrapper with live getters so callers see state mutations (e.g.
195
- // after doClientFetch sets state.data), PLUS a custom .then so the result
196
- // is directly awaitable.
197
- //
198
- // Critically, we do NOT add .then to `state` itself — doing so would cause
199
- // async functions that `return state` (e.g. doClientFetch) to deadlock:
200
- // the async function's Promise would try to assimilate state as a thenable,
201
- // calling state.then which waits for settling, which can't resolve until
202
- // the async function finishes — a circular dependency.
203
- return {
204
- get data() { return state.data },
205
- get pending() { return state.pending },
206
- get error() { return state.error },
207
- get refresh() { return state.refresh },
208
- then(
209
- onFulfilled?: (v: UseFetchReturn<T>) => unknown,
210
- onRejected?: (e: unknown) => unknown,
211
- ) {
212
- const p = settling ?? Promise.resolve()
213
- // Remove .then from this wrapper BEFORE resolving so the Promise
214
- // machinery won't treat it as a thenable when resolve(this) is called.
215
- // After deletion the wrapper becomes a plain UseFetchReturn<T> with
216
- // live getters — callers can still read .data, .error, etc. and get
217
- // up-to-date values (e.g. after .refresh()).
218
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
- delete (this as any).then
220
- const self = this as UseFetchReturn<T>
221
- return p.then(() => {
222
- if (onFulfilled) return onFulfilled(self)
223
- }, onRejected)
224
- },
225
- } as UseFetchResult<T>
226
- }
227
-
228
- // ─── Component-context composable factory ─────────────────────────────────────
229
-
230
- /**
231
- * Called when `useFetch` detects it is running inside a component render
232
- * function. The factory is created fresh per call so each component instance
233
- * gets isolated reactive state; `createComposable` binds lifecycle hooks
234
- * (`useOnConnected`) to the calling component.
235
- */
236
- function makeComponentFetch<T>(
237
- url: string | (() => string),
238
- options: UseFetchOptions<T> | undefined,
239
- ctx: Record<string, unknown>,
240
- ): UseFetchReactiveReturn<T> {
241
- const factory = createComposable((): UseFetchReactiveReturn<T> => {
242
- const isLazy = options?.lazy === true || options?.server === false
243
- const resolvedUrl = appendQuery(
244
- typeof url === 'function' ? url() : url,
245
- options?.query,
246
- )
247
-
248
- const data = ref<T | null>(options?.default ? options.default() : null)
249
- const pending = ref(false)
250
- const error = ref<Error | null>(null)
251
-
252
- const doFetch = async (): Promise<void> => {
253
- pending.value = true
254
- error.value = null
255
- try {
256
- const res = await _fetch(resolvedUrl, buildInit(options))
257
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
258
- const raw = await res.json()
259
- data.value = applyTransform<T>(raw, options)
260
- } catch (err) {
261
- error.value = err instanceof Error ? err : new Error(String(err))
262
- } finally {
263
- pending.value = false
264
- }
265
- }
266
-
267
- if (!isLazy) {
268
- useOnConnected(doFetch)
269
- }
270
-
271
- return { data, pending, error, refresh: doFetch }
272
- })
273
-
274
- return factory(ctx)
275
- }
276
-
277
- // ─── Core implementation ──────────────────────────────────────────────────────
278
-
279
- /**
280
- * Isomorphic data fetching composable.
281
- *
282
- * - **Inside a `component()` render function**: returns reactive
283
- * {@link UseFetchReactiveReturn} — `data`, `pending`, and `error` are
284
- * `ReactiveState` refs that trigger re-renders automatically. The fetch
285
- * fires via `useOnConnected` unless `lazy: true`.
286
- * - **Inside a loader / SSR context**: returns a thenable
287
- * {@link UseFetchResult} — `await useFetch(...)` blocks rendering until data
288
- * is ready.
289
- */
290
- export function useFetch<T = unknown>(
291
- url: string | (() => string),
292
- options?: UseFetchOptions<T>,
293
- ): UseFetchResult<T> | UseFetchReactiveReturn<T> {
294
- // ── Component context ───────────────────────────────────────────────────────
295
- // When called inside a component render function, use createComposable so
296
- // that useOnConnected is bound to the calling component's lifecycle and
297
- // state is isolated per component instance.
298
- const componentCtx = getCurrentComponentContext()
299
- if (componentCtx) {
300
- return makeComponentFetch(url, options, componentCtx)
301
- }
302
- const g = globalThis as Record<string, unknown>
303
- const resolvedUrl = appendQuery(
304
- typeof url === 'function' ? url() : url,
305
- options?.query,
306
- )
307
- const key = options?.key ?? resolvedUrl
308
- const isLazy = options?.lazy === true || options?.server === false
309
- const defaultValue = (options?.default ? options.default() : null) as T | null
310
-
311
- // ── Server path ─────────────────────────────────────────────────────────────
312
- // __CER_FETCH_STORE__ is only present in Node.js (tree-shaken from client bundle).
313
- const fetchStoreAls = g['__CER_FETCH_STORE__'] as { getStore(): unknown } | undefined
314
- if (fetchStoreAls) {
315
- const fetchMap = fetchStoreAls.getStore() as Map<string, unknown> | null
316
-
317
- if (fetchMap) {
318
- // Already fetched in this request — return cached result synchronously.
319
- if (fetchMap.has(key)) {
320
- const state: UseFetchReturn<T> = {
321
- data: applyTransform(fetchMap.get(key), options),
322
- pending: false,
323
- error: null,
324
- refresh: async () => state,
325
- }
326
- return makeResult(state, null)
327
- }
328
-
329
- if (isLazy) {
330
- const state: UseFetchReturn<T> = {
331
- data: defaultValue,
332
- pending: false,
333
- error: null,
334
- refresh: async () => state,
335
- }
336
- return makeResult(state, null)
337
- }
338
-
339
- // Perform the SSR fetch.
340
- const state: UseFetchReturn<T> = {
341
- data: defaultValue,
342
- pending: true,
343
- error: null,
344
- refresh: async () => state,
345
- }
346
-
347
- const settling = (async () => {
348
- try {
349
- const res = await _fetch(resolvedUrl, buildInit(options))
350
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
351
- const raw = await res.json()
352
- const transformed = applyTransform<T>(raw, options)
353
- // Store in per-request map so the same key isn't fetched twice.
354
- fetchMap.set(key, transformed)
355
- // Also write to the flat accumulator that the entry-server serialises.
356
- const acc = (g['__CER_FETCH_DATA__'] ?? {}) as Record<string, unknown>
357
- acc[key] = transformed
358
- ;(g as Record<string, unknown>)['__CER_FETCH_DATA__'] = acc
359
- state.data = transformed
360
- state.pending = false
361
- } catch (err) {
362
- state.error = err instanceof Error ? err : new Error(String(err))
363
- state.pending = false
364
- }
365
- })()
366
-
367
- return makeResult(state, settling)
368
- }
369
- }
370
-
371
- // ── Client hydration path ───────────────────────────────────────────────────
372
- // Read the pre-fetched data injected by the server into window.__CER_FETCH_DATA__.
373
- const fetchData = g['__CER_FETCH_DATA__'] as Record<string, unknown> | undefined
374
- if (fetchData && key in fetchData) {
375
- const raw = fetchData[key]
376
- // Consume the key — next navigation will fetch fresh.
377
- delete fetchData[key]
378
- const hydrated = applyTransform<T>(raw, options)
379
- const state: UseFetchReturn<T> = {
380
- data: hydrated,
381
- pending: false,
382
- error: null,
383
- refresh: () => doClientFetch(key, resolvedUrl, options, state),
384
- }
385
- return makeResult(state, null)
386
- }
387
-
388
- // ── Client fetch ─────────────────────────────────────────────────────────────
389
- const state: UseFetchReturn<T> = {
390
- data: defaultValue,
391
- pending: !isLazy,
392
- error: null,
393
- refresh: async () => state,
394
- }
395
- state.refresh = () => doClientFetch(key, resolvedUrl, options, state)
396
-
397
- let settling: Promise<void> | null = null
398
- if (!isLazy) {
399
- settling = doClientFetch(key, resolvedUrl, options, state).then(() => undefined)
400
- }
401
-
402
- return makeResult(state, settling)
403
- }
404
-
405
- async function doClientFetch<T>(
406
- key: string,
407
- resolvedUrl: string,
408
- options: UseFetchOptions<T> | undefined,
409
- state: UseFetchReturn<T>,
410
- ): Promise<UseFetchReturn<T>> {
411
- // P2-3: Deduplicate concurrent requests for the same key.
412
- // If another request for this key is already in-flight, share its raw Promise.
413
- let rawPromise: Promise<unknown>
414
-
415
- if (_inflight.has(key)) {
416
- rawPromise = _inflight.get(key)!
417
- } else {
418
- state.pending = true
419
- state.error = null
420
- rawPromise = _fetch(resolvedUrl, buildInit(options))
421
- .then(async (res) => {
422
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
423
- return res.json()
424
- })
425
- .finally(() => { _inflight.delete(key) })
426
- _inflight.set(key, rawPromise)
427
- }
428
-
429
- try {
430
- const raw = await rawPromise
431
- state.data = applyTransform<T>(raw, options)
432
- state.pending = false
433
- } catch (err) {
434
- state.error = err instanceof Error ? err : new Error(String(err))
435
- state.pending = false
436
- }
437
- return state
438
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * Describes the `<head>` tags to inject for a page or component.
3
- * Pass to `useHead()`. All fields are optional; only set fields are emitted.
4
- */
5
- export interface HeadInput {
6
- title?: string
7
- meta?: Array<Record<string, string>>
8
- link?: Array<Record<string, string>>
9
- script?: Array<Record<string, string>>
10
- style?: Array<Record<string, string>>
11
- }
12
-
13
- // SSR: store collector on globalThis so all module instances (server entry +
14
- // dynamically-imported page chunks) share the same reference. A module-level
15
- // variable would create separate instances per chunk, breaking the singleton.
16
- const _g = globalThis as Record<string, unknown>
17
- const _KEY = '__CER_HEAD_COLLECTOR__'
18
-
19
- /**
20
- * Begin collecting head tags for an SSR render pass.
21
- * Call this before invoking the render function.
22
- */
23
- export function beginHeadCollection(): void {
24
- _g[_KEY] = [] as HeadInput[]
25
- }
26
-
27
- /**
28
- * End collecting head tags and return the collected inputs.
29
- * Resets the collector to null.
30
- */
31
- export function endHeadCollection(): HeadInput[] {
32
- const collected = (_g[_KEY] as HeadInput[]) ?? []
33
- _g[_KEY] = null
34
- return collected
35
- }
36
-
37
- /**
38
- * Serializes an array of HeadInput objects into an HTML string
39
- * suitable for injection into the document <head>.
40
- */
41
- export function serializeHeadTags(heads: HeadInput[]): string {
42
- const parts: string[] = []
43
-
44
- // Merge titles — last one wins
45
- let title: string | undefined
46
- for (const head of heads) {
47
- if (head.title !== undefined) {
48
- title = head.title
49
- }
50
- }
51
- if (title !== undefined) {
52
- parts.push(`<title>${escapeHtml(title)}</title>`)
53
- }
54
-
55
- // Merge meta tags, deduplicate by name/property/charset
56
- const metaMap = new Map<string, Record<string, string>>()
57
- for (const head of heads) {
58
- if (head.meta) {
59
- for (const meta of head.meta) {
60
- const key = meta.name ?? meta.property ?? meta.charset ?? JSON.stringify(meta)
61
- metaMap.set(key, meta)
62
- }
63
- }
64
- }
65
- for (const attrs of metaMap.values()) {
66
- parts.push(`<meta ${renderAttrs(attrs)}>`)
67
- }
68
-
69
- // Merge link tags, deduplicate by rel+href
70
- const linkMap = new Map<string, Record<string, string>>()
71
- for (const head of heads) {
72
- if (head.link) {
73
- for (const link of head.link) {
74
- const key = `${link.rel ?? ''}:${link.href ?? ''}`
75
- linkMap.set(key, link)
76
- }
77
- }
78
- }
79
- for (const attrs of linkMap.values()) {
80
- parts.push(`<link ${renderAttrs(attrs)}>`)
81
- }
82
-
83
- // Script tags
84
- for (const head of heads) {
85
- if (head.script) {
86
- for (const script of head.script) {
87
- const { innerHTML, ...rest } = script as Record<string, string>
88
- if (innerHTML !== undefined) {
89
- parts.push(`<script ${renderAttrs(rest)}>${innerHTML}</script>`)
90
- } else {
91
- parts.push(`<script ${renderAttrs(rest)}></script>`)
92
- }
93
- }
94
- }
95
- }
96
-
97
- // Style tags
98
- for (const head of heads) {
99
- if (head.style) {
100
- for (const style of head.style) {
101
- const { innerHTML, ...rest } = style as Record<string, string>
102
- if (innerHTML !== undefined) {
103
- parts.push(`<style ${renderAttrs(rest)}>${innerHTML}</style>`)
104
- } else {
105
- parts.push(`<style ${renderAttrs(rest)}></style>`)
106
- }
107
- }
108
- }
109
- }
110
-
111
- return parts.join('\n')
112
- }
113
-
114
- /**
115
- * useHead composable.
116
- *
117
- * - During SSR: pushes to the request-scoped collector (set up via beginHeadCollection)
118
- * - On the client: imperatively updates document.title and meta/link tags
119
- */
120
- export function useHead(input: HeadInput): void {
121
- const collector = _g[_KEY] as HeadInput[] | null | undefined
122
- if (collector != null) {
123
- // SSR mode: push to the shared globalThis collector
124
- collector.push(input)
125
- } else if (typeof document !== 'undefined') {
126
- // Client-side
127
- if (input.title !== undefined) {
128
- document.title = input.title
129
- }
130
-
131
- if (input.meta) {
132
- for (const meta of input.meta) {
133
- const name = meta.name ?? meta.property
134
- if (name) {
135
- // Try to update existing meta, or create a new one
136
- let el = document.querySelector(
137
- meta.name ? `meta[name="${meta.name}"]` : `meta[property="${meta.property}"]`,
138
- )
139
- if (!el) {
140
- el = document.createElement('meta')
141
- document.head.appendChild(el)
142
- }
143
- for (const [k, v] of Object.entries(meta)) {
144
- el.setAttribute(k, v)
145
- }
146
- }
147
- }
148
- }
149
-
150
- if (input.link) {
151
- for (const link of input.link) {
152
- const rel = link.rel
153
- const href = link.href
154
- if (rel && 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}"]`)
163
- if (!el) {
164
- el = document.createElement('link')
165
- document.head.appendChild(el)
166
- }
167
- for (const [k, v] of Object.entries(link)) {
168
- el.setAttribute(k, v)
169
- }
170
- }
171
- }
172
- }
173
-
174
- if (input.script) {
175
- for (const script of input.script) {
176
- const { innerHTML, src, ...rest } = script as Record<string, string>
177
- // Only add if not already present (by src)
178
- if (src && !document.querySelector(`script[src="${src}"]`)) {
179
- const el = document.createElement('script')
180
- el.src = src
181
- for (const [k, v] of Object.entries(rest)) {
182
- el.setAttribute(k, v)
183
- }
184
- document.head.appendChild(el)
185
- } else if (innerHTML && !src) {
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)
206
- }
207
- }
208
- }
209
- }
210
- }
211
- }
212
-
213
- // ─── Helpers ────────────────────────────────────────────────────────────────
214
-
215
- function escapeHtml(str: string): string {
216
- return str
217
- .replace(/&/g, '&amp;')
218
- .replace(/</g, '&lt;')
219
- .replace(/>/g, '&gt;')
220
- }
221
-
222
- function escapeAttr(value: string): string {
223
- return value
224
- .replace(/&/g, '&amp;')
225
- .replace(/"/g, '&quot;')
226
- .replace(/</g, '&lt;')
227
- .replace(/>/g, '&gt;')
228
- }
229
-
230
- function renderAttrs(attrs: Record<string, string>): string {
231
- return Object.entries(attrs)
232
- .map(([k, v]) => (v === '' ? k : `${k}="${escapeAttr(v)}"`))
233
- .join(' ')
234
- }