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