@kirchdev/duxt 0.10.0 → 0.10.1-dev-main.8-902bddd
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.
|
@@ -42,7 +42,8 @@ const props = defineProps<{
|
|
|
42
42
|
|
|
43
43
|
const page = computed(() => props.page);
|
|
44
44
|
|
|
45
|
-
const { source } = useDuxtCollection();
|
|
45
|
+
const { source, sources } = useDuxtCollection();
|
|
46
|
+
const path = useDuxtPath();
|
|
46
47
|
const { locale, t } = useI18n();
|
|
47
48
|
const duxt = useDuxtConfig();
|
|
48
49
|
|
|
@@ -73,6 +74,52 @@ const link = computed(() =>
|
|
|
73
74
|
: undefined
|
|
74
75
|
);
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Where a reader of a TAG edits this page: the same page on the branch the
|
|
79
|
+
* source also publishes — see `editableEdition` — and only where that edition
|
|
80
|
+
* still has it, so a page renamed since the tag gets no form on a 404.
|
|
81
|
+
*
|
|
82
|
+
* The stem is read off the branch's own row rather than reused from this one:
|
|
83
|
+
* a numbered folder can move between the two, and the stem is the only thing
|
|
84
|
+
* that names the file.
|
|
85
|
+
*
|
|
86
|
+
* Nothing is read after the `await` but values captured before it — the rule
|
|
87
|
+
* `tests/async-data-context.test.ts` holds.
|
|
88
|
+
*/
|
|
89
|
+
const { data: editLink } = useAsyncData(
|
|
90
|
+
() => `duxt-edit-link-${path.value}`,
|
|
91
|
+
async () => {
|
|
92
|
+
const own = source.value;
|
|
93
|
+
const branch = editableEdition(sources.value, own);
|
|
94
|
+
if (!own || !branch) return null;
|
|
95
|
+
|
|
96
|
+
const counterpart = versionPath(
|
|
97
|
+
path.value,
|
|
98
|
+
own.prefix,
|
|
99
|
+
branch.prefix || '/'
|
|
100
|
+
);
|
|
101
|
+
const hit = await queryCollection(branch.collection as DuxtCollectionArg)
|
|
102
|
+
.path(counterpart)
|
|
103
|
+
.select('stem')
|
|
104
|
+
.first();
|
|
105
|
+
if (!hit) return null;
|
|
106
|
+
|
|
107
|
+
const target = branch.generated
|
|
108
|
+
? branch.path
|
|
109
|
+
: sourceFilePath(
|
|
110
|
+
hit.stem,
|
|
111
|
+
branch.prefix,
|
|
112
|
+
branch.originPath ?? branch.path
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
sourceLink(branch.repositoryUrl, branch.ref, branch.refKind, target) ??
|
|
117
|
+
null
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
{ default: () => null, watch: [path] }
|
|
121
|
+
);
|
|
122
|
+
|
|
76
123
|
/**
|
|
77
124
|
* The day this page names, where it names one.
|
|
78
125
|
*
|
|
@@ -130,6 +177,18 @@ const actions = computed(() => {
|
|
|
130
177
|
});
|
|
131
178
|
}
|
|
132
179
|
|
|
180
|
+
// A tag has no edit form of its own, but its branch may — see `editLink`.
|
|
181
|
+
// Drawn first, beside the tag's own source rather than instead of it: the
|
|
182
|
+
// reader may want either the text as released or the place to fix it.
|
|
183
|
+
if (editLink.value) {
|
|
184
|
+
entries.push({
|
|
185
|
+
icon: 'lucide:pencil',
|
|
186
|
+
label: t('duxt.page.edit'),
|
|
187
|
+
to: editLink.value.url,
|
|
188
|
+
external: true
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
133
192
|
// A tag has no edit form — see `sourceLink`. The label follows the kind
|
|
134
193
|
// rather than promising a form that answers with a 404.
|
|
135
194
|
if (link.value) {
|
package/app/types/duxt.d.ts
CHANGED
|
@@ -287,7 +287,7 @@ declare global {
|
|
|
287
287
|
* `duxt-typesense@v0.1.0`. Applies to `latest` and `releases`; plain
|
|
288
288
|
* `vX.Y.Z` tags still count.
|
|
289
289
|
*/
|
|
290
|
-
tagComponent?: string;
|
|
290
|
+
tagComponent?: string | readonly string[];
|
|
291
291
|
/**
|
|
292
292
|
* The repository a source read off disk lives in, for links back to it.
|
|
293
293
|
* Not `repo`, which is what makes Content download a source.
|
package/app/utils/source-file.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sameArtefact } from './version-choices';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* From a page back to the file it was written in.
|
|
3
5
|
*
|
|
@@ -82,3 +84,34 @@ export function sourceLink(
|
|
|
82
84
|
|
|
83
85
|
return undefined;
|
|
84
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The edition a reader of a TAG can edit instead: the branch the same source
|
|
90
|
+
* also publishes.
|
|
91
|
+
*
|
|
92
|
+
* `sourceLink` refuses an edit form on a tag, and rightly — but a reader of the
|
|
93
|
+
* default version is on a tag nearly always, so the refusal alone left the
|
|
94
|
+
* whole default edition without an edit link. The branch is where a fix to the
|
|
95
|
+
* page actually lands. Whether the page still EXISTS there is the caller's
|
|
96
|
+
* question, answered against that edition's collection before the link is
|
|
97
|
+
* drawn: a file renamed since the tag would otherwise be an edit form on a 404.
|
|
98
|
+
*
|
|
99
|
+
* Same repository, same artefact and same language, so a German page of
|
|
100
|
+
* v0.9.0 is edited as the German page on `main` and an API reference as the
|
|
101
|
+
* API reference. The first such branch in manifest order wins.
|
|
102
|
+
*/
|
|
103
|
+
export function editableEdition(
|
|
104
|
+
sources: DuxtResolvedSource[],
|
|
105
|
+
current: DuxtResolvedSource | undefined
|
|
106
|
+
): DuxtResolvedSource | undefined {
|
|
107
|
+
if (current?.refKind !== 'tag' || !current.repositoryUrl) return undefined;
|
|
108
|
+
|
|
109
|
+
return sources.find(
|
|
110
|
+
(source) =>
|
|
111
|
+
source.refKind === 'branch' &&
|
|
112
|
+
Boolean(source.ref) &&
|
|
113
|
+
source.repositoryUrl === current.repositoryUrl &&
|
|
114
|
+
source.locale === current.locale &&
|
|
115
|
+
sameArtefact(source, current)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -159,28 +159,54 @@ function tagsFor(source: DuxtSource, what: string): string[] {
|
|
|
159
159
|
/**
|
|
160
160
|
* The version tags a source may publish from: every tag that reads as a
|
|
161
161
|
* version and belongs to the source's `tagComponent`, when it names one.
|
|
162
|
+
*
|
|
163
|
+
* A family of components shares one version line, so one release is often
|
|
164
|
+
* tagged once per member — `duxt@v0.10.0` beside `duxt-typesense@v0.10.0`.
|
|
165
|
+
* Those are one version, kept once, from the member listed first.
|
|
162
166
|
*/
|
|
163
167
|
function versionTags(
|
|
164
168
|
tags: string[],
|
|
165
|
-
component: string | undefined
|
|
169
|
+
component: string | readonly string[] | undefined
|
|
166
170
|
): { tag: string; version: DuxtVersionTag }[] {
|
|
167
|
-
|
|
171
|
+
const matching = tags
|
|
168
172
|
.map((tag) => ({ tag, version: parseVersionTag(tag) }))
|
|
169
173
|
.filter(
|
|
170
174
|
(entry): entry is { tag: string; version: DuxtVersionTag } =>
|
|
171
175
|
Boolean(entry.version) && tagBelongsTo(entry.version!, component)
|
|
172
176
|
);
|
|
177
|
+
if (typeof component === 'string' || !component) return matching;
|
|
178
|
+
|
|
179
|
+
const rank = (entry: { version: DuxtVersionTag }) =>
|
|
180
|
+
entry.version.component
|
|
181
|
+
? component.indexOf(entry.version.component)
|
|
182
|
+
: component.length;
|
|
183
|
+
const byVersion = new Map<string, { tag: string; version: DuxtVersionTag }>();
|
|
184
|
+
for (const entry of matching) {
|
|
185
|
+
const key = versionKey(entry.version);
|
|
186
|
+
const kept = byVersion.get(key);
|
|
187
|
+
if (!kept || rank(entry) < rank(kept)) byVersion.set(key, entry);
|
|
188
|
+
}
|
|
189
|
+
return [...byVersion.values()];
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
/** One version regardless of who tagged it or whether it carries a `v`. */
|
|
193
|
+
const versionKey = (version: DuxtVersionTag) =>
|
|
194
|
+
version.numbers.join('.') + (version.pre ? `-${version.pre}` : '');
|
|
195
|
+
|
|
175
196
|
/** How a source's tag restriction reads in an error, or nothing without one. */
|
|
176
|
-
const componentClause = (source: DuxtSource) =>
|
|
177
|
-
|
|
197
|
+
const componentClause = (source: DuxtSource) => {
|
|
198
|
+
const names = source.tagComponent;
|
|
199
|
+
if (!names || names.length === 0) return '';
|
|
200
|
+
return typeof names === 'string'
|
|
201
|
+
? ` for the component "${names}"`
|
|
202
|
+
: ` for the components ${names.map((name) => `"${name}"`).join(', ')}`;
|
|
203
|
+
};
|
|
178
204
|
|
|
179
205
|
/** Tags selected by a source's explicit release policy, newest first. */
|
|
180
206
|
function releaseTags(
|
|
181
207
|
tags: string[],
|
|
182
208
|
releases: DuxtSourceReleases,
|
|
183
|
-
component: string | undefined
|
|
209
|
+
component: string | readonly string[] | undefined
|
|
184
210
|
): string[] {
|
|
185
211
|
const parsed = versionTags(tags, component)
|
|
186
212
|
.filter((entry) => releases.prereleases || !entry.version.pre)
|
|
@@ -60,8 +60,13 @@ export interface DuxtSource {
|
|
|
60
60
|
* component tags, and dropping them would unpublish every earlier release.
|
|
61
61
|
*
|
|
62
62
|
* Unset, every tag that reads as a version is a candidate, prefixed or not.
|
|
63
|
+
*
|
|
64
|
+
* A LIST names a family released on one version line — the layer and its
|
|
65
|
+
* companion layers under `versioning: linked` — so `latest` and `releases`
|
|
66
|
+
* follow whichever of them released last. One version carried by several
|
|
67
|
+
* of them is one release, published from the tag of the name listed first.
|
|
63
68
|
*/
|
|
64
|
-
tagComponent?: string;
|
|
69
|
+
tagComponent?: string | readonly string[];
|
|
65
70
|
/**
|
|
66
71
|
* Languages this source is available in, beyond the one written in `path`.
|
|
67
72
|
*
|
|
@@ -964,9 +969,12 @@ export const versionTagName = (tag: string): string =>
|
|
|
964
969
|
*/
|
|
965
970
|
export function tagBelongsTo(
|
|
966
971
|
tag: DuxtVersionTag,
|
|
967
|
-
component: string | undefined
|
|
972
|
+
component: string | readonly string[] | undefined
|
|
968
973
|
): boolean {
|
|
969
|
-
|
|
974
|
+
if (!component || !tag.component) return true;
|
|
975
|
+
return typeof component === 'string'
|
|
976
|
+
? tag.component === component
|
|
977
|
+
: component.includes(tag.component);
|
|
970
978
|
}
|
|
971
979
|
|
|
972
980
|
/**
|
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.10.
|
|
4
|
+
"version": "0.10.1-dev-main.8-902bddd",
|
|
5
5
|
"description": "duxt is a Nuxt layer: extend it and your docs/ folder becomes a site, with theme, search, API reference and llms.txt included. Point it at other repositories, or at tags of the same one, and each becomes a version.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a request alive on Cloudflare until it has produced its response, even
|
|
3
|
+
* after the reader has gone.
|
|
4
|
+
*
|
|
5
|
+
* Content restores a collection into D1 the first time a request reads it
|
|
6
|
+
* after a deploy: it writes the collection's `_content_info` row with
|
|
7
|
+
* `ready = 0`, replays the dump one statement at a time — about ten seconds a
|
|
8
|
+
* collection on duxt.app — and flips the row to ready as the last statement.
|
|
9
|
+
* A Worker CANCELS a request's work the moment its client disconnects. A
|
|
10
|
+
* reader who gives up, a crawler with a timeout, a prefetch the router drops:
|
|
11
|
+
* any of them stops that replay halfway, and the row is never flipped. Every
|
|
12
|
+
* later read of that collection then waits on `ready` for 90 seconds and
|
|
13
|
+
* fails, on every isolate, until the row is removed by hand — and a docs page
|
|
14
|
+
* reads every version's collection to draw its version switcher, so one stuck
|
|
15
|
+
* collection hangs every page of every version. duxt.app ran into exactly
|
|
16
|
+
* that after the v0.10.0 deploy.
|
|
17
|
+
*
|
|
18
|
+
* `waitUntil` is the one thing a Worker lets outlive a disconnect, so every
|
|
19
|
+
* request hands it a promise that settles when the request is done. Anything
|
|
20
|
+
* it started — the restore included — then runs to completion inside the
|
|
21
|
+
* platform's grace period rather than being cut off. Other runtimes do not
|
|
22
|
+
* cancel on disconnect, so this only arms where `cloudflare` is in the context.
|
|
23
|
+
*/
|
|
24
|
+
const settle = Symbol('duxt.settle');
|
|
25
|
+
|
|
26
|
+
type Settling = { [settle]?: () => void };
|
|
27
|
+
|
|
28
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
29
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
30
|
+
if (!event.context.cloudflare || !event.waitUntil) return;
|
|
31
|
+
|
|
32
|
+
event.waitUntil(
|
|
33
|
+
new Promise<void>((resolve) => {
|
|
34
|
+
(event.context as Settling)[settle] = resolve;
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const done = (event: { context: Settling } | undefined) => {
|
|
40
|
+
event?.context[settle]?.();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
nitroApp.hooks.hook('afterResponse', (event) => done(event));
|
|
44
|
+
nitroApp.hooks.hook('error', (_error, { event }) => done(event));
|
|
45
|
+
});
|