Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions cli/tools/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use deno_ast::EmitOptions;
use deno_ast::MediaType;
use deno_ast::SourceMapOption;
use deno_ast::TranspileModuleOptions;
use deno_ast::swc::ast;
use deno_ast::swc::ecma_visit::VisitMut;
use deno_ast::swc::ecma_visit::VisitMutWith;
use deno_core::ModuleSpecifier;
use deno_core::anyhow;
use deno_core::anyhow::Context;
Expand All @@ -23,6 +26,8 @@ use crate::args::SourceMapMode;
use crate::args::TranspileFlags;
use crate::factory::CliFactory;

mod import_extensions;

pub async fn transpile(
flags: Arc<Flags>,
transpile_flags: TranspileFlags,
Expand Down Expand Up @@ -112,14 +117,20 @@ pub async fn transpile(
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,
})?;
let parsed_source = deno_ast::parse_module_with_post_process(
deno_ast::ParseParams {
specifier: specifier.clone(),
text: source_code.into(),
media_type: *media_type,
capture_tokens: false,
scope_analysis: false,
maybe_syntax: None,
},
|mut module, _| {
module.visit_mut_with(&mut ImportExtensionRewriter);
module
},
)?;

// Use transpile options from deno.json, with source map overridden by CLI flag
let transpile_and_emit_options = compiler_options_resolver
Expand Down Expand Up @@ -233,6 +244,52 @@ pub async fn transpile(
Ok(())
}

struct ImportExtensionRewriter;

impl VisitMut for ImportExtensionRewriter {
fn visit_mut_module_decl(&mut self, node: &mut ast::ModuleDecl) {
match node {
ast::ModuleDecl::Import(node) => rewrite_import_specifier(&mut node.src),
ast::ModuleDecl::ExportNamed(node) => {
if let Some(src) = &mut node.src {
rewrite_import_specifier(src);
}
}
ast::ModuleDecl::ExportAll(node) => {
rewrite_import_specifier(&mut node.src)
}
ast::ModuleDecl::TsImportEquals(node) => {
if let ast::TsModuleRef::TsExternalModuleRef(module_ref) =
&mut node.module_ref
{
rewrite_import_specifier(&mut module_ref.expr);
}
}
_ => {}
Comment thread
rathodkunj2005 marked this conversation as resolved.
Resolved
}
node.visit_mut_children_with(self);
}

fn visit_mut_call_expr(&mut self, node: &mut ast::CallExpr) {
if matches!(node.callee, ast::Callee::Import(_))
&& let Some(ast::ExprOrSpread { spread: None, expr }) =
node.args.first_mut()
&& let ast::Expr::Lit(ast::Lit::Str(src)) = &mut **expr
{
rewrite_import_specifier(src);
}
node.visit_mut_children_with(self);
}
}

fn rewrite_import_specifier(specifier: &mut ast::Str) {
let value = specifier.value.to_string_lossy();
if let Some(replacement) = import_extensions::rewrite(&value) {
specifier.value = replacement.into();
specifier.raw = None;
}
}

