@abinnovision/payloadcms-wayfinder 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +132 -0
  3. package/dist/admin/LinkLabelFeature.client.d.mts +5 -0
  4. package/dist/admin/LinkLabelFeature.client.mjs +35 -0
  5. package/dist/admin/index.d.mts +2 -0
  6. package/dist/admin/index.mjs +3 -0
  7. package/dist/config/collection-string-field.d.mts +20 -0
  8. package/dist/config/collection-string-field.mjs +33 -0
  9. package/dist/config/has-duplicates.d.mts +15 -0
  10. package/dist/config/has-duplicates.mjs +14 -0
  11. package/dist/config/index.d.mts +8 -0
  12. package/dist/config/index.mjs +8 -0
  13. package/dist/config/link-field.d.mts +35 -0
  14. package/dist/config/link-field.mjs +150 -0
  15. package/dist/config/load-mappings.d.mts +38 -0
  16. package/dist/config/load-mappings.mjs +60 -0
  17. package/dist/config/mapping-global.d.mts +34 -0
  18. package/dist/config/mapping-global.mjs +86 -0
  19. package/dist/config/plugin.d.mts +31 -0
  20. package/dist/config/plugin.mjs +36 -0
  21. package/dist/config/translations.d.mts +34 -0
  22. package/dist/config/translations.mjs +45 -0
  23. package/dist/index.d.mts +16 -0
  24. package/dist/index.mjs +15 -0
  25. package/dist/lexical/index.d.mts +52 -0
  26. package/dist/lexical/index.mjs +57 -0
  27. package/dist/montage/index.d.mts +32 -0
  28. package/dist/montage/index.mjs +30 -0
  29. package/dist/pattern/define-links.d.mts +187 -0
  30. package/dist/pattern/define-links.mjs +41 -0
  31. package/dist/pattern/define-mappings.d.mts +15 -0
  32. package/dist/pattern/define-mappings.mjs +14 -0
  33. package/dist/pattern/derive-link-label.d.mts +23 -0
  34. package/dist/pattern/derive-link-label.mjs +35 -0
  35. package/dist/pattern/index.d.mts +8 -0
  36. package/dist/pattern/index.mjs +8 -0
  37. package/dist/pattern/matcher.d.mts +26 -0
  38. package/dist/pattern/matcher.mjs +74 -0
  39. package/dist/pattern/param-query-path.d.mts +42 -0
  40. package/dist/pattern/param-query-path.mjs +53 -0
  41. package/dist/pattern/resolver.d.mts +30 -0
  42. package/dist/pattern/resolver.mjs +104 -0
  43. package/dist/pattern/types.d.mts +169 -0
  44. package/dist/pattern/types.mjs +11 -0
  45. package/dist/runtime/build-href.d.mts +39 -0
  46. package/dist/runtime/build-href.mjs +73 -0
  47. package/dist/runtime/build-path.d.mts +32 -0
  48. package/dist/runtime/build-path.mjs +49 -0
  49. package/dist/runtime/diagnostics.d.mts +35 -0
  50. package/dist/runtime/index.d.mts +7 -0
  51. package/dist/runtime/index.mjs +6 -0
  52. package/dist/runtime/resolve-link.d.mts +63 -0
  53. package/dist/runtime/resolve-link.mjs +89 -0
  54. package/dist/runtime/resolve-path.d.mts +54 -0
  55. package/dist/runtime/resolve-path.mjs +108 -0
  56. package/dist/runtime/resolve-relationship-slug.d.mts +33 -0
  57. package/dist/runtime/resolve-relationship-slug.mjs +51 -0
  58. package/package.json +133 -0
