svelte-compiler 0.1.1

Core compiler API for the Rust Svelte toolchain
Documentation
use std::collections::BTreeMap;
use std::sync::Arc;

use camino::Utf8PathBuf;
use svelte_compiler::{
    CompileOptions, CssHashGetterCallback, GenerateTarget, MigrateOptions, ModernPrintTarget,
    ParseOptions, PreprocessAttributeValue, PreprocessOptions, PreprocessOutput, PreprocessorGroup,
    PrintOptions, SourceMap, VERSION, WarningFilterCallback, compile, compile_module, migrate,
    parse, preprocess, print, print_modern, walk,
};

#[test]
fn preprocess_passthrough_without_custom_steps() {
    let source = include_str!("fixtures/api/preprocess_input.svelte");
    let result =
        preprocess(source, PreprocessOptions::default()).expect("preprocess should succeed");

    assert_eq!(result.code.as_ref(), source);
    assert!(result.dependencies.is_empty());
    assert!(result.map.is_none());
}

#[test]
fn preprocess_runs_markup_and_tag_steps() {
    let source = "<h1>Hello __NAME__!</h1>\n<style color=\"red\"/>\n";
    let result = preprocess(
        source,
        PreprocessOptions {
            filename: Some(Utf8PathBuf::from("file.svelte")),
            groups: vec![PreprocessorGroup {
                markup: Some(Arc::new(|markup| {
                    Ok(Some(PreprocessOutput {
                        code: Arc::from(markup.content.replace("__NAME__", "world")),
                        ..PreprocessOutput::default()
                    }))
                })),
                style: Some(Arc::new(|style| {
                    let color = match style.attributes.get("color") {
                        Some(PreprocessAttributeValue::String(value)) => value.as_ref(),
                        _ => "",
                    };
                    Ok(Some(PreprocessOutput {
                        code: Arc::from(format!("div {{ color: {color}; }}")),
                        ..PreprocessOutput::default()
                    }))
                })),
                ..PreprocessorGroup::default()
            }]
            .into_boxed_slice(),
        },
    )
    .expect("preprocess should succeed");

    assert_eq!(
        result.code.as_ref(),
        "<h1>Hello world!</h1>\n<style color=\"red\">div { color: red; }</style>\n"
    );
}

#[test]
fn preprocess_collects_dependencies() {
    let source = "<style>\n\t@import './foo.css';\n</style>\n";
    let result = preprocess(
        source,
        PreprocessOptions {
            groups: vec![PreprocessorGroup {
                style: Some(Arc::new(|style| {
                    Ok(Some(PreprocessOutput {
                        code: Arc::from(
                            style
                                .content
                                .replace("@import './foo.css';", "/* removed */"),
                        ),
                        dependencies: vec![Utf8PathBuf::from("./foo.css")].into_boxed_slice(),
                        ..PreprocessOutput::default()
                    }))
                })),
                ..PreprocessorGroup::default()
            }]
            .into_boxed_slice(),
            ..PreprocessOptions::default()
        },
    )
    .expect("preprocess should succeed");

    assert_eq!(result.code.as_ref(), "<style>\n\t/* removed */\n</style>\n");
    assert_eq!(
        result.dependencies.as_ref(),
        &[Utf8PathBuf::from("./foo.css")]
    );
}

#[test]
fn preprocess_runs_async_tag_steps() {
    let source = "<style>\n\t.brand-color { color: $brand; }\n</style>\n";
    let result = preprocess(
        source,
        PreprocessOptions {
            groups: vec![PreprocessorGroup {
                style_async: Some(Arc::new(|style| {
                    Box::pin(async move {
                        Ok(Some(PreprocessOutput {
                            code: Arc::from(style.content.replace("$brand", "purple")),
                            ..PreprocessOutput::default()
                        }))
                    })
                })),
                ..PreprocessorGroup::default()
            }]
            .into_boxed_slice(),
            ..PreprocessOptions::default()
        },
    )
    .expect("preprocess should succeed");

    assert_eq!(
        result.code.as_ref(),
        "<style>\n\t.brand-color { color: purple; }\n</style>\n"
    );
}

