@benjavicente/angular-router-experimental 1.142.11

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
  4. package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
  5. package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
  6. package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
  7. package/experimental/injectRouteErrorHandler.ts +51 -0
  8. package/experimental/public_api.ts +8 -0
  9. package/package.json +98 -0
  10. package/src/DefaultNotFound.ts +9 -0
  11. package/src/Link.ts +352 -0
  12. package/src/Match.ts +338 -0
  13. package/src/Matches.ts +37 -0
  14. package/src/RouterProvider.ts +162 -0
  15. package/src/document/build-match-managed-document.ts +308 -0
  16. package/src/document/document-dehydration.ts +27 -0
  17. package/src/document/document-equality.ts +29 -0
  18. package/src/document/document-router-token.ts +6 -0
  19. package/src/document/index.ts +33 -0
  20. package/src/document/install-unified-document-sync.ts +108 -0
  21. package/src/document/managed-document-types.ts +36 -0
  22. package/src/document/managed-dom.ts +307 -0
  23. package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
  24. package/src/document/provide-tanstack-document-title.ts +59 -0
  25. package/src/document/provide-tanstack-document.ts +62 -0
  26. package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
  27. package/src/fileRoute.ts +232 -0
  28. package/src/index.ts +173 -0
  29. package/src/injectBlocker.ts +196 -0
  30. package/src/injectCanGoBack.ts +11 -0
  31. package/src/injectErrorState.ts +21 -0
  32. package/src/injectIntersectionObserver.ts +28 -0
  33. package/src/injectLoaderData.ts +49 -0
  34. package/src/injectLoaderDeps.ts +45 -0
  35. package/src/injectLocation.ts +38 -0
  36. package/src/injectMatch.ts +122 -0
  37. package/src/injectMatchRoute.ts +58 -0
  38. package/src/injectMatches.ts +79 -0
  39. package/src/injectNavigate.ts +24 -0
  40. package/src/injectParams.ts +71 -0
  41. package/src/injectRouteContext.ts +31 -0
  42. package/src/injectRouter.ts +17 -0
  43. package/src/injectRouterState.ts +53 -0
  44. package/src/injectSearch.ts +71 -0
  45. package/src/injectStore.ts +87 -0
  46. package/src/matchInjectorToken.ts +23 -0
  47. package/src/renderer/injectIsCatchingError.ts +40 -0
  48. package/src/renderer/injectRender.ts +69 -0
  49. package/src/route.ts +641 -0
  50. package/src/router.ts +141 -0
  51. package/src/routerInjectionToken.ts +24 -0
  52. package/src/routerStores.ts +107 -0
  53. package/src/ssr-scroll-restoration.ts +48 -0
  54. package/src/transitioner.ts +255 -0
