use oxc_allocator::Allocator;
use oxc_ast::ast::{Expression, Statement};
use oxc_ast_visit::walk;
use oxc_ast_visit::Visit;
use oxc_parser::Parser;
use oxc_span::SourceType;
use vize_carton::cstr;
use vize_carton::String;
use vize_carton::ToCompactString;
#[derive(Debug, Clone)]
pub struct OffsetAdjustment {
pub original_offset: u32,
pub adjustment: i32,
}
#[derive(Debug)]
pub struct RewriteResult {
pub code: String,
pub source_map: ImportSourceMap,
}
#[derive(Debug, Default)]
pub struct ImportSourceMap {
adjustments: Vec<OffsetAdjustment>,
}
impl ImportSourceMap {
pub fn new(adjustments: Vec<OffsetAdjustment>) -> Self {
Self { adjustments }
}
pub fn empty() -> Self {
Self::default()
}
pub fn get_original_offset(&self, virtual_offset: u32) -> u32 {
let mut cumulative: i32 = 0;
for adj in &self.adjustments {
let adjusted = (adj.original_offset as i32 + cumulative) as u32;
if virtual_offset < adjusted {
break;
}
cumulative += adj.adjustment;
}
(virtual_offset as i32 - cumulative) as u32
}
pub fn get_virtual_offset(&self, original_offset: u32) -> u32 {
let mut cumulative: i32 = 0;
for adj in &self.adjustments {
if original_offset < adj.original_offset {
break;
}
cumulative += adj.adjustment;
}
(original_offset as i32 + cumulative) as u32
}
}
pub struct ImportRewriter;
impl ImportRewriter {
pub fn new() -> Self {
Self
}
pub fn rewrite(&self, source: &str, source_type: SourceType) -> RewriteResult {
let allocator = Allocator::default();
let parser = Parser::new(&allocator, source, source_type);
let result = parser.parse();
let mut rewrites: Vec<(u32, u32, String)> = Vec::new();
for stmt in &result.program.body {
match stmt {
Statement::ImportDeclaration(decl) => {
if let Some(rewrite) = self.rewrite_module_specifier(&decl.source.value) {
rewrites.push((
decl.source.span.start + 1, decl.source.span.end - 1, rewrite,
));
}
}
Statement::ExportNamedDeclaration(decl) => {
if let Some(source) = &decl.source {
if let Some(rewrite) = self.rewrite_module_specifier(&source.value) {
rewrites.push((source.span.start + 1, source.span.end - 1, rewrite));
}
}
}
Statement::ExportAllDeclaration(decl) => {
if let Some(rewrite) = self.rewrite_module_specifier(&decl.source.value) {
rewrites.push((
decl.source.span.start + 1,
decl.source.span.end - 1,
rewrite,
));
}
}
_ => {}
}
}
let mut collector = DynamicImportCollector::new();
collector.visit_program(&result.program);
for (start, end, path) in collector.imports {
if let Some(rewrite) = self.rewrite_module_specifier(&path) {
rewrites.push((start, end, rewrite));
}
}
rewrites.sort_by(|a, b| b.0.cmp(&a.0));
let mut output = source.to_compact_string();
let mut adjustments = Vec::new();
for (start, end, new_path) in rewrites {
let original_len = (end - start) as i32;
let new_len = new_path.len() as i32;
output.replace_range(start as usize..end as usize, new_path.as_str());
adjustments.push(OffsetAdjustment {
original_offset: start,
adjustment: new_len - original_len,
});
}
adjustments.reverse();
RewriteResult {
code: output,
source_map: ImportSourceMap::new(adjustments),
}
}
fn rewrite_module_specifier(&self, path: &str) -> Option<String> {
if path.ends_with(".vue") && (path.starts_with("./") || path.starts_with("../")) {
Some(cstr!("{path}.ts"))
} else {
None
}
}
}
impl Default for ImportRewriter {
fn default() -> Self {
Self::new()
}
}
struct DynamicImportCollector {
imports: Vec<(u32, u32, String)>,
}
impl DynamicImportCollector {
fn new() -> Self {
Self {
imports: Vec::new(),
}
}
}
impl<'a> Visit<'a> for DynamicImportCollector {
fn visit_import_expression(&mut self, expr: &oxc_ast::ast::ImportExpression<'a>) {
if let Expression::StringLiteral(lit) = &expr.source {
self.imports.push((
lit.span.start + 1, lit.span.end - 1, lit.value.as_str().into(),
));
}
walk::walk_import_expression(self, expr);
}
}
#[cfg(test)]
mod tests {
use super::ImportRewriter;
use oxc_span::SourceType;
#[test]
fn test_rewrite_default_import() {
let rewriter = ImportRewriter::new();
let source = r#"import App from './App.vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(result.code, r#"import App from './App.vue.ts';"#);
}
#[test]
fn test_rewrite_named_import() {
let rewriter = ImportRewriter::new();
let source = r#"import { helper, type Props } from './helper.vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(
result.code,
r#"import { helper, type Props } from './helper.vue.ts';"#
);
}
#[test]
fn test_rewrite_side_effect_import() {
let rewriter = ImportRewriter::new();
let source = r#"import './global.vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(result.code, r#"import './global.vue.ts';"#);
}
#[test]
fn test_no_rewrite_npm_import() {
let rewriter = ImportRewriter::new();
let source = r#"import { ref } from 'vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(result.code, r#"import { ref } from 'vue';"#);
}
#[test]
fn test_rewrite_export_from() {
let rewriter = ImportRewriter::new();
let source = r#"export { default as App } from './App.vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(
result.code,
r#"export { default as App } from './App.vue.ts';"#
);
}
#[test]
fn test_rewrite_dynamic_import() {
let rewriter = ImportRewriter::new();
let source = r#"const App = () => import('./App.vue');"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(result.code, r#"const App = () => import('./App.vue.ts');"#);
}
#[test]
fn test_rewrite_parent_path() {
let rewriter = ImportRewriter::new();
let source = r#"import Parent from '../Parent.vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert_eq!(result.code, r#"import Parent from '../Parent.vue.ts';"#);
}
#[test]
fn test_source_map_offset() {
let rewriter = ImportRewriter::new();
let source = r#"import App from './App.vue';
import { ref } from 'vue';
const x = 1;"#;
let result = rewriter.rewrite(source, SourceType::ts());
let virtual_offset = 30; let original_offset = result.source_map.get_original_offset(virtual_offset);
assert!(original_offset < virtual_offset);
}
#[test]
fn test_multiple_rewrites() {
let rewriter = ImportRewriter::new();
let source = r#"import App from './App.vue';
import Child from './Child.vue';
import { ref } from 'vue';"#;
let result = rewriter.rewrite(source, SourceType::ts());
assert!(result.code.contains("./App.vue.ts"));
assert!(result.code.contains("./Child.vue.ts"));
assert!(result.code.contains("from 'vue'"));
}
}