deno/cli/emit.rs
David Sherret f091d1ad69
feat(node): stabilize detecting if CJS via "type": "commonjs" in a package.json (#26439)
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.
2024-11-13 15:10:09 +00:00

392 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(())
}