diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index 650e40c196..bcd03a67fd 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -70,7 +70,6 @@ async function applyDefaultPlugins( import('./plugins/splitChunks').then(({ pluginSplitChunks }) => pluginSplitChunks(), ), - import('./plugins/open').then(({ pluginOpen }) => pluginOpen()), import('./plugins/inlineChunk').then(({ pluginInlineChunk }) => pluginInlineChunk(), ), diff --git a/packages/core/src/plugins/output.ts b/packages/core/src/plugins/output.ts index 94965e8d3c..1c77a7f25f 100644 --- a/packages/core/src/plugins/output.ts +++ b/packages/core/src/plugins/output.ts @@ -7,13 +7,13 @@ import { } from '../constants'; import { formatPublicPath, getFilename } from '../helpers'; import { getCssExtractPlugin } from '../pluginHelper'; +import { replacePortPlaceholder } from '../server/open'; import type { NormalizedEnvironmentConfig, RsbuildContext, RsbuildPlugin, } from '../types'; import { isUseCssExtract } from './css'; -import { replacePortPlaceholder } from './open'; function getPublicPath({ isProd, diff --git a/packages/core/src/plugins/server.ts b/packages/core/src/plugins/server.ts index c671870f1e..6ef1f784f2 100644 --- a/packages/core/src/plugins/server.ts +++ b/packages/core/src/plugins/server.ts @@ -1,13 +1,28 @@ import fs from 'node:fs'; import { isAbsolute, join } from 'node:path'; import { normalizePublicDirs } from '../config'; -import type { RsbuildPlugin } from '../types'; +import { open } from '../server/open'; +import type { OnAfterStartDevServerFn, RsbuildPlugin } from '../types'; // For Rsbuild Server Config export const pluginServer = (): RsbuildPlugin => ({ name: 'rsbuild:server', setup(api) { + const onStartServer: OnAfterStartDevServerFn = async ({ port, routes }) => { + const config = api.getNormalizedConfig(); + if (config.server.open) { + open({ + https: api.context.devServer?.https, + port, + routes, + config, + }); + } + }; + + api.onAfterStartDevServer(onStartServer); + api.onAfterStartProdServer(onStartServer); api.onBeforeBuild(async ({ isFirstCompile }) => { if (!isFirstCompile) { return; diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 21ea0653b5..8f44e938cd 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -29,6 +29,7 @@ import { } from './helper'; import { createHttpServer } from './httpServer'; import { notFoundMiddleware } from './middlewares'; +import { open } from './open'; import { onBeforeRestartServer } from './restart'; import { setupWatchFiles } from './watchFiles'; @@ -78,6 +79,10 @@ export type RsbuildDevServer = { * Print the server URLs. */ printUrls: () => void; + /** + * Open URL in the browser after starting the server. + */ + open: () => Promise; }; const formatDevConfig = (config: NormalizedDevConfig, port: number) => { @@ -333,6 +338,15 @@ export async function createDevServer< await Promise.all([devMiddlewares.close(), fileWatcher?.close()]); }, printUrls, + open: async () => { + return open({ + https, + port, + routes, + config, + clearCache: true, + }); + }, }; logger.debug('create dev server done'); diff --git a/packages/core/src/plugins/open.ts b/packages/core/src/server/open.ts similarity index 57% rename from packages/core/src/plugins/open.ts rename to packages/core/src/server/open.ts index 5ec1112085..0e0acf99e8 100644 --- a/packages/core/src/plugins/open.ts +++ b/packages/core/src/server/open.ts @@ -3,7 +3,7 @@ import { promisify } from 'node:util'; import { STATIC_PATH } from '../constants'; import { canParse, castArray } from '../helpers'; import { logger } from '../logger'; -import type { NormalizedConfig, Routes, RsbuildPlugin } from '../types'; +import type { NormalizedConfig, Routes } from '../types'; const execAsync = promisify(exec); @@ -37,7 +37,7 @@ const getTargetBrowser = async () => { * Copyright (c) 2015-present, Facebook, Inc. * https://github.com/facebook/create-react-app/blob/master/LICENSE */ -export async function openBrowser(url: string): Promise { +async function openBrowser(url: string): Promise { // If we're on OS X, the user hasn't specifically // requested a different browser, we can try opening // a Chromium browser with AppleScript. This lets us reuse an @@ -80,6 +80,12 @@ export async function openBrowser(url: string): Promise { } } +let openedURLs: string[] = []; + +const clearOpenedURLs = () => { + openedURLs = []; +}; + export const replacePortPlaceholder = (url: string, port: number): string => url.replace(//g, String(port)); @@ -98,17 +104,12 @@ export function resolveUrl(str: string, base: string): string { } } -const openedURLs: string[] = []; - const normalizeOpenConfig = ( config: NormalizedConfig, -): { targets?: string[]; before?: () => Promise | void } => { +): { targets: string[]; before?: () => Promise | void } => { const { open } = config.server; - if (open === false) { - return {}; - } - if (open === true) { + if (typeof open === 'boolean') { return { targets: [] }; } if (typeof open === 'string') { @@ -124,67 +125,62 @@ const normalizeOpenConfig = ( }; }; -export function pluginOpen(): RsbuildPlugin { - return { - name: 'rsbuild:open', - setup(api) { - const onStartServer = async (params: { - port: number; - routes: Routes; - }) => { - const { port, routes } = params; - const config = api.getNormalizedConfig(); - const { https } = api.context.devServer || {}; - const { targets, before } = normalizeOpenConfig(config); - - // Skip open in codesandbox. After being bundled, the `open` package will - // try to call system xdg-open, which will cause an error on codesandbox. - // https://github.com/codesandbox/codesandbox-client/issues/6642 - const isCodesandbox = process.env.CSB === 'true'; - const shouldOpen = targets !== undefined && !isCodesandbox; - - if (!shouldOpen) { - return; - } - - const urls: string[] = []; - const protocol = https ? 'https' : 'http'; - const baseUrl = `${protocol}://localhost:${port}`; - - if (!targets.length) { - if (routes.length) { - // auto open the first one - urls.push(`${baseUrl}${routes[0].pathname}`); - } - } else { - urls.push( - ...targets.map((target) => - resolveUrl(replacePortPlaceholder(target, port), baseUrl), - ), - ); - } - - const openUrls = () => { - for (const url of urls) { - /** - * If a URL has been opened in current process, we will not open it again. - * It can prevent opening the same URL multiple times. - */ - if (!openedURLs.includes(url)) { - openBrowser(url); - openedURLs.push(url); - } - } - }; - - if (before) { - await before(); - } - openUrls(); - }; - - api.onAfterStartDevServer(onStartServer); - api.onAfterStartProdServer(onStartServer); - }, - }; +export async function open({ + https, + port, + routes, + config, + clearCache, +}: { + https?: boolean; + port: number; + routes: Routes; + config: NormalizedConfig; + clearCache?: boolean; +}): Promise { + const { targets, before } = normalizeOpenConfig(config); + + // Skip open in codesandbox. After being bundled, the `open` package will + // try to call system xdg-open, which will cause an error on codesandbox. + // https://github.com/codesandbox/codesandbox-client/issues/6642 + const isCodesandbox = process.env.CSB === 'true'; + if (isCodesandbox) { + return; + } + + if (clearCache) { + clearOpenedURLs(); + } + + const urls: string[] = []; + const protocol = https ? 'https' : 'http'; + const baseUrl = `${protocol}://localhost:${port}`; + + if (!targets.length) { + if (routes.length) { + // auto open the first one + urls.push(`${baseUrl}${routes[0].pathname}`); + } + } else { + urls.push( + ...targets.map((target) => + resolveUrl(replacePortPlaceholder(target, port), baseUrl), + ), + ); + } + + if (before) { + await before(); + } + + for (const url of urls) { + /** + * If an URL has been opened in current process, we will not open it again. + * It can prevent opening the same URL multiple times. + */ + if (!openedURLs.includes(url)) { + openBrowser(url); + openedURLs.push(url); + } + } } diff --git a/packages/core/tests/__snapshots__/environments.test.ts.snap b/packages/core/tests/__snapshots__/environments.test.ts.snap index e0d9382554..cb806516e2 100644 --- a/packages/core/tests/__snapshots__/environments.test.ts.snap +++ b/packages/core/tests/__snapshots__/environments.test.ts.snap @@ -356,7 +356,6 @@ exports[`environment config > should print environment config when inspect confi "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -513,7 +512,6 @@ exports[`environment config > should print environment config when inspect confi "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -688,7 +686,6 @@ exports[`environment config > should support modify environment config by api.mo "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -846,7 +843,6 @@ exports[`environment config > should support modify environment config by api.mo "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -1005,7 +1001,6 @@ exports[`environment config > should support modify environment config by api.mo "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -1167,7 +1162,6 @@ exports[`environment config > should support modify single environment config by "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", @@ -1325,7 +1319,6 @@ exports[`environment config > should support modify single environment config by "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", diff --git a/packages/core/tests/__snapshots__/inspect.test.ts.snap b/packages/core/tests/__snapshots__/inspect.test.ts.snap index 17b63c4328..0f183c6b9e 100644 --- a/packages/core/tests/__snapshots__/inspect.test.ts.snap +++ b/packages/core/tests/__snapshots__/inspect.test.ts.snap @@ -23,7 +23,6 @@ exports[`inspectConfig > should print plugin names when inspect config 1`] = ` "rsbuild:swc", "rsbuild:externals", "rsbuild:split-chunks", - "rsbuild:open", "rsbuild:inline-chunk", "rsbuild:rsdoctor", "rsbuild:resource-hints", diff --git a/packages/core/tests/open.test.ts b/packages/core/tests/open.test.ts index e5587761fe..c5d2b6a3b9 100644 --- a/packages/core/tests/open.test.ts +++ b/packages/core/tests/open.test.ts @@ -1,4 +1,4 @@ -import { replacePortPlaceholder, resolveUrl } from '../src/plugins/open'; +import { replacePortPlaceholder, resolveUrl } from '../src/server/open'; describe('plugin-open', () => { it('#replacePortPlaceholder - should replace port number correctly', () => { diff --git a/website/docs/en/api/javascript-api/instance.mdx b/website/docs/en/api/javascript-api/instance.mdx index 29367e928a..cf3219475d 100644 --- a/website/docs/en/api/javascript-api/instance.mdx +++ b/website/docs/en/api/javascript-api/instance.mdx @@ -328,6 +328,10 @@ type RsbuildDevServer = { * Print the server URLs. */ printUrls: () => void; + /** + * Open URL in the browser after starting the server. + */ + open: () => Promise; }; type CreateDevServerOptions = { diff --git a/website/docs/zh/api/javascript-api/instance.mdx b/website/docs/zh/api/javascript-api/instance.mdx index b978dfdf7d..3c98d962cc 100644 --- a/website/docs/zh/api/javascript-api/instance.mdx +++ b/website/docs/zh/api/javascript-api/instance.mdx @@ -351,6 +351,10 @@ type RsbuildDevServer = { * 打印 server URLs */ printUrls: () => void; + /** + * 启动服务器后,在浏览器中打开 URL + */ + open: () => Promise; }; type CreateDevServerOptions = {