diff --git a/packages/bundler-helper/README.md b/packages/bundler-helper/README.md new file mode 100644 index 0000000000..61e74c4d18 --- /dev/null +++ b/packages/bundler-helper/README.md @@ -0,0 +1,12 @@ +# @vuepress/bundler-utils + +[![npm](https://badgen.net/npm/v/@vuepress/bundler-utils/next)](https://www.npmjs.com/package/@vuepress/bundler-utils) +[![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-helper/package.json b/packages/bundler-helper/package.json new file mode 100644 index 0000000000..dd0852e849 --- /dev/null +++ b/packages/bundler-helper/package.json @@ -0,0 +1,66 @@ +{ + "name": "@vuepress/bundlerhelper", + "version": "2.0.0-rc.30", + "description": "Helper package of VuePress bundlers", + "keywords": [ + "bundler", + "utils", + "vuepress" + ], + "homepage": "https://github.com/vuepress", + "bugs": { + "url": "https://github.com/vuepress/core/issues" + }, + "license": "MIT", + "author": "Mister-Hope", + "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", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsdown", + "clean": "rimraf dist" + }, + "dependencies": { + "@vuepress/core": "workspace:*", + "@vuepress/shared": "workspace:*", + "@vuepress/utils": "workspace:*" + }, + "devDependencies": { + "@types/connect": "^3.4.38" + }, + "peerDependencies": { + "@vuepress/bundler-vite": "workspace:*", + "@vuepress/bundler-webpack": "workspace:*" + }, + "peerDependenciesMeta": { + "@vuepress/bundler-vite": { + "optional": true + }, + "@vuepress/bundler-webpack": { + "optional": true + } + }, + "tsdown": { + "dts": true, + "entry": "./src/index.ts", + "fixedExtension": false, + "format": "esm", + "target": "es2023", + "tsconfig": "../../tsconfig.dts.json" + } +} diff --git a/packages/bundler-helper/src/addCustomElement.ts b/packages/bundler-helper/src/addCustomElement.ts new file mode 100644 index 0000000000..d1ecc3f4b7 --- /dev/null +++ b/packages/bundler-helper/src/addCustomElement.ts @@ -0,0 +1,96 @@ +import type { ViteBundlerOptions } from '@vuepress/bundler-vite' +import type { WebpackBundlerOptions } from '@vuepress/bundler-webpack' +import type { App } from '@vuepress/core' +import { isString } from '@vuepress/shared' + +import { getBundlerName } from './getBundlerName.js' + +/** + * Add tags as customElement + * + * @example + * // Add single custom element + * addCustomElement(bundlerOptions, app, 'my-element') + * + * // Add multiple custom elements + * addCustomElement(bundlerOptions, app, ['element1', 'element2']) + * + * // Add elements matching a pattern + * addCustomElement(bundlerOptions, app, /^my-/) + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param customElement - Tags recognized as custom element + */ +export const addCustomElement = ( + bundlerOptions: unknown, + app: App, + customElement: RegExp | string[] | string, +): void => { + const customElements = isString(customElement) + ? [customElement] + : customElement + const bundlerName = getBundlerName(app) + + switch (bundlerName) { + // for vite + case 'vite': { + const viteBundlerConfig = bundlerOptions as ViteBundlerOptions + + viteBundlerConfig.vuePluginOptions ??= {} + viteBundlerConfig.vuePluginOptions.template ??= {} + viteBundlerConfig.vuePluginOptions.template.compilerOptions ??= {} + const { isCustomElement } = + viteBundlerConfig.vuePluginOptions.template.compilerOptions + + /** + * @param tag - The tag name to check + * @returns Whether the tag is a custom element + * @see https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/README.md + */ + viteBundlerConfig.vuePluginOptions.template.compilerOptions.isCustomElement = + (tag: string): boolean | void => { + if ( + customElements instanceof RegExp + ? customElements.test(tag) + : customElements.includes(tag) + ) + return true + + return isCustomElement?.(tag) + } + break + } + // for webpack + case 'webpack': { + const webpackBundlerConfig = bundlerOptions as WebpackBundlerOptions + + webpackBundlerConfig.vue ??= {} + webpackBundlerConfig.vue.compilerOptions ??= {} + const { isCustomElement } = webpackBundlerConfig.vue.compilerOptions + + /** + * @param tag - The tag name to check + * @returns Whether the tag is a custom element + * @see https://vue-loader.vuejs.org/options.html#compileroptions + */ + webpackBundlerConfig.vue.compilerOptions.isCustomElement = ( + tag: string, + ): boolean | void => { + if ( + customElements instanceof RegExp + ? customElements.test(tag) + : customElements.includes(tag) + ) + return true + + return isCustomElement?.(tag) + } + break + } + default: { + // eslint-disable-next-line no-console + console.error(`[addCustomElement]: ${bundlerName} is not supported yet.`) + } + } +} diff --git a/packages/bundler-helper/src/customizeDevServer.ts b/packages/bundler-helper/src/customizeDevServer.ts new file mode 100644 index 0000000000..4f0373b6ed --- /dev/null +++ b/packages/bundler-helper/src/customizeDevServer.ts @@ -0,0 +1,134 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' + +import type { ViteBundlerOptions } from '@vuepress/bundler-vite' +import type { + WebpackBundlerOptions, + WebpackDevServer, +} from '@vuepress/bundler-webpack' +import type { App } from '@vuepress/core' +import { removeLeadingSlash } from '@vuepress/shared' +import type { HandleFunction } from 'connect' +import type { Plugin } from 'vite' + +import { getBundlerName } from './getBundlerName.js' +import { mergeViteConfig } from './vite/index.js' + +/** Options for customizing VuePress Dev Server */ +export interface DevServerOptions { + /** Path to be responded */ + path: string + /** Respond handler */ + response: ( + request: IncomingMessage, + response: ServerResponse, + ) => Promise + + /** Error msg */ + errMsg?: string +} + +/** + * Handle specific path when running VuePress Dev Server + * + * @example + * // handle `/api/` path + * useCustomDevServer(bundlerOptions, app, { + * path: '/api/', + * response: async () => { + * const data = await prepareYourData(); + * return JSON.stringify({ message: 'Hello from custom dev server!' }) + * }, + * errMsg: 'Unexpected api error', + * }) + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param options - Dev server options + */ +export const customizeDevServer = ( + bundlerOptions: unknown, + app: App, + { + errMsg = 'The server encountered an error', + response: responseHandler, + path, + }: DevServerOptions, +): void => { + // must in dev + if (!app.env.isDev) return + + const { base } = app.siteData + const bundlerName = getBundlerName(app) + + switch (bundlerName) { + // for vite + case 'vite': { + const viteBundlerOptions = bundlerOptions as ViteBundlerOptions + const handler: HandleFunction = ( + request: IncomingMessage, + response: ServerResponse, + ) => { + responseHandler(request, response) + .then((data) => { + response.statusCode = 200 + response.end(data) + }) + .catch(() => { + response.statusCode = 500 + response.end(errMsg) + }) + } + + const viteMockRequestPlugin: Plugin = { + name: `virtual:dev-server-mock/${path}`, + configureServer: ({ middlewares }) => { + middlewares.use(`${base}${removeLeadingSlash(path)}`, handler) + }, + } + + viteBundlerOptions.viteOptions = mergeViteConfig( + viteBundlerOptions.viteOptions ?? {}, + { plugins: [viteMockRequestPlugin] }, + ) + break + } + // for webpack + case 'webpack': { + const webpackBundlerOptions = bundlerOptions as WebpackBundlerOptions + + const { devServerSetupMiddlewares } = webpackBundlerOptions + + /** + * @param middlewares - Existing middlewares + * @param server - Webpack Dev Server instance + * @returns Updated middlewares + * @see https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares + */ + webpackBundlerOptions.devServerSetupMiddlewares = ( + middlewares: WebpackDevServer.Middleware[], + server: WebpackDevServer, + ): WebpackDevServer.Middleware[] => { + server.app?.get( + `${base}${removeLeadingSlash(path)}`, + (request, response) => { + responseHandler(request, response) + .then((data) => response.status(200).send(data)) + .catch(() => response.status(500).send(errMsg)) + }, + ) + + return devServerSetupMiddlewares + ? devServerSetupMiddlewares(middlewares, server) + : middlewares + } + break + } + + default: { + // eslint-disable-next-line no-console + console.error( + `[customizeDevServer]: ${bundlerName} is not supported yet.`, + ) + } + } +} diff --git a/packages/bundler-helper/src/getBundlerName.ts b/packages/bundler-helper/src/getBundlerName.ts new file mode 100644 index 0000000000..5d14dc2eb1 --- /dev/null +++ b/packages/bundler-helper/src/getBundlerName.ts @@ -0,0 +1,19 @@ +import type { App } from '@vuepress/core' + +/** + * Get short bundler name + * + * @example + * // With @vuepress/bundler-vite + * getBundlerName(app) // 'vite' + * // With @vuepress/bundler-webpack + * getBundlerName(app) // 'webpack' + * + * @param app - VuePress Node App + * @returns Short bundler name + */ +export const getBundlerName = (app: App): string => { + const { name } = app.options.bundler + + return /^@vuepress\/bundler-(.*)$/u.exec(name)?.[1] ?? name +} diff --git a/packages/bundler-helper/src/index.ts b/packages/bundler-helper/src/index.ts new file mode 100644 index 0000000000..1769ac0e09 --- /dev/null +++ b/packages/bundler-helper/src/index.ts @@ -0,0 +1,5 @@ +export * from './addCustomElement.js' +export * from './customizeDevServer.js' +export * from './getBundlerName.js' +export * from './vite/index.js' +export * from './webpack/index.js' diff --git a/packages/bundler-helper/src/vite/index.ts b/packages/bundler-helper/src/vite/index.ts new file mode 100644 index 0000000000..7be5de33d9 --- /dev/null +++ b/packages/bundler-helper/src/vite/index.ts @@ -0,0 +1,2 @@ +export * from './mergeViteConfig.js' +export * from './viteHelper.js' diff --git a/packages/bundler-helper/src/vite/mergeViteConfig.ts b/packages/bundler-helper/src/vite/mergeViteConfig.ts new file mode 100644 index 0000000000..96c08e129f --- /dev/null +++ b/packages/bundler-helper/src/vite/mergeViteConfig.ts @@ -0,0 +1,294 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Forked from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts + * + * Inlined because vite is optional + * + * MIT License + * + * Copyright (c) 2019-present, VoidZero Inc. and Vite contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { isPlainObject, isString } from '@vuepress/shared' +import type { Plugin } from 'vite' + +interface Alias { + find: RegExp | string + replacement: string + /** + * Instructs the plugin to use an alternative resolving algorithm, rather than + * the Rollup's resolver. + * + * @default null + */ + customResolver?: ResolverFunction | ResolverObject | null +} + +type ResolverFunction = (...args: unknown[]) => unknown + +interface ResolverObject { + buildStart?: (...args: unknown[]) => unknown + resolveId: ResolverFunction +} + +type AliasOptions = Record | readonly Alias[] + +const environmentPathRE = /^environments\.[^.]+$/ + +const rollupOptionsRootPaths = new Set([ + 'build', + 'worker', + 'optimizeDeps', + 'ssr.optimizeDeps', +]) +const runtimeDeprecatedPath = new Set(['optimizeDeps', 'ssr.optimizeDeps']) + +const rollupOptionsDeprecationCall = (() => { + return () => { + const method = process.env.VITE_DEPRECATION_TRACE ? 'trace' : 'warn' + // eslint-disable-next-line no-console + console[method]( + `\`optimizeDeps.rollupOptions\` / \`ssr.optimizeDeps.rollupOptions\` is deprecated. ` + + `Use \`optimizeDeps.rolldownOptions\` instead. Note that this option may be set by a plugin. ${ + method === 'trace' + ? 'Showing trace because VITE_DEPRECATION_TRACE is set.' + : 'Set VITE_DEPRECATION_TRACE=1 to see where it is called.' + }`, + ) + } +})() + +const setupRollupOptionCompat = ( + buildConfig: Record, + path: string, +): void => { + // if both rollupOptions and rolldownOptions are present, + // ignore rollupOptions and use rolldownOptions + buildConfig.rolldownOptions ??= buildConfig.rollupOptions + if ( + runtimeDeprecatedPath.has(path) && + buildConfig.rollupOptions && + buildConfig.rolldownOptions !== buildConfig.rollupOptions + ) { + rollupOptionsDeprecationCall() + } + + // proxy rolldownOptions to rollupOptions + Object.defineProperty(buildConfig, 'rollupOptions', { + get() { + return buildConfig.rolldownOptions + }, + set(newValue) { + if (runtimeDeprecatedPath.has(path)) { + rollupOptionsDeprecationCall() + } + buildConfig.rolldownOptions = newValue + }, + configurable: true, + enumerable: true, + }) +} + +/** + * Sets up `rollupOptions` compat proxies for an environment. + */ +function setupRollupOptionCompatForEnvironment(environment: any): any { + if (!isPlainObject(environment)) { + return environment + } + const merged: Record = { ...environment } + if (isPlainObject(merged.build)) { + setupRollupOptionCompat(merged.build, 'build') + } + return merged +} + +const arraify = (target: T | T[]): T[] => + Array.isArray(target) ? target : [target] + +const normalizeSingleAlias = ({ + find, + replacement, + customResolver, +}: Alias): Alias => { + const alias: Alias = { find, replacement } + + if (isString(find) && find.endsWith('/') && replacement.endsWith('/')) { + alias.find = find.slice(0, find.length - 1) + alias.replacement = replacement.slice(0, replacement.length - 1) + } + + if (customResolver) alias.customResolver = customResolver + + return alias +} + +export function normalizeAlias(aliasOption: AliasOptions = []): Alias[] { + return Array.isArray(aliasOption) + ? aliasOption.map(normalizeSingleAlias) + : Object.keys(aliasOption).map((find) => + normalizeSingleAlias({ + find, + replacement: (aliasOption as Record)[find], + }), + ) +} + +const mergeAlias = ( + defaults?: AliasOptions, + overrides?: AliasOptions, +): AliasOptions | undefined => { + if (!defaults) return overrides + if (!overrides) return defaults + + if (isPlainObject(defaults) && isPlainObject(overrides)) + return { + ...(defaults as Record), + ...(overrides as Record), + } + + // the order is flipped because the alias is resolved from top-down, + // where the later should have higher priority + return [...normalizeAlias(overrides), ...normalizeAlias(defaults)] +} + +const backwardCompatibleWorkerPlugins = ( + plugins: Plugin[] | (() => Plugin[]), +): Plugin[] => { + if (Array.isArray(plugins)) return plugins + if (typeof plugins === 'function') return plugins() + + return [] +} + +// oxlint-disable-next-line complexity +const mergeConfigRecursively = ( + { ...merged }: Record, + overrides: Record, + rootPath: string, +): Record => { + if (rollupOptionsRootPaths.has(rootPath)) + setupRollupOptionCompat(merged, rootPath) + + for (const [key, value] of Object.entries(overrides)) { + if (value == null) continue + + let existing = merged[key] + + if (key === 'rollupOptions' && rollupOptionsRootPaths.has(rootPath)) { + // if both rollupOptions and rolldownOptions are present, + // ignore rollupOptions and use rolldownOptions + if (overrides.rolldownOptions) continue + existing = merged.rolldownOptions + } + + if (existing == null) { + if (rootPath === '' && key === 'environments' && isPlainObject(value)) { + // Clone to avoid mutating the original override object + const environments = { ...value } + for (const envName in environments) { + environments[envName] = setupRollupOptionCompatForEnvironment( + environments[envName], + ) + } + merged[key] = environments + } else if (rootPath === 'environments') { + // `environments` exists, but a new environment is added + merged[key] = setupRollupOptionCompatForEnvironment(value) + } else { + merged[key] = value + } + continue + } + + // fields that require special handling + if (key === 'alias' && (rootPath === 'resolve' || rootPath === '')) { + merged[key] = mergeAlias(existing, value) + continue + } else if (key === 'assetsInclude' && rootPath === '') { + // oxlint-disable-next-line unicorn/prefer-spread + merged[key] = [].concat(existing, value) + continue + } else if ( + (((key === 'noExternal' || key === 'external') && + (rootPath === 'ssr' || rootPath === 'resolve')) || + (key === 'allowedHosts' && rootPath === 'server')) && + (existing === true || value === true) + ) { + merged[key] = true + continue + } else if (key === 'plugins' && rootPath === 'worker') { + // oxlint-disable-next-line typescript/no-unsafe-return + merged[key] = (): any[] => [ + ...backwardCompatibleWorkerPlugins(existing), + ...backwardCompatibleWorkerPlugins(value), + ] + continue + } else if (key === 'server' && rootPath === 'server.hmr') { + merged[key] = value + continue + } + + if (Array.isArray(existing) || Array.isArray(value)) { + merged[key] = [...arraify(existing), ...arraify(value)] + continue + } + + if (isPlainObject(existing) && isPlainObject(value)) { + merged[key] = mergeConfigRecursively( + existing, + value, + // treat environment.* as root + rootPath && !environmentPathRE.test(rootPath) + ? `${rootPath}.${key}` + : key, + ) + continue + } + + merged[key] = value + } + + return merged +} + +/** + * Merge Vite configurations + * + * @param defaults - Default configuration + * @param overrides - Override configuration + * @param isRoot - Whether it's root level merge + * @returns Merged configuration + */ +export const mergeViteConfig = ( + defaults: Record, + overrides: Record, + isRoot = true, +): Record => { + if (typeof defaults === 'function' || typeof overrides === 'function') { + throw new Error(`Cannot merge config in form of callback`) + } + + return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.') +} diff --git a/packages/bundler-helper/src/vite/viteHelper.ts b/packages/bundler-helper/src/vite/viteHelper.ts new file mode 100644 index 0000000000..63664eb810 --- /dev/null +++ b/packages/bundler-helper/src/vite/viteHelper.ts @@ -0,0 +1,138 @@ +import type { ViteBundlerOptions } from '@vuepress/bundler-vite' +import type { App } from '@vuepress/core' +import { isString } from '@vuepress/shared' +import { getRunningPackageManager } from '@vuepress/utils' + +import { getBundlerName } from '../getBundlerName.js' +import { mergeViteConfig } from './mergeViteConfig.js' + +const runningPackageManger = getRunningPackageManager() + +/** + * Add Vite config + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param config - Vite config + */ +export const addViteConfig = ( + bundlerOptions: unknown, + app: App, + config: Record, +): void => { + if (getBundlerName(app) === 'vite') { + const viteBundlerOptions = bundlerOptions as ViteBundlerOptions + + viteBundlerOptions.viteOptions = mergeViteConfig( + viteBundlerOptions.viteOptions ?? {}, + config, + ) + } +} + +/** + * Add modules to Vite `optimizeDeps.include` list + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param module - Module name(s) to include + * @param isDirectDep - Whether the module is a direct dependency + */ +export const addViteOptimizeDepsInclude = ( + bundlerOptions: unknown, + app: App, + module: string[] | string, + isDirectDep = true, +): void => { + if ( + runningPackageManger?.name !== 'pnpm' || + // pnpm is not able to optimize deps in tree at first startup + // as it user dependencies is not accessible in vite directly + // vite needs to build a dependency graph + ('FORCE_OPTIMIZE_DEPS' in process.env + ? Boolean(process.env.FORCE_OPTIMIZE_DEPS) + : isDirectDep) + ) { + addViteConfig(bundlerOptions, app, { + optimizeDeps: { + include: isString(module) ? [module] : module, + }, + }) + } +} + +/** + * Add modules to Vite `optimizeDeps.exclude` list + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param module - Module name(s) to exclude + */ +export const addViteOptimizeDepsExclude = ( + bundlerOptions: unknown, + app: App, + module: string[] | string, +): void => { + addViteConfig(bundlerOptions, app, { + optimizeDeps: { + exclude: isString(module) ? [module] : module, + }, + }) +} + +/** + * Add modules to Vite `optimizeDeps.needsInterop` list + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param module - Module name(s) that needs interop + */ +export const addViteOptimizeDepsNeedsInterop = ( + bundlerOptions: unknown, + app: App, + module: string[] | string, +): void => { + addViteConfig(bundlerOptions, app, { + optimizeDeps: { + needsInterop: isString(module) ? [module] : module, + }, + }) +} + +/** + * Add modules to Vite `ssr.external` list + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param module - Module name(s) to externalize + */ +export const addViteSsrExternal = ( + bundlerOptions: unknown, + app: App, + module: string[] | string, +): void => { + addViteConfig(bundlerOptions, app, { + ssr: { + external: isString(module) ? [module] : module, + }, + }) +} + +/** + * Add modules to Vite `ssr.noExternal` list + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param module - Module name(s) to not externalize + */ +export const addViteSsrNoExternal = ( + bundlerOptions: unknown, + app: App, + module: string[] | string, +): void => { + addViteConfig(bundlerOptions, app, { + ssr: { + noExternal: isString(module) ? [module] : module, + }, + }) +} diff --git a/packages/bundler-helper/src/webpack/chainWebpack.ts b/packages/bundler-helper/src/webpack/chainWebpack.ts new file mode 100644 index 0000000000..fb29488026 --- /dev/null +++ b/packages/bundler-helper/src/webpack/chainWebpack.ts @@ -0,0 +1,41 @@ +import type { + WebpackBundlerOptions, + WebpackChainConfig, +} from '@vuepress/bundler-webpack' +import type { App } from '@vuepress/core' + +import { getBundlerName } from '../getBundlerName.js' + +/** + * Chain webpack + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param chain - Chain function + */ +export const chainWebpack = ( + bundlerOptions: unknown, + app: App, + chain: ( + config: WebpackChainConfig, + isServer: boolean, + isBuild: boolean, + ) => void, +): void => { + if (getBundlerName(app) === 'webpack') { + const webpackBundlerOptions = bundlerOptions as WebpackBundlerOptions + const { chainWebpack: originalChainWebpack } = webpackBundlerOptions + + /** + * Chain webpack config + * + * @param config - Webpack chain config + * @param isServer - Whether it's for server + * @param isBuild - Whether it's for build + */ + webpackBundlerOptions.chainWebpack = (config, isServer, isBuild): void => { + originalChainWebpack?.(config, isServer, isBuild) + chain(config, isServer, isBuild) + } + } +} diff --git a/packages/bundler-helper/src/webpack/configWebpack.ts b/packages/bundler-helper/src/webpack/configWebpack.ts new file mode 100644 index 0000000000..beaa407910 --- /dev/null +++ b/packages/bundler-helper/src/webpack/configWebpack.ts @@ -0,0 +1,49 @@ +import type { + WebpackBundlerOptions, + WebpackConfiguration, +} from '@vuepress/bundler-webpack' +import type { App } from '@vuepress/core' + +import { getBundlerName } from '../getBundlerName.js' + +/** + * Configure webpack options + * + * @param bundlerOptions - VuePress Bundler config + * @param app - VuePress Node App + * @param configureWebpack - Function to configure webpack + */ +export const configWebpack = ( + bundlerOptions: unknown, + app: App, + configureWebpack: ( + config: WebpackConfiguration, + isServer: boolean, + isBuild: boolean, + ) => void, +): void => { + if (getBundlerName(app) === 'webpack') { + const webpackBundlerOptions = bundlerOptions as WebpackBundlerOptions + const { configureWebpack: originalConfigWebpack } = webpackBundlerOptions + + /** + * Configure webpack options + * + * @param config - Webpack config + * @param isServer - Whether it's for server + * @param isBuild - Whether it's for build + * @returns Updated webpack config + */ + webpackBundlerOptions.configureWebpack = ( + config, + isServer, + isBuild, + ): WebpackConfiguration | void => { + const result = originalConfigWebpack?.(config, isServer, isBuild) + + configureWebpack(config, isServer, isBuild) + + return result + } + } +} diff --git a/packages/bundler-helper/src/webpack/index.ts b/packages/bundler-helper/src/webpack/index.ts new file mode 100644 index 0000000000..39edab0997 --- /dev/null +++ b/packages/bundler-helper/src/webpack/index.ts @@ -0,0 +1,2 @@ +export * from './chainWebpack.js' +export * from './configWebpack.js' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1eb00167c8..b70d68e300 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -11,4 +11,5 @@ export { debug, colors, fs, hash, ora, path, picomatch, tinyglobby } export * from './console/index.js' export * from './module/index.js' +export * from './packageManager/index.js' export * from './ssr/index.js' diff --git a/packages/utils/src/packageManager/getRunningPackageManager.ts b/packages/utils/src/packageManager/getRunningPackageManager.ts new file mode 100644 index 0000000000..d4005e38d5 --- /dev/null +++ b/packages/utils/src/packageManager/getRunningPackageManager.ts @@ -0,0 +1,24 @@ +export interface RunningPackageMangerInfo { + name: string + version: string +} + +const getPackageManagerFromUserAgent = ( + userAgent: string, +): RunningPackageMangerInfo => { + const packageMangerSpec = userAgent.split(' ')[0] + + const separatorPos = packageMangerSpec.lastIndexOf('/') + const name = packageMangerSpec.substring(0, separatorPos) + + return { + name: name === 'npminstall' ? 'cnpm' : name, + version: packageMangerSpec.substring(separatorPos + 1), + } +} + +export const getRunningPackageManager = (): RunningPackageMangerInfo | null => { + if (!process.env.npm_config_user_agent) return null + + return getPackageManagerFromUserAgent(process.env.npm_config_user_agent) +} diff --git a/packages/utils/src/packageManager/index.ts b/packages/utils/src/packageManager/index.ts new file mode 100644 index 0000000000..f8004b3443 --- /dev/null +++ b/packages/utils/src/packageManager/index.ts @@ -0,0 +1 @@ +export * from './getRunningPackageManager.js' diff --git a/packages/vuepress/package.json b/packages/vuepress/package.json index 28bc8dd5e5..25cd2fa49e 100644 --- a/packages/vuepress/package.json +++ b/packages/vuepress/package.json @@ -35,6 +35,7 @@ "exports": { ".": "./dist/index.js", "./bin": "./bin/vuepress.js", + "./bundler-helper": "./dist/bundler-helper.js", "./cli": "./dist/cli.js", "./client": "./dist/client.js", "./client-app": "./dist/client-app.js", @@ -50,6 +51,7 @@ "clean": "rimraf dist" }, "dependencies": { + "@vuepress/bundlerhelper": "workspace:*", "@vuepress/cli": "workspace:*", "@vuepress/client": "workspace:*", "@vuepress/core": "workspace:*", @@ -75,6 +77,7 @@ "dts": true, "entry": [ "./src/index.ts", + "./src/bundler-helper.ts", "./src/cli.ts", "./src/client-app.ts", "./src/client.ts", diff --git a/packages/vuepress/src/bundler-helper.ts b/packages/vuepress/src/bundler-helper.ts new file mode 100644 index 0000000000..8cc49a595c --- /dev/null +++ b/packages/vuepress/src/bundler-helper.ts @@ -0,0 +1 @@ +export * from '@vuepress/bundlerhelper' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b724b5ab36..701bd19a0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,28 @@ importers: specifier: ^14.2.6 version: 14.2.6 + packages/bundler-helper: + dependencies: + '@vuepress/bundler-vite': + specifier: workspace:* + version: link:../bundler-vite + '@vuepress/bundler-webpack': + specifier: workspace:* + version: link:../bundler-webpack + '@vuepress/core': + specifier: workspace:* + version: link:../core + '@vuepress/shared': + specifier: workspace:* + version: link:../shared + '@vuepress/utils': + specifier: workspace:* + version: link:../utils + devDependencies: + '@types/connect': + specifier: ^3.4.38 + version: 3.4.38 + packages/bundler-vite: dependencies: '@vitejs/plugin-vue': @@ -472,6 +494,9 @@ importers: '@vuepress/bundler-webpack': specifier: workspace:* version: link:../bundler-webpack + '@vuepress/bundlerhelper': + specifier: workspace:* + version: link:../bundler-helper '@vuepress/cli': specifier: workspace:* version: link:../cli @@ -6872,7 +6897,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.3 '@types/debug@4.1.13': dependencies: