fob-bundler 0.5.0

Fob bundler - Rolldown-based bundling on top of fob foundation
Documentation
//! Plugin invocation verification tests
//!
//! These tests ensure that plugins are actually invoked when processing
//! their respective file types. This catches regressions where plugins
//! are registered but not called.

mod helpers;

#[cfg(not(target_family = "wasm"))]
mod plugin_invocation_tests {
    use super::helpers::*;
    use fob_bundler::runtime::BundlerRuntime;
    use fob_bundler::{BuildOptions, Platform, Runtime};
    use fob_plugin_css::FobCssPlugin;
    use fob_plugin_mdx::FobMdxPlugin;
    use std::sync::Arc;
    use tempfile::TempDir;

    /// Verify MDX plugin is invoked for .mdx files
    ///
    /// This is the critical regression test - if MDX plugin isn't invoked,
    /// the bundler will try to parse raw MDX as JavaScript and fail.
    #[tokio::test]
    async fn test_mdx_plugin_is_invoked() {
        let temp_dir = TempDir::new().expect("create temp dir");

        // MDX content that would fail if parsed as raw JS
        std::fs::write(
            temp_dir.path().join("content.mdx"),
            r#"---
title: "Plugin Invocation Test"
---

# Heading That Would Be Syntax Error in JS

This **markdown** would fail if not processed by MDX plugin.

```js
const code = "block";
```
"#,
        )
        .expect("write mdx file");

        std::fs::write(
            temp_dir.path().join("entry.tsx"),
            "import Content from './content.mdx';\nexport { Content };",
        )
        .expect("write entry");

        let runtime: Arc<dyn Runtime> = Arc::new(BundlerRuntime::new(temp_dir.path()));

        let result = BuildOptions::new(temp_dir.path().join("entry.tsx"))
            .externalize_from("package.json")
            .platform(Platform::Node)
            .cwd(temp_dir.path())
            .runtime(Arc::clone(&runtime))
            .plugin(Arc::new(FobMdxPlugin::new(runtime)))
            .build()
            .await
            .expect("MDX plugin should process .mdx file");

        // Verify MDX was compiled to JSX
        let chunk = result.chunks().next().expect("should have chunk");

        // Content should be present (MDX processed it)
        assert!(
            chunk.code.contains("Plugin Invocation Test")
                || chunk.code.contains("Heading That Would Be Syntax Error"),
            "MDX content should be in output"
        );

        // Raw markdown syntax should NOT be present
        assert!(
            !chunk.code.contains("# Heading"),
            "Raw markdown heading syntax should be transformed"
        );

        // STRONGER ASSERTIONS: Verify JSX compilation happened
        assert!(
            chunk.code.contains("jsx")
                || chunk.code.contains("jsxs")
                || chunk.code.contains("createElement"),
            "Output should contain JSX runtime calls - proves MDX→JSX compilation"
        );

        // Verify MDXContent function was generated
        assert!(
            chunk.code.contains("MDXContent") || chunk.code.contains("function"),
            "Output should contain MDXContent component function"
        );

        // Verify component structure (props handling)
        assert!(
            chunk.code.contains("components") || chunk.code.contains("_components"),
            "Output should have component mapping for MDX elements"
        );
    }

    /// Verify CSS plugin is invoked for .css files
    #[tokio::test]
    async fn test_css_plugin_is_invoked() {
        let runtime: Arc<dyn Runtime> = Arc::new(BundlerRuntime::new("."));

        // CSS content with modern features
        let css_content = r#"
.container {
    display: flex;
    gap: 1rem;
    --custom-prop: #3b82f6;
}

.btn {
    background: var(--custom-prop);
    padding: 0.5rem 1rem;
}
"#;

        // Use virtual: prefix consistently for imports
        let result = BuildOptions::new("virtual:entry.js")
            .externalize_from("package.json")
            .platform(Platform::Node)
            .virtual_file(
                "virtual:entry.js",
                "import 'virtual:styles.css';\nexport const loaded = true;",
            )
            .virtual_file("virtual:styles.css", css_content)
            .runtime(Arc::clone(&runtime))
            .plugin(Arc::new(FobCssPlugin::new(runtime)))
            .build()
            .await
            .expect("CSS plugin should process .css file");

        // Build should succeed (CSS was handled)
        let bundle = result.output.as_single().expect("single bundle");
        assert!(!bundle.assets.is_empty(), "Should produce assets");
    }

    /// Verify that MDX files fail gracefully without the plugin
    ///
    /// This documents the expected behavior when MDX plugin is missing.
    /// The bundler should either:
    /// 1. Fail with a clear error about unhandled file type, OR
    /// 2. Fail with a parse error (since MDX isn't valid JS)
    #[tokio::test]
    async fn test_mdx_without_plugin_behavior() {
        let temp_dir = TempDir::new().expect("create temp dir");

        std::fs::write(
            temp_dir.path().join("content.mdx"),
            "# Heading\n\nSome **content**.",
        )
        .expect("write mdx file");

        std::fs::write(
            temp_dir.path().join("entry.tsx"),
            "import Content from './content.mdx';\nexport { Content };",
        )
        .expect("write entry");

        let result = BuildOptions::new(temp_dir.path().join("entry.tsx"))
            .externalize_from("package.json")
            .platform(Platform::Node)
            .cwd(temp_dir.path())
            // NOTE: NO MDX plugin registered!
            .build()
            .await;

        // Should fail - MDX is not valid JavaScript
        // The exact error depends on how Rolldown handles unrecognized extensions
        match result {
            Ok(_) => {
                // If it somehow succeeds, the MDX wasn't imported (tree-shaken or ignored)
                println!("Warning: Build succeeded without MDX plugin - verify MDX import is used");
            }
            Err(e) => {
                // Expected: should fail with parse error or unhandled extension
                let err_msg = e.to_string();
                assert!(
                    err_msg.contains("parse")
                        || err_msg.contains("syntax")
                        || err_msg.contains("mdx")
                        || err_msg.contains("Cannot assign")
                        || err_msg.contains("unresolved"),
                    "Error should indicate MDX parsing failed: {}",
                    err_msg
                );
            }
        }
    }

