farmfe_toolkit 2.1.0

Toolkit for farm.
use std::sync::Arc;

use farmfe_core::{
  context::CompilationContext,
  module::ModuleSystem,
  plugin::{PluginFinalizeModuleHookParam, ResolveKind},
  swc_common::Mark,
  swc_ecma_ast::{Expr, Ident, IdentName, MemberProp, Module as SwcModule, ModuleItem, Stmt},
};
use swc_ecma_visit::{Visit, VisitWith};

use crate::script::swc_try_with::try_with;

fn module_system_from_deps(deps: Vec<ResolveKind>) -> ModuleSystem {
  let mut module_system = ModuleSystem::Custom(String::from("unknown"));

  for resolve_kind in deps {
    if matches!(resolve_kind, ResolveKind::Import)
      || matches!(resolve_kind, ResolveKind::DynamicImport)
      || matches!(resolve_kind, ResolveKind::ExportFrom)
    {
      match module_system {
        ModuleSystem::EsModule => continue,
        ModuleSystem::CommonJs => {
          module_system = ModuleSystem::Hybrid;
          break;
        }
        _ => module_system = ModuleSystem::EsModule,
      }
    } else if matches!(resolve_kind, ResolveKind::Require) {
      match module_system {
        ModuleSystem::CommonJs => continue,
        ModuleSystem::EsModule => {
          module_system = ModuleSystem::Hybrid;
          break;
        }
        _ => module_system = ModuleSystem::CommonJs,
      }
    }
  }

  module_system
}

struct ModuleSystemAnalyzer {
  unresolved_mark: Mark,
  contain_module_exports: bool,
  contain_esm: bool,
}

impl Visit for ModuleSystemAnalyzer {
  fn visit_ident(&mut self, node: &Ident) {
    if (node.sym == "module" || node.sym == "exports") && node.ctxt.outer() == self.unresolved_mark
    {
      self.contain_module_exports = true;
    }
  }

  fn visit_stmt(&mut self, n: &Stmt) {
    if self.contain_module_exports && self.contain_esm {
      return;
    }

    n.visit_children_with(self);
  }

  fn visit_member_expr(&mut self, n: &farmfe_core::swc_ecma_ast::MemberExpr) {
    if self.contain_module_exports {
      return;
    }

    if let box Expr::Ident(Ident { sym, ctxt, .. }) = &n.obj {
      if sym == "module" && ctxt.outer() == self.unresolved_mark {
        if let MemberProp::Ident(IdentName { sym, .. }) = &n.prop {
          if sym == "exports" {
            self.contain_module_exports = true;
          } else {
            n.visit_children_with(self);
          }
        } else {
          n.visit_children_with(self);
        }
      } else if sym == "exports" && ctxt.outer() == self.unresolved_mark {
        self.contain_module_exports = true;
      } else {
        n.visit_children_with(self);
      }
    } else {
      n.visit_children_with(self);
    }
  }

  fn visit_module_decl(&mut self, n: &farmfe_core::swc_ecma_ast::ModuleDecl) {
    if self.contain_esm && self.contain_module_exports {
      return;
    }

    self.contain_esm = true;

    n.visit_children_with(self);
  }
}

pub fn module_system_from_ast(ast: &SwcModule, module_system: ModuleSystem) -> ModuleSystem {
  if module_system != ModuleSystem::Hybrid {
    // if the ast contains ModuleDecl, it's a esm module
    for item in ast.body.iter() {
      if let ModuleItem::ModuleDecl(_) = item {
        if module_system == ModuleSystem::CommonJs {
          return ModuleSystem::Hybrid;
        } else {
          return ModuleSystem::EsModule;
        }
      }
    }
  }

  module_system
}

pub fn set_module_system_for_module_meta(
  param: &mut PluginFinalizeModuleHookParam,
  context: &Arc<CompilationContext>,
) {
  if param.module.meta.as_script().module_system != ModuleSystem::UnInitial {
    return;
  }

  // default to commonjs
  let module_system_from_deps_option = if !param.deps.is_empty() {
    module_system_from_deps(param.deps.iter().map(|d| d.kind.clone()).collect())
  } else {
    ModuleSystem::UnInitial
  };

  let unresolved_mark = Mark::from_u32(param.module.meta.as_script().unresolved_mark);
  let mut analyzer = ModuleSystemAnalyzer {
    unresolved_mark,
    contain_module_exports: false,
    contain_esm: false,
  };

  let ast = &param.module.meta.as_script().ast;

  let mut module_system_from_ast: ModuleSystem = ModuleSystem::UnInitial;
  {
    let cm = context.meta.get_module_source_map(&param.module.id);

    try_with(
      cm,
      context.meta.get_globals(&param.module.id).value(),
      || {
        ast.visit_with(&mut analyzer);

        if analyzer.contain_module_exports {
          module_system_from_ast = module_system_from_ast.merge(ModuleSystem::CommonJs);
        }

        if analyzer.contain_esm {
          module_system_from_ast = module_system_from_ast.merge(ModuleSystem::EsModule);
        }
      },
    )
    .unwrap();
  }

  let mut v = [module_system_from_deps_option, module_system_from_ast]
    .into_iter()
    .reduce(|a, b| a.merge(b))
    .unwrap_or(ModuleSystem::UnInitial);

  if matches!(v, ModuleSystem::UnInitial) {
    // v = ModuleSystem::Hybrid;
    v = ModuleSystem::EsModule;
  }

  let meta = param.module.meta.as_script_mut();
  meta.module_system = v;
  meta.contains_esm_decl = analyzer.contain_esm;
  meta.contains_module_exports = analyzer.contain_module_exports;
}