/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/repl/session.rs
1 160 строк
34 KB
crowlbot
fix: don't print duplicate parse diagnostics (#35933)
10 июл 2026, 22:02
Не верифицирован
10 июл 2026, 22:02
ddebb90
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicI32; use deno_ast::ImportsNotUsedAsValues; use deno_ast::JsxAutomaticOptions; use deno_ast::JsxClassicOptions; use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; use deno_ast::ParseDiagnosticsError; use deno_ast::ParsedSource; use deno_ast::SourcePos; use deno_ast::SourceRangedForSpanned; use deno_ast::SourceTextInfo; use deno_ast::diagnostics::Diagnostic; use deno_ast::swc::ast as swc_ast; use deno_ast::swc::atoms::Wtf8Atom; use deno_ast::swc::common::comments::CommentKind; use deno_ast::swc::ecma_visit::Visit; use deno_ast::swc::ecma_visit::VisitWith; use deno_ast::swc::ecma_visit::noop_visit_type; use deno_core::LocalInspectorSession; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::error::CoreError; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; use deno_core::futures::channel::mpsc::UnboundedReceiver; use deno_core::futures::channel::mpsc::UnboundedSender; use deno_core::futures::channel::mpsc::unbounded; use deno_core::parking_lot::Mutex as SyncMutex; use deno_core::serde_json; use deno_core::serde_json::Value; use deno_core::unsync::spawn; use deno_core::url::Url; use deno_error::JsErrorBox; use deno_graph::Position; use deno_graph::PositionRange; use deno_graph::analysis::SpecifierWithRange; use deno_lib::util::result::any_and_jserrorbox_downcast_ref; use deno_resolver::deno_json::CompilerOptionsResolver; use deno_runtime::worker::MainWorker; use deno_semver::npm::NpmPackageReqReference; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; use once_cell::sync::Lazy; use regex::Match; use regex::Regex; use tokio::sync::Mutex; use tokio::sync::oneshot; use crate::args::CliOptions; use crate::cdp; use crate::cdp::RemoteObjectId; use crate::colors; use crate::npm::CliNpmInstaller; use crate::resolver::CliResolver; use crate::tools::test::TestEventReceiver; use crate::tools::test::TestEventTracker; use crate::tools::test::TestFailureFormatOptions; use crate::tools::test::report_tests; use crate::tools::test::reporters::PrettyTestReporter; use crate::tools::test::reporters::TestReporter; use crate::tools::test::run_tests_for_worker; use crate::tools::test::worker_has_tests; fn comment_source_to_position_range( comment_start: SourcePos, m: &Match, text_info: &SourceTextInfo, is_jsx_import_source: bool, ) -> PositionRange { // the comment text starts after the double slash or slash star, so add 2 let comment_start = comment_start + 2; // -1 and +1 to include the quotes, but not for jsx import sources because // they don't have quotes let padding = if is_jsx_import_source { 0 } else { 1 }; PositionRange { start: Position::from_source_pos( comment_start + m.start() - padding, text_info, ), end: Position::from_source_pos( comment_start + m.end() + padding, text_info, ), } } fn get_prelude() -> String { r#"(() => { const repl_internal = { String, lastEvalResult: undefined, lastThrownError: undefined, inspectArgs: Deno[Deno.internal].inspectArgs, noColor: Deno.noColor, get closed() { try { return typeof globalThis.closed === 'undefined' ? false : globalThis.closed; } catch { return false; } } }; Object.defineProperty(globalThis, "_", { configurable: true, get: () => repl_internal.lastEvalResult, set: (value) => { Object.defineProperty(globalThis, "_", { value: value, writable: true, enumerable: true, configurable: true, }); console.log("Last evaluation result is no longer saved to _."); }, }); Object.defineProperty(globalThis, "_error", { configurable: true, get: () => repl_internal.lastThrownError, set: (value) => { Object.defineProperty(globalThis, "_error", { value: value, writable: true, enumerable: true, configurable: true, }); console.log("Last thrown error is no longer saved to _error."); }, }); globalThis.clear = console.clear.bind(console); return repl_internal; })()"#.to_string() } pub enum EvaluationOutput { Value(String), Error(String), } impl std::fmt::Display for EvaluationOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EvaluationOutput::Value(value) => f.write_str(value), EvaluationOutput::Error(value) => f.write_str(value), } } } pub fn result_to_evaluation_output( r: Result<EvaluationOutput, AnyError>, ) -> EvaluationOutput { match r { Ok(value) => value, Err(err) => { EvaluationOutput::Error(format!("{} {:#}", colors::red("error:"), err)) } } } #[derive(Debug)] pub struct TsEvaluateResponse { pub value: cdp::EvaluateResponse, } pub struct ReplSession { internal_object_id: Option<RemoteObjectId>, npm_installer: Option<Arc<CliNpmInstaller>>, resolver: Arc<CliResolver>, // NB: `session` and `state` must come before Worker, so that relevant V8 objects // are dropped before the isolate is dropped with `worker`. session: LocalInspectorSession, state: ReplSessionState, pub worker: MainWorker, pub context_id: u64, pub notifications: Arc<Mutex<UnboundedReceiver<Value>>>, referrer: ModuleSpecifier, main_module: ModuleSpecifier, test_reporter_factory: Box<dyn Fn() -> Box<dyn TestReporter>>, /// This is only optional because it's temporarily taken when evaluating. test_event_receiver: Option<TestEventReceiver>, jsx: deno_ast::JsxRuntime, decorators: deno_ast::DecoratorsTranspileOption, } // TODO: duplicated in `cli/tools/run/hmr.rs` #[derive(Debug)] enum InspectorMessageState { Ready(serde_json::Value), WaitingFor(oneshot::Sender<serde_json::Value>), } #[derive(Debug)] pub struct ReplSessionInner { messages: HashMap<i32, InspectorMessageState>, notification_tx: UnboundedSender<serde_json::Value>, } #[derive(Clone, Debug)] pub struct ReplSessionState(Arc<SyncMutex<ReplSessionInner>>); impl ReplSessionState { pub fn new(notification_tx: UnboundedSender<serde_json::Value>) -> Self { Self(Arc::new(SyncMutex::new(ReplSessionInner { messages: HashMap::new(), notification_tx, }))) } fn callback(&self, msg: deno_core::InspectorMsg) { let deno_core::InspectorMsgKind::Message(msg_id) = msg.kind else { if let Ok(value) = serde_json::from_str(&msg.content) { let _ = self.0.lock().notification_tx.unbounded_send(value); } return; }; let message: serde_json::Value = match serde_json::from_str(&msg.content) { Ok(v) => v, Err(error) => match error.classify() { serde_json::error::Category::Syntax => serde_json::json!({ "id": msg_id, "result": { "result": { "type": "error", "description": "Unterminated string literal", "value": "Unterminated string literal", }, "exceptionDetails": { "exceptionId": 0, "text": "Unterminated string literal", "lineNumber": 0, "columnNumber": 0 }, }, }), _ => panic!("Could not parse inspector message"), }, }; let mut state = self.0.lock(); let Some(message_state) = state.messages.remove(&msg_id) else { state .messages .insert(msg_id, InspectorMessageState::Ready(message)); return; }; let InspectorMessageState::WaitingFor(sender) = message_state else { return; }; let _ = sender.send(message); } async fn wait_for_response(&self, msg_id: i32) -> serde_json::Value { let rx = { let mut state = self.0.lock(); if let Some(message_state) = state.messages.remove(&msg_id) { let InspectorMessageState::Ready(value) = message_state else { unreachable!(); }; return Self::extract_result(value); } let (tx, rx) = oneshot::channel(); state .messages .insert(msg_id, InspectorMessageState::WaitingFor(tx)); rx }; let value = rx.await.unwrap(); Self::extract_result(value) } #[allow(clippy::print_stderr, reason = "diagnostic for flaky CDP responses")] fn extract_result(mut value: serde_json::Value) -> serde_json::Value { let result = value["result"].take(); if result.is_null() { eprintln!( "CDP response has null result. Full response: {}", serde_json::to_string(&value).unwrap_or_default() ); } result } } static NEXT_MSG_ID: AtomicI32 = AtomicI32::new(0); fn next_msg_id() -> i32 { NEXT_MSG_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) } impl ReplSession { #[allow(clippy::too_many_arguments, reason = "construction")] pub async fn initialize( cli_options: &CliOptions, npm_installer: Option<Arc<CliNpmInstaller>>, resolver: Arc<CliResolver>, compiler_options_resolver: &CompilerOptionsResolver, mut worker: MainWorker, main_module: ModuleSpecifier, test_event_receiver: TestEventReceiver, ) -> Result<Self, AnyError> { let (notification_tx, mut notification_rx) = unbounded(); let repl_session_state = ReplSessionState::new(notification_tx); let state = repl_session_state.clone(); let callback = Box::new(move |message| repl_session_state.callback(message)); let mut session = worker.create_inspector_session(callback); session.post_message::<()>(next_msg_id(), "Runtime.enable", None); // Enabling the runtime domain will always send trigger one executionContextCreated for each // context the inspector knows about so we grab the execution context from that since // our inspector does not support a default context (0 is an invalid context id). let context_id: u64; loop { let notification = notification_rx.next().await.unwrap(); let notification = serde_json::from_value::<cdp::Notification>(notification)?; if notification.method == "Runtime.executionContextCreated" { let execution_context_created = serde_json::from_value::< cdp::ExecutionContextCreated, >(notification.params)?; assert!( execution_context_created .context .aux_data .get("isDefault") .unwrap() .as_bool() .unwrap() ); context_id = execution_context_created.context.id; break; } } assert_ne!(context_id, 0); let referrer = deno_core::resolve_path("./$deno$repl.mts", cli_options.initial_cwd()) .unwrap(); let cwd_url = Url::from_directory_path(cli_options.initial_cwd()).map_err(|_| { anyhow!( "Unable to construct URL from the path of cwd: {}", cli_options.initial_cwd().to_string_lossy(), ) })?; let transpile_options = &compiler_options_resolver .for_specifier(&cwd_url) .transpile_options()? .transpile; let mut repl_session = ReplSession { internal_object_id: None, npm_installer, resolver, worker, session, state, context_id, referrer, notifications: Arc::new(Mutex::new(notification_rx)), test_reporter_factory: Box::new(move || { Box::new(PrettyTestReporter::new( false, true, false, true, cwd_url.clone(), TestFailureFormatOptions::default(), )) }), main_module, test_event_receiver: Some(test_event_receiver), jsx: transpile_options.jsx.clone().unwrap_or_default(), decorators: transpile_options.decorators.clone(), }; // inject prelude let evaluated = repl_session.evaluate_expression(&get_prelude()).await?; repl_session.internal_object_id = evaluated.result.object_id; Ok(repl_session) } pub async fn closing(&mut self) -> Result<bool, AnyError> { let result = self .call_function_on_repl_internal_obj( r#"function () { return this.closed; }"#.to_string(), &[], ) .await? .result; let closed = result .value .ok_or_else(|| anyhow!(result.description.unwrap()))? .as_bool() .unwrap(); Ok(closed) } pub async fn post_message_with_event_loop<T: serde::Serialize>( &mut self, method: &str, params: Option<T>, ) -> Value { let msg_id = next_msg_id(); self.session.post_message(msg_id, method, params); // Under Explicit microtask policy, V8's REPL-mode evaluation creates // a promise whose resolution callback is a queued microtask. Without // this checkpoint, GC can collect the weakly-held promise before the // microtask drains, causing a "Promise was collected" CDP error. self .worker .js_runtime .v8_isolate() .perform_microtask_checkpoint(); let fut = self .state .wait_for_response(msg_id) .map(Ok::<_, ()>) .boxed_local(); self .worker .js_runtime .with_event_loop_future(fut, Default::default()) .await .unwrap() } pub async fn run_event_loop(&mut self) -> Result<(), CoreError> { // Pump inspector sessions ahead of the rest of the event-loop tick and // drain microtasks before any other V8 work runs. Under V8's Explicit // microtask policy, REPL-mode Runtime.evaluate (replMode: true) wraps // the expression in an async IIFE and tracks the result promise via a // weak handle. If GC runs after the inspector dispatch but before the // resolution microtask drains, the weakly-held promise is collected and // the CDP client gets a `-32000 "Promise was collected"` error. Doing // the dispatch + drain pair here closes that window for external // debuggers attached via `deno repl --inspect`. // (post_message_with_event_loop above handles the same race for the // in-process REPL session.) std::future::poll_fn(|cx| { self .worker .js_runtime .inspector() .poll_sessions_from_event_loop(cx); self .worker .js_runtime .v8_isolate() .perform_microtask_checkpoint(); let poll_result = self.worker.js_runtime.poll_event_loop( cx, deno_core::PollEventLoopOptions { wait_for_inspector: true, }, ); // Flush inspector sessions again after the event-loop tick. A timer (or // other async) callback that ran during `poll_event_loop` may have // produced inspector notifications (e.g. `Runtime.exceptionThrown` from // an uncaught error). These are queued on the session's outbound channel // and are only delivered to the REPL's notification channel when the // sessions are pumped. Without this second pump they would linger until // the next evaluation, so an uncaught exception thrown from a timeout // would not be printed until the user evaluated another expression (see // https://github.com/denoland/deno/issues/21622). Delivering them here // wakes the `notifications` stream in the REPL read loop right away. self .worker .js_runtime .inspector() .poll_sessions_from_event_loop(cx); poll_result }) .await } pub async fn evaluate_line_and_get_output( &mut self, line: &str, ) -> EvaluationOutput { fn format_diagnostic(diagnostic: &deno_ast::ParseDiagnostic) -> String { let display_position = diagnostic.display_position(); format!( "{}: {} at {}:{}", colors::red("parse error"), diagnostic.message(), display_position.line_number, display_position.column_number, ) } async fn inner( session: &mut ReplSession, line: &str, ) -> Result<EvaluationOutput, AnyError> { match session.evaluate_line_with_object_wrapping(line).await { Ok(evaluate_response) => { let cdp::EvaluateResponse { result, exception_details, } = evaluate_response.value; Ok(if let Some(exception_details) = exception_details { session.set_last_thrown_error(&result).await?; let description = match exception_details.exception { Some(exception) => { if let Some(description) = exception.description { description } else if let Some(value) = exception.value { value.to_string() } else { "undefined".to_string() } } None => "Unknown exception".to_string(), }; EvaluationOutput::Error(format!( "{} {}", exception_details.text, description )) } else { session.set_last_eval_result(&result).await?; let value = session.get_eval_value(&result).await?; EvaluationOutput::Value(value) }) } Err(err) => { // handle a parsing diagnostic match any_and_jserrorbox_downcast_ref::<deno_ast::ParseDiagnostic>( &err, ) { Some(diagnostic) => { Ok(EvaluationOutput::Error(format_diagnostic(diagnostic))) } None => { match any_and_jserrorbox_downcast_ref::<ParseDiagnosticsError>( &err, ) { Some(diagnostics) => Ok(EvaluationOutput::Error( diagnostics .0 .iter() .map(format_diagnostic) .collect::<Vec<_>>() .join("\n\n"), )), None => Err(err), } } } } } } let result = inner(self, line).await; result_to_evaluation_output(result) } pub async fn evaluate_line_with_object_wrapping( &mut self, line: &str, ) -> Result<TsEvaluateResponse, AnyError> { // Expressions like { "foo": "bar" } are interpreted as block expressions at the // statement level rather than an object literal so we interpret it as an expression statement // to match the behavior found in a typical prompt including browser developer tools. let wrapped_line = if line.trim_start().starts_with('{') && !line.trim_end().ends_with(';') { format!("({})", &line) } else { line.to_string() }; let evaluate_response = self.evaluate_ts_expression(&wrapped_line).await; // If that fails, we retry it without wrapping in parens letting the error bubble up to the // user if it is still an error. let result = if wrapped_line != line && (evaluate_response.is_err() || evaluate_response .as_ref() .unwrap() .value .exception_details .is_some()) { self.evaluate_ts_expression(line).await } else { evaluate_response }; if worker_has_tests(&mut self.worker) { let report_tests_handle = spawn(report_tests( self.test_event_receiver.take().unwrap(), (self.test_reporter_factory)(), )); let event_tracker = TestEventTracker::new(self.worker.js_runtime.op_state()); run_tests_for_worker( &mut self.worker, &self.main_module, &Default::default(), &Default::default(), &event_tracker, ) .await .unwrap(); event_tracker.force_end_report().unwrap(); self.test_event_receiver = Some(report_tests_handle.await.unwrap().1); } result } pub async fn set_last_thrown_error( &mut self, error: &cdp::RemoteObject, ) -> Result<(), AnyError> { self .post_message_with_event_loop( "Runtime.callFunctionOn", Some(cdp::CallFunctionOnArgs { function_declaration: r#"function (object) { this.lastThrownError = object; }"# .to_string(), object_id: self.internal_object_id.clone(), arguments: Some(vec![error.into()]), silent: None, return_by_value: None, generate_preview: None, user_gesture: None, await_promise: None, execution_context_id: None, object_group: None, throw_on_side_effect: None, }), ) .await; Ok(()) } pub async fn set_last_eval_result( &mut self, evaluate_result: &cdp::RemoteObject, ) -> Result<(), AnyError> { self .post_message_with_event_loop( "Runtime.callFunctionOn", Some(cdp::CallFunctionOnArgs { function_declaration: r#"function (object) { this.lastEvalResult = object; }"#.to_string(), object_id: self.internal_object_id.clone(), arguments: Some(vec![evaluate_result.into()]), silent: None, return_by_value: None, generate_preview: None, user_gesture: None, await_promise: None, execution_context_id: None, object_group: None, throw_on_side_effect: None, }), ) .await; Ok(()) } pub async fn call_function_on_args( &mut self, function_declaration: String, args: &[cdp::RemoteObject], ) -> Result<cdp::CallFunctionOnResponse, AnyError> { let arguments: Option<Vec<cdp::CallArgument>> = if args.is_empty() { None } else { Some(args.iter().map(|a| a.into()).collect()) }; let inspect_response = self .post_message_with_event_loop( "Runtime.callFunctionOn", Some(cdp::CallFunctionOnArgs { function_declaration, object_id: None, arguments, silent: None, return_by_value: None, generate_preview: None, user_gesture: None, await_promise: None, execution_context_id: Some(self.context_id), object_group: None, throw_on_side_effect: None, }), ) .await; let response: cdp::CallFunctionOnResponse = serde_json::from_value(inspect_response)?; Ok(response) } pub async fn call_function_on_repl_internal_obj( &mut self, function_declaration: String, args: &[cdp::RemoteObject], ) -> Result<cdp::CallFunctionOnResponse, AnyError> { let arguments: Option<Vec<cdp::CallArgument>> = if args.is_empty() { None } else { Some(args.iter().map(|a| a.into()).collect()) }; let inspect_response = self .post_message_with_event_loop( "Runtime.callFunctionOn", Some(cdp::CallFunctionOnArgs { function_declaration, object_id: self.internal_object_id.clone(), arguments, silent: None, return_by_value: None, generate_preview: None, user_gesture: None, await_promise: None, execution_context_id: None, object_group: None, throw_on_side_effect: None, }), ) .await; let response: cdp::CallFunctionOnResponse = serde_json::from_value(inspect_response)?; Ok(response) } pub async fn get_eval_value( &mut self, evaluate_result: &cdp::RemoteObject, ) -> Result<String, AnyError> { // TODO(caspervonb) we should investigate using previews here but to keep things // consistent with the previous implementation we just get the preview result from // Deno.inspectArgs. let response = self .call_function_on_repl_internal_obj( r#"function (object) { try { return this.inspectArgs(["%o", object], { colors: !this.noColor }); } catch (err) { return this.inspectArgs(["%o", err]); } }"# .to_string(), std::slice::from_ref(evaluate_result), ) .await?; let s = response .result .value .map(|v| v.as_str().unwrap().to_string()) .or(response.result.description) .ok_or_else(|| anyhow!("failed to evaluate expression"))?; Ok(s) } async fn evaluate_ts_expression( &mut self, expression: &str, ) -> Result<TsEvaluateResponse, AnyError> { let tsx_result = parse_source_as(expression.to_string(), deno_ast::MediaType::Tsx); // Prefer a clean `.tsx` parse. Fall back to parsing as TypeScript when the // `.tsx` parse fails outright, or when it only recovered by inserting an // `Invalid` placeholder node (e.g. a TypeScript type assertion like // `<string>x` is misread as JSX in `.tsx`). let needs_ts_fallback = match &tsx_result { Ok(parsed) => { deno_resolver::emit::invalid_syntax_parse_diagnostics(parsed).is_some() } Err(_) => true, }; let parsed_source = match tsx_result { Ok(parsed) if !needs_ts_fallback => parsed, tsx_result => match parse_source_as( expression.to_string(), deno_ast::MediaType::TypeScript, ) { Ok(parsed) => parsed, Err(ts_err) => match tsx_result { // Both parses have errors; report the recovered `.tsx` diagnostics // via the check below. Ok(parsed) => parsed, Err(_) => return Err(ts_err), }, }, }; // If `swc` recovered from a syntax error by inserting an `Invalid` // placeholder node, surface the precise parse diagnostic instead of // transpiling to `<invalid>` and letting V8 report a misleading // `Unexpected token '<'`. See denoland/deno#19457. if let Some(diagnostics) = deno_resolver::emit::invalid_syntax_parse_diagnostics(&parsed_source) { return Err(diagnostics.into()); } self .check_for_npm_or_node_imports(&parsed_source.program()) .await?; self.analyze_and_handle_jsx(&parsed_source); let transpiled_src = parsed_source .transpile( &deno_ast::TranspileOptions { decorators: self.decorators.clone(), imports_not_used_as_values: ImportsNotUsedAsValues::Preserve, jsx: Some(self.jsx.clone()), var_decl_imports: true, verbatim_module_syntax: false, }, &deno_ast::TranspileModuleOptions { module_kind: Some(ModuleKind::Esm), }, &deno_ast::EmitOptions { source_map: deno_ast::SourceMapOption::None, source_map_base: None, source_map_file: None, inline_sources: false, remove_comments: false, }, ) .map_err(deno_resolver::emit::dedupe_transpile_error)? .into_source() .text; let value = self .evaluate_expression(&format!("'use strict'; void 0;{transpiled_src}")) .await?; Ok(TsEvaluateResponse { value }) } fn analyze_and_handle_jsx(&mut self, parsed_source: &ParsedSource) { let Some(analyzed_pragmas) = analyze_jsx_pragmas(parsed_source) else { return; }; if !analyzed_pragmas.has_any() { return; } if let Some(jsx) = analyzed_pragmas.jsx { match &mut self.jsx { deno_ast::JsxRuntime::Classic(jsx_classic_options) => { jsx_classic_options.factory = jsx.text; } deno_ast::JsxRuntime::Automatic(_) | deno_ast::JsxRuntime::Precompile(_) => { self.jsx = deno_ast::JsxRuntime::Classic(JsxClassicOptions { factory: jsx.text, ..Default::default() }); } } } if let Some(jsx_frag) = analyzed_pragmas.jsx_fragment { match &mut self.jsx { deno_ast::JsxRuntime::Classic(jsx_classic_options) => { jsx_classic_options.fragment_factory = jsx_frag.text; } deno_ast::JsxRuntime::Automatic(_) | deno_ast::JsxRuntime::Precompile(_) => { self.jsx = deno_ast::JsxRuntime::Classic(JsxClassicOptions { fragment_factory: jsx_frag.text, ..Default::default() }); } } } if let Some(jsx_import_source) = analyzed_pragmas.jsx_import_source { match &mut self.jsx { deno_ast::JsxRuntime::Classic(_) => { self.jsx = deno_ast::JsxRuntime::Automatic(JsxAutomaticOptions { import_source: Some(jsx_import_source.text), development: false, }); } deno_ast::JsxRuntime::Automatic(automatic) | deno_ast::JsxRuntime::Precompile(deno_ast::JsxPrecompileOptions { automatic, .. }) => { automatic.import_source = Some(jsx_import_source.text); } } } } async fn check_for_npm_or_node_imports( &mut self, program: &swc_ast::Program, ) -> Result<(), AnyError> { let Some(npm_installer) = &self.npm_installer else { return Ok(()); }; let mut collector = ImportCollector::new(); program.visit_with(&mut collector); let resolved_imports = collector .imports .iter() .flat_map(|i| { let specifier = i.to_string_lossy(); self .resolver .resolve( &specifier, &self.referrer, deno_graph::Position::zeroed(), ResolutionMode::Import, NodeResolutionKind::Execution, ) .ok() .or_else(|| ModuleSpecifier::parse(&specifier).ok()) }) .collect::<Vec<_>>(); let npm_imports = resolved_imports .iter() .flat_map(|url| NpmPackageReqReference::from_specifier(url).ok()) .map(|r| r.into_inner().req) .collect::<Vec<_>>(); if !npm_imports.is_empty() { npm_installer .add_and_cache_package_reqs(&npm_imports) .await?; } Ok(()) } async fn evaluate_expression( &mut self, expression: &str, ) -> Result<cdp::EvaluateResponse, JsErrorBox> { let res = self .post_message_with_event_loop( "Runtime.evaluate", Some(cdp::EvaluateArgs { expression: expression.to_string(), object_group: None, include_command_line_api: None, silent: None, context_id: Some(self.context_id), return_by_value: None, generate_preview: None, user_gesture: None, await_promise: Some(true), throw_on_side_effect: None, timeout: None, disable_breaks: None, repl_mode: Some(true), allow_unsafe_eval_blocked_by_csp: None, unique_context_id: None, }), ) .await; serde_json::from_value(res).map_err(JsErrorBox::from_err) } } /// Walk an AST and get all import specifiers for analysis if any of them is /// an npm specifier. struct ImportCollector { pub imports: Vec<Wtf8Atom>, } impl ImportCollector { pub fn new() -> Self { Self { imports: vec![] } } } impl Visit for ImportCollector { noop_visit_type!(); fn visit_call_expr(&mut self, call_expr: &swc_ast::CallExpr) { if !matches!(call_expr.callee, swc_ast::Callee::Import(_)) { return; } if !call_expr.args.is_empty() { let arg = &call_expr.args[0]; if let swc_ast::Expr::Lit(swc_ast::Lit::Str(str_lit)) = &*arg.expr { self.imports.push(str_lit.value.clone()); } } } fn visit_module_decl(&mut self, module_decl: &swc_ast::ModuleDecl) { use deno_ast::swc::ast::*; match module_decl { ModuleDecl::Import(import_decl) => { if import_decl.type_only { return; } self.imports.push(import_decl.src.value.clone()); } ModuleDecl::ExportAll(export_all) => { self.imports.push(export_all.src.value.clone()); } ModuleDecl::ExportNamed(export_named) => { if let Some(src) = &export_named.src { self.imports.push(src.value.clone()); } } _ => {} } } } fn parse_source_as( source: String, media_type: deno_ast::MediaType, ) -> Result<deno_ast::ParsedSource, AnyError> { let specifier = if media_type == deno_ast::MediaType::Tsx { ModuleSpecifier::parse("file:///repl.tsx").unwrap() } else { ModuleSpecifier::parse("file:///repl.ts").unwrap() }; let parsed = deno_ast::parse_module(deno_ast::ParseParams { specifier, text: source.into(), media_type, capture_tokens: true, maybe_syntax: None, scope_analysis: false, })?; Ok(parsed) } // TODO(bartlomieju): remove these and use regexes from `deno_graph` /// Matches the `@jsxImportSource` pragma. static JSX_IMPORT_SOURCE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsxImportSource\s+(\S+)").unwrap()); /// Matches the `@jsx` pragma. static JSX_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsx\s+(\S+)").unwrap()); /// Matches the `@jsxFrag` pragma. static JSX_FRAG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsxFrag\s+(\S+)").unwrap()); #[derive(Default, Debug)] struct AnalyzedJsxPragmas { /// Information about `@jsxImportSource` pragma. jsx_import_source: Option<SpecifierWithRange>, /// Matches the `@jsx` pragma. jsx: Option<SpecifierWithRange>, /// Matches the `@jsxFrag` pragma. jsx_fragment: Option<SpecifierWithRange>, } impl AnalyzedJsxPragmas { fn has_any(&self) -> bool { self.jsx_import_source.is_some() || self.jsx.is_some() || self.jsx_fragment.is_some() } } /// Analyze provided source and return information about carious pragmas /// used to configure the JSX transforms. fn analyze_jsx_pragmas( parsed_source: &ParsedSource, ) -> Option<AnalyzedJsxPragmas> { if !matches!( parsed_source.media_type(), deno_ast::MediaType::Jsx | deno_ast::MediaType::Tsx ) { return None; } let mut analyzed_pragmas = AnalyzedJsxPragmas::default(); for c in parsed_source.get_leading_comments()?.iter() { if c.kind != CommentKind::Block { continue; // invalid } if let Some(captures) = JSX_IMPORT_SOURCE_RE.captures(&c.text) && let Some(m) = captures.get(1) { analyzed_pragmas.jsx_import_source = Some(SpecifierWithRange { text: m.as_str().to_string(), range: comment_source_to_position_range( c.start(), &m, parsed_source.text_info_lazy(), true, ), }); } if let Some(captures) = JSX_RE.captures(&c.text) && let Some(m) = captures.get(1) { analyzed_pragmas.jsx = Some(SpecifierWithRange { text: m.as_str().to_string(), range: comment_source_to_position_range( c.start(), &m, parsed_source.text_info_lazy(), false, ), }); } if let Some(captures) = JSX_FRAG_RE.captures(&c.text) && let Some(m) = captures.get(1) { analyzed_pragmas.jsx_fragment = Some(SpecifierWithRange { text: m.as_str().to_string(), range: comment_source_to_position_range( c.start(), &m, parsed_source.text_info_lazy(), false, ), }); } } Some(analyzed_pragmas) }