Skip to content

Commit 4a007e0

Browse files
committed
Support import attributes in ambient module declarations for dynamic imports
When a dynamic import like `import('./file.ts', { with: { type: 'url' } })` matches a pattern ambient module declaration with import attributes like `declare module '*' with { type: 'url' }`, the ambient module type now takes precedence over the resolved source file. This extends the existing static import support to dynamic imports by: - Extracting import attributes from the options object in import() calls - Creating synthetic ImportAttributes from PropertyAssignment nodes - Checking for matching pattern ambient modules before returning resolved files
1 parent d020329 commit 4a007e0

6 files changed

+795
-47
lines changed

src/compiler/checker.ts

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,7 @@ import {
10801080
tryGetJSDocSatisfiesTypeNode,
10811081
tryGetModuleSpecifierFromDeclaration,
10821082
tryGetPropertyAccessOrIdentifierToString,
1083+
tryGetTextOfPropertyName,
10831084
TryStatement,
10841085
TupleType,
10851086
TupleTypeNode,
@@ -4706,7 +4707,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47064707
}
47074708

47084709
function getImportAttributesFromLocation(location: Node): ImportAttributes | undefined {
4709-
// Try to find import attributes from the location node
47104710
const importDecl = findAncestor(location, isImportDeclaration);
47114711
if (importDecl?.attributes) {
47124712
return importDecl.attributes;
@@ -4719,17 +4719,55 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47194719
if (importType?.attributes) {
47204720
return importType.attributes;
47214721
}
4722+
const importCall = findAncestor(location, isImportCall);
4723+
if (importCall) {
4724+
return getImportAttributesFromImportCall(importCall);
4725+
}
4726+
return undefined;
4727+
}
4728+
4729+
function getImportAttributesFromImportCall(importCall: ImportCall): ImportAttributes | undefined {
4730+
// import("./module", { with: { type: "json" } })
4731+
if (importCall.arguments.length < 2) {
4732+
return undefined;
4733+
}
4734+
const optionsArg = importCall.arguments[1];
4735+
if (!isObjectLiteralExpression(optionsArg)) {
4736+
return undefined;
4737+
}
4738+
for (const prop of optionsArg.properties) {
4739+
if (isPropertyAssignment(prop) && isObjectLiteralExpression(prop.initializer)) {
4740+
const propName = tryGetTextOfPropertyName(prop.name);
4741+
if (propName === "with" as __String || propName === "assert" as __String) {
4742+
return createSyntheticImportAttributes(prop.initializer);
4743+
}
4744+
}
4745+
}
47224746
return undefined;
47234747
}
47244748

4749+
function createSyntheticImportAttributes(objectLiteral: ObjectLiteralExpression): ImportAttributes | undefined {
4750+
// Create synthetic ImportAttribute elements from PropertyAssignment nodes
4751+
// PropertyAssignment has { name, initializer } but ImportAttribute needs { name, value }
4752+
const elements: { name: PropertyName; value: Expression; }[] = [];
4753+
for (const prop of objectLiteral.properties) {
4754+
if (isPropertyAssignment(prop) && isStringLiteral(prop.initializer)) {
4755+
elements.push({ name: prop.name, value: prop.initializer });
4756+
}
4757+
}
4758+
if (elements.length === 0) {
4759+
return undefined;
4760+
}
4761+
return { elements: elements as unknown as NodeArray<ImportAttribute> } as unknown as ImportAttributes;
4762+
}
4763+
47254764
function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node | undefined, isForAugmentation = false): Symbol | undefined {
47264765
if (errorNode && startsWith(moduleReference, "@types/")) {
47274766
const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1;
47284767
const withoutAtTypePrefix = removePrefix(moduleReference, "@types/");
47294768
error(errorNode, diag, withoutAtTypePrefix, moduleReference);
47304769
}
47314770

4732-
// Get import attributes from the import/export statement
47334771
const importAttributes = getImportAttributesFromLocation(location);
47344772

47354773
const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true, importAttributes);
@@ -4820,6 +4858,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
48204858
}
48214859

