@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.
- package/content/package.json +4 -0
- package/fesm2022/analogjs-router-content.mjs +63 -0
- package/fesm2022/analogjs-router-content.mjs.map +1 -0
- package/fesm2022/analogjs-router-server-actions.mjs +327 -31
- package/fesm2022/analogjs-router-server-actions.mjs.map +1 -1
- package/fesm2022/analogjs-router-server.mjs +151 -199
- package/fesm2022/analogjs-router-server.mjs.map +1 -1
- package/fesm2022/analogjs-router-tanstack-query-server.mjs +22 -0
- package/fesm2022/analogjs-router-tanstack-query-server.mjs.map +1 -0
- package/fesm2022/analogjs-router-tanstack-query.mjs +39 -0
- package/fesm2022/analogjs-router-tanstack-query.mjs.map +1 -0
- package/fesm2022/analogjs-router-tokens.mjs +15 -18
- package/fesm2022/analogjs-router-tokens.mjs.map +1 -1
- package/fesm2022/analogjs-router.mjs +960 -815
- package/fesm2022/analogjs-router.mjs.map +1 -1
- package/fesm2022/debug.page.mjs +135 -0
- package/fesm2022/debug.page.mjs.map +1 -0
- package/fesm2022/provide-analog-query.mjs +23 -0
- package/fesm2022/provide-analog-query.mjs.map +1 -0
- package/fesm2022/route-files.mjs +345 -0
- package/fesm2022/route-files.mjs.map +1 -0
- package/fesm2022/routes.mjs +28 -0
- package/fesm2022/routes.mjs.map +1 -0
- package/package.json +67 -27
- package/server/actions/package.json +4 -0
- package/server/package.json +4 -0
- package/tanstack-query/package.json +4 -0
- package/tanstack-query/server/package.json +4 -0
- package/tokens/package.json +4 -0
- package/types/content/src/index.d.ts +4 -0
- package/types/content/src/lib/debug/routes.d.ts +10 -0
- package/types/content/src/lib/markdown-helpers.d.ts +2 -0
- package/types/content/src/lib/routes.d.ts +8 -0
- package/types/content/src/lib/with-content-routes.d.ts +2 -0
- package/types/server/actions/src/actions.d.ts +13 -0
- package/types/server/actions/src/define-action.d.ts +54 -0
- package/types/server/actions/src/define-api-route.d.ts +57 -0
- package/types/server/actions/src/define-page-load.d.ts +55 -0
- package/types/server/actions/src/define-server-route.d.ts +68 -0
- package/types/server/actions/src/index.d.ts +9 -0
- package/types/server/actions/src/parse-request-data.d.ts +9 -0
- package/types/server/actions/src/validate.d.ts +8 -0
- package/types/server/src/index.d.ts +4 -0
- package/types/server/src/provide-server-context.d.ts +11 -0
- package/types/server/src/render.d.ts +12 -0
- package/types/server/src/server-component-render.d.ts +4 -0
- package/types/server/src/tokens.d.ts +7 -0
- package/types/src/index.d.ts +27 -0
- package/types/src/lib/cache-key.d.ts +3 -0
- package/types/src/lib/constants.d.ts +2 -0
- package/types/src/lib/cookie-interceptor.d.ts +4 -0
- package/types/src/lib/debug/debug.page.d.ts +20 -0
- package/types/src/lib/debug/index.d.ts +10 -0
- package/types/src/lib/debug/routes.d.ts +10 -0
- package/types/src/lib/define-route.d.ts +51 -0
- package/types/src/lib/endpoints.d.ts +5 -0
- package/types/src/lib/experimental.d.ts +140 -0
- package/types/src/lib/form-action.directive.d.ts +25 -0
- package/types/src/lib/get-load-resolver.d.ts +8 -0
- package/types/src/lib/inject-load.d.ts +9 -0
- package/types/src/lib/inject-navigate.d.ts +23 -0
- package/types/src/lib/inject-route-context.d.ts +32 -0
- package/types/src/lib/inject-route-endpoint-url.d.ts +2 -0
- package/types/src/lib/inject-typed-params.d.ts +63 -0
- package/types/src/lib/json-ld.d.ts +31 -0
- package/types/src/lib/meta-tags.d.ts +33 -0
- package/types/src/lib/models.d.ts +32 -0
- package/types/src/lib/provide-file-router-base.d.ts +4 -0
- package/types/src/lib/provide-file-router.d.ts +12 -0
- package/types/src/lib/request-context.d.ts +13 -0
- package/types/src/lib/route-builder.d.ts +5 -0
- package/types/src/lib/route-config.d.ts +2 -0
- package/types/src/lib/route-files.d.ts +18 -0
- package/types/src/lib/route-path.d.ts +124 -0
- package/types/src/lib/route-types.d.ts +12 -0
- package/types/src/lib/routes.d.ts +11 -0
- package/types/src/lib/server.component.d.ts +33 -0
- package/types/src/lib/validation-errors.d.ts +7 -0
- package/types/tanstack-query/server/src/index.d.ts +1 -0
- package/types/tanstack-query/src/index.d.ts +2 -0
- package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
- package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
- package/types/tanstack-query/src/server-query.d.ts +16 -0
- package/types/tokens/src/index.d.ts +23 -0
- package/fesm2022/analogjs-router-debug.page-jzggTA45.mjs +0 -91
- package/fesm2022/analogjs-router-debug.page-jzggTA45.mjs.map +0 -1
- package/types/analogjs-router-server-actions.d.ts +0 -17
- package/types/analogjs-router-server.d.ts +0 -29
- package/types/analogjs-router-tokens.d.ts +0 -27
- package/types/analogjs-router.d.ts +0 -269
|
@@ -1,854 +1,999 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { isPlatformServer } from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region packages/router/src/lib/provide-file-router.ts
|
|
62
131
|
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
130
|
-
|
|
147
|
+
function injectLoad(options) {
|
|
148
|
+
return (options?.injector ?? inject(Injector)).get(ActivatedRoute).data.pipe(map((data) => data["load"]));
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
172
|
-
const APP_DIR = 'src/app';
|
|
173
|
-
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region packages/router/src/lib/get-load-resolver.ts
|
|
174
158
|
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
...ANALOG_CONTENT_ROUTE_FILES,
|
|
386
|
-
});
|
|
387
|
-
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region packages/router/src/lib/request-context.ts
|
|
388
197
|
/**
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
//# sourceMappingURL=analogjs-router.mjs.map
|
|
999
|
+
//# sourceMappingURL=analogjs-router.mjs.map
|