@kirchdev/duxt 0.7.0 → 0.7.1-dev-main.54-3461f6d
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.
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
10
|
+
import { resolvesInside } from '../sections/section-input';
|
|
11
|
+
|
|
12
|
+
const PRIVATE_DIRECTORIES = new Set([
|
|
13
|
+
'.git',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.data',
|
|
16
|
+
'.nuxt',
|
|
17
|
+
'.output',
|
|
18
|
+
'.turbo',
|
|
19
|
+
'.cache'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const IMAGES = new Set([
|
|
23
|
+
'.png',
|
|
24
|
+
'.jpg',
|
|
25
|
+
'.jpeg',
|
|
26
|
+
'.gif',
|
|
27
|
+
'.svg',
|
|
28
|
+
'.webp',
|
|
29
|
+
'.avif',
|
|
30
|
+
'.apng',
|
|
31
|
+
'.ico'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export const documentationMediaDirectory = (root = process.cwd()) =>
|
|
35
|
+
join(root, '.data/duxt-media/public');
|
|
36
|
+
|
|
37
|
+
export interface DocumentationMedia {
|
|
38
|
+
root: string;
|
|
39
|
+
prefix: string;
|
|
40
|
+
files: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Publish only image outputs in _assets, leaving capture sources private. */
|
|
44
|
+
export function publishDocumentationMedia(
|
|
45
|
+
root: string,
|
|
46
|
+
output: string,
|
|
47
|
+
identity: string,
|
|
48
|
+
checkout?: string
|
|
49
|
+
): DocumentationMedia {
|
|
50
|
+
const prefix = `/_duxt/media/${createHash('sha256').update(identity).digest('hex')}`;
|
|
51
|
+
const media: DocumentationMedia = { root, prefix, files: [] };
|
|
52
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink()) return media;
|
|
53
|
+
if (checkout && !resolvesInside(root, checkout)) return media;
|
|
54
|
+
const outputRoot = resolve(output);
|
|
55
|
+
const descend = (directory: string, asset = false) => {
|
|
56
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
57
|
+
if (entry.isSymbolicLink()) continue;
|
|
58
|
+
const absolute = join(directory, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (
|
|
61
|
+
!PRIVATE_DIRECTORIES.has(entry.name) &&
|
|
62
|
+
resolve(absolute) !== outputRoot
|
|
63
|
+
)
|
|
64
|
+
descend(absolute, asset || entry.name === '_assets');
|
|
65
|
+
} else if (
|
|
66
|
+
asset &&
|
|
67
|
+
entry.isFile() &&
|
|
68
|
+
IMAGES.has(extname(entry.name).toLowerCase())
|
|
69
|
+
) {
|
|
70
|
+
const target = join(output, prefix, relative(root, absolute));
|
|
71
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
72
|
+
copyFileSync(absolute, target);
|
|
73
|
+
media.files.push(relative(root, absolute).split(sep).join('/'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
descend(root);
|
|
78
|
+
return media;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve file-relative documentation images; external/public images pass through. */
|
|
82
|
+
export function documentationImageUrl(
|
|
83
|
+
value: string,
|
|
84
|
+
file: string,
|
|
85
|
+
media: DocumentationMedia
|
|
86
|
+
): string {
|
|
87
|
+
if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(value)) return value;
|
|
88
|
+
const owner = relative(media.root, file);
|
|
89
|
+
if (owner === '..' || owner.startsWith(`..${sep}`)) return value;
|
|
90
|
+
const match = /^([^?#]*)(.*)$/.exec(value)!;
|
|
91
|
+
let path: string;
|
|
92
|
+
try {
|
|
93
|
+
path = decodeURIComponent(match[1]!);
|
|
94
|
+
} catch {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
const absolute = path.startsWith('/')
|
|
98
|
+
? resolve(media.root, `.${path}`)
|
|
99
|
+
: resolve(dirname(file), path);
|
|
100
|
+
const rel = relative(media.root, absolute).split(sep).join('/');
|
|
101
|
+
if (
|
|
102
|
+
rel.startsWith('../') ||
|
|
103
|
+
rel === '..' ||
|
|
104
|
+
!rel.split('/').includes('_assets') ||
|
|
105
|
+
!media.files.includes(rel)
|
|
106
|
+
)
|
|
107
|
+
return value;
|
|
108
|
+
return `${media.prefix}/${rel.split('/').map(encodeURIComponent).join('/')}${match[2]}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Rewrite only image nodes in Content's compressed MDC tree. */
|
|
112
|
+
export function rewriteDocumentationImages(
|
|
113
|
+
content: Record<string, unknown>,
|
|
114
|
+
file: string,
|
|
115
|
+
media: DocumentationMedia
|
|
116
|
+
): void {
|
|
117
|
+
const walk = (node: unknown) => {
|
|
118
|
+
if (Array.isArray(node)) {
|
|
119
|
+
const [tag, props] = node;
|
|
120
|
+
if (
|
|
121
|
+
['img', 'prose-img', 'ProseImg'].includes(tag) &&
|
|
122
|
+
props &&
|
|
123
|
+
typeof props === 'object'
|
|
124
|
+
) {
|
|
125
|
+
for (const key of ['src', 'dark']) {
|
|
126
|
+
if (typeof props[key] === 'string')
|
|
127
|
+
props[key] = documentationImageUrl(props[key], file, media);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const child of node) walk(child);
|
|
131
|
+
} else if (node && typeof node === 'object') {
|
|
132
|
+
for (const child of Object.values(node)) walk(child);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk(content.body);
|
|
136
|
+
}
|
package/build/sources/sources.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { resolveLatestRefs } from './sources-git';
|
|
|
14
14
|
// module could reach it without this file's imports coming with it.
|
|
15
15
|
import { repositoryRoot } from './repository-root';
|
|
16
16
|
import { resolvesInside } from '../sections/section-input';
|
|
17
|
+
import {
|
|
18
|
+
documentationMediaDirectory,
|
|
19
|
+
publishDocumentationMedia
|
|
20
|
+
} from './documentation-media';
|
|
17
21
|
export { repositoryRoot };
|
|
18
22
|
export type {
|
|
19
23
|
DuxtResolvedSource,
|
|
@@ -232,7 +236,9 @@ const excluded = (source: DuxtSource) => {
|
|
|
232
236
|
const partials = source.exclude?.partials ?? PARTIALS;
|
|
233
237
|
const drafts = source.exclude?.drafts ?? DRAFTS;
|
|
234
238
|
|
|
235
|
-
return includeDrafts()
|
|
239
|
+
return includeDrafts()
|
|
240
|
+
? [partials, '**/_assets/**']
|
|
241
|
+
: [partials, '**/_assets/**', drafts];
|
|
236
242
|
};
|
|
237
243
|
|
|
238
244
|
/**
|
|
@@ -315,6 +321,20 @@ export function duxtSources(
|
|
|
315
321
|
|
|
316
322
|
containedKeys(collection);
|
|
317
323
|
|
|
324
|
+
const base = source.path ?? 'docs';
|
|
325
|
+
const mediaPath =
|
|
326
|
+
effective.path === base || effective.path.startsWith(`${base}/`)
|
|
327
|
+
? base
|
|
328
|
+
: effective.path;
|
|
329
|
+
const identity = JSON.stringify([
|
|
330
|
+
effective.repo ?? repositoryRoot(),
|
|
331
|
+
mediaPath,
|
|
332
|
+
usedRef ? [refIsTag(usedRef) ? 'tag' : 'branch', refName(usedRef)] : null
|
|
333
|
+
]);
|
|
334
|
+
for (const contentSource of collection.source ?? []) {
|
|
335
|
+
prepareMedia(contentSource, effective.repo, mediaPath, identity);
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
collections[entry.collection] = collection;
|
|
319
339
|
});
|
|
320
340
|
|
|
@@ -327,6 +347,37 @@ export function duxtSources(
|
|
|
327
347
|
return collections;
|
|
328
348
|
}
|
|
329
349
|
|
|
350
|
+
/** Prepare media for a source, including partial sources sharing a collection. */
|
|
351
|
+
function prepareMedia(
|
|
352
|
+
contentSource: NonNullable<
|
|
353
|
+
ReturnType<typeof defineCollection>['source']
|
|
354
|
+
>[number],
|
|
355
|
+
repo: string | undefined,
|
|
356
|
+
mediaPath: string,
|
|
357
|
+
identity: string
|
|
358
|
+
) {
|
|
359
|
+
const output = documentationMediaDirectory();
|
|
360
|
+
const keys = contentSource.getKeys!;
|
|
361
|
+
const prepared = contentSource as typeof contentSource & {
|
|
362
|
+
duxtMediaIdentity?: string;
|
|
363
|
+
duxtMedia?: import('./documentation-media').DocumentationMedia;
|
|
364
|
+
};
|
|
365
|
+
// Include source ownership in Content's checksum, even when Markdown is unchanged.
|
|
366
|
+
prepared.duxtMediaIdentity = identity;
|
|
367
|
+
contentSource.getKeys = async () => {
|
|
368
|
+
const root = repo
|
|
369
|
+
? join(contentSource.cwd, mediaPath)
|
|
370
|
+
: join(repositoryRoot(), mediaPath);
|
|
371
|
+
prepared.duxtMedia = publishDocumentationMedia(
|
|
372
|
+
root,
|
|
373
|
+
output,
|
|
374
|
+
identity,
|
|
375
|
+
repo ? contentSource.cwd : undefined
|
|
376
|
+
);
|
|
377
|
+
return keys();
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
/**
|
|
331
382
|
* Keep a downloaded repository's keys to files that are inside its checkout.
|
|
332
383
|
*
|
|
@@ -367,11 +418,11 @@ function partialFolders(
|
|
|
367
418
|
expanded: ReturnType<typeof expandSources>
|
|
368
419
|
): Map<
|
|
369
420
|
string | undefined,
|
|
370
|
-
{ repo?: string; path: string; partials: string }[]
|
|
421
|
+
{ repo?: string; path: string; partials: string; mediaPath: string }[]
|
|
371
422
|
> {
|
|
372
423
|
const byLocale = new Map<
|
|
373
424
|
string | undefined,
|
|
374
|
-
{ repo?: string; path: string; partials: string }[]
|
|
425
|
+
{ repo?: string; path: string; partials: string; mediaPath: string }[]
|
|
375
426
|
>();
|
|
376
427
|
const seen = new Set<string>();
|
|
377
428
|
|
|
@@ -389,7 +440,17 @@ function partialFolders(
|
|
|
389
440
|
seen.add(claim);
|
|
390
441
|
|
|
391
442
|
const list = byLocale.get(key) ?? [];
|
|
392
|
-
|
|
443
|
+
const base = source.path ?? 'docs';
|
|
444
|
+
const mediaPath =
|
|
445
|
+
effective.path === base || effective.path.startsWith(`${base}/`)
|
|
446
|
+
? base
|
|
447
|
+
: effective.path;
|
|
448
|
+
list.push({
|
|
449
|
+
repo: effective.repo,
|
|
450
|
+
path: effective.path,
|
|
451
|
+
partials,
|
|
452
|
+
mediaPath
|
|
453
|
+
});
|
|
393
454
|
byLocale.set(key, list);
|
|
394
455
|
});
|
|
395
456
|
|
|
@@ -411,7 +472,12 @@ function partialFolders(
|
|
|
411
472
|
* than resolves; see `modules/validate.ts`.
|
|
412
473
|
*/
|
|
413
474
|
function definePartials(
|
|
414
|
-
folders: {
|
|
475
|
+
folders: {
|
|
476
|
+
repo?: string;
|
|
477
|
+
path: string;
|
|
478
|
+
partials: string;
|
|
479
|
+
mediaPath: string;
|
|
480
|
+
}[]
|
|
415
481
|
) {
|
|
416
482
|
const entries = folders.map((folder) =>
|
|
417
483
|
folder.repo
|
|
@@ -438,6 +504,16 @@ function definePartials(
|
|
|
438
504
|
});
|
|
439
505
|
|
|
440
506
|
containedKeys(collection);
|
|
507
|
+
for (const [index, contentSource] of (collection.source ?? []).entries()) {
|
|
508
|
+
const folder = folders[index]!;
|
|
509
|
+
// Partials stay shared across versions and read the repository's default ref.
|
|
510
|
+
const identity = JSON.stringify([
|
|
511
|
+
folder.repo ?? repositoryRoot(),
|
|
512
|
+
folder.mediaPath,
|
|
513
|
+
null
|
|
514
|
+
]);
|
|
515
|
+
prepareMedia(contentSource, folder.repo, folder.mediaPath, identity);
|
|
516
|
+
}
|
|
441
517
|
|
|
442
518
|
return collection;
|
|
443
519
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import type { Nuxt } from '@nuxt/schema';
|
|
3
|
+
import type { ModuleOptions } from '@nuxt/image';
|
|
4
|
+
import {
|
|
5
|
+
documentationMediaDirectory,
|
|
6
|
+
rewriteDocumentationImages
|
|
7
|
+
} from '../build/sources/documentation-media';
|
|
8
|
+
import type { DocumentationMedia } from '../build/sources/documentation-media';
|
|
9
|
+
|
|
10
|
+
interface ParseContext {
|
|
11
|
+
file?: { path?: string };
|
|
12
|
+
content?: Record<string, unknown>;
|
|
13
|
+
collection?: { source?: { duxtMedia?: DocumentationMedia }[] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function duxtDocumentationMedia(_options: unknown, nuxt: Nuxt) {
|
|
17
|
+
const directory = documentationMediaDirectory(nuxt.options.rootDir);
|
|
18
|
+
// Rebuild the generated output even when Content reuses every parsed page.
|
|
19
|
+
// Removed source assets must not survive a later build of the same ref.
|
|
20
|
+
rmSync(directory, { recursive: true, force: true });
|
|
21
|
+
mkdirSync(directory, { recursive: true });
|
|
22
|
+
const image = nuxt.options.image as
|
|
23
|
+
| Partial<ModuleOptions>
|
|
24
|
+
| false
|
|
25
|
+
| undefined;
|
|
26
|
+
if (image !== false) {
|
|
27
|
+
const options = image ?? {};
|
|
28
|
+
options.dirs = [...(options.dirs ?? []), directory];
|
|
29
|
+
nuxt.options.image = options as ModuleOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nuxt.hook('nitro:config', (nitro) => {
|
|
33
|
+
nitro.publicAssets ??= [];
|
|
34
|
+
if (!nitro.publicAssets.some((asset) => asset?.dir === directory)) {
|
|
35
|
+
nitro.publicAssets.push({ dir: directory, maxAge: 0 });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
nuxt.hook(
|
|
40
|
+
'content:file:afterParse' as never,
|
|
41
|
+
((context: ParseContext) => {
|
|
42
|
+
const file = context.file?.path;
|
|
43
|
+
if (!file || !context.content) return;
|
|
44
|
+
for (const source of context.collection?.source ?? []) {
|
|
45
|
+
const media = source.duxtMedia;
|
|
46
|
+
if (media) rewriteDocumentationImages(context.content, file, media);
|
|
47
|
+
}
|
|
48
|
+
}) as never
|
|
49
|
+
);
|
|
50
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -252,6 +252,9 @@ export default defineNuxtConfig({
|
|
|
252
252
|
// history at parse time.
|
|
253
253
|
layer('./modules/git-meta.ts'),
|
|
254
254
|
|
|
255
|
+
// Mount documentation media before @nuxt/image discovers its input dirs.
|
|
256
|
+
layer('./modules/documentation-media.ts'),
|
|
257
|
+
|
|
255
258
|
// `redirectFrom:` in a page's frontmatter, under every prefix the site
|
|
256
259
|
// serves that page at.
|
|
257
260
|
layer('./modules/redirects.ts'),
|
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.7.
|
|
4
|
+
"version": "0.7.1-dev-main.54-3461f6d",
|
|
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",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"@codemirror/state": "^6.7.6",
|
|
76
76
|
"@codemirror/view": "^6.43.14",
|
|
77
77
|
"@iconify-json/flag": "^1.2.12",
|
|
78
|
-
"@iconify-json/lucide": "^1.2.
|
|
78
|
+
"@iconify-json/lucide": "^1.2.142",
|
|
79
79
|
"@iconify-json/simple-icons": "^1.2.99",
|
|
80
80
|
"@lezer/highlight": "^1.2.5",
|
|
81
|
-
"@lucide/vue": "^1.
|
|
81
|
+
"@lucide/vue": "^1.54.0",
|
|
82
82
|
"@nuxt/content": "^3.16.1",
|
|
83
83
|
"@nuxt/icon": "^2.5.1",
|
|
84
84
|
"@nuxt/image": "^2.1.0",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"jiti": "^2.7.0",
|
|
99
99
|
"mermaid": "^12.1.0",
|
|
100
100
|
"reka-ui": "^2.11.0",
|
|
101
|
-
"satori": "^0.
|
|
101
|
+
"satori": "^0.47.1",
|
|
102
102
|
"shadcn-nuxt": "^2.8.2",
|
|
103
103
|
"shiki": "^4.5.0",
|
|
104
104
|
"tailwind-merge": "^3.7.0",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@types/jsdom": "^30.0.0",
|
|
113
|
-
"@types/node": "^26.6.
|
|
113
|
+
"@types/node": "^26.6.5",
|
|
114
114
|
"jsdom": "^30.1.2",
|
|
115
115
|
"nuxt": "^4.6.0",
|
|
116
116
|
"typescript": "^6.0.3",
|