@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Built-in Script handle for injecting <script> tags into the document from
3
+ * route/layout handlers.
4
+ *
5
+ * Push from a SERVER handler with `ctx.use(Script)(config)`; render with the
6
+ * `<Scripts />` component (from `@rangojs/router/client`) placed in the Document
7
+ * `<head>` (and optionally a second `<Scripts position="body" />` at the top of
8
+ * `<body>`). This mirrors the Meta / <MetaTags> pair.
9
+ *
10
+ * The request CSP nonce is applied AUTOMATICALLY by <Scripts> to document-rendered
11
+ * scripts; consumers never pass a nonce. (An async script first loaded on a soft
12
+ * navigation is injected client-side without a nonce — it relies on
13
+ * 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT below and the
14
+ * /scripts skill.) A ScriptConfig is fully serializable (it crosses the
15
+ * server -> client handle-collection boundary), so callbacks like onLoad are NOT
16
+ * supported — a consumer needing them renders their own "use client" script.
17
+ *
18
+ * EXECUTION CONTRACT (see the /scripts skill for the full story):
19
+ * - Inline (`children`) and ordered external (`src`, optional `defer`) scripts
20
+ * are DOCUMENT-LOAD scripts: they execute only when present in the initial HTML
21
+ * response. <Scripts> freezes them after hydration, so a later client (soft)
22
+ * navigation never inserts an inert copy — React creates client-mounted
23
+ * <script> elements via innerHTML, which the HTML spec makes non-executing.
24
+ * - Async external scripts (`src` + `async: true`) are React RESOURCES: they load
25
+ * once when first encountered, including after a soft navigation, deduped by
26
+ * `src`. Use this for a vendor that should load on first visit to a route.
27
+ * - Reusing an `id` shapes the INITIAL document output (last-push-wins); it does
28
+ * not re-run a script during navigation. Per-navigation behavior belongs in a
29
+ * "use client" component or hook (see the GtmPageViews pattern in the demo).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // External async loader (React resource — loads on first visit, even soft nav):
34
+ * ctx.use(Script)({ id: "stripe", src: "https://js.stripe.com/v3", async: true });
35
+ *
36
+ * // Inline bootstrap that self-injects its loader (GTM/GA4) — keep it inline so
37
+ * // React cannot hoist a declarative loader above the bootstrap:
38
+ * ctx.use(Script)({ id: "gtm", children: gtmBootstrap(containerId) });
39
+ *
40
+ * // External ordered (defer) with vendor attributes (document-load):
41
+ * ctx.use(Script)({
42
+ * id: "plausible",
43
+ * src: "https://plausible.io/js/script.js",
44
+ * defer: true,
45
+ * attributes: { "data-domain": "example.com" },
46
+ * });
47
+ * ```
48
+ */
49
+
50
+ import type { ScriptHTMLAttributes } from "react";
51
+ import { createHandle, type Handle } from "../handle.js";
52
+
53
+ /**
54
+ * Extra attributes forwarded onto the emitted <script>. Typed by React, so the
55
+ * casing is React's (`crossOrigin`, not `crossorigin`) and value shapes are
56
+ * checked at compile time. `data-*` attributes are allowed. Two groups are
57
+ * excluded: the fields the Script handle manages itself (`id`, `src`, `async`,
58
+ * `defer`, `type`, `children`, `nonce`, `dangerouslySetInnerHTML` — set those via
59
+ * the ScriptConfig fields), and ALL `on*` event handlers (`onLoad`, `onError`,
60
+ * …): a ScriptConfig is serialized across the server -> client handle boundary, so
61
+ * a function cannot survive it — render your own "use client" script for callbacks.
62
+ */
63
+ export type ScriptAttributes = Omit<
64
+ ScriptHTMLAttributes<HTMLScriptElement>,
65
+ | "id"
66
+ | "src"
67
+ | "async"
68
+ | "defer"
69
+ | "type"
70
+ | "children"
71
+ | "nonce"
72
+ | "dangerouslySetInnerHTML"
73
+ | `on${string}`
74
+ > & {
75
+ [dataAttr: `data-${string}`]: string | number | boolean | undefined;
76
+ };
77
+
78
+ /** Fields shared by every script shape. */
79
+ interface ScriptConfigBase {
80
+ /**
81
+ * Where <Scripts> renders this script.
82
+ * - "head" (default): the `<head>` <Scripts> site.
83
+ * - "body": the `<Scripts position="body" />` site at the top of <body>.
84
+ * Note: an external `async` script is hoisted into <head> by React regardless.
85
+ */
86
+ position?: "head" | "body";
87
+ /**
88
+ * The `type` attribute, as a free string: "module", "application/ld+json",
89
+ * "text/partytown", etc. Omitted means a classic script.
90
+ */
91
+ type?: string;
92
+ /** Extra React-cased attributes (`data-*`, `crossOrigin`, `integrity`, ...). */
93
+ attributes?: ScriptAttributes;
94
+ }
95
+
96
+ /**
97
+ * Inline script: a raw JS body rendered in place, escaped against `</script>`
98
+ * breakout. DOCUMENT-LOAD only (executes when present in the initial HTML;
99
+ * <Scripts> freezes it after hydration so navigation never inserts an inert
100
+ * copy). `id` is REQUIRED — inline scripts are never deduped by React, so a
101
+ * layout and a child pushing the same bootstrap would inject it twice. It is also
102
+ * rendered as the script's DOM `id`. Forbids `src`/`async`/`defer`. For analytics
103
+ * vendors (GTM/GA4/Segment) the body should
104
+ * create+append its own loader, so the loader is never a separate declarative tag
105
+ * React could hoist out of order.
106
+ */
107
+ export interface InlineScriptConfig extends ScriptConfigBase {
108
+ id: string;
109
+ children: string;
110
+ src?: never;
111
+ async?: never;
112
+ defer?: never;
113
+ }
114
+
115
+ /**
116
+ * External async script: a React-hoisted, `src`-deduped RESOURCE (the
117
+ * fire-and-forget loader case). Loads once when first encountered, including
118
+ * after a soft navigation. Deduped by `src` (matching React); `id` is optional
119
+ * and, when set, is rendered as the DOM `id` (not used as the dedup key here).
120
+ * Forbids `children`/`defer`.
121
+ */
122
+ export interface AsyncScriptConfig extends ScriptConfigBase {
123
+ src: string;
124
+ async: true;
125
+ id?: string;
126
+ children?: never;
127
+ defer?: never;
128
+ }
129
+
130
+ /**
131
+ * External ordered script: in-place, optionally `defer`. DOCUMENT-LOAD only
132
+ * (executes when present in the initial HTML; not re-run on navigation). `id` is
133
+ * optional (the dedup key falls back to `src`) and, when set, is rendered as the
134
+ * DOM `id`. Forbids `children`/`async`.
135
+ */
136
+ export interface OrderedScriptConfig extends ScriptConfigBase {
137
+ src: string;
138
+ defer?: boolean;
139
+ id?: string;
140
+ children?: never;
141
+ async?: never;
142
+ }
143
+
144
+ /**
145
+ * A single script to inject, as a discriminated union — exactly one of:
146
+ * inline (`id` + `children`), external async (`src` + `async: true`), or external
147
+ * ordered (`src`, optional `defer`). Invalid combinations (both `src`+`children`,
148
+ * `async`+`defer`, inline without `id`) are compile errors. The CSP nonce is
149
+ * applied by <Scripts>, never here.
150
+ */
151
+ export type ScriptConfig =
152
+ | InlineScriptConfig
153
+ | AsyncScriptConfig
154
+ | OrderedScriptConfig;
155
+
156
+ /** A config's runtime view, for validating untyped/serialized input. */
157
+ type LooseScriptConfig = {
158
+ id?: string;
159
+ src?: string;
160
+ children?: string;
161
+ async?: boolean;
162
+ defer?: boolean;
163
+ };
164
+
165
+ /**
166
+ * Dev-only validation. The discriminated union makes these states unrepresentable
167
+ * in TypeScript; the runtime checks exist only for untyped JavaScript callers and
168
+ * malformed serialized input, not as the primary contract.
169
+ */
170
+ function validateConfigDev(config: ScriptConfig): void {
171
+ if (process.env.NODE_ENV === "production") return;
172
+ const c = config as LooseScriptConfig;
173
+ if (c.src != null && c.children != null) {
174
+ console.warn(
175
+ `[Script] A config has both "src" and "children"; they are mutually ` +
176
+ `exclusive — "src" wins and the inline body is ignored.`,
177
+ );
178
+ } else if (c.src == null && c.children == null) {
179
+ console.warn(
180
+ `[Script] A config has neither "src" nor "children"; it injects nothing.`,
181
+ );
182
+ } else if (c.src == null && c.id == null) {
183
+ console.warn(
184
+ `[Script] An inline script was pushed without an "id" and cannot be ` +
185
+ `deduplicated. Pass an "id" so a layout + child pushing the same script ` +
186
+ `inject it only once.`,
187
+ );
188
+ }
189
+ if (c.async && c.defer) {
190
+ console.warn(
191
+ `[Script] A config has both "async" and "defer"; they are mutually ` +
192
+ `exclusive.`,
193
+ );
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Accumulate scripts across matched segments, parent -> child, preserving push
199
+ * order, last-push-wins per dedup key (mirroring the Meta handle).
200
+ *
201
+ * Dedup key:
202
+ * - async resources key by `src` ONLY — React itself dedups async scripts by src,
203
+ * so two async configs with different ids but the same src must collapse to one
204
+ * here (last wins) for a single, deterministic winner; otherwise React would
205
+ * silently pick one with undefined attribute precedence.
206
+ * - everything else keys by `id ?? src`.
207
+ *
208
+ * An (untyped) inline script with neither `id` nor `src` cannot be deduplicated;
209
+ * it is kept and validateConfigDev warns.
210
+ */
211
+ function collectScripts(segments: ScriptConfig[][]): ScriptConfig[] {
212
+ const result: ScriptConfig[] = [];
213
+ const keyToIndex = new Map<string, number>();
214
+
215
+ for (const configs of segments) {
216
+ for (const config of configs) {
217
+ validateConfigDev(config);
218
+ const isAsyncResource = config.src != null && config.async === true;
219
+ const key = isAsyncResource ? config.src : (config.id ?? config.src);
220
+ if (key === undefined) {
221
+ result.push(config);
222
+ continue;
223
+ }
224
+ const existing = keyToIndex.get(key);
225
+ if (existing !== undefined) {
226
+ result[existing] = config;
227
+ } else {
228
+ keyToIndex.set(key, result.length);
229
+ result.push(config);
230
+ }
231
+ }
232
+ }
233
+
234
+ return result;
235
+ }
236
+
237
+ /**
238
+ * Built-in handle for injecting scripts. Uses an explicit stable id (built-ins
239
+ * do not rely on the Vite id-injection plugin, which only covers consumer code).
240
+ */
241
+ export const Script: Handle<ScriptConfig, ScriptConfig[]> = createHandle<
242
+ ScriptConfig,
243
+ ScriptConfig[]
244
+ >(collectScripts, "__rsc_router_script__");
@@ -1,9 +1,3 @@
1
- /**
2
- * Cookie Override Handler
3
- *
4
- * Manages cookie-based host override for development environments.
5
- */
6
-
7
1
  import type { HostOverrideConfig } from "./types.js";
8
2
  import type { RouterRequestInput } from "../router/router-interfaces.js";
9
3
  import { matchPattern, parseRequest } from "./pattern-matcher.js";
@@ -12,52 +6,21 @@ import {
12
6
  InvalidHostnameError,
13
7
  HostValidationError,
14
8
  } from "./errors.js";
9
+ import { parseCookiesFromHeader } from "../server/cookie-parse.js";
15
10
 
16
- /**
17
- * Parse cookies from request
18
- */
19
11
  export function parseCookies(request: Request): Record<string, string> {
20
- const cookieHeader = request.headers.get("cookie");
21
- if (!cookieHeader) {
22
- return {};
23
- }
24
-
25
- const cookies: Record<string, string> = {};
26
- const pairs = cookieHeader.split(";");
27
-
28
- for (const pair of pairs) {
29
- const [name, ...rest] = pair.trim().split("=");
30
- if (name && rest.length > 0) {
31
- const value = rest.join("=");
32
- try {
33
- cookies[name] = decodeURIComponent(value);
34
- } catch {
35
- cookies[name] = value;
36
- }
37
- }
38
- }
39
-
40
- return cookies;
12
+ return parseCookiesFromHeader(request.headers.get("cookie"));
41
13
  }
42
14
 
43
- /**
44
- * Get cookie value from request
45
- */
46
15
  export function getCookie(request: Request, name: string): string | undefined {
47
16
  const cookies = parseCookies(request);
48
17
  return cookies[name];
49
18
  }
50
19
 
51
- /**
52
- * Create Set-Cookie header to delete a cookie
53
- */
54
20
  export function createDeleteCookieHeader(name: string): string {
55
21
  return `${name}=; Max-Age=0; Path=/; Secure; HttpOnly`;
56
22
  }
57
23
 
58
- /**
59
- * Create error response with cookie deletion
60
- */
61
24
  export function createCookieErrorResponse(
62
25
  cookieName: string,
63
26
  message: string,
@@ -77,9 +40,6 @@ export function createCookieErrorResponse(
77
40
  );
78
41
  }
79
42
 
80
- /**
81
- * Check if current host is allowed to use override
82
- */
83
43
  export function isHostAllowed(
84
44
  request: Request,
85
45
  allowedHosts: string[],
@@ -95,12 +55,6 @@ export function isHostAllowed(
95
55
  return false;
96
56
  }
97
57
 
98
- /**
99
- * Handle cookie override logic
100
- *
101
- * Returns overridden hostname if valid, original hostname if no override.
102
- * Throws errors for invalid overrides.
103
- */
104
58
  export function handleCookieOverride(
105
59
  request: Request,
106
60
  config: HostOverrideConfig | undefined,
@@ -115,51 +69,46 @@ export function handleCookieOverride(
115
69
  const cookieValue = getCookie(request, cookieName);
116
70
  const { hostname: originalHostname } = parseRequest(request);
117
71
 
118
- // No cookie - return original hostname
119
72
  if (!cookieValue) {
120
73
  return originalHostname;
121
74
  }
122
75
 
123
- // Check if current host is allowed
124
76
  const allowed = isHostAllowed(request, allowedHosts);
125
77
 
126
- // If not allowed, throw error
127
78
  if (!allowed) {
128
79
  throw new HostOverrideNotAllowedError(originalHostname, cookieName, {
129
80
  cause: { cookieValue, currentHost: originalHostname },
130
81
  });
131
82
  }
132
83
 
133
- // If allowed and has custom validation, run it
134
84
  if (validate) {
135
85
  try {
136
86
  const validatedHostname = validate(request, cookieValue, input);
137
87
  return validatedHostname;
138
88
  } catch (error) {
139
- // Wrap in HostValidationError
140
89
  const message = error instanceof Error ? error.message : String(error);
141
90
  throw new HostValidationError(message, error);
142
91
  }
143
92
  }
144
93
 
145
- // Default validation - verify it's a valid hostname using URL constructor
94
+ // URL.hostname ASCII-lowercases the host, so compare the cookie value against
95
+ // its canonical lowercase form (a mixed-case host is valid) and reject only
96
+ // when it carries a path/port. Return the canonical host so downstream
97
+ // matching, which assumes lowercase, sees a consistent value.
146
98
  try {
147
- // Try to construct a URL with the hostname to validate it
148
99
  const testUrl = new URL(`https://${cookieValue}`);
