feat(theme): migrate to @vuepress/plugin-replace-assets (#644)

This commit is contained in:
pengzhanbo 2025-07-10 11:23:05 +08:00 committed by GitHub
parent cd1d457d31
commit 0fe98a38f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 86 additions and 844 deletions

View File

@ -13,7 +13,6 @@ In the `plugins` directory:
- `plugin-search`: Provides full-text fuzzy search functionality for the theme.
- `plugin-md-power`: Provides enhanced markdown features.
- `plugin-replace-assets`: Provides resource link replacement functionality
- `plugin-fonts`: Provides special character font support
## Development Configuration

View File

@ -13,7 +13,6 @@
- `plugin-search`: 为主题提供 全文模糊搜索 功能
- `plugin-md-power`: 提供 markdown 增强功能
- `plugin-replace-assets`: 提供资源链接替换功能
- `plugin-fonts`: 提供特殊字符字体支持
## 开发配置

View File

@ -8,11 +8,12 @@ badge: 新
## 概述
此功能由 [vuepress-plugin-replace-assets](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/plugins/plugin-replace-assets) 插件提供。
此功能由 [@vuepress/plugin-replace-assets](https://ecosystem.vuejs.press/zh/plugins/tools/replace-assets.html) 插件提供。
替换站点内的本地资源链接,比如 图片、视频、音频、PDF 等资源的链接地址,将本地资源地址改写到新的地址。
::: tip 为什么需要这个功能?
## 为什么需要这个功能?
不少用户会选择将站点的资源存放到 CDN 服务上,从而加速站点的访问速度,提升站点的可用性。
在这个过程中,通常需要先将资源上传到 CDN 服务,然后再获取 CDN 服务的资源链接,最后才在站点内容中使用。
@ -26,10 +27,6 @@ badge: 新
在此过程中,内容创作被频繁的打断。
此功能旨在解决这个问题。在内容创作过程中,只需要直接使用本地资源地址,由主题内部在合适的阶段,完成资源地址的替换。
:::
::: important 此功能仅查找 `/` 开头的本地静态资源链接,比如 `/images/foo.jpg`
:::
::: important 此功能不会修改源文件,仅在编译后的内容中进行替换
:::
@ -67,6 +64,56 @@ export default defineUserConfig({
})
```
### 资源管理
**你应该将资源存放在 [.vuepress/public](https://v2.vuepress.vuejs.org/zh/guide/assets.html#public-%E6%96%87%E4%BB%B6) 目录下**:
```sh
./docs
├── .vuepress
│ └── public # [!code hl:6]
│ ├── images
│ │ ├── foo.jpg
│ │ └── bar.jpg
│ └── medias
│ └── foo.mp4
└── README.md
```
::: tip 为什么需要存放在这个目录下?
当站点完成编译准备部署前,我们可以很方便地直接将这个目录下的文件上传到 CDN 。
:::
在 markdown 中,直接使用本地资源地址:
```md
![foo](/images/foo.jpg)
<img src="/images/foo.jpg">
```
`javascript` 中:
```js
const foo = '/images/foo.jpg'
const img = document.createElement('img')
img.src = '/images/foo.jpg'
```
以及在 样式文件 中:
```css
.foo {
background: url('/images/foo.jpg');
}
```
插件会正确识别这些资源,并在编译后的内容中进行替换。
:::warning 插件不支持识别 `'/images/' + 'foo.jpg'` 拼接的路径。
:::
## 配置说明
```ts
@ -120,11 +167,11 @@ interface ReplaceAssetsOptions {
为便于使用,主题插件内部提供了内置的资源匹配规则,你可以直接使用它们。
- `image`: 查找图片资源,包括 `['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'avif']` 格式的本地图片资源链接
- `media`: 查找媒体资源,包括 `['mp4', 'webm', 'ogg', 'mp3', 'wav', 'flac', 'aac', 'm3u8', 'm3u', 'flv', 'pdf']` 格式的本地媒体资源链接
- `image`: 查找图片资源,包括 `['apng','bmp','png','jpeg','jpg','jfif','pjpeg','pjp','gif','svg','ico','webp','avif','cur','jxl']` 格式的本地图片资源链接
- `media`: 查找媒体资源,包括 `['mp4','webm','ogg','mp3','wav','flac','aac','opus','mov','m4a','vtt','pdf']` 格式的本地媒体资源链接
- `all`: 查找 图片 和 媒体资源,即 `image``media` 的合集
直接传入 __资源链接前缀__ 或 __资源链接替换函数__ 时,主题使用 `all` 规则替换资源链接。
直接传入 **资源链接前缀** 或 **资源链接替换函数** 时,主题使用 `all` 规则替换资源链接。
```ts title=".vuepress/config.ts"
import process from 'node:process'
@ -204,11 +251,11 @@ export default defineUserConfig({
})
```
__`find` 字段说明__
**`find` 字段说明**
`find` 字段用于匹配资源链接,可以是一个 __正则表达式__ 或 __字符串__
`find` 字段用于匹配资源链接,可以是一个 **正则表达式** 或 **字符串**
当传入的是一个 `字符串` 时,如果是以 `^` 开头或者以 `$` 结尾的字符串,则会自动转换为一个 __正则表达式__
当传入的是一个 `字符串` 时,如果是以 `^` 开头或者以 `$` 结尾的字符串,则会自动转换为一个 **正则表达式**
否则则会检查资源链接是否 以 `find` 结尾 或者 以 `find` 开头。
```txt
@ -217,3 +264,4 @@ __`find` 字段说明__
```
::: important 所有匹配的资源地址都是以 `/` 开头。
:::

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,51 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`plugin-replace-assets > transformAssets > should work with like css 1`] = `
".foo {
background-image: url("https://example.com/assets/images/foo.jpg");
background-image: url("https://example.com/assets/images/foo.png");
background-image: url("https://example.com/assets/images/foo.gif");
background-image: url("https://example.com/assets/images/foo.svg");
background-image: url("https://example.com/assets/medias/foo.mp4");
background-image: url("https://example.com/assets/images/foo.jpg");
background-image: url("https://example.com/assets/images/foo.png");
background-image: url("https://example.com/assets/images/foo.jpg?a=1");
background-image: url("https://not-replace.com/images/foo.jpg");
background: url("https://example.com/assets/images/foo.png");
}
"
`;
exports[`plugin-replace-assets > transformAssets > should work with like html 1`] = `
"<img src="https://example.com/assets/images/foo.jpg" />
<img src="https://example.com/assets/images/foo.png" />
<img src="https://example.com/assets/images/foo.gif" />
<img src="https://example.com/assets/images/foo.svg" />
<img src="/images/foo.txt" />
<img src="https://example.com/assets/medias/foo.mp4" />
<img src="https://example.com/assets/images/foo.jpg?a=1" />
<img src="https://not-replace.com/images/foo.jpg" />
<video src="https://example.com/assets/medias/foo.mp4" />
<audio src="https://example.com/assets/medias/foo.mp3" />
<embed src="https://example.com/assets/medias/foo.pdf" />
"
`;
exports[`plugin-replace-assets > transformAssets > should work with like js 1`] = `
" const a = "https://example.com/assets/images/foo.jpg"
const b = "https://example.com/assets/images/foo.jpg"
const c = "https://example.com/assets/images/foo.jpg?a=1"
const d = "https://not-replace.com/images/foo.jpg"
const json_string = JSON.parse("{\\"a\\":\\"https://example.com/assets/images/foo.jpg\\"}")
"
`;

View File

@ -1,131 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { KNOWN_ASSET_EXTENSIONS, KNOWN_IMAGE_EXTENSIONS, KNOWN_MEDIA_EXTENSIONS } from '../src/constants.js'
import { createFindPattern, normalizeRules } from '../src/normalizeRules.js'
describe('plugin-replace-assets > normalizeRules', () => {
it('should work with empty options', () => {
expect(normalizeRules('')).toEqual([])
expect(normalizeRules([])).toEqual([])
expect(normalizeRules({})).toEqual([])
expect(normalizeRules({ rules: [] })).toEqual([])
})
it('should work with string', () => {
const rules = normalizeRules('https://example.com/assets/')
expect(rules).toEqual([{
find: createFindPattern(KNOWN_ASSET_EXTENSIONS),
replacement: 'https://example.com/assets/',
}])
})
it('should work with function', () => {
const replacement = vi.fn((url: string) => `https://example.com/assets/${url}`)
const rules = normalizeRules(replacement)
expect(rules).toEqual([{
find: createFindPattern(KNOWN_ASSET_EXTENSIONS),
replacement,
}])
})
it('should work with single rule', () => {
const rules = normalizeRules({
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
})
expect(rules).toEqual([{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
}])
})
it('should work with multiple rules', () => {
const rules = normalizeRules([
{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
},
{
find: '^/medias/.*\\.(mp4|ogg|ogv|webm)$',
replacement: 'https://example.com/medias/',
},
])
expect(rules).toEqual([
{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
},
{
find: '^/medias/.*\\.(mp4|ogg|ogv|webm)$',
replacement: 'https://example.com/medias/',
},
])
})
it('should work with presets', () => {
const media = vi.fn((url: string) => `https://example.com/medias/${url}`)
const rules = normalizeRules({
image: 'https://example.com/images/',
media,
all: 'https://example.com/assets/',
})
expect(rules).toEqual([
{
find: createFindPattern(KNOWN_IMAGE_EXTENSIONS),
replacement: 'https://example.com/images/',
},
{
find: createFindPattern(KNOWN_MEDIA_EXTENSIONS),
replacement: media,
},
{
find: createFindPattern(KNOWN_ASSET_EXTENSIONS),
replacement: 'https://example.com/assets/',
},
])
})
it('should work with custom single rule', () => {
const rules = normalizeRules({
rules: {
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
},
})
expect(rules).toEqual([{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
}])
})
it('should work with custom multiple rules', () => {
const rules = normalizeRules({
rules: [
{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
},
{
find: '^/medias/.*\\.(mp4|ogg|ogv|webm)$',
replacement: 'https://example.com/medias/',
},
],
})
expect(rules).toEqual([
{
find: '^/images/.*\\.(jpe?g|png|gif|svg)$',
replacement: 'https://example.com/images/',
},
{
find: '^/medias/.*\\.(mp4|ogg|ogv|webm)$',
replacement: 'https://example.com/medias/',
},
])
})
})

View File

@ -1,218 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { normalizeRules } from '../src/normalizeRules.js'
import { isMatchUrl, replacementAssetWithRules, transformAssets } from '../src/unplugin/transform.js'
import { createAssetPattern } from '../src/unplugin/utils.js'
describe('plugin-replace-assets > isMatchUrl', () => {
it.each([
{
name: 'string like regexp with ^ and $',
find: '^/images/.*\\.(jpe?g|png|gif|svg)(\\?.*)?$',
expects: [
['/images/foo.jpg', true],
['/images/foo.png', true],
['/images/foo.gif', true],
['/images/foo.svg', true],
['/images/foo.jpg?a=1', true],
['/images/foo.txt', false],
['/medias/foo.mp4', false],
] as const,
},
{
name: 'string like regexp start with ^',
find: '^/medias/',
expects: [
['/medias/foo.mp4', true],
['/medias/foo.ogg', true],
['/medias/foo.ogv', true],
['/medias/foo.webm', true],
['/images/foo.jpg', false],
] as const,
},
{
name: 'string like regexp end with $',
find: '\\.(jpe?g|png|gif|svg)$',
expects: [
['/images/foo.jpg', true],
['/images/foo.png', true],
['/images/foo.gif', true],
['/images/foo.svg', true],
['/images/foo.txt', false],
['/medias/foo.mp4', false],
] as const,
},
{
name: 'string start width pathname',
find: '/images/',
expects: [
['/images/foo.jpg', true],
['/images/foo.png', true],
['/images/foo.gif', true],
['/images/foo', true],
['/medias/foo.mp4', false],
] as const,
},
{
name: 'string end width extension',
find: '.jpg',
expects: [
['/images/foo.jpg', true],
['/images/foo.png', false],
['/images/foo.gif', false],
['/images/foo', false],
['/medias/foo.mp4', false],
] as const,
},
{
name: 'regexp',
find: /^\/images\/.*\.(jpe?g|png|gif|svg)$/,
expects: [
['/images/foo.jpg', true],
['/images/foo.png', true],
['/images/foo.gif', true],
['/images/foo.svg', true],
['/images/foo.txt', false],
['/medias/foo.mp4', false],
] as const,
},
])('$name', ({ find, expects }) => {
for (const [url, expected] of expects) {
expect(isMatchUrl(find, url)).toBe(expected)
}
})
})
describe('plugin-replace-assets > replacementAssetWithRules', () => {
const IMAGE_SUPPORTED = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif']
const MEDIA_SUPPORTED = ['mp4', 'webm', 'ogg', 'mp3', 'wav', 'flac', 'aac', 'm3u8', 'm3u', 'flv', 'pdf']
const replacementFn = vi.fn(url => `https://example.com/assets${url}`)
it.each([
{
name: 'string replacement',
rules: normalizeRules('https://example.com/assets/'),
expects: [
// images
...IMAGE_SUPPORTED.map(ext => [
`/images/foo.${ext}`,
`https://example.com/assets/images/foo.${ext}`,
]),
// media
...MEDIA_SUPPORTED.map(ext => [
`/medias/foo.${ext}`,
`https://example.com/assets/medias/foo.${ext}`,
]),
// have query string
['/images/foo.jpg?a=1', 'https://example.com/assets/images/foo.jpg?a=1'],
// cached images
['/images/foo.jpg', 'https://example.com/assets/images/foo.jpg'],
// no supported
['/images/foo.txt', undefined],
['/medias/foo', undefined],
] as const,
},
{
name: 'function replacement',
rules: normalizeRules(replacementFn),
expects: [
// images
...IMAGE_SUPPORTED.map(ext => [
`/images-1/foo.${ext}`,
`https://example.com/assets/images-1/foo.${ext}`,
]),
// media
...MEDIA_SUPPORTED.map(ext => [
`/medias-1/foo.${ext}`,
`https://example.com/assets/medias-1/foo.${ext}`,
]),
// have query string
['/images-1/foo.jpg?a=1', 'https://example.com/assets/images-1/foo.jpg?a=1'],
// cached images
['/images-1/foo.jpg', 'https://example.com/assets/images-1/foo.jpg'],
// no supported
['/images-1/foo.txt', undefined],
['/medias-1/foo', undefined],
] as const,
},
])('$name', ({ name, rules, expects }) => {
for (const [url, expected] of expects) {
expect(replacementAssetWithRules(rules, url)).toBe(expected)
}
if (name === 'function replacement') {
// should not called with cached, and not called with no supported
expect(replacementFn).toBeCalledTimes(expects.length - 3)
}
})
})
describe('plugin-replace-assets > transformAssets', () => {
const rules = normalizeRules('https://example.com/assets/')
const pattern = createAssetPattern('/[^/]')
it('should work with like html', () => {
const source = `\
<img src="/images/foo.jpg" />
<img src="/images/foo.png" />
<img src="/images/foo.gif" />
<img src="/images/foo.svg" />
<img src="/images/foo.txt" />
<img src="/medias/foo.mp4" />
<img src="/images/foo.jpg?a=1" />
<img src="https://not-replace.com/images/foo.jpg" />
<video src="/medias/foo.mp4" />
<audio src="/medias/foo.mp3" />
<embed src="/medias/foo.pdf" />
`
expect(transformAssets(source, pattern, rules)).toMatchSnapshot()
})
it('should work with like css', () => {
const source = `\
.foo {
background-image: url("/images/foo.jpg");
background-image: url("/images/foo.png");
background-image: url("/images/foo.gif");
background-image: url("/images/foo.svg");
background-image: url("/medias/foo.mp4");
background-image: url('/images/foo.jpg');
background-image: url(/images/foo.png);
background-image: url("/images/foo.jpg?a=1");
background-image: url("https://not-replace.com/images/foo.jpg");
background: url('/images/foo.png');
}
`
expect(transformAssets(source, pattern, rules)).toMatchSnapshot()
})
it('should work with like js', () => {
const source = `\
const a = "/images/foo.jpg"
const b = '/images/foo.jpg'
const c = '/images/foo.jpg?a=1'
const d = "https://not-replace.com/images/foo.jpg"
const json_string = JSON.parse("{\\"a\\":\\"/images/foo.jpg\\"}")
`
expect(transformAssets(source, pattern, rules)).toMatchSnapshot()
})
it('should work with no match', () => {
const source = `\
<video src="./medias/foo.mp4" />
<img src="https://not-replace.com/images/foo.jpg" />
const a = "images/foo.jpg"
`
expect(transformAssets(source, pattern, rules)).toBe(source)
})
})

View File

@ -1,25 +0,0 @@
import { describe, expect, it } from 'vitest'
import { createAssetPattern, normalizeUrl } from '../src/unplugin/utils.js'
describe('plugin-replace-assets > utils', () => {
it('createAssetPattern', () => {
expect(createAssetPattern('/[^/]').test(`'/images/foo.jpg'`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`"/images/foo.jpg"`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`(/images/foo.jpg)`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`('/images/foo.jpg')`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`("/images/foo.jpg")`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`"/images/foo.jpg?a=1"`)).toBe(true)
expect(createAssetPattern('/[^/]').test(`'https://example.com/images/foo.jpg'`)).toBe(false)
expect(createAssetPattern('/[^/]').test(`"./images/foo.jpg"`)).toBe(false)
expect(createAssetPattern('/[^/]').test(`"images/foo.jpg"`)).toBe(false)
})
it('normalizeUrl', () => {
expect(normalizeUrl('')).toBe('')
expect(normalizeUrl('/images/foo.jpg')).toBe('/images/foo.jpg')
expect(normalizeUrl('/images/foo.jpg?a=1')).toBe('/images/foo.jpg?a=1')
expect(normalizeUrl('/images/foo.jpg', 'https://example.com/')).toBe('https://example.com/images/foo.jpg')
expect(normalizeUrl('/images/foo.jpg?a=1', 'https://example.com/')).toBe('https://example.com/images/foo.jpg?a=1')
})
})

View File

@ -1,50 +0,0 @@
{
"name": "vuepress-plugin-replace-assets",
"type": "module",
"version": "1.0.0-rc.156",
"description": "The Plugin for VuePress 2 - replace assets url",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git",
"directory": "plugins/plugin-replace-assets"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js"
},
"./package.json": "./package.json"
},
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run tsdown",
"tsdown": "tsdown"
},
"peerDependencies": {
"vuepress": "catalog:vuepress"
},
"dependencies": {
"@vuepress/helper": "catalog:vuepress",
"magic-string": "catalog:prod",
"unplugin": "catalog:prod"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"replace assets",
"vuepress-plugin-replace-assets"
]
}

