From 15ed34af4a77fd5ff6ab231989de404241733b53 Mon Sep 17 00:00:00 2001 From: nyqykk Date: Tue, 16 Dec 2025 17:21:52 +0800 Subject: [PATCH 1/5] feat: support prune dep tree from specific project --- README.md | 5 +++-- README.zh-CN.md | 4 +++- src/constant.ts | 2 +- src/logger.ts | 6 +++--- src/plugin.ts | 2 +- src/workspace-dev.ts | 30 ++++++++++++++++++++++-------- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 438a29a..a6929e4 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,9 @@ interface projects { */ match?: (stdout: string) => boolean; /** - * Whether to skip starting the current sub-project. Default is `false`. - * Useful for sub-projects that do not need to be started. + * Whether to skip starting the current sub-project. The default value is `false`, typically used to skip sub-projects that don't need to be started. + * When the value is `prune`, the specified project will be pruned, meaning that the project and all its direct and indirect dependencies will not be started by the plugin. + * When the value is `true`, the current sub-project will be skipped from starting, but no pruning will be performed, meaning that the project's direct and indirect dependencies will still be started by the plugin. */ skip?: boolean; } diff --git a/README.zh-CN.md b/README.zh-CN.md index 7121d25..0dfbb4b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -99,8 +99,10 @@ interface Projects { match?: (stdout: string) => boolean; /** * 是否跳过当前子项目的启动,默认值为 `false`,通常用于跳过一些不需要启动的子项目。 + * 当值为 `prune` 时,会从指定项目进行剪枝,这意味着该项目以及他的所有直接和间接依赖都不会被插件启动。 + * 当值为 `true` 时,会跳过当前子项目的启动,但不会进行剪枝,这意味着该项目的直接和间接依赖仍然会被插件启动。 */ - skip?: boolean; + skip?: 'prune' | boolean; } // 例如,配置 lib1 子项目,用 build:watch 命令启动,匹配 watch success 日志 diff --git a/src/constant.ts b/src/constant.ts index 28eaa21..7467a33 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -1,5 +1,5 @@ export const PACKAGE_JSON = 'package.json'; -export const DEBUG_LOG_TITLE = '[Rsbuild Workspace Dev Plugin]: '; +export const PLUGIN_LOG_TITLE = '[Rsbuild Workspace Dev Plugin]: '; export const RSLIB_READY_MESSAGE = 'build complete, watching for changes'; export const MODERN_MODULE_READY_MESSAGE = 'Watching for file changes'; diff --git a/src/logger.ts b/src/logger.ts index f0e88d3..6cb5f16 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { DEBUG_LOG_TITLE } from './constant.js'; +import { PLUGIN_LOG_TITLE } from './constant.js'; import { isDebug } from './utils.js'; enum LogType { @@ -26,7 +26,7 @@ export class Logger { this.name = name; this.stdout = ''; this.stderr = ''; - this.logTitle = DEBUG_LOG_TITLE; + this.logTitle = PLUGIN_LOG_TITLE; } appendLog(type: 'stdout' | 'stderr', log: string) { @@ -71,7 +71,7 @@ export class Logger { } } -export const debugLog = (msg: string, prefix = DEBUG_LOG_TITLE) => { +export const debugLog = (msg: string, prefix = PLUGIN_LOG_TITLE) => { if (isDebug) { console.log(prefix + msg); } diff --git a/src/plugin.ts b/src/plugin.ts index ad70bb0..7d54cb2 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -12,7 +12,7 @@ export function pluginWorkspaceDev( name: 'rsbuild-plugin-workspace-dev', async setup(api) { const rootPath = api.context.rootPath; - api.onBeforeStartDevServer(async () => { + api.modifyRsbuildConfig(async () => { const runner = new WorkspaceDevRunner({ cwd: rootPath, ...options, diff --git a/src/workspace-dev.ts b/src/workspace-dev.ts index 852de2a..021f822 100644 --- a/src/workspace-dev.ts +++ b/src/workspace-dev.ts @@ -4,9 +4,9 @@ import graphlib, { Graph } from 'graphlib'; import path from 'path'; import { - DEBUG_LOG_TITLE, MODERN_MODULE_READY_MESSAGE, PACKAGE_JSON, + PLUGIN_LOG_TITLE, RSLIB_READY_MESSAGE, TSUP_READY_MESSAGE, } from './constant.js'; @@ -27,7 +27,7 @@ export interface WorkspaceDevRunnerOptions { { match?: (stdout: string) => boolean; command?: string; - skip?: boolean; + skip?: boolean | 'prune'; } >; startCurrent?: boolean; @@ -86,7 +86,14 @@ export class WorkspaceDevRunner { packageJson, path: dir, }; + if (this.options.projects?.[name]?.skip === 'prune') { + console.log( + `${PLUGIN_LOG_TITLE} Prune project ${name} and its dependencies because it is marked as skip: prune`, + ); + return; + } this.graph.setNode(name, node); + // this.visited[name] = this.options.projects?.[name]?.skip ? true : false; this.visited[name] = false; this.visiting[name] = false; this.matched[name] = false; @@ -103,7 +110,10 @@ export class WorkspaceDevRunner { (p) => p.packageJson.name === depName, ); - if (isInternalDep) { + if ( + isInternalDep && + this.options.projects?.[depName]?.skip !== 'prune' + ) { this.graph.setEdge(packageName, depName); this.checkGraph(); const depPackage = packages.find( @@ -122,10 +132,13 @@ export class WorkspaceDevRunner { checkGraph() { const cycles = graphlib.alg.findCycles(this.graph); const nonSelfCycles = cycles.filter((c) => c.length !== 1); - debugLog(`cycles check: ${cycles}`); - if (nonSelfCycles.length) { + const nonSkipCycles = nonSelfCycles.filter((group) => { + const isSkip = group.some((node) => this.options.projects?.[node]?.skip); + return !isSkip; + }); + if (nonSkipCycles.length) { throw new Error( - `${DEBUG_LOG_TITLE}Dependency graph do not allow cycles.`, + `${PLUGIN_LOG_TITLE} Cycle dependency graph found: ${nonSkipCycles}, you should config projects in plugin options to skip someone, or fix the cycle dependency. Otherwise, a loop of dev will occur.`, ); } } @@ -143,7 +156,8 @@ export class WorkspaceDevRunner { const canStart = dependencies.every((dep) => { const selfStart = node === dep; const isVisiting = this.visiting[dep]; - const isVisited = selfStart || this.visited[dep]; + const skipDep = this.options.projects?.[dep]?.skip; + const isVisited = selfStart || this.visited[dep] || skipDep; return isVisited && !isVisiting; }); @@ -167,7 +181,7 @@ export class WorkspaceDevRunner { this.visited[node] = true; this.visiting[node] = false; debugLog(`Skip visit node: ${node}`); - logger.emitLogOnce('stdout', `skip visit node: ${name}`); + logger.emitLogOnce('stdout', `Skip visit node: ${name}`); return this.start().then(() => resolve()); } this.visiting[node] = true; From 2134993385a13f90523a024df56b79da046a6710 Mon Sep 17 00:00:00 2001 From: nyqykk Date: Tue, 16 Dec 2025 17:38:29 +0800 Subject: [PATCH 2/5] docs: modify skip doc --- README.md | 2 +- README.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6929e4..214787d 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ interface projects { /** * Whether to skip starting the current sub-project. The default value is `false`, typically used to skip sub-projects that don't need to be started. * When the value is `prune`, the specified project will be pruned, meaning that the project and all its direct and indirect dependencies will not be started by the plugin. - * When the value is `true`, the current sub-project will be skipped from starting, but no pruning will be performed, meaning that the project's direct and indirect dependencies will still be started by the plugin. + * When the value is `true`, the specified project will be skipped from starting, but no pruning will be performed, meaning that the project's direct and indirect dependencies will still be started by the plugin. */ skip?: boolean; } diff --git a/README.zh-CN.md b/README.zh-CN.md index 0dfbb4b..603178a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -100,7 +100,7 @@ interface Projects { /** * 是否跳过当前子项目的启动,默认值为 `false`,通常用于跳过一些不需要启动的子项目。 * 当值为 `prune` 时,会从指定项目进行剪枝,这意味着该项目以及他的所有直接和间接依赖都不会被插件启动。 - * 当值为 `true` 时,会跳过当前子项目的启动,但不会进行剪枝,这意味着该项目的直接和间接依赖仍然会被插件启动。 + * 当值为 `true` 时,会跳过指定项目的启动,但不会进行剪枝,这意味着该项目的直接和间接依赖仍然会被插件启动。 */ skip?: 'prune' | boolean; } From e907a2be6247793098b4368a9ca3cf192517591b Mon Sep 17 00:00:00 2001 From: nyqykk Date: Tue, 16 Dec 2025 19:12:36 +0800 Subject: [PATCH 3/5] chore: prune tree as default skip api --- README.md | 8 ++++---- README.zh-CN.md | 6 +++--- src/plugin.ts | 15 ++++++++++++++- src/workspace-dev.ts | 14 ++++++-------- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 214787d..5ebafd9 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,10 @@ interface projects { match?: (stdout: string) => boolean; /** * Whether to skip starting the current sub-project. The default value is `false`, typically used to skip sub-projects that don't need to be started. - * When the value is `prune`, the specified project will be pruned, meaning that the project and all its direct and indirect dependencies will not be started by the plugin. - * When the value is `true`, the specified project will be skipped from starting, but no pruning will be performed, meaning that the project's direct and indirect dependencies will still be started by the plugin. - */ - skip?: boolean; + * When the value is `true`, pruning will be performed on the specified project, meaning that this project and all its direct and indirect dependencies will not be started by the plugin. + * When the value is `only`, starting the specified project will be skipped, but no pruning will be performed, meaning that the project's direct and indirect dependencies will still be started by the plugin. + */ + skip?: boolean | 'only'; } diff --git a/README.zh-CN.md b/README.zh-CN.md index 603178a..ba7ebec 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -99,10 +99,10 @@ interface Projects { match?: (stdout: string) => boolean; /** * 是否跳过当前子项目的启动,默认值为 `false`,通常用于跳过一些不需要启动的子项目。 - * 当值为 `prune` 时,会从指定项目进行剪枝,这意味着该项目以及他的所有直接和间接依赖都不会被插件启动。 - * 当值为 `true` 时,会跳过指定项目的启动,但不会进行剪枝,这意味着该项目的直接和间接依赖仍然会被插件启动。 + * 当值为 `true` 时,会从指定项目进行剪枝,这意味着该项目以及他的所有直接和间接依赖都不会被插件启动。 + * 当值为 `only` 时,会跳过指定项目的启动,但不会进行剪枝,这意味着该项目的直接和间接依赖仍然会被插件启动。 */ - skip?: 'prune' | boolean; + skip?: boolean | 'only'; } // 例如,配置 lib1 子项目,用 build:watch 命令启动,匹配 watch success 日志 diff --git a/src/plugin.ts b/src/plugin.ts index 7d54cb2..2b7f6c3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -12,12 +12,25 @@ export function pluginWorkspaceDev( name: 'rsbuild-plugin-workspace-dev', async setup(api) { const rootPath = api.context.rootPath; - api.modifyRsbuildConfig(async () => { + api.onBeforeStartDevServer(async () => { const runner = new WorkspaceDevRunner({ cwd: rootPath, ...options, }); + await runner.init(); + await runner.start(); + Logger.setEndBanner(); + }); + + api.onBeforeBuild(async ({ isWatch, isFirstCompile }) => { + if (!isWatch || !isFirstCompile) { + return; + } + const runner = new WorkspaceDevRunner({ + cwd: rootPath, + ...options, + }); await runner.init(); await runner.start(); Logger.setEndBanner(); diff --git a/src/workspace-dev.ts b/src/workspace-dev.ts index 021f822..21f7d63 100644 --- a/src/workspace-dev.ts +++ b/src/workspace-dev.ts @@ -27,7 +27,7 @@ export interface WorkspaceDevRunnerOptions { { match?: (stdout: string) => boolean; command?: string; - skip?: boolean | 'prune'; + skip?: boolean | 'only'; } >; startCurrent?: boolean; @@ -86,14 +86,14 @@ export class WorkspaceDevRunner { packageJson, path: dir, }; - if (this.options.projects?.[name]?.skip === 'prune') { + const skip = this.options.projects?.[name]?.skip; + if (skip && skip !== 'only') { console.log( - `${PLUGIN_LOG_TITLE} Prune project ${name} and its dependencies because it is marked as skip: prune`, + `${PLUGIN_LOG_TITLE} Prune project ${name} and its dependencies because it is marked as skip: true`, ); return; } this.graph.setNode(name, node); - // this.visited[name] = this.options.projects?.[name]?.skip ? true : false; this.visited[name] = false; this.visiting[name] = false; this.matched[name] = false; @@ -110,10 +110,8 @@ export class WorkspaceDevRunner { (p) => p.packageJson.name === depName, ); - if ( - isInternalDep && - this.options.projects?.[depName]?.skip !== 'prune' - ) { + const skip = this.options.projects?.[depName]?.skip; + if (isInternalDep && skip !== true) { this.graph.setEdge(packageName, depName); this.checkGraph(); const depPackage = packages.find( From a50d98b89798f6cb45fa4aa3998a17df88a7e841 Mon Sep 17 00:00:00 2001 From: nyqykk Date: Wed, 17 Dec 2025 15:06:34 +0800 Subject: [PATCH 4/5] chore: use log util to log info --- src/workspace-dev.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/workspace-dev.ts b/src/workspace-dev.ts index 21f7d63..a2df84b 100644 --- a/src/workspace-dev.ts +++ b/src/workspace-dev.ts @@ -87,10 +87,7 @@ export class WorkspaceDevRunner { path: dir, }; const skip = this.options.projects?.[name]?.skip; - if (skip && skip !== 'only') { - console.log( - `${PLUGIN_LOG_TITLE} Prune project ${name} and its dependencies because it is marked as skip: true`, - ); + if (skip === true) { return; } this.graph.setNode(name, node); @@ -111,14 +108,24 @@ export class WorkspaceDevRunner { ); const skip = this.options.projects?.[depName]?.skip; - if (isInternalDep && skip !== true) { - this.graph.setEdge(packageName, depName); - this.checkGraph(); - const depPackage = packages.find( - (pkg) => pkg.packageJson.name === depName, - )!; - if (!this.getNode(depName)) { - initNode(depPackage); + if (isInternalDep) { + if (skip !== true) { + this.graph.setEdge(packageName, depName); + this.checkGraph(); + const depPackage = packages.find( + (pkg) => pkg.packageJson.name === depName, + )!; + if (!this.getNode(depName)) { + initNode(depPackage); + } + } else { + const logger = new Logger({ + name: depName, + }); + logger.emitLogOnce( + 'stdout', + `Prune project ${depName} and its dependencies because it is marked as skip: true`, + ); } } } From 834924c85d3fce822a4d4bfef2682386d9df11b1 Mon Sep 17 00:00:00 2001 From: nyqykk Date: Wed, 17 Dec 2025 15:20:38 +0800 Subject: [PATCH 5/5] chore: use debug log to show prune message --- src/workspace-dev.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/workspace-dev.ts b/src/workspace-dev.ts index a2df84b..450628a 100644 --- a/src/workspace-dev.ts +++ b/src/workspace-dev.ts @@ -119,11 +119,7 @@ export class WorkspaceDevRunner { initNode(depPackage); } } else { - const logger = new Logger({ - name: depName, - }); - logger.emitLogOnce( - 'stdout', + debugLog( `Prune project ${depName} and its dependencies because it is marked as skip: true`, ); }