vize_l4 0.538.0

L4 - the Davinci emission level (codename Stesura): writer, expression rewriting, module assembly and targets
Documentation
//! Assemble prepared script and target fragments into one SFC module.
//!
//! The script lane rewrites its default export and supplies a component binding.
//! The target supplies hoists/cache declarations and either a declared render
//! function or an inline render expression. This module never parses or rewrites
//! JavaScript. It joins their writers, preserving every authored link, then
//! builds helper imports from the complete used-helper set. Runtime vocabulary
//! groups retain each export owner; target generation stays separate (#6840).

use crate::runtime::{Helper, Runtime, Vocabulary, vocabulary_for};
use crate::write::{Emitted, LinkSink, Writer};

/// The component field a declared render function implements.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderProperty {
    Render,
    SsrRender,
}

impl RenderProperty {
    const fn name(self) -> &'static str {
        match self {
            Self::Render => "render",
            Self::SsrRender => "ssrRender",
        }
    }
}

/// The closed render-function vocabulary shared by prepared module fragments.
///
/// A function binding and its component field must move together. Keeping the
/// three supported pairs here prevents each module assembler from spelling the
/// same generated identifiers independently.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderFunctionName {
    /// A standalone client render function named `render`.
    Render,
    /// A client render function renamed for attachment to an SFC component.
    SfcRender,
    /// A server render function named `ssrRender`.
    SsrRender,
}

impl RenderFunctionName {
    /// The generated JavaScript binding for this render function.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Render => "render",
            Self::SfcRender => "_sfc_render",
            Self::SsrRender => "ssrRender",
        }
    }

    /// The component field that receives this render function.
    #[must_use]
    pub const fn component_field(self) -> &'static str {
        match self {
            Self::Render | Self::SfcRender => "render",
            Self::SsrRender => "ssrRender",
        }
    }

    /// A prepared-function placement with the matching binding and field.
    #[must_use]
    pub const fn placement(self) -> RenderPlacement<'static> {
        RenderPlacement::Function {
            binding: self.as_str(),
            property: match self {
                Self::Render | Self::SfcRender => RenderProperty::Render,
                Self::SsrRender => RenderProperty::SsrRender,
            },
        }
    }
}

/// How the target's prepared render fragment joins the component.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderPlacement<'a> {
    /// No template; there must be no render fragment.
    None,
    /// A complete function declaration, attached to the component afterwards.
    /// `binding` is the generated JavaScript identifier of that declaration.
    Function {
        binding: &'a str,
        property: RenderProperty,
    },
    /// An expression inserted between the script lane's prepared fragments.
    /// The script lane owns `setup()`, `return` and all surrounding punctuation.
    Inline,
}

/// The authored script, already rewritten and linked by the script lane.
#[derive(Debug)]
pub enum ScriptPart<L: LinkSink> {
    /// A script defining the component binding, with no default export.
    Body(Writer<L>),
    /// Script output around the inline render expression. These fragments are
    /// concatenated without added whitespace, so `return` semantics are kept.
    Inline {
        before_render: Writer<L>,
        after_render: Writer<L>,
    },
}

/// Prepared pieces of one module, in their final emission order.
#[derive(Debug)]
pub struct ModuleParts<'a, L: LinkSink> {
    /// The vocabulary used by every fragment. Helpers are imported as `_name`.
    /// Names must be distinct JavaScript identifiers; fragments use that alias.
    pub vocabulary: &'a Vocabulary,
    /// Linked hoists and cache declarations, already generated by the target.
    pub prelude: Option<Writer<L>>,
    /// Rewritten script. Without one, assembly declares `const component = {}`.
    pub script: Option<ScriptPart<L>>,
    /// A complete declared function or an inline expression, as specified below.
    pub render: Option<Writer<L>>,
    pub placement: RenderPlacement<'a>,
    /// The generated JavaScript identifier defined by the script lane.
    pub component: &'a str,
    /// Validated Vue component scope identity, attached after complete fragments.
    pub scope_id: Option<ScopeId<'a>>,
}

impl<'a, L: LinkSink> ModuleParts<'a, L> {
    /// Start a module with the audited target vocabulary. Prepared fragments
    /// must use helper indices from this exact vocabulary.
    pub fn for_runtime(
        runtime: Runtime,
        version: &str,
        component: &'a str,
    ) -> Result<Self, AssemblyError> {
        Ok(Self {
            vocabulary: vocabulary_for(runtime, version)
                .ok_or(AssemblyError::UnsupportedRuntimeVersion)?,
            prelude: None,
            script: None,
            render: None,
            placement: RenderPlacement::None,
            component,
            scope_id: None,
        })
    }
}

mod imports;
pub mod ordinary;
mod output;
pub use output::OutputModule;
mod scope;
pub use scope::ScopeId;
pub mod setup;
#[cfg(test)]
mod template_tests;