48224860
if (sourceFile.symbol) {
4861+
// Check for pattern ambient module with matching import attributes
4862+
if (importAttributes?.elements.length) {
4863+
const patternMatch = tryFindPatternAmbientModuleWithAttributes(moduleReference, importAttributes);
4864+
if (patternMatch) {
4865+
return patternMatch;
4866+
}
4867+
}
4868+
48234869
if (errorNode && resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
48244870
errorOnImplicitAnyModule(/*isError*/ false, errorNode, currentSourceFile, mode, resolvedModule, moduleReference);
48254871
}
@@ -4865,32 +4911,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
48654911
if (patternAmbientModules) {
48664912
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference);
48674913
if (pattern) {
4868-
// Check if the pattern module has matching import attributes
4869-
const patternSymbol = pattern.symbol;
4870-
if (patternSymbol.declarations) {
4871-
const hasMatchingDeclaration = patternSymbol.declarations.some(decl => {
4872-
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
4873-
return importAttributesMatch(importAttributes, decl.withClause);
4874-
}
4875-
return false;
4876-
});
4877-
if (!hasMatchingDeclaration) {
4878-
// Pattern matched but attributes don't match
4879-
if (errorNode && moduleNotFoundError) {
4880-
error(errorNode, Diagnostics.No_ambient_module_declaration_matches_import_of_0_with_the_specified_import_attributes, moduleReference);
4881-
}
4882-
return undefined;
4914+
if (!hasMatchingImportAttributes(pattern.symbol, importAttributes)) {
4915+
if (errorNode && moduleNotFoundError) {
4916+
error(errorNode, Diagnostics.No_ambient_module_declaration_matches_import_of_0_with_the_specified_import_attributes, moduleReference);
48834917
}
4918+
return undefined;
48844919
}
4885-
// If the module reference matched a pattern ambient module ('*.foo') but there's also a
4886-
// module augmentation by the specific name requested ('a.foo'), we store the merged symbol
4887-
// by the augmentation name ('a.foo'), because asking for *.foo should not give you exports
4888-
// from a.foo.
4889-
const augmentation = patternAmbientModuleAugmentations && patternAmbientModuleAugmentations.get(moduleReference);
4890-
if (augmentation) {
4891-
return getMergedSymbol(augmentation);
4892-
}
4893-
return getMergedSymbol(pattern.symbol);
4920+
const augmentation = patternAmbientModuleAugmentations?.get(moduleReference);
4921+
return getMergedSymbol(augmentation || pattern.symbol);
48944922
}
48954923
}
48964924

@@ -16048,20 +16076,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1604816076
}
1604916077

1605016078
function importAttributesMatch(importAttributes: ImportAttributes | undefined, moduleAttributes: ImportAttributes | undefined): boolean {
16051-
// If neither has attributes, they match
1605216079
if (!importAttributes && !moduleAttributes) {
1605316080
return true;
1605416081
}
16055-
// If only one has attributes, they don't match
1605616082
if (!importAttributes || !moduleAttributes) {
1605716083
return false;
1605816084
}
16059-
// Both have attributes - check if they match
16060-
// For now, we require exact match of all attributes
1606116085
if (importAttributes.elements.length !== moduleAttributes.elements.length) {
1606216086
return false;
1606316087
}
16064-
// Create a map of module attributes for easier lookup
1606516088
const moduleAttrsMap = new Map<string, string>();
1606616089
for (const attr of moduleAttributes.elements) {
1606716090
const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text;
@@ -16070,7 +16093,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1607016093
moduleAttrsMap.set(name, value);
1607116094
}
1607216095
}
16073-
// Check that all import attributes match
1607416096
for (const attr of importAttributes.elements) {
1607516097
const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text;
1607616098
const value = isStringLiteral(attr.value) ? attr.value.text : undefined;
@@ -16081,29 +16103,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1608116103
return true;
1608216104
}
1608316105

16106+
function hasMatchingImportAttributes(symbol: Symbol, importAttributes: ImportAttributes | undefined): boolean {
16107+
return !symbol.declarations || symbol.declarations.some(decl => isModuleDeclaration(decl) && isStringLiteral(decl.name) && importAttributesMatch(importAttributes, decl.withClause));
16108+
}
16109+
1608416110
function tryFindAmbientModule(moduleName: string, withAugmentations: boolean, importAttributes?: ImportAttributes) {
1608516111
if (isExternalModuleNameRelative(moduleName)) {
1608616112
return undefined;
1608716113
}
1608816114
const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule);
16089-
if (!symbol) {
16115+
if (!symbol || !hasMatchingImportAttributes(symbol, importAttributes)) {
1609016116
return undefined;
1609116117
}
16092-
// Check if the module declaration has matching import attributes
16093-
const declarations = symbol.declarations;
16094-
if (declarations) {
16095-
const hasMatchingDeclaration = declarations.some(decl => {
16096-
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
16097-
return importAttributesMatch(importAttributes, decl.withClause);
16098-
}
16099-
return false;
16100-
});
16101-
if (!hasMatchingDeclaration) {
16102-
return undefined;
16103-
}
16118+
return withAugmentations ? getMergedSymbol(symbol) : symbol;
16119+
}
16120+
16121+
function tryFindPatternAmbientModuleWithAttributes(moduleReference: string, importAttributes: ImportAttributes | undefined): Symbol | undefined {
16122+
if (!patternAmbientModules) {
16123+
return undefined;
16124+
}
16125+
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference);
16126+
if (!pattern || !hasMatchingImportAttributes(pattern.symbol, importAttributes)) {
16127+
return undefined;
1610416128
}
16105-
// merged symbol is module declaration symbol combined with all augmentations
16106-
return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol;
16129+
const augmentation = patternAmbientModuleAugmentations?.get(moduleReference);
16130+
return getMergedSymbol(augmentation || pattern.symbol);
1610716131
}
1610816132

