Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
109 changes: 109 additions & 0 deletions tsc/internal/execute/tsctests/tscbuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"time"

"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/execute"
"github.com/microsoft/TypeScript/tsc/internal/execute/tsc"
"github.com/microsoft/TypeScript/tsc/internal/testutil/contentmappertest"
"github.com/microsoft/TypeScript/tsc/internal/testutil/harnessutil"
"github.com/microsoft/TypeScript/tsc/internal/testutil/stringtestutil"
Expand Down Expand Up @@ -4788,3 +4790,110 @@ func TestBuildProjectReferenceRedirectWithMultipleSubProjects(t *testing.T) {
test.run(t, "projectReferenceRedirect")
}
}

func TestBuildWithSymlinkedCurrentDirectory(t *testing.T) {
t.Parallel()

buildFiles := func(root string) FileMap {
return FileMap{
root + "/lib/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": { "composite": true, "outDir": "./dist", "rootDir": "./" },
"include": ["./index.ts"]
}`),
root + "/lib/package.json": stringtestutil.Dedent(`
{
"name": "lib",
"version": "1.0.0",
"type": "module",
"exports": { ".": "./index.ts" }
}`),
root + "/lib/index.ts": stringtestutil.Dedent(`
declare const Something: unique symbol;
export interface Duration {
readonly [Something]: "Duration";
}
export declare const dur2: Duration;`),
root + "/app/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": { "composite": true, "outDir": "./dist", "rootDir": "./" },
"include": ["./index.ts"],
"references": [{ "path": "../lib" }]
}`),
root + "/app/index.ts": stringtestutil.Dedent(`
import type { Duration } from "lib";
export declare const dur1: Duration`),
root + "/app/node_modules/lib": vfstest.Symlink(root + "/lib"),
root + "/check.ts": stringtestutil.Dedent(`
import { dur1 } from "app";
import { dur2 } from "lib";

console.log(dur1 === dur2)`),
root + "/tsconfig.json": stringtestutil.Dedent(`
{
"include": ["check.ts"],
"references": [{ "path": "./lib" }, { "path": "./app" }],
"compilerOptions": {
"composite": true,
"noEmit": true,
"paths": { "lib": ["./lib/index.ts"], "app": ["./app/index.ts"] }
}
}`),
}
}

withLinks := func(files FileMap, links map[string]string) FileMap {
for path, target := range links {
files[path] = vfstest.Symlink(target)
}
return files
}

for _, test := range []struct {
name string
files FileMap
cwd string
}{
{
name: "real path",
files: buildFiles("/home/alice/workspaces/project"),
cwd: "/home/alice/workspaces/project",
},
{
name: "current directory is the symlink",
files: withLinks(buildFiles("/home/alice/workspaces/project"), map[string]string{
"/home/alice/workspaces/symlinked": "/home/alice/workspaces/project",
}),
cwd: "/home/alice/workspaces/symlinked",
},
{
name: "parent directory is the symlink",
files: withLinks(buildFiles("/home/bob/projects/playground-app"), map[string]string{
"/home/alice/projects": "/home/bob/projects",
}),
cwd: "/home/alice/projects/playground-app",
},
{
name: "symlinked ancestor reached through a symlink chain",
files: withLinks(buildFiles("/home/bob/projects/playground-app"), map[string]string{
"/home/alice/aliased": "/home/bob/projects",
"/home/alice/nested/aliased": "/home/alice/aliased",
}),
cwd: "/home/alice/nested/aliased/playground-app",
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
sys := newTestSys(&tscInput{files: test.files, cwd: test.cwd}, false)
result := execute.CommandLine(t.Context(), sys, []string{"-b"}, sys)
assert.Equal(
t,
result.Status,
tsc.ExitStatusSuccess,
"building via %s should succeed:\n%s",
test.cwd,
sys.currentWrite.String(),
)
})
}
}
13 changes: 6 additions & 7 deletions tsc/internal/vfs/vfstest/vfstest.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,14 +605,13 @@ func (m *MapFS) Remove(path string) error {
func (m *MapFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
canonical := m.getCanonicalPath(path)
canonicalString := string(canonical)
fileInfo := m.m[canonicalString]
if fileInfo == nil {
// file does not exist
return fs.ErrNotExist

// Like [os.Chtimes], follow symlinks, including symlinked parent directories.
file, _, err := m.getFollowingSymlinks(m.getCanonicalPath(path))
if err != nil {
return err
}
fileInfo.ModTime = mTime
file.ModTime = mTime
return nil
}

Expand Down
22 changes: 22 additions & 0 deletions tsc/internal/vfs/vfstest/vfstest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"testing"
"testing/fstest"
"time"
"unicode/utf16"

"github.com/microsoft/TypeScript/tsc/internal/testutil"
Expand Down Expand Up @@ -658,6 +659,27 @@ func TestWritableFSSymlink(t *testing.T) {
assert.Equal(t, content, "hello, world")
}

func TestChtimesFollowsSymlinks(t *testing.T) {
t.Parallel()

fsys := FromMap(map[string]any{
"/some/dir/a.ts": "hello, world",
"/some/dirlink": Symlink("/some/dir"),
"/real.ts": "hello, world",
"/link.ts": Symlink("/real.ts"),
}, false)

modified := time.Unix(123, 0)

assert.NilError(t, fsys.Chtimes("/some/dirlink/a.ts", modified, modified))
assert.Equal(t, fsys.Stat("/some/dir/a.ts").ModTime(), modified)

assert.NilError(t, fsys.Chtimes("/link.ts", modified, modified))
assert.Equal(t, fsys.Stat("/real.ts").ModTime(), modified)

assert.ErrorIs(t, fsys.Chtimes("/missing.ts", modified, modified), fs.ErrNotExist)
}

func TestWritableFSSymlinkChain(t *testing.T) {
t.Parallel()

Expand Down