@analogjs/router 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (90) 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 +327 -31
  5. package/fesm2022/analogjs-router-server-actions.mjs.map +1 -1
  6. package/fesm2022/analogjs-router-server.mjs +151 -199
  7. package/fesm2022/analogjs-router-server.mjs.map +1 -1
  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 +15 -18
  13. package/fesm2022/analogjs-router-tokens.mjs.map +1 -1
  14. package/fesm2022/analogjs-router.mjs +960 -815
  15. package/fesm2022/analogjs-router.mjs.map +1 -1
  16. package/fesm2022/debug.page.mjs +135 -0
  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 +345 -0
  21. package/fesm2022/route-files.mjs.map +1 -0
  22. package/fesm2022/routes.mjs +28 -0
  23. package/fesm2022/routes.mjs.map +1 -0
  24. package/package.json +67 -27
  25. package/server/actions/package.json +4 -0
  26. package/server/package.json +4 -0
  27. package/tanstack-query/package.json +4 -0
  28. package/tanstack-query/server/package.json +4 -0
  29. package/tokens/package.json +4 -0
  30. package/types/content/src/index.d.ts +4 -0
  31. package/types/content/src/lib/debug/routes.d.ts +10 -0
  32. package/types/content/src/lib/markdown-helpers.d.ts +2 -0
  33. package/types/content/src/lib/routes.d.ts +8 -0
  34. package/types/content/src/lib/with-content-routes.d.ts +2 -0
  35. package/types/server/actions/src/actions.d.ts +13 -0
  36. package/types/server/actions/src/define-action.d.ts +54 -0
  37. package/types/server/actions/src/define-api-route.d.ts +57 -0
  38. package/types/server/actions/src/define-page-load.d.ts +55 -0
  39. package/types/server/actions/src/define-server-route.d.ts +68 -0
  40. package/types/server/actions/src/index.d.ts +9 -0
  41. package/types/server/actions/src/parse-request-data.d.ts +9 -0
  42. package/types/server/actions/src/validate.d.ts +8 -0
  43. package/types/server/src/index.d.ts +4 -0
  44. package/types/server/src/provide-server-context.d.ts +11 -0
  45. package/types/server/src/render.d.ts +12 -0
  46. package/types/server/src/server-component-render.d.ts +4 -0
  47. package/types/server/src/tokens.d.ts +7 -0
  48. package/types/src/index.d.ts +27 -0
  49. package/types/src/lib/cache-key.d.ts +3 -0
  50. package/types/src/lib/constants.d.ts +2 -0
  51. package/types/src/lib/cookie-interceptor.d.ts +4 -0
  52. package/types/src/lib/debug/debug.page.d.ts +20 -0
  53. package/types/src/lib/debug/index.d.ts +10 -0
  54. package/types/src/lib/debug/routes.d.ts +10 -0
  55. package/types/src/lib/define-route.d.ts +51 -0
  56. package/types/src/lib/endpoints.d.ts +5 -0
  57. package/types/src/lib/experimental.d.ts +140 -0
  58. package/types/src/lib/form-action.directive.d.ts +25 -0
  59. package/types/src/lib/get-load-resolver.d.ts +8 -0
  60. package/types/src/lib/inject-load.d.ts +9 -0
  61. package/types/src/lib/inject-navigate.d.ts +23 -0
  62. package/types/src/lib/inject-route-context.d.ts +32 -0
  63. package/types/src/lib/inject-route-endpoint-url.d.ts +2 -0
  64. package/types/src/lib/inject-typed-params.d.ts +63 -0
  65. package/types/src/lib/json-ld.d.ts +31 -0
  66. package/types/src/lib/meta-tags.d.ts +33 -0
  67. package/types/src/lib/models.d.ts +32 -0
  68. package/types/src/lib/provide-file-router-base.d.ts +4 -0
  69. package/types/src/lib/provide-file-router.d.ts +12 -0
  70. package/types/src/lib/request-context.d.ts +13 -0
  71. package/types/src/lib/route-builder.d.ts +5 -0
  72. package/types/src/lib/route-config.d.ts +2 -0
  73. package/types/src/lib/route-files.d.ts +18 -0
  74. package/types/src/lib/route-path.d.ts +124 -0
  75. package/types/src/lib/route-types.d.ts +12 -0
  76. package/types/src/lib/routes.d.ts +11 -0
  77. package/types/src/lib/server.component.d.ts +33 -0
  78. package/types/src/lib/validation-errors.d.ts +7 -0
  79. package/types/tanstack-query/server/src/index.d.ts +1 -0
  80. package/types/tanstack-query/src/index.d.ts +2 -0
  81. package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
  82. package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
  83. package/types/tanstack-query/src/server-query.d.ts +16 -0
  84. package/types/tokens/src/index.d.ts +23 -0
  85. package/fesm2022/analogjs-router-debug.page-jzggTA45.mjs +0 -91
  86. package/fesm2022/analogjs-router-debug.page-jzggTA45.mjs.map +0 -1
  87. package/types/analogjs-router-server-actions.d.ts +0 -17
  88. package/types/analogjs-router-server.d.ts +0 -29
  89. package/types/analogjs-router-tokens.d.ts +0 -27
  90. package/types/analogjs-router.d.ts +0 -269