View File

@ -1,31 +0,0 @@
export const PLUGIN_NAME = 'vuepress-plugin-replace-assets'
export const KNOWN_IMAGE_EXTENSIONS: string[] = [
'png',
'jpg',
'jpeg',
'gif',
'webp',
'svg',
'avif',
]
export const KNOWN_MEDIA_EXTENSIONS: string[] = [
'mp4',
'webm',
'ogg',
'mp3',
'wav',
'flac',
'aac',
'm3u8',
'm3u',
'flv',
'pdf',
]
export const KNOWN_ASSET_EXTENSIONS: string[] = [
...KNOWN_IMAGE_EXTENSIONS,
...KNOWN_MEDIA_EXTENSIONS,
]

View File

@ -1,4 +0,0 @@
export * from './normalizeRules.js'
export * from './options.js'
export * from './plugin.js'
export * from './unplugin/index.js'

View File

@ -1,56 +0,0 @@
import type { ReplaceAssetsPluginOptions, ReplacementRule } from './options.js'
import { isArray, isFunction } from '@vuepress/helper'
import { KNOWN_ASSET_EXTENSIONS, KNOWN_IMAGE_EXTENSIONS, KNOWN_MEDIA_EXTENSIONS } from './constants.js'
export function createFindPattern(extensions: string[]): RegExp {
return new RegExp(`\\.(?:${extensions.join('|')})(\\?.*)?$`)
}
export function normalizeRules(options: ReplaceAssetsPluginOptions): ReplacementRule[] {
const normalized: ReplacementRule[] = []
if ((typeof options === 'string' || isFunction(options))) {
options && normalized.push({
find: createFindPattern(KNOWN_ASSET_EXTENSIONS),
replacement: options,
})
return normalized
}
if (isArray(options)) {
normalized.push(...options)
return normalized
}
if ('find' in options) {
options.find && options.replacement && normalized.push(options)
return normalized
}
if (options.image) {
normalized.push({
find: createFindPattern(KNOWN_IMAGE_EXTENSIONS),
replacement: options.image,
})
}
if (options.media) {
normalized.push({
find: createFindPattern(KNOWN_MEDIA_EXTENSIONS),
replacement: options.media,
})
}
if (options.all) {
normalized.push({
find: createFindPattern(KNOWN_ASSET_EXTENSIONS),
replacement: options.all,
})
}
if (options.rules) {
normalized.push(...isArray(options.rules) ? options.rules : [options.rules])
}
return normalized
}

