@@ -400,6 +400,71 @@ 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: tsdownConfig` 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 tsdownConfig from './tsdown.config.ts'`
428+ /// 2. The lib config in defineConfig: `lib: tsdownConfig`
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 tsdownConfig 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 JavaScript extensions for TypeScript files (TypeScript module resolution convention)
448+ // .ts → .js, .mts → .mjs, .cts → .cjs
449+ let import_path = if tsdown_config_path. ends_with ( ".mts" ) {
450+ tsdown_config_path. replace ( ".mts" , ".mjs" )
451+ } else if tsdown_config_path. ends_with ( ".cts" ) {
452+ tsdown_config_path. replace ( ".cts" , ".cjs" )
453+ } else if tsdown_config_path. ends_with ( ".ts" ) {
454+ tsdown_config_path. replace ( ".ts" , ".js" )
455+ } else {
456+ tsdown_config_path. to_string ( )
457+ } ;
458+ let content_with_import =
459+ format ! ( "import tsdownConfig from '{import_path}';\n \n {vite_config_content}" ) ;
460+
461+ // Step 2: Add lib: tsdownConfig to defineConfig
462+ let lib_rule = generate_merge_rule ( "tsdownConfig" , "lib" ) ;
463+ let ( final_content, _) = ast_grep:: apply_rules ( & content_with_import, & lib_rule) ?;
464+
465+ Ok ( MergeResult { content : final_content, updated : true , uses_function_callback } )
466+ }
467+
403468#[ cfg( test) ]
404469mod tests {
405470 use std:: io:: Write ;
@@ -1044,4 +1109,200 @@ export default defineConfig({
10441109})"
10451110 ) ;
10461111 }
1112+
1113+ #[ test]
1114+ fn test_merge_tsdown_config_content_simple ( ) {
1115+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1116+
1117+ export default defineConfig({
1118+ plugins: [],
1119+ });"# ;
1120+
1121+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1122+ assert ! ( result. updated) ;
1123+ assert ! ( !result. uses_function_callback) ;
1124+ // TypeScript files use .js extension in imports
1125+ assert_eq ! (
1126+ result. content,
1127+ r#"import tsdownConfig from './tsdown.config.js';
1128+
1129+ import { defineConfig } from '@voidzero-dev/vite-plus';
1130+
1131+ export default defineConfig({
1132+ lib: tsdownConfig,
1133+ plugins: [],
1134+ });"#
1135+ ) ;
1136+ }
1137+
1138+ #[ test]
1139+ fn test_merge_tsdown_config_content_with_existing_imports ( ) {
1140+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1141+ import react from '@vitejs/plugin-react';
1142+
1143+ export default defineConfig({
1144+ plugins: [react()],
1145+ });"# ;
1146+
1147+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1148+ assert ! ( result. updated) ;
1149+ assert ! ( !result. uses_function_callback) ;
1150+ assert_eq ! (
1151+ result. content,
1152+ r#"import tsdownConfig from './tsdown.config.js';
1153+
1154+ import { defineConfig } from '@voidzero-dev/vite-plus';
1155+ import react from '@vitejs/plugin-react';
1156+
1157+ export default defineConfig({
1158+ lib: tsdownConfig,
1159+ plugins: [react()],
1160+ });"#
1161+ ) ;
1162+ }
1163+
1164+ #[ test]
1165+ fn test_merge_tsdown_config_content_function_callback ( ) {
1166+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1167+
1168+ export default defineConfig((env) => ({
1169+ plugins: [],
1170+ }));"# ;
1171+
1172+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1173+ assert ! ( result. updated) ;
1174+ assert ! ( result. uses_function_callback) ;
1175+ assert_eq ! (
1176+ result. content,
1177+ r#"import tsdownConfig from './tsdown.config.js';
1178+
1179+ import { defineConfig } from '@voidzero-dev/vite-plus';
1180+
1181+ export default defineConfig((env) => ({
1182+ lib: tsdownConfig,
1183+ plugins: [],
1184+ }));"#
1185+ ) ;
1186+ }
1187+
1188+ #[ test]
1189+ fn test_merge_tsdown_config_content_idempotent ( ) {
1190+ // Already migrated config - import at the beginning
1191+ let already_migrated = r#"import tsdownConfig from './tsdown.config.js';
1192+
1193+ import { defineConfig } from '@voidzero-dev/vite-plus';
1194+
1195+ export default defineConfig({
1196+ lib: tsdownConfig,
1197+ plugins: [],
1198+ });"# ;
1199+
1200+ let result = merge_tsdown_config_content ( already_migrated, "./tsdown.config.ts" ) . unwrap ( ) ;
1201+ assert ! ( !result. updated, "Should not update already migrated config" ) ;
1202+ assert_eq ! ( result. content, already_migrated) ;
1203+
1204+ // Run migration twice and verify no duplicates
1205+ let fresh_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1206+
1207+ export default defineConfig({
1208+ plugins: [],
1209+ });"# ;
1210+
1211+ let expected_migrated = r#"import tsdownConfig from './tsdown.config.js';
1212+
1213+ import { defineConfig } from '@voidzero-dev/vite-plus';
1214+
1215+ export default defineConfig({
1216+ lib: tsdownConfig,
1217+ plugins: [],
1218+ });"# ;
1219+
1220+ let first_result = merge_tsdown_config_content ( fresh_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1221+ assert ! ( first_result. updated) ;
1222+ assert_eq ! ( first_result. content, expected_migrated) ;
1223+
1224+ // Run again on the result - should return unchanged
1225+ let second_result =
1226+ merge_tsdown_config_content ( & first_result. content , "./tsdown.config.ts" ) . unwrap ( ) ;
1227+ assert ! ( !second_result. updated, "Second migration should not update" ) ;
1228+ assert_eq ! ( second_result. content, expected_migrated) ;
1229+ }
1230+
1231+ #[ test]
1232+ fn test_merge_tsdown_config_content_no_imports ( ) {
1233+ // vite.config.ts without any import statements
1234+ let vite_config = r#"export default {
1235+ server: { port: 3000 }
1236+ }"# ;
1237+
1238+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1239+ assert ! ( result. updated) ;
1240+ assert ! ( !result. uses_function_callback) ;
1241+ assert_eq ! (
1242+ result. content,
1243+ r#"import tsdownConfig from './tsdown.config.js';
1244+
1245+ export default {
1246+ lib: tsdownConfig,
1247+ server: { port: 3000 }
1248+ }"#
1249+ ) ;
1250+ }
1251+
1252+ #[ test]
1253+ fn test_merge_tsdown_config_content_no_false_positive_stdlib ( ) {
1254+ // "stdlib:" should not be detected as "lib:" key
1255+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1256+
1257+ export default defineConfig({
1258+ stdlib: 'some-value',
1259+ });"# ;
1260+
1261+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.ts" ) . unwrap ( ) ;
1262+ assert ! ( result. updated) ;
1263+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.js'" ) ) ;
1264+ assert ! ( result. content. contains( "lib: tsdownConfig" ) ) ;
1265+ assert ! ( result. content. contains( "stdlib: 'some-value'" ) ) ;
1266+ }
1267+
1268+ #[ test]
1269+ fn test_merge_tsdown_config_content_mts_extension ( ) {
1270+ // .mts files should use .mjs extension in imports
1271+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1272+
1273+ export default defineConfig({});"# ;
1274+
1275+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.mts" ) . unwrap ( ) ;
1276+ assert ! ( result. updated) ;
1277+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.mjs'" ) ) ;
1278+ }
1279+
1280+ #[ test]
1281+ fn test_merge_tsdown_config_content_cts_extension ( ) {
1282+ // .cts files should use .cjs extension in imports
1283+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1284+
1285+ export default defineConfig({});"# ;
1286+
1287+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.cts" ) . unwrap ( ) ;
1288+ assert ! ( result. updated) ;
1289+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.cjs'" ) ) ;
1290+ }
1291+
1292+ #[ test]
1293+ fn test_merge_tsdown_config_content_js_extension_unchanged ( ) {
1294+ // .js, .mjs, .cjs files should keep their extensions unchanged
1295+ let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus';
1296+
1297+ export default defineConfig({});"# ;
1298+
1299+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.js" ) . unwrap ( ) ;
1300+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.js'" ) ) ;
1301+
1302+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.mjs" ) . unwrap ( ) ;
1303+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.mjs'" ) ) ;
1304+
1305+ let result = merge_tsdown_config_content ( vite_config, "./tsdown.config.cjs" ) . unwrap ( ) ;
1306+ assert ! ( result. content. contains( "import tsdownConfig from './tsdown.config.cjs'" ) ) ;
1307+ }
10471308}
0 commit comments