@analogjs/router 3.0.0-alpha.13 → 3.0.0-alpha.15
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/fesm2022/analogjs-router.mjs +430 -5
- package/fesm2022/routes.mjs +93 -12
- package/package.json +8 -4
- package/types/server/actions/src/define-api-route.d.ts +57 -0
- package/types/src/index.d.ts +14 -3
- package/types/src/lib/define-route.d.ts +6 -1
- package/types/src/lib/experimental.d.ts +140 -0
- package/types/src/lib/inject-navigate.d.ts +23 -0
- package/types/src/lib/inject-route-context.d.ts +32 -0
- package/types/src/lib/inject-typed-params.d.ts +63 -0
- package/types/src/lib/json-ld.d.ts +31 -0
- package/types/src/lib/models.d.ts +3 -0
- package/types/src/lib/route-path.d.ts +124 -0
- package/types/src/lib/validation-errors.d.ts +7 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { a as updateMetaTagsOnRouteChange, i as injectRouteEndpointURL, n as createRoutes, r as routes, t as injectDebugRoutes } from "./routes.mjs";
|
|
1
|
+
import { a as updateMetaTagsOnRouteChange, i as injectRouteEndpointURL, n as createRoutes, o as updateJsonLdOnRouteChange, r as routes, t as injectDebugRoutes } from "./routes.mjs";
|
|
2
2
|
import { ActivatedRoute, ROUTES, Router, provideRouter } from "@angular/router";
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
-
import { ChangeDetectionStrategy, Component, Directive, ENVIRONMENT_INITIALIZER, Injector, PLATFORM_ID, TransferState, effect, inject, input, makeEnvironmentProviders, makeStateKey, output, signal } from "@angular/core";
|
|
4
|
+
import { ChangeDetectionStrategy, Component, Directive, ENVIRONMENT_INITIALIZER, InjectionToken, Injector, PLATFORM_ID, TransferState, effect, inject, input, isDevMode, makeEnvironmentProviders, makeStateKey, output, signal } from "@angular/core";
|
|
5
5
|
import { HttpClient, HttpHeaders, HttpRequest, HttpResponse, ɵHTTP_ROOT_INTERCEPTOR_FNS } from "@angular/common/http";
|
|
6
|
-
import { catchError, from, map, of, throwError } from "rxjs";
|
|
6
|
+
import { catchError, from, map, of, take, throwError } from "rxjs";
|
|
7
7
|
import { API_PREFIX, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest } from "@analogjs/router/tokens";
|
|
8
|
-
import { DomSanitizer } from "@angular/platform-browser";
|
|
9
8
|
import { isPlatformServer } from "@angular/common";
|
|
9
|
+
import { DomSanitizer } from "@angular/platform-browser";
|
|
10
|
+
import { toSignal } from "@angular/core/rxjs-interop";
|
|
10
11
|
//#region packages/router/src/lib/define-route.ts
|
|
11
12
|
/**
|
|
12
13
|
* @deprecated Use `RouteMeta` type instead.
|
|
@@ -86,6 +87,11 @@ function provideFileRouter(...features) {
|
|
|
86
87
|
multi: true,
|
|
87
88
|
useValue: () => updateMetaTagsOnRouteChange()
|
|
88
89
|
},
|
|
90
|
+
{
|
|
91
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
92
|
+
multi: true,
|
|
93
|
+
useValue: () => updateJsonLdOnRouteChange()
|
|
94
|
+
},
|
|
89
95
|
{
|
|
90
96
|
provide: ɵHTTP_ROOT_INTERCEPTOR_FNS,
|
|
91
97
|
multi: true,
|
|
@@ -550,6 +556,425 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
550
556
|
}
|
|
551
557
|
});
|
|
552
558
|
//#endregion
|
|
553
|
-
|
|
559
|
+
//#region packages/router/src/lib/validation-errors.ts
|
|
560
|
+
function getPathSegmentKey(segment) {
|
|
561
|
+
return typeof segment === "object" ? segment.key : segment;
|
|
562
|
+
}
|
|
563
|
+
function issuePathToFieldName(path) {
|
|
564
|
+
return path.map((segment) => String(getPathSegmentKey(segment))).join(".");
|
|
565
|
+
}
|
|
566
|
+
function issuesToFieldErrors(issues) {
|
|
567
|
+
return issues.reduce((errors, issue) => {
|
|
568
|
+
if (!issue.path?.length) return errors;
|
|
569
|
+
const fieldName = issuePathToFieldName(issue.path);
|
|
570
|
+
errors[fieldName] ??= [];
|
|
571
|
+
errors[fieldName].push(issue.message);
|
|
572
|
+
return errors;
|
|
573
|
+
}, {});
|
|
574
|
+
}
|
|
575
|
+
function issuesToFormErrors(issues) {
|
|
576
|
+
return issues.filter((issue) => !issue.path?.length).map((issue) => issue.message);
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region packages/router/src/lib/route-path.ts
|
|
580
|
+
/**
|
|
581
|
+
* Typed route path utilities for Analog.
|
|
582
|
+
*
|
|
583
|
+
* This module provides:
|
|
584
|
+
* - The `AnalogRouteTable` base interface (augmented by generated code)
|
|
585
|
+
* - The `AnalogRoutePath` union type
|
|
586
|
+
* - The `routePath()` URL builder function
|
|
587
|
+
*
|
|
588
|
+
* No Angular dependencies — can be used in any context.
|
|
589
|
+
*/
|
|
590
|
+
/**
|
|
591
|
+
* Builds a typed route link object from a route path pattern and options.
|
|
592
|
+
*
|
|
593
|
+
* The returned object separates path, query params, and fragment for
|
|
594
|
+
* direct use with Angular's routerLink directive inputs.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* routePath('/about')
|
|
598
|
+
* // → { path: '/about', queryParams: null, fragment: undefined }
|
|
599
|
+
*
|
|
600
|
+
* routePath('/users/[id]', { params: { id: '42' } })
|
|
601
|
+
* // → { path: '/users/42', queryParams: null, fragment: undefined }
|
|
602
|
+
*
|
|
603
|
+
* routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
|
|
604
|
+
* // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
|
|
605
|
+
*
|
|
606
|
+
* @example Template usage
|
|
607
|
+
* ```html
|
|
608
|
+
* @let link = routePath('/users/[id]', { params: { id: userId } });
|
|
609
|
+
* <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
function routePath(path, ...args) {
|
|
613
|
+
const options = args[0];
|
|
614
|
+
return buildRouteLink(path, options);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Internal: builds a `RouteLinkResult` from path and options.
|
|
618
|
+
* Exported for direct use in tests (avoids generic constraints).
|
|
619
|
+
*/
|
|
620
|
+
function buildRouteLink(path, options) {
|
|
621
|
+
const resolvedPath = buildPath(path, options?.params);
|
|
622
|
+
let queryParams = null;
|
|
623
|
+
if (options?.query) {
|
|
624
|
+
const filtered = {};
|
|
625
|
+
let hasEntries = false;
|
|
626
|
+
for (const [key, value] of Object.entries(options.query)) if (value !== void 0) {
|
|
627
|
+
filtered[key] = value;
|
|
628
|
+
hasEntries = true;
|
|
629
|
+
}
|
|
630
|
+
if (hasEntries) queryParams = filtered;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
path: resolvedPath,
|
|
634
|
+
queryParams,
|
|
635
|
+
fragment: options?.hash
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Resolves param placeholders and normalises slashes.
|
|
640
|
+
* Returns only the path — no query string or hash.
|
|
641
|
+
*/
|
|
642
|
+
function buildPath(path, params) {
|
|
643
|
+
let url = path;
|
|
644
|
+
if (params) {
|
|
645
|
+
url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
|
|
646
|
+
const value = params[name];
|
|
647
|
+
if (value == null) return "";
|
|
648
|
+
if (Array.isArray(value)) return value.map((v) => encodeURIComponent(v)).join("/");
|
|
649
|
+
return encodeURIComponent(String(value));
|
|
650
|
+
});
|
|
651
|
+
url = url.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
|
|
652
|
+
const value = params[name];
|
|
653
|
+
if (value == null) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
|
|
654
|
+
if (Array.isArray(value)) {
|
|
655
|
+
if (value.length === 0) throw new Error(`Missing required catch-all param "${name}" for path "${path}"`);
|
|
656
|
+
return value.map((v) => encodeURIComponent(v)).join("/");
|
|
657
|
+
}
|
|
658
|
+
return encodeURIComponent(String(value));
|
|
659
|
+
});
|
|
660
|
+
url = url.replace(/\[([^\]]+)\]/g, (_, name) => {
|
|
661
|
+
const value = params[name];
|
|
662
|
+
if (value == null) throw new Error(`Missing required param "${name}" for path "${path}"`);
|
|
663
|
+
return encodeURIComponent(String(value));
|
|
664
|
+
});
|
|
665
|
+
} else {
|
|
666
|
+
url = url.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "");
|
|
667
|
+
url = url.replace(/\[\.\.\.([^\]]+)\]/g, "");
|
|
668
|
+
url = url.replace(/\[([^\]]+)\]/g, "");
|
|
669
|
+
}
|
|
670
|
+
url = url.replace(/\/+/g, "/");
|
|
671
|
+
if (url.length > 1 && url.endsWith("/")) url = url.slice(0, -1);
|
|
672
|
+
if (!url.startsWith("/")) url = "/" + url;
|
|
673
|
+
return url;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Internal URL builder. Separated from `routePath` so it can be
|
|
677
|
+
* used without generic constraints (e.g., in `injectNavigate`).
|
|
678
|
+
*/
|
|
679
|
+
function buildUrl(path, options) {
|
|
680
|
+
let url = buildPath(path, options?.params);
|
|
681
|
+
if (options?.query) {
|
|
682
|
+
const parts = [];
|
|
683
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
684
|
+
if (value === void 0) continue;
|
|
685
|
+
if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
|
686
|
+
else parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
687
|
+
}
|
|
688
|
+
if (parts.length > 0) url += "?" + parts.join("&");
|
|
689
|
+
}
|
|
690
|
+
if (options?.hash) url += "#" + options.hash;
|
|
691
|
+
return url;
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region packages/router/src/lib/inject-navigate.ts
|
|
695
|
+
function isRoutePathOptionsBase(value) {
|
|
696
|
+
return !!value && typeof value === "object" && ("params" in value || "query" in value || "hash" in value);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Injects a typed navigate function.
|
|
700
|
+
*
|
|
701
|
+
* @example
|
|
702
|
+
* ```ts
|
|
703
|
+
* const navigate = injectNavigate();
|
|
704
|
+
*
|
|
705
|
+
* navigate('/users/[id]', { params: { id: '42' } }); // ✅
|
|
706
|
+
* navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
|
|
707
|
+
*
|
|
708
|
+
* // With navigation extras
|
|
709
|
+
* navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
function injectNavigate() {
|
|
713
|
+
const router = inject(Router);
|
|
714
|
+
const navigate = ((path, ...args) => {
|
|
715
|
+
let options;
|
|
716
|
+
let extras;
|
|
717
|
+
if (args.length > 1) {
|
|
718
|
+
options = args[0];
|
|
719
|
+
extras = args[1];
|
|
720
|
+
} else if (args.length === 1) if (isRoutePathOptionsBase(args[0])) options = args[0];
|
|
721
|
+
else extras = args[0];
|
|
722
|
+
const url = buildUrl(path, options);
|
|
723
|
+
return router.navigateByUrl(url, extras);
|
|
724
|
+
});
|
|
725
|
+
return navigate;
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region packages/router/src/lib/experimental.ts
|
|
729
|
+
/** @experimental */
|
|
730
|
+
var EXPERIMENTAL_TYPED_ROUTER = new InjectionToken("EXPERIMENTAL_TYPED_ROUTER");
|
|
731
|
+
/** @experimental */
|
|
732
|
+
var EXPERIMENTAL_ROUTE_CONTEXT = new InjectionToken("EXPERIMENTAL_ROUTE_CONTEXT");
|
|
733
|
+
/** @experimental */
|
|
734
|
+
var EXPERIMENTAL_LOADER_CACHE = new InjectionToken("EXPERIMENTAL_LOADER_CACHE");
|
|
735
|
+
/**
|
|
736
|
+
* Enables experimental typed router features.
|
|
737
|
+
*
|
|
738
|
+
* When active, `routePath()`, `injectNavigate()`, `injectParams()`,
|
|
739
|
+
* and `injectQuery()` will enforce route table constraints and
|
|
740
|
+
* optionally log warnings in strict mode.
|
|
741
|
+
*
|
|
742
|
+
* Inspired by TanStack Router's `Register` interface and strict type
|
|
743
|
+
* checking across the entire navigation surface.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* provideFileRouter(
|
|
748
|
+
* withTypedRouter({ strictRouteParams: true }),
|
|
749
|
+
* )
|
|
750
|
+
* ```
|
|
751
|
+
*
|
|
752
|
+
* @experimental
|
|
753
|
+
*/
|
|
754
|
+
function withTypedRouter(options) {
|
|
755
|
+
return {
|
|
756
|
+
ɵkind: 102,
|
|
757
|
+
ɵproviders: [{
|
|
758
|
+
provide: EXPERIMENTAL_TYPED_ROUTER,
|
|
759
|
+
useValue: {
|
|
760
|
+
strictRouteParams: false,
|
|
761
|
+
...options
|
|
762
|
+
}
|
|
763
|
+
}]
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Provides root-level route context available to all route loaders
|
|
768
|
+
* and components via `injectRouteContext()`.
|
|
769
|
+
*
|
|
770
|
+
* Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
|
|
771
|
+
* a typed context object is required at router creation and automatically
|
|
772
|
+
* available in every route's `beforeLoad` and `loader`.
|
|
773
|
+
*
|
|
774
|
+
* In Angular terms, this creates a DI token that server-side load
|
|
775
|
+
* functions and components can inject to access shared services
|
|
776
|
+
* without importing them individually.
|
|
777
|
+
*
|
|
778
|
+
* @example
|
|
779
|
+
* ```ts
|
|
780
|
+
* // app.config.ts
|
|
781
|
+
* provideFileRouter(
|
|
782
|
+
* withRouteContext({
|
|
783
|
+
* auth: inject(AuthService),
|
|
784
|
+
* db: inject(DatabaseService),
|
|
785
|
+
* }),
|
|
786
|
+
* )
|
|
787
|
+
*
|
|
788
|
+
* // In a component
|
|
789
|
+
* const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
|
|
790
|
+
* ```
|
|
791
|
+
*
|
|
792
|
+
* @experimental
|
|
793
|
+
*/
|
|
794
|
+
function withRouteContext(context) {
|
|
795
|
+
return {
|
|
796
|
+
ɵkind: 103,
|
|
797
|
+
ɵproviders: [{
|
|
798
|
+
provide: EXPERIMENTAL_ROUTE_CONTEXT,
|
|
799
|
+
useValue: context
|
|
800
|
+
}]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Configures experimental loader caching behavior for server-loaded
|
|
805
|
+
* route data.
|
|
806
|
+
*
|
|
807
|
+
* Inspired by TanStack Router's built-in cache where `createRouter()`
|
|
808
|
+
* accepts `defaultStaleTime` and `defaultGcTime` to control when
|
|
809
|
+
* loaders re-execute and when cached data is discarded.
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* provideFileRouter(
|
|
814
|
+
* withLoaderCaching({
|
|
815
|
+
* defaultStaleTime: 30_000, // 30s before re-fetch
|
|
816
|
+
* defaultGcTime: 300_000, // 5min cache retention
|
|
817
|
+
* defaultPendingMs: 200, // 200ms loading delay
|
|
818
|
+
* }),
|
|
819
|
+
* )
|
|
820
|
+
* ```
|
|
821
|
+
*
|
|
822
|
+
* @experimental
|
|
823
|
+
*/
|
|
824
|
+
function withLoaderCaching(options) {
|
|
825
|
+
return {
|
|
826
|
+
ɵkind: 104,
|
|
827
|
+
ɵproviders: [{
|
|
828
|
+
provide: EXPERIMENTAL_LOADER_CACHE,
|
|
829
|
+
useValue: {
|
|
830
|
+
defaultStaleTime: 0,
|
|
831
|
+
defaultGcTime: 3e5,
|
|
832
|
+
defaultPendingMs: 0,
|
|
833
|
+
...options
|
|
834
|
+
}
|
|
835
|
+
}]
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region packages/router/src/lib/inject-typed-params.ts
|
|
840
|
+
function extractRouteParams(routePath) {
|
|
841
|
+
const params = [];
|
|
842
|
+
for (const match of routePath.matchAll(/\[\[\.\.\.([^\]]+)\]\]/g)) params.push({
|
|
843
|
+
name: match[1],
|
|
844
|
+
type: "optionalCatchAll"
|
|
845
|
+
});
|
|
846
|
+
for (const match of routePath.matchAll(/(?<!\[)\[\.\.\.([^\]]+)\](?!\])/g)) params.push({
|
|
847
|
+
name: match[1],
|
|
848
|
+
type: "catchAll"
|
|
849
|
+
});
|
|
850
|
+
for (const match of routePath.matchAll(/(?<!\[)\[(?!\.)([^\]]+)\](?!\])/g)) params.push({
|
|
851
|
+
name: match[1],
|
|
852
|
+
type: "dynamic"
|
|
853
|
+
});
|
|
854
|
+
return params;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* When `strictRouteParams` is enabled, warns if expected params from the
|
|
858
|
+
* `_from` pattern are missing from the active `ActivatedRoute`.
|
|
859
|
+
*/
|
|
860
|
+
function assertRouteMatch(from, route, kind) {
|
|
861
|
+
const expectedParams = extractRouteParams(from).filter((param) => param.type === "dynamic" || param.type === "catchAll").map((param) => param.name);
|
|
862
|
+
if (expectedParams.length === 0) return;
|
|
863
|
+
route.params.pipe(take(1)).subscribe((params) => {
|
|
864
|
+
for (const name of expectedParams) if (!(name in params)) {
|
|
865
|
+
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}'.`);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Injects typed route params as a signal, constrained by the route table.
|
|
872
|
+
*
|
|
873
|
+
* Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
|
|
874
|
+
* pattern where the `from` parameter narrows the return type to only
|
|
875
|
+
* the params defined for that route.
|
|
876
|
+
*
|
|
877
|
+
* The `from` parameter is used purely for TypeScript type inference —
|
|
878
|
+
* at runtime, params are read from the current `ActivatedRoute`. This
|
|
879
|
+
* means it works correctly when used inside a component rendered by
|
|
880
|
+
* the specified route.
|
|
881
|
+
*
|
|
882
|
+
* When `withTypedRouter({ strictRouteParams: true })` is configured,
|
|
883
|
+
* a dev-mode assertion checks that the expected params from `from`
|
|
884
|
+
* exist in the active route and warns on mismatch.
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```ts
|
|
888
|
+
* // In a component rendered at /users/[id]
|
|
889
|
+
* const params = injectParams('/users/[id]');
|
|
890
|
+
* // params() → { id: string }
|
|
891
|
+
*
|
|
892
|
+
* // With schema validation output types
|
|
893
|
+
* const params = injectParams('/products/[slug]');
|
|
894
|
+
* // params() → validated output type from routeParamsSchema
|
|
895
|
+
* ```
|
|
896
|
+
*
|
|
897
|
+
* @experimental
|
|
898
|
+
*/
|
|
899
|
+
function injectParams(_from, options) {
|
|
900
|
+
const injector = options?.injector;
|
|
901
|
+
const route = injector ? injector.get(ActivatedRoute) : inject(ActivatedRoute);
|
|
902
|
+
if (isDevMode()) {
|
|
903
|
+
if ((injector ? injector.get(EXPERIMENTAL_TYPED_ROUTER, null) : inject(EXPERIMENTAL_TYPED_ROUTER, { optional: true }))?.strictRouteParams) assertRouteMatch(_from, route, "injectParams");
|
|
904
|
+
}
|
|
905
|
+
return toSignal(route.params.pipe(map((params) => params)), { requireSync: true });
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Injects typed route query params as a signal, constrained by the
|
|
909
|
+
* route table.
|
|
910
|
+
*
|
|
911
|
+
* Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
|
|
912
|
+
* where search params are validated and typed per-route via
|
|
913
|
+
* `validateSearch` schemas.
|
|
914
|
+
*
|
|
915
|
+
* In Analog, the typing comes from `routeQuerySchema` exports that are
|
|
916
|
+
* detected at build time and recorded in the generated route table.
|
|
917
|
+
*
|
|
918
|
+
* The `from` parameter is used purely for TypeScript type inference.
|
|
919
|
+
* When `withTypedRouter({ strictRouteParams: true })` is configured,
|
|
920
|
+
* a dev-mode assertion checks that the expected params from `from`
|
|
921
|
+
* exist in the active route and warns on mismatch.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* // In a component rendered at /issues
|
|
926
|
+
* // (where routeQuerySchema validates { page: number, status: string })
|
|
927
|
+
* const query = injectQuery('/issues');
|
|
928
|
+
* // query() → { page: number; status: string }
|
|
929
|
+
* ```
|
|
930
|
+
*
|
|
931
|
+
* @experimental
|
|
932
|
+
*/
|
|
933
|
+
function injectQuery(_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, "injectQuery");
|
|
938
|
+
}
|
|
939
|
+
return toSignal(route.queryParams.pipe(map((params) => params)), { requireSync: true });
|
|
940
|
+
}
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region packages/router/src/lib/inject-route-context.ts
|
|
943
|
+
/**
|
|
944
|
+
* Injects the root route context provided via `withRouteContext()`.
|
|
945
|
+
*
|
|
946
|
+
* Inspired by TanStack Router's context inheritance where
|
|
947
|
+
* `createRootRouteWithContext<T>()` makes a typed context available
|
|
948
|
+
* to every route's `beforeLoad` and `loader` callbacks.
|
|
949
|
+
*
|
|
950
|
+
* In Angular, this uses DI under the hood — `withRouteContext(ctx)`
|
|
951
|
+
* provides the value, and `injectRouteContext<T>()` retrieves it
|
|
952
|
+
* with the expected type.
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* ```ts
|
|
956
|
+
* // app.config.ts
|
|
957
|
+
* provideFileRouter(
|
|
958
|
+
* withRouteContext({
|
|
959
|
+
* auth: inject(AuthService),
|
|
960
|
+
* analytics: inject(AnalyticsService),
|
|
961
|
+
* }),
|
|
962
|
+
* )
|
|
963
|
+
*
|
|
964
|
+
* // any-page.page.ts
|
|
965
|
+
* const ctx = injectRouteContext<{
|
|
966
|
+
* auth: AuthService;
|
|
967
|
+
* analytics: AnalyticsService;
|
|
968
|
+
* }>();
|
|
969
|
+
* ctx.analytics.trackPageView();
|
|
970
|
+
* ```
|
|
971
|
+
*
|
|
972
|
+
* @experimental
|
|
973
|
+
*/
|
|
974
|
+
function injectRouteContext() {
|
|
975
|
+
return inject(EXPERIMENTAL_ROUTE_CONTEXT);
|
|
976
|
+
}
|
|
977
|
+
//#endregion
|
|
978
|
+
export { EXPERIMENTAL_LOADER_CACHE, EXPERIMENTAL_ROUTE_CONTEXT, EXPERIMENTAL_TYPED_ROUTER, FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectLoadData, injectNavigate, injectParams, injectQuery, injectRouteContext, injectRouteEndpointURL, injectRouter, issuePathToFieldName, issuesToFieldErrors, issuesToFormErrors, provideFileRouter, requestContextInterceptor, routePath, routes, withDebugRoutes, withExtraRoutes, withLoaderCaching, withRouteContext, withTypedRouter };
|
|
554
979
|
|
|
555
980
|
//# sourceMappingURL=analogjs-router.mjs.map
|
package/fesm2022/routes.mjs
CHANGED
|
@@ -3,8 +3,61 @@ import { InjectionToken, inject } from "@angular/core";
|
|
|
3
3
|
import { HttpClient } from "@angular/common/http";
|
|
4
4
|
import { firstValueFrom } from "rxjs";
|
|
5
5
|
import { injectAPIPrefix, injectBaseURL, injectInternalServerFetch } from "@analogjs/router/tokens";
|
|
6
|
-
import {
|
|
6
|
+
import { DOCUMENT } from "@angular/common";
|
|
7
7
|
import { filter } from "rxjs/operators";
|
|
8
|
+
import { Meta } from "@angular/platform-browser";
|
|
9
|
+
//#region packages/router/src/lib/json-ld.ts
|
|
10
|
+
function isJsonLdObject(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function normalizeJsonLd(value) {
|
|
14
|
+
if (Array.isArray(value)) return value.filter(isJsonLdObject);
|
|
15
|
+
return isJsonLdObject(value) ? [value] : [];
|
|
16
|
+
}
|
|
17
|
+
var ROUTE_JSON_LD_KEY = Symbol("@analogjs/router Route JSON-LD Key");
|
|
18
|
+
var JSON_LD_SCRIPT_SELECTOR = "script[data-analog-json-ld]";
|
|
19
|
+
function updateJsonLdOnRouteChange() {
|
|
20
|
+
const router = inject(Router);
|
|
21
|
+
const document = inject(DOCUMENT);
|
|
22
|
+
router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
|
|
23
|
+
applyJsonLdToDocument(document, getJsonLdEntries(router.routerState.snapshot.root));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function serializeJsonLd(entry) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(entry).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getJsonLdEntries(route) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
let currentRoute = route;
|
|
36
|
+
while (currentRoute) {
|
|
37
|
+
entries.push(...normalizeJsonLd(currentRoute.data[ROUTE_JSON_LD_KEY]));
|
|
38
|
+
currentRoute = currentRoute.firstChild;
|
|
39
|
+
}
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
function applyJsonLdToDocument(document, entries) {
|
|
43
|
+
document.querySelectorAll(JSON_LD_SCRIPT_SELECTOR).forEach((element) => {
|
|
44
|
+
element.remove();
|
|
45
|
+
});
|
|
46
|
+
if (entries.length === 0) return;
|
|
47
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
48
|
+
if (!head) return;
|
|
49
|
+
entries.forEach((entry, index) => {
|
|
50
|
+
const serialized = serializeJsonLd(entry);
|
|
51
|
+
if (!serialized) return;
|
|
52
|
+
const script = document.createElement("script");
|
|
53
|
+
script.type = "application/ld+json";
|
|
54
|
+
script.setAttribute("data-analog-json-ld", "true");
|
|
55
|
+
script.setAttribute("data-analog-json-ld-index", String(index));
|
|
56
|
+
script.textContent = serialized;
|
|
57
|
+
head.appendChild(script);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
8
61
|
//#region packages/router/src/lib/meta-tags.ts
|
|
9
62
|
var ROUTE_META_TAGS_KEY = Symbol("@analogjs/router Route Meta Tags Key");
|
|
10
63
|
var CHARSET_KEY = "charset";
|
|
@@ -74,8 +127,8 @@ function injectRouteEndpointURL(route) {
|
|
|
74
127
|
//#endregion
|
|
75
128
|
//#region packages/router/src/lib/route-config.ts
|
|
76
129
|
function toRouteConfig(routeMeta) {
|
|
77
|
-
if (routeMeta && isRedirectRouteMeta(routeMeta)) return routeMeta;
|
|
78
|
-
const { meta, ...routeConfig } = routeMeta ?? {};
|
|
130
|
+
if (routeMeta && isRedirectRouteMeta$1(routeMeta)) return routeMeta;
|
|
131
|
+
const { meta, jsonLd, ...routeConfig } = routeMeta ?? {};
|
|
79
132
|
if (Array.isArray(meta)) routeConfig.data = {
|
|
80
133
|
...routeConfig.data,
|
|
81
134
|
[ROUTE_META_TAGS_KEY]: meta
|
|
@@ -84,6 +137,14 @@ function toRouteConfig(routeMeta) {
|
|
|
84
137
|
...routeConfig.resolve,
|
|
85
138
|
[ROUTE_META_TAGS_KEY]: meta
|
|
86
139
|
};
|
|
140
|
+
if (Array.isArray(jsonLd) || isJsonLdObject(jsonLd)) routeConfig.data = {
|
|
141
|
+
...routeConfig.data,
|
|
142
|
+
[ROUTE_JSON_LD_KEY]: jsonLd
|
|
143
|
+
};
|
|
144
|
+
else if (typeof jsonLd === "function") routeConfig.resolve = {
|
|
145
|
+
...routeConfig.resolve,
|
|
146
|
+
[ROUTE_JSON_LD_KEY]: jsonLd
|
|
147
|
+
};
|
|
87
148
|
routeConfig.runGuardsAndResolvers = routeConfig.runGuardsAndResolvers ?? "paramsOrQueryParamsChange";
|
|
88
149
|
routeConfig.resolve = {
|
|
89
150
|
...routeConfig.resolve,
|
|
@@ -107,7 +168,7 @@ function toRouteConfig(routeMeta) {
|
|
|
107
168
|
};
|
|
108
169
|
return routeConfig;
|
|
109
170
|
}
|
|
110
|
-
function isRedirectRouteMeta(routeMeta) {
|
|
171
|
+
function isRedirectRouteMeta$1(routeMeta) {
|
|
111
172
|
return !!routeMeta.redirectTo;
|
|
112
173
|
}
|
|
113
174
|
//#endregion
|
|
@@ -118,13 +179,14 @@ function toMarkdownModule(markdownFileFactory) {
|
|
|
118
179
|
const createLoader = () => Promise.all([import("@analogjs/content"), markdownFileFactory()]);
|
|
119
180
|
const [{ parseRawContentFile, MarkdownRouteComponent, ContentRenderer }, markdownFile] = await (isNgZoneEnabled ? Zone.root.run(createLoader) : createLoader());
|
|
120
181
|
const { content, attributes } = parseRawContentFile(markdownFile);
|
|
121
|
-
const { title, meta } = attributes;
|
|
182
|
+
const { title, meta, jsonLd } = attributes;
|
|
122
183
|
return {
|
|
123
184
|
default: MarkdownRouteComponent,
|
|
124
185
|
routeMeta: {
|
|
125
186
|
data: { _analogContent: content },
|
|
126
187
|
title,
|
|
127
188
|
meta,
|
|
189
|
+
jsonLd,
|
|
128
190
|
resolve: { renderedAnalogContent: async () => {
|
|
129
191
|
const rendered = await inject(ContentRenderer).render(content);
|
|
130
192
|
return typeof rendered === "string" ? rendered : rendered.content;
|
|
@@ -239,17 +301,24 @@ function toRoutes(rawRoutes, files, debug = false) {
|
|
|
239
301
|
const route = module ? {
|
|
240
302
|
path: rawRoute.segment,
|
|
241
303
|
loadChildren: () => module().then((m) => {
|
|
242
|
-
|
|
304
|
+
const routeConfig = toRouteConfig(mergeRouteJsonLdIntoRouteMeta(m.routeMeta, m.routeJsonLd));
|
|
305
|
+
const hasRedirect = "redirectTo" in routeConfig;
|
|
306
|
+
return [{ ...hasRedirect ? {
|
|
307
|
+
path: "",
|
|
308
|
+
...routeConfig
|
|
309
|
+
} : {
|
|
243
310
|
path: "",
|
|
244
311
|
component: m.default,
|
|
245
|
-
...
|
|
312
|
+
...routeConfig,
|
|
246
313
|
children,
|
|
247
314
|
[ANALOG_META_KEY]: analogMeta
|
|
248
|
-
}, ...optCatchAllParam ? [{
|
|
315
|
+
} }, ...optCatchAllParam ? [{
|
|
249
316
|
matcher: createOptionalCatchAllMatcher(optCatchAllParam),
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
317
|
+
...hasRedirect ? routeConfig : {
|
|
318
|
+
component: m.default,
|
|
319
|
+
...routeConfig,
|
|
320
|
+
[ANALOG_META_KEY]: analogMeta
|
|
321
|
+
}
|
|
253
322
|
}] : []];
|
|
254
323
|
})
|
|
255
324
|
} : {
|
|
@@ -264,6 +333,18 @@ function toRoutes(rawRoutes, files, debug = false) {
|
|
|
264
333
|
}
|
|
265
334
|
return routes;
|
|
266
335
|
}
|
|
336
|
+
function mergeRouteJsonLdIntoRouteMeta(routeMeta, routeJsonLd) {
|
|
337
|
+
if (!routeJsonLd) return routeMeta;
|
|
338
|
+
if (!routeMeta) return { jsonLd: routeJsonLd };
|
|
339
|
+
if (isRedirectRouteMeta(routeMeta) || routeMeta.jsonLd) return routeMeta;
|
|
340
|
+
return {
|
|
341
|
+
...routeMeta,
|
|
342
|
+
jsonLd: routeJsonLd
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function isRedirectRouteMeta(routeMeta) {
|
|
346
|
+
return "redirectTo" in routeMeta && !!routeMeta.redirectTo;
|
|
347
|
+
}
|
|
267
348
|
function sortRawRoutes(rawRoutes) {
|
|
268
349
|
rawRoutes.sort((a, b) => {
|
|
269
350
|
let segmentA = deprioritizeSegment(a.segment);
|
|
@@ -296,6 +377,6 @@ function injectDebugRoutes() {
|
|
|
296
377
|
return inject(DEBUG_ROUTES);
|
|
297
378
|
}
|
|
298
379
|
//#endregion
|
|
299
|
-
export { updateMetaTagsOnRouteChange as a, injectRouteEndpointURL as i, createRoutes as n, routes as r, injectDebugRoutes as t };
|
|
380
|
+
export { updateMetaTagsOnRouteChange as a, injectRouteEndpointURL as i, createRoutes as n, updateJsonLdOnRouteChange as o, routes as r, injectDebugRoutes as t };
|
|
300
381
|
|
|
301
382
|
//# sourceMappingURL=routes.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analogjs/router",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.15",
|
|
4
4
|
"description": "Filesystem-based routing for Angular",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Brandon Roberts <robertsbt@gmail.com>",
|
|
@@ -54,11 +54,12 @@
|
|
|
54
54
|
"url": "https://github.com/sponsors/brandonroberts"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@analogjs/content": "^3.0.0-alpha.
|
|
57
|
+
"@analogjs/content": "^3.0.0-alpha.15",
|
|
58
58
|
"@angular/core": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
59
59
|
"@angular/platform-server": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
60
60
|
"@angular/router": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
61
|
-
"@tanstack/angular-query-experimental": ">=5.95.0"
|
|
61
|
+
"@tanstack/angular-query-experimental": ">=5.95.0",
|
|
62
|
+
"schema-dts": "^2.0.0"
|
|
62
63
|
},
|
|
63
64
|
"peerDependenciesMeta": {
|
|
64
65
|
"@angular/platform-server": {
|
|
@@ -66,6 +67,9 @@
|
|
|
66
67
|
},
|
|
67
68
|
"@tanstack/angular-query-experimental": {
|
|
68
69
|
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"schema-dts": {
|
|
72
|
+
"optional": true
|
|
69
73
|
}
|
|
70
74
|
},
|
|
71
75
|
"dependencies": {
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
"tslib": "^2.0.0"
|
|
74
78
|
},
|
|
75
79
|
"devDependencies": {
|
|
76
|
-
"@analogjs/vite-plugin-angular": "^3.0.0-alpha.
|
|
80
|
+
"@analogjs/vite-plugin-angular": "^3.0.0-alpha.15"
|
|
77
81
|
},
|
|
78
82
|
"ng-update": {
|
|
79
83
|
"packageGroup": [
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { H3Event } from 'nitro/h3';
|
|
3
|
+
export type DefineApiRouteResult = Response | unknown;
|
|
4
|
+
type OptionalSchema = StandardSchemaV1 | undefined;
|
|
5
|
+
type InferSchema<TSchema extends OptionalSchema, TFallback = unknown> = TSchema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TSchema> : TFallback;
|
|
6
|
+
type ResolveDataSchema<TInput extends OptionalSchema, TQuery extends OptionalSchema, TBody extends OptionalSchema> = TInput extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TInput> : TQuery extends StandardSchemaV1 ? TBody extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TQuery> | StandardSchemaV1.InferOutput<TBody> : StandardSchemaV1.InferOutput<TQuery> : TBody extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TBody> : unknown;
|
|
7
|
+
export interface DefineApiRouteContext<TInput extends StandardSchemaV1 | undefined = undefined, TQuery extends StandardSchemaV1 | undefined = undefined, TBody extends StandardSchemaV1 | undefined = undefined, TParams extends StandardSchemaV1 | undefined = undefined> {
|
|
8
|
+
data: ResolveDataSchema<TInput, TQuery, TBody>;
|
|
9
|
+
query: InferSchema<TQuery, undefined>;
|
|
10
|
+
body: InferSchema<TBody, undefined>;
|
|
11
|
+
params: InferSchema<TParams, H3Event['context']['params']>;
|
|
12
|
+
event: H3Event;
|
|
13
|
+
}
|
|
14
|
+
export interface DefineApiRouteOptions<TInput extends StandardSchemaV1 | undefined = undefined, TOutput extends StandardSchemaV1 | undefined = undefined, TQuery extends StandardSchemaV1 | undefined = undefined, TBody extends StandardSchemaV1 | undefined = undefined, TParams extends StandardSchemaV1 | undefined = undefined, TResult extends DefineApiRouteResult = DefineApiRouteResult> {
|
|
15
|
+
input?: TInput;
|
|
16
|
+
query?: TQuery;
|
|
17
|
+
body?: TBody;
|
|
18
|
+
params?: TParams;
|
|
19
|
+
output?: TOutput;
|
|
20
|
+
handler: (context: DefineApiRouteContext<TInput, TQuery, TBody, TParams>) => Promise<TResult> | TResult;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an h3-compatible event handler with Standard Schema validation.
|
|
24
|
+
*
|
|
25
|
+
* - `input` schema validates the request body (POST/PUT/PATCH) or query
|
|
26
|
+
* params (GET). Returns 422 with `StandardSchemaV1.Issue[]` on failure.
|
|
27
|
+
* - `output` schema validates the response in development only (stripped
|
|
28
|
+
* in production for zero overhead). Logs a warning on mismatch.
|
|
29
|
+
* - Plain return values are serialized with `json(...)`; raw `Response`
|
|
30
|
+
* objects are returned unchanged.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { defineApiRoute } from '@analogjs/router/server/actions';
|
|
35
|
+
* import * as v from 'valibot';
|
|
36
|
+
*
|
|
37
|
+
* const Input = v.object({
|
|
38
|
+
* name: v.pipe(v.string(), v.minLength(1)),
|
|
39
|
+
* email: v.pipe(v.string(), v.email()),
|
|
40
|
+
* });
|
|
41
|
+
* const Output = v.object({
|
|
42
|
+
* id: v.string(),
|
|
43
|
+
* name: v.string(),
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* export default defineApiRoute({
|
|
47
|
+
* input: Input,
|
|
48
|
+
* output: Output,
|
|
49
|
+
* handler: async ({ data }) => {
|
|
50
|
+
* const user = await db.users.create(data);
|
|
51
|
+
* return user;
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function defineApiRoute<TInput extends StandardSchemaV1 | undefined = undefined, TOutput extends StandardSchemaV1 | undefined = undefined, TQuery extends StandardSchemaV1 | undefined = undefined, TBody extends StandardSchemaV1 | undefined = undefined, TParams extends StandardSchemaV1 | undefined = undefined, TResult extends DefineApiRouteResult = DefineApiRouteResult>(options: DefineApiRouteOptions<TInput, TOutput, TQuery, TBody, TParams, TResult>): (event: H3Event) => Promise<Response>;
|
|
57
|
+
export {};
|
package/types/src/index.d.ts
CHANGED
|
@@ -2,15 +2,26 @@ export type { RouteExport } from './lib/models';
|
|
|
2
2
|
export type { Files } from './lib/routes';
|
|
3
3
|
export { routes, createRoutes } from './lib/routes';
|
|
4
4
|
export { defineRouteMeta, injectActivatedRoute, injectRouter, } from './lib/define-route';
|
|
5
|
-
export { RouteMeta } from './lib/models';
|
|
5
|
+
export type { RouteMeta } from './lib/models';
|
|
6
6
|
export { provideFileRouter, withExtraRoutes } from './lib/provide-file-router';
|
|
7
|
-
export { MetaTag } from './lib/meta-tags';
|
|
8
|
-
export { PageServerLoad, LoadResult, LoadDataResult } from './lib/route-types';
|
|
7
|
+
export type { MetaTag } from './lib/meta-tags';
|
|
8
|
+
export type { PageServerLoad, LoadResult, LoadDataResult, } from './lib/route-types';
|
|
9
9
|
export { injectLoad, injectLoadData } from './lib/inject-load';
|
|
10
10
|
export { getLoadResolver } from './lib/get-load-resolver';
|
|
11
11
|
export { requestContextInterceptor } from './lib/request-context';
|
|
12
12
|
export { injectRouteEndpointURL } from './lib/inject-route-endpoint-url';
|
|
13
13
|
export { FormAction } from './lib/form-action.directive';
|
|
14
|
+
export type { FormActionState } from './lib/form-action.directive';
|
|
14
15
|
export { injectDebugRoutes } from './lib/debug/routes';
|
|
15
16
|
export { withDebugRoutes } from './lib/debug';
|
|
16
17
|
export { ServerOnly } from './lib/server.component';
|
|
18
|
+
export type { AnalogJsonLdDocument } from './lib/json-ld';
|
|
19
|
+
export { issuesToFieldErrors, issuesToFormErrors, issuePathToFieldName, } from './lib/validation-errors';
|
|
20
|
+
export type { ValidationFieldErrors } from './lib/validation-errors';
|
|
21
|
+
export type { AnalogRouteTable, AnalogRoutePath, RoutePathOptions, RoutePathArgs, RoutePathOptionsBase, RouteParamsOutput, RouteQueryOutput, RouteLinkResult, } from './lib/route-path';
|
|
22
|
+
export { routePath } from './lib/route-path';
|
|
23
|
+
export { injectNavigate } from './lib/inject-navigate';
|
|
24
|
+
export { withTypedRouter, withRouteContext, withLoaderCaching, EXPERIMENTAL_TYPED_ROUTER, EXPERIMENTAL_ROUTE_CONTEXT, EXPERIMENTAL_LOADER_CACHE, } from './lib/experimental';
|
|
25
|
+
export type { TypedRouterOptions, LoaderCacheOptions, } from './lib/experimental';
|
|
26
|
+
export { injectParams, injectQuery } from './lib/inject-typed-params';
|
|
27
|
+
export { injectRouteContext } from './lib/inject-route-context';
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Route as NgRoute, Router } from '@angular/router';
|
|
2
2
|
import { ActivatedRoute } from '@angular/router';
|
|
3
|
+
import { AnalogJsonLdDocument } from './json-ld';
|
|
4
|
+
import { MetaTag } from './meta-tags';
|
|
3
5
|
type RouteOmitted = 'component' | 'loadComponent' | 'loadChildren' | 'path' | 'pathMatch';
|
|
4
|
-
type RestrictedRoute = Omit<NgRoute, RouteOmitted
|
|
6
|
+
type RestrictedRoute = Omit<NgRoute, RouteOmitted> & {
|
|
7
|
+
meta?: MetaTag[];
|
|
8
|
+
jsonLd?: AnalogJsonLdDocument;
|
|
9
|
+
};
|
|
5
10
|
/**
|
|
6
11
|
* @deprecated Use `RouteMeta` type instead.
|
|
7
12
|
* For more info see: https://github.com/analogjs/analog/issues/223
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import type { RouterFeatures } from '@angular/router';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for experimental typed router features.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by TanStack Router's type-safe navigation system where
|
|
7
|
+
* routes are registered globally and all navigation/hooks are typed
|
|
8
|
+
* against the route tree.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export interface TypedRouterOptions {
|
|
13
|
+
/**
|
|
14
|
+
* When true, logs warnings in development when navigating to
|
|
15
|
+
* routes with params that don't match the generated route table.
|
|
16
|
+
*
|
|
17
|
+
* Similar to TanStack Router's strict mode where `useParams()`
|
|
18
|
+
* without a `from` constraint returns a union of all possible params.
|
|
19
|
+
*
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
strictRouteParams?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for experimental loader caching.
|
|
26
|
+
*
|
|
27
|
+
* Inspired by TanStack Router's built-in data caching where route
|
|
28
|
+
* loaders automatically cache results and support stale-while-revalidate.
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export interface LoaderCacheOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Time in milliseconds before loader data is considered stale.
|
|
35
|
+
* While data is fresh, navigating back to the route uses cached
|
|
36
|
+
* data without re-invoking the server load function.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors TanStack Router's `defaultStaleTime` option on `createRouter()`.
|
|
39
|
+
*
|
|
40
|
+
* @default 0 (always re-fetch)
|
|
41
|
+
*/
|
|
42
|
+
defaultStaleTime?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Time in milliseconds to retain unused loader data in cache
|
|
45
|
+
* after leaving a route. After this period the cached entry is
|
|
46
|
+
* garbage-collected.
|
|
47
|
+
*
|
|
48
|
+
* Mirrors TanStack Router's `defaultGcTime` (default 30 min).
|
|
49
|
+
*
|
|
50
|
+
* @default 300_000 (5 minutes)
|
|
51
|
+
*/
|
|
52
|
+
defaultGcTime?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Delay in milliseconds before showing a pending/loading indicator
|
|
55
|
+
* during route transitions. Prevents flash-of-loading-state for
|
|
56
|
+
* fast navigations.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors TanStack Router's `defaultPendingMs`.
|
|
59
|
+
*
|
|
60
|
+
* @default 0 (show immediately)
|
|
61
|
+
*/
|
|
62
|
+
defaultPendingMs?: number;
|
|
63
|
+
}
|
|
64
|
+
/** @experimental */
|
|
65
|
+
export declare const EXPERIMENTAL_TYPED_ROUTER: InjectionToken<TypedRouterOptions>;
|
|
66
|
+
/** @experimental */
|
|
67
|
+
export declare const EXPERIMENTAL_ROUTE_CONTEXT: InjectionToken<Record<string, unknown>>;
|
|
68
|
+
/** @experimental */
|
|
69
|
+
export declare const EXPERIMENTAL_LOADER_CACHE: InjectionToken<LoaderCacheOptions>;
|
|
70
|
+
/**
|
|
71
|
+
* Enables experimental typed router features.
|
|
72
|
+
*
|
|
73
|
+
* When active, `routePath()`, `injectNavigate()`, `injectParams()`,
|
|
74
|
+
* and `injectQuery()` will enforce route table constraints and
|
|
75
|
+
* optionally log warnings in strict mode.
|
|
76
|
+
*
|
|
77
|
+
* Inspired by TanStack Router's `Register` interface and strict type
|
|
78
|
+
* checking across the entire navigation surface.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* provideFileRouter(
|
|
83
|
+
* withTypedRouter({ strictRouteParams: true }),
|
|
84
|
+
* )
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @experimental
|
|
88
|
+
*/
|
|
89
|
+
export declare function withTypedRouter(options?: TypedRouterOptions): RouterFeatures;
|
|
90
|
+
/**
|
|
91
|
+
* Provides root-level route context available to all route loaders
|
|
92
|
+
* and components via `injectRouteContext()`.
|
|
93
|
+
*
|
|
94
|
+
* Inspired by TanStack Router's `createRootRouteWithContext<T>()` where
|
|
95
|
+
* a typed context object is required at router creation and automatically
|
|
96
|
+
* available in every route's `beforeLoad` and `loader`.
|
|
97
|
+
*
|
|
98
|
+
* In Angular terms, this creates a DI token that server-side load
|
|
99
|
+
* functions and components can inject to access shared services
|
|
100
|
+
* without importing them individually.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* // app.config.ts
|
|
105
|
+
* provideFileRouter(
|
|
106
|
+
* withRouteContext({
|
|
107
|
+
* auth: inject(AuthService),
|
|
108
|
+
* db: inject(DatabaseService),
|
|
109
|
+
* }),
|
|
110
|
+
* )
|
|
111
|
+
*
|
|
112
|
+
* // In a component
|
|
113
|
+
* const ctx = injectRouteContext<{ auth: AuthService; db: DatabaseService }>();
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @experimental
|
|
117
|
+
*/
|
|
118
|
+
export declare function withRouteContext<T extends Record<string, unknown>>(context: T): RouterFeatures;
|
|
119
|
+
/**
|
|
120
|
+
* Configures experimental loader caching behavior for server-loaded
|
|
121
|
+
* route data.
|
|
122
|
+
*
|
|
123
|
+
* Inspired by TanStack Router's built-in cache where `createRouter()`
|
|
124
|
+
* accepts `defaultStaleTime` and `defaultGcTime` to control when
|
|
125
|
+
* loaders re-execute and when cached data is discarded.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* provideFileRouter(
|
|
130
|
+
* withLoaderCaching({
|
|
131
|
+
* defaultStaleTime: 30_000, // 30s before re-fetch
|
|
132
|
+
* defaultGcTime: 300_000, // 5min cache retention
|
|
133
|
+
* defaultPendingMs: 200, // 200ms loading delay
|
|
134
|
+
* }),
|
|
135
|
+
* )
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @experimental
|
|
139
|
+
*/
|
|
140
|
+
export declare function withLoaderCaching(options?: LoaderCacheOptions): RouterFeatures;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type NavigationBehaviorOptions } from '@angular/router';
|
|
2
|
+
import type { AnalogRoutePath, RoutePathArgs } from './route-path';
|
|
3
|
+
type NavigateWithExtrasArgs<P extends AnalogRoutePath> = RoutePathArgs<P> extends [options?: infer Options] ? [extras: NavigationBehaviorOptions] | [options: Options | undefined, extras: NavigationBehaviorOptions] : [options: RoutePathArgs<P>[0], extras: NavigationBehaviorOptions];
|
|
4
|
+
type TypedNavigate = {
|
|
5
|
+
<P extends AnalogRoutePath>(path: P, ...args: RoutePathArgs<P>): Promise<boolean>;
|
|
6
|
+
<P extends AnalogRoutePath>(path: P, ...args: NavigateWithExtrasArgs<P>): Promise<boolean>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Injects a typed navigate function.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const navigate = injectNavigate();
|
|
14
|
+
*
|
|
15
|
+
* navigate('/users/[id]', { params: { id: '42' } }); // ✅
|
|
16
|
+
* navigate('/users/[id]', { params: { id: 42 } }); // ❌ type error
|
|
17
|
+
*
|
|
18
|
+
* // With navigation extras
|
|
19
|
+
* navigate('/users/[id]', { params: { id: '42' } }, { replaceUrl: true });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function injectNavigate(): TypedNavigate;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injects the root route context provided via `withRouteContext()`.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by TanStack Router's context inheritance where
|
|
5
|
+
* `createRootRouteWithContext<T>()` makes a typed context available
|
|
6
|
+
* to every route's `beforeLoad` and `loader` callbacks.
|
|
7
|
+
*
|
|
8
|
+
* In Angular, this uses DI under the hood — `withRouteContext(ctx)`
|
|
9
|
+
* provides the value, and `injectRouteContext<T>()` retrieves it
|
|
10
|
+
* with the expected type.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // app.config.ts
|
|
15
|
+
* provideFileRouter(
|
|
16
|
+
* withRouteContext({
|
|
17
|
+
* auth: inject(AuthService),
|
|
18
|
+
* analytics: inject(AnalyticsService),
|
|
19
|
+
* }),
|
|
20
|
+
* )
|
|
21
|
+
*
|
|
22
|
+
* // any-page.page.ts
|
|
23
|
+
* const ctx = injectRouteContext<{
|
|
24
|
+
* auth: AuthService;
|
|
25
|
+
* analytics: AnalyticsService;
|
|
26
|
+
* }>();
|
|
27
|
+
* ctx.analytics.trackPageView();
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export declare function injectRouteContext<T extends Record<string, unknown> = Record<string, unknown>>(): T;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Injector, Signal } from '@angular/core';
|
|
2
|
+
import type { AnalogRoutePath, RouteParamsOutput, RouteQueryOutput } from './route-path';
|
|
3
|
+
/**
|
|
4
|
+
* Injects typed route params as a signal, constrained by the route table.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by TanStack Router's `useParams({ from: '/users/$userId' })`
|
|
7
|
+
* pattern where the `from` parameter narrows the return type to only
|
|
8
|
+
* the params defined for that route.
|
|
9
|
+
*
|
|
10
|
+
* The `from` parameter is used purely for TypeScript type inference —
|
|
11
|
+
* at runtime, params are read from the current `ActivatedRoute`. This
|
|
12
|
+
* means it works correctly when used inside a component rendered by
|
|
13
|
+
* the specified route.
|
|
14
|
+
*
|
|
15
|
+
* When `withTypedRouter({ strictRouteParams: true })` is configured,
|
|
16
|
+
* a dev-mode assertion checks that the expected params from `from`
|
|
17
|
+
* exist in the active route and warns on mismatch.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // In a component rendered at /users/[id]
|
|
22
|
+
* const params = injectParams('/users/[id]');
|
|
23
|
+
* // params() → { id: string }
|
|
24
|
+
*
|
|
25
|
+
* // With schema validation output types
|
|
26
|
+
* const params = injectParams('/products/[slug]');
|
|
27
|
+
* // params() → validated output type from routeParamsSchema
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export declare function injectParams<P extends AnalogRoutePath>(_from: P, options?: {
|
|
33
|
+
injector?: Injector;
|
|
34
|
+
}): Signal<RouteParamsOutput<P>>;
|
|
35
|
+
/**
|
|
36
|
+
* Injects typed route query params as a signal, constrained by the
|
|
37
|
+
* route table.
|
|
38
|
+
*
|
|
39
|
+
* Inspired by TanStack Router's `useSearch({ from: '/issues' })` pattern
|
|
40
|
+
* where search params are validated and typed per-route via
|
|
41
|
+
* `validateSearch` schemas.
|
|
42
|
+
*
|
|
43
|
+
* In Analog, the typing comes from `routeQuerySchema` exports that are
|
|
44
|
+
* detected at build time and recorded in the generated route table.
|
|
45
|
+
*
|
|
46
|
+
* The `from` parameter is used purely for TypeScript type inference.
|
|
47
|
+
* When `withTypedRouter({ strictRouteParams: true })` is configured,
|
|
48
|
+
* a dev-mode assertion checks that the expected params from `from`
|
|
49
|
+
* exist in the active route and warns on mismatch.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* // In a component rendered at /issues
|
|
54
|
+
* // (where routeQuerySchema validates { page: number, status: string })
|
|
55
|
+
* const query = injectQuery('/issues');
|
|
56
|
+
* // query() → { page: number; status: string }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @experimental
|
|
60
|
+
*/
|
|
61
|
+
export declare function injectQuery<P extends AnalogRoutePath>(_from: P, options?: {
|
|
62
|
+
injector?: Injector;
|
|
63
|
+
}): Signal<RouteQueryOutput<P>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Graph, Thing, WithContext } from 'schema-dts';
|
|
2
|
+
export type JsonLdObject = Record<string, unknown>;
|
|
3
|
+
export declare function isJsonLdObject(value: unknown): value is JsonLdObject;
|
|
4
|
+
export declare function normalizeJsonLd(value: unknown): JsonLdObject[];
|
|
5
|
+
export type JsonLd = JsonLdObject | JsonLdObject[];
|
|
6
|
+
/**
|
|
7
|
+
* Typed JSON-LD document based on `schema-dts`.
|
|
8
|
+
*
|
|
9
|
+
* Accepts single Schema.org nodes (`WithContext<Thing>`),
|
|
10
|
+
* `@graph`-based documents (`Graph`), or arrays of nodes.
|
|
11
|
+
*
|
|
12
|
+
* This is the canonical JSON-LD type for route authoring surfaces
|
|
13
|
+
* (`routeMeta.jsonLd`, `routeJsonLd`, generated manifest).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import type { WebPage, WithContext } from 'schema-dts';
|
|
18
|
+
*
|
|
19
|
+
* export const routeMeta = {
|
|
20
|
+
* jsonLd: {
|
|
21
|
+
* '@context': 'https://schema.org',
|
|
22
|
+
* '@type': 'WebPage',
|
|
23
|
+
* name: 'Products',
|
|
24
|
+
* } satisfies WithContext<WebPage>,
|
|
25
|
+
* };
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export type AnalogJsonLdDocument = WithContext<Thing> | Graph | Array<WithContext<Thing>>;
|
|
29
|
+
export declare const ROUTE_JSON_LD_KEY: unique symbol;
|
|
30
|
+
export declare function updateJsonLdOnRouteChange(): void;
|
|
31
|
+
export declare function serializeJsonLd(entry: JsonLdObject): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from '@angular/core';
|
|
2
2
|
import { CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanMatchFn, DeprecatedGuard, ResolveFn, Route } from '@angular/router';
|
|
3
3
|
import { defineRouteMeta } from './define-route';
|
|
4
|
+
import { AnalogJsonLdDocument } from './json-ld';
|
|
4
5
|
import { MetaTag } from './meta-tags';
|
|
5
6
|
type OmittedRouteProps = 'path' | 'matcher' | 'component' | 'loadComponent' | 'children' | 'loadChildren' | 'canLoad' | 'outlet';
|
|
6
7
|
export type RouteConfig = Omit<Route, OmittedRouteProps>;
|
|
@@ -14,6 +15,7 @@ export interface DefaultRouteMeta extends Omit<Route, OmittedRouteProps | keyof
|
|
|
14
15
|
};
|
|
15
16
|
title?: string | ResolveFn<string>;
|
|
16
17
|
meta?: MetaTag[] | ResolveFn<MetaTag[]>;
|
|
18
|
+
jsonLd?: AnalogJsonLdDocument | ResolveFn<AnalogJsonLdDocument | undefined>;
|
|
17
19
|
}
|
|
18
20
|
export interface RedirectRouteMeta {
|
|
19
21
|
redirectTo: string;
|
|
@@ -25,5 +27,6 @@ export type RouteMeta = (DefaultRouteMeta & {
|
|
|
25
27
|
export type RouteExport = {
|
|
26
28
|
default: Type<unknown>;
|
|
27
29
|
routeMeta?: RouteMeta | ReturnType<typeof defineRouteMeta>;
|
|
30
|
+
routeJsonLd?: AnalogJsonLdDocument | ResolveFn<AnalogJsonLdDocument | undefined>;
|
|
28
31
|
};
|
|
29
32
|
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed route path utilities for Analog.
|
|
3
|
+
*
|
|
4
|
+
* This module provides:
|
|
5
|
+
* - The `AnalogRouteTable` base interface (augmented by generated code)
|
|
6
|
+
* - The `AnalogRoutePath` union type
|
|
7
|
+
* - The `routePath()` URL builder function
|
|
8
|
+
*
|
|
9
|
+
* No Angular dependencies — can be used in any context.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Base interface for the typed route table.
|
|
13
|
+
*
|
|
14
|
+
* This interface is augmented by generated code in `src/routeTree.gen.ts`.
|
|
15
|
+
* When no routes are generated, it is empty and `AnalogRoutePath` falls
|
|
16
|
+
* back to `string`.
|
|
17
|
+
*/
|
|
18
|
+
export interface AnalogRouteTable {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Union of all valid route paths.
|
|
22
|
+
*
|
|
23
|
+
* When routes are generated, this is a string literal union.
|
|
24
|
+
* When no routes are generated, this falls back to `string`.
|
|
25
|
+
*/
|
|
26
|
+
export type AnalogRoutePath = keyof AnalogRouteTable extends never ? string : Extract<keyof AnalogRouteTable, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Options for building a route URL.
|
|
29
|
+
*/
|
|
30
|
+
export interface RoutePathOptionsBase {
|
|
31
|
+
params?: Record<string, string | string[] | undefined>;
|
|
32
|
+
query?: Record<string, string | string[] | undefined>;
|
|
33
|
+
hash?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extracts the validated output type for route params.
|
|
37
|
+
*
|
|
38
|
+
* When a route exports `routeParamsSchema`, this resolves to the schema's
|
|
39
|
+
* output type (e.g., `{ id: number }` after coercion).
|
|
40
|
+
* When no schema exists, this is the same as the navigation param type.
|
|
41
|
+
*/
|
|
42
|
+
export type RouteParamsOutput<P extends string> = P extends keyof AnalogRouteTable ? AnalogRouteTable[P] extends {
|
|
43
|
+
paramsOutput: infer O;
|
|
44
|
+
} ? O : AnalogRouteTable[P] extends {
|
|
45
|
+
params: infer Params;
|
|
46
|
+
} ? Params : Record<string, unknown> : Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the validated output type for route query params.
|
|
49
|
+
*/
|
|
50
|
+
export type RouteQueryOutput<P extends string> = P extends keyof AnalogRouteTable ? AnalogRouteTable[P] extends {
|
|
51
|
+
queryOutput: infer O;
|
|
52
|
+
} ? O : Record<string, string | string[] | undefined> : Record<string, string | string[] | undefined>;
|
|
53
|
+
type RequiredRouteParamKeys<Params> = Params extends Record<string, never> ? never : {
|
|
54
|
+
[K in keyof Params]-?: Record<string, never> extends Pick<Params, K> ? never : K;
|
|
55
|
+
}[keyof Params];
|
|
56
|
+
type HasRequiredRouteParams<Params> = [RequiredRouteParamKeys<Params>] extends [
|
|
57
|
+
never
|
|
58
|
+
] ? false : true;
|
|
59
|
+
/**
|
|
60
|
+
* Typed options that infer params from the route table when available.
|
|
61
|
+
*/
|
|
62
|
+
export type RoutePathOptions<P extends string = string> = P extends keyof AnalogRouteTable ? AnalogRouteTable[P] extends {
|
|
63
|
+
params: infer Params;
|
|
64
|
+
} ? Params extends Record<string, never> ? {
|
|
65
|
+
query?: RouteQueryOutput<P>;
|
|
66
|
+
hash?: string;
|
|
67
|
+
} : HasRequiredRouteParams<Params> extends true ? {
|
|
68
|
+
params: Params;
|
|
69
|
+
query?: RouteQueryOutput<P>;
|
|
70
|
+
hash?: string;
|
|
71
|
+
} : {
|
|
72
|
+
params?: Params;
|
|
73
|
+
query?: RouteQueryOutput<P>;
|
|
74
|
+
hash?: string;
|
|
75
|
+
} : RoutePathOptionsBase : RoutePathOptionsBase;
|
|
76
|
+
/**
|
|
77
|
+
* Conditional args: require options when the route has params.
|
|
78
|
+
*/
|
|
79
|
+
export type RoutePathArgs<P extends string = string> = P extends keyof AnalogRouteTable ? AnalogRouteTable[P] extends {
|
|
80
|
+
params: infer Params;
|
|
81
|
+
} ? Params extends Record<string, never> ? [options?: RoutePathOptions<P>] : HasRequiredRouteParams<Params> extends true ? [options: RoutePathOptions<P>] : [options?: RoutePathOptions<P>] : [options?: RoutePathOptionsBase] : [options?: RoutePathOptionsBase];
|
|
82
|
+
/**
|
|
83
|
+
* Result of `routePath()` — contains properties that map directly
|
|
84
|
+
* to Angular's `[routerLink]`, `[queryParams]`, and `[fragment]` inputs.
|
|
85
|
+
*/
|
|
86
|
+
export interface RouteLinkResult {
|
|
87
|
+
path: string;
|
|
88
|
+
queryParams: Record<string, string | string[]> | null;
|
|
89
|
+
fragment: string | undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Builds a typed route link object from a route path pattern and options.
|
|
93
|
+
*
|
|
94
|
+
* The returned object separates path, query params, and fragment for
|
|
95
|
+
* direct use with Angular's routerLink directive inputs.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* routePath('/about')
|
|
99
|
+
* // → { path: '/about', queryParams: null, fragment: undefined }
|
|
100
|
+
*
|
|
101
|
+
* routePath('/users/[id]', { params: { id: '42' } })
|
|
102
|
+
* // → { path: '/users/42', queryParams: null, fragment: undefined }
|
|
103
|
+
*
|
|
104
|
+
* routePath('/users/[id]', { params: { id: '42' }, query: { tab: 'settings' }, hash: 'bio' })
|
|
105
|
+
* // → { path: '/users/42', queryParams: { tab: 'settings' }, fragment: 'bio' }
|
|
106
|
+
*
|
|
107
|
+
* @example Template usage
|
|
108
|
+
* ```html
|
|
109
|
+
* @let link = routePath('/users/[id]', { params: { id: userId } });
|
|
110
|
+
* <a [routerLink]="link.path" [queryParams]="link.queryParams" [fragment]="link.fragment">
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function routePath<P extends AnalogRoutePath>(path: P, ...args: RoutePathArgs<P>): RouteLinkResult;
|
|
114
|
+
/**
|
|
115
|
+
* Internal: builds a `RouteLinkResult` from path and options.
|
|
116
|
+
* Exported for direct use in tests (avoids generic constraints).
|
|
117
|
+
*/
|
|
118
|
+
export declare function buildRouteLink(path: string, options?: RoutePathOptionsBase): RouteLinkResult;
|
|
119
|
+
/**
|
|
120
|
+
* Internal URL builder. Separated from `routePath` so it can be
|
|
121
|
+
* used without generic constraints (e.g., in `injectNavigate`).
|
|
122
|
+
*/
|
|
123
|
+
export declare function buildUrl(path: string, options?: RoutePathOptionsBase): string;
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
export type ValidationFieldErrors = Record<string, string[]>;
|
|
3
|
+
export declare function issuePathToFieldName(path: ReadonlyArray<string | number | symbol | {
|
|
4
|
+
key: string | number | symbol;
|
|
5
|
+
}>): string;
|
|
6
|
+
export declare function issuesToFieldErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): ValidationFieldErrors;
|
|
7
|
+
export declare function issuesToFormErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): string[];
|