    /// Verify multiple plugins work together
    #[tokio::test]
    async fn test_multiple_plugins_invoked() {
        let temp_dir = TempDir::new().expect("create temp dir");

        // Create MDX file (without CSS import to avoid path resolution complexity)
        std::fs::write(
            temp_dir.path().join("content.mdx"),
            r#"# Multi-Plugin Test

This MDX file tests multiple plugin coexistence.

Some **content** here.
"#,
        )
        .expect("write mdx file");

        // Create CSS file
        std::fs::write(
            temp_dir.path().join("styles.css"),
            ".content { color: blue; }",
        )
        .expect("write css file");

        // Create entry that imports both MDX and CSS
        std::fs::write(
            temp_dir.path().join("entry.tsx"),
            "import Content from './content.mdx';\nimport './styles.css';\nexport { Content };",
        )
        .expect("write entry");

        let runtime: Arc<dyn Runtime> = Arc::new(BundlerRuntime::new(temp_dir.path()));

        let result = BuildOptions::new(temp_dir.path().join("entry.tsx"))
            .externalize_from("package.json")
            .platform(Platform::Node)
            .cwd(temp_dir.path())
            .runtime(Arc::clone(&runtime))
            // Register both plugins
            .plugin(Arc::new(FobMdxPlugin::new(Arc::clone(&runtime))))
            .plugin(Arc::new(FobCssPlugin::new(runtime)))
            .build()
            .await
            .expect("Multiple plugins should work together");

        // Verify both plugins processed their files
        assert_has_assets(&result);
        assert_chunk_contains(&result, "Multi-Plugin Test");

        // STRONGER ASSERTIONS: Verify MDX was properly compiled
        let chunk = result.chunks().next().expect("should have chunk");

        // MDX plugin should have compiled markdown to JSX
        assert!(
            chunk.code.contains("jsx") || chunk.code.contains("jsxs"),
            "MDX should be compiled to JSX runtime calls"
        );

        // Raw markdown should not be present
        assert!(
            !chunk.code.contains("# Multi-Plugin Test"),
            "Raw markdown heading should be transformed"
        );
        assert!(
            !chunk.code.contains("**content**"),
            "Raw markdown bold should be transformed"
        );
    }

    /// Verify plugin order doesn't break functionality
    #[tokio::test]
    async fn test_plugin_order_independence() {
        let temp_dir = TempDir::new().expect("create temp dir");

        std::fs::write(
            temp_dir.path().join("content.mdx"),
            "# Plugin Order Test\n\nContent here.",
        )
        .expect("write mdx file");

        std::fs::write(
            temp_dir.path().join("entry.tsx"),
            "import Content from './content.mdx';\nexport { Content };",
        )
        .expect("write entry");

        let runtime1: Arc<dyn Runtime> = Arc::new(BundlerRuntime::new(temp_dir.path()));

        // Test with CSS before MDX
        let result1 = BuildOptions::new(temp_dir.path().join("entry.tsx"))
            .platform(Platform::Node)
            .cwd(temp_dir.path())
            .runtime(Arc::clone(&runtime1))
            .plugin(Arc::new(FobCssPlugin::new(Arc::clone(&runtime1))))
            .plugin(Arc::new(FobMdxPlugin::new(runtime1)))
            .build()
            .await
            .expect("CSS before MDX should work");

        let runtime2: Arc<dyn Runtime> = Arc::new(BundlerRuntime::new(temp_dir.path()));

        // Test with MDX before CSS
        let result2 = BuildOptions::new(temp_dir.path().join("entry.tsx"))
            .platform(Platform::Node)
            .cwd(temp_dir.path())
            .runtime(Arc::clone(&runtime2))
            .plugin(Arc::new(FobMdxPlugin::new(Arc::clone(&runtime2))))
            .plugin(Arc::new(FobCssPlugin::new(runtime2)))
            .build()
            .await
            .expect("MDX before CSS should work");

        // Both should produce output with the same content
        assert_chunk_contains(&result1, "Plugin Order Test");
        assert_chunk_contains(&result2, "Plugin Order Test");

        // STRONGER ASSERTIONS: Both orders should produce valid JSX
        let chunk1 = result1.chunks().next().expect("should have chunk");
        let chunk2 = result2.chunks().next().expect("should have chunk");

        // Both should have JSX runtime calls
        assert!(
            chunk1.code.contains("jsx") || chunk1.code.contains("jsxs"),
            "CSS-first order should compile MDX to JSX"
        );
        assert!(
            chunk2.code.contains("jsx") || chunk2.code.contains("jsxs"),
            "MDX-first order should compile MDX to JSX"
        );

        // Both should NOT have raw markdown
        assert!(
            !chunk1.code.contains("# Plugin Order Test"),
            "CSS-first: raw markdown should be transformed"
        );
        assert!(
            !chunk2.code.contains("# Plugin Order Test"),
            "MDX-first: raw markdown should be transformed"
        );
    }
}