@kirchdev/duxt 0.1.1-dev.29-765cae3 → 0.2.1-dev-main.32-677294a
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/app/components/DuxtLandingProse.vue +1 -2
- package/app/components/DuxtVersion.vue +33 -3
- package/app/components/content/ProseA.vue +13 -8
- package/app/types/duxt.d.ts +25 -2
- package/app/utils/documentation-link.ts +14 -0
- package/package.json +1 -1
- package/public/devtools/checks.html +1 -1
- package/sections-changelog.ts +17 -7
- package/sections-resolve.ts +18 -6
- package/server/middleware/raw-markdown.ts +1 -0
- package/sources-git.ts +29 -13
- package/sources-resolve.ts +60 -17
- package/sources.ts +5 -1
- package/validate-report.ts +20 -11
|
@@ -42,8 +42,7 @@ const localeLink = useDuxtLink();
|
|
|
42
42
|
<component
|
|
43
43
|
:is="`h${level}`"
|
|
44
44
|
:id="headingId"
|
|
45
|
-
class="font-semibold tracking-tight text-balance"
|
|
46
|
-
:class="level === 2 ? 'text-2xl sm:text-3xl' : 'text-xl sm:text-2xl'"
|
|
45
|
+
class="text-2xl font-semibold tracking-tight text-balance sm:text-3xl"
|
|
47
46
|
>
|
|
48
47
|
{{ prose.title }}
|
|
49
48
|
</component>
|
|
@@ -24,6 +24,7 @@ const props = withDefaults(defineProps<{ variant?: 'badge' | 'block' }>(), {
|
|
|
24
24
|
const duxt = useDuxtConfig();
|
|
25
25
|
const path = useDuxtPath();
|
|
26
26
|
const localeLink = useDuxtLink();
|
|
27
|
+
const { collection, source } = useDuxtCollection();
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Versions come from the resolved source manifest, so the control can only
|
|
@@ -34,6 +35,31 @@ const localeLink = useDuxtLink();
|
|
|
34
35
|
*/
|
|
35
36
|
const { choices: versions } = useDuxtVersion();
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A changelog is one global history, so it deliberately has no version
|
|
40
|
+
* selector. Its current release is still useful context, though: read the
|
|
41
|
+
* latest one from the overview's generated frontmatter instead of falling
|
|
42
|
+
* back to the site's package version. The parser writes that value while
|
|
43
|
+
* turning the changelog into the overview and release pages.
|
|
44
|
+
*/
|
|
45
|
+
const changelog = computed(
|
|
46
|
+
() =>
|
|
47
|
+
source.value?.generated?.type === 'changelog' &&
|
|
48
|
+
source.value.generated.versioning === 'global'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { data: changelogPage } = await useAsyncData(
|
|
52
|
+
() => `duxt-changelog-version-${collection.value}-${path.value}`,
|
|
53
|
+
async () => {
|
|
54
|
+
if (!changelog.value) return undefined;
|
|
55
|
+
|
|
56
|
+
return (await queryCollection(collection.value as DuxtCollectionArg)
|
|
57
|
+
.path(path.value)
|
|
58
|
+
.first()) as { release?: string } | null;
|
|
59
|
+
},
|
|
60
|
+
{ watch: [collection, path, changelog] }
|
|
61
|
+
);
|
|
62
|
+
|
|
37
63
|
const current = computed(() =>
|
|
38
64
|
sourceForPath(
|
|
39
65
|
path.value,
|
|
@@ -41,6 +67,10 @@ const current = computed(() =>
|
|
|
41
67
|
)
|
|
42
68
|
);
|
|
43
69
|
|
|
70
|
+
const label = computed(
|
|
71
|
+
() => changelogPage.value?.release ?? current.value?.label ?? duxt.version
|
|
72
|
+
);
|
|
73
|
+
|
|
44
74
|
const { t, te } = useI18n();
|
|
45
75
|
|
|
46
76
|
/**
|
|
@@ -94,7 +124,7 @@ function pathIn(version: { to?: string }) {
|
|
|
94
124
|
|
|
95
125
|
<span class="grid min-w-0 flex-1 leading-tight">
|
|
96
126
|
<span class="truncate font-mono text-sm font-medium">
|
|
97
|
-
{{
|
|
127
|
+
{{ label }}
|
|
98
128
|
</span>
|
|
99
129
|
<span class="truncate text-xs text-muted-foreground">
|
|
100
130
|
{{ (current && caption(current)) ?? $t('duxt.version.label') }}
|
|
@@ -112,7 +142,7 @@ function pathIn(version: { to?: string }) {
|
|
|
112
142
|
variant="secondary"
|
|
113
143
|
class="gap-1 font-mono text-[10px] hover:bg-accent"
|
|
114
144
|
>
|
|
115
|
-
{{
|
|
145
|
+
{{ label }}
|
|
116
146
|
<Icon name="lucide:chevron-down" class="size-3 opacity-60" />
|
|
117
147
|
</UiBadge>
|
|
118
148
|
</button>
|
|
@@ -163,6 +193,6 @@ function pathIn(version: { to?: string }) {
|
|
|
163
193
|
variant="secondary"
|
|
164
194
|
class="font-mono text-[10px]"
|
|
165
195
|
>
|
|
166
|
-
{{
|
|
196
|
+
{{ label }}
|
|
167
197
|
</UiBadge>
|
|
168
198
|
</template>
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* mid-sentence.
|
|
12
12
|
*
|
|
13
13
|
* So an absolute internal path is read as relative TO ITS OWN SOURCE, and the
|
|
14
|
-
* prefix is put back here.
|
|
15
|
-
*
|
|
14
|
+
* prefix is put back here. `~/` is the explicit website-root escape hatch for
|
|
15
|
+
* a link that has to leave that source. A path that already carries the prefix
|
|
16
|
+
* is left alone, so a link written the long way round still works.
|
|
16
17
|
*/
|
|
17
18
|
const props = defineProps<{
|
|
18
19
|
href?: string;
|
|
@@ -26,22 +27,26 @@ const external = computed(() =>
|
|
|
26
27
|
Boolean(props.href && /^(?:[a-z]+:|\/\/)/i.test(props.href))
|
|
27
28
|
);
|
|
28
29
|
|
|
30
|
+
const internal = computed(() =>
|
|
31
|
+
Boolean(props.href?.startsWith('/') || props.href?.startsWith('~/'))
|
|
32
|
+
);
|
|
33
|
+
|
|
29
34
|
const to = computed(() => {
|
|
30
35
|
const href = props.href ?? '';
|
|
31
|
-
if (external.value || !
|
|
36
|
+
if (external.value || !internal.value) return href;
|
|
32
37
|
|
|
33
38
|
const prefix = source.value?.prefix ?? '';
|
|
34
|
-
const [
|
|
35
|
-
const
|
|
36
|
-
|
|
39
|
+
const [pathAndQuery, hash] = href.split('#');
|
|
40
|
+
const [path, query] = pathAndQuery!.split('?');
|
|
41
|
+
const resolved = resolveDocumentationPath(path!, prefix);
|
|
37
42
|
|
|
38
|
-
return `${localeLink(resolved)}${hash ? `#${hash}` : ''}`;
|
|
43
|
+
return `${localeLink(resolved)}${query ? `?${query}` : ''}${hash ? `#${hash}` : ''}`;
|
|
39
44
|
});
|
|
40
45
|
</script>
|
|
41
46
|
|
|
42
47
|
<template>
|
|
43
48
|
<a
|
|
44
|
-
v-if="external || !
|
|
49
|
+
v-if="external || !internal"
|
|
45
50
|
:href="href"
|
|
46
51
|
:target="target ?? (external ? '_blank' : undefined)"
|
|
47
52
|
:rel="external ? 'noopener' : undefined"
|
package/app/types/duxt.d.ts
CHANGED
|
@@ -143,6 +143,16 @@ declare global {
|
|
|
143
143
|
| 'deprecated'
|
|
144
144
|
| 'eol';
|
|
145
145
|
|
|
146
|
+
/** Lifecycle defaults a source may opt into for its different ref kinds. */
|
|
147
|
+
type DuxtSourceStatusDefaultsInput = {
|
|
148
|
+
/** The moving `latest` shorthand. */
|
|
149
|
+
latest?: DuxtSourceStatusInput;
|
|
150
|
+
/** An explicitly listed tag. */
|
|
151
|
+
tag?: DuxtSourceStatusInput;
|
|
152
|
+
/** A named branch, such as `main`. */
|
|
153
|
+
branch?: DuxtSourceStatusInput;
|
|
154
|
+
};
|
|
155
|
+
|
|
146
156
|
/**
|
|
147
157
|
* A branch by name, or a tag stated as one.
|
|
148
158
|
*
|
|
@@ -154,12 +164,16 @@ declare global {
|
|
|
154
164
|
| {
|
|
155
165
|
branch: string;
|
|
156
166
|
label?: string;
|
|
167
|
+
/** Serve this ref without a version prefix. */
|
|
168
|
+
default?: boolean;
|
|
157
169
|
status?: DuxtSourceStatusInput;
|
|
158
170
|
locales?: DuxtSourceLocaleInput[];
|
|
159
171
|
}
|
|
160
172
|
| {
|
|
161
173
|
tag: string;
|
|
162
174
|
label?: string;
|
|
175
|
+
/** Serve this ref without a version prefix. */
|
|
176
|
+
default?: boolean;
|
|
163
177
|
status?: DuxtSourceStatusInput;
|
|
164
178
|
locales?: DuxtSourceLocaleInput[];
|
|
165
179
|
};
|
|
@@ -185,6 +199,13 @@ declare global {
|
|
|
185
199
|
interface DuxtSourceInput {
|
|
186
200
|
/** Folder holding the Markdown, relative to the repository root. */
|
|
187
201
|
path?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Whether this source's Markdown is published as documentation pages.
|
|
204
|
+
*
|
|
205
|
+
* Set `false` for a source that only provides generated sections. It can
|
|
206
|
+
* then share the default URL prefix of a versioned documentation source.
|
|
207
|
+
*/
|
|
208
|
+
content?: boolean;
|
|
188
209
|
/** `owner/name` or a full git URL. Omitted means this repository. */
|
|
189
210
|
repo?: string;
|
|
190
211
|
/** Refs to publish as versions. Omitted means the current checkout. */
|
|
@@ -208,8 +229,8 @@ declare global {
|
|
|
208
229
|
*
|
|
209
230
|
* Named here, everything else follows the ref path exactly: the URL segment,
|
|
210
231
|
* the switcher entry (scoped to the same artefact), the banner and the
|
|
211
|
-
* canonical.
|
|
212
|
-
*
|
|
232
|
+
* canonical. A ref may name itself as the default; otherwise
|
|
233
|
+
* `sourceOptions.defaultRef` names which is served without a prefix.
|
|
213
234
|
*
|
|
214
235
|
* Never beside `refs` — a source with both would have to be served at two
|
|
215
236
|
* prefixes at once, and the resolver says so rather than picking one.
|
|
@@ -227,6 +248,8 @@ declare global {
|
|
|
227
248
|
slug?: string;
|
|
228
249
|
/** Lifecycle of every version this entry publishes, unless a ref says otherwise. */
|
|
229
250
|
status?: DuxtSourceStatusInput;
|
|
251
|
+
/** Lifecycle defaults by ref kind; an explicit ref status wins. */
|
|
252
|
+
statusDefaults?: DuxtSourceStatusDefaultsInput;
|
|
230
253
|
/**
|
|
231
254
|
* The repository a source read off disk lives in, for links back to it.
|
|
232
255
|
* Not `repo`, which is what makes Content download a source.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isInside } from './version-paths';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a documentation path as the reader will see it.
|
|
5
|
+
*
|
|
6
|
+
* Markdown paths normally belong to the source that contains them. `~/` is the
|
|
7
|
+
* explicit escape hatch: it starts at the website root so one source can name
|
|
8
|
+
* a page served by another source without hard-coding its own prefix.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDocumentationPath(path: string, prefix: string): string {
|
|
11
|
+
if (path.startsWith('~/')) return path.slice(1);
|
|
12
|
+
|
|
13
|
+
return prefix && !isInside(path, prefix) ? `${prefix}${path}` : path;
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@kirchdev/duxt",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.1-dev-main.32-677294a",
|
|
5
5
|
"description": "Nuxt documentation layer on Content v3 — multi-repo, versioned docs from one compact source list.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
<p class="hint">The build validator, run against what the site is serving now.</p>
|
|
239
239
|
<div class="stats"><div class="stat "><b>34</b><span>pages checked</span></div><div class="stat "><b>6</b><span>collections</span></div><div class="stat ok"><b>0</b><span>errors</span></div><div class="stat warn"><b>5</b><span>warnings</span></div></div><h2>Warnings</h2><div class="scroll"><table>
|
|
240
240
|
<thead><tr><th scope="col"></th><th scope="col">Warning</th></tr></thead>
|
|
241
|
-
<tbody><tr><td><span class="tag warn">warn</span></td><td>the generated section "Releases" (CHANGELOG.md): the heading "1.3 (Feb 2026)" on line 41 reads like a release but does not parse as one, so it became part of the release above it instead of a page of its own. A release heading is a version, optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk/3.reference/2.errors.md">"docs_sdk/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_de_sdk%2Fde%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_de_sdk/de/3.reference/2.errors.md">"docs_de_sdk/de/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F1.getting-started%2Findex.md" target="_blank" title="/srv/acme-docs/docs_sdk/1.getting-started/index.md">"docs_sdk/1.getting-started/index.md"</a> links to "/guides/cacheing", which no page serves.</td></tr></tbody>
|
|
241
|
+
<tbody><tr><td><span class="tag warn">warn</span></td><td>the generated section "Releases" (CHANGELOG.md): the heading "1.3 (Feb 2026)" on line 41 reads like a release but does not parse as one, so it became part of the release above it instead of a page of its own. A release heading is a version, optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk/3.reference/2.errors.md">"docs_sdk/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_de_sdk%2Fde%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_de_sdk/de/3.reference/2.errors.md">"docs_de_sdk/de/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F1.getting-started%2Findex.md" target="_blank" title="/srv/acme-docs/docs_sdk/1.getting-started/index.md">"docs_sdk/1.getting-started/index.md"</a> links to "/sdk/guides/cacheing", which no page serves.</td></tr></tbody>
|
|
242
242
|
</table></div><h2>Translations</h2><ul class="steps"><li><strong>de under "/sdk": 6/8 pages</strong></li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F2.guides%2F2.caching.md" target="_blank" title="/srv/acme-docs/docs_sdk/2.guides/2.caching.md">"docs_sdk/2.guides/2.caching.md"</a> has no de translation.</li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F4.changelog.md" target="_blank" title="/srv/acme-docs/docs_sdk/4.changelog.md">"docs_sdk/4.changelog.md"</a> has no de translation.</li><li><strong>de under "/sdk/v1.9": 5/7 pages</strong></li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no de translation.</li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F4.changelog.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/4.changelog.md">"docs_sdk_v1_9/4.changelog.md"</a> has no de translation.</li></ul>
|
|
243
243
|
</main>
|
|
244
244
|
<script>
|
package/sections-changelog.ts
CHANGED
|
@@ -192,13 +192,15 @@ function parseChangelog(
|
|
|
192
192
|
context: DuxtSectionContext
|
|
193
193
|
): DuxtSectionPage[] {
|
|
194
194
|
const lines = artefact.split(/\r?\n/);
|
|
195
|
-
|
|
196
|
-
if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
|
|
197
|
-
|
|
198
195
|
const headings = headingsOf(lines);
|
|
199
|
-
|
|
200
196
|
const starts = headings.filter((heading) => release(heading.text));
|
|
201
197
|
|
|
198
|
+
const newest = starts[0] ? release(starts[0].text)?.version : undefined;
|
|
199
|
+
|
|
200
|
+
if (granularityOf(context.options) === 'flat') {
|
|
201
|
+
return [flat(lines, context, newest)];
|
|
202
|
+
}
|
|
203
|
+
|
|
202
204
|
reportNearMisses(headings, starts, context);
|
|
203
205
|
const releases: Release[] = starts.map((heading, index) => {
|
|
204
206
|
const parsed = release(heading.text)!;
|
|
@@ -236,11 +238,15 @@ function parseChangelog(
|
|
|
236
238
|
* exactly where the release tool put it — which is what "unchanged" has to
|
|
237
239
|
* mean, or the mode is not the escape hatch it exists to be.
|
|
238
240
|
*/
|
|
239
|
-
function flat(
|
|
241
|
+
function flat(
|
|
242
|
+
lines: string[],
|
|
243
|
+
context: DuxtSectionContext,
|
|
244
|
+
newest?: string
|
|
245
|
+
): DuxtSectionPage {
|
|
240
246
|
return {
|
|
241
247
|
file: 'index.md',
|
|
242
248
|
body: [
|
|
243
|
-
frontmatter({ title: context.label }),
|
|
249
|
+
frontmatter({ title: context.label, release: newest }),
|
|
244
250
|
'',
|
|
245
251
|
...trim(withoutTitle(lines)),
|
|
246
252
|
''
|
|
@@ -437,7 +443,11 @@ function index(
|
|
|
437
443
|
return {
|
|
438
444
|
file: 'index.md',
|
|
439
445
|
body: [
|
|
440
|
-
|
|
446
|
+
// The header stays outside this component, so it cannot read the MDC
|
|
447
|
+
// props below. Keep the newest release beside the page title instead:
|
|
448
|
+
// Content exposes frontmatter on the page query that `DuxtVersion` uses,
|
|
449
|
+
// and parsing the file again there would make the two answers drift.
|
|
450
|
+
frontmatter({ title: context.label, release: releases[0]?.version }),
|
|
441
451
|
'',
|
|
442
452
|
// NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
|
|
443
453
|
// title, description, the copy control beside it — for exactly the
|
package/sections-resolve.ts
CHANGED
|
@@ -368,10 +368,16 @@ export function resolveGeneratedSections(
|
|
|
368
368
|
// Claimed per locale, exactly as `resolveSources` claims its own prefixes:
|
|
369
369
|
// two languages serving one prefix is not a collision, it is the point.
|
|
370
370
|
const taken = new Map<string, string>(
|
|
371
|
-
resolved.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
resolved.flatMap((entry, index) =>
|
|
372
|
+
expanded[index]!.source.content === false
|
|
373
|
+
? []
|
|
374
|
+
: [
|
|
375
|
+
[
|
|
376
|
+
`${entry.locale ?? ''}|${entry.prefix}`,
|
|
377
|
+
`the documentation at "${entry.prefix || '/'}"`
|
|
378
|
+
] as [string, string]
|
|
379
|
+
]
|
|
380
|
+
)
|
|
375
381
|
);
|
|
376
382
|
|
|
377
383
|
// Counted over the whole list rather than per source, so the identity is
|
|
@@ -540,7 +546,10 @@ export function resolveGeneratedSections(
|
|
|
540
546
|
}
|
|
541
547
|
});
|
|
542
548
|
|
|
543
|
-
assertCollectionIdentities([
|
|
549
|
+
assertCollectionIdentities([
|
|
550
|
+
...resolved.filter((_, index) => expanded[index]!.source.content !== false),
|
|
551
|
+
...generated
|
|
552
|
+
]);
|
|
544
553
|
return generated;
|
|
545
554
|
}
|
|
546
555
|
|
|
@@ -706,8 +715,11 @@ export function duxtManifest(
|
|
|
706
715
|
options: DuxtSourcesOptions = {},
|
|
707
716
|
types: DuxtSectionTypes = duxtSectionTypes()
|
|
708
717
|
): DuxtResolvedSource[] {
|
|
718
|
+
const resolved = resolveSources(sources, options);
|
|
719
|
+
const expanded = expandSources(sources, options);
|
|
720
|
+
|
|
709
721
|
return [
|
|
710
|
-
...
|
|
722
|
+
...resolved.filter((_, index) => expanded[index]!.source.content !== false),
|
|
711
723
|
...resolveGeneratedSections(sources, options, types)
|
|
712
724
|
];
|
|
713
725
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
import { defineEventHandler, setHeader } from 'h3';
|
|
2
3
|
import { splitLocalePath } from '../../app/utils/locale-path';
|
|
3
4
|
import { sourcesForRoute } from '../../sources-resolve';
|
|
4
5
|
import { duxtDefaults, mergeDuxtConfig } from '../../app/utils/duxt-config';
|
package/sources-git.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
expandSources,
|
|
5
5
|
isLatestRef,
|
|
6
6
|
newestTag,
|
|
7
|
+
refIsTag,
|
|
8
|
+
refName,
|
|
7
9
|
repoUrl
|
|
8
10
|
} from './sources-resolve';
|
|
9
11
|
|
|
@@ -102,19 +104,33 @@ export function resolveLatestRefs(sources: DuxtSource[]): DuxtSource[] {
|
|
|
102
104
|
|
|
103
105
|
return {
|
|
104
106
|
...source,
|
|
105
|
-
refs: source.refs.
|
|
106
|
-
isLatestRef(ref)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
refs: source.refs.flatMap((ref) => {
|
|
108
|
+
if (isLatestRef(ref)) {
|
|
109
|
+
return {
|
|
110
|
+
tag: newest,
|
|
111
|
+
// The URL keeps saying `latest`, so a bookmark survives the next
|
|
112
|
+
// release; only the ref underneath moves. `label` is what the
|
|
113
|
+
// switcher and the prefix are built from.
|
|
114
|
+
label: typeof ref === 'object' ? (ref.label ?? 'latest') : 'latest',
|
|
115
|
+
// `latest` stops being distinguishable from an ordinary tag after
|
|
116
|
+
// resolution, so settle its source-level default while that fact
|
|
117
|
+
// still exists. An explicit ref or all-refs source status wins.
|
|
118
|
+
status:
|
|
119
|
+
(typeof ref === 'object' ? ref.status : undefined) ??
|
|
120
|
+
source.status ??
|
|
121
|
+
source.statusDefaults?.latest,
|
|
122
|
+
locales: typeof ref === 'object' ? ref.locales : undefined,
|
|
123
|
+
default: typeof ref === 'object' ? ref.default : undefined
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Keep an older release ready in the declaration, without serving the
|
|
128
|
+
// current release twice. Once a newer tag exists this condition stops
|
|
129
|
+
// matching and the retained release becomes its own edition.
|
|
130
|
+
if (refIsTag(ref) && refName(ref) === newest) return [];
|
|
131
|
+
|
|
132
|
+
return ref;
|
|
133
|
+
})
|
|
118
134
|
};
|
|
119
135
|
});
|
|
120
136
|
}
|
package/sources-resolve.ts
CHANGED
|
@@ -12,6 +12,14 @@ import type {
|
|
|
12
12
|
export interface DuxtSource {
|
|
13
13
|
/** Folder holding the Markdown, relative to the repository root. */
|
|
14
14
|
path?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether this source's Markdown is published as documentation pages.
|
|
17
|
+
*
|
|
18
|
+
* A source can carry generated sections without publishing its own tree. That
|
|
19
|
+
* lets a versioned documentation source and a generated artefact share their
|
|
20
|
+
* default URL prefix without declaring two page collections for it.
|
|
21
|
+
*/
|
|
22
|
+
content?: boolean;
|
|
15
23
|
/** `owner/name` or a full git URL. Omitted reads the local checkout. */
|
|
16
24
|
repo?: string;
|
|
17
25
|
/**
|
|
@@ -47,8 +55,8 @@ export interface DuxtSource {
|
|
|
47
55
|
*
|
|
48
56
|
* Named here, everything else follows the ref path exactly: the URL segment,
|
|
49
57
|
* the switcher entry (scoped to the same artefact), the banner and the
|
|
50
|
-
* canonical.
|
|
51
|
-
*
|
|
58
|
+
* canonical. A ref may name itself as the default; otherwise
|
|
59
|
+
* `sourceOptions.defaultRef` names which is served without a prefix.
|
|
52
60
|
*
|
|
53
61
|
* Never beside `refs` — a source with both would have to be served at two
|
|
54
62
|
* prefixes at once, and the resolver says so rather than picking one.
|
|
@@ -72,6 +80,12 @@ export interface DuxtSource {
|
|
|
72
80
|
* otherwise. See `DuxtSourceStatus`.
|
|
73
81
|
*/
|
|
74
82
|
status?: DuxtSourceStatus;
|
|
83
|
+
/**
|
|
84
|
+
* Lifecycle defaults by ref kind. An explicit ref `status` wins; `status`
|
|
85
|
+
* above remains the legacy all-refs fallback. A source opts in because only
|
|
86
|
+
* its maintainer knows whether an older release is really deprecated.
|
|
87
|
+
*/
|
|
88
|
+
statusDefaults?: DuxtSourceStatusDefaults;
|
|
75
89
|
/**
|
|
76
90
|
* The repository these pages live in, for a source read off disk.
|
|
77
91
|
*
|
|
@@ -126,6 +140,16 @@ export type DuxtSourceStatus =
|
|
|
126
140
|
| 'deprecated'
|
|
127
141
|
| 'eol';
|
|
128
142
|
|
|
143
|
+
/** Lifecycle defaults a source may opt into for its different ref kinds. */
|
|
144
|
+
export interface DuxtSourceStatusDefaults {
|
|
145
|
+
/** The moving `latest` shorthand, before it becomes a concrete tag. */
|
|
146
|
+
latest?: DuxtSourceStatus;
|
|
147
|
+
/** A tag explicitly listed beside `latest`. */
|
|
148
|
+
tag?: DuxtSourceStatus;
|
|
149
|
+
/** A named branch, such as `main` or a preview branch. */
|
|
150
|
+
branch?: DuxtSourceStatus;
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/** A branch by name, or a tag stated as one. */
|
|
130
154
|
export type DuxtRef =
|
|
131
155
|
| string
|
|
@@ -135,6 +159,13 @@ export type DuxtRef =
|
|
|
135
159
|
interface DuxtRefOptions {
|
|
136
160
|
/** Shown in the switcher and used in the URL; defaults to the ref name. */
|
|
137
161
|
label?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Serve this ref without a version prefix, even when another source has its
|
|
164
|
+
* own default. A site with several independently versioned trees needs this:
|
|
165
|
+
* its docs can serve the newest release at `/` while an API stays at its own
|
|
166
|
+
* current edition.
|
|
167
|
+
*/
|
|
168
|
+
default?: boolean;
|
|
138
169
|
/** This one version's lifecycle, overriding the source's. */
|
|
139
170
|
status?: DuxtSourceStatus;
|
|
140
171
|
/**
|
|
@@ -569,7 +600,10 @@ export function resolveSources(
|
|
|
569
600
|
const code = locale ? localeCode(locale) : defaultLocale;
|
|
570
601
|
const isDefaultLocale = !code || code === defaultLocale;
|
|
571
602
|
const version = name ? slugify(label ?? name) : undefined;
|
|
572
|
-
const isDefault =
|
|
603
|
+
const isDefault =
|
|
604
|
+
!name ||
|
|
605
|
+
(ref && typeof ref === 'object' && Boolean(ref.default)) ||
|
|
606
|
+
name === defaultRef;
|
|
573
607
|
|
|
574
608
|
const segments: string[] = [];
|
|
575
609
|
if (segmented(source)) segments.push(repoSlug(source));
|
|
@@ -589,21 +623,23 @@ export function resolveSources(
|
|
|
589
623
|
|
|
590
624
|
// Claimed per locale: two languages serving one prefix is not a collision,
|
|
591
625
|
// it is the point.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
`
|
|
600
|
-
|
|
626
|
+
if (source.content !== false) {
|
|
627
|
+
const claim = `${code ?? ''}|${prefix}`;
|
|
628
|
+
const previous = taken.get(claim);
|
|
629
|
+
if (previous) {
|
|
630
|
+
// The one ambiguity the build-time decision leaves: a docs folder named
|
|
631
|
+
// like a repository or a version. Rejected rather than resolved silently.
|
|
632
|
+
throw new Error(
|
|
633
|
+
`duxt: two sources resolve to the same URL prefix "${prefix || '/'}" ` +
|
|
634
|
+
`(${previous} and ${source.repo ?? 'this repository'}${ref ? `@${ref}` : ''}). ` +
|
|
635
|
+
'Give one of them a `slug` or a `label`.'
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
taken.set(
|
|
639
|
+
claim,
|
|
640
|
+
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
601
641
|
);
|
|
602
642
|
}
|
|
603
|
-
taken.set(
|
|
604
|
-
claim,
|
|
605
|
-
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
606
|
-
);
|
|
607
643
|
|
|
608
644
|
resolved.push({
|
|
609
645
|
collection,
|
|
@@ -629,6 +665,11 @@ export function resolveSources(
|
|
|
629
665
|
status:
|
|
630
666
|
(ref && typeof ref === 'object' ? ref.status : undefined) ??
|
|
631
667
|
source.status ??
|
|
668
|
+
(effectiveRef
|
|
669
|
+
? refIsTag(effectiveRef)
|
|
670
|
+
? source.statusDefaults?.tag
|
|
671
|
+
: source.statusDefaults?.branch
|
|
672
|
+
: undefined) ??
|
|
632
673
|
'current',
|
|
633
674
|
// A local source is a full checkout already; a remote one has to be
|
|
634
675
|
// unshallowed, which is why it has to be asked for.
|
|
@@ -636,7 +677,9 @@ export function resolveSources(
|
|
|
636
677
|
});
|
|
637
678
|
}
|
|
638
679
|
|
|
639
|
-
assertCollectionIdentities(
|
|
680
|
+
assertCollectionIdentities(
|
|
681
|
+
resolved.filter((_, index) => expanded[index]!.source.content !== false)
|
|
682
|
+
);
|
|
640
683
|
return resolved;
|
|
641
684
|
}
|
|
642
685
|
|
package/sources.ts
CHANGED
|
@@ -37,6 +37,8 @@ export const pageSchema = z.object({
|
|
|
37
37
|
icon: z.string().optional(),
|
|
38
38
|
/** `landing` renders the page without the docs chrome. */
|
|
39
39
|
layout: z.string().optional(),
|
|
40
|
+
/** The latest release of a generated changelog overview. */
|
|
41
|
+
release: z.string().optional(),
|
|
40
42
|
/** false hides the page from the navigation. */
|
|
41
43
|
navigation: z.boolean().optional(),
|
|
42
44
|
/**
|
|
@@ -153,6 +155,7 @@ export function duxtSources(
|
|
|
153
155
|
|
|
154
156
|
resolved.forEach((entry, index) => {
|
|
155
157
|
const { source, ref, effective } = expanded[index]!;
|
|
158
|
+
if (source.content === false) return;
|
|
156
159
|
|
|
157
160
|
// A locale entry may carry a ref of its own — a translation repository
|
|
158
161
|
// that tags on its own schedule.
|
|
@@ -228,7 +231,8 @@ function partialFolders(
|
|
|
228
231
|
const seen = new Set<string>();
|
|
229
232
|
|
|
230
233
|
resolved.forEach((entry, index) => {
|
|
231
|
-
const { effective } = expanded[index]!;
|
|
234
|
+
const { source, effective } = expanded[index]!;
|
|
235
|
+
if (source.content === false) return;
|
|
232
236
|
const key = entry.isDefaultLocale ? undefined : entry.locale;
|
|
233
237
|
|
|
234
238
|
// One entry per REPOSITORY AND FOLDER, not per version: a partial is a
|
package/validate-report.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { reservedSegments } from './sources-resolve';
|
|
12
12
|
import { packageCommandIssues } from './app/utils/package-command';
|
|
13
|
+
import { resolveDocumentationPath } from './app/utils/documentation-link';
|
|
13
14
|
|
|
14
15
|
export interface PageRecord {
|
|
15
16
|
collection: string;
|
|
@@ -203,9 +204,9 @@ export function report(
|
|
|
203
204
|
// verify, and checking it would put the network in the build.
|
|
204
205
|
//
|
|
205
206
|
// Resolved exactly as `ProseA` resolves them at render time: an absolute
|
|
206
|
-
// path written in a page is relative to that page's OWN source
|
|
207
|
-
//
|
|
208
|
-
//
|
|
207
|
+
// path written in a page is relative to that page's OWN source. `~/` is
|
|
208
|
+
// the one explicit website-root form. Checking a normal path bare as a
|
|
209
|
+
// fallback would green-light a rendered 404 from a prefixed source.
|
|
209
210
|
//
|
|
210
211
|
// PER LANGUAGE, and that is not a refinement. Every language of one source
|
|
211
212
|
// serves IDENTICAL content paths — the locale lives in front of the URL,
|
|
@@ -259,9 +260,15 @@ export function report(
|
|
|
259
260
|
|
|
260
261
|
for (const link of page.links) {
|
|
261
262
|
const { href } = link;
|
|
262
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
!href.startsWith('/') &&
|
|
265
|
+
!href.startsWith('~/') &&
|
|
266
|
+
!href.startsWith('#')
|
|
267
|
+
)
|
|
268
|
+
continue;
|
|
263
269
|
|
|
264
|
-
const [
|
|
270
|
+
const [targetAndQuery, fragment] = href.split('#');
|
|
271
|
+
const [target, query] = targetAndQuery!.split('?');
|
|
265
272
|
|
|
266
273
|
// DECODED, because the two halves are written in different alphabets. A
|
|
267
274
|
// heading's id is the text as it stands — `icônes` — while the link
|
|
@@ -269,20 +276,22 @@ export function report(
|
|
|
269
276
|
// accented anchor on the site as missing and told the author to point at
|
|
270
277
|
// a heading that was already there.
|
|
271
278
|
const anchor = fragment && decodeAnchor(fragment);
|
|
272
|
-
const
|
|
273
|
-
? (
|
|
274
|
-
:
|
|
279
|
+
const renderedTarget = target
|
|
280
|
+
? resolveDocumentationPath(target, prefix)
|
|
281
|
+
: target;
|
|
282
|
+
const renderedHref = `${renderedTarget}${query ? `?${query}` : ''}${fragment ? `#${fragment}` : ''}`;
|
|
283
|
+
const destination = renderedTarget ? resolve(renderedTarget, page) : page;
|
|
275
284
|
|
|
276
|
-
if (
|
|
285
|
+
if (renderedTarget && !destination) {
|
|
277
286
|
warnings.push(
|
|
278
|
-
`"${page.file}" links to "${
|
|
287
|
+
`"${page.file}" links to "${renderedHref}", which no page serves.`
|
|
279
288
|
);
|
|
280
289
|
continue;
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
if (anchor && destination && !destination.anchors.has(anchor)) {
|
|
284
293
|
warnings.push(
|
|
285
|
-
`"${page.file}" links to "${
|
|
294
|
+
`"${page.file}" links to "${renderedHref}", but that page has no "${anchor}" heading.`
|
|
286
295
|
);
|
|
287
296
|
}
|
|
288
297
|
}
|