@@ -1,854 +1,999 @@
1
- import { Router, NavigationEnd, UrlSegment, ActivatedRoute, provideRouter, ROUTES } from '@angular/router';
2
- import * as i0 from '@angular/core';
3
- import { inject, PLATFORM_ID, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, Injector, makeStateKey, TransferState, input, output, Directive, InjectionToken, signal, effect, ChangeDetectionStrategy, Component } from '@angular/core';
4
- import { HttpClient, HttpHeaders, ɵHTTP_ROOT_INTERCEPTOR_FNS as _HTTP_ROOT_INTERCEPTOR_FNS, HttpResponse, HttpRequest } from '@angular/common/http';
5
- import { firstValueFrom, map, from, of, throwError, catchError } from 'rxjs';
6
- import { injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest, API_PREFIX } from '@analogjs/router/tokens';
7
- import { Meta, DomSanitizer } from '@angular/platform-browser';
8
- import { filter } from 'rxjs/operators';
9
- import { isPlatformServer } from '@angular/common';
10
-
11
- const ROUTE_META_TAGS_KEY = Symbol('@analogjs/router Route Meta Tags Key');
12
- const CHARSET_KEY = 'charset';
13
- const HTTP_EQUIV_KEY = 'httpEquiv';
14
- // httpEquiv selector key needs to be in kebab case format
15
- const HTTP_EQUIV_SELECTOR_KEY = 'http-equiv';
16
- const NAME_KEY = 'name';
17
- const PROPERTY_KEY = 'property';
18
- const CONTENT_KEY = 'content';
19
- const ITEMPROP_KEY = 'itemprop';
20
- function updateMetaTagsOnRouteChange() {
21
- const router = inject(Router);
22
- const metaService = inject(Meta);
23
- router.events
24
- .pipe(filter((event) => event instanceof NavigationEnd))
25
- .subscribe(() => {
26
- const metaTagMap = getMetaTagMap(router.routerState.snapshot.root);
27
- for (const metaTagSelector in metaTagMap) {
28
- const metaTag = metaTagMap[metaTagSelector];
29
- metaService.updateTag(metaTag, metaTagSelector);
30
- }
31
- });
1
+ 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";
2
+ import { n as createRoutes, r as routes, t as injectDebugRoutes } from "./routes.mjs";
3
+ import { ActivatedRoute, ROUTES, Router, provideRouter } from "@angular/router";
4
+ import * as i0 from "@angular/core";
5
+ import { ChangeDetectionStrategy, Component, Directive, ENVIRONMENT_INITIALIZER, InjectionToken, Injector, PLATFORM_ID, TransferState, effect, inject, input, isDevMode, makeEnvironmentProviders, makeStateKey, output, signal } from "@angular/core";
6
+ import { HttpClient, HttpHeaders, HttpRequest, HttpResponse, ɵHTTP_ROOT_INTERCEPTOR_FNS } from "@angular/common/http";
7
+ import { catchError, from, map, of, take, throwError } from "rxjs";
8
+ import { API_PREFIX, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest } from "@analogjs/router/tokens";
9
+ import { isPlatformServer } from "@angular/common";
10
+ import { DomSanitizer } from "@angular/platform-browser";
11
+ import { toSignal } from "@angular/core/rxjs-interop";
12
+ //#region packages/router/src/lib/define-route.ts
13
+ /**
14
+ * @deprecated Use `RouteMeta` type instead.
15
+ * For more info see: https://github.com/analogjs/analog/issues/223
16
+ *
17
+ * Defines additional route config metadata. This
18
+ * object is merged into the route config with
19
+ * the predefined file-based route.
20
+ *
21
+ * @usageNotes
22
+ *
23
+ * ```
24
+ * import { Component } from '@angular/core';
25
+ * import { defineRouteMeta } from '@analogjs/router';
26
+ *
27
+ * export const routeMeta = defineRouteMeta({
28
+ * title: 'Welcome'
29
+ * });
30
+ *
31
+ * @Component({
32
+ * template: `Home`,
33
+ * standalone: true,
34
+ * })
35
+ * export default class HomeComponent {}
36
+ * ```
37
+ *
38
+ * @param route
39
+ * @returns
40
+ */
41
+ var defineRouteMeta = (route) => {
42
+ return route;
43
+ };
44
+ /**
45
+ * Returns the instance of Angular Router
46
+ *
47
+ * @returns The router
48
+ */
49
+ var injectRouter = () => {
50
+ return inject(Router);
51
+ };
52
+ /**
53
+ * Returns the instance of the Activate Route for the component
54
+ *
55
+ * @returns The activated route
56
+ */
57
+ var injectActivatedRoute = () => {
58
+ return inject(ActivatedRoute);
59
+ };
60
+ //#endregion
61
+ //#region packages/router/src/lib/cookie-interceptor.ts
62
+ function cookieInterceptor(req, next, location = inject(PLATFORM_ID), serverRequest = injectRequest()) {
63
+ if (isPlatformServer(location) && req.url.includes("/_analog/")) {
64
+ let headers = new HttpHeaders();
65
+ const cookies = serverRequest?.headers.cookie;
66
+ headers = headers.set("cookie", cookies ?? "");
67
+ return next(req.clone({ headers }));
68
+ } else return next(req);
32
69
  }
33
- function getMetaTagMap(route) {
34
- const metaTagMap = {};
35
- let currentRoute = route;
36
- while (currentRoute) {
37
- const metaTags = currentRoute.data[ROUTE_META_TAGS_KEY] ?? [];
38
- for (const metaTag of metaTags) {
39
- metaTagMap[getMetaTagSelector(metaTag)] = metaTag;
40
- }
41
- currentRoute = currentRoute.firstChild;
42
- }
43
- return metaTagMap;
70
+ //#endregion
71
+ //#region packages/router/src/lib/provide-file-router-base.ts
72
+ function provideFileRouterWithRoutes(...features) {
73
+ const extraRoutesFeature = features.filter((feat) => feat.ɵkind >= 100);
74
+ const routerFeatures = features.filter((feat) => feat.ɵkind < 100);
75
+ return makeEnvironmentProviders([
76
+ extraRoutesFeature.map((erf) => erf.ɵproviders),
77
+ provideRouter([], ...routerFeatures),
78
+ {
79
+ provide: ROUTES,
80
+ multi: true,
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
+ }
95
+ },
96
+ {
97
+ provide: ENVIRONMENT_INITIALIZER,
98
+ multi: true,
99
+ useValue: () => updateMetaTagsOnRouteChange()
100
+ },
101
+ {
102
+ provide: ENVIRONMENT_INITIALIZER,
103
+ multi: true,
104
+ useValue: () => updateJsonLdOnRouteChange()
105
+ },
106
+ {
107
+ provide: ɵHTTP_ROOT_INTERCEPTOR_FNS,
108
+ multi: true,
109
+ useValue: cookieInterceptor
110
+ },
111
+ {
112
+ provide: API_PREFIX,
113
+ useFactory() {
114
+ return typeof ANALOG_API_PREFIX !== "undefined" ? ANALOG_API_PREFIX : "api";
115
+ }
116
+ }
117
+ ]);
44
118
  }
45
- function getMetaTagSelector(metaTag) {
46
- if (metaTag.name) {
47
- return `${NAME_KEY}="${metaTag.name}"`;
48
- }
49
- if (metaTag.property) {
50
- return `${PROPERTY_KEY}="${metaTag.property}"`;
51
- }
52
- if (metaTag.httpEquiv) {
53
- return `${HTTP_EQUIV_SELECTOR_KEY}="${metaTag.httpEquiv}"`;
54
- }
55
- if (metaTag.itemprop) {
56
- return `${ITEMPROP_KEY}="${metaTag.itemprop}"`;
57
- }
58
- return CHARSET_KEY;
119
+ function withExtraRoutes(routes) {
120
+ return {
121
+ ɵkind: 100,
122
+ ɵproviders: [{
123
+ provide: ROUTES,
124
+ useValue: routes,
125
+ multi: true
126
+ }]
127
+ };
59
128
  }
60
-
61
- const ANALOG_META_KEY = Symbol('@analogjs/router Analog Route Metadata Key');
129
+ //#endregion
130
+ //#region packages/router/src/lib/provide-file-router.ts
62
131
  /**
63
- * This variable reference is replaced with a glob of all route endpoints.
64
- */
65
- let ANALOG_PAGE_ENDPOINTS = {};
66
-
67
- function injectRouteEndpointURL(route) {
68
- const routeConfig = route.routeConfig;
69
- const apiPrefix = injectAPIPrefix();
70
- const baseUrl = injectBaseURL();
71
- const { queryParams, fragment: hash, params, parent } = route;
72
- const segment = parent?.url.map((segment) => segment.path).join('/') || '';
73
- const url = new URL('', import.meta.env['VITE_ANALOG_PUBLIC_BASE_URL'] ||
74
- baseUrl ||
75
- (typeof window !== 'undefined' && window.location.origin
76
- ? window.location.origin
77
- : ''));
78
- url.pathname = `${url.pathname.endsWith('/') ? url.pathname : url.pathname + '/'}${apiPrefix}/_analog${routeConfig[ANALOG_META_KEY].endpoint}`;
79
- url.search = `${new URLSearchParams(queryParams).toString()}`;
80
- url.hash = hash ?? '';
81
- Object.keys(params).forEach((param) => {
82
- url.pathname = url.pathname.replace(`[${param}]`, params[param]);
83
- });
84
- url.pathname = url.pathname.replace('**', segment);
85
- return url;
132
+ * Sets up providers for the Angular router, and registers
133
+ * file-based routes. Additional features can be provided
134
+ * to further configure the behavior of the router.
135
+ *
136
+ * @param features
137
+ * @returns Providers and features to configure the router with routes
138
+ */
139
+ function provideFileRouter(...features) {
140
+ return provideFileRouterWithRoutes(...features);
86
141
  }
87
-
88
- function toRouteConfig(routeMeta) {
89
- if (routeMeta && isRedirectRouteMeta(routeMeta)) {
90
- return routeMeta;
91
- }
92
- let { meta, ...routeConfig } = routeMeta ?? {};
93
- if (Array.isArray(meta)) {
94
- routeConfig.data = { ...routeConfig.data, [ROUTE_META_TAGS_KEY]: meta };
95
- }
96
- else if (typeof meta === 'function') {
97
- routeConfig.resolve = {
98
- ...routeConfig.resolve,
99
- [ROUTE_META_TAGS_KEY]: meta,
100
- };
101
- }
102
- if (!routeConfig) {
103
- routeConfig = {};
104
- }
105
- routeConfig.runGuardsAndResolvers =
106
- routeConfig.runGuardsAndResolvers ?? 'paramsOrQueryParamsChange';
107
- routeConfig.resolve = {
108
- ...routeConfig.resolve,
109
- load: async (route) => {
110
- const routeConfig = route.routeConfig;
111
- if (ANALOG_PAGE_ENDPOINTS[routeConfig[ANALOG_META_KEY].endpointKey]) {
112
- const http = inject(HttpClient);
113
- const url = injectRouteEndpointURL(route);
114
- const internalFetch = injectInternalServerFetch();
115
- if (internalFetch) {
116
- return internalFetch(url.pathname);
117
- }
118
- if (!!import.meta.env['VITE_ANALOG_PUBLIC_BASE_URL'] &&
119
- globalThis.$fetch) {
120
- return globalThis.$fetch(url.pathname);
121
- }
122
- return firstValueFrom(http.get(`${url.href}`));
123
- }
124
- return {};
125
- },
126
- };
127
- return routeConfig;
142
+ //#endregion
143
+ //#region packages/router/src/lib/inject-load.ts
144
+ function isResponse(value) {
145
+ return typeof value === "object" && value instanceof Response;
128
146
  }
129
- function isRedirectRouteMeta(routeMeta) {
130
- return !!routeMeta.redirectTo;
147
+ function injectLoad(options) {
148
+ return (options?.injector ?? inject(Injector)).get(ActivatedRoute).data.pipe(map((data) => data["load"]));
131
149
  }
132
-
133
- // The Zone is currently enabled by default, so we wouldn't need this check.
134
- // However, leaving this open space will be useful if zone.js becomes optional
135
- // in the future. This means we won't have to modify the current code, and it will
136
- // continue to work seamlessly.
137
- const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.root;
138
- function toMarkdownModule(markdownFileFactory) {
139
- return async () => {
140
- const createLoader = () => Promise.all([import('@analogjs/content'), markdownFileFactory()]);
141
- const [{ parseRawContentFile, MarkdownRouteComponent, ContentRenderer }, markdownFile,] = await (isNgZoneEnabled
142
- ? // We are not able to use `runOutsideAngular` because we are not inside
143
- // an injection context to retrieve the `NgZone` instance.
144
- // The `Zone.root.run` is required when the code is running in the
145
- // browser since asynchronous tasks being scheduled in the current context
146
- // are a reason for unnecessary change detection cycles.
147
- Zone.root.run(createLoader)
148
- : createLoader());
149
- const { content, attributes } = parseRawContentFile(markdownFile);
150
- const { title, meta } = attributes;
151
- return {
152
- default: MarkdownRouteComponent,
153
- routeMeta: {
154
- data: { _analogContent: content },
155
- title,
156
- meta,
157
- resolve: {
158
- renderedAnalogContent: async () => {
159
- const contentRenderer = inject(ContentRenderer);
160
- const rendered = await contentRenderer.render(content);
161
- return typeof rendered === 'string'
162
- ? rendered
163
- : rendered.content;
164
- },
165
- },
166
- },
167
- };
168
- };
150
+ function injectLoadData(options) {
151
+ return injectLoad(options).pipe(map((result) => {
152
+ if (isResponse(result)) throw new Error("Expected page load data but received a response.");
153
+ return result;
154
+ }));
169
155
  }
170
-
171
- const ENDPOINT_EXTENSION = '.server.ts';
172
- const APP_DIR = 'src/app';
173
-
156
+ //#endregion
157
+ //#region packages/router/src/lib/get-load-resolver.ts
174
158
  /**
175
- * This variable reference is replaced with a glob of all page routes.
176
- */
177
- let ANALOG_ROUTE_FILES = {};
178
- /**
179
- * This variable reference is replaced with a glob of all content routes.
180
- */
181
- let ANALOG_CONTENT_ROUTE_FILES = {};
182
- /**
183
- * A function used to parse list of files and create configuration of routes.
184
- *
185
- * @param files
186
- * @returns Array of routes
187
- */
188
- function createRoutes(files, debug = false) {
189
- const filenames = Object.keys(files);
190
- if (filenames.length === 0) {
191
- return [];
192
- }
193
- // map filenames to raw routes and group them by level
194
- const rawRoutesByLevelMap = filenames.reduce((acc, filename) => {
195
- const rawPath = toRawPath(filename);
196
- const rawSegments = rawPath.split('/');
197
- // nesting level starts at 0
198
- // rawPath: /products => level: 0
199
- // rawPath: /products/:id => level: 1
200
- const level = rawSegments.length - 1;
201
- const rawSegment = rawSegments[level];
202
- const ancestorRawSegments = rawSegments.slice(0, level);
203
- return {
204
- ...acc,
205
- [level]: {
206
- ...acc[level],
207
- [rawPath]: {
208
- filename,
209
- rawSegment,
210
- ancestorRawSegments,
211
- segment: toSegment(rawSegment),
212
- level,
213
- children: [],
214
- },
215
- },
216
- };
217
- }, {});
218
- const allLevels = Object.keys(rawRoutesByLevelMap).map(Number);
219
- const maxLevel = Math.max(...allLevels);
220
- // add each raw route to its parent's children array
221
- for (let level = maxLevel; level > 0; level--) {
222
- const rawRoutesMap = rawRoutesByLevelMap[level];
223
- const rawPaths = Object.keys(rawRoutesMap);
224
- for (const rawPath of rawPaths) {
225
- const rawRoute = rawRoutesMap[rawPath];
226
- const parentRawPath = rawRoute.ancestorRawSegments.join('/');
227
- const parentRawSegmentIndex = rawRoute.ancestorRawSegments.length - 1;
228
- const parentRawSegment = rawRoute.ancestorRawSegments[parentRawSegmentIndex];
229
- // create the parent level and/or raw route if it does not exist
230
- // parent route won't exist for nested routes that don't have a layout route
231
- rawRoutesByLevelMap[level - 1] ||= {};
232
- rawRoutesByLevelMap[level - 1][parentRawPath] ||= {
233
- filename: null,
234
- rawSegment: parentRawSegment,
235
- ancestorRawSegments: rawRoute.ancestorRawSegments.slice(0, parentRawSegmentIndex),
236
- segment: toSegment(parentRawSegment),
237
- level: level - 1,
238
- children: [],
239
- };
240
- rawRoutesByLevelMap[level - 1][parentRawPath].children.push(rawRoute);
241
- }
242
- }
243
- // only take raw routes from the root level
244
- // since they already contain nested routes as their children
245
- const rootRawRoutesMap = rawRoutesByLevelMap[0];
246
- const rawRoutes = Object.keys(rootRawRoutesMap).map((segment) => rootRawRoutesMap[segment]);
247
- sortRawRoutes(rawRoutes);
248
- return toRoutes(rawRoutes, files, debug);
249
- }
250
- function toRawPath(filename) {
251
- return (filename
252
- .replace(
253
- // convert to relative path and remove file extension
254
- /^(?:[a-zA-Z]:[\\/])?(.*?)[\\/](?:routes|pages)[\\/]|(?:[\\/](?:app[\\/](?:routes|pages)|src[\\/]content)[\\/])|(\.page\.(js|ts|analog|ag)$)|(\.(ts|md|analog|ag)$)/g, '')
255
- // [[...slug]] => placeholder (named empty) which is stripped by toSegment
256
- .replace(/\[\[\.\.\.([^\]]+)\]\]/g, '(opt-$1)')
257
- .replace(/\[\.{3}.+\]/, '**') // [...not-found] => **
258
- .replace(/\[([^\]]+)\]/g, ':$1')); // [id] => :id
259
- }
260
- function toSegment(rawSegment) {
261
- return rawSegment
262
- .replace(/index|\(.*?\)/g, '') // replace named empty segments
263
- .replace(/\.|\/+/g, '/') // replace dots with slashes and remove redundant slashes
264
- .replace(/^\/+|\/+$/g, ''); // remove trailing slashes
265
- }
266
- function createOptionalCatchAllMatcher(paramName) {
267
- return (segments) => {
268
- if (segments.length === 0) {
269
- return null;
270
- }
271
- const joined = segments.map((s) => s.path).join('/');
272
- return {
273
- consumed: segments,
274
- posParams: { [paramName]: new UrlSegment(joined, {}) },
275
- };
276
- };
159
+ * Get server load resolver data for the route
160
+ *
161
+ * @param route Provides the route to get server load resolver
162
+ * @returns Returns server load resolver data for the route
163
+ */
164
+ async function getLoadResolver(route) {
165
+ return route.routeConfig?.resolve?.["load"]?.(route);
277
166
  }
278
- function toRoutes(rawRoutes, files, debug = false) {
279
- const routes = [];
280
- for (const rawRoute of rawRoutes) {
281
- const children = rawRoute.children.length > 0
282
- ? toRoutes(rawRoute.children, files, debug)
283
- : undefined;
284
- let module = undefined;
285
- let analogMeta = undefined;
286
- if (rawRoute.filename) {
287
- const isMarkdownFile = rawRoute.filename.endsWith('.md');
288
- if (!debug) {
289
- module = isMarkdownFile
290
- ? toMarkdownModule(files[rawRoute.filename])
291
- : files[rawRoute.filename];
292
- }
293
- const endpointKey = rawRoute.filename.replace(/\.page\.(ts|analog|ag)$/, ENDPOINT_EXTENSION);
294
- // get endpoint path
295
- const rawEndpoint = rawRoute.filename
296
- .replace(/\.page\.(ts|analog|ag)$/, '')
297
- .replace(/\[\[\.\.\..+\]\]/, '**')
298
- .replace(/\[\.{3}.+\]/, '**') // [...not-found] => **
299
- .replace(/^(.*?)\/pages/, '/pages');
300
- // replace periods, remove (index) paths
301
- const endpoint = (rawEndpoint || '')
302
- .replace(/\./g, '/')
303
- .replace(/\/\((.*?)\)$/, '/-$1-');
304
- analogMeta = {
305
- endpoint,
306
- endpointKey,
307
- };
308
- }
309
- // Detect Next.js-style optional catch-all at this node: [[...param]]
310
- const optCatchAllMatch = rawRoute.filename?.match(/\[\[\.\.\.([^\]]+)\]\]/);
311
- const optCatchAllParam = optCatchAllMatch ? optCatchAllMatch[1] : null;
312
- const route = module
313
- ? {
314
- path: rawRoute.segment,
315
- loadChildren: () => module().then((m) => {
316
- if (import.meta.env.DEV) {
317
- const hasModuleDefault = !!m.default;
318
- const hasRedirect = !!m.routeMeta?.redirectTo;
319
- if (!hasModuleDefault && !hasRedirect) {
320
- console.warn(`[Analog] Missing default export at ${rawRoute.filename}`);
321
- }
322
- }
323
- const baseChild = {
324
- path: '',
325
- component: m.default,
326
- ...toRouteConfig(m.routeMeta),
327
- children,
328
- [ANALOG_META_KEY]: analogMeta,
329
- };
330
- // Base route first so static matches win, then optional catch-all matcher
331
- return [
332
- {
333
- ...baseChild,
334
- },
335
- ...(optCatchAllParam
336
- ? [
337
- {
338
- matcher: createOptionalCatchAllMatcher(optCatchAllParam),
339
- component: m.default,
340
- ...toRouteConfig(m.routeMeta),
341
- [ANALOG_META_KEY]: analogMeta,
342
- },
343
- ]
344
- : []),
345
- ];
346
- }),
347
- }
348
- : {
349
- path: rawRoute.segment,
350
- ...(debug
351
- ? {
352
- filename: rawRoute.filename ? rawRoute.filename : undefined,
353
- isLayout: children && children.length > 0 ? true : false,
354
- }
355
- : {}),
356
- children,
357
- };
358
- routes.push(route);
359
- }
360
- return routes;
167
+ //#endregion
168
+ //#region packages/router/src/lib/cache-key.ts
169
+ function sortAndConcatParams(params) {
170
+ return [...params.keys()].sort().map((k) => `${k}=${params.getAll(k)}`).join("&");
361
171
  }
362
- function sortRawRoutes(rawRoutes) {
363
- rawRoutes.sort((a, b) => {
364
- let segmentA = deprioritizeSegment(a.segment);
365
- let segmentB = deprioritizeSegment(b.segment);
366
- // prioritize routes with fewer children
367
- if (a.children.length > b.children.length) {
368
- segmentA = `~${segmentA}`;
369
- }
370
- else if (a.children.length < b.children.length) {
371
- segmentB = `~${segmentB}`;
372
- }
373
- return segmentA > segmentB ? 1 : -1;
374
- });
375
- for (const rawRoute of rawRoutes) {
376
- sortRawRoutes(rawRoute.children);
377
- }
172
+ function makeCacheKey(request, mappedRequestUrl) {
173
+ const { params, method, responseType } = request;
174
+ const encodedParams = sortAndConcatParams(params);
175
+ let serializedBody = request.serializeBody();
176
+ if (serializedBody instanceof URLSearchParams) serializedBody = sortAndConcatParams(serializedBody);
177
+ else if (typeof serializedBody !== "string") serializedBody = "";
178
+ return makeStateKey(generateHash([
179
+ method,
180
+ responseType,
181
+ mappedRequestUrl,
182
+ serializedBody,
183
+ encodedParams
184
+ ].join("|")));
378
185
  }
379
- function deprioritizeSegment(segment) {
380
- // deprioritize param and wildcard segments
381
- return segment.replace(':', '~~').replace('**', '~~~~');
186
+ function generateHash(str) {
187
+ let hash = 0;
188
+ for (let i = 0, len = str.length; i < len; i++) {
189
+ const chr = str.charCodeAt(i);
190
+ hash = (hash << 5) - hash + chr;
191
+ hash |= 0;
192
+ }
193
+ return `${hash}`;
382
194
  }
383
- const routes = createRoutes({
384
- ...ANALOG_ROUTE_FILES,
385
- ...ANALOG_CONTENT_ROUTE_FILES,
386
- });
387
-
195
+ //#endregion
196
+ //#region packages/router/src/lib/request-context.ts
388
197
  /**
389
- * @deprecated Use `RouteMeta` type instead.
390
- * For more info see: https://github.com/analogjs/analog/issues/223
391
- *
392
- * Defines additional route config metadata. This
393
- * object is merged into the route config with
394
- * the predefined file-based route.
395
- *
396
- * @usageNotes
397
- *
398
- * ```
399
- * import { Component } from '@angular/core';
400
- * import { defineRouteMeta } from '@analogjs/router';
401
- *
402
- * export const routeMeta = defineRouteMeta({
403
- * title: 'Welcome'
404
- * });
405
- *
406
- * @Component({
407
- * template: `Home`,
408
- * standalone: true,
409
- * })
410
- * export default class HomeComponent {}
411
- * ```
412
- *
413
- * @param route
414
- * @returns
415
- */
416
- const defineRouteMeta = (route) => {
417
- return route;
198
+ * Interceptor that is server-aware when making HttpClient requests.
199
+ * Server-side requests use the full URL
200
+ * Prerendering uses the internal Nitro $fetch function, along with state transfer
201
+ * Client-side requests use the window.location.origin
202
+ *
203
+ * @param req HttpRequest<unknown>
204
+ * @param next HttpHandlerFn
205
+ * @returns
206
+ */
207
+ function requestContextInterceptor(req, next) {
208
+ const apiPrefix = injectAPIPrefix();
209
+ const baseUrl = injectBaseURL();
210
+ const transferState = inject(TransferState);
211
+ const nitroGlobal = globalThis;
212
+ const serverFetch = injectInternalServerFetch() ?? nitroGlobal.$fetch;
213
+ if (serverFetch && baseUrl && (req.url.startsWith("/") || req.url.startsWith(baseUrl) || req.url.startsWith(`/${apiPrefix}`))) {
214
+ const requestUrl = new URL(req.url, baseUrl);
215
+ const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
216
+ const fetchUrl = requestUrl.pathname;
217
+ const responseType = req.responseType === "arraybuffer" ? "arrayBuffer" : req.responseType;
218
+ return from(serverFetch.raw(fetchUrl, {
219
+ method: req.method,
220
+ body: req.body ? req.body : void 0,
221
+ params: requestUrl.searchParams,
222
+ responseType,
223
+ headers: req.headers.keys().reduce((hdrs, current) => {
224
+ const value = req.headers.get(current);
225
+ return value != null ? {
226
+ ...hdrs,
227
+ [current]: value
228
+ } : hdrs;
229
+ }, {})
230
+ }).then((res) => {
231
+ const cacheResponse = {
232
+ body: res._data,
233
+ headers: new HttpHeaders(res.headers),
234
+ status: res.status ?? 200,
235
+ statusText: res.statusText ?? "OK",
236
+ url: fetchUrl
237
+ };
238
+ const transferResponse = new HttpResponse(cacheResponse);
239
+ transferState.set(storeKey, cacheResponse);
240
+ return transferResponse;
241
+ }));
242
+ }
243
+ if (req.url.startsWith("/") || req.url.includes("/_analog/")) {
244
+ const requestUrl = req.url.includes("/_analog/") ? req.url : `${window.location.origin}${req.url}`;
245
+ const storeKey = makeStateKey(`analog_${makeCacheKey(req, new URL(requestUrl).pathname)}`);
246
+ const cacheRestoreResponse = transferState.get(storeKey, null);
247
+ if (cacheRestoreResponse) {
248
+ transferState.remove(storeKey);
249
+ return of(new HttpResponse(cacheRestoreResponse));
250
+ }
251
+ return next(req.clone({ url: requestUrl }));
252
+ }
253
+ if (baseUrl && (req.url.startsWith("/") || req.url.startsWith(baseUrl))) {
254
+ const requestUrl = req.url.startsWith(baseUrl) && !req.url.startsWith("/") ? req.url : `${baseUrl}${req.url}`;
255
+ return next(req.clone({ url: requestUrl }));
256
+ }
257
+ return next(req);
258
+ }
259
+ //#endregion
260
+ //#region packages/router/src/lib/form-action.directive.ts
261
+ var FormAction = class FormAction {
262
+ constructor() {
263
+ this.action = input("", ...[]);
264
+ this.onSuccess = output();
265
+ this.onError = output();
266
+ this.state = output();
267
+ this.router = inject(Router);
268
+ this.route = inject(ActivatedRoute);
269
+ this.currentState = signal("idle", ...[]);
270
+ /** Cached during construction (injection context) so inject() works. */
271
+ this._endpointUrl = this.route ? injectRouteEndpointURL(this.route.snapshot) : void 0;
272
+ }
273
+ submitted($event) {
274
+ $event.preventDefault();
275
+ const form = $event.target;
276
+ this._emitState("submitting");
277
+ const body = new FormData(form);
278
+ if (form.method.toUpperCase() === "GET") this._handleGet(body, this._getGetPath(form));
279
+ else this._handlePost(body, this._getPostPath(form), form.method);
280
+ }
281
+ _handleGet(body, path) {
282
+ const url = new URL(path, window.location.href);
283
+ const params = new URLSearchParams(url.search);
284
+ body.forEach((value, key) => {
285
+ params.append(key, value instanceof File ? value.name : value);
286
+ });
287
+ url.search = params.toString();
288
+ this._emitState("navigate");
289
+ this._navigateTo(url);
290
+ }
291
+ _handlePost(body, path, method) {
292
+ fetch(path, {
293
+ method,
294
+ body
295
+ }).then((res) => {
296
+ if (res.ok) if (res.redirected) {
297
+ this._emitState("redirect");
298
+ this._navigateTo(new URL(res.url, window.location.href));
299
+ } else if (this._isJSON(res.headers.get("Content-type"))) res.json().then((result) => {
300
+ this.onSuccess.emit(result);
301
+ this._emitState("success");
302
+ });
303
+ else res.text().then((result) => {
304
+ this.onSuccess.emit(result);
305
+ this._emitState("success");
306
+ });
307
+ else if (res.headers.get("X-Analog-Errors")) res.json().then((errors) => {
308
+ this.onError.emit(errors);
309
+ this._emitState("error");
310
+ });
311
+ else this._emitState("error");
312
+ }).catch((_) => {
313
+ this._emitState("error");
314
+ });
315
+ }
316
+ _getExplicitAction(form) {
317
+ return this.action().trim() || form.getAttribute("action")?.trim() || void 0;
318
+ }
319
+ _getGetPath(form) {
320
+ return this._getExplicitAction(form) ?? this.router.url;
321
+ }
322
+ _getPostPath(form) {
323
+ const explicitAction = this._getExplicitAction(form);
324
+ if (explicitAction) return new URL(explicitAction, window.location.href).toString();
325
+ if (this._endpointUrl) return this._endpointUrl.pathname;
326
+ return `/api/_analog/pages${window.location.pathname}`;
327
+ }
328
+ _emitState(state) {
329
+ this.currentState.set(state);
330
+ this.state.emit(state);
331
+ }
332
+ _navigateTo(url) {
333
+ if (url.origin === window.location.origin) {
334
+ this.router.navigateByUrl(`${url.pathname}${url.search}${url.hash}`, { onSameUrlNavigation: "reload" });
335
+ return;
336
+ }
337
+ window.location.assign(url.toString());
338
+ }
339
+ _isJSON(contentType) {
340
+ return (contentType ? contentType.split(";") : [])[0] === "application/json";
341
+ }
342
+ static {
343
+ this.ɵfac = i0.ɵɵngDeclareFactory({
344
+ minVersion: "12.0.0",
345
+ version: "21.2.6",
346
+ ngImport: i0,
347
+ type: FormAction,
348
+ deps: [],
349
+ target: i0.ɵɵFactoryTarget.Directive
350
+ });
351
+ }
352
+ static {
353
+ this.ɵdir = i0.ɵɵngDeclareDirective({
354
+ minVersion: "17.1.0",
355
+ version: "21.2.6",
356
+ type: FormAction,
357
+ isStandalone: true,
358
+ selector: "form[action],form[method]",
359
+ inputs: { action: {
360
+ classPropertyName: "action",
361
+ publicName: "action",
362
+ isSignal: true,
363
+ isRequired: false,
364
+ transformFunction: null
365
+ } },
366
+ outputs: {
367
+ onSuccess: "onSuccess",
368
+ onError: "onError",
369
+ state: "state"
370
+ },
371
+ host: {
372
+ listeners: { "submit": "submitted($event)" },
373
+ properties: {
374
+ "attr.data-state": "currentState()",
375
+ "attr.aria-busy": "currentState() === \"submitting\" ? \"true\" : null"
376
+ }
377
+ },
378
+ ngImport: i0
379
+ });
380
+ }
418
381
  };
382
+ i0.ɵɵngDeclareClassMetadata({
383
+ minVersion: "12.0.0",
384
+ version: "21.2.6",
385
+ ngImport: i0,
386
+ type: FormAction,
387
+ decorators: [{
388
+ type: Directive,
389
+ args: [{
390
+ selector: "form[action],form[method]",
391
+ host: {
392
+ "(submit)": `submitted($event)`,
393
+ "[attr.data-state]": "currentState()",
394
+ "[attr.aria-busy]": "currentState() === \"submitting\" ? \"true\" : null"
395
+ },
396
+ standalone: true
397
+ }]
398
+ }],
399
+ propDecorators: {
400
+ action: [{
401
+ type: i0.Input,
402
+ args: [{
403
+ isSignal: true,
404
+ alias: "action",
405
+ required: false
406
+ }]
407
+ }],
408
+ onSuccess: [{
409
+ type: i0.Output,
410
+ args: ["onSuccess"]
411
+ }],
412
+ onError: [{
413
+ type: i0.Output,
414
+ args: ["onError"]
415
+ }],
416
+ state: [{
417
+ type: i0.Output,
418
+ args: ["state"]
419
+ }]
420
+ }
421
+ });
422
+ //#endregion
423
+ //#region packages/router/src/lib/debug/index.ts
419
424
  /**
420
- * Returns the instance of Angular Router
421
- *
422
- * @returns The router
423
- */
424
- const injectRouter = () => {
425
- return inject(Router);
426
- };
425
+ * Provides routes that provide additional
426
+ * pages for displaying and debugging
427
+ * routes.
428
+ */
429
+ function withDebugRoutes() {
430
+ return {
431
+ ɵkind: 101,
432
+ ɵproviders: [{
433
+ provide: ROUTES,
434
+ useValue: [{
435
+ path: "__analog/routes",
436
+ loadComponent: () => import("./debug.page.mjs")
437
+ }],
438
+ multi: true
439
+ }]
440
+ };
441
+ }
442
+ //#endregion
443
+ //#region packages/router/src/lib/server.component.ts
427
444
  /**
428
- * Returns the instance of the Activate Route for the component
429
- *
430
- * @returns The activated route
431
- */
432
- const injectActivatedRoute = () => {
433
- return inject(ActivatedRoute);
445
+ * @description
446
+ * Component that defines the bridge between the client and server-only
447
+ * components. The component passes the component ID and props to the server
448
+ * and retrieves the rendered HTML and outputs from the server-only component.
449
+ *
450
+ * Status: experimental
451
+ */
452
+ var ServerOnly = class ServerOnly {
453
+ constructor() {
454
+ this.component = input.required(...[]);
455
+ this.props = input(...[]);
456
+ this.outputs = output();
457
+ this.http = inject(HttpClient);
458
+ this.sanitizer = inject(DomSanitizer);
459
+ this.content = signal("", ...[]);
460
+ this.route = inject(ActivatedRoute, { optional: true });
461
+ this.baseURL = injectBaseURL();
462
+ this.transferState = inject(TransferState);
463
+ effect(() => {
464
+ const routeComponentId = this.route?.snapshot.data["component"];
465
+ const props = this.props() || {};
466
+ const componentId = routeComponentId || this.component();
467
+ const headers = new HttpHeaders(new Headers({
468
+ "Content-type": "application/json",
469
+ "X-Analog-Component": componentId
470
+ }));
471
+ const componentUrl = this.getComponentUrl(componentId);
472
+ const httpRequest = new HttpRequest("POST", componentUrl, props, { headers });
473
+ const storeKey = makeStateKey(makeCacheKey(httpRequest, new URL(componentUrl).pathname));
474
+ const componentState = this.transferState.get(storeKey, null);
475
+ if (componentState) {
476
+ this.updateContent(componentState);
477
+ this.transferState.remove(storeKey);
478
+ } else this.http.request(httpRequest).pipe(map((response) => {
479
+ if (response instanceof HttpResponse) return response.body;
480
+ return throwError(() => ({}));
481
+ }), catchError((error) => {
482
+ console.log(error);
483
+ return of({
484
+ html: "",
485
+ outputs: {}
486
+ });
487
+ })).subscribe((content) => this.updateContent(content));
488
+ });
489
+ }
490
+ updateContent(content) {
491
+ this.content.set(this.sanitizer.bypassSecurityTrustHtml(content.html));
492
+ this.outputs.emit(content.outputs);
493
+ }
494
+ getComponentUrl(componentId) {
495
+ let baseURL = this.baseURL;
496
+ if (!baseURL && typeof window !== "undefined") baseURL = window.location.origin;
497
+ return `${baseURL}/_analog/components/${componentId}`;
498
+ }
499
+ static {
500
+ this.ɵfac = i0.ɵɵngDeclareFactory({
501
+ minVersion: "12.0.0",
502
+ version: "21.2.6",
503
+ ngImport: i0,
504
+ type: ServerOnly,
505
+ deps: [],
506
+ target: i0.ɵɵFactoryTarget.Component
507
+ });
508
+ }
509
+ static {
510
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
511
+ minVersion: "17.1.0",
512
+ version: "21.2.6",
513
+ type: ServerOnly,
514
+ isStandalone: true,
515
+ selector: "server-only,ServerOnly,Server",
516
+ inputs: {
517
+ component: {
518
+ classPropertyName: "component",
519
+ publicName: "component",
520
+ isSignal: true,
521
+ isRequired: true,
522
+ transformFunction: null
523
+ },
524
+ props: {
525
+ classPropertyName: "props",
526
+ publicName: "props",
527
+ isSignal: true,
528
+ isRequired: false,
529
+ transformFunction: null
530
+ }
531
+ },
532
+ outputs: { outputs: "outputs" },
533
+ ngImport: i0,
534
+ template: ` <div [innerHTML]="content()"></div> `,
535
+ isInline: true,
536
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
537
+ });
538
+ }
434
539
  };
435
-
436
- function cookieInterceptor(req, next, location = inject(PLATFORM_ID), serverRequest = injectRequest()) {
437
- if (isPlatformServer(location) && req.url.includes('/_analog/')) {
438
- let headers = new HttpHeaders();
439
- const cookies = serverRequest?.headers.cookie;
440
- headers = headers.set('cookie', cookies ?? '');
441
- const cookiedRequest = req.clone({
442
- headers,
443
- });
444
- return next(cookiedRequest);
445
- }
446
- else {
447
- return next(req);
448
- }
540
+ i0.ɵɵngDeclareClassMetadata({
541
+ minVersion: "12.0.0",
542
+ version: "21.2.6",
543
+ ngImport: i0,
544
+ type: ServerOnly,
545
+ decorators: [{
546
+ type: Component,
547
+ args: [{
548
+ selector: "server-only,ServerOnly,Server",
549
+ changeDetection: ChangeDetectionStrategy.OnPush,
550
+ template: ` <div [innerHTML]="content()"></div> `
551
+ }]
552
+ }],
553
+ ctorParameters: () => [],
554
+ propDecorators: {
555
+ component: [{
556
+ type: i0.Input,
557
+ args: [{
558
+ isSignal: true,
559
+ alias: "component",
560
+ required: true
561
+ }]
562
+ }],
563
+ props: [{
564
+ type: i0.Input,
565
+ args: [{
566
+ isSignal: true,
567
+ alias: "props",
568
+ required: false
569
+ }]
570
+ }],
571
+ outputs: [{
572
+ type: i0.Output,
573
+ args: ["outputs"]
574
+ }]
575
+ }
576
+ });
577
+ //#endregion
578
+ //#region packages/router/src/lib/validation-errors.ts
579
+ function getPathSegmentKey(segment) {
580
+ return typeof segment === "object" ? segment.key : segment;
449
581
  }
450
-
582
+ function issuePathToFieldName(path) {
583
+ return path.map((segment) => String(getPathSegmentKey(segment))).join(".");
584
+ }
585
+ function issuesToFieldErrors(issues) {
586
+ return issues.reduce((errors, issue) => {
587
+ if (!issue.path?.length) return errors;
588
+ const fieldName = issuePathToFieldName(issue.path);
589
+ errors[fieldName] ??= [];
590
+ errors[fieldName].push(issue.message);
591
+ return errors;
592
+ }, {});
593
+ }
594
+ function issuesToFormErrors(issues) {
595
+ return issues.filter((issue) => !issue.path?.length).map((issue) => issue.message);
596
+ }
597
+ //#endregion
598
+ //#region packages/router/src/lib/route-path.ts
451
599
  /**
452
- * Sets up providers for the Angular router, and registers
453
- * file-based routes. Additional features can be provided
454
- * to further configure the behavior of the router.
455
- *
456
- * @param features
457
- * @returns Providers and features to configure the router with routes
458
- */
459
- function provideFileRouter(...features) {
460
- const extraRoutesFeature = features.filter((feat) => feat.ɵkind >= 100);
461
- const routerFeatures = features.filter((feat) => feat.ɵkind < 100);
462
- return makeEnvironmentProviders([
463
- extraRoutesFeature.map((erf) => erf.ɵproviders),
464
- provideRouter(routes, ...routerFeatures),
465
- {
466
- provide: ENVIRONMENT_INITIALIZER,
467
- multi: true,
468
- useValue: () => updateMetaTagsOnRouteChange(),
469
- },
470
- {
471
- provide: _HTTP_ROOT_INTERCEPTOR_FNS,
472
- multi: true,
473
- useValue: cookieInterceptor,
474
- },
475
- {
476
- provide: API_PREFIX,
477
- useFactory() {
478
- return typeof ANALOG_API_PREFIX !== 'undefined'
479
- ? ANALOG_API_PREFIX
480
- : 'api';
481
- },
482
- },
483
- ]);
600
+ * Typed route path utilities for Analog.
601
+ *
602
+ * This module provides:
603
+ * - The `AnalogRouteTable` base interface (augmented by generated code)
604
+ * - The `AnalogRoutePath` union type
605
+ * - The `routePath()` URL builder function
606
+ *
607
+ * No Angular dependencies — can be used in any context.
608
+ */
609
+ /**
610
+ * Builds a typed route link object from a route path pattern and options.
611
+ *
612
+ * The returned object separates path, query params, and fragment for
613
+ * direct use with Angular's routerLink directive inputs.
614
+ *
615
+ * @example
616
+ * routePath('/about')
617
+ * // → { path: '/about', queryParams: null, fragment: undefined }
618
+ *
619
+ * routePath('/users/[id]', { params: { id: '42' } })
620
+ * // → { path: '/users/42', queryParams: null, fragment: undefined }
621
+ *
622
+ * routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
623
+ * // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
624
+ *
625
+ * @example Template usage
626
+ * ```html
627
+ * @let link = routePath('/users/[id]', { params: { id: userId } });
628
+ * <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
629
+ * ```
630
+ */
631
+ function routePath(path, ...args) {
632
+ const options = args[0];
633
+ return buildRouteLink(path, options);
484
634
  }
485
635
  /**
486
- * Provides extra custom routes in addition to the routes
487
- * discovered from the filesystem-based routing. These routes are
488
- * inserted before the filesystem-based routes, and take priority in
489
- * route matching.
490
- */
491
- function withExtraRoutes(routes) {
492
- return {
493
- ɵkind: 100,
494
- ɵproviders: [{ provide: ROUTES, useValue: routes, multi: true }],
495
- };
636
+ * Internal: builds a `RouteLinkResult` from path and options.
637
+ * Exported for direct use in tests (avoids generic constraints).
638
+ */
639
+ function buildRouteLink(path, options) {
640
+ const resolvedPath = buildPath(path, options?.params);
641
+ let queryParams = null;
642
+ if (options?.query) {
643
+ const filtered = {};
644
+ let hasEntries = false;
645
+ for (const [key, value] of Object.entries(options.query)) if (value !== void 0) {
646
+ filtered[key] = value;
647
+ hasEntries = true;
648
+ }
649
+ if (hasEntries) queryParams = filtered;
650
+ }
651
+ return {
652
+ path: resolvedPath,
653
+ queryParams,
654
+ fragment: options?.hash
655
+ };
496
656
  }
497
-
498
- function injectLoad(options) {
499
- const injector = options?.injector ?? inject(Injector);
500
- const route = injector.get(ActivatedRoute);
501
- return route.data.pipe(map((data) => data['load']));
657
+ /**
658
+ * Resolves param placeholders and normalises slashes.
659
+ * Returns only the path — no query string or hash.
660
+ */
661
+ function buildPath(path, params) {
662
+ let url = path;
663
+ if (params) {
664
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
665
+ const value = params[name];
666
+ if (value == null) return "";
667
+ if (Array.isArray(value)) return value.map((v) => encodeURIComponent(v)).join("/");
668
+ return encodeURIComponent(String(value));
669
+ });
670
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
671
+ const value = params[name];
672
+ if (value == null) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
673
+ if (Array.isArray(value)) {
674
+ if (value.length === 0) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
675
+ return value.map((v) => encodeURIComponent(v)).join("/");
676
+ }
677
+ return encodeURIComponent(String(value));
678
+ });
679
+ url = url.replace(/\[([^\]]+)\]/g, (_, name) => {
680
+ const value = params[name];
681
+ if (value == null) throw new Error(`Missing required param "${name}" for path "${path}"`);
682
+ return encodeURIComponent(String(value));
683
+ });
684
+ } else {
685
+ url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "");
686
+ url = url.replace(/\[\.\.\.([^\]]+)\]/g, "");
687
+ url = url.replace(/\[([^\]]+)\]/g, "");
688
+ }
689
+ url = url.replace(/\/+/g, "/");
690
+ if (url.length > 1 && url.endsWith("/")) url = url.slice(0, -1);
691
+ if (!url.startsWith("/")) url = "/" + url;
692
+ return url;
502
693
  }
503
-
504
694
  /**
505
- * Get server load resolver data for the route
506
- *
507
- * @param route Provides the route to get server load resolver
508
- * @returns Returns server load resolver data for the route
509
- */
510
- async function getLoadResolver(route) {
511
- return route.routeConfig?.resolve?.['load']?.(route);
695
+ * Internal URL builder. Separated from `routePath` so it can be
696
+ * used without generic constraints (e.g., in `injectNavigate`).
697
+ */
698
+ function buildUrl(path, options) {
699
+ let url = buildPath(path, options?.params);
700
+ if (options?.query) {
701
+ const parts = [];
702
+ for (const [key, value] of Object.entries(options.query)) {
703
+ if (value === void 0) continue;
704
+ if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
705
+ else parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
706
+ }
707
+ if (parts.length > 0) url += "?" + parts.join("&");
708
+ }
709
+ if (options?.hash) url += "#" + options.hash;
710
+ return url;
512
711
  }
513
-
514
- function sortAndConcatParams(params) {
515
- return [...params.keys()]
516
- .sort()
517
- .map((k) => `${k}=${params.getAll(k)}`)
518
- .join('&');
712
+ //#endregion
713
+ //#region packages/router/src/lib/inject-navigate.ts
714
+ function isRoutePathOptionsBase(value) {
715
+ return !!value && typeof value === "object" && ("params" in value || "query" in value || "hash" in value);
519
716
  }
520
- function makeCacheKey(request, mappedRequestUrl) {
521
- // make the params encoded same as a url so it's easy to identify
522
- const { params, method, responseType } = request;
523
- const encodedParams = sortAndConcatParams(params);
524
- let serializedBody = request.serializeBody();
525
- if (serializedBody instanceof URLSearchParams) {
526
- serializedBody = sortAndConcatParams(serializedBody);
527
- }
528
- else if (typeof serializedBody !== 'string') {
529
- serializedBody = '';
530
- }
531
- const key = [
532
- method,
533
- responseType,
534
- mappedRequestUrl,
535
- serializedBody,
536
- encodedParams,
537
- ].join('|');
538
- const hash = generateHash(key);
539
- return makeStateKey(hash);
717
+ /**
718
+ * Injects a typed navigate function.
719
+ *
720
+ * @example
721
+ * ```ts
722
+ * const navigate = injectNavigate();
723
+ *
724
+ * navigate('/users/[id]', { params: { id: '42' } }); // ✅
725
+ * navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
726
+ *
727
+ * // With navigation extras
728
+ * navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
729
+ * ```
730
+ */
731
+ function injectNavigate() {
732
+ const router = inject(Router);
733
+ const navigate = ((path, ...args) => {
734
+ let options;
735
+ let extras;
736
+ if (args.length > 1) {
737
+ options = args[0];
738
+ extras = args[1];
739
+ } else if (args.length === 1) if (isRoutePathOptionsBase(args[0])) options = args[0];
740
+ else extras = args[0];
741
+ const url = buildUrl(path, options);
742
+ return router.navigateByUrl(url, extras);
743
+ });
744
+ return navigate;
540
745
  }
541
- function generateHash(str) {
542
- let hash = 0;
543
- for (let i = 0, len = str.length; i < len; i++) {
544
- let chr = str.charCodeAt(i);
545
- hash = (hash << 5) - hash + chr;
546
- hash |= 0; // Convert to 32bit integer
547
- }
548
- return `${hash}`;
746
+ //#endregion
747
+ //#region packages/router/src/lib/experimental.ts
748
+ /** @experimental */
749
+ var EXPERIMENTAL_TYPED_ROUTER = new InjectionToken("EXPERIMENTAL_TYPED_ROUTER");
750
+ /** @experimental */
751
+ var EXPERIMENTAL_ROUTE_CONTEXT = new InjectionToken("EXPERIMENTAL_ROUTE_CONTEXT");
752
+ /** @experimental */
753
+ var EXPERIMENTAL_LOADER_CACHE = new InjectionToken("EXPERIMENTAL_LOADER_CACHE");
754
+ /**
755
+ * Enables experimental typed router features.
756
+ *
757
+ * When active, `routePath()`, `injectNavigate()`, `injectParams()`,
758
+ * and `injectQuery()` will enforce route table constraints and
759
+ * optionally log warnings in strict mode.
760
+ *
761
+ * Inspired by TanStack Router's `Register` interface and strict type
762
+ * checking across the entire navigation surface.
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * provideFileRouter(
767
+ * withTypedRouter({ strictRouteParams: true }),
768
+ * )
769
+ * ```
770
+ *
771
+ * @experimental
772
+ */
773
+ function withTypedRouter(options) {
774
+ return {
775
+ ɵkind: 102,
776
+ ɵproviders: [{
777
+ provide: EXPERIMENTAL_TYPED_ROUTER,
778
+ useValue: {
779
+ strictRouteParams: false,
780
+ ...options
781
+ }
782
+ }]
783
+ };
549
784
  }
550
-
551
785
  /**
552
- * Interceptor that is server-aware when making HttpClient requests.
553
- * Server-side requests use the full URL
554
- * Prerendering uses the internal Nitro $fetch function, along with state transfer
555
- * Client-side requests use the window.location.origin
556
- *
557
- * @param req HttpRequest<unknown>
558
- * @param next HttpHandlerFn
559
- * @returns
560
- */
561
- function requestContextInterceptor(req, next) {
562
- const apiPrefix = injectAPIPrefix();
563
- const baseUrl = injectBaseURL();
564
- const transferState = inject(TransferState);
565
- const nitroGlobal = globalThis;
566
- const internalFetch = injectInternalServerFetch();
567
- const serverFetch = internalFetch ?? nitroGlobal.$fetch;
568
- // during prerendering with Nitro
569
- if (serverFetch &&
570
- baseUrl &&
571
- (req.url.startsWith('/') ||
572
- req.url.startsWith(baseUrl) ||
573
- req.url.startsWith(`/${apiPrefix}`))) {
574
- const requestUrl = new URL(req.url, baseUrl);
575
- const cacheKey = makeCacheKey(req, new URL(requestUrl).pathname);
576
- const storeKey = makeStateKey(`analog_${cacheKey}`);
577
- const fetchUrl = requestUrl.pathname;
578
- const responseType = req.responseType === 'arraybuffer' ? 'arrayBuffer' : req.responseType;
579
- return from(serverFetch
580
- .raw(fetchUrl, {
581
- method: req.method,
582
- body: req.body ? req.body : undefined,
583
- params: requestUrl.searchParams,
584
- responseType,
585
- headers: req.headers
586
- .keys()
587
- .reduce((hdrs, current) => {
588
- return {
589
- ...hdrs,
590
- [current]: req.headers.get(current),
591
- };
592
- }, {}),
593
- })
594
- .then((res) => {
595
- const cacheResponse = {
596
- body: res._data,
597
- headers: new HttpHeaders(res.headers),
598
- status: res.status ?? 200,
599
- statusText: res.statusText ?? 'OK',
600
- url: fetchUrl,
601
- };
602
- const transferResponse = new HttpResponse(cacheResponse);
603
- transferState.set(storeKey, cacheResponse);
604
- return transferResponse;
605
- }));
606
- }
607
- // on the client
608
- if (!import.meta.env.SSR &&
609
- (req.url.startsWith('/') || req.url.includes('/_analog/'))) {
610
- // /_analog/ requests are full URLs
611
- const requestUrl = req.url.includes('/_analog/')
612
- ? req.url
613
- : `${window.location.origin}${req.url}`;
614
- const cacheKey = makeCacheKey(req, new URL(requestUrl).pathname);
615
- const storeKey = makeStateKey(`analog_${cacheKey}`);
616
- const cacheRestoreResponse = transferState.get(storeKey, null);
617
- if (cacheRestoreResponse) {
618
- transferState.remove(storeKey);
619
- return of(new HttpResponse(cacheRestoreResponse));
620
- }
621
- return next(req.clone({
622
- url: requestUrl,
623
- }));
624
- }
625
- // on the server
626
- if (baseUrl && (req.url.startsWith('/') || req.url.startsWith(baseUrl))) {
627
- const requestUrl = req.url.startsWith(baseUrl) && !req.url.startsWith('/')
628
- ? req.url
629
- : `${baseUrl}${req.url}`;
630
- return next(req.clone({
631
- url: requestUrl,
632
- }));
633
- }
634
- return next(req);
786
+ * Provides root-level route context available to all route loaders
787
+ * and components via `injectRouteContext()`.
788
+ *
789
+ * Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
790
+ * a typed context object is required at router creation and automatically
791
+ * available in every route's `beforeLoad` and `loader`.
792
+ *
793
+ * In Angular terms, this creates a DI token that server-side load
794
+ * functions and components can inject to access shared services
795
+ * without importing them individually.
796
+ *
797
+ * @example
798
+ * ```ts
799
+ * // app.config.ts
800
+ * provideFileRouter(
801
+ * withRouteContext({
802
+ * auth: inject(AuthService),
803
+ * db: inject(DatabaseService),
804
+ * }),
805
+ * )
806
+ *
807
+ * // In a component
808
+ * const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
809
+ * ```
810
+ *
811
+ * @experimental
812
+ */
813
+ function withRouteContext(context) {
814
+ return {
815
+ ɵkind: 103,
816
+ ɵproviders: [{
817
+ provide: EXPERIMENTAL_ROUTE_CONTEXT,
818
+ useValue: context
819
+ }]
820
+ };
635
821
  }
636
-
637
- class FormAction {
638
- constructor() {
639
- this.action = input('', ...(ngDevMode ? [{ debugName: "action" }] : []));
640
- this.onSuccess = output();
641
- this.onError = output();
642
- this.state = output();
643
- this.router = inject(Router);
644
- this.route = inject(ActivatedRoute);
645
- this.path = this._getPath();
646
- }
647
- submitted($event) {
648
- $event.preventDefault();
649
- this.state.emit('submitting');
650
- const body = new FormData($event.target);
651
- if ($event.target.method.toUpperCase() === 'GET') {
652
- this._handleGet(body, this.router.url);
653
- }
654
- else {
655
- this._handlePost(body, this.path, $event);
656
- }
657
- }
658
- _handleGet(body, path) {
659
- const params = {};
660
- body.forEach((formVal, formKey) => (params[formKey] = formVal));
661
- this.state.emit('navigate');
662
- const url = path.split('?')[0];
663
- this.router.navigate([url], {
664
- queryParams: params,
665
- onSameUrlNavigation: 'reload',
666
- });
667
- }
668
- _handlePost(body, path, $event) {
669
- fetch(path, {
670
- method: $event.target.method,
671
- body,
672
- })
673
- .then((res) => {
674
- if (res.ok) {
675
- if (res.redirected) {
676
- const redirectUrl = new URL(res.url).pathname;
677
- this.state.emit('redirect');
678
- this.router.navigate([redirectUrl]);
679
- }
680
- else if (this._isJSON(res.headers.get('Content-type'))) {
681
- res.json().then((result) => {
682
- this.onSuccess.emit(result);
683
- this.state.emit('success');
684
- });
685
- }
686
- else {
687
- res.text().then((result) => {
688
- this.onSuccess.emit(result);
689
- this.state.emit('success');
690
- });
691
- }
692
- }
693
- else {
694
- if (res.headers.get('X-Analog-Errors')) {
695
- res.json().then((errors) => {
696
- this.onError.emit(errors);
697
- this.state.emit('error');
698
- });
699
- }
700
- else {
701
- this.state.emit('error');
702
- }
703
- }
704
- })
705
- .catch((_) => {
706
- this.state.emit('error');
707
- });
708
- }
709
- _getPath() {
710
- if (this.route) {
711
- return injectRouteEndpointURL(this.route.snapshot).pathname;
712
- }
713
- return `/api/_analog/pages${window.location.pathname}`;
714
- }
715
- _isJSON(contentType) {
716
- const mime = contentType ? contentType.split(';') : [];
717
- const essence = mime[0];
718
- return essence === 'application/json';
719
- }
720
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: FormAction, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
721
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: FormAction, isStandalone: true, selector: "form[action],form[method]", inputs: { action: { classPropertyName: "action", publicName: "action", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuccess: "onSuccess", onError: "onError", state: "state" }, host: { listeners: { "submit": "submitted($event)" } }, ngImport: i0 }); }
822
+ /**
823
+ * Configures experimental loader caching behavior for server-loaded
824
+ * route data.
825
+ *
826
+ * Inspired by TanStack Router's built-in cache where `createRouter()`
827
+ * accepts `defaultStaleTime` and `defaultGcTime` to control when
828
+ * loaders re-execute and when cached data is discarded.
829
+ *
830
+ * @example
831
+ * ```ts
832
+ * provideFileRouter(
833
+ * withLoaderCaching({
834
+ * defaultStaleTime: 30_000, // 30s before re-fetch
835
+ * defaultGcTime: 300_000, // 5min cache retention
836
+ * defaultPendingMs: 200, // 200ms loading delay
837
+ * }),
838
+ * )
839
+ * ```
840
+ *
841
+ * @experimental
842
+ */
843
+ function withLoaderCaching(options) {
844
+ return {
845
+ ɵkind: 104,
846
+ ɵproviders: [{
847
+ provide: EXPERIMENTAL_LOADER_CACHE,
848
+ useValue: {
849
+ defaultStaleTime: 0,
850
+ defaultGcTime: 3e5,
851
+ defaultPendingMs: 0,
852
+ ...options
853
+ }
854
+ }]
855
+ };
722
856
  }
723
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: FormAction, decorators: [{
724
- type: Directive,
725
- args: [{
726
- selector: 'form[action],form[method]',
727
- host: {
728
- '(submit)': `submitted($event)`,
729
- },
730
- standalone: true,
731
- }]
732
- }], propDecorators: { action: [{ type: i0.Input, args: [{ isSignal: true, alias: "action", required: false }] }], onSuccess: [{ type: i0.Output, args: ["onSuccess"] }], onError: [{ type: i0.Output, args: ["onError"] }], state: [{ type: i0.Output, args: ["state"] }] } });
733
-
734
- const DEBUG_ROUTES = new InjectionToken('@analogjs/router debug routes', {
735
- providedIn: 'root',
736
- factory() {
737
- const debugRoutes = createRoutes({
738
- ...ANALOG_ROUTE_FILES,
739
- ...ANALOG_CONTENT_ROUTE_FILES,
740
- }, true);
741
- return debugRoutes;
742
- },
743
- });
744
- function injectDebugRoutes() {
745
- return inject(DEBUG_ROUTES);
857
+ //#endregion
858
+ //#region packages/router/src/lib/inject-typed-params.ts
859
+ function extractRouteParams(routePath) {
860
+ const params = [];
861
+ for (const match of routePath.matchAll(/\[\[\.\.\.([^\]]+)\]\]/g)) params.push({
862
+ name: match[1],
863
+ type: "optionalCatchAll"
864
+ });
865
+ for (const match of routePath.matchAll(/(?<!\[)\[\.\.\.([^\]]+)\](?!\])/g)) params.push({
866
+ name: match[1],
867
+ type: "catchAll"
868
+ });
869
+ for (const match of routePath.matchAll(/(?<!\[)\[(?!\.)([^\]]+)\](?!\])/g)) params.push({
870
+ name: match[1],
871
+ type: "dynamic"
872
+ });
873
+ return params;
746
874
  }
747
-
748
875
  /**
749
- * Provides routes that provide additional
750
- * pages for displaying and debugging
751
- * routes.
752
- */
753
- function withDebugRoutes() {
754
- const routes = [
755
- {
756
- path: '__analog/routes',
757
- loadComponent: () => import('./analogjs-router-debug.page-jzggTA45.mjs'),
758
- },
759
- ];
760
- return {
761
- ɵkind: 101,
762
- ɵproviders: [{ provide: ROUTES, useValue: routes, multi: true }],
763
- };
876
+ * When `strictRouteParams` is enabled, warns if expected params from the
877
+ * `_from` pattern are missing from the active `ActivatedRoute`.
878
+ */
879
+ function assertRouteMatch(from, route, kind) {
880
+ const expectedParams = extractRouteParams(from).filter((param) => param.type === "dynamic" || param.type === "catchAll").map((param) => param.name);
881
+ if (expectedParams.length === 0) return;
882
+ route.params.pipe(take(1)).subscribe((params) => {
883
+ for (const name of expectedParams) if (!(name in params)) {
884
+ 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}'.`);
885
+ break;
886
+ }
887
+ });
764
888
  }
765
-
766
889
  /**
767
- * @description
768
- * Component that defines the bridge between the client and server-only
769
- * components. The component passes the component ID and props to the server
770
- * and retrieves the rendered HTML and outputs from the server-only component.
771
- *
772
- * Status: experimental
773
- */
774
- class ServerOnly {
775
- constructor() {
776
- this.component = input.required(...(ngDevMode ? [{ debugName: "component" }] : []));
777
- this.props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : []));
778
- this.outputs = output();
779
- this.http = inject(HttpClient);
780
- this.sanitizer = inject(DomSanitizer);
781
- this.content = signal('', ...(ngDevMode ? [{ debugName: "content" }] : []));
782
- this.route = inject(ActivatedRoute, { optional: true });
783
- this.baseURL = injectBaseURL();
784
- this.transferState = inject(TransferState);
785
- effect(() => {
786
- const routeComponentId = this.route?.snapshot.data['component'];
787
- const props = this.props() || {};
788
- const componentId = routeComponentId || this.component();
789
- const headers = new HttpHeaders(new Headers({
790
- 'Content-type': 'application/json',
791
- 'X-Analog-Component': componentId,
792
- }));
793
- const componentUrl = this.getComponentUrl(componentId);
794
- const httpRequest = new HttpRequest('POST', componentUrl, props, {
795
- headers,
796
- });
797
- const cacheKey = makeCacheKey(httpRequest, new URL(componentUrl).pathname);
798
- const storeKey = makeStateKey(cacheKey);
799
- const componentState = this.transferState.get(storeKey, null);
800
- if (componentState) {
801
- this.updateContent(componentState);
802
- this.transferState.remove(storeKey);
803
- }
804
- else {
805
- this.http
806
- .request(httpRequest)
807
- .pipe(map((response) => {
808
- if (response instanceof HttpResponse) {
809
- if (import.meta.env.SSR) {
810
- this.transferState.set(storeKey, response.body);
811
- }
812
- return response.body;
813
- }
814
- return throwError(() => ({}));
815
- }), catchError((error) => {
816
- console.log(error);
817
- return of({
818
- html: '',
819
- outputs: {},
820
- });
821
- }))
822
- .subscribe((content) => this.updateContent(content));
823
- }
824
- });
825
- }
826
- updateContent(content) {
827
- this.content.set(this.sanitizer.bypassSecurityTrustHtml(content.html));
828
- this.outputs.emit(content.outputs);
829
- }
830
- getComponentUrl(componentId) {
831
- let baseURL = this.baseURL;
832
- if (!baseURL && typeof window !== 'undefined') {
833
- baseURL = window.location.origin;
834
- }
835
- return `${baseURL}/_analog/components/${componentId}`;
836
- }
837
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ServerOnly, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
838
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.1", type: ServerOnly, isStandalone: true, selector: "server-only,ServerOnly,Server", inputs: { component: { classPropertyName: "component", publicName: "component", isSignal: true, isRequired: true, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outputs: "outputs" }, ngImport: i0, template: ` <div [innerHTML]="content()"></div> `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
890
+ * Injects typed route params as a signal, constrained by the route table.
891
+ *
892
+ * Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
893
+ * pattern where the `from` parameter narrows the return type to only
894
+ * the params defined for that route.
895
+ *
896
+ * The `from` parameter is used purely for TypeScript type inference —
897
+ * at runtime, params are read from the current `ActivatedRoute`. This
898
+ * means it works correctly when used inside a component rendered by
899
+ * the specified route.
900
+ *
901
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
902
+ * a dev-mode assertion checks that the expected params from `from`
903
+ * exist in the active route and warns on mismatch.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * // In a component rendered at /users/[id]
908
+ * const params = injectParams('/users/[id]');
909
+ * // params() → { id: string }
910
+ *
911
+ * // With schema validation output types
912
+ * const params = injectParams('/products/[slug]');
913
+ * // params() → validated output type from routeParamsSchema
914
+ * ```
915
+ *
916
+ * @experimental
917
+ */
918
+ function injectParams(_from, options) {
919
+ const injector = options?.injector;
920
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
921
+ if (isDevMode()) {
922
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectParams");
923
+ }
924
+ return toSignal(route.params.pipe(map((params) => params)), { requireSync: true });
839
925
  }
840
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ServerOnly, decorators: [{
841
- type: Component,
842
- args: [{
843
- selector: 'server-only,ServerOnly,Server',
844
- changeDetection: ChangeDetectionStrategy.OnPush,
845
- template: ` <div [innerHTML]="content()"></div> `,
846
- }]
847
- }], ctorParameters: () => [], propDecorators: { component: [{ type: i0.Input, args: [{ isSignal: true, alias: "component", required: true }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], outputs: [{ type: i0.Output, args: ["outputs"] }] } });
848
-
849
926
  /**
850
- * Generated bundle index. Do not edit.
851
- */
927
+ * Injects typed route query params as a signal, constrained by the
928
+ * route table.
929
+ *
930
+ * Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
931
+ * where search params are validated and typed per-route via
932
+ * `validateSearch` schemas.
933
+ *
934
+ * In Analog, the typing comes from `routeQuerySchema` exports that are
935
+ * detected at build time and recorded in the generated route table.
936
+ *
937
+ * The `from` parameter is used purely for TypeScript type inference.
938
+ * When `withTypedRouter({ strictRouteParams: true })` is configured,
939
+ * a dev-mode assertion checks that the expected params from `from`
940
+ * exist in the active route and warns on mismatch.
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * // In a component rendered at /issues
945
+ * // (where routeQuerySchema validates { page: number, status: string })
946
+ * const query = injectQuery('/issues');
947
+ * // query() → { page: number; status: string }
948
+ * ```
949
+ *
950
+ * @experimental
951
+ */
952
+ function injectQuery(_from, options) {
953
+ const injector = options?.injector;
954
+ const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
955
+ if (isDevMode()) {
956
+ if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectQuery");
957
+ }
958
+ return toSignal(route.queryParams.pipe(map((params) => params)), { requireSync: true });
959
+ }
960
+ //#endregion
961
+ //#region packages/router/src/lib/inject-route-context.ts
962
+ /**
963
+ * Injects the root route context provided via `withRouteContext()`.
964
+ *
965
+ * Inspired by TanStack Router's context inheritance where
966
+ * `createRootRouteWithContext<T>()` makes a typed context available
967
+ * to every route's `beforeLoad` and `loader` callbacks.
968
+ *
969
+ * In Angular, this uses DI under the hood — `withRouteContext(ctx)`
970
+ * provides the value, and `injectRouteContext<T>()` retrieves it
971
+ * with the expected type.
972
+ *
973
+ * @example
974
+ * ```ts
975
+ * // app.config.ts
976
+ * provideFileRouter(
977
+ * withRouteContext({
978
+ * auth: inject(AuthService),
979
+ * analytics: inject(AnalyticsService),
980
+ * }),
981
+ * )
982
+ *
983
+ * // any-page.page.ts
984
+ * const ctx = injectRouteContext<{
985
+ * auth: AuthService;
986
+ * analytics: AnalyticsService;
987
+ * }>();
988
+ * ctx.analytics.trackPageView();
989
+ * ```
990
+ *
991
+ * @experimental
992
+ */
993
+ function injectRouteContext() {
994
+ return inject(EXPERIMENTAL_ROUTE_CONTEXT);
995
+ }
996
+ //#endregion
997
+ 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 };
852
998
 
853
- export { FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectRouteEndpointURL, injectRouter, provideFileRouter, requestContextInterceptor, routes, withDebugRoutes, withExtraRoutes };
854
- //# sourceMappingURL=analogjs-router.mjs.map
999
+ //# sourceMappingURL=analogjs-router.mjs.map