diff --git a/crates/vite_error/src/lib.rs b/crates/vite_error/src/lib.rs index a5b4ceb56f..45eee77712 100644 --- a/crates/vite_error/src/lib.rs +++ b/crates/vite_error/src/lib.rs @@ -102,6 +102,9 @@ pub enum Error { #[error("Test failed")] TestFailed { status: Str, reason: Str }, + #[error("Doc failed")] + DocFailed { status: Str, reason: Str }, + #[error( "The stripped path ({stripped_path:?}) is not a valid relative path because: {invalid_path_data_error}" )] diff --git a/crates/vite_task/src/doc.rs b/crates/vite_task/src/doc.rs new file mode 100644 index 0000000000..d715a1c8be --- /dev/null +++ b/crates/vite_task/src/doc.rs @@ -0,0 +1,29 @@ +use std::future::Future; +use std::iter; + +use petgraph::stable_graph::StableGraph; + +use crate::config::ResolvedTask; +use crate::schedule::ExecutionPlan; +use crate::{Error, ResolveCommandResult, Workspace}; + +pub async fn doc< + Doc: Future>, + DocFn: Fn() -> Doc, +>( + resolve_doc_command: DocFn, + workspace: &mut Workspace, + args: &Vec, +) -> Result<(), Error> { + let resolved_task = ResolvedTask::resolve_from_builtin( + workspace, + resolve_doc_command, + "doc", + iter::once("dev").chain(args.iter().map(std::string::String::as_str)), + ) + .await?; + let mut task_graph: StableGraph = Default::default(); + task_graph.add_node(resolved_task); + ExecutionPlan::plan(task_graph, false)?.execute(workspace).await?; + Ok(()) +} \ No newline at end of file diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index eab056fc5b..b20568cfc9 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -2,6 +2,7 @@ mod cache; mod cmd; mod collections; mod config; +mod doc; mod execute; mod fingerprint; mod fmt; @@ -100,6 +101,11 @@ pub enum Commands { /// Arguments to pass to vite install args: Vec, }, + Doc { + #[clap(last = true)] + /// Arguments to pass to vitepress + args: Vec, + }, /// Manage the task cache Cache { #[clap(subcommand)] @@ -156,11 +162,16 @@ pub struct CliOptions< Box>>, >, TestFn: Fn() -> Test = Box Test>, + Doc: Future> = Pin< + Box>>, + >, + DocFn: Fn() -> Doc = Box Doc>, > { pub lint: LintFn, pub fmt: FmtFn, pub vite: ViteFn, pub test: TestFn, + pub doc: DocFn, } pub struct ResolveCommandResult { @@ -207,10 +218,12 @@ pub async fn main< ViteFn: Fn() -> Vite, Test: Future>, TestFn: Fn() -> Test, + Doc: Future>, + DocFn: Fn() -> Doc, >( cwd: AbsolutePathBuf, args: Args, - options: Option>, + options: Option>, ) -> Result<(), Error> { // Auto-install dependencies if needed, but skip for install command itself, or if `VITE_DISABLE_AUTO_INSTALL=1` is set. if !matches!(args.commands, Some(Commands::Install { .. })) @@ -283,6 +296,14 @@ pub async fn main< install::InstallCommand::builder(cwd).build().execute(&args).await?; return Ok(()); } + Some(Commands::Doc { args }) => { + let mut workspace = Workspace::partial_load(cwd)?; + if let Some(doc_fn) = options.map(|o| o.doc) { + doc::doc(doc_fn, &mut workspace, args).await?; + workspace.unload().await?; + } + return Ok(()); + } Some(Commands::Cache { subcmd }) => { let cache_path = Workspace::get_cache_path(&cwd)?; match subcmd { @@ -434,6 +455,28 @@ mod tests { } } + #[test] + fn test_args_doc_command() { + let args = Args::try_parse_from(&["vite-plus", "doc"]).unwrap(); + assert_eq!(args.task, None); + assert!(args.task_args.is_empty()); + assert!(matches!(args.commands, Some(Commands::Doc { .. }))); + assert!(!args.debug); + } + + #[test] + fn test_args_doc_command_with_args() { + let args = + Args::try_parse_from(&["vite-plus", "doc", "--", "--port", "3000", "--host"]).unwrap(); + assert_eq!(args.task, None); + assert!(args.task_args.is_empty()); + if let Some(Commands::Doc { args }) = &args.commands { + assert_eq!(args, &vec!["--port".to_string(), "3000".to_string(), "--host".to_string()]); + } else { + panic!("Expected Doc command"); + } + } + #[test] fn test_args_debug_flag() { let args = Args::try_parse_from(&["vite-plus", "--debug", "build"]).unwrap(); diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 2d033d74db..40e25098a0 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -46,6 +46,8 @@ pub struct CliOptions { pub vite: Arc>>, /// Resolver function for the test tool (vitest) pub test: Arc>>, + /// Resolver function for the doc tool (vitepress) + pub doc: Arc>>, /// Optional working directory override pub cwd: Option, } @@ -70,7 +72,7 @@ impl From for ResolveCommandResult { } } -static BUILTIN_COMMANDS: &[&str] = &["lint", "fmt", "build", "test"]; +static BUILTIN_COMMANDS: &[&str] = &["lint", "fmt", "build", "test", "doc"]; /// Main entry point for the CLI, called from JavaScript. /// @@ -104,6 +106,7 @@ pub async fn run(options: CliOptions) -> Result<()> { let fmt = options.fmt; let vite = options.vite; let test = options.test; + let doc = options.doc; // Call the Rust core with wrapped resolver functions if let Err(e) = vite_task::main( cwd, @@ -154,6 +157,17 @@ pub async fn run(options: CliOptions) -> Result<()> { Ok(resolved.into()) }, + // Wrap the doc resolver to be callable from Rust + doc: || async { + let resolved = doc + .call_async(Ok(())) + .await + .map_err(js_error_to_doc_error)? + .await + .map_err(js_error_to_doc_error)?; + + Ok(resolved.into()) + }, }), ) .await @@ -184,6 +198,11 @@ fn js_error_to_test_error(err: napi::Error) -> Error { Error::TestFailed { status: err.status.to_string().into(), reason: err.to_string().into() } } +/// Convert JavaScript errors to Rust doc errors +fn js_error_to_doc_error(err: napi::Error) -> Error { + Error::DocFailed { status: err.status.to_string().into(), reason: err.to_string().into() } +} + fn parse_args() -> Args { // ArgsOs [node, vite-plus, ...] let mut raw_args = std::env::args_os().skip(2); @@ -204,6 +223,7 @@ fn parse_args() -> Args { "fmt" => Commands::Fmt { args: forwarded_args }, "build" => Commands::Build { args: forwarded_args }, "test" => Commands::Test { args: forwarded_args }, + "doc" => Commands::Doc { args: forwarded_args }, _ => unreachable!(), }), debug: false, diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 0fa21f008a..5888b8fb23 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -10,6 +10,7 @@ */ import { run } from '../binding/index.js'; +import { doc } from './doc.ts'; import { fmt } from './fmt.ts'; import { lint } from './lint.ts'; import { test } from './test.ts'; @@ -22,4 +23,5 @@ run({ fmt, // Resolves oxfmt binary for formatting vite, // Resolves vite binary for build/dev commands test, // Resolves vitest binary for test commands + doc, // Resolves vitepress binary for documentation }); diff --git a/packages/cli/src/doc.ts b/packages/cli/src/doc.ts new file mode 100644 index 0000000000..d5b41c4c97 --- /dev/null +++ b/packages/cli/src/doc.ts @@ -0,0 +1,45 @@ +/** + * VitePress tool resolver for the vite-plus CLI. + * + * This module exports a function that resolves the VitePress binary path + * using Node.js module resolution. The resolved path is passed back + * to the Rust core, which then executes VitePress for documentation. + * + * Used for: `vite-plus doc` command + */ + +import { createRequire } from 'node:module'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); + +/** + * Resolves the VitePress binary path and environment variables. + * + * @returns Promise containing: + * - binPath: Absolute path to the VitePress CLI entry point + * - envs: Environment variables to set when executing VitePress + * + * VitePress is a Vite & Vue powered static site generator for + * building documentation websites with excellent performance. + */ +export async function doc(): Promise<{ + binPath: string; + envs: Record; +}> { + // Resolve the VitePress CLI module directly + const binPath = require.resolve('vitepress/bin/vitepress.js', { + paths: [process.cwd(), dirname(fileURLToPath(import.meta.url))], + }); + + return { + binPath, + // Pass through source map debugging environment variable if set + envs: process.env.DEBUG_DISABLE_SOURCE_MAP + ? { + DEBUG_DISABLE_SOURCE_MAP: process.env.DEBUG_DISABLE_SOURCE_MAP, + } + : {}, + }; +} \ No newline at end of file