149
100
 
150
- // Ensure the hostname matches what we provided (URL constructor normalizes it)
151
- if (testUrl.hostname !== cookieValue) {
101
+ if (testUrl.hostname !== cookieValue.toLowerCase()) {
152
102
  throw new InvalidHostnameError(cookieValue, {
153
103
  cause: { original: cookieValue, normalized: testUrl.hostname },
154
104
  });
155
105
  }
106
+
107
+ return testUrl.hostname;
156
108
  } catch (error) {
157
- // If URL constructor failed, throw InvalidHostnameError with cause
158
109
  if (error instanceof InvalidHostnameError) {
159
110
  throw error;
160
111
  }
161
112
  throw new InvalidHostnameError(cookieValue, { cause: error });
162
113
  }
163
-
164
- return cookieValue;
165
114
  }
@@ -4,16 +4,10 @@
4
4
  * All host router errors extend HostRouterError for easy instance checking.
5
5
  */
6
6
 
7
- /**
8
- * Error options with cause
9
- */
10
7
  interface ErrorOptions {
11
8
  cause?: unknown;
12
9
  }
13
10
 
14
- /**
15
- * Base error class for all host router errors
16
- */
17
11
  export class HostRouterError extends Error {
18
12
  cause?: unknown;
19
13
 
@@ -27,9 +21,6 @@ export class HostRouterError extends Error {
27
21
  }
28
22
  }
