mirror of
https://github.com/denoland/deno.git
synced 2024-11-27 16:10:57 -05:00
f091d1ad69
This will respect `"type": "commonjs"` in a package.json to determine if `.js`/`.jsx`/`.ts`/.tsx` files are CJS or ESM. If the file is found to be ESM it will be loaded as ESM though.
391 lines
12 KiB
Rust
391 lines
12 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use crate::cache::EmitCache;
|
|
use crate::cache::FastInsecureHasher;
|
|
use crate::cache::ParsedSourceCache;
|
|
use crate::resolver::CjsTracker;
|
|
|
|
use deno_ast::ModuleKind;
|
|
use deno_ast::SourceMapOption;
|
|
use deno_ast::SourceRange;
|
|
use deno_ast::SourceRanged;
|
|
use deno_ast::SourceRangedForSpanned;
|
|
use deno_ast::TranspileModuleOptions;
|
|
use deno_ast::TranspileResult;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::futures::stream::FuturesUnordered;
|
|
use deno_core::futures::FutureExt;
|
|
use deno_core::futures::StreamExt;
|
|
use deno_core::ModuleSpecifier;
|
|
use deno_graph::MediaType;
|
|
use deno_graph::Module;
|
|
use deno_graph::ModuleGraph;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Debug)]
|
|
pub struct Emitter {
|
|
cjs_tracker: Arc<CjsTracker>,
|
|
emit_cache: Arc<EmitCache>,
|
|
parsed_source_cache: Arc<ParsedSourceCache>,
|
|
transpile_and_emit_options:
|
|
Arc<(deno_ast::TranspileOptions, deno_ast::EmitOptions)>,
|
|
// cached hash of the transpile and emit options
|
|
transpile_and_emit_options_hash: u64,
|
|
}
|
|
|
|
impl Emitter {
|
|
pub fn new(
|
|
cjs_tracker: Arc<CjsTracker>,
|
|
emit_cache: Arc<EmitCache>,
|
|
parsed_source_cache: Arc<ParsedSourceCache>,
|
|
transpile_options: deno_ast::TranspileOptions,
|
|
emit_options: deno_ast::EmitOptions,
|
|
) -> Self {
|
|
let transpile_and_emit_options_hash = {
|
|
let mut hasher = FastInsecureHasher::new_without_deno_version();
|
|
hasher.write_hashable(&transpile_options);
|
|
hasher.write_hashable(&emit_options);
|
|
hasher.finish()
|
|
};
|
|
Self {
|
|
cjs_tracker,
|
|
emit_cache,
|
|
parsed_source_cache,
|
|
transpile_and_emit_options: Arc::new((transpile_options, emit_options)),
|
|
transpile_and_emit_options_hash,
|
|
}
|
|
}
|
|
|
|
pub async fn cache_module_emits(
|
|
&self,
|
|
graph: &ModuleGraph,
|
|
) -> Result<(), AnyError> {
|
|
let mut futures = FuturesUnordered::new();
|
|
for module in graph.modules() {
|
|
let Module::Js(module) = module else {
|
|
continue;
|
|
};
|
|
|
|
if module.media_type.is_emittable() {
|
|
futures.push(
|
|
self
|
|
.emit_parsed_source(
|
|
&module.specifier,
|
|
module.media_type,
|
|
ModuleKind::from_is_cjs(
|
|
self.cjs_tracker.is_cjs_with_known_is_script(
|
|
&module.specifier,
|
|
module.media_type,
|
|
module.is_script,
|
|
)?,
|
|
),
|
|
&module.source,
|
|
)
|
|
.boxed_local(),
|
|
);
|
|
}
|
|
}
|
|
|
|
while let Some(result) = futures.next().await {
|
|
result?; // surface errors
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets a cached emit if the source matches the hash found in the cache.
|
|
pub fn maybe_cached_emit(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
module_kind: deno_ast::ModuleKind,
|
|
source: &str,
|
|
) -> Option<String> {
|
|
let source_hash = self.get_source_hash(module_kind, source);
|
|
self.emit_cache.get_emit_code(specifier, source_hash)
|
|
}
|
|
|
|
pub async fn emit_parsed_source(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
media_type: MediaType,
|
|
module_kind: deno_ast::ModuleKind,
|
|
source: &Arc<str>,
|
|
) -> Result<String, AnyError> {
|
|
// Note: keep this in sync with the sync version below
|
|
let helper = EmitParsedSourceHelper(self);
|
|
match helper.pre_emit_parsed_source(specifier, module_kind, source) {
|
|
PreEmitResult::Cached(emitted_text) => Ok(emitted_text),
|
|
PreEmitResult::NotCached { source_hash } => {
|
|
let parsed_source_cache = self.parsed_source_cache.clone();
|
|
let transpile_and_emit_options =
|
|
self.transpile_and_emit_options.clone();
|
|
let transpiled_source = deno_core::unsync::spawn_blocking({
|
|
let specifier = specifier.clone();
|
|
let source = source.clone();
|
|
move || -> Result<_, AnyError> {
|
|
EmitParsedSourceHelper::transpile(
|
|
&parsed_source_cache,
|
|
&specifier,
|
|
media_type,
|
|
module_kind,
|
|
source.clone(),
|
|
&transpile_and_emit_options.0,
|
|
&transpile_and_emit_options.1,
|
|
)
|
|
}
|
|
})
|
|
.await
|
|
.unwrap()?;
|
|
helper.post_emit_parsed_source(
|
|
specifier,
|
|
&transpiled_source,
|
|
source_hash,
|
|
);
|
|
Ok(transpiled_source)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn emit_parsed_source_sync(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
media_type: MediaType,
|
|
module_kind: deno_ast::ModuleKind,
|
|
source: &Arc<str>,
|
|
) -> Result<String, AnyError> {
|
|
// Note: keep this in sync with the async version above
|
|
let helper = EmitParsedSourceHelper(self);
|
|
match helper.pre_emit_parsed_source(specifier, module_kind, source) {
|
|
PreEmitResult::Cached(emitted_text) => Ok(emitted_text),
|
|
PreEmitResult::NotCached { source_hash } => {
|
|
let transpiled_source = EmitParsedSourceHelper::transpile(
|
|
&self.parsed_source_cache,
|
|
specifier,
|
|
media_type,
|
|
module_kind,
|
|
source.clone(),
|
|
&self.transpile_and_emit_options.0,
|
|
&self.transpile_and_emit_options.1,
|
|
)?;
|
|
helper.post_emit_parsed_source(
|
|
specifier,
|
|
&transpiled_source,
|
|
source_hash,
|
|
);
|
|
Ok(transpiled_source)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Expects a file URL, panics otherwise.
|
|
pub async fn load_and_emit_for_hmr(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
) -> Result<String, AnyError> {
|
|
let media_type = MediaType::from_specifier(specifier);
|
|
let source_code = tokio::fs::read_to_string(
|
|
ModuleSpecifier::to_file_path(specifier).unwrap(),
|
|
)
|
|
.await?;
|
|
match media_type {
|
|
MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Jsx
|
|
| MediaType::Tsx => {
|
|
let source_arc: Arc<str> = source_code.into();
|
|
let parsed_source = self
|
|
.parsed_source_cache
|
|
.remove_or_parse_module(specifier, source_arc, media_type)?;
|
|
// HMR doesn't work with embedded source maps for some reason, so set
|
|
// the option to not use them (though you should test this out because
|
|
// this statement is probably wrong)
|
|
let mut options = self.transpile_and_emit_options.1.clone();
|
|
options.source_map = SourceMapOption::None;
|
|
let is_cjs = self.cjs_tracker.is_cjs_with_known_is_script(
|
|
specifier,
|
|
media_type,
|
|
parsed_source.compute_is_script(),
|
|
)?;
|
|
let transpiled_source = parsed_source
|
|
.transpile(
|
|
&self.transpile_and_emit_options.0,
|
|
&deno_ast::TranspileModuleOptions {
|
|
module_kind: Some(ModuleKind::from_is_cjs(is_cjs)),
|
|
},
|
|
&options,
|
|
)?
|
|
.into_source();
|
|
Ok(transpiled_source.text)
|
|
}
|
|
MediaType::JavaScript
|
|
| MediaType::Mjs
|
|
| MediaType::Cjs
|
|
| MediaType::Dts
|
|
| MediaType::Dmts
|
|
| MediaType::Dcts
|
|
| MediaType::Json
|
|
| MediaType::Wasm
|
|
| MediaType::Css
|
|
| MediaType::SourceMap
|
|
| MediaType::Unknown => {
|
|
// clear this specifier from the parsed source cache as it's now out of date
|
|
self.parsed_source_cache.free(specifier);
|
|
Ok(source_code)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A hashing function that takes the source code and uses the global emit
|
|
/// options then generates a string hash which can be stored to
|
|
/// determine if the cached emit is valid or not.
|
|
fn get_source_hash(&self, module_kind: ModuleKind, source_text: &str) -> u64 {
|
|
FastInsecureHasher::new_without_deno_version() // stored in the transpile_and_emit_options_hash
|
|
.write_str(source_text)
|
|
.write_u64(self.transpile_and_emit_options_hash)
|
|
.write_hashable(module_kind)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
enum PreEmitResult {
|
|
Cached(String),
|
|
NotCached { source_hash: u64 },
|
|
}
|
|
|
|
/// Helper to share code between async and sync emit_parsed_source methods.
|
|
struct EmitParsedSourceHelper<'a>(&'a Emitter);
|
|
|
|
impl<'a> EmitParsedSourceHelper<'a> {
|
|
pub fn pre_emit_parsed_source(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
module_kind: deno_ast::ModuleKind,
|
|
source: &Arc<str>,
|
|
) -> PreEmitResult {
|
|
let source_hash = self.0.get_source_hash(module_kind, source);
|
|
|
|
if let Some(emit_code) =
|
|
self.0.emit_cache.get_emit_code(specifier, source_hash)
|
|
{
|
|
PreEmitResult::Cached(emit_code)
|
|
} else {
|
|
PreEmitResult::NotCached { source_hash }
|
|
}
|
|
}
|
|
|
|
pub fn transpile(
|
|
parsed_source_cache: &ParsedSourceCache,
|
|
specifier: &ModuleSpecifier,
|
|
media_type: MediaType,
|
|
module_kind: deno_ast::ModuleKind,
|
|
source: Arc<str>,
|
|
transpile_options: &deno_ast::TranspileOptions,
|
|
emit_options: &deno_ast::EmitOptions,
|
|
) -> Result<String, AnyError> {
|
|
// nothing else needs the parsed source at this point, so remove from
|
|
// the cache in order to not transpile owned
|
|
let parsed_source = parsed_source_cache
|
|
.remove_or_parse_module(specifier, source, media_type)?;
|
|
ensure_no_import_assertion(&parsed_source)?;
|
|
let transpile_result = parsed_source.transpile(
|
|
transpile_options,
|
|
&TranspileModuleOptions {
|
|
module_kind: Some(module_kind),
|
|
},
|
|
emit_options,
|
|
)?;
|
|
let transpiled_source = match transpile_result {
|
|
TranspileResult::Owned(source) => source,
|
|
TranspileResult::Cloned(source) => {
|
|
debug_assert!(false, "Transpile owned failed.");
|
|
source
|
|
}
|
|
};
|
|
debug_assert!(transpiled_source.source_map.is_none());
|
|
Ok(transpiled_source.text)
|
|
}
|
|
|
|
pub fn post_emit_parsed_source(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
transpiled_source: &str,
|
|
source_hash: u64,
|
|
) {
|
|
self.0.emit_cache.set_emit_code(
|
|
specifier,
|
|
source_hash,
|
|
transpiled_source.as_bytes(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// todo(dsherret): this is a temporary measure until we have swc erroring for this
|
|
fn ensure_no_import_assertion(
|
|
parsed_source: &deno_ast::ParsedSource,
|
|
) -> Result<(), AnyError> {
|
|
fn has_import_assertion(text: &str) -> bool {
|
|
// good enough
|
|
text.contains(" assert ") && !text.contains(" with ")
|
|
}
|
|
|
|
fn create_err(
|
|
parsed_source: &deno_ast::ParsedSource,
|
|
range: SourceRange,
|
|
) -> AnyError {
|
|
let text_info = parsed_source.text_info_lazy();
|
|
let loc = text_info.line_and_column_display(range.start);
|
|
let mut msg = "Import assertions are deprecated. Use `with` keyword, instead of 'assert' keyword.".to_string();
|
|
msg.push_str("\n\n");
|
|
msg.push_str(range.text_fast(text_info));
|
|
msg.push_str("\n\n");
|
|
msg.push_str(&format!(
|
|
" at {}:{}:{}\n",
|
|
parsed_source.specifier(),
|
|
loc.line_number,
|
|
loc.column_number,
|
|
));
|
|
deno_core::anyhow::anyhow!("{}", msg)
|
|
}
|
|
|
|
let deno_ast::ProgramRef::Module(module) = parsed_source.program_ref() else {
|
|
return Ok(());
|
|
};
|
|
|
|
for item in &module.body {
|
|
match item {
|
|
deno_ast::swc::ast::ModuleItem::ModuleDecl(decl) => match decl {
|
|
deno_ast::swc::ast::ModuleDecl::Import(n) => {
|
|
if n.with.is_some()
|
|
&& has_import_assertion(n.text_fast(parsed_source.text_info_lazy()))
|
|
{
|
|
return Err(create_err(parsed_source, n.range()));
|
|
}
|
|
}
|
|
deno_ast::swc::ast::ModuleDecl::ExportAll(n) => {
|
|
if n.with.is_some()
|
|
&& has_import_assertion(n.text_fast(parsed_source.text_info_lazy()))
|
|
{
|
|
return Err(create_err(parsed_source, n.range()));
|
|
}
|
|
}
|
|
deno_ast::swc::ast::ModuleDecl::ExportNamed(n) => {
|
|
if n.with.is_some()
|
|
&& has_import_assertion(n.text_fast(parsed_source.text_info_lazy()))
|
|
{
|
|
return Err(create_err(parsed_source, n.range()));
|
|
}
|
|
}
|
|
deno_ast::swc::ast::ModuleDecl::ExportDecl(_)
|
|
| deno_ast::swc::ast::ModuleDecl::ExportDefaultDecl(_)
|
|
| deno_ast::swc::ast::ModuleDecl::ExportDefaultExpr(_)
|
|
| deno_ast::swc::ast::ModuleDecl::TsImportEquals(_)
|
|
| deno_ast::swc::ast::ModuleDecl::TsExportAssignment(_)
|
|
| deno_ast::swc::ast::ModuleDecl::TsNamespaceExport(_) => {}
|
|
},
|
|
deno_ast::swc::ast::ModuleItem::Stmt(_) => {}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|