scavold 0.2.0-rc.6 → 0.2.0-rc.9
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/CHANGELOG.md +44 -0
- package/README.md +6 -1
- package/SECURITY.md +38 -0
- package/lib/config.js +23 -6
- package/lib/media.js +19 -5
- package/package.json +9 -5
- package/scripts/check-fixture.js +69 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,45 @@ changes may occur in any release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.2.0-rc.9] — 2026-10-01
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- An image wider than the largest of `image_widths` named that width twice in its
|
|
17
|
+
`srcset`, and had its widest variant written twice at the same time.
|
|
18
|
+
|
|
19
|
+
## [0.2.0-rc.8] — 2026-10-01
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `SECURITY.md` says how to report a vulnerability and which versions receive fixes,
|
|
24
|
+
beside the [cratly security policy](https://cratly.io/security) it belongs to.
|
|
25
|
+
Every release's tag pipeline keeps a CycloneDX SBOM of what a site installs along
|
|
26
|
+
with scavold, and checks those packages for known vulnerabilities; `bun run sbom`
|
|
27
|
+
and `bun run audit:runtime` do the same locally.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- A photo whose camera stored it turned, saying in its EXIF data how to show it, came
|
|
32
|
+
out upside down or lying on its side on the site, and a portrait got the variant
|
|
33
|
+
widths of a landscape. The variants are now turned the way the photo says, and
|
|
34
|
+
measured by the width it is shown at. Every image gets new variant names once, so
|
|
35
|
+
variants kept between builds are written again rather than served as they were.
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- `sharp` 0.35, whose bundled libvips and libheif fix vulnerabilities reported against
|
|
40
|
+
0.34 (GHSA-f88m-g3jw-g9cj, GHSA-rgj7-g3m4-5g8c). It needs Node 20.9 or newer, and so
|
|
41
|
+
does scavold now.
|
|
42
|
+
|
|
43
|
+
## [0.2.0-rc.7] — 2026-09-29
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- A link to a media file whose extension VitePress does not know, such as `.diff`,
|
|
48
|
+
pointed at the file name with `.html` appended and therefore at nothing. It now names
|
|
49
|
+
the file as published.
|
|
50
|
+
|
|
12
51
|
## [0.2.0-rc.6] — 2026-09-13
|
|
13
52
|
|
|
14
53
|
### Added
|
|
@@ -250,6 +289,11 @@ First published release. Version `0.1.0` existed in-tree only.
|
|
|
250
289
|
- Only images are processed out of `media_folder`; other file types (video, documents)
|
|
251
290
|
are never copied into the build output and have to live in the static folder.
|
|
252
291
|
|
|
292
|
+
[Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.9...main
|
|
293
|
+
[0.2.0-rc.9]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.8...v0.2.0-rc.9
|
|
294
|
+
[0.2.0-rc.8]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.7...v0.2.0-rc.8
|
|
295
|
+
[0.2.0-rc.7]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.6...v0.2.0-rc.7
|
|
296
|
+
[0.2.0-rc.6]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.5...v0.2.0-rc.6
|
|
253
297
|
[0.2.0-rc.5]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.4...v0.2.0-rc.5
|
|
254
298
|
[0.2.0-rc.4]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.3...v0.2.0-rc.4
|
|
255
299
|
[0.2.0-rc.3]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.2...v0.2.0-rc.3
|
package/README.md
CHANGED
|
@@ -18,9 +18,14 @@ bun add vitepress vue scavold@^0.2.0-rc.3
|
|
|
18
18
|
|
|
19
19
|
Responsive image generation uses [sharp](https://sharp.pixelplumbing.com/), which
|
|
20
20
|
installs a platform-specific native binary — build environments therefore need to
|
|
21
|
-
be able to fetch it (Node 20 or newer).
|
|
21
|
+
be able to fetch it (Node 20.9 or newer).
|
|
22
22
|
|
|
23
23
|
Full documentation, including a quick-start guide, component and front matter
|
|
24
24
|
references, and deployment instructions, is available at
|
|
25
25
|
|
|
26
26
|
**[https://scavold.io](https://scavold.io)**.
|
|
27
|
+
|
|
28
|
+
## Security
|
|
29
|
+
|
|
30
|
+
To report a security vulnerability, contact **security@cepharum.de**. How reports are
|
|
31
|
+
handled, and where to find the SBOM of every release, is in [SECURITY.md](SECURITY.md).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
scavold is part of cratly, for which cepharum GmbH acts as open-source software steward
|
|
4
|
+
under the EU Cyber Resilience Act. The [cratly security policy](https://cratly.io/security)
|
|
5
|
+
covers how vulnerabilities are reported, handled and disclosed; this page adds what is
|
|
6
|
+
specific to scavold.
|
|
7
|
+
|
|
8
|
+
## Reporting a vulnerability
|
|
9
|
+
|
|
10
|
+
Please report it privately, not in a public issue:
|
|
11
|
+
|
|
12
|
+
- open a [confidential issue](https://gitlab.com/cratly/scavold/-/issues/new?issue[confidential]=true)
|
|
13
|
+
in the scavold project, or
|
|
14
|
+
- write to [security@cepharum.de](mailto:security@cepharum.de).
|
|
15
|
+
|
|
16
|
+
Useful to include: the affected version, what an attacker can achieve, and the steps
|
|
17
|
+
or a minimal site that shows it. A vulnerability in one of scavold's dependencies is
|
|
18
|
+
welcome too, when scavold is how it reaches a site.
|
|
19
|
+
|
|
20
|
+
## Supported versions
|
|
21
|
+
|
|
22
|
+
Until 1.0.0, only the latest published release receives security fixes; there are no
|
|
23
|
+
backports to earlier pre-releases. Update to the latest release before reporting, if
|
|
24
|
+
you can.
|
|
25
|
+
|
|
26
|
+
## What scavold brings into a site
|
|
27
|
+
|
|
28
|
+
scavold runs while a site is built, not when it is visited. Its runtime dependencies
|
|
29
|
+
are few — `sharp` for image processing among them — and `vitepress` and `vue` are
|
|
30
|
+
peer dependencies the site chooses itself.
|
|
31
|
+
|
|
32
|
+
- **SBOM.** Every release comes with a CycloneDX software bill of materials, kept as
|
|
33
|
+
the `sbom` job's artifact of the release's tag pipeline in GitLab, from 0.2.0-rc.8
|
|
34
|
+
onwards. `bun run sbom` writes the same for the working tree.
|
|
35
|
+
- **Known vulnerabilities.** The runtime dependencies are checked against the npm
|
|
36
|
+
advisory database in every tag pipeline and in a weekly scheduled pipeline;
|
|
37
|
+
`bun run audit:runtime` runs the same check locally. A finding is fixed by a release
|
|
38
|
+
of its own.
|
package/lib/config.js
CHANGED
|
@@ -653,7 +653,7 @@ export async function augmentConfig( rawConfig, options = {} ) {
|
|
|
653
653
|
// during in-app use.
|
|
654
654
|
//
|
|
655
655
|
// The redirect value can be either:
|
|
656
|
-
// - a site-relative URL ("/de/jobs") — written by the
|
|
656
|
+
// - a site-relative URL ("/de/jobs") — written by the cratly editor
|
|
657
657
|
// - a relative .md path ("jobs.md") — written by hand in frontmatter
|
|
658
658
|
// - an absolute external URL — e.g. "https://example.com"
|
|
659
659
|
const redirect = pageData.frontmatter?.redirect;
|
|
@@ -714,14 +714,31 @@ export async function augmentConfig( rawConfig, options = {} ) {
|
|
|
714
714
|
// offers, which is relative to that folder. Rewrite it to the URL the
|
|
715
715
|
// built site serves. Targets that are not media files (ordinary page
|
|
716
716
|
// links, anchors, remote URLs) resolve to null and stay untouched.
|
|
717
|
-
|
|
718
|
-
|
|
717
|
+
//
|
|
718
|
+
// VitePress then takes any extension missing from its own list of file
|
|
719
|
+
// types (`.diff`, say) for part of a page name and appends `.html`. The
|
|
720
|
+
// published URL is final, so that suffix is taken back — after VitePress
|
|
721
|
+
// ran, which leaves prefixing the site's base to it.
|
|
722
|
+
patchRenderer( md, "link_open", ( defaultHandler, tokens, idx, mdOptions, env, slf ) => {
|
|
723
|
+
const token = tokens[idx];
|
|
724
|
+
const published = media.collectAsset( token.attrGet( "href" ) );
|
|
725
|
+
|
|
726
|
+
if ( !published ) {
|
|
727
|
+
return defaultHandler( tokens, idx, mdOptions, env, slf );
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
token.attrSet( "href", published );
|
|
731
|
+
|
|
732
|
+
const rendered = defaultHandler( tokens, idx, mdOptions, env, slf );
|
|
733
|
+
const href = token.attrGet( "href" );
|
|
734
|
+
|
|
735
|
+
if ( !published.endsWith( ".html" ) && href?.endsWith( published + ".html" ) ) {
|
|
736
|
+
token.attrSet( "href", href.slice( 0, -".html".length ) );
|
|
719
737
|
|
|
720
|
-
|
|
721
|
-
tokens[idx].attrSet( "href", published );
|
|
738
|
+
return slf.renderToken( tokens, idx, mdOptions );
|
|
722
739
|
}
|
|
723
740
|
|
|
724
|
-
return
|
|
741
|
+
return rendered;
|
|
725
742
|
} );
|
|
726
743
|
|
|
727
744
|
registerContainers( md, containerMap, {
|
package/lib/media.js
CHANGED
|
@@ -6,6 +6,13 @@ import sharp from "sharp";
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_WIDTHS = [ 320, 640, 960, 1280, 1920 ];
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Enters every variant's name along with the source content. Raise it whenever the
|
|
11
|
+
* pipeline would write different pixels for the same source, or variants a site keeps
|
|
12
|
+
* between builds are taken for correct and never written again.
|
|
13
|
+
*/
|
|
14
|
+
const PIPELINE_REVISION = "2";
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* File types the image pipeline can scale. Everything else in the media folder is
|
|
11
18
|
* published by copying it verbatim — authors upload PDFs, videos and other downloads
|
|
@@ -41,7 +48,7 @@ function variantsPublicDir( root, staticDir ) {
|
|
|
41
48
|
* @returns {string}
|
|
42
49
|
*/
|
|
43
50
|
function contentHash( filePath ) {
|
|
44
|
-
return createHash( "sha1" ).update( readFileSync( filePath ) ).digest( "hex" ).slice( 0, 8 );
|
|
51
|
+
return createHash( "sha1" ).update( PIPELINE_REVISION ).update( readFileSync( filePath ) ).digest( "hex" ).slice( 0, 8 );
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/**
|
|
@@ -73,14 +80,21 @@ async function generateVariants( srcPath, outDir, widths ) {
|
|
|
73
80
|
const hash = contentHash( srcPath );
|
|
74
81
|
const ext = extname( srcPath ).toLowerCase().replace( ".", "" ) || "jpg";
|
|
75
82
|
const base = toKebabCase( basename( srcPath, extname( srcPath ) ) );
|
|
76
|
-
|
|
83
|
+
// A camera stores pixels as the sensor saw them and an EXIF tag saying how to turn
|
|
84
|
+
// them. The variants carry no metadata, so the turn has to go into their pixels —
|
|
85
|
+
// and the width that counts is the one the photo is shown at.
|
|
86
|
+
const image = sharp( srcPath ).autoOrient();
|
|
77
87
|
const meta = await image.metadata();
|
|
78
|
-
const sourceWidth = meta.width ?? Infinity;
|
|
88
|
+
const sourceWidth = meta.autoOrient?.width ?? meta.width ?? Infinity;
|
|
79
89
|
|
|
80
90
|
const targets = widths.filter( w => w <= sourceWidth );
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
// A source between two steps of the ladder caps it at its own width; one wider
|
|
93
|
+
// than the ladder already ends on its last step.
|
|
94
|
+
const widest = Math.min( sourceWidth, widths[widths.length - 1] );
|
|
95
|
+
|
|
96
|
+
if ( targets.length === 0 || targets[targets.length - 1] < widest ) {
|
|
97
|
+
targets.push( widest );
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
const srcsetParts = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scavold",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VitePress theme framework — a scaffold for building custom VitePress themes with Vue at the core",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"homepage": "https://scavold.io",
|
|
24
24
|
"bugs": "https://gitlab.com/cratly/scavold/-/issues",
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=20"
|
|
26
|
+
"node": ">=20.9.0"
|
|
27
27
|
},
|
|
28
28
|
"types": "./index.d.ts",
|
|
29
29
|
"exports": {
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
"COMPONENTS.md",
|
|
54
54
|
"FRONTMATTER.md",
|
|
55
55
|
"README.md",
|
|
56
|
-
"
|
|
56
|
+
"SECURITY.md",
|
|
57
|
+
"!**/*.test.js",
|
|
58
|
+
"!scripts/supply-chain.js"
|
|
57
59
|
],
|
|
58
60
|
"scripts": {
|
|
59
61
|
"check-csp": "node scripts/check-csp.js",
|
|
@@ -65,7 +67,9 @@
|
|
|
65
67
|
"test:fixture": "bun scripts/check-fixture.js",
|
|
66
68
|
"test:unit:dev": "bun test --watch",
|
|
67
69
|
"lint": "eslint .",
|
|
68
|
-
"lint:fix": "eslint . --fix"
|
|
70
|
+
"lint:fix": "eslint . --fix",
|
|
71
|
+
"sbom": "node scripts/supply-chain.js sbom",
|
|
72
|
+
"audit:runtime": "node scripts/supply-chain.js audit"
|
|
69
73
|
},
|
|
70
74
|
"peerDependencies": {
|
|
71
75
|
"vitepress": ">=1.0.0",
|
|
@@ -74,7 +78,7 @@
|
|
|
74
78
|
"dependencies": {
|
|
75
79
|
"@cepharum/vue3-i18n": "^2.0.0",
|
|
76
80
|
"markdown-it-container": "^4.0.0",
|
|
77
|
-
"sharp": "^0.
|
|
81
|
+
"sharp": "^0.35.5",
|
|
78
82
|
"yaml": "^2.8.3"
|
|
79
83
|
},
|
|
80
84
|
"devDependencies": {
|
package/scripts/check-fixture.js
CHANGED
|
@@ -53,7 +53,7 @@ function linkSelf() {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Creates the fixture's media files. Images are real (the pipeline reads their
|
|
56
|
-
* dimensions); the
|
|
56
|
+
* dimensions); the others only ever get copied, so a few bytes carrying the right
|
|
57
57
|
* extension are enough.
|
|
58
58
|
*/
|
|
59
59
|
async function writeMedia() {
|
|
@@ -74,8 +74,26 @@ async function writeMedia() {
|
|
|
74
74
|
// its variants exist only if the front matter pass reached the media pipeline.
|
|
75
75
|
await sharp( gradient( 1200, 800 ) ).jpeg( { quality: 70 } ).toFile( join( media, "teaser.jpg" ) );
|
|
76
76
|
|
|
77
|
+
// Photos the way a phone camera writes them: pixels as the sensor saw them, plus an
|
|
78
|
+
// EXIF tag saying how to turn them. Red marks the edge that has to end up on top.
|
|
79
|
+
const halves = ( w, h, red ) => Buffer.from(
|
|
80
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">` +
|
|
81
|
+
`<rect width="${w}" height="${h}" fill="#0000ff"/>` +
|
|
82
|
+
`<rect ${red === "bottom" ? `y="${h / 2}"` : ""} width="${red === "left" ? w / 2 : w}" ` +
|
|
83
|
+
`height="${red === "left" ? h : h / 2}" fill="#ff0000"/></svg>`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Stored upside down, to be turned by 180°.
|
|
87
|
+
await sharp( halves( 1200, 800, "bottom" ) ).jpeg( { quality: 70 } ).withMetadata( { orientation: 3 } )
|
|
88
|
+
.toFile( join( media, "upside-down.jpg" ) );
|
|
89
|
+
// Stored lying on its side, 1600 × 1200, to be turned by 90° into a 1200px wide portrait.
|
|
90
|
+
await sharp( halves( 1600, 1200, "left" ) ).jpeg( { quality: 70 } ).withMetadata( { orientation: 6 } )
|
|
91
|
+
.toFile( join( media, "portrait.jpg" ) );
|
|
92
|
+
|
|
77
93
|
writeFileSync( join( media, "doc.pdf" ), "%PDF-1.4\n% fixture placeholder\n" );
|
|
78
94
|
writeFileSync( join( media, "clip.mp4" ), "fixture placeholder\n" );
|
|
95
|
+
// An extension VitePress does not know, so its link rules take the file for a page.
|
|
96
|
+
writeFileSync( join( media, "patch.diff" ), "--- a\n+++ b\n" );
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
/**
|
|
@@ -182,11 +200,59 @@ expect( published.some( f => /^narrow--800w-[0-9a-f]+\.jpg$/.test( f ) ),
|
|
|
182
200
|
"caps the widest variant at the source width" );
|
|
183
201
|
expect( !published.some( f => /^narrow--(960|1280|1920)w-/.test( f ) ),
|
|
184
202
|
"does not upscale a narrow source" );
|
|
203
|
+
|
|
204
|
+
const wideSrcset = ( media.match( /srcset="([^"]*wide--[^"]*\.jpg[^"]*)"/ ) ?? [ "", "" ] )[1];
|
|
205
|
+
const wideWidths = wideSrcset.split( "," ).map( entry => entry.trim().split( " " )[1] );
|
|
206
|
+
|
|
207
|
+
expect( wideWidths.join( " " ) === "320w 640w 960w 1280w 1920w",
|
|
208
|
+
"names each width once in the srcset of a source wider than the ladder",
|
|
209
|
+
`found ${wideWidths.join( " " ) || "nothing"}` );
|
|
185
210
|
expect( media.includes( 'sizes="(min-width: 40rem) 32rem, 100vw"' ),
|
|
186
211
|
"applies the sizes value from the site configuration" );
|
|
187
212
|
expect( media.includes( 'sizes="100vw"' ), "lets a single image override sizes through its title" );
|
|
188
213
|
expect( media.includes( 'src="https://example.com/absent.png"' ), "leaves a remote image untouched" );
|
|
189
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Tells which edge of a published variant is red, the colour marking the top.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} file name of the variant in the published media folder
|
|
219
|
+
* @returns {Promise<{width: number, height: number, top: string}>}
|
|
220
|
+
*/
|
|
221
|
+
async function orientationOf( file ) {
|
|
222
|
+
const { data, info } = await sharp( join( MEDIA_OUT, file ) ).raw().toBuffer( { resolveWithObject: true } );
|
|
223
|
+
const red = offset => data[offset] > data[offset + 2];
|
|
224
|
+
const middle = x => ( Math.floor( info.height / 2 ) * info.width + x ) * info.channels;
|
|
225
|
+
const top = red( Math.floor( info.width / 2 ) * info.channels ) ? "top"
|
|
226
|
+
: red( ( ( info.height - 1 ) * info.width + Math.floor( info.width / 2 ) ) * info.channels ) ? "bottom"
|
|
227
|
+
: red( middle( 0 ) ) ? "left" : red( middle( info.width - 1 ) ) ? "right" : "none";
|
|
228
|
+
|
|
229
|
+
return { width: info.width, height: info.height, top };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Browsers honour the EXIF tag only on the file they are given. The variants carry
|
|
233
|
+
// no metadata, so the turn has to be in their pixels already.
|
|
234
|
+
const turned = await Promise.all( published
|
|
235
|
+
.filter( f => /^(upside-down|portrait)--/.test( f ) )
|
|
236
|
+
.map( async f => ( { f, ...await orientationOf( f ) } ) ) );
|
|
237
|
+
|
|
238
|
+
for ( const name of [ "upside-down", "portrait" ] ) {
|
|
239
|
+
const found = turned.filter( v => v.f.startsWith( `${name}--` ) );
|
|
240
|
+
const wrong = found.filter( v => v.top !== "top" );
|
|
241
|
+
|
|
242
|
+
expect( found.length > 0 && wrong.length === 0, `turns the variants of ${name}.jpg the way its EXIF tag says`,
|
|
243
|
+
wrong.map( v => `${v.f} has red on the ${v.top}` ).join( ", " ) );
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const portraits = turned.filter( v => v.f.startsWith( "portrait--" ) );
|
|
247
|
+
const named = v => Number( v.f.match( /--(\d+)w-/ )[1] );
|
|
248
|
+
const misshapen = portraits.filter( v => v.width !== named( v ) || v.height <= v.width );
|
|
249
|
+
|
|
250
|
+
expect( portraits.some( v => named( v ) === 1200 ) && !portraits.some( v => named( v ) > 1200 ),
|
|
251
|
+
"measures a turned photo by the width it is shown at, not the width it is stored at",
|
|
252
|
+
`found ${[ ...new Set( portraits.map( named ) ) ].sort( ( a, b ) => a - b ).join( ", " )}` );
|
|
253
|
+
expect( portraits.length > 0 && misshapen.length === 0, "writes a turned photo's variants upright, at the width they are named for",
|
|
254
|
+
misshapen.map( v => `${v.f} is ${v.width} × ${v.height}` ).join( ", " ) );
|
|
255
|
+
|
|
190
256
|
// ── published media that is not an image ─────────────────────────────────────
|
|
191
257
|
|
|
192
258
|
console.log( "\nmedia that is not an image" );
|
|
@@ -194,6 +260,8 @@ console.log( "\nmedia that is not an image" );
|
|
|
194
260
|
expect( published.includes( "doc.pdf" ), "copies a document into the site output" );
|
|
195
261
|
expect( published.includes( "clip.mp4" ), "copies a video into the site output" );
|
|
196
262
|
expect( media.includes( 'href="/media/doc.pdf"' ), "rewrites a link to a media file" );
|
|
263
|
+
expect( media.includes( 'href="/media/patch.diff"' ),
|
|
264
|
+
"links a media file of an extension VitePress does not know as that file, not as a page" );
|
|
197
265
|
// On the entry page, where the fixture's page links live. VitePress resolves them to
|
|
198
266
|
// .html; what matters is that the media pipeline did not touch them.
|
|
199
267
|
expect( built( "index.html" ).includes( 'href="/containers.html"' ), "leaves a link to a page alone" );
|