Skip to content

Commit a59df0a

Browse files
authored
feat: add the types package of a mapped dependency to the dev dependencies (#509)
1 parent 7d311ec commit a59df0a

8 files changed

Lines changed: 548 additions & 12 deletions

File tree

lib/package_json.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Deno.test("single entrypoint", () => {
3232
}],
3333
},
3434
warnings: [],
35+
typesDependencies: [],
3536
},
3637
entryPoints: [{
3738
name: ".",
@@ -252,6 +253,7 @@ Deno.test("exports have default last", () => {
252253
dependencies: [],
253254
},
254255
warnings: [],
256+
typesDependencies: [],
255257
},
256258
entryPoints: [
257259
{
@@ -299,6 +301,7 @@ Deno.test("multiple entrypoints", () => {
299301
dependencies: [],
300302
},
301303
warnings: [],
304+
typesDependencies: [],
302305
},
303306
entryPoints: [{
304307
name: ".",
@@ -375,6 +378,7 @@ Deno.test("binary entrypoints", () => {
375378
dependencies: [],
376379
},
377380
warnings: [],
381+
typesDependencies: [],
378382
},
379383
entryPoints: [{
380384
name: ".",
@@ -457,6 +461,7 @@ Deno.test("peer dependencies", () => {
457461
}],
458462
},
459463
warnings: [],
464+
typesDependencies: [],
460465
},
461466
entryPoints: [{
462467
name: ".",
@@ -520,3 +525,58 @@ Deno.test("peer dependencies", () => {
520525
),
521526
);
522527
});
528+
529+
Deno.test("types dependencies", () => {
530+
const props: GetPackageJsonOptions = {
531+
transformOutput: {
532+
main: {
533+
files: [],
534+
dependencies: [{
535+
name: "svg-path-parser",
536+
version: "1.1.0",
537+
}, {
538+
name: "other",
539+
version: "^1.0.0",
540+
}],
541+
entryPoints: ["mod.ts"],
542+
},
543+
test: {
544+
entryPoints: [],
545+
files: [],
546+
dependencies: [],
547+
},
548+
warnings: [],
549+
typesDependencies: [{
550+
name: "@types/svg-path-parser",
551+
version: "~1.1.6",
552+
}, {
553+
// a types package that's already a dependency is not duplicated
554+
name: "other",
555+
version: "^1.0.0",
556+
}],
557+
},
558+
entryPoints: [{
559+
name: ".",
560+
path: "./mod.ts",
561+
}],
562+
package: {
563+
name: "package",
564+
version: "0.1.0",
565+
},
566+
testEnabled: false,
567+
includeEsModule: true,
568+
includeScriptModule: true,
569+
includeDeclarations: true,
570+
includeTsLib: false,
571+
shims: {},
572+
};
573+
574+
const packageJson = getPackageJson(props);
575+
assertEquals(packageJson.dependencies, {
576+
"svg-path-parser": "1.1.0",
577+
"other": "^1.0.0",
578+
});
579+
assertEquals(packageJson.devDependencies, {
580+
"@types/svg-path-parser": "~1.1.6",
581+
});
582+
});

lib/package_json.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ export function getPackageJson({
7979
})
8080
: {};
8181
const devDependencies = {
82+
// packages that provide the type declarations of a dependency
83+
// (ex. an `@types/` package specified by an `X-TypeScript-Types` header)
84+
...Object.fromEntries(
85+
transformOutput.typesDependencies
86+
.filter((d) =>
87+
!Object.keys(dependencies).includes(d.name) &&
88+
!Object.keys(peerDependencies).includes(d.name)
89+
)
90+
.map((d) => [d.name, d.version]),
91+
),
8292
...(shouldIncludeTypesNode()
8393
? {
8494
"@types/node": "^20.9.0",

rs-lib/src/declaration_file_resolution.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use deno_graph::Module;
1010
use deno_graph::Resolution;
1111

1212
use crate::graph::ModuleGraph;
13+
use crate::loader::get_all_specifier_mappers;
14+
use crate::loader::get_types_package_for_specifier;
1315
use crate::PackageMappedSpecifier;
1416

1517
#[derive(Debug)]
@@ -27,29 +29,50 @@ pub struct TypesDependency {
2729
pub referrer: ModuleSpecifier,
2830
}
2931

32+
pub struct DeclarationFileResolutions {
33+
pub mappings: BTreeMap<ModuleSpecifier, DeclarationFileResolution>,
34+
/// Packages that provide the declaration files of a mapped package
35+
/// (ex. an `@types/` package served by a cdn).
36+
pub types_packages: BTreeMap<String, PackageMappedSpecifier>,
37+
/// Declaration files that a types package provides, which don't
38+
/// need to be included in the output.
39+
pub types_package_files: HashSet<ModuleSpecifier>,
40+
}
41+
3042
pub fn resolve_declaration_file_mappings(
3143
module_graph: &ModuleGraph,
3244
modules: &[&Module],
3345
mapped_specifiers: &BTreeMap<ModuleSpecifier, PackageMappedSpecifier>,
34-
) -> Result<BTreeMap<ModuleSpecifier, DeclarationFileResolution>> {
46+
) -> Result<DeclarationFileResolutions> {
3547
let mut type_dependencies = BTreeMap::new();
3648

3749
for module in modules.iter().filter_map(|m| m.js()) {
3850
fill_types_for_module(module_graph, module, &mut type_dependencies)?;
3951
}
4052

4153
// get the resolved type dependencies
54+
let mappers = get_all_specifier_mappers();
4255
let mut mappings = BTreeMap::new();
56+
let mut types_packages = BTreeMap::new();
57+
let mut types_package_files = HashSet::new();
4358
for (code_specifier, deps) in type_dependencies.into_iter() {
44-
// if this type_dependency is mapped, then pass it.
45-
if mapped_specifiers.contains_key(&code_specifier) {
46-
continue;
47-
}
48-
4959
let deps = deps.into_iter().collect::<Vec<_>>();
5060
let selected_dep =
5161
select_best_types_dep(module_graph, &code_specifier, &deps);
5262

63+
// when the code is mapped to a package, the declaration file it
64+
// specifies may be provided by a types package that needs to go in
65+
// the package.json instead of being included in the output
66+
if mapped_specifiers.contains_key(&code_specifier) {
67+
if let Some(types_package) =
68+
get_types_package_for_specifier(&mappers, &selected_dep.specifier)
69+
{
70+
types_packages.insert(types_package.name.clone(), types_package);
71+
types_package_files.insert(selected_dep.specifier);
72+
}
73+
continue;
74+
}
75+
5376
// get the declaration file specifiers that weren't used
5477
let mut ignored = deps
5578
.into_iter()
@@ -66,7 +89,11 @@ pub fn resolve_declaration_file_mappings(
6689
);
6790
}
6891

69-
Ok(mappings)
92+
Ok(DeclarationFileResolutions {
93+
mappings,
94+
types_packages,
95+
types_package_files,
96+
})
7097
}
7198

7299
/// This resolution process works as follows:

rs-lib/src/lib.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use analyze::get_top_level_decls;
1515
use anyhow::Context;
1616
use anyhow::Result;
1717

18+
use crate::loader::get_all_specifier_mappers;
19+
use crate::loader::get_types_package_for_specifier;
1820
use analyze::get_ignore_line_indexes;
1921
use anyhow::bail;
2022
use deno_ast::apply_text_changes;
@@ -107,6 +109,9 @@ pub struct TransformOutput {
107109
/// This is `None` when no config file was found, when one was explicitly
108110
/// provided, or when auto-discovery is disabled.
109111
pub discovered_config_file: Option<PathBuf>,
112+
/// Packages that provide the type declarations of a mapped dependency
113+
/// (ex. an `@types/` package specified by an `X-TypeScript-Types` header).
114+
pub types_dependencies: Vec<Dependency>,
110115
}
111116

112117
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
@@ -462,8 +467,9 @@ pub async fn transform(
462467
.await?
463468
.cloned();
464469

470+
let file_fetcher = Rc::new(file_fetcher);
465471
let loader = Rc::new(DenoGraphLoader::new(
466-
Rc::new(file_fetcher),
472+
file_fetcher.clone(),
467473
resolver_factory
468474
.workspace_factory()
469475
.global_http_cache()?
@@ -520,6 +526,8 @@ pub async fn transform(
520526
.collect();
521527

522528
let mut warnings = get_declaration_warnings(&specifiers);
529+
let types_dependencies =
530+
get_types_dependencies(&specifiers, &file_fetcher, &mut warnings).await;
523531
let mut main_env_context = EnvironmentContext {
524532
environment: TransformOutputEnvironment {
525533
entry_points: options
@@ -705,6 +713,7 @@ pub async fn transform(
705713
test: test_env_context.environment,
706714
warnings,
707715
discovered_config_file,
716+
types_dependencies,
708717
})
709718
}
710719

@@ -974,6 +983,95 @@ fn check_add_shim_file_to_environment(
974983
}
975984
}
976985

986+
/// Gets the packages that provide the type declarations of the mapped
987+
/// dependencies, which are added to the dev dependencies.
988+
///
989+
/// A mapped module isn't part of the module graph, so the `X-TypeScript-Types`
990+
/// header of each is resolved here.
991+
async fn get_types_dependencies<TSys: WorkspaceFactorySys>(
992+
specifiers: &Specifiers,
993+
file_fetcher: &PermissionedFileFetcher<
994+
NullBlobStore,
995+
TSys,
996+
impl deno_cache_dir::file_fetcher::HttpClient,
997+
>,
998+
warnings: &mut Vec<String>,
999+
) -> Vec<Dependency> {
1000+
let mappers = get_all_specifier_mappers();
1001+
let mut types_packages = specifiers.types_packages.clone();
1002+
let header_packages = futures::future::join_all(
1003+
specifiers
1004+
.main
1005+
.mapped
1006+
.keys()
1007+
.chain(specifiers.test.mapped.keys())
1008+
// only a module mapped by one of the cdn mappers may have a types
1009+
// header, so don't fetch the modules the user mapped themselves
1010+
.filter(|s| mappers.iter().any(|m| m.map(s).is_some()))
1011+
.map(|specifier| {
1012+
let mappers = &mappers;
1013+
async move {
1014+
match get_types_header_specifier(file_fetcher, specifier).await {
1015+
Ok(types_specifier) => Ok(
1016+
types_specifier
1017+
.and_then(|s| get_types_package_for_specifier(mappers, &s)),
1018+
),
1019+
Err(err) => Err(format!(
1020+
"Failed getting the type declarations of {}. {:#}",
1021+
specifier, err
1022+
)),
1023+
}
1024+
}
1025+
}),
1026+
)
1027+
.await;
1028+
for result in header_packages {
1029+
match result {
1030+
Ok(Some(package)) => {
1031+
// a declaration file specified in the code takes precedence
1032+
types_packages
1033+
.entry(package.name.clone())
1034+
.or_insert(package);
1035+
}
1036+
Ok(None) => {}
1037+
Err(warning) => warnings.push(warning),
1038+
}
1039+
}
1040+
1041+
types_packages
1042+
.into_values()
1043+
.filter_map(|p| {
1044+
Some(Dependency {
1045+
name: p.name,
1046+
version: p.version?,
1047+
peer_dependency: false,
1048+
})
1049+
})
1050+
.collect()
1051+
}
1052+
1053+
/// Gets the specifier of the declaration file in a module's
1054+
/// `X-TypeScript-Types` header.
1055+
async fn get_types_header_specifier<TSys: WorkspaceFactorySys>(
1056+
file_fetcher: &PermissionedFileFetcher<
1057+
NullBlobStore,
1058+
TSys,
1059+
impl deno_cache_dir::file_fetcher::HttpClient,
1060+
>,
1061+
specifier: &ModuleSpecifier,
1062+
) -> Result<Option<ModuleSpecifier>> {
1063+
let mut file = file_fetcher.fetch_bypass_permissions(specifier).await?;
1064+
let Some(types_header) = file
1065+
.maybe_headers
1066+
.as_mut()
1067+
.and_then(|h| h.remove("x-typescript-types"))
1068+
else {
1069+
return Ok(None);
1070+
};
1071+
// resolve relative to the final url so that redirects are handled
1072+
Ok(file.url.join(&types_header).ok())
1073+
}
1074+
9771075
fn get_dependencies(
9781076
mappings: BTreeMap<ModuleSpecifier, PackageMappedSpecifier>,
9791077
) -> Vec<Dependency> {

0 commit comments

Comments
 (0)