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