@analogjs/router 3.0.0-alpha.4 → 3.0.0-alpha.40

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 (71) 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-server-actions.mjs +309 -1
  5. package/fesm2022/analogjs-router-server-actions.mjs.map +1 -0
  6. package/fesm2022/analogjs-router-server.mjs +60 -3
  7. package/fesm2022/analogjs-router-server.mjs.map +1 -0
  8. package/fesm2022/analogjs-router-tanstack-query-server.mjs +22 -0
  9. package/fesm2022/analogjs-router-tanstack-query-server.mjs.map +1 -0
  10. package/fesm2022/analogjs-router-tanstack-query.mjs +39 -0
  11. package/fesm2022/analogjs-router-tanstack-query.mjs.map +1 -0
  12. package/fesm2022/analogjs-router-tokens.mjs +7 -2
  13. package/fesm2022/analogjs-router-tokens.mjs.map +1 -0
  14. package/fesm2022/analogjs-router.mjs +711 -61
  15. package/fesm2022/analogjs-router.mjs.map +1 -0
  16. package/fesm2022/debug.page.mjs +53 -31
  17. package/fesm2022/debug.page.mjs.map +1 -0
  18. package/fesm2022/provide-analog-query.mjs +23 -0
  19. package/fesm2022/provide-analog-query.mjs.map +1 -0
  20. package/fesm2022/route-files.mjs +361 -0
  21. package/fesm2022/route-files.mjs.map +1 -0
  22. package/fesm2022/routes.mjs +5 -278
  23. package/fesm2022/routes.mjs.map +1 -0
  24. package/package.json +71 -25
  25. package/tanstack-query/package.json +4 -0
  26. package/tanstack-query/server/package.json +4 -0
  27. package/types/content/src/index.d.ts +4 -0
  28. package/types/content/src/lib/debug/routes.d.ts +10 -0
  29. package/types/{src → content/src}/lib/markdown-helpers.d.ts +1 -1
  30. package/types/content/src/lib/routes.d.ts +8 -0
  31. package/types/content/src/lib/with-content-routes.d.ts +2 -0
  32. package/types/server/actions/src/define-action.d.ts +54 -0
  33. package/types/server/actions/src/define-api-route.d.ts +57 -0
  34. package/types/server/actions/src/define-page-load.d.ts +55 -0
  35. package/types/server/actions/src/define-server-route.d.ts +68 -0
  36. package/types/server/actions/src/index.d.ts +9 -1
  37. package/types/server/actions/src/parse-request-data.d.ts +9 -0
  38. package/types/server/actions/src/validate.d.ts +8 -0
  39. package/types/server/src/provide-server-context.d.ts +15 -1
  40. package/types/server/src/render.d.ts +1 -1
  41. package/types/server/src/server-component-render.d.ts +1 -1
  42. package/types/src/index.d.ts +17 -5
  43. package/types/src/lib/cache-key.d.ts +1 -1
  44. package/types/src/lib/cookie-interceptor.d.ts +1 -1
  45. package/types/src/lib/debug/debug.page.d.ts +4 -2
  46. package/types/src/lib/define-route.d.ts +6 -1
  47. package/types/src/lib/endpoints.d.ts +1 -1
  48. package/types/src/lib/experimental.d.ts +140 -0
  49. package/types/src/lib/form-action.directive.d.ts +12 -5
  50. package/types/src/lib/i18n/provide-i18n.d.ts +92 -0
  51. package/types/src/lib/inject-load.d.ts +5 -2
  52. package/types/src/lib/inject-navigate.d.ts +23 -0
  53. package/types/src/lib/inject-route-context.d.ts +32 -0
  54. package/types/src/lib/inject-typed-params.d.ts +63 -0
  55. package/types/src/lib/json-ld.d.ts +32 -0
  56. package/types/src/lib/meta-tags.d.ts +3 -1
  57. package/types/src/lib/models.d.ts +3 -0
  58. package/types/src/lib/provide-file-router-base.d.ts +4 -0
  59. package/types/src/lib/provide-file-router.d.ts +2 -8
  60. package/types/src/lib/route-builder.d.ts +5 -0
  61. package/types/src/lib/route-files.d.ts +18 -0
  62. package/types/src/lib/route-path.d.ts +124 -0
  63. package/types/src/lib/route-types.d.ts +2 -1
  64. package/types/src/lib/routes.d.ts +2 -10
  65. package/types/src/lib/validation-errors.d.ts +7 -0
  66. package/types/tanstack-query/server/src/index.d.ts +1 -0
  67. package/types/tanstack-query/src/index.d.ts +2 -0
  68. package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
  69. package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
  70. package/types/tanstack-query/src/server-query.d.ts +16 -0
  71. package/types/tokens/src/index.d.ts +2 -0
@@ -1,12 +1,15 @@
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";
6
+ import { ChangeDetectionStrategy, Component, DOCUMENT, Directive, InjectionToken, Injector, PLATFORM_ID, TransferState, assertInInjectionContext, effect, inject, input, isDevMode, makeEnvironmentProviders, makeStateKey, output, provideAppInitializer, signal } from "@angular/core";
5
7
  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";
8
+ import { catchError, from, map, of, take, throwError } from "rxjs";
9
+ import { DomSanitizer, Meta } from "@angular/platform-browser";
9
10
  import { isPlatformServer } from "@angular/common";
