@kirchdev/duxt 0.3.4-dev-main.40-7fa9937 → 0.3.4

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.
@@ -24,7 +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
+ const { source } = useDuxtCollection();
28
28
 
29
29
  /**
30
30
  * Versions come from the resolved source manifest, so the control can only
@@ -35,31 +35,13 @@ const { collection, source } = useDuxtCollection();
35
35
  */
36
36
  const { choices: versions } = useDuxtVersion();
37
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
- */
38
+ /** A global changelog switches between its source's documentation editions. */
45
39
  const changelog = computed(
46
40
  () =>
47
41
  source.value?.generated?.type === 'changelog' &&
48
42
  source.value.generated.versioning === 'global'
49
43
  );
50
44
 
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
-
63
45
  const current = computed(() =>
64
46
  sourceForPath(
65
47
  path.value,
@@ -67,9 +49,7 @@ const current = computed(() =>
67
49
  )
68
50
  );
69
51
 
70
- const label = computed(
71
- () => changelogPage.value?.release ?? current.value?.label ?? duxt.version
72
- );
52
+ const label = computed(() => current.value?.label ?? duxt.version);
73
53
 
74
54
  const { t, te } = useI18n();
75
55
 
@@ -92,7 +72,12 @@ function caption(version: DuxtLink) {
92
72
  /** Same page, other version: swap the prefix rather than jumping to its root. */
93
73
  function pathIn(version: { to?: string }) {
94
74
  return localeLink(
95
- versionPath(path.value, current.value?.to, version.to ?? '/')
75
+ versionPath(
76
+ path.value,
77
+ current.value?.to,
78
+ version.to ?? '/',
79
+ changelog.value
80
+ )
96
81
  );
97
82
  }
98
83
  </script>
@@ -15,14 +15,60 @@
15
15
  * that lives there: prefix work done inline in a component can only be checked
16
16
  * by clicking.
17
17
  */
18
- import { isInside, sourceForPath } from './version-paths';
18
+ import { isInside, sourceForPath, versionPath } from './version-paths';
19
19
 
20
20
  /** The minimum a source has to carry to place a path or an entry. */
21
21
  interface Area {
22
22
  prefix: string;
23
+ repo?: string;
24
+ version?: string;
23
25
  generated?: unknown;
24
26
  }
25
27
 
28
+ /** The documentation source that owns an area, without a generated section. */
29
+ function documentationFor(source: Area | undefined, sources: Area[]) {
30
+ if (!source?.generated) return source;
31
+
32
+ const candidates = source.repo
33
+ ? sources.filter(
34
+ (other) =>
35
+ !other.generated &&
36
+ other.repo === source.repo &&
37
+ (!source.version || other.version === source.version)
38
+ )
39
+ : sources.filter(
40
+ (other) => !other.generated && isInside(source.prefix, other.prefix)
41
+ );
42
+
43
+ // A generated section belongs to the deepest documentation source it hangs
44
+ // under. A global section has no version of its own, so its area is the
45
+ // shallowest edition of that source instead.
46
+ return candidates.sort((a, b) =>
47
+ source.version || !source.repo
48
+ ? b.prefix.length - a.prefix.length
49
+ : a.prefix.length - b.prefix.length
50
+ )[0];
51
+ }
52
+
53
+ /** The version-neutral root of a documentation area. */
54
+ function areaRoot(source: Area | undefined, sources: Area[]): string {
55
+ const documentation = documentationFor(source, sources);
56
+ if (!documentation) return '';
57
+
58
+ // A slug is the source identity and survives the version segment. All
59
+ // versions of `/demo` are therefore one area, whose shallowest prefix is
60
+ // the default edition. Sources without an identity retain their own prefix.
61
+ if (!documentation.repo) return documentation.prefix;
62
+
63
+ return sources
64
+ .filter((other) => !other.generated && other.repo === documentation.repo)
65
+ .reduce(
66
+ (root, other) =>
67
+ other.prefix.length < root.prefix.length ? other : root,
68
+ documentation
69
+ ).prefix;
70
+ }
71
+
26
72
  /**
27
73
  * The AREAS a site has — one per documentation tree, never per generated
28
74
  * section.
@@ -33,7 +79,11 @@ interface Area {
33
79
  * of the site and the row would empty out on it.
34
80
  */
35
81
  const areas = (sources: Area[]): string[] => [
36
- ...new Set(sources.filter((source) => !source.generated).map((s) => s.prefix))
82
+ ...new Set(
83
+ sources
84
+ .filter((source) => !source.generated)
85
+ .map((source) => areaRoot(source, sources))
86
+ )
37
87
  ];
38
88
 
39
89
  /**
@@ -43,12 +93,7 @@ const areas = (sources: Area[]): string[] => [
43
93
  * never asked for any of this.
44
94
  */
45
95
  export function areaForPath(path: string, sources: Area[]): string {
46
- return (
47
- sourceForPath(
48
- path,
49
- areas(sources).map((prefix) => ({ prefix }))
50
- )?.prefix ?? ''
51
- );
96
+ return areaRoot(sourceForPath(path, sources), sources);
52
97
  }
53
98
 
54
99
  /**
@@ -66,13 +111,40 @@ export function sectionsForPath<T extends { to?: string }>(
66
111
  const area = areaForPath(path, sources);
67
112
  const all = areas(sources);
68
113
 
69
- // Only when the site HAS two areas: with one, every entry is in it, and the
70
- // filter is a walk over the list for an answer that cannot change.
71
- if (all.length < 2) return sections;
114
+ // Only when the site HAS two areas does the filter change the membership.
115
+ // Even one area's hand-written entries may need their URL moved to the
116
+ // reader's version, so do not return before that mapping below.
117
+ const scoped =
118
+ all.length < 2
119
+ ? sections
120
+ : sections.filter(
121
+ (section) => !section.to || areaForPath(section.to, sources) === area
122
+ );
123
+
124
+ const reader = documentationFor(sourceForPath(path, sources), sources);
125
+ if (!reader?.version) return scoped;
126
+
127
+ return scoped.map((section) => {
128
+ const target = section.to ? sourceForPath(section.to, sources) : undefined;
129
+
130
+ // A consumer declares the overview once, at the default edition. It is a
131
+ // part of every version of the same documentation source, so move it to
132
+ // the reader's edition. Generated sections already resolve their own
133
+ // per-version destination, and global generated sections must stay global.
134
+ if (
135
+ target &&
136
+ !target.generated &&
137
+ target.repo === reader.repo &&
138
+ target.version !== reader.version
139
+ ) {
140
+ return {
141
+ ...section,
142
+ to: versionPath(section.to!, target.prefix, reader.prefix)
143
+ } as T;
144
+ }
72
145
 
73
- return sections.filter(
74
- (section) => !section.to || areaForPath(section.to, sources) === area
75
- );
146
+ return section;
147
+ });
76
148
  }
77
149
 
78
150
  /**
@@ -65,11 +65,22 @@ export function versionChoices(
65
65
  current: DuxtResolvedSource | undefined,
66
66
  configured: DuxtLink[] | undefined
67
67
  ): DuxtLink[] {
68
- // A version-neutral generated section suppresses the control entirely, and
69
- // that is the point of the policy rather than a tidy-up: a changelog is one
70
- // global history, so every entry the switcher could offer would move the
71
- // reader to a URL that section does not serve.
72
- if (current?.generated?.versioning === 'global') return [];
68
+ // A global generated section has no version of its own, but its reader still
69
+ // needs the documentation context of the source that owns it. Start from
70
+ // that source's default edition: its choices lead to the version overviews,
71
+ // never to non-existent versioned copies of the generated section.
72
+ const global = current?.generated?.versioning === 'global';
73
+
74
+ if (global && current?.generated?.type !== 'changelog') return [];
75
+
76
+ const context = global
77
+ ? sources.find(
78
+ (source) =>
79
+ source.repo === current.repo && source.isDefault && !source.generated
80
+ )
81
+ : current;
82
+
83
+ if (!context) return [];
73
84
 
74
85
  // `versions` in the config still wins where a label needs to read differently
75
86
  // from the URL segment.
@@ -81,8 +92,8 @@ export function versionChoices(
81
92
  .filter(
82
93
  (source) =>
83
94
  source.version &&
84
- source.repo === current?.repo &&
85
- sameArtefact(source, current)
95
+ source.repo === context.repo &&
96
+ sameArtefact(source, context)
86
97
  )
87
98
  .filter((source) => {
88
99
  // Repository and artefact are scoped above; translations share the
@@ -43,8 +43,11 @@ export function isInside(path: string, prefix: string): boolean {
43
43
  export function versionPath(
44
44
  path: string,
45
45
  from: string | undefined,
46
- to: string
46
+ to: string,
47
+ overview = false
47
48
  ): string {
49
+ if (overview) return to || '/';
50
+
48
51
  const rest =
49
52
  from && from !== '/' && isInside(path, from)
50
53
  ? path.slice(from.length)
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.3.4-dev-main.40-7fa9937",
4
+ "version": "0.3.4",
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",