diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e50862dc93..1002d2234e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,7 +20,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node: ['22', '24'] - bundler: ['webpack', 'vite'] + bundler: ['rspack', 'webpack', 'vite'] runs-on: ${{ matrix.os }} diff --git a/.vscode/settings.json b/.vscode/settings.json index b2caabf7db..58b417f6be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "mdit", "prefetch", "preload", + "rspack", "slugify", "tinyglobby", "unmount", diff --git a/e2e/docs/.vuepress/config.ts b/e2e/docs/.vuepress/config.ts index 4e9fada490..316dc14137 100644 --- a/e2e/docs/.vuepress/config.ts +++ b/e2e/docs/.vuepress/config.ts @@ -1,5 +1,6 @@ import process from 'node:process' +import { rspackBundler } from '@vuepress/bundler-rspack' import { viteBundler } from '@vuepress/bundler-vite' import { webpackBundler } from '@vuepress/bundler-webpack' import { defineUserConfig } from 'vuepress' @@ -56,15 +57,17 @@ export default defineUserConfig({ }, bundler: - E2E_BUNDLER === 'webpack' - ? webpackBundler() - : viteBundler({ - viteOptions: { - optimizeDeps: { - include: ['@vuepress-e2e/conditional-exports'], + E2E_BUNDLER === 'rspack' + ? rspackBundler() + : E2E_BUNDLER === 'webpack' + ? webpackBundler() + : viteBundler({ + viteOptions: { + optimizeDeps: { + include: ['@vuepress-e2e/conditional-exports'], + }, }, - }, - }), + }), theme: e2eTheme(), diff --git a/e2e/package.json b/e2e/package.json index 89295e7208..d57a4929bb 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -5,19 +5,24 @@ "type": "module", "scripts": { "docs:build": "vuepress build docs --clean-cache --clean-temp", + "docs:build:rspack": "cross-env E2E_BUNDLER=rspack pnpm docs:build", "docs:build:webpack": "cross-env E2E_BUNDLER=webpack pnpm docs:build", "docs:clean": "rimraf docs/.vuepress/.temp docs/.vuepress/.cache docs/.vuepress/dist", "docs:dev": "vuepress dev docs --clean-cache --clean-temp", + "docs:dev:rspack": "cross-env E2E_BUNDLER=rspack pnpm docs:dev", "docs:dev:webpack": "cross-env E2E_BUNDLER=webpack pnpm docs:dev", "docs:serve": "serve -l 9080 docs/.vuepress/dist", "e2e:build": "cross-env E2E_COMMAND=build playwright test", + "e2e:build:rspack": "cross-env E2E_COMMAND=build E2E_BUNDLER=rspack playwright test", "e2e:build:webpack": "cross-env E2E_COMMAND=build E2E_BUNDLER=webpack playwright test", "e2e:dev": "cross-env E2E_COMMAND=dev playwright test", + "e2e:dev:rspack": "cross-env E2E_COMMAND=dev E2E_BUNDLER=rspack playwright test", "e2e:dev:webpack": "cross-env E2E_COMMAND=dev E2E_BUNDLER=webpack playwright test" }, "dependencies": { "@vuepress-e2e/conditional-exports": "file:./modules/conditional-exports", "@vuepress-e2e/style-exports": "file:./modules/style-exports", + "@vuepress/bundler-rspack": "workspace:*", "@vuepress/bundler-vite": "workspace:*", "@vuepress/bundler-webpack": "workspace:*", "sass": "^1.99.0", diff --git a/packages/bundler-rspack/README.md b/packages/bundler-rspack/README.md new file mode 100644 index 0000000000..964763a99f --- /dev/null +++ b/packages/bundler-rspack/README.md @@ -0,0 +1,12 @@ +# @vuepress/bundler-rspack + +[![npm](https://badgen.net/npm/v/@vuepress/bundler-rspack/next)](https://www.npmjs.com/package/@vuepress/bundler-rspack) +[![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/bundler-rspack/package.json b/packages/bundler-rspack/package.json new file mode 100644 index 0000000000..faedd71f1c --- /dev/null +++ b/packages/bundler-rspack/package.json @@ -0,0 +1,69 @@ +{ + "name": "@vuepress/bundler-rspack", + "version": "2.0.0-rc.29", + "description": "Bundler rspack package of VuePress", + "keywords": [ + "bundler", + "rspack", + "vuepress", + "vuepress-bundler" + ], + "homepage": "https://github.com/vuepress", + "bugs": { + "url": "https://github.com/vuepress/core/issues" + }, + "license": "MIT", + "author": "meteorlxy", + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/core.git" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "imports": { + "#vuepress-markdown-loader": "./dist/vuepress-markdown-loader.cjs", + "#vuepress-ssr-loader": "./dist/vuepress-ssr-loader.cjs" + }, + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsdown", + "clean": "rimraf dist" + }, + "dependencies": { + "@rspack/core": "^2.0.3", + "@rspack/dev-server": "^2.0.1", + "@vuepress/bundlerutils": "workspace:*", + "@vuepress/client": "workspace:*", + "@vuepress/core": "workspace:*", + "@vuepress/shared": "workspace:*", + "@vuepress/utils": "workspace:*", + "autoprefixer": "^10.5.0", + "css-loader": "^7.1.4", + "esbuild-loader": "~4.4.3", + "postcss": "^8.5.14", + "postcss-loader": "^8.2.1", + "rspack-chain": "^2.0.1", + "rspack-merge": "^0.1.1", + "style-loader": "^4.0.0", + "vue": "catalog:vue", + "vue-loader": "^17.4.2", + "vue-router": "catalog:vue" + }, + "devDependencies": { + "connect-next": "^4.0.1" + }, + "engines": { + "node": "^22.18.0 || ^24 || >=26" + } +} diff --git a/packages/bundler-rspack/src/build/build.ts b/packages/bundler-rspack/src/build/build.ts new file mode 100644 index 0000000000..b7d8b3345a --- /dev/null +++ b/packages/bundler-rspack/src/build/build.ts @@ -0,0 +1,114 @@ +import { rspack } from '@rspack/core' +import type { MultiRspackOptions } from '@rspack/core' +import { createVueServerApp, getSsrTemplate } from '@vuepress/bundlerutils' +import type { App, Bundler } from '@vuepress/core' +import { colors, debug, fs, logger, withSpinner } from '@vuepress/utils' + +import { resolveRspackConfig } from '../resolveRspackConfig.js' +import type { RspackBundlerOptions } from '../types.js' +import { + CLIENT_MANIFEST_FILENAME, + createClientConfig, +} from './createClientConfig.js' +import { createServerConfig } from './createServerConfig.js' +import { renderPage } from './renderPage.js' +import { resolveClientManifestMeta } from './resolveClientManifestMeta.js' +import type { ClientManifest } from './types.js' + +const log = debug('vuepress:bundler-rspack/build') + +export const build = async ( + options: RspackBundlerOptions, + app: App, +): ReturnType => { + // plugin hook: extendsBundlerOptions + await app.pluginApi.hooks.extendsBundlerOptions.process(options, app) + + // rspack compile + log('compiling start') + await withSpinner('Compiling with rspack')(async () => { + // create rspack config + const clientConfig = resolveRspackConfig({ + config: await createClientConfig(app, options), + options, + isServer: false, + isBuild: true, + }) + const serverConfig = resolveRspackConfig({ + config: await createServerConfig(app, options), + options, + isServer: true, + isBuild: true, + }) + + await new Promise((resolve, reject) => { + rspack( + [clientConfig, serverConfig] as MultiRspackOptions, + (err, stats) => { + if (err) { + reject(err) + } else { + if (stats) { + const { warnings, errors } = stats.toJson('errors-warnings') + + errors?.forEach((item) => { + logger.error(item) + }) + warnings?.forEach((warning) => { + logger.warn(warning) + }) + + if (stats.hasErrors()) + reject(new Error('Failed to compile with errors')) + } + + resolve() + } + }, + ) + }) + }) + log('compiling finish') + + // render pages + await withSpinner(`Rendering ${app.pages.length} pages`)(async (spinner) => { + // load the client manifest file + const clientManifestPath = app.dir.temp(CLIENT_MANIFEST_FILENAME) + const clientManifest = (await fs.readJson( + clientManifestPath, + )) as ClientManifest + + // resolve client files meta + const { initialFilesMeta, asyncFilesMeta, moduleFilesMetaMap } = + resolveClientManifestMeta(clientManifest) + + // 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) { + if (spinner) { + spinner.text = `Rendering pages ${colors.magenta(page.path)}` + } + await renderPage({ + app, + page, + vueApp, + vueRouter, + ssrTemplate, + initialFilesMeta, + asyncFilesMeta, + moduleFilesMetaMap, + }) + } + }) + + // keep the server bundle files in debug mode + if (!app.env.isDebug) { + // remove server temp directory after pages rendered + await fs.remove(app.dir.temp('.server')) + } +} diff --git a/packages/bundler-rspack/src/build/createClientConfig.ts b/packages/bundler-rspack/src/build/createClientConfig.ts new file mode 100644 index 0000000000..a4d9a509b9 --- /dev/null +++ b/packages/bundler-rspack/src/build/createClientConfig.ts @@ -0,0 +1,96 @@ +import { + CopyRspackPlugin, + CssExtractRspackPlugin, + LightningCssMinimizerRspackPlugin, +} from '@rspack/core' +import type { Module } from '@rspack/core' +import type { App } from '@vuepress/core' +import { fs } from '@vuepress/utils' +import type { RspackChain } from 'rspack-chain' + +import { createClientBaseConfig } from '../config/index.js' +import type { RspackBundlerOptions } from '../types.js' +import { createClientPlugin } from './createClientPlugin.js' + +/** + * Filename of the client manifest file that generated by client plugin + */ +export const CLIENT_MANIFEST_FILENAME = '.server/client-manifest.json' + +export const createClientConfig = async ( + app: App, + options: RspackBundlerOptions, +): Promise => { + const config = await createClientBaseConfig({ + app, + options, + isBuild: true, + }) + + // vuepress client plugin, handle client assets info for ssr + config + .plugin('vuepress-client') + .use(createClientPlugin(app.dir.temp(CLIENT_MANIFEST_FILENAME))) + + // copy files from public dir to dest dir + if (fs.pathExistsSync(app.dir.public())) { + config.plugin('copy').use(CopyRspackPlugin, [ + { + patterns: [{ from: app.dir.public(), to: app.dir.dest() }], + }, + ]) + } + + // optimizations for production mode + // css-extract + config.plugin('css-extract').use(CssExtractRspackPlugin, [ + { + filename: 'assets/css/styles.[chunkhash:8].css', + }, + ]) + + config.optimization.splitChunks({ + cacheGroups: { + // ensure all css are extracted together. + // since most of the CSS will be from the theme and very little + // CSS will be from async chunks + styles: { + idHint: 'styles', + // necessary to ensure async chunks are also extracted + test: (m: Module) => m.type.includes('css/mini-extract'), + chunks: 'all', + enforce: true, + reuseExistingChunk: true, + }, + // extract external library to a standalone chunk + vendor: { + idHint: 'vendor', + test: /node_modules/, + chunks: 'all', + priority: -10, + reuseExistingChunk: true, + }, + }, + }) + + // enable runtimeChunk + config.optimization.runtimeChunk(true) + + // minimize + config.optimization.minimize(true) + + // minimizer + config.optimization.set('minimizer', [ + // keep the default minimizer + '...', + // add css minimizer + new LightningCssMinimizerRspackPlugin(), + ]) + + // disable performance hints + if (!app.env.isDebug) { + config.performance.hints(false) + } + + return config +} diff --git a/packages/bundler-rspack/src/build/createClientPlugin.ts b/packages/bundler-rspack/src/build/createClientPlugin.ts new file mode 100644 index 0000000000..adc1a895aa --- /dev/null +++ b/packages/bundler-rspack/src/build/createClientPlugin.ts @@ -0,0 +1,115 @@ +import type { RspackPluginInstance, StatsModule } from '@rspack/core' +import { fs } from '@vuepress/utils' + +import type { ClientManifest } from './types.js' + +const isJS = (file: string): boolean => /\.js(\?[^.]+)?$/.test(file) + +const isCSS = (file: string): boolean => /\.css(\?[^.]+)?$/.test(file) + +/** + * Vuepress client plugin + * + * Collecting rspack bundled files info for SSR + */ +export const createClientPlugin = ( + outputFile: string, +): RspackPluginInstance => { + const clientPlugin: RspackPluginInstance = { + apply(compiler) { + compiler.hooks.emit.tapPromise( + 'vuepress-client-plugin', + async (compilation) => { + // get rspack stats object + const { + modules = [], + entrypoints = {}, + chunks = [], + } = compilation.getStats().toJson({ + all: false, + modules: true, + entrypoints: true, + chunks: true, + chunkModules: true, + }) + + // get all js/css files from all chunks + const allFiles = chunks.flatMap((c) => c.files ?? []) + + // get initial entry files + const initialFiles = Object.keys(entrypoints) + .flatMap( + (entry) => + entrypoints[entry].assets?.map(({ name }) => name) ?? [], + ) + .filter((file) => isJS(file) || isCSS(file)) + + // get files that should be loaded asynchronously + // i.e. script and style files that are not included in the initial entry files + const asyncFiles = allFiles.filter( + (file) => + (isJS(file) || isCSS(file)) && !initialFiles.includes(file), + ) + + // get asset modules + const assetModules = modules.filter( + (m): m is Required> & StatsModule => + Boolean(m.assets?.length), + ) + + // get modules for client manifest + const manifestModules: ClientManifest['modules'] = {} + + const fileToIndex = (file: number | string): number => + allFiles.indexOf(file.toString()) + + modules.forEach((m) => { + // ignore modules duplicated in multiple chunks + if (m.chunks?.length !== 1) { + return + } + + const cid = m.chunks[0] + const chunk = chunks.find((c) => c.id === cid) + + if (!chunk?.files) { + return + } + + // remove appended hash of module identifier + // which is the request string of the module + const request = m.identifier?.replace(/\|\w+$/, '') + + // get chunk files index + const files = [...chunk.files.map(fileToIndex)] + + // find all asset modules associated with the same chunk + assetModules.forEach((item) => { + if (item.chunks?.some((id) => id === cid)) { + // get asset files + files.push(...item.assets.map(fileToIndex)) + } + }) + + // map the module request to files index + if (request) manifestModules[request] = files + }) + + // generate client manifest json file + const clientManifest: ClientManifest = { + all: allFiles, + initial: initialFiles, + async: asyncFiles, + modules: manifestModules, + } + + const clientManifestJson = JSON.stringify(clientManifest, null, 2) + + await fs.outputFile(outputFile, clientManifestJson) + }, + ) + }, + } + + return clientPlugin +} diff --git a/packages/bundler-rspack/src/build/createServerConfig.ts b/packages/bundler-rspack/src/build/createServerConfig.ts new file mode 100644 index 0000000000..2eba869e0b --- /dev/null +++ b/packages/bundler-rspack/src/build/createServerConfig.ts @@ -0,0 +1,45 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +import { createBaseConfig } from '../config/index.js' +import type { RspackBundlerOptions } from '../types.js' + +export const createServerConfig = async ( + app: App, + options: RspackBundlerOptions, +): Promise => { + const isBuild = true + const isServer = true + + const config = await createBaseConfig({ + app, + options, + isBuild, + isServer, + }) + + // server output + // remove after pages rendered + config.output + .path(app.dir.temp('.server')) + .filename('app.cjs') + .publicPath(app.options.base) + .library({ type: 'commonjs2' }) + + // set target to node + // vue-loader will use compiler-ssr internally + config.target('node') + + // set externals + // externalize vue in ssr mode, because we need to import `'vue/server-renderer'` in node side + // for ssr usage, then we also need vue as peer-dependency when using pnpm + config.externals(['vue']) + + // devtool + config.devtool('source-map') + + // do not need to minimize server bundle + config.optimization.minimize(false) + + return config +} diff --git a/packages/bundler-rspack/src/build/index.ts b/packages/bundler-rspack/src/build/index.ts new file mode 100644 index 0000000000..33b5713ae3 --- /dev/null +++ b/packages/bundler-rspack/src/build/index.ts @@ -0,0 +1 @@ +export * from './build.js' diff --git a/packages/bundler-rspack/src/build/renderPage.ts b/packages/bundler-rspack/src/build/renderPage.ts new file mode 100644 index 0000000000..1f4a90ede1 --- /dev/null +++ b/packages/bundler-rspack/src/build/renderPage.ts @@ -0,0 +1,83 @@ +import type { PageSSRContext } from '@vuepress/bundlerutils' +import { renderPageToString } from '@vuepress/bundlerutils' +import type { App, Page } from '@vuepress/core' +import { fs, renderHead } from '@vuepress/utils' +import type { App as VueApp } from 'vue' +import type { Router } from 'vue-router' + +import { renderPagePrefetchLinks } from './renderPagePrefetchLinks.js' +import { renderPagePreloadLinks } from './renderPagePreloadLinks.js' +import { renderPageScripts } from './renderPageScripts.js' +import { renderPageStyles } from './renderPageStyles.js' +import { resolvePageClientFilesMeta } from './resolvePageClientFilesMeta.js' +import type { FileMeta, ModuleFilesMetaMap } from './types.js' + +interface RspackPageSSRContext extends PageSSRContext { + /** + * Injected by vuepress-ssr-loader + * + * Store the module request of components that used by current page + */ + _registeredComponents: Set +} + +/** + * Render page to html file, return the html file path + */ +export const renderPage = async ({ + app, + page, + vueApp, + vueRouter, + ssrTemplate, + initialFilesMeta, + asyncFilesMeta, + moduleFilesMetaMap, +}: { + app: App + page: Page + vueApp: VueApp + vueRouter: Router + ssrTemplate: string + initialFilesMeta: FileMeta[] + asyncFilesMeta: FileMeta[] + moduleFilesMetaMap: ModuleFilesMetaMap +}): Promise => { + // render current page to string + const { ssrContext, ssrString } = + await renderPageToString({ + page, + vueApp, + vueRouter, + ssrContextInit: { _registeredComponents: new Set() }, + }) + + // resolve client files that used by this page + const pageClientFilesMeta = resolvePageClientFilesMeta({ + moduleRequests: Array.from(ssrContext._registeredComponents), + moduleFilesMetaMap, + }) + + // generate html string + const html = await app.options.templateBuildRenderer(ssrTemplate, { + content: ssrString, + head: ssrContext.head.map(renderHead).join(''), + lang: ssrContext.lang, + prefetch: renderPagePrefetchLinks({ + app, + asyncFilesMeta, + pageClientFilesMeta, + }), + preload: renderPagePreloadLinks({ + app, + initialFilesMeta, + pageClientFilesMeta, + }), + scripts: renderPageScripts({ app, initialFilesMeta, pageClientFilesMeta }), + styles: renderPageStyles({ app, initialFilesMeta, pageClientFilesMeta }), + version: app.version, + }) + + // write html file + await fs.outputFile(page.htmlFilePath, html) +} diff --git a/packages/bundler-rspack/src/build/renderPagePrefetchLinks.ts b/packages/bundler-rspack/src/build/renderPagePrefetchLinks.ts new file mode 100644 index 0000000000..810fd115d0 --- /dev/null +++ b/packages/bundler-rspack/src/build/renderPagePrefetchLinks.ts @@ -0,0 +1,39 @@ +import type { App } from '@vuepress/core' + +import type { FileMeta } from './types.js' + +/** + * Render prefetch links of current page + */ +export const renderPagePrefetchLinks = ({ + app, + asyncFilesMeta, + pageClientFilesMeta, +}: { + app: App + asyncFilesMeta: FileMeta[] + pageClientFilesMeta: FileMeta[] +}): string => { + // shouldPrefetch option + const { shouldPrefetch } = app.options + + // do not render prefetch links + if (shouldPrefetch === false) { + return '' + } + + // async files excluding files used by current page should be prefetch + const prefetchFilesMeta = asyncFilesMeta.filter( + ({ file }) => !pageClientFilesMeta.some((f) => f.file === file), + ) + + return prefetchFilesMeta + .map(({ file, type }) => { + // user wants to explicitly control what to prefetch + if (shouldPrefetch !== true && !shouldPrefetch(file, type)) { + return '' + } + return `` + }) + .join('') +} diff --git a/packages/bundler-rspack/src/build/renderPagePreloadLinks.ts b/packages/bundler-rspack/src/build/renderPagePreloadLinks.ts new file mode 100644 index 0000000000..e3dda92dee --- /dev/null +++ b/packages/bundler-rspack/src/build/renderPagePreloadLinks.ts @@ -0,0 +1,45 @@ +import type { App } from '@vuepress/core' + +import type { FileMeta } from './types.js' + +/** + * Render preload links of current page + */ +export const renderPagePreloadLinks = ({ + app, + initialFilesMeta, + pageClientFilesMeta, +}: { + app: App + initialFilesMeta: FileMeta[] + pageClientFilesMeta: FileMeta[] +}): string => { + // shouldPreload option + const { shouldPreload } = app.options + + // do not render preload links + if (shouldPreload === false) { + return '' + } + + // initial files and files used by current page should be preload + const preloadFilesMeta = [...initialFilesMeta, ...pageClientFilesMeta] + + return preloadFilesMeta + .map(({ file, extension, type }) => { + // by default, we only preload scripts or css + if (shouldPreload === true && type !== 'script' && type !== 'style') { + return '' + } + + // user wants to explicitly control what to preload + if (shouldPreload !== true && !shouldPreload(file, type)) { + return '' + } + + return `` + }) + .join('') +} diff --git a/packages/bundler-rspack/src/build/renderPageScripts.ts b/packages/bundler-rspack/src/build/renderPageScripts.ts new file mode 100644 index 0000000000..09ca2c619f --- /dev/null +++ b/packages/bundler-rspack/src/build/renderPageScripts.ts @@ -0,0 +1,23 @@ +import type { App } from '@vuepress/core' + +import type { FileMeta } from './types.js' + +/** + * Render scripts of current page + */ +export const renderPageScripts = ({ + app, + initialFilesMeta, + pageClientFilesMeta, +}: { + app: App + initialFilesMeta: FileMeta[] + pageClientFilesMeta: FileMeta[] +}): string => + // include initial JS files and other async JS files of current page + [...pageClientFilesMeta, ...initialFilesMeta] + .filter(({ type }) => type === 'script') + .map( + ({ file }) => ``, + ) + .join('') diff --git a/packages/bundler-rspack/src/build/renderPageStyles.ts b/packages/bundler-rspack/src/build/renderPageStyles.ts new file mode 100644 index 0000000000..d97ac87b16 --- /dev/null +++ b/packages/bundler-rspack/src/build/renderPageStyles.ts @@ -0,0 +1,24 @@ +import type { App } from '@vuepress/core' + +import type { FileMeta } from './types.js' + +/** + * Render styles of current page + */ +export const renderPageStyles = ({ + app, + initialFilesMeta, + pageClientFilesMeta, +}: { + app: App + initialFilesMeta: FileMeta[] + pageClientFilesMeta: FileMeta[] +}): string => + // include initial CSS files and other async CSS files of current page + // notice here we put async CSS files after initial CSS files + [...initialFilesMeta, ...pageClientFilesMeta] + .filter(({ type }) => type === 'style') + .map( + ({ file }) => ``, + ) + .join('') diff --git a/packages/bundler-rspack/src/build/resolveClientManifestMeta.ts b/packages/bundler-rspack/src/build/resolveClientManifestMeta.ts new file mode 100644 index 0000000000..8cef9fdb27 --- /dev/null +++ b/packages/bundler-rspack/src/build/resolveClientManifestMeta.ts @@ -0,0 +1,46 @@ +import { resolveFileMeta } from './resolveFileMeta.js' +import type { ClientManifest, FileMeta, ModuleFilesMetaMap } from './types.js' + +/** + * Resolve files meta from clientManifest + */ +export const resolveClientManifestMeta = ({ + all, + initial, + async, + modules, +}: ClientManifest): { + allFilesMeta: FileMeta[] + initialFilesMeta: FileMeta[] + asyncFilesMeta: FileMeta[] + moduleFilesMetaMap: ModuleFilesMetaMap +} => { + // all files meta + const allFilesMeta = all.map(resolveFileMeta) + + // initial files meta + const initialFilesMeta = initial.map(resolveFileMeta) + + // async files meta + const asyncFilesMeta = async.map(resolveFileMeta) + + // module to files meta map + const moduleFilesMetaMap = Object.fromEntries( + Object.entries(modules).map(([moduleRequest, assetFilesIndex]) => [ + moduleRequest, + assetFilesIndex + .map((fileIndex) => allFilesMeta[fileIndex]) + .filter( + ({ file, type }) => + async.includes(file) || (type !== 'style' && type !== 'script'), + ), + ]), + ) + + return { + allFilesMeta, + initialFilesMeta, + asyncFilesMeta, + moduleFilesMetaMap, + } +} diff --git a/packages/bundler-rspack/src/build/resolveFileMeta.ts b/packages/bundler-rspack/src/build/resolveFileMeta.ts new file mode 100644 index 0000000000..f28d0db986 --- /dev/null +++ b/packages/bundler-rspack/src/build/resolveFileMeta.ts @@ -0,0 +1,16 @@ +import { path } from '@vuepress/utils' + +import { resolveFileMetaType } from './resolveFileMetaType.js' +import type { FileMeta } from './types.js' + +/** + * Resolve client file meta from to file name + */ +export const resolveFileMeta = (file: string): FileMeta => { + const extension = path.extname(file).slice(1) + return { + file, + extension, + type: resolveFileMetaType(extension), + } +} diff --git a/packages/bundler-rspack/src/build/resolveFileMetaType.ts b/packages/bundler-rspack/src/build/resolveFileMetaType.ts new file mode 100644 index 0000000000..c19fb4245d --- /dev/null +++ b/packages/bundler-rspack/src/build/resolveFileMetaType.ts @@ -0,0 +1,21 @@ +import type { FileMetaType } from './types.js' + +/** + * Resolve client file type by extension + */ +export const resolveFileMetaType = (extension: string): FileMetaType => { + if (extension === 'js') { + return 'script' + } + if (extension === 'css') { + return 'style' + } + if (/jpe?g|png|svg|gif|webp|ico/i.test(extension)) { + return 'image' + } + if (/woff2?|ttf|otf|eot/i.test(extension)) { + return 'font' + } + // not exhausting all possibilities here, but above covers common cases + return '' +} diff --git a/packages/bundler-rspack/src/build/resolvePageClientFilesMeta.ts b/packages/bundler-rspack/src/build/resolvePageClientFilesMeta.ts new file mode 100644 index 0000000000..17a3c57963 --- /dev/null +++ b/packages/bundler-rspack/src/build/resolvePageClientFilesMeta.ts @@ -0,0 +1,19 @@ +import type { FileMeta, ModuleFilesMetaMap } from './types.js' + +/** + * Get all client files according to module requests of a page + */ +export const resolvePageClientFilesMeta = ({ + moduleRequests, + moduleFilesMetaMap, +}: { + moduleRequests: string[] + moduleFilesMetaMap: ModuleFilesMetaMap +}): FileMeta[] => { + const files = new Set() + moduleRequests.forEach((request) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unsafe indexed access + moduleFilesMetaMap[request]?.forEach((file) => files.add(file)) + }) + return Array.from(files) +} diff --git a/packages/bundler-rspack/src/build/types.ts b/packages/bundler-rspack/src/build/types.ts new file mode 100644 index 0000000000..11df28d5b6 --- /dev/null +++ b/packages/bundler-rspack/src/build/types.ts @@ -0,0 +1,39 @@ +/** + * Client file meta + */ +export interface FileMeta { + /** + * file name + */ + file: string + + /** + * file extension + */ + extension: string + + /** + * file type + */ + type: FileMetaType +} + +/** + * Client file meta type, mainly used for + */ +export type FileMetaType = '' | 'font' | 'image' | 'script' | 'style' + +/** + * A "module request" to "client files meta" key-value map + */ +export type ModuleFilesMetaMap = Record + +/** + * Client manifest that collected from rspack stats + */ +export interface ClientManifest { + all: string[] + initial: string[] + async: string[] + modules: Record +} diff --git a/packages/bundler-rspack/src/config/createBaseConfig.ts b/packages/bundler-rspack/src/config/createBaseConfig.ts new file mode 100644 index 0000000000..48476ca747 --- /dev/null +++ b/packages/bundler-rspack/src/config/createBaseConfig.ts @@ -0,0 +1,69 @@ +import type { App } from '@vuepress/core' +import { RspackChain } from 'rspack-chain' + +import type { RspackBundlerOptions } from '../types.js' +import { handleDevtool } from './handleDevtool.js' +import { handleEntry } from './handleEntry.js' +import { handleMode } from './handleMode.js' +import { handleModule } from './handleModule.js' +import { handleNode } from './handleNode.js' +import { handleOtherOptions } from './handleOtherOptions.js' +import { handlePluginDefine } from './handlePluginDefine.js' +import { handleResolve } from './handleResolve.js' + +export const createBaseConfig = async ({ + app, + options, + isBuild, + isServer, +}: { + app: App + options: RspackBundlerOptions + isBuild: boolean + isServer: boolean +}): Promise => { + // create new rspack-chain config + const config = new RspackChain() + + /** + * entry + */ + handleEntry({ app, config }) + + /** + * mode + */ + handleMode({ app, config, isBuild }) + + /** + * node + */ + handleNode({ config }) + + /** + * devtool + */ + handleDevtool({ app, config, isBuild }) + + /** + * resolve + */ + await handleResolve({ app, config, isServer }) + + /** + * module + */ + handleModule({ app, options, config, isBuild, isServer }) + + /** + * plugins + */ + await handlePluginDefine({ app, config, isBuild, isServer }) + + /** + * other options + */ + handleOtherOptions({ app, config, isBuild, isServer }) + + return config +} diff --git a/packages/bundler-rspack/src/config/createClientBaseConfig.ts b/packages/bundler-rspack/src/config/createClientBaseConfig.ts new file mode 100644 index 0000000000..b6259513eb --- /dev/null +++ b/packages/bundler-rspack/src/config/createClientBaseConfig.ts @@ -0,0 +1,32 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +import type { RspackBundlerOptions } from '../types.js' +import { createBaseConfig } from './createBaseConfig.js' + +export const createClientBaseConfig = async ({ + app, + options, + isBuild, +}: { + app: App + options: RspackBundlerOptions + isBuild: boolean +}): Promise => { + const config = await createBaseConfig({ + app, + options, + isServer: false, + isBuild, + }) + + // client output + config.output + .path(app.dir.dest()) + .filename( + isBuild ? 'assets/js/[name].[chunkhash:8].js' : 'assets/js/[name].js', + ) + .publicPath(app.options.base) + + return config +} diff --git a/packages/bundler-rspack/src/config/handleDevtool.ts b/packages/bundler-rspack/src/config/handleDevtool.ts new file mode 100644 index 0000000000..8e16791ef6 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleDevtool.ts @@ -0,0 +1,23 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack devtool + */ +export const handleDevtool = ({ + app, + config, + isBuild, +}: { + app: App + config: RspackChain + isBuild: boolean +}): void => { + if (app.env.isDebug) { + // always enable source-map in debug mode + config.devtool('source-map') + } else if (!isBuild) { + // only enable eval-source-map in dev mode + config.devtool('eval-cheap-module-source-map') + } +} diff --git a/packages/bundler-rspack/src/config/handleEntry.ts b/packages/bundler-rspack/src/config/handleEntry.ts new file mode 100644 index 0000000000..e41b5cf5de --- /dev/null +++ b/packages/bundler-rspack/src/config/handleEntry.ts @@ -0,0 +1,25 @@ +import type { App } from '@vuepress/core' +import { fs } from '@vuepress/utils' +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack entry + */ +export const handleEntry = ({ + app, + config, +}: { + app: App + config: RspackChain +}): void => { + // set client app as entry point + config.entry('app').add( + app.dir.client( + ( + fs.readJsonSync(app.dir.client('package.json')) as { + exports: { './app': string } + } + ).exports['./app'], + ), + ) +} diff --git a/packages/bundler-rspack/src/config/handleMode.ts b/packages/bundler-rspack/src/config/handleMode.ts new file mode 100644 index 0000000000..6437d150bd --- /dev/null +++ b/packages/bundler-rspack/src/config/handleMode.ts @@ -0,0 +1,17 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack mode + */ +export const handleMode = ({ + app, + config, + isBuild, +}: { + app: App + config: RspackChain + isBuild: boolean +}): void => { + config.mode(!isBuild || app.env.isDebug ? 'development' : 'production') +} diff --git a/packages/bundler-rspack/src/config/handleModule.ts b/packages/bundler-rspack/src/config/handleModule.ts new file mode 100644 index 0000000000..675d4ec854 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModule.ts @@ -0,0 +1,48 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +import type { RspackBundlerOptions } from '../types.js' +import { handleModuleAssets } from './handleModuleAssets.js' +import { handleModuleJs } from './handleModuleJs.js' +import { handleModulePug } from './handleModulePug.js' +import { handleModuleStyles } from './handleModuleStyles.js' +import { handleModuleTs } from './handleModuleTs.js' +import { handleModuleVue } from './handleModuleVue.js' + +/** + * Set rspack module + */ +export const handleModule = ({ + app, + options, + config, + isBuild, + isServer, +}: { + app: App + options: RspackBundlerOptions + config: RspackChain + isBuild: boolean + isServer: boolean +}): void => { + // noParse + config.module.noParse(/^(?:vue|vue-router|(?:@vue\/[^/]+))$/) + + // vue files + handleModuleVue({ app, options, config, isBuild, isServer }) + + // pug files, for templates + handleModulePug({ config }) + + // images & media & fonts + handleModuleAssets({ config }) + + // js files + handleModuleJs({ options, config, isBuild, isServer }) + + // ts files + handleModuleTs({ config }) + + // styles files + handleModuleStyles({ options, config, isBuild, isServer }) +} diff --git a/packages/bundler-rspack/src/config/handleModuleAssets.ts b/packages/bundler-rspack/src/config/handleModuleAssets.ts new file mode 100644 index 0000000000..deaa25bcad --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModuleAssets.ts @@ -0,0 +1,48 @@ +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack config to handle assets files + */ +export const handleModuleAssets = ({ + config, +}: { + config: RspackChain +}): void => { + // images + config.module + .rule('images') + .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/i) + .type('asset') + .generator({ + filename: 'assets/img/[name].[contenthash:8][ext]', + }) + + // svg + // do not base64-inline SVGs. + // https://github.com/facebookincubator/create-react-app/pull/1180 + config.module + .rule('svg') + .test(/\.(svg)(\?.*)?$/i) + .type('asset/resource') + .generator({ + filename: 'assets/img/[name].[contenthash:8][ext]', + }) + + // media + config.module + .rule('media') + .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i) + .type('asset/resource') + .generator({ + filename: 'assets/media/[name].[contenthash:8][ext]', + }) + + // fonts + config.module + .rule('fonts') + .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i) + .type('asset/resource') + .generator({ + filename: 'assets/fonts/[name].[contenthash:8][ext]', + }) +} diff --git a/packages/bundler-rspack/src/config/handleModuleJs.ts b/packages/bundler-rspack/src/config/handleModuleJs.ts new file mode 100644 index 0000000000..afd19f4a99 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModuleJs.ts @@ -0,0 +1,54 @@ +import type { RspackChain } from 'rspack-chain' + +import type { RspackBundlerOptions } from '../types.js' +import { resolveSwcLoaderOptions } from './resolveSwcLoaderOptions.js' + +/** + * Set rspack module to handle js files + */ +export const handleModuleJs = ({ + options, + config, + isBuild, + isServer, +}: { + options: RspackBundlerOptions + config: RspackChain + isBuild: boolean + isServer: boolean +}): void => { + // only enable transpilation in production client bundle + // when `evergreen` option is set to `false` + if (options.evergreen !== false || !isBuild || isServer) { + return + } + + config.module + .rule('js') + .test(/\.jsx?$/) + .exclude.add((filePath) => { + // always transpile js / jsx in vue files + if (/\.vue\.jsx?$/.test(filePath)) { + return false + } + // transpile all core packages and vuepress related packages. + // i.e. + // @vuepress/* + // vuepress-* + if ( + /(@vuepress[/\\][^/\\]*|vuepress-[^/\\]*)[/\\](?!node_modules).*\.js$/.test( + filePath, + ) + ) { + return false + } + // don't transpile node_modules + return filePath.includes('node_modules') + }) + .end() + // use swc-loader + .use('swc-loader') + .loader('builtin:swc-loader') + .options(resolveSwcLoaderOptions()) + .end() +} diff --git a/packages/bundler-rspack/src/config/handleModulePug.ts b/packages/bundler-rspack/src/config/handleModulePug.ts new file mode 100644 index 0000000000..e410f6469d --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModulePug.ts @@ -0,0 +1,12 @@ +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack module to handle pug files + */ +export const handleModulePug = ({ config }: { config: RspackChain }): void => { + config.module + .rule('pug') + .test(/\.pug$/) + .use('pug-plain-loader') + .loader('pug-plain-loader') +} diff --git a/packages/bundler-rspack/src/config/handleModuleStyles.ts b/packages/bundler-rspack/src/config/handleModuleStyles.ts new file mode 100644 index 0000000000..802e76b1bf --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModuleStyles.ts @@ -0,0 +1,131 @@ +import { createRequire } from 'node:module' + +import { CssExtractRspackPlugin } from '@rspack/core' +import autoprefixer from 'autoprefixer' +import type { RspackChain } from 'rspack-chain' + +import type { + RspackBundlerOptions, + StylePreprocessorLoaderOptions, +} from '../types.js' + +const require = createRequire(import.meta.url) + +/** + * Set rspack module to handle style files + */ +export const handleModuleStyles = ({ + options, + config, + isBuild, + isServer, +}: { + options: RspackBundlerOptions + config: RspackChain + isBuild: boolean + isServer: boolean +}): void => { + const handleStyle = ({ + lang, + test, + loaderName, + loaderOptions, + }: { + lang: string + test: RegExp + loaderName?: string + loaderOptions?: StylePreprocessorLoaderOptions + }): void => { + // override rspack's native CSS type so CssExtractRspackPlugin + css-loader can process it + const rule = config.module.rule(lang).test(test).type('javascript/auto') + + if (!isServer) { + if (isBuild) { + rule.use('css-extract-loader').loader(CssExtractRspackPlugin.loader) + } else { + rule.use('style-loader').loader(require.resolve('style-loader')) + } + } + + // use css-loader + rule + .use('css-loader') + .loader(require.resolve('css-loader')) + .options({ + modules: { + auto: true, + exportLocalsConvention: 'as-is', + exportOnlyLocals: isServer, + localIdentName: `[local]_[contenthash:base64:8]`, + namedExport: false, + }, + importLoaders: loaderName ? 2 : 1, + }) + + // use postcss-loader + rule + .use('postcss-loader') + .loader(require.resolve('postcss-loader')) + .options({ + postcssOptions: { + plugins: [autoprefixer], + }, + ...options.postcss, + }) + + // use extra loader + if (loaderName) { + rule + .use(loaderName) + .loader(loaderName) + .options(loaderOptions ?? {}) + } + } + + handleStyle({ + lang: 'css', + test: /\.css$/, + }) + + handleStyle({ + lang: 'postcss', + test: /\.p(ost)?css$/, + }) + + handleStyle({ + lang: 'scss', + test: /\.scss$/, + loaderName: 'sass-loader', + loaderOptions: options.scss, + }) + + handleStyle({ + lang: 'sass', + test: /\.sass$/, + loaderName: 'sass-loader', + loaderOptions: options.sass, + }) + + handleStyle({ + lang: 'less', + test: /\.less$/, + loaderName: 'less-loader', + loaderOptions: options.less, + }) + + handleStyle({ + lang: 'stylus', + test: /\.styl(us)?$/, + loaderName: 'stylus-loader', + loaderOptions: { + stylusOptions: { + // allow literal css import + includeCSS: true, + // no need to compress with stylus + // we will handle it by postcss-loader + compress: false, + }, + ...options.stylus, + }, + }) +} diff --git a/packages/bundler-rspack/src/config/handleModuleTs.ts b/packages/bundler-rspack/src/config/handleModuleTs.ts new file mode 100644 index 0000000000..5f4b672192 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModuleTs.ts @@ -0,0 +1,17 @@ +import type { RspackChain } from 'rspack-chain' + +import { resolveSwcLoaderOptions } from './resolveSwcLoaderOptions.js' + +/** + * Set rspack module to handle ts files + */ +export const handleModuleTs = ({ config }: { config: RspackChain }): void => { + config.module + .rule('ts') + .test(/\.tsx?/) + // use swc-loader + .use('swc-loader') + .loader('builtin:swc-loader') + .options(resolveSwcLoaderOptions({}, true)) + .end() +} diff --git a/packages/bundler-rspack/src/config/handleModuleVue.ts b/packages/bundler-rspack/src/config/handleModuleVue.ts new file mode 100644 index 0000000000..0b407eb14d --- /dev/null +++ b/packages/bundler-rspack/src/config/handleModuleVue.ts @@ -0,0 +1,71 @@ +import { createRequire } from 'node:module' + +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' +import type { VueLoaderOptions } from 'vue-loader' +import { VueLoaderPlugin } from 'vue-loader' + +import type { VuepressMarkdownLoaderOptions } from '../loaders/vuepressMarkdownLoader.js' +import type { RspackBundlerOptions } from '../types.js' + +const require = createRequire(import.meta.url) + +/** + * Set rspack module to handle vue files + */ +export const handleModuleVue = ({ + app, + options, + config, + isBuild, + isServer, +}: { + app: App + options: RspackBundlerOptions + config: RspackChain + isBuild: boolean + isServer: boolean +}): void => { + const handleVue = ({ + lang, + test, + }: { + lang: 'md' | 'vue' + test: RegExp + }): void => { + const rule = config.module.rule(lang).test(test) + + // use internal vuepress-ssr-loader to handle SSR dependencies + if (isBuild) { + rule + .use('vuepress-ssr-loader') + .loader(require.resolve('#vuepress-ssr-loader')) + .end() + } + + // use official vue-loader + rule + .use('vue-loader') + .loader(require.resolve('vue-loader')) + .options({ + ...options.vue, + isServerBuild: isServer, + } satisfies VueLoaderOptions) + .end() + + // use internal vuepress-markdown-loader to handle markdown files + if (lang === 'md') { + rule + .use('vuepress-markdown-loader') + .loader(require.resolve('#vuepress-markdown-loader')) + .options({ app } satisfies VuepressMarkdownLoaderOptions) + .end() + } + } + + handleVue({ lang: 'md', test: /\.md$/ }) + handleVue({ lang: 'vue', test: /\.vue$/ }) + + // use vue-loader plugin + config.plugin('vue-loader').use(VueLoaderPlugin) +} diff --git a/packages/bundler-rspack/src/config/handleNode.ts b/packages/bundler-rspack/src/config/handleNode.ts new file mode 100644 index 0000000000..878960b123 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleNode.ts @@ -0,0 +1,12 @@ +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack node config + */ +export const handleNode = ({ config }: { config: RspackChain }): void => { + // do not polyfill or mock node globals and modules + config.node + .set('__filename', false) + .set('__dirname', false) + .set('global', false) +} diff --git a/packages/bundler-rspack/src/config/handleOtherOptions.ts b/packages/bundler-rspack/src/config/handleOtherOptions.ts new file mode 100644 index 0000000000..9e3a75d29f --- /dev/null +++ b/packages/bundler-rspack/src/config/handleOtherOptions.ts @@ -0,0 +1,52 @@ +import { createRequire } from 'node:module' + +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +const require = createRequire(import.meta.url) + +/** + * Set rspack other options + * + * @see https://rspack.rs/config/other-options + */ +export const handleOtherOptions = ({ + app, + config, + isBuild, + isServer, +}: { + app: App + config: RspackChain + isBuild: boolean + isServer: boolean +}): void => { + // set infrastructureLogging options + config.infrastructureLogging({ + level: app.env.isDebug ? 'info' : 'error', + }) + + // set cache options + config.cache({ + type: 'persistent', + storage: { + type: 'filesystem', + directory: app.dir.cache(), + }, + version: JSON.stringify({ + // vuepress identifiers + isBuild, + isServer, + 'version': app.version, + // dependencies + 'esbuild-loader': ( + require('esbuild-loader/package.json') as { version: string } + ).version, + 'vue-loader': (require('vue-loader/package.json') as { version: string }) + .version, + '@rspack/core': ( + require('@rspack/core/package.json') as { version: string } + ).version, + }), + }) +} diff --git a/packages/bundler-rspack/src/config/handlePluginDefine.ts b/packages/bundler-rspack/src/config/handlePluginDefine.ts new file mode 100644 index 0000000000..d2de6c9246 --- /dev/null +++ b/packages/bundler-rspack/src/config/handlePluginDefine.ts @@ -0,0 +1,49 @@ +import { DefinePlugin } from '@rspack/core' +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack DefinePlugin + */ +export const handlePluginDefine = async ({ + app, + config, + isBuild, + isServer, +}: { + app: App + config: RspackChain + isBuild: boolean + isServer: boolean +}): Promise => { + // define plugin + config.plugin('define').use(DefinePlugin, [ + { + __VUEPRESS_VERSION__: JSON.stringify(app.version), + __VUEPRESS_BASE__: JSON.stringify(app.options.base), + __VUEPRESS_DEV__: JSON.stringify(!isBuild), + __VUEPRESS_SSR__: JSON.stringify(isServer), + // @see http://link.vuejs.org/feature-flags + // enable options API by default + __VUE_OPTIONS_API__: JSON.stringify(true), + __VUE_PROD_DEVTOOLS__: JSON.stringify(app.env.isDebug), + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(app.env.isDebug), + }, + ]) + + // plugin hook: define + const defineResult = await app.pluginApi.hooks.define.process(app, isServer) + + // tap the arguments of DefinePlugin + config.plugin('define').tap(([options]) => { + defineResult.forEach((defineObject) => { + Object.entries(defineObject).forEach(([key, value]) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + options[key] = JSON.stringify(value) + }) + }) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return [options] + }) +} diff --git a/packages/bundler-rspack/src/config/handleResolve.ts b/packages/bundler-rspack/src/config/handleResolve.ts new file mode 100644 index 0000000000..986ece5320 --- /dev/null +++ b/packages/bundler-rspack/src/config/handleResolve.ts @@ -0,0 +1,53 @@ +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +/** + * Set rspack resolve + */ +export const handleResolve = async ({ + app, + config, + isServer, +}: { + app: App + config: RspackChain + isServer: boolean +}): Promise => { + // aliases + const alias = { + '@source': app.dir.source(), + '@temp': app.dir.temp(), + '@internal': app.dir.temp('internal'), + } + + // plugin hook: alias + const aliasResult = await app.pluginApi.hooks.alias.process(app, isServer) + + aliasResult.forEach((aliasObject) => { + Object.assign(alias, aliasObject) + }) + + // set aliases + config.resolve.alias.merge( + Object.fromEntries( + // sort alias by length in descending order to ensure longer alias is handled first + Object.entries(alias).sort(([a], [b]) => b.length - a.length), + ), + ) + + // extensions + config.resolve.extensions.merge([ + '.js', + '.jsx', + '.ts', + '.tsx', + '.vue', + '.json', + ]) + + // extensionAlias + config.resolve.extensionAlias.merge({ + '.js': ['.js', '.ts'], + '.mjs': ['.mjs', '.mts'], + }) +} diff --git a/packages/bundler-rspack/src/config/index.ts b/packages/bundler-rspack/src/config/index.ts new file mode 100644 index 0000000000..0d24bfe05d --- /dev/null +++ b/packages/bundler-rspack/src/config/index.ts @@ -0,0 +1,2 @@ +export * from './createBaseConfig.js' +export * from './createClientBaseConfig.js' diff --git a/packages/bundler-rspack/src/config/resolveSwcLoaderOptions.ts b/packages/bundler-rspack/src/config/resolveSwcLoaderOptions.ts new file mode 100644 index 0000000000..e74786ab3c --- /dev/null +++ b/packages/bundler-rspack/src/config/resolveSwcLoaderOptions.ts @@ -0,0 +1,57 @@ +import type { SwcLoaderJscConfig, SwcLoaderOptions } from '@rspack/core' + +export const resolveSwcLoaderOptions = ( + { env, jsc = {}, ...rest }: SwcLoaderOptions = {}, + isTypescript = false, +): SwcLoaderOptions => { + const { parser = {}, transform = {}, ...jscRest } = jsc as SwcLoaderJscConfig + + return { + env: { + /** + * keep consistent with vite + * + * @see https://vite.dev/config/build-options.html#build-target + */ + targets: { + chrome: '111', + edge: '111', + firefox: '114', + safari: '16.4', + }, + ...env, + }, + + jsc: { + parser: isTypescript + ? { + syntax: 'typescript', + tsx: true, + ...parser, + } + : { + syntax: 'ecmascript', + jsx: true, + ...parser, + }, + + transform: { + ...transform, + react: { + runtime: 'classic', + pragma: 'jsx', + pragmaFrag: 'Fragment', + development: false, + ...transform.react, + }, + }, + + ...jscRest, + }, + + /** + * overrides + */ + ...rest, + } +} diff --git a/packages/bundler-rspack/src/dev/createDevConfig.ts b/packages/bundler-rspack/src/dev/createDevConfig.ts new file mode 100644 index 0000000000..71a715bada --- /dev/null +++ b/packages/bundler-rspack/src/dev/createDevConfig.ts @@ -0,0 +1,27 @@ +import { HotModuleReplacementPlugin, HtmlRspackPlugin } from '@rspack/core' +import type { App } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' + +import { createClientBaseConfig } from '../config/index.js' +import type { RspackBundlerOptions } from '../types.js' + +export const createDevConfig = async ( + app: App, + options: RspackBundlerOptions, +): Promise => { + const config = await createClientBaseConfig({ + app, + options, + isBuild: false, + }) + + config.plugin('html').use(HtmlRspackPlugin, [ + { + template: app.options.templateDev, + }, + ]) + + config.plugin('hmr').use(HotModuleReplacementPlugin) + + return config +} diff --git a/packages/bundler-rspack/src/dev/createDevServerConfig.ts b/packages/bundler-rspack/src/dev/createDevServerConfig.ts new file mode 100644 index 0000000000..d8cc27ca56 --- /dev/null +++ b/packages/bundler-rspack/src/dev/createDevServerConfig.ts @@ -0,0 +1,50 @@ +import { sep } from 'node:path' + +import type { Configuration, RspackDevServer } from '@rspack/dev-server' +import type { App } from '@vuepress/core' +import { path } from '@vuepress/utils' + +import type { RspackBundlerOptions } from '../types.js' +import { trailingSlashMiddleware } from './trailingSlashMiddleware.js' + +export const createDevServerConfig = ( + app: App, + options: RspackBundlerOptions, +): Configuration => ({ + allowedHosts: 'all', + compress: true, + devMiddleware: { + publicPath: app.options.base, + writeToDisk: false, + stats: app.env.isDebug ? 'normal' : 'errors-warnings', + }, + headers: { + 'access-control-allow-origin': '*', + }, + historyApiFallback: { + disableDotRule: true, + rewrites: [{ from: /./, to: path.join(app.options.base, 'index.html') }], + }, + host: app.options.host, + hot: true, + setupMiddlewares: (middlewares, devServer: RspackDevServer) => { + devServer.app?.use(trailingSlashMiddleware) + return ( + options.devServerSetupMiddlewares?.(middlewares, devServer) ?? middlewares + ) + }, + open: app.options.open, + port: app.options.port, + static: { + // `static.directory` will fail on Windows if we do not replace / with \ + directory: app.dir.public().replace('/', sep), + publicPath: app.options.base, + watch: { + ignoreInitial: true, + ignored: [ + // Do not watch node_modules + 'node_modules', + ], + }, + }, +}) diff --git a/packages/bundler-rspack/src/dev/dev.ts b/packages/bundler-rspack/src/dev/dev.ts new file mode 100644 index 0000000000..83cdaaf5ca --- /dev/null +++ b/packages/bundler-rspack/src/dev/dev.ts @@ -0,0 +1,91 @@ +import { rspack } from '@rspack/core' +import { RspackDevServer } from '@rspack/dev-server' +import type { App, Bundler } from '@vuepress/core' +import { colors, logger, ora } from '@vuepress/utils' + +import { resolveRspackConfig } from '../resolveRspackConfig.js' +import type { RspackBundlerOptions } from '../types.js' +import { createDevConfig } from './createDevConfig.js' +import { createDevServerConfig } from './createDevServerConfig.js' + +/** + * Create the dev method of rspack bundler + */ +export const dev = async ( + options: RspackBundlerOptions, + app: App, +): ReturnType => { + // plugin hook: extendsBundlerOptions + await app.pluginApi.hooks.extendsBundlerOptions.process(options, app) + + // create rspack config + const rspackConfig = resolveRspackConfig({ + config: await createDevConfig(app, options), + options, + isServer: false, + isBuild: false, + }) + + // create rspack compiler + const compiler = rspack(rspackConfig) + + // create rspack-dev-server + const serverConfig = createDevServerConfig(app, options) + const server = new RspackDevServer(serverConfig, compiler) + + const [, close] = await Promise.all([ + // wait for rspack-dev-server to start + server.start(), + + // wait for rspack compilation to complete + new Promise<() => Promise>((resolve, reject) => { + // create spinner + const spinner = ora() + let hasStarted = false + let hasFinished = false + + // start spinner before the first compilation + compiler.hooks.beforeCompile.tap('vuepress-dev', () => { + if (hasStarted) return + hasStarted = true + + spinner.start('Compiling with rspack...') + }) + + // stop spinner, show compilation time and print url after first compilation + compiler.hooks.done.tap('vuepress-dev', ({ endTime, startTime }) => { + if (hasFinished) return + hasFinished = true + + spinner.succeed( + endTime && startTime + ? `Compilation finished in ${endTime - startTime}ms` + : 'Compilation finished', + ) + + // replace `0.0.0.0` with `localhost` as `0.0.0.0` is not available on windows + const url = `http://${ + serverConfig.host === '0.0.0.0' ? 'localhost' : serverConfig.host + }:${serverConfig.port}${app.options.base}` + logger.success( + `VuePress rspack dev server is listening at ${colors.cyan(url)}`, + ) + + // resolve the close function + resolve(async (): Promise => server.stop()) + }) + + // stop spinner and reject error if the first compilation is failed + compiler.hooks.failed.tap('vuepress-dev', (err) => { + if (hasFinished) return + hasFinished = true + + spinner.fail('Compilation failed') + reject(err) + }) + }), + ]) + + // return the close function + return close +} diff --git a/packages/bundler-rspack/src/dev/index.ts b/packages/bundler-rspack/src/dev/index.ts new file mode 100644 index 0000000000..b55b120468 --- /dev/null +++ b/packages/bundler-rspack/src/dev/index.ts @@ -0,0 +1 @@ +export * from './dev.js' diff --git a/packages/bundler-rspack/src/dev/trailingSlashMiddleware.ts b/packages/bundler-rspack/src/dev/trailingSlashMiddleware.ts new file mode 100644 index 0000000000..d99d677717 --- /dev/null +++ b/packages/bundler-rspack/src/dev/trailingSlashMiddleware.ts @@ -0,0 +1,41 @@ +import type { NextHandleFunction } from 'connect-next' + +const FAKE_HOST = 'http://.' + +/** + * A middleware to add trailing slash to the url + * + * It will redirect '/foo' to '/foo/' with 302 + */ +export const trailingSlashMiddleware: NextHandleFunction = (req, res, next) => { + if ( + !req.method || + !req.url || + // only add trailing slash in GET and HEAD requests + !['GET', 'HEAD'].includes(req.method) + ) { + next() + return + } + + const { pathname } = new URL(req.url, FAKE_HOST) + + if ( + // if the last section of the path has a dot, we think it has extension + // and should not add trailing slash + pathname.split('/').pop()?.includes('.') || + // if the path already has trailing slash + pathname.endsWith('/') + ) { + next() + return + } + + // add trailing slash and retain query + // notice that we should not use 301 in dev-server + const query = req.url.slice(pathname.length) + + res.statusCode = 302 + res.setHeader('Location', `${pathname}/${query}`) + res.end() +} diff --git a/packages/bundler-rspack/src/index.ts b/packages/bundler-rspack/src/index.ts new file mode 100644 index 0000000000..dfe07538ae --- /dev/null +++ b/packages/bundler-rspack/src/index.ts @@ -0,0 +1,5 @@ +import { rspackBundler } from './rspackBundler.js' + +export type * from './types.js' +export * from './rspackBundler.js' +export default rspackBundler diff --git a/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.cts b/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.cts new file mode 100644 index 0000000000..f4b3001431 --- /dev/null +++ b/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.cts @@ -0,0 +1,3 @@ +const loader = require('./vuepressMarkdownLoader.js') + +module.exports = loader.vuepressMarkdownLoader diff --git a/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.ts b/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.ts new file mode 100644 index 0000000000..28941f48a2 --- /dev/null +++ b/packages/bundler-rspack/src/loaders/vuepressMarkdownLoader.ts @@ -0,0 +1,39 @@ +import type { LoaderDefinitionFunction } from '@rspack/core' +import type { App } from '@vuepress/core' + +export interface VuepressMarkdownLoaderOptions { + app: App +} + +/** + * A rspack loader to transform markdown content to vue component + */ +export const vuepressMarkdownLoader: LoaderDefinitionFunction = + async function vuepressMarkdownLoader(source) { + // import esm dependencies + const [{ createPage, renderPageToVue }, { path }] = await Promise.all([ + import('@vuepress/core'), + import('@vuepress/utils'), + ]) + + // get app instance from loader options + const { app } = this.getOptions() + + // normalize the resource path to use forward slashes on Windows + const filePath = path.normalize(this.resourcePath) + + // get the matched page by file path + const page = app.pagesMap[filePath] + + // if the page content is not changed, render it to vue component directly + if (page?.content === source) { + return renderPageToVue(app, page) + } + + // create a new page with the new content + const newPage = await createPage(app, { + content: source, + filePath, + }) + return renderPageToVue(app, newPage) + } diff --git a/packages/bundler-rspack/src/loaders/vuepressSsrLoader.cts b/packages/bundler-rspack/src/loaders/vuepressSsrLoader.cts new file mode 100644 index 0000000000..91a7941ebb --- /dev/null +++ b/packages/bundler-rspack/src/loaders/vuepressSsrLoader.cts @@ -0,0 +1,3 @@ +const { vuepressSsrLoader } = require('./vuepressSsrLoader.js') + +module.exports = vuepressSsrLoader diff --git a/packages/bundler-rspack/src/loaders/vuepressSsrLoader.ts b/packages/bundler-rspack/src/loaders/vuepressSsrLoader.ts new file mode 100644 index 0000000000..e1b83c28d7 --- /dev/null +++ b/packages/bundler-rspack/src/loaders/vuepressSsrLoader.ts @@ -0,0 +1,29 @@ +import type { LoaderDefinitionFunction } from '@rspack/core' + +/** + * A rspack loader to handle SSR dependencies + * + * This loader will only take effect in server bundle + * because we only replace `ssrRender` code + * + * But we still need to use this loader in client, + * to ensure that the module `request` in client and + * server bundle are the same + */ +export const vuepressSsrLoader: LoaderDefinitionFunction = + function vuepressSsrLoader(source) { + // add `request` to `ssrContext._registeredComponents` to handle SSR dependencies + // notice that this could only handle those sfc that cannot use inline template + // see https://github.com/vuejs/vue-loader/blob/1b1a195612f885a8dec3f371edf1cb8b35d341e4/src/index.ts#L167-L183 + return source.replace( + /import { ssrRender } from (.*)\n/, + `import { ssrRender as _ssrRender } from $1 +import { ssrContextKey } from 'vue' +const ssrRender = (...args) => { + const ssrContext = args[2].appContext.provides[ssrContextKey] + ssrContext._registeredComponents.add(${JSON.stringify(this.request)}) + return _ssrRender(...args) +} +`, + ) + } diff --git a/packages/bundler-rspack/src/resolveRspackConfig.ts b/packages/bundler-rspack/src/resolveRspackConfig.ts new file mode 100644 index 0000000000..500e3e92d8 --- /dev/null +++ b/packages/bundler-rspack/src/resolveRspackConfig.ts @@ -0,0 +1,38 @@ +import type { Configuration } from '@rspack/core' +import type { RspackChain } from 'rspack-chain' +import { merge } from 'rspack-merge' + +import type { RspackBundlerOptions } from './types.js' + +export const resolveRspackConfig = ({ + config, + options, + isServer, + isBuild, +}: { + config: RspackChain + options: RspackBundlerOptions + isServer: boolean + isBuild: boolean +}): Configuration => { + // allow modifying rspack config via `chainRspack` + options.chainRspack?.(config, isServer, isBuild) + + // generate rspack config from rspack-chain + const rspackConfig = config.toConfig() + + // allow modifying rspack config via `configureRspack` + const configureRspackResult = options.configureRspack?.( + rspackConfig, + isServer, + isBuild, + ) + + // if `configureRspack` returns a configuration object, + // use rspack-merge to merge it + if (configureRspackResult) { + return merge(rspackConfig, configureRspackResult) + } + + return rspackConfig +} diff --git a/packages/bundler-rspack/src/rspackBundler.ts b/packages/bundler-rspack/src/rspackBundler.ts new file mode 100644 index 0000000000..bb05b2ba22 --- /dev/null +++ b/packages/bundler-rspack/src/rspackBundler.ts @@ -0,0 +1,11 @@ +import type { Bundler } from '@vuepress/core' + +import { build } from './build/index.js' +import { dev } from './dev/index.js' +import type { RspackBundlerOptions } from './types.js' + +export const rspackBundler = (options: RspackBundlerOptions = {}): Bundler => ({ + name: '@vuepress/bundler-rspack', + dev: async (app) => dev(options, app), + build: async (app) => build(options, app), +}) diff --git a/packages/bundler-rspack/src/types.ts b/packages/bundler-rspack/src/types.ts new file mode 100644 index 0000000000..e5f6e71476 --- /dev/null +++ b/packages/bundler-rspack/src/types.ts @@ -0,0 +1,143 @@ +import type { + LoaderContext, + Configuration as RspackConfiguration, +} from '@rspack/core' +import type { + RspackDevServer, + Configuration as RspackDevServerConfiguration, +} from '@rspack/dev-server' +import type { BundlerOptions } from '@vuepress/core' +import type { RspackChain } from 'rspack-chain' +import type { VueLoaderOptions } from 'vue-loader' + +export type { VueLoaderOptions, RspackConfiguration, RspackDevServer } + +/** + * Options for bundler-rspack + */ +export interface RspackBundlerOptions extends BundlerOptions { + /** + * use rspack-merge to set rspack config + */ + configureRspack?: ( + config: RspackConfiguration, + isServer: boolean, + isBuild: boolean, + ) => RspackConfiguration | void + + /** + * use rspack-chain to set rspack config + */ + chainRspack?: ( + config: RspackChain, + isServer: boolean, + isBuild: boolean, + ) => void + + /** + * `setupMiddlewares` config of rspack-dev-server + */ + devServerSetupMiddlewares?: RspackDevServerConfiguration['setupMiddlewares'] + + /** + * vue-loader options + */ + vue?: VueLoaderOptions + + /** + * postcss-loader options + */ + postcss?: PostcssLoaderOptions + + /** + * stylus-loader options + */ + stylus?: StylusLoaderOptions + + /** + * sass-loader options for scss files + */ + scss?: SassLoaderOptions + + /** + * sass-loader options for sass files + */ + sass?: SassLoaderOptions + + /** + * less-loader options + */ + less?: LessLoaderOptions + + /** + * only target evergreen browsers or not + */ + evergreen?: boolean +} + +/** + * Common options for style preprocessor rspack loaders + */ +export interface StylePreprocessorLoaderOptions { + additionalData?: + | string + | (( + content: string, + loaderContext: LoaderContext>, + ) => string) + sourceMap?: boolean +} + +/** + * Common type for style pre-processor options + */ +export type StylePreprocessorOptions< + T extends Record = Record, +> = T | ((loaderContext: LoaderContext) => TextDecodeOptions) + +/** + * Options for postcss-loader + * + * @see https://github.com/webpack-contrib/postcss-loader#options + */ +export interface PostcssLoaderOptions extends Pick< + StylePreprocessorLoaderOptions, + 'sourceMap' +> { + execute?: boolean + implementation?: string | ((...args: unknown[]) => unknown) + postcssOptions?: StylePreprocessorOptions +} + +/** + * Options for stylus-loader + * + * @see https://github.com/webpack-contrib/stylus-loader#options + */ +export interface StylusLoaderOptions extends StylePreprocessorLoaderOptions { + implementation?: string | ((...args: unknown[]) => unknown) + stylusOptions?: StylePreprocessorOptions +} + +/** + * Options for sass-loader + * + * @see https://github.com/webpack-contrib/sass-loader#options + */ +export interface SassLoaderOptions extends StylePreprocessorLoaderOptions { + api?: 'legacy' | 'modern-compiler' | 'modern' + implementation?: Record | string + sassOptions?: StylePreprocessorOptions + warnRuleAsWarning?: boolean +} + +/** + * Options for less-loader + * + * @see https://github.com/webpack-contrib/less-loader#options + */ +export interface LessLoaderOptions extends StylePreprocessorLoaderOptions { + implementation?: Record | string + lessLogAsWarnOrErr?: boolean + lessOptions?: StylePreprocessorOptions +} diff --git a/packages/bundler-rspack/tsdown.config.ts b/packages/bundler-rspack/tsdown.config.ts new file mode 100644 index 0000000000..5faba7d5a0 --- /dev/null +++ b/packages/bundler-rspack/tsdown.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'tsdown' + +const shared = defineConfig({ + fixedExtension: false, + target: 'es2023', + tsconfig: '../tsconfig.dts.json', +}) + +export default defineConfig([ + { + ...shared, + dts: true, + entry: './src/index.ts', + format: 'esm', + }, + { + ...shared, + dts: false, + entry: { + 'vuepress-markdown-loader': './src/loaders/vuepressMarkdownLoader.cts', + 'vuepress-ssr-loader': './src/loaders/vuepressSsrLoader.cts', + }, + format: 'cjs', + }, +]) diff --git a/packages/core/src/app/resolveAppMarkdown.ts b/packages/core/src/app/resolveAppMarkdown.ts index f0446b8239..0de3922556 100644 --- a/packages/core/src/app/resolveAppMarkdown.ts +++ b/packages/core/src/app/resolveAppMarkdown.ts @@ -19,6 +19,7 @@ export const resolveAppMarkdown = async (app: App): Promise => { if (app.options.markdown.assets !== false) { app.options.markdown.assets ??= {} app.options.markdown.assets.absolutePathPrependBase ??= + app.options.bundler.name === '@vuepress/bundler-rspack' || app.options.bundler.name === '@vuepress/bundler-webpack' } diff --git a/packages/vuepress/package.json b/packages/vuepress/package.json index 28bc8dd5e5..a32ae078fe 100644 --- a/packages/vuepress/package.json +++ b/packages/vuepress/package.json @@ -59,6 +59,7 @@ "vue": "catalog:vue" }, "peerDependencies": { + "@vuepress/bundler-rspack": "workspace:*", "@vuepress/bundler-vite": "workspace:*", "@vuepress/bundler-webpack": "workspace:*", "vue": "catalog:vue" @@ -67,6 +68,9 @@ "@vuepress/bundler-vite": { "optional": true }, + "@vuepress/bundler-rspack": { + "optional": true + }, "@vuepress/bundler-webpack": { "optional": true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b724b5ab36..245bf416fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: '@vuepress-e2e/style-exports': specifier: file:./modules/style-exports version: file:e2e/modules/style-exports + '@vuepress/bundler-rspack': + specifier: workspace:* + version: link:../packages/bundler-rspack '@vuepress/bundler-vite': specifier: workspace:* version: link:../packages/bundler-vite @@ -128,7 +131,7 @@ importers: version: 1.99.0 sass-loader: specifier: ^16.0.7 - version: 16.0.7(sass-embedded@1.99.0)(sass@1.99.0)(webpack@5.106.2(esbuild@0.28.0)) + version: 16.0.7(@rspack/core@2.0.3)(sass-embedded@1.99.0)(sass@1.99.0)(webpack@5.106.2(esbuild@0.28.0)) vue: specifier: catalog:vue version: 3.5.34(typescript@6.0.3) @@ -146,6 +149,67 @@ importers: specifier: ^14.2.6 version: 14.2.6 + packages/bundler-rspack: + dependencies: + '@rspack/core': + specifier: ^2.0.3 + version: 2.0.3 + '@rspack/dev-server': + specifier: ^2.0.1 + version: 2.0.1(@rspack/core@2.0.3)(selfsigned@5.5.0) + '@vuepress/bundlerutils': + specifier: workspace:* + version: link:../bundlerutils + '@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 + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.14) + css-loader: + specifier: ^7.1.4 + version: 7.1.4(@rspack/core@2.0.3)(webpack@5.106.2(esbuild@0.28.0)) + esbuild-loader: + specifier: ~4.4.3 + version: 4.4.3(webpack@5.106.2(esbuild@0.28.0)) + postcss: + specifier: ^8.5.14 + version: 8.5.14 + postcss-loader: + specifier: ^8.2.1 + version: 8.2.1(@rspack/core@2.0.3)(postcss@8.5.14)(typescript@6.0.3)(webpack@5.106.2(esbuild@0.28.0)) + rspack-chain: + specifier: ^2.0.1 + version: 2.0.1(@rspack/core@2.0.3) + rspack-merge: + specifier: ^0.1.1 + version: 0.1.1 + style-loader: + specifier: ^4.0.0 + version: 4.0.0(webpack@5.106.2(esbuild@0.28.0)) + vue: + specifier: catalog:vue + version: 3.5.34(typescript@6.0.3) + vue-loader: + specifier: ^17.4.2 + version: 17.4.2(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3))(webpack@5.106.2(esbuild@0.28.0)) + vue-router: + specifier: catalog:vue + version: 5.0.6(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3)) + devDependencies: + connect-next: + specifier: ^4.0.1 + version: 4.0.1 + packages/bundler-vite: dependencies: '@vitejs/plugin-vue': @@ -226,7 +290,7 @@ importers: version: 14.0.0(webpack@5.106.2(esbuild@0.28.0)) css-loader: specifier: ^7.1.4 - version: 7.1.4(webpack@5.106.2(esbuild@0.28.0)) + version: 7.1.4(@rspack/core@2.0.3)(webpack@5.106.2(esbuild@0.28.0)) css-minimizer-webpack-plugin: specifier: ^8.0.0 version: 8.0.0(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.2(esbuild@0.28.0)) @@ -250,7 +314,7 @@ importers: version: 8.5.14 postcss-loader: specifier: ^8.2.1 - version: 8.2.1(postcss@8.5.14)(typescript@6.0.3)(webpack@5.106.2(esbuild@0.28.0)) + version: 8.2.1(@rspack/core@2.0.3)(postcss@8.5.14)(typescript@6.0.3)(webpack@5.106.2(esbuild@0.28.0)) style-loader: specifier: ^4.0.0 version: 4.0.0(webpack@5.106.2(esbuild@0.28.0)) @@ -466,6 +530,9 @@ importers: packages/vuepress: dependencies: + '@vuepress/bundler-rspack': + specifier: workspace:* + version: link:../bundler-rspack '@vuepress/bundler-vite': specifier: workspace:* version: link:../bundler-vite @@ -1772,6 +1839,93 @@ packages: '@rolldown/pluginutils@1.0.0-rc.18': resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + '@rspack/binding-darwin-arm64@2.0.3': + resolution: {integrity: sha512-4UyCjLJwU/WxR6K1/gG4u3+jUsoaRHJ5rNu9fto/UbvrItwdlVNULChAApqZFw6mcSetMddSjSICeuj5pSB6sA==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@2.0.3': + resolution: {integrity: sha512-K3evrbTgZNa8emEqk+AjDtbuoXZp5tPZz3pcEgETxuu3KanW8Zu+Fb+TUp1DEUcL0xOmHPPox8H2cZ3pF4Zmug==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@2.0.3': + resolution: {integrity: sha512-aPLDaaTtX1wqjLYAIHc2MGDQZtv1Hbjx47oaaefbWz5GbAnSA4P8jdYIeeGRyrqvQ0WqJXIWXgT0d/iXtes00A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-arm64-musl@2.0.3': + resolution: {integrity: sha512-0WulUQPop6vmSDfrTxghmVlm+6crU8/XqD2f0dOWbEniZVuDZJ5/Y/cBqTRyk3rjl0vrmUv3lc87/t7UgQJQSw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-x64-gnu@2.0.3': + resolution: {integrity: sha512-fAhiMuV5omT53YMft+f3Y9euAFgspuyBAk9ZpeW2buL2TkuUMwP07adhhvQfKdQ5gpELfzmjQaRDGqaIT8UWiA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-musl@2.0.3': + resolution: {integrity: sha512-0kcuFoZ8vy2iNWoISFOZt+/Ujo7LRLrzE7h07AV5r+oN/mv+/v14Sd/8NUtDIScCkrYOszYq/QS31e6t0UrVfw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/binding-wasm32-wasi@2.0.3': + resolution: {integrity: sha512-x2fsw7GzNZEnw444ikj4/b8kVjM0Y0TllxmizHpYZ9gmaQrOk5OXo9RQdz+l4zzoGors0l2IZP5Cc4GJNCaSoQ==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@2.0.3': + resolution: {integrity: sha512-jqlxuVPdrgMuwj/HEjSkC/jmhl4fAuKyob36zJXq2uAusn2FRJ4kClGe1fLFpfxRXFVQAWwlAOwLJg8T0suuaA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@2.0.3': + resolution: {integrity: sha512-QM4JEuyk5QaZ5gnvnAIaCwVQzCkrD2E4Sud77kx/MVGDsRkcOlMx3blMC5QNHPDamRmWGk+7314YOQvRhKuWyg==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@2.0.3': + resolution: {integrity: sha512-vSQNnAy0wswG6AfNRuArTHQBiXOXl+A9ddQxBFup4PMHUzXxKtsBLQzw7BgFC0EgrPeHbt+30j7sXVZKYukj4A==} + cpu: [x64] + os: [win32] + + '@rspack/binding@2.0.3': + resolution: {integrity: sha512-4exVNhGhW5RFHjK87XeTKbkA/qAgI5NHJlT1jNqiJv0gcUXLqTOEU3w7f8+f9zUo4JMFvPc0c9veOi4M19YYTg==} + + '@rspack/core@2.0.3': + resolution: {integrity: sha512-2ufO/8FHIA/lX6UOgSsKPhpDvHr0sh9lYq/n/LsIZsTwu3973BGbu2fg1Akvuu3rEnskPqXjsqH2EPBzEA42uA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + + '@rspack/dev-middleware@2.0.1': + resolution: {integrity: sha512-cXSubf5/C+dvkWV2/+rGTtiZ93wSLd3OlTQhwMvsmsmGDdPlkYqIvQ+BTkOk9UCXxKIaF0DDYYmCpBeRRYJfJw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rspack/core': ^2.0.0-0 + peerDependenciesMeta: + '@rspack/core': + optional: true + + '@rspack/dev-server@2.0.1': + resolution: {integrity: sha512-N5sIdU9v6WhwPH6Ek8QCj4IMvGd8jA3ZW1NMAyHP1ezzKIr2/idcNV2zX5vbC6FJvOLRLww0rFjfKA+z4tshDA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rspack/core': ^2.0.0-0 + selfsigned: ^5.0.0 + peerDependenciesMeta: + selfsigned: + optional: true + '@simple-libs/child-process-utils@1.0.2': resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} engines: {node: '>=18'} @@ -2664,6 +2818,10 @@ packages: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} + connect-next@4.0.1: + resolution: {integrity: sha512-nkHJWto78sXAooScrgvRt9E9omtwHBUTjCWRgxe3NbwNVC9cW4Lu0il0m2O867pgzJV66YXi2tXtSPtZy3Zmeg==} + engines: {node: ^20.19.0 || >=22.12.0} + content-disposition@0.5.2: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} @@ -4793,6 +4951,17 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rspack-chain@2.0.1: + resolution: {integrity: sha512-/ePnFJvIDiIn/nMB7JI42Sju8oGRfUbKTV+1Vg2GnZpXktmTVZNJMQe7+VCfG9YTN66ZW7Qc51Q8SDN0mr6e0w==} + peerDependencies: + '@rspack/core': ^2.0.0-0 + peerDependenciesMeta: + '@rspack/core': + optional: true + + rspack-merge@0.1.1: + resolution: {integrity: sha512-RHKi8nECjP0lM1V7RnM+8G8dRvWgrsnntzfogJQz7oSHo104ca4jsL3KN/0gru233n/lVGrqDPDNlCA4x0UNlg==} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -6241,7 +6410,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 jest-regex-util: 30.0.1 '@jest/schemas@30.0.5': @@ -6834,6 +7003,68 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.18': {} + '@rspack/binding-darwin-arm64@2.0.3': + optional: true + + '@rspack/binding-darwin-x64@2.0.3': + optional: true + + '@rspack/binding-linux-arm64-gnu@2.0.3': + optional: true + + '@rspack/binding-linux-arm64-musl@2.0.3': + optional: true + + '@rspack/binding-linux-x64-gnu@2.0.3': + optional: true + + '@rspack/binding-linux-x64-musl@2.0.3': + optional: true + + '@rspack/binding-wasm32-wasi@2.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rspack/binding-win32-arm64-msvc@2.0.3': + optional: true + + '@rspack/binding-win32-ia32-msvc@2.0.3': + optional: true + + '@rspack/binding-win32-x64-msvc@2.0.3': + optional: true + + '@rspack/binding@2.0.3': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.0.3 + '@rspack/binding-darwin-x64': 2.0.3 + '@rspack/binding-linux-arm64-gnu': 2.0.3 + '@rspack/binding-linux-arm64-musl': 2.0.3 + '@rspack/binding-linux-x64-gnu': 2.0.3 + '@rspack/binding-linux-x64-musl': 2.0.3 + '@rspack/binding-wasm32-wasi': 2.0.3 + '@rspack/binding-win32-arm64-msvc': 2.0.3 + '@rspack/binding-win32-ia32-msvc': 2.0.3 + '@rspack/binding-win32-x64-msvc': 2.0.3 + + '@rspack/core@2.0.3': + dependencies: + '@rspack/binding': 2.0.3 + + '@rspack/dev-middleware@2.0.1(@rspack/core@2.0.3)': + optionalDependencies: + '@rspack/core': 2.0.3 + + '@rspack/dev-server@2.0.1(@rspack/core@2.0.3)(selfsigned@5.5.0)': + dependencies: + '@rspack/core': 2.0.3 + '@rspack/dev-middleware': 2.0.1(@rspack/core@2.0.3) + optionalDependencies: + selfsigned: 5.5.0 + '@simple-libs/child-process-utils@1.0.2': dependencies: '@simple-libs/stream-utils': 1.2.0 @@ -6858,7 +7089,7 @@ snapshots: '@types/bonjour@3.5.13': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 '@types/chai@5.2.3': dependencies: @@ -6934,7 +7165,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 '@types/istanbul-lib-coverage@2.0.6': {} @@ -7016,7 +7247,7 @@ snapshots: '@types/sockjs@0.3.36': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 '@types/unist@3.0.3': {} @@ -7024,7 +7255,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 '@types/yargs-parser@21.0.3': {} @@ -7834,6 +8065,8 @@ snapshots: connect-history-api-fallback@2.0.0: {} + connect-next@4.0.1: {} + content-disposition@0.5.2: {} content-disposition@0.5.4: @@ -7929,7 +8162,7 @@ snapshots: dependencies: postcss: 8.5.14 - css-loader@7.1.4(webpack@5.106.2(esbuild@0.28.0)): + css-loader@7.1.4(@rspack/core@2.0.3)(webpack@5.106.2(esbuild@0.28.0)): dependencies: icss-utils: 5.1.0(postcss@8.5.14) postcss: 8.5.14 @@ -7940,6 +8173,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: + '@rspack/core': 2.0.3 webpack: 5.106.2(esbuild@0.28.0) css-minimizer-webpack-plugin@8.0.0(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.2(esbuild@0.28.0)): @@ -9827,13 +10061,14 @@ snapshots: postcss: 8.5.14 yaml: 2.8.4 - postcss-loader@8.2.1(postcss@8.5.14)(typescript@6.0.3)(webpack@5.106.2(esbuild@0.28.0)): + postcss-loader@8.2.1(@rspack/core@2.0.3)(postcss@8.5.14)(typescript@6.0.3)(webpack@5.106.2(esbuild@0.28.0)): dependencies: cosmiconfig: 9.0.1(typescript@6.0.3) jiti: 2.7.0 postcss: 8.5.14 semver: 7.7.4 optionalDependencies: + '@rspack/core': 2.0.3 webpack: 5.106.2(esbuild@0.28.0) transitivePeerDependencies: - typescript @@ -10170,6 +10405,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + rspack-chain@2.0.1(@rspack/core@2.0.3): + optionalDependencies: + '@rspack/core': 2.0.3 + + rspack-merge@0.1.1: {} + run-applescript@7.1.0: {} rxjs@7.8.2: @@ -10269,10 +10510,11 @@ snapshots: sass-embedded-win32-arm64: 1.99.0 sass-embedded-win32-x64: 1.99.0 - sass-loader@16.0.7(sass-embedded@1.99.0)(sass@1.99.0)(webpack@5.106.2(esbuild@0.28.0)): + sass-loader@16.0.7(@rspack/core@2.0.3)(sass-embedded@1.99.0)(sass@1.99.0)(webpack@5.106.2(esbuild@0.28.0)): dependencies: neo-async: 2.6.2 optionalDependencies: + '@rspack/core': 2.0.3 sass: 1.99.0 sass-embedded: 1.99.0 webpack: 5.106.2(esbuild@0.28.0)