View File

@ -1,19 +0,0 @@
export type Replacement = string | ((url: string) => string)
export interface ReplacementRule {
find: RegExp | string
replacement: Replacement
}
export interface ReplaceAssetsOptions {
rules?: ReplacementRule | ReplacementRule[]
all?: Replacement
image?: Replacement
media?: Replacement
}
export type ReplaceAssetsPluginOptions
= | Replacement
| ReplacementRule
| ReplacementRule[]
| ReplaceAssetsOptions

View File

@ -1,40 +0,0 @@
import type { Plugin } from 'vuepress/core'
import type { ReplaceAssetsPluginOptions } from './options.js'
import { addViteConfig, configWebpack, getBundlerName } from '@vuepress/helper'
import { PLUGIN_NAME } from './constants.js'
import { normalizeRules } from './normalizeRules.js'
import { createVitePlugin, createWebpackPlugin } from './unplugin/index.js'
const EMPTY_PLUGIN = { name: PLUGIN_NAME }
export function replaceAssetsPlugin(
options: ReplaceAssetsPluginOptions = {},
): Plugin {
const rules = normalizeRules(options)
if (rules.length === 0)
return EMPTY_PLUGIN
return {
...EMPTY_PLUGIN,
extendsBundlerOptions(bundlerOptions, app) {
const bundle = getBundlerName(app)
if (bundle === 'vite') {
const replaceAssets = createVitePlugin()
addViteConfig(bundlerOptions, app, {
plugins: [replaceAssets(rules)],
})
}
if (bundle === 'webpack') {
const replaceAssets = createWebpackPlugin()
configWebpack(bundlerOptions, app, (config) => {
config.plugins ??= []
config.plugins.push(replaceAssets(rules))
})
}
},
}
}

