Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add transpile_owned and transpile_owned_with_fallback #237

Merged
merged 4 commits into from
Apr 12, 2024
Merged
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
20 changes: 10 additions & 10 deletions src/parsed_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ pub(crate) struct SyntaxContexts {
pub top_level: SyntaxContext,
}

struct ParsedSourceInner {
specifier: ModuleSpecifier,
media_type: MediaType,
text_info: SourceTextInfo,
comments: MultiThreadedComments,
program: Arc<Program>,
tokens: Option<Arc<Vec<TokenAndSpan>>>,
syntax_contexts: Option<SyntaxContexts>,
diagnostics: Vec<ParseDiagnostic>,
pub(crate) struct ParsedSourceInner {
pub specifier: ModuleSpecifier,
pub media_type: MediaType,
pub text_info: SourceTextInfo,
pub comments: MultiThreadedComments,
pub program: Arc<Program>,
pub tokens: Option<Arc<Vec<TokenAndSpan>>>,
pub syntax_contexts: Option<SyntaxContexts>,
pub diagnostics: Vec<ParseDiagnostic>,
}

/// A parsed source containing an AST, comments, and possibly tokens.
///
/// Note: This struct is cheap to clone.
#[derive(Clone)]
pub struct ParsedSource {
inner: Arc<ParsedSourceInner>,
pub(crate) inner: Arc<ParsedSourceInner>,
}

impl ParsedSource {
Expand Down
235 changes: 211 additions & 24 deletions src/transpiling/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use std::rc::Rc;
use std::sync::Arc;

use anyhow::Result;
use swc_ecma_visit::as_folder;
Expand All @@ -26,6 +27,7 @@ use crate::swc::visit::FoldWith;
use crate::EmitError;
use crate::EmitOptions;
use crate::EmittedSource;
use crate::ModuleSpecifier;
use crate::ParseDiagnostic;
use crate::ParseDiagnosticsError;
use crate::ParsedSource;
Expand Down Expand Up @@ -161,43 +163,117 @@ impl TranspileOptions {

impl ParsedSource {
/// Transform a TypeScript file into a JavaScript file.
///
/// Note: This will clone the program if it's shared, which
/// might be expensive.
pub fn transpile(
Copy link
Member Author

@dsherret dsherret Apr 11, 2024

Choose a reason for hiding this comment

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

We should probably get rid of this in the future and make it internal. I just wanted to do this in a backwards compatible way for the time being (that said, I also wrote this so we can manually implement transpile_owned_with_fallback and have logging for that, but perhaps we could add logging in here for when it transpiles a non-exclusive ParsedSource)

&self,
transpile_options: &TranspileOptions,
emit_options: &EmitOptions,
) -> Result<EmittedSource, TranspileError> {
if transpile_options.use_decorators_proposal
&& transpile_options.use_ts_decorators
{
return Err(TranspileError::DecoratorOptionsConflict);
}

let program = (*self.program()).clone();

let source_map = SourceMap::single(
transpile(
self.specifier().clone(),
self.text_info().text_str().to_string(),
);
program,
// we need the comments to be mutable, so make it single threaded
self.comments().as_single_threaded(),
transpile_options,
emit_options,
self.diagnostics(),
)
}

// we need the comments to be mutable, so make it single threaded
let comments = self.comments().as_single_threaded();
let globals = Globals::new();
let program = crate::swc::common::GLOBALS.set(&globals, || {
let top_level_mark = Mark::fresh(Mark::root());
fold_program(
program,
transpile_options,
&source_map,
&comments,
top_level_mark,
self.diagnostics(),
)
})?;
/// Transform a TypeScript file into a JavaScript file consuming
/// the internals of the `ParsedSource`. Returns an `Err(ParsedSource)`
/// when the `ParsedSource` is shared.
pub fn transpile_owned(
self,
transpile_options: &TranspileOptions,
emit_options: &EmitOptions,
) -> Result<Result<EmittedSource, TranspileError>, ParsedSource> {
let inner = match Arc::try_unwrap(self.inner) {
Ok(inner) => inner,
Err(inner) => return Err(ParsedSource { inner }),
};
let program = match Arc::try_unwrap(inner.program) {
Ok(program) => program,
Err(program) => {
return Err(ParsedSource {
inner: Arc::new(crate::ParsedSourceInner {
specifier: inner.specifier,
media_type: inner.media_type,
text_info: inner.text_info,
comments: inner.comments,
program,
tokens: inner.tokens,
syntax_contexts: inner.syntax_contexts,
diagnostics: inner.diagnostics,
}),
})
}
};
Ok(transpile(
inner.specifier,
inner.text_info.text_str().to_string(),
program,
// we need the comments to be mutable, so make it single threaded
inner.comments.into_single_threaded(),
transpile_options,
emit_options,
&inner.diagnostics,
))
}

Ok(emit(&program, &comments, &source_map, emit_options)?)
/// Attempts to transpile owned, then falls back to cloning the program.
pub fn transpile_owned_with_fallback(
self,
transpile_options: &TranspileOptions,
emit_options: &EmitOptions,
) -> Result<EmittedSource, TranspileError> {
match self.transpile_owned(transpile_options, emit_options) {
Ok(result) => result,
Err(parsed_source) => {
// fallback
parsed_source.transpile(transpile_options, emit_options)
}
}
}
}

fn transpile(
specifier: ModuleSpecifier,
source: String,
program: Program,
comments: SingleThreadedComments,
transpile_options: &TranspileOptions,
emit_options: &EmitOptions,
diagnostics: &[ParseDiagnostic],
) -> Result<EmittedSource, TranspileError> {
if transpile_options.use_decorators_proposal
&& transpile_options.use_ts_decorators
{
return Err(TranspileError::DecoratorOptionsConflict);
}

let source_map = SourceMap::single(specifier, source);

let globals = Globals::new();
let program = crate::swc::common::GLOBALS.set(&globals, || {
let top_level_mark = Mark::fresh(Mark::root());
fold_program(
program,
transpile_options,
&source_map,
&comments,
top_level_mark,
diagnostics,
)
})?;

Ok(emit(&program, &comments, &source_map, emit_options)?)
}

#[derive(Default, Clone)]
struct DiagnosticCollector {
diagnostics_cell: Rc<RefCell<Vec<SwcDiagnostic>>>,
Expand Down Expand Up @@ -1413,4 +1489,115 @@ const a = _jsx(Foo, {
);
assert_eq!(emit_result.source_map, None);
}

#[test]
fn test_transpile_owned_when_owned() {
let specifier =
ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap();
let source = r#"const foo: string = "bar";"#;
let module = parse_module(ParseParams {
specifier,
text_info: SourceTextInfo::from_string(source.to_string()),
media_type: MediaType::TypeScript,
capture_tokens: false,
maybe_syntax: None,
scope_analysis: false,
})
.unwrap();
let emit_result = module
.transpile_owned(
&TranspileOptions::default(),
&EmitOptions {
source_map: SourceMapOption::None,
..Default::default()
},
)
.unwrap()
.unwrap();
assert_eq!(&emit_result.text, "const foo = \"bar\";\n");
}

#[test]
fn test_transpile_owned_when_cloned() {
let specifier =
ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap();
let source = r#"const foo: string = "bar";"#;
let module = parse_module(ParseParams {
specifier,
text_info: SourceTextInfo::from_string(source.to_string()),
media_type: MediaType::TypeScript,
capture_tokens: false,
maybe_syntax: None,
scope_analysis: false,
})
.unwrap();

// won't transpile since the module is cloned
let borrowed_module = module.clone();
let result =
module.transpile_owned(&Default::default(), &Default::default());
let module = result.err().unwrap();
drop(borrowed_module);

// won't transpile since the program is cloned
let borrowed_program = module.program().clone();
let result =
module.transpile_owned(&Default::default(), &Default::default());
let module = result.err().unwrap();
drop(borrowed_program);

// now it will work
let emit_result = module
.transpile_owned(
&TranspileOptions::default(),
&EmitOptions {
source_map: SourceMapOption::None,
..Default::default()
},
)
.unwrap()
.unwrap();
assert_eq!(&emit_result.text, "const foo = \"bar\";\n");
}

#[test]
fn test_transpile_owned_with_fallback() {
let specifier =
ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap();
let source = r#"const foo: string = "bar";"#;
let module = parse_module(ParseParams {
specifier,
text_info: SourceTextInfo::from_string(source.to_string()),
media_type: MediaType::TypeScript,
capture_tokens: false,
maybe_syntax: None,
scope_analysis: false,
})
.unwrap();

// even though it's borrowed, it will transpile
let borrowed_module = module.clone();
let emit_result = module
.transpile_owned_with_fallback(
&TranspileOptions::default(),
&EmitOptions {
source_map: SourceMapOption::None,
..Default::default()
},
)
.unwrap();
assert_eq!(&emit_result.text, "const foo = \"bar\";\n");

// now it's owned, should still work
let emit_result = borrowed_module
.transpile_owned_with_fallback(
&TranspileOptions::default(),
&EmitOptions {
source_map: SourceMapOption::None,
..Default::default()
},
)
.unwrap();
assert_eq!(&emit_result.text, "const foo = \"bar\";\n");
}
}
Loading