Skip to content

Commit fa4bffc

Browse files
authored
feat: add whether dependency was optional to ResolveError (#52)
1 parent e2a9841 commit fa4bffc

File tree

10 files changed

+206
-68
lines changed

10 files changed

+206
-68
lines changed

src/mod.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ export class ResolveError extends Error {
8383
specifier?: string;
8484
/** Node.js error code. */
8585
code?: string;
86+
/**
87+
* If the specifier being resolved was an optional npm dependency.
88+
*
89+
* @remarks This will only be true when the error code is
90+
* `ERR_MODULE_NOT_FOUND`.
91+
*/
92+
isOptionalDependency?: boolean;
8693
}
8794

8895
/** File type. */

src/rs_lib/lib.rs

Lines changed: 138 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ use node_resolver::NodeResolverOptions;
7272
use node_resolver::PackageJsonThreadLocalCache;
7373
use node_resolver::analyze::NodeCodeTranslatorMode;
7474
use node_resolver::cache::NodeResolutionThreadLocalCache;
75+
use node_resolver::errors::NodeJsErrorCode;
7576
use node_resolver::errors::NodeJsErrorCoded;
7677
use serde::Deserialize;
7778
use serde::Serialize;
@@ -196,11 +197,11 @@ impl DenoWorkspace {
196197
console_error_panic_hook::set_once();
197198
let options = serde_wasm_bindgen::from_value(options).map_err(|err| {
198199
create_js_error(
199-
anyhow::anyhow!("{}", err)
200+
&anyhow::anyhow!("{}", err)
200201
.context("Failed deserializing workspace options."),
201202
)
202203
})?;
203-
Self::new_inner(options).map_err(create_js_error)
204+
Self::new_inner(options).map_err(|e| create_js_error(&e))
204205
}
205206

206207
fn new_inner(options: DenoWorkspaceOptions) -> Result<Self, anyhow::Error> {
@@ -336,7 +337,10 @@ impl DenoWorkspace {
336337
}
337338

338339
pub async fn create_loader(&self) -> Result<DenoLoader, JsValue> {
339-
self.create_loader_inner().await.map_err(create_js_error)
340+
self
341+
.create_loader_inner()
342+
.await
343+
.map_err(|e| create_js_error(&e))
340344
}
341345

342346
async fn create_loader_inner(&self) -> Result<DenoLoader, anyhow::Error> {
@@ -416,7 +420,7 @@ impl DenoLoader {
416420
self
417421
.add_entrypoints_internal(entrypoints)
418422
.await
419-
.map_err(create_js_error)
423+
.map_err(|e| create_js_error(&e))
420424
}
421425

422426
async fn add_entrypoints_internal(
@@ -533,19 +537,24 @@ impl DenoLoader {
533537
importer: Option<String>,
534538
resolution_mode: u8,
535539
) -> Result<String, JsValue> {
540+
let importer = self
541+
.resolve_provided_referrer(importer)
542+
.map_err(|e| create_js_error(&e))?;
536543
self
537544
.resolve_sync_inner(
538545
&specifier,
539-
importer,
546+
importer.as_ref(),
540547
parse_resolution_mode(resolution_mode),
541548
)
542-
.map_err(create_js_error)
549+
.map_err(|err| {
550+
self.create_resolve_js_error(&err, &specifier, importer.as_ref())
551+
})
543552
}
544553

545554
fn resolve_sync_inner(
546555
&self,
547556
specifier: &str,
548-
importer: Option<String>,
557+
importer: Option<&Url>,
549558
resolution_mode: node_resolver::ResolutionMode,
550559
) -> Result<String, anyhow::Error> {
551560
let (specifier, referrer) = self.resolve_specifier_and_referrer(
@@ -573,25 +582,30 @@ impl DenoLoader {
573582
importer: Option<String>,
574583
resolution_mode: u8,
575584
) -> Result<String, JsValue> {
585+
let importer = self
586+
.resolve_provided_referrer(importer)
587+
.map_err(|e| create_js_error(&e))?;
576588
self
577589
.resolve_inner(
578590
&specifier,
579-
importer,
591+
importer.as_ref(),
580592
parse_resolution_mode(resolution_mode),
581593
)
582594
.await
583-
.map_err(create_js_error)
595+
.map_err(|err| {
596+
self.create_resolve_js_error(&err, &specifier, importer.as_ref())
597+
})
584598
}
585599

586600
async fn resolve_inner(
587601
&self,
588602
specifier: &str,
589-
importer: Option<String>,
603+
importer: Option<&Url>,
590604
resolution_mode: node_resolver::ResolutionMode,
591605
) -> Result<String, anyhow::Error> {
592606
let (specifier, referrer) = self.resolve_specifier_and_referrer(
593607
specifier,
594-
importer.clone(),
608+
importer,
595609
resolution_mode,
596610
)?;
597611
let resolved = self.resolver.resolve_with_graph(
@@ -618,24 +632,11 @@ impl DenoLoader {
618632
fn resolve_specifier_and_referrer<'a>(
619633
&self,
620634
specifier: &'a str,
621-
importer: Option<String>,
635+
referrer: Option<&'a Url>,
622636
resolution_mode: node_resolver::ResolutionMode,
623-
) -> Result<(Cow<'a, str>, Url), anyhow::Error> {
624-
let importer = importer.filter(|v| !v.is_empty());
625-
Ok(match importer {
626-
Some(referrer)
627-
if referrer.starts_with("http:")
628-
|| referrer.starts_with("https:")
629-
|| referrer.starts_with("file:") =>
630-
{
631-
(Cow::Borrowed(specifier), Url::parse(&referrer)?)
632-
}
633-
Some(referrer) => (
634-
Cow::Borrowed(specifier),
635-
deno_path_util::url_from_file_path(
636-
&sys_traits::impls::wasm_string_to_path(referrer),
637-
)?,
638-
),
637+
) -> Result<(Cow<'a, str>, Cow<'a, Url>), anyhow::Error> {
638+
Ok(match referrer {
639+
Some(referrer) => (Cow::Borrowed(specifier), Cow::Borrowed(referrer)),
639640
None => {
640641
let entrypoint = Cow::Owned(
641642
self
@@ -644,14 +645,34 @@ impl DenoLoader {
644645
);
645646
(
646647
entrypoint,
647-
deno_path_util::url_from_directory_path(
648+
Cow::Owned(deno_path_util::url_from_directory_path(
648649
self.workspace_factory.initial_cwd(),
649-
)?,
650+
)?),
650651
)
651652
}
652653
})
653654
}
654655

656+
fn resolve_provided_referrer(
657+
&self,
658+
importer: Option<String>,
659+
) -> Result<Option<Url>, anyhow::Error> {
660+
let importer = importer.filter(|v| !v.is_empty());
661+
Ok(match importer {
662+
Some(referrer)
663+
if referrer.starts_with("http:")
664+
|| referrer.starts_with("https:")
665+
|| referrer.starts_with("file:") =>
666+
{
667+
Some(Url::parse(&referrer)?)
668+
}
669+
Some(referrer) => Some(deno_path_util::url_from_file_path(
670+
&sys_traits::impls::wasm_string_to_path(referrer),
671+
)?),
672+
None => None,
673+
})
674+
}
675+
655676
pub async fn load(
656677
&self,
657678
url: String,
@@ -663,7 +684,7 @@ impl DenoLoader {
663684
2 => RequestedModuleType::Text,
664685
3 => RequestedModuleType::Bytes,
665686
_ => {
666-
return Err(create_js_error(anyhow::anyhow!(
687+
return Err(create_js_error(&anyhow::anyhow!(
667688
"Invalid requested module type: {}",
668689
requested_module_type
669690
)));
@@ -672,7 +693,7 @@ impl DenoLoader {
672693
self
673694
.load_inner(url, &requested_module_type)
674695
.await
675-
.map_err(create_js_error)
696+
.map_err(|err| create_js_error(&err))
676697
}
677698

678699
async fn load_inner(
@@ -811,19 +832,92 @@ impl DenoLoader {
811832
node_resolver::NodeResolutionKind::Execution,
812833
)?)
813834
}
835+
836+
fn is_optional_npm_dep(&self, specifier: &str, referrer: &Url) -> bool {
837+
let Ok(referrer_path) = deno_path_util::url_to_file_path(referrer) else {
838+
return false;
839+
};
840+
for result in self
841+
.resolver_factory
842+
.pkg_json_resolver()
843+
.get_closest_package_jsons(&referrer_path)
844+
{
845+
let Ok(pkg_json) = result else {
846+
continue;
847+
};
848+
if let Some(optional_deps) = &pkg_json.optional_dependencies
849+
&& optional_deps.contains_key(specifier)
850+
{
851+
return true;
852+
}
853+
if let Some(meta) = &pkg_json.peer_dependencies_meta
854+
&& let Some(obj) = meta.get(specifier)
855+
&& let Some(value) = obj.get("optional")
856+
&& let Some(is_optional) = value.as_bool()
857+
&& is_optional
858+
{
859+
return true;
860+
}
861+
if let Some(deps) = &pkg_json.dependencies
862+
&& deps.contains_key(specifier)
863+
{
864+
return false;
865+
}
866+
if let Some(deps) = &pkg_json.peer_dependencies
867+
&& deps.contains_key(specifier)
868+
{
869+
return false;
870+
}
871+
}
872+
false
873+
}
874+
875+
fn create_resolve_js_error(
876+
&self,
877+
err: &anyhow::Error,
878+
specifier: &str,
879+
maybe_referrer: Option<&Url>,
880+
) -> JsValue {
881+
let err_value = create_js_error(err);
882+
if let Some(err) = err.downcast_ref::<ResolveWithGraphError>() {
883+
if let Some(code) = resolve_with_graph_error_code(err) {
884+
_ = js_sys::Reflect::set(
885+
&err_value,
886+
&JsValue::from_str("code"),
887+
&JsValue::from_str(code.as_str()),
888+
);
889+
if code == NodeJsErrorCode::ERR_MODULE_NOT_FOUND
890+
&& let Some(referrer) = maybe_referrer
891+
&& self.is_optional_npm_dep(specifier, referrer)
892+
{
893+
_ = js_sys::Reflect::set(
894+
&err_value,
895+
&JsValue::from_str("isOptionalDependency"),
896+
&JsValue::from_bool(true),
897+
);
898+
}
899+
}
900+
if let Some(specifier) = err.maybe_specifier()
901+
&& let Ok(url) = specifier.into_owned().into_url()
902+
{
903+
_ = js_sys::Reflect::set(
904+
&err_value,
905+
&JsValue::from_str("specifier"),
906+
&JsValue::from_str(url.as_str()),
907+
);
908+
}
909+
}
910+
err_value
911+
}
814912
}
815913

816914
fn resolve_with_graph_error_code(
817915
err: &ResolveWithGraphError,
818-
) -> Option<&'static str> {
916+
) -> Option<NodeJsErrorCode> {
819917
match err.as_kind() {
820-
ResolveWithGraphErrorKind::CouldNotResolveNpmNv(err) => {
821-
Some(err.code().as_str())
822-
}
918+
ResolveWithGraphErrorKind::CouldNotResolveNpmNv(err) => Some(err.code()),
823919
ResolveWithGraphErrorKind::ResolvePkgFolderFromDenoModule(_) => None,
824-
ResolveWithGraphErrorKind::ResolveNpmReqRef(err) => {
825-
err.err.maybe_code().map(|c| c.as_str())
826-
}
920+
ResolveWithGraphErrorKind::ResolveNpmReqRef(err) => err.err.maybe_code(),
827921
ResolveWithGraphErrorKind::Resolution(err) => err
828922
.source()
829923
.and_then(|s| s.downcast_ref::<DenoResolveError>())
@@ -833,7 +927,7 @@ fn resolve_with_graph_error_code(
833927
}
834928
}
835929

836-
fn deno_resolve_error_code(err: &DenoResolveError) -> Option<&'static str> {
930+
fn deno_resolve_error_code(err: &DenoResolveError) -> Option<NodeJsErrorCode> {
837931
match err.as_kind() {
838932
DenoResolveErrorKind::InvalidVendorFolderImport
839933
| DenoResolveErrorKind::UnsupportedPackageJsonFileSpecifier
@@ -843,10 +937,8 @@ fn deno_resolve_error_code(err: &DenoResolveError) -> Option<&'static str> {
843937
| MappedResolutionError::ImportMap(_)
844938
| MappedResolutionError::Workspace(_) => None,
845939
},
846-
DenoResolveErrorKind::Node(err) => err.maybe_code().map(|c| c.as_str()),
847-
DenoResolveErrorKind::ResolveNpmReqRef(err) => {
848-
err.err.maybe_code().map(|c| c.as_str())
849-
}
940+
DenoResolveErrorKind::Node(err) => err.maybe_code(),
941+
DenoResolveErrorKind::ResolveNpmReqRef(err) => err.err.maybe_code(),
850942
DenoResolveErrorKind::NodeModulesOutOfDate(_)
851943
| DenoResolveErrorKind::PackageJsonDepValueParse(_)
852944
| DenoResolveErrorKind::PackageJsonDepValueUrlParse(_)
@@ -909,28 +1001,8 @@ fn resolve_absolute_path(
9091001
}
9101002
}
9111003

912-
fn create_js_error(err: anyhow::Error) -> JsValue {
913-
let err_value: JsValue =
914-
wasm_bindgen::JsError::new(&format!("{:#}", err)).into();
915-
if let Some(err) = err.downcast_ref::<ResolveWithGraphError>() {
916-
if let Some(code) = resolve_with_graph_error_code(err) {
917-
_ = js_sys::Reflect::set(
918-
&err_value,
919-
&JsValue::from_str("code"),
920-
&JsValue::from_str(code),
921-
);
922-
}
923-
if let Some(specifier) = err.maybe_specifier()
924-
&& let Ok(url) = specifier.into_owned().into_url()
925-
{
926-
_ = js_sys::Reflect::set(
927-
&err_value,
928-
&JsValue::from_str("specifier"),
929-
&JsValue::from_str(url.as_str()),
930-
);
931-
}
932-
}
933-
err_value
1004+
fn create_js_error(err: &anyhow::Error) -> JsValue {
1005+
wasm_bindgen::JsError::new(&format!("{:#}", err)).into()
9341006
}
9351007

9361008
fn parse_resolution_mode(resolution_mode: u8) -> node_resolver::ResolutionMode {

0 commit comments

Comments
 (0)