View File

@ -1,18 +0,0 @@
import type { UnpluginFactory } from 'unplugin'
import type { ReplacementRule } from '../options.js'
import { transformAssets } from './transform.js'
import { createAssetPattern } from './utils.js'
export const unpluginFactory: UnpluginFactory<ReplacementRule[]> = (rules) => {
const pattern = createAssetPattern('/[^/]')
return {
name: 'vuepress:replace-assets',
enforce: 'pre',
transform: {
filter: { id: { exclude: [/\.json(?:$|\?)/, /\.html?$/] } },
handler(code) {
return transformAssets(code, pattern, rules)
},
},
}
}

View File

@ -1,17 +0,0 @@
import type { VitePlugin, WebpackPluginInstance } from 'unplugin'
import type { ReplacementRule } from '../options.js'
import {
createVitePlugin as _createVitePlugin,
createWebpackPlugin as _createWebpackPlugin,
} from 'unplugin'
import { unpluginFactory } from './factory.js'
export const createVitePlugin: () => (
options: ReplacementRule[]
) => VitePlugin | VitePlugin[] = () => _createVitePlugin(unpluginFactory)
export const createWebpackPlugin: () => (
options: ReplacementRule[]
) => WebpackPluginInstance = () => _createWebpackPlugin(unpluginFactory)
export * from './transform.js'

