Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14729,6 +14729,21 @@ func (c *Checker) resolveExternalModuleNameWorker(location *ast.Node, moduleRefe
return nil
}

// tryExtractTSExtensionLoose does a best-effort extraction of a TS extension from a string
// by checking if it contains any supported TS extension. This is useful for error reporting
// in cases where a TS extension appears in the middle of a module specifier rather than at
// the end (e.g., "#/foo.ts.omg" where the wildcard matched ".ts").
// Note: This may misidentify in pathological cases like "foo.ts.mts.cts.oops", but such
// cases are unlikely in practice.
func tryExtractTSExtensionLoose(fileName string) string {
for _, ext := range tspath.SupportedTSExtensionsFlat {
if strings.Contains(fileName, ext) {
return ext
}
}
return ""
}

func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference string, moduleNotFoundError *diagnostics.Message, errorNode *ast.Node, isForAugmentation bool) *ast.Symbol {
if errorNode != nil && strings.HasPrefix(moduleReference, "@types/") {
withoutAtTypePrefix := moduleReference[len("@types/"):]
Expand Down Expand Up @@ -14822,7 +14837,13 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri
if ast.FindAncestor(location, ast.IsEmittableImport) != nil {
tsExtension := tspath.TryExtractTSExtension(moduleReference)
if tsExtension == "" {
panic("should be able to extract TS extension from string that passes IsDeclarationFileName")
// Fallback: Try to extract a TS extension using a loose search.
// This handles cases where a wildcard pattern matches a TS extension that's
// not at the end of the module specifier, e.g., "#/foo.ts.omg" through "#/*.omg": "./src/*"
tsExtension = tryExtractTSExtensionLoose(moduleReference)
}
if tsExtension == "" {
panic("should be able to extract TS extension from string when resolvedUsingTsExtension is true")
}
c.error(
errorNode,
Expand Down
9 changes: 8 additions & 1 deletion internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1601,10 +1601,17 @@ func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions
if extensions&extensionsTypeScript != 0 && tspath.HasImplementationTSFileExtension(candidate) || extensions&extensionsDeclaration != 0 && tspath.IsDeclarationFileName(candidate) {
if path, ok := r.tryFile(candidate, onlyRecordFailures); ok {
extension := tspath.TryExtractTSExtension(path)
// resolvedUsingTsExtension should be true when the pattern ends with * and the
// candidate file ends in a TS extension. This means the * matched a TS extension
// from the module specifier. For example:
// - import "pkg/foo.ts" with pattern "./*" -> true
// - import "pkg/foo.ts.omg" with pattern "./*.omg" -> true (star matched .ts)
// - import "pkg/foo" with pattern "./*.ts" -> false (extension in pattern, not specifier)
resolvedUsingTsExtension := strings.HasSuffix(packageJSONValue, "*") && extension != ""
return &resolved{
path: path,
extension: extension,
resolvedUsingTsExtension: packageJSONValue != "" && !strings.HasSuffix(packageJSONValue, extension),
resolvedUsingTsExtension: resolvedUsingTsExtension,
}
}
return continueSearching()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//// [tests/cases/compiler/packageJsonImportsMalformedPattern.ts] ////

//// [package.json]
{
"imports": {
"#/*": {
"types": "./src/*ts",
"default": "./dist/*js"
}
}
}

//// [a.ts]
import * as b from "#/b.";

b.foo();

//// [b.ts]
export function foo() {}


//// [b.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.foo = foo;
function foo() { }
//// [a.js]
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const b = __importStar(require("#/b."));
b.foo();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//// [tests/cases/compiler/packageJsonImportsMalformedPattern.ts] ////

=== src/a.ts ===
import * as b from "#/b.";
>b : Symbol(b, Decl(a.ts, 0, 6))

b.foo();
>b.foo : Symbol(b.foo, Decl(b.ts, 0, 0))
>b : Symbol(b, Decl(a.ts, 0, 6))
>foo : Symbol(b.foo, Decl(b.ts, 0, 0))

=== src/b.ts ===
export function foo() {}
>foo : Symbol(foo, Decl(b.ts, 0, 0))

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//// [tests/cases/compiler/packageJsonImportsMalformedPattern.ts] ////

=== src/a.ts ===
import * as b from "#/b.";
>b : typeof b

b.foo();
>b.foo() : void
>b.foo : () => void
>b : typeof b
>foo : () => void

=== src/b.ts ===
export function foo() {}
>foo : () => void

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
src/index.ts(1,23): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.


==== tsconfig.json (0 errors) ====
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}

==== src/foo.ts (0 errors) ====
export function hello() {
return "world";
}

==== src/index.ts (1 errors) ====
import { hello } from "#/foo.ts.omg";
~~~~~~~~~~~~~~
!!! error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.

hello();

==== package.json (0 errors) ====
{
"type": "module",
"imports": {
"#/*.omg": "./src/*"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [tests/cases/compiler/packageJsonImportsWildcardMatchesTsExtension.ts] ////

//// [package.json]
{
"type": "module",
"imports": {
"#/*.omg": "./src/*"
}
}

//// [foo.ts]
export function hello() {
return "world";
}

//// [index.ts]
import { hello } from "#/foo.ts.omg";

hello();


//// [foo.js]
export function hello() {
return "world";
}
//// [index.js]
import { hello } from "#/foo.ts.omg";
hello();
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//// [tests/cases/compiler/packageJsonImportsWildcardMatchesTsExtension.ts] ////

=== src/foo.ts ===
export function hello() {
>hello : Symbol(hello, Decl(foo.ts, 0, 0))

return "world";
}

=== src/index.ts ===
import { hello } from "#/foo.ts.omg";
>hello : Symbol(hello, Decl(index.ts, 0, 8))

hello();
>hello : Symbol(hello, Decl(index.ts, 0, 8))

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//// [tests/cases/compiler/packageJsonImportsWildcardMatchesTsExtension.ts] ////

=== src/foo.ts ===
export function hello() {
>hello : () => string

return "world";
>"world" : "world"
}

=== src/index.ts ===
import { hello } from "#/foo.ts.omg";
>hello : () => string

hello();
>hello() : string
>hello : () => string

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @module: nodenext
// @moduleResolution: nodenext
// @filename: src/a.ts
import * as b from "#/b.";

b.foo();

// @filename: src/b.ts
export function foo() {}

// @filename: package.json
{
"imports": {
"#/*": {
"types": "./src/*ts",
"default": "./dist/*js"
}
}
}

// @filename: tsconfig.json
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @module: nodenext
// @moduleResolution: nodenext
// Test verifies that when a module specifier contains ".ts" that gets matched by a
// wildcard pattern, resolvedUsingTsExtension is correctly set to true.
// Example: import "#/foo.ts.omg" with pattern "#/*.omg": "./src/*"
// The * matches "foo.ts", and when expanded becomes "./src/foo.ts"
// Since the wildcard matched ".ts" from the specifier, an error should be reported.

// @filename: src/foo.ts
export function hello() {
return "world";
}

// @filename: src/index.ts
import { hello } from "#/foo.ts.omg";

hello();

// @filename: package.json
{
"type": "module",
"imports": {
"#/*.omg": "./src/*"
}
}

// @filename: tsconfig.json
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}