/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/transpile.rs
565 строк
17 KB
Bartek Iwańczuk
chore: add deno_print crate with drop_println! macros (#35737)
03 июл 2026, 15:22
Не верифицирован
03 июл 2026, 15:22
c0f2b11
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use deno_ast::EmitOptions; use deno_ast::MediaType; use deno_ast::SourceMapOption; use deno_ast::TranspileModuleOptions; use deno_core::ModuleSpecifier; use deno_core::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_graph::GraphKind; use deno_graph::ModuleGraph; use deno_resolver::emit::patch_public_decorator_access_has; use deno_terminal::colors; use sys_traits::PathsInErrorsExt; use crate::args::Flags; use crate::args::SourceMapMode; use crate::args::TranspileFlags; use crate::factory::CliFactory; pub async fn transpile( flags: Arc<Flags>, transpile_flags: TranspileFlags, ) -> Result<(), AnyError> { let files = &transpile_flags.files; if files.is_empty() { anyhow::bail!("No input files specified"); } if files.len() > 1 && transpile_flags.output.is_some() { anyhow::bail!( "Cannot use --output with multiple input files. Use --outdir instead." ); } if transpile_flags.declaration && transpile_flags.output.is_none() && transpile_flags.output_dir.is_none() { anyhow::bail!( "Cannot use --declaration without --output or --outdir. Declaration files must be written to disk." ); } let is_stdout_mode = files.len() == 1 && transpile_flags.output.is_none() && transpile_flags.output_dir.is_none(); if is_stdout_mode && matches!(transpile_flags.source_map, SourceMapMode::Separate) { anyhow::bail!( "Cannot use --source-map separate when outputting to stdout. Use --output or --outdir, or use --source-map inline instead." ); } let factory = CliFactory::from_flags(flags.clone()); let cli_options = factory.cli_options()?; let cwd = cli_options.initial_cwd(); let real_sys = factory.sys(); let sys = real_sys.with_paths_in_errors(); let source_map_option = match transpile_flags.source_map { SourceMapMode::None => SourceMapOption::None, SourceMapMode::Inline => SourceMapOption::Inline, SourceMapMode::Separate => SourceMapOption::Separate, }; // Resolve transpile options from deno.json compilerOptions let compiler_options_resolver = factory.compiler_options_resolver()?; // Collect file paths and specifiers let mut file_entries = Vec::new(); for file_path_str in files { let file_path = cwd.join(file_path_str); let media_type = MediaType::from_path(&file_path); if !media_type.is_emittable() { log::warn!( "{} Skipping {} (not a TypeScript/JSX/TSX file)", colors::yellow("Warning"), file_path.display() ); continue; } let specifier = ModuleSpecifier::from_file_path(&file_path).map_err(|_| { anyhow::anyhow!("Invalid file path: {}", file_path.display()) })?; file_entries.push((file_path, specifier, media_type)); } if file_entries.is_empty() { anyhow::bail!( "No emittable files found. Only TypeScript/JSX/TSX files can be transpiled." ); } // Transpile each file (TS -> JS) for (file_path, specifier, media_type) in &file_entries { let source_bytes = sys .fs_read(file_path) .with_context(|| format!("Failed to read {}", file_path.display()))?; let source_code = String::from_utf8(source_bytes.into_owned()) .with_context(|| format!("{} is not valid UTF-8", file_path.display()))?; let parsed_source = deno_ast::parse_module(deno_ast::ParseParams { specifier: specifier.clone(), text: source_code.into(), media_type: *media_type, capture_tokens: false, scope_analysis: false, maybe_syntax: None, })?; // Use transpile options from deno.json, with source map overridden by CLI flag let transpile_and_emit_options = compiler_options_resolver .for_specifier(specifier) .transpile_options()?; let transpile_options = &transpile_and_emit_options.transpile; // Compute the output path/filename so source_map_file references the // generated file (e.g. "out.js"), not the input (e.g. "main.ts"). let maybe_output_path = if !is_stdout_mode { Some(compute_output_path( file_path, cwd, transpile_flags.output.as_deref(), transpile_flags.output_dir.as_deref(), *media_type, )?) } else { None }; let source_map_filename = if let Some(output_path) = &maybe_output_path { output_path .file_name() .unwrap_or_default() .to_string_lossy() .into_owned() } else { // In stdout mode, use the input-derived JS filename let ext = js_extension_for_media_type(*media_type); file_path .with_extension(ext) .file_name() .unwrap_or_default() .to_string_lossy() .into_owned() }; // Always apply the CLI source map mode. The built-in emit defaults // have inlineSourceMap: true (for the runtime), but `deno transpile` // should produce clean output by default. Users must pass --source-map // explicitly to enable source maps. let emit_options = EmitOptions { source_map: source_map_option, source_map_file: Some(source_map_filename), ..transpile_and_emit_options.emit.clone() }; let transpile_result = parsed_source.transpile( transpile_options, &TranspileModuleOptions { module_kind: None }, &emit_options, )?; let mut emitted = transpile_result.into_source(); patch_public_decorator_access_has(&mut emitted.text); // Skip the newline restoration pass when source maps are requested, // since inserting newlines would shift line numbers in the generated // output without a corresponding adjustment to the mappings. let emitted_text; let js_text = if matches!(source_map_option, SourceMapOption::None) { emitted_text = restore_block_comment_newlines(&emitted.text); &emitted_text } else { &emitted.text }; let source_map_text = &emitted.source_map; // Determine output path if is_stdout_mode { // Single file, no output specified: print to stdout deno_print::drop_write_stdout(js_text.as_bytes()); } else { let output_path = maybe_output_path.unwrap(); // Ensure parent directory exists if let Some(parent) = output_path.parent() { sys.fs_create_dir_all(parent).with_context(|| { format!("Failed to create directory {}", parent.display()) })?; } sys .fs_write(&output_path, js_text.as_bytes()) .with_context(|| { format!("Failed to write {}", output_path.display()) })?; log::info!("{} {}", colors::green("Emit"), output_path.display()); // Write separate source map file if let Some(sm) = source_map_text && matches!(transpile_flags.source_map, SourceMapMode::Separate) { let ext = output_path .extension() .unwrap_or_default() .to_string_lossy(); let map_path = output_path.with_extension(format!("{ext}.map")); sys .fs_write(&map_path, sm.as_bytes()) .with_context(|| format!("Failed to write {}", map_path.display()))?; log::info!("{} {}", colors::green("Emit"), map_path.display()); } } } // Generate .d.ts declaration files if requested if transpile_flags.declaration && !file_entries.is_empty() { emit_declarations(&factory, &transpile_flags, &file_entries, cwd).await?; } Ok(()) } async fn emit_declarations( factory: &CliFactory, transpile_flags: &TranspileFlags, file_entries: &[(PathBuf, ModuleSpecifier, MediaType)], cwd: &Path, ) -> Result<(), AnyError> { let cli_options = factory.cli_options()?; let real_sys = factory.sys(); let sys = real_sys.with_paths_in_errors(); // Build a module graph for the input files let root_names: Vec<(ModuleSpecifier, MediaType)> = file_entries .iter() .map(|(_, specifier, media_type)| (specifier.clone(), *media_type)) .collect(); let specifiers: Vec<ModuleSpecifier> = root_names.iter().map(|(s, _)| s.clone()).collect(); let mut graph = ModuleGraph::new(GraphKind::All); let module_graph_builder = factory.module_graph_builder().await?; module_graph_builder .build_graph_roots_with_npm_resolution( &mut graph, specifiers, crate::graph_util::BuildGraphWithNpmOptions { is_dynamic: false, loader: None, npm_caching: cli_options.default_npm_caching_strategy(), }, ) .await?; // Validate that all files share the same compiler options scope, since // TSC type-checks all files in a single pass. let compiler_options_resolver = factory.compiler_options_resolver()?; let first_specifier = &root_names[0].0; let (first_key, _) = compiler_options_resolver.entry_for_specifier(first_specifier); for (specifier, _) in root_names.iter().skip(1) { let (key, _) = compiler_options_resolver.entry_for_specifier(specifier); if key != first_key { anyhow::bail!( "Cannot generate declarations for files with different compiler option scopes.\n\ '{}' uses scope '{}' but '{}' uses scope '{}'.\n\ Separate these files into different invocations.", first_specifier, first_key, specifier, key, ); } } let type_checker = factory.type_checker().await?; let result = type_checker.emit_declarations( Arc::new(graph), root_names, cli_options.ts_type_lib_window(), )?; if result.diagnostics.has_diagnostic() { anyhow::bail!("Type checking failed:\n{}", result.diagnostics); } // Determine the output directory for .d.ts files. // With -o, place .d.ts next to the output file. // With --outdir, use the outdir. // Otherwise, place next to the source file (handled by TSC path). let output_base_dir = if let Some(output) = &transpile_flags.output { let output_path = cwd.join(output); output_path.parent().map(|p| p.to_path_buf()) } else { None }; // Write emitted .d.ts files for (file_name, content) in &result.emitted_files { // The file names from TSC are specifier-based, convert to output paths let output_path = resolve_dts_output_path( file_name, transpile_flags.output_dir.as_deref(), output_base_dir.as_deref(), cwd, )?; if let Some(parent) = output_path.parent() { sys.fs_create_dir_all(parent).with_context(|| { format!("Failed to create directory {}", parent.display()) })?; } sys .fs_write(&output_path, content.as_bytes()) .with_context(|| format!("Failed to write {}", output_path.display()))?; log::info!("{} {}", colors::green("Emit"), output_path.display()); } if result.emitted_files.is_empty() { log::warn!( "{} No declaration files were emitted", colors::yellow("Warning") ); } Ok(()) } fn resolve_dts_output_path( tsc_file_name: &str, output_dir: Option<&str>, output_base_dir: Option<&Path>, cwd: &Path, ) -> Result<PathBuf, AnyError> { // TSC emits file names like "file:///path/to/file.d.ts" let path = if let Ok(specifier) = ModuleSpecifier::parse(tsc_file_name) { deno_path_util::url_to_file_path(&specifier).with_context(|| { format!("Cannot convert specifier to path: {tsc_file_name}") })? } else { PathBuf::from(tsc_file_name) }; if let Some(outdir) = output_dir { // --outdir: preserve relative directory structure let outdir = cwd.join(outdir); let relative = path.strip_prefix(cwd).map_err(|_| { anyhow::anyhow!( "Declaration file {} is not under the current directory", path.display() ) })?; Ok(outdir.join(relative)) } else if let Some(base_dir) = output_base_dir { // -o: place .d.ts next to the output file let file_name = path .file_name() .ok_or_else(|| anyhow::anyhow!("Invalid declaration file path"))?; Ok(base_dir.join(file_name)) } else { // No output specified: place next to source file Ok(path) } } /// SWC's code generator always writes a single space after a block /// comment's closing `*/`, regardless of whether the original source had a /// newline. For multi-line block comments (typically JSDoc) this collapses /// the comment onto the same line as the following statement. This function /// walks the emitted source and replaces that single space with a newline /// (plus the comment's own leading indentation) when the preceding block /// comment spans multiple lines. fn restore_block_comment_newlines(input: &str) -> String { let bytes = input.as_bytes(); let n = bytes.len(); let mut out: Vec<u8> = Vec::with_capacity(n); let mut i = 0; // Stack of brace depths at which template-literal substitutions started. // When `}` is reached at the top of the stack, we re-enter the template. let mut template_stack: Vec<u32> = Vec::new(); let mut brace_depth: u32 = 0; // 0 = code, 1 = single-quote string, 2 = double-quote string, // 3 = template literal. let mut mode: u8 = 0; while i < n { let c = bytes[i]; match mode { 0 => match c { b'\'' => { out.push(c); mode = 1; i += 1; } b'"' => { out.push(c); mode = 2; i += 1; } b'`' => { out.push(c); mode = 3; i += 1; } b'{' => { brace_depth += 1; out.push(c); i += 1; } b'}' => { if template_stack.last() == Some(&brace_depth) { template_stack.pop(); mode = 3; } brace_depth = brace_depth.saturating_sub(1); out.push(c); i += 1; } b'/' if i + 1 < n && bytes[i + 1] == b'/' => { // Line comment: copy through end of line. while i < n && bytes[i] != b'\n' { out.push(bytes[i]); i += 1; } } b'/' if i + 1 < n && bytes[i + 1] == b'*' => { // Block comment: copy through `*/`, then maybe restore newline. let start = i; out.push(bytes[i]); out.push(bytes[i + 1]); i += 2; let mut has_newline = false; while i + 1 < n && !(bytes[i] == b'*' && bytes[i + 1] == b'/') { if bytes[i] == b'\n' { has_newline = true; } out.push(bytes[i]); i += 1; } if i + 1 < n { out.push(b'*'); out.push(b'/'); i += 2; } // If multi-line and SWC emitted the trailing space, replace it // with a newline + the comment's own indentation. We only act // when the comment is indented by pure whitespace on its line; // otherwise leave the output untouched to avoid breaking layout. if has_newline && i < n && bytes[i] == b' ' { // Locate the start of the line containing the comment. let mut line_start = start; while line_start > 0 && bytes[line_start - 1] != b'\n' { line_start -= 1; } let indent = &bytes[line_start..start]; if indent.iter().all(|&b| b == b' ' || b == b'\t') { out.push(b'\n'); out.extend_from_slice(indent); i += 1; // skip the space SWC wrote } } } _ => { out.push(c); i += 1; } }, 1 | 2 => { let quote = if mode == 1 { b'\'' } else { b'"' }; out.push(c); i += 1; if c == b'\\' && i < n { out.push(bytes[i]); i += 1; } else if c == quote { mode = 0; } } 3 => { if c == b'\\' && i + 1 < n { out.push(c); out.push(bytes[i + 1]); i += 2; } else if c == b'`' { out.push(c); mode = 0; i += 1; } else if c == b'$' && i + 1 < n && bytes[i + 1] == b'{' { out.push(c); out.push(b'{'); brace_depth += 1; template_stack.push(brace_depth); mode = 0; i += 2; } else { out.push(c); i += 1; } } _ => unreachable!(), } } // Safety: input is valid UTF-8 and we only branch on ASCII bytes; the // newlines and spaces we insert are ASCII. Multi-byte UTF-8 sequences are // copied byte-for-byte so the output remains valid UTF-8. String::from_utf8(out).expect("post-processed text must remain valid UTF-8") } fn js_extension_for_media_type(media_type: MediaType) -> &'static str { match media_type { MediaType::Mts => "mjs", MediaType::Cts => "cjs", _ => "js", } } fn compute_output_path( input_path: &Path, cwd: &Path, output: Option<&str>, output_dir: Option<&str>, media_type: MediaType, ) -> Result<PathBuf, AnyError> { if let Some(output) = output { // Explicit output file return Ok(cwd.join(output)); } let ext = js_extension_for_media_type(media_type); let js_filename = input_path.with_extension(ext); if let Some(outdir) = output_dir { let outdir = cwd.join(outdir); let relative = js_filename .strip_prefix(cwd) .map_err(|_| { anyhow::anyhow!( "Input file {} is not under the current directory. Use --output instead.", input_path.display() ) })?; Ok(outdir.join(relative)) } else { // Write alongside source file Ok(js_filename) } }