11
+ import { toSignal } from "@angular/core/rxjs-interop";
12
+ import { LOCALE, REQUEST } from "@analogjs/router/tokens";
10
13
  //#region packages/router/src/lib/define-route.ts
11
14
  /**
12
15
  * @deprecated Use `RouteMeta` type instead.
@@ -66,26 +69,38 @@ function cookieInterceptor(req, next, location = inject(PLATFORM_ID), serverRequ
66
69
  } else return next(req);
67
70
  }
68
71
  //#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) {
72
+ //#region packages/router/src/lib/provide-file-router-base.ts
73
+ function provideFileRouterWithRoutes(...features) {
79
74
  const extraRoutesFeature = features.filter((feat) => feat.ɵkind >= 100);
80
75
  const routerFeatures = features.filter((feat) => feat.ɵkind < 100);
81
76
  return makeEnvironmentProviders([
82
77
  extraRoutesFeature.map((erf) => erf.ɵproviders),
83
- provideRouter(routes, ...routerFeatures),
78
+ provideRouter([], ...routerFeatures),
84
79
  {
85
- provide: ENVIRONMENT_INITIALIZER,
80
+ provide: ROUTES,
86
81
  multi: true,
87
- useValue: () => updateMetaTagsOnRouteChange()
82
+ useFactory: () => {
83
+ const extraSources = inject(ANALOG_EXTRA_ROUTE_FILE_SOURCES, { optional: true }) ?? [];
84
+ if (extraSources.length === 0) return createRoutes$1(ANALOG_ROUTE_FILES, (_filename, fileLoader) => fileLoader);
85
+ const allFiles = { ...ANALOG_ROUTE_FILES };
86
+ const resolverMap = /* @__PURE__ */ new Map();
87
+ for (const source of extraSources) for (const [key, loader] of Object.entries(source.files)) {
88
+ allFiles[key] = loader;
89
+ resolverMap.set(key, source.resolveModule);
90
+ }
91
+ return createRoutes$1(allFiles, (filename, fileLoader) => {
92
+ const resolver = resolverMap.get(filename);
93
+ return resolver ? resolver(filename, fileLoader) : fileLoader;
94
+ });
95
+ }
88
96
  },
97
+ provideAppInitializer(() => {
98
+ const router = inject(Router);
99
+ const meta = inject(Meta);
100
+ const document = inject(DOCUMENT, { optional: true });
101
+ updateMetaTagsOnRouteChange(router, meta);
102
+ updateJsonLdOnRouteChange(router, document);
103
+ }),
89
104
  {
90
105
  provide: ɵHTTP_ROOT_INTERCEPTOR_FNS,
91
106
  multi: true,
@@ -99,12 +114,6 @@ function provideFileRouter(...features) {
99
114
  }
100
115
  ]);
101
116
  }
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
117
  function withExtraRoutes(routes) {
109
118
  return {
110
119
  ɵkind: 100,
@@ -116,10 +125,32 @@ function withExtraRoutes(routes) {
116
125
  };
117
126
  }
118
127
  //#endregion
128
+ //#region packages/router/src/lib/provide-file-router.ts
129
+ /**
130
+ * Sets up providers for the Angular router, and registers
131
+ * file-based routes. Additional features can be provided
132
+ * to further configure the behavior of the router.
133
+ *
134
+ * @param features
135
+ * @returns Providers and features to configure the router with routes
136
+ */
137
+ function provideFileRouter(...features) {
138
+ return provideFileRouterWithRoutes(...features);
139
+ }
140
+ //#endregion
119
141
  //#region packages/router/src/lib/inject-load.ts
142
+ function isResponse(value) {
143
+ return typeof value === "object" && value instanceof Response;
144
+ }
120
145
  function injectLoad(options) {
121
146
  return (options?.injector ?? inject(Injector)).get(ActivatedRoute).data.pipe(map((data) => data["load"]));
122
147
  }
148
+ function injectLoadData(options) {
149
+ return injectLoad(options).pipe(map((result) => {
150
+ if (isResponse(result)) throw new Error("Expected page load data but received a response.");
151
+ return result;
152
+ }));
153
+ }
123
154
  //#endregion
124
155
  //#region packages/router/src/lib/get-load-resolver.ts
125
156
  /**
@@ -161,6 +192,22 @@ function generateHash(str) {
161
192
  }
162
193
  //#endregion
163
194
  //#region packages/router/src/lib/request-context.ts
195
+ function mergeFetchParams(requestUrl, request) {
196
+ const merged = /* @__PURE__ */ new Map();
197
+ for (const key of requestUrl.searchParams.keys()) {
198
+ const values = requestUrl.searchParams.getAll(key);
199
+ if (values.length > 0) merged.set(key, values);
200
+ }
201
+ for (const key of request.params.keys()) {
202
+ const values = request.params.getAll(key);
203
+ if (values?.length) merged.set(key, values);
204
+ }
205
+ if (merged.size === 0) return;
206
+ return [...merged.entries()].reduce((params, [key, values]) => {
207
+ params[key] = values.length === 1 ? values[0] : values;
208
+ return params;
209
+ }, {});
210
+ }
164
211
  /**
165
212
  * Interceptor that is server-aware when making HttpClient requests.
166
213
  * Server-side requests use the full URL
@@ -181,17 +228,19 @@ function requestContextInterceptor(req, next) {
181
228
  const requestUrl = new URL(req.url, baseUrl);
182
229
  const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
183
230
  const fetchUrl = requestUrl.pathname;
231
+ const fetchParams = mergeFetchParams(requestUrl, req);
184
232
  const responseType = req.responseType === "arraybuffer" ? "arrayBuffer" : req.responseType;
185
233
  return from(serverFetch.raw(fetchUrl, {
186
234
  method: req.method,
187
235
  body: req.body ? req.body : void 0,
188
- params: requestUrl.searchParams,
236
+ params: fetchParams,
189
237
  responseType,
190
238
  headers: req.headers.keys().reduce((hdrs, current) => {
191
- return {
239
+ const value = req.headers.get(current);
240
+ return value != null ? {
192
241
  ...hdrs,
193
- [current]: req.headers.get(current)
194
- };
242
+ [current]: value
243
+ } : hdrs;
195
244
  }, {})
196
245
  }).then((res) => {
197
246
  const cacheResponse = {
@@ -232,62 +281,83 @@ var FormAction = class FormAction {
232
281
  this.state = output();
233
282
  this.router = inject(Router);
234
283
  this.route = inject(ActivatedRoute);
235
- this.path = this._getPath();
284
+ this.currentState = signal("idle", ...[]);
285
+ /** Cached during construction (injection context) so inject() works. */
286
+ this._endpointUrl = this.route ? injectRouteEndpointURL(this.route.snapshot) : void 0;
236
287
  }
