Skip to content

Commit 15d194b

Browse files
fengmk2claude
andcommitted
feat(migration): auto-merge tsdown config into vite.config.ts
Add functionality to automatically import tsdown.config.ts and add `lib: libConfig` to the vite defineConfig when migrating. - Add merge_tsdown_config function in Rust - Reuse generate_merge_rule for adding lib config (consistent with lint/fmt) - Add NAPI binding for mergeTsdownConfig - Update migrator to call mergeTsdownConfigFile instead of notifyTsdownConfig - Create vite.config.ts if it doesn't exist before merging tsdown config - Support vite.config.ts files without any import statements - Use ast-grep for consistent import addition (add_lib_config_import) - Always show documentation link for manual tsdown config merging - Add idempotency checks to prevent duplicates when running migration multiple times - Add unit tests with complete content comparison for tsdown config merging Note: ast-grep patterns match the call expression without the trailing semicolon. A code formatter (e.g., `vite fmt`) can add semicolons back. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1d90b46 commit 15d194b

15 files changed

Lines changed: 572 additions & 31 deletions

File tree

crates/vite_migration/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ mod vite_config;
77
pub use file_walker::{WalkResult, find_ts_files};
88
pub use import_rewriter::{BatchRewriteResult, rewrite_imports_in_directory};
99
pub use package::rewrite_scripts;
10-
pub use vite_config::{MergeResult, merge_json_config};
10+
pub use vite_config::{MergeResult, merge_json_config, merge_tsdown_config};

crates/vite_migration/src/vite_config.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,66 @@ fn escape_single_quotes(s: &str) -> String {
400400
s.replace('\\', "\\\\").replace('\'', "\\'")
401401
}
402402

