Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tsconfig.json compilerOptions.paths support #144

Merged
merged 10 commits into from
May 29, 2020
47 changes: 47 additions & 0 deletions internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,53 @@ console.log(main_esm_default());
})
}

func TestTsConfigPaths(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import fn from 'core/test'
console.log(fn())
`,
"/Users/user/project/tsconfig.json": `
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"core/*": ["./src/*"]
}
}
}
`,
"/Users/user/project/src/test.js": `
module.exports = function() {
return 123
}
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
parseOptions: parser.ParseOptions{
IsBundling: true,
},
bundleOptions: BundleOptions{
IsBundling: true,
AbsOutputFile: "/Users/user/project/out.js",
},
expected: map[string]string{
"/Users/user/project/out.js": `// /Users/user/project/src/test.js
var require_test = __commonJS((exports, module) => {
module.exports = function() {
return 123;
};
});

// /Users/user/project/src/entry.js
const test = __toModule(require_test());
console.log(test.default());
`,
},
})
}

func TestPackageJsonBrowserString(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
Expand Down
63 changes: 58 additions & 5 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package resolver

import (
"os"
fp "path/filepath"
"regexp"
"strings"
"sync"

Expand Down Expand Up @@ -230,7 +233,8 @@ type packageJson struct {
}

type tsConfigJson struct {
absPathBaseUrl *string // The absolute path of "compilerOptions.baseUrl"
absPathBaseUrl *string // The absolute path of "compilerOptions.baseUrl"
paths map[string][]string // The absolute paths of "compilerOptions.paths"
}

type dirInfo struct {
Expand Down Expand Up @@ -301,6 +305,25 @@ func (r *resolver) parseJsTsConfig(file string, path string, info *dirInfo) {
info.tsConfigJson.absPathBaseUrl = &baseUrl
}
}
if pathsJson, ok := getProperty(compilerOptionsJson, "paths"); ok {
if paths, ok := pathsJson.Data.(*ast.EObject); ok {
info.tsConfigJson.paths = map[string][]string{}
for _, prop := range paths.Properties {
if key, ok := getString(prop.Key); ok {
if value, ok := getProperty(pathsJson, key); ok {
if array, ok := value.Data.(*ast.EArray); ok {
for _, item := range array.Items {
if str, ok := getString(item); ok {
// If this is a string, it's a replacement module
info.tsConfigJson.paths[key] = append(info.tsConfigJson.paths[key], str)
}
}
}
}
}
}
}
}
}
}
}
Expand Down Expand Up @@ -578,13 +601,43 @@ func (r *resolver) loadAsFileOrDirectory(path string) (string, bool) {
return "", false
}

func resolvePathWithoutStar(from, path string) (string, error) {
replaced := strings.Replace(path, "/*", "", -1)
return fp.Join(from, replaced), nil
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in this file is careful to always use the fs interface in the resolver instead of path/filepath. All platform-dependent things are supposed to happen inside the fs interface so that the resolver can be platform-independent. That interface uses the real path/filepath module for the esbuild command and the path module for tests. That way tests don't do different things on Windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! resolved in 444bfae

}

func (r *resolver) loadNodeModules(path string, dirInfo *dirInfo) (string, bool) {
for {
// Handle TypeScript base URLs for TypeScript code
if dirInfo.tsConfigJson != nil && dirInfo.tsConfigJson.absPathBaseUrl != nil {
basePath := r.fs.Join(*dirInfo.tsConfigJson.absPathBaseUrl, path)
if absolute, ok := r.loadAsFileOrDirectory(basePath); ok {
return absolute, true
if dirInfo.tsConfigJson != nil {

if dirInfo.tsConfigJson.absPathBaseUrl != nil {
if dirInfo.tsConfigJson.paths != nil {
for key, originalPaths := range dirInfo.tsConfigJson.paths {
for _, originalPath := range originalPaths {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't follow the behavior of the TypeScript compiler which prefers exact matches over pattern matches and prefers matches with longer prefixes over matches with shorter prefixes. That's ok though, I'll fix it.

if matched, err := regexp.MatchString("^"+key, path); matched && err == nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a regex to match this isn't correct. The pattern is an asterisk, which causes a regex to repeat the previous character zero or more times. That would mean a pattern foo* would also match a directory named foooo and fo. I took some time to look at what the TypeScript compiler itself does. The relevant code is in tryLoadModuleUsingPaths() here.

Other relevant code is tryParsePattern() here:

    export function tryParsePattern(pattern: string): Pattern | undefined {
        // This should be verified outside of here and a proper error thrown.
        Debug.assert(hasZeroOrOneAsteriskCharacter(pattern));
        const indexOfStar = pattern.indexOf("*");
        return indexOfStar === -1 ? undefined : {
            prefix: pattern.substr(0, indexOfStar),
            suffix: pattern.substr(indexOfStar + 1)
        };
    }

and isPatternMatch() here:

    function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) {
        return candidate.length >= prefix.length + suffix.length &&
            startsWith(candidate, prefix) &&
            endsWith(candidate, suffix);
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! i've added some commits to handle this more like ts compiler, but i haven't written the test case where * is at the start or middle of the path.

if absoluteOriginalPath, err := resolvePathWithoutStar(*dirInfo.tsConfigJson.absPathBaseUrl, originalPath); err == nil {
elements := strings.Split(path, "/")

elements = elements[1:]

resolved := append(strings.Split(absoluteOriginalPath, string(os.PathSeparator)), elements...)
basePath := strings.Join(resolved, "/")
if absolute, ok := r.loadAsFileOrDirectory(basePath); ok {
return absolute, true
}
}
}
}

}
} else {
basePath := r.fs.Join(*dirInfo.tsConfigJson.absPathBaseUrl, path)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the TypeScript compiler still respects baseUrl if no paths matched, so we should do that too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! resolved on 2587e08

if absolute, ok := r.loadAsFileOrDirectory(basePath); ok {
return absolute, true
}
}

}
}

Expand Down
Binary file added npm/esbuild-linux-arm64/bin/esbuild
Binary file not shown.