async fn emit_declarations(
factory: &CliFactory,
transpile_flags: &TranspileFlags,
Expand Down
68 changes: 68 additions & 0 deletions cli/tools/transpile/import_extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2018-2026 the Deno authors. MIT license.

pub fn rewrite(specifier: &str) -> Option<String> {
// Only relative specifiers can refer to a file we're emitting. Remote URLs,
// `npm:`/`jsr:` and bare specifiers all point at modules we don't rewrite,
// so changing their extension would just break the import.
if !specifier.starts_with("./") && !specifier.starts_with("../") {
return None;
}
let path_end = specifier.find(['?', '#']).unwrap_or(specifier.len());
let (path, suffix) = specifier.split_at(path_end);
if path.ends_with(".d.ts")
|| path.ends_with(".d.mts")
|| path.ends_with(".d.cts")
{
None
} else if let Some(path) = path.strip_suffix(".tsx") {
Some(format!("{path}.js{suffix}"))
} else if let Some(path) = path.strip_suffix(".jsx") {
Some(format!("{path}.js{suffix}"))
} else if let Some(path) = path.strip_suffix(".ts") {
Some(format!("{path}.js{suffix}"))
} else if let Some(path) = path.strip_suffix(".mts") {
Some(format!("{path}.mjs{suffix}"))
} else {
Comment thread
rathodkunj2005 marked this conversation as resolved.
Resolved
path
.strip_suffix(".cts")
.map(|path| format!("{path}.cjs{suffix}"))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn rewrites_typescript_extensions() {
assert_eq!(rewrite("./mod.ts").as_deref(), Some("./mod.js"));
assert_eq!(rewrite("../mod.tsx").as_deref(), Some("../mod.js"));
assert_eq!(rewrite("./mod.jsx").as_deref(), Some("./mod.js"));
assert_eq!(rewrite("./mod.mts").as_deref(), Some("./mod.mjs"));
assert_eq!(rewrite("./mod.cts").as_deref(), Some("./mod.cjs"));
}

#[test]
fn preserves_suffixes_and_non_typescript_extensions() {
assert_eq!(
rewrite("./mod.ts?raw#fragment").as_deref(),
Some("./mod.js?raw#fragment")
);
assert_eq!(rewrite("./mod.d.ts"), None);
assert_eq!(rewrite("./mod.js"), None);
assert_eq!(rewrite("./literal.ts.txt"), None);
}

#[test]
fn preserves_non_relative_specifiers() {
// None of these resolve to a file `deno transpile` emits, so rewriting
// their extension would break the import.
assert_eq!(rewrite("https://deno.land/x/foo/mod.ts"), None);
assert_eq!(rewrite("http://localhost:8000/mod.ts"), None);
assert_eq!(rewrite("file:///abs/mod.ts"), None);
assert_eq!(rewrite("npm:some-pkg/mod.ts"), None);
assert_eq!(rewrite("jsr:@scope/pkg/mod.ts"), None);
assert_eq!(rewrite("some-pkg/mod.ts"), None);
assert_eq!(rewrite("/abs/mod.ts"), None);
}
}
12 changes: 12 additions & 0 deletions tests/specs/transpile/__test__.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
}
]
},
"rewrite_import_extensions": {
"steps": [
{
"args": "transpile import_extensions.ts helpers.ts helper.mts helper.cts view.jsx --outdir dist",
"output": "rewrite_import_extensions_emit.out"
},
{
"args": "run --allow-read check_import_extensions.ts",
"output": "true\n"
}
]
},
"source_map_inline": {
"args": "transpile main.ts --source-map inline",
"output": "source_map_inline.out"
Expand Down
13 changes: 13 additions & 0 deletions tests/specs/transpile/check_import_extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const output = await Deno.readTextFile("dist/import_extensions.js");

export {};

console.log(
output.includes('import "./helpers.js";') &&
output.includes('export { add } from "./helpers.js";') &&
output.includes('export * from "./helper.mjs";') &&
output.includes('await import("./helper.cjs");') &&
output.includes('import helper = require("./helper.cjs");') &&
output.includes('import "./view.js";') &&
output.includes('console.log("./literal.ts");'),
);
10 changes: 10 additions & 0 deletions tests/specs/transpile/import_extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "./helpers.ts";
import "./view.jsx";
import helper = require("./helper.cts");
console.log(helper);
export { add } from "./helpers.ts";
export * from "./helper.mts";

await import("./helper.cts");

console.log("./literal.ts");
6 changes: 6 additions & 0 deletions tests/specs/transpile/rewrite_import_extensions_emit.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[WILDCARD]deno transpile is experimental and subject to changes
Emit [WILDCARD]dist[WILDCARD]import_extensions.js
Emit [WILDCARD]dist[WILDCARD]helpers.js
Emit [WILDCARD]dist[WILDCARD]helper.mjs
Emit [WILDCARD]dist[WILDCARD]helper.cjs
Emit [WILDCARD]dist[WILDCARD]view.js
3 changes: 3 additions & 0 deletions tests/specs/transpile/view.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function View() {
return "view";
}
Loading