mirror of
https://github.com/denoland/deno.git
synced 2024-11-22 15:06:54 -05:00
feat(lsp): diagnostics for deno types and triple-slash refs (#10699)
Fixes #9823
This commit is contained in:
parent
6044b037fb
commit
cd4194a5e4
6 changed files with 335 additions and 75 deletions
|
@ -140,6 +140,7 @@ pub struct Dependency {
|
|||
pub maybe_code: Option<ResolvedDependency>,
|
||||
pub maybe_code_specifier_range: Option<Range>,
|
||||
pub maybe_type: Option<ResolvedDependency>,
|
||||
pub maybe_type_specifier_range: Option<Range>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -259,13 +260,24 @@ pub fn analyze_dependencies(
|
|||
|
||||
// Parse leading comments for supported triple slash references.
|
||||
for comment in parsed_module.get_leading_comments().iter() {
|
||||
if let Some(ts_reference) = parse_ts_reference(&comment.text) {
|
||||
if let Some((ts_reference, span)) = parse_ts_reference(&comment) {
|
||||
let loc = parsed_module.source_map.lookup_char_pos(span.lo);
|
||||
match ts_reference {
|
||||
TypeScriptReference::Path(import) => {
|
||||
let dep = dependencies.entry(import.clone()).or_default();
|
||||
let resolved_import =
|
||||
resolve_import(&import, specifier, maybe_import_map);
|
||||
dep.maybe_code = Some(resolved_import);
|
||||
dep.maybe_code_specifier_range = Some(Range {
|
||||
start: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: loc.col_display as u32,
|
||||
},
|
||||
end: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: (loc.col_display + import.chars().count() + 2) as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
TypeScriptReference::Types(import) => {
|
||||
let resolved_import =
|
||||
|
@ -273,11 +285,20 @@ pub fn analyze_dependencies(
|
|||
if media_type == &MediaType::JavaScript
|
||||
|| media_type == &MediaType::Jsx
|
||||
{
|
||||
maybe_type = Some(resolved_import)
|
||||
} else {
|
||||
let dep = dependencies.entry(import).or_default();
|
||||
dep.maybe_type = Some(resolved_import);
|
||||
maybe_type = Some(resolved_import.clone());
|
||||
}
|
||||
let dep = dependencies.entry(import.clone()).or_default();
|
||||
dep.maybe_type = Some(resolved_import);
|
||||
dep.maybe_type_specifier_range = Some(Range {
|
||||
start: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: loc.col_display as u32,
|
||||
},
|
||||
end: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: (loc.col_display + import.chars().count() + 2) as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +315,13 @@ pub fn analyze_dependencies(
|
|||
let maybe_resolved_type_dependency =
|
||||
// Check for `@deno-types` pragmas that affect the import
|
||||
if let Some(comment) = desc.leading_comments.last() {
|
||||
parse_deno_types(&comment.text).as_ref().map(|deno_types| resolve_import(deno_types, specifier, maybe_import_map))
|
||||
parse_deno_types(&comment).as_ref().map(|(deno_types, span)| {
|
||||
(
|
||||
resolve_import(deno_types, specifier, maybe_import_map),
|
||||
deno_types.clone(),
|
||||
parsed_module.source_map.lookup_char_pos(span.lo)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -304,6 +331,17 @@ pub fn analyze_dependencies(
|
|||
match desc.kind {
|
||||
swc_ecmascript::dep_graph::DependencyKind::ExportType
|
||||
| swc_ecmascript::dep_graph::DependencyKind::ImportType => {
|
||||
dep.maybe_type_specifier_range = Some(Range {
|
||||
start: Position {
|
||||
line: (desc.specifier_line - 1) as u32,
|
||||
character: desc.specifier_col as u32,
|
||||
},
|
||||
end: Position {
|
||||
line: (desc.specifier_line - 1) as u32,
|
||||
character: (desc.specifier_col + desc.specifier.chars().count() + 2)
|
||||
as u32,
|
||||
},
|
||||
});
|
||||
dep.maybe_type = Some(resolved_import)
|
||||
}
|
||||
_ => {
|
||||
|
@ -321,8 +359,22 @@ pub fn analyze_dependencies(
|
|||
dep.maybe_code = Some(resolved_import);
|
||||
}
|
||||
}
|
||||
if maybe_resolved_type_dependency.is_some() && dep.maybe_type.is_none() {
|
||||
dep.maybe_type = maybe_resolved_type_dependency;
|
||||
if dep.maybe_type.is_none() {
|
||||
if let Some((resolved_dependency, specifier, loc)) =
|
||||
maybe_resolved_type_dependency
|
||||
{
|
||||
dep.maybe_type_specifier_range = Some(Range {
|
||||
start: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: (loc.col_display + 1) as u32,
|
||||
},
|
||||
end: Position {
|
||||
line: (loc.line - 1) as u32,
|
||||
character: (loc.col_display + 1 + specifier.chars().count()) as u32,
|
||||
},
|
||||
});
|
||||
dep.maybe_type = Some(resolved_dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -723,6 +775,16 @@ mod tests {
|
|||
character: 58,
|
||||
}
|
||||
}),
|
||||
maybe_type_specifier_range: Some(Range {
|
||||
start: Position {
|
||||
line: 7,
|
||||
character: 20,
|
||||
},
|
||||
end: Position {
|
||||
line: 7,
|
||||
character: 62,
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -743,6 +805,7 @@ mod tests {
|
|||
character: 50,
|
||||
}
|
||||
}),
|
||||
maybe_type_specifier_range: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use super::analysis;
|
||||
use super::documents::DocumentCache;
|
||||
use super::language_server;
|
||||
use super::sources::Sources;
|
||||
use super::tsc;
|
||||
|
||||
use crate::diagnostics;
|
||||
use crate::media_type::MediaType;
|
||||
use crate::tokio_util::create_basic_runtime;
|
||||
|
||||
use analysis::ResolvedDependency;
|
||||
use deno_core::error::anyhow;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::resolve_url;
|
||||
|
@ -392,6 +395,64 @@ async fn generate_ts_diagnostics(
|
|||
Ok(diagnostics_vec)
|
||||
}
|
||||
|
||||
fn diagnose_dependency(
|
||||
diagnostics: &mut Vec<lsp::Diagnostic>,
|
||||
documents: &DocumentCache,
|
||||
sources: &Sources,
|
||||
maybe_dependency: &Option<ResolvedDependency>,
|
||||
maybe_range: &Option<lsp::Range>,
|
||||
) {
|
||||
if let (Some(dep), Some(range)) = (maybe_dependency, *maybe_range) {
|
||||
match dep {
|
||||
analysis::ResolvedDependency::Err(err) => {
|
||||
diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Error),
|
||||
code: Some(err.as_code()),
|
||||
code_description: None,
|
||||
source: Some("deno".to_string()),
|
||||
message: err.to_string(),
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
analysis::ResolvedDependency::Resolved(specifier) => {
|
||||
if !(documents.contains_key(&specifier)
|
||||
|| sources.contains_key(&specifier))
|
||||
{
|
||||
let (code, message) = match specifier.scheme() {
|
||||
"file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)),
|
||||
"data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()),
|
||||
"blob" => (Some(lsp::NumberOrString::String("no-cache-blob".to_string())), "Uncached blob URL.".to_string()),
|
||||
_ => (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Uncached or missing remote URL: \"{}\".", specifier)),
|
||||
};
|
||||
diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Error),
|
||||
code,
|
||||
source: Some("deno".to_string()),
|
||||
message,
|
||||
data: Some(json!({ "specifier": specifier })),
|
||||
..Default::default()
|
||||
});
|
||||
} else if sources.contains_key(&specifier) {
|
||||
if let Some(message) = sources.get_maybe_warning(&specifier) {
|
||||
diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Warning),
|
||||
code: Some(lsp::NumberOrString::String("deno-warn".to_string())),
|
||||
source: Some("deno".to_string()),
|
||||
message,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate diagnostics for dependencies of a module, attempting to resolve
|
||||
/// dependencies on the local file system or in the DENO_DIR cache.
|
||||
async fn generate_deps_diagnostics(
|
||||
|
@ -417,54 +478,20 @@ async fn generate_deps_diagnostics(
|
|||
let mut diagnostics = Vec::new();
|
||||
if let Some(dependencies) = documents.dependencies(specifier) {
|
||||
for (_, dependency) in dependencies {
|
||||
// TODO(@kitsonk) add diagnostics for maybe_type dependencies
|
||||
if let (Some(code), Some(range)) =
|
||||
(dependency.maybe_code, dependency.maybe_code_specifier_range)
|
||||
{
|
||||
match code {
|
||||
analysis::ResolvedDependency::Err(err) => diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Error),
|
||||
code: Some(err.as_code()),
|
||||
code_description: None,
|
||||
source: Some("deno".to_string()),
|
||||
message: err.to_string(),
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}),
|
||||
analysis::ResolvedDependency::Resolved(specifier) => {
|
||||
if !(documents.contains_key(&specifier) || sources.contains_key(&specifier)) {
|
||||
let (code, message) = match specifier.scheme() {
|
||||
"file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)),
|
||||
"data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()),
|
||||
"blob" => (Some(lsp::NumberOrString::String("no-cache-blob".to_string())), "Uncached blob URL.".to_string()),
|
||||
_ => (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Uncached or missing remote URL: \"{}\".", specifier)),
|
||||
};
|
||||
diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Error),
|
||||
code,
|
||||
source: Some("deno".to_string()),
|
||||
message,
|
||||
data: Some(json!({ "specifier": specifier })),
|
||||
..Default::default()
|
||||
});
|
||||
} else if sources.contains_key(&specifier) {
|
||||
if let Some(message) = sources.get_maybe_warning(&specifier) {
|
||||
diagnostics.push(lsp::Diagnostic {
|
||||
range,
|
||||
severity: Some(lsp::DiagnosticSeverity::Warning),
|
||||
code: Some(lsp::NumberOrString::String("deno-warn".to_string())),
|
||||
source: Some("deno".to_string()),
|
||||
message,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
diagnose_dependency(
|
||||
&mut diagnostics,
|
||||
&documents,
|
||||
&sources,
|
||||
&dependency.maybe_code,
|
||||
&dependency.maybe_code_specifier_range,
|
||||
);
|
||||
diagnose_dependency(
|
||||
&mut diagnostics,
|
||||
&documents,
|
||||
&sources,
|
||||
&dependency.maybe_type,
|
||||
&dependency.maybe_type_specifier_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
diagnostics_vec.push((specifier.clone(), version, diagnostics));
|
||||
|
|
|
@ -52,6 +52,9 @@ use std::result;
|
|||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
use swc_common::comments::Comment;
|
||||
use swc_common::BytePos;
|
||||
use swc_common::Span;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Matched the `@deno-types` pragma.
|
||||
|
@ -188,35 +191,48 @@ pub enum TypeScriptReference {
|
|||
Types(String),
|
||||
}
|
||||
|
||||
fn match_to_span(comment: &Comment, m: ®ex::Match) -> Span {
|
||||
Span {
|
||||
lo: comment.span.lo + BytePos((m.start() + 1) as u32),
|
||||
hi: comment.span.lo + BytePos((m.end() + 1) as u32),
|
||||
ctxt: comment.span.ctxt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a comment contains a triple slash reference and optionally
|
||||
/// return its kind and value.
|
||||
pub fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
|
||||
if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
|
||||
pub fn parse_ts_reference(
|
||||
comment: &Comment,
|
||||
) -> Option<(TypeScriptReference, Span)> {
|
||||
if !TRIPLE_SLASH_REFERENCE_RE.is_match(&comment.text) {
|
||||
None
|
||||
} else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
|
||||
Some(TypeScriptReference::Path(
|
||||
captures.get(1).unwrap().as_str().to_string(),
|
||||
} else if let Some(captures) = PATH_REFERENCE_RE.captures(&comment.text) {
|
||||
let m = captures.get(1).unwrap();
|
||||
Some((
|
||||
TypeScriptReference::Path(m.as_str().to_string()),
|
||||
match_to_span(comment, &m),
|
||||
))
|
||||
} else {
|
||||
TYPES_REFERENCE_RE.captures(comment).map(|captures| {
|
||||
TypeScriptReference::Types(captures.get(1).unwrap().as_str().to_string())
|
||||
TYPES_REFERENCE_RE.captures(&comment.text).map(|captures| {
|
||||
let m = captures.get(1).unwrap();
|
||||
(
|
||||
TypeScriptReference::Types(m.as_str().to_string()),
|
||||
match_to_span(comment, &m),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a comment contains a `@deno-types` pragma and optionally return
|
||||
/// its value.
|
||||
pub fn parse_deno_types(comment: &str) -> Option<String> {
|
||||
if let Some(captures) = DENO_TYPES_RE.captures(comment) {
|
||||
if let Some(m) = captures.get(1) {
|
||||
Some(m.as_str().to_string())
|
||||
} else if let Some(m) = captures.get(2) {
|
||||
Some(m.as_str().to_string())
|
||||
} else {
|
||||
panic!("unreachable");
|
||||
}
|
||||
pub fn parse_deno_types(comment: &Comment) -> Option<(String, Span)> {
|
||||
let captures = DENO_TYPES_RE.captures(&comment.text)?;
|
||||
if let Some(m) = captures.get(1) {
|
||||
Some((m.as_str().to_string(), match_to_span(comment, &m)))
|
||||
} else if let Some(m) = captures.get(2) {
|
||||
Some((m.as_str().to_string(), match_to_span(comment, &m)))
|
||||
} else {
|
||||
None
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,7 +343,7 @@ impl Module {
|
|||
|
||||
// parse out any triple slash references
|
||||
for comment in parsed_module.get_leading_comments().iter() {
|
||||
if let Some(ts_reference) = parse_ts_reference(&comment.text) {
|
||||
if let Some((ts_reference, _)) = parse_ts_reference(&comment) {
|
||||
let location = parsed_module.get_location(&comment.span);
|
||||
match ts_reference {
|
||||
TypeScriptReference::Path(import) => {
|
||||
|
@ -392,7 +408,7 @@ impl Module {
|
|||
// Parse out any `@deno-types` pragmas and modify dependency
|
||||
let maybe_type = if !desc.leading_comments.is_empty() {
|
||||
let comment = desc.leading_comments.last().unwrap();
|
||||
if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() {
|
||||
if let Some((deno_types, _)) = parse_deno_types(&comment).as_ref() {
|
||||
Some(self.resolve_import(deno_types, Some(location.clone()))?)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -1398,6 +1398,11 @@ fn lsp_code_actions_deno_cache() {
|
|||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let (id, method, _) = client.read_request::<Value>().unwrap();
|
||||
assert_eq!(method, "workspace/configuration");
|
||||
client
|
||||
.write_response(id, json!({ "enable": true }))
|
||||
.unwrap();
|
||||
let (method, _) = client.read_notification::<Value>().unwrap();
|
||||
assert_eq!(method, "textDocument/publishDiagnostics");
|
||||
let (method, _) = client.read_notification::<Value>().unwrap();
|
||||
|
@ -1851,6 +1856,46 @@ fn lsp_diagnostics_warn() {
|
|||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_diagnostics_deno_types() {
|
||||
let mut client = init("initialize_params.json");
|
||||
client
|
||||
.write_notification(
|
||||
"textDocument/didOpen",
|
||||
load_fixture("did_open_params_deno_types.json"),
|
||||
)
|
||||
.unwrap();
|
||||
let (id, method, _) = client.read_request::<Value>().unwrap();
|
||||
assert_eq!(method, "workspace/configuration");
|
||||
client
|
||||
.write_response(id, json!({ "enable": true }))
|
||||
.unwrap();
|
||||
let (maybe_res, maybe_err) = client
|
||||
.write_request::<_, _, Value>(
|
||||
"textDocument/documentSymbol",
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(maybe_res.is_some());
|
||||
assert!(maybe_err.is_none());
|
||||
let (method, _) = client.read_notification::<Value>().unwrap();
|
||||
assert_eq!(method, "textDocument/publishDiagnostics");
|
||||
let (method, _) = client.read_notification::<Value>().unwrap();
|
||||
assert_eq!(method, "textDocument/publishDiagnostics");
|
||||
let (method, maybe_params) = client
|
||||
.read_notification::<lsp::PublishDiagnosticsParams>()
|
||||
.unwrap();
|
||||
assert_eq!(method, "textDocument/publishDiagnostics");
|
||||
assert!(maybe_params.is_some());
|
||||
let params = maybe_params.unwrap();
|
||||
assert_eq!(params.diagnostics.len(), 5);
|
||||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PerformanceAverage {
|
||||
|
|
101
cli/tests/lsp/diagnostics_deno_types.json
Normal file
101
cli/tests/lsp/diagnostics_deno_types.json
Normal file
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"uri": "file:///a/file.ts",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 21
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 51
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: \"https://example.com/a/b.d.ts\".",
|
||||
"data": {
|
||||
"specifier": "https://example.com/a/b.d.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 7,
|
||||
"character": 19
|
||||
},
|
||||
"end": {
|
||||
"line": 7,
|
||||
"character": 47
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: \"https://example.com/a/e.js\".",
|
||||
"data": {
|
||||
"specifier": "https://example.com/a/e.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 6,
|
||||
"character": 16
|
||||
},
|
||||
"end": {
|
||||
"line": 6,
|
||||
"character": 44
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: \"https://example.com/a/e.d.ts\".",
|
||||
"data": {
|
||||
"specifier": "https://example.com/a/e.d.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 4,
|
||||
"character": 19
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"character": 47
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: \"https://example.com/a/d.js\".",
|
||||
"data": {
|
||||
"specifier": "https://example.com/a/d.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 3,
|
||||
"character": 15
|
||||
},
|
||||
"end": {
|
||||
"line": 3,
|
||||
"character": 43
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: \"https://example.com/a/d.d.ts\".",
|
||||
"data": {
|
||||
"specifier": "https://example.com/a/d.d.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
8
cli/tests/lsp/did_open_params_deno_types.json
Normal file
8
cli/tests/lsp/did_open_params_deno_types.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "/// <reference types=\"https://example.com/a/b.d.ts\" />\n/// <reference path=\"https://example.com/a/c.ts\"\n\n// @deno-types=https://example.com/a/d.d.ts\nimport * as d from \"https://example.com/a/d.js\";\n\n// @deno-types=\"https://example.com/a/e.d.ts\"\nimport * as e from \"https://example.com/a/e.js\";\n\nconsole.log(d, e);\n"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue