@analogjs/router 3.0.0-alpha.3 → 3.0.0-alpha.30

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 (68) 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 +2 -2
  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.map +1 -0
  13. package/fesm2022/analogjs-router.mjs +560 -62
  14. package/fesm2022/analogjs-router.mjs.map +1 -0
  15. package/fesm2022/debug.page.mjs +53 -31
  16. package/fesm2022/debug.page.mjs.map +1 -0
  17. package/fesm2022/provide-analog-query.mjs +23 -0
  18. package/fesm2022/provide-analog-query.mjs.map +1 -0
  19. package/fesm2022/route-files.mjs +362 -0
  20. package/fesm2022/route-files.mjs.map +1 -0
  21. package/fesm2022/routes.mjs +5 -278
  22. package/fesm2022/routes.mjs.map +1 -0
  23. package/package.json +71 -25
  24. package/tanstack-query/package.json +4 -0
  25. package/tanstack-query/server/package.json +4 -0
  26. package/types/content/src/index.d.ts +4 -0
  27. package/types/content/src/lib/debug/routes.d.ts +10 -0
  28. package/types/{src → content/src}/lib/markdown-helpers.d.ts +1 -1
  29. package/types/content/src/lib/routes.d.ts +8 -0
  30. package/types/content/src/lib/with-content-routes.d.ts +2 -0
  31. package/types/server/actions/src/define-action.d.ts +54 -0
  32. package/types/server/actions/src/define-api-route.d.ts +57 -0
  33. package/types/server/actions/src/define-page-load.d.ts +55 -0
  34. package/types/server/actions/src/define-server-route.d.ts +68 -0
  35. package/types/server/actions/src/index.d.ts +9 -1
  36. package/types/server/actions/src/parse-request-data.d.ts +9 -0
  37. package/types/server/actions/src/validate.d.ts +8 -0
  38. package/types/server/src/provide-server-context.d.ts +1 -1
  39. package/types/server/src/render.d.ts +1 -1
  40. package/types/server/src/server-component-render.d.ts +1 -1
  41. package/types/src/index.d.ts +16 -5
  42. package/types/src/lib/cache-key.d.ts +1 -1
  43. package/types/src/lib/cookie-interceptor.d.ts +1 -1
  44. package/types/src/lib/debug/debug.page.d.ts +4 -2
  45. package/types/src/lib/define-route.d.ts +6 -1
  46. package/types/src/lib/endpoints.d.ts +1 -1
  47. package/types/src/lib/experimental.d.ts +140 -0
  48. package/types/src/lib/form-action.directive.d.ts +12 -5
  49. package/types/src/lib/inject-load.d.ts +5 -2
  50. package/types/src/lib/inject-navigate.d.ts +23 -0
  51. package/types/src/lib/inject-route-context.d.ts +32 -0
  52. package/types/src/lib/inject-typed-params.d.ts +63 -0
  53. package/types/src/lib/json-ld.d.ts +32 -0
  54. package/types/src/lib/meta-tags.d.ts +3 -1
  55. package/types/src/lib/models.d.ts +3 -0
  56. package/types/src/lib/provide-file-router-base.d.ts +4 -0
  57. package/types/src/lib/provide-file-router.d.ts +2 -8
  58. package/types/src/lib/route-builder.d.ts +5 -0
  59. package/types/src/lib/route-files.d.ts +18 -0
  60. package/types/src/lib/route-path.d.ts +124 -0
  61. package/types/src/lib/route-types.d.ts +2 -1
  62. package/types/src/lib/routes.d.ts +2 -10
  63. package/types/src/lib/validation-errors.d.ts +7 -0
  64. package/types/tanstack-query/server/src/index.d.ts +1 -0
  65. package/types/tanstack-query/src/index.d.ts +2 -0
  66. package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
  67. package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
  68. package/types/tanstack-query/src/server-query.d.ts +16 -0
@@ -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";
6
+ import { ChangeDetectionStrategy, Component, Directive, InjectionToken, Injector, PLATFORM_ID, TransferState, 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";
9
- import { isPlatformServer } from "@angular/common";
8
+ import { catchError, from, map, of, take, throwError } from "rxjs";
9
+ import { DOCUMENT, isPlatformServer } from "@angular/common";
10
+ import { DomSanitizer, Meta } from "@angular/platform-browser";
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
@@ -181,17 +227,19 @@ function requestContextInterceptor(req, next) {
181
227
  const requestUrl = new URL(req.url, baseUrl);
182
228
  const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
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 = {
@@ -232,62 +280,83 @@ var FormAction = class FormAction {
232
280
  this.state = output();
233
281
  this.router = inject(Router);
234
282
  this.route = inject(ActivatedRoute);
235
- this.path = this._getPath();
283
+ this.currentState = signal("idle", ...[]);
284
+ /** Cached during construction (injection context) so inject() works. */
285
+ this._endpointUrl = this.route ? injectRouteEndpointURL(this.route.snapshot) : void 0;
236
286
  }
237
287
  submitted($event) {
238
288
  $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);
289
+ const form = $event.target;
290
+ this._emitState("submitting");
291
+ const body = new FormData(form);
292
+ if (form.method.toUpperCase() === "GET") this._handleGet(body, this._getGetPath(form));
293
+ else this._handlePost(body, this._getPostPath(form), form.method);
243
294
  }
244
295
  _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"
296
+ const url = new URL(path, window.location.href);
297
+ const params = new URLSearchParams(url.search);
298
+ body.forEach((value, key) => {
299
+ params.append(key, value instanceof File ? value.name : value);
252
300
  });
301
+ url.search = params.toString();
302
+ this._emitState("navigate");
303
+ this._navigateTo(url);
253
304
  }
254
- _handlePost(body, path, $event) {
305
+ _handlePost(body, path, method) {
255
306
  fetch(path, {
256
- method: $event.target.method,
307
+ method,
257
308
  body
258
309
  }).then((res) => {
259
310
  if (res.ok) if (res.redirected) {
260
- const redirectUrl = new URL(res.url).pathname;
261
- this.state.emit("redirect");
262
- this.router.navigate([redirectUrl]);
311
+ this._emitState("redirect");
312
+ this._navigateTo(new URL(res.url, window.location.href));
263
313
  } else if (this._isJSON(res.headers.get("Content-type"))) res.json().then((result) => {
264
314
  this.onSuccess.emit(result);
265
- this.state.emit("success");
315
+ this._emitState("success");
266
316
  });
267
317
  else res.text().then((result) => {
268
318
  this.onSuccess.emit(result);
269
- this.state.emit("success");
319
+ this._emitState("success");
270
320
  });
271
321
  else if (res.headers.get("X-Analog-Errors")) res.json().then((errors) => {
272
322
  this.onError.emit(errors);
273
- this.state.emit("error");
323
+ this._emitState("error");
274
324
  });
275
- else this.state.emit("error");
325
+ else this._emitState("error");
276
326
  }).catch((_) => {
277
- this.state.emit("error");
327
+ this._emitState("error");
278
328
  });
279
329
  }
280
- _getPath() {
281
- if (this.route) return injectRouteEndpointURL(this.route.snapshot).pathname;
330
+ _getExplicitAction(form) {
331
+ return this.action().trim() || form.getAttribute("action")?.trim() || void 0;
332
+ }
333
+ _getGetPath(form) {
334
+ return this._getExplicitAction(form) ?? this.router.url;
335
+ }
336
+ _getPostPath(form) {
337
+ const explicitAction = this._getExplicitAction(form);
338
+ if (explicitAction) return new URL(explicitAction, window.location.href).toString();
339
+ if (this._endpointUrl) return this._endpointUrl.pathname;
282
340
  return `/api/_analog/pages${window.location.pathname}`;
283
341
  }
342
+ _emitState(state) {
343
+ this.currentState.set(state);
344
+ this.state.emit(state);
345
+ }
346
+ _navigateTo(url) {
347
+ if (url.origin === window.location.origin) {
348
+ this.router.navigateByUrl(`${url.pathname}${url.search}${url.hash}`, { onSameUrlNavigation: "reload" });
349
+ return;
350
+ }
351
+ window.location.assign(url.toString());
352
+ }
284
353
  _isJSON(contentType) {
285
354
  return (contentType ? contentType.split(";") : [])[0] === "application/json";
286
355
  }
287
356
  static {
288
357
  this.ɵfac = i0.ɵɵngDeclareFactory({
289
358
  minVersion: "12.0.0",
290
- version: "21.1.1",
359
+ version: "21.2.8",
291
360
  ngImport: i0,
292
361
  type: FormAction,
293
362
  deps: [],
@@ -297,7 +366,7 @@ var FormAction = class FormAction {
297
366
  static {
298
367
  this.ɵdir = i0.ɵɵngDeclareDirective({
299
368
  minVersion: "17.1.0",
300
- version: "21.1.1",
369
+ version: "21.2.8",
301
370
  type: FormAction,
302
371
  isStandalone: true,
303
372
  selector: "form[action],form[method]",
@@ -313,21 +382,31 @@ var FormAction = class FormAction {
313
382
  onError: "onError",
314
383
  state: "state"
315
384
  },
316
- host: { listeners: { "submit": "submitted($event)" } },
385
+ host: {
386
+ listeners: { "submit": "submitted($event)" },
387
+ properties: {
388
+ "attr.data-state": "currentState()",
389
+ "attr.aria-busy": "currentState() === \"submitting\" ? \"true\" : null"
390
+ }
391
+ },
317
392
  ngImport: i0
318
393
  });
319
394
  }
320
395
  };
321
396
  i0.ɵɵngDeclareClassMetadata({
322
397
  minVersion: "12.0.0",
323
- version: "21.1.1",
398
+ version: "21.2.8",
324
399
  ngImport: i0,
325
400
  type: FormAction,
326
401
  decorators: [{
327
402
  type: Directive,
328
403
  args: [{
329
404
  selector: "form[action],form[method]",
330
- host: { "(submit)": `submitted($event)` },
405
+ host: {
406
+ "(submit)": `submitted($event)`,
407
+ "[attr.data-state]": "currentState()",
408
+ "[attr.aria-busy]": "currentState() === \"submitting\" ? \"true\" : null"
409
+ },
331
410
  standalone: true
332
411
  }]
333
412
  }],
@@ -434,7 +513,7 @@ var ServerOnly = class ServerOnly {
434
513
  static {
435
514
  this.ɵfac = i0.ɵɵngDeclareFactory({
436
515
  minVersion: "12.0.0",
437
- version: "21.1.1",
516
+ version: "21.2.8",
438
517
  ngImport: i0,
439
518
  type: ServerOnly,
440
519
  deps: [],
@@ -444,7 +523,7 @@ var ServerOnly = class ServerOnly {
444
523
  static {
445
524
  this.ɵcmp = i0.ɵɵngDeclareComponent({
446
525
  minVersion: "17.1.0",
447
- version: "21.1.1",
526
+ version: "21.2.8",
448
527
  type: ServerOnly,
449
528
  isStandalone: true,
450
529
  selector: "server-only,ServerOnly,Server",
@@ -474,7 +553,7 @@ var ServerOnly = class ServerOnly {
474
553
  };
475
554
  i0.ɵɵngDeclareClassMetadata({
476
555
  minVersion: "12.0.0",
477
- version: "21.1.1",
556
+ version: "21.2.8",
478
557
  ngImport: i0,
479
558
  type: ServerOnly,
480
559
  decorators: [{
@@ -510,6 +589,425 @@ i0.ɵɵngDeclareClassMetadata({
510
589
  }
511
590
  });
512
591
  //#endregion
513
- export { FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectRouteEndpointURL, injectRouter, provideFileRouter, requestContextInterceptor, routes, withDebugRoutes, withExtraRoutes };
592
+ //#region packages/router/src/lib/validation-errors.ts
593
+ function getPathSegmentKey(segment) {
594
+ return typeof segment === "object" ? segment.key : segment;
595
+ }
596
+ function issuePathToFieldName(path) {
597
+ return path.map((segment) => String(getPathSegmentKey(segment))).join(".");
598
+ }
599
+ function issuesToFieldErrors(issues) {
600
+ return issues.reduce((errors, issue) => {
601
+ if (!issue.path?.length) return errors;
602
+ const fieldName = issuePathToFieldName(issue.path);
603
+ errors[fieldName] ??= [];
604
+ errors[fieldName].push(issue.message);
605
+ return errors;
606
+ }, {});
607
+ }
608
+ function issuesToFormErrors(issues) {
609
+ return issues.filter((issue) => !issue.path?.length).map((issue) => issue.message);
610
+ }
611
+ //#endregion
612
+ //#region packages/router/src/lib/route-path.ts
613
+ /**
614
+ * Typed route path utilities for Analog.
615
+ *
616
+ * This module provides:
617
+ * - The `AnalogRouteTable` base interface (augmented by generated code)
618
+ * - The `AnalogRoutePath` union type
619
+ * - The `routePath()` URL builder function
620
+ *
621
+ * No Angular dependencies — can be used in any context.
622
+ */
623
+ /**
624
+ * Builds a typed route link object from a route path pattern and options.
625
+ *
626
+ * The returned object separates path, query params, and fragment for
627
+ * direct use with Angular's routerLink directive inputs.
628
+ *
629
+ * @example
630
+ * routePath('/about')
631
+ * // → { path: '/about', queryParams: null, fragment: undefined }
632
+ *
633
+ * routePath('/users/[id]', { params: { id: '42' } })
634
+ * // → { path: '/users/42', queryParams: null, fragment: undefined }
635
+ *
636
+ * routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
637
+ * // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
638
+ *
639
+ * @example Template usage
640
+ * ```html
641
+ * @let link = routePath('/users/[id]', { params: { id: userId } });
642
+ * <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
643
+ * ```
644
+ */
645
+ function routePath(path, ...args) {
646
+ const options = args[0];
647
+ return buildRouteLink(path, options);
648
+ }
649
+ /**
650
+ * Internal: builds a `RouteLinkResult` from path and options.
651
+ * Exported for direct use in tests (avoids generic constraints).
652
+ */
653
+ function buildRouteLink(path, options) {
654
+ const resolvedPath = buildPath(path, options?.params);
655
+ let queryParams = null;
656
+ if (options?.query) {
657
+ const filtered = {};
658
+ let hasEntries = false;
659
+ for (const [key, value] of Object.entries(options.query)) if (value !== void 0) {
660
+ filtered[key] = value;
661
+ hasEntries = true;
662
+ }
663
+ if (hasEntries) queryParams = filtered;
664
+ }
665
+ return {
666
+ path: resolvedPath,
667
+ queryParams,
668
+ fragment: options?.hash
669
+ };
670
+ }
671
+ /**
672
+ * Resolves param placeholders and normalises slashes.
673
+ * Returns only the path — no query string or hash.
674
+ */
675
+ function buildPath(path, params) {
676
+ let url = path;
677
+ if (params) {
678
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
679
+ const value = params[name];
680
+ if (value == null) return "";
681
+ if (Array.isArray(value)) return value.map((v) => encodeURIComponent(v)).join("/");
682
+ return encodeURIComponent(String(value));
683
+ });
684
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
685
+ const value = params[name];
686
+ if (value == null) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
687
+ if (Array.isArray(value)) {
688
+ if (value.length === 0) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
689
+ return value.map((v) => encodeURIComponent(v)).join("/");
690
+ }
691
+ return encodeURIComponent(String(value));
692
+ });
693
+ url = url.replace(/\[([^\]]+)\]/g, (_, name) => {
694
+ const value = params[name];
695
+ if (value == null) throw new Error(`Missing required param "${name}" for path "${path}"`);
696
+ return encodeURIComponent(String(value));
697
+ });
698
+ } else {
699
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "");
700
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, "");
701
+ url = url.replace(/\[([^\]]+)\]/g, "");
702
+ }
703
+ url = url.replace(/\/+/g, "/");
704
+ if (url.length > 1 && url.endsWith("/")) url = url.slice(0, -1);
705
+ if (!url.startsWith("/")) url = "/" + url;
706
+ return url;
707
+ }
708
+ /**
709
+ * Internal URL builder. Separated from `routePath` so it can be
710
+ * used without generic constraints (e.g., in `injectNavigate`).
711
+ */
712
+ function buildUrl(path, options) {
713
+ let url = buildPath(path, options?.params);
714
+ if (options?.query) {
715
+ const parts = [];
716
+ for (const [key, value] of Object.entries(options.query)) {
717
+ if (value === void 0) continue;
718
+ if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
719
+ else parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
720
+ }
721
+ if (parts.length > 0) url += "?" + parts.join("&");
722
+ }
723
+ if (options?.hash) url += "#" + options.hash;
724
+ return url;
725
+ }
726
+ //#endregion
727
+ //#region packages/router/src/lib/inject-navigate.ts
728
+ function isRoutePathOptionsBase(value) {
729
+ return !!value && typeof value === "object" && ("params" in value || "query" in value || "hash" in value);
730
+ }
731
+ /**
732
+ * Injects a typed navigate function.
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * const navigate = injectNavigate();
737
+ *
738
+ * navigate('/users/[id]', { params: { id: '42' } }); // ✅
739
+ * navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
740
+ *
741
+ * // With navigation extras
742
+ * navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
743
+ * ```
744
+ */
745
+ function injectNavigate() {
746
+ const router = inject(Router);
747
+ const navigate = ((path, ...args) => {
748
+ let options;
749
+ let extras;
750
+ if (args.length > 1) {
751
+ options = args[0];
752
+ extras = args[1];
753
+ } else if (args.length === 1) if (isRoutePathOptionsBase(args[0])) options = args[0];
754
+ else extras = args[0];
755
+ const url = buildUrl(path, options);
756
+ return router.navigateByUrl(url, extras);
757
+ });
758
+ return navigate;
759
+ }
760
+ //#endregion
761
+ //#region packages/router/src/lib/experimental.ts
762
+ /** @experimental */
763
+ var EXPERIMENTAL_TYPED_ROUTER = new InjectionToken("EXPERIMENTAL_TYPED_ROUTER");
764
+ /** @experimental */
765
+ var EXPERIMENTAL_ROUTE_CONTEXT = new InjectionToken("EXPERIMENTAL_ROUTE_CONTEXT");
766
+ /** @experimental */
767
+ var EXPERIMENTAL_LOADER_CACHE = new InjectionToken("EXPERIMENTAL_LOADER_CACHE");
768
+ /**
769
+ * Enables experimental typed router features.
770
+ *
771
+ * When active, `routePath()`, `injectNavigate()`, `injectParams()`,
772
+ * and `injectQuery()` will enforce route table constraints and
773
+ * optionally log warnings in strict mode.
774
+ *
775
+ * Inspired by TanStack Router's `Register` interface and strict type
776
+ * checking across the entire navigation surface.
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * provideFileRouter(
781
+ * withTypedRouter({ strictRouteParams: true }),
782
+ * )
783
+ * ```
784
+ *
785
+ * @experimental
786
+ */
787
+ function withTypedRouter(options) {
788
+ return {
789
+ ɵkind: 102,
790
+ ɵproviders: [{
791
+ provide: EXPERIMENTAL_TYPED_ROUTER,
792
+ useValue: {
793
+ strictRouteParams: false,
794
+ ...options
795
+ }
796
+ }]
797
+ };
798
+ }
799
+ /**
800
+ * Provides root-level route context available to all route loaders
801
+ * and components via `injectRouteContext()`.
802
+ *
803
+ * Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
804
+ * a typed context object is required at router creation and automatically
805
+ * available in every route's `beforeLoad` and `loader`.
806
+ *
807
+ * In Angular terms, this creates a DI token that server-side load
808
+ * functions and components can inject to access shared services
809
+ * without importing them individually.
810
+ *
811
+ * @example
812
+ * ```ts
813
+ * // app.config.ts
814
+ * provideFileRouter(
815
+ * withRouteContext({
816
+ * auth: inject(AuthService),
817
+ * db: inject(DatabaseService),
818
+ * }),
819
+ * )
820
+ *
821
+ * // In a component
822
+ * const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
823
+ * ```
824
+ *
825
+ * @experimental
826
+ */
827
+ function withRouteContext(context) {
828
+ return {
829
+ ɵkind: 103,
830
+ ɵproviders: [{
831
+ provide: EXPERIMENTAL_ROUTE_CONTEXT,
832
+ useValue: context
833
+ }]
834
+ };
835
+ }
836
+ /**
837
+ * Configures experimental loader caching behavior for server-loaded
838
+ * route data.
839
+ *
840
+ * Inspired by TanStack Router's built-in cache where `createRouter()`
841
+ * accepts `defaultStaleTime` and `defaultGcTime` to control when
842
+ * loaders re-execute and when cached data is discarded.
843
+ *
844
+ * @example
845
+ * ```ts
846
+ * provideFileRouter(
847
+ * withLoaderCaching({
848
+ * defaultStaleTime: 30_000, // 30s before re-fetch
849
+ * defaultGcTime: 300_000, // 5min cache retention
850
+ * defaultPendingMs: 200, // 200ms loading delay
851
+ * }),
852
+ * )
853
+ * ```
854
+ *
855
+ * @experimental
856
+ */
857
+ function withLoaderCaching(options) {
858
+ return {
859
+ ɵkind: 104,
860
+ ɵproviders: [{
861
+ provide: EXPERIMENTAL_LOADER_CACHE,
862
+ useValue: {
863
+ defaultStaleTime: 0,
864
+ defaultGcTime: 3e5,
865
+ defaultPendingMs: 0,
866
+ ...options
867
+ }
868
+ }]
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region packages/router/src/lib/inject-typed-params.ts
873
+ function extractRouteParams(routePath) {
874
+ const params = [];
875
+ for (const match of routePath.matchAll(/\[\[\.\.\.([^\]]+)\]\]/g)) params.push({
876
+ name: match[1],
877
+ type: "optionalCatchAll"
878
+ });
879
+ for (const match of routePath.matchAll(/(?<!\[)\[\.\.\.([^\]]+)\](?!\])/g)) params.push({
880
+ name: match[1],
881
+ type: "catchAll"
882
+ });
883
+ for (const match of routePath.matchAll(/(?<!\[)\[(?!\.)([^\]]+)\](?!\])/g)) params.push({
884
+ name: match[1],
885
+ type: "dynamic"
886
+ });
887
+ return params;
888
+ }
889
+ /**
890
+ * When `strictRouteParams` is enabled, warns if expected params from the
891
+ * `_from` pattern are missing from the active `ActivatedRoute`.
892
+ */
893
+ function assertRouteMatch(from, route, kind) {
894
+ const expectedParams = extractRouteParams(from).filter((param) => param.type === "dynamic" || param.type === "catchAll").map((param) => param.name);
895
+ if (expectedParams.length === 0) return;
896
+ route.params.pipe(take(1)).subscribe((params) => {
897
+ for (const name of expectedParams) if (!(name in params)) {
898
+ 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}'.`);
899
+ break;
900
+ }
901
+ });
902
+ }
903
+ /**
904
+ * Injects typed route params as a signal, constrained by the route table.
905
+ *
906
+ * Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
907
+ * pattern where the `from` parameter narrows the return type to only
908
+ * the params defined for that route.
909
+ *
910
+ * The `from` parameter is used purely for TypeScript type inference —
911
+ * at runtime, params are read from the current `ActivatedRoute`. This
912
+ * means it works correctly when used inside a component rendered by
913
+ * the specified route.
914
+ *
915
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
916
+ * a dev-mode assertion checks that the expected params from `from`
917
+ * exist in the active route and warns on mismatch.
918
+ *
919
+ * @example
920
+ * ```ts
921
+ * // In a component rendered at /users/[id]
922
+ * const params = injectParams('/users/[id]');
923
+ * // params() → { id: string }
924
+ *
925
+ * // With schema validation output types
926
+ * const params = injectParams('/products/[slug]');
927
+ * // params() → validated output type from routeParamsSchema
928
+ * ```
929
+ *
930
+ * @experimental
931
+ */
932
+ function injectParams(_from, options) {
933
+ const injector = options?.injector;
934
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
935
+ if (isDevMode()) {
936
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectParams");
937
+ }
938
+ return toSignal(route.params.pipe(map((params) => params)), { requireSync: true });
939
+ }
940
+ /**
941
+ * Injects typed route query params as a signal, constrained by the
942
+ * route table.
943
+ *
944
+ * Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
945
+ * where search params are validated and typed per-route via
946
+ * `validateSearch` schemas.
947
+ *
948
+ * In Analog, the typing comes from `routeQuerySchema` exports that are
949
+ * detected at build time and recorded in the generated route table.
950
+ *
951
+ * The `from` parameter is used purely for TypeScript type inference.
952
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
953
+ * a dev-mode assertion checks that the expected params from `from`
954
+ * exist in the active route and warns on mismatch.
955
+ *
956
+ * @example
957
+ * ```ts
958
+ * // In a component rendered at /issues
959
+ * // (where routeQuerySchema validates { page: number, status: string })
960
+ * const query = injectQuery('/issues');
961
+ * // query() → { page: number; status: string }
962
+ * ```
963
+ *
964
+ * @experimental
965
+ */
966
+ function injectQuery(_from, options) {
967
+ const injector = options?.injector;
968
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
969
+ if (isDevMode()) {
970
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectQuery");
971
+ }
972
+ return toSignal(route.queryParams.pipe(map((params) => params)), { requireSync: true });
973
+ }
974
+ //#endregion
975
+ //#region packages/router/src/lib/inject-route-context.ts
976
+ /**
977
+ * Injects the root route context provided via `withRouteContext()`.
978
+ *
979
+ * Inspired by TanStack Router's context inheritance where
980
+ * `createRootRouteWithContext<T>()` makes a typed context available
981
+ * to every route's `beforeLoad` and `loader` callbacks.
982
+ *
983
+ * In Angular, this uses DI under the hood — `withRouteContext(ctx)`
984
+ * provides the value, and `injectRouteContext<T>()` retrieves it
985
+ * with the expected type.
986
+ *
987
+ * @example
988
+ * ```ts
989
+ * // app.config.ts
990
+ * provideFileRouter(
991
+ * withRouteContext({
992
+ * auth: inject(AuthService),
993
+ * analytics: inject(AnalyticsService),
994
+ * }),
995
+ * )
996
+ *
997
+ * // any-page.page.ts
998
+ * const ctx = injectRouteContext<{
999
+ * auth: AuthService;
1000
+ * analytics: AnalyticsService;
1001
+ * }>();
1002
+ * ctx.analytics.trackPageView();
1003
+ * ```
1004
+ *
1005
+ * @experimental
1006
+ */
1007
+ function injectRouteContext() {
1008
+ return inject(EXPERIMENTAL_ROUTE_CONTEXT);
1009
+ }
1010
+ //#endregion
1011
+ 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, issuePathToFieldName, issuesToFieldErrors, issuesToFormErrors, provideFileRouter, requestContextInterceptor, routePath, routes, withDebugRoutes, withExtraRoutes, withLoaderCaching, withRouteContext, withTypedRouter };
514
1012
 
515
1013
  //# sourceMappingURL=analogjs-router.mjs.map