Skip to main content

perry_wit/
api.rs

1//! Content-based compiler API over caller-supplied sources and modules.
2//!
3//! [`compile`] validates a WIT world and returns its core module plus an optional
4//! [`crate::ComponentOutput`] containing both component forms. [`Options::core_only`]
5//! returns only the world-bound core module. [`compile_core`] exports raw TypeScript
6//! functions with core signatures.
7//!
8//! ```
9//! # fn main() -> anyhow::Result<()> {
10//! use perry_wit::api::{compile_core, Program};
11//! let program = Program {
12//!     source: b"export function twice(n: number): number { return n * 2; }".as_slice().into(),
13//!     ..Default::default()
14//! };
15//! let core = compile_core(&program)?;
16//! # assert!(!core.is_empty());
17//! # Ok(())
18//! # }
19//! ```
20
21use anyhow::{Context, Result, ensure};
22use std::collections::BTreeMap;
23
24#[derive(Clone, Debug, Default)]
25pub struct Source {
26    /// Optional diagnostic name. For a TypeScript entrypoint, this also serves as its logical identity.
27    pub name: Option<String>,
28    pub contents: Vec<u8>,
29}
30
31impl From<&[u8]> for Source {
32    fn from(contents: &[u8]) -> Self {
33        Self {
34            contents: contents.to_vec(),
35            name: None,
36        }
37    }
38}
39
40/// A supplied dependency module resolved from in-memory contents by package-relative name.
41#[derive(Clone, Debug)]
42pub struct Module {
43    pub name: String,
44    pub contents: Vec<u8>,
45}
46
47#[derive(Clone, Debug, Default)]
48pub struct Program {
49    pub source: Source,
50    pub modules: Vec<Module>,
51}
52
53#[derive(Clone, Debug)]
54pub enum Package {
55    Sources(Vec<Source>),
56    Encoded(Vec<u8>),
57}
58
59impl From<&[u8]> for Package {
60    fn from(contents: &[u8]) -> Self {
61        Self::Sources(vec![contents.into()])
62    }
63}
64
65#[derive(Clone, Debug)]
66pub struct Wit {
67    pub application: Package,
68    pub dependencies: Vec<Package>,
69}
70
71#[derive(Clone, Debug, Default)]
72pub struct Options {
73    pub world: Option<String>,
74    /// Emit the linked core Wasm module directly while preserving the selected WIT world's ABI.
75    pub core_only: bool,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct Artifact {
80    pub name: String,
81    pub contents: Vec<u8>,
82}
83
84pub(crate) fn text<'a>(contents: &'a [u8], name: Option<&str>) -> Result<&'a str> {
85    std::str::from_utf8(contents)
86        .with_context(|| format!("{} is not UTF-8", name.unwrap_or("input")))
87}
88
89fn module_name(name: &str) -> Result<&str> {
90    ensure!(
91        !name.is_empty()
92            && !name.contains(['\\', '\0', ':'])
93            && name
94                .split('/')
95                .all(|part| !part.is_empty() && part != "." && part != ".."),
96        "module identity must be package-relative: {name:?}"
97    );
98    Ok(name)
99}
100
101pub(crate) fn program(input: &Program) -> Result<(&str, &str, BTreeMap<String, String>)> {
102    let identity = module_name(input.source.name.as_deref().unwrap_or("input.ts"))?;
103    let mut modules = BTreeMap::new();
104    for source in &input.modules {
105        let name = module_name(&source.name)?;
106        ensure!(
107            name != identity && !modules.contains_key(name),
108            "duplicate module identity: {name}"
109        );
110        modules.insert(
111            name.to_owned(),
112            text(&source.contents, Some(name))?.to_owned(),
113        );
114    }
115    Ok((
116        text(&input.source.contents, Some(identity))?,
117        identity,
118        modules,
119    ))
120}
121
122/// Compile supplied TypeScript and WIT contents with the embedded standard packages.
123fn compile_impl(input: &Program, wit: &Wit, options: &Options) -> Result<crate::Compiled> {
124    let (source, identity, modules) = program(input)?;
125    let (resolve, package) = crate::component::wit::resolve_contents(wit)?;
126    let world = crate::component::wit::select_world(&resolve, package, options.world.as_deref())?;
127    crate::compiler::compile_sources(
128        source,
129        identity,
130        &modules,
131        &resolve,
132        world,
133        !options.core_only,
134        false,
135    )
136}
137
138/// Compile raw TypeScript exports with core signatures from supplied in-memory sources.
139fn compile_core_impl(input: &Program) -> Result<Vec<u8>> {
140    let (source, identity, modules) = program(input)?;
141    crate::compiler::compile_core_sources(source, identity, &modules, false)
142}
143
144/// Generate declaration contents. Project layout and writing belong to the caller.
145fn generate_sdk_impl(wit: &Wit, world: Option<&str>) -> Result<Vec<Artifact>> {
146    let (resolve, package) = crate::component::wit::resolve_contents(wit)?;
147    let world = crate::component::wit::select_world(&resolve, package, world)?;
148    crate::sdk::declarations(&resolve, world)
149}
150
151/// Compile in-memory TypeScript source modules and WIT package definitions into a
152/// validated WASI 0.3 WebAssembly component.
153///
154/// ```
155/// # fn main() -> Result<(), perry_wit::CompileError> {
156/// use perry_wit::api::{self, Options, Program, Wit};
157///
158/// let program = Program {
159///     source: br#"
160///         export function authorize(token: string): boolean {
161///             return token.startsWith("Bearer ") && token.length > 16;
162///         }
163///     "#
164///     .as_slice()
165///     .into(),
166///     modules: vec![],
167/// };
168///
169/// let wit = Wit {
170///     application: br#"
171///         package example:auth@0.1.0;
172///         world filter {
173///             export authorize: func(token: string) -> bool;
174///         }
175///     "#
176///     .as_slice()
177///     .into(),
178///     dependencies: vec![],
179/// };
180///
181/// let compiled = api::compile(&program, &wit, &Options::default())?;
182/// assert!(!compiled.core.is_empty());
183/// assert!(compiled.component.is_some_and(|component| !component.bytes.is_empty()));
184/// # Ok(())
185/// # }
186/// ```
187pub fn compile(
188    input: &Program,
189    wit: &Wit,
190    options: &Options,
191) -> std::result::Result<crate::Compiled, crate::CompileError> {
192    compile_impl(input, wit, options).map_err(|error| {
193        crate::CompileError::from_error(error, crate::DiagnosticCode::InvalidInput)
194    })
195}
196
197pub fn compile_core(input: &Program) -> std::result::Result<Vec<u8>, crate::CompileError> {
198    compile_core_impl(input).map_err(|error| {
199        crate::CompileError::from_error(error, crate::DiagnosticCode::InvalidInput)
200    })
201}
202
203pub fn generate_sdk(
204    wit: &Wit,
205    world: Option<&str>,
206) -> std::result::Result<Vec<Artifact>, crate::CompileError> {
207    generate_sdk_impl(wit, world).map_err(|error| {
208        crate::CompileError::from_error(error, crate::DiagnosticCode::WitResolution)
209    })
210}