View File

@ -1,69 +0,0 @@
import type { ReplacementRule } from '../options.js'
import MagicString from 'magic-string'
import { normalizeUrl } from './utils.js'
const cache = new Map<string, string>()
export function transformAssets(code: string, pattern: RegExp, rules: ReplacementRule[]): string {
const s = new MagicString(code)
let matched: RegExpExecArray | null
let hasMatched = false
// eslint-disable-next-line no-cond-assign
while ((matched = pattern.exec(code))) {
const assetUrl = matched[6] || matched[5] || matched[4] || matched[3] || matched[2] || matched[1]
const [left, right] = matched[0].startsWith('(')
? ['("', '")']
: matched[0].startsWith('\\"')
? ['\\"', '\\"']
: ['"', '"']
const start = matched.index
const end = start + matched[0].length
const resolved = replacementAssetWithRules(rules, assetUrl)
if (resolved) {
hasMatched = true
s.update(start, end, `${left}${resolved}${right}`)
}
}
if (!hasMatched)
return code
return s.toString()
}
export function replacementAssetWithRules(rules: ReplacementRule[], url: string): string | void {
if (cache.has(url))
return cache.get(url)
for (const { find, replacement } of rules) {
if (find && isMatchUrl(find, url)) {
let replaced = ''
if (typeof replacement === 'function') {
replaced = normalizeUrl(replacement(url))
}
else {
replaced = normalizeUrl(url, replacement)
}
/* istanbul ignore if -- @preserve */
if (replaced) {
cache.set(url, replaced)
return replaced
}
}
}
return undefined
}
export function isMatchUrl(find: string | RegExp, url: string): boolean {
if (typeof find === 'string') {
if (find[0] === '^' || find[find.length - 1] === '$') {
return new RegExp(find).test(url)
}
else {
return url.endsWith(find) || url.startsWith(find)
}
}
return find.test(url)
}