@@ -0,0 +1,104 @@
1
+ import { DEFAULT_LOCALE_KEY } from "./types.mjs";
2
+ import * as ptr from "path-to-regexp";
3
+ //#region src/pattern/resolver.ts
4
+ /**
5
+ * A wildcard stands for a whole path, so its value carries a leading slash to
6
+ * match how full paths are stored — a wildcard-mapped collection's identifier
7
+ * is `/about/team`, not `about/team`.
8
+ *
9
+ * @param value Raw parameter value from path-to-regexp.
10
+ * @param isWildcard Whether the parameter was declared as a wildcard.
11
+ */ const normaliseValue = (value, isWildcard) => {
12
+ let joined = "";
13
+ if (Array.isArray(value)) joined = value.filter((it) => typeof it === "string").join("/");
14
+ else if (typeof value === "string") joined = value;
15
+ if (!isWildcard) return joined;
16
+ return joined.startsWith("/") ? joined : `/${joined}`;
17
+ };
18
+ /**
19
+ * Reverses {@link normaliseValue} so a stored full path can be fed back into
20
+ * `compile`, which requires a non-empty array for a wildcard.
21
+ *
22
+ * @param value The parameter value to convert.
23
+ */ const denormaliseWildcard = (value) => (typeof value === "string" ? value : "").split("/").filter(Boolean);
24
+ const measureSpecificity = (pattern, keys) => {
25
+ const segments = pattern.split("/").filter(Boolean);
26
+ return {
27
+ literalSegments: segments.filter((segment) => !segment.startsWith(":") && !segment.startsWith("*")).length,
28
+ hasWildcard: keys.some((key) => key.type === "wildcard"),
29
+ totalSegments: segments.length
30
+ };
31
+ };
32
+ const createResolvers = (pattern) => {
33
+ const { keys } = ptr.pathToRegexp(pattern);
34
+ const matchFn = ptr.match(pattern);
35
+ const buildFn = ptr.compile(pattern);
36
+ const wildcardNames = new Set(keys.filter((key) => key.type === "wildcard").map((key) => key.name));
37
+ return {
38
+ specificity: measureSpecificity(pattern, keys),
39
+ paramNames: keys.map((key) => key.name),
40
+ match: (path) => {
41
+ const matched = matchFn(path);
42
+ if (!matched) return false;
43
+ const ordered = keys.map((key) => {
44
+ const name = key.name;
45
+ return {
46
+ name,
47
+ value: normaliseValue(matched.params[name], wildcardNames.has(name))
48
+ };
49
+ });
50
+ const identifying = ordered.at(-1);
51
+ if (!identifying || !identifying.value) return false;
52
+ return {
53
+ identifier: {
54
+ field: identifying.name,
55
+ value: identifying.value
56
+ },
57
+ scope: Object.fromEntries(ordered.slice(0, -1).map((it) => [it.name, it.value]))
58
+ };
59
+ },
60
+ build: (params) => buildFn(Object.fromEntries(Object.entries(params).map(([name, value]) => [name, wildcardNames.has(name) ? denormaliseWildcard(value) : value])))
61
+ };
62
+ };
63
+ /**
64
+ * Normalises the two shapes a mapping's `path` can take.
65
+ *
66
+ * A localized project yields one pattern per locale. A project with no
67
+ * `localization` block yields a plain string, which would otherwise be walked
68
+ * character by character by `Object.entries` and compile into one nonsense
69
+ * resolver per character.
70
+ *
71
+ * @param path The pattern, or per-locale patterns.
72
+ */ const normalisePath = (path) => typeof path === "string" ? { [DEFAULT_LOCALE_KEY]: path } : path;
73
+ /**
74
+ * Whether a wildcard pattern was handed the site root. `compile` rejects an
75
+ * empty wildcard, so the home page — a document whose identifier is just `/` —
76
+ * has to be built as the bare root instead.
77
+ *
78
+ * @param resolvers The pattern's compiled resolvers.
79
+ * @param values The parameter values, in pattern order.
80
+ */ const isRootWildcard = (resolvers, values) => resolvers.specificity.hasWildcard && values.every((value) => value === "" || value === "/");
81
+ /**
82
+ * Picks the resolvers for a locale, falling back to the unlocalized bucket.
83
+ *
84
+ * A project without localization has exactly one bucket under
85
+ * {@link DEFAULT_LOCALE_KEY}, so callers pass whatever locale they have and
86
+ * still get the right pattern.
87
+ *
88
+ * @param mapping The compiled mapping.
89
+ * @param locale The locale to resolve for.
90
+ */ const resolversFor = (mapping, locale) => mapping.resolvers[locale] ?? mapping.resolvers["__default"];
91
+ /**
92
+ * Compiles a mapping's patterns into match and build functions.
93
+ *
94
+ * @param input The mapping as authored.
95
+ */ const resolveCollectionMapping = (input) => {
96
+ const path = normalisePath(input.path);
97
+ return {
98
+ collection: input.collection,
99
+ path,
100
+ resolvers: Object.fromEntries(Object.entries(path).map(([locale, pattern]) => [locale, createResolvers(pattern)]))
101
+ };
102
+ };
103
+ //#endregion
104
+ export { isRootWildcard, resolveCollectionMapping, resolversFor };
@@ -0,0 +1,169 @@
1
+ import { ParamData } from "path-to-regexp";
2
+ import { Field } from "payload";
3
+ //#region src/pattern/types.d.ts
4
+ /**
5
+ * Locale bucket used when a project has no localization configured.
6
+ *
7
+ * Payload only returns a per-locale record for a localized field, and only
8
+ * when the config declares locales at all. Without them the mapping's `path`
9
+ * arrives as a plain string, which normalises into this single bucket so the
10
+ * rest of the package has one shape to work with.
11
+ */
12
+ declare const DEFAULT_LOCALE_KEY = "__default";
13
+ /**
14
+ * Maps one collection onto the URL patterns its documents live at.
15
+ *
16
+ * A localized project supplies one pattern per locale; a project without
17
+ * localization supplies a single pattern. Authored in the mapping global, so
18
+ * adding a page type never means touching routing code.
19
+ */
20
+ interface PayloadCollectionMapping {
21
+ collection: string;
22
+ /**
23
+ * Either one pattern per locale (`{ de: "/:format/:slug" }`) or a single
24
+ * pattern for a project with no locales.
25
+ */
26
+ path: string | Record<string, string>;
27
+ }
28
+ /**
29
+ * Result of matching a path against one mapping.
30
+ */
31
+ interface PayloadCollectionMappingMatch {
32
+ /**
33
+ * The pattern's last parameter, which identifies the document within its
34
+ * collection.
35
+ */
36
+ identifier: {
37
+ field: string;
38
+ value: string;
39
+ };
40
+ /**
41
+ * Any earlier parameters, keyed by their raw parameter name. These narrow
42
+ * the lookup without identifying the document — `format` in
43
+ * `/:format/:slug` is one.
44
+ */
45
+ scope: Record<string, string>;
46
+ }
47
+ /**
48
+ * How specific a pattern is, used to order candidates so that the most
49
+ * precise pattern wins regardless of the order rows sit in the admin UI.
50
+ */
51
+ interface PayloadCollectionMappingSpecificity {
52
+ /** Segments that are literal text rather than a parameter. */
53
+ literalSegments: number;
54
+ /** Whether the pattern ends in a catch-all. */
55
+ hasWildcard: boolean;
56
+ totalSegments: number;
57
+ }
58
+ interface PayloadCollectionMappingResolvers {
59
+ match: (path: string) => false | PayloadCollectionMappingMatch;
60
+ build: (params: ParamData) => string;
61
+ /**
62
+ * Parameter names in pattern order, so callers can collect the values a
63
+ * `build` needs without re-parsing the pattern. The last entry identifies
64
+ * the document.
65
+ */
66
+ paramNames: string[];
67
+ specificity: PayloadCollectionMappingSpecificity;
68
+ }
69
+ interface PayloadCollectionMappingResolved {
70
+ collection: string;
71
+ /** Always a record after normalisation, keyed by locale or by {@link DEFAULT_LOCALE_KEY}. */
72
+ path: Record<string, string>;
73
+ resolvers: Record<string, PayloadCollectionMappingResolvers>;
74
+ }
75
+ /**
76
+ * Rewrites a built path before it is returned.
77
+ *
78
+ * Locale prefixing and preview prefixing are the same transform applied at the
79
+ * same point, so they share one hook rather than competing for two options:
80
+ *
81
+ * ```ts
82
+ * formatHref: ({ path, locale }) => `/${locale}${path}`
83
+ * formatHref: ({ path, locale }) => `/${locale}${isPreview ? "/-preview" : ""}${path}`
84
+ * ```
85
+ *
86
+ * Defaults to returning the path untouched, so a project serving unprefixed
87
+ * URLs needs no configuration.
88
+ */
89
+ type FormatHref = (args: {
90
+ path: string;
91
+ locale: string;
92
+ }) => string;
93
+ /** Label accepted wherever the admin panel shows one. */
94
+ type LabelLike = string | Record<string, string>;
95
+ /**
96
+ * Optional-everything, including an explicit `undefined`.
97
+ *
98
+ * `Partial<T>` is not enough under `exactOptionalPropertyTypes`: it marks a
99
+ * property optional without letting it hold `undefined`, so a variant that
100
+ * reads one of its own optional fields and passes it straight back would not
101
+ * typecheck against its own declared shape.
102
+ */
103
+ type Contributed<T> = { [K in keyof T]?: T[K] | undefined; };
104
+ /** The link types the package understands without configuration. */
105
+ type BuiltinLinkVariant = "none" | "reference" | "custom" | "same-page";
106
+ /**
107
+ * Structural shape of the `link` field group.
108
+ *
109
+ * Declared here rather than imported from a project's generated types so the
110
+ * field definition that produces it does not depend on its own output.
111
+ * Nullable throughout because that is how Payload emits optional fields — a
112
+ * mismatch here shows up at every call site.
113
+ *
114
+ * `TVariant` carries any app-declared variant names, and `TExtra` the fields
115
+ * those variants contribute, so a variant's own resolver can read the data its
116
+ * own fields produced without a cast.
117
+ */
118
+ type LinkFieldData<TVariant extends string = never, TExtra = object> = {
119
+ type?: BuiltinLinkVariant | TVariant | null;
120
+ label?: string | null;
121
+ reference?: {
122
+ relationTo: string;
123
+ value: string | {
124
+ id: string;
125
+ [field: string]: unknown;
126
+ };
127
+ } | null;
128
+ url?: string | null;
129
+ samePageIdentifier?: string | null;
130
+ newTab?: boolean | null;
131
+ } & Contributed<TExtra>;
132
+ /** What every link resolves to, before any variant adds to it. */
133
+ interface BaseResolvedLink {
134
+ href: string;
135
+ target?: string;
136
+ rel?: string;
137
+ }
138
+ /**
139
+ * A resolved link, widened by whatever an app-declared variant returns.
140
+ *
141
+ * `E` defaults to an empty object rather than `unknown`: an intersection with
142
+ * an uninstantiated type parameter stays deferred, which would force a cast at
143
+ * every built-in branch.
144
+ *
145
+ * Every contributed property is optional, for the same reason it is on
146
+ * {@link LinkFieldData}: `E` is the union of what *every* variant contributes,
147
+ * so requiring all of it would mean each variant had to return the other
148
+ * variants' properties alongside its own.
149
+ */
150
+ type ResolvedLink<E = object> = BaseResolvedLink & Contributed<E>;
151
+ /**
152
+ * One variant, flattened out of a declaration with its key put back on it.
153
+ *
154
+ * The shape everything downstream of {@link defineLinks} works with. Not an
155
+ * authoring form: variants are written through the builder, which is what
156
+ * derives their field types.
157
+ */
158
+ interface DeclaredLinkVariant<TCtx = unknown, TExtra = object> {
159
+ value: string;
160
+ label: LabelLike;
161
+ /** Readonly, because a `const` type parameter infers a readonly tuple. */
162
+ fields?: readonly Field[];
163
+ resolve?: (args: {
164
+ link: LinkFieldData<string, TExtra>;
165
+ context: TCtx;
166
+ }) => ResolvedLink<TExtra> | null;
167
+ }
168
+ //#endregion
169
+ export { BaseResolvedLink, BuiltinLinkVariant, Contributed, DEFAULT_LOCALE_KEY, DeclaredLinkVariant, FormatHref, LabelLike, LinkFieldData, PayloadCollectionMapping, PayloadCollectionMappingMatch, PayloadCollectionMappingResolved, PayloadCollectionMappingResolvers, PayloadCollectionMappingSpecificity, ResolvedLink };
@@ -0,0 +1,11 @@
1
+ //#region src/pattern/types.ts
2
+ /**
3
+ * Locale bucket used when a project has no localization configured.
4
+ *
5
+ * Payload only returns a per-locale record for a localized field, and only
6
+ * when the config declares locales at all. Without them the mapping's `path`
7
+ * arrives as a plain string, which normalises into this single bucket so the
8
+ * rest of the package has one shape to work with.
9
+ */ const DEFAULT_LOCALE_KEY = "__default";
10
+ //#endregion
11
+ export { DEFAULT_LOCALE_KEY };
@@ -0,0 +1,39 @@
1
+ import { FormatHref, PayloadCollectionMappingResolved } from "../pattern/types.mjs";
2
+ import { BuildDiagnosticReason, OnDiagnostic } from "./diagnostics.mjs";
3
+ //#region src/runtime/build-href.d.ts
4
+ /**
5
+ * A document as it arrives on a populated relationship.
6
+ *
7
+ * Typed as a plain object rather than an indexed record so generated
8
+ * collection interfaces assign to it; the fields the path pattern names are
9
+ * read dynamically below.
10
+ */
11
+ type LinkableDocument = object;
12
+ /** Applied when no {@link FormatHref} is supplied: the path, untouched. */
13
+ declare const identityFormatHref: FormatHref;
14
+ interface BuildHrefArgs {
15
+ mappings: PayloadCollectionMappingResolved[];
16
+ collection: string;
17
+ document: LinkableDocument;
18
+ locale: string;
19
+ formatHref?: FormatHref;
20
+ /**
21
+ * The field a populated relationship is identified by, when it cannot be
22
+ * derived from the target's own pattern. Must match what
23
+ * `resolveParamQueryPath` filters on, or build and match disagree.
24
+ */
25
+ identifierField?: string;
26
+ onDiagnostic?: OnDiagnostic<BuildDiagnosticReason>;
27
+ }
28
+ /**
29
+ * Builds the href a document is served at, using its collection's pattern.
30
+ *
31
+ * Returns null when the collection has no mapping or the document is missing a
32
+ * value the pattern needs — a relationship left unpopulated is the usual
33
+ * cause, so check `defaultPopulate` before assuming the mapping is wrong.
34
+ *
35
+ * @param args The mappings, target collection, document and locale.
36
+ */
37
+ declare const buildHref: (args: BuildHrefArgs) => string | null;
38
+ //#endregion
39
+ export { BuildHrefArgs, LinkableDocument, buildHref, identityFormatHref };
@@ -0,0 +1,73 @@
1
+ import { resolversFor } from "../pattern/resolver.mjs";
2
+ //#region src/runtime/build-href.ts
3
+ /** Applied when no {@link FormatHref} is supplied: the path, untouched. */ const identityFormatHref = ({ path }) => path;
4
+ /**
5
+ * Reads the value a path parameter needs off a document.
6
+ *
7
+ * A plain field yields its own value; a populated relationship yields the
8
+ * related document's identifier, which is what the query matches on.
9
+ *
10
+ * @param document The document being linked to.
11
+ * @param param The path parameter name.
12
+ * @param identifierField The related document's identifying field.
13
+ */ const readParam = (document, param, identifierField) => {
14
+ const value = document[param];
15
+ if (typeof value === "string") return value;
16
+ if (value && typeof value === "object" && identifierField in value) {
17
+ const identifier = value[identifierField];
18
+ return typeof identifier === "string" ? identifier : void 0;
19
+ }
20
+ };
21
+ /**
22
+ * Builds the href a document is served at, using its collection's pattern.
23
+ *
24
+ * Returns null when the collection has no mapping or the document is missing a
25
+ * value the pattern needs — a relationship left unpopulated is the usual
26
+ * cause, so check `defaultPopulate` before assuming the mapping is wrong.
27
+ *
28
+ * @param args The mappings, target collection, document and locale.
29
+ */ const buildHref = (args) => {
30
+ const format = args.formatHref ?? identityFormatHref;
31
+ const identifierField = args.identifierField ?? "slug";
32
+ const mapping = args.mappings.find((it) => it.collection === args.collection);
33
+ if (!mapping) {
34
+ args.onDiagnostic?.({
35
+ reason: "no-mapping",
36
+ collection: args.collection
37
+ });
38
+ return null;
39
+ }
40
+ const resolvers = resolversFor(mapping, args.locale);
41
+ if (!resolvers) {
42
+ args.onDiagnostic?.({
43
+ reason: "no-locale-pattern",
44
+ collection: args.collection,
45
+ locale: args.locale
46
+ });
47
+ return null;
48
+ }
49
+ const params = {};
50
+ for (const name of resolvers.paramNames) {
51
+ const value = readParam(args.document, name, identifierField);
52
+ if (value === void 0) {
53
+ args.onDiagnostic?.({
54
+ reason: "missing-param",
55
+ collection: args.collection,
56
+ param: name
57
+ });
58
+ return null;
59
+ }
60
+ params[name] = value;
61
+ }
62
+ const identifying = resolvers.paramNames.at(-1);
63
+ if (identifying && params[identifying] === "/") return format({
64
+ path: "/",
65
+ locale: args.locale
66
+ });
67
+ return format({
68
+ path: resolvers.build(params),
69
+ locale: args.locale
70
+ });
71
+ };
72
+ //#endregion
73
+ export { buildHref, identityFormatHref };
@@ -0,0 +1,32 @@
1
+ import { FormatHref, PayloadCollectionMappingResolved } from "../pattern/types.mjs";
2
+ import { BuildDiagnosticReason, OnDiagnostic } from "./diagnostics.mjs";
3
+ //#region src/runtime/build-path.d.ts
4
+ interface BuildPathArgs {
5
+ mappings: PayloadCollectionMappingResolved[];
6
+ collection: string;
7
+ locale: string;
8
+ /**
9
+ * Parameter values, either in pattern order or keyed by parameter name.
10
+ * Positional values let a caller fill a pattern without knowing what its
11
+ * parameters are called, so renaming one in the CMS needs no code change.
12
+ */
13
+ values: string[] | Record<string, string>;
14
+ formatHref?: FormatHref;
15
+ onDiagnostic?: OnDiagnostic<BuildDiagnosticReason>;
16
+ }
17
+ /**
18
+ * Builds a path from parameter values alone, with no document.
19
+ *
20
+ * Sitemaps and feeds run outside a request's rendering context and select only
21
+ * the fields they need, so they never hold a document shaped the way
22
+ * `buildHref` expects. They do hold the values, which is all a pattern needs.
23
+ *
24
+ * An unmapped collection falls back to the site root rather than returning
25
+ * nothing: emitting a bare root into a feed is recoverable, emitting an empty
26
+ * href is not.
27
+ *
28
+ * @param args The mappings, collection, locale and parameter values.
29
+ */
30
+ declare const buildPath: (args: BuildPathArgs) => string;
31
+ //#endregion
32
+ export { BuildPathArgs, buildPath };
@@ -0,0 +1,49 @@
1
+ import { isRootWildcard, resolversFor } from "../pattern/resolver.mjs";
2
+ import { identityFormatHref } from "./build-href.mjs";
3
+ //#region src/runtime/build-path.ts
4
+ /**
5
+ * Builds a path from parameter values alone, with no document.
6
+ *
7
+ * Sitemaps and feeds run outside a request's rendering context and select only
8
+ * the fields they need, so they never hold a document shaped the way
9
+ * `buildHref` expects. They do hold the values, which is all a pattern needs.
10
+ *
11
+ * An unmapped collection falls back to the site root rather than returning
12
+ * nothing: emitting a bare root into a feed is recoverable, emitting an empty
13
+ * href is not.
14
+ *
15
+ * @param args The mappings, collection, locale and parameter values.
16
+ */ const buildPath = (args) => {
17
+ const format = args.formatHref ?? identityFormatHref;
18
+ const root = () => format({
19
+ path: "/",
20
+ locale: args.locale
21
+ });
22
+ const mapping = args.mappings.find((it) => it.collection === args.collection);
23
+ if (!mapping) {
24
+ args.onDiagnostic?.({
25
+ reason: "no-mapping",
26
+ collection: args.collection
27
+ });
28
+ return root();
29
+ }
30
+ const resolvers = resolversFor(mapping, args.locale);
31
+ if (!resolvers) {
32
+ args.onDiagnostic?.({
33
+ reason: "no-locale-pattern",
34
+ collection: args.collection,
35
+ locale: args.locale
36
+ });
37
+ return root();
38
+ }
39
+ const values = args.values;
40
+ const ordered = Array.isArray(values) ? resolvers.paramNames.map((_, index) => values[index] ?? "") : resolvers.paramNames.map((name) => values[name] ?? "");
41
+ if (isRootWildcard(resolvers, ordered)) return root();
42
+ const params = Object.fromEntries(resolvers.paramNames.map((name, index) => [name, ordered[index] ?? ""]));
43
+ return format({
44
+ path: resolvers.build(params),
45
+ locale: args.locale
46
+ });
47
+ };
48
+ //#endregion
49
+ export { buildPath };
@@ -0,0 +1,35 @@
1
+ //#region src/runtime/diagnostics.d.ts
2
+ /**
3
+ * Why a routing call produced nothing.
4
+ *
5
+ * Every failure here returns `null` so the happy path stays a plain value
6
+ * rather than a wrapper type. That leaves no way to tell "this collection has
7
+ * no mapping" from "this relationship was never populated", which is exactly
8
+ * the distinction someone staring at a missing link needs. These reasons carry
9
+ * it out of band.
10
+ */
11
+ type DiagnosticReason = "no-mapping" | "no-locale-pattern" | "unpopulated-reference" | "missing-param" | "no-document" | "unknown-variant";
12
+ interface Diagnostic<TReason extends DiagnosticReason> {
13
+ reason: TReason;
14
+ collection?: string;
15
+ locale?: string;
16
+ path?: string;
17
+ param?: string;
18
+ variant?: string;
19
+ }
20
+ /**
21
+ * Reports why a call returned nothing.
22
+ *
23
+ * Fires once per failed call, so a page whose footer holds forty links to a
24
+ * misconfigured collection reports forty times. Deduplicating is the caller's
25
+ * job — the package cannot know which of them are worth surfacing.
26
+ */
27
+ type OnDiagnostic<TReason extends DiagnosticReason> = (diagnostic: Diagnostic<TReason>) => void;
28
+ /** Reasons {@link buildHref} and {@link buildPath} can report. */
29
+ type BuildDiagnosticReason = "no-mapping" | "no-locale-pattern" | "missing-param";
30
+ /** Reasons link resolution can report. */
31
+ type ResolveLinkDiagnosticReason = BuildDiagnosticReason | "unpopulated-reference" | "unknown-variant";
32
+ /** Reasons path resolution can report. */
33
+ type ResolvePathDiagnosticReason = "no-mapping" | "no-document";
34
+ //#endregion
35
+ export { BuildDiagnosticReason, Diagnostic, DiagnosticReason, OnDiagnostic, ResolveLinkDiagnosticReason, ResolvePathDiagnosticReason };
@@ -0,0 +1,7 @@
1
+ import { BuildDiagnosticReason, Diagnostic, DiagnosticReason, OnDiagnostic, ResolveLinkDiagnosticReason, ResolvePathDiagnosticReason } from "./diagnostics.mjs";
2
+ import { BuildHrefArgs, LinkableDocument, buildHref, identityFormatHref } from "./build-href.mjs";
3
+ import { BuildPathArgs, buildPath } from "./build-path.mjs";
4
+ import { ResolveLinkArgs, ResolveReference, isAvailableLink, resolveLink } from "./resolve-link.mjs";
5
+ import { PayloadDocument, ResolvePathToDocumentArgs, ResolvePathWhere, ResolvedPath, resolvePathToDocument } from "./resolve-path.mjs";
6
+ import { ResolveRelationshipSlugArgs, resolveRelationshipSlug } from "./resolve-relationship-slug.mjs";
7
+ export { type BuildDiagnosticReason, type BuildHrefArgs, type BuildPathArgs, type Diagnostic, type DiagnosticReason, type LinkableDocument, type OnDiagnostic, type PayloadDocument, type ResolveLinkArgs, type ResolveLinkDiagnosticReason, type ResolvePathDiagnosticReason, type ResolvePathToDocumentArgs, type ResolvePathWhere, type ResolveReference, type ResolveRelationshipSlugArgs, type ResolvedPath, buildHref, buildPath, identityFormatHref, isAvailableLink, resolveLink, resolvePathToDocument, resolveRelationshipSlug };
@@ -0,0 +1,6 @@
1
+ import { buildHref, identityFormatHref } from "./build-href.mjs";
2
+ import { buildPath } from "./build-path.mjs";
3
+ import { isAvailableLink, resolveLink } from "./resolve-link.mjs";
4
+ import { resolvePathToDocument } from "./resolve-path.mjs";
5
+ import { resolveRelationshipSlug } from "./resolve-relationship-slug.mjs";
6
+ export { buildHref, buildPath, identityFormatHref, isAvailableLink, resolveLink, resolvePathToDocument, resolveRelationshipSlug };
@@ -0,0 +1,63 @@
1
+ import { BaseResolvedLink, FormatHref, LinkFieldData, PayloadCollectionMappingResolved, ResolvedLink } from "../pattern/types.mjs";
2
+ import { LinkContextOf, LinkDeclaration, LinkVariantSource, ResolvedLinkOf } from "../pattern/define-links.mjs";
3
+ import { OnDiagnostic, ResolveLinkDiagnosticReason } from "./diagnostics.mjs";
4
+ //#region src/runtime/resolve-link.d.ts
5
+ /**
6
+ * Turns a reference into an href by some means other than the populated
7
+ * document — an id-to-identifier index, typically.
8
+ *
9
+ * Supplied only by projects that cap relationship depth, where a reference
10
+ * arrives as a bare id. One injected function rather than a second built-in
11
+ * strategy: the package resolves from the populated document, and this is the
12
+ * documented way out for setups that cannot.
13
+ */
14
+ type ResolveReference = (args: {
15
+ relationTo: string;
16
+ value: string | {
17
+ id: string;
18
+ };
19
+ locale: string;
20
+ }) => string | null;
21
+ interface ResolveLinkArgs<TExtra = object, TDeclaration extends LinkDeclaration = LinkDeclaration> extends LinkVariantSource<TDeclaration> {
22
+ link: LinkFieldData<string, TExtra> | undefined;
23
+ mappings: PayloadCollectionMappingResolved[];
24
+ locale: string;
25
+ /** Passed through to a variant's resolver untouched. */
26
+ /**
27
+ * Passed through to a variant's resolver untouched.
28
+ *
29
+ * When a declaration is supplied its resolvers already say what they
30
+ * expect, so that shape wins over the array form's own parameter: without
31
+ * this, any context at all would satisfy a declaration-based call.
32
+ */
33
+ context?: LinkContextOf<TDeclaration>;
34
+ formatHref?: FormatHref;
35
+ identifierField?: string;
36
+ resolveReference?: ResolveReference;
37
+ onDiagnostic?: OnDiagnostic<ResolveLinkDiagnosticReason>;
38
+ }
39
+ /**
40
+ * Turns a link field's value into an href.
41
+ *
42
+ * Internal references route through the collection mapping rather than any
43
+ * hardcoded prefix, so changing a collection's URL pattern in the CMS updates
44
+ * every link to it.
45
+ *
46
+ * Returns null rather than degrading to the site root. A link that silently
47
+ * points somewhere plausible is harder to find than one that renders nothing.
48
+ *
49
+ * @param args The link value, mappings, locale and any app-declared variants.
50
+ */
51
+ declare const resolveLink: <TExtra = object, TDeclaration extends LinkDeclaration = LinkDeclaration>(args: ResolveLinkArgs<TExtra, TDeclaration>) => BaseResolvedLink | ResolvedLink<TExtra> | ResolvedLinkOf<TDeclaration> | null;
52
+ /**
53
+ * Whether a link would resolve to something navigable. Use it to decide
54
+ * whether to render a link at all, rather than emitting a dead anchor.
55
+ *
56
+ * @param args The same arguments as {@link resolveLink}, plus whether a label
57
+ * is required for the link to be worth rendering.
58
+ */
59
+ declare const isAvailableLink: <TExtra = object, TDeclaration extends LinkDeclaration = LinkDeclaration>(args: ResolveLinkArgs<TExtra, TDeclaration> & {
60
+ withLabel?: boolean;
61
+ }) => boolean;
62
+ //#endregion
63
+ export { ResolveLinkArgs, ResolveReference, isAvailableLink, resolveLink };