Skip to main content

node_resolver/
analyze.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::collections::BTreeSet;
5use std::collections::HashSet;
6use std::path::Path;
7use std::path::PathBuf;
8
9use deno_error::JsErrorBox;
10use deno_path_util::url_to_file_path;
11use futures::FutureExt;
12use futures::StreamExt;
13use futures::future::LocalBoxFuture;
14use futures::stream::FuturesUnordered;
15use once_cell::sync::Lazy;
16use serde::Deserialize;
17use serde::Serialize;
18use url::Url;
19
20use crate::InNpmPackageChecker;
21use crate::IsBuiltInNodeModuleChecker;
22use crate::NodeResolutionKind;
23use crate::NodeResolverSys;
24use crate::NpmPackageFolderResolver;
25use crate::PackageJsonResolverRc;
26use crate::PathClean;
27use crate::ResolutionMode;
28use crate::UrlOrPath;
29use crate::UrlOrPathRef;
30use crate::errors::ModuleNotFoundError;
31use crate::resolution::NodeResolverRc;
32use crate::resolution::parse_npm_pkg_name;
33
34#[derive(Debug, Clone)]
35pub enum CjsAnalysis<'a> {
36  /// File was found to be an ES module and the translator should
37  /// load the code as ESM.
38  Esm(Cow<'a, str>, Option<CjsAnalysisExports>),
39  Cjs(CjsAnalysisExports),
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CjsAnalysisExports {
44  pub exports: Vec<String>,
45  pub reexports: Vec<String>,
46}
47
48/// What parts of an ES module should be analyzed.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum EsmAnalysisMode {
51  SourceOnly,
52  SourceImportsAndExports,
53}
54
55/// Code analyzer for CJS and ESM files.
56#[async_trait::async_trait(?Send)]
57pub trait CjsCodeAnalyzer {
58  /// Analyzes CommonJs code for exports and reexports, which is
59  /// then used to determine the wrapper ESM module exports.
60  ///
61  /// Note that the source is provided by the caller when the caller
62  /// already has it. If the source is needed by the implementation,
63  /// then it can use the provided source, or otherwise load it if
64  /// necessary.
65  async fn analyze_cjs<'a>(
66    &self,
67    specifier: &Url,
68    maybe_source: Option<Cow<'a, str>>,
69    esm_analysis_mode: EsmAnalysisMode,
70  ) -> Result<CjsAnalysis<'a>, JsErrorBox>;
71}
72
73pub enum ResolvedCjsAnalysis<'a> {
74  Esm(Cow<'a, str>),
75  Cjs(BTreeSet<String>),
76}
77
78#[sys_traits::auto_impl]
79pub trait CjsModuleExportAnalyzerSys: NodeResolverSys {}
80
81#[allow(clippy::disallowed_types)]
82pub type CjsModuleExportAnalyzerRc<
83  TCjsCodeAnalyzer,
84  TInNpmPackageChecker,
85  TIsBuiltInNodeModuleChecker,
86  TNpmPackageFolderResolver,
87  TSys,
88> = deno_maybe_sync::MaybeArc<
89  CjsModuleExportAnalyzer<
90    TCjsCodeAnalyzer,
91    TInNpmPackageChecker,
92    TIsBuiltInNodeModuleChecker,
93    TNpmPackageFolderResolver,
94    TSys,
95  >,
96>;
97
98pub struct CjsModuleExportAnalyzer<
99  TCjsCodeAnalyzer: CjsCodeAnalyzer,
100  TInNpmPackageChecker: InNpmPackageChecker,
101  TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker,
102  TNpmPackageFolderResolver: NpmPackageFolderResolver,
103  TSys: CjsModuleExportAnalyzerSys,
104> {
105  cjs_code_analyzer: TCjsCodeAnalyzer,
106  in_npm_pkg_checker: TInNpmPackageChecker,
107  node_resolver: NodeResolverRc<
108    TInNpmPackageChecker,
109    TIsBuiltInNodeModuleChecker,
110    TNpmPackageFolderResolver,
111    TSys,
112  >,
113  npm_resolver: TNpmPackageFolderResolver,
114  pkg_json_resolver: PackageJsonResolverRc<TSys>,
115  sys: TSys,
116}
117
118impl<
119  TCjsCodeAnalyzer: CjsCodeAnalyzer,
120  TInNpmPackageChecker: InNpmPackageChecker,
121  TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker,
122  TNpmPackageFolderResolver: NpmPackageFolderResolver,
123  TSys: CjsModuleExportAnalyzerSys,
124>
125  CjsModuleExportAnalyzer<
126    TCjsCodeAnalyzer,
127    TInNpmPackageChecker,
128    TIsBuiltInNodeModuleChecker,
129    TNpmPackageFolderResolver,
130    TSys,
131  >
132{
133  pub fn new(
134    cjs_code_analyzer: TCjsCodeAnalyzer,
135    in_npm_pkg_checker: TInNpmPackageChecker,
136    node_resolver: NodeResolverRc<
137      TInNpmPackageChecker,
138      TIsBuiltInNodeModuleChecker,
139      TNpmPackageFolderResolver,
140      TSys,
141    >,
142    npm_resolver: TNpmPackageFolderResolver,
143    pkg_json_resolver: PackageJsonResolverRc<TSys>,
144    sys: TSys,
145  ) -> Self {
146    Self {
147      cjs_code_analyzer,
148      in_npm_pkg_checker,
149      node_resolver,
150      npm_resolver,
151      pkg_json_resolver,
152      sys,
153    }
154  }
155
156  pub async fn analyze_all_exports<'a>(
157    &self,
158    entry_specifier: &Url,
159    source: Option<Cow<'a, str>>,
160  ) -> Result<ResolvedCjsAnalysis<'a>, TranslateCjsToEsmError> {
161    let analysis = self
162      .cjs_code_analyzer
163      .analyze_cjs(entry_specifier, source, EsmAnalysisMode::SourceOnly)
164      .await
165      .map_err(TranslateCjsToEsmError::CjsCodeAnalysis)?;
166
167    let analysis = match analysis {
168      CjsAnalysis::Esm(source, _) => {
169        return Ok(ResolvedCjsAnalysis::Esm(source));
170      }
171      CjsAnalysis::Cjs(analysis) => analysis,
172    };
173
174    // use a BTreeSet to make the output deterministic for v8's code cache
175    let mut all_exports = analysis.exports.into_iter().collect::<BTreeSet<_>>();
176
177    if !analysis.reexports.is_empty() {
178      let mut errors = Vec::new();
179      self
180        .analyze_reexports(
181          entry_specifier,
182          analysis.reexports,
183          &mut all_exports,
184          &mut errors,
185        )
186        .await;
187
188      // surface errors afterwards in a deterministic way
189      if !errors.is_empty() {
190        errors.sort_by_cached_key(|e| e.to_string());
191        return Err(TranslateCjsToEsmError::ExportAnalysis(errors.remove(0)));
192      }
193    }
194
195    Ok(ResolvedCjsAnalysis::Cjs(all_exports))
196  }
197
198  #[allow(clippy::needless_lifetimes)]
199  async fn analyze_reexports<'a>(
200    &'a self,
201    entry_specifier: &url::Url,
202    reexports: Vec<String>,
203    all_exports: &mut BTreeSet<String>,
204    // this goes through the modules concurrently, so collect
205    // the errors in order to be deterministic
206    errors: &mut Vec<JsErrorBox>,
207  ) {
208    struct Analysis {
209      reexport_specifier: url::Url,
210      analysis: CjsAnalysis<'static>,
211    }
212
213    type AnalysisFuture<'a> = LocalBoxFuture<'a, Result<Analysis, JsErrorBox>>;
214
215    let mut handled_reexports: HashSet<Url> = HashSet::default();
216    handled_reexports.insert(entry_specifier.clone());
217    let mut analyze_futures: FuturesUnordered<AnalysisFuture<'a>> =
218      FuturesUnordered::new();
219    let cjs_code_analyzer = &self.cjs_code_analyzer;
220    let mut handle_reexports =
221      |referrer: url::Url,
222       reexports: Vec<String>,
223       analyze_futures: &mut FuturesUnordered<AnalysisFuture<'a>>,
224       errors: &mut Vec<JsErrorBox>| {
225        // 1. Resolve the re-exports and start a future to analyze each one
226        for reexport in reexports {
227          let result = self
228            .resolve(
229              &reexport,
230              &referrer,
231              // FIXME(bartlomieju): check if these conditions are okay, probably
232              // should be `deno-require`, because `deno` is already used in `esm_resolver.rs`
233              &[
234                Cow::Borrowed("deno"),
235                Cow::Borrowed("node"),
236                Cow::Borrowed("require"),
237                Cow::Borrowed("default"),
238              ],
239              NodeResolutionKind::Execution,
240            )
241            .and_then(|value| {
242              value
243                .map(|url_or_path| url_or_path.into_url())
244                .transpose()
245                .map_err(JsErrorBox::from_err)
246            });
247          let reexport_specifier = match result {
248            Ok(Some(specifier)) => specifier,
249            Ok(None) => continue,
250            Err(err) => {
251              errors.push(err);
252              continue;
253            }
254          };
255
256          if !handled_reexports.insert(reexport_specifier.clone()) {
257            continue;
258          }
259
260          let referrer = referrer.clone();
261          let future = async move {
262            let analysis = cjs_code_analyzer
263              .analyze_cjs(
264                &reexport_specifier,
265                None,
266                EsmAnalysisMode::SourceImportsAndExports,
267              )
268              .await
269              .map_err(|source| {
270                JsErrorBox::from_err(CjsAnalysisCouldNotLoadError {
271                  reexport,
272                  reexport_specifier: reexport_specifier.clone(),
273                  referrer: referrer.clone(),
274                  source,
275                })
276              })?;
277
278            Ok(Analysis {
279              reexport_specifier,
280              analysis,
281            })
282          }
283          .boxed_local();
284          analyze_futures.push(future);
285        }
286      };
287
288    handle_reexports(
289      entry_specifier.clone(),
290      reexports,
291      &mut analyze_futures,
292      errors,
293    );
294
295    while let Some(analysis_result) = analyze_futures.next().await {
296      // 2. Look at the analysis result and resolve its exports and re-exports
297      let Analysis {
298        reexport_specifier,
299        analysis,
300      } = match analysis_result {
301        Ok(analysis) => analysis,
302        Err(err) => {
303          errors.push(err);
304          continue;
305        }
306      };
307      match analysis {
308        CjsAnalysis::Cjs(analysis) | CjsAnalysis::Esm(_, Some(analysis)) => {
309          if !analysis.reexports.is_empty() {
310            handle_reexports(
311              reexport_specifier.clone(),
312              analysis.reexports,
313              &mut analyze_futures,
314              errors,
315            );
316          }
317
318          all_exports.extend(
319            analysis
320              .exports
321              .into_iter()
322              .filter(|e| e.as_str() != "default"),
323          );
324        }
325        CjsAnalysis::Esm(_, None) => {
326          // should not hit this due to EsmAnalysisMode::SourceImportsAndExports
327          debug_assert!(false);
328        }
329      }
330    }
331  }
332
333  // todo(dsherret): what is going on here? Isn't this a bunch of duplicate code?
334  fn resolve(
335    &self,
336    specifier: &str,
337    referrer: &Url,
338    conditions: &[Cow<'static, str>],
339    resolution_kind: NodeResolutionKind,
340  ) -> Result<Option<UrlOrPath>, JsErrorBox> {
341    if specifier.starts_with('/') {
342      todo!();
343    }
344
345    let referrer = UrlOrPathRef::from_url(referrer);
346    let referrer_path = referrer.path().unwrap();
347    if specifier.starts_with("./") || specifier.starts_with("../") {
348      if let Some(parent) = referrer_path.parent() {
349        return self
350          .file_extension_probe(parent.join(specifier), referrer_path)
351          .map(|p| Some(UrlOrPath::Path(p)));
352      } else {
353        todo!();
354      }
355    }
356
357    // We've got a bare specifier or maybe bare_specifier/blah.js"
358    let (package_specifier, package_subpath, _is_scoped) =
359      parse_npm_pkg_name(specifier, &referrer).map_err(JsErrorBox::from_err)?;
360
361    let module_dir = match self
362      .npm_resolver
363      .resolve_package_folder_from_package(package_specifier, &referrer)
364    {
365      Err(err)
366        if matches!(
367          err.as_kind(),
368          crate::errors::PackageFolderResolveErrorKind::PackageNotFound(..)
369        ) =>
370      {
371        return Ok(None);
372      }
373      other => other.map_err(JsErrorBox::from_err)?,
374    };
375
376    let package_json_path = module_dir.join("package.json");
377    let maybe_package_json = self
378      .pkg_json_resolver
379      .load_package_json(&package_json_path)
380      .map_err(JsErrorBox::from_err)?;
381    if let Some(package_json) = maybe_package_json {
382      if let Some(exports) = &package_json.exports {
383        return Some(
384          self
385            .node_resolver
386            .package_exports_resolve(
387              &package_json_path,
388              &package_subpath,
389              exports,
390              Some(&referrer),
391              ResolutionMode::Require,
392              conditions,
393              resolution_kind,
394            )
395            .map_err(JsErrorBox::from_err),
396        )
397        .transpose();
398      }
399
400      // old school
401      if package_subpath != "." {
402        let d = module_dir.join(package_subpath.as_ref());
403        if self.sys.fs_is_dir_no_err(&d) {
404          // subdir might have a package.json that specifies the entrypoint
405          let package_json_path = d.join("package.json");
406          let maybe_package_json = self
407            .pkg_json_resolver
408            .load_package_json(&package_json_path)
409            .map_err(JsErrorBox::from_err)?;
410          if let Some(package_json) = maybe_package_json
411            && let Some(main) =
412              self.node_resolver.legacy_fallback_resolve(&package_json)
413          {
414            return Ok(Some(UrlOrPath::Path(d.join(main).clean())));
415          }
416
417          return Ok(Some(UrlOrPath::Path(d.join("index.js").clean())));
418        }
419        return self
420          .file_extension_probe(d, referrer_path)
421          .map(|p| Some(UrlOrPath::Path(p)));
422      } else if let Some(main) =
423        self.node_resolver.legacy_fallback_resolve(&package_json)
424      {
425        return Ok(Some(UrlOrPath::Path(module_dir.join(main).clean())));
426      } else {
427        return Ok(Some(UrlOrPath::Path(module_dir.join("index.js").clean())));
428      }
429    }
430
431    // as a fallback, attempt to resolve it via the ancestor directories
432    let mut last = referrer_path;
433    while let Some(parent) = last.parent() {
434      if !self.in_npm_pkg_checker.in_npm_package_at_dir_path(parent) {
435        break;
436      }
437      let path = if parent.ends_with("node_modules") {
438        parent.join(specifier)
439      } else {
440        parent.join("node_modules").join(specifier)
441      };
442      if let Ok(path) = self.file_extension_probe(path, referrer_path) {
443        return Ok(Some(UrlOrPath::Path(path)));
444      }
445      last = parent;
446    }
447
448    Err(JsErrorBox::from_err(ModuleNotFoundError {
449      specifier: UrlOrPath::Path(PathBuf::from(specifier)),
450      maybe_referrer: Some(UrlOrPath::Path(referrer_path.to_path_buf())),
451      suggested_ext: None,
452    }))
453  }
454
455  fn file_extension_probe(
456    &self,
457    p: PathBuf,
458    referrer: &Path,
459  ) -> Result<PathBuf, JsErrorBox> {
460    let p = p.clean();
461    if self.sys.fs_exists_no_err(&p) {
462      let file_name = p.file_name().unwrap();
463      let p_js =
464        p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
465      if self.sys.fs_is_file_no_err(&p_js) {
466        return Ok(p_js);
467      } else if self.sys.fs_is_dir_no_err(&p) {
468        return Ok(p.join("index.js"));
469      } else {
470        return Ok(p);
471      }
472    } else if let Some(file_name) = p.file_name() {
473      {
474        let p_js =
475          p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
476        if self.sys.fs_is_file_no_err(&p_js) {
477          return Ok(p_js);
478        }
479      }
480      {
481        let p_json =
482          p.with_file_name(format!("{}.json", file_name.to_str().unwrap()));
483        if self.sys.fs_is_file_no_err(&p_json) {
484          return Ok(p_json);
485        }
486      }
487    }
488    Err(JsErrorBox::from_err(ModuleNotFoundError {
489      specifier: UrlOrPath::Path(p),
490      maybe_referrer: Some(UrlOrPath::Path(referrer.to_path_buf())),
491      suggested_ext: None,
492    }))
493  }
494}
495
496#[derive(Debug, thiserror::Error, deno_error::JsError)]
497pub enum TranslateCjsToEsmError {
498  #[class(inherit)]
499  #[error(transparent)]
500  CjsCodeAnalysis(JsErrorBox),
501  #[class(inherit)]
502  #[error(transparent)]
503  ExportAnalysis(JsErrorBox),
504}
505
506#[derive(Debug, thiserror::Error, deno_error::JsError)]
507#[class(generic)]
508#[error(
509  "Could not load '{reexport}' ({reexport_specifier}) referenced from {referrer}"
510)]
511pub struct CjsAnalysisCouldNotLoadError {
512  reexport: String,
513  reexport_specifier: Url,
514  referrer: Url,
515  #[source]
516  source: JsErrorBox,
517}
518
519#[sys_traits::auto_impl]
520pub trait NodeCodeTranslatorSys: CjsModuleExportAnalyzerSys {}
521
522#[allow(clippy::disallowed_types)]
523pub type NodeCodeTranslatorRc<
524  TCjsCodeAnalyzer,
525  TInNpmPackageChecker,
526  TIsBuiltInNodeModuleChecker,
527  TNpmPackageFolderResolver,
528  TSys,
529> = deno_maybe_sync::MaybeArc<
530  NodeCodeTranslator<
531    TCjsCodeAnalyzer,
532    TInNpmPackageChecker,
533    TIsBuiltInNodeModuleChecker,
534    TNpmPackageFolderResolver,
535    TSys,
536  >,
537>;
538
539pub struct NodeCodeTranslator<
540  TCjsCodeAnalyzer: CjsCodeAnalyzer,
541  TInNpmPackageChecker: InNpmPackageChecker,
542  TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker,
543  TNpmPackageFolderResolver: NpmPackageFolderResolver,
544  TSys: NodeCodeTranslatorSys,
545> {
546  module_export_analyzer: CjsModuleExportAnalyzerRc<
547    TCjsCodeAnalyzer,
548    TInNpmPackageChecker,
549    TIsBuiltInNodeModuleChecker,
550    TNpmPackageFolderResolver,
551    TSys,
552  >,
553  mode: NodeCodeTranslatorMode,
554}
555
556#[derive(Debug, Default, Clone, Copy)]
557pub enum NodeCodeTranslatorMode {
558  Disabled,
559  #[default]
560  ModuleLoader,
561}
562
563impl<
564  TCjsCodeAnalyzer: CjsCodeAnalyzer,
565  TInNpmPackageChecker: InNpmPackageChecker,
566  TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker,
567  TNpmPackageFolderResolver: NpmPackageFolderResolver,
568  TSys: NodeCodeTranslatorSys,
569>
570  NodeCodeTranslator<
571    TCjsCodeAnalyzer,
572    TInNpmPackageChecker,
573    TIsBuiltInNodeModuleChecker,
574    TNpmPackageFolderResolver,
575    TSys,
576  >
577{
578  pub fn new(
579    module_export_analyzer: CjsModuleExportAnalyzerRc<
580      TCjsCodeAnalyzer,
581      TInNpmPackageChecker,
582      TIsBuiltInNodeModuleChecker,
583      TNpmPackageFolderResolver,
584      TSys,
585    >,
586    mode: NodeCodeTranslatorMode,
587  ) -> Self {
588    Self {
589      module_export_analyzer,
590      mode,
591    }
592  }
593
594  /// Translates given CJS module into ESM. This function will perform static
595  /// analysis on the file to find defined exports and reexports.
596  ///
597  /// For all discovered reexports the analysis will be performed recursively.
598  ///
599  /// If successful a source code for equivalent ES module is returned.
600  pub async fn translate_cjs_to_esm<'a>(
601    &self,
602    entry_specifier: &Url,
603    source: Option<Cow<'a, str>>,
604  ) -> Result<Cow<'a, str>, TranslateCjsToEsmError> {
605    let all_exports = if matches!(self.mode, NodeCodeTranslatorMode::Disabled) {
606      return Ok(source.unwrap());
607    } else {
608      let analysis = self
609        .module_export_analyzer
610        .analyze_all_exports(entry_specifier, source)
611        .await?;
612
613      match analysis {
614        ResolvedCjsAnalysis::Esm(source) => return Ok(source),
615        ResolvedCjsAnalysis::Cjs(all_exports) => all_exports,
616      }
617    };
618    Ok(Cow::Owned(exports_to_wrapper_module(
619      entry_specifier,
620      &all_exports,
621    )))
622  }
623}
624
625static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| {
626  HashSet::from([
627    "abstract",
628    "arguments",
629    "async",
630    "await",
631    "boolean",
632    "break",
633    "byte",
634    "case",
635    "catch",
636    "char",
637    "class",
638    "const",
639    "continue",
640    "debugger",
641    "default",
642    "delete",
643    "do",
644    "double",
645    "else",
646    "enum",
647    "eval",
648    "export",
649    "extends",
650    "false",
651    "final",
652    "finally",
653    "float",
654    "for",
655    "function",
656    "get",
657    "goto",
658    "if",
659    "implements",
660    "import",
661    "in",
662    "instanceof",
663    "int",
664    "interface",
665    "let",
666    "long",
667    "mod",
668    "native",
669    "new",
670    "null",
671    "package",
672    "private",
673    "protected",
674    "public",
675    "return",
676    "set",
677    "short",
678    "static",
679    "super",
680    "switch",
681    "synchronized",
682    "this",
683    "throw",
684    "throws",
685    "transient",
686    "true",
687    "try",
688    "typeof",
689    "var",
690    "void",
691    "volatile",
692    "while",
693    "with",
694    "yield",
695  ])
696});
697
698fn exports_to_wrapper_module(
699  entry_specifier: &Url,
700  all_exports: &BTreeSet<String>,
701) -> String {
702  let quoted_entry_specifier_text = to_double_quote_string(
703    url_to_file_path(entry_specifier).unwrap().to_str().unwrap(),
704  );
705  let export_names_with_quoted = all_exports
706    .iter()
707    .map(|export| (export.as_str(), to_double_quote_string(export)))
708    .collect::<Vec<_>>();
709  capacity_builder::StringBuilder::<String>::build(|builder| {
710      let mut temp_var_count = 0;
711      builder.append(
712        r#"import { createRequire as __internalCreateRequire, Module as __internalModule } from "node:module";
713const require = __internalCreateRequire(import.meta.url);
714let mod;
715if (import.meta.main) {
716  mod = __internalModule._load("#,
717      );
718      builder.append(&quoted_entry_specifier_text);
719      builder.append(
720        r#", null, true)
721} else {
722  mod = require("#,
723      );
724      builder.append(&quoted_entry_specifier_text);
725      builder.append(r#");
726}
727"#);
728
729      for (export_name, quoted_name) in &export_names_with_quoted {
730        if !matches!(*export_name, "default" | "module.exports") {
731          add_export(
732            builder,
733            export_name,
734            quoted_name,
735            |builder| {
736              builder.append("mod[");
737              builder.append(quoted_name);
738              builder.append("]");
739            },
740            &mut temp_var_count,
741          );
742        }
743      }
744
745      builder.append("export default mod;\n");
746      add_export(
747        builder,
748        "module.exports",
749        "\"module.exports\"",
750        |builder| builder.append("mod"),
751        &mut temp_var_count,
752      );
753    }).unwrap()
754}
755
756fn add_export<'a>(
757  builder: &mut capacity_builder::StringBuilder<'a, String>,
758  name: &'a str,
759  quoted_name: &'a str,
760  build_initializer: impl FnOnce(&mut capacity_builder::StringBuilder<'a, String>),
761  temp_var_count: &mut usize,
762) {
763  fn is_valid_var_decl(name: &str) -> bool {
764    // it's ok to be super strict here
765    if name.is_empty() {
766      return false;
767    }
768
769    if let Some(first) = name.chars().next()
770      && !first.is_ascii_alphabetic()
771      && first != '_'
772      && first != '$'
773    {
774      return false;
775    }
776
777    name
778      .chars()
779      .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
780  }
781
782  // TODO(bartlomieju): Node actually checks if a given export exists in `exports` object,
783  // but it might not be necessary here since our analysis is more detailed?
784  if RESERVED_WORDS.contains(name) || !is_valid_var_decl(name) {
785    *temp_var_count += 1;
786    // we can't create an identifier with a reserved word or invalid identifier name,
787    // so assign it to a temporary variable that won't have a conflict, then re-export
788    // it as a string
789    builder.append("const __deno_export_");
790    builder.append(*temp_var_count);
791    builder.append("__ = ");
792    build_initializer(builder);
793    builder.append(";\nexport { __deno_export_");
794    builder.append(*temp_var_count);
795    builder.append("__ as ");
796    builder.append(quoted_name);
797    builder.append(" };\n");
798  } else {
799    builder.append("export const ");
800    builder.append(name);
801    builder.append(" = ");
802    build_initializer(builder);
803    builder.append(";\n");
804  }
805}
806
807fn to_double_quote_string(text: &str) -> String {
808  // serde can handle this for us
809  serde_json::to_string(text).unwrap()
810}
811
812#[cfg(test)]
813mod tests {
814  use pretty_assertions::assert_eq;
815
816  use super::*;
817
818  #[test]
819  fn test_exports_to_wrapper_module() {
820    let url = Url::parse("file:///test/test.ts").unwrap();
821    let exports = BTreeSet::from(
822      ["static", "server", "app", "dashed-export", "3d"].map(|s| s.to_string()),
823    );
824    let text = exports_to_wrapper_module(&url, &exports);
825    assert_eq!(
826      text,
827      r#"import { createRequire as __internalCreateRequire, Module as __internalModule } from "node:module";
828const require = __internalCreateRequire(import.meta.url);
829let mod;
830if (import.meta.main) {
831  mod = __internalModule._load("/test/test.ts", null, true)
832} else {
833  mod = require("/test/test.ts");
834}
835const __deno_export_1__ = mod["3d"];
836export { __deno_export_1__ as "3d" };
837export const app = mod["app"];
838const __deno_export_2__ = mod["dashed-export"];
839export { __deno_export_2__ as "dashed-export" };
840export const server = mod["server"];
841const __deno_export_3__ = mod["static"];
842export { __deno_export_3__ as "static" };
843export default mod;
844const __deno_export_4__ = mod;
845export { __deno_export_4__ as "module.exports" };
846"#
847    );
848  }
849
850  #[test]
851  fn test_to_double_quote_string() {
852    assert_eq!(to_double_quote_string("test"), "\"test\"");
853    assert_eq!(
854      to_double_quote_string("\r\n\t\"test"),
855      "\"\\r\\n\\t\\\"test\""
856    );
857  }
858}