Skip to content

feat(cli): add deno transpile subcommand#32691

Open
bartlomieju wants to merge 9 commits intomainfrom
feature/deno-transpile
Open

feat(cli): add deno transpile subcommand#32691
bartlomieju wants to merge 9 commits intomainfrom
feature/deno-transpile

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

Summary

  • Add a new deno transpile subcommand that transpiles TypeScript/JSX/TSX files to JavaScript using deno_ast, with configurable source map output (--source-map none|inline|separate)
  • Support --declaration flag to generate .d.ts type declaration files via the TypeScript compiler
  • Support single file (-o), multi-file (--outdir), and stdout output modes

Changes

  • cli/args/flags.rsTranspileFlags, SourceMapMode, clap subcommand definition and parser
  • cli/tools/transpile.rs — New tool: transpilation via deno_ast, declaration generation via tsc::exec()
  • cli/tsc/js.rs / cli/tsc/mod.rs / cli/tsc/go.rs — Extend op_emit to capture .d.ts files, add emitted_files to Response
  • cli/tsc/99_main_compiler.js — Call program.emit() when declaration/emitDeclarationOnly is set
  • cli/factory.rs / cli/lib.rs / cli/tools/mod.rs — Wiring

Test plan

  • 9 spec tests in tests/specs/transpile/ covering:
    • Basic TS→JS transpile to stdout
    • Output to file (-o) and directory (--outdir)
    • Inline and separate source map modes
    • .d.ts declaration generation
    • Error handling (multiple files with -o)
    • Skipping non-emittable .js files
    • TSX transpilation
  • All pass via ./x test-spec transpile

🤖 Generated with Claude Code

bartlomieju and others added 3 commits March 13, 2026 16:41
Add a new `deno transpile` subcommand that transpiles TypeScript/JSX/TSX
files to JavaScript using deno_ast. Supports single and multiple file
transpilation with configurable source map output (none/inline/separate).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `--declaration` is passed, invoke TSC with `emitDeclarationOnly`
to generate .d.ts type declaration files alongside the transpiled JS.

Changes:
- Extend TSC `op_emit` to capture .d.ts files instead of panicking
- Add `emitted_files` field to TSC Response to propagate declaration output
- Call `program.emit()` in the JS compiler when declaration mode is active
- Build module graph and invoke TSC from the transpile tool

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9 test cases covering:
- Basic transpile to stdout
- Output to file with -o flag
- Multiple files with --outdir
- Inline and separate source map modes
- .d.ts declaration generation with --declaration
- Error on -o with multiple files
- Skipping non-emittable .js files
- TSX transpilation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ok(())
}

fn transpile_parse(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arg parsing tests?

Comment on lines +193 to +255
// Resolve compiler options, adding declaration-specific settings
let compiler_options_resolver = factory.compiler_options_resolver()?;
let first_specifier = &root_names[0].0;
let base_compiler_options = compiler_options_resolver
.for_specifier(first_specifier)
.compiler_options_for_lib(cli_options.ts_type_lib_window())?;

// Merge declaration options into the base compiler options
let mut config_value =
deno_core::serde_json::to_value(base_compiler_options.as_ref())?;
let config_obj = config_value
.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("Invalid compiler options"))?;
config_obj.insert("declaration".into(), json!(true));
config_obj.insert("emitDeclarationOnly".into(), json!(true));
config_obj.insert("noEmit".into(), json!(false));

let compiler_options = Arc::new(CompilerOptions::new(config_value));

let hash_data =
deno_lib::util::hash::FastInsecureHasher::new_deno_versioned()
.write_hashable(&compiler_options)
.finish();

let jsx_import_source_config_resolver = Arc::new(
deno_resolver::deno_json::JsxImportSourceConfigResolver::from_compiler_options_resolver(
compiler_options_resolver,
)?,
);

// Set up npm state if available
let maybe_npm = {
let cjs_tracker = Arc::new(tsc::TypeCheckingCjsTracker::new(
factory.cjs_tracker()?.clone(),
factory.module_info_cache()?.clone(),
));
let node_resolver = factory.node_resolver().await?.clone();
let npm_resolver = factory.npm_resolver().await?.clone();
let package_json_resolver = factory.pkg_json_resolver()?.clone();
Some(tsc::RequestNpmState {
cjs_tracker,
node_resolver,
npm_resolver,
package_json_resolver,
})
};

let response = tsc::exec(
tsc::Request {
config: compiler_options,
debug: cli_options.log_level() == Some(log::Level::Debug),
graph: Arc::new(graph),
jsx_import_source_config_resolver,
hash_data,
maybe_npm,
maybe_tsbuildinfo: None,
root_names,
check_mode: TypeCheckMode::All,
initial_cwd: cwd.to_path_buf(),
},
None,
None,
)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like way too much setup code. Can't we use the TypeChecker here instead? tsc::exec( is very low level to be calling here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into using TypeChecker here but it doesn't currently support returning emitted files — check_diagnostics calls tsc::exec internally but only exposes the diagnostics, not the emitted_files from the Response.

To make this work via TypeChecker, we'd need to add a new method (something like emit_declarations) that wraps the tsc::exec call and returns the emitted .d.ts files. Happy to do that if you think it belongs there — would keep transpile.rs much simpler and make declaration emission reusable.

In the meantime I've simplified the setup by using factory.create_request_npm_state() (new factory method) and getting all other components from the factory directly.

Copy link
Copy Markdown
Contributor

@dsherret dsherret Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we should look into adding a method and create_request_npm_state() into the tsc module. We shouldn't be exposing such low level stuff so high up or the code is going to get complex/sloppy.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can look into refactoring this if you'd like. I think we could move it into TypeChecker.

bartlomieju and others added 6 commits March 13, 2026 18:27
- Add arg parsing unit tests for transpile subcommand
- Create CliFactory at top of transpile() and use it throughout
- Use sys.with_paths_in_errors() for filesystem ops with better error context
- Add create_request_npm_state() factory method to reduce duplication
- Use deno_path_util::url_to_file_path instead of specifier.to_file_path()
- Add error context to all filesystem operations and compute_output_path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Preserve relative directory structure in --outdir instead of flattening
- Fix outdir spec test to work on Windows (backslash path separators)
- Remove unnecessary canonicalize_path call
- Clarify --declaration behavior in help text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Make --output and --outdir mutually exclusive via clap conflicts_with
- Error when --declaration used without -o/--outdir
- Make type-check errors from --declaration hard errors (exit code 1)
- Map .mts->.mjs, .cts->.cjs instead of always .js
- Error on --source-map separate when outputting to stdout
- Gate .d.ts capture in op_emit_inner behind capture_emitted_files flag
- Use BTreeMap for emitted_files for deterministic output order
- Error when all input files are skipped (nothing transpiled)
- Fix strip_prefix(cwd) producing bad paths for files outside cwd
- Add tests for new error cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants