Skip to content

Commit

Permalink
fix #3201: rewrite .js to .ts with exports
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 12, 2023
1 parent 5b50684 commit cc25614
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Rewrite `.js` to `.ts` inside packages with `exports` ([#3201](https://github.com/evanw/esbuild/issues/3201))

Packages with the `exports` field are supposed to disable node's path resolution behavior that allows you to import a file with a different extension than the one in the source code (for example, importing `foo/bar` to get `foo/bar.js`). And TypeScript has behavior where you can import a non-existent `.js` file and you will get the `.ts` file instead. Previously the presence of the `exports` field caused esbuild to disable all extension manipulation stuff which included both node's implicit file extension searching and TypeScript's file extension swapping. However, TypeScript appears to always apply file extension swapping even in this case. So with this release, esbuild will now rewrite `.js` to `.ts` even inside packages with `exports`.

* Fix a redirect edge case in esbuild's development server ([#3208](https://github.com/evanw/esbuild/issues/3208))

The development server canonicalizes directory URLs by adding a trailing slash. For example, visiting `/about` redirects to `/about/` if `/about/index.html` would be served. However, if the requested path begins with two slashes, then the redirect incorrectly turned into a protocol-relative URL. For example, visiting `//about` redirected to `//about/` which the browser turns into `http://about/`. This release fixes the bug by canonicalizing the URL path when doing this redirect.
Expand Down
41 changes: 41 additions & 0 deletions internal/bundler_tests/bundler_ts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,14 @@ func TestTSImplicitExtensions(t *testing.T) {
import './pick-tsx.jsx'
import './order-js.js'
import './order-jsx.jsx'
import 'pkg/foo-js.js'
import 'pkg/foo-jsx.jsx'
import 'pkg-exports/xyz-js'
import 'pkg-exports/xyz-jsx'
import 'pkg-exports/foo-js.js'
import 'pkg-exports/foo-jsx.jsx'
import 'pkg-imports'
`,

"/pick-js.js": `console.log("correct")`,
Expand All @@ -1346,6 +1354,39 @@ func TestTSImplicitExtensions(t *testing.T) {

"/order-jsx.ts": `console.log("correct")`,
"/order-jsx.tsx": `console.log("wrong")`,

"/node_modules/pkg/foo-js.ts": `console.log("correct")`,
"/node_modules/pkg/foo-jsx.tsx": `console.log("correct")`,

"/node_modules/pkg-exports/package.json": `{
"exports": {
"./xyz-js": "./abc-js.js",
"./xyz-jsx": "./abc-jsx.jsx",
"./*": "./lib/*"
}
}`,
"/node_modules/pkg-exports/abc-js.ts": `console.log("correct")`,
"/node_modules/pkg-exports/abc-jsx.tsx": `console.log("correct")`,
"/node_modules/pkg-exports/lib/foo-js.ts": `console.log("correct")`,
"/node_modules/pkg-exports/lib/foo-jsx.tsx": `console.log("correct")`,

"/node_modules/pkg-imports/package.json": `{
"imports": {
"#xyz-js": "./abc-js.js",
"#xyz-jsx": "./abc-jsx.jsx",
"#bar/*": "./lib/*"
}
}`,
"/node_modules/pkg-imports/index.js": `
import "#xyz-js"
import "#xyz-jsx"
import "#bar/foo-js.js"
import "#bar/foo-jsx.jsx"
`,
"/node_modules/pkg-imports/abc-js.ts": `console.log("correct")`,
"/node_modules/pkg-imports/abc-jsx.tsx": `console.log("correct")`,
"/node_modules/pkg-imports/lib/foo-js.ts": `console.log("correct")`,
"/node_modules/pkg-imports/lib/foo-jsx.tsx": `console.log("correct")`,
},
entryPaths: []string{"/entry.ts"},
options: config.Options{
Expand Down
30 changes: 30 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_ts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,36 @@ console.log("correct");
// order-jsx.ts
console.log("correct");

// node_modules/pkg/foo-js.ts
console.log("correct");

// node_modules/pkg/foo-jsx.tsx
console.log("correct");

// node_modules/pkg-exports/abc-js.ts
console.log("correct");

// node_modules/pkg-exports/abc-jsx.tsx
console.log("correct");

// node_modules/pkg-exports/lib/foo-js.ts
console.log("correct");

// node_modules/pkg-exports/lib/foo-jsx.tsx
console.log("correct");

// node_modules/pkg-imports/abc-js.ts
console.log("correct");

// node_modules/pkg-imports/abc-jsx.tsx
console.log("correct");

// node_modules/pkg-imports/lib/foo-js.ts
console.log("correct");

// node_modules/pkg-imports/lib/foo-jsx.tsx
console.log("correct");

================================================================================
TestTSImportCTS
---------- /out.js ----------
Expand Down
54 changes: 38 additions & 16 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,21 @@ func (r resolverQuery) dirInfoUncached(path string) *dirInfo {
return info
}

// TypeScript-specific behavior: if the extension is ".js" or ".jsx", try
// replacing it with ".ts" or ".tsx". At the time of writing this specific
// behavior comes from the function "loadModuleFromFile()" in the file
// "moduleNameResolver.ts" in the TypeScript compiler source code. It
// contains this comment:
//
// If that didn't work, try stripping a ".js" or ".jsx" extension and
// replacing it with a TypeScript one; e.g. "./foo.js" can be matched
// by "./foo.ts" or "./foo.d.ts"
//
// We don't care about ".d.ts" files because we can't do anything with
// those, so we ignore that part of the behavior.
//
// See the discussion here for more historical context:
// https://github.com/microsoft/TypeScript/issues/4595
var rewrittenFileExtensions = map[string][]string{
// Note that the official compiler code always tries ".ts" before
// ".tsx" even if the original extension was ".jsx".
Expand Down Expand Up @@ -1572,21 +1587,7 @@ func (r resolverQuery) loadAsFile(path string, extensionOrder []string) (string,
}
}

// TypeScript-specific behavior: if the extension is ".js" or ".jsx", try
// replacing it with ".ts" or ".tsx". At the time of writing this specific
// behavior comes from the function "loadModuleFromFile()" in the file
// "moduleNameResolver.ts" in the TypeScript compiler source code. It
// contains this comment:
//
// If that didn't work, try stripping a ".js" or ".jsx" extension and
// replacing it with a TypeScript one; e.g. "./foo.js" can be matched
// by "./foo.ts" or "./foo.d.ts"
//
// We don't care about ".d.ts" files because we can't do anything with
// those, so we ignore that part of the behavior.
//
// See the discussion here for more historical context:
// https://github.com/microsoft/TypeScript/issues/4595
// TypeScript-specific behavior: try rewriting ".js" to ".ts"
for old, exts := range rewrittenFileExtensions {
if !strings.HasSuffix(base, old) {
continue
Expand Down Expand Up @@ -2298,7 +2299,28 @@ func (r resolverQuery) finalizeImportsExportsResult(
if resolvedDirInfo == nil {
status = pjStatusModuleNotFound
} else {
if entry, diffCase := resolvedDirInfo.entries.Get(base); entry == nil {
entry, diffCase := resolvedDirInfo.entries.Get(base)

// TypeScript-specific behavior: try rewriting ".js" to ".ts"
if entry == nil {
for old, exts := range rewrittenFileExtensions {
if !strings.HasSuffix(base, old) {
continue
}
lastDot := strings.LastIndexByte(base, '.')
for _, ext := range exts {
baseWithExt := base[:lastDot] + ext
entry, diffCase = resolvedDirInfo.entries.Get(baseWithExt)
if entry != nil {
absResolvedPath = r.fs.Join(resolvedDirInfo.absPath, baseWithExt)
break
}
}
break
}
}

if entry == nil {
endsWithStar := status == pjStatusExactEndsWithStar
status = pjStatusModuleNotFound

Expand Down

0 comments on commit cc25614

Please sign in to comment.