svelte-compiler 0.1.0

Core compiler API for the Rust Svelte toolchain
Documentation
use oxc_allocator::Allocator;
use oxc_ast::ast::Statement;
use oxc_parser::Parser as OxcParser;
use oxc_span::SourceType as OxcSourceType;
use std::collections::BTreeMap;

use crate::api::modern::{
    RawField, attach_estree_comments_to_tree, estree_node_field, normalize_estree_node,
    parse_all_comment_nodes, parse_leading_comment_nodes, position_raw_node,
};
use crate::ast::modern::{EstreeNode, EstreeValue};

pub(crate) struct OxcProgramOffsets {
    pub global_start: usize,
    pub start_line: usize,
    pub start_column: usize,
    pub end_line: usize,
    pub end_column: usize,
}

impl OxcProgramOffsets {
    pub(crate) fn for_root_source(source_len: usize) -> Self {
        Self {
            global_start: 0,
            start_line: 1,
            start_column: 0,
            end_line: 1,
            end_column: source_len,
        }
    }
}

pub(crate) struct SvelteOxcParser<'src> {
    source: &'src str,
    offsets: OxcProgramOffsets,
    is_ts: bool,
}

struct ParsedRawNode {
    node: EstreeNode,
}

fn program_body_is_empty(program: &EstreeNode) -> bool {
    match estree_node_field(program, RawField::Body) {
        Some(EstreeValue::Array(items)) => items.is_empty(),
        _ => false,
    }
}

impl<'src> SvelteOxcParser<'src> {
    pub(crate) fn new(source: &'src str) -> Self {
        Self {
            source,
            offsets: OxcProgramOffsets::for_root_source(source.len()),
            is_ts: false,
        }
    }

    #[allow(dead_code)]
    pub(crate) fn with_offsets(mut self, offsets: OxcProgramOffsets) -> Self {
        self.offsets = offsets;
        self
    }

    #[allow(dead_code)]
    pub(crate) fn with_typescript(mut self, is_ts: bool) -> Self {
        self.is_ts = is_ts;
        self
    }

    pub(crate) fn parse_program_for_compile(&self) -> Option<EstreeNode> {
        let allocator = Allocator::default();
        let source_type = if self.is_ts {
            OxcSourceType::ts().with_module(true)
        } else {
            OxcSourceType::mjs()
        };
        let parsed = OxcParser::new(&allocator, self.source, source_type).parse();

        let json = if self.is_ts {
            parsed.program.to_estree_ts_json(true)
        } else {
            parsed.program.to_estree_js_json(true)
        };
        let mut parsed_program = self.parse_and_normalize_raw_node(&json)?;
        let program = &mut parsed_program.node;

        program.fields.insert(
            "start".to_string(),
            EstreeValue::UInt(self.offsets.global_start as u64),
        );
        program.fields.insert(
            "end".to_string(),
            EstreeValue::UInt((self.offsets.global_start + self.source.len()) as u64),
        );

        let mut loc_fields = BTreeMap::new();
        loc_fields.insert(
            "start".to_string(),
            EstreeValue::Object(position_raw_node(
                self.offsets.start_line,
                self.offsets.start_column,
            )),
        );
        loc_fields.insert(
            "end".to_string(),
            EstreeValue::Object(position_raw_node(
                self.offsets.end_line,
                self.offsets.end_column,
            )),
        );
        program.fields.insert(
            "loc".to_string(),
            EstreeValue::Object(EstreeNode { fields: loc_fields }),
        );

        if program_body_is_empty(program) {
            let trailing = parse_leading_comment_nodes(self.source, self.offsets.global_start);
            if !trailing.is_empty() {
                let trailing_values = trailing
                    .into_iter()
                    .map(EstreeValue::Object)
                    .collect::<Vec<_>>()
                    .into_boxed_slice();
                program.fields.insert(
                    "trailingComments".to_string(),
                    EstreeValue::Array(trailing_values),
                );
            }
        }

        Some(parsed_program.node)
    }

    pub(crate) fn parse_import_ranges_for_compile(&self) -> Option<Vec<(usize, usize)>> {
        let allocator = Allocator::default();
        let source_type = if self.is_ts {
            OxcSourceType::ts().with_module(true)
        } else {
            OxcSourceType::mjs()
        };
        let parsed = OxcParser::new(&allocator, self.source, source_type).parse();
        if !parsed.errors.is_empty() {
            return None;
        }

        let mut ranges = Vec::with_capacity(parsed.program.body.len());
        for statement in parsed.program.body.iter() {
            let Statement::ImportDeclaration(declaration) = statement else {
                return None;
            };
            ranges.push((
                declaration.span.start as usize,
                declaration.span.end as usize,
            ));
        }

        Some(ranges)
    }

    pub(crate) fn can_parse_program(&self) -> bool {
        let allocator = Allocator::default();
        let source_type = if self.is_ts {
            OxcSourceType::ts().with_module(true)
        } else {
            OxcSourceType::mjs()
        };
        let parsed = OxcParser::new(&allocator, self.source, source_type).parse();
        parsed.errors.is_empty()
    }

    fn parse_and_normalize_raw_node(&self, estree_json: &str) -> Option<ParsedRawNode> {
        let mut node = serde_json::from_str::<EstreeNode>(estree_json).ok()?;
        normalize_estree_node(
            &mut node,
            self.source,
            self.offsets.global_start,
            self.offsets.start_line,
            self.offsets.start_column,
        );

        let comments = parse_all_comment_nodes(self.source, self.offsets.global_start);
        if !comments.is_empty() {
            attach_estree_comments_to_tree(
                &mut node,
                self.source,
                self.offsets.global_start,
                &comments,
            );
        }

        Some(ParsedRawNode { node })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn program_body_len(program: &EstreeNode) -> usize {
        match estree_node_field(program, RawField::Body) {
            Some(EstreeValue::Array(items)) => items.len(),
            _ => 0,
        }
    }

    #[test]
    fn parses_program_with_rune_call_in_js_mode() {
        let source = "let playbackRate = $state(0.5);";
        let program = SvelteOxcParser::new(source)
            .parse_program_for_compile()
            .expect("program should parse");

        assert_eq!(program_body_len(&program), 1);
    }

    #[test]
    fn parses_program_with_dollar_dollar_props_in_js_mode() {
        let source = "let x = $$props;";
        let program = SvelteOxcParser::new(source)
            .parse_program_for_compile()
            .expect("program should parse");

        assert_eq!(program_body_len(&program), 1);
    }

    #[test]
    fn estree_js_json_for_rune_call_deserializes() {
        let source = "let playbackRate = $state(0.5);";
        let allocator = Allocator::default();
        let parsed =
            oxc_parser::Parser::new(&allocator, source, oxc_span::SourceType::mjs()).parse();
        let json = parsed.program.to_estree_js_json(true);

        let _value: serde_json::Value =
            serde_json::from_str(&json).expect("serializer should emit valid json");
        let _node: EstreeNode =
            serde_json::from_str(&json).expect("serializer output should fit EstreeNode");
    }
}