403+
/// Merge tsdown config into vite.config.ts by importing it
404+
///
405+
/// This function adds an import statement for the tsdown config file
406+
/// and adds `lib: libConfig` to the defineConfig.
407+
///
408+
/// # Arguments
409+
///
410+
/// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file
411+
/// * `tsdown_config_path` - Path to the tsdown.config.ts file (relative path like "./tsdown.config.ts")
412+
///
413+
/// # Returns
414+
///
415+
/// Returns a `MergeResult` with the updated content
416+
pub fn merge_tsdown_config(
417+
vite_config_path: &Path,
418+
tsdown_config_path: &str,
419+
) -> Result<MergeResult, Error> {
420+
let vite_config_content = std::fs::read_to_string(vite_config_path)?;
421+
merge_tsdown_config_content(&vite_config_content, tsdown_config_path)
422+
}
423+
424+
/// Merge tsdown config into vite config content
425+
///
426+
/// This adds:
427+
/// 1. An import statement: `import libConfig from './tsdown.config.ts'`
428+
/// 2. The lib config in defineConfig: `lib: libConfig`
429+
///
430+
/// This function is idempotent - running it multiple times will not create duplicates.
431+
fn merge_tsdown_config_content(
432+
vite_config_content: &str,
433+
tsdown_config_path: &str,
434+
) -> Result<MergeResult, Error> {
435+
let uses_function_callback = check_function_callback(vite_config_content)?;
436+
437+
// Check if already migrated (idempotency check)
438+
if vite_config_content.contains("import libConfig from") {
439+
return Ok(MergeResult {
440+
content: vite_config_content.to_string(),
441+
updated: false,
442+
uses_function_callback,
443+
});
444+
}
445+
446+
// Step 1: Add import statement at the beginning
447+
// Use .js extension for TypeScript files (TypeScript convention)
448+
let import_path = if tsdown_config_path.ends_with(".ts") {
449+
tsdown_config_path.replace(".ts", ".js")
450+
} else {
451+
tsdown_config_path.to_string()
452+
};
453+
let content_with_import =
454+
format!("import libConfig from '{import_path}';\n\n{vite_config_content}");
455+
456+
// Step 2: Add lib: libConfig to defineConfig
457+
let lib_rule = generate_merge_rule("libConfig", "lib");
458+
let (final_content, _) = ast_grep::apply_rules(&content_with_import, &lib_rule)?;
459+
460+
Ok(MergeResult { content: final_content, updated: true, uses_function_callback })
461+
}
462+
403463
#[cfg(test)]
404464
mod tests {
405465
use std::io::Write;
@@ -1044,4 +1104,159 @@ export default defineConfig({
10441104
})"
10451105
);
10461106
}
1107+
1108+
#[test]
1109+
fn test_merge_tsdown_config_content_simple() {
1110+
let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1111+
1112+
export default defineConfig({
1113+
plugins: [],
1114+
});"#;
1115+
1116+
let result = merge_tsdown_config_content(vite_config, "./tsdown.config.ts").unwrap();
1117+
assert!(result.updated);
1118+
assert!(!result.uses_function_callback);
1119+
// TypeScript files use .js extension in imports
1120+
assert_eq!(
1121+
result.content,
1122+
r#"import libConfig from './tsdown.config.js';
1123+
1124+
import { defineConfig } from '@voidzero-dev/vite-plus';
1125+
1126+
export default defineConfig({
1127+
lib: libConfig,
1128+
plugins: [],
1129+
});"#
1130+
);
1131+
}
1132+
1133+
#[test]
1134+
fn test_merge_tsdown_config_content_with_existing_imports() {
1135+
let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1136+
import react from '@vitejs/plugin-react';
1137+
1138+
export default defineConfig({
1139+
plugins: [react()],
1140+
});"#;
1141+
1142+
let result = merge_tsdown_config_content(vite_config, "./tsdown.config.ts").unwrap();
1143+
assert!(result.updated);
1144+
assert!(!result.uses_function_callback);
1145+
assert_eq!(
1146+
result.content,
1147+
r#"import libConfig from './tsdown.config.js';
1148+
1149+
import { defineConfig } from '@voidzero-dev/vite-plus';
1150+
import react from '@vitejs/plugin-react';
1151+
1152+
export default defineConfig({
1153+
lib: libConfig,
1154+
plugins: [react()],
1155+
});"#
1156+
);
1157+
}
1158+
1159+
#[test]
1160+
fn test_merge_tsdown_config_content_function_callback() {
1161+
let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1162+
1163+
export default defineConfig((env) => ({
1164+
plugins: [],
1165+
}));"#;
1166+
1167+
let result = merge_tsdown_config_content(vite_config, "./tsdown.config.ts").unwrap();
1168+
assert!(result.updated);
1169+
assert!(result.uses_function_callback);
1170+
assert_eq!(
1171+
result.content,
1172+
r#"import libConfig from './tsdown.config.js';
1173+
1174+
import { defineConfig } from '@voidzero-dev/vite-plus';
1175+
1176+
export default defineConfig((env) => ({
1177+
lib: libConfig,
1178+
plugins: [],
1179+
}));"#
1180+
);
1181+
}
1182+
1183+
#[test]
1184+
fn test_merge_tsdown_config_content_idempotent() {
1185+
// Already migrated config - import at the beginning
1186+
let already_migrated = r#"import libConfig from './tsdown.config.js';
1187+
1188+
import { defineConfig } from '@voidzero-dev/vite-plus';
1189+
1190+
export default defineConfig({
1191+
lib: libConfig,
1192+
plugins: [],
1193+
});"#;
1194+
1195+
let result = merge_tsdown_config_content(already_migrated, "./tsdown.config.ts").unwrap();
1196+
assert!(!result.updated, "Should not update already migrated config");
1197+
assert_eq!(result.content, already_migrated);
1198+
1199+
// Run migration twice and verify no duplicates
1200+
let fresh_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1201+
1202+
export default defineConfig({
1203+
plugins: [],
1204+
});"#;
1205+
1206+
let expected_migrated = r#"import libConfig from './tsdown.config.js';
1207+
1208+
import { defineConfig } from '@voidzero-dev/vite-plus';
1209+
1210+
export default defineConfig({
1211+
lib: libConfig,
1212+
plugins: [],
1213+
});"#;
1214+
1215+
let first_result = merge_tsdown_config_content(fresh_config, "./tsdown.config.ts").unwrap();
1216+
assert!(first_result.updated);
1217+
assert_eq!(first_result.content, expected_migrated);
1218+
1219+
// Run again on the result - should return unchanged
1220+
let second_result =
1221+
merge_tsdown_config_content(&first_result.content, "./tsdown.config.ts").unwrap();
1222+
assert!(!second_result.updated, "Second migration should not update");
1223+
assert_eq!(second_result.content, expected_migrated);
1224+
}
1225+
1226+
#[test]
1227+
fn test_merge_tsdown_config_content_no_imports() {
1228+
// vite.config.ts without any import statements
1229+
let vite_config = r#"export default {
1230+
server: { port: 3000 }
1231+
}"#;
1232+
1233+
let result = merge_tsdown_config_content(vite_config, "./tsdown.config.ts").unwrap();
1234+
assert!(result.updated);
1235+
assert!(!result.uses_function_callback);
1236+
assert_eq!(
1237+
result.content,
1238+
r#"import libConfig from './tsdown.config.js';
1239+
1240+
export default {
1241+
lib: libConfig,
1242+
server: { port: 3000 }
1243+
}"#
1244+
);
1245+
}
1246+
1247+
#[test]
1248+
fn test_merge_tsdown_config_content_no_false_positive_stdlib() {
1249+
// "stdlib:" should not be detected as "lib:" key
1250+
let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1251+
1252+
export default defineConfig({
1253+
stdlib: 'some-value',
1254+
});"#;
1255+
1256+
let result = merge_tsdown_config_content(vite_config, "./tsdown.config.ts").unwrap();
1257+
assert!(result.updated);
1258+
assert!(result.content.contains("import libConfig from './tsdown.config.js'"));
1259+
assert!(result.content.contains("lib: libConfig"));
1260+
assert!(result.content.contains("stdlib: 'some-value'"));
1261+
}
10471262
}