@@ -0,0 +1,308 @@
1
+ import { escapeHtml } from '@benjavicente/router-core'
2
+ import {
3
+ collectDehydrationScriptManagedTags,
4
+ mergeDehydrationPrefixIntoDocument,
5
+ } from './document-dehydration'
6
+ import { normalizeManagedTag, uniqManagedTags } from './managed-dom'
7
+ import type { AnyRouter, RouterManagedTag } from '@benjavicente/router-core'
8
+ import type { ManagedDocumentContent, RouteMetaEntry } from './managed-document-types'
9
+
10
+ /**
11
+ * Document content from the current route tree only (meta, links, route body scripts, etc.).
12
+ * Does not call `takeBufferedScripts` — mirrors the route-derived arrays passed into
13
+ * `renderScripts` before the server-buffered script is unshifted in React/Solid/Vue.
14
+ */
15
+ export function buildMatchManagedDocumentContent(
16
+ router: AnyRouter,
17
+ ): ManagedDocumentContent {
18
+ const nonce = router.options.ssr?.nonce
19
+ const matches = router.stores.activeMatchesSnapshot.state
20
+ const title = selectTitle(matches)
21
+ const metaTags = selectMetaTags(matches, nonce)
22
+ const links = selectConstructedLinks(matches, nonce)
23
+ const manifestLinks = selectManifestLinks(router, matches, nonce)
24
+ const preloadLinks = selectPreloadLinks(router, matches, nonce)
25
+ const styles = selectStyles(matches, nonce)
26
+ const headScripts = selectHeadScripts(matches, nonce)
27
+ const routeScripts = selectRouteScripts(matches, nonce)
28
+ const manifestScripts = selectManifestScripts(router, matches, nonce)
29
+
30
+ const head = uniqManagedTags(
31
+ [
32
+ ...metaTags,
33
+ ...preloadLinks,
34
+ ...links,
35
+ ...manifestLinks,
36
+ ...styles,
37
+ ...headScripts,
38
+ ].flatMap((tag) => normalizeManagedTag(tag)),
39
+ )
40
+
41
+ const body = uniqManagedTags(
42
+ [...routeScripts, ...manifestScripts].flatMap((tag) =>
43
+ normalizeManagedTag(tag),
44
+ ),
45
+ )
46
+
47
+ return {
48
+ title,
49
+ head,
50
+ body,
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Full managed document for a single snapshot: match-derived tags plus one
56
+ * `takeBufferedScripts()` read (for tests and tooling). Prefer composing
57
+ * `buildMatchManagedDocumentContent` + a one-time dehydration prefix in SSR, as
58
+ * the body managed-tags provider does.
59
+ */
60
+ export function buildManagedDocumentContent(
61
+ router: AnyRouter,
62
+ ): ManagedDocumentContent {
63
+ return mergeDehydrationPrefixIntoDocument(
64
+ buildMatchManagedDocumentContent(router),
65
+ collectDehydrationScriptManagedTags(router),
66
+ )
67
+ }
68
+
69
+ function selectTitle(matches: Array<any>) {
70
+ const routeMeta = selectRouteMeta(matches)
71
+
72
+ for (let i = routeMeta.length - 1; i >= 0; i--) {
73
+ const metas = routeMeta[i]!
74
+ for (let j = metas.length - 1; j >= 0; j--) {
75
+ const meta = metas[j]
76
+ if (meta?.title) {
77
+ return meta.title
78
+ }
79
+ }
80
+ }
81
+
82
+ return undefined
83
+ }
84
+
85
+ function selectMetaTags(
86
+ matches: Array<any>,
87
+ nonce: string | undefined,
88
+ ) {
89
+ const routeMeta = selectRouteMeta(matches)
90
+ const metaTags: Array<RouterManagedTag> = []
91
+ const metaByAttribute: Record<string, true> = {}
92
+
93
+ for (let i = routeMeta.length - 1; i >= 0; i--) {
94
+ const metas = routeMeta[i]!
95
+ for (let j = metas.length - 1; j >= 0; j--) {
96
+ const m = metas[j]
97
+ if (!m || m.title) continue
98
+
99
+ if ('script:ld+json' in m && m['script:ld+json'] !== undefined) {
100
+ try {
101
+ const json = JSON.stringify(m['script:ld+json'])
102
+ metaTags.push({
103
+ tag: 'script',
104
+ attrs: {
105
+ type: 'application/ld+json',
106
+ nonce,
107
+ },
108
+ children: escapeHtml(json),
109
+ })
110
+ } catch {
111
+ // Skip invalid JSON-LD objects
112
+ }
113
+ continue
114
+ }
115
+
116
+ const attribute = m.name ?? m.property
117
+ if (attribute) {
118
+ if (metaByAttribute[attribute]) {
119
+ continue
120
+ }
121
+ metaByAttribute[attribute] = true
122
+ }
123
+
124
+ metaTags.push({
125
+ tag: 'meta',
126
+ attrs: {
127
+ ...m,
128
+ nonce,
129
+ },
130
+ })
131
+ }
132
+ }
133
+
134
+ if (nonce) {
135
+ metaTags.push({
136
+ tag: 'meta',
137
+ attrs: {
138
+ property: 'csp-nonce',
139
+ content: nonce,
140
+ },
141
+ })
142
+ }
143
+
144
+ metaTags.reverse()
145
+
146
+ return metaTags
147
+ }
148
+
149
+ function selectConstructedLinks(
150
+ matches: Array<any>,
151
+ nonce: string | undefined,
152
+ ) {
153
+ return matches
154
+ .map((match) => match.links!)
155
+ .filter(Boolean)
156
+ .flat(1)
157
+ .map(
158
+ (link) =>
159
+ ({
160
+ tag: 'link',
161
+ attrs: {
162
+ ...link,
163
+ nonce,
164
+ },
165
+ }) satisfies RouterManagedTag,
166
+ )
167
+ }
168
+
169
+ function selectManifestLinks(
170
+ router: AnyRouter,
171
+ matches: Array<any>,
172
+ nonce: string | undefined,
173
+ ) {
174
+ return matches
175
+ .map((match) => router.ssr?.manifest?.routes[match.routeId]?.assets ?? [])
176
+ .filter(Boolean)
177
+ .flat(1)
178
+ .filter((asset) => asset.tag === 'link')
179
+ .map(
180
+ (asset) =>
181
+ ({
182
+ tag: 'link',
183
+ attrs: {
184
+ ...asset.attrs,
185
+ suppressHydrationWarning: true,
186
+ nonce,
187
+ },
188
+ }) satisfies RouterManagedTag,
189
+ )
190
+ }
191
+
192
+ function selectPreloadLinks(
193
+ router: AnyRouter,
194
+ matches: Array<any>,
195
+ nonce: string | undefined,
196
+ ) {
197
+ const preloadLinks: Array<RouterManagedTag> = []
198
+
199
+ matches
200
+ .map((match) => router.looseRoutesById[match.routeId]!)
201
+ .forEach((route) =>
202
+ router.ssr?.manifest?.routes[route.id]?.preloads
203
+ ?.filter(Boolean)
204
+ .forEach((preload) => {
205
+ preloadLinks.push({
206
+ tag: 'link',
207
+ attrs: {
208
+ rel: 'modulepreload',
209
+ href: preload,
210
+ nonce,
211
+ },
212
+ })
213
+ }),
214
+ )
215
+
216
+ return preloadLinks
217
+ }
218
+
219
+ function selectStyles(
220
+ matches: Array<any>,
221
+ nonce: string | undefined,
222
+ ) {
223
+ return (
224
+ matches
225
+ .map((match) => match.styles!)
226
+ .flat(1)
227
+ .filter(Boolean) as Array<Record<string, unknown>>
228
+ ).map(({ children, ...attrs }) => ({
229
+ tag: 'style',
230
+ attrs: {
231
+ ...attrs,
232
+ nonce,
233
+ },
234
+ children: typeof children === 'string' ? children : undefined,
235
+ })) satisfies Array<RouterManagedTag>
236
+ }
237
+
238
+ function selectHeadScripts(
239
+ matches: Array<any>,
240
+ nonce: string | undefined,
241
+ ) {
242
+ return (
243
+ matches
244
+ .map((match) => match.headScripts!)
245
+ .flat(1)
246
+ .filter(Boolean) as Array<Record<string, unknown>>
247
+ ).map(({ children, ...script }) => ({
248
+ tag: 'script',
249
+ attrs: {
250
+ ...script,
251
+ nonce,
252
+ },
253
+ children: typeof children === 'string' ? children : undefined,
254
+ })) satisfies Array<RouterManagedTag>
255
+ }
256
+
257
+ function selectRouteScripts(
258
+ matches: Array<any>,
259
+ nonce: string | undefined,
260
+ ) {
261
+ return (
262
+ matches
263
+ .map((match) => match.scripts!)
264
+ .flat(1)
265
+ .filter(Boolean) as Array<Record<string, unknown>>
266
+ ).map(({ children, ...script }) => ({
267
+ tag: 'script',
268
+ attrs: {
269
+ ...script,
270
+ suppressHydrationWarning: true,
271
+ nonce,
272
+ },
273
+ children: typeof children === 'string' ? children : undefined,
274
+ })) satisfies Array<RouterManagedTag>
275
+ }
276
+
277
+ function selectManifestScripts(
278
+ router: AnyRouter,
279
+ matches: Array<any>,
280
+ nonce: string | undefined,
281
+ ) {
282
+ return matches
283
+ .map((match) => router.looseRoutesById[match.routeId]!)
284
+ .flatMap((route) =>
285
+ router.ssr?.manifest?.routes[route.id]?.assets
286
+ ?.filter((asset) => asset.tag === 'script')
287
+ .map(
288
+ (asset) =>
289
+ ({
290
+ tag: 'script',
291
+ attrs: {
292
+ ...asset.attrs,
293
+ nonce,
294
+ },
295
+ children: asset.children,
296
+ }) satisfies RouterManagedTag,
297
+ ) ?? [],
298
+ )
299
+ }
300
+
301
+ function selectRouteMeta(matches: Array<any>) {
302
+ return matches.reduce<Array<Array<RouteMetaEntry>>>((acc, match) => {
303
+ if (match.meta) {
304
+ acc.push(match.meta as Array<RouteMetaEntry>)
305
+ }
306
+ return acc
307
+ }, [])
308
+ }
@@ -0,0 +1,27 @@
1
+ import { normalizeManagedTag, uniqManagedTags } from './managed-dom'
2
+ import type { AnyRouter } from '@benjavicente/router-core'
3
+ import type { ManagedDocumentContent, ManagedTag } from './managed-document-types'
4
+
5
+ export function collectDehydrationScriptManagedTags(
6
+ router: AnyRouter,
7
+ ): Array<ManagedTag> {
8
+ return router.serverSsr
9
+ ? uniqManagedTags(
10
+ normalizeManagedTag(router.serverSsr.takeBufferedScripts()),
11
+ )
12
+ : []
13
+ }
14
+
15
+ export function mergeDehydrationPrefixIntoDocument(
16
+ matchContent: ManagedDocumentContent,
17
+ dehydrationPrefix: Array<ManagedTag>,
18
+ ): ManagedDocumentContent {
19
+ if (dehydrationPrefix.length === 0) {
20
+ return matchContent
21
+ }
22
+
23
+ return {
24
+ ...matchContent,
25
+ body: uniqManagedTags([...dehydrationPrefix, ...matchContent.body]),
26
+ }
27
+ }
@@ -0,0 +1,29 @@
1
+ import type { ManagedDocumentContent, ManagedTag } from './managed-document-types'
2
+
3
+ export function areManagedDocumentContentsEqual(
4
+ prev: ManagedDocumentContent,
5
+ next: ManagedDocumentContent,
6
+ ) {
7
+ return (
8
+ prev.title === next.title &&
9
+ areManagedTagArraysEqual(prev.head, next.head) &&
10
+ areManagedTagArraysEqual(prev.body, next.body)
11
+ )
12
+ }
13
+
14
+ export function areManagedTagArraysEqual(
15
+ prev: Array<ManagedTag>,
16
+ next: Array<ManagedTag>,
17
+ ) {
18
+ if (prev.length !== next.length) {
19
+ return false
20
+ }
21
+
22
+ for (let i = 0; i < prev.length; i++) {
23
+ if (prev[i]?.id !== next[i]?.id) {
24
+ return false
25
+ }
26
+ }
27
+
28
+ return true
29
+ }
@@ -0,0 +1,6 @@
1
+ import * as Angular from '@angular/core'
2
+ import type { AnyRouter } from '@benjavicente/router-core'
3
+
4
+ export const TANSTACK_DOCUMENT_ROUTER = new Angular.InjectionToken<AnyRouter>(
5
+ 'TANSTACK_DOCUMENT_ROUTER',
6
+ )
@@ -0,0 +1,33 @@
1
+ export type { TanstackDocumentFeatures } from './provide-tanstack-document'
2
+ export {
3
+ provideHeadContent,
4
+ provideTanstackDocument,
5
+ } from './provide-tanstack-document'
6
+ export { provideTanstackDocumentTitle } from './provide-tanstack-document-title'
7
+ export { provideTanstackHeadManagedTags } from './provide-tanstack-head-managed-tags'
8
+ export { provideTanstackBodyManagedTags } from './provide-tanstack-body-managed-tags'
9
+ export {
10
+ buildManagedDocumentContent,
11
+ buildMatchManagedDocumentContent,
12
+ } from './build-match-managed-document'
13
+ export { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
14
+ export type {
15
+ ManagedBucket,
16
+ ManagedDocumentContent,
17
+ ManagedTag,
18
+ ManagedTagCollection,
19
+ ManagedTagId,
20
+ ManagedTagRecord,
21
+ RouteMetaEntry,
22
+ } from './managed-document-types'
23
+ export { TSR_ID_ATTR, TSR_MANAGED_ATTR } from './managed-document-types'
24
+ export {
25
+ createManagedTagCollection,
26
+ hashManagedTag,
27
+ normalizeManagedTag,
28
+ uniqManagedTags,
29
+ } from './managed-dom'
30
+ export {
31
+ collectDehydrationScriptManagedTags,
32
+ mergeDehydrationPrefixIntoDocument,
33
+ } from './document-dehydration'
@@ -0,0 +1,108 @@
1
+ import { DOCUMENT } from '@angular/common'
2
+ import * as Angular from '@angular/core'
3
+ import { injectStore } from '../injectStore'
4
+ import { buildMatchManagedDocumentContent } from './build-match-managed-document'
5
+ import {
6
+ collectDehydrationScriptManagedTags,
7
+ mergeDehydrationPrefixIntoDocument,
8
+ } from './document-dehydration'
9
+ import { areManagedDocumentContentsEqual } from './document-equality'
10
+ import { createManagedTagCollection } from './managed-dom'
11
+ import type { AnyRouter } from '@benjavicente/router-core'
12
+ import type { ManagedDocumentContent, ManagedTagCollection } from './managed-document-types'
13
+
14
+ function applyManagedDocumentContent({
15
+ document,
16
+ initialTitle,
17
+ headCollection,
18
+ bodyCollection,
19
+ nextContent,
20
+ }: {
21
+ document: Document
22
+ initialTitle: string
23
+ headCollection: ManagedTagCollection
24
+ bodyCollection: ManagedTagCollection
25
+ nextContent: ManagedDocumentContent
26
+ }) {
27
+ if (nextContent.title !== undefined) {
28
+ document.title = nextContent.title
29
+ } else {
30
+ document.title = initialTitle
31
+ }
32
+
33
+ headCollection.sync(nextContent.head)
34
+ bodyCollection.sync(nextContent.body)
35
+ }
36
+
37
+ /**
38
+ * One `activeMatches` subscription: title, head, and body stay in sync together.
39
+ * Used by `provideTanstackDocument` when all features are enabled.
40
+ */
41
+ export function installUnifiedTanstackDocumentSync(injectedRouter: AnyRouter) {
42
+ const document = Angular.inject(DOCUMENT)
43
+ const rendererFactory = Angular.inject(Angular.RendererFactory2, {
44
+ optional: true,
45
+ })
46
+ const destroyRef = Angular.inject(Angular.DestroyRef)
47
+ const activeMatches = injectStore(
48
+ injectedRouter.stores.activeMatchesSnapshot,
49
+ (matches) => matches,
50
+ )
51
+
52
+ const renderer = rendererFactory?.createRenderer(null, null) ?? null
53
+ const initialTitle = document.title
54
+ const headCollection = createManagedTagCollection({
55
+ root: document.head,
56
+ bucket: 'head',
57
+ renderer,
58
+ })
59
+ const bodyCollection = createManagedTagCollection({
60
+ root: document.body,
61
+ bucket: 'body',
62
+ renderer,
63
+ })
64
+
65
+ const dehydrationBodyPrefix =
66
+ collectDehydrationScriptManagedTags(injectedRouter)
67
+
68
+ const composeDocumentContent = (): ManagedDocumentContent =>
69
+ mergeDehydrationPrefixIntoDocument(
70
+ buildMatchManagedDocumentContent(injectedRouter),
71
+ dehydrationBodyPrefix,
72
+ )
73
+
74
+ headCollection.mount()
75
+ bodyCollection.mount()
76
+
77
+ let currentContent = composeDocumentContent()
78
+ applyManagedDocumentContent({
79
+ document,
80
+ initialTitle,
81
+ headCollection,
82
+ bodyCollection,
83
+ nextContent: currentContent,
84
+ })
85
+
86
+ const syncEffect = Angular.effect(() => {
87
+ activeMatches()
88
+ const nextContent = composeDocumentContent()
89
+ if (areManagedDocumentContentsEqual(currentContent, nextContent)) {
90
+ return
91
+ }
92
+ currentContent = nextContent
93
+ applyManagedDocumentContent({
94
+ document,
95
+ initialTitle,
96
+ headCollection,
97
+ bodyCollection,
98
+ nextContent,
99
+ })
100
+ })
101
+
102
+ destroyRef.onDestroy(() => {
103
+ syncEffect.destroy()
104
+ headCollection.destroy()
105
+ bodyCollection.destroy()
106
+ document.title = initialTitle
107
+ })
108
+ }
@@ -0,0 +1,36 @@
1
+ import type { RouterManagedTag } from '@benjavicente/router-core'
2
+
3
+ export const TSR_MANAGED_ATTR = 'data-tsr-managed'
4
+ export const TSR_ID_ATTR = 'data-tsr-id'
5
+
6
+ export type ManagedBucket = 'head' | 'body'
7
+ export type ManagedTagId = string
8
+
9
+ /** Route-driven tag with stable hash id for DOM reconciliation. */
10
+ export type ManagedTag = Exclude<RouterManagedTag, { tag: 'title' }> & {
11
+ id: ManagedTagId
12
+ }
13
+
14
+ export type ManagedDocumentContent = {
15
+ title?: string
16
+ head: Array<ManagedTag>
17
+ body: Array<ManagedTag>
18
+ }
19
+
20
+ export type ManagedTagRecord = {
21
+ id: ManagedTagId
22
+ tag: ManagedTag
23
+ }
24
+
25
+ export type ManagedTagCollection = {
26
+ mount: (initialNodes?: Array<HTMLElement>) => void
27
+ sync: (nextTags: Array<ManagedTag>) => void
28
+ destroy: () => void
29
+ }
30
+
31
+ export type RouteMetaEntry = {
32
+ title?: string
33
+ name?: string
34
+ property?: string
35
+ [key: string]: unknown
36
+ }