@analogjs/router 3.0.0-alpha.7 → 3.0.0-alpha.70

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 (99) hide show
  1. package/content/package.json +4 -0
  2. package/fesm2022/analogjs-router-content.mjs +63 -0
  3. package/fesm2022/analogjs-router-content.mjs.map +1 -0
  4. package/fesm2022/analogjs-router-i18n.mjs +156 -0
  5. package/fesm2022/analogjs-router-i18n.mjs.map +1 -0
  6. package/fesm2022/analogjs-router-server-actions.mjs +2 -27
  7. package/fesm2022/analogjs-router-server.mjs +734 -105
  8. package/fesm2022/analogjs-router-server.mjs.map +1 -0
  9. package/fesm2022/analogjs-router-tanstack-query-server.mjs +71 -0
  10. package/fesm2022/analogjs-router-tanstack-query-server.mjs.map +1 -0
  11. package/fesm2022/analogjs-router-tanstack-query.mjs +39 -0
  12. package/fesm2022/analogjs-router-tanstack-query.mjs.map +1 -0
  13. package/fesm2022/analogjs-router-tokens.mjs +7 -2
  14. package/fesm2022/analogjs-router-tokens.mjs.map +1 -0
  15. package/fesm2022/analogjs-router.mjs +670 -170
  16. package/fesm2022/analogjs-router.mjs.map +1 -0
  17. package/fesm2022/debug.page.mjs +53 -31
  18. package/fesm2022/debug.page.mjs.map +1 -0
  19. package/fesm2022/provide-analog-query.mjs +72 -0
  20. package/fesm2022/provide-analog-query.mjs.map +1 -0
  21. package/fesm2022/route-files.mjs +361 -0
  22. package/fesm2022/route-files.mjs.map +1 -0
  23. package/fesm2022/routes.mjs +5 -278
  24. package/fesm2022/routes.mjs.map +1 -0
  25. package/fesm2022/src.mjs +335 -0
  26. package/fesm2022/src.mjs.map +1 -0
  27. package/i18n/package.json +4 -0
  28. package/package.json +52 -5
  29. package/tanstack-query/package.json +4 -0
  30. package/tanstack-query/server/package.json +4 -0
  31. package/types/content/src/index.d.ts +4 -0
  32. package/types/content/src/lib/debug/routes.d.ts +10 -0
  33. package/types/{src → content/src}/lib/markdown-helpers.d.ts +1 -1
  34. package/types/content/src/lib/routes.d.ts +8 -0
  35. package/types/content/src/lib/with-content-routes.d.ts +2 -0
  36. package/types/i18n/src/index.d.ts +1 -0
  37. package/types/i18n/src/provide-i18n.d.ts +92 -0
  38. package/types/server/actions/src/define-action.d.ts +54 -0
  39. package/types/server/actions/src/define-api-route.d.ts +57 -0
  40. package/types/server/actions/src/define-page-load.d.ts +55 -0
  41. package/types/server/actions/src/define-server-route.d.ts +68 -0
  42. package/types/server/actions/src/index.d.ts +9 -1
  43. package/types/server/actions/src/parse-request-data.d.ts +9 -0
  44. package/types/server/actions/src/validate.d.ts +8 -0
  45. package/types/server/src/defer-reconcile-runtime.d.ts +23 -0
  46. package/types/server/src/index.d.ts +9 -2
  47. package/types/server/src/provide-server-context.d.ts +15 -1
  48. package/types/server/src/render-stream.d.ts +40 -0
  49. package/types/server/src/render.d.ts +3 -3
  50. package/types/server/src/server-fn/app-injector.d.ts +30 -0
  51. package/types/server/src/server-fn/dispatch.d.ts +56 -0
  52. package/types/server/src/server-fn/event-handler.d.ts +22 -0
  53. package/types/server/src/server-fn/interceptors.d.ts +30 -0
  54. package/types/server/src/server-fn/node-context.d.ts +14 -0
  55. package/types/server/src/server-fn/registry.d.ts +7 -0
  56. package/types/server/src/server-fn/same-origin.d.ts +46 -0
  57. package/types/server/src/server-fn/server-fn.d.ts +21 -0
  58. package/types/server/src/server-fn/ssr-dispatcher.d.ts +17 -0
  59. package/types/server/src/utils/reset-component-def-tviews.d.ts +14 -0
  60. package/types/server/src/utils/stream-html.d.ts +13 -0
  61. package/types/server/src/utils/stream-request.d.ts +24 -0
  62. package/types/src/index.d.ts +20 -6
  63. package/types/src/lib/cache-key.d.ts +1 -1
  64. package/types/src/lib/cookie-interceptor.d.ts +1 -1
  65. package/types/src/lib/debug/debug.page.d.ts +4 -2
  66. package/types/src/lib/define-route.d.ts +6 -1
  67. package/types/src/lib/endpoints.d.ts +1 -1
  68. package/types/src/lib/experimental.d.ts +140 -0
  69. package/types/src/lib/form-action.directive.d.ts +12 -5
  70. package/types/src/lib/inject-load.d.ts +5 -2
  71. package/types/src/lib/inject-navigate.d.ts +23 -0
  72. package/types/src/lib/inject-route-context.d.ts +32 -0
  73. package/types/src/lib/inject-typed-params.d.ts +63 -0
  74. package/types/src/lib/json-ld.d.ts +32 -0
  75. package/types/src/lib/meta-tags.d.ts +3 -1
  76. package/types/src/lib/models.d.ts +3 -0
  77. package/types/src/lib/provide-file-router-base.d.ts +4 -0
  78. package/types/src/lib/provide-file-router.d.ts +2 -8
  79. package/types/src/lib/route-builder.d.ts +5 -0
  80. package/types/src/lib/route-files.d.ts +18 -0
  81. package/types/src/lib/route-path.d.ts +124 -0
  82. package/types/src/lib/route-types.d.ts +2 -1
  83. package/types/src/lib/routes.d.ts +2 -10
  84. package/types/src/lib/server-fn/dispatcher.d.ts +23 -0
  85. package/types/src/lib/server-fn/inject-server-fn.d.ts +44 -0
  86. package/types/src/lib/server-fn/server-fn-ref.d.ts +24 -0
  87. package/types/src/lib/server-fn/types.d.ts +55 -0
  88. package/types/src/lib/validation-errors.d.ts +7 -0
  89. package/types/tanstack-query/server/src/define-page-load-queries.d.ts +66 -0
  90. package/types/tanstack-query/server/src/index.d.ts +3 -0
  91. package/types/tanstack-query/src/constants.d.ts +9 -0
  92. package/types/tanstack-query/src/index.d.ts +2 -0
  93. package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
  94. package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
  95. package/types/tanstack-query/src/server-query.d.ts +16 -0
  96. package/types/tokens/src/index.d.ts +2 -0
  97. package/types/server/src/server-component-render.d.ts +0 -4
  98. package/types/server/src/tokens.d.ts +0 -7
  99. package/types/src/lib/server.component.d.ts +0 -33