237
288
  submitted($event) {
238
289
  $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);
290
+ const form = $event.target;
291
+ this._emitState("submitting");
292
+ const body = new FormData(form);
293
+ if (form.method.toUpperCase() === "GET") this._handleGet(body, this._getGetPath(form));
294
+ else this._handlePost(body, this._getPostPath(form), form.method);
243
295
  }
244
296
  _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"
297
+ const url = new URL(path, window.location.href);
298
+ const params = new URLSearchParams(url.search);
299
+ body.forEach((value, key) => {
300
+ params.append(key, value instanceof File ? value.name : value);
252
301
  });
302
+ url.search = params.toString();
303
+ this._emitState("navigate");
304
+ this._navigateTo(url);
253
305
  }
254
- _handlePost(body, path, $event) {
306
+ _handlePost(body, path, method) {
255
307
  fetch(path, {
256
- method: $event.target.method,
308
+ method,
257
309
  body
258
310
  }).then((res) => {
259
311
  if (res.ok) if (res.redirected) {
260
- const redirectUrl = new URL(res.url).pathname;
261
- this.state.emit("redirect");
262
- this.router.navigate([redirectUrl]);
312
+ this._emitState("redirect");
313
+ this._navigateTo(new URL(res.url, window.location.href));
263
314
  } else if (this._isJSON(res.headers.get("Content-type"))) res.json().then((result) => {
264
315
  this.onSuccess.emit(result);
265
- this.state.emit("success");
316
+ this._emitState("success");
266
317
  });
267
318
  else res.text().then((result) => {
268
319
  this.onSuccess.emit(result);
269
- this.state.emit("success");
320
+ this._emitState("success");
270
321
  });
271
322
  else if (res.headers.get("X-Analog-Errors")) res.json().then((errors) => {
272
323
  this.onError.emit(errors);
273
- this.state.emit("error");
324
+ this._emitState("error");
274
325
  });
275
- else this.state.emit("error");
326
+ else this._emitState("error");
276
327
  }).catch((_) => {
277
- this.state.emit("error");
328
+ this._emitState("error");
278
329
  });
279
330
  }
280
- _getPath() {
281
- if (this.route) return injectRouteEndpointURL(this.route.snapshot).pathname;
331
+ _getExplicitAction(form) {
332
+ return this.action().trim() || form.getAttribute("action")?.trim() || void 0;
333
+ }
334
+ _getGetPath(form) {
335
+ return this._getExplicitAction(form) ?? this.router.url;
336
+ }
337
+ _getPostPath(form) {
338
+ const explicitAction = this._getExplicitAction(form);
339
+ if (explicitAction) return new URL(explicitAction, window.location.href).toString();
340
+ if (this._endpointUrl) return this._endpointUrl.pathname;
282
341
  return `/api/_analog/pages${window.location.pathname}`;
283
342
  }
343
+ _emitState(state) {
344
+ this.currentState.set(state);
345
+ this.state.emit(state);
346
+ }
347
+ _navigateTo(url) {
348
+ if (url.origin === window.location.origin) {
349
+ this.router.navigateByUrl(`${url.pathname}${url.search}${url.hash}`, { onSameUrlNavigation: "reload" });
350
+ return;
351
+ }
352
+ window.location.assign(url.toString());
353
+ }
284
354
  _isJSON(contentType) {
285
355
  return (contentType ? contentType.split(";") : [])[0] === "application/json";
286
356
  }
287
357
  static {
288
358
  this.ɵfac = i0.ɵɵngDeclareFactory({
289
359
  minVersion: "12.0.0",
290
- version: "21.1.1",
360
+ version: "21.2.8",
291
361
  ngImport: i0,
292
362
  type: FormAction,
293
363
  deps: [],
@@ -297,7 +367,7 @@ var FormAction = class FormAction {
297
367
  static {
298
368
  this.ɵdir = i0.ɵɵngDeclareDirective({
299
369
  minVersion: "17.1.0",
300
- version: "21.1.1",
370
+ version: "21.2.8",
301
371
  type: FormAction,
302
372
  isStandalone: true,
303
373
  selector: "form[action],form[method]",
@@ -313,21 +383,31 @@ var FormAction = class FormAction {
313
383
  onError: "onError",
314
384
  state: "state"
315
385
  },
316
- host: { listeners: { "submit": "submitted($event)" } },
386
+ host: {
387
+ listeners: { "submit": "submitted($event)" },
388
+ properties: {
389
+ "attr.data-state": "currentState()",
390
+ "attr.aria-busy": "currentState() === \"submitting\" ? \"true\" : null"
391
+ }
392
+ },
317
393
  ngImport: i0
318
394
  });
319
395
  }
320
396
  };
321
397
  i0.ɵɵngDeclareClassMetadata({
322
398
  minVersion: "12.0.0",
323
- version: "21.1.1",
399
+ version: "21.2.8",
324
400
  ngImport: i0,
325
401
  type: FormAction,
326
402
  decorators: [{
327
403
  type: Directive,
328
404
  args: [{
329
405
  selector: "form[action],form[method]",
330
- host: { "(submit)": `submitted($event)` },
406
+ host: {
407
+ "(submit)": `submitted($event)`,
408
+ "[attr.data-state]": "currentState()",
409
+ "[attr.aria-busy]": "currentState() === \"submitting\" ? \"true\" : null"
410
+ },
331
411
  standalone: true
332
412
  }]
333
413
  }],
@@ -434,7 +514,7 @@ var ServerOnly = class ServerOnly {
434
514
  static {
435
515
  this.ɵfac = i0.ɵɵngDeclareFactory({
436
516
  minVersion: "12.0.0",
437
- version: "21.1.1",
517
+ version: "21.2.8",
438
518
  ngImport: i0,
439
519
  type: ServerOnly,
440
520
  deps: [],
@@ -444,7 +524,7 @@ var ServerOnly = class ServerOnly {
444
524
  static {
445
525
  this.ɵcmp = i0.ɵɵngDeclareComponent({
446
526
  minVersion: "17.1.0",
447
- version: "21.1.1",
527
+ version: "21.2.8",
448
528
  type: ServerOnly,
449
529
  isStandalone: true,
450
530
  selector: "server-only,ServerOnly,Server",
@@ -474,7 +554,7 @@ var ServerOnly = class ServerOnly {
474
554
  };
475
555
  i0.ɵɵngDeclareClassMetadata({
476
556
  minVersion: "12.0.0",
477
- version: "21.1.1",
557
+ version: "21.2.8",
478
558
  ngImport: i0,
479
559
  type: ServerOnly,
480
560
  decorators: [{
@@ -510,6 +590,576 @@ i0.ɵɵngDeclareClassMetadata({
510
590
  }
511
591
  });
512
592
  //#endregion
513
- export { FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectRouteEndpointURL, injectRouter, provideFileRouter, requestContextInterceptor, routes, withDebugRoutes, withExtraRoutes };
593
+ //#region packages/router/src/lib/validation-errors.ts
594
+ function getPathSegmentKey(segment) {
595
+ return typeof segment === "object" ? segment.key : segment;
596
+ }
597
+ function issuePathToFieldName(path) {
598
+ return path.map((segment) => String(getPathSegmentKey(segment))).join(".");
599
+ }
600
+ function issuesToFieldErrors(issues) {
601
+ return issues.reduce((errors, issue) => {
602
+ if (!issue.path?.length) return errors;
603
+ const fieldName = issuePathToFieldName(issue.path);
604
+ errors[fieldName] ??= [];
605
+ errors[fieldName].push(issue.message);
606
+ return errors;
607
+ }, {});
608
+ }
609
+ function issuesToFormErrors(issues) {
610
+ return issues.filter((issue) => !issue.path?.length).map((issue) => issue.message);
611
+ }
612
+ //#endregion
613
+ //#region packages/router/src/lib/route-path.ts
614
+ /**
615
+ * Typed route path utilities for Analog.
616
+ *
617
+ * This module provides:
618
+ * - The `AnalogRouteTable` base interface (augmented by generated code)
619
+ * - The `AnalogRoutePath` union type
620
+ * - The `routePath()` URL builder function
621
+ *
622
+ * No Angular dependencies — can be used in any context.
623
+ */
624
+ /**
625
+ * Builds a typed route link object from a route path pattern and options.
626
+ *
627
+ * The returned object separates path, query params, and fragment for
628
+ * direct use with Angular's routerLink directive inputs.
629
+ *
630
+ * @example
631
+ * routePath('/about')
632
+ * // → { path: '/about', queryParams: null, fragment: undefined }
633
+ *
634
+ * routePath('/users/[id]', { params: { id: '42' } })
635
+ * // → { path: '/users/42', queryParams: null, fragment: undefined }
636
+ *
637
+ * routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
638
+ * // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
639
+ *
640
+ * @example Template usage
641
+ * ```html
642
+ * @let link = routePath('/users/[id]', { params: { id: userId } });
643
+ * <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
644
+ * ```
645
+ */
646
+ function routePath(path, ...args) {
647
+ const options = args[0];
648
+ return buildRouteLink(path, options);
649
+ }
650
+ /**
651
+ * Internal: builds a `RouteLinkResult` from path and options.
652
+ * Exported for direct use in tests (avoids generic constraints).
653
+ */
654
+ function buildRouteLink(path, options) {
655
+ const resolvedPath = buildPath(path, options?.params);
656
+ let queryParams = null;
657
+ if (options?.query) {
658
+ const filtered = {};
659
+ let hasEntries = false;
660
+ for (const [key, value] of Object.entries(options.query)) if (value !== void 0) {
661
+ filtered[key] = value;
662
+ hasEntries = true;
663
+ }
664
+ if (hasEntries) queryParams = filtered;
665
+ }
666
+ return {
667
+ path: resolvedPath,
668
+ queryParams,
669
+ fragment: options?.hash
670
+ };
671
+ }
672
+ /**
673
+ * Resolves param placeholders and normalises slashes.
674
+ * Returns only the path — no query string or hash.
675
+ */
676
+ function buildPath(path, params) {
677
+ let url = path;
678
+ if (params) {
679
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
680
+ const value = params[name];
681
+ if (value == null) return "";
682
+ if (Array.isArray(value)) return value.map((v) => encodeURIComponent(v)).join("/");
683
+ return encodeURIComponent(String(value));
684
+ });
685
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
686
+ const value = params[name];
687
+ if (value == null) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
688
+ if (Array.isArray(value)) {
689
+ if (value.length === 0) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
690
+ return value.map((v) => encodeURIComponent(v)).join("/");
691
+ }
692
+ return encodeURIComponent(String(value));
693
+ });
694
+ url = url.replace(/\[([^\]]+)\]/g, (_, name) => {
695
+ const value = params[name];
696
+ if (value == null) throw new Error(`Missing required param "${name}" for path "${path}"`);
697
+ return encodeURIComponent(String(value));
698
+ });
699
+ } else {
700
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "");
701
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, "");
702
+ url = url.replace(/\[([^\]]+)\]/g, "");
703
+ }
704
+ url = url.replace(/\/+/g, "/");
705
+ if (url.length > 1 && url.endsWith("/")) url = url.slice(0, -1);
706
+ if (!url.startsWith("/")) url = "/" + url;
707
+ return url;
708
+ }
709
+ /**
710
+ * Internal URL builder. Separated from `routePath` so it can be
711
+ * used without generic constraints (e.g., in `injectNavigate`).
712
+ */
713
+ function buildUrl(path, options) {
714
+ let url = buildPath(path, options?.params);
715
+ if (options?.query) {
716
+ const parts = [];
717
+ for (const [key, value] of Object.entries(options.query)) {
718
+ if (value === void 0) continue;
719
+ if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
720
+ else parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
721
+ }
722
+ if (parts.length > 0) url += "?" + parts.join("&");
723
+ }
724
+ if (options?.hash) url += "#" + options.hash;
725
+ return url;
726
+ }
727
+ //#endregion
728
+ //#region packages/router/src/lib/inject-navigate.ts
729
+ function isRoutePathOptionsBase(value) {
730
+ return !!value && typeof value === "object" && ("params" in value || "query" in value || "hash" in value);
731
+ }
732
+ /**
733
+ * Injects a typed navigate function.
734
+ *
735
+ * @example
736
+ * ```ts
737
+ * const navigate = injectNavigate();
738
+ *
739
+ * navigate('/users/[id]', { params: { id: '42' } }); // ✅
740
+ * navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
741
+ *
742
+ * // With navigation extras
743
+ * navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
744
+ * ```
745
+ */
746
+ function injectNavigate() {
747
+ const router = inject(Router);
748
+ const navigate = ((path, ...args) => {
749
+ let options;
750
+ let extras;
751
+ if (args.length > 1) {
752
+ options = args[0];
753
+ extras = args[1];
754
+ } else if (args.length === 1) if (isRoutePathOptionsBase(args[0])) options = args[0];
755
+ else extras = args[0];
756
+ const url = buildUrl(path, options);
757
+ return router.navigateByUrl(url, extras);
758
+ });
759
+ return navigate;
760
+ }
761
+ //#endregion
762
+ //#region packages/router/src/lib/experimental.ts
763
+ /** @experimental */
764
+ var EXPERIMENTAL_TYPED_ROUTER = new InjectionToken("EXPERIMENTAL_TYPED_ROUTER");
765
+ /** @experimental */
766
+ var EXPERIMENTAL_ROUTE_CONTEXT = new InjectionToken("EXPERIMENTAL_ROUTE_CONTEXT");
767
+ /** @experimental */
768
+ var EXPERIMENTAL_LOADER_CACHE = new InjectionToken("EXPERIMENTAL_LOADER_CACHE");
769
+ /**
770
+ * Enables experimental typed router features.
771
+ *
772
+ * When active, `routePath()`, `injectNavigate()`, `injectParams()`,
773
+ * and `injectQuery()` will enforce route table constraints and
774
+ * optionally log warnings in strict mode.
775
+ *
776
+ * Inspired by TanStack Router's `Register` interface and strict type
777
+ * checking across the entire navigation surface.
778
+ *
779
+ * @example
780
+ * ```ts
781
+ * provideFileRouter(
782
+ * withTypedRouter({ strictRouteParams: true }),
783
+ * )
784
+ * ```
785
+ *
786
+ * @experimental
787
+ */
788
+ function withTypedRouter(options) {
789
+ return {
790
+ ɵkind: 102,
791
+ ɵproviders: [{
792
+ provide: EXPERIMENTAL_TYPED_ROUTER,
793
+ useValue: {
794
+ strictRouteParams: false,
795
+ ...options
796
+ }
797
+ }]
798
+ };
799
+ }
800
+ /**
801
+ * Provides root-level route context available to all route loaders
802
+ * and components via `injectRouteContext()`.
803
+ *
804
+ * Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
805
+ * a typed context object is required at router creation and automatically
806
+ * available in every route's `beforeLoad` and `loader`.
807
+ *
808
+ * In Angular terms, this creates a DI token that server-side load
809
+ * functions and components can inject to access shared services
810
+ * without importing them individually.
811
+ *
812
+ * @example
813
+ * ```ts
814
+ * // app.config.ts
815
+ * provideFileRouter(
816
+ * withRouteContext({
817
+ * auth: inject(AuthService),
818
+ * db: inject(DatabaseService),
819
+ * }),
820
+ * )
821
+ *
822
+ * // In a component
823
+ * const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
824
+ * ```
825
+ *
826
+ * @experimental
827
+ */
828
+ function withRouteContext(context) {
829
+ return {
830
+ ɵkind: 103,
831
+ ɵproviders: [{
832
+ provide: EXPERIMENTAL_ROUTE_CONTEXT,
833
+ useValue: context
834
+ }]
835
+ };
836
+ }
837
+ /**
838
+ * Configures experimental loader caching behavior for server-loaded
839
+ * route data.
840
+ *
841
+ * Inspired by TanStack Router's built-in cache where `createRouter()`
842
+ * accepts `defaultStaleTime` and `defaultGcTime` to control when
843
+ * loaders re-execute and when cached data is discarded.
844
+ *
845
+ * @example
846
+ * ```ts
847
+ * provideFileRouter(
848
+ * withLoaderCaching({
849
+ * defaultStaleTime: 30_000, // 30s before re-fetch
850
+ * defaultGcTime: 300_000, // 5min cache retention
851
+ * defaultPendingMs: 200, // 200ms loading delay
852
+ * }),
853
+ * )
854
+ * ```
855
+ *
856
+ * @experimental
857
+ */
858
+ function withLoaderCaching(options) {
859
+ return {
860
+ ɵkind: 104,
861
+ ɵproviders: [{
862
+ provide: EXPERIMENTAL_LOADER_CACHE,
863
+ useValue: {
864
+ defaultStaleTime: 0,
865
+ defaultGcTime: 3e5,
866
+ defaultPendingMs: 0,
867
+ ...options
868
+ }
869
+ }]
870
+ };
871
+ }
872
+ //#endregion
873
+ //#region packages/router/src/lib/inject-typed-params.ts
874
+ function extractRouteParams(routePath) {
875
+ const params = [];
876
+ for (const match of routePath.matchAll(/\[\[\.\.\.([^\]]+)\]\]/g)) params.push({
877
+ name: match[1],
878
+ type: "optionalCatchAll"
879
+ });
880
+ for (const match of routePath.matchAll(/(?<!\[)\[\.\.\.([^\]]+)\](?!\])/g)) params.push({
881
+ name: match[1],
882
+ type: "catchAll"
883
+ });
884
+ for (const match of routePath.matchAll(/(?<!\[)\[(?!\.)([^\]]+)\](?!\])/g)) params.push({
885
+ name: match[1],
886
+ type: "dynamic"
887
+ });
888
+ return params;
889
+ }
890
+ /**
891
+ * When `strictRouteParams` is enabled, warns if expected params from the
892
+ * `_from` pattern are missing from the active `ActivatedRoute`.
893
+ */
894
+ function assertRouteMatch(from, route, kind) {
895
+ const expectedParams = extractRouteParams(from).filter((param) => param.type === "dynamic" || param.type === "catchAll").map((param) => param.name);
896
+ if (expectedParams.length === 0) return;
897
+ route.params.pipe(take(1)).subscribe((params) => {
898
+ for (const name of expectedParams) if (!(name in params)) {
899
+ 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}'.`);
900
+ break;
901
+ }
902
+ });
903
+ }
904
+ /**
905
+ * Injects typed route params as a signal, constrained by the route table.
906
+ *
907
+ * Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
908
+ * pattern where the `from` parameter narrows the return type to only
909
+ * the params defined for that route.
910
+ *
911
+ * The `from` parameter is used purely for TypeScript type inference —
912
+ * at runtime, params are read from the current `ActivatedRoute`. This
913
+ * means it works correctly when used inside a component rendered by
914
+ * the specified route.
915
+ *
916
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
917
+ * a dev-mode assertion checks that the expected params from `from`
918
+ * exist in the active route and warns on mismatch.
919
+ *
920
+ * @example
921
+ * ```ts
922
+ * // In a component rendered at /users/[id]
923
+ * const params = injectParams('/users/[id]');
924
+ * // params() → { id: string }
925
+ *
926
+ * // With schema validation output types
927
+ * const params = injectParams('/products/[slug]');
928
+ * // params() → validated output type from routeParamsSchema
929
+ * ```
930
+ *
931
+ * @experimental
932
+ */
933
+ function injectParams(_from, options) {
934
+ const injector = options?.injector;
935
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
936
+ if (isDevMode()) {
937
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectParams");
938
+ }
939
+ return toSignal(route.params.pipe(map((params) => params)), { requireSync: true });
940
+ }
941
+ /**
942
+ * Injects typed route query params as a signal, constrained by the
943
+ * route table.
944
+ *
945
+ * Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
946
+ * where search params are validated and typed per-route via
947
+ * `validateSearch` schemas.
948
+ *
949
+ * In Analog, the typing comes from `routeQuerySchema` exports that are
950
+ * detected at build time and recorded in the generated route table.
951
+ *
952
+ * The `from` parameter is used purely for TypeScript type inference.
953
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
954
+ * a dev-mode assertion checks that the expected params from `from`
955
+ * exist in the active route and warns on mismatch.
956
+ *
957
+ * @example
958
+ * ```ts
959
+ * // In a component rendered at /issues
960
+ * // (where routeQuerySchema validates { page: number, status: string })
961
+ * const query = injectQuery('/issues');
962
+ * // query() → { page: number; status: string }
963
+ * ```
964
+ *
965
+ * @experimental
966
+ */
967
+ function injectQuery(_from, options) {
968
+ const injector = options?.injector;
969
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
970
+ if (isDevMode()) {
971
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectQuery");
972
+ }
973
+ return toSignal(route.queryParams.pipe(map((params) => params)), { requireSync: true });
974
+ }
975
+ //#endregion
976
+ //#region packages/router/src/lib/inject-route-context.ts
977
+ /**
978
+ * Injects the root route context provided via `withRouteContext()`.
979
+ *
980
+ * Inspired by TanStack Router's context inheritance where
981
+ * `createRootRouteWithContext<T>()` makes a typed context available
982
+ * to every route's `beforeLoad` and `loader` callbacks.
983
+ *
984
+ * In Angular, this uses DI under the hood — `withRouteContext(ctx)`
985
+ * provides the value, and `injectRouteContext<T>()` retrieves it
986
+ * with the expected type.
987
+ *
988
+ * @example
989
+ * ```ts
990
+ * // app.config.ts
991
+ * provideFileRouter(
992
+ * withRouteContext({
993
+ * auth: inject(AuthService),
994
+ * analytics: inject(AnalyticsService),
995
+ * }),
996
+ * )
997
+ *
998
+ * // any-page.page.ts
999
+ * const ctx = injectRouteContext<{
1000
+ * auth: AuthService;
1001
+ * analytics: AnalyticsService;
1002
+ * }>();
1003
+ * ctx.analytics.trackPageView();
1004
+ * ```
1005
+ *
1006
+ * @experimental
1007
+ */
1008
+ function injectRouteContext() {
1009
+ return inject(EXPERIMENTAL_ROUTE_CONTEXT);
1010
+ }
1011
+ //#endregion
1012
+ //#region packages/router/src/lib/i18n/provide-i18n.ts
1013
+ /**
1014
+ * Injection token for the resolved i18n configuration.
1015
+ * Provided by `provideI18n()` and consumed by `injectSwitchLocale()`.
1016
+ * @internal
1017
+ */
1018
+ var I18N_CONFIG = new InjectionToken("@analogjs/router I18n Config");
1019
+ /**
1020
+ * Resolves the full i18n config by merging explicit values with
1021
+ * build-time globals injected by the platform plugin.
1022
+ */
1023
+ function resolveI18nConfig(config) {
1024
+ const defaultLocale = config.defaultLocale ?? (typeof ANALOG_I18N_DEFAULT_LOCALE !== "undefined" ? ANALOG_I18N_DEFAULT_LOCALE : void 0);
1025
+ const locales = config.locales ?? (typeof ANALOG_I18N_LOCALES !== "undefined" ? ANALOG_I18N_LOCALES : void 0);
1026
+ if (!defaultLocale || !locales) throw new Error("[@analogjs/router] provideI18n() requires defaultLocale and locales. Either pass them explicitly or configure i18n in the analog() plugin in vite.config.ts.");
1027
+ return {
1028
+ defaultLocale,
1029
+ locales,
1030
+ loader: config.loader
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Provides runtime i18n support using Angular's $localize.
1035
+ *
1036
+ * This provider:
1037
+ * 1. Detects the active locale from the URL or falls back to the default.
1038
+ * 2. Makes the current locale available via the LOCALE injection token.
1039
+ * 3. Loads translations for the active locale at startup using $localize.
1040
+ *
1041
+ * Works in both SSR and client-only modes. On the client, locale is detected
1042
+ * from `window.location.pathname`. On the server, locale is detected from
1043
+ * the request in `provideServerContext()` and provided at the platform level;
1044
+ * this function does not shadow it.
1045
+ *
1046
+ * When the platform plugin is configured with `i18n` in `vite.config.ts`,
1047
+ * `defaultLocale` and `locales` are injected automatically — only
1048
+ * `loader` is required:
1049
+ *
1050
+ * ```typescript
1051
+ * provideI18n({
1052
+ * loader: (locale) => import(`./i18n/${locale}.json`),
1053
+ * })
1054
+ * ```
1055
+ */
1056
+ function provideI18n(config) {
1057
+ const resolved = resolveI18nConfig(config);
1058
+ const localeProviders = typeof window !== "undefined" ? [{
1059
+ provide: LOCALE,
1060
+ useValue: detectClientLocale(resolved)
1061
+ }] : [];
1062
+ return makeEnvironmentProviders([
1063
+ {
1064
+ provide: I18N_CONFIG,
1065
+ useValue: resolved
1066
+ },
1067
+ ...localeProviders,
1068
+ provideAppInitializer(async () => {
1069
+ await initI18n(resolved, resolveActiveLocale(resolved));
1070
+ })
1071
+ ]);
1072
+ }
1073
+ /**
1074
+ * Resolves the active locale, preferring the injected `LOCALE` token
1075
+ * (which on the server reads from the platform-level provider set by
1076
+ * `provideServerContext()`) and falling back to the request URL,
1077
+ * `window.location.pathname`, or `defaultLocale`.
1078
+ */
1079
+ function resolveActiveLocale(config) {
1080
+ const injected = inject(LOCALE, { optional: true });
1081
+ if (injected && config.locales.includes(injected)) return injected;
1082
+ const req = inject(REQUEST, { optional: true });
1083
+ const first = (req?.originalUrl ?? req?.url ?? (typeof window !== "undefined" ? window.location.pathname : "/")).split("?")[0].split("/").filter(Boolean)[0];
1084
+ if (first && config.locales.includes(first)) return first;
1085
+ return config.defaultLocale;
1086
+ }
1087
+ function detectClientLocale(config) {
1088
+ if (typeof window === "undefined") return config.defaultLocale;
1089
+ const firstSegment = window.location.pathname.split("/").filter(Boolean)[0];
1090
+ if (firstSegment && config.locales.includes(firstSegment)) return firstSegment;
1091
+ return config.defaultLocale;
1092
+ }
1093
+ /**
1094
+ * Loads translations for the given locale and registers them with $localize.
1095
+ *
1096
+ * Always clears any previously loaded translations first so that switching
1097
+ * between locales in a single SSR process does not mix translation maps.
1098
+ */
1099
+ async function initI18n(config, locale) {
1100
+ const activeLocale = locale ?? config.defaultLocale;
1101
+ await clearTranslationsRuntime();
1102
+ if (activeLocale === config.locales[0]) return;
1103
+ const translations = await config.loader(activeLocale);
1104
+ if (translations && Object.keys(translations).length > 0) await loadTranslationsRuntime(translations);
1105
+ }
1106
+ /**
1107
+ * Loads translations into the global $localize translation map.
1108
+ *
1109
+ * Uses `@angular/localize`'s `loadTranslations` when available so that
1110
+ * each translation string is parsed into the `{text, messageParts,
1111
+ * placeholderNames}` shape that `$localize.translate` expects. Falls back
1112
+ * to writing raw strings only as a last resort (in which case lookups
1113
+ * will not succeed — the fallback exists to keep error messages useful
1114
+ * for users who have not installed `@angular/localize`).
1115
+ *
1116
+ * Requires `@angular/localize/init` to be imported in the application
1117
+ * entry point so that `globalThis.$localize` is defined.
1118
+ */
1119
+ async function loadTranslationsRuntime(translations) {
1120
+ const $localize = globalThis.$localize;
1121
+ if (!$localize) {
1122
+ console.warn("[@analogjs/router] $localize is not available. Make sure to import @angular/localize/init in your application entry point.");
1123
+ return;
1124
+ }
1125
+ try {
1126
+ const { loadTranslations } = await import("@angular/localize");
1127
+ loadTranslations(translations);
1128
+ } catch {
1129
+ console.warn("[@analogjs/router] Unable to import @angular/localize. Install it as a dependency to enable runtime translation loading.");
1130
+ $localize.TRANSLATIONS ??= {};
1131
+ for (const [id, message] of Object.entries(translations)) $localize.TRANSLATIONS[id] = message;
1132
+ }
1133
+ }
1134
+ /** @internal — exported for tests; not re-exported from the package entry. */
1135
+ async function clearTranslationsRuntime() {
1136
+ const $localize = globalThis.$localize;
1137
+ if (!$localize) return;
1138
+ try {
1139
+ const { clearTranslations } = await import("@angular/localize");
1140
+ clearTranslations();
1141
+ } catch {
1142
+ $localize.translate = void 0;
1143
+ $localize.TRANSLATIONS = {};
1144
+ }
1145
+ }
1146
+ function injectSwitchLocale() {
1147
+ assertInInjectionContext(injectSwitchLocale);
1148
+ const config = inject(I18N_CONFIG);
1149
+ return (targetLocale) => {
1150
+ if (typeof window === "undefined") return;
1151
+ const { pathname, search, hash } = window.location;
1152
+ const newPath = replaceLocaleInPath(pathname, targetLocale, config.locales);
1153
+ window.location.href = `${newPath}${search}${hash}`;
1154
+ };
1155
+ }
1156
+ function replaceLocaleInPath(pathname, targetLocale, locales) {
1157
+ const segments = pathname.split("/").filter(Boolean);
1158
+ if (segments.length > 0 && locales.includes(segments[0])) segments[0] = targetLocale;
1159
+ else segments.unshift(targetLocale);
1160
+ return "/" + segments.join("/");
1161
+ }
1162
+ //#endregion
1163
+ export { EXPERIMENTAL_LOADER_CACHE, EXPERIMENTAL_ROUTE_CONTEXT, EXPERIMENTAL_TYPED_ROUTER, FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectLoadData, injectNavigate, injectParams, injectQuery, injectRouteContext, injectRouteEndpointURL, injectRouter, injectSwitchLocale, issuePathToFieldName, issuesToFieldErrors, issuesToFormErrors, loadTranslationsRuntime, provideFileRouter, provideI18n, requestContextInterceptor, routePath, routes, withDebugRoutes, withExtraRoutes, withLoaderCaching, withRouteContext, withTypedRouter };
514
1164
 
515
1165
  //# sourceMappingURL=analogjs-router.mjs.map