ttsc 0.18.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/ttscgraph/main.go +18 -12
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
- package/cmd/ttscgraph/serve.go +863 -0
- package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
- package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
- package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
- package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
- package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
- package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
- package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
- package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
- package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
- package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
- package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
- package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
- package/driver/emit_plugin.go +27 -12
- package/driver/plugins.go +21 -5
- package/driver/program.go +43 -14
- package/driver/session.go +11 -3
- package/lib/TtscCompiler.d.ts +3 -2
- package/lib/TtscCompiler.js +6 -14
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +14 -1
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +7 -0
- package/lib/compiler/internal/runBuild.js +176 -16
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/structures/ITtscCompilerResult.d.ts +8 -5
- package/package.json +8 -8
- package/shim/printer/shim.go +22 -4
- package/src/TtscCompiler.ts +4 -16
- package/src/compiler/internal/compileProjectInMemory.ts +17 -1
- package/src/compiler/internal/runBuild.ts +237 -23
- package/src/structures/ITtscCompilerResult.ts +8 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestServeProtocolRejectsInvalidRequestLine verifies a malformed NDJSON line
|
|
11
|
+
// answers with a protocol error and never poisons the following requests.
|
|
12
|
+
//
|
|
13
|
+
// The launcher only ever writes well-formed requests, so a garbage line means
|
|
14
|
+
// a corrupted pipe or a foreign writer; the server must report it (with the
|
|
15
|
+
// zero ID, since none could be parsed) and keep serving instead of exiting.
|
|
16
|
+
//
|
|
17
|
+
// 1. Send a non-JSON line followed by a valid request.
|
|
18
|
+
// 2. Assert the first response carries an error and no dump.
|
|
19
|
+
// 3. Assert the second response is the normal initial snapshot.
|
|
20
|
+
func TestServeProtocolRejectsInvalidRequestLine(t *testing.T) {
|
|
21
|
+
root := graphSessionFixture(t)
|
|
22
|
+
input := strings.NewReader("not-json\n{\"id\":1}\n")
|
|
23
|
+
var output bytes.Buffer
|
|
24
|
+
|
|
25
|
+
if code := serveSnapshots(input, &output, root, "tsconfig.json"); code != 0 {
|
|
26
|
+
t.Fatalf("serveSnapshots exited %d", code)
|
|
27
|
+
}
|
|
28
|
+
decoder := json.NewDecoder(&output)
|
|
29
|
+
var invalid serveResponse
|
|
30
|
+
var initial serveResponse
|
|
31
|
+
if err := decoder.Decode(&invalid); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
if err := decoder.Decode(&initial); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
if invalid.ID != 0 || invalid.Error == "" || !strings.Contains(invalid.Error, "invalid request") || invalid.Dump != nil || invalid.Changed {
|
|
38
|
+
t.Fatalf("invalid line response: %#v", invalid)
|
|
39
|
+
}
|
|
40
|
+
if initial.ID != 1 || initial.Mode != "initial" || !initial.Changed || initial.Dump == nil || initial.Error != "" {
|
|
41
|
+
t.Fatalf("follow-up response: %#v", initial)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// stepReader hands the scanner one scripted chunk per Read call, running each
|
|
13
|
+
// step's side effect at the moment the server asks for more input — after the
|
|
14
|
+
// previous request has been fully answered.
|
|
15
|
+
type stepReader struct {
|
|
16
|
+
steps []func() []byte
|
|
17
|
+
buf []byte
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func (r *stepReader) Read(p []byte) (int, error) {
|
|
21
|
+
if len(r.buf) == 0 {
|
|
22
|
+
if len(r.steps) == 0 {
|
|
23
|
+
return 0, io.EOF
|
|
24
|
+
}
|
|
25
|
+
r.buf = r.steps[0]()
|
|
26
|
+
r.steps = r.steps[1:]
|
|
27
|
+
}
|
|
28
|
+
n := copy(p, r.buf)
|
|
29
|
+
r.buf = r.buf[n:]
|
|
30
|
+
return n, nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// TestServeProtocolRetriesSessionCreation verifies a project that is invalid
|
|
34
|
+
// at startup answers with an error and recovers on a later request.
|
|
35
|
+
//
|
|
36
|
+
// The MCP server starts the native process lazily, so the first request may
|
|
37
|
+
// hit a broken tsconfig. Failing session creation must stay per-request: the
|
|
38
|
+
// server keeps running, and once the config is fixed the same process builds
|
|
39
|
+
// the session and serves the initial dump.
|
|
40
|
+
//
|
|
41
|
+
// 1. Request a snapshot while tsconfig.json is invalid; assert an error.
|
|
42
|
+
// 2. Fix the config between requests, request again.
|
|
43
|
+
// 3. Assert the second response is a normal initial snapshot.
|
|
44
|
+
func TestServeProtocolRetriesSessionCreation(t *testing.T) {
|
|
45
|
+
root := t.TempDir()
|
|
46
|
+
config := filepath.Join(root, "tsconfig.json")
|
|
47
|
+
writeGraphFile(t, config, "{ invalid")
|
|
48
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "export class Recovered {}\n")
|
|
49
|
+
|
|
50
|
+
valid := `{"compilerOptions":{"target":"ES2022","module":"commonjs","strict":true},"include":["src"]}`
|
|
51
|
+
input := &stepReader{steps: []func() []byte{
|
|
52
|
+
func() []byte { return []byte("{\"id\":1}\n") },
|
|
53
|
+
func() []byte {
|
|
54
|
+
if err := os.WriteFile(config, []byte(valid), 0o644); err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
return []byte("{\"id\":2}\n")
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
var output bytes.Buffer
|
|
61
|
+
|
|
62
|
+
if code := serveSnapshots(input, &output, root, "tsconfig.json"); code != 0 {
|
|
63
|
+
t.Fatalf("serveSnapshots exited %d", code)
|
|
64
|
+
}
|
|
65
|
+
decoder := json.NewDecoder(&output)
|
|
66
|
+
var failed serveResponse
|
|
67
|
+
var recovered serveResponse
|
|
68
|
+
if err := decoder.Decode(&failed); err != nil {
|
|
69
|
+
t.Fatal(err)
|
|
70
|
+
}
|
|
71
|
+
if err := decoder.Decode(&recovered); err != nil {
|
|
72
|
+
t.Fatal(err)
|
|
73
|
+
}
|
|
74
|
+
if failed.ID != 1 || failed.Error == "" || failed.Dump != nil || failed.Changed {
|
|
75
|
+
t.Fatalf("invalid project response: %#v", failed)
|
|
76
|
+
}
|
|
77
|
+
if recovered.ID != 2 || recovered.Mode != "initial" || !recovered.Changed || recovered.Dump == nil || recovered.Error != "" {
|
|
78
|
+
t.Fatalf("recovered response: %#v", recovered)
|
|
79
|
+
}
|
|
80
|
+
if !hasDumpNode(*recovered.Dump, "Recovered") {
|
|
81
|
+
t.Fatalf("recovered dump omitted the declaration: %#v", recovered.Dump.Nodes)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestServeProtocolReusesSession verifies multiple NDJSON requests share one
|
|
11
|
+
// compiler session and unchanged responses omit the full dump.
|
|
12
|
+
func TestServeProtocolReusesSession(t *testing.T) {
|
|
13
|
+
root := graphSessionFixture(t)
|
|
14
|
+
input := strings.NewReader("{\"id\":1}\n{\"id\":2}\n")
|
|
15
|
+
var output bytes.Buffer
|
|
16
|
+
|
|
17
|
+
if code := serveSnapshots(input, &output, root, "tsconfig.json"); code != 0 {
|
|
18
|
+
t.Fatalf("serveSnapshots exited %d", code)
|
|
19
|
+
}
|
|
20
|
+
decoder := json.NewDecoder(&output)
|
|
21
|
+
var initial serveResponse
|
|
22
|
+
var unchanged serveResponse
|
|
23
|
+
if err := decoder.Decode(&initial); err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
if err := decoder.Decode(&unchanged); err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
if initial.ID != 1 || initial.Mode != "initial" || !initial.Changed || initial.Dump == nil {
|
|
30
|
+
t.Fatalf("initial response: %#v", initial)
|
|
31
|
+
}
|
|
32
|
+
if unchanged.ID != 2 || unchanged.Mode != "unchanged" || unchanged.Changed || unchanged.Dump != nil {
|
|
33
|
+
t.Fatalf("unchanged response: %#v", unchanged)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestServeSessionAppliesSourceEditIncrementally verifies a content-only source
|
|
12
|
+
// edit updates the graph through the resident tsgo Program.
|
|
13
|
+
//
|
|
14
|
+
// A declaration rename changes graph nodes but not imports, so UpdateProgram can
|
|
15
|
+
// reuse every unchanged AST. The new dump must expose the replacement symbol
|
|
16
|
+
// and remove the old one without reopening the project.
|
|
17
|
+
//
|
|
18
|
+
// 1. Build the initial graph containing `BeforeEdit`.
|
|
19
|
+
// 2. Replace it with `AfterEdit` in the same source file.
|
|
20
|
+
// 3. Assert incremental mode and an exact post-edit node set.
|
|
21
|
+
func TestServeSessionAppliesSourceEditIncrementally(t *testing.T) {
|
|
22
|
+
root := graphSessionFixture(t)
|
|
23
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
24
|
+
if err != nil {
|
|
25
|
+
t.Fatal(err)
|
|
26
|
+
}
|
|
27
|
+
defer session.Close()
|
|
28
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
29
|
+
t.Fatal(err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
file := filepath.Join(root, "src", "index.ts")
|
|
33
|
+
if err := os.WriteFile(file, []byte("export class AfterEdit {}\n"), 0o644); err != nil {
|
|
34
|
+
t.Fatal(err)
|
|
35
|
+
}
|
|
36
|
+
dump, mode, changed, err := session.Snapshot()
|
|
37
|
+
if err != nil {
|
|
38
|
+
t.Fatal(err)
|
|
39
|
+
}
|
|
40
|
+
if dump == nil || mode != "incremental" || !changed {
|
|
41
|
+
t.Fatalf("edited snapshot = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
42
|
+
}
|
|
43
|
+
if !hasDumpNode(*dump, "AfterEdit") || hasDumpNode(*dump, "BeforeEdit") {
|
|
44
|
+
t.Fatalf("incremental dump kept stale nodes: %#v", dump.Nodes)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func hasDumpNode(dump graph.Dump, name string) bool {
|
|
49
|
+
for _, node := range dump.Nodes {
|
|
50
|
+
if node.Name == name {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeSessionFailsClosedOnDeletedConfig verifies removing the project
|
|
10
|
+
// config fails the snapshot without poisoning the resident session.
|
|
11
|
+
//
|
|
12
|
+
// The boundary twin of the invalid-config case: deletion takes the
|
|
13
|
+
// hashesChanged ErrNotExist branch and its forced reload fails at session
|
|
14
|
+
// construction rather than config parsing. A byte-identical restore must then
|
|
15
|
+
// report unchanged — the resident graph still matches the disk exactly — and
|
|
16
|
+
// the compiler session must keep serving incremental edits, proving the
|
|
17
|
+
// failed reload episode left no partial state behind.
|
|
18
|
+
//
|
|
19
|
+
// 1. Build a valid initial graph, then delete tsconfig.json.
|
|
20
|
+
// 2. Assert the snapshot fails with no dump.
|
|
21
|
+
// 3. Restore the identical config and assert the snapshot is unchanged.
|
|
22
|
+
// 4. Edit the source and assert an incremental refresh still works.
|
|
23
|
+
func TestServeSessionFailsClosedOnDeletedConfig(t *testing.T) {
|
|
24
|
+
root := graphSessionFixture(t)
|
|
25
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
defer session.Close()
|
|
30
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
config := filepath.Join(root, "tsconfig.json")
|
|
35
|
+
saved, err := os.ReadFile(config)
|
|
36
|
+
if err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
if err := os.Remove(config); err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
dump, _, changed, err := session.Snapshot()
|
|
43
|
+
if err == nil || dump != nil || changed {
|
|
44
|
+
t.Fatalf("deleted config must fail closed: dump:%v changed:%v err:%v", dump != nil, changed, err)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if err := os.WriteFile(config, saved, 0o644); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
dump, mode, changed, err := session.Snapshot()
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
if dump != nil || mode != "unchanged" || changed {
|
|
55
|
+
t.Fatalf("identical restored config must be unchanged: dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if err := os.WriteFile(filepath.Join(root, "src", "index.ts"), []byte("export class AfterEdit {}\n"), 0o644); err != nil {
|
|
59
|
+
t.Fatal(err)
|
|
60
|
+
}
|
|
61
|
+
dump, mode, changed, err = session.Snapshot()
|
|
62
|
+
if err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
if dump == nil || mode != "incremental" || !changed || !hasDumpNode(*dump, "AfterEdit") {
|
|
66
|
+
t.Fatalf("session did not survive the failed reload: dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeSessionFailsClosedOnInvalidConfig verifies a broken project config
|
|
10
|
+
// never falls back to the last valid graph and recovers after the config is fixed.
|
|
11
|
+
//
|
|
12
|
+
// Returning stale compiler facts is worse than returning an MCP error. A config
|
|
13
|
+
// parse failure must therefore suppress the dump while preserving enough
|
|
14
|
+
// session state to retry the reload on the next request.
|
|
15
|
+
//
|
|
16
|
+
// 1. Build a valid initial graph, then replace tsconfig.json with invalid JSON.
|
|
17
|
+
// 2. Assert snapshot fails with no dump instead of serving the cached graph.
|
|
18
|
+
// 3. Restore a valid config and assert the next request reloads successfully.
|
|
19
|
+
func TestServeSessionFailsClosedOnInvalidConfig(t *testing.T) {
|
|
20
|
+
root := graphSessionFixture(t)
|
|
21
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
22
|
+
if err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
defer session.Close()
|
|
26
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
config := filepath.Join(root, "tsconfig.json")
|
|
31
|
+
if err := os.WriteFile(config, []byte("{ invalid"), 0o644); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
dump, _, changed, err := session.Snapshot()
|
|
35
|
+
if err == nil || dump != nil || changed {
|
|
36
|
+
t.Fatalf("invalid config must fail closed: dump:%v changed:%v err:%v", dump != nil, changed, err)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
valid := `{"compilerOptions":{"target":"ES2022","module":"commonjs","strict":true},"include":["src"]}`
|
|
40
|
+
if err := os.WriteFile(config, []byte(valid), 0o644); err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
dump, mode, changed, err := session.Snapshot()
|
|
44
|
+
if err != nil {
|
|
45
|
+
t.Fatal(err)
|
|
46
|
+
}
|
|
47
|
+
if dump == nil || mode != "reload" || !changed || !hasDumpNode(*dump, "BeforeEdit") {
|
|
48
|
+
t.Fatalf("fixed config did not recover: dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestServeSessionKeepsMissingLiteralRootUnchanged verifies a permanently
|
|
9
|
+
// absent `files` entry does not churn the session.
|
|
10
|
+
//
|
|
11
|
+
// The negative twin of the created-literal-root reload: tracking a missing
|
|
12
|
+
// root as a freshness input must record a stable absent state. If the stored
|
|
13
|
+
// parse-time root set and the reloaded one disagreed about missing literal
|
|
14
|
+
// entries, or the absent state flapped, every snapshot would degrade into a
|
|
15
|
+
// full reload and silently erase the resident session's entire benefit.
|
|
16
|
+
//
|
|
17
|
+
// 1. Open a session whose tsconfig lists absent `src/generated.ts` in files.
|
|
18
|
+
// 2. Take repeated snapshots without touching the project.
|
|
19
|
+
// 3. Assert each one reports unchanged with no replacement dump.
|
|
20
|
+
func TestServeSessionKeepsMissingLiteralRootUnchanged(t *testing.T) {
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
23
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs" },
|
|
24
|
+
"files": ["src/index.ts", "src/generated.ts"]
|
|
25
|
+
}`)
|
|
26
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "export class Present {}\n")
|
|
27
|
+
|
|
28
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
defer session.Close()
|
|
33
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
34
|
+
t.Fatal(err)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for i := 0; i < 2; i++ {
|
|
38
|
+
dump, mode, changed, err := session.Snapshot()
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if dump != nil || mode != "unchanged" || changed {
|
|
43
|
+
t.Fatalf("iteration %d: missing literal root churned = dump:%v mode:%q changed:%v", i, dump != nil, mode, changed)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeSessionRebuildsImportGraphChange verifies an edited import set uses
|
|
10
|
+
// tsgo's safe rebuild path instead of claiming structural reuse.
|
|
11
|
+
//
|
|
12
|
+
// UpdateProgram can replace one AST only while its module references are
|
|
13
|
+
// unchanged. Adding an import must rebuild the Program, refresh the Checker, and
|
|
14
|
+
// emit the newly-resolved call edge in the same resident native process.
|
|
15
|
+
//
|
|
16
|
+
// 1. Start with two exported functions and no edge between them.
|
|
17
|
+
// 2. Edit the first file to import and call the second.
|
|
18
|
+
// 3. Assert rebuild mode and a calls edge from `main` to `helper`.
|
|
19
|
+
func TestServeSessionRebuildsImportGraphChange(t *testing.T) {
|
|
20
|
+
root := graphSessionFixture(t)
|
|
21
|
+
helper := filepath.Join(root, "src", "helper.ts")
|
|
22
|
+
if err := os.WriteFile(helper, []byte("export function helper(): void {}\n"), 0o644); err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
defer session.Close()
|
|
30
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
index := filepath.Join(root, "src", "index.ts")
|
|
35
|
+
content := "import { helper } from './helper';\nexport function main(): void { helper(); }\n"
|
|
36
|
+
if err := os.WriteFile(index, []byte(content), 0o644); err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
dump, mode, changed, err := session.Snapshot()
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
if dump == nil || mode != "rebuild" || !changed {
|
|
44
|
+
t.Fatalf("import edit = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
45
|
+
}
|
|
46
|
+
mainID, helperID := "", ""
|
|
47
|
+
for _, node := range dump.Nodes {
|
|
48
|
+
if node.Name == "main" {
|
|
49
|
+
mainID = node.ID
|
|
50
|
+
} else if node.Name == "helper" {
|
|
51
|
+
helperID = node.ID
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for _, edge := range dump.Edges {
|
|
55
|
+
if edge.From == mainID && edge.To == helperID && edge.Kind == "calls" {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
t.Fatalf("rebuilt graph omitted main -> helper call: nodes=%#v edges=%#v", dump.Nodes, dump.Edges)
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestServeSessionReloadsCreatedLiteralRoot verifies a `files`-listed root
|
|
9
|
+
// that does not exist yet joins the graph once it is created.
|
|
10
|
+
//
|
|
11
|
+
// A literal `files` entry survives a root-set reload whether or not the file
|
|
12
|
+
// exists, so the root comparison never flips for it, and a file absent from
|
|
13
|
+
// the program has no source hash and no unresolved-import candidate. The
|
|
14
|
+
// session must track missing literal roots as freshness inputs of their own.
|
|
15
|
+
//
|
|
16
|
+
// 1. Open a session whose tsconfig lists absent `src/generated.ts` in files.
|
|
17
|
+
// 2. Create that file without touching any other project input.
|
|
18
|
+
// 3. Assert the next snapshot reloads and exposes the new declaration.
|
|
19
|
+
func TestServeSessionReloadsCreatedLiteralRoot(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
22
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs" },
|
|
23
|
+
"files": ["src/index.ts", "src/generated.ts"]
|
|
24
|
+
}`)
|
|
25
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "export class Present {}\n")
|
|
26
|
+
|
|
27
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
28
|
+
if err != nil {
|
|
29
|
+
t.Fatal(err)
|
|
30
|
+
}
|
|
31
|
+
defer session.Close()
|
|
32
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
writeGraphFile(t, filepath.Join(root, "src", "generated.ts"), "export class Generated {}\n")
|
|
37
|
+
dump, mode, changed, err := session.Snapshot()
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
if dump == nil || mode != "reload" || !changed || !hasDumpNode(*dump, "Generated") {
|
|
42
|
+
t.Fatalf("created files-listed root was not reloaded: dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeSessionReloadsExtendedConfigChange verifies every config in an
|
|
10
|
+
// extends chain participates in snapshot freshness.
|
|
11
|
+
//
|
|
12
|
+
// Watching only the requested tsconfig misses compiler-option changes inherited
|
|
13
|
+
// from a base file. The session records all parsed extended source files and
|
|
14
|
+
// must full-reload before answering after any one changes.
|
|
15
|
+
//
|
|
16
|
+
// 1. Create a project whose tsconfig extends `base.json`.
|
|
17
|
+
// 2. Build once, then change a compiler option only in the base config.
|
|
18
|
+
// 3. Assert the next snapshot reports a full reload.
|
|
19
|
+
func TestServeSessionReloadsExtendedConfigChange(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
writeGraphFile(t, filepath.Join(root, "base.json"), `{"compilerOptions":{"strict":true,"target":"ES2022"}}`)
|
|
22
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{"extends":"./base.json","include":["src"]}`)
|
|
23
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "export class Value {}\n")
|
|
24
|
+
|
|
25
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
defer session.Close()
|
|
30
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
base := filepath.Join(root, "base.json")
|
|
35
|
+
if err := os.WriteFile(base, []byte(`{"compilerOptions":{"strict":false,"target":"ES2022"}}`), 0o644); err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
dump, mode, changed, err := session.Snapshot()
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if dump == nil || mode != "reload" || !changed {
|
|
43
|
+
t.Fatalf("extended config edit = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeSessionReloadsNewlyResolvedConditionalExport verifies conditional
|
|
10
|
+
// exports objects and array fallbacks contribute resolution candidates.
|
|
11
|
+
//
|
|
12
|
+
// Real packages rarely map a subpath straight to one string: `exports`
|
|
13
|
+
// commonly nests condition objects (`types`, `default`) and array fallbacks.
|
|
14
|
+
// Every leaf target must be tracked, or a package shipping its compiled
|
|
15
|
+
// output later stays invisible to the resident graph.
|
|
16
|
+
//
|
|
17
|
+
// 1. Import a package subpath whose exports leaf targets do not exist yet.
|
|
18
|
+
// 2. Create the `types` condition's declaration target.
|
|
19
|
+
// 3. Assert the next snapshot reports a full reload.
|
|
20
|
+
func TestServeSessionReloadsNewlyResolvedConditionalExport(t *testing.T) {
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
23
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs" },
|
|
24
|
+
"files": ["src/index.ts"]
|
|
25
|
+
}`)
|
|
26
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "import { feature } from 'fixture-package/feature';\nexport function main(): void { feature(); }\n")
|
|
27
|
+
writeGraphFile(t, filepath.Join(root, "node_modules", "fixture-package", "package.json"), `{
|
|
28
|
+
"name": "fixture-package",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./feature": {
|
|
31
|
+
"types": "./dist/feature.d.ts",
|
|
32
|
+
"default": ["./dist/feature.js"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}`)
|
|
36
|
+
|
|
37
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
defer session.Close()
|
|
42
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
target := filepath.Join(root, "node_modules", "fixture-package", "dist", "feature.d.ts")
|
|
47
|
+
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
if err := os.WriteFile(target, []byte("export declare function feature(): void;\n"), 0o644); err != nil {
|
|
51
|
+
t.Fatal(err)
|
|
52
|
+
}
|
|
53
|
+
dump, mode, changed, err := session.Snapshot()
|
|
54
|
+
if err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
if dump == nil || mode != "reload" || !changed {
|
|
58
|
+
t.Fatalf("new conditional export target = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestServeSessionReloadsNewlyResolvedConfigTypes verifies compilerOptions
|
|
9
|
+
// `types` entries participate in freshness without any source-level syntax.
|
|
10
|
+
//
|
|
11
|
+
// A `types` package is requested by the config alone, so no triple-slash
|
|
12
|
+
// directive or import exists to contribute resolution candidates. Generated
|
|
13
|
+
// typeRoots packages appear without touching package.json or the lockfile,
|
|
14
|
+
// which would leave the graph permanently blind to them.
|
|
15
|
+
//
|
|
16
|
+
// 1. Open a session whose config lists absent types package `fixture-types`.
|
|
17
|
+
// 2. Create the package under the configured typeRoots directory.
|
|
18
|
+
// 3. Assert the next snapshot reports a full reload.
|
|
19
|
+
func TestServeSessionReloadsNewlyResolvedConfigTypes(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
22
|
+
"compilerOptions": { "target": "ES2022", "typeRoots": ["./types"], "types": ["fixture-types"] },
|
|
23
|
+
"files": ["src/index.ts"]
|
|
24
|
+
}`)
|
|
25
|
+
writeGraphFile(t, filepath.Join(root, "src", "index.ts"), "export const value = 1;\n")
|
|
26
|
+
|
|
27
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
28
|
+
if err != nil {
|
|
29
|
+
t.Fatal(err)
|
|
30
|
+
}
|
|
31
|
+
defer session.Close()
|
|
32
|
+
if _, _, _, err := session.Snapshot(); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
writeGraphFile(t, filepath.Join(root, "types", "fixture-types", "index.d.ts"), "interface FixtureType { id: number }\n")
|
|
37
|
+
dump, mode, changed, err := session.Snapshot()
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
if dump == nil || mode != "reload" || !changed {
|
|
42
|
+
t.Fatalf("new config types package = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
|
|
43
|
+
}
|
|
44
|
+
}
|