@valkyrianlabs/payload-markdown-docs 0.5.3 → 0.7.1
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 +98 -6
- package/dist/next/PayloadMarkdownDocsNavbar.d.ts +28 -0
- package/dist/next/PayloadMarkdownDocsNavbar.js +107 -0
- package/dist/next/PayloadMarkdownDocsNavbar.js.map +1 -0
- package/dist/next/index.d.ts +4 -2
- package/dist/next/index.js +2 -1
- package/dist/next/index.js.map +1 -1
- package/dist/next/links.d.ts +54 -1
- package/dist/next/links.js +236 -4
- package/dist/next/links.js.map +1 -1
- package/dist/security/githubOidc.js +2 -1
- package/dist/security/githubOidc.js.map +1 -1
- package/dist/skills/codex/SKILL.md +9 -0
- package/dist/skills/codex/reference/formatting.md +24 -0
- package/dist/skills/codex/reference/frontmatter.md +2 -0
- package/dist/skills/codex/reference/routing.md +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -73,6 +73,10 @@ to a matching docs set from the configured branch.
|
|
|
73
73
|
|
|
74
74
|
## Render In Next
|
|
75
75
|
|
|
76
|
+
The plugin does not mutate your Pages collection and does not register public
|
|
77
|
+
frontend routes. Add route handlers in your Next app where you want docs to
|
|
78
|
+
render.
|
|
79
|
+
|
|
76
80
|
```tsx
|
|
77
81
|
import config from '@payload-config'
|
|
78
82
|
import {
|
|
@@ -82,11 +86,7 @@ import {
|
|
|
82
86
|
import { notFound } from 'next/navigation'
|
|
83
87
|
import { getPayload } from 'payload'
|
|
84
88
|
|
|
85
|
-
export default async function Page({
|
|
86
|
-
params,
|
|
87
|
-
}: {
|
|
88
|
-
params: Promise<{ slug?: string[] }>
|
|
89
|
-
}) {
|
|
89
|
+
export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
|
|
90
90
|
const { slug } = await params
|
|
91
91
|
const payload = await getPayload({ config })
|
|
92
92
|
const resolved = await resolvePayloadMarkdownDocsRoute({ payload, slug })
|
|
@@ -99,7 +99,54 @@ export default async function Page({
|
|
|
99
99
|
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
For
|
|
102
|
+
For docs navigation, use the drop-in navbar when you want the plugin to own the
|
|
103
|
+
docs menu UI:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { PayloadMarkdownDocsNavbar } from '@valkyrianlabs/payload-markdown-docs/next'
|
|
107
|
+
import type { Payload } from 'payload'
|
|
108
|
+
|
|
109
|
+
export async function HeaderDocsNav({ payload }: { payload: Payload }) {
|
|
110
|
+
return (
|
|
111
|
+
<PayloadMarkdownDocsNavbar currentPath="/plugins/payload-markdown-docs" payload={payload} />
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The navbar reads docs groups and docs sets, renders nested docs navigation, and
|
|
117
|
+
accepts `classNames` and `renderLink` overrides for app-specific Tailwind,
|
|
118
|
+
routing, and analytics.
|
|
119
|
+
|
|
120
|
+
If you already have a site header, use the Header adapter to append top-level
|
|
121
|
+
docs groups and top-level ungrouped docs sets without exceeding your existing
|
|
122
|
+
menu cap:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { appendPayloadMarkdownDocsHeaderNavItems } from '@valkyrianlabs/payload-markdown-docs/next'
|
|
126
|
+
|
|
127
|
+
const navItems = await appendPayloadMarkdownDocsHeaderNavItems({
|
|
128
|
+
existingItems: header.navItems ?? [],
|
|
129
|
+
maxItems: headerNavItemsMaxRows,
|
|
130
|
+
payload,
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The adapter defaults to custom URL links so it does not require CMSLink changes.
|
|
135
|
+
Use `mode: 'relationship'` only when your renderer understands `docs-groups`
|
|
136
|
+
and `docs-sets` relationships.
|
|
137
|
+
|
|
138
|
+
For fully custom navigation, use the headless nav builder:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { getPayloadMarkdownDocsNavItems } from '@valkyrianlabs/payload-markdown-docs/next'
|
|
142
|
+
|
|
143
|
+
const docsNav = await getPayloadMarkdownDocsNavItems({
|
|
144
|
+
availableSlots: 4,
|
|
145
|
+
payload,
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For simple flat header links, use the compatibility link helper:
|
|
103
150
|
|
|
104
151
|
```ts
|
|
105
152
|
import { getPayloadMarkdownDocsLinks } from '@valkyrianlabs/payload-markdown-docs/next'
|
|
@@ -108,6 +155,37 @@ const docsLinks = await getPayloadMarkdownDocsLinks({ payload })
|
|
|
108
155
|
// [{ label: 'Payload Markdown Docs', url: '/plugins/payload-markdown-docs' }]
|
|
109
156
|
```
|
|
110
157
|
|
|
158
|
+
## Serve Raw Markdown
|
|
159
|
+
|
|
160
|
+
The AI-facing raw Markdown export is a route-handler response, not a generated
|
|
161
|
+
Payload Page. Add a `route.ts` at the exported path, usually the value from
|
|
162
|
+
`docs/index.ai.yml`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// app/(frontend)/plugins/payload-markdown-docs.md/route.ts
|
|
166
|
+
import config from '@payload-config'
|
|
167
|
+
import { createPayloadMarkdownDocsMarkdownResponse } from '@valkyrianlabs/payload-markdown-docs/next'
|
|
168
|
+
import { notFound } from 'next/navigation'
|
|
169
|
+
import { getPayload } from 'payload'
|
|
170
|
+
|
|
171
|
+
export async function GET() {
|
|
172
|
+
const payload = await getPayload({ config })
|
|
173
|
+
const response = await createPayloadMarkdownDocsMarkdownResponse({
|
|
174
|
+
payload,
|
|
175
|
+
path: '/plugins/payload-markdown-docs.md',
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (response) {
|
|
179
|
+
return response
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
notFound()
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The response is `text/markdown; charset=utf-8` and is assembled from synced
|
|
187
|
+
docs records using `docs/index.ai.yml` when present.
|
|
188
|
+
|
|
111
189
|
## Validate Locally
|
|
112
190
|
|
|
113
191
|
In an app or docs repository that has installed this package:
|
|
@@ -127,6 +205,19 @@ pnpm cli validate ./docs --source payload-markdown-docs
|
|
|
127
205
|
In GitHub Actions, `--source` can be omitted when the docs set slug matches the
|
|
128
206
|
repository name. The CLI infers it from `GITHUB_REPOSITORY`.
|
|
129
207
|
|
|
208
|
+
## Maintain Docs With Codex
|
|
209
|
+
|
|
210
|
+
In a docs set target application, install the local Codex skill so agents have
|
|
211
|
+
repo-local guidance for maintaining Markdown docs, frontmatter, `index.ai.yml`,
|
|
212
|
+
validation, and sync safety rules.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pnpm exec payload-markdown-docs install skill --codex
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The installer writes `.agents/skills/payload-markdown-docs/`. It does not sync
|
|
219
|
+
docs, call Payload, or publish content.
|
|
220
|
+
|
|
130
221
|
## Publish From GitHub Actions
|
|
131
222
|
|
|
132
223
|
```yaml
|
|
@@ -186,5 +277,6 @@ empty list rejects all workflow publishing for that docs set.
|
|
|
186
277
|
- [Quick Start](docs/getting-started/quick-start.md)
|
|
187
278
|
- [Plugin Config](docs/configuration/plugin-config.md)
|
|
188
279
|
- [GitHub Actions](docs/workflow/ci-github-actions.md)
|
|
280
|
+
- [Docs Navbar](docs/frontend/navbar.md)
|
|
189
281
|
- [CLI](docs/reference/cli.md)
|
|
190
282
|
- [Migration Notes](docs/reference/migration.md)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsNavItem } from './links.js';
|
|
3
|
+
export type PayloadMarkdownDocsNavbarClassNames = {
|
|
4
|
+
activeLink?: string;
|
|
5
|
+
childrenList?: string;
|
|
6
|
+
item?: string;
|
|
7
|
+
link?: string;
|
|
8
|
+
list?: string;
|
|
9
|
+
panel?: string;
|
|
10
|
+
root?: string;
|
|
11
|
+
trigger?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PayloadMarkdownDocsNavbarRenderLinkOptions = {
|
|
14
|
+
active: boolean;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
className: string;
|
|
17
|
+
current: boolean;
|
|
18
|
+
href: string;
|
|
19
|
+
item: PayloadMarkdownDocsNavItem;
|
|
20
|
+
};
|
|
21
|
+
export type PayloadMarkdownDocsNavbarProps = {
|
|
22
|
+
ariaLabel?: string;
|
|
23
|
+
classNames?: PayloadMarkdownDocsNavbarClassNames;
|
|
24
|
+
currentPath?: string;
|
|
25
|
+
items?: PayloadMarkdownDocsNavItem[];
|
|
26
|
+
renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode;
|
|
27
|
+
} & Partial<GetPayloadMarkdownDocsNavItemsOptions>;
|
|
28
|
+
export declare const PayloadMarkdownDocsNavbar: ({ ariaLabel, classNames, currentPath, items, renderLink, ...options }: PayloadMarkdownDocsNavbarProps) => Promise<import("react/jsx-runtime").JSX.Element | null>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isRouteDescendant, normalizeRoutePath } from '../routing/index.js';
|
|
3
|
+
import { getPayloadMarkdownDocsNavItems } from './links.js';
|
|
4
|
+
const cx = (...values)=>values.filter(Boolean).join(' ');
|
|
5
|
+
const defaultClassNames = {
|
|
6
|
+
activeLink: 'bg-cyan-400/10 text-cyan-200',
|
|
7
|
+
childrenList: 'space-y-1',
|
|
8
|
+
item: 'relative list-none',
|
|
9
|
+
link: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/70 transition-colors hover:bg-white/[0.04] hover:text-foreground',
|
|
10
|
+
list: 'm-0 flex list-none items-center gap-1 p-0',
|
|
11
|
+
panel: 'absolute left-0 top-full z-50 hidden min-w-60 rounded-xl border border-border bg-background p-2 shadow-xl group-hover:block group-focus-within:block',
|
|
12
|
+
root: 'relative',
|
|
13
|
+
trigger: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/60 transition-colors group-hover:bg-white/[0.04] group-hover:text-foreground'
|
|
14
|
+
};
|
|
15
|
+
const isItemActive = ({ currentPath, item })=>{
|
|
16
|
+
if (!currentPath) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (item.url) {
|
|
20
|
+
const normalizedUrl = normalizeRoutePath(item.url);
|
|
21
|
+
if (currentPath === normalizedUrl || isRouteDescendant(normalizedUrl, currentPath)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return (item.children ?? []).some((child)=>isItemActive({
|
|
26
|
+
currentPath,
|
|
27
|
+
item: child
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
30
|
+
const isItemCurrent = ({ currentPath, item })=>Boolean(currentPath && item.url && currentPath === normalizeRoutePath(item.url));
|
|
31
|
+
const renderDefaultLink = ({ children, className, current, href })=>/*#__PURE__*/ _jsx("a", {
|
|
32
|
+
"aria-current": current ? 'page' : undefined,
|
|
33
|
+
className: className,
|
|
34
|
+
href: href,
|
|
35
|
+
children: children
|
|
36
|
+
});
|
|
37
|
+
const renderNavItems = ({ classNames, currentPath, depth = 0, items, renderLink = renderDefaultLink })=>{
|
|
38
|
+
if (items.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return /*#__PURE__*/ _jsx("ul", {
|
|
42
|
+
className: cx(depth === 0 ? classNames.list : classNames.childrenList),
|
|
43
|
+
children: items.map((item)=>{
|
|
44
|
+
const active = isItemActive({
|
|
45
|
+
currentPath,
|
|
46
|
+
item
|
|
47
|
+
});
|
|
48
|
+
const current = isItemCurrent({
|
|
49
|
+
currentPath,
|
|
50
|
+
item
|
|
51
|
+
});
|
|
52
|
+
const linkClassName = cx(classNames.link, active && classNames.activeLink);
|
|
53
|
+
const children = item.children?.length ? renderNavItems({
|
|
54
|
+
classNames,
|
|
55
|
+
currentPath,
|
|
56
|
+
depth: depth + 1,
|
|
57
|
+
items: item.children,
|
|
58
|
+
renderLink
|
|
59
|
+
}) : null;
|
|
60
|
+
return /*#__PURE__*/ _jsxs("li", {
|
|
61
|
+
className: cx('group', classNames.item),
|
|
62
|
+
children: [
|
|
63
|
+
item.url ? renderLink({
|
|
64
|
+
active,
|
|
65
|
+
children: item.label,
|
|
66
|
+
className: linkClassName,
|
|
67
|
+
current,
|
|
68
|
+
href: item.url,
|
|
69
|
+
item
|
|
70
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
71
|
+
className: cx(classNames.trigger, active && classNames.activeLink),
|
|
72
|
+
children: item.label
|
|
73
|
+
}),
|
|
74
|
+
children ? /*#__PURE__*/ _jsx("div", {
|
|
75
|
+
className: classNames.panel,
|
|
76
|
+
children: children
|
|
77
|
+
}) : null
|
|
78
|
+
]
|
|
79
|
+
}, `${item.collection}:${item.id}`);
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
export const PayloadMarkdownDocsNavbar = async ({ ariaLabel = 'Docs navigation', classNames, currentPath, items, renderLink, ...options })=>{
|
|
84
|
+
const navItems = items ?? (options.payload ? await getPayloadMarkdownDocsNavItems({
|
|
85
|
+
...options,
|
|
86
|
+
payload: options.payload
|
|
87
|
+
}) : []);
|
|
88
|
+
if (navItems.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const mergedClassNames = {
|
|
92
|
+
...defaultClassNames,
|
|
93
|
+
...classNames
|
|
94
|
+
};
|
|
95
|
+
return /*#__PURE__*/ _jsx("nav", {
|
|
96
|
+
"aria-label": ariaLabel,
|
|
97
|
+
className: mergedClassNames.root,
|
|
98
|
+
children: renderNavItems({
|
|
99
|
+
classNames: mergedClassNames,
|
|
100
|
+
currentPath: currentPath ? normalizeRoutePath(currentPath) : undefined,
|
|
101
|
+
items: navItems,
|
|
102
|
+
renderLink
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=PayloadMarkdownDocsNavbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/next/PayloadMarkdownDocsNavbar.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\n\nimport type { GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsNavItem } from './links.js'\n\nimport { isRouteDescendant, normalizeRoutePath } from '../routing/index.js'\nimport { getPayloadMarkdownDocsNavItems } from './links.js'\n\nexport type PayloadMarkdownDocsNavbarClassNames = {\n activeLink?: string\n childrenList?: string\n item?: string\n link?: string\n list?: string\n panel?: string\n root?: string\n trigger?: string\n}\n\nexport type PayloadMarkdownDocsNavbarRenderLinkOptions = {\n active: boolean\n children: ReactNode\n className: string\n current: boolean\n href: string\n item: PayloadMarkdownDocsNavItem\n}\n\nexport type PayloadMarkdownDocsNavbarProps = {\n ariaLabel?: string\n classNames?: PayloadMarkdownDocsNavbarClassNames\n currentPath?: string\n items?: PayloadMarkdownDocsNavItem[]\n renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode\n} & Partial<GetPayloadMarkdownDocsNavItemsOptions>\n\nconst cx = (...values: (false | null | string | undefined)[]): string =>\n values.filter(Boolean).join(' ')\n\nconst defaultClassNames = {\n activeLink: 'bg-cyan-400/10 text-cyan-200',\n childrenList: 'space-y-1',\n item: 'relative list-none',\n link: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/70 transition-colors hover:bg-white/[0.04] hover:text-foreground',\n list: 'm-0 flex list-none items-center gap-1 p-0',\n panel:\n 'absolute left-0 top-full z-50 hidden min-w-60 rounded-xl border border-border bg-background p-2 shadow-xl group-hover:block group-focus-within:block',\n root: 'relative',\n trigger:\n 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/60 transition-colors group-hover:bg-white/[0.04] group-hover:text-foreground',\n} satisfies Required<PayloadMarkdownDocsNavbarClassNames>\n\nconst isItemActive = ({\n currentPath,\n item,\n}: {\n currentPath?: string\n item: PayloadMarkdownDocsNavItem\n}): boolean => {\n if (!currentPath) {\n return false\n }\n\n if (item.url) {\n const normalizedUrl = normalizeRoutePath(item.url)\n\n if (currentPath === normalizedUrl || isRouteDescendant(normalizedUrl, currentPath)) {\n return true\n }\n }\n\n return (item.children ?? []).some((child) =>\n isItemActive({\n currentPath,\n item: child,\n }),\n )\n}\n\nconst isItemCurrent = ({\n currentPath,\n item,\n}: {\n currentPath?: string\n item: PayloadMarkdownDocsNavItem\n}): boolean => Boolean(currentPath && item.url && currentPath === normalizeRoutePath(item.url))\n\nconst renderDefaultLink = ({\n children,\n className,\n current,\n href,\n}: PayloadMarkdownDocsNavbarRenderLinkOptions) => (\n <a aria-current={current ? 'page' : undefined} className={className} href={href}>\n {children}\n </a>\n)\n\nconst renderNavItems = ({\n classNames,\n currentPath,\n depth = 0,\n items,\n renderLink = renderDefaultLink,\n}: {\n classNames: PayloadMarkdownDocsNavbarClassNames\n currentPath?: string\n depth?: number\n items: PayloadMarkdownDocsNavItem[]\n renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode\n}): ReactNode => {\n if (items.length === 0) {\n return null\n }\n\n return (\n <ul className={cx(depth === 0 ? classNames.list : classNames.childrenList)}>\n {items.map((item) => {\n const active = isItemActive({\n currentPath,\n item,\n })\n const current = isItemCurrent({\n currentPath,\n item,\n })\n const linkClassName = cx(classNames.link, active && classNames.activeLink)\n const children = item.children?.length\n ? renderNavItems({\n classNames,\n currentPath,\n depth: depth + 1,\n items: item.children,\n renderLink,\n })\n : null\n\n return (\n <li className={cx('group', classNames.item)} key={`${item.collection}:${item.id}`}>\n {item.url ? (\n renderLink({\n active,\n children: item.label,\n className: linkClassName,\n current,\n href: item.url,\n item,\n })\n ) : (\n <span className={cx(classNames.trigger, active && classNames.activeLink)}>\n {item.label}\n </span>\n )}\n {children ? <div className={classNames.panel}>{children}</div> : null}\n </li>\n )\n })}\n </ul>\n )\n}\n\nexport const PayloadMarkdownDocsNavbar = async ({\n ariaLabel = 'Docs navigation',\n classNames,\n currentPath,\n items,\n renderLink,\n ...options\n}: PayloadMarkdownDocsNavbarProps) => {\n const navItems =\n items ??\n (options.payload\n ? await getPayloadMarkdownDocsNavItems({\n ...options,\n payload: options.payload,\n })\n : [])\n\n if (navItems.length === 0) {\n return null\n }\n\n const mergedClassNames = {\n ...defaultClassNames,\n ...classNames,\n }\n\n return (\n <nav aria-label={ariaLabel} className={mergedClassNames.root}>\n {renderNavItems({\n classNames: mergedClassNames,\n currentPath: currentPath ? normalizeRoutePath(currentPath) : undefined,\n items: navItems,\n renderLink,\n })}\n </nav>\n )\n}\n"],"names":["isRouteDescendant","normalizeRoutePath","getPayloadMarkdownDocsNavItems","cx","values","filter","Boolean","join","defaultClassNames","activeLink","childrenList","item","link","list","panel","root","trigger","isItemActive","currentPath","url","normalizedUrl","children","some","child","isItemCurrent","renderDefaultLink","className","current","href","a","aria-current","undefined","renderNavItems","classNames","depth","items","renderLink","length","ul","map","active","linkClassName","li","label","span","div","collection","id","PayloadMarkdownDocsNavbar","ariaLabel","options","navItems","payload","mergedClassNames","nav","aria-label"],"mappings":"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"}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export { getPayloadMarkdownDocsLinks } from './links.js';
|
|
2
|
-
export type { GetPayloadMarkdownDocsLinksOptions, PayloadMarkdownDocsLink } from './links.js';
|
|
1
|
+
export { appendPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsLinks, getPayloadMarkdownDocsNavItems, } from './links.js';
|
|
2
|
+
export type { AppendPayloadMarkdownDocsHeaderNavItemsOptions, GetPayloadMarkdownDocsHeaderNavItemsOptions, GetPayloadMarkdownDocsLinksOptions, GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsHeaderNavItem, PayloadMarkdownDocsHeaderNavLink, PayloadMarkdownDocsLink, PayloadMarkdownDocsNavCapacityOptions, PayloadMarkdownDocsNavItem, PayloadMarkdownDocsNavItemType, } from './links.js';
|
|
3
3
|
export { createPayloadMarkdownDocsMarkdownResponse, resolvePayloadMarkdownDocsMarkdownRoute, } from './markdown.js';
|
|
4
4
|
export type { ResolvedPayloadMarkdownDocsMarkdownRoute, ResolvePayloadMarkdownDocsMarkdownRouteOptions, } from './markdown.js';
|
|
5
5
|
export { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js';
|
|
6
|
+
export { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js';
|
|
7
|
+
export type { PayloadMarkdownDocsNavbarClassNames, PayloadMarkdownDocsNavbarProps, PayloadMarkdownDocsNavbarRenderLinkOptions, } from './PayloadMarkdownDocsNavbar.js';
|
|
6
8
|
export { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js';
|
|
7
9
|
export type { PayloadMarkdownDocsPageProps } from './PayloadMarkdownDocsPage.js';
|
|
8
10
|
export { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js';
|
package/dist/next/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { getPayloadMarkdownDocsLinks } from './links.js';
|
|
1
|
+
export { appendPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsLinks, getPayloadMarkdownDocsNavItems } from './links.js';
|
|
2
2
|
export { createPayloadMarkdownDocsMarkdownResponse, resolvePayloadMarkdownDocsMarkdownRoute } from './markdown.js';
|
|
3
3
|
export { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js';
|
|
4
|
+
export { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js';
|
|
4
5
|
export { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js';
|
|
5
6
|
export { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js';
|
|
6
7
|
export { buildPayloadMarkdownDocsSidebar, getPayloadMarkdownDocsSidebar } from './sidebar.js';
|
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["export {
|
|
1
|
+
{"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["export {\n appendPayloadMarkdownDocsHeaderNavItems,\n getPayloadMarkdownDocsHeaderNavItems,\n getPayloadMarkdownDocsLinks,\n getPayloadMarkdownDocsNavItems,\n} from './links.js'\nexport type {\n AppendPayloadMarkdownDocsHeaderNavItemsOptions,\n GetPayloadMarkdownDocsHeaderNavItemsOptions,\n GetPayloadMarkdownDocsLinksOptions,\n GetPayloadMarkdownDocsNavItemsOptions,\n PayloadMarkdownDocsHeaderNavItem,\n PayloadMarkdownDocsHeaderNavLink,\n PayloadMarkdownDocsLink,\n PayloadMarkdownDocsNavCapacityOptions,\n PayloadMarkdownDocsNavItem,\n PayloadMarkdownDocsNavItemType,\n} from './links.js'\nexport {\n createPayloadMarkdownDocsMarkdownResponse,\n resolvePayloadMarkdownDocsMarkdownRoute,\n} from './markdown.js'\nexport type {\n ResolvedPayloadMarkdownDocsMarkdownRoute,\n ResolvePayloadMarkdownDocsMarkdownRouteOptions,\n} from './markdown.js'\nexport { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js'\nexport { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js'\nexport type {\n PayloadMarkdownDocsNavbarClassNames,\n PayloadMarkdownDocsNavbarProps,\n PayloadMarkdownDocsNavbarRenderLinkOptions,\n} from './PayloadMarkdownDocsNavbar.js'\nexport { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js'\nexport type { PayloadMarkdownDocsPageProps } from './PayloadMarkdownDocsPage.js'\nexport { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js'\nexport { buildPayloadMarkdownDocsSidebar, getPayloadMarkdownDocsSidebar } from './sidebar.js'\nexport type {\n BuildPayloadMarkdownDocsSidebarOptions,\n GetPayloadMarkdownDocsSidebarOptions,\n} from './sidebar.js'\nexport type {\n PayloadMarkdownDocsCollectionSlugs,\n PayloadMarkdownDocsDefaults,\n PayloadMarkdownDocsFindArgs,\n PayloadMarkdownDocsHeroImage,\n PayloadMarkdownDocsMetadata,\n PayloadMarkdownDocsOverrides,\n PayloadMarkdownDocsReadPayload,\n PayloadMarkdownDocsSidebarItem,\n ResolvedPayloadMarkdownDocsGroup,\n ResolvedPayloadMarkdownDocsRecord,\n ResolvedPayloadMarkdownDocsRoute,\n ResolvedPayloadMarkdownDocsSet,\n ResolvePayloadMarkdownDocsRouteOptions,\n} from './types.js'\n"],"names":["appendPayloadMarkdownDocsHeaderNavItems","getPayloadMarkdownDocsHeaderNavItems","getPayloadMarkdownDocsLinks","getPayloadMarkdownDocsNavItems","createPayloadMarkdownDocsMarkdownResponse","resolvePayloadMarkdownDocsMarkdownRoute","generatePayloadMarkdownDocsMetadata","getPayloadMarkdownDocsMetadata","PayloadMarkdownDocsNavbar","PayloadMarkdownDocsPage","getPayloadMarkdownDocsRoutePath","resolvePayloadMarkdownDocsRoute","buildPayloadMarkdownDocsSidebar","getPayloadMarkdownDocsSidebar"],"mappings":"AAAA,SACEA,uCAAuC,EACvCC,oCAAoC,EACpCC,2BAA2B,EAC3BC,8BAA8B,QACzB,aAAY;AAanB,SACEC,yCAAyC,EACzCC,uCAAuC,QAClC,gBAAe;AAKtB,SAASC,mCAAmC,EAAEC,8BAA8B,QAAQ,gBAAe;AACnG,SAASC,yBAAyB,QAAQ,iCAAgC;AAM1E,SAASC,uBAAuB,QAAQ,+BAA8B;AAEtE,SAASC,+BAA+B,EAAEC,+BAA+B,QAAQ,aAAY;AAC7F,SAASC,+BAA+B,EAAEC,6BAA6B,QAAQ,eAAc"}
|
package/dist/next/links.d.ts
CHANGED
|
@@ -3,9 +3,62 @@ export type PayloadMarkdownDocsLink = {
|
|
|
3
3
|
label: string;
|
|
4
4
|
url: string;
|
|
5
5
|
};
|
|
6
|
+
export type PayloadMarkdownDocsNavItemType = 'docsGroup' | 'docsSet';
|
|
7
|
+
export type PayloadMarkdownDocsNavItem = {
|
|
8
|
+
children?: PayloadMarkdownDocsNavItem[];
|
|
9
|
+
collection: string;
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
order: number;
|
|
13
|
+
route: string;
|
|
14
|
+
type: PayloadMarkdownDocsNavItemType;
|
|
15
|
+
url?: string;
|
|
16
|
+
};
|
|
17
|
+
export type PayloadMarkdownDocsNavCapacityOptions = {
|
|
18
|
+
availableSlots?: number;
|
|
19
|
+
existingItemsCount?: number;
|
|
20
|
+
maxItems?: number;
|
|
21
|
+
};
|
|
22
|
+
export type GetPayloadMarkdownDocsNavItemsOptions = {
|
|
23
|
+
collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>;
|
|
24
|
+
fetchLimit?: number;
|
|
25
|
+
includeDrafts?: boolean;
|
|
26
|
+
overrideAccess?: boolean;
|
|
27
|
+
payload: PayloadMarkdownDocsReadPayload;
|
|
28
|
+
} & PayloadMarkdownDocsNavCapacityOptions;
|
|
6
29
|
export type GetPayloadMarkdownDocsLinksOptions = {
|
|
7
30
|
collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>;
|
|
31
|
+
includeDrafts?: boolean;
|
|
8
32
|
overrideAccess?: boolean;
|
|
9
33
|
payload: PayloadMarkdownDocsReadPayload;
|
|
10
34
|
};
|
|
11
|
-
export
|
|
35
|
+
export type PayloadMarkdownDocsHeaderNavLink = {
|
|
36
|
+
label: string;
|
|
37
|
+
newTab?: false;
|
|
38
|
+
reference: {
|
|
39
|
+
relationTo: string;
|
|
40
|
+
value: string;
|
|
41
|
+
};
|
|
42
|
+
type: 'reference';
|
|
43
|
+
} | {
|
|
44
|
+
label: string;
|
|
45
|
+
newTab?: false;
|
|
46
|
+
type: 'custom';
|
|
47
|
+
url: string;
|
|
48
|
+
};
|
|
49
|
+
export type PayloadMarkdownDocsHeaderNavItem = {
|
|
50
|
+
childItems?: PayloadMarkdownDocsHeaderNavItem[];
|
|
51
|
+
link: PayloadMarkdownDocsHeaderNavLink;
|
|
52
|
+
subItems?: PayloadMarkdownDocsHeaderNavItem[];
|
|
53
|
+
};
|
|
54
|
+
export type GetPayloadMarkdownDocsHeaderNavItemsOptions = {
|
|
55
|
+
existingItems?: unknown[];
|
|
56
|
+
mode?: 'relationship' | 'url';
|
|
57
|
+
} & GetPayloadMarkdownDocsNavItemsOptions;
|
|
58
|
+
export type AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem> = {
|
|
59
|
+
existingItems: TExistingItem[];
|
|
60
|
+
} & GetPayloadMarkdownDocsHeaderNavItemsOptions;
|
|
61
|
+
export declare const getPayloadMarkdownDocsNavItems: ({ collections, fetchLimit, includeDrafts, overrideAccess, payload, ...capacityOptions }: GetPayloadMarkdownDocsNavItemsOptions) => Promise<PayloadMarkdownDocsNavItem[]>;
|
|
62
|
+
export declare const getPayloadMarkdownDocsLinks: ({ collections, includeDrafts, overrideAccess, payload, }: GetPayloadMarkdownDocsLinksOptions) => Promise<PayloadMarkdownDocsLink[]>;
|
|
63
|
+
export declare const getPayloadMarkdownDocsHeaderNavItems: ({ existingItems, existingItemsCount, mode, ...options }: GetPayloadMarkdownDocsHeaderNavItemsOptions) => Promise<PayloadMarkdownDocsHeaderNavItem[]>;
|
|
64
|
+
export declare const appendPayloadMarkdownDocsHeaderNavItems: <TExistingItem>({ existingItems, ...options }: AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem>) => Promise<(PayloadMarkdownDocsHeaderNavItem | TExistingItem)[]>;
|
package/dist/next/links.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { DEFAULT_DOCS_GROUPS_COLLECTION_SLUG, DEFAULT_DOCS_SETS_COLLECTION_SLUG } from '../constants.js';
|
|
2
2
|
import { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js';
|
|
3
|
-
import { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsSet } from './records.js';
|
|
3
|
+
import { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsGroup, toResolvedDocsSet } from './records.js';
|
|
4
|
+
const getAvailableSlots = ({ availableSlots, existingItemsCount = 0, maxItems })=>{
|
|
5
|
+
if (availableSlots !== undefined) {
|
|
6
|
+
return Math.max(0, availableSlots);
|
|
7
|
+
}
|
|
8
|
+
if (maxItems === undefined) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return Math.max(0, maxItems - existingItemsCount);
|
|
12
|
+
};
|
|
13
|
+
const applyTopLevelCapacity = (items, options)=>{
|
|
14
|
+
const availableSlots = getAvailableSlots(options);
|
|
15
|
+
return availableSlots === undefined ? items : items.slice(0, availableSlots);
|
|
16
|
+
};
|
|
4
17
|
const getGroupRoutePath = ({ groupId, groupsById, seen = new Set() })=>{
|
|
5
18
|
if (!groupId || seen.has(groupId)) {
|
|
6
19
|
return undefined;
|
|
@@ -18,14 +31,172 @@ const getGroupRoutePath = ({ groupId, groupsById, seen = new Set() })=>{
|
|
|
18
31
|
])
|
|
19
32
|
}), group.slug);
|
|
20
33
|
};
|
|
21
|
-
|
|
34
|
+
const sortByOrderThenLabel = (items)=>[
|
|
35
|
+
...items
|
|
36
|
+
].sort((first, second)=>{
|
|
37
|
+
if (first.order !== second.order) {
|
|
38
|
+
return first.order - second.order;
|
|
39
|
+
}
|
|
40
|
+
return first.label.localeCompare(second.label);
|
|
41
|
+
});
|
|
42
|
+
const getFirstLinkableUrl = (item)=>{
|
|
43
|
+
if (item.url) {
|
|
44
|
+
return item.url;
|
|
45
|
+
}
|
|
46
|
+
for (const child of item.children ?? []){
|
|
47
|
+
const url = getFirstLinkableUrl(child);
|
|
48
|
+
if (url) {
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
export const getPayloadMarkdownDocsNavItems = async ({ collections, fetchLimit = 1000, includeDrafts = false, overrideAccess = true, payload, ...capacityOptions })=>{
|
|
55
|
+
const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG;
|
|
56
|
+
const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG;
|
|
57
|
+
const [docsSetsResult, docsGroupsResult] = await Promise.all([
|
|
58
|
+
payload.find({
|
|
59
|
+
collection: docsSetsCollectionSlug,
|
|
60
|
+
depth: 0,
|
|
61
|
+
draft: includeDrafts,
|
|
62
|
+
limit: fetchLimit,
|
|
63
|
+
overrideAccess
|
|
64
|
+
}),
|
|
65
|
+
payload.find({
|
|
66
|
+
collection: docsGroupsCollectionSlug,
|
|
67
|
+
depth: 0,
|
|
68
|
+
limit: fetchLimit,
|
|
69
|
+
overrideAccess
|
|
70
|
+
})
|
|
71
|
+
]);
|
|
72
|
+
const groupsById = new Map(docsGroupsResult.docs.flatMap((group)=>{
|
|
73
|
+
if (!isRecord(group)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const id = getRelationshipId(group);
|
|
77
|
+
return id ? [
|
|
78
|
+
[
|
|
79
|
+
id,
|
|
80
|
+
group
|
|
81
|
+
]
|
|
82
|
+
] : [];
|
|
83
|
+
}));
|
|
84
|
+
const childGroupIdsByParentId = new Map();
|
|
85
|
+
const topLevelGroupIds = [];
|
|
86
|
+
for (const [groupId, group] of groupsById){
|
|
87
|
+
const parentId = getRelationshipId(group.parent);
|
|
88
|
+
if (parentId) {
|
|
89
|
+
childGroupIdsByParentId.set(parentId, [
|
|
90
|
+
...childGroupIdsByParentId.get(parentId) ?? [],
|
|
91
|
+
groupId
|
|
92
|
+
]);
|
|
93
|
+
} else {
|
|
94
|
+
topLevelGroupIds.push(groupId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const docsSetItemsByGroupId = new Map();
|
|
98
|
+
const topLevelDocsSetItems = [];
|
|
99
|
+
for (const doc of docsSetsResult.docs){
|
|
100
|
+
const docsSet = toResolvedDocsSet(doc);
|
|
101
|
+
if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({
|
|
102
|
+
docsSet,
|
|
103
|
+
includeDrafts
|
|
104
|
+
})) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const groupId = getRelationshipId(doc.group);
|
|
108
|
+
const groupRoutePath = groupId ? getGroupRoutePath({
|
|
109
|
+
groupId,
|
|
110
|
+
groupsById
|
|
111
|
+
}) : undefined;
|
|
112
|
+
if (groupId && !groupRoutePath) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const item = {
|
|
116
|
+
id: docsSet.id,
|
|
117
|
+
type: 'docsSet',
|
|
118
|
+
collection: docsSetsCollectionSlug,
|
|
119
|
+
label: docsSet.navTitle ?? docsSet.title,
|
|
120
|
+
order: docsSet.order,
|
|
121
|
+
route: deriveDocsSetRouteBase({
|
|
122
|
+
docsSetSlug: docsSet.slug,
|
|
123
|
+
groupRoutePath
|
|
124
|
+
}),
|
|
125
|
+
url: deriveDocsSetRouteBase({
|
|
126
|
+
docsSetSlug: docsSet.slug,
|
|
127
|
+
groupRoutePath
|
|
128
|
+
})
|
|
129
|
+
};
|
|
130
|
+
if (groupId) {
|
|
131
|
+
docsSetItemsByGroupId.set(groupId, [
|
|
132
|
+
...docsSetItemsByGroupId.get(groupId) ?? [],
|
|
133
|
+
item
|
|
134
|
+
]);
|
|
135
|
+
} else {
|
|
136
|
+
topLevelDocsSetItems.push(item);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const buildGroupItem = (groupId, seen = new Set())=>{
|
|
140
|
+
if (seen.has(groupId)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const doc = groupsById.get(groupId);
|
|
144
|
+
const group = toResolvedDocsGroup(doc);
|
|
145
|
+
const routePath = getGroupRoutePath({
|
|
146
|
+
groupId,
|
|
147
|
+
groupsById
|
|
148
|
+
});
|
|
149
|
+
if (!group || !routePath) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const nextSeen = new Set([
|
|
153
|
+
groupId,
|
|
154
|
+
...seen
|
|
155
|
+
]);
|
|
156
|
+
const childGroups = (childGroupIdsByParentId.get(groupId) ?? []).flatMap((childGroupId)=>{
|
|
157
|
+
const item = buildGroupItem(childGroupId, nextSeen);
|
|
158
|
+
return item ? [
|
|
159
|
+
item
|
|
160
|
+
] : [];
|
|
161
|
+
});
|
|
162
|
+
const childDocsSets = docsSetItemsByGroupId.get(groupId) ?? [];
|
|
163
|
+
const children = sortByOrderThenLabel([
|
|
164
|
+
...childGroups,
|
|
165
|
+
...childDocsSets
|
|
166
|
+
]);
|
|
167
|
+
return {
|
|
168
|
+
...children.length > 0 ? {
|
|
169
|
+
children
|
|
170
|
+
} : {},
|
|
171
|
+
id: group.id,
|
|
172
|
+
type: 'docsGroup',
|
|
173
|
+
collection: docsGroupsCollectionSlug,
|
|
174
|
+
label: group.navTitle ?? group.title,
|
|
175
|
+
order: group.order,
|
|
176
|
+
route: routePath,
|
|
177
|
+
...group.serveIndex ? {
|
|
178
|
+
url: routePath
|
|
179
|
+
} : {}
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
return applyTopLevelCapacity(sortByOrderThenLabel([
|
|
183
|
+
...topLevelGroupIds.flatMap((groupId)=>{
|
|
184
|
+
const item = buildGroupItem(groupId);
|
|
185
|
+
return item ? [
|
|
186
|
+
item
|
|
187
|
+
] : [];
|
|
188
|
+
}),
|
|
189
|
+
...topLevelDocsSetItems
|
|
190
|
+
]), capacityOptions);
|
|
191
|
+
};
|
|
192
|
+
export const getPayloadMarkdownDocsLinks = async ({ collections, includeDrafts = false, overrideAccess = true, payload })=>{
|
|
22
193
|
const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG;
|
|
23
194
|
const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG;
|
|
24
195
|
const [docsSetsResult, docsGroupsResult] = await Promise.all([
|
|
25
196
|
payload.find({
|
|
26
197
|
collection: docsSetsCollectionSlug,
|
|
27
198
|
depth: 0,
|
|
28
|
-
draft:
|
|
199
|
+
draft: includeDrafts,
|
|
29
200
|
limit: 1000,
|
|
30
201
|
overrideAccess
|
|
31
202
|
}),
|
|
@@ -51,7 +222,8 @@ export const getPayloadMarkdownDocsLinks = async ({ collections, overrideAccess
|
|
|
51
222
|
return docsSetsResult.docs.flatMap((doc)=>{
|
|
52
223
|
const docsSet = toResolvedDocsSet(doc);
|
|
53
224
|
if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({
|
|
54
|
-
docsSet
|
|
225
|
+
docsSet,
|
|
226
|
+
includeDrafts
|
|
55
227
|
})) {
|
|
56
228
|
return [];
|
|
57
229
|
}
|
|
@@ -78,5 +250,65 @@ export const getPayloadMarkdownDocsLinks = async ({ collections, overrideAccess
|
|
|
78
250
|
url
|
|
79
251
|
}));
|
|
80
252
|
};
|
|
253
|
+
const toHeaderLink = ({ item, mode })=>{
|
|
254
|
+
if (mode === 'relationship') {
|
|
255
|
+
return {
|
|
256
|
+
type: 'reference',
|
|
257
|
+
label: item.label,
|
|
258
|
+
reference: {
|
|
259
|
+
relationTo: item.collection,
|
|
260
|
+
value: item.id
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const url = getFirstLinkableUrl(item);
|
|
265
|
+
return url ? {
|
|
266
|
+
type: 'custom',
|
|
267
|
+
label: item.label,
|
|
268
|
+
url
|
|
269
|
+
} : undefined;
|
|
270
|
+
};
|
|
271
|
+
const toHeaderNavItem = (item, mode, depth = 0)=>{
|
|
272
|
+
const link = toHeaderLink({
|
|
273
|
+
item,
|
|
274
|
+
mode
|
|
275
|
+
});
|
|
276
|
+
if (!link) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const children = (item.children ?? []).flatMap((child)=>{
|
|
280
|
+
const childItem = toHeaderNavItem(child, mode, depth + 1);
|
|
281
|
+
return childItem ? [
|
|
282
|
+
childItem
|
|
283
|
+
] : [];
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
link,
|
|
287
|
+
...children.length > 0 ? depth === 0 ? {
|
|
288
|
+
subItems: children
|
|
289
|
+
} : {
|
|
290
|
+
childItems: children
|
|
291
|
+
} : {}
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
export const getPayloadMarkdownDocsHeaderNavItems = async ({ existingItems, existingItemsCount = existingItems?.length ?? 0, mode = 'url', ...options })=>{
|
|
295
|
+
const items = await getPayloadMarkdownDocsNavItems({
|
|
296
|
+
...options,
|
|
297
|
+
existingItemsCount
|
|
298
|
+
});
|
|
299
|
+
return items.flatMap((item)=>{
|
|
300
|
+
const headerItem = toHeaderNavItem(item, mode);
|
|
301
|
+
return headerItem ? [
|
|
302
|
+
headerItem
|
|
303
|
+
] : [];
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
export const appendPayloadMarkdownDocsHeaderNavItems = async ({ existingItems, ...options })=>[
|
|
307
|
+
...existingItems,
|
|
308
|
+
...await getPayloadMarkdownDocsHeaderNavItems({
|
|
309
|
+
...options,
|
|
310
|
+
existingItems
|
|
311
|
+
})
|
|
312
|
+
];
|
|
81
313
|
|
|
82
314
|
//# sourceMappingURL=links.js.map
|
package/dist/next/links.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/links.ts"],"sourcesContent":["import type { PayloadMarkdownDocsCollectionSlugs, PayloadMarkdownDocsReadPayload } from './types.js'\n\nimport {\n DEFAULT_DOCS_GROUPS_COLLECTION_SLUG,\n DEFAULT_DOCS_SETS_COLLECTION_SLUG,\n} from '../constants.js'\nimport { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js'\nimport { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsSet } from './records.js'\n\nexport type PayloadMarkdownDocsLink = {\n label: string\n url: string\n}\n\nexport type GetPayloadMarkdownDocsLinksOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n}\n\nconst getGroupRoutePath = ({\n groupId,\n groupsById,\n seen = new Set<string>(),\n}: {\n groupId?: string\n groupsById: Map<string, unknown>\n seen?: Set<string>\n}): string | undefined => {\n if (!groupId || seen.has(groupId)) {\n return undefined\n }\n\n const group = groupsById.get(groupId)\n\n if (!isRecord(group) || typeof group.slug !== 'string') {\n return undefined\n }\n\n return joinRouteSegments(\n getGroupRoutePath({\n groupId: getRelationshipId(group.parent),\n groupsById,\n seen: new Set([groupId, ...seen]),\n }),\n group.slug,\n )\n}\n\nexport const getPayloadMarkdownDocsLinks = async ({\n collections,\n overrideAccess = true,\n payload,\n}: GetPayloadMarkdownDocsLinksOptions): Promise<PayloadMarkdownDocsLink[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: false,\n limit: 1000,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: 1000,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n\n return docsSetsResult.docs\n .flatMap((doc) => {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet })) {\n return []\n }\n\n return [\n {\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath: getGroupRoutePath({\n groupId: getRelationshipId(doc.group),\n groupsById,\n }),\n }),\n },\n ]\n })\n .sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n .map(({ label, url }) => ({\n label,\n url,\n }))\n}\n"],"names":["DEFAULT_DOCS_GROUPS_COLLECTION_SLUG","DEFAULT_DOCS_SETS_COLLECTION_SLUG","deriveDocsSetRouteBase","joinRouteSegments","getRelationshipId","isRecord","isVisibleDocsSet","toResolvedDocsSet","getGroupRoutePath","groupId","groupsById","seen","Set","has","undefined","group","get","slug","parent","getPayloadMarkdownDocsLinks","collections","overrideAccess","payload","docsGroupsCollectionSlug","docsGroups","docsSetsCollectionSlug","docsSets","docsSetsResult","docsGroupsResult","Promise","all","find","collection","depth","draft","limit","Map","docs","flatMap","id","doc","docsSet","label","navTitle","title","order","url","docsSetSlug","groupRoutePath","sort","first","second","localeCompare","map"],"mappings":"AAEA,SACEA,mCAAmC,EACnCC,iCAAiC,QAC5B,kBAAiB;AACxB,SAASC,sBAAsB,EAAEC,iBAAiB,QAAQ,sBAAqB;AAC/E,SAASC,iBAAiB,EAAEC,QAAQ,EAAEC,gBAAgB,EAAEC,iBAAiB,QAAQ,eAAc;AAa/F,MAAMC,oBAAoB,CAAC,EACzBC,OAAO,EACPC,UAAU,EACVC,OAAO,IAAIC,KAAa,EAKzB;IACC,IAAI,CAACH,WAAWE,KAAKE,GAAG,CAACJ,UAAU;QACjC,OAAOK;IACT;IAEA,MAAMC,QAAQL,WAAWM,GAAG,CAACP;IAE7B,IAAI,CAACJ,SAASU,UAAU,OAAOA,MAAME,IAAI,KAAK,UAAU;QACtD,OAAOH;IACT;IAEA,OAAOX,kBACLK,kBAAkB;QAChBC,SAASL,kBAAkBW,MAAMG,MAAM;QACvCR;QACAC,MAAM,IAAIC,IAAI;YAACH;eAAYE;SAAK;IAClC,IACAI,MAAME,IAAI;AAEd;AAEA,OAAO,MAAME,8BAA8B,OAAO,EAChDC,WAAW,EACXC,iBAAiB,IAAI,EACrBC,OAAO,EAC4B;IACnC,MAAMC,2BAA2BH,aAAaI,cAAcxB;IAC5D,MAAMyB,yBAAyBL,aAAaM,YAAYzB;IACxD,MAAM,CAAC0B,gBAAgBC,iBAAiB,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC3DR,QAAQS,IAAI,CAAC;YACXC,YAAYP;YACZQ,OAAO;YACPC,OAAO;YACPC,OAAO;YACPd;QACF;QACAC,QAAQS,IAAI,CAAC;YACXC,YAAYT;YACZU,OAAO;YACPE,OAAO;YACPd;QACF;KACD;IACD,MAAMX,aAAa,IAAI0B,IACrBR,iBAAiBS,IAAI,CAACC,OAAO,CAAC,CAACvB;QAC7B,IAAI,CAACV,SAASU,QAAQ;YACpB,OAAO,EAAE;QACX;QAEA,MAAMwB,KAAKnC,kBAAkBW;QAE7B,OAAOwB,KAAK;YAAC;gBAACA;gBAAIxB;aAAM;SAAC,GAAG,EAAE;IAChC;IAGF,OAAOY,eAAeU,IAAI,CACvBC,OAAO,CAAC,CAACE;QACR,MAAMC,UAAUlC,kBAAkBiC;QAElC,IAAI,CAACC,SAASxB,QAAQ,CAACZ,SAASmC,QAAQ,CAAClC,iBAAiB;YAAEmC;QAAQ,IAAI;YACtE,OAAO,EAAE;QACX;QAEA,OAAO;YACL;gBACEC,OAAOD,QAAQE,QAAQ,IAAIF,QAAQG,KAAK;gBACxCC,OAAOJ,QAAQI,KAAK;gBACpBC,KAAK5C,uBAAuB;oBAC1B6C,aAAaN,QAAQxB,IAAI;oBACzB+B,gBAAgBxC,kBAAkB;wBAChCC,SAASL,kBAAkBoC,IAAIzB,KAAK;wBACpCL;oBACF;gBACF;YACF;SACD;IACH,GACCuC,IAAI,CAAC,CAACC,OAAOC;QACZ,IAAID,MAAML,KAAK,KAAKM,OAAON,KAAK,EAAE;YAChC,OAAOK,MAAML,KAAK,GAAGM,OAAON,KAAK;QACnC;QAEA,OAAOK,MAAMR,KAAK,CAACU,aAAa,CAACD,OAAOT,KAAK;IAC/C,GACCW,GAAG,CAAC,CAAC,EAAEX,KAAK,EAAEI,GAAG,EAAE,GAAM,CAAA;YACxBJ;YACAI;QACF,CAAA;AACJ,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../src/next/links.ts"],"sourcesContent":["import type { PayloadMarkdownDocsCollectionSlugs, PayloadMarkdownDocsReadPayload } from './types.js'\n\nimport {\n DEFAULT_DOCS_GROUPS_COLLECTION_SLUG,\n DEFAULT_DOCS_SETS_COLLECTION_SLUG,\n} from '../constants.js'\nimport { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js'\nimport {\n getRelationshipId,\n isRecord,\n isVisibleDocsSet,\n toResolvedDocsGroup,\n toResolvedDocsSet,\n} from './records.js'\n\nexport type PayloadMarkdownDocsLink = {\n label: string\n url: string\n}\n\nexport type PayloadMarkdownDocsNavItemType = 'docsGroup' | 'docsSet'\n\nexport type PayloadMarkdownDocsNavItem = {\n children?: PayloadMarkdownDocsNavItem[]\n collection: string\n id: string\n label: string\n order: number\n route: string\n type: PayloadMarkdownDocsNavItemType\n url?: string\n}\n\nexport type PayloadMarkdownDocsNavCapacityOptions = {\n availableSlots?: number\n existingItemsCount?: number\n maxItems?: number\n}\n\nexport type GetPayloadMarkdownDocsNavItemsOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n fetchLimit?: number\n includeDrafts?: boolean\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n} & PayloadMarkdownDocsNavCapacityOptions\n\nexport type GetPayloadMarkdownDocsLinksOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n includeDrafts?: boolean\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n}\n\nexport type PayloadMarkdownDocsHeaderNavLink =\n | {\n label: string\n newTab?: false\n reference: {\n relationTo: string\n value: string\n }\n type: 'reference'\n }\n | {\n label: string\n newTab?: false\n type: 'custom'\n url: string\n }\n\nexport type PayloadMarkdownDocsHeaderNavItem = {\n childItems?: PayloadMarkdownDocsHeaderNavItem[]\n link: PayloadMarkdownDocsHeaderNavLink\n subItems?: PayloadMarkdownDocsHeaderNavItem[]\n}\n\nexport type GetPayloadMarkdownDocsHeaderNavItemsOptions = {\n existingItems?: unknown[]\n mode?: 'relationship' | 'url'\n} & GetPayloadMarkdownDocsNavItemsOptions\n\nexport type AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem> = {\n existingItems: TExistingItem[]\n} & GetPayloadMarkdownDocsHeaderNavItemsOptions\n\nconst getAvailableSlots = ({\n availableSlots,\n existingItemsCount = 0,\n maxItems,\n}: PayloadMarkdownDocsNavCapacityOptions): number | undefined => {\n if (availableSlots !== undefined) {\n return Math.max(0, availableSlots)\n }\n\n if (maxItems === undefined) {\n return undefined\n }\n\n return Math.max(0, maxItems - existingItemsCount)\n}\n\nconst applyTopLevelCapacity = (\n items: PayloadMarkdownDocsNavItem[],\n options: PayloadMarkdownDocsNavCapacityOptions,\n): PayloadMarkdownDocsNavItem[] => {\n const availableSlots = getAvailableSlots(options)\n\n return availableSlots === undefined ? items : items.slice(0, availableSlots)\n}\n\nconst getGroupRoutePath = ({\n groupId,\n groupsById,\n seen = new Set<string>(),\n}: {\n groupId?: string\n groupsById: Map<string, unknown>\n seen?: Set<string>\n}): string | undefined => {\n if (!groupId || seen.has(groupId)) {\n return undefined\n }\n\n const group = groupsById.get(groupId)\n\n if (!isRecord(group) || typeof group.slug !== 'string') {\n return undefined\n }\n\n return joinRouteSegments(\n getGroupRoutePath({\n groupId: getRelationshipId(group.parent),\n groupsById,\n seen: new Set([groupId, ...seen]),\n }),\n group.slug,\n )\n}\n\nconst sortByOrderThenLabel = <T extends { label: string; order: number }>(items: T[]): T[] =>\n [...items].sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n\nconst getFirstLinkableUrl = (item: PayloadMarkdownDocsNavItem): string | undefined => {\n if (item.url) {\n return item.url\n }\n\n for (const child of item.children ?? []) {\n const url = getFirstLinkableUrl(child)\n\n if (url) {\n return url\n }\n }\n\n return undefined\n}\n\nexport const getPayloadMarkdownDocsNavItems = async ({\n collections,\n fetchLimit = 1000,\n includeDrafts = false,\n overrideAccess = true,\n payload,\n ...capacityOptions\n}: GetPayloadMarkdownDocsNavItemsOptions): Promise<PayloadMarkdownDocsNavItem[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: includeDrafts,\n limit: fetchLimit,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: fetchLimit,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n const childGroupIdsByParentId = new Map<string, string[]>()\n const topLevelGroupIds: string[] = []\n\n for (const [groupId, group] of groupsById) {\n const parentId = getRelationshipId(group.parent)\n\n if (parentId) {\n childGroupIdsByParentId.set(parentId, [\n ...(childGroupIdsByParentId.get(parentId) ?? []),\n groupId,\n ])\n } else {\n topLevelGroupIds.push(groupId)\n }\n }\n\n const docsSetItemsByGroupId = new Map<string, PayloadMarkdownDocsNavItem[]>()\n const topLevelDocsSetItems: PayloadMarkdownDocsNavItem[] = []\n\n for (const doc of docsSetsResult.docs) {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet, includeDrafts })) {\n continue\n }\n\n const groupId = getRelationshipId(doc.group)\n const groupRoutePath = groupId\n ? getGroupRoutePath({\n groupId,\n groupsById,\n })\n : undefined\n\n if (groupId && !groupRoutePath) {\n continue\n }\n\n const item: PayloadMarkdownDocsNavItem = {\n id: docsSet.id,\n type: 'docsSet',\n collection: docsSetsCollectionSlug,\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n route: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath,\n }),\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath,\n }),\n }\n\n if (groupId) {\n docsSetItemsByGroupId.set(groupId, [...(docsSetItemsByGroupId.get(groupId) ?? []), item])\n } else {\n topLevelDocsSetItems.push(item)\n }\n }\n\n const buildGroupItem = (\n groupId: string,\n seen = new Set<string>(),\n ): PayloadMarkdownDocsNavItem | undefined => {\n if (seen.has(groupId)) {\n return undefined\n }\n\n const doc = groupsById.get(groupId)\n const group = toResolvedDocsGroup(doc)\n const routePath = getGroupRoutePath({\n groupId,\n groupsById,\n })\n\n if (!group || !routePath) {\n return undefined\n }\n\n const nextSeen = new Set([groupId, ...seen])\n const childGroups = (childGroupIdsByParentId.get(groupId) ?? []).flatMap((childGroupId) => {\n const item = buildGroupItem(childGroupId, nextSeen)\n\n return item ? [item] : []\n })\n const childDocsSets = docsSetItemsByGroupId.get(groupId) ?? []\n const children = sortByOrderThenLabel([...childGroups, ...childDocsSets])\n\n return {\n ...(children.length > 0 ? { children } : {}),\n id: group.id,\n type: 'docsGroup',\n collection: docsGroupsCollectionSlug,\n label: group.navTitle ?? group.title,\n order: group.order,\n route: routePath,\n ...(group.serveIndex ? { url: routePath } : {}),\n }\n }\n\n return applyTopLevelCapacity(\n sortByOrderThenLabel([\n ...topLevelGroupIds.flatMap((groupId) => {\n const item = buildGroupItem(groupId)\n\n return item ? [item] : []\n }),\n ...topLevelDocsSetItems,\n ]),\n capacityOptions,\n )\n}\n\nexport const getPayloadMarkdownDocsLinks = async ({\n collections,\n includeDrafts = false,\n overrideAccess = true,\n payload,\n}: GetPayloadMarkdownDocsLinksOptions): Promise<PayloadMarkdownDocsLink[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: includeDrafts,\n limit: 1000,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: 1000,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n\n return docsSetsResult.docs\n .flatMap((doc) => {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet, includeDrafts })) {\n return []\n }\n\n return [\n {\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath: getGroupRoutePath({\n groupId: getRelationshipId(doc.group),\n groupsById,\n }),\n }),\n },\n ]\n })\n .sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n .map(({ label, url }) => ({\n label,\n url,\n }))\n}\n\nconst toHeaderLink = ({\n item,\n mode,\n}: {\n item: PayloadMarkdownDocsNavItem\n mode: 'relationship' | 'url'\n}): PayloadMarkdownDocsHeaderNavLink | undefined => {\n if (mode === 'relationship') {\n return {\n type: 'reference',\n label: item.label,\n reference: {\n relationTo: item.collection,\n value: item.id,\n },\n }\n }\n\n const url = getFirstLinkableUrl(item)\n\n return url\n ? {\n type: 'custom',\n label: item.label,\n url,\n }\n : undefined\n}\n\nconst toHeaderNavItem = (\n item: PayloadMarkdownDocsNavItem,\n mode: 'relationship' | 'url',\n depth = 0,\n): PayloadMarkdownDocsHeaderNavItem | undefined => {\n const link = toHeaderLink({\n item,\n mode,\n })\n\n if (!link) {\n return undefined\n }\n\n const children = (item.children ?? []).flatMap((child) => {\n const childItem = toHeaderNavItem(child, mode, depth + 1)\n\n return childItem ? [childItem] : []\n })\n\n return {\n link,\n ...(children.length > 0\n ? depth === 0\n ? { subItems: children }\n : { childItems: children }\n : {}),\n }\n}\n\nexport const getPayloadMarkdownDocsHeaderNavItems = async ({\n existingItems,\n existingItemsCount = existingItems?.length ?? 0,\n mode = 'url',\n ...options\n}: GetPayloadMarkdownDocsHeaderNavItemsOptions): Promise<PayloadMarkdownDocsHeaderNavItem[]> => {\n const items = await getPayloadMarkdownDocsNavItems({\n ...options,\n existingItemsCount,\n })\n\n return items.flatMap((item) => {\n const headerItem = toHeaderNavItem(item, mode)\n\n return headerItem ? [headerItem] : []\n })\n}\n\nexport const appendPayloadMarkdownDocsHeaderNavItems = async <TExistingItem>({\n existingItems,\n ...options\n}: AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem>): Promise<\n (PayloadMarkdownDocsHeaderNavItem | TExistingItem)[]\n> => [\n ...existingItems,\n ...(await getPayloadMarkdownDocsHeaderNavItems({\n ...options,\n existingItems,\n })),\n]\n"],"names":["DEFAULT_DOCS_GROUPS_COLLECTION_SLUG","DEFAULT_DOCS_SETS_COLLECTION_SLUG","deriveDocsSetRouteBase","joinRouteSegments","getRelationshipId","isRecord","isVisibleDocsSet","toResolvedDocsGroup","toResolvedDocsSet","getAvailableSlots","availableSlots","existingItemsCount","maxItems","undefined","Math","max","applyTopLevelCapacity","items","options","slice","getGroupRoutePath","groupId","groupsById","seen","Set","has","group","get","slug","parent","sortByOrderThenLabel","sort","first","second","order","label","localeCompare","getFirstLinkableUrl","item","url","child","children","getPayloadMarkdownDocsNavItems","collections","fetchLimit","includeDrafts","overrideAccess","payload","capacityOptions","docsGroupsCollectionSlug","docsGroups","docsSetsCollectionSlug","docsSets","docsSetsResult","docsGroupsResult","Promise","all","find","collection","depth","draft","limit","Map","docs","flatMap","id","childGroupIdsByParentId","topLevelGroupIds","parentId","set","push","docsSetItemsByGroupId","topLevelDocsSetItems","doc","docsSet","groupRoutePath","type","navTitle","title","route","docsSetSlug","buildGroupItem","routePath","nextSeen","childGroups","childGroupId","childDocsSets","length","serveIndex","getPayloadMarkdownDocsLinks","map","toHeaderLink","mode","reference","relationTo","value","toHeaderNavItem","link","childItem","subItems","childItems","getPayloadMarkdownDocsHeaderNavItems","existingItems","headerItem","appendPayloadMarkdownDocsHeaderNavItems"],"mappings":"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"}
|
|
@@ -69,6 +69,7 @@ const getRepositoryName = (repository)=>{
|
|
|
69
69
|
const [, name] = repository.split('/', 2);
|
|
70
70
|
return name ?? repository;
|
|
71
71
|
};
|
|
72
|
+
const isReleaseTagRef = (claims)=>claims.event_name === 'release' && claims.ref.startsWith('refs/tags/');
|
|
72
73
|
const repositoryMatches = ({ allowed, owner, repository })=>{
|
|
73
74
|
const normalized = allowed.trim();
|
|
74
75
|
if (!normalized) {
|
|
@@ -179,7 +180,7 @@ export const verifyGitHubOidcToken = async ({ config, fetchJson, now = new Date(
|
|
|
179
180
|
return issue('oidc_owner_not_allowed', `GitHub OIDC token repository owner "${claims.repository_owner}" is not trusted.`);
|
|
180
181
|
}
|
|
181
182
|
const repositoryName = getRepositoryName(claims.repository);
|
|
182
|
-
if (!includesIfConfigured(config.allowedRefs, claims.ref)) {
|
|
183
|
+
if (!includesIfConfigured(config.allowedRefs, claims.ref) && !isReleaseTagRef(claims)) {
|
|
183
184
|
return issue('oidc_ref_not_allowed', `GitHub OIDC token ref "${claims.ref}" is not allowed for "${repositoryName}".`);
|
|
184
185
|
}
|
|
185
186
|
const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/security/githubOidc.ts"],"sourcesContent":["import {\n createPublicKey,\n type JsonWebKey,\n verify,\n} from 'node:crypto'\n\nimport type { FetchJson } from './jwks.js'\n\nimport {\n DEFAULT_GITHUB_OIDC_ISSUER,\n DEFAULT_MAX_SKEW_SECONDS,\n} from '../constants.js'\nimport {\n fetchJwks,\n findJwkByKid,\n getGithubOidcJwksUrl,\n} from './jwks.js'\nimport { decodeJwt } from './jwt.js'\n\nexport type GitHubOidcErrorCode =\n | 'oidc_expired'\n | 'oidc_invalid_audience'\n | 'oidc_invalid_issuer'\n | 'oidc_invalid_token'\n | 'oidc_jwks_unavailable'\n | 'oidc_missing_claim'\n | 'oidc_missing_jti'\n | 'oidc_not_yet_valid'\n | 'oidc_owner_not_allowed'\n | 'oidc_pull_request_not_allowed'\n | 'oidc_ref_not_allowed'\n | 'oidc_repository_not_allowed'\n | 'oidc_workflow_not_allowed'\n\nexport type GitHubOidcClaims = {\n actor?: string\n aud: string | string[]\n environment?: string\n event_name?: string\n exp: number\n iat: number\n iss: string\n job_workflow_ref?: string\n jti: string\n nbf?: number\n ref: string\n repository: string\n repository_owner: string\n sha?: string\n sub: string\n workflow?: string\n workflow_ref?: string\n}\n\nexport type GitHubOidcTrustedSource = {\n limitRepos?: boolean\n owner: string\n repositories?: string[]\n}\n\nexport type GitHubOidcVerifyConfig = {\n allowedRefs?: string[]\n allowedWorkflowRefs?: string[]\n allowPullRequests?: boolean\n audience: string\n enforceWorkflowRefs?: boolean\n issuer?: string\n jwksUrl?: string\n maxSkewSeconds?: number\n trustedSources: GitHubOidcTrustedSource[]\n}\n\nexport type VerifiedGitHubOidcToken = {\n claims: GitHubOidcClaims\n expiresAt: Date\n keyId: string\n}\n\nexport type VerifyGitHubOidcTokenResult =\n | {\n code: GitHubOidcErrorCode\n message: string\n ok: false\n }\n | {\n ok: true\n token: VerifiedGitHubOidcToken\n }\n\nconst isString = (value: unknown): value is string =>\n typeof value === 'string' && value.trim() !== ''\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every(isString)\n\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst getStringClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): string | undefined => {\n const value = payload[claim]\n\n return isString(value) ? value : undefined\n}\n\nconst getNumberClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): number | undefined => {\n const value = payload[claim]\n\n return isNumber(value) ? value : undefined\n}\n\nconst getAudienceClaim = (\n payload: Record<string, unknown>,\n): string | string[] | undefined => {\n const value = payload.aud\n\n if (isString(value) || isStringArray(value)) {\n return value\n }\n\n return undefined\n}\n\nconst toClaims = (\n payload: Record<string, unknown>,\n): GitHubOidcClaims | undefined => {\n const aud = getAudienceClaim(payload)\n const exp = getNumberClaim(payload, 'exp')\n const iat = getNumberClaim(payload, 'iat')\n const iss = getStringClaim(payload, 'iss')\n const jti = getStringClaim(payload, 'jti')\n const ref = getStringClaim(payload, 'ref')\n const repository = getStringClaim(payload, 'repository')\n const repositoryOwner = getStringClaim(payload, 'repository_owner')\n const sub = getStringClaim(payload, 'sub')\n\n if (\n !aud ||\n exp === undefined ||\n iat === undefined ||\n !iss ||\n !jti ||\n !ref ||\n !repository ||\n !repositoryOwner ||\n !sub\n ) {\n return undefined\n }\n\n return {\n actor: getStringClaim(payload, 'actor'),\n aud,\n environment: getStringClaim(payload, 'environment'),\n event_name: getStringClaim(payload, 'event_name'),\n exp,\n iat,\n iss,\n job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),\n jti,\n nbf: getNumberClaim(payload, 'nbf'),\n ref,\n repository,\n repository_owner: repositoryOwner,\n sha: getStringClaim(payload, 'sha'),\n sub,\n workflow: getStringClaim(payload, 'workflow'),\n workflow_ref: getStringClaim(payload, 'workflow_ref'),\n }\n}\n\nconst issue = (\n code: GitHubOidcErrorCode,\n message: string,\n): VerifyGitHubOidcTokenResult => ({\n code,\n message,\n ok: false,\n})\n\nconst includesIfConfigured = (\n allowed: string[] | undefined,\n value: string | undefined,\n): boolean => {\n if (!allowed || allowed.length === 0) {\n return true\n }\n\n return value !== undefined && allowed.includes(value)\n}\n\nconst audienceMatches = (\n audience: string | string[],\n expected: string,\n): boolean =>\n Array.isArray(audience) ? audience.includes(expected) : audience === expected\n\nconst getRepositoryName = (repository: string): string => {\n const [, name] = repository.split('/', 2)\n\n return name ?? repository\n}\n\nconst repositoryMatches = ({\n allowed,\n owner,\n repository,\n}: {\n allowed: string\n owner: string\n repository: string\n}): boolean => {\n const normalized = allowed.trim()\n\n if (!normalized) {\n return false\n }\n\n return normalized.includes('/')\n ? normalized.toLowerCase() === repository.toLowerCase()\n : `${owner}/${normalized}`.toLowerCase() === repository.toLowerCase()\n}\n\nconst findTrustedSource = ({\n repository,\n repositoryOwner,\n trustedSources,\n}: {\n repository: string\n repositoryOwner: string\n trustedSources: GitHubOidcTrustedSource[]\n}): GitHubOidcTrustedSource | undefined =>\n trustedSources.find((source) => {\n if (source.owner.toLowerCase() !== repositoryOwner.toLowerCase()) {\n return false\n }\n\n if (source.limitRepos !== true) {\n return true\n }\n\n return (source.repositories ?? []).some((allowedRepository) =>\n repositoryMatches({\n allowed: allowedRepository,\n owner: source.owner,\n repository,\n }),\n )\n })\n\nconst verifyJwtSignature = ({\n jwk,\n signature,\n signingInput,\n}: {\n jwk: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}): boolean => {\n try {\n const publicKey = createPublicKey({\n format: 'jwk',\n key: jwk as JsonWebKey,\n })\n\n return verify(\n 'RSA-SHA256',\n Buffer.from(signingInput, 'utf8'),\n publicKey,\n signature,\n )\n } catch {\n return false\n }\n}\n\nexport const verifyGitHubOidcToken = async ({\n config,\n fetchJson,\n now = new Date(),\n token,\n}: {\n config: GitHubOidcVerifyConfig\n fetchJson?: FetchJson\n now?: Date\n token: string\n}): Promise<VerifyGitHubOidcTokenResult> => {\n const decoded = decodeJwt(token)\n\n if (!decoded) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.')\n }\n\n if (decoded.header.alg !== 'RS256') {\n return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.')\n }\n\n if (!isString(decoded.header.kid)) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.')\n }\n\n const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER\n let jwksUrl: string\n\n try {\n jwksUrl = await getGithubOidcJwksUrl({\n fetchJson,\n issuer,\n jwksUrl: config.jwksUrl,\n })\n const jwks = await fetchJwks({\n fetchJson,\n now,\n url: jwksUrl,\n })\n const jwk = findJwkByKid({\n jwks,\n kid: decoded.header.kid,\n })\n\n if (\n !jwk ||\n !verifyJwtSignature({\n jwk,\n signature: decoded.signature,\n signingInput: decoded.signingInput,\n })\n ) {\n return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.')\n }\n } catch {\n return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.')\n }\n\n if (!isString(decoded.payload.jti)) {\n return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.')\n }\n\n const claims = toClaims(decoded.payload)\n\n if (!claims) {\n return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.')\n }\n\n if (claims.iss !== issuer) {\n return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.')\n }\n\n if (!audienceMatches(claims.aud, config.audience)) {\n return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.')\n }\n\n const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS\n const nowSeconds = now.getTime() / 1000\n\n if (claims.exp + maxSkewSeconds < nowSeconds) {\n return issue('oidc_expired', 'GitHub OIDC token has expired.')\n }\n\n if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.')\n }\n\n if (claims.iat - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.')\n }\n\n const trustedSources = config.trustedSources ?? []\n\n if (trustedSources.length === 0) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC auth requires a trusted GitHub owner.',\n )\n }\n\n const trustedSource = findTrustedSource({\n repository: claims.repository,\n repositoryOwner: claims.repository_owner,\n trustedSources,\n })\n\n if (!trustedSource) {\n const matchingOwner = trustedSources.find(\n (source) =>\n source.owner.toLowerCase() === claims.repository_owner.toLowerCase(),\n )\n\n if (matchingOwner) {\n return issue(\n 'oidc_repository_not_allowed',\n `GitHub OIDC token repository \"${claims.repository}\" is not trusted for owner \"${claims.repository_owner}\".`,\n )\n }\n\n return issue(\n 'oidc_owner_not_allowed',\n `GitHub OIDC token repository owner \"${claims.repository_owner}\" is not trusted.`,\n )\n }\n\n const repositoryName = getRepositoryName(claims.repository)\n\n if (!includesIfConfigured(config.allowedRefs, claims.ref)) {\n return issue(\n 'oidc_ref_not_allowed',\n `GitHub OIDC token ref \"${claims.ref}\" is not allowed for \"${repositoryName}\".`,\n )\n }\n\n const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref\n\n if (\n config.enforceWorkflowRefs === true &&\n (config.allowedWorkflowRefs?.length ?? 0) === 0\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'Advanced workflow security is enabled but no workflow refs are trusted.',\n )\n }\n\n if (\n config.enforceWorkflowRefs === true &&\n !includesIfConfigured(config.allowedWorkflowRefs, workflowRef)\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow ref is not allowed.',\n )\n }\n\n if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {\n return issue(\n 'oidc_pull_request_not_allowed',\n 'GitHub OIDC pull request events are not allowed.',\n )\n }\n\n return {\n ok: true,\n token: {\n claims,\n expiresAt: new Date(claims.exp * 1000),\n keyId: `github-oidc:${claims.repository}`,\n },\n }\n}\n"],"names":["createPublicKey","verify","DEFAULT_GITHUB_OIDC_ISSUER","DEFAULT_MAX_SKEW_SECONDS","fetchJwks","findJwkByKid","getGithubOidcJwksUrl","decodeJwt","isString","value","trim","isStringArray","Array","isArray","every","isNumber","Number","isFinite","getStringClaim","payload","claim","undefined","getNumberClaim","getAudienceClaim","aud","toClaims","exp","iat","iss","jti","ref","repository","repositoryOwner","sub","actor","environment","event_name","job_workflow_ref","nbf","repository_owner","sha","workflow","workflow_ref","issue","code","message","ok","includesIfConfigured","allowed","length","includes","audienceMatches","audience","expected","getRepositoryName","name","split","repositoryMatches","owner","normalized","toLowerCase","findTrustedSource","trustedSources","find","source","limitRepos","repositories","some","allowedRepository","verifyJwtSignature","jwk","signature","signingInput","publicKey","format","key","Buffer","from","verifyGitHubOidcToken","config","fetchJson","now","Date","token","decoded","header","alg","kid","issuer","jwksUrl","jwks","url","claims","maxSkewSeconds","nowSeconds","getTime","trustedSource","matchingOwner","repositoryName","allowedRefs","workflowRef","enforceWorkflowRefs","allowedWorkflowRefs","allowPullRequests","expiresAt","keyId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/security/githubOidc.ts"],"sourcesContent":["import {\n createPublicKey,\n type JsonWebKey,\n verify,\n} from 'node:crypto'\n\nimport type { FetchJson } from './jwks.js'\n\nimport {\n DEFAULT_GITHUB_OIDC_ISSUER,\n DEFAULT_MAX_SKEW_SECONDS,\n} from '../constants.js'\nimport {\n fetchJwks,\n findJwkByKid,\n getGithubOidcJwksUrl,\n} from './jwks.js'\nimport { decodeJwt } from './jwt.js'\n\nexport type GitHubOidcErrorCode =\n | 'oidc_expired'\n | 'oidc_invalid_audience'\n | 'oidc_invalid_issuer'\n | 'oidc_invalid_token'\n | 'oidc_jwks_unavailable'\n | 'oidc_missing_claim'\n | 'oidc_missing_jti'\n | 'oidc_not_yet_valid'\n | 'oidc_owner_not_allowed'\n | 'oidc_pull_request_not_allowed'\n | 'oidc_ref_not_allowed'\n | 'oidc_repository_not_allowed'\n | 'oidc_workflow_not_allowed'\n\nexport type GitHubOidcClaims = {\n actor?: string\n aud: string | string[]\n environment?: string\n event_name?: string\n exp: number\n iat: number\n iss: string\n job_workflow_ref?: string\n jti: string\n nbf?: number\n ref: string\n repository: string\n repository_owner: string\n sha?: string\n sub: string\n workflow?: string\n workflow_ref?: string\n}\n\nexport type GitHubOidcTrustedSource = {\n limitRepos?: boolean\n owner: string\n repositories?: string[]\n}\n\nexport type GitHubOidcVerifyConfig = {\n allowedRefs?: string[]\n allowedWorkflowRefs?: string[]\n allowPullRequests?: boolean\n audience: string\n enforceWorkflowRefs?: boolean\n issuer?: string\n jwksUrl?: string\n maxSkewSeconds?: number\n trustedSources: GitHubOidcTrustedSource[]\n}\n\nexport type VerifiedGitHubOidcToken = {\n claims: GitHubOidcClaims\n expiresAt: Date\n keyId: string\n}\n\nexport type VerifyGitHubOidcTokenResult =\n | {\n code: GitHubOidcErrorCode\n message: string\n ok: false\n }\n | {\n ok: true\n token: VerifiedGitHubOidcToken\n }\n\nconst isString = (value: unknown): value is string =>\n typeof value === 'string' && value.trim() !== ''\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every(isString)\n\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst getStringClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): string | undefined => {\n const value = payload[claim]\n\n return isString(value) ? value : undefined\n}\n\nconst getNumberClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): number | undefined => {\n const value = payload[claim]\n\n return isNumber(value) ? value : undefined\n}\n\nconst getAudienceClaim = (\n payload: Record<string, unknown>,\n): string | string[] | undefined => {\n const value = payload.aud\n\n if (isString(value) || isStringArray(value)) {\n return value\n }\n\n return undefined\n}\n\nconst toClaims = (\n payload: Record<string, unknown>,\n): GitHubOidcClaims | undefined => {\n const aud = getAudienceClaim(payload)\n const exp = getNumberClaim(payload, 'exp')\n const iat = getNumberClaim(payload, 'iat')\n const iss = getStringClaim(payload, 'iss')\n const jti = getStringClaim(payload, 'jti')\n const ref = getStringClaim(payload, 'ref')\n const repository = getStringClaim(payload, 'repository')\n const repositoryOwner = getStringClaim(payload, 'repository_owner')\n const sub = getStringClaim(payload, 'sub')\n\n if (\n !aud ||\n exp === undefined ||\n iat === undefined ||\n !iss ||\n !jti ||\n !ref ||\n !repository ||\n !repositoryOwner ||\n !sub\n ) {\n return undefined\n }\n\n return {\n actor: getStringClaim(payload, 'actor'),\n aud,\n environment: getStringClaim(payload, 'environment'),\n event_name: getStringClaim(payload, 'event_name'),\n exp,\n iat,\n iss,\n job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),\n jti,\n nbf: getNumberClaim(payload, 'nbf'),\n ref,\n repository,\n repository_owner: repositoryOwner,\n sha: getStringClaim(payload, 'sha'),\n sub,\n workflow: getStringClaim(payload, 'workflow'),\n workflow_ref: getStringClaim(payload, 'workflow_ref'),\n }\n}\n\nconst issue = (\n code: GitHubOidcErrorCode,\n message: string,\n): VerifyGitHubOidcTokenResult => ({\n code,\n message,\n ok: false,\n})\n\nconst includesIfConfigured = (\n allowed: string[] | undefined,\n value: string | undefined,\n): boolean => {\n if (!allowed || allowed.length === 0) {\n return true\n }\n\n return value !== undefined && allowed.includes(value)\n}\n\nconst audienceMatches = (\n audience: string | string[],\n expected: string,\n): boolean =>\n Array.isArray(audience) ? audience.includes(expected) : audience === expected\n\nconst getRepositoryName = (repository: string): string => {\n const [, name] = repository.split('/', 2)\n\n return name ?? repository\n}\n\nconst isReleaseTagRef = (claims: GitHubOidcClaims): boolean =>\n claims.event_name === 'release' && claims.ref.startsWith('refs/tags/')\n\nconst repositoryMatches = ({\n allowed,\n owner,\n repository,\n}: {\n allowed: string\n owner: string\n repository: string\n}): boolean => {\n const normalized = allowed.trim()\n\n if (!normalized) {\n return false\n }\n\n return normalized.includes('/')\n ? normalized.toLowerCase() === repository.toLowerCase()\n : `${owner}/${normalized}`.toLowerCase() === repository.toLowerCase()\n}\n\nconst findTrustedSource = ({\n repository,\n repositoryOwner,\n trustedSources,\n}: {\n repository: string\n repositoryOwner: string\n trustedSources: GitHubOidcTrustedSource[]\n}): GitHubOidcTrustedSource | undefined =>\n trustedSources.find((source) => {\n if (source.owner.toLowerCase() !== repositoryOwner.toLowerCase()) {\n return false\n }\n\n if (source.limitRepos !== true) {\n return true\n }\n\n return (source.repositories ?? []).some((allowedRepository) =>\n repositoryMatches({\n allowed: allowedRepository,\n owner: source.owner,\n repository,\n }),\n )\n })\n\nconst verifyJwtSignature = ({\n jwk,\n signature,\n signingInput,\n}: {\n jwk: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}): boolean => {\n try {\n const publicKey = createPublicKey({\n format: 'jwk',\n key: jwk as JsonWebKey,\n })\n\n return verify(\n 'RSA-SHA256',\n Buffer.from(signingInput, 'utf8'),\n publicKey,\n signature,\n )\n } catch {\n return false\n }\n}\n\nexport const verifyGitHubOidcToken = async ({\n config,\n fetchJson,\n now = new Date(),\n token,\n}: {\n config: GitHubOidcVerifyConfig\n fetchJson?: FetchJson\n now?: Date\n token: string\n}): Promise<VerifyGitHubOidcTokenResult> => {\n const decoded = decodeJwt(token)\n\n if (!decoded) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.')\n }\n\n if (decoded.header.alg !== 'RS256') {\n return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.')\n }\n\n if (!isString(decoded.header.kid)) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.')\n }\n\n const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER\n let jwksUrl: string\n\n try {\n jwksUrl = await getGithubOidcJwksUrl({\n fetchJson,\n issuer,\n jwksUrl: config.jwksUrl,\n })\n const jwks = await fetchJwks({\n fetchJson,\n now,\n url: jwksUrl,\n })\n const jwk = findJwkByKid({\n jwks,\n kid: decoded.header.kid,\n })\n\n if (\n !jwk ||\n !verifyJwtSignature({\n jwk,\n signature: decoded.signature,\n signingInput: decoded.signingInput,\n })\n ) {\n return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.')\n }\n } catch {\n return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.')\n }\n\n if (!isString(decoded.payload.jti)) {\n return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.')\n }\n\n const claims = toClaims(decoded.payload)\n\n if (!claims) {\n return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.')\n }\n\n if (claims.iss !== issuer) {\n return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.')\n }\n\n if (!audienceMatches(claims.aud, config.audience)) {\n return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.')\n }\n\n const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS\n const nowSeconds = now.getTime() / 1000\n\n if (claims.exp + maxSkewSeconds < nowSeconds) {\n return issue('oidc_expired', 'GitHub OIDC token has expired.')\n }\n\n if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.')\n }\n\n if (claims.iat - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.')\n }\n\n const trustedSources = config.trustedSources ?? []\n\n if (trustedSources.length === 0) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC auth requires a trusted GitHub owner.',\n )\n }\n\n const trustedSource = findTrustedSource({\n repository: claims.repository,\n repositoryOwner: claims.repository_owner,\n trustedSources,\n })\n\n if (!trustedSource) {\n const matchingOwner = trustedSources.find(\n (source) =>\n source.owner.toLowerCase() === claims.repository_owner.toLowerCase(),\n )\n\n if (matchingOwner) {\n return issue(\n 'oidc_repository_not_allowed',\n `GitHub OIDC token repository \"${claims.repository}\" is not trusted for owner \"${claims.repository_owner}\".`,\n )\n }\n\n return issue(\n 'oidc_owner_not_allowed',\n `GitHub OIDC token repository owner \"${claims.repository_owner}\" is not trusted.`,\n )\n }\n\n const repositoryName = getRepositoryName(claims.repository)\n\n if (!includesIfConfigured(config.allowedRefs, claims.ref) && !isReleaseTagRef(claims)) {\n return issue(\n 'oidc_ref_not_allowed',\n `GitHub OIDC token ref \"${claims.ref}\" is not allowed for \"${repositoryName}\".`,\n )\n }\n\n const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref\n\n if (\n config.enforceWorkflowRefs === true &&\n (config.allowedWorkflowRefs?.length ?? 0) === 0\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'Advanced workflow security is enabled but no workflow refs are trusted.',\n )\n }\n\n if (\n config.enforceWorkflowRefs === true &&\n !includesIfConfigured(config.allowedWorkflowRefs, workflowRef)\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow ref is not allowed.',\n )\n }\n\n if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {\n return issue(\n 'oidc_pull_request_not_allowed',\n 'GitHub OIDC pull request events are not allowed.',\n )\n }\n\n return {\n ok: true,\n token: {\n claims,\n expiresAt: new Date(claims.exp * 1000),\n keyId: `github-oidc:${claims.repository}`,\n },\n }\n}\n"],"names":["createPublicKey","verify","DEFAULT_GITHUB_OIDC_ISSUER","DEFAULT_MAX_SKEW_SECONDS","fetchJwks","findJwkByKid","getGithubOidcJwksUrl","decodeJwt","isString","value","trim","isStringArray","Array","isArray","every","isNumber","Number","isFinite","getStringClaim","payload","claim","undefined","getNumberClaim","getAudienceClaim","aud","toClaims","exp","iat","iss","jti","ref","repository","repositoryOwner","sub","actor","environment","event_name","job_workflow_ref","nbf","repository_owner","sha","workflow","workflow_ref","issue","code","message","ok","includesIfConfigured","allowed","length","includes","audienceMatches","audience","expected","getRepositoryName","name","split","isReleaseTagRef","claims","startsWith","repositoryMatches","owner","normalized","toLowerCase","findTrustedSource","trustedSources","find","source","limitRepos","repositories","some","allowedRepository","verifyJwtSignature","jwk","signature","signingInput","publicKey","format","key","Buffer","from","verifyGitHubOidcToken","config","fetchJson","now","Date","token","decoded","header","alg","kid","issuer","jwksUrl","jwks","url","maxSkewSeconds","nowSeconds","getTime","trustedSource","matchingOwner","repositoryName","allowedRefs","workflowRef","enforceWorkflowRefs","allowedWorkflowRefs","allowPullRequests","expiresAt","keyId"],"mappings":"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"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: payload-markdown-docs
|
|
3
|
+
description: Use this skill when maintaining Git-backed documentation for a project that uses `@valkyrianlabs/payload-markdown-docs`.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Payload Markdown Docs Skill
|
|
2
7
|
|
|
3
8
|
Use this skill when maintaining Git-backed documentation for a project that uses `@valkyrianlabs/payload-markdown-docs`.
|
|
@@ -11,8 +16,10 @@ The docs source lives in `{{docsRoot}}` unless the user says otherwise. Edit Mar
|
|
|
11
16
|
- Use supported frontmatter only.
|
|
12
17
|
- Keep internal docs links root-relative inside the docs set, such as `/getting-started/quick-start`.
|
|
13
18
|
- Use `payload-markdown` directives only when they are supported.
|
|
19
|
+
- Keep formatting plain Markdown: supported frontmatter, one H1, clear H2/H3 sections, root-relative links, and supported directives.
|
|
14
20
|
- Do not invent directives, frontmatter fields, CLI flags, sync modes, or runtime features.
|
|
15
21
|
- Do not describe unsupported features as implemented.
|
|
22
|
+
- Do not add MDX, arbitrary YAML objects, inline frontmatter arrays, HTML widgets, or one Payload Page per Markdown file.
|
|
16
23
|
- Run validation before finishing docs edits.
|
|
17
24
|
- Treat sync and publishing as CMS/server-owned. The request may ask; Payload
|
|
18
25
|
docs sets and plugin config decide.
|
|
@@ -39,6 +46,7 @@ Rules for `index.ai.yml`:
|
|
|
39
46
|
- Do not show the manifest in human docs navigation.
|
|
40
47
|
- Do not render the manifest as a normal docs page.
|
|
41
48
|
- Use it as the canonical ordering source for the `.md` export.
|
|
49
|
+
- Remember that the `.md` export is served by a Next route handler, not by a generated Payload Page.
|
|
42
50
|
- Avoid backend hand-sorting unless the project already has a docs sort field.
|
|
43
51
|
|
|
44
52
|
Ordering source preference:
|
|
@@ -154,6 +162,7 @@ Sync writes require `sync.allowWrites: true`. Publishing additionally requires `
|
|
|
154
162
|
## References
|
|
155
163
|
|
|
156
164
|
- `reference/payload-markdown-directives.md`
|
|
165
|
+
- `reference/formatting.md`
|
|
157
166
|
- `reference/frontmatter.md`
|
|
158
167
|
- `reference/workflow.md`
|
|
159
168
|
- `reference/sync.md`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Formatting
|
|
2
|
+
|
|
3
|
+
Keep docs as plain Markdown that validates before sync.
|
|
4
|
+
|
|
5
|
+
Expected shape:
|
|
6
|
+
|
|
7
|
+
- supported frontmatter at the top
|
|
8
|
+
- one H1 for the page title
|
|
9
|
+
- H2 and H3 sections for structure
|
|
10
|
+
- root-relative internal links, such as `/workflow/publishing`
|
|
11
|
+
- supported `payload-markdown` directives only
|
|
12
|
+
- `index.ai.yml` updated when pages are added, moved, renamed, or removed
|
|
13
|
+
|
|
14
|
+
Do not add:
|
|
15
|
+
|
|
16
|
+
- `.mdx` files
|
|
17
|
+
- arbitrary YAML objects
|
|
18
|
+
- inline frontmatter arrays
|
|
19
|
+
- invented directive names or props
|
|
20
|
+
- HTML scripts, iframes, or client-side widgets
|
|
21
|
+
- one Payload Page per Markdown file
|
|
22
|
+
- production docs domains for internal links
|
|
23
|
+
|
|
24
|
+
Prefer short, concrete sections over long pages with deeply nested headings.
|
|
@@ -35,5 +35,7 @@ Rules:
|
|
|
35
35
|
- `tags` and `redirectFrom` must use list item syntax.
|
|
36
36
|
- `slug` may contain letters, numbers, and hyphens.
|
|
37
37
|
- Avoid arbitrary nested YAML objects.
|
|
38
|
+
- Avoid inline arrays such as `tags: [getting-started]`.
|
|
38
39
|
- Avoid unsupported fields unless the user accepts validation warnings.
|
|
39
40
|
- Explicit `title` is preferred even though title fallback exists.
|
|
41
|
+
- Use `slug` only to override the final route segment; move files to change route hierarchy.
|
|
@@ -34,3 +34,11 @@ Do not hardcode production docs domains for internal navigation.
|
|
|
34
34
|
## Route Adapter
|
|
35
35
|
|
|
36
36
|
The `/next` export can resolve docs routes and let an app fall back to normal Pages rendering when no docs route matches. It does not mutate Pages.
|
|
37
|
+
|
|
38
|
+
## Raw Markdown
|
|
39
|
+
|
|
40
|
+
The AI-facing `.md` export is served by a Next route handler. It is not a
|
|
41
|
+
generated Payload Page and cannot be returned from a `page.tsx` catch-all.
|
|
42
|
+
|
|
43
|
+
Use `createPayloadMarkdownDocsMarkdownResponse` at the output path from
|
|
44
|
+
`index.ai.yml`, or place AI exports in a dedicated namespace such as `/ai`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valkyrianlabs/payload-markdown-docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Git-backed Markdown documentation sync for Payload CMS, powered by payload-markdown.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"payload-markdown-docs": "./dist/cli/index.js"
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"copyfiles": "2.4.1",
|
|
87
87
|
"cross-env": "^7.0.3",
|
|
88
88
|
"eslint": "^9.39.4",
|
|
89
|
-
"eslint-config-next": "
|
|
89
|
+
"eslint-config-next": "latest",
|
|
90
90
|
"graphql": "^16.14.0",
|
|
91
91
|
"mongodb-memory-server": "10.1.4",
|
|
92
|
-
"next": "
|
|
92
|
+
"next": "latest",
|
|
93
93
|
"open": "^10.2.0",
|
|
94
94
|
"payload": "latest",
|
|
95
95
|
"postcss": "^8.5.14",
|