29
23
 
30
- /**
31
- * Error thrown when pattern validation fails
32
- */
33
24
  export class InvalidPatternError extends HostRouterError {
34
25
  constructor(pattern: string, reason: string, options?: ErrorOptions) {
35
26
  super(`Invalid pattern "${pattern}": ${reason}`, options);
@@ -38,9 +29,6 @@ export class InvalidPatternError extends HostRouterError {
38
29
  }
39
30
  }
40
31
 
41
- /**
42
- * Error thrown when cookie override is not allowed
43
- */
44
32
  export class HostOverrideNotAllowedError extends HostRouterError {
45
33
  constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
46
34
  super(
@@ -52,9 +40,6 @@ export class HostOverrideNotAllowedError extends HostRouterError {
52
40
  }
53
41
  }
54
42
 
55
- /**
56
- * Error thrown when cookie hostname is invalid
57
- */
58
43
  export class InvalidHostnameError extends HostRouterError {
59
44
  constructor(hostname: string, options?: ErrorOptions) {
60
45
  super(`Invalid hostname format: "${hostname}"`, options);
@@ -63,9 +48,6 @@ export class InvalidHostnameError extends HostRouterError {
63
48
  }
64
49
  }
65
50
 
66
- /**
67
- * Error thrown when custom validation fails
68
- */
69
51
  export class HostValidationError extends HostRouterError {
70
52
  constructor(message: string, cause?: unknown) {
71
53
  super(message, { cause });
@@ -74,9 +56,6 @@ export class HostValidationError extends HostRouterError {
74
56
  }
75
57
  }
76
58
 
77
- /**
78
- * Error thrown when no route matches
79
- */
80
59
  export class NoRouteMatchError extends HostRouterError {
81
60
  constructor(hostname: string, pathname: string, options?: ErrorOptions) {
82
61
  super(`No route matched for ${hostname}${pathname}`, options);
@@ -85,9 +64,6 @@ export class NoRouteMatchError extends HostRouterError {
85
64
  }
86
65
  }
87
66
 
88
- /**
89
- * Error thrown when handler type is invalid
90
- */
91
67
  export class InvalidHandlerError extends HostRouterError {
92
68
  constructor(handler: unknown, options?: ErrorOptions) {
93
69
  super(`Invalid handler type: ${typeof handler}`, options);
package/src/host/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * const router = createHostRouter();
13
13
  *
14
- * router.host(['.']).map(() => import('./apps/main'));
15
- * router.host(['admin.*']).map(() => import('./apps/admin'));
14
+ * router.host(['.']).lazy(() => import('./apps/main'));
15
+ * router.host(['admin.*']).lazy(() => import('./apps/admin'));
16
16
  *
17
17
  * export default {
18
18
  * fetch(request) {
@@ -20,6 +20,12 @@
20
20
  * }
21
21
  * };
22
22
  * ```
23
+ *
24
+ * The host surface (`Handler`, `Middleware`, `match`, `HostOverrideConfig.validate`)
25
+ * types `input` as `RouterRequestInput<any>` by design: a host router fans out to
26
+ * heterogeneous sub-apps with differing env/vars shapes, so there is no single
27
+ * `TEnv`/`TVars` to thread through. `input.env`/`input.vars` are therefore `any`
28
+ * here; the typed env shape lives on each sub-app's `createRouter<TEnv>()`.
23
29
  */
24
30
 
25
31
  // Core router
@@ -12,15 +12,18 @@
12
12
  * - `**.example.com` - any depth subdomain
13
13
  * - `admin.*` - admin subdomain of any apex
14
14
  * - `example.com/admin` - specific domain with path prefix
15
+ *
16
+ * Apex vs subdomain is classified purely by dot-part COUNT (apex == exactly 2
17
+ * parts) — there is no Public Suffix List. A registrable domain under a
18
+ * multi-label public suffix (example.co.uk, shop.com.au) has 3+ parts and is
19
+ * therefore treated as a SUBDOMAIN, not an apex: `.`/`*` will NOT match it and
20
+ * `*.` WILL. If registrable-domain accuracy matters for a host-router consumer,
21
+ * supply an explicit apex/host hint rather than relying on the part count.
15
22
  */
16
23
 
17
24
  import { InvalidPatternError } from "./errors.js";
18
25
 
19
- /**
20
- * Normalize a pattern by removing trailing slashes from paths
21
- */
22
26
  export function normalizePattern(pattern: string): string {
23
- // If pattern has a path component, remove trailing slash
24
27
  const slashIndex = pattern.indexOf("/");
25
28
  if (slashIndex !== -1) {
26
29
  const domain = pattern.slice(0, slashIndex);
@@ -30,9 +33,6 @@ export function normalizePattern(pattern: string): string {
30
33
  return pattern;
31
34
  }
32
35
 
33
- /**
34
- * Parse hostname and path from request URL
35
- */
36
36
  export function parseRequest(request: Request): {
37
37
  hostname: string;
38
38
  pathname: string;
@@ -46,26 +46,14 @@ export function parseRequest(request: Request): {
46
46
  return { hostname, pathname, parts };
47
47
  }
48
48
 
49
- /**
50
- * Count subdomain levels (0 for apex, 1+ for subdomains)
51
- */
52
49
  function getSubdomainLevel(parts: string[]): number {
53
- // Apex domain has 2 parts (example.com)
54
- // Single subdomain has 3 parts (www.example.com)
55
- // Multi-level has 4+ parts (a.b.example.com)
56
50
  return Math.max(0, parts.length - 2);
57
51
  }
58
52
 
59
- /**
60
- * Check if hostname is an apex domain (no subdomains)
61
- */
62
53
  function isApexDomain(parts: string[]): boolean {
63
54
  return parts.length === 2;
64
55
  }
65
56
 
66
- /**
67
- * Match a single pattern against hostname and path
68
- */
69
57
  export function matchPattern(
70
58
  pattern: string,
71
59
  hostname: string,
@@ -74,19 +62,30 @@ export function matchPattern(
74
62
  ): boolean {
75
63
  const normalized = normalizePattern(pattern);
76
64
 
77
- // Check if pattern has path component
78
65
  const slashIndex = normalized.indexOf("/");
79
66
  const hasPath = slashIndex !== -1;
80
- const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
67
+ // Hosts are case-insensitive (RFC 3986): lowercase the domain literal and the
68
+ // request host once so matching folds case. Wildcards (*, **, .) are
69
+ // unaffected by lowercasing. The path is left untouched (paths are
70
+ // case-sensitive).
71
+ const domainPattern = (
72
+ hasPath ? normalized.slice(0, slashIndex) : normalized
73
+ ).toLowerCase();
81
74
  const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
82
75
 
83
- // First match domain
84
- const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
76
+ const lowerHostname = hostname.toLowerCase();
77
+ const lowerParts =
78
+ lowerHostname === hostname ? parts : lowerHostname.split(".");
79
+
80
+ const domainMatch = matchDomainPattern(
81
+ domainPattern,
82
+ lowerHostname,
83
+ lowerParts,
84
+ );
85
85
  if (!domainMatch) {
86
86
  return false;
87
87
  }
88
88
 
89
- // Then match path (prefix match)
90
89
  if (pathPattern) {
91
90
  return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
92
91
  }
@@ -94,81 +93,62 @@ export function matchPattern(
94
93
  return true;
95
94
  }
96
95
 
97
- /**
98
- * Match domain pattern against hostname
99
- */
100
96
  function matchDomainPattern(
101
97
  pattern: string,
102
98
  hostname: string,
103
99
  parts: string[],
104
100
  ): boolean {
105
- // Exact match
106
101
  if (pattern === hostname) {
107
102
  return true;
108
103
  }
109
104
 
110
- // `.` or `*` - any apex domain
111
105
  if (pattern === "." || pattern === "*") {
112
106
  return isApexDomain(parts);
113
107
  }
114
108
 
115
- // `**` - any domain (apex + all subdomains)
116
109
  if (pattern === "**") {
117
110
  return true;
118
111
  }
119
112
 
120
- // `*.` - any single-level subdomain
121
113
  if (pattern === "*.") {
122
114
  return getSubdomainLevel(parts) === 1;
123
115
  }
124
116
 
125
- // `**.` - any multi-level subdomain (2+ levels)
126
117
  if (pattern === "**.") {
127
118
  return getSubdomainLevel(parts) >= 2;
128
119
  }
129
120
 
130
- // `*.tld` - any apex domain with specific TLD (e.g., *.com)
131
121
  if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
132
122
  const tld = pattern.slice(2);
133
123
  return isApexDomain(parts) && hostname.endsWith("." + tld);
134
124
  }
135
125
 
136
- // `*.example.com` - single subdomain of specific domain
137
126
  if (pattern.startsWith("*.")) {
138
127
  const baseDomain = pattern.slice(2);
139
128
  if (hostname.endsWith("." + baseDomain)) {
140
- // Count parts: if pattern is *.example.com (3 parts),
141
- // hostname should have exactly 4 parts (www.example.com)
142
129
  const patternParts = baseDomain.split(".");
143
130
  return parts.length === patternParts.length + 1;
144
131
  }
145
132
  return false;
146
133
  }
147
134
 
148
- // `**.example.com` - any depth subdomain of specific domain
149
135
  if (pattern.startsWith("**.")) {
150
136
  const baseDomain = pattern.slice(3);
151
137
  if (hostname.endsWith("." + baseDomain)) {
152
138
  const patternParts = baseDomain.split(".");
153
- // Must have more parts than the base domain (i.e., has subdomains)
154
139
  return parts.length > patternParts.length;
155
140
  }
156
141
  return false;
157
142
  }
158
143
 
159
- // `subdomain.*` - specific subdomain of any apex domain
160
- // e.g., admin.* matches admin.example.com, admin.google.com
161
144
  if (pattern.endsWith(".*")) {
162
145
  const subdomain = pattern.slice(0, -2);
163
- // Must be single-level subdomain (3 parts total)
164
146
  if (parts.length === 3 && parts[0] === subdomain) {
165
147
  return true;
166
148
  }
167
149
  return false;
168
150
  }
169
151
 
170
- // `subdomain.**` - specific subdomain of any domain (including multi-level)
171
- // e.g., admin.** matches admin.example.com, admin.sub.example.com
172
152
  if (pattern.endsWith(".**")) {
173
153
  const subdomain = pattern.slice(0, -3);
174
154
  if (parts.length >= 3 && parts[0] === subdomain) {
@@ -177,11 +157,8 @@ function matchDomainPattern(
177
157
  return false;
178
158
  }
179
159
 
180
- // `subdomain.` - specific subdomain of any apex domain (no wildcard)
181
- // e.g., admin. matches admin.example.com, admin.google.com
182
160
  if (pattern.endsWith(".") && !pattern.includes("*")) {
183
161
  const subdomain = pattern.slice(0, -1);
184
- // Must be exactly 3 parts (subdomain.domain.tld)
185
162
  if (parts.length === 3 && parts[0] === subdomain) {
186
163
  return true;
187
164
  }
@@ -191,9 +168,6 @@ function matchDomainPattern(
191
168
  return false;
192
169
  }
193
170
 
194
- /**
195
- * Validate pattern format
196
- */
197
171
  export function validatePattern(pattern: string): void {
198
172
  if (!pattern || typeof pattern !== "string") {
199
173
  throw new InvalidPatternError(
@@ -203,12 +177,9 @@ export function validatePattern(pattern: string): void {
203
177
  );
204
178
  }
205
179
 
206
- // Check for invalid characters (spaces, etc.)
207
180
  if (/\s/.test(pattern)) {
208
181
  throw new InvalidPatternError(pattern, "contains whitespace", {
209
182
  cause: { pattern },
210
183
  });
211
184
  }
212
-
213
- // Additional validation can be added here
214
185
  }