@@ -1,12 +1,14 @@
1
- import { a as updateMetaTagsOnRouteChange, i as injectRouteEndpointURL, n as createRoutes, r as routes, t as injectDebugRoutes } from "./routes.mjs";
1
+ import { API_PREFIX, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest } from "./analogjs-router-tokens.mjs";
2
+ import { a as updateMetaTagsOnRouteChange, i as injectRouteEndpointURL, n as ANALOG_ROUTE_FILES, o as updateJsonLdOnRouteChange, r as createRoutes$1, t as ANALOG_EXTRA_ROUTE_FILE_SOURCES } from "./route-files.mjs";
3
+ import { n as createRoutes, r as routes, t as injectDebugRoutes } from "./routes.mjs";
2
4
  import { ActivatedRoute, ROUTES, Router, provideRouter } from "@angular/router";
3
5
  import * as i0 from "@angular/core";
4
- import { ChangeDetectionStrategy, Component, Directive, ENVIRONMENT_INITIALIZER, Injector, PLATFORM_ID, TransferState, effect, inject, input, makeEnvironmentProviders, makeStateKey, output, signal } from "@angular/core";
5
- import { HttpClient, HttpHeaders, HttpRequest, HttpResponse, ɵHTTP_ROOT_INTERCEPTOR_FNS } from "@angular/common/http";
6
- import { catchError, from, map, of, throwError } from "rxjs";
7
- import { API_PREFIX, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest } from "@analogjs/router/tokens";
8
- import { DomSanitizer } from "@angular/platform-browser";
6
+ import { DOCUMENT, Directive, Injectable, InjectionToken, Injector, PLATFORM_ID, TransferState, assertInInjectionContext, inject, input, isDevMode, makeEnvironmentProviders, makeStateKey, output, provideAppInitializer, resource, signal } from "@angular/core";
7
+ import { HttpClient, HttpHeaders, HttpResponse, ɵHTTP_ROOT_INTERCEPTOR_FNS } from "@angular/common/http";
8
+ import { firstValueFrom, from, map, of, take } from "rxjs";
9
+ import { Meta } from "@angular/platform-browser";
9
10
  import { isPlatformServer } from "@angular/common";
11
+ import { toSignal } from "@angular/core/rxjs-interop";
10
12
  //#region packages/router/src/lib/define-route.ts