1610916133
function hasEffectiveQuestionToken(node: ParameterDeclaration | JSDocParameterTag | JSDocPropertyTag) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
test.ts(19,32): error TS18063: No ambient module declaration matches import of 'file2.txt' with the specified import attributes.
2+
test.ts(23,34): error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes.
3+
test.ts(27,34): error TS18063: No ambient module declaration matches import of 'styles3.css' with the specified import attributes.
4+
test.ts(31,38): error TS18063: No ambient module declaration matches import of 'module2.wasm' with the specified import attributes.
5+
test.ts(35,38): error TS18063: No ambient module declaration matches import of 'module3.wasm' with the specified import attributes.
6+
7+
8+
==== types.d.ts (0 errors) ====
9+
// Test exact match with attributes
10+
declare module "*.css" with { type: "css" } {
11+
const stylesheet: CSSStyleSheet;
12+
export default stylesheet;
13+
}
14+
15+
// Test exact match with different attributes
16+
declare module "*.json" with { type: "json" } {
17+
const data: any;
18+
export default data;
19+
}
20+
21+
// Test exact match without attributes
22+
declare module "*.txt" {
23+
const content: string;
24+
export default content;
25+
}
26+
27+
// Test multiple attributes
28+
declare module "*.wasm" with { type: "module", version: "1" } {
29+
const module: WebAssembly.Module;
30+
export default module;
31+
}
32+
33+
==== test.ts (5 errors) ====
34+
async function testDynamicImports() {
35+
// Should resolve correctly - matching attributes
36+
const styles = await import("styles.css", { with: { type: "css" } });
37+
styles.default; // Should be CSSStyleSheet
38+
39+
// Should resolve correctly - matching attributes
40+
const data = await import("data.json", { with: { type: "json" } });
41+
data.default; // Should be any
42+
43+
// Should resolve correctly - no attributes on either side
44+
const text = await import("file.txt");
45+
text.default; // Should be string
46+
47+
// Should resolve correctly - multiple matching attributes
48+
const wasmModule = await import("module.wasm", { with: { type: "module", version: "1" } });
49+
wasmModule.default; // Should be WebAssembly.Module
50+
51+
// Should NOT resolve - import has attributes but declaration doesn't
52+
const text2 = await import("file2.txt", { with: { type: "text" } });
53+
~~~~~~~~~~~
54+
!!! error TS18063: No ambient module declaration matches import of 'file2.txt' with the specified import attributes.
55+
text2.default; // Should be any (no match)
56+
57+
// Should NOT resolve - import has no attributes but declaration does
58+
const styles2 = await import("styles2.css");
59+
~~~~~~~~~~~~~
60+
!!! error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes.
61+
styles2.default; // Should be any (no match)
62+
63+
// Should NOT resolve - mismatched attribute values
64+
const styles3 = await import("styles3.css", { with: { type: "style" } });
65+
~~~~~~~~~~~~~
66+
!!! error TS18063: No ambient module declaration matches import of 'styles3.css' with the specified import attributes.
67+
styles3.default; // Should be any (no match)
68+
69+
// Should NOT resolve - missing attribute
70+
const wasmModule2 = await import("module2.wasm", { with: { type: "module" } });
71+
~~~~~~~~~~~~~~
72+
!!! error TS18063: No ambient module declaration matches import of 'module2.wasm' with the specified import attributes.
73+
wasmModule2.default; // Should be any (no match - missing version attribute)
74+
75+
// Should NOT resolve - extra attribute
76+
const wasmModule3 = await import("module3.wasm", { with: { type: "module", version: "1", extra: "value" } });
77+
~~~~~~~~~~~~~~
78+
!!! error TS18063: No ambient module declaration matches import of 'module3.wasm' with the specified import attributes.
79+
wasmModule3.default; // Should be any (no match - extra attribute)
80+
}
81+
82+
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesDynamicImport.ts] ////
2+
3+
//// [types.d.ts]
4+
// Test exact match with attributes
5+
declare module "*.css" with { type: "css" } {
6+
const stylesheet: CSSStyleSheet;
7+
export default stylesheet;
8+
}
9+
10+
// Test exact match with different attributes
11+
declare module "*.json" with { type: "json" } {
12+
const data: any;
13+
export default data;
14+
}
15+
16+
// Test exact match without attributes
17+
declare module "*.txt" {
18+
const content: string;
19+
export default content;
20+
}
21+
22+
// Test multiple attributes
23+
declare module "*.wasm" with { type: "module", version: "1" } {
24+
const module: WebAssembly.Module;
25+
export default module;
26+
}
27+
28+
//// [test.ts]
29+
async function testDynamicImports() {
30+
// Should resolve correctly - matching attributes
31+
const styles = await import("styles.css", { with: { type: "css" } });
32+
styles.default; // Should be CSSStyleSheet
33+
34+
// Should resolve correctly - matching attributes
35+
const data = await import("data.json", { with: { type: "json" } });
36+
data.default; // Should be any
37+
38+
// Should resolve correctly - no attributes on either side
39+
const text = await import("file.txt");
40+
text.default; // Should be string
41+
42+
// Should resolve correctly - multiple matching attributes
43+
const wasmModule = await import("module.wasm", { with: { type: "module", version: "1" } });
44+
wasmModule.default; // Should be WebAssembly.Module
45+
46+
// Should NOT resolve - import has attributes but declaration doesn't
47+
const text2 = await import("file2.txt", { with: { type: "text" } });
48+
text2.default; // Should be any (no match)
49+
50+
// Should NOT resolve - import has no attributes but declaration does
51+
const styles2 = await import("styles2.css");
52+
styles2.default; // Should be any (no match)
53+
54+
// Should NOT resolve - mismatched attribute values
55+
const styles3 = await import("styles3.css", { with: { type: "style" } });
56+
styles3.default; // Should be any (no match)
57+
58+
// Should NOT resolve - missing attribute
59+
const wasmModule2 = await import("module2.wasm", { with: { type: "module" } });
60+
wasmModule2.default; // Should be any (no match - missing version attribute)
61+
62+
// Should NOT resolve - extra attribute
63+
const wasmModule3 = await import("module3.wasm", { with: { type: "module", version: "1", extra: "value" } });
64+
wasmModule3.default; // Should be any (no match - extra attribute)
65+
}
66+
67+
68+
69+
//// [test.js]
70+
"use strict";
71+
Object.defineProperty(exports, "__esModule", { value: true });
72+
async function testDynamicImports() {
73+
// Should resolve correctly - matching attributes
74+
const styles = await import("styles.css", { with: { type: "css" } });
75+
styles.default; // Should be CSSStyleSheet
76+
// Should resolve correctly - matching attributes
77+
const data = await import("data.json", { with: { type: "json" } });
78+
data.default; // Should be any
79+
// Should resolve correctly - no attributes on either side
80+
const text = await import("file.txt");
81+
text.default; // Should be string
82+
// Should resolve correctly - multiple matching attributes
83+
const wasmModule = await import("module.wasm", { with: { type: "module", version: "1" } });
84+
wasmModule.default; // Should be WebAssembly.Module
85+
// Should NOT resolve - import has attributes but declaration doesn't
86+
const text2 = await import("file2.txt", { with: { type: "text" } });
87+
text2.default; // Should be any (no match)
88+
// Should NOT resolve - import has no attributes but declaration does
89+
const styles2 = await import("styles2.css");
90+
styles2.default; // Should be any (no match)
91+
// Should NOT resolve - mismatched attribute values
92+
const styles3 = await import("styles3.css", { with: { type: "style" } });
93+
styles3.default; // Should be any (no match)
94+
// Should NOT resolve - missing attribute
95+
const wasmModule2 = await import("module2.wasm", { with: { type: "module" } });
96+
wasmModule2.default; // Should be any (no match - missing version attribute)
97+
// Should NOT resolve - extra attribute
98+
const wasmModule3 = await import("module3.wasm", { with: { type: "module", version: "1", extra: "value" } });
99+
wasmModule3.default; // Should be any (no match - extra attribute)
100+
}

0 commit comments

Comments
 (0)