1use crate::normalize_path;
4use std::error::Error;
5use std::fmt;
6use std::path::Path;
7use std::path::PathBuf;
8use url::ParseError;
9use url::Url;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum ModuleResolutionError {
14 InvalidUrl(ParseError),
15 InvalidBaseUrl(ParseError),
16 InvalidPath(PathBuf),
17 ImportPrefixMissing(String, Option<String>),
18}
19use ModuleResolutionError::*;
20
21impl Error for ModuleResolutionError {
22 fn source(&self) -> Option<&(dyn Error + 'static)> {
23 match self {
24 InvalidUrl(ref err) | InvalidBaseUrl(ref err) => Some(err),
25 _ => None,
26 }
27 }
28}
29
30impl fmt::Display for ModuleResolutionError {
31 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32 match self {
33 InvalidUrl(ref err) => write!(f, "invalid URL: {err}"),
34 InvalidBaseUrl(ref err) => {
35 write!(f, "invalid base URL for relative import: {err}")
36 }
37 InvalidPath(ref path) => write!(f, "invalid module path: {path:?}"),
38 ImportPrefixMissing(ref specifier, ref maybe_referrer) => write!(
39 f,
40 "Relative import path \"{}\" not prefixed with / or ./ or ../{}",
41 specifier,
42 match maybe_referrer {
43 Some(referrer) => format!(" from \"{referrer}\""),
44 None => String::new(),
45 }
46 ),
47 }
48 }
49}
50
51pub type ModuleSpecifier = Url;
53
54pub fn resolve_import(
57 specifier: &str,
58 base: &str,
59) -> Result<ModuleSpecifier, ModuleResolutionError> {
60 let url = match Url::parse(specifier) {
61 Ok(url) => url,
64
65 Err(ParseError::RelativeUrlWithoutBase)
70 if !(specifier.starts_with('/')
71 || specifier.starts_with("./")
72 || specifier.starts_with("../")) =>
73 {
74 let maybe_referrer = if base.is_empty() {
75 None
76 } else {
77 Some(base.to_string())
78 };
79 return Err(ImportPrefixMissing(specifier.to_string(), maybe_referrer));
80 }
81
82 Err(ParseError::RelativeUrlWithoutBase) => {
85 let base = Url::parse(base).map_err(InvalidBaseUrl)?;
86 base.join(specifier).map_err(InvalidUrl)?
87 }
88
89 Err(err) => return Err(InvalidUrl(err)),
94 };
95
96 Ok(url)
97}
98
99pub fn resolve_url(
101 url_str: &str,
102) -> Result<ModuleSpecifier, ModuleResolutionError> {
103 Url::parse(url_str).map_err(ModuleResolutionError::InvalidUrl)
104}
105
106pub fn resolve_url_or_path(
113 specifier: &str,
114 current_dir: &Path,
115) -> Result<ModuleSpecifier, ModuleResolutionError> {
116 if specifier_has_uri_scheme(specifier) {
117 resolve_url(specifier)
118 } else {
119 resolve_path(specifier, current_dir)
120 }
121}
122
123pub fn resolve_path(
127 path_str: &str,
128 current_dir: &Path,
129) -> Result<ModuleSpecifier, ModuleResolutionError> {
130 let path = current_dir.join(path_str);
131 let path = normalize_path(path);
132 Url::from_file_path(&path)
133 .map_err(|()| ModuleResolutionError::InvalidPath(path))
134}
135
136fn specifier_has_uri_scheme(specifier: &str) -> bool {
147 let mut chars = specifier.chars();
148 let mut len = 0usize;
149 match chars.next() {
151 Some(c) if c.is_ascii_alphabetic() => len += 1,
152 _ => return false,
153 }
154 loop {
157 match chars.next() {
158 Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1,
159 Some(':') if len >= 2 => return true,
160 _ => return false,
161 }
162 }
163}
164
165#[cfg(all(test, not(miri)))]
166mod tests {
167 use super::*;
168 use crate::serde_json::from_value;
169 use crate::serde_json::json;
170 use std::env::current_dir;
171 use std::path::Path;
172
173 #[test]
174 fn test_resolve_import() {
175 let tests = vec![
176 (
177 "./005_more_imports.ts",
178 "http://deno.land/core/tests/006_url_imports.ts",
179 "http://deno.land/core/tests/005_more_imports.ts",
180 ),
181 (
182 "../005_more_imports.ts",
183 "http://deno.land/core/tests/006_url_imports.ts",
184 "http://deno.land/core/005_more_imports.ts",
185 ),
186 (
187 "http://deno.land/core/tests/005_more_imports.ts",
188 "http://deno.land/core/tests/006_url_imports.ts",
189 "http://deno.land/core/tests/005_more_imports.ts",
190 ),
191 (
192 "data:text/javascript,export default 'grapes';",
193 "http://deno.land/core/tests/006_url_imports.ts",
194 "data:text/javascript,export default 'grapes';",
195 ),
196 (
197 "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f",
198 "http://deno.land/core/tests/006_url_imports.ts",
199 "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f",
200 ),
201 (
202 "javascript:export default 'artichokes';",
203 "http://deno.land/core/tests/006_url_imports.ts",
204 "javascript:export default 'artichokes';",
205 ),
206 (
207 "data:text/plain,export default 'kale';",
208 "http://deno.land/core/tests/006_url_imports.ts",
209 "data:text/plain,export default 'kale';",
210 ),
211 (
212 "/dev/core/tests/005_more_imports.ts",
213 "file:///home/yeti",
214 "file:///dev/core/tests/005_more_imports.ts",
215 ),
216 (
217 "//zombo.com/1999.ts",
218 "https://cherry.dev/its/a/thing",
219 "https://zombo.com/1999.ts",
220 ),
221 (
222 "http://deno.land/this/url/is/valid",
223 "base is clearly not a valid url",
224 "http://deno.land/this/url/is/valid",
225 ),
226 (
227 "//server/some/dir/file",
228 "file:///home/yeti/deno",
229 "file://server/some/dir/file",
230 ),
231 ];
239
240 for (specifier, base, expected_url) in tests {
241 let url = resolve_import(specifier, base).unwrap().to_string();
242 assert_eq!(url, expected_url);
243 }
244 }
245
246 #[test]
247 fn test_resolve_import_error() {
248 use url::ParseError::*;
249 use ModuleResolutionError::*;
250
251 let tests = vec![
252 (
253 "awesome.ts",
254 "<unknown>",
255 ImportPrefixMissing(
256 "awesome.ts".to_string(),
257 Some("<unknown>".to_string()),
258 ),
259 ),
260 (
261 "005_more_imports.ts",
262 "http://deno.land/core/tests/006_url_imports.ts",
263 ImportPrefixMissing(
264 "005_more_imports.ts".to_string(),
265 Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
266 ),
267 ),
268 (
269 ".tomato",
270 "http://deno.land/core/tests/006_url_imports.ts",
271 ImportPrefixMissing(
272 ".tomato".to_string(),
273 Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
274 ),
275 ),
276 (
277 "..zucchini.mjs",
278 "http://deno.land/core/tests/006_url_imports.ts",
279 ImportPrefixMissing(
280 "..zucchini.mjs".to_string(),
281 Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
282 ),
283 ),
284 (
285 r".\yam.es",
286 "http://deno.land/core/tests/006_url_imports.ts",
287 ImportPrefixMissing(
288 r".\yam.es".to_string(),
289 Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
290 ),
291 ),
292 (
293 r"..\yam.es",
294 "http://deno.land/core/tests/006_url_imports.ts",
295 ImportPrefixMissing(
296 r"..\yam.es".to_string(),
297 Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
298 ),
299 ),
300 (
301 "https://eggplant:b/c",
302 "http://deno.land/core/tests/006_url_imports.ts",
303 InvalidUrl(InvalidPort),
304 ),
305 (
306 "https://eggplant@/c",
307 "http://deno.land/core/tests/006_url_imports.ts",
308 InvalidUrl(EmptyHost),
309 ),
310 (
311 "./foo.ts",
312 "/relative/base/url",
313 InvalidBaseUrl(RelativeUrlWithoutBase),
314 ),
315 ];
316
317 for (specifier, base, expected_err) in tests {
318 let err = resolve_import(specifier, base).unwrap_err();
319 assert_eq!(err, expected_err);
320 }
321 }
322
323 #[test]
324 fn test_resolve_url_or_path() {
325 let mut tests: Vec<(&str, String)> = vec![
327 (
328 "http://deno.land/core/tests/006_url_imports.ts",
329 "http://deno.land/core/tests/006_url_imports.ts".to_string(),
330 ),
331 (
332 "https://deno.land/core/tests/006_url_imports.ts",
333 "https://deno.land/core/tests/006_url_imports.ts".to_string(),
334 ),
335 ];
336
337 let cwd = current_dir().unwrap();
339 let cwd_str = cwd.to_str().unwrap();
340
341 if cfg!(target_os = "windows") {
342 let expected_url = "file:///C:/deno/tests/006_url_imports.ts";
344 tests.extend(vec![
345 (
346 r"C:/deno/tests/006_url_imports.ts",
347 expected_url.to_string(),
348 ),
349 (
350 r"C:\deno\tests\006_url_imports.ts",
351 expected_url.to_string(),
352 ),
353 (
354 r"\\?\C:\deno\tests\006_url_imports.ts",
355 expected_url.to_string(),
356 ),
357 ]);
362
363 let expected_url = format!(
365 "file:///{}:/deno/tests/006_url_imports.ts",
366 cwd_str.get(..1).unwrap(),
367 );
368 tests.extend(vec![
369 (r"/deno/tests/006_url_imports.ts", expected_url.to_string()),
370 (r"\deno\tests\006_url_imports.ts", expected_url.to_string()),
371 (
372 r"\deno\..\deno\tests\006_url_imports.ts",
373 expected_url.to_string(),
374 ),
375 (r"\deno\.\tests\006_url_imports.ts", expected_url),
376 ]);
377
378 let expected_url = format!(
380 "file:///{}/tests/006_url_imports.ts",
381 cwd_str.replace('\\', "/")
382 );
383 tests.extend(vec![
384 (r"tests/006_url_imports.ts", expected_url.to_string()),
385 (r"tests\006_url_imports.ts", expected_url.to_string()),
386 (r"./tests/006_url_imports.ts", (*expected_url).to_string()),
387 (r".\tests\006_url_imports.ts", (*expected_url).to_string()),
388 ]);
389
390 let expected_url = "file://server/share/deno/cool";
392 tests.extend(vec![
393 (r"\\server\share\deno\cool", expected_url.to_string()),
394 (r"\\server/share/deno/cool", expected_url.to_string()),
395 ]);
398 } else {
399 let expected_url = "file:///deno/tests/006_url_imports.ts";
401 tests.extend(vec![
402 ("/deno/tests/006_url_imports.ts", expected_url.to_string()),
403 ("//deno/tests/006_url_imports.ts", expected_url.to_string()),
404 ]);
405
406 let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts");
408 tests.extend(vec![
409 ("tests/006_url_imports.ts", expected_url.to_string()),
410 ("./tests/006_url_imports.ts", expected_url.to_string()),
411 (
412 "tests/../tests/006_url_imports.ts",
413 expected_url.to_string(),
414 ),
415 ("tests/./006_url_imports.ts", expected_url),
416 ]);
417 }
418
419 for (specifier, expected_url) in tests {
420 let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string();
421 assert_eq!(url, expected_url);
422 }
423 }
424
425 #[test]
426 fn test_resolve_url_or_path_deprecated_error() {
427 use url::ParseError::*;
428 use ModuleResolutionError::*;
429
430 let mut tests = vec![
431 ("https://eggplant:b/c", InvalidUrl(InvalidPort)),
432 ("https://:8080/a/b/c", InvalidUrl(EmptyHost)),
433 ];
434 if cfg!(target_os = "windows") {
435 let p = r"\\.\c:/stuff/deno/script.ts";
436 tests.push((p, InvalidPath(PathBuf::from(p))));
437 }
438
439 for (specifier, expected_err) in tests {
440 let err =
441 resolve_url_or_path(specifier, &PathBuf::from("/")).unwrap_err();
442 assert_eq!(err, expected_err);
443 }
444 }
445
446 #[test]
447 fn test_specifier_has_uri_scheme() {
448 let tests = vec![
449 ("http://foo.bar/etc", true),
450 ("HTTP://foo.bar/etc", true),
451 ("http:ftp:", true),
452 ("http:", true),
453 ("hTtP:", true),
454 ("ftp:", true),
455 ("mailto:spam@please.me", true),
456 ("git+ssh://git@github.com/denoland/deno", true),
457 ("blob:https://whatwg.org/mumbojumbo", true),
458 ("abc.123+DEF-ghi:", true),
459 ("abc.123+def-ghi:@", true),
460 ("", false),
461 (":not", false),
462 ("http", false),
463 ("c:dir", false),
464 ("X:", false),
465 ("./http://not", false),
466 ("1abc://kinda/but/no", false),
467 ("schluẞ://no/more", false),
468 ];
469
470 for (specifier, expected) in tests {
471 let result = specifier_has_uri_scheme(specifier);
472 assert_eq!(result, expected);
473 }
474 }
475
476 #[test]
477 fn test_normalize_path() {
478 assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b"));
479 assert_eq!(normalize_path(Path::new("a/./b/")), PathBuf::from("a/b/"));
480 assert_eq!(
481 normalize_path(Path::new("a/./b/../c")),
482 PathBuf::from("a/c")
483 );
484
485 if cfg!(windows) {
486 assert_eq!(
487 normalize_path(Path::new("C:\\a\\.\\b\\..\\c")),
488 PathBuf::from("C:\\a\\c")
489 );
490 }
491 }
492
493 #[test]
494 fn test_deserialize_module_specifier() {
495 let actual: ModuleSpecifier =
496 from_value(json!("http://deno.land/x/mod.ts")).unwrap();
497 let expected = resolve_url("http://deno.land/x/mod.ts").unwrap();
498 assert_eq!(actual, expected);
499 }
500}