11
13
  /**
12
14
  * @deprecated Use `RouteMeta` type instead.
@@ -66,26 +68,38 @@ function cookieInterceptor(req, next, location = inject(PLATFORM_ID), serverRequ
66
68
  } else return next(req);
67
69
  }
68
70
  //#endregion
69
- //#region packages/router/src/lib/provide-file-router.ts
70
- /**
71
- * Sets up providers for the Angular router, and registers
72
- * file-based routes. Additional features can be provided
73
- * to further configure the behavior of the router.
74
- *
75
- * @param features
76
- * @returns Providers and features to configure the router with routes
77
- */
78
- function provideFileRouter(...features) {
71
+ //#region packages/router/src/lib/provide-file-router-base.ts
72
+ function provideFileRouterWithRoutes(...features) {
79
73
  const extraRoutesFeature = features.filter((feat) => feat.ɵkind >= 100);
80
74
  const routerFeatures = features.filter((feat) => feat.ɵkind < 100);
81
75
  return makeEnvironmentProviders([
82
76
  extraRoutesFeature.map((erf) => erf.ɵproviders),
83
- provideRouter(routes, ...routerFeatures),
77
+ provideRouter([], ...routerFeatures),
84
78
  {
85
- provide: ENVIRONMENT_INITIALIZER,
79
+ provide: ROUTES,
86
80
  multi: true,
87
- useValue: () => updateMetaTagsOnRouteChange()
81
+ useFactory: () => {
82
+ const extraSources = inject(ANALOG_EXTRA_ROUTE_FILE_SOURCES, { optional: true }) ?? [];
83
+ if (extraSources.length === 0) return createRoutes$1(ANALOG_ROUTE_FILES, (_filename, fileLoader) => fileLoader);
84
+ const allFiles = { ...ANALOG_ROUTE_FILES };
85
+ const resolverMap = /* @__PURE__ */ new Map();
86
+ for (const source of extraSources) for (const [key, loader] of Object.entries(source.files)) {
87
+ allFiles[key] = loader;
88
+ resolverMap.set(key, source.resolveModule);
89
+ }
90
+ return createRoutes$1(allFiles, (filename, fileLoader) => {
91
+ const resolver = resolverMap.get(filename);
92
+ return resolver ? resolver(filename, fileLoader) : fileLoader;
93
+ });
94
+ }
88
95
  },
96
+ provideAppInitializer(() => {
97
+ const router = inject(Router);
98
+ const meta = inject(Meta);
99
+ const document = inject(DOCUMENT, { optional: true });
100
+ updateMetaTagsOnRouteChange(router, meta);
101
+ updateJsonLdOnRouteChange(router, document);
102
+ }),
89
103
  {
90
104
  provide: ɵHTTP_ROOT_INTERCEPTOR_FNS,
91
105
  multi: true,
@@ -99,12 +113,6 @@ function provideFileRouter(...features) {
99
113
  }
100
114
  ]);
101
115
  }
102
- /**
103
- * Provides extra custom routes in addition to the routes
104
- * discovered from the filesystem-based routing. These routes are
105
- * inserted before the filesystem-based routes, and take priority in
106
- * route matching.
107
- */
108
116
  function withExtraRoutes(routes) {
109
117
  return {
110
118
  ɵkind: 100,
@@ -116,10 +124,32 @@ function withExtraRoutes(routes) {
116
124
  };
117
125
  }
118
126
  //#endregion
127
+ //#region packages/router/src/lib/provide-file-router.ts
128
+ /**
129
+ * Sets up providers for the Angular router, and registers
130
+ * file-based routes. Additional features can be provided
131
+ * to further configure the behavior of the router.
132
+ *
133
+ * @param features
134
+ * @returns Providers and features to configure the router with routes
135
+ */
136
+ function provideFileRouter(...features) {
137
+ return provideFileRouterWithRoutes(...features);
138
+ }
139
+ //#endregion
119
140
  //#region packages/router/src/lib/inject-load.ts
141
+ function isResponse(value) {
142
+ return typeof value === "object" && value instanceof Response;
143
+ }
120
144
  function injectLoad(options) {
121
145
  return (options?.injector ?? inject(Injector)).get(ActivatedRoute).data.pipe(map((data) => data["load"]));
122
146
  }
147
+ function injectLoadData(options) {
148
+ return injectLoad(options).pipe(map((result) => {
149
+ if (isResponse(result)) throw new Error("Expected page load data but received a response.");
150
+ return result;
151
+ }));
152
+ }
123
153
  //#endregion
124
154
  //#region packages/router/src/lib/get-load-resolver.ts
125
155
  /**
@@ -161,6 +191,22 @@ function generateHash(str) {
161
191
  }
162
192
  //#endregion
163
193
  //#region packages/router/src/lib/request-context.ts
194
+ function mergeFetchParams(requestUrl, request) {
195
+ const merged = /* @__PURE__ */ new Map();
196
+ for (const key of requestUrl.searchParams.keys()) {
197
+ const values = requestUrl.searchParams.getAll(key);
198
+ if (values.length > 0) merged.set(key, values);
199
+ }
200
+ for (const key of request.params.keys()) {
201
+ const values = request.params.getAll(key);
202
+ if (values?.length) merged.set(key, values);
203
+ }
204
+ if (merged.size === 0) return;
205
+ return [...merged.entries()].reduce((params, [key, values]) => {
206
+ params[key] = values.length === 1 ? values[0] : values;
207
+ return params;
208
+ }, {});
209
+ }
164
210
  /**
165
211
  * Interceptor that is server-aware when making HttpClient requests.
166
212
  * Server-side requests use the full URL
@@ -178,20 +224,22 @@ function requestContextInterceptor(req, next) {
178
224
  const nitroGlobal = globalThis;
179
225
  const serverFetch = injectInternalServerFetch() ?? nitroGlobal.$fetch;
180
226
  if (serverFetch && baseUrl && (req.url.startsWith("/") || req.url.startsWith(baseUrl) || req.url.startsWith(`/${apiPrefix}`))) {
181
- const requestUrl = new URL(req.url, baseUrl);
182
- const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
227
+ const requestUrl = new URL(req.urlWithParams, baseUrl);
228
+ const storeKey = makeStateKey(`analog_${makeCacheKey(req, `${requestUrl.pathname}${requestUrl.search}`)}`);
183
229
  const fetchUrl = requestUrl.pathname;
230
+ const fetchParams = mergeFetchParams(requestUrl, req);
184
231
  const responseType = req.responseType === "arraybuffer" ? "arrayBuffer" : req.responseType;
185
232
  return from(serverFetch.raw(fetchUrl, {
186
233
  method: req.method,
187
234
  body: req.body ? req.body : void 0,
188
- params: requestUrl.searchParams,
235
+ params: fetchParams,
189
236
  responseType,
190
237
  headers: req.headers.keys().reduce((hdrs, current) => {
191
- return {
238
+ const value = req.headers.get(current);
239
+ return value != null ? {
192
240
  ...hdrs,
193
- [current]: req.headers.get(current)
194
- };
241
+ [current]: value
242
+ } : hdrs;
195
243
  }, {})
196
244
  }).then((res) => {
197
245
  const cacheResponse = {
@@ -207,8 +255,10 @@ function requestContextInterceptor(req, next) {
207
255
  }));
208
256
  }
209
257
  if (req.url.startsWith("/") || req.url.includes("/_analog/")) {
210
- const requestUrl = req.url.includes("/_analog/") ? req.url : `${window.location.origin}${req.url}`;
211
- const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
258
+ const toAbsoluteUrl = (url) => new URL(url, window.location.origin);
259
+ const requestUrl = toAbsoluteUrl(req.url).href;
260
+ const { pathname, search } = toAbsoluteUrl(req.urlWithParams);
261
+ const storeKey = makeStateKey(`analog_${makeCacheKey(req, `${pathname}${search}`)}`);
212
262
  const cacheRestoreResponse = transferState.get(storeKey, null);
213
263
  if (cacheRestoreResponse) {
214
264
  transferState.remove(storeKey);
@@ -232,62 +282,83 @@ var FormAction = class FormAction {
232
282
  this.state = output();
233
283
  this.router = inject(Router);
234
284
  this.route = inject(ActivatedRoute);
235
- this.path = this._getPath();
285
+ this.currentState = signal("idle", ...[]);
286
+ /** Cached during construction (injection context) so inject() works. */
287
+ this._endpointUrl = this.route ? injectRouteEndpointURL(this.route.snapshot) : void 0;
236
288
  }
237
289
  submitted($event) {
238
290
  $event.preventDefault();
239
- this.state.emit("submitting");
240
- const body = new FormData($event.target);
241
- if ($event.target.method.toUpperCase() === "GET") this._handleGet(body, this.router.url);
242
- else this._handlePost(body, this.path, $event);
291
+ const form = $event.target;
292
+ this._emitState("submitting");
293
+ const body = new FormData(form);
294
+ if (form.method.toUpperCase() === "GET") this._handleGet(body, this._getGetPath(form));
295
+ else this._handlePost(body, this._getPostPath(form), form.method);
243
296
  }
244
297
  _handleGet(body, path) {
245
- const params = {};
246
- body.forEach((formVal, formKey) => params[formKey] = formVal);
247
- this.state.emit("navigate");
248
- const url = path.split("?")[0];
249
- this.router.navigate([url], {
250
- queryParams: params,
251
- onSameUrlNavigation: "reload"
298
+ const url = new URL(path, window.location.href);
299
+ const params = new URLSearchParams(url.search);
300
+ body.forEach((value, key) => {
301
+ params.append(key, value instanceof File ? value.name : value);
252
302
  });
303
+ url.search = params.toString();
304
+ this._emitState("navigate");
305
+ this._navigateTo(url);
253
306
  }
254
- _handlePost(body, path, $event) {
307
+ _handlePost(body, path, method) {
255
308
  fetch(path, {
256
- method: $event.target.method,
309
+ method,
257
310
  body
258
311
  }).then((res) => {
259
312
  if (res.ok) if (res.redirected) {
260
- const redirectUrl = new URL(res.url).pathname;
261
- this.state.emit("redirect");
262
- this.router.navigate([redirectUrl]);
313
+ this._emitState("redirect");
314
+ this._navigateTo(new URL(res.url, window.location.href));
263
315
  } else if (this._isJSON(res.headers.get("Content-type"))) res.json().then((result) => {
264
316
  this.onSuccess.emit(result);
265
- this.state.emit("success");
317
+ this._emitState("success");
266
318
  });
267
319
  else res.text().then((result) => {
268
320
  this.onSuccess.emit(result);
269
- this.state.emit("success");
321
+ this._emitState("success");
270
322
  });
271
323
  else if (res.headers.get("X-Analog-Errors")) res.json().then((errors) => {
272
324
  this.onError.emit(errors);
273
- this.state.emit("error");
325
+ this._emitState("error");
274
326
  });
275
- else this.state.emit("error");
327
+ else this._emitState("error");
276
328
  }).catch((_) => {
277
- this.state.emit("error");
329
+ this._emitState("error");
278
330
  });
279
331
  }
280
- _getPath() {
281
- if (this.route) return injectRouteEndpointURL(this.route.snapshot).pathname;
332
+ _getExplicitAction(form) {
333
+ return this.action().trim() || form.getAttribute("action")?.trim() || void 0;
334
+ }
335
+ _getGetPath(form) {
336
+ return this._getExplicitAction(form) ?? this.router.url;
337
+ }
338
+ _getPostPath(form) {
339
+ const explicitAction = this._getExplicitAction(form);
340
+ if (explicitAction) return new URL(explicitAction, window.location.href).toString();
341
+ if (this._endpointUrl) return this._endpointUrl.pathname;
282
342
  return `/api/_analog/pages${window.location.pathname}`;
283
343
  }
344
+ _emitState(state) {
345
+ this.currentState.set(state);
346
+ this.state.emit(state);
347
+ }
348
+ _navigateTo(url) {
349
+ if (url.origin === window.location.origin) {
350
+ this.router.navigateByUrl(`${url.pathname}${url.search}${url.hash}`, { onSameUrlNavigation: "reload" });
351
+ return;
352
+ }
353
+ window.location.assign(url.toString());
354
+ }
284
355
  _isJSON(contentType) {
285
356
  return (contentType ? contentType.split(";") : [])[0] === "application/json";
286
357
  }
287
358
  static {
288
359
  this.ɵfac = i0.ɵɵngDeclareFactory({
289
360
  minVersion: "12.0.0",
290
- version: "21.1.1",
361
+ version: "22.0.0",
291
362
  ngImport: i0,
292
363
  type: FormAction,
293
364
  deps: [],
@@ -297,7 +368,7 @@ var FormAction = class FormAction {
297
368
  static {
298
369
  this.ɵdir = i0.ɵɵngDeclareDirective({
299
370
  minVersion: "17.1.0",
300
- version: "21.1.1",
371
+ version: "22.0.0",
301
372
  type: FormAction,
302
373
  isStandalone: true,
303
374
  selector: "form[action],form[method]",
@@ -313,21 +384,31 @@ var FormAction = class FormAction {
313
384
  onError: "onError",
314
385
  state: "state"
315
386
  },
316
- host: { listeners: { "submit": "submitted($event)" } },
387
+ host: {
388
+ listeners: { "submit": "submitted($event)" },
389
+ properties: {
390
+ "attr.data-state": "currentState()",
391
+ "attr.aria-busy": "currentState() === \"submitting\" ? \"true\" : null"
392
+ }
393
+ },
317
394
  ngImport: i0
318
395
  });
319
396
  }
320
397
  };
321
398
  i0.ɵɵngDeclareClassMetadata({
322
399
  minVersion: "12.0.0",
323
- version: "21.1.1",
400
+ version: "22.0.0",
324
401
  ngImport: i0,
325
402
  type: FormAction,
326
403
  decorators: [{
327
404
  type: Directive,
328
405
  args: [{
329
406
  selector: "form[action],form[method]",
330
- host: { "(submit)": `submitted($event)` },
407
+ host: {
408
+ "(submit)": `submitted($event)`,
409
+ "[attr.data-state]": "currentState()",
410
+ "[attr.aria-busy]": "currentState() === \"submitting\" ? \"true\" : null"
411
+ },
331
412
  standalone: true
332
413
  }]
333
414
  }],
@@ -375,141 +456,560 @@ function withDebugRoutes() {
375
456
  };
376
457
  }
377
458
  //#endregion
378
- //#region packages/router/src/lib/server.component.ts
459
+ //#region packages/router/src/lib/validation-errors.ts
460
+ function getPathSegmentKey(segment) {
461
+ return typeof segment === "object" ? segment.key : segment;
462
+ }
463
+ function issuePathToFieldName(path) {
464
+ return path.map((segment) => String(getPathSegmentKey(segment))).join(".");
465
+ }
466
+ function issuesToFieldErrors(issues) {
467
+ return issues.reduce((errors, issue) => {
468
+ if (!issue.path?.length) return errors;
469
+ const fieldName = issuePathToFieldName(issue.path);
470
+ errors[fieldName] ??= [];
471
+ errors[fieldName].push(issue.message);
472
+ return errors;
473
+ }, {});
474
+ }
475
+ function issuesToFormErrors(issues) {
476
+ return issues.filter((issue) => !issue.path?.length).map((issue) => issue.message);
477
+ }
478
+ //#endregion
479
+ //#region packages/router/src/lib/server-fn/dispatcher.ts
480
+ var SERVER_FN_DISPATCHER = new InjectionToken("@analogjs/router Server Function Dispatcher");
481
+ //#endregion
482
+ //#region packages/router/src/lib/server-fn/inject-server-fn.ts
379
483
  /**
380
- * @description
381
- * Component that defines the bridge between the client and server-only
382
- * components. The component passes the component ID and props to the server
383
- * and retrieves the rendered HTML and outputs from the server-only component.
384
- *
385
- * Status: experimental
484
+ * Client transport for server functions. In the browser it goes through Angular
485
+ * `HttpClient`, so client `HttpInterceptorFn`s apply. During SSR the dispatcher
486
+ * token is provided, and the call short-circuits the HTTP round-trip: the
487
+ * handler runs in-process in the current request injector. Lives in the client
488
+ * entry (client-safe).
386
489
  */
387
- var ServerOnly = class ServerOnly {
490
+ var ServerFnClient = class ServerFnClient {
388
491
  constructor() {
389
- this.component = input.required(...[]);
390
- this.props = input(...[]);
391
- this.outputs = output();
392
492
  this.http = inject(HttpClient);
393
- this.sanitizer = inject(DomSanitizer);
394
- this.content = signal("", ...[]);
395
- this.route = inject(ActivatedRoute, { optional: true });
396
- this.baseURL = injectBaseURL();
397
493
  this.transferState = inject(TransferState);
398
- effect(() => {
399
- const routeComponentId = this.route?.snapshot.data["component"];
400
- const props = this.props() || {};
401
- const componentId = routeComponentId || this.component();
402
- const headers = new HttpHeaders(new Headers({
403
- "Content-type": "application/json",
404
- "X-Analog-Component": componentId
405
- }));
406
- const componentUrl = this.getComponentUrl(componentId);
407
- const httpRequest = new HttpRequest("POST", componentUrl, props, { headers });
408
- const storeKey = makeStateKey(makeCacheKey(httpRequest, new URL(componentUrl).pathname));
409
- const componentState = this.transferState.get(storeKey, null);
410
- if (componentState) {
411
- this.updateContent(componentState);
412
- this.transferState.remove(storeKey);
413
- } else this.http.request(httpRequest).pipe(map((response) => {
414
- if (response instanceof HttpResponse) return response.body;
415
- return throwError(() => ({}));
416
- }), catchError((error) => {
417
- console.log(error);
418
- return of({
419
- html: "",
420
- outputs: {}
421
- });
422
- })).subscribe((content) => this.updateContent(content));
423
- });
494
+ this.injector = inject(Injector);
495
+ this.dispatcher = inject(SERVER_FN_DISPATCHER, { optional: true });
496
+ }
497
+ /** True while rendering on the server (the in-process dispatcher is provided). */
498
+ get isServer() {
499
+ return !!this.dispatcher;
424
500
  }
425
- updateContent(content) {
426
- this.content.set(this.sanitizer.bypassSecurityTrustHtml(content.html));
427
- this.outputs.emit(content.outputs);
501
+ async call(fn, input) {
502
+ if (this.dispatcher) return this.dispatcher(fn, input, this.injector);
503
+ return firstValueFrom(fn.method === "GET" ? this.http.get(fn.url) : this.http.post(fn.url, input ?? {}));
428
504
  }
429
- getComponentUrl(componentId) {
430
- let baseURL = this.baseURL;
431
- if (!baseURL && typeof window !== "undefined") baseURL = window.location.origin;
432
- return `${baseURL}/_analog/components/${componentId}`;
505
+ /** Key a read's value for TransferState hydration (fn id + input). */
506
+ stateKey(fn, input) {
507
+ return makeStateKey(`__analog_fn_${fn.id}_${stableInput(input)}`);
508
+ }
509
+ readSeed(fn, input) {
510
+ const key = this.stateKey(fn, input);
511
+ if (this.transferState.hasKey(key)) {
512
+ const value = this.transferState.get(key, void 0);
513
+ this.transferState.remove(key);
514
+ return value;
515
+ }
516
+ }
517
+ writeSeed(fn, input, value) {
518
+ this.transferState.set(this.stateKey(fn, input), value);
433
519
  }
434
520
  static {
435
521
  this.ɵfac = i0.ɵɵngDeclareFactory({
436
522
  minVersion: "12.0.0",
437
- version: "21.1.1",
523
+ version: "22.0.0",
438
524
  ngImport: i0,
439
- type: ServerOnly,
525
+ type: ServerFnClient,
440
526
  deps: [],
441
- target: i0.ɵɵFactoryTarget.Component
527
+ target: i0.ɵɵFactoryTarget.Injectable
442
528
  });
443
529
  }
444
530
  static {
445
- this.ɵcmp = i0.ɵɵngDeclareComponent({
446
- minVersion: "17.1.0",
447
- version: "21.1.1",
448
- type: ServerOnly,
449
- isStandalone: true,
450
- selector: "server-only,ServerOnly,Server",
451
- inputs: {
452
- component: {
453
- classPropertyName: "component",
454
- publicName: "component",
455
- isSignal: true,
456
- isRequired: true,
457
- transformFunction: null
458
- },
459
- props: {
460
- classPropertyName: "props",
461
- publicName: "props",
462
- isSignal: true,
463
- isRequired: false,
464
- transformFunction: null
465
- }
466
- },
467
- outputs: { outputs: "outputs" },
531
+ this.ɵprov = i0.ɵɵngDeclareInjectable({
532
+ minVersion: "12.0.0",
533
+ version: "22.0.0",
468
534
  ngImport: i0,
469
- template: ` <div [innerHTML]="content()"></div> `,
470
- isInline: true,
471
- changeDetection: i0.ChangeDetectionStrategy.OnPush
535
+ type: ServerFnClient,
536
+ providedIn: "root"
472
537
  });
473
538
  }
474
539
  };
475
540
  i0.ɵɵngDeclareClassMetadata({
476
541
  minVersion: "12.0.0",
477
- version: "21.1.1",
542
+ version: "22.0.0",
478
543
  ngImport: i0,
479
- type: ServerOnly,
544
+ type: ServerFnClient,
480
545
  decorators: [{
481
- type: Component,
482
- args: [{
483
- selector: "server-only,ServerOnly,Server",
484
- changeDetection: ChangeDetectionStrategy.OnPush,
485
- template: ` <div [innerHTML]="content()"></div> `
546
+ type: Injectable,
547
+ args: [{ providedIn: "root" }]
548
+ }]
549
+ });
550
+ /** No-op provider hook; ServerFnClient is `providedIn: 'root'`. */
551
+ function provideServerFnClient() {
552
+ return [];
553
+ }
554
+ var NO_INPUT = Symbol("analog.serverFn.noInput");
555
+ function injectServerFn(fn, args) {
556
+ assertInInjectionContext(injectServerFn);
557
+ const client = inject(ServerFnClient);
558
+ return resource({
559
+ params: () => args ? args() : NO_INPUT,
560
+ loader: async ({ params }) => {
561
+ const input = params === NO_INPUT ? void 0 : params;
562
+ const seeded = client.readSeed(fn, input);
563
+ if (seeded !== void 0) return seeded;
564
+ const value = await client.call(fn, input);
565
+ if (client.isServer) client.writeSeed(fn, input, value);
566
+ return value;
567
+ }
568
+ });
569
+ }
570
+ /**
571
+ * Imperative binding of a server function: returns a callable that dispatches
572
+ * the call through `HttpClient` (so client interceptors apply) and resolves the
573
+ * result. Use for mutations and event-driven calls; use `injectServerFn` for
574
+ * reactive reads.
575
+ */
576
+ function injectServerFnMutation(fn) {
577
+ assertInInjectionContext(injectServerFnMutation);
578
+ const client = inject(ServerFnClient);
579
+ return (input) => client.call(fn, input);
580
+ }
581
+ function stableInput(input) {
582
+ if (input === void 0 || input === null) return "_";
583
+ return JSON.stringify(input);
584
+ }
585
+ //#endregion
586
+ //#region packages/router/src/lib/server-fn/server-fn-ref.ts
587
+ /**
588
+ * Builds a server-function reference: the client-safe `{ __serverFn, id, url,
589
+ * method }` metadata that `injectServerFn`/`ServerFnClient` dispatch through.
590
+ *
591
+ * Shared by both sides so they produce identical refs: the server `serverFn`
592
+ * wraps this with registration + the handler, and the client build's scrub
593
+ * transform emits a call to this factory in place of the server module so the
594
+ * browser bundle carries only the ref, never the handler or its server imports.
595
+ *
596
+ * The returned value is callable-typed but throws if invoked directly — it is
597
+ * always dispatched via `injectServerFn`/`ServerFnClient`, never called.
598
+ */
599
+ function createServerFnRef(config) {
600
+ if (!config.id) throw new Error("[analog] serverFn is missing its build-derived id. Server functions require the Analog build transform (@analogjs/platform / @analogjs/vite-plugin-nitro); a raw import without it is not supported.");
601
+ const method = config.method ?? (config.input ? "POST" : "GET");
602
+ const url = `/_analog/fn/${config.id}`;
603
+ const ref = (() => {
604
+ throw new Error(`serverFn "${config.id}" must be called via injectServerFn/ServerFnClient`);
605
+ });
606
+ return Object.assign(ref, {
607
+ __serverFn: true,
608
+ id: config.id,
609
+ url,
610
+ method
611
+ });
612
+ }
613
+ //#endregion
614
+ //#region packages/router/src/lib/route-path.ts
615
+ /**
616
+ * Typed route path utilities for Analog.
617
+ *
618
+ * This module provides:
619
+ * - The `AnalogRouteTable` base interface (augmented by generated code)
620
+ * - The `AnalogRoutePath` union type
621
+ * - The `routePath()` URL builder function
622
+ *
623
+ * No Angular dependencies — can be used in any context.
624
+ */
625
+ /**
626
+ * Builds a typed route link object from a route path pattern and options.
627
+ *
628
+ * The returned object separates path, query params, and fragment for
629
+ * direct use with Angular's routerLink directive inputs.
630
+ *
631
+ * @example
632
+ * routePath('/about')
633
+ * // → { path: '/about', queryParams: null, fragment: undefined }
634
+ *
635
+ * routePath('/users/[id]', { params: { id: '42' } })
636
+ * // → { path: '/users/42', queryParams: null, fragment: undefined }
637
+ *
638
+ * routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
639
+ * // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
640
+ *
641
+ * @example Template usage
642
+ * ```html
643
+ * @let link = routePath('/users/[id]', { params: { id: userId } });
644
+ * <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
645
+ * ```
646
+ */
647
+ function routePath(path, ...args) {
648
+ const options = args[0];
649
+ return buildRouteLink(path, options);
650
+ }
651
+ /**
652
+ * Internal: builds a `RouteLinkResult` from path and options.
653
+ * Exported for direct use in tests (avoids generic constraints).
654
+ */
655
+ function buildRouteLink(path, options) {
656
+ const resolvedPath = buildPath(path, options?.params);
657
+ let queryParams = null;
658
+ if (options?.query) {
659
+ const filtered = {};
660
+ let hasEntries = false;
661
+ for (const [key, value] of Object.entries(options.query)) if (value !== void 0) {
662
+ filtered[key] = value;
663
+ hasEntries = true;
664
+ }
665
+ if (hasEntries) queryParams = filtered;
666
+ }
667
+ return {
668
+ path: resolvedPath,
669
+ queryParams,
670
+ fragment: options?.hash
671
+ };
672
+ }
673
+ /**
674
+ * Resolves param placeholders and normalises slashes.
675
+ * Returns only the path — no query string or hash.
676
+ */
677
+ function buildPath(path, params) {
678
+ let url = path;
679
+ if (params) {
680
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
681
+ const value = params[name];
682
+ if (value == null) return "";
683
+ if (Array.isArray(value)) return value.map((v) => encodeURIComponent(v)).join("/");
684
+ return encodeURIComponent(String(value));
685
+ });
686
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
687
+ const value = params[name];
688
+ if (value == null) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
689
+ if (Array.isArray(value)) {
690
+ if (value.length === 0) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
691
+ return value.map((v) => encodeURIComponent(v)).join("/");
692
+ }
693
+ return encodeURIComponent(String(value));
694
+ });
695
+ url = url.replace(/\[([^\]]+)\]/g, (_, name) => {
696
+ const value = params[name];
697
+ if (value == null) throw new Error(`Missing required param "${name}" for path "${path}"`);
698
+ return encodeURIComponent(String(value));
699
+ });
700
+ } else {
701
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "");
702
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, "");
703
+ url = url.replace(/\[([^\]]+)\]/g, "");
704
+ }
705
+ url = url.replace(/\/+/g, "/");
706
+ if (url.length > 1 && url.endsWith("/")) url = url.slice(0, -1);
707
+ if (!url.startsWith("/")) url = "/" + url;
708
+ return url;
709
+ }
710
+ /**
711
+ * Internal URL builder. Separated from `routePath` so it can be
712
+ * used without generic constraints (e.g., in `injectNavigate`).
713
+ */
714
+ function buildUrl(path, options) {
715
+ let url = buildPath(path, options?.params);
716
+ if (options?.query) {
717
+ const parts = [];
718
+ for (const [key, value] of Object.entries(options.query)) {
719
+ if (value === void 0) continue;
720
+ if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
721
+ else parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
722
+ }
723
+ if (parts.length > 0) url += "?" + parts.join("&");
724
+ }
725
+ if (options?.hash) url += "#" + options.hash;
726
+ return url;
727
+ }
728
+ //#endregion
729
+ //#region packages/router/src/lib/inject-navigate.ts
730
+ function isRoutePathOptionsBase(value) {
731
+ return !!value && typeof value === "object" && ("params" in value || "query" in value || "hash" in value);
732
+ }
733
+ /**
734
+ * Injects a typed navigate function.
735
+ *
736
+ * @example
737
+ * ```ts
738
+ * const navigate = injectNavigate();
739
+ *
740
+ * navigate('/users/[id]', { params: { id: '42' } }); // ✅
741
+ * navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
742
+ *
743
+ * // With navigation extras
744
+ * navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
745
+ * ```
746
+ */
747
+ function injectNavigate() {
748
+ const router = inject(Router);
749
+ const navigate = ((path, ...args) => {
750
+ let options;
751
+ let extras;
752
+ if (args.length > 1) {
753
+ options = args[0];
754
+ extras = args[1];
755
+ } else if (args.length === 1) if (isRoutePathOptionsBase(args[0])) options = args[0];
756
+ else extras = args[0];
757
+ const url = buildUrl(path, options);
758
+ return router.navigateByUrl(url, extras);
759
+ });
760
+ return navigate;
761
+ }
762
+ //#endregion
763
+ //#region packages/router/src/lib/experimental.ts
764
+ /** @experimental */
765
+ var EXPERIMENTAL_TYPED_ROUTER = new InjectionToken("EXPERIMENTAL_TYPED_ROUTER");
766
+ /** @experimental */
767
+ var EXPERIMENTAL_ROUTE_CONTEXT = new InjectionToken("EXPERIMENTAL_ROUTE_CONTEXT");
768
+ /** @experimental */
769
+ var EXPERIMENTAL_LOADER_CACHE = new InjectionToken("EXPERIMENTAL_LOADER_CACHE");
770
+ /**
771
+ * Enables experimental typed router features.
772
+ *
773
+ * When active, `routePath()`, `injectNavigate()`, `injectParams()`,
774
+ * and `injectQuery()` will enforce route table constraints and
775
+ * optionally log warnings in strict mode.
776
+ *
777
+ * Inspired by TanStack Router's `Register` interface and strict type
778
+ * checking across the entire navigation surface.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * provideFileRouter(
783
+ * withTypedRouter({ strictRouteParams: true }),
784
+ * )
785
+ * ```
786
+ *
787
+ * @experimental
788
+ */
789
+ function withTypedRouter(options) {
790
+ return {
791
+ ɵkind: 102,
792
+ ɵproviders: [{
793
+ provide: EXPERIMENTAL_TYPED_ROUTER,
794
+ useValue: {
795
+ strictRouteParams: false,
796
+ ...options
797
+ }
486
798
  }]
487
- }],
488
- ctorParameters: () => [],
489
- propDecorators: {
490
- component: [{
491
- type: i0.Input,
492
- args: [{
493
- isSignal: true,
494
- alias: "component",
495
- required: true
496
- }]
497
- }],
498
- props: [{
499
- type: i0.Input,
500
- args: [{
501
- isSignal: true,
502
- alias: "props",
503
- required: false
504
- }]
505
- }],
506
- outputs: [{
507
- type: i0.Output,
508
- args: ["outputs"]
799
+ };
800
+ }
801
+ /**
802
+ * Provides root-level route context available to all route loaders
803
+ * and components via `injectRouteContext()`.
804
+ *
805
+ * Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
806
+ * a typed context object is required at router creation and automatically
807
+ * available in every route's `beforeLoad` and `loader`.
808
+ *
809
+ * In Angular terms, this creates a DI token that server-side load
810
+ * functions and components can inject to access shared services
811
+ * without importing them individually.
812
+ *
813
+ * @example
814
+ * ```ts
815
+ * // app.config.ts
816
+ * provideFileRouter(
817
+ * withRouteContext({
818
+ * auth: inject(AuthService),
819
+ * db: inject(DatabaseService),
820
+ * }),
821
+ * )
822
+ *
823
+ * // In a component
824
+ * const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
825
+ * ```
826
+ *
827
+ * @experimental
828
+ */
829
+ function withRouteContext(context) {
830
+ return {
831
+ ɵkind: 103,
832
+ ɵproviders: [{
833
+ provide: EXPERIMENTAL_ROUTE_CONTEXT,
834
+ useValue: context
509
835
  }]
836
+ };
837
+ }
838
+ /**
839
+ * Configures experimental loader caching behavior for server-loaded
840
+ * route data.
841
+ *
842
+ * Inspired by TanStack Router's built-in cache where `createRouter()`
843
+ * accepts `defaultStaleTime` and `defaultGcTime` to control when
844
+ * loaders re-execute and when cached data is discarded.
845
+ *
846
+ * @example
847
+ * ```ts
848
+ * provideFileRouter(
849
+ * withLoaderCaching({
850
+ * defaultStaleTime: 30_000, // 30s before re-fetch
851
+ * defaultGcTime: 300_000, // 5min cache retention
852
+ * defaultPendingMs: 200, // 200ms loading delay
853
+ * }),
854
+ * )
855
+ * ```
856
+ *
857
+ * @experimental
858
+ */
859
+ function withLoaderCaching(options) {
860
+ return {
861
+ ɵkind: 104,
862
+ ɵproviders: [{
863
+ provide: EXPERIMENTAL_LOADER_CACHE,
864
+ useValue: {
865
+ defaultStaleTime: 0,
866
+ defaultGcTime: 3e5,
867
+ defaultPendingMs: 0,
868
+ ...options
869
+ }
870
+ }]
871
+ };
872
+ }
873
+ //#endregion
874
+ //#region packages/router/src/lib/inject-typed-params.ts
875
+ function extractRouteParams(routePath) {
876
+ const params = [];
877
+ for (const match of routePath.matchAll(/\[\[\.\.\.([^\]]+)\]\]/g)) params.push({
878
+ name: match[1],
879
+ type: "optionalCatchAll"
880
+ });
881
+ for (const match of routePath.matchAll(/(?<!\[)\[\.\.\.([^\]]+)\](?!\])/g)) params.push({
882
+ name: match[1],
883
+ type: "catchAll"
884
+ });
885
+ for (const match of routePath.matchAll(/(?<!\[)\[(?!\.)([^\]]+)\](?!\])/g)) params.push({
886
+ name: match[1],
887
+ type: "dynamic"
888
+ });
889
+ return params;
890
+ }
891
+ /**
892
+ * When `strictRouteParams` is enabled, warns if expected params from the
893
+ * `_from` pattern are missing from the active `ActivatedRoute`.
894
+ */
895
+ function assertRouteMatch(from, route, kind) {
896
+ const expectedParams = extractRouteParams(from).filter((param) => param.type === "dynamic" || param.type === "catchAll").map((param) => param.name);
897
+ if (expectedParams.length === 0) return;
898
+ route.params.pipe(take(1)).subscribe((params) => {
899
+ for (const name of expectedParams) if (!(name in params)) {
900
+ console.warn(`[Analog] ${kind}('${from}'): expected param "${name}" is not present in the active route's params. Ensure this hook is used inside a component rendered by '${from}'.`);
901
+ break;
902
+ }
903
+ });
904
+ }
905
+ /**
906
+ * Injects typed route params as a signal, constrained by the route table.
907
+ *
908
+ * Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
909
+ * pattern where the `from` parameter narrows the return type to only
910
+ * the params defined for that route.
911
+ *
912
+ * The `from` parameter is used purely for TypeScript type inference —
913
+ * at runtime, params are read from the current `ActivatedRoute`. This
914
+ * means it works correctly when used inside a component rendered by
915
+ * the specified route.
916
+ *
917
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
918
+ * a dev-mode assertion checks that the expected params from `from`
919
+ * exist in the active route and warns on mismatch.
920
+ *
921
+ * @example
922
+ * ```ts
923
+ * // In a component rendered at /users/[id]
924
+ * const params = injectParams('/users/[id]');
925
+ * // params() → { id: string }
926
+ *
927
+ * // With schema validation output types
928
+ * const params = injectParams('/products/[slug]');
929
+ * // params() → validated output type from routeParamsSchema
930
+ * ```
931
+ *
932
+ * @experimental
933
+ */
934
+ function injectParams(_from, options) {
935
+ const injector = options?.injector;
936
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
937
+ if (isDevMode()) {
938
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectParams");
510
939
  }
511
- });
940
+ return toSignal(route.params.pipe(map((params) => params)), { requireSync: true });
941
+ }
942
+ /**
943
+ * Injects typed route query params as a signal, constrained by the
944
+ * route table.
945
+ *
946
+ * Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
947
+ * where search params are validated and typed per-route via
948
+ * `validateSearch` schemas.
949
+ *
950
+ * In Analog, the typing comes from `routeQuerySchema` exports that are
951
+ * detected at build time and recorded in the generated route table.
952
+ *
953
+ * The `from` parameter is used purely for TypeScript type inference.
954
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
955
+ * a dev-mode assertion checks that the expected params from `from`
956
+ * exist in the active route and warns on mismatch.
957
+ *
958
+ * @example
959
+ * ```ts
960
+ * // In a component rendered at /issues
961
+ * // (where routeQuerySchema validates { page: number, status: string })
962
+ * const query = injectQuery('/issues');
963
+ * // query() → { page: number; status: string }
964
+ * ```
965
+ *
966
+ * @experimental
967
+ */
968
+ function injectQuery(_from, options) {
969
+ const injector = options?.injector;
970
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
971
+ if (isDevMode()) {
972
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectQuery");
973
+ }
974
+ return toSignal(route.queryParams.pipe(map((params) => params)), { requireSync: true });
975
+ }
976
+ //#endregion
977
+ //#region packages/router/src/lib/inject-route-context.ts
978
+ /**
979
+ * Injects the root route context provided via `withRouteContext()`.
980
+ *
981
+ * Inspired by TanStack Router's context inheritance where
982
+ * `createRootRouteWithContext<T>()` makes a typed context available
983
+ * to every route's `beforeLoad` and `loader` callbacks.
984
+ *
985
+ * In Angular, this uses DI under the hood — `withRouteContext(ctx)`
986
+ * provides the value, and `injectRouteContext<T>()` retrieves it
987
+ * with the expected type.
988
+ *
989
+ * @example
990
+ * ```ts
991
+ * // app.config.ts
992
+ * provideFileRouter(
993
+ * withRouteContext({
994
+ * auth: inject(AuthService),
995
+ * analytics: inject(AnalyticsService),
996
+ * }),
997
+ * )
998
+ *
999
+ * // any-page.page.ts
1000
+ * const ctx = injectRouteContext<{
1001
+ * auth: AuthService;
1002
+ * analytics: AnalyticsService;
1003
+ * }>();
1004
+ * ctx.analytics.trackPageView();
1005
+ * ```
1006
+ *
1007
+ * @experimental
1008
+ */
1009
+ function injectRouteContext() {
1010
+ return inject(EXPERIMENTAL_ROUTE_CONTEXT);
1011
+ }
512
1012
  //#endregion
513
- export { FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectRouteEndpointURL, injectRouter, provideFileRouter, requestContextInterceptor, routes, withDebugRoutes, withExtraRoutes };
1013
+ export { EXPERIMENTAL_LOADER_CACHE, EXPERIMENTAL_ROUTE_CONTEXT, EXPERIMENTAL_TYPED_ROUTER, FormAction, SERVER_FN_DISPATCHER, ServerFnClient, createRoutes, createServerFnRef, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectLoadData, injectNavigate, injectParams, injectQuery, injectRouteContext, injectRouteEndpointURL, injectRouter, injectServerFn, injectServerFnMutation, issuePathToFieldName, issuesToFieldErrors, issuesToFormErrors, provideFileRouter, provideServerFnClient, requestContextInterceptor, routePath, routes, withDebugRoutes, withExtraRoutes, withLoaderCaching, withRouteContext, withTypedRouter };
514
1014
 
515
1015
  //# sourceMappingURL=analogjs-router.mjs.map