packages/global/binding/index.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,38 @@ export interface MergeJsonConfigResult {
159159
usesFunctionCallback: boolean;
160160
}
161161

162+
/**
163+
* Merge tsdown config into vite config by importing it
164+
*
165+
* This function adds an import statement for the tsdown config file
166+
* and adds `lib: libConfig` to the defineConfig.
167+
*
168+
* # Arguments
169+
*
170+
* * `vite_config_path` - Path to the vite.config.ts or vite.config.js file
171+
* * `tsdown_config_path` - Relative path to the tsdown.config.ts file (e.g., "./tsdown.config.ts")
172+
*
173+
* # Returns
174+
*
175+
* Returns a `MergeJsonConfigResult` containing:
176+
* - `content`: The updated vite config content
177+
* - `updated`: Whether any changes were made
178+
* - `usesFunctionCallback`: Whether the config uses a function callback
179+
*
180+
* # Example
181+
*
182+
* ```javascript
183+
* const result = mergeTsdownConfig('vite.config.ts', './tsdown.config.ts');
184+
* if (result.updated) {
185+
* fs.writeFileSync('vite.config.ts', result.content);
186+
* }
187+
* ```
188+
*/
189+
export declare function mergeTsdownConfig(
190+
viteConfigPath: string,
191+
tsdownConfigPath: string,
192+
): MergeJsonConfigResult;
193+
162194
/** Access modes for a path. */
163195
export interface PathAccess {
164196
/** Whether the path was read */

packages/global/binding/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ const {
766766
detectWorkspace,
767767
downloadPackageManager,
768768
mergeJsonConfig,
769+
mergeTsdownConfig,
769770
rewriteImportsInDirectory,
770771
rewriteScripts,
771772
run,
@@ -774,6 +775,7 @@ const {
774775
export { detectWorkspace };
775776
export { downloadPackageManager };
776777
export { mergeJsonConfig };
778+
export { mergeTsdownConfig };
777779
export { rewriteImportsInDirectory };
778780
export { rewriteScripts };
779781
export { run };

packages/global/binding/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use vite_path::current_dir;
1515

1616
use crate::cli::Args;
1717
pub use crate::{
18-
migration::{merge_json_config, rewrite_imports_in_directory, rewrite_scripts},
18+
migration::{
19+
merge_json_config, merge_tsdown_config, rewrite_imports_in_directory, rewrite_scripts,
20+
},
1921
package_manager::{detect_workspace, download_package_manager},
2022
};
2123

packages/global/binding/src/migration.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,47 @@ pub struct BatchRewriteResult {
102102
pub errors: Vec<BatchRewriteError>,
103103
}
104104

105+
/// Merge tsdown config into vite config by importing it
106+
///
107+
/// This function adds an import statement for the tsdown config file
108+
/// and adds `lib: libConfig` to the defineConfig.
109+
///
110+
/// # Arguments
111+
///
112+
/// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file
113+
/// * `tsdown_config_path` - Relative path to the tsdown.config.ts file (e.g., "./tsdown.config.ts")
114+
///
115+
/// # Returns
116+
///
117+
/// Returns a `MergeJsonConfigResult` containing:
118+
/// - `content`: The updated vite config content
119+
/// - `updated`: Whether any changes were made
120+
/// - `usesFunctionCallback`: Whether the config uses a function callback
121+
///
122+
/// # Example
123+
///
124+
/// ```javascript
125+
/// const result = mergeTsdownConfig('vite.config.ts', './tsdown.config.ts');
126+
/// if (result.updated) {
127+
/// fs.writeFileSync('vite.config.ts', result.content);
128+
/// }
129+
/// ```
130+
#[napi]
131+
pub fn merge_tsdown_config(
132+
vite_config_path: String,
133+
tsdown_config_path: String,
134+
) -> Result<MergeJsonConfigResult> {
135+
let result =
136+
vite_migration::merge_tsdown_config(Path::new(&vite_config_path), &tsdown_config_path)
137+
.map_err(anyhow::Error::from)?;
138+
139+
Ok(MergeJsonConfigResult {
140+
content: result.content,
141+
updated: result.updated,
142+
uses_function_callback: result.uses_function_callback,
143+
})
144+
}
145+
105146
/// Rewrite imports in all TypeScript/JavaScript files under a directory
106147
///
107148
/// This function finds all TypeScript and JavaScript files in the specified directory
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "migration-from-tsdown-json-config",
3+
"devDependencies": {
4+
"vite": "^7.0.0",
5+
"tsdown": "^0.5.0"
6+
},
7+
"scripts": {
8+
"build": "tsdown",
9+
"build:watch": "tsdown --watch",
10+
"build:dts": "tsdown --dts"
11+
}
12+
}

0 commit comments

Comments
 (0)