#[test]
fn migrate_api_is_exposed() {
    let source = include_str!("fixtures/api/preprocess_input.svelte");
    let result = migrate(source, MigrateOptions::default()).expect("migrate should succeed");
    let normalized = result.code.replace("\r\n", "\n");

    assert_eq!(
        normalized.as_str(),
        "<script>\n  /**\n   * @typedef {Object} Props\n   * @property {string} [name]\n   */\n\n  /** @type {Props} */\n  let { name = \"world\" } = $props();\n</script>\n\n<h1>Hello {name}</h1>\n"
    );
}

#[test]
fn compile_component_has_no_js_map_without_request() {
    let result = compile("<h1>Hello</h1>", CompileOptions::default()).expect("compile succeeds");
    assert!(result.js.map.is_none());
    assert!(!result.metadata.runes);
    assert!(result.ast.is_some());
}

#[test]
fn compile_component_preserves_requested_js_map_slot() {
    let result = compile(
        "<h1>Hello</h1>",
        CompileOptions {
            filename: Some(Utf8PathBuf::from("input.svelte")),
            output_filename: Some(Utf8PathBuf::from("_output/client/input.svelte.js")),
            sourcemap: Some(SourceMap::default()),
            ..CompileOptions::default()
        },
    )
    .expect("compile succeeds");
    let map = result.js.map.expect("requested component sourcemap");
    assert_eq!(map.sources.as_ref(), &[Arc::from("../../input.svelte")]);
    assert!(!map.mappings.is_empty());
}

#[test]
fn compile_component_emits_css_sourcemap_when_requested() {
    let result = compile(
        "<style>.foo { color: red; }</style><div class=\"foo\"></div>",
        CompileOptions {
            filename: Some(Utf8PathBuf::from("input.svelte")),
            css_hash: Some(Arc::from("svelte-abc123")),
            css_output_filename: Some(Utf8PathBuf::from("_output/client/input.svelte.css")),
            sourcemap: Some(SourceMap::default()),
            ..CompileOptions::default()
        },
    )
    .expect("compile succeeds");

    let css = result.css.expect("css output");
    let map = css.map.expect("requested css sourcemap");
    assert_eq!(map.sources.as_ref(), &[Arc::from("../../input.svelte")]);
    assert!(!map.mappings.is_empty());
}

#[test]
fn compile_module_has_no_js_map_without_request() {
    let result = compile_module(
        "export const answer = 42;",
        CompileOptions {
            generate: GenerateTarget::Client,
            ..CompileOptions::default()
        },
    )
    .expect("compile module succeeds");
    assert!(result.js.map.is_none());
    assert!(result.metadata.runes);
    assert!(result.ast.is_none());
}

#[test]
fn compile_component_scopes_css_by_default() {
    let result = compile(
        "<style>.foo { color: red; }</style><div class=\"foo\"></div>",
        CompileOptions::default(),
    )
    .expect("compile succeeds");

    let css = result.css.expect("css output");
    assert!(css.code.contains(".foo.svelte-"));
}

#[test]
fn compile_component_uses_custom_css_hash_getter() {
    let result = compile(
        "<style>.foo { color: red; }</style><div class=\"foo\"></div>",
        CompileOptions {
            css_hash_getter: Some(CssHashGetterCallback::new(|input| {
                Arc::from(format!("custom-{}", (input.hash)(input.filename)))
            })),
            ..CompileOptions::default()
        },
    )
    .expect("compile succeeds");

    let css = result.css.expect("css output");
    assert!(css.code.contains(".foo.custom-"));
}

#[test]
fn compile_component_applies_warning_filter_callback() {
    let result = compile(
        "<svelte:component this={Thing} />",
        CompileOptions {
            generate: GenerateTarget::None,
            runes: Some(true),
            warning_filter: Some(WarningFilterCallback::new(|warning| {
                warning.code.as_ref() != "svelte_component_deprecated"
            })),
            ..CompileOptions::default()
        },
    )
    .expect("compile succeeds");

    assert!(result.warnings.is_empty());
}

