#![warn(missing_docs)]
#![allow(clippy::doc_markdown)]
pub mod builder;
pub mod codegen;
pub mod error;
pub mod hir;
pub mod manifest;
pub use error::{JsGenError, Result};
pub mod prelude {
pub use crate::builder::{JsClassBuilder, JsModuleBuilder, JsSwitchBuilder};
pub use crate::codegen::generate;
pub use crate::error::{JsGenError, Result};
pub use crate::hir::{
BinOp, Expr, GenerationMetadata, Identifier, JsClass, JsMethod, JsModule, JsSwitch, Stmt,
UnaryOp,
};
pub use crate::manifest::{verify, write_with_manifest, FileManifest};
}
pub mod validator {
pub const FORBIDDEN_PATTERNS: &[&str] = &[
"window.", "window)", "window,", "document.", "importScripts", "eval(", "Function(", "with(", "__proto__", ];
pub const REQUIRED_WORKER_PATTERNS: &[&str] = &[
"self.", "import(", ];
pub fn validate_worker_js(js: &str) -> Vec<String> {
let mut errors = Vec::new();
for pattern in FORBIDDEN_PATTERNS {
if js.contains(pattern) {
errors.push(format!("Forbidden pattern found: '{pattern}'"));
}
}
for pattern in REQUIRED_WORKER_PATTERNS {
if !js.contains(pattern) {
errors.push(format!("Required pattern missing: '{pattern}'"));
}
}
errors
}
pub fn validate_worklet_js(js: &str) -> Vec<String> {
let mut errors = Vec::new();
for pattern in FORBIDDEN_PATTERNS {
if js.contains(pattern) {
errors.push(format!("Forbidden pattern found: '{pattern}'"));
}
}
if !js.contains("extends AudioWorkletProcessor") {
errors.push("Missing AudioWorkletProcessor base class".to_string());
}
if !js.contains("registerProcessor") {
errors.push("Missing registerProcessor call".to_string());
}
if !js.contains("process(") {
errors.push("Missing process() method".to_string());
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forbidden_patterns_detected() {
let js = "window.alert('test')";
let errors = validate_worker_js(js);
assert!(!errors.is_empty());
assert!(errors[0].contains("window."));
}
#[test]
fn valid_worker_js() {
let js = r#"
self.onmessage = async function(e) {
const mod = await import("./module.js");
};
"#;
let errors = validate_worker_js(js);
assert!(errors.is_empty(), "Errors: {:?}", errors);
}
#[test]
fn valid_worklet_js() {
let js = r#"
class MyProcessor extends AudioWorkletProcessor {
process(inputs, outputs, params) {
return true;
}
}
registerProcessor("my-processor", MyProcessor);
"#;
let errors = validate_worklet_js(js);
assert!(errors.is_empty(), "Errors: {:?}", errors);
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod integration_tests {
use super::prelude::*;
#[test]
fn end_to_end_worker_generation() {
let module = JsModuleBuilder::new()
.comment("Whisper.apr Transcription Worker (ES Module)")
.comment("Generated by probar-js-gen - DO NOT EDIT MANUALLY")
.let_decl("wasm", Expr::null())
.unwrap()
.let_decl("worker", Expr::null())
.unwrap()
.let_decl("initialized", Expr::bool(false))
.unwrap()
.stmt(Stmt::on_message(vec![
Stmt::const_decl("msg", Expr::ident("e").unwrap().dot("data").unwrap()).unwrap(),
Stmt::if_then(
Expr::ident("msg")
.unwrap()
.dot("type")
.unwrap()
.eq(Expr::str("bootstrap")),
vec![Stmt::expr(
Expr::ident("console")
.unwrap()
.dot("log")
.unwrap()
.call(vec![Expr::str("[Worker] Bootstrap received")]),
)],
),
]))
.expr(
Expr::ident("console")
.unwrap()
.dot("log")
.unwrap()
.call(vec![Expr::str(
"[Worker] Module loaded, waiting for bootstrap",
)]),
)
.build();
let js = generate(&module);
assert!(js.contains("self.onmessage"), "Missing self.onmessage");
assert!(!js.contains("window"), "Has window (forbidden)");
}
#[test]
fn end_to_end_worklet_generation() {
let class = JsClassBuilder::new("WhisperProcessor")
.unwrap()
.extends("AudioWorkletProcessor")
.unwrap()
.constructor(vec![Stmt::member_assign(
Expr::this(),
"buffer",
Expr::null(),
)
.unwrap()])
.method(
"process",
&["inputs", "outputs", "params"],
vec![Stmt::ret_val(Expr::bool(true))],
)
.unwrap()
.build();
let module = JsModuleBuilder::new()
.comment("WhisperAudioProcessor - AudioWorklet for real-time audio capture")
.class(class)
.register_processor("whisper-audio-processor", "WhisperProcessor")
.unwrap()
.build();
let js = generate(&module);
assert!(js.contains("extends AudioWorkletProcessor"));
assert!(js.contains("super();"));
assert!(js.contains("process(inputs, outputs, params)"));
assert!(js.contains("return true;"));
assert!(js.contains("registerProcessor"));
assert!(!js.contains("window"));
assert!(!js.contains("document"));
}
#[test]
fn deterministic_generation() {
let build_module = || {
JsModuleBuilder::new()
.let_decl("x", Expr::num(1))
.unwrap()
.const_decl("y", Expr::str("test"))
.unwrap()
.build()
};
let js1 = generate(&build_module());
let js2 = generate(&build_module());
assert_eq!(js1, js2, "Generation must be deterministic");
}
#[test]
fn identifier_validation_rejects_reserved() {
assert!(Identifier::new("class").is_err());
assert!(Identifier::new("function").is_err());
assert!(Identifier::new("await").is_err());
assert!(Identifier::new("import").is_err());
}
#[test]
fn identifier_validation_rejects_invalid() {
assert!(Identifier::new("123start").is_err());
assert!(Identifier::new("has-dash").is_err());
assert!(Identifier::new("").is_err());
}
#[test]
fn identifier_validation_accepts_valid() {
assert!(Identifier::new("validName").is_ok());
assert!(Identifier::new("_private").is_ok());
assert!(Identifier::new("$jquery").is_ok());
assert!(Identifier::new("name123").is_ok());
}
}