diff --git a/crates/binding/src/lib.rs b/crates/binding/src/lib.rs index 703c19978..25719b1ff 100644 --- a/crates/binding/src/lib.rs +++ b/crates/binding/src/lib.rs @@ -74,7 +74,7 @@ pub struct BuildParams { }; } >; - copy?: string[]; + copy?: (string | { from: string; to: string })[]; codeSplitting?: | false | { diff --git a/crates/mako/src/config.rs b/crates/mako/src/config.rs index 1dbc8c3ef..54e0500ab 100644 --- a/crates/mako/src/config.rs +++ b/crates/mako/src/config.rs @@ -123,6 +123,13 @@ pub enum Platform { Node, } +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +pub enum CopyConfig { + Basic(String), + Advanced { from: String, to: String }, +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Config { @@ -137,7 +144,7 @@ pub struct Config { pub devtool: Option, pub externals: HashMap, pub providers: Providers, - pub copy: Vec, + pub copy: Vec, pub public_path: String, pub inline_limit: usize, pub inline_excludes_extensions: Vec, diff --git a/crates/mako/src/plugins/copy.rs b/crates/mako/src/plugins/copy.rs index e62b3195d..1120126a3 100644 --- a/crates/mako/src/plugins/copy.rs +++ b/crates/mako/src/plugins/copy.rs @@ -1,7 +1,8 @@ +use std::fs; use std::path::Path; use std::sync::Arc; -use anyhow::Result; +use anyhow::{anyhow, Result}; use fs_extra; use glob::glob; use notify::event::{CreateKind, DataChange, ModifyKind, RenameMode}; @@ -11,6 +12,7 @@ use tracing::debug; use crate::ast::file::win_path; use crate::compiler::Context; +use crate::config::CopyConfig; use crate::plugin::Plugin; use crate::stats::StatsJsonMap; use crate::utils::tokio_runtime; @@ -29,8 +31,12 @@ impl CopyPlugin { notify::Config::default(), ) .unwrap(); - for src in context.config.copy.iter() { - let src = context.root.join(src); + for config in context.config.copy.iter() { + let src = match config { + CopyConfig::Basic(src) => context.root.join(src), + CopyConfig::Advanced { from, .. } => context.root.join(from), + }; + if src.exists() { debug!("watch {:?}", src); let mode = if src.is_dir() { @@ -62,10 +68,31 @@ impl CopyPlugin { fn copy(context: &Arc) -> Result<()> { debug!("copy"); let dest = context.config.output.path.as_path(); - for src in context.config.copy.iter() { - let src = context.root.join(src); - debug!("copy {:?} to {:?}", src, dest); - copy(src.as_path(), dest)?; + for config in context.config.copy.iter() { + match config { + CopyConfig::Basic(src) => { + let src = context.root.join(src); + debug!("copy {:?} to {:?}", src, dest); + copy(&src, dest)?; + } + + CopyConfig::Advanced { from, to } => { + let src = context.root.join(from); + let target = dest.join(to.trim_start_matches("/")); + + let dest_path = dest.to_path_buf(); + if !target.starts_with(&dest_path) { + return Err(anyhow!("Invalid target path: {:?}", target)); + } + + if !target.exists() { + fs::create_dir_all(&target)?; + } + + debug!("copy {:?} to {:?}", src, target); + copy(&src, &target)?; + } + } } Ok(()) } diff --git a/docs/config.md b/docs/config.md index 2a5897b1a..fb9dbe83b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -116,7 +116,7 @@ Specify the code splitting strategy. Use `auto` or `granular` strategy for SPA, ### copy -- Type: `string[]` +- Type: `(string | { from: string; to: string })[]` - Default: `["public"]` Specify the files or directories to be copied. By default, the files under the `public` directory will be copied to the output directory. diff --git a/docs/config.zh-CN.md b/docs/config.zh-CN.md index 2558196f2..dc796498f 100644 --- a/docs/config.zh-CN.md +++ b/docs/config.zh-CN.md @@ -116,7 +116,7 @@ ### copy -- 类型:`string[]` +- 类型:`(string | { from: string; to: string })[]` - 默认值:`["public"]` 指定需要复制的文件或目录。默认情况下,会将 `public` 目录下的文件复制到输出目录。 diff --git a/e2e/fixtures/config.copy/expect.js b/e2e/fixtures/config.copy/expect.js index 7e305d64c..abac99d15 100644 --- a/e2e/fixtures/config.copy/expect.js +++ b/e2e/fixtures/config.copy/expect.js @@ -2,4 +2,8 @@ const assert = require("assert"); const { parseBuildResult } = require("../../../scripts/test-utils"); const { files } = parseBuildResult(__dirname); -assert("foo.js" in files, "assets files not copy"); +// Test original string pattern (copies to root) +assert("foo.js" in files, "assets files not copied (string pattern)"); + +// Test new from/to pattern (copies to assets-from-to directory) +assert("assets-from-to/foo.js" in files, "assets files not copied to correct location (from/to pattern)"); diff --git a/e2e/fixtures/config.copy/mako.config.json b/e2e/fixtures/config.copy/mako.config.json index 0e292c656..2b4d09b8d 100644 --- a/e2e/fixtures/config.copy/mako.config.json +++ b/e2e/fixtures/config.copy/mako.config.json @@ -1,3 +1,9 @@ { - "copy": ["src/assets"] -} + "copy": [ + "src/assets", + { + "from": "src/assets", + "to": "assets-from-to" + } + ] +} \ No newline at end of file diff --git a/packages/bundler-mako/index.js b/packages/bundler-mako/index.js index 04974f9b7..93a574467 100644 --- a/packages/bundler-mako/index.js +++ b/packages/bundler-mako/index.js @@ -249,13 +249,13 @@ function checkConfig(opts) { `umi config mako.${key} is not supported`, ); }); - // 暂不支持 { from, to } 格式 const { copy } = opts.config; if (copy) { for (const item of copy) { assert( - typeof item === 'string', - `copy config item must be string in Mako bundler, but got ${item}`, + typeof item === 'string' || + (typeof item === 'object' && item.from && item.to), + `copy config item must be string or { from: string, to: string } in Mako bundler, but got ${JSON.stringify(item)}`, ); } } diff --git a/packages/mako/binding.d.ts b/packages/mako/binding.d.ts index fa889880d..3dd8ab012 100644 --- a/packages/mako/binding.d.ts +++ b/packages/mako/binding.d.ts @@ -134,7 +134,7 @@ export interface BuildParams { }; } >; - copy?: string[]; + copy?: (string | { from: string; to: string })[]; codeSplitting?: | false | {