/// Assemble an exported template render declaration with its late helper imports.
///
/// The target owns the complete declaration; no script or component attachment
/// is synthesized for this standalone compiler module surface.
pub fn assemble_template<L: LinkSink>(
    render: Writer<L>,
    vocabulary: &Vocabulary,
) -> Result<Emitted<L>, AssemblyError> {
    assemble_template_with_prelude(Writer::default(), render, vocabulary)
}

/// Assemble a target's linked static declarations before its exported render.
/// No source parsing, script rewriting or component attachment is synthesized.
pub fn assemble_template_with_prelude<L: LinkSink>(
    prelude: Writer<L>,
    render: Writer<L>,
    vocabulary: &Vocabulary,
) -> Result<Emitted<L>, AssemblyError> {
    let mut body = prelude;
    end_line(&mut body);
    body.push("\nexport ");
    body.append(render);
    let preamble = imports::preamble(vocabulary, body.helpers())?;
    Ok(body.finish_with_preamble(preamble))
}

/// A mismatch between prepared fragments and their declared contracts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssemblyError {
    UnsupportedRuntimeVersion,
    MissingRender,
    UnexpectedRender,
    MissingInlineScript,
    UnexpectedInlineScript,
    UnknownHelper(Helper),
    InvalidModuleSpecifier,
    InvalidScopeId,
}

/// Assemble one module without inserting text into any fragment.
///
/// Generated identifiers and fragment syntax belong to their providers. The
/// assembler checks placement and helper membership; it does not admit targets
/// or claim native product coverage. Its returned links cover the whole module.
pub fn assemble<L: LinkSink>(parts: ModuleParts<'_, L>) -> Result<Emitted<L>, AssemblyError> {
    let ModuleParts {
        vocabulary,
        prelude,
        script,
        render,
        placement,
        component,
        scope_id,
    } = parts;
    let mut body = prelude.unwrap_or_default();
    separate_statement(&mut body);
    match (placement, script, render) {
        (RenderPlacement::None, _, Some(_)) => return Err(AssemblyError::UnexpectedRender),
        (RenderPlacement::None, Some(ScriptPart::Inline { .. }), None) => {
            return Err(AssemblyError::UnexpectedInlineScript);
        }
        (RenderPlacement::None, script, None) => append_script(&mut body, script, component),
        (RenderPlacement::Function { .. } | RenderPlacement::Inline, _, None) => {
            return Err(AssemblyError::MissingRender);
        }
        (RenderPlacement::Function { .. }, Some(ScriptPart::Inline { .. }), Some(_)) => {
            return Err(AssemblyError::UnexpectedInlineScript);
        }
        (RenderPlacement::Function { binding, property }, script, Some(render)) => {
            append_script(&mut body, script, component);
            separate_statement(&mut body);
            body.append(render);
            end_line(&mut body);
            body.push(component);
            body.push(".");
            body.push(property.name());
            body.push(" = ");
            body.push(binding);
            body.push("\n");
        }
        (
            RenderPlacement::Inline,
            Some(ScriptPart::Inline {
                before_render,
                after_render,
            }),
            Some(render),
        ) => {
            body.append(before_render);
            body.append(render);
            body.append(after_render);
            end_line(&mut body);
        }
        (RenderPlacement::Inline, _, Some(_)) => return Err(AssemblyError::MissingInlineScript),
    }
    if let Some(scope_id) = scope_id {
        body.push(component);
        body.push(".__scopeId = \"");
        body.push(scope_id.as_str());
        body.push("\"\n");
    }
    body.push("export default ");
    body.push(component);
    body.push("\n");

    let preamble = imports::preamble(vocabulary, body.helpers())?;
    Ok(body.finish_with_preamble(preamble))
}

fn append_script<L: LinkSink>(
    body: &mut Writer<L>,
    script: Option<ScriptPart<L>>,
    component: &str,
) {
    if let Some(ScriptPart::Body(script)) = script {
        body.append(script);
    } else {
        body.push("const ");
        body.push(component);
        body.push(" = {}\n");
    }
    end_line(body);
}

fn end_line<L: LinkSink>(writer: &mut Writer<L>) {
    if !writer.is_empty() && !writer.as_str().ends_with('\n') {
        writer.push("\n");
    }
}

// A newline alone can continue a preceding initializer when the next prepared
// fragment starts with `(`, `[` or a template literal. Put the delimiter on its
// own line so a trailing authored line comment cannot consume it. Inline
// expression insertion deliberately does not use this statement boundary.
fn separate_statement<L: LinkSink>(writer: &mut Writer<L>) {
    if !writer.is_empty() {
        end_line(writer);
        writer.push(";\n");
    }
}

#[cfg(test)]
mod tests;

#[cfg(test)]
mod runtime_tests;