diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts index 91fc313040..0c0d5f6ff6 100644 --- a/e2e/cypress.config.ts +++ b/e2e/cypress.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'cypress' export default defineConfig({ e2e: { - baseUrl: 'http://localhost:8080', + baseUrl: 'http://localhost:9080', specPattern: 'tests/**/*.cy.ts', }, env: { diff --git a/e2e/docs/.vuepress/config.ts b/e2e/docs/.vuepress/config.ts index c39a11e295..e7a8b3a240 100644 --- a/e2e/docs/.vuepress/config.ts +++ b/e2e/docs/.vuepress/config.ts @@ -6,12 +6,15 @@ import { path } from '@vuepress/utils' import { e2eTheme } from './theme/node/e2eTheme.js' const E2E_BASE = (process.env.E2E_BASE ?? '/') as '/' | `/${string}/` +const E2E_BUNDLER = process.env.E2E_BUNDLER ?? 'vite' export default defineUserConfig({ base: E2E_BASE, dest: path.join(__dirname, 'dist', E2E_BASE), + port: 9080, + head: [ ['meta', { name: 'foo', content: 'foo' }], ['meta', { name: 'bar', content: 'bar' }], @@ -40,8 +43,13 @@ export default defineUserConfig({ }, }, - bundler: - process.env.E2E_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(), + markdown: { + assets: { + absolutePathPrependBase: E2E_BUNDLER === 'webpack', + }, + }, + + bundler: E2E_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(), theme: e2eTheme(), diff --git a/e2e/docs/markdown/images/images.md b/e2e/docs/markdown/images/images.md new file mode 100644 index 0000000000..ce6d169902 --- /dev/null +++ b/e2e/docs/markdown/images/images.md @@ -0,0 +1,3 @@ +![logo-public](/logo.png) + +![logo-relative](./logo-relative.png) diff --git a/e2e/docs/markdown/images/logo-relative.png b/e2e/docs/markdown/images/logo-relative.png new file mode 100644 index 0000000000..ac6beaff06 Binary files /dev/null and b/e2e/docs/markdown/images/logo-relative.png differ diff --git a/e2e/package.json b/e2e/package.json index 199868f0d8..7d80a47dd7 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -6,13 +6,13 @@ "scripts": { "e2e:build": "vuepress-cli build docs --clean-cache --clean-temp", "e2e:build-webpack": "E2E_BUNDLER=webpack pnpm e2e:build", - "e2e:ci:build": "pnpm e2e:build && start-server-and-test e2e:serve http-get://localhost:8088 'pnpm e2e:run --config baseUrl=http://localhost:8088'", - "e2e:ci:dev": "start-server-and-test e2e:dev http-get://127.0.0.1:8080 'pnpm e2e:run --config baseUrl=http://localhost:8080'", - "e2e:clean": "rimraf .vuepress/.temp .vuepress/.cache .vuepress/dist", + "e2e:ci:build": "pnpm e2e:build && start-server-and-test e2e:serve http-get://localhost:9080 e2e:run", + "e2e:ci:dev": "start-server-and-test e2e:dev http-get://127.0.0.1:9080 e2e:run", + "e2e:clean": "rimraf docs/.vuepress/.temp docs/.vuepress/.cache docs/.vuepress/dist", "e2e:dev": "vuepress-cli dev docs --clean-cache --clean-temp", "e2e:dev-webpack": "E2E_BUNDLER=webpack pnpm e2e:dev", "e2e:run": "cypress run", - "e2e:serve": "anywhere -s -h localhost -p 8088 -d docs/.vuepress/dist" + "e2e:serve": "anywhere -s -h localhost -p 9080 -d docs/.vuepress/dist" }, "dependencies": { "@vuepress/bundler-vite": "workspace:*", diff --git a/e2e/tests/markdown/images.cy.ts b/e2e/tests/markdown/images.cy.ts new file mode 100644 index 0000000000..9e7388b93a --- /dev/null +++ b/e2e/tests/markdown/images.cy.ts @@ -0,0 +1,22 @@ +describe('markdown > images', () => { + it('should render images correctly', () => { + cy.visit('/markdown/images/images.html') + + cy.get('.e2e-theme-content img') + .should('have.length', 2) + .each(([el]) => { + cy.request({ url: el.src, failOnStatusCode: false }).then((res) => { + expect(res.status).to.equal(200) + expect(el.naturalWidth).to.be.greaterThan(0) + }) + }) + + cy.get('.e2e-theme-content img') + .first() + .should('have.attr', 'alt', 'logo-public') + + cy.get('.e2e-theme-content img') + .last() + .should('have.attr', 'alt', 'logo-relative') + }) +}) diff --git a/packages/bundler-vite/src/plugins/index.ts b/packages/bundler-vite/src/plugins/index.ts index 10a59f021f..396d670398 100644 --- a/packages/bundler-vite/src/plugins/index.ts +++ b/packages/bundler-vite/src/plugins/index.ts @@ -1,2 +1,3 @@ -export * from './mainPlugin.js' -export * from './userConfigPlugin.js' +export * from './vuepressMainPlugin.js' +export * from './vuepressUserConfigPlugin.js' +export * from './vuepressVuePlugin.js' diff --git a/packages/bundler-vite/src/plugins/mainPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts similarity index 99% rename from packages/bundler-vite/src/plugins/mainPlugin.ts rename to packages/bundler-vite/src/plugins/vuepressMainPlugin.ts index 7c964592da..db0e5e7a4b 100644 --- a/packages/bundler-vite/src/plugins/mainPlugin.ts +++ b/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts @@ -9,7 +9,7 @@ import type { AliasOptions, Connect, Plugin, UserConfig } from 'vite' /** * The main plugin to compat vuepress with vite */ -export const mainPlugin = ({ +export const vuepressMainPlugin = ({ app, isBuild, isServer, diff --git a/packages/bundler-vite/src/plugins/userConfigPlugin.ts b/packages/bundler-vite/src/plugins/vuepressUserConfigPlugin.ts similarity index 72% rename from packages/bundler-vite/src/plugins/userConfigPlugin.ts rename to packages/bundler-vite/src/plugins/vuepressUserConfigPlugin.ts index c4bd70ff93..dd01ad4818 100644 --- a/packages/bundler-vite/src/plugins/userConfigPlugin.ts +++ b/packages/bundler-vite/src/plugins/vuepressUserConfigPlugin.ts @@ -4,7 +4,9 @@ import type { ViteBundlerOptions } from '../types.js' /** * A plugin to allow user config to override vite config */ -export const userConfigPlugin = (options: ViteBundlerOptions): Plugin => ({ +export const vuepressUserConfigPlugin = ( + options: ViteBundlerOptions, +): Plugin => ({ name: 'vuepress:user-config', config: () => options.viteOptions ?? {}, }) diff --git a/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts new file mode 100644 index 0000000000..35d72951de --- /dev/null +++ b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts @@ -0,0 +1,58 @@ +import vuePlugin from '@vitejs/plugin-vue' +import type { Plugin } from 'vite' +import type { AssetURLOptions, AssetURLTagConfig } from 'vue/compiler-sfc' +import type { ViteBundlerOptions } from '../types.js' + +/** + * Wrapper of official vue plugin + */ +export const vuepressVuePlugin = (options: ViteBundlerOptions): Plugin => { + return vuePlugin({ + ...options.vuePluginOptions, + template: { + ...options.vuePluginOptions?.template, + transformAssetUrls: resolveTransformAssetUrls(options), + }, + }) +} + +/** + * Determine if the given `transformAssetUrls` option is `AssetURLTagConfig` + */ +const isAssetURLTagConfig = ( + transformAssetUrls: AssetURLOptions | AssetURLTagConfig, +): transformAssetUrls is AssetURLTagConfig => + Object.values(transformAssetUrls).some((val) => Array.isArray(val)) + +/** + * Resolve `template.transformAssetUrls` option from user config + */ +const resolveTransformAssetUrls = ( + options: ViteBundlerOptions, +): AssetURLOptions => { + // default transformAssetUrls option + const defaultTransformAssetUrls = { includeAbsolute: true } + + // user provided transformAssetUrls option + const { transformAssetUrls: userTransformAssetUrls } = + options.vuePluginOptions?.template ?? {} + + // if user does not provide an object as transformAssetUrls + if (typeof userTransformAssetUrls !== 'object') { + return defaultTransformAssetUrls + } + + // AssetURLTagConfig + if (isAssetURLTagConfig(userTransformAssetUrls)) { + return { + ...defaultTransformAssetUrls, + tags: userTransformAssetUrls, + } + } + + // AssetURLOptions + return { + ...defaultTransformAssetUrls, + ...userTransformAssetUrls, + } +} diff --git a/packages/bundler-vite/src/resolveViteConfig.ts b/packages/bundler-vite/src/resolveViteConfig.ts index 2bff1201ec..ee18480c1e 100644 --- a/packages/bundler-vite/src/resolveViteConfig.ts +++ b/packages/bundler-vite/src/resolveViteConfig.ts @@ -1,8 +1,11 @@ -import { default as vuePlugin } from '@vitejs/plugin-vue' import type { App } from '@vuepress/core' import type { InlineConfig } from 'vite' import { mergeConfig } from 'vite' -import { mainPlugin, userConfigPlugin } from './plugins/index.js' +import { + vuepressMainPlugin, + vuepressUserConfigPlugin, + vuepressVuePlugin, +} from './plugins/index.js' import type { ViteBundlerOptions } from './types.js' export const resolveViteConfig = async ({ @@ -25,9 +28,9 @@ export const resolveViteConfig = async ({ charset: 'utf8', }, plugins: [ - vuePlugin(options.vuePluginOptions), - mainPlugin({ app, isBuild, isServer }), - userConfigPlugin(options), + vuepressVuePlugin(options), + vuepressMainPlugin({ app, isBuild, isServer }), + vuepressUserConfigPlugin(options), ], }, // some vite options would not take effect inside a plugin, so we still need to merge them here in addition to userConfigPlugin diff --git a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts index 2323206746..cc6ca7f7b9 100644 --- a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts +++ b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts @@ -4,6 +4,11 @@ import type { MarkdownEnv } from '../../types.js' import { resolveLink } from './resolveLink.js' export interface AssetsPluginOptions { + /** + * Whether to prepend base to absolute path + */ + absolutePathPrependBase?: boolean + /** * Prefix to add to relative assets links */ @@ -15,7 +20,10 @@ export interface AssetsPluginOptions { */ export const assetsPlugin: PluginWithOptions = ( md, - { relativePathPrefix = '@source' }: AssetsPluginOptions = {}, + { + absolutePathPrependBase = false, + relativePathPrefix = '@source', + }: AssetsPluginOptions = {}, ) => { // wrap raw image renderer rule const rawImageRule = md.renderer.rules.image! @@ -27,7 +35,10 @@ export const assetsPlugin: PluginWithOptions = ( if (link) { // replace the original link with resolved link - token.attrSet('src', resolveLink(link, relativePathPrefix, env)) + token.attrSet( + 'src', + resolveLink(link, { env, absolutePathPrependBase, relativePathPrefix }), + ) } return rawImageRule(tokens, idx, options, env, self) @@ -43,12 +54,12 @@ export const assetsPlugin: PluginWithOptions = ( .replace( /( - `${prefix}${quote}${resolveLink( - src.trim(), - relativePathPrefix, + `${prefix}${quote}${resolveLink(src.trim(), { env, - true, - )}${quote}`, + absolutePathPrependBase, + relativePathPrefix, + strict: true, + })}${quote}`, ) // handle srcset .replace( @@ -57,18 +68,16 @@ export const assetsPlugin: PluginWithOptions = ( `${prefix}${quote}${srcset .split(',') .map((item) => - item - .trim() - .replace( - /^([^ ]*?)([ \n].*)?$/, - (_, url, descriptor = '') => - `${resolveLink( - url.trim(), - relativePathPrefix, - env, - true, - )}${descriptor.replace(/[ \n]+/g, ' ').trimEnd()}`, - ), + item.trim().replace( + /^([^ ]*?)([ \n].*)?$/, + (_, url, descriptor = '') => + `${resolveLink(url.trim(), { + env, + absolutePathPrependBase, + relativePathPrefix, + strict: true, + })}${descriptor.replace(/[ \n]+/g, ' ').trimEnd()}`, + ), ) .join(', ')}${quote}`, ) diff --git a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts index c490a0df0a..b9277ed855 100644 --- a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts +++ b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts @@ -2,11 +2,21 @@ import { path } from '@vuepress/utils' import { decode } from 'mdurl' import type { MarkdownEnv } from '../../types.js' +interface ResolveLinkOptions { + env: MarkdownEnv + absolutePathPrependBase?: boolean + relativePathPrefix: string + strict?: boolean +} + export const resolveLink = ( link: string, - relativePathPrefix: string, - env: MarkdownEnv, - strict = false, + { + env, + absolutePathPrependBase = false, + relativePathPrefix, + strict = false, + }: ResolveLinkOptions, ): string => { // do not resolve data uri if (link.startsWith('data:')) return link @@ -30,5 +40,10 @@ export const resolveLink = ( )}` } + // prepend base to absolute path if needed + if (absolutePathPrependBase && env.base && link.startsWith('/')) { + resolvedLink = path.join(env.base, resolvedLink) + } + return resolvedLink } diff --git a/packages/markdown/tests/plugins/assetsPlugin.spec.ts b/packages/markdown/tests/plugins/assetsPlugin.spec.ts index 6c8c81f008..fa69f7e25c 100644 --- a/packages/markdown/tests/plugins/assetsPlugin.spec.ts +++ b/packages/markdown/tests/plugins/assetsPlugin.spec.ts @@ -48,6 +48,7 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { description: 'should handle assets link with default options', md: MarkdownIt().use(assetsPlugin), env: { + base: '/base/', filePathRelative: 'sub/foo.md', }, expected: [ @@ -83,6 +84,48 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { 'data-uri', ], }, + { + description: 'should respect `absolutePathPrependBase` option', + md: MarkdownIt().use(assetsPlugin, { + absolutePathPrependBase: true, + }), + env: { + base: '/base/', + filePathRelative: 'sub/foo.md', + }, + expected: [ + // relative paths + 'foo', + 'foo2', + 'foo-bar', + 'foo-bar2', + 'baz', + 'out', + '汉字', + '100%', + // absolute paths + 'absolute', + 'absolute-foo', + // no-prefix paths + 'no-prefix', + 'no-prefix-foo', + 'alias', + '汉字', + '100%', + '~alias', + '~汉字', + '~100%', + // keep as is + 'url', + 'empty', + // invalid paths + 'invalid', + '汉字', + '100%', + // data uri + 'data-uri', + ], + }, { description: 'should respect `relativePathPrefix` option', md: MarkdownIt().use(assetsPlugin, {