diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b8d66d7d9..66d88322dd 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`. +- `utils-bundler`: Utilities for bundler packages. ## Development Setup diff --git a/CONTRIBUTING_zh.md b/CONTRIBUTING_zh.md index d4f6060cae..4e04f55f9d 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` 操作。 +- `utils-bundler`: 供 Bundler 模块使用的工具函数模块。 ## 开发配置 diff --git a/packages/bundler-vite/package.json b/packages/bundler-vite/package.json index f98c07bf4f..b12dbbbce5 100644 --- a/packages/bundler-vite/package.json +++ b/packages/bundler-vite/package.json @@ -40,6 +40,7 @@ "@vuepress/core": "workspace:*", "@vuepress/shared": "workspace:*", "@vuepress/utils": "workspace:*", + "@vuepress/utils-bundler": "workspace:*", "autoprefixer": "^10.4.20", "connect-history-api-fallback": "^2.0.0", "postcss": "^8.4.45", diff --git a/packages/bundler-vite/src/build/build.ts b/packages/bundler-vite/src/build/build.ts index 33466ea6b0..ad2950a7ee 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 type { App, Bundler } from '@vuepress/core' -import { colors, debug, fs, importFile, withSpinner } from '@vuepress/utils' +import { colors, debug, fs, withSpinner } from '@vuepress/utils' +import { createVueServerApp, getSsrTemplate } from '@vuepress/utils-bundler' 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..0410975dd5 100644 --- a/packages/bundler-vite/src/build/renderPage.ts +++ b/packages/bundler-vite/src/build/renderPage.ts @@ -1,10 +1,8 @@ import type { App, Page } from '@vuepress/core' -import type { VuepressSSRContext } from '@vuepress/shared' import { fs, renderHead } from '@vuepress/utils' +import { renderPageToString } from '@vuepress/utils-bundler' 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..c179a512e3 100644 --- a/packages/bundler-webpack/package.json +++ b/packages/bundler-webpack/package.json @@ -43,6 +43,7 @@ "@vuepress/core": "workspace:*", "@vuepress/shared": "workspace:*", "@vuepress/utils": "workspace:*", + "@vuepress/utils-bundler": "workspace:*", "autoprefixer": "^10.4.20", "chokidar": "^3.6.0", "copy-webpack-plugin": "^12.0.2", diff --git a/packages/bundler-webpack/src/build/build.ts b/packages/bundler-webpack/src/build/build.ts index 82774429d3..9443e5c82b 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 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 { createVueServerApp, getSsrTemplate } from '@vuepress/utils-bundler' 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..e06082afab 100644 --- a/packages/bundler-webpack/src/build/renderPage.ts +++ b/packages/bundler-webpack/src/build/renderPage.ts @@ -1,9 +1,8 @@ import type { App, Page } from '@vuepress/core' -import type { VuepressSSRContext } from '@vuepress/shared' import { fs, renderHead } from '@vuepress/utils' +import type { PageSSRContext } from '@vuepress/utils-bundler' +import { renderPageToString } from '@vuepress/utils-bundler' 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/utils-bundler/README.md b/packages/utils-bundler/README.md new file mode 100644 index 0000000000..e3862713be --- /dev/null +++ b/packages/utils-bundler/README.md @@ -0,0 +1,12 @@ +# @vuepress/utils-bundler + +[![npm](https://badgen.net/npm/v/@vuepress/utils-bundler/next)](https://www.npmjs.com/package/@vuepress/utils-bundler) +[![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/utils-bundler/package.json b/packages/utils-bundler/package.json new file mode 100644 index 0000000000..3eee8106f3 --- /dev/null +++ b/packages/utils-bundler/package.json @@ -0,0 +1,59 @@ +{ + "name": "@vuepress/utils-bundler", + "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/utils-bundler/src/build/createVueServerApp.ts b/packages/utils-bundler/src/build/createVueServerApp.ts new file mode 100644 index 0000000000..515525527a --- /dev/null +++ b/packages/utils-bundler/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/utils-bundler/src/build/getSsrTemplate.ts b/packages/utils-bundler/src/build/getSsrTemplate.ts new file mode 100644 index 0000000000..113f04e4e2 --- /dev/null +++ b/packages/utils-bundler/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/utils-bundler/src/build/index.ts b/packages/utils-bundler/src/build/index.ts new file mode 100644 index 0000000000..a3e53e9f79 --- /dev/null +++ b/packages/utils-bundler/src/build/index.ts @@ -0,0 +1,3 @@ +export * from './createVueServerApp' +export * from './getSsrTemplate' +export * from './renderPageToString' diff --git a/packages/utils-bundler/src/build/renderPageToString.ts b/packages/utils-bundler/src/build/renderPageToString.ts new file mode 100644 index 0000000000..8885738401 --- /dev/null +++ b/packages/utils-bundler/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/utils-bundler/src/index.ts b/packages/utils-bundler/src/index.ts new file mode 100644 index 0000000000..5cdad37752 --- /dev/null +++ b/packages/utils-bundler/src/index.ts @@ -0,0 +1 @@ +export * from './build/index.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e966ef62..92c106f63d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@vuepress/utils': specifier: workspace:* version: link:../utils + '@vuepress/utils-bundler': + specifier: workspace:* + version: link:../utils-bundler autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.45) @@ -187,6 +190,9 @@ importers: '@vuepress/utils': specifier: workspace:* version: link:../utils + '@vuepress/utils-bundler': + specifier: workspace:* + version: link:../utils-bundler autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.45) @@ -406,6 +412,27 @@ importers: specifier: ^2.0.1 version: 2.0.1 + packages/utils-bundler: + 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/vuepress: dependencies: '@vuepress/bundler-vite': diff --git a/vitest.config.ts b/vitest.config.ts index b273f940d2..5a23a49981 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ exclude: [ 'packages/bundler-*/**', 'packages/client/**', + 'packages/utils-bundler/**', 'packages/vuepress/**', ], include: ['packages/*/src/**'],