View File

@ -1,26 +0,0 @@
import { removeEndingSlash, removeLeadingSlash } from '@vuepress/helper'
export function createAssetPattern(prefix: string): RegExp {
const s = `(${prefix}.*?)`
return new RegExp(
[
`(?:"${s}")`, // "prefix"
`(?:'${s}')`, // 'prefix'
`(?:\\(${s}\\))`, // (prefix)
`(?:\\('${s}'\\))`, // ('prefix')
`(?:\\("${s}"\\))`, // ("prefix")
`(?:\\\\"${s}\\\\")`, // \"prefix\"
].join('|'),
'gu',
)
}
export function normalizeUrl(url: string, base?: string): string {
if (!url)
return ''
if (base) {
url = `${removeEndingSlash(base)}/${removeLeadingSlash(url)}`
}
return url
}

View File

@ -1,23 +0,0 @@
import type { Options } from 'tsdown'
import { defineConfig } from 'tsdown'
import { argv } from '../../scripts/tsup-args.js'
export default defineConfig(() => {
const DEFAULT_OPTIONS: Options = {
dts: true,
sourcemap: false,
format: 'esm',
}
const options: Options[] = []
if (argv.node) {
options.push({
...DEFAULT_OPTIONS,
entry: ['./src/index.ts'],
outDir: './lib',
target: 'node20.6.0',
})
}
return options
}) as Options[]

