@jasonshimmy/vite-plugin-cer-app 0.23.2 → 0.25.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 (404) hide show
  1. package/dist/cli/adapters/netlify.js +4 -2
  2. package/dist/cli/adapters/netlify.js.map +1 -1
  3. package/dist/cli/checks/lighthouse.d.ts +26 -0
  4. package/dist/cli/checks/lighthouse.d.ts.map +1 -0
  5. package/dist/cli/checks/lighthouse.js +121 -0
  6. package/dist/cli/checks/lighthouse.js.map +1 -0
  7. package/dist/cli/checks/links.d.ts +12 -0
  8. package/dist/cli/checks/links.d.ts.map +1 -0
  9. package/dist/cli/checks/links.js +89 -0
  10. package/dist/cli/checks/links.js.map +1 -0
  11. package/dist/cli/checks/performance.d.ts +17 -0
  12. package/dist/cli/checks/performance.d.ts.map +1 -0
  13. package/dist/cli/checks/performance.js +61 -0
  14. package/dist/cli/checks/performance.js.map +1 -0
  15. package/dist/cli/commands/check.d.ts +3 -0
  16. package/dist/cli/commands/check.d.ts.map +1 -0
  17. package/dist/cli/commands/check.js +85 -0
  18. package/dist/cli/commands/check.js.map +1 -0
  19. package/dist/cli/commands/preview.d.ts.map +1 -1
  20. package/dist/cli/commands/preview.js +4 -2
  21. package/dist/cli/commands/preview.js.map +1 -1
  22. package/dist/cli/create/index.d.ts +11 -1
  23. package/dist/cli/create/index.d.ts.map +1 -1
  24. package/dist/cli/create/index.js +135 -65
  25. package/dist/cli/create/index.js.map +1 -1
  26. package/dist/cli/create/templates/spa/package.json.tpl +5 -3
  27. package/dist/cli/create/templates/ssg/cer.config.ts.tpl +2 -0
  28. package/dist/cli/create/templates/ssg/package.json.tpl +5 -3
  29. package/dist/cli/create/templates/ssr/package.json.tpl +5 -3
  30. package/dist/cli/index.js +2 -0
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/plugin/build-ssg.d.ts +22 -6
  35. package/dist/plugin/build-ssg.d.ts.map +1 -1
  36. package/dist/plugin/build-ssg.js +226 -17
  37. package/dist/plugin/build-ssg.js.map +1 -1
  38. package/dist/plugin/content/index.d.ts +1 -1
  39. package/dist/plugin/content/index.d.ts.map +1 -1
  40. package/dist/plugin/content/index.js +8 -8
  41. package/dist/plugin/content/index.js.map +1 -1
  42. package/dist/plugin/content/parser.d.ts +3 -3
  43. package/dist/plugin/content/parser.d.ts.map +1 -1
  44. package/dist/plugin/content/parser.js +103 -24
  45. package/dist/plugin/content/parser.js.map +1 -1
  46. package/dist/plugin/dev-server.d.ts +4 -1
  47. package/dist/plugin/dev-server.d.ts.map +1 -1
  48. package/dist/plugin/dev-server.js.map +1 -1
  49. package/dist/plugin/dts-generator.d.ts.map +1 -1
  50. package/dist/plugin/dts-generator.js +50 -8
  51. package/dist/plugin/dts-generator.js.map +1 -1
  52. package/dist/plugin/generated-dir.d.ts +2 -0
  53. package/dist/plugin/generated-dir.d.ts.map +1 -1
  54. package/dist/plugin/generated-dir.js +17 -6
  55. package/dist/plugin/generated-dir.js.map +1 -1
  56. package/dist/plugin/html-post-process.d.ts +7 -0
  57. package/dist/plugin/html-post-process.d.ts.map +1 -1
  58. package/dist/plugin/html-post-process.js +26 -0
  59. package/dist/plugin/html-post-process.js.map +1 -1
  60. package/dist/plugin/index.d.ts.map +1 -1
  61. package/dist/plugin/index.js +60 -13
  62. package/dist/plugin/index.js.map +1 -1
  63. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  64. package/dist/plugin/transforms/auto-import.js +7 -0
  65. package/dist/plugin/transforms/auto-import.js.map +1 -1
  66. package/dist/plugin/transforms/prune-runtime-css.d.ts +8 -0
  67. package/dist/plugin/transforms/prune-runtime-css.d.ts.map +1 -0
  68. package/dist/plugin/transforms/prune-runtime-css.js +40 -0
  69. package/dist/plugin/transforms/prune-runtime-css.js.map +1 -0
  70. package/dist/runtime/app-template.d.ts +1 -1
  71. package/dist/runtime/app-template.d.ts.map +1 -1
  72. package/dist/runtime/app-template.js +206 -36
  73. package/dist/runtime/app-template.js.map +1 -1
  74. package/dist/runtime/composables/index.d.ts +4 -0
  75. package/dist/runtime/composables/index.d.ts.map +1 -1
  76. package/dist/runtime/composables/index.js +2 -0
  77. package/dist/runtime/composables/index.js.map +1 -1
  78. package/dist/runtime/composables/use-active-headings.d.ts +24 -0
  79. package/dist/runtime/composables/use-active-headings.d.ts.map +1 -0
  80. package/dist/runtime/composables/use-active-headings.js +112 -0
  81. package/dist/runtime/composables/use-active-headings.js.map +1 -0
  82. package/dist/runtime/composables/use-content-page.d.ts +45 -0
  83. package/dist/runtime/composables/use-content-page.d.ts.map +1 -0
  84. package/dist/runtime/composables/use-content-page.js +181 -0
  85. package/dist/runtime/composables/use-content-page.js.map +1 -0
  86. package/dist/runtime/composables/use-content-search.d.ts.map +1 -1
  87. package/dist/runtime/composables/use-content-search.js +1 -5
  88. package/dist/runtime/composables/use-content-search.js.map +1 -1
  89. package/dist/runtime/composables/use-fetch.d.ts +2 -1
  90. package/dist/runtime/composables/use-fetch.d.ts.map +1 -1
  91. package/dist/runtime/composables/use-fetch.js +112 -43
  92. package/dist/runtime/composables/use-fetch.js.map +1 -1
  93. package/dist/runtime/composables/use-route.d.ts.map +1 -1
  94. package/dist/runtime/composables/use-route.js +39 -11
  95. package/dist/runtime/composables/use-route.js.map +1 -1
  96. package/dist/runtime/content/client.js +2 -2
  97. package/dist/runtime/content/client.js.map +1 -1
  98. package/dist/runtime/entry-server-template.d.ts +16 -1
  99. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  100. package/dist/runtime/entry-server-template.js +120 -13
  101. package/dist/runtime/entry-server-template.js.map +1 -1
  102. package/dist/types/config.d.ts +45 -4
  103. package/dist/types/config.d.ts.map +1 -1
  104. package/dist/types/config.js.map +1 -1
  105. package/dist/types/content.d.ts +18 -1
  106. package/dist/types/content.d.ts.map +1 -1
  107. package/dist/types/middleware.d.ts +2 -2
  108. package/dist/types/middleware.d.ts.map +1 -1
  109. package/package.json +22 -19
  110. package/.github/copilot-instructions.md +0 -169
  111. package/.github/workflows/publish.yml +0 -257
  112. package/.nvmrc +0 -1
  113. package/CHANGELOG.md +0 -236
  114. package/CLAUDE.md +0 -113
  115. package/commits.txt +0 -1
  116. package/cypress.config.ts +0 -16
  117. package/docs/authentication.md +0 -285
  118. package/docs/cli.md +0 -324
  119. package/docs/components.md +0 -237
  120. package/docs/composables.md +0 -907
  121. package/docs/configuration.md +0 -696
  122. package/docs/content.md +0 -570
  123. package/docs/data-loading.md +0 -336
  124. package/docs/getting-started.md +0 -237
  125. package/docs/head-management.md +0 -212
  126. package/docs/i18n.md +0 -159
  127. package/docs/layouts.md +0 -224
  128. package/docs/middleware.md +0 -234
  129. package/docs/plugins.md +0 -170
  130. package/docs/rendering-modes.md +0 -377
  131. package/docs/routing.md +0 -485
  132. package/docs/server-api.md +0 -253
  133. package/docs/testing.md +0 -455
  134. package/docs/use-fetch.md +0 -183
  135. package/e2e/cypress/e2e/api.cy.ts +0 -123
  136. package/e2e/cypress/e2e/async-component.cy.ts +0 -27
  137. package/e2e/cypress/e2e/component-splitting.cy.ts +0 -50
  138. package/e2e/cypress/e2e/content.cy.ts +0 -425
  139. package/e2e/cypress/e2e/cookie.cy.ts +0 -85
  140. package/e2e/cypress/e2e/data.cy.ts +0 -158
  141. package/e2e/cypress/e2e/error-boundary.cy.ts +0 -88
  142. package/e2e/cypress/e2e/fouc.cy.ts +0 -254
  143. package/e2e/cypress/e2e/group-meta.cy.ts +0 -36
  144. package/e2e/cypress/e2e/head.cy.ts +0 -89
  145. package/e2e/cypress/e2e/i18n.cy.ts +0 -79
  146. package/e2e/cypress/e2e/interactive.cy.ts +0 -137
  147. package/e2e/cypress/e2e/isr-nested-runtime.cy.ts +0 -102
  148. package/e2e/cypress/e2e/jit-css-custom-colors.cy.ts +0 -43
  149. package/e2e/cypress/e2e/middleware-chain.cy.ts +0 -89
  150. package/e2e/cypress/e2e/middleware-status.cy.ts +0 -30
  151. package/e2e/cypress/e2e/middleware.cy.ts +0 -45
  152. package/e2e/cypress/e2e/navigate.cy.ts +0 -39
  153. package/e2e/cypress/e2e/observability.cy.ts +0 -77
  154. package/e2e/cypress/e2e/per-route-error.cy.ts +0 -42
  155. package/e2e/cypress/e2e/per-route-render.cy.ts +0 -70
  156. package/e2e/cypress/e2e/preview-hardening.cy.ts +0 -88
  157. package/e2e/cypress/e2e/routes.cy.ts +0 -128
  158. package/e2e/cypress/e2e/seo-meta.cy.ts +0 -108
  159. package/e2e/cypress/e2e/server-middleware.cy.ts +0 -46
  160. package/e2e/cypress/e2e/session-rotation.cy.ts +0 -25
  161. package/e2e/cypress/e2e/session.cy.ts +0 -73
  162. package/e2e/cypress/e2e/ssr-render-error.cy.ts +0 -68
  163. package/e2e/cypress/e2e/synthetic-404.cy.ts +0 -45
  164. package/e2e/cypress/e2e/use-page-data.cy.ts +0 -122
  165. package/e2e/cypress/e2e/use-state.cy.ts +0 -138
  166. package/e2e/cypress/support/commands.ts +0 -120
  167. package/e2e/cypress/support/e2e.ts +0 -10
  168. package/e2e/cypress/tsconfig.json +0 -14
  169. package/e2e/kitchen-sink/app/components/ks-badge.ts +0 -8
  170. package/e2e/kitchen-sink/app/composables/useKsCounter.ts +0 -9
  171. package/e2e/kitchen-sink/app/error.ts +0 -18
  172. package/e2e/kitchen-sink/app/layouts/admin.ts +0 -13
  173. package/e2e/kitchen-sink/app/layouts/default.ts +0 -25
  174. package/e2e/kitchen-sink/app/layouts/group.ts +0 -9
  175. package/e2e/kitchen-sink/app/layouts/minimal.ts +0 -7
  176. package/e2e/kitchen-sink/app/loading.ts +0 -9
  177. package/e2e/kitchen-sink/app/middleware/auth.ts +0 -9
  178. package/e2e/kitchen-sink/app/middleware/custom-log.ts +0 -4
  179. package/e2e/kitchen-sink/app/middleware/group-auth.ts +0 -7
  180. package/e2e/kitchen-sink/app/middleware/logger.ts +0 -14
  181. package/e2e/kitchen-sink/app/pages/(auth)/login.ts +0 -13
  182. package/e2e/kitchen-sink/app/pages/(auth)/protected.ts +0 -16
  183. package/e2e/kitchen-sink/app/pages/404.ts +0 -9
  184. package/e2e/kitchen-sink/app/pages/about.ts +0 -17
  185. package/e2e/kitchen-sink/app/pages/admin/_layout.ts +0 -1
  186. package/e2e/kitchen-sink/app/pages/admin/dashboard.ts +0 -11
  187. package/e2e/kitchen-sink/app/pages/async-component-error-test.ts +0 -18
  188. package/e2e/kitchen-sink/app/pages/async-component-test.ts +0 -18
  189. package/e2e/kitchen-sink/app/pages/blog/[slug].ts +0 -59
  190. package/e2e/kitchen-sink/app/pages/blog/index.ts +0 -51
  191. package/e2e/kitchen-sink/app/pages/content-blog.ts +0 -37
  192. package/e2e/kitchen-sink/app/pages/content-doc.ts +0 -42
  193. package/e2e/kitchen-sink/app/pages/content-fallback.ts +0 -36
  194. package/e2e/kitchen-sink/app/pages/content-guides.ts +0 -35
  195. package/e2e/kitchen-sink/app/pages/content-index.ts +0 -39
  196. package/e2e/kitchen-sink/app/pages/content-search.ts +0 -36
  197. package/e2e/kitchen-sink/app/pages/cookie-test.ts +0 -22
  198. package/e2e/kitchen-sink/app/pages/counter.ts +0 -17
  199. package/e2e/kitchen-sink/app/pages/group-auth-blocked.ts +0 -9
  200. package/e2e/kitchen-sink/app/pages/group-meta-test/_layout.ts +0 -6
  201. package/e2e/kitchen-sink/app/pages/group-meta-test/custom-mw.ts +0 -14
  202. package/e2e/kitchen-sink/app/pages/group-meta-test/page.ts +0 -9
  203. package/e2e/kitchen-sink/app/pages/group-meta-test/protected.ts +0 -8
  204. package/e2e/kitchen-sink/app/pages/head.ts +0 -20
  205. package/e2e/kitchen-sink/app/pages/i18n-test.ts +0 -14
  206. package/e2e/kitchen-sink/app/pages/index.ts +0 -27
  207. package/e2e/kitchen-sink/app/pages/isr-test.ts +0 -17
  208. package/e2e/kitchen-sink/app/pages/items/[id].ts +0 -20
  209. package/e2e/kitchen-sink/app/pages/jit-css-custom-colors-test.ts +0 -14
  210. package/e2e/kitchen-sink/app/pages/loader-error-test.ts +0 -13
  211. package/e2e/kitchen-sink/app/pages/loader-props-test.ts +0 -19
  212. package/e2e/kitchen-sink/app/pages/loader-response-error-test.ts +0 -13
  213. package/e2e/kitchen-sink/app/pages/middleware-chain-test.ts +0 -12
  214. package/e2e/kitchen-sink/app/pages/middleware-status-test.ts +0 -9
  215. package/e2e/kitchen-sink/app/pages/navigate-test.ts +0 -16
  216. package/e2e/kitchen-sink/app/pages/observability-test.ts +0 -25
  217. package/e2e/kitchen-sink/app/pages/per-route-error-test.error.ts +0 -13
  218. package/e2e/kitchen-sink/app/pages/per-route-error-test.ts +0 -15
  219. package/e2e/kitchen-sink/app/pages/plugin-dsd-test.ts +0 -10
  220. package/e2e/kitchen-sink/app/pages/render-error-test.ts +0 -12
  221. package/e2e/kitchen-sink/app/pages/render-server-test.ts +0 -12
  222. package/e2e/kitchen-sink/app/pages/render-spa-test.ts +0 -12
  223. package/e2e/kitchen-sink/app/pages/route-info.ts +0 -14
  224. package/e2e/kitchen-sink/app/pages/seo-test.ts +0 -23
  225. package/e2e/kitchen-sink/app/pages/session-rotation-test.ts +0 -25
  226. package/e2e/kitchen-sink/app/pages/use-state-test.ts +0 -37
  227. package/e2e/kitchen-sink/app/plugins/01.setup.ts +0 -7
  228. package/e2e/kitchen-sink/app/plugins/02.ks-plugin-components.ts +0 -27
  229. package/e2e/kitchen-sink/cer-auto-imports.d.ts +0 -51
  230. package/e2e/kitchen-sink/cer-env.d.ts +0 -35
  231. package/e2e/kitchen-sink/cer-tsconfig.json +0 -30
  232. package/e2e/kitchen-sink/cer.config.ts +0 -43
  233. package/e2e/kitchen-sink/content/01.guides/01.index.md +0 -8
  234. package/e2e/kitchen-sink/content/01.guides/02.intro.md +0 -8
  235. package/e2e/kitchen-sink/content/01.guides/10.advanced.md +0 -8
  236. package/e2e/kitchen-sink/content/blog/2026-04-01-hello.md +0 -26
  237. package/e2e/kitchen-sink/content/blog/2026-04-02-draft.md +0 -10
  238. package/e2e/kitchen-sink/content/blog/index.md +0 -8
  239. package/e2e/kitchen-sink/content/blog/no-frontmatter.md +0 -7
  240. package/e2e/kitchen-sink/content/docs/getting-started.md +0 -48
  241. package/e2e/kitchen-sink/content/index.md +0 -16
  242. package/e2e/kitchen-sink/server/api/echo.ts +0 -12
  243. package/e2e/kitchen-sink/server/api/health.ts +0 -3
  244. package/e2e/kitchen-sink/server/api/posts/[slug].ts +0 -11
  245. package/e2e/kitchen-sink/server/api/posts/index.ts +0 -5
  246. package/e2e/kitchen-sink/server/api/session.ts +0 -23
  247. package/e2e/kitchen-sink/server/data/posts.ts +0 -21
  248. package/e2e/kitchen-sink/server/middleware/01-headers.ts +0 -6
  249. package/e2e/kitchen-sink/server/middleware/02-status-test.ts +0 -10
  250. package/e2e/kitchen-sink/tsconfig.json +0 -3
  251. package/e2e/scripts/clean.mjs +0 -14
  252. package/eslint.config.ts +0 -22
  253. package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +0 -132
  254. package/src/__tests__/cli/adapters/cloudflare.test.ts +0 -332
  255. package/src/__tests__/cli/adapters/netlify-bridge.integration.test.ts +0 -139
  256. package/src/__tests__/cli/adapters/netlify.test.ts +0 -286
  257. package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +0 -129
  258. package/src/__tests__/cli/adapters/vercel.test.ts +0 -263
  259. package/src/__tests__/cli/preview-hardening.test.ts +0 -265
  260. package/src/__tests__/cli/preview-isr.test.ts +0 -320
  261. package/src/__tests__/index.test.ts +0 -21
  262. package/src/__tests__/plugin/app-template.test.ts +0 -166
  263. package/src/__tests__/plugin/build-ssg-render.test.ts +0 -155
  264. package/src/__tests__/plugin/build-ssg.test.ts +0 -598
  265. package/src/__tests__/plugin/build-ssr.test.ts +0 -192
  266. package/src/__tests__/plugin/cer-app-plugin.test.ts +0 -552
  267. package/src/__tests__/plugin/content/emitter.test.ts +0 -117
  268. package/src/__tests__/plugin/content/loader.test.ts +0 -154
  269. package/src/__tests__/plugin/content/parser.test.ts +0 -381
  270. package/src/__tests__/plugin/content/path-utils.test.ts +0 -69
  271. package/src/__tests__/plugin/content/search.test.ts +0 -119
  272. package/src/__tests__/plugin/dev-server.test.ts +0 -813
  273. package/src/__tests__/plugin/dts-generator.test.ts +0 -394
  274. package/src/__tests__/plugin/entry-server-template.test.ts +0 -385
  275. package/src/__tests__/plugin/generated-dir.test.ts +0 -144
  276. package/src/__tests__/plugin/html-post-process.test.ts +0 -146
  277. package/src/__tests__/plugin/path-utils.test.ts +0 -411
  278. package/src/__tests__/plugin/resolve-config.test.ts +0 -233
  279. package/src/__tests__/plugin/scanner.test.ts +0 -186
  280. package/src/__tests__/plugin/transforms/auto-import.test.ts +0 -510
  281. package/src/__tests__/plugin/transforms/head-inject.test.ts +0 -178
  282. package/src/__tests__/plugin/virtual/composables.test.ts +0 -57
  283. package/src/__tests__/plugin/virtual/content-components.test.ts +0 -66
  284. package/src/__tests__/plugin/virtual/error.test.ts +0 -71
  285. package/src/__tests__/plugin/virtual/layouts.test.ts +0 -70
  286. package/src/__tests__/plugin/virtual/loading.test.ts +0 -72
  287. package/src/__tests__/plugin/virtual/middleware.test.ts +0 -83
  288. package/src/__tests__/plugin/virtual/plugins.test.ts +0 -129
  289. package/src/__tests__/plugin/virtual/routes.test.ts +0 -884
  290. package/src/__tests__/plugin/virtual/server-api.test.ts +0 -164
  291. package/src/__tests__/plugin/virtual/server-middleware.test.ts +0 -102
  292. package/src/__tests__/runtime/app-template.test.ts +0 -208
  293. package/src/__tests__/runtime/define-middleware.test.ts +0 -64
  294. package/src/__tests__/runtime/define-server-middleware.test.ts +0 -54
  295. package/src/__tests__/runtime/entry-client-template.test.ts +0 -61
  296. package/src/__tests__/runtime/isr-handler.test.ts +0 -338
  297. package/src/__tests__/runtime/oauth/handler.test.ts +0 -358
  298. package/src/__tests__/runtime/oauth/providers.test.ts +0 -47
  299. package/src/__tests__/runtime/oauth-index.test.ts +0 -43
  300. package/src/__tests__/runtime/use-auth.test.ts +0 -169
  301. package/src/__tests__/runtime/use-content-search-composable.test.ts +0 -405
  302. package/src/__tests__/runtime/use-content-search.test.ts +0 -163
  303. package/src/__tests__/runtime/use-content.test.ts +0 -226
  304. package/src/__tests__/runtime/use-cookie.test.ts +0 -277
  305. package/src/__tests__/runtime/use-fetch-component.test.ts +0 -214
  306. package/src/__tests__/runtime/use-fetch.test.ts +0 -377
  307. package/src/__tests__/runtime/use-head.test.ts +0 -311
  308. package/src/__tests__/runtime/use-inject-client.test.ts +0 -67
  309. package/src/__tests__/runtime/use-inject.test.ts +0 -66
  310. package/src/__tests__/runtime/use-locale.test.ts +0 -152
  311. package/src/__tests__/runtime/use-navigate.test.ts +0 -121
  312. package/src/__tests__/runtime/use-page-data.test.ts +0 -298
  313. package/src/__tests__/runtime/use-route.test.ts +0 -156
  314. package/src/__tests__/runtime/use-runtime-config.test.ts +0 -182
  315. package/src/__tests__/runtime/use-seo-meta.test.ts +0 -109
  316. package/src/__tests__/runtime/use-session.test.ts +0 -320
  317. package/src/__tests__/runtime/use-state.test.ts +0 -340
  318. package/src/__tests__/types/config.test.ts +0 -18
  319. package/src/cli/adapters/cloudflare.ts +0 -335
  320. package/src/cli/adapters/netlify.ts +0 -326
  321. package/src/cli/adapters/vercel.ts +0 -311
  322. package/src/cli/commands/adapt.ts +0 -84
  323. package/src/cli/commands/build.ts +0 -165
  324. package/src/cli/commands/dev.ts +0 -125
  325. package/src/cli/commands/generate.ts +0 -91
  326. package/src/cli/commands/preview-isr.ts +0 -172
  327. package/src/cli/commands/preview.ts +0 -523
  328. package/src/cli/create/index.ts +0 -275
  329. package/src/cli/create/templates/shared/.gitignore.tpl +0 -25
  330. package/src/cli/create/templates/shared/app/layouts/default.ts.tpl +0 -15
  331. package/src/cli/create/templates/shared/app/pages/index.ts.tpl +0 -8
  332. package/src/cli/create/templates/shared/index.html.tpl +0 -12
  333. package/src/cli/create/templates/shared/tsconfig.json.tpl +0 -6
  334. package/src/cli/create/templates/spa/cer.config.ts.tpl +0 -6
  335. package/src/cli/create/templates/spa/package.json.tpl +0 -18
  336. package/src/cli/create/templates/ssg/cer.config.ts.tpl +0 -10
  337. package/src/cli/create/templates/ssg/package.json.tpl +0 -19
  338. package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -6
  339. package/src/cli/create/templates/ssr/package.json.tpl +0 -18
  340. package/src/cli/index.ts +0 -22
  341. package/src/index.ts +0 -20
  342. package/src/plugin/build-ssg.ts +0 -403
  343. package/src/plugin/build-ssr.ts +0 -134
  344. package/src/plugin/content/emitter.ts +0 -50
  345. package/src/plugin/content/index.ts +0 -233
  346. package/src/plugin/content/parser.ts +0 -259
  347. package/src/plugin/content/path-utils.ts +0 -55
  348. package/src/plugin/content/scanner.ts +0 -26
  349. package/src/plugin/content/search.ts +0 -28
  350. package/src/plugin/dev-server.ts +0 -379
  351. package/src/plugin/dts-generator.ts +0 -273
  352. package/src/plugin/generated-dir.ts +0 -121
  353. package/src/plugin/html-post-process.ts +0 -96
  354. package/src/plugin/index.ts +0 -584
  355. package/src/plugin/path-utils.ts +0 -186
  356. package/src/plugin/scanner.ts +0 -65
  357. package/src/plugin/transforms/auto-import.ts +0 -206
  358. package/src/plugin/transforms/head-inject.ts +0 -161
  359. package/src/plugin/virtual/composables.ts +0 -28
  360. package/src/plugin/virtual/content-components.ts +0 -83
  361. package/src/plugin/virtual/error.ts +0 -34
  362. package/src/plugin/virtual/layouts.ts +0 -41
  363. package/src/plugin/virtual/loading.ts +0 -32
  364. package/src/plugin/virtual/middleware.ts +0 -45
  365. package/src/plugin/virtual/plugins.ts +0 -46
  366. package/src/plugin/virtual/routes.ts +0 -514
  367. package/src/plugin/virtual/server-api.ts +0 -85
  368. package/src/plugin/virtual/server-middleware.ts +0 -44
  369. package/src/runtime/app-template.ts +0 -377
  370. package/src/runtime/composables/define-middleware.ts +0 -17
  371. package/src/runtime/composables/define-server-middleware.ts +0 -18
  372. package/src/runtime/composables/index.ts +0 -27
  373. package/src/runtime/composables/use-auth.ts +0 -125
  374. package/src/runtime/composables/use-content-search.ts +0 -205
  375. package/src/runtime/composables/use-content.ts +0 -146
  376. package/src/runtime/composables/use-cookie.ts +0 -135
  377. package/src/runtime/composables/use-fetch.ts +0 -438
  378. package/src/runtime/composables/use-head.ts +0 -234
  379. package/src/runtime/composables/use-inject.ts +0 -49
  380. package/src/runtime/composables/use-locale.ts +0 -109
  381. package/src/runtime/composables/use-navigate.ts +0 -58
  382. package/src/runtime/composables/use-page-data.ts +0 -93
  383. package/src/runtime/composables/use-route.ts +0 -71
  384. package/src/runtime/composables/use-runtime-config.ts +0 -123
  385. package/src/runtime/composables/use-seo-meta.ts +0 -80
  386. package/src/runtime/composables/use-session.ts +0 -186
  387. package/src/runtime/composables/use-state.ts +0 -92
  388. package/src/runtime/content/client.ts +0 -168
  389. package/src/runtime/entry-client-template.ts +0 -39
  390. package/src/runtime/entry-server-template.ts +0 -493
  391. package/src/runtime/isr-handler.ts +0 -184
  392. package/src/runtime/oauth/handler.ts +0 -407
  393. package/src/runtime/oauth/index.ts +0 -49
  394. package/src/runtime/oauth/providers.ts +0 -38
  395. package/src/types/api.ts +0 -40
  396. package/src/types/config.ts +0 -379
  397. package/src/types/content.ts +0 -66
  398. package/src/types/index.ts +0 -7
  399. package/src/types/middleware.ts +0 -63
  400. package/src/types/page.ts +0 -117
  401. package/src/types/plugin.ts +0 -34
  402. package/tsconfig.build.json +0 -10
  403. package/tsconfig.json +0 -19
  404. 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
- }