diff --git a/.vscode/settings.json b/.vscode/settings.json index 8bbae20cf8..ab616e0d15 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,7 @@ ], "cSpell.words": [ "bumpp", + "bundlerutils", "composables", "devtool", "docsearch", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b8d66d7d9..f6b667a1e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,11 @@ Wrapper of core packages: - `vuepress`: A wrapper of the above packages, and provides `vuepress` command line tool. Users need to choose and install bundler and theme by themselves. -Bundler packages: +Bundler and related packages: - `bundler-vite`: The VuePress bundler package with vite. Use vite to `dev` and `build` VuePress app that generated by `@vuepress/core`. - `bundler-webpack`: The VuePress bundler package with webpack. Use webpack to `dev` and `build` VuePress app that generated by `@vuepress/core`. +- `bundlerutils`: Utilities for bundler packages. ## Development Setup diff --git a/CONTRIBUTING_zh.md b/CONTRIBUTING_zh.md index d4f6060cae..4fde3085b5 100644 --- a/CONTRIBUTING_zh.md +++ b/CONTRIBUTING_zh.md @@ -17,10 +17,11 @@ Core Packages 的封装: - `vuepress`: 是上述 Core Packages 的封装,提供了 `vuepress` 命令行工具。用户需要在此包的基础上自行选择并安装打包工具和主题。 -Bundler Packages : +Bundler 及其相关 Packages : - `bundler-vite`: 基于 Vite 的 Bundler 模块。使用 Vite 对 VuePress App 执行 `dev` 和 `build` 操作。 - `bundler-webpack`: 基于 Webpack 的 Bundler 模块。使用 Webpack 对 VuePress App 执行 `dev` 和 `build` 操作。 +- `bundlerutils`: 供 Bundler 模块使用的工具函数模块。 ## 开发配置 diff --git a/packages/bundler-vite/package.json b/packages/bundler-vite/package.json index f98c07bf4f..b5c0f7b754 100644 --- a/packages/bundler-vite/package.json +++ b/packages/bundler-vite/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@vitejs/plugin-vue": "^5.1.3", + "@vuepress/bundlerutils": "workspace:*", "@vuepress/client": "workspace:*", "@vuepress/core": "workspace:*", "@vuepress/shared": "workspace:*", diff --git a/packages/bundler-vite/src/build/build.ts b/packages/bundler-vite/src/build/build.ts index 33466ea6b0..12ddffc26c 100644 --- a/packages/bundler-vite/src/build/build.ts +++ b/packages/bundler-vite/src/build/build.ts @@ -1,6 +1,6 @@ -import type { CreateVueAppFunction } from '@vuepress/client' +import { createVueServerApp, getSsrTemplate } from '@vuepress/bundlerutils' import type { App, Bundler } from '@vuepress/core' -import { colors, debug, fs, importFile, withSpinner } from '@vuepress/utils' +import { colors, debug, fs, withSpinner } from '@vuepress/utils' import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup' import { build as viteBuild } from 'vite' import { resolveViteConfig } from '../resolveViteConfig.js' @@ -58,19 +58,11 @@ export const build = async ( (item) => item.type === 'chunk' && item.isEntry, ) as OutputChunk - // load the compiled server bundle - const serverEntryPath = app.dir.temp('.server', serverEntryChunk.fileName) - const { createVueApp } = await importFile<{ - createVueApp: CreateVueAppFunction - }>(serverEntryPath) - // create vue ssr app - const { app: vueApp, router: vueRouter } = await createVueApp() - const { renderToString } = await import('vue/server-renderer') - - // load ssr template file - const ssrTemplate = await fs.readFile(app.options.templateBuild, { - encoding: 'utf8', - }) + // create vue ssr app and get ssr template + const { vueApp, vueRouter } = await createVueServerApp( + app.dir.temp('.server', serverEntryChunk.fileName), + ) + const ssrTemplate = await getSsrTemplate(app) // pre-render pages to html files for (const page of app.pages) { @@ -80,7 +72,6 @@ export const build = async ( page, vueApp, vueRouter, - renderToString, ssrTemplate, output: clientOutput.output, outputEntryChunk: clientEntryChunk, diff --git a/packages/bundler-vite/src/build/renderPage.ts b/packages/bundler-vite/src/build/renderPage.ts index a0af5b2374..a81175b451 100644 --- a/packages/bundler-vite/src/build/renderPage.ts +++ b/packages/bundler-vite/src/build/renderPage.ts @@ -1,10 +1,8 @@ +import { renderPageToString } from '@vuepress/bundlerutils' import type { App, Page } from '@vuepress/core' -import type { VuepressSSRContext } from '@vuepress/shared' import { fs, renderHead } from '@vuepress/utils' import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup' import type { App as VueApp } from 'vue' -import { ssrContextKey } from 'vue' -import type { SSRContext } from 'vue/server-renderer' import type { Router } from 'vue-router' import { renderPagePrefetchLinks } from './renderPagePrefetchLinks.js' import { renderPagePreloadLinks } from './renderPagePreloadLinks.js' @@ -17,7 +15,6 @@ export const renderPage = async ({ page, vueApp, vueRouter, - renderToString, ssrTemplate, output, outputEntryChunk, @@ -27,32 +24,24 @@ export const renderPage = async ({ page: Page vueApp: VueApp vueRouter: Router - renderToString: (input: VueApp, context: SSRContext) => Promise ssrTemplate: string output: RollupOutput['output'] outputEntryChunk: OutputChunk outputCssAsset: OutputAsset | undefined }): Promise => { - // switch to current page route - await vueRouter.push(page.path) - await vueRouter.isReady() - - // create vue ssr context with default values - delete vueApp._context.provides[ssrContextKey] - const ssrContext: VuepressSSRContext = { - lang: 'en', - head: [], - } - // render current page to string - const pageRendered = await renderToString(vueApp, ssrContext) + const { ssrContext, ssrString } = await renderPageToString({ + page, + vueApp, + vueRouter, + }) // resolve page chunks const pageChunkFiles = resolvePageChunkFiles({ page, output }) // generate html string const html = await app.options.templateBuildRenderer(ssrTemplate, { - content: pageRendered, + content: ssrString, head: ssrContext.head.map(renderHead).join(''), lang: ssrContext.lang, prefetch: renderPagePrefetchLinks({ diff --git a/packages/bundler-webpack/package.json b/packages/bundler-webpack/package.json index 893252e963..28ee050c6e 100644 --- a/packages/bundler-webpack/package.json +++ b/packages/bundler-webpack/package.json @@ -39,6 +39,7 @@ "dependencies": { "@types/express": "^4.17.21", "@types/webpack-env": "^1.18.5", + "@vuepress/bundlerutils": "workspace:*", "@vuepress/client": "workspace:*", "@vuepress/core": "workspace:*", "@vuepress/shared": "workspace:*", diff --git a/packages/bundler-webpack/src/build/build.ts b/packages/bundler-webpack/src/build/build.ts index 82774429d3..fed7bf18ae 100644 --- a/packages/bundler-webpack/src/build/build.ts +++ b/packages/bundler-webpack/src/build/build.ts @@ -1,13 +1,6 @@ -import type { CreateVueAppFunction } from '@vuepress/client' +import { createVueServerApp, getSsrTemplate } from '@vuepress/bundlerutils' import type { App, Bundler } from '@vuepress/core' -import { - colors, - debug, - fs, - importFileDefault, - logger, - withSpinner, -} from '@vuepress/utils' +import { colors, debug, fs, logger, withSpinner } from '@vuepress/utils' import webpack from 'webpack' import { resolveWebpackConfig } from '../resolveWebpackConfig.js' import type { WebpackBundlerOptions } from '../types.js' @@ -80,19 +73,11 @@ export const build = async ( const { initialFilesMeta, asyncFilesMeta, moduleFilesMetaMap } = resolveClientManifestMeta(clientManifest) - // load the compiled server bundle - const serverEntryPath = app.dir.temp('.server/app.cjs') - const { createVueApp } = await importFileDefault<{ - createVueApp: CreateVueAppFunction - }>(serverEntryPath) - // create vue ssr app - const { app: vueApp, router: vueRouter } = await createVueApp() - const { renderToString } = await import('vue/server-renderer') - - // load ssr template file - const ssrTemplate = await fs.readFile(app.options.templateBuild, { - encoding: 'utf8', - }) + // create vue ssr app and get ssr template + const { vueApp, vueRouter } = await createVueServerApp( + app.dir.temp('.server/app.cjs'), + ) + const ssrTemplate = await getSsrTemplate(app) // pre-render pages to html files for (const page of app.pages) { @@ -104,7 +89,6 @@ export const build = async ( page, vueApp, vueRouter, - renderToString, ssrTemplate, initialFilesMeta, asyncFilesMeta, diff --git a/packages/bundler-webpack/src/build/renderPage.ts b/packages/bundler-webpack/src/build/renderPage.ts index 6d3c888dd9..87c5b7c54b 100644 --- a/packages/bundler-webpack/src/build/renderPage.ts +++ b/packages/bundler-webpack/src/build/renderPage.ts @@ -1,9 +1,8 @@ +import type { PageSSRContext } from '@vuepress/bundlerutils' +import { renderPageToString } from '@vuepress/bundlerutils' import type { App, Page } from '@vuepress/core' -import type { VuepressSSRContext } from '@vuepress/shared' import { fs, renderHead } from '@vuepress/utils' import type { App as VueApp } from 'vue' -import { ssrContextKey } from 'vue' -import type { SSRContext } from 'vue/server-renderer' import type { Router } from 'vue-router' import { renderPagePrefetchLinks } from './renderPagePrefetchLinks.js' import { renderPagePreloadLinks } from './renderPagePreloadLinks.js' @@ -12,7 +11,7 @@ import { renderPageStyles } from './renderPageStyles.js' import { resolvePageClientFilesMeta } from './resolvePageClientFilesMeta.js' import type { FileMeta, ModuleFilesMetaMap } from './types.js' -interface PageRenderContext extends SSRContext, VuepressSSRContext { +interface WebpackPageSSRContext extends PageSSRContext { /** * Injected by vuepress-ssr-loader * @@ -29,7 +28,6 @@ export const renderPage = async ({ page, vueApp, vueRouter, - renderToString, ssrTemplate, initialFilesMeta, asyncFilesMeta, @@ -39,26 +37,19 @@ export const renderPage = async ({ page: Page vueApp: VueApp vueRouter: Router - renderToString: (input: VueApp, context: SSRContext) => Promise ssrTemplate: string initialFilesMeta: FileMeta[] asyncFilesMeta: FileMeta[] moduleFilesMetaMap: ModuleFilesMetaMap }): Promise => { - // switch to current page route - await vueRouter.push(page.path) - await vueRouter.isReady() - - // create vue ssr context with default values - delete vueApp._context.provides[ssrContextKey] - const ssrContext: PageRenderContext = { - _registeredComponents: new Set(), - lang: 'en', - head: [], - } - // render current page to string - const pageRendered = await renderToString(vueApp, ssrContext) + const { ssrContext, ssrString } = + await renderPageToString({ + page, + vueApp, + vueRouter, + ssrContextInit: { _registeredComponents: new Set() }, + }) // resolve client files that used by this page const pageClientFilesMeta = resolvePageClientFilesMeta({ @@ -68,7 +59,7 @@ export const renderPage = async ({ // generate html string const html = await app.options.templateBuildRenderer(ssrTemplate, { - content: pageRendered, + content: ssrString, head: ssrContext.head.map(renderHead).join(''), lang: ssrContext.lang, prefetch: renderPagePrefetchLinks({ diff --git a/packages/bundlerutils/README.md b/packages/bundlerutils/README.md new file mode 100644 index 0000000000..080a27ad7f --- /dev/null +++ b/packages/bundlerutils/README.md @@ -0,0 +1,12 @@ +# @vuepress/bundlerutils + +[![npm](https://badgen.net/npm/v/@vuepress/bundlerutils/next)](https://www.npmjs.com/package/@vuepress/bundlerutils) +[![license](https://badgen.net/github/license/vuepress/core)](https://github.com/vuepress/core/blob/main/LICENSE) + +## Documentation + +https://vuepress.vuejs.org + +## License + +[MIT](https://github.com/vuepress/core/blob/main/LICENSE) diff --git a/packages/bundlerutils/package.json b/packages/bundlerutils/package.json new file mode 100644 index 0000000000..7ab36cc1f0 --- /dev/null +++ b/packages/bundlerutils/package.json @@ -0,0 +1,59 @@ +{ + "name": "@vuepress/bundlerutils", + "version": "2.0.0-rc.15", + "description": "Utils package of VuePress bundler", + "keywords": [ + "bundler", + "vuepress", + "utils" + ], + "homepage": "https://github.com/vuepress", + "bugs": { + "url": "https://github.com/vuepress/core/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/core.git" + }, + "license": "MIT", + "author": "meteorlxy", + "type": "module", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "clean": "rimraf dist" + }, + "dependencies": { + "@vuepress/client": "workspace:*", + "@vuepress/core": "workspace:*", + "@vuepress/shared": "workspace:*", + "@vuepress/utils": "workspace:*", + "vue": "^3.5.3", + "vue-router": "^4.4.3" + }, + "publishConfig": { + "access": "public" + }, + "tsup": { + "clean": true, + "dts": "./src/index.ts", + "entry": [ + "./src/index.ts" + ], + "format": [ + "esm" + ], + "outDir": "./dist", + "sourcemap": false, + "target": "es2022", + "tsconfig": "../../tsconfig.dts.json" + } +} diff --git a/packages/bundlerutils/src/build/createVueServerApp.ts b/packages/bundlerutils/src/build/createVueServerApp.ts new file mode 100644 index 0000000000..515525527a --- /dev/null +++ b/packages/bundlerutils/src/build/createVueServerApp.ts @@ -0,0 +1,29 @@ +import type { CreateVueAppFunction } from '@vuepress/client' +import { importFile, importFileDefault } from '@vuepress/utils' +import type { App } from 'vue' +import type { Router } from 'vue-router' + +/** + * Create vue app and router for server side rendering + */ +export const createVueServerApp = async ( + serverAppPath: string, +): Promise<{ + vueApp: App + vueRouter: Router +}> => { + // use different import function for cjs and esm + const importer = serverAppPath.endsWith('.cjs') + ? importFileDefault + : importFile + + // import the server app entry file + const { createVueApp } = await importer<{ + createVueApp: CreateVueAppFunction + }>(serverAppPath) + + // create vue app + const { app, router } = await createVueApp() + + return { vueApp: app, vueRouter: router } +} diff --git a/packages/bundlerutils/src/build/getSsrTemplate.ts b/packages/bundlerutils/src/build/getSsrTemplate.ts new file mode 100644 index 0000000000..113f04e4e2 --- /dev/null +++ b/packages/bundlerutils/src/build/getSsrTemplate.ts @@ -0,0 +1,8 @@ +import type { App } from '@vuepress/core' +import { fs } from '@vuepress/utils' + +/** + * Util to read the ssr template file + */ +export const getSsrTemplate = async (app: App): Promise => + fs.readFile(app.options.templateBuild, { encoding: 'utf8' }) diff --git a/packages/bundlerutils/src/build/index.ts b/packages/bundlerutils/src/build/index.ts new file mode 100644 index 0000000000..a3e53e9f79 --- /dev/null +++ b/packages/bundlerutils/src/build/index.ts @@ -0,0 +1,3 @@ +export * from './createVueServerApp' +export * from './getSsrTemplate' +export * from './renderPageToString' diff --git a/packages/bundlerutils/src/build/renderPageToString.ts b/packages/bundlerutils/src/build/renderPageToString.ts new file mode 100644 index 0000000000..8885738401 --- /dev/null +++ b/packages/bundlerutils/src/build/renderPageToString.ts @@ -0,0 +1,51 @@ +import type { Page } from '@vuepress/core' +import type { VuepressSSRContext } from '@vuepress/shared' +import type { App as VueApp } from 'vue' +import { ssrContextKey } from 'vue' +import type { SSRContext } from 'vue/server-renderer' +import type { Router } from 'vue-router' + +export type PageSSRContext = SSRContext & VuepressSSRContext + +/** + * Render a vuepress page to string + */ +export const renderPageToString = async < + T extends PageSSRContext = PageSSRContext, +>({ + page, + vueApp, + vueRouter, + ssrContextInit, +}: { + page: Page + vueApp: VueApp + vueRouter: Router + ssrContextInit?: Partial +}): Promise<{ + ssrContext: T + ssrString: string +}> => { + // switch to current page route + await vueRouter.push(page.path) + await vueRouter.isReady() + + // create vue ssr context with default values + delete vueApp._context.provides[ssrContextKey] + const ssrContext = { + lang: 'en', + head: [], + ...ssrContextInit, + } satisfies PageSSRContext as T + + // lazy load renderToString function + const { renderToString } = await import('vue/server-renderer') + + // render current page to string + const ssrString = await renderToString(vueApp, ssrContext) + + return { + ssrContext, + ssrString, + } +} diff --git a/packages/bundlerutils/src/index.ts b/packages/bundlerutils/src/index.ts new file mode 100644 index 0000000000..5cdad37752 --- /dev/null +++ b/packages/bundlerutils/src/index.ts @@ -0,0 +1 @@ +export * from './build/index.js' diff --git a/packages/core/src/page/resolvePageChunkInfo.ts b/packages/core/src/page/resolvePageChunkInfo.ts index 39b9945ef9..4e5fb32029 100644 --- a/packages/core/src/page/resolvePageChunkInfo.ts +++ b/packages/core/src/page/resolvePageChunkInfo.ts @@ -2,7 +2,7 @@ import { path, transformPathToFileName } from '@vuepress/utils' import type { App } from '../types/index.js' /** - * Resolve page chunk file relative info + * Resolve page chunk file related info * * @internal */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e966ef62..bcbf38f2c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@vitejs/plugin-vue': specifier: ^5.1.3 version: 5.1.3(vite@5.4.3(@types/node@22.5.4)(lightningcss@1.26.0)(sass-embedded@1.78.0)(sass@1.78.0)(terser@5.32.0))(vue@3.5.3(typescript@5.6.2)) + '@vuepress/bundlerutils': + specifier: workspace:* + version: link:../bundlerutils '@vuepress/client': specifier: workspace:* version: link:../client @@ -175,6 +178,9 @@ importers: '@types/webpack-env': specifier: ^1.18.5 version: 1.18.5 + '@vuepress/bundlerutils': + specifier: workspace:* + version: link:../bundlerutils '@vuepress/client': specifier: workspace:* version: link:../client @@ -248,6 +254,27 @@ importers: specifier: ^6.0.1 version: 6.0.1 + packages/bundlerutils: + dependencies: + '@vuepress/client': + specifier: workspace:* + version: link:../client + '@vuepress/core': + specifier: workspace:* + version: link:../core + '@vuepress/shared': + specifier: workspace:* + version: link:../shared + '@vuepress/utils': + specifier: workspace:* + version: link:../utils + vue: + specifier: ^3.5.3 + version: 3.5.3(typescript@5.6.2) + vue-router: + specifier: ^4.4.3 + version: 4.4.3(vue@3.5.3(typescript@5.6.2)) + packages/cli: dependencies: '@vuepress/core': diff --git a/vitest.config.ts b/vitest.config.ts index b273f940d2..27f8a252c5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ coverage: { all: true, exclude: [ - 'packages/bundler-*/**', + 'packages/bundler*/**', 'packages/client/**', 'packages/vuepress/**', ],