use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use oxc_allocator::Allocator;
use oxc_ast::ast::Statement;
use oxc_parser::Parser;
use oxc_span::SourceType;
use super::config::CompileOptions;
use super::error::EngineError;
use super::model::{CompiledTemplate, HydrateMode, ImportEdge};
use super::security;
const JSX_PRELUDE: &str = "/** @jsx h */\n";
pub fn compile(source: &str, options: CompileOptions) -> Result<CompiledTemplate, EngineError> {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compile_inner(source, options)
}));
match result {
Ok(r) => r,
Err(_) => {
let _ = std::fs::write("/tmp/rwe-parse-failed.tsx", source);
Err(EngineError::new(
"RWE_PARSE_PANIC",
"oxc compiler panicked — source written to /tmp/rwe-parse-failed.tsx",
))
}
}
}
fn compile_inner(source: &str, options: CompileOptions) -> Result<CompiledTemplate, EngineError> {
let alloc = Allocator::default();
let source_type = source_type_from_options(&options);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
let _ = std::fs::write("/tmp/rwe-parse-failed.tsx", source);
return Err(EngineError::new(
"RWE_PARSE",
"oxc parser panicked while parsing TSX",
));
}
if !parsed.errors.is_empty() {
let msg = parsed
.errors
.iter()
.map(|e| format!("{e:?}"))
.collect::<Vec<_>>()
.join("; ");
eprintln!("[RWE] parse diagnostics (non-fatal): {msg}");
}
ensure_default_export(&parsed.program)?;
let mut diagnostics = security::analyze(source, &options.security)?;
let raw_imports = collect_imports(&parsed.program);
validate_zeb_exclusive_symbols(&parsed.program)?;
validate_zeb_hook_imports(source)?;
validate_import_allowlist(&raw_imports, &options)?;
let (rewritten_source, imports) =
rewrite_imports(source, &raw_imports, &options, &mut diagnostics)?;
let normalized_page_source = rewrite_page_root_tag(&rewritten_source);
let (bundled_server, _, server_deps, inline_styles) = bundle_for_client(
&normalized_page_source,
&imports,
options.template_root.as_deref(),
&options.library_roots,
)?;
let transformed_server = format!("{}{}", JSX_PRELUDE, bundled_server);
let (bundled_client, detected_zeb_libs, client_deps, _) = bundle_for_client(
&normalized_page_source,
&imports,
options.template_root.as_deref(),
&options.library_roots,
)?;
let transformed_client = format!("{}{}", JSX_PRELUDE, bundled_client);
let mut dependency_paths = server_deps;
dependency_paths.extend(client_deps);
let hydrate_mode = detect_hydrate_mode(source);
Ok(CompiledTemplate {
engine: "rwe".to_string(),
source_path: options.file_path.clone(),
runtime_mode: options.runtime_mode.clone(),
deno_timeout_ms: options.deno_timeout_ms,
server_module_source: transformed_server,
client_module_source: transformed_client,
imports,
diagnostics,
hydrate_mode,
compile_options: options,
detected_zeb_libs,
inline_styles,
dependency_paths,
})
}
fn source_type_from_options(options: &CompileOptions) -> SourceType {
if let Some(path) = &options.file_path {
SourceType::from_path(path)
.unwrap_or_default()
.with_module(true)
.with_jsx(true)
.with_typescript(true)
} else {
SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true)
}
}
fn ensure_default_export(program: &oxc_ast::ast::Program<'_>) -> Result<(), EngineError> {
let has_default = program
.body
.iter()
.any(|stmt| matches!(stmt, Statement::ExportDefaultDeclaration(_)));
if has_default {
Ok(())
} else {
Err(EngineError::new(
"RWE_EXPORT_DEFAULT",
"template must have one default export component",
))
}
}
fn replace_import_specifiers(source: &str, mut map: impl FnMut(&str) -> Option<String>) -> String {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return source.to_string();
}
let mut edits: Vec<(usize, usize, String)> = Vec::new();
for stmt in &parsed.program.body {
if let Statement::ImportDeclaration(import) = stmt {
let spec = import.source.value.as_str();
if let Some(new) = map(spec) {
let span = import.source.span;
edits.push((span.start as usize + 1, span.end as usize - 1, new));
}
}
}
let mut out = source.to_string();
for (start, end, new) in edits.into_iter().rev() {
out.replace_range(start..end, &new);
}
out
}
fn collect_imports(program: &oxc_ast::ast::Program<'_>) -> Vec<String> {
let mut imports = Vec::new();
for stmt in &program.body {
if let Statement::ImportDeclaration(import) = stmt {
imports.push(import.source.value.as_str().to_string());
}
}
imports
}
fn is_external_specifier(import: &str) -> bool {
import.starts_with("npm:")
|| import.starts_with("node:")
|| import.starts_with("jsr:")
|| import.starts_with("http://")
|| import.starts_with("https://")
}
fn relative_import_refusal(import: &str) -> EngineError {
let bare = import.trim_start_matches(['.', '/']);
EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
format!(
"relative import '{import}' is not allowed; \
use \"@/{bare}\" addressed from the template root instead"
),
)
}
fn runtime_import_refusal(import: &str) -> Option<EngineError> {
let familiar = matches!(
import,
"react"
| "react-dom"
| "react/jsx-runtime"
| "react-dom/client"
| "preact"
| "preact/hooks"
| "preact/compat"
);
familiar.then(|| {
EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
format!(
"import from '{import}' is not allowed; \
hooks and helpers come from \"zeb/react\" — write `from \"zeb/react\"` instead"
),
)
})
}
fn validate_import_allowlist(
imports: &[String],
options: &CompileOptions,
) -> Result<(), EngineError> {
for import in imports {
if import == "zeb" {
return Err(EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
"import from 'zeb' is not allowed; hooks and helpers come from \"zeb/react\" \
— write `from \"zeb/react\"` instead"
.to_string(),
));
}
if import.starts_with("zeb/") {
continue;
}
if import.starts_with("@/") {
continue;
}
if import.starts_with("./") || import.starts_with("../") {
return Err(relative_import_refusal(import));
}
if let Some(error) = runtime_import_refusal(import) {
return Err(error);
}
if is_external_specifier(import) {
continue;
}
if import.starts_with('/') {
ensure_bundle_read_allowed(
import,
options.template_root.as_deref(),
&options.library_roots,
)?;
continue;
}
return Err(EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
format!(
"import '{import}' is not allowed; valid imports are \"zeb/react\", \"zeb/*\", \"@/…\", and npm:/node:/jsr:/http(s): specifiers"
),
));
}
Ok(())
}
const ZEB_HOOK_SYMBOLS: &[&str] = &[
"useState",
"useEffect",
"useRef",
"useMemo",
"useCallback",
"useContext",
"useReducer",
"useId",
"useLayoutEffect",
"useSyncExternalStore",
"usePageState",
"useRouter",
];
const ZEB_EXCLUSIVE_SYMBOLS: &[&str] = &[
"useState",
"useEffect",
"useRef",
"useMemo",
"useCallback",
"useContext",
"useReducer",
"useId",
"useLayoutEffect",
"useSyncExternalStore",
"usePageState",
"useRouter",
"cx",
"Link",
"forwardRef",
"memo",
"createContext",
"Fragment",
];
fn validate_zeb_exclusive_symbols(program: &oxc_ast::ast::Program<'_>) -> Result<(), EngineError> {
use oxc_ast::ast::ImportDeclarationSpecifier;
for stmt in &program.body {
if let Statement::ImportDeclaration(import) = stmt {
let specifier = import.source.value.as_str();
if specifier == super::zeb_react::SPECIFIER || specifier.starts_with('/') {
continue;
}
if let Some(specifiers) = &import.specifiers {
for s in specifiers.iter() {
if let ImportDeclarationSpecifier::ImportSpecifier(named) = s {
let name = named.imported.name().as_str();
if ZEB_EXCLUSIVE_SYMBOLS.contains(&name) {
return Err(EngineError::new(
"RWE_IMPORT_ZEB_ONLY",
format!(
"'{name}' must be imported from \"zeb/react\", not \"{specifier}\""
),
));
}
}
}
}
}
}
Ok(())
}
fn validate_zeb_hook_imports(source: &str) -> Result<(), EngineError> {
let missing = find_unimported_zeb_hooks(source);
let Some(first) = missing.first() else {
return Ok(());
};
Err(EngineError::new(
"RWE_HOOK_NOT_IMPORTED",
format!(
"'{first}' is used but not imported; add `import {{ {} }} from \"zeb/react\";` to this file",
missing.join(", ")
),
))
}
fn find_unimported_zeb_hooks(source: &str) -> Vec<String> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return Vec::new();
}
let program = &parsed.program;
let mut bound = collect_top_level_declared_names(program);
for stmt in &program.body {
if let Statement::ImportDeclaration(import) = stmt {
let Some(specifiers) = &import.specifiers else {
continue;
};
for specifier in specifiers.iter() {
bound.insert(specifier.local().name.as_str().to_string());
}
}
}
let (masked, _) = super::js_masker::mask(source);
let mut used = ZEB_HOOK_SYMBOLS
.iter()
.filter(|name| !bound.contains(**name))
.filter(|name| is_called_in(&masked, name))
.map(|name| (*name).to_string())
.collect::<Vec<_>>();
used.sort();
used.dedup();
used
}
fn is_called_in(source: &str, name: &str) -> bool {
let bytes = source.as_bytes();
let mut from = 0;
while let Some(offset) = source[from..].find(name) {
let start = from + offset;
let end = start + name.len();
let before_ok = start == 0
|| !(bytes[start - 1].is_ascii_alphanumeric()
|| bytes[start - 1] == b'_'
|| bytes[start - 1] == b'$'
|| bytes[start - 1] == b'.');
if before_ok && source[end..].trim_start().starts_with('(') {
return true;
}
from = end;
}
false
}
#[allow(dead_code)]
fn strip_runtime_imports(source: &str) -> String {
source
.lines()
.filter(|line| {
let trimmed = line.trim();
if !trimmed.starts_with("import ") {
return true;
}
!trimmed.contains("from 'rwe'")
&& !trimmed.contains("from \"rwe\"")
&& !trimmed.contains("from 'zeb/react'")
&& !trimmed.contains("from \"zeb/react\"")
})
.collect::<Vec<_>>()
.join("\n")
}
fn rewrite_imports(
source: &str,
imports: &[String],
options: &CompileOptions,
diagnostics: &mut Vec<super::model::Diagnostic>,
) -> Result<(String, Vec<ImportEdge>), EngineError> {
let mut resolved_specs: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut out = Vec::new();
for import in imports {
if import == "zeb" {
return Err(EngineError::new(
"RWE_IMPORT_NOT_ALLOWED",
"import from 'zeb' is not allowed; hooks and helpers come from \"zeb/react\" \
— write `from \"zeb/react\"` instead"
.to_string(),
));
}
if import.starts_with("zeb/") && resolve_library_import(import, &options.library_roots)?.is_none() {
continue;
}
let resolved = resolve_import(import, options)?;
if let Some(path) = &resolved {
resolved_specs.insert(import.clone(), path.clone());
}
if import.starts_with("@/") && resolved.is_none() {
diagnostics.push(super::model::Diagnostic {
code: "RWE_IMPORT_UNRESOLVED".to_string(),
message: format!("could not resolve alias import '{import}'"),
line: None,
});
}
out.push(ImportEdge {
source: import.clone(),
resolved,
});
}
let rewritten = replace_import_specifiers(source, |spec| resolved_specs.get(spec).cloned());
Ok((rewritten, out))
}
fn resolve_library_import(
import: &str,
library_roots: &std::collections::BTreeMap<String, String>,
) -> Result<Option<String>, EngineError> {
let Some((prefix, root)) = library_roots
.iter()
.filter(|(prefix, _)| import == prefix.as_str() || import.starts_with(&format!("{prefix}/")))
.max_by_key(|(prefix, _)| prefix.len())
else {
return Ok(None);
};
let rest = import.strip_prefix(prefix.as_str()).unwrap_or("").trim_start_matches('/');
let root_path = canonical_or_current(Path::new(root))?;
let joined = root_path.join(if rest.is_empty() { "index" } else { rest });
let resolved = resolve_module_path(&joined).ok().filter(|p| p.is_file());
let Some(resolved) = resolved else {
return Err(EngineError::new(
"RWE_IMPORT_UNRESOLVED",
format!("'{import}' is not a component of {prefix}"),
));
};
let final_path = normalize_path(&canonical_or_fallback(&resolved)?);
ensure_within_root(&final_path, &root_path)?;
Ok(Some(final_path.to_string_lossy().to_string()))
}
fn resolve_import(import: &str, options: &CompileOptions) -> Result<Option<String>, EngineError> {
if let Some(path) = resolve_library_import(import, &options.library_roots)? {
return Ok(Some(path));
}
if import.starts_with("npm:")
|| import.starts_with("node:")
|| import.starts_with("jsr:")
|| import.starts_with("http://")
|| import.starts_with("https://")
{
return Ok(None);
}
if import.starts_with("@/") {
let root = options.template_root.as_ref().ok_or_else(|| {
EngineError::new(
"RWE_TEMPLATE_ROOT",
format!("template_root is required for alias import '{import}'"),
)
})?;
let root_path = canonical_or_current(Path::new(root))?;
let joined = root_path.join(import.trim_start_matches("@/"));
let resolved = if import.ends_with(".css") {
resolve_style_path(&joined)?
} else {
resolve_module_path(&joined)?
};
let final_path = normalize_path(&canonical_or_fallback(&resolved)?);
ensure_within_root(&final_path, &root_path)?;
return Ok(Some(final_path.to_string_lossy().to_string()));
}
if import.starts_with("./") || import.starts_with("../") {
let file_path = options.file_path.as_ref().ok_or_else(|| {
EngineError::new(
"RWE_FILE_PATH",
format!("file_path is required for relative import '{import}'"),
)
})?;
let base = Path::new(file_path)
.parent()
.ok_or_else(|| EngineError::new("RWE_FILE_PATH", "invalid file_path"))?;
let base = canonical_or_current(base)?;
let joined = base.join(import);
let resolved = if import.ends_with(".css") {
resolve_style_path(&joined)?
} else {
resolve_module_path(&joined)?
};
let final_path = normalize_path(&canonical_or_fallback(&resolved)?);
if let Some(root) = &options.template_root {
let root_path = canonical_or_current(Path::new(root))?;
ensure_within_root(&final_path, &root_path)?;
}
return Ok(Some(final_path.to_string_lossy().to_string()));
}
Ok(None)
}
fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut out = PathBuf::new();
for comp in path.components() {
match comp {
Component::CurDir => {}
Component::ParentDir => {
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
fn canonical_or_current(path: &Path) -> Result<PathBuf, EngineError> {
fs::canonicalize(path).or_else(|_| {
let cwd = std::env::current_dir().map_err(|e| {
EngineError::new(
"RWE_PATH",
format!(
"failed reading current_dir while resolving '{}': {e}",
path.display()
),
)
})?;
Ok(cwd.join(path))
})
}
fn canonical_or_fallback(path: &Path) -> Result<PathBuf, EngineError> {
if path.exists() {
fs::canonicalize(path).map_err(|e| {
EngineError::new(
"RWE_IMPORT_RESOLVE",
format!("failed canonicalizing '{}': {e}", path.display()),
)
})
} else {
Ok(path.to_path_buf())
}
}
fn resolve_module_path(base: &Path) -> Result<PathBuf, EngineError> {
if base.is_file() {
return Ok(base.to_path_buf());
}
const FILE_EXTS: [&str; 4] = [".tsx", ".ts", ".jsx", ".js"];
for ext in FILE_EXTS {
let candidate = PathBuf::from(format!("{}{}", base.display(), ext));
if candidate.exists() {
return Ok(candidate);
}
}
const INDEX_FILES: [&str; 4] = ["index.tsx", "index.ts", "index.jsx", "index.js"];
for index in INDEX_FILES {
let candidate = base.join(index);
if candidate.exists() {
return Ok(candidate);
}
}
Ok(base.to_path_buf())
}
fn resolve_style_path(base: &Path) -> Result<PathBuf, EngineError> {
if base.is_file() {
return Ok(base.to_path_buf());
}
if base.extension().and_then(|v| v.to_str()) == Some("css") {
return Ok(base.to_path_buf());
}
let candidate = PathBuf::from(format!("{}.css", base.display()));
if candidate.exists() {
return Ok(candidate);
}
let index = base.join("index.css");
if index.exists() {
return Ok(index);
}
Ok(base.to_path_buf())
}
fn ensure_bundle_read_allowed(
path: &str,
template_root: Option<&str>,
library_roots: &std::collections::BTreeMap<String, String>,
) -> Result<(), EngineError> {
let target = canonical_module_identity(path)?;
let inside = template_root
.into_iter()
.chain(library_roots.values().map(String::as_str))
.filter_map(|root| canonical_or_current(Path::new(root)).ok())
.any(|root| target.starts_with(normalize_path(&root)));
if inside {
Ok(())
} else {
Err(EngineError::new(
"RWE_IMPORT_BOUNDARY",
format!(
"import '{path}' is outside the template root and the libraries; \
address a template as \"@/…\" and a library as \"zeb/…\""
),
))
}
}
fn ensure_within_root(path: &Path, root: &Path) -> Result<(), EngineError> {
if path.starts_with(root) {
Ok(())
} else {
Err(EngineError::new(
"RWE_IMPORT_BOUNDARY",
format!(
"resolved import '{}' escapes template_root '{}'",
path.display(),
root.display()
),
))
}
}
fn detect_hydrate_mode(source: &str) -> HydrateMode {
if source.contains("hydrate=\"off\"") || source.contains("hydrate={'off'}") {
HydrateMode::Off
} else if source.contains("hydrate=\"onview\"") || source.contains("hydrate={'onview'}") {
HydrateMode::Onview
} else if source.contains("hydrate=\"oninteract\"") || source.contains("hydrate={'oninteract'}")
{
HydrateMode::Oninteract
} else {
HydrateMode::Onload
}
}
fn rewrite_page_root_tag(source: &str) -> String {
source
.replace("<Page>", "<Fragment>")
.replace("</Page>", "</Fragment>")
.replace("<Page />", "<Fragment />")
.replace("<Page/>", "<Fragment/>")
.replace("<Page ", "<Fragment ")
}
fn rewrite_at_imports(
source: &str,
template_root: &Path,
library_roots: &std::collections::BTreeMap<String, String>,
) -> Result<String, EngineError> {
let mut failure: Option<EngineError> = None;
let out = replace_import_specifiers(source, |spec| {
if spec.starts_with("zeb/") {
return match resolve_library_import(spec, library_roots) {
Ok(path) => path,
Err(err) => {
failure.get_or_insert(err);
None
}
};
}
let rel = spec.strip_prefix("@/")?;
let joined = template_root.join(rel);
let resolved = if spec.ends_with(".css") {
resolve_style_path(&joined)
} else {
resolve_module_path(&joined)
}
.ok()?;
let canonical = canonical_or_fallback(&resolved).ok()?;
Some(normalize_path(&canonical).to_string_lossy().to_string())
});
match failure {
Some(err) => Err(err),
None => Ok(out),
}
}
fn bundle_for_client(
page_source: &str,
imports: &[ImportEdge],
template_root: Option<&str>,
library_roots: &std::collections::BTreeMap<String, String>,
) -> Result<(String, Vec<String>, HashSet<String>, Vec<String>), EngineError> {
let mut visited: HashSet<String> = HashSet::new();
let mut inlined_parts: Vec<String> = Vec::new();
let mut inline_styles: Vec<String> = Vec::new();
let mut counter: usize = 0;
let mut rwe_names: HashSet<String> = HashSet::new();
let mut zeb_libs: HashSet<String> = HashSet::new();
let mut declared: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut sources: std::collections::HashMap<String, String> = std::collections::HashMap::new();
rwe_names.extend(extract_rwe_import_names(page_source));
zeb_libs.extend(extract_zeb_lib_specifiers(page_source));
for edge in imports {
let path = edge.resolved.as_deref().unwrap_or(&edge.source);
if path.starts_with('/') && path.ends_with(".css") {
collect_inline_style(path, &mut inline_styles, &mut visited, template_root, library_roots)?;
} else if path.starts_with('/') && !is_rwe_runtime_path(path) {
collect_inlined_module(
path,
&mut inlined_parts,
&mut visited,
&mut inline_styles,
&mut counter,
&mut rwe_names,
&mut zeb_libs,
template_root,
library_roots,
&mut declared,
&mut sources,
)?;
}
}
{
let alloc = Allocator::default();
let source_type = SourceType::default().with_module(true).with_jsx(true).with_typescript(true);
let parsed = Parser::new(&alloc, page_source, source_type).parse();
for name in collect_top_level_declared_names(&parsed.program) {
if let Some(owner) = declared.get(&name) {
return Err(name_collision(&name, owner, "the page"));
}
}
}
let clean_main = strip_local_imports(page_source, &sources)?;
let mut result = inlined_parts.join("\n\n");
if !result.is_empty() {
result.push('\n');
}
result.push_str(&clean_main);
let mut libs: Vec<String> = zeb_libs.into_iter().collect();
libs.sort();
Ok((result, libs, visited, inline_styles))
}
fn name_collision(name: &str, first: &str, second: &str) -> EngineError {
EngineError::new(
"RWE_BUNDLE_NAME_COLLISION",
format!(
"'{name}' is declared by both {first} and {second}; a page's bundle declares each \
exported name once. Use one of them, or clone and rename the other."
),
)
}
fn collect_inlined_module(
path: &str,
parts: &mut Vec<String>,
visited: &mut HashSet<String>,
inline_styles: &mut Vec<String>,
counter: &mut usize,
rwe_names: &mut HashSet<String>,
zeb_libs: &mut HashSet<String>,
template_root: Option<&str>,
library_roots: &std::collections::BTreeMap<String, String>,
declared: &mut std::collections::HashMap<String, String>,
sources: &mut std::collections::HashMap<String, String>,
) -> Result<(), EngineError> {
ensure_bundle_read_allowed(path, template_root, library_roots)?;
let canonical_path = canonical_module_identity(path)?;
let visit_key = canonical_path.to_string_lossy().to_string();
if visited.contains(&visit_key) {
return Ok(());
}
visited.insert(visit_key);
let raw = fs::read_to_string(&canonical_path).map_err(|e| {
EngineError::new(
"RWE_BUNDLE_READ",
format!("cannot read '{}': {e}", canonical_path.display()),
)
})?;
let content = rewrite_at_imports(&raw, Path::new(template_root.unwrap_or("/")), library_roots)?;
sources.insert(canonical_path.to_string_lossy().to_string(), content.clone());
if let Some(import) = first_relative_import(&content) {
return Err(relative_import_refusal(&import));
}
validate_zeb_hook_imports(&content)?;
rwe_names.extend(extract_rwe_import_names(&content));
zeb_libs.extend(extract_zeb_lib_specifiers(&content));
let sub_paths = extract_filesystem_import_paths(&content);
for sub_path in &sub_paths {
if sub_path.ends_with(".css") {
collect_inline_style(sub_path, inline_styles, visited, template_root, library_roots)?;
} else {
collect_inlined_module(
sub_path,
parts,
visited,
inline_styles,
counter,
rwe_names,
zeb_libs,
template_root,
library_roots,
declared,
sources,
)?;
}
}
let exported = collect_top_level_exported_names(&content);
let this_module = canonical_path.to_string_lossy().to_string();
for name in &exported {
if let Some(owner) = declared.get(name) {
if owner != &this_module {
return Err(name_collision(name, owner, &this_module));
}
}
declared.insert(name.clone(), this_module.clone());
}
let stripped = strip_local_imports(&content, sources)?;
let (masked, masks) = super::js_masker::mask(&stripped);
let localized = localize_exports(&masked);
let prefix = format!("__c{counter}_");
*counter += 1;
let prefixed = prefix_module_locals(&localized, &prefix, &exported);
let processed = super::js_masker::unmask(&prefixed, &masks);
parts.push(processed);
Ok(())
}
fn collect_inline_style(
path: &str,
inline_styles: &mut Vec<String>,
visited: &mut HashSet<String>,
template_root: Option<&str>,
library_roots: &std::collections::BTreeMap<String, String>,
) -> Result<(), EngineError> {
ensure_bundle_read_allowed(path, template_root, library_roots)?;
let canonical_path = canonical_module_identity(path)?;
let visit_key = canonical_path.to_string_lossy().to_string();
if visited.contains(&visit_key) {
return Ok(());
}
visited.insert(visit_key);
let css = fs::read_to_string(&canonical_path).map_err(|e| {
EngineError::new(
"RWE_STYLE_READ",
format!("cannot read stylesheet '{}': {e}", canonical_path.display()),
)
})?;
inline_styles.push(css);
Ok(())
}
fn canonical_module_identity(path: &str) -> Result<PathBuf, EngineError> {
let canonical = canonical_or_fallback(Path::new(path))?;
Ok(normalize_path(&canonical))
}
fn collect_top_level_exported_names(source: &str) -> HashSet<String> {
use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind, ModuleExportName};
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return HashSet::new();
}
let mut exported: HashSet<String> = HashSet::new();
for stmt in &parsed.program.body {
match stmt {
Statement::ExportNamedDeclaration(ed) => {
if let Some(decl) = &ed.declaration {
match decl {
Declaration::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
exported.insert(id.name.as_str().to_string());
}
}
Declaration::ClassDeclaration(c) => {
if let Some(id) = &c.id {
exported.insert(id.name.as_str().to_string());
}
}
Declaration::VariableDeclaration(vd) => {
for d in &vd.declarations {
if let Some(name) = binding_ident_name(&d.id) {
exported.insert(name);
}
}
}
_ => {}
}
}
for spec in &ed.specifiers {
let name = match &spec.exported {
ModuleExportName::IdentifierName(n) => n.name.as_str().to_string(),
ModuleExportName::IdentifierReference(r) => r.name.as_str().to_string(),
ModuleExportName::StringLiteral(s) => s.value.as_str().to_string(),
};
exported.insert(name);
}
}
Statement::ExportDefaultDeclaration(edd) => match &edd.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
exported.insert(id.name.as_str().to_string());
}
}
ExportDefaultDeclarationKind::ClassDeclaration(c) => {
if let Some(id) = &c.id {
exported.insert(id.name.as_str().to_string());
}
}
_ => {}
},
_ => {}
}
}
exported
}
fn collect_top_level_declared_names(program: &oxc_ast::ast::Program<'_>) -> HashSet<String> {
use oxc_ast::ast::Declaration;
let mut declared = HashSet::new();
for stmt in &program.body {
match stmt {
Statement::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
declared.insert(id.name.as_str().to_string());
}
}
Statement::ClassDeclaration(c) => {
if let Some(id) = &c.id {
declared.insert(id.name.as_str().to_string());
}
}
Statement::VariableDeclaration(vd) => {
for d in &vd.declarations {
if let Some(name) = binding_ident_name(&d.id) {
declared.insert(name);
}
}
}
Statement::ExportNamedDeclaration(ed) => {
if let Some(decl) = &ed.declaration {
match decl {
Declaration::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
declared.insert(id.name.as_str().to_string());
}
}
Declaration::ClassDeclaration(c) => {
if let Some(id) = &c.id {
declared.insert(id.name.as_str().to_string());
}
}
Declaration::VariableDeclaration(vd) => {
for d in &vd.declarations {
if let Some(name) = binding_ident_name(&d.id) {
declared.insert(name);
}
}
}
_ => {}
}
}
}
Statement::ExportDefaultDeclaration(ed) => match &ed.declaration {
oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
declared.insert(id.name.as_str().to_string());
}
}
oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(c) => {
if let Some(id) = &c.id {
declared.insert(id.name.as_str().to_string());
}
}
_ => {}
},
_ => {}
}
}
declared
}
fn binding_ident_name(pattern: &oxc_ast::ast::BindingPattern) -> Option<String> {
if let oxc_ast::ast::BindingPattern::BindingIdentifier(id) = pattern {
Some(id.name.as_str().to_string())
} else {
None
}
}
fn prefix_module_locals(source: &str, prefix: &str, exported: &HashSet<String>) -> String {
use oxc_ast::ast::Declaration;
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return source.to_string();
}
let mut local_names: Vec<String> = Vec::new();
for stmt in &parsed.program.body {
match stmt {
Statement::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
let name = id.name.as_str().to_string();
if !exported.contains(&name) {
local_names.push(name);
}
}
}
Statement::ClassDeclaration(c) => {
if let Some(id) = &c.id {
let name = id.name.as_str().to_string();
if !exported.contains(&name) {
local_names.push(name);
}
}
}
Statement::VariableDeclaration(vd) => {
for d in &vd.declarations {
if let Some(name) = binding_ident_name(&d.id) {
if !exported.contains(&name) {
local_names.push(name);
}
}
}
}
Statement::ExportNamedDeclaration(ed) => {
if let Some(decl) = &ed.declaration {
match decl {
Declaration::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
let name = id.name.as_str().to_string();
if !exported.contains(&name) {
local_names.push(name);
}
}
}
Declaration::ClassDeclaration(c) => {
if let Some(id) = &c.id {
let name = id.name.as_str().to_string();
if !exported.contains(&name) {
local_names.push(name);
}
}
}
Declaration::VariableDeclaration(vd) => {
for d in &vd.declarations {
if let Some(name) = binding_ident_name(&d.id) {
if !exported.contains(&name) {
local_names.push(name);
}
}
}
}
_ => {}
}
}
}
_ => {}
}
}
if local_names.is_empty() {
return source.to_string();
}
use oxc_ast::AstKind;
use oxc_span::GetSpan;
let semantic = oxc_semantic::SemanticBuilder::new()
.build(&parsed.program)
.semantic;
let scoping = semantic.scoping();
let mut edits = std::collections::BTreeMap::new();
for symbol in scoping.symbol_ids() {
let name = scoping.symbol_name(symbol);
if scoping.symbol_scope_id(symbol) != scoping.root_scope_id()
|| !local_names.iter().any(|local| local == name)
{
continue;
}
let renamed = format!("{prefix}{name}");
let span = scoping.symbol_span(symbol);
edits.insert((span.start, span.end), renamed.clone());
for reference in scoping.get_resolved_references(symbol) {
let node = semantic.nodes().get_node(reference.node_id());
let span = node.kind().span();
let replacement = match semantic.nodes().parent_kind(reference.node_id()) {
AstKind::ObjectProperty(property) if property.shorthand => {
format!("{name}: {renamed}")
}
_ => renamed.clone(),
};
edits.insert((span.start, span.end), replacement);
}
}
let mut result = source.to_string();
for ((start, end), replacement) in edits.into_iter().rev() {
result.replace_range(start as usize..end as usize, &replacement);
}
result
}
fn is_rwe_runtime_path(path: &str) -> bool {
let fname = path.rsplit('/').next().unwrap_or("");
fname == "rwe.ts" || fname == "rwe.js" || fname == "rwe.tsx"
}
fn extract_rwe_import_names(source: &str) -> Vec<String> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return Vec::new();
}
let mut names = Vec::new();
for stmt in &parsed.program.body {
if let Statement::ImportDeclaration(import) = stmt {
let specifier = import.source.value.as_str();
if specifier.starts_with("zeb/") {
if let Some(ref specifiers) = import.specifiers {
for s in specifiers {
match s {
oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(named) => {
names.push(named.local.name.as_str().to_string());
}
oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
def,
) => {
names.push(def.local.name.as_str().to_string());
}
_ => {}
}
}
}
}
}
}
names
}
fn extract_zeb_lib_specifiers(source: &str) -> Vec<String> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return Vec::new();
}
let mut libs = Vec::new();
for stmt in &parsed.program.body {
if let Statement::ImportDeclaration(import) = stmt {
let specifier = import.source.value.as_str();
if specifier.starts_with("zeb/") && specifier != super::zeb_react::SPECIFIER {
let s = specifier.to_string();
if !libs.contains(&s) {
libs.push(s);
}
}
}
}
libs
}
fn first_relative_import(source: &str) -> Option<String> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return None;
}
parsed.program.body.iter().find_map(|stmt| {
let Statement::ImportDeclaration(import) = stmt else {
return None;
};
let path = import.source.value.as_str();
(path.starts_with("./") || path.starts_with("../")).then(|| path.to_string())
})
}
fn extract_filesystem_import_paths(source: &str) -> Vec<String> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return Vec::new();
}
parsed
.program
.body
.iter()
.filter_map(|stmt| {
if let Statement::ImportDeclaration(import) = stmt {
let path = import.source.value.as_str();
if path.starts_with('/') {
return Some(path.to_string());
}
}
None
})
.collect()
}
fn bind_inlined_import(
import: &oxc_ast::ast::ImportDeclaration<'_>,
path: &str,
sources: &std::collections::HashMap<String, String>,
) -> Result<String, EngineError> {
use oxc_ast::ast::ImportDeclarationSpecifier;
if import.import_kind.is_type() {
return Ok(String::new());
}
let mut out = String::new();
let Some(specifiers) = &import.specifiers else {
return Ok(out);
};
for specifier in specifiers {
let local = specifier.local().name.as_str();
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(named) => {
if named.import_kind.is_type() {
continue;
}
let imported = named.imported.name();
let imported = imported.as_str();
let source_name = if imported == "default" {
default_export_name(path, sources)?
} else {
imported.to_string()
};
if source_name != local {
out.push_str(&format!("const {local} = {source_name};\n"));
}
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => {
let source_name = default_export_name(path, sources)?;
if source_name != local {
out.push_str(&format!("const {local} = {source_name};\n"));
}
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => {
return Err(EngineError::new(
"RWE_IMPORT_NAMESPACE",
format!(
"`import * as {local}` from a component file is not supported — import the names you use"
),
));
}
}
}
Ok(out)
}
fn default_export_name(
path: &str,
sources: &std::collections::HashMap<String, String>,
) -> Result<String, EngineError> {
use oxc_ast::ast::{ExportDefaultDeclarationKind, Expression};
let key = canonical_module_identity(path)?.to_string_lossy().to_string();
let source = match sources.get(&key) {
Some(s) => s.clone(),
None => fs::read_to_string(path).map_err(|e| {
EngineError::new("RWE_BUNDLE_READ", format!("cannot read '{path}': {e}"))
})?,
};
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, &source, source_type).parse();
for stmt in &parsed.program.body {
if let Statement::ExportDefaultDeclaration(ed) = stmt {
let name = match &ed.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(f) => {
f.id.as_ref().map(|id| id.name.to_string())
}
ExportDefaultDeclarationKind::ClassDeclaration(c) => {
c.id.as_ref().map(|id| id.name.to_string())
}
ExportDefaultDeclarationKind::Identifier(id) => Some(id.name.to_string()),
other => match other.as_expression() {
Some(Expression::Identifier(id)) => Some(id.name.to_string()),
_ => None,
},
};
return name.ok_or_else(|| {
EngineError::new(
"RWE_DEFAULT_EXPORT_ANONYMOUS",
format!(
"'{path}' has an anonymous default export; name it (`const X = …; export default X;`) so an importer can bind it"
),
)
});
}
}
Err(EngineError::new(
"RWE_DEFAULT_EXPORT_MISSING",
format!("'{path}' has no default export"),
))
}
fn strip_local_imports(
source: &str,
sources: &std::collections::HashMap<String, String>,
) -> Result<String, EngineError> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return Ok(source.to_string());
}
let mut remove_ranges: Vec<(usize, usize, String)> = Vec::new();
for stmt in &parsed.program.body {
if let Statement::ImportDeclaration(import) = stmt {
let specifier = import.source.value.as_str();
let should_strip = specifier.starts_with("zeb/") || specifier.starts_with('/');
if should_strip {
let start = import.span.start as usize;
let mut end = import.span.end as usize;
if end < source.len() && source.as_bytes()[end] == b'\n' {
end += 1;
}
let replacement = if specifier == super::zeb_react::SPECIFIER {
super::zeb_react::lower_import(import)?
} else if specifier.starts_with('/') {
bind_inlined_import(import, specifier, sources)?
} else {
String::new()
};
remove_ranges.push((start, end, replacement));
}
}
}
if remove_ranges.is_empty() {
return Ok(source.to_string());
}
let mut result = String::with_capacity(source.len());
let mut cursor = 0;
for (start, end, replacement) in &remove_ranges {
if *start > cursor {
result.push_str(&source[cursor..*start]);
}
result.push_str(replacement);
cursor = *end;
}
if cursor < source.len() {
result.push_str(&source[cursor..]);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
#[cfg(unix)]
use std::os::unix::fs::symlink;
fn temp_root(name: &str) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::Builder::new()
.prefix(&format!("{name}-"))
.tempdir()
.expect("tempdir");
let root = tmp.path().to_path_buf();
(tmp, root)
}
#[test]
fn compile_detects_codemirror_library_imports() {
let source = r#"
import { presets } from "zeb/codemirror";
export default function DemoPage() {
return <Page><div data-kind={typeof presets}>ok</div></Page>;
}
"#;
let compiled = compile(source, CompileOptions::default()).expect("compile should succeed");
assert!(
compiled
.detected_zeb_libs
.iter()
.any(|lib| lib == "zeb/codemirror"),
"expected zeb/codemirror to be detected, got {:?}",
compiled.detected_zeb_libs
);
}
#[test]
fn prepare_path_rewrite_is_not_required_for_zeb_imports() {
let (_tmp, root) = temp_root("rwe-zeb-rewrite-test");
let file = root.join("page.tsx");
let source = r#"import { useState } from "zeb/react";
export default function Page() { return <div />; }"#;
fs::write(&file, source).expect("write source");
crate::rwe::core::prepare_template_root(&root).expect("prepare template root");
let rewritten = fs::read_to_string(&file).expect("read rewritten source");
assert!(
rewritten.contains(r#"from "zeb/react""#),
"zeb/react imports must stay logical, got: {rewritten}"
);
}
#[test]
fn an_absolute_import_outside_the_roots_is_never_read() {
let (_tmp, root) = temp_root("rwe-absolute-import-test");
let (_other_tmp, other) = temp_root("rwe-absolute-import-other");
fs::create_dir_all(root.join("components")).expect("create components dir");
fs::write(
root.join("components/ok.tsx"),
"export default function Ok() { return <b>ok</b>; }",
)
.expect("write component");
fs::write(
other.join("secret.tsx"),
"export default function Secret() { return <i>secret</i>; }",
)
.expect("write other");
fs::write(other.join("secret.css"), ".secret { color: red; }").expect("write css");
let options = || CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(root.join("page.tsx").display().to_string()),
..Default::default()
};
for import in [
format!("import Secret from \"{}\";", other.join("secret.tsx").display()),
format!("import \"{}\";", other.join("secret.css").display()),
format!("import Secret from \"{}/../{}/secret.tsx\";", root.display(), other.file_name().unwrap().to_string_lossy()),
] {
let page = format!("{import}\nexport default function Page() {{ return <div />; }}");
let err = compile(&page, options()).expect_err("an absolute import outside the roots");
assert_eq!(err.code, "RWE_IMPORT_BOUNDARY", "{import}: {err:?}");
}
fs::write(
root.join("components/leak.tsx"),
format!(
"import Secret from \"{}\";\nexport default function Leak() {{ return <Secret />; }}",
other.join("secret.tsx").display()
),
)
.expect("write leaking component");
let page = "import Leak from \"@/components/leak\";\nexport default function Page() { return <Leak />; }";
let err = compile(page, options()).expect_err("a component's absolute import");
assert_eq!(err.code, "RWE_IMPORT_BOUNDARY", "{err:?}");
let page = "import Ok from \"@/components/ok\";\nexport default function Page() { return <Ok />; }";
let compiled = compile(page, options()).expect("an @/ import still compiles");
assert!(compiled.client_module_source.contains("ok"));
}
#[test]
fn compile_collects_side_effect_css_imports() {
let (_tmp, root) = temp_root("rwe-css-import-test");
fs::create_dir_all(root.join("styles")).expect("create styles dir");
fs::create_dir_all(root.join("pages")).expect("create pages dir");
fs::write(
root.join("styles/editor.css"),
".editor-shell { color: rgb(1, 2, 3); }",
)
.expect("write css");
let file = root.join("pages/page.tsx");
fs::write(
&file,
r#"
import "@/styles/editor.css";
export default function Page() {
return <div className="editor-shell">ok</div>;
}
"#,
)
.expect("write page");
let compiled = compile(
&fs::read_to_string(&file).expect("read page"),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect("compile should succeed");
assert_eq!(compiled.inline_styles.len(), 1);
assert!(
compiled.inline_styles[0].contains(".editor-shell"),
"expected collected inline CSS, got {:?}",
compiled.inline_styles
);
assert!(
compiled
.dependency_paths
.iter()
.any(|path| path.ends_with("styles/editor.css")),
"expected stylesheet dependency path, got {:?}",
compiled.dependency_paths
);
}
#[test]
fn compile_refuses_a_hook_that_was_never_imported() {
let (_tmp, root) = temp_root("rwe-hook-import");
let file = root.join("page.tsx");
fs::write(
&file,
"export default function Page() { const [n] = useState(0); return <div>{n}</div>; }\n",
)
.expect("write page");
let err = compile(
&fs::read_to_string(&file).unwrap(),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect_err("a hook with no import must be refused");
assert_eq!(err.code, "RWE_HOOK_NOT_IMPORTED");
assert!(err.message.contains("useState"), "{}", err.message);
assert!(err.message.contains("from \"zeb/react\""), "{}", err.message);
}
#[test]
fn compile_accepts_a_hook_imported_from_zeb_react() {
let (_tmp, root) = temp_root("rwe-hook-ok");
let file = root.join("page.tsx");
fs::write(
&file,
"import { useState } from \"zeb/react\";\nexport default function Page() { const [n] = useState(0); return <div>{n}</div>; }\n",
)
.expect("write page");
compile(
&fs::read_to_string(&file).unwrap(),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect("an imported hook compiles");
}
#[test]
fn compile_accepts_a_locally_declared_hook_name() {
let (_tmp, root) = temp_root("rwe-hook-local");
let file = root.join("page.tsx");
fs::write(
&file,
"function useId() { return \"x\"; }\nexport default function Page() { return <div>{useId()}</div>; }\n",
)
.expect("write page");
compile(
&fs::read_to_string(&file).unwrap(),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect("a locally declared name is not the zeb hook");
}
#[test]
fn compile_refuses_a_react_import_and_names_zeb_react() {
let (_tmp, root) = temp_root("rwe-react-import");
for source in [
"import { useState } from \"react\";",
"import { useState } from \"zeb\";",
"import ReactDOM from \"react-dom\";",
"import Preact from \"preact\";",
] {
let file = root.join("page.tsx");
fs::write(
&file,
format!(
"{source}\n\
export default function Page() {{ return <div />; }}\n"
),
)
.expect("write page");
let err = compile(
&fs::read_to_string(&file).unwrap(),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect_err("a runtime import must be refused");
assert!(
err.message.contains("\"zeb/react\""),
"the refusal must name the replacement, got: {}",
err.message
);
}
}
#[test]
fn compile_refuses_a_relative_component_import() {
let (_tmp, root) = temp_root("rwe-relative-import-test");
fs::create_dir_all(root.join("pages")).expect("create pages dir");
fs::create_dir_all(root.join("components")).expect("create components dir");
fs::write(
root.join("components/badge.tsx"),
"export function Badge() { return <span>badge</span>; }\n",
)
.expect("write badge");
let file = root.join("pages/page.tsx");
fs::write(
&file,
r#"
import { Badge } from "../components/badge";
export default function Page() {
return <div><Badge /></div>;
}
"#,
)
.expect("write page");
let err = compile(
&fs::read_to_string(&file).expect("read page"),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect_err("a relative import must be refused");
assert_eq!(err.code, "RWE_IMPORT_NOT_ALLOWED");
assert!(
err.message.contains("@/components/badge"),
"the refusal must name the replacement, got: {}",
err.message
);
}
#[test]
fn compile_refuses_a_relative_stylesheet_import() {
let (_tmp, root) = temp_root("rwe-relative-css-test");
fs::create_dir_all(root.join("pages")).expect("create pages dir");
fs::write(
root.join("pages/editor.css"),
".editor-pane { background: rgb(10, 20, 30); }",
)
.expect("write css");
let file = root.join("pages/page.tsx");
fs::write(
&file,
r#"
import "./editor.css";
export default function Page() {
return <section className="editor-pane">ok</section>;
}
"#,
)
.expect("write page");
let err = compile(
&fs::read_to_string(&file).expect("read page"),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect_err("a relative stylesheet import must be refused");
assert_eq!(err.code, "RWE_IMPORT_NOT_ALLOWED");
assert!(err.message.contains("@/editor.css"), "{}", err.message);
}
#[test]
fn compile_leaves_external_specifiers_alone() {
let (_tmp, root) = temp_root("rwe-external-import-test");
let file = root.join("page.tsx");
fs::write(
&file,
r#"
import confetti from "npm:canvas-confetti";
export default function Page() {
return <div onClick={() => confetti()}>go</div>;
}
"#,
)
.expect("write page");
compile(
&fs::read_to_string(&file).expect("read page"),
CompileOptions {
template_root: Some(root.display().to_string()),
file_path: Some(file.display().to_string()),
..Default::default()
},
)
.expect("an npm: specifier is one of the four legal forms");
}
#[cfg(unix)]
#[test]
fn canonical_module_identity_collapses_symlink_aliases() {
let (_tmp, root) = temp_root("rwe-canonical-module-test");
let target = root.join("real.tsx");
let alias = root.join("alias.tsx");
fs::write(
&target,
"export default function Real() { return <div />; }",
)
.expect("write real file");
symlink(&target, &alias).expect("create symlink");
let real = canonical_module_identity(target.to_str().expect("real path utf8"))
.expect("canonical real");
let aliased = canonical_module_identity(alias.to_str().expect("alias path utf8"))
.expect("canonical alias");
assert_eq!(real, aliased);
}
}
fn localize_exports(source: &str) -> String {
use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind};
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return source.to_string();
}
let src_bytes = source.as_bytes();
let mut ops: Vec<(usize, usize)> = Vec::new();
for stmt in &parsed.program.body {
match stmt {
Statement::ExportNamedDeclaration(ed) => {
let Some(decl) = &ed.declaration else {
continue;
};
let export_start = ed.span.start as usize;
match decl {
Declaration::TSTypeAliasDeclaration(_)
| Declaration::TSInterfaceDeclaration(_) => {
let mut end = ed.span.end as usize;
if end < source.len() && src_bytes[end] == b'\n' {
end += 1;
}
ops.push((export_start, end));
}
Declaration::FunctionDeclaration(f) => {
ops.push((export_start, f.span.start as usize));
}
Declaration::ClassDeclaration(c) => {
ops.push((export_start, c.span.start as usize));
}
Declaration::VariableDeclaration(v) => {
ops.push((export_start, v.span.start as usize));
}
_ => {}
}
}
Statement::ExportDefaultDeclaration(ed) => {
let export_start = ed.span.start as usize;
let inner_start = match &ed.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(f) => f.span.start as usize,
ExportDefaultDeclarationKind::ClassDeclaration(c) => c.span.start as usize,
_ => {
let mut i = export_start;
while i < src_bytes.len() && src_bytes[i] != b' ' && src_bytes[i] != b'\t' {
i += 1;
}
while i < src_bytes.len() && (src_bytes[i] == b' ' || src_bytes[i] == b'\t')
{
i += 1;
}
while i < src_bytes.len()
&& src_bytes[i] != b' '
&& src_bytes[i] != b'\t'
&& src_bytes[i] != b'\n'
{
i += 1;
}
while i < src_bytes.len() && (src_bytes[i] == b' ' || src_bytes[i] == b'\t')
{
i += 1;
}
i
}
};
if inner_start > export_start {
ops.push((export_start, inner_start));
}
}
_ => {}
}
}
if ops.is_empty() {
return source.to_string();
}
ops.sort_by_key(|r| r.0);
let mut result = String::with_capacity(source.len());
let mut cursor = 0;
for (start, end) in &ops {
if *start > cursor {
result.push_str(&source[cursor..*start]);
}
cursor = *end;
}
if cursor < source.len() {
result.push_str(&source[cursor..]);
}
result
}