#[test]
fn print_returns_sourcemap() {
    let ast = parse("<h1>Hello</h1>", ParseOptions::default()).expect("parse succeeds");
    let printed = print(&ast, PrintOptions::default()).expect("print succeeds");
    assert_eq!(printed.code.as_ref(), "<h1>Hello</h1>");
    assert_eq!(printed.map.sources.as_ref(), &[Arc::from("input.svelte")]);
    assert!(!printed.map.mappings.is_empty());
}

#[test]
fn parse_accepts_package_style_modern_options() {
    let ast = parse(
        "<h1>Hello</h1>",
        svelte_compiler::ParseOptions {
            filename: Some(Utf8PathBuf::from("Component.svelte")),
            root_dir: Some(Utf8PathBuf::from("src")),
            modern: Some(true),
            loose: false,
            ..Default::default()
        },
    )
    .expect("parse succeeds");

    assert!(matches!(ast.root, svelte_compiler::ast::Root::Modern(_)));
}

#[test]
fn print_modern_accepts_source_backed_subnodes() {
    let source = "<div><h1>Hello</h1></div>";
    let ast = parse(
        source,
        svelte_compiler::ParseOptions {
            modern: Some(true),
            ..Default::default()
        },
    )
    .expect("parse succeeds");
    let svelte_compiler::ast::Root::Modern(root) = &ast.root else {
        panic!("expected modern root");
    };
    let node = root.fragment.nodes.first().expect("top-level node");

    let printed = print_modern(
        ModernPrintTarget::node(source, node),
        PrintOptions::default(),
    )
    .expect("print modern succeeds");

    assert_eq!(printed.code.as_ref(), "<div><h1>Hello</h1></div>");
    assert_eq!(printed.map.sources.as_ref(), &[Arc::from("input.svelte")]);
    assert!(!printed.map.mappings.is_empty());
}

#[test]
fn print_modern_script_uses_comment_hook_callbacks() {
    let source = "<script>const answer = 42;</script>";
    let ast = parse(
        source,
        svelte_compiler::ParseOptions {
            modern: Some(true),
            ..Default::default()
        },
    )
    .expect("parse succeeds");
    let svelte_compiler::ast::Root::Modern(root) = &ast.root else {
        panic!("expected modern root");
    };
    let script = root.instance.as_ref().expect("instance script");

    let comment = || {
        let mut fields = BTreeMap::new();
        fields.insert(
            "type".to_string(),
            svelte_compiler::ast::modern::EstreeValue::String(Arc::from("Line")),
        );
        fields.insert(
            "value".to_string(),
            svelte_compiler::ast::modern::EstreeValue::String(Arc::from(" injected")),
        );
        fields.insert(
            "start".to_string(),
            svelte_compiler::ast::modern::EstreeValue::UInt(0),
        );
        fields.insert(
            "end".to_string(),
            svelte_compiler::ast::modern::EstreeValue::UInt(0),
        );
        svelte_compiler::ast::modern::EstreeNode { fields }
    };

    let printed = print_modern(
        ModernPrintTarget::script(source, script),
        PrintOptions {
            get_leading_comments: Some(svelte_compiler::PrintCommentGetterCallback::new(
                move |_| vec![comment()].into_boxed_slice(),
            )),
            ..Default::default()
        },
    )
    .expect("print modern succeeds");

    assert!(printed.code.contains("// injected"));
    assert!(printed.code.contains("const answer = 42;"));
}

#[test]
fn compiler_version_matches_svelte_package() {
    assert_eq!(VERSION, "5.53.9");
}

#[test]
#[should_panic(
    expected = "'svelte/compiler' no longer exports a `walk` utility — please import it directly from `estree-walker` instead"
)]
fn deprecated_walk_panics_with_upstream_message() {
    walk();
}