Skip to content

Commit

Permalink
feat(copy): support advanced copy configuration with custom target pa…
Browse files Browse the repository at this point in the history
…ths (#1711)

* feat(copy): support advanced copy configuration with custom target paths

- Add CopyConfig enum to support both basic and advanced copy modes
- Basic mode: maintains backward compatibility with string[] format
- Advanced mode: supports {from: string, to: string} format for custom paths
- Update copy plugin to handle both configuration formats
- Ensure target directories are created automatically

Example config:
{
  'copy': [
    'public',                              // basic mode
    { 'from': 'assets', 'to': 'static' }  // advanced mode
  ]
}

* fix(copy): prevent path traversal in copy plugin

Add path canonicalization and validation to ensure target paths remain within the destination directory

* chore: Update `copy` config type in Mako bundler

- Updated the type of the `copy` property in the `BuildParams` interface to support both `string` and `{ from: string; to: string }`.
- Ensured the `copy` configuration is properly validated to handle both types.

* docs: Update `copy` config type in documentation

- Updated the `copy` property type in the configuration documentation to reflect the change from `string[]` to `(string | { from: string; to: string })[]`.
- Clarified that the `copy` configuration can now accept both strings and objects with `from` and `to` properties.

* test(copy): add e2e tests for copy plugin from/to pattern

- Update config.copy test fixtures to cover from/to pattern
- Add assertions for copied files in new location
- Adjust copy plugin path validation

* fix(copy): improve path validation and cleanup for copy plugin

- Add directory cleanup when path validation fails
- Use canonicalized paths for more reliable path validation
- Add concatenateModules option type to BuildParams
  • Loading branch information
BQXBQX authored Jan 22, 2025
1 parent 3524214 commit 8bdd985
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 18 deletions.
2 changes: 1 addition & 1 deletion crates/binding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub struct BuildParams {
};
}
>;
copy?: string[];
copy?: (string | { from: string; to: string })[];
codeSplitting?:
| false
| {
Expand Down
9 changes: 8 additions & 1 deletion crates/mako/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,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 {
Expand All @@ -139,7 +146,7 @@ pub struct Config {
pub devtool: Option<DevtoolConfig>,
pub externals: HashMap<String, ExternalConfig>,
pub providers: Providers,
pub copy: Vec<String>,
pub copy: Vec<CopyConfig>,
pub public_path: String,
pub inline_limit: usize,
pub inline_excludes_extensions: Vec<String>,
Expand Down
46 changes: 39 additions & 7 deletions crates/mako/src/plugins/copy.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -62,10 +68,36 @@ impl CopyPlugin {
fn copy(context: &Arc<Context>) -> 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 was_created = if !target.exists() {
fs::create_dir_all(&target).is_ok()
} else {
false
};
let canonical_target = target.canonicalize()?;
let canonical_dest_path = dest.canonicalize()?;
if !canonical_target.starts_with(&canonical_dest_path) {
if was_created {
fs::remove_dir_all(&target)?;
}
return Err(anyhow!("Invalid target path: {:?}", target));
}

debug!("copy {:?} to {:?}", src, target);
copy(&src, &target)?;
}
}
}
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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.
Expand Down
2 changes: 1 addition & 1 deletion docs/config.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ e.g.

### copy

- 类型:`string[]`
- 类型:`(string | { from: string; to: string })[]`
- 默认值:`["public"]`

指定需要复制的文件或目录。默认情况下,会将 `public` 目录下的文件复制到输出目录。
Expand Down
6 changes: 5 additions & 1 deletion e2e/fixtures/config.copy/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
10 changes: 8 additions & 2 deletions e2e/fixtures/config.copy/mako.config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"copy": ["src/assets"]
}
"copy": [
"src/assets",
{
"from": "src/assets",
"to": "assets-from-to"
}
]
}
6 changes: 3 additions & 3 deletions packages/bundler-mako/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/mako/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export interface BuildParams {
};
}
>;
copy?: string[];
copy?: (string | { from: string; to: string })[];
codeSplitting?:
| false
| {
Expand Down

0 comments on commit 8bdd985

Please sign in to comment.