@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,32 @@
1
+ import { PayloadCollectionMappingResolved } from "../pattern/types.mjs";
2
+ import { LoadMappingsArgs } from "../config/load-mappings.mjs";
3
+ import { MontageSlots } from "@abinnovision/payloadcms-montage";
4
+ //#region src/montage/index.d.ts
5
+ /**
6
+ * The slot the compiled mappings ride in.
7
+ *
8
+ * Prefixed with the package name because extension names share one namespace
9
+ * across every library using montage.
10
+ */
11
+ declare const wayfinderExtension: import("@abinnovision/payloadcms-montage").ContextExtension<PayloadCollectionMappingResolved[]>;
12
+ /**
13
+ * Loads the mappings once and parks them on the render context.
14
+ *
15
+ * Blocks resolve links individually and there may be dozens on a page, so the
16
+ * read happens once per request rather than once per link.
17
+ *
18
+ * @param ctx The render context.
19
+ * @param args The Payload instance and mapping-global settings.
20
+ */
21
+ declare const initWayfinder: (ctx: MontageSlots, args: LoadMappingsArgs) => Promise<void>;
22
+ /**
23
+ * Reads the mappings off the render context.
24
+ *
25
+ * Returns an empty list when {@link initWayfinder} has not run, so a block
26
+ * rendered outside a request renders without links rather than throwing.
27
+ *
28
+ * @param ctx The render context.
29
+ */
30
+ declare const getMappings: (ctx: MontageSlots) => PayloadCollectionMappingResolved[];
31
+ //#endregion
32
+ export { getMappings, initWayfinder, wayfinderExtension };
@@ -0,0 +1,30 @@
1
+ import { loadMappings } from "../config/load-mappings.mjs";
2
+ import { createContextExtension } from "@abinnovision/payloadcms-montage";
3
+ //#region src/montage/index.ts
4
+ /**
5
+ * The slot the compiled mappings ride in.
6
+ *
7
+ * Prefixed with the package name because extension names share one namespace
8
+ * across every library using montage.
9
+ */ const wayfinderExtension = createContextExtension("wayfinder:mappings");
10
+ /**
11
+ * Loads the mappings once and parks them on the render context.
12
+ *
13
+ * Blocks resolve links individually and there may be dozens on a page, so the
14
+ * read happens once per request rather than once per link.
15
+ *
16
+ * @param ctx The render context.
17
+ * @param args The Payload instance and mapping-global settings.
18
+ */ const initWayfinder = async (ctx, args) => {
19
+ wayfinderExtension.set(ctx, await loadMappings(args));
20
+ };
21
+ /**
22
+ * Reads the mappings off the render context.
23
+ *
24
+ * Returns an empty list when {@link initWayfinder} has not run, so a block
25
+ * rendered outside a request renders without links rather than throwing.
26
+ *
27
+ * @param ctx The render context.
28
+ */ const getMappings = (ctx) => wayfinderExtension.get(ctx) ?? [];
29
+ //#endregion
30
+ export { getMappings, initWayfinder, wayfinderExtension };
@@ -0,0 +1,187 @@
1
+ import { BaseResolvedLink, Contributed, DeclaredLinkVariant, LabelLike, LinkFieldData } from "./types.mjs";
2
+ import { Field } from "payload";
3
+ //#region src/pattern/define-links.d.ts
4
+ /**
5
+ * Collapses a union of object types into their intersection.
6
+ *
7
+ * Indexing a record of variants yields a union, and `Contributed<T>` is
8
+ * homomorphic, so it distributes over that union and every property becomes
9
+ * invisible. Intersecting first is what keeps them readable.
10
+ */
11
+ type UnionToIntersection<U> = (U extends unknown ? (it: U) => void : never) extends ((it: infer I) => void) ? I : never;
12
+ /** The values a `select` field accepts, from either option shape. */
13
+ type OptionValue<O1> = O1 extends readonly (infer I)[] ? I extends string ? I : I extends {
14
+ value: infer V;
15
+ } ? V : never : never;
16
+ /** What a relationship or upload holds, populated or not. */
17
+ type RelatedValue = string | number | {
18
+ id: string | number;
19
+ };
20
+ /**
21
+ * The value one field holds, as Payload emits it.
22
+ *
23
+ * Covers the field types a link variant realistically uses to describe a
24
+ * destination. Anything else resolves to `unknown` rather than a guess: a wrong
25
+ * type would be worse than an unhelpful one, because it would be believed.
26
+ *
27
+ * `hasMany` is checked before each scalar case for that reason — it changes the
28
+ * value to an array, and typing it as a scalar would be wrong rather than
29
+ * merely vague.
30
+ */
31
+ type FieldData<F1> = F1 extends {
32
+ name: infer N extends string;
33
+ } ? F1 extends {
34
+ type: "select" | "radio";
35
+ options: infer O;
36
+ hasMany: true;
37
+ } ? { [K in N]?: OptionValue<O>[] | null; } : F1 extends {
38
+ type: "select" | "radio";
39
+ options: infer O;
40
+ } ? { [K in N]?: OptionValue<O> | null; } : F1 extends {
41
+ type: "relationship" | "upload";
42
+ hasMany: true;
43
+ } ? { [K in N]?: RelatedValue[] | null; } : F1 extends {
44
+ type: "relationship" | "upload";
45
+ } ? { [K in N]?: RelatedValue | null; } : F1 extends {
46
+ type: "text" | "textarea" | "email" | "code" | "date";
47
+ } ? { [K in N]?: string | null; } : F1 extends {
48
+ type: "number";
49
+ } ? { [K in N]?: number | null; } : F1 extends {
50
+ type: "checkbox";
51
+ } ? { [K in N]?: boolean | null; } : { [K in N]?: unknown; } : object;
52
+ /** Everything a variant's own fields contribute, as one object type. */
53
+ type DataOfFields<TFields extends readonly unknown[]> = UnionToIntersection<FieldData<TFields[number]>>;
54
+ /** A variant that has had no resolver attached. */
55
+ interface LinkVariantSpec<TFields extends readonly Field[]> {
56
+ label: LabelLike;
57
+ fields?: TFields;
58
+ }
59
+ /**
60
+ * A declared link type: its admin presentation, the fields it contributes, and
61
+ * how it turns into an href.
62
+ */
63
+ interface LinkVariantDefinition<TCtx = unknown, TFields extends readonly Field[] = readonly Field[], TExtra = object, TData = DataOfFields<TFields>> {
64
+ label: LabelLike;
65
+ fields?: TFields;
66
+ resolve?: (args: {
67
+ link: LinkFieldData<string, TData>;
68
+ context: TCtx;
69
+ }) => (BaseResolvedLink & TExtra) | null;
70
+ /**
71
+ * The variant's data shape, in a position it can be read back from.
72
+ *
73
+ * Declared and never assigned, so no such key exists at runtime. It is
74
+ * here because `.data<T>()` has to override what a variant contributes,
75
+ * and re-deriving from `fields` would ignore it.
76
+ */
77
+ readonly __data?: TData;
78
+ }
79
+ /**
80
+ * The loosest shape a variant can take, used wherever a declaration is only
81
+ * being constrained rather than read.
82
+ *
83
+ * `resolve` takes `never` so that any resolver satisfies it: a parameter is
84
+ * contravariant, and `never` is assignable to every argument type.
85
+ */
86
+ interface AnyLinkVariantDefinition {
87
+ label: LabelLike;
88
+ fields?: readonly Field[];
89
+ resolve?: (args: never) => unknown;
90
+ }
91
+ /** The link vocabulary an app declares: which types exist and how each resolves. */
92
+ interface LinkDeclaration<TVariants1 extends Record<string, AnyLinkVariantDefinition> = Record<string, AnyLinkVariantDefinition>> {
93
+ variants: TVariants1;
94
+ }
95
+ /** The stored shape of a link field built from a declaration. */
96
+ /**
97
+ * What one variant contributes.
98
+ *
99
+ * Read off `__data` rather than re-derived from `fields`, so a variant that
100
+ * named its own shape with `.data<T>()` keeps it.
101
+ */
102
+ type DataOfVariant<V> = V extends {
103
+ __data?: infer D;
104
+ } ? unknown extends D ? object : D : V extends {
105
+ fields?: infer F extends readonly unknown[];
106
+ } ? DataOfFields<F> : object;
107
+ type LinkDataOf<T> = T extends LinkDeclaration<infer TVariants> ? LinkFieldData<Extract<keyof TVariants, string>, UnionToIntersection<{ [K in keyof TVariants]: DataOfVariant<TVariants[K]>; }[keyof TVariants]>> : never;
108
+ /** What resolving a link built from a declaration can produce. */
109
+ type ResolvedLinkOf<T> = T extends LinkDeclaration<infer TVariants> ? BaseResolvedLink & Contributed<UnionToIntersection<{ [K in keyof TVariants]: TVariants[K] extends {
110
+ resolve?: (args: never) => infer R;
111
+ } ? Extract<R, BaseResolvedLink> extends (infer E) ? Omit<E, keyof BaseResolvedLink> : object : object; }[keyof TVariants]>> : never;
112
+ /**
113
+ * Builds one variant, inferring what its fields contribute.
114
+ *
115
+ * Two calls rather than one object, because TypeScript will not contextually
116
+ * type a resolver from a sibling property of the same object literal. Passing
117
+ * the fields through `variant(...)` first is what lets `.resolve()` see them.
118
+ */
119
+ /** A variant part-way through being built. */
120
+ interface VariantStep<TCtx, TFields extends readonly Field[], TData> {
121
+ resolve: <TExtra extends object>(fn: (args: {
122
+ link: LinkFieldData<string, TData>;
123
+ context: TCtx;
124
+ }) => (BaseResolvedLink & TExtra) | null) => LinkVariantDefinition<TCtx, TFields, TExtra, TData>;
125
+ /**
126
+ * Names the shape for fields that cannot be derived.
127
+ *
128
+ * A few field types resolve to `unknown`, because guessing at their shape
129
+ * would produce something wrong rather than something vague, and a wrong
130
+ * type gets believed. This replaces the derived shape for one variant,
131
+ * leaving every other variant alone.
132
+ */
133
+ data: <TOverride>() => VariantStep<TCtx, TFields, TOverride>;
134
+ }
135
+ type VariantBuilder<TCtx> = <const TFields extends readonly Field[]>(spec: LinkVariantSpec<TFields>) => LinkVariantDefinition<TCtx, TFields, object, DataOfFields<TFields>> & VariantStep<TCtx, TFields, DataOfFields<TFields>>;
136
+ /**
137
+ * Declares the link types an app offers.
138
+ *
139
+ * One declaration feeds every place a link is handled: the field an editor
140
+ * fills in, the resolver that turns it into an href, and the rich-text
141
+ * feature. Passing the same declaration to all of them is what stops a variant
142
+ * existing in the admin panel but resolving to nothing.
143
+ *
144
+ * Curried because TypeScript has no partial type-argument inference: naming the
145
+ * context type up front would otherwise force every variant's fields to be
146
+ * named too.
147
+ *
148
+ * Returns plain data. It builds no field and resolves no link itself, so it
149
+ * stays free of the Payload runtime and a frontend can hold the same
150
+ * declaration the CMS config does.
151
+ */
152
+ declare const defineLinks: <TCtx = unknown>() => <const TVariants1 extends Record<string, AnyLinkVariantDefinition>>(build: (variant: VariantBuilder<TCtx>) => {
153
+ variants: TVariants1;
154
+ }) => LinkDeclaration<TVariants1>;
155
+ /**
156
+ * The context a declaration's resolvers expect, recovered from the declaration
157
+ * itself so a caller does not have to name it twice.
158
+ */
159
+ type ContextOfVariant<V> = V extends {
160
+ resolve?: infer R;
161
+ } ? NonNullable<R> extends ((...args: never[]) => unknown) ? Parameters<NonNullable<R>>[0] extends {
162
+ context: infer C;
163
+ } ? C : never : never : never;
164
+ type LinkContextOf<T> = T extends LinkDeclaration<infer TVariants> ? UnionToIntersection<{ [K in keyof TVariants]: ContextOfVariant<TVariants[K]>; }[keyof TVariants]> : unknown;
165
+ /**
166
+ * Carries a declaration to whichever function needs it.
167
+ *
168
+ * `TDeclaration` is a type parameter rather than the bare
169
+ * {@link LinkDeclaration} so that what the declaration's resolvers return, and
170
+ * the context they expect, both flow to the call site. Without it a caller
171
+ * would have to annotate the result to see its own variants' properties.
172
+ */
173
+ interface LinkVariantSource<TDeclaration extends LinkDeclaration = LinkDeclaration> {
174
+ /** The declaration built by {@link defineLinks}. */
175
+ links?: TDeclaration;
176
+ }
177
+ /**
178
+ * Flattens a declaration into a list.
179
+ *
180
+ * The keyed form carries each variant's value as its key, so it is put back on
181
+ * the object here and everything downstream works with one shape.
182
+ *
183
+ * @param source The declaration, or nothing.
184
+ */
185
+ declare const variantsOf: <TCtx, TExtra>(source: LinkVariantSource) => readonly DeclaredLinkVariant<TCtx, TExtra>[];
186
+ //#endregion
187
+ export { AnyLinkVariantDefinition, DataOfFields, LinkContextOf, LinkDataOf, LinkDeclaration, LinkVariantDefinition, LinkVariantSource, LinkVariantSpec, ResolvedLinkOf, VariantBuilder, defineLinks, variantsOf };
@@ -0,0 +1,41 @@
1
+ //#region src/pattern/define-links.ts
2
+ const createVariantBuilder = () => {
3
+ const step = (spec) => ({
4
+ ...spec,
5
+ resolve: (fn) => ({
6
+ ...spec,
7
+ resolve: fn
8
+ }),
9
+ data: () => step(spec)
10
+ });
11
+ return (spec) => step(spec);
12
+ };
13
+ /**
14
+ * Declares the link types an app offers.
15
+ *
16
+ * One declaration feeds every place a link is handled: the field an editor
17
+ * fills in, the resolver that turns it into an href, and the rich-text
18
+ * feature. Passing the same declaration to all of them is what stops a variant
19
+ * existing in the admin panel but resolving to nothing.
20
+ *
21
+ * Curried because TypeScript has no partial type-argument inference: naming the
22
+ * context type up front would otherwise force every variant's fields to be
23
+ * named too.
24
+ *
25
+ * Returns plain data. It builds no field and resolves no link itself, so it
26
+ * stays free of the Payload runtime and a frontend can hold the same
27
+ * declaration the CMS config does.
28
+ */ const defineLinks = () => (build) => build(createVariantBuilder());
29
+ /**
30
+ * Flattens a declaration into a list.
31
+ *
32
+ * The keyed form carries each variant's value as its key, so it is put back on
33
+ * the object here and everything downstream works with one shape.
34
+ *
35
+ * @param source The declaration, or nothing.
36
+ */ const variantsOf = (source) => source.links ? Object.entries(source.links.variants).map(([value, definition]) => ({
37
+ ...definition,
38
+ value
39
+ })) : [];
40
+ //#endregion
41
+ export { defineLinks, variantsOf };
@@ -0,0 +1,15 @@
1
+ import { PayloadCollectionMapping, PayloadCollectionMappingResolved } from "./types.mjs";
2
+ //#region src/pattern/define-mappings.d.ts
3
+ /**
4
+ * Compiles a code-defined collection map.
5
+ *
6
+ * Every runtime function takes its mappings as plain data, so the CMS-authored
7
+ * global is one way to produce them and this is the other. Reach for it when
8
+ * routing is fixed at build time, when a project has no admin panel, or in
9
+ * tests — none of which should have to stand up a global first.
10
+ *
11
+ * @param mappings The collection-to-pattern map.
12
+ */
13
+ declare const defineMappings: (mappings: PayloadCollectionMapping[]) => PayloadCollectionMappingResolved[];
14
+ //#endregion
15
+ export { defineMappings };
@@ -0,0 +1,14 @@
1
+ import { resolveCollectionMapping } from "./resolver.mjs";
2
+ //#region src/pattern/define-mappings.ts
3
+ /**
4
+ * Compiles a code-defined collection map.
5
+ *
6
+ * Every runtime function takes its mappings as plain data, so the CMS-authored
7
+ * global is one way to produce them and this is the other. Reach for it when
8
+ * routing is fixed at build time, when a project has no admin panel, or in
9
+ * tests — none of which should have to stand up a global first.
10
+ *
11
+ * @param mappings The collection-to-pattern map.
12
+ */ const defineMappings = (mappings) => mappings.map((mapping) => resolveCollectionMapping(mapping));
13
+ //#endregion
14
+ export { defineMappings };
@@ -0,0 +1,23 @@
1
+ import { LinkFieldData } from "./types.mjs";
2
+ import { LinkVariantSource } from "./define-links.mjs";
3
+ //#region src/pattern/derive-link-label.d.ts
4
+ /**
5
+ * Derives a short destination hint for a link.
6
+ *
7
+ * Payload's floating link editor builds its hover preview from a top-level
8
+ * `label` / `url`, neither of which a nested link group populates on its own,
9
+ * so without this the preview is blank for every link.
10
+ *
11
+ * The hint describes where the link points rather than what it says. Anchor
12
+ * text would look like a resolved title while telling an editor nothing about
13
+ * the destination.
14
+ *
15
+ * Lives in the pure layer because both the editor feature and the admin
16
+ * component need it, and neither may import the other.
17
+ *
18
+ * @param link The nested link group of a link node.
19
+ * @param source App-declared link types, which may describe themselves.
20
+ */
21
+ declare const deriveLinkLabel: <TExtra>(link: LinkFieldData<string, TExtra>, source?: LinkVariantSource) => string | undefined;
22
+ //#endregion
23
+ export { deriveLinkLabel };
@@ -0,0 +1,35 @@
1
+ import { variantsOf } from "./define-links.mjs";
2
+ //#region src/pattern/derive-link-label.ts
3
+ /**
4
+ * Derives a short destination hint for a link.
5
+ *
6
+ * Payload's floating link editor builds its hover preview from a top-level
7
+ * `label` / `url`, neither of which a nested link group populates on its own,
8
+ * so without this the preview is blank for every link.
9
+ *
10
+ * The hint describes where the link points rather than what it says. Anchor
11
+ * text would look like a resolved title while telling an editor nothing about
12
+ * the destination.
13
+ *
14
+ * Lives in the pure layer because both the editor feature and the admin
15
+ * component need it, and neither may import the other.
16
+ *
17
+ * @param link The nested link group of a link node.
18
+ * @param source App-declared link types, which may describe themselves.
19
+ */ const deriveLinkLabel = (link, source = {}) => {
20
+ switch (link.type) {
21
+ case "custom": return link.url ?? void 0;
22
+ case "reference": {
23
+ if (!link.reference) return;
24
+ const { relationTo, value } = link.reference;
25
+ return `${relationTo}/${typeof value === "object" ? value.id : value}`;
26
+ }
27
+ case "same-page": return link.samePageIdentifier ? `#${link.samePageIdentifier}` : void 0;
28
+ case "none":
29
+ case void 0:
30
+ case null: return;
31
+ default: return variantsOf(source).find((it) => it.value === link.type)?.value;
32
+ }
33
+ };
34
+ //#endregion
35
+ export { deriveLinkLabel };
@@ -0,0 +1,8 @@
1
+ import { BaseResolvedLink, BuiltinLinkVariant, Contributed, DEFAULT_LOCALE_KEY, DeclaredLinkVariant, FormatHref, LabelLike, LinkFieldData, PayloadCollectionMapping, PayloadCollectionMappingMatch, PayloadCollectionMappingResolved, PayloadCollectionMappingResolvers, PayloadCollectionMappingSpecificity, ResolvedLink } from "./types.mjs";
2
+ import { AnyLinkVariantDefinition, DataOfFields, LinkDataOf, LinkDeclaration, LinkVariantDefinition, LinkVariantSource, LinkVariantSpec, ResolvedLinkOf, VariantBuilder, defineLinks, variantsOf } from "./define-links.mjs";
3
+ import { defineMappings } from "./define-mappings.mjs";
4
+ import { deriveLinkLabel } from "./derive-link-label.mjs";
5
+ import { PayloadCollectionMatch, matchCollectionMappings } from "./matcher.mjs";
6
+ import { DEFAULT_IDENTIFIER_FIELD, RegisteredCollections, ResolveParamQueryPathArgs, resolveParamQueryPath } from "./param-query-path.mjs";
7
+ import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./resolver.mjs";
8
+ export { type AnyLinkVariantDefinition, type BaseResolvedLink, type BuiltinLinkVariant, type Contributed, DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, type DataOfFields, type DeclaredLinkVariant, type FormatHref, type LabelLike, type LinkDataOf, type LinkDeclaration, type LinkFieldData, type LinkVariantDefinition, type LinkVariantSource, type LinkVariantSpec, type PayloadCollectionMapping, type PayloadCollectionMappingMatch, type PayloadCollectionMappingResolved, type PayloadCollectionMappingResolvers, type PayloadCollectionMappingSpecificity, type PayloadCollectionMatch, type RegisteredCollections, type ResolveParamQueryPathArgs, type ResolvedLink, type ResolvedLinkOf, type VariantBuilder, defineLinks, defineMappings, deriveLinkLabel, isRootWildcard, matchCollectionMappings, resolveCollectionMapping, resolveParamQueryPath, resolversFor, variantsOf };
@@ -0,0 +1,8 @@
1
+ import { defineLinks, variantsOf } from "./define-links.mjs";
2
+ import { DEFAULT_LOCALE_KEY } from "./types.mjs";
3
+ import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./resolver.mjs";
4
+ import { defineMappings } from "./define-mappings.mjs";
5
+ import { deriveLinkLabel } from "./derive-link-label.mjs";
6
+ import { matchCollectionMappings } from "./matcher.mjs";
7
+ import { DEFAULT_IDENTIFIER_FIELD, resolveParamQueryPath } from "./param-query-path.mjs";
8
+ export { DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, defineLinks, defineMappings, deriveLinkLabel, isRootWildcard, matchCollectionMappings, resolveCollectionMapping, resolveParamQueryPath, resolversFor, variantsOf };
@@ -0,0 +1,26 @@
1
+ import { PayloadCollectionMappingMatch, PayloadCollectionMappingResolved } from "./types.mjs";
2
+ //#region src/pattern/matcher.d.ts
3
+ interface MatchOpts {
4
+ path: string;
5
+ locale: string;
6
+ mappings: PayloadCollectionMappingResolved[];
7
+ }
8
+ type PayloadCollectionMatch = {
9
+ mapping: PayloadCollectionMappingResolved;
10
+ } & PayloadCollectionMappingMatch;
11
+ /**
12
+ * Finds every collection a path could belong to, most specific first.
13
+ *
14
+ * More than one pattern can match the same path: `/legal/imprint` fits both
15
+ * `/:section/:slug` and a wildcard. Returning all of them lets the caller fall
16
+ * through to the next candidate when the first yields no document, instead of
17
+ * 404ing on a page that exists.
18
+ *
19
+ * Ordering is by specificity rather than by the order rows happen to sit in
20
+ * the CMS, so resolution stays deterministic.
21
+ *
22
+ * @param opts The path, locale and candidate mappings.
23
+ */
24
+ declare const matchCollectionMappings: (opts: MatchOpts) => PayloadCollectionMatch[];
25
+ //#endregion
26
+ export { PayloadCollectionMatch, matchCollectionMappings };
@@ -0,0 +1,74 @@
1
+ import { resolversFor } from "./resolver.mjs";
2
+ //#region src/pattern/matcher.ts
3
+ /**
4
+ * Orders two candidates most-specific first.
5
+ *
6
+ * Literal segments win outright, so `/journal/:slug` beats `/:section/:slug`
7
+ * for `/journal/first-post`. Failing that, a catch-all yields to a fixed-arity
8
+ * pattern. Length breaks the remaining ties, and the collection name breaks
9
+ * the last one: two collections may legally hold the same pattern, and
10
+ * without a final tiebreak the winner would be whichever row an editor
11
+ * happened to drag higher.
12
+ */ const bySpecificity = (a, b) => {
13
+ const left = a.resolvers.specificity;
14
+ const right = b.resolvers.specificity;
15
+ if (left.literalSegments !== right.literalSegments) return right.literalSegments - left.literalSegments;
16
+ if (left.hasWildcard !== right.hasWildcard) return left.hasWildcard ? 1 : -1;
17
+ if (left.totalSegments !== right.totalSegments) return right.totalSegments - left.totalSegments;
18
+ return a.mapping.collection.localeCompare(b.mapping.collection);
19
+ };
20
+ /**
21
+ * Resolves the site root. A wildcard pattern cannot match `/` — path-to-regexp
22
+ * requires at least one segment — so the collection whose pattern is a bare
23
+ * catch-all owns it, identified by an empty path.
24
+ *
25
+ * @param opts The path, locale and candidate mappings.
26
+ */ const matchRoot = (opts) => {
27
+ for (const mapping of opts.mappings) {
28
+ const resolvers = resolversFor(mapping, opts.locale);
29
+ if (!resolvers || !resolvers.specificity.hasWildcard || resolvers.specificity.literalSegments > 0 || resolvers.specificity.totalSegments !== 1) continue;
30
+ const field = resolvers.paramNames[0];
31
+ if (!field) continue;
32
+ return {
33
+ mapping,
34
+ identifier: {
35
+ field,
36
+ value: "/"
37
+ },
38
+ scope: {}
39
+ };
40
+ }
41
+ };
42
+ /**
43
+ * Finds every collection a path could belong to, most specific first.
44
+ *
45
+ * More than one pattern can match the same path: `/legal/imprint` fits both
46
+ * `/:section/:slug` and a wildcard. Returning all of them lets the caller fall
47
+ * through to the next candidate when the first yields no document, instead of
48
+ * 404ing on a page that exists.
49
+ *
50
+ * Ordering is by specificity rather than by the order rows happen to sit in
51
+ * the CMS, so resolution stays deterministic.
52
+ *
53
+ * @param opts The path, locale and candidate mappings.
54
+ */ const matchCollectionMappings = (opts) => {
55
+ if (opts.path === "/") {
56
+ const root = matchRoot(opts);
57
+ return root ? [root] : [];
58
+ }
59
+ return opts.mappings.flatMap((mapping) => {
60
+ const resolvers = resolversFor(mapping, opts.locale);
61
+ return resolvers ? [{
62
+ mapping,
63
+ resolvers
64
+ }] : [];
65
+ }).sort(bySpecificity).flatMap(({ mapping, resolvers }) => {
66
+ const matched = resolvers.match(opts.path);
67
+ return matched ? [{
68
+ mapping,
69
+ ...matched
70
+ }] : [];
71
+ });
72
+ };
73
+ //#endregion
74
+ export { matchCollectionMappings };
@@ -0,0 +1,42 @@
1
+ import { PayloadCollectionMappingResolved } from "./types.mjs";
2
+ import { SanitizedCollectionConfig } from "payload";
3
+ //#region src/pattern/param-query-path.d.ts
4
+ /** The default field a relationship parameter matches on. */
5
+ declare const DEFAULT_IDENTIFIER_FIELD = "slug";
6
+ type RegisteredCollections = Record<string, {
7
+ config: SanitizedCollectionConfig;
8
+ } | undefined>;
9
+ interface ResolveParamQueryPathArgs {
10
+ /** The collection the mapping belongs to. */
11
+ config: SanitizedCollectionConfig;
12
+ /** The parameter name taken from the path pattern. */
13
+ param: string;
14
+ /** Every registered collection, for following relationships. */
15
+ collections: RegisteredCollections;
16
+ /**
17
+ * The compiled mappings, when available. Supplying them lets a
18
+ * relationship's identifier field be derived from the target collection's
19
+ * own pattern instead of assumed.
20
+ */
21
+ mappings?: PayloadCollectionMappingResolved[];
22
+ /** The locale to derive against; patterns may differ per locale. */
23
+ locale?: string;
24
+ /** Fallback when the target's identifier cannot be derived. */
25
+ identifierField?: string;
26
+ }
27
+ /**
28
+ * Resolves a path parameter to the query path it filters on.
29
+ *
30
+ * A parameter naming a plain field filters that field directly. A parameter
31
+ * naming a relationship filters the related document's identifier, so
32
+ * `:section` becomes `section.slug` for a target keyed by `slug`.
33
+ *
34
+ * @param args The collection, parameter and resolution context.
35
+ */
36
+ declare const resolveParamQueryPath: (args: ResolveParamQueryPathArgs) => {
37
+ queryPath: string;
38
+ } | {
39
+ error: string;
40
+ };
41
+ //#endregion
42
+ export { DEFAULT_IDENTIFIER_FIELD, RegisteredCollections, ResolveParamQueryPathArgs, resolveParamQueryPath };
@@ -0,0 +1,53 @@
1
+ import { resolversFor } from "./resolver.mjs";
2
+ //#region src/pattern/param-query-path.ts
3
+ /** The default field a relationship parameter matches on. */ const DEFAULT_IDENTIFIER_FIELD = "slug";
4
+ /**
5
+ * Works out which field of a related collection a parameter matches on.
6
+ *
7
+ * The target's own pattern already names it: the last parameter of the pattern
8
+ * a collection is served at is, by definition, what identifies its documents.
9
+ * Deriving it means a project keyed by `permalink` or `handle` needs no
10
+ * configuration, and cannot drift out of sync with its own routes.
11
+ *
12
+ * A wildcard target is excluded on purpose. Its stored value carries a leading
13
+ * slash, while the value arriving from a match is a bare segment, so a query
14
+ * built from it would never match — and would fail as an empty result rather
15
+ * than as an error.
16
+ *
17
+ * @param target The related collection's slug.
18
+ * @param args The resolution arguments.
19
+ */ const deriveIdentifierField = (target, args) => {
20
+ const fallback = args.identifierField ?? "slug";
21
+ if (!args.mappings || args.locale === void 0) return fallback;
22
+ const mapping = args.mappings.find((it) => it.collection === target);
23
+ if (!mapping) return fallback;
24
+ const resolvers = resolversFor(mapping, args.locale);
25
+ if (!resolvers || resolvers.specificity.hasWildcard) return fallback;
26
+ return resolvers.paramNames.at(-1) ?? fallback;
27
+ };
28
+ /**
29
+ * Resolves a path parameter to the query path it filters on.
30
+ *
31
+ * A parameter naming a plain field filters that field directly. A parameter
32
+ * naming a relationship filters the related document's identifier, so
33
+ * `:section` becomes `section.slug` for a target keyed by `slug`.
34
+ *
35
+ * @param args The collection, parameter and resolution context.
36
+ */ const resolveParamQueryPath = (args) => {
37
+ const field = args.config.flattenedFields.find((it) => it.name === args.param);
38
+ if (!field) return { error: `Collection "${args.config.slug}" does not define a field named "${args.param}"` };
39
+ if (field.type !== "relationship") return { queryPath: args.param };
40
+ const targets = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
41
+ const identifiers = /* @__PURE__ */ new Set();
42
+ let identifier = args.identifierField ?? "slug";
43
+ for (const target of targets) {
44
+ const targetConfig = args.collections[target]?.config;
45
+ identifier = deriveIdentifierField(target, args);
46
+ if (!targetConfig?.flattenedFields.some((it) => it.name === identifier)) return { error: `Relationship "${args.param}" points at "${target}", which has no "${identifier}" field to match on` };
47
+ identifiers.add(identifier);
48
+ }
49
+ if (identifiers.size > 1) return { error: `Relationship "${args.param}" points at collections identified by different fields (${[...identifiers].sort().join(", ")}), which cannot be matched in one query` };
50
+ return { queryPath: `${args.param}.${identifier}` };
51
+ };
52
+ //#endregion
53
+ export { DEFAULT_IDENTIFIER_FIELD, resolveParamQueryPath };
@@ -0,0 +1,30 @@
1
+ import { PayloadCollectionMapping, PayloadCollectionMappingResolved, PayloadCollectionMappingResolvers } from "./types.mjs";
2
+ //#region src/pattern/resolver.d.ts
3
+ /**
4
+ * Whether a wildcard pattern was handed the site root. `compile` rejects an
5
+ * empty wildcard, so the home page — a document whose identifier is just `/` —
6
+ * has to be built as the bare root instead.
7
+ *
8
+ * @param resolvers The pattern's compiled resolvers.
9
+ * @param values The parameter values, in pattern order.
10
+ */
11
+ declare const isRootWildcard: (resolvers: PayloadCollectionMappingResolvers, values: string[]) => boolean;
12
+ /**
13
+ * Picks the resolvers for a locale, falling back to the unlocalized bucket.
14
+ *
15
+ * A project without localization has exactly one bucket under
16
+ * {@link DEFAULT_LOCALE_KEY}, so callers pass whatever locale they have and
17
+ * still get the right pattern.
18
+ *
19
+ * @param mapping The compiled mapping.
20
+ * @param locale The locale to resolve for.
21
+ */
22
+ declare const resolversFor: (mapping: PayloadCollectionMappingResolved, locale: string) => PayloadCollectionMappingResolvers | undefined;
23
+ /**
24
+ * Compiles a mapping's patterns into match and build functions.
25
+ *
26
+ * @param input The mapping as authored.
27
+ */
28
+ declare const resolveCollectionMapping: (input: PayloadCollectionMapping) => PayloadCollectionMappingResolved;
29
+ //#endregion
30
+ export { isRootWildcard, resolveCollectionMapping, resolversFor };