44
pnpm-lock.yaml generated
View File

@ -245,9 +245,6 @@ catalogs:
lru-cache:
specifier: ^11.1.0
version: 11.1.0
magic-string:
specifier: ^0.30.17
version: 0.30.17
mark.js:
specifier: ^8.11.1
version: 8.11.1
@ -296,9 +293,6 @@ catalogs:
tm-themes:
specifier: ^1.10.6
version: 1.10.6
unplugin:
specifier: ^2.3.5
version: 2.3.5
vue:
specifier: ^3.5.17
version: 3.5.17
@ -351,6 +345,9 @@ catalogs:
'@vuepress/plugin-reading-time':
specifier: 2.0.0-rc.112
version: 2.0.0-rc.112
'@vuepress/plugin-replace-assets':
specifier: 2.0.0-rc.112
version: 2.0.0-rc.112
'@vuepress/plugin-seo':
specifier: 2.0.0-rc.112
version: 2.0.0-rc.112
@ -706,21 +703,6 @@ importers:
specifier: catalog:peer
version: 1.7.3
plugins/plugin-replace-assets:
dependencies:
'@vuepress/helper':
specifier: catalog:vuepress
version: 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))
magic-string:
specifier: catalog:prod
version: 0.30.17
unplugin:
specifier: catalog:prod
version: 2.3.5
vuepress:
specifier: catalog:vuepress
version: 2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))
plugins/plugin-search:
dependencies:
'@vuepress/helper':
@ -813,6 +795,9 @@ importers:
'@vuepress/plugin-reading-time':
specifier: catalog:vuepress
version: 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))
'@vuepress/plugin-replace-assets':
specifier: catalog:vuepress
version: 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))
'@vuepress/plugin-seo':
specifier: catalog:vuepress
version: 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))
@ -879,9 +864,6 @@ importers:
vuepress-plugin-md-power:
specifier: workspace:*
version: link:../plugins/plugin-md-power
vuepress-plugin-replace-assets:
specifier: workspace:*
version: link:../plugins/plugin-replace-assets
devDependencies:
'@iconify/json':
specifier: catalog:peer
@ -2943,6 +2925,11 @@ packages:
peerDependencies:
vuepress: 2.0.0-rc.24
'@vuepress/plugin-replace-assets@2.0.0-rc.112':
resolution: {integrity: sha512-i502eqHUhZU+9kBALOHQ81t/aoJU10i6IegztXE+/ZOnKh8ryeW5q4o8b1uDUQqfEazo4kM4tjGC+l2n//5T3w==}
peerDependencies:
vuepress: 2.0.0-rc.24
'@vuepress/plugin-seo@2.0.0-rc.112':
resolution: {integrity: sha512-WWZ0Dx1MxF9Mj6UVdB8TP5GozTNv51ZQQP6EAKYzprKCw0RVQYg5/tXWlg7IWcSw72go5iFiMBj5wZQigN+t4g==}
peerDependencies:
@ -9375,6 +9362,15 @@ snapshots:
transitivePeerDependencies:
- typescript
'@vuepress/plugin-replace-assets@2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))':
dependencies:
'@vuepress/helper': 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))
magic-string: 0.30.17
unplugin: 2.3.5
vuepress: 2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))
transitivePeerDependencies:
- typescript
'@vuepress/plugin-seo@2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))':
dependencies:
'@vuepress/helper': 2.0.0-rc.112(typescript@5.8.3)(vuepress@2.0.0-rc.24(@vuepress/bundler-vite@2.0.0-rc.24(@types/node@24.0.12)(jiti@2.4.2)(less@4.3.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(typescript@5.8.3)(yaml@2.8.0))(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))

View File

@ -98,7 +98,6 @@ catalogs:
katex: ^0.16.22
local-pkg: ^1.1.1
lru-cache: ^11.1.0
magic-string: ^0.30.17
mark.js: ^8.11.1
markdown-it-container: ^4.0.0
markmap-lib: ^0.18.12
@ -115,7 +114,6 @@ catalogs:
tinyglobby: 0.2.13
tm-grammars: ^1.23.26
tm-themes: ^1.10.6
unplugin: ^2.3.5
vue: ^3.5.17
vuepress:
'@vuepress/bundler-vite': 2.0.0-rc.24
@ -134,6 +132,7 @@ catalogs:
'@vuepress/plugin-nprogress': 2.0.0-rc.112
'@vuepress/plugin-photo-swipe': 2.0.0-rc.112
'@vuepress/plugin-reading-time': 2.0.0-rc.112
'@vuepress/plugin-replace-assets': 2.0.0-rc.112
'@vuepress/plugin-seo': 2.0.0-rc.112
'@vuepress/plugin-shiki': 2.0.0-rc.112
'@vuepress/plugin-sitemap': 2.0.0-rc.112

View File

@ -105,6 +105,7 @@
"@vuepress/plugin-nprogress": "catalog:vuepress",
"@vuepress/plugin-photo-swipe": "catalog:vuepress",
"@vuepress/plugin-reading-time": "catalog:vuepress",
"@vuepress/plugin-replace-assets": "catalog:vuepress",
"@vuepress/plugin-seo": "catalog:vuepress",
"@vuepress/plugin-shiki": "catalog:vuepress",
"@vuepress/plugin-sitemap": "catalog:vuepress",
@ -123,8 +124,7 @@
"nanoid": "catalog:prod",
"package-manager-detector": "catalog:prod",
"vue": "catalog:prod",
"vuepress-plugin-md-power": "workspace:*",
"vuepress-plugin-replace-assets": "workspace:*"
"vuepress-plugin-md-power": "workspace:*"
},
"devDependencies": {
"@iconify/json": "catalog:peer",

View File

@ -11,10 +11,10 @@ import { docsearchPlugin } from '@vuepress/plugin-docsearch'
import { nprogressPlugin } from '@vuepress/plugin-nprogress'
import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe'
import { readingTimePlugin } from '@vuepress/plugin-reading-time'
import { replaceAssetsPlugin } from '@vuepress/plugin-replace-assets'
import { seoPlugin } from '@vuepress/plugin-seo'
import { sitemapPlugin } from '@vuepress/plugin-sitemap'
import { watermarkPlugin } from '@vuepress/plugin-watermark'
import { replaceAssetsPlugin } from 'vuepress-plugin-replace-assets'
import { getThemeConfig } from '../loadConfig/index.js'
import { codePlugins } from './code.js'
import { gitPlugin } from './git.js'

View File

@ -2,9 +2,9 @@ import type { CommentPluginOptions } from '@vuepress/plugin-comment'
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
import type { ChangelogOptions, ContributorsOptions } from '@vuepress/plugin-git'
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
import type { ReplaceAssetsPluginOptions } from '@vuepress/plugin-replace-assets'
import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
import type { WatermarkPluginOptions } from '@vuepress/plugin-watermark'
import type { ReplaceAssetsPluginOptions } from 'vuepress-plugin-replace-assets'
import type { ThemeBaseData, ThemeData } from './data.js'
import type {
AutoFrontmatterOptions,

View File

@ -9,12 +9,12 @@ import type { MarkdownIncludePluginOptions } from '@vuepress/plugin-markdown-inc
import type { MarkdownMathPluginOptions } from '@vuepress/plugin-markdown-math'
import type { PhotoSwipePluginOptions } from '@vuepress/plugin-photo-swipe'
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
import type { ReplaceAssetsPluginOptions } from '@vuepress/plugin-replace-assets'
import type { SeoPluginOptions } from '@vuepress/plugin-seo'
import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
import type { SitemapPluginOptions } from '@vuepress/plugin-sitemap'
import type { WatermarkPluginOptions } from '@vuepress/plugin-watermark'
import type { MarkdownPowerPluginOptions } from 'vuepress-plugin-md-power'
import type { ReplaceAssetsPluginOptions } from 'vuepress-plugin-replace-assets'
export interface ThemeBuiltinPlugins {
/**