@abinnovision/payloadcms-wayfinder 1.0.0-beta.2 → 1.0.0-beta.3
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/README.md +22 -19
- package/dist/config/load-mappings.d.mts +17 -1
- package/dist/config/load-mappings.mjs +16 -4
- package/dist/config/mapping-global.d.mts +14 -5
- package/dist/config/mapping-global.mjs +2 -1
- package/dist/config/plugin.d.mts +6 -3
- package/dist/config/plugin.mjs +5 -2
- package/dist/index.d.mts +10 -14
- package/dist/index.mjs +4 -13
- package/dist/internal.d.mts +11 -0
- package/dist/internal.mjs +11 -0
- package/dist/lexical/index.d.mts +9 -23
- package/dist/lexical/index.mjs +11 -25
- package/dist/montage/index.d.mts +33 -11
- package/dist/montage/index.mjs +25 -11
- package/dist/pattern/define-links.d.mts +19 -7
- package/dist/pattern/define-mappings.d.mts +5 -1
- package/dist/pattern/define-mappings.mjs +3 -1
- package/dist/pattern/derive-link-label.mjs +2 -1
- package/dist/pattern/link-node.d.mts +21 -0
- package/dist/pattern/link-node.mjs +34 -0
- package/dist/pattern/param-query-path.d.mts +7 -5
- package/dist/pattern/param-query-path.mjs +6 -7
- package/dist/pattern/resolver.d.mts +3 -1
- package/dist/pattern/resolver.mjs +5 -2
- package/dist/pattern/types.d.mts +31 -4
- package/dist/pattern/types.mjs +2 -1
- package/dist/runtime/build-href.d.mts +0 -6
- package/dist/runtime/build-href.mjs +1 -2
- package/dist/runtime/build-path.mjs +9 -0
- package/dist/runtime/create-router.d.mts +72 -0
- package/dist/runtime/create-router.mjs +64 -0
- package/dist/runtime/resolve-link.d.mts +15 -10
- package/dist/runtime/resolve-link.mjs +10 -10
- package/dist/runtime/resolve-path.d.mts +22 -7
- package/dist/runtime/resolve-path.mjs +2 -4
- package/dist/runtime/resolve-relationship-slug.d.mts +0 -1
- package/dist/runtime/resolve-relationship-slug.mjs +16 -8
- package/package.json +6 -2
- package/dist/pattern/index.d.mts +0 -8
- package/dist/pattern/index.mjs +0 -8
- package/dist/runtime/index.d.mts +0 -7
- package/dist/runtime/index.mjs +0 -6
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ Editor-authored URL routing for [Payload CMS](https://payloadcms.com/).
|
|
|
6
6
|
|
|
7
7
|
Wayfinder owns one thing: the map from collections to the URL patterns their documents are served
|
|
8
8
|
at. That map is authored in the CMS, so adding a page type is an editorial act rather than a code
|
|
9
|
-
change. Everything else in the package falls out of it. `
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
`
|
|
9
|
+
change. Everything else in the package falls out of it. `createRouter` binds the mapping, the
|
|
10
|
+
locale and the href formatter once per request and hands back a router: `router.href` turns a
|
|
11
|
+
document into an href, `router.path` builds a path from parameter values alone for sitemaps and
|
|
12
|
+
feeds, `router.resolve` turns a request path back into a document for a catch-all route, and
|
|
13
|
+
`router.link` plus `linkField` give editors a link that follows its target when that collection's
|
|
13
14
|
pattern changes. [`docs/concepts.md`](./docs/concepts.md) describes the model;
|
|
14
15
|
[`docs/limitations.md`](./docs/limitations.md) states what is out of scope and why.
|
|
15
16
|
|
|
@@ -42,7 +43,7 @@ import { buildConfig } from "payload";
|
|
|
42
43
|
|
|
43
44
|
export default buildConfig({
|
|
44
45
|
// ...
|
|
45
|
-
plugins: [wayfinderPlugin({
|
|
46
|
+
plugins: [wayfinderPlugin({ checkDefaultPopulateOn: ["pages", "articles"] })],
|
|
46
47
|
});
|
|
47
48
|
```
|
|
48
49
|
|
|
@@ -63,7 +64,7 @@ Read the mapping and hand it to a catch-all route:
|
|
|
63
64
|
|
|
64
65
|
```tsx
|
|
65
66
|
// app/[[...path]]/page.tsx
|
|
66
|
-
import {
|
|
67
|
+
import { createRouter } from "@abinnovision/payloadcms-wayfinder";
|
|
67
68
|
import { loadMappings } from "@abinnovision/payloadcms-wayfinder/config";
|
|
68
69
|
import { notFound } from "next/navigation";
|
|
69
70
|
import { getPayload } from "payload";
|
|
@@ -75,11 +76,10 @@ const Page = async ({ params }: { params: Promise<{ path?: string[] }> }) => {
|
|
|
75
76
|
const mappings = await loadMappings({ payload });
|
|
76
77
|
const { path } = await params;
|
|
77
78
|
|
|
78
|
-
const
|
|
79
|
+
const wayfinder = createRouter({ mappings, locale: "en" });
|
|
80
|
+
|
|
81
|
+
const resolved = await wayfinder.resolve(`/${(path ?? []).join("/")}`, {
|
|
79
82
|
payload,
|
|
80
|
-
mappings,
|
|
81
|
-
path: `/${(path ?? []).join("/")}`,
|
|
82
|
-
locale: "en",
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
if (!resolved) {
|
|
@@ -98,17 +98,20 @@ export default Page;
|
|
|
98
98
|
## Entrypoints
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
"."
|
|
102
|
-
"./
|
|
103
|
-
"./
|
|
104
|
-
"./
|
|
105
|
-
"./
|
|
101
|
+
"." createRouter, defineMappings, defineLinks, resolveRelationshipSlug, deriveLinkLabel, types
|
|
102
|
+
"./internal" the unbound functions the router is built out of, plus the pattern internals
|
|
103
|
+
"./config" wayfinderPlugin, createMappingGlobal, loadMappings, linkField
|
|
104
|
+
"./lexical" wayfinderLinkFeature, linkLabelFeature, resolveLinkNode
|
|
105
|
+
"./admin" LinkLabelFeatureClient, mounted by linkLabelFeature through the import map
|
|
106
|
+
"./montage" initWayfinder, wayfinderFrom, wayfinderExtension
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
`.` is the runtime half. It takes mappings as plain data and never reads the CMS, so it runs in a
|
|
109
|
-
route handler, a sitemap, a script or a test alike. `./
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
route handler, a sitemap, a script or a test alike. `./internal` is the escape hatch behind it,
|
|
111
|
+
for a caller that holds no request or wants a different set of arguments per call; nothing there
|
|
112
|
+
carries a compatibility guarantee. `./config` is loaded by the CLI, by migrations and by
|
|
113
|
+
`payload generate:types`, so it must stay React-free. `./lexical`, `./admin` and `./montage` each
|
|
114
|
+
pull in an optional peer and are separate for that reason.
|
|
112
115
|
[`docs/layers.md`](./docs/layers.md) explains the split.
|
|
113
116
|
|
|
114
117
|
## Documentation
|
|
@@ -119,7 +122,7 @@ and by `payload generate:types`, so it must stay React-free. `./lexical`, `./adm
|
|
|
119
122
|
works without.
|
|
120
123
|
- [`docs/integration.md`](./docs/integration.md): plugin setup, caching, catch-all routes,
|
|
121
124
|
sitemaps and `defaultPopulate`.
|
|
122
|
-
- [`docs/linking.md`](./docs/linking.md): `linkField`, `defineLinks`, `
|
|
125
|
+
- [`docs/linking.md`](./docs/linking.md): `linkField`, `defineLinks`, `router.link` and the Lexical
|
|
123
126
|
feature.
|
|
124
127
|
- [`docs/code-defined-mappings.md`](./docs/code-defined-mappings.md): `defineMappings`, with no CMS
|
|
125
128
|
global at all.
|
|
@@ -17,8 +17,24 @@ interface MappingCache {
|
|
|
17
17
|
interface LoadMappingsArgs {
|
|
18
18
|
payload: Payload;
|
|
19
19
|
globalSlug?: string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Whether patterns are per-locale.
|
|
22
|
+
*
|
|
23
|
+
* Derived from the instance's own `localization` config, which is the same
|
|
24
|
+
* authority `wayfinderPlugin` derives it from, so the two sides cannot
|
|
25
|
+
* disagree. Set it only to override that, and then on both sides.
|
|
26
|
+
*/
|
|
21
27
|
localized?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The field a relationship parameter falls back to when the target
|
|
30
|
+
* collection's own pattern cannot name one.
|
|
31
|
+
*
|
|
32
|
+
* Read off the mapping global's own config when the plugin was given it,
|
|
33
|
+
* so a project states it once, where the global is declared, and both the
|
|
34
|
+
* save-time validation and the compiled mappings get the same answer. Set
|
|
35
|
+
* it here only to override that.
|
|
36
|
+
*/
|
|
37
|
+
fallbackIdentifierField?: string;
|
|
22
38
|
/** Reuses compiled patterns across reads. @see MappingCache */
|
|
23
39
|
cache?: MappingCache;
|
|
24
40
|
}
|
|
@@ -10,6 +10,16 @@ import "./mapping-global.mjs";
|
|
|
10
10
|
};
|
|
11
11
|
const defaultCache = createMemoryCache();
|
|
12
12
|
/**
|
|
13
|
+
* Reads what {@link createMappingGlobal} was told, off the global's own config.
|
|
14
|
+
*
|
|
15
|
+
* The write side validates patterns against this and the read side compiles
|
|
16
|
+
* mappings with it, so the two have to agree. Carrying it on the config means
|
|
17
|
+
* a project states it where it declares the global and nowhere else.
|
|
18
|
+
*/ const declaredIdentifierField = (payload, slug) => {
|
|
19
|
+
const declared = (payload.config.globals.find((it) => it.slug === slug)?.custom)?.wayfinder?.fallbackIdentifierField;
|
|
20
|
+
return typeof declared === "string" ? declared : void 0;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
13
23
|
* Reads the mapping global and compiles it.
|
|
14
24
|
*
|
|
15
25
|
* Returns an empty list rather than throwing when the global has never been
|
|
@@ -20,10 +30,12 @@ const defaultCache = createMemoryCache();
|
|
|
20
30
|
*
|
|
21
31
|
* @param args The Payload instance and mapping-global settings.
|
|
22
32
|
*/ const loadMappings = async (args) => {
|
|
23
|
-
const
|
|
33
|
+
const slug = args.globalSlug ?? "collections-mapping";
|
|
34
|
+
const localized = args.localized ?? Boolean(args.payload.config.localization);
|
|
24
35
|
const cache = args.cache ?? defaultCache;
|
|
36
|
+
const fallbackIdentifierField = args.fallbackIdentifierField ?? declaredIdentifierField(args.payload, slug);
|
|
25
37
|
const global = await args.payload.findGlobal({
|
|
26
|
-
slug
|
|
38
|
+
slug,
|
|
27
39
|
depth: 0,
|
|
28
40
|
overrideAccess: true,
|
|
29
41
|
...localized ? { locale: "all" } : {}
|
|
@@ -43,12 +55,12 @@ const defaultCache = createMemoryCache();
|
|
|
43
55
|
path: patterns
|
|
44
56
|
}] : [];
|
|
45
57
|
});
|
|
46
|
-
const key = JSON.stringify(usable);
|
|
58
|
+
const key = JSON.stringify([usable, fallbackIdentifierField]);
|
|
47
59
|
const cached = cache.get(key);
|
|
48
60
|
if (cached) return cached;
|
|
49
61
|
const compiled = usable.flatMap((mapping) => {
|
|
50
62
|
try {
|
|
51
|
-
return [resolveCollectionMapping(mapping)];
|
|
63
|
+
return [resolveCollectionMapping(mapping, fallbackIdentifierField)];
|
|
52
64
|
} catch {
|
|
53
65
|
return [];
|
|
54
66
|
}
|
|
@@ -6,13 +6,22 @@ interface CreateMappingGlobalArgs {
|
|
|
6
6
|
/** Defaults to {@link DEFAULT_MAPPING_GLOBAL_SLUG}. */
|
|
7
7
|
globalSlug?: string;
|
|
8
8
|
/**
|
|
9
|
-
* Whether path patterns differ per locale.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Whether path patterns differ per locale.
|
|
10
|
+
*
|
|
11
|
+
* Derived from the config's `localization` block by `wayfinderPlugin`, and
|
|
12
|
+
* from the running instance by `loadMappings`, so a project normally never
|
|
13
|
+
* sets it. Pass it only to override that — and then to both sides, because
|
|
14
|
+
* Payload returns a scalar for an unlocalized field and a per-locale record
|
|
15
|
+
* for a localized one, and the read has to expect the shape the write
|
|
16
|
+
* produced.
|
|
12
17
|
*/
|
|
13
18
|
localized?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The field a relationship parameter falls back to when the target
|
|
21
|
+
* collection's pattern cannot name one. Used here to validate a pattern at
|
|
22
|
+
* save time, before any mapping has been compiled to carry it.
|
|
23
|
+
*/
|
|
24
|
+
fallbackIdentifierField?: string;
|
|
16
25
|
label?: GlobalConfig["label"];
|
|
17
26
|
adminGroup?: string;
|
|
18
27
|
access?: GlobalConfig["access"];
|
|
@@ -41,7 +41,7 @@ import { text } from "payload/shared";
|
|
|
41
41
|
config: collection.config,
|
|
42
42
|
param: key.name,
|
|
43
43
|
collections: opts.req.payload.collections,
|
|
44
|
-
...args.
|
|
44
|
+
...args.fallbackIdentifierField ? { fallbackIdentifierField: args.fallbackIdentifierField } : {}
|
|
45
45
|
});
|
|
46
46
|
if ("error" in resolved) return resolved.error;
|
|
47
47
|
}
|
|
@@ -51,6 +51,7 @@ import { text } from "payload/shared";
|
|
|
51
51
|
return {
|
|
52
52
|
slug: args.globalSlug ?? "collections-mapping",
|
|
53
53
|
label: args.label ?? "Collections Mapping",
|
|
54
|
+
...args.fallbackIdentifierField ? { custom: { wayfinder: { fallbackIdentifierField: args.fallbackIdentifierField } } } : {},
|
|
54
55
|
...args.access ? { access: args.access } : {},
|
|
55
56
|
admin: {
|
|
56
57
|
group: args.adminGroup ?? "Settings",
|
package/dist/config/plugin.d.mts
CHANGED
|
@@ -3,10 +3,13 @@ import { Plugin } from "payload";
|
|
|
3
3
|
//#region src/config/plugin.d.ts
|
|
4
4
|
interface WayfinderPluginArgs extends CreateMappingGlobalArgs {
|
|
5
5
|
/**
|
|
6
|
-
* Collections
|
|
7
|
-
*
|
|
6
|
+
* Collections to check for `defaultPopulate` at boot.
|
|
7
|
+
*
|
|
8
|
+
* Named for what it does. It grants nothing: which collections can be
|
|
9
|
+
* linked to is decided by the link field's own `relationTo`, and an
|
|
10
|
+
* earlier name implied this list governed that.
|
|
8
11
|
*/
|
|
9
|
-
|
|
12
|
+
checkDefaultPopulateOn?: string[];
|
|
10
13
|
/**
|
|
11
14
|
* Set when the project resolves references through its own index rather
|
|
12
15
|
* than a populated document. Suppresses the `defaultPopulate` warning,
|
package/dist/config/plugin.mjs
CHANGED
|
@@ -13,7 +13,10 @@ import { createMappingGlobal } from "./mapping-global.mjs";
|
|
|
13
13
|
*/ const wayfinderPlugin = (args = {}) => (incoming) => {
|
|
14
14
|
const config = {
|
|
15
15
|
...incoming,
|
|
16
|
-
globals: [...incoming.globals ?? [], createMappingGlobal(
|
|
16
|
+
globals: [...incoming.globals ?? [], createMappingGlobal({
|
|
17
|
+
localized: Boolean(incoming.localization),
|
|
18
|
+
...args
|
|
19
|
+
})],
|
|
17
20
|
i18n: {
|
|
18
21
|
...incoming.i18n,
|
|
19
22
|
translations: {
|
|
@@ -24,7 +27,7 @@ import { createMappingGlobal } from "./mapping-global.mjs";
|
|
|
24
27
|
};
|
|
25
28
|
if (args.quiet) return config;
|
|
26
29
|
if (!args.resolvesReferencesExternally) {
|
|
27
|
-
const missing = (args.
|
|
30
|
+
const missing = (args.checkDefaultPopulateOn ?? []).filter((slug) => {
|
|
28
31
|
const collection = config.collections?.find((it) => it.slug === slug);
|
|
29
32
|
return collection && !collection.defaultPopulate;
|
|
30
33
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { BaseResolvedLink, BuiltinLinkVariant,
|
|
2
|
-
import {
|
|
3
|
-
import { defineMappings } from "./pattern/define-mappings.mjs";
|
|
4
|
-
import { deriveLinkLabel } from "./pattern/derive-link-label.mjs";
|
|
5
|
-
import { PayloadCollectionMatch, matchCollectionMappings } from "./pattern/matcher.mjs";
|
|
6
|
-
import { DEFAULT_IDENTIFIER_FIELD, RegisteredCollections, ResolveParamQueryPathArgs, resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
7
|
-
import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./pattern/resolver.mjs";
|
|
8
|
-
import "./pattern/index.mjs";
|
|
1
|
+
import { BaseResolvedLink, BuiltinLinkVariant, DocumentId, FormatHref, LabelLike, LinkFieldData, PayloadCollectionMapping, PayloadCollectionMappingResolved } from "./pattern/types.mjs";
|
|
2
|
+
import { LinkContextOf, LinkDataOf, LinkDeclaration, ResolvedLinkOf, defineLinks } from "./pattern/define-links.mjs";
|
|
9
3
|
import { BuildDiagnosticReason, Diagnostic, DiagnosticReason, OnDiagnostic, ResolveLinkDiagnosticReason, ResolvePathDiagnosticReason } from "./runtime/diagnostics.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { PayloadDocument,
|
|
4
|
+
import { LinkableDocument } from "./runtime/build-href.mjs";
|
|
5
|
+
import { ResolveReference } from "./runtime/resolve-link.mjs";
|
|
6
|
+
import { PayloadCollectionMatch } from "./pattern/matcher.mjs";
|
|
7
|
+
import { PayloadDocument, ResolvePathWhere, ResolvedPath } from "./runtime/resolve-path.mjs";
|
|
8
|
+
import { CreateRouterArgs, Router, createRouter } from "./runtime/create-router.mjs";
|
|
14
9
|
import { ResolveRelationshipSlugArgs, resolveRelationshipSlug } from "./runtime/resolve-relationship-slug.mjs";
|
|
15
|
-
import "./
|
|
16
|
-
|
|
10
|
+
import { defineMappings } from "./pattern/define-mappings.mjs";
|
|
11
|
+
import { deriveLinkLabel } from "./pattern/derive-link-label.mjs";
|
|
12
|
+
export { type BaseResolvedLink, type BuildDiagnosticReason, type BuiltinLinkVariant, type CreateRouterArgs, type Diagnostic, type DiagnosticReason, type DocumentId, type FormatHref, type LabelLike, type LinkContextOf, type LinkDataOf, type LinkDeclaration, type LinkFieldData, type LinkableDocument, type OnDiagnostic, type PayloadCollectionMapping, type PayloadCollectionMappingResolved, type PayloadCollectionMatch, type PayloadDocument, type ResolveLinkDiagnosticReason, type ResolvePathDiagnosticReason, type ResolvePathWhere, type ResolveReference, type ResolveRelationshipSlugArgs, type ResolvedLinkOf, type ResolvedPath, type Router, createRouter, defineLinks, defineMappings, deriveLinkLabel, resolveRelationshipSlug };
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import { defineLinks
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { defineLinks } from "./pattern/define-links.mjs";
|
|
2
|
+
import { createRouter } from "./runtime/create-router.mjs";
|
|
3
|
+
import { resolveRelationshipSlug } from "./runtime/resolve-relationship-slug.mjs";
|
|
4
4
|
import { defineMappings } from "./pattern/define-mappings.mjs";
|
|
5
5
|
import { deriveLinkLabel } from "./pattern/derive-link-label.mjs";
|
|
6
|
-
|
|
7
|
-
import { DEFAULT_IDENTIFIER_FIELD, resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
8
|
-
import "./pattern/index.mjs";
|
|
9
|
-
import { buildHref, identityFormatHref } from "./runtime/build-href.mjs";
|
|
10
|
-
import { buildPath } from "./runtime/build-path.mjs";
|
|
11
|
-
import { isAvailableLink, resolveLink } from "./runtime/resolve-link.mjs";
|
|
12
|
-
import { resolvePathToDocument } from "./runtime/resolve-path.mjs";
|
|
13
|
-
import { resolveRelationshipSlug } from "./runtime/resolve-relationship-slug.mjs";
|
|
14
|
-
import "./runtime/index.mjs";
|
|
15
|
-
export { DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, buildHref, buildPath, defineLinks, defineMappings, deriveLinkLabel, identityFormatHref, isAvailableLink, isRootWildcard, matchCollectionMappings, resolveCollectionMapping, resolveLink, resolveParamQueryPath, resolvePathToDocument, resolveRelationshipSlug, resolversFor, variantsOf };
|
|
6
|
+
export { createRouter, defineLinks, defineMappings, deriveLinkLabel, resolveRelationshipSlug };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Contributed, DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, DeclaredLinkVariant, PayloadCollectionMappingMatch, PayloadCollectionMappingResolvers, PayloadCollectionMappingSpecificity, ResolvedLink } from "./pattern/types.mjs";
|
|
2
|
+
import { AnyLinkVariantDefinition, DataOfFields, LinkVariantDefinition, LinkVariantSource, LinkVariantSpec, VariantBuilder, variantsOf } from "./pattern/define-links.mjs";
|
|
3
|
+
import { BuildHrefArgs, buildHref, identityFormatHref } from "./runtime/build-href.mjs";
|
|
4
|
+
import { ResolveLinkArgs, isAvailableLink, resolveLink } from "./runtime/resolve-link.mjs";
|
|
5
|
+
import { matchCollectionMappings } from "./pattern/matcher.mjs";
|
|
6
|
+
import { ResolvePathToDocumentArgs, resolvePathToDocument } from "./runtime/resolve-path.mjs";
|
|
7
|
+
import { BuildPathArgs, buildPath } from "./runtime/build-path.mjs";
|
|
8
|
+
import { normaliseLinkNodeFields } from "./pattern/link-node.mjs";
|
|
9
|
+
import { RegisteredCollections, ResolveParamQueryPathArgs, resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
10
|
+
import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./pattern/resolver.mjs";
|
|
11
|
+
export { type AnyLinkVariantDefinition, type BuildHrefArgs, type BuildPathArgs, type Contributed, DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, type DataOfFields, type DeclaredLinkVariant, type LinkVariantDefinition, type LinkVariantSource, type LinkVariantSpec, type PayloadCollectionMappingMatch, type PayloadCollectionMappingResolvers, type PayloadCollectionMappingSpecificity, type RegisteredCollections, type ResolveLinkArgs, type ResolveParamQueryPathArgs, type ResolvePathToDocumentArgs, type ResolvedLink, type VariantBuilder, buildHref, buildPath, identityFormatHref, isAvailableLink, isRootWildcard, matchCollectionMappings, normaliseLinkNodeFields, resolveCollectionMapping, resolveLink, resolveParamQueryPath, resolvePathToDocument, resolversFor, variantsOf };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY } from "./pattern/types.mjs";
|
|
2
|
+
import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./pattern/resolver.mjs";
|
|
3
|
+
import { buildHref, identityFormatHref } from "./runtime/build-href.mjs";
|
|
4
|
+
import { buildPath } from "./runtime/build-path.mjs";
|
|
5
|
+
import { variantsOf } from "./pattern/define-links.mjs";
|
|
6
|
+
import { isAvailableLink, resolveLink } from "./runtime/resolve-link.mjs";
|
|
7
|
+
import { matchCollectionMappings } from "./pattern/matcher.mjs";
|
|
8
|
+
import { resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
9
|
+
import { resolvePathToDocument } from "./runtime/resolve-path.mjs";
|
|
10
|
+
import { normaliseLinkNodeFields } from "./pattern/link-node.mjs";
|
|
11
|
+
export { DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, buildHref, buildPath, identityFormatHref, isAvailableLink, isRootWildcard, matchCollectionMappings, normaliseLinkNodeFields, resolveCollectionMapping, resolveLink, resolveParamQueryPath, resolvePathToDocument, resolversFor, variantsOf };
|
package/dist/lexical/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BaseResolvedLink, LinkFieldData, ResolvedLink } from "../pattern/types.mjs";
|
|
2
1
|
import { LinkDeclaration, ResolvedLinkOf } from "../pattern/define-links.mjs";
|
|
3
2
|
import { LinkFieldArgs } from "../config/link-field.mjs";
|
|
4
3
|
import { ResolveLinkArgs } from "../runtime/resolve-link.mjs";
|
|
@@ -18,35 +17,22 @@ declare const linkLabelFeature: import("@payloadcms/richtext-lexical").FeaturePr
|
|
|
18
17
|
* @param args The same arguments as the standalone link field.
|
|
19
18
|
*/
|
|
20
19
|
declare const wayfinderLinkFeature: <TDeclaration extends LinkDeclaration = LinkDeclaration>(args: LinkFieldArgs<TDeclaration>) => ReturnType<typeof LinkFeature>;
|
|
21
|
-
/**
|
|
22
|
-
* The two shapes a link node's fields arrive in.
|
|
23
|
-
*
|
|
24
|
-
* A node written by {@link wayfinderLinkFeature} nests the group under `link`.
|
|
25
|
-
* A node written by Lexical's stock link feature stores `linkType` and `doc`
|
|
26
|
-
* at the top level, which existing content will still hold.
|
|
27
|
-
*/
|
|
28
|
-
interface SerializedLinkFields {
|
|
29
|
-
link?: LinkFieldData;
|
|
30
|
-
linkType?: "custom" | "internal";
|
|
31
|
-
url?: string | null;
|
|
32
|
-
newTab?: boolean | null;
|
|
33
|
-
doc?: {
|
|
34
|
-
relationTo: string;
|
|
35
|
-
value: string | {
|
|
36
|
-
id: string;
|
|
37
|
-
};
|
|
38
|
-
} | null;
|
|
39
|
-
}
|
|
40
20
|
/**
|
|
41
21
|
* Resolves a rich-text link node to an href.
|
|
42
22
|
*
|
|
23
|
+
* Takes the node's `fields` as `unknown`, because Lexical types them as an
|
|
24
|
+
* open record and a narrower parameter would make every converter cast. The
|
|
25
|
+
* two shapes a node can hold are unwrapped by
|
|
26
|
+
* {@link normaliseLinkNodeFields}, so a link written in rich text and a link
|
|
27
|
+
* authored in a block resolve through exactly the same call.
|
|
28
|
+
*
|
|
43
29
|
* Returns null when the node points nowhere resolvable, so a converter can
|
|
44
30
|
* render the text without an anchor rather than emitting a dead one.
|
|
45
31
|
*
|
|
46
32
|
* @param args The node's fields plus the usual link-resolution arguments.
|
|
47
33
|
*/
|
|
48
|
-
declare const resolveLinkNode: <
|
|
49
|
-
fields:
|
|
50
|
-
}) =>
|
|
34
|
+
declare const resolveLinkNode: <TDeclaration extends LinkDeclaration = LinkDeclaration>(args: Omit<ResolveLinkArgs<TDeclaration>, "link"> & {
|
|
35
|
+
fields: unknown;
|
|
36
|
+
}) => ResolvedLinkOf<TDeclaration> | null;
|
|
51
37
|
//#endregion
|
|
52
38
|
export { linkLabelFeature, resolveLinkNode, wayfinderLinkFeature };
|
package/dist/lexical/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveLink } from "../runtime/resolve-link.mjs";
|
|
2
|
+
import { normaliseLinkNodeFields } from "../pattern/link-node.mjs";
|
|
2
3
|
import { linkField } from "../config/link-field.mjs";
|
|
3
4
|
import { LinkFeature, createServerFeature } from "@payloadcms/richtext-lexical";
|
|
4
5
|
//#region src/lexical/index.ts
|
|
@@ -22,36 +23,21 @@ import { LinkFeature, createServerFeature } from "@payloadcms/richtext-lexical";
|
|
|
22
23
|
* @param args The same arguments as the standalone link field.
|
|
23
24
|
*/ const wayfinderLinkFeature = (args) => LinkFeature({ fields: () => [linkField(args)] });
|
|
24
25
|
/**
|
|
25
|
-
* Normalises a link node's fields into the link field's own shape.
|
|
26
|
-
*
|
|
27
|
-
* @param fields The node's `fields` object.
|
|
28
|
-
*/ const normaliseNodeFields = (fields) => {
|
|
29
|
-
if (!fields) return;
|
|
30
|
-
if (fields.link) return fields.link;
|
|
31
|
-
if (fields.linkType === "internal" && fields.doc) return {
|
|
32
|
-
type: "reference",
|
|
33
|
-
reference: fields.doc,
|
|
34
|
-
newTab: fields.newTab ?? null
|
|
35
|
-
};
|
|
36
|
-
if (fields.url) return {
|
|
37
|
-
type: "custom",
|
|
38
|
-
url: fields.url,
|
|
39
|
-
newTab: fields.newTab ?? null
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
26
|
* Resolves a rich-text link node to an href.
|
|
44
27
|
*
|
|
28
|
+
* Takes the node's `fields` as `unknown`, because Lexical types them as an
|
|
29
|
+
* open record and a narrower parameter would make every converter cast. The
|
|
30
|
+
* two shapes a node can hold are unwrapped by
|
|
31
|
+
* {@link normaliseLinkNodeFields}, so a link written in rich text and a link
|
|
32
|
+
* authored in a block resolve through exactly the same call.
|
|
33
|
+
*
|
|
45
34
|
* Returns null when the node points nowhere resolvable, so a converter can
|
|
46
35
|
* render the text without an anchor rather than emitting a dead one.
|
|
47
36
|
*
|
|
48
37
|
* @param args The node's fields plus the usual link-resolution arguments.
|
|
49
|
-
*/ const resolveLinkNode = (args) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
link
|
|
54
|
-
});
|
|
55
|
-
};
|
|
38
|
+
*/ const resolveLinkNode = (args) => resolveLink({
|
|
39
|
+
...args,
|
|
40
|
+
link: normaliseLinkNodeFields(args.fields)
|
|
41
|
+
});
|
|
56
42
|
//#endregion
|
|
57
43
|
export { linkLabelFeature, resolveLinkNode, wayfinderLinkFeature };
|
package/dist/montage/index.d.mts
CHANGED
|
@@ -1,32 +1,54 @@
|
|
|
1
1
|
import { PayloadCollectionMappingResolved } from "../pattern/types.mjs";
|
|
2
|
+
import { AnyLinkVariantDefinition, LinkDeclaration } from "../pattern/define-links.mjs";
|
|
2
3
|
import { LoadMappingsArgs } from "../config/load-mappings.mjs";
|
|
4
|
+
import { CreateRouterArgs, Router } from "../runtime/create-router.mjs";
|
|
3
5
|
import { MontageSlots } from "@abinnovision/payloadcms-montage";
|
|
4
6
|
//#region src/montage/index.d.ts
|
|
5
7
|
/**
|
|
6
|
-
* The slot the
|
|
8
|
+
* The slot the bound router rides in.
|
|
7
9
|
*
|
|
8
10
|
* Prefixed with the package name because extension names share one namespace
|
|
9
11
|
* across every library using montage.
|
|
10
12
|
*/
|
|
11
|
-
declare const wayfinderExtension: import("@abinnovision/payloadcms-montage").ContextExtension<
|
|
13
|
+
declare const wayfinderExtension: import("@abinnovision/payloadcms-montage").ContextExtension<Router<LinkDeclaration<Record<string, AnyLinkVariantDefinition>>>>;
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
15
|
+
* What `initWayfinder` needs: a router's settings, plus where the mappings
|
|
16
|
+
* come from.
|
|
17
|
+
*
|
|
18
|
+
* Either hand over mappings already in hand, or the instance to read them
|
|
19
|
+
* from. A route that resolved a path has them already, and reading the global
|
|
20
|
+
* a second time to render the same page is a query for nothing.
|
|
21
|
+
*/
|
|
22
|
+
type InitWayfinderArgs<TDeclaration extends LinkDeclaration = LinkDeclaration> = Omit<CreateRouterArgs<TDeclaration>, "mappings"> & ({
|
|
23
|
+
mappings: PayloadCollectionMappingResolved[];
|
|
24
|
+
} | {
|
|
25
|
+
load: Omit<LoadMappingsArgs, "localized"> & {
|
|
26
|
+
localized?: boolean;
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Builds the router once and parks it on the render context.
|
|
14
31
|
*
|
|
15
32
|
* Blocks resolve links individually and there may be dozens on a page, so the
|
|
16
|
-
* read
|
|
33
|
+
* mappings are read once per request rather than once per link — and the
|
|
34
|
+
* locale and href formatter travel with them, which is what stops a block
|
|
35
|
+
* resolving a link into the wrong locale or out of preview.
|
|
17
36
|
*
|
|
18
37
|
* @param ctx The render context.
|
|
19
|
-
* @param args The
|
|
38
|
+
* @param args The router's settings, and the mappings or where to read them.
|
|
20
39
|
*/
|
|
21
|
-
declare const initWayfinder: (ctx: MontageSlots, args:
|
|
40
|
+
declare const initWayfinder: <TDeclaration extends LinkDeclaration = LinkDeclaration>(ctx: MontageSlots, args: InitWayfinderArgs<TDeclaration>) => Promise<void>;
|
|
22
41
|
/**
|
|
23
|
-
* Reads the
|
|
42
|
+
* Reads the router off the render context.
|
|
24
43
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
44
|
+
* Named for what it does — the router is already built, and this is a slot
|
|
45
|
+
* read rather than a load. Throws when {@link initWayfinder} has not run,
|
|
46
|
+
* because a block rendering links without a locale would silently produce the
|
|
47
|
+
* wrong ones, and a blank page is easier to explain than a page of quietly
|
|
48
|
+
* mislocalised links.
|
|
27
49
|
*
|
|
28
50
|
* @param ctx The render context.
|
|
29
51
|
*/
|
|
30
|
-
declare const
|
|
52
|
+
declare const wayfinderFrom: <TDeclaration extends LinkDeclaration = LinkDeclaration>(ctx: MontageSlots) => Router<TDeclaration>;
|
|
31
53
|
//#endregion
|
|
32
|
-
export {
|
|
54
|
+
export { InitWayfinderArgs, initWayfinder, wayfinderExtension, wayfinderFrom };
|
package/dist/montage/index.mjs
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
|
+
import { createRouter } from "../runtime/create-router.mjs";
|
|
1
2
|
import { loadMappings } from "../config/load-mappings.mjs";
|
|
2
3
|
import { createContextExtension } from "@abinnovision/payloadcms-montage";
|
|
3
4
|
//#region src/montage/index.ts
|
|
4
5
|
/**
|
|
5
|
-
* The slot the
|
|
6
|
+
* The slot the bound router rides in.
|
|
6
7
|
*
|
|
7
8
|
* Prefixed with the package name because extension names share one namespace
|
|
8
9
|
* across every library using montage.
|
|
9
|
-
*/ const wayfinderExtension = createContextExtension("wayfinder:
|
|
10
|
+
*/ const wayfinderExtension = createContextExtension("wayfinder:router");
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Builds the router once and parks it on the render context.
|
|
12
13
|
*
|
|
13
14
|
* Blocks resolve links individually and there may be dozens on a page, so the
|
|
14
|
-
* read
|
|
15
|
+
* mappings are read once per request rather than once per link — and the
|
|
16
|
+
* locale and href formatter travel with them, which is what stops a block
|
|
17
|
+
* resolving a link into the wrong locale or out of preview.
|
|
15
18
|
*
|
|
16
19
|
* @param ctx The render context.
|
|
17
|
-
* @param args The
|
|
20
|
+
* @param args The router's settings, and the mappings or where to read them.
|
|
18
21
|
*/ const initWayfinder = async (ctx, args) => {
|
|
19
|
-
|
|
22
|
+
const mappings = "mappings" in args ? args.mappings : await loadMappings(args.load);
|
|
23
|
+
wayfinderExtension.set(ctx, createRouter({
|
|
24
|
+
...args,
|
|
25
|
+
mappings
|
|
26
|
+
}));
|
|
20
27
|
};
|
|
21
28
|
/**
|
|
22
|
-
* Reads the
|
|
29
|
+
* Reads the router off the render context.
|
|
23
30
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* Named for what it does — the router is already built, and this is a slot
|
|
32
|
+
* read rather than a load. Throws when {@link initWayfinder} has not run,
|
|
33
|
+
* because a block rendering links without a locale would silently produce the
|
|
34
|
+
* wrong ones, and a blank page is easier to explain than a page of quietly
|
|
35
|
+
* mislocalised links.
|
|
26
36
|
*
|
|
27
37
|
* @param ctx The render context.
|
|
28
|
-
*/ const
|
|
38
|
+
*/ const wayfinderFrom = (ctx) => {
|
|
39
|
+
const router = wayfinderExtension.get(ctx);
|
|
40
|
+
if (!router) throw new Error("[wayfinder] No router on the render context. Call `initWayfinder(ctx, ...)` in the route before rendering blocks.");
|
|
41
|
+
return router;
|
|
42
|
+
};
|
|
29
43
|
//#endregion
|
|
30
|
-
export {
|
|
44
|
+
export { initWayfinder, wayfinderExtension, wayfinderFrom };
|
|
@@ -49,8 +49,16 @@ type FieldData<F1> = F1 extends {
|
|
|
49
49
|
} ? { [K in N]?: number | null; } : F1 extends {
|
|
50
50
|
type: "checkbox";
|
|
51
51
|
} ? { [K in N]?: boolean | null; } : { [K in N]?: unknown; } : object;
|
|
52
|
-
/**
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Everything a variant's own fields contribute, as one object type.
|
|
54
|
+
*
|
|
55
|
+
* A variant declared without `fields`, or one whose fields were not captured
|
|
56
|
+
* as a literal tuple, arrives here as the bare `readonly Field[]`. Deriving
|
|
57
|
+
* from the whole `Field` union would intersect every field type into index
|
|
58
|
+
* signatures that reject ordinary link data, so such a variant contributes
|
|
59
|
+
* nothing rather than something wrong.
|
|
60
|
+
*/
|
|
61
|
+
type DataOfFields<TFields extends readonly unknown[]> = number extends TFields["length"] ? object : UnionToIntersection<FieldData<TFields[number]>>;
|
|
54
62
|
/** A variant that has had no resolver attached. */
|
|
55
63
|
interface LinkVariantSpec<TFields extends readonly Field[]> {
|
|
56
64
|
label: LabelLike;
|
|
@@ -93,17 +101,21 @@ interface LinkDeclaration<TVariants1 extends Record<string, AnyLinkVariantDefini
|
|
|
93
101
|
variants: TVariants1;
|
|
94
102
|
}
|
|
95
103
|
/** The stored shape of a link field built from a declaration. */
|
|
104
|
+
type FieldsData<V> = V extends {
|
|
105
|
+
fields?: infer F extends readonly unknown[];
|
|
106
|
+
} ? DataOfFields<F> : object;
|
|
96
107
|
/**
|
|
97
108
|
* What one variant contributes.
|
|
98
109
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
110
|
+
* `__data` is read first, so a variant that named its own shape with
|
|
111
|
+
* `.data<T>()` keeps it. Every object type structurally satisfies an optional
|
|
112
|
+
* property, so its absence shows up as `unknown` and falls through to the
|
|
113
|
+
* fields — which is what lets a variant with no resolver be written as a plain
|
|
114
|
+
* object literal and still be typed.
|
|
101
115
|
*/
|
|
102
116
|
type DataOfVariant<V> = V extends {
|
|
103
117
|
__data?: infer D;
|
|
104
|
-
} ? unknown extends D ?
|
|
105
|
-
fields?: infer F extends readonly unknown[];
|
|
106
|
-
} ? DataOfFields<F> : object;
|
|
118
|
+
} ? unknown extends D ? FieldsData<V> : D : FieldsData<V>;
|
|
107
119
|
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
120
|
/** What resolving a link built from a declaration can produce. */
|
|
109
121
|
type ResolvedLinkOf<T> = T extends LinkDeclaration<infer TVariants> ? BaseResolvedLink & Contributed<UnionToIntersection<{ [K in keyof TVariants]: TVariants[K] extends {
|
|
@@ -9,7 +9,11 @@ import { PayloadCollectionMapping, PayloadCollectionMappingResolved } from "./ty
|
|
|
9
9
|
* tests — none of which should have to stand up a global first.
|
|
10
10
|
*
|
|
11
11
|
* @param mappings The collection-to-pattern map.
|
|
12
|
+
* @param options Where a relationship parameter falls back to when the target
|
|
13
|
+
* collection's own pattern cannot name an identifier.
|
|
12
14
|
*/
|
|
13
|
-
declare const defineMappings: (mappings: PayloadCollectionMapping[]
|
|
15
|
+
declare const defineMappings: (mappings: PayloadCollectionMapping[], options?: {
|
|
16
|
+
fallbackIdentifierField?: string;
|
|
17
|
+
}) => PayloadCollectionMappingResolved[];
|
|
14
18
|
//#endregion
|
|
15
19
|
export { defineMappings };
|
|
@@ -9,6 +9,8 @@ import { resolveCollectionMapping } from "./resolver.mjs";
|
|
|
9
9
|
* tests — none of which should have to stand up a global first.
|
|
10
10
|
*
|
|
11
11
|
* @param mappings The collection-to-pattern map.
|
|
12
|
-
|
|
12
|
+
* @param options Where a relationship parameter falls back to when the target
|
|
13
|
+
* collection's own pattern cannot name an identifier.
|
|
14
|
+
*/ const defineMappings = (mappings, options = {}) => mappings.map((mapping) => resolveCollectionMapping(mapping, options.fallbackIdentifierField));
|
|
13
15
|
//#endregion
|
|
14
16
|
export { defineMappings };
|