ttsc 0.23.0 → 0.24.0
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/serve.go +264 -48
- package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
- package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
- package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
- package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
- package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
- package/cmd/ttscgraph/serve_shards.go +1390 -0
- package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
- package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
- package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
- package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
- package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
- package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
- package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
- package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
- package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
- package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
- package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
- package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
- package/driver/declaration_shape.go +85 -0
- package/driver/program.go +39 -10
- package/internal/graph/build.go +62 -10
- package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
- package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
- package/internal/graph/dump.go +84 -43
- package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
- package/internal/graph/dump_paths.go +134 -11
- package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
- package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
- package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
- package/internal/graph/edges.go +58 -16
- package/internal/graph/exports.go +9 -2
- package/internal/graph/graph.go +47 -7
- package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
- package/internal/graph/provenance.go +11 -1
- package/internal/graph/resolve.go +1 -4
- package/internal/graph/source.go +23 -0
- package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
- package/package.json +8 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestServeShardsRefreshCrossFileImplementation verifies that an assignment
|
|
11
|
+
// source and the declaration node whose implementation span it owns enter one
|
|
12
|
+
// replacement transaction.
|
|
13
|
+
func TestServeShardsRefreshCrossFileImplementation(t *testing.T) {
|
|
14
|
+
root := t.TempDir()
|
|
15
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
16
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
17
|
+
"include": ["src"]
|
|
18
|
+
}`)
|
|
19
|
+
service := filepath.Join(root, "src", "service.ts")
|
|
20
|
+
install := filepath.Join(root, "src", "install.ts")
|
|
21
|
+
writeGraphFile(t, service, "export class Service { run(): void {} }\n")
|
|
22
|
+
writeGraphFile(t, install, "import { Service } from './service';\nexport function helper(): void {}\nexport function install(value: Service): void { value.run = (): void => { helper(); }; }\n")
|
|
23
|
+
|
|
24
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
25
|
+
if err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
defer session.Close()
|
|
29
|
+
if snapshot, _, _, err := session.SnapshotShards(); err != nil || snapshot == nil {
|
|
30
|
+
t.Fatalf("initial shard snapshot = snapshot:%v error:%v", snapshot != nil, err)
|
|
31
|
+
}
|
|
32
|
+
serviceSource := session.compiler.Program().SourceFile(service)
|
|
33
|
+
if serviceSource == nil {
|
|
34
|
+
t.Fatal("service source was absent from resident program")
|
|
35
|
+
}
|
|
36
|
+
serviceKey := session.graphStore.sourceKeys[serviceSource.FileName()]
|
|
37
|
+
before := session.graphStore.shards[serviceKey].digest
|
|
38
|
+
evidenceFile := ""
|
|
39
|
+
for _, edge := range session.graphStore.shards[serviceKey].shard.Edges {
|
|
40
|
+
if strings.Contains(edge.From, "#Service.run:method") && strings.Contains(edge.To, "#helper:function") && edge.Evidence != nil {
|
|
41
|
+
evidenceFile = edge.Evidence.File
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if evidenceFile != "src/install.ts" {
|
|
46
|
+
t.Fatalf("cross-file implementation edge evidence = %q", evidenceFile)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if err := os.WriteFile(install, []byte("import { Service } from './service';\nexport function helper(): void {}\nexport function install(value: Service): void { void value; }\n"), 0o644); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
snapshot, mode, changed, err := session.SnapshotShards()
|
|
53
|
+
if err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
if snapshot == nil || mode != serveModeIncremental || !changed {
|
|
57
|
+
t.Fatalf("cross-file implementation edit = snapshot:%v mode:%q changed:%v", snapshot != nil, mode, changed)
|
|
58
|
+
}
|
|
59
|
+
if session.graphStore.shards[serviceKey].digest == before {
|
|
60
|
+
t.Fatal("declaration shard retained its cross-file implementation evidence")
|
|
61
|
+
}
|
|
62
|
+
assertServeShardFactsMatchFullDump(t, session)
|
|
63
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeShardsRefreshDependentDiagnostics proves a public API edit rebuilds
|
|
10
|
+
// the reverse semantic closure. The dependent source text is unchanged while
|
|
11
|
+
// its checker diagnostic changes, so reusing that shard would be stale.
|
|
12
|
+
//
|
|
13
|
+
// 1. Commit a producer and consumer whose public types initially agree.
|
|
14
|
+
// 2. Change only the producer's exported return type.
|
|
15
|
+
// 3. Require both sources to be re-extracted and the new consumer diagnostic
|
|
16
|
+
// to be transmitted in its replacement shard.
|
|
17
|
+
func TestServeShardsRefreshDependentDiagnostics(t *testing.T) {
|
|
18
|
+
root := t.TempDir()
|
|
19
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
20
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
21
|
+
"include": ["src"]
|
|
22
|
+
}`)
|
|
23
|
+
value := filepath.Join(root, "src", "value.ts")
|
|
24
|
+
consumer := filepath.Join(root, "src", "consumer.ts")
|
|
25
|
+
writeGraphFile(t, value, "export function value(): number { return 1; }\n")
|
|
26
|
+
writeGraphFile(t, consumer, "import { value } from './value';\nexport const consumed: number = value();\n")
|
|
27
|
+
|
|
28
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
defer session.Close()
|
|
33
|
+
if snapshot, _, _, err := session.SnapshotShards(); err != nil || snapshot == nil {
|
|
34
|
+
t.Fatalf("initial shard snapshot = snapshot:%v error:%v", snapshot != nil, err)
|
|
35
|
+
}
|
|
36
|
+
consumerSource := session.compiler.Program().SourceFile(consumer)
|
|
37
|
+
if consumerSource == nil {
|
|
38
|
+
t.Fatal("consumer source was absent from resident program")
|
|
39
|
+
}
|
|
40
|
+
consumerKey := session.graphStore.sourceKeys[consumerSource.FileName()]
|
|
41
|
+
before := session.graphStore.shards[consumerKey].digest
|
|
42
|
+
|
|
43
|
+
if err := os.WriteFile(value, []byte("export function value(): string { return 'one'; }\n"), 0o644); err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
snapshot, mode, changed, err := session.SnapshotShards()
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
if snapshot == nil || mode != serveModeIncremental || !changed {
|
|
51
|
+
t.Fatalf("public API edit = snapshot:%v mode:%q changed:%v", snapshot != nil, mode, changed)
|
|
52
|
+
}
|
|
53
|
+
after := session.graphStore.shards[consumerKey].digest
|
|
54
|
+
if after == before {
|
|
55
|
+
t.Fatal("dependent diagnostic retained its previous shard digest")
|
|
56
|
+
}
|
|
57
|
+
upserted := false
|
|
58
|
+
for _, upsert := range snapshot.Upserts {
|
|
59
|
+
if upsert.Shard.Key == consumerKey {
|
|
60
|
+
upserted = true
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if !upserted {
|
|
65
|
+
t.Fatal("public API edit did not transmit the changed dependent shard")
|
|
66
|
+
}
|
|
67
|
+
valueKeyFile := session.compiler.Program().SourceFile(value).FileName()
|
|
68
|
+
consumerKeyFile := consumerSource.FileName()
|
|
69
|
+
if len(session.graphStore.extractedFiles) != 2 ||
|
|
70
|
+
session.graphStore.extractedFiles[0] != consumerKeyFile ||
|
|
71
|
+
session.graphStore.extractedFiles[1] != valueKeyFile {
|
|
72
|
+
t.Fatalf(
|
|
73
|
+
"public API edit extracted %v, want reverse closure [%s %s]",
|
|
74
|
+
session.graphStore.extractedFiles,
|
|
75
|
+
consumerKeyFile,
|
|
76
|
+
valueKeyFile,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
assertServeShardFactsMatchFullDump(t, session)
|
|
80
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestServeShardsRenameMovesSourceIdentity verifies a renamed source deletes its
|
|
10
|
+
// old shard and publishes a new one whose coordinates name the new path.
|
|
11
|
+
//
|
|
12
|
+
// A rename is a delete and a create at the filesystem layer but not at the
|
|
13
|
+
// resolution layer: the old path can still be imported, and the shard key binds
|
|
14
|
+
// the path, so carrying the old identity forward would leave the consumer with
|
|
15
|
+
// two coordinates for one declaration. The dependent's import is rewritten in
|
|
16
|
+
// the same step, which is what keeps the program resolvable and proves the
|
|
17
|
+
// closure covers both ends of the move.
|
|
18
|
+
//
|
|
19
|
+
// 1. Commit a project whose consumer imports a source by its original path.
|
|
20
|
+
// 2. Rename that source and repoint the import in the same generation.
|
|
21
|
+
// 3. Require the old shard deleted, a new shard published, and the dependent replaced.
|
|
22
|
+
func TestServeShardsRenameMovesSourceIdentity(t *testing.T) {
|
|
23
|
+
root := t.TempDir()
|
|
24
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
25
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
26
|
+
"include": ["src"]
|
|
27
|
+
}`)
|
|
28
|
+
original := filepath.Join(root, "src", "original.ts")
|
|
29
|
+
consumerFile := filepath.Join(root, "src", "consumer.ts")
|
|
30
|
+
writeGraphFile(t, original, "export function moved(): number { return 3; }\n")
|
|
31
|
+
writeGraphFile(t, consumerFile, "import { moved } from './original';\nexport function consume(): number { return moved(); }\n")
|
|
32
|
+
|
|
33
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
defer session.Close()
|
|
38
|
+
initial, mode, changed, err := session.SnapshotShards()
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if initial == nil || mode != serveModeInitial || !changed {
|
|
43
|
+
t.Fatalf("initial snapshot = snapshot:%v mode:%q changed:%v", initial != nil, mode, changed)
|
|
44
|
+
}
|
|
45
|
+
originalSource := session.compiler.Program().SourceFile(original)
|
|
46
|
+
consumerSource := session.compiler.Program().SourceFile(consumerFile)
|
|
47
|
+
if originalSource == nil || consumerSource == nil {
|
|
48
|
+
t.Fatal("fixture source was absent from resident program")
|
|
49
|
+
}
|
|
50
|
+
originalKeyFile := originalSource.FileName()
|
|
51
|
+
consumerKeyFile := consumerSource.FileName()
|
|
52
|
+
originalKey := session.graphStore.sourceKeys[originalKeyFile]
|
|
53
|
+
initialConsumerKey := session.graphStore.sourceKeys[consumerKeyFile]
|
|
54
|
+
if originalKey == "" || initialConsumerKey == "" {
|
|
55
|
+
t.Fatal("initial generation omitted a fixture source shard")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
renamed := filepath.Join(root, "src", "renamed.ts")
|
|
59
|
+
if err := os.Rename(original, renamed); err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
writeGraphFile(t, consumerFile, "import { moved } from './renamed';\nexport function consume(): number { return moved(); }\n")
|
|
63
|
+
|
|
64
|
+
next, mode, changed, err := session.SnapshotShards()
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
if next == nil || !changed || next.BaseGeneration != initial.Generation {
|
|
69
|
+
t.Fatalf("rename generation = snapshot:%#v mode:%q changed:%v", next, mode, changed)
|
|
70
|
+
}
|
|
71
|
+
if !containsString(next.Deletes, originalKey) {
|
|
72
|
+
t.Fatalf("rename did not delete the superseded shard %q: %v", originalKey, next.Deletes)
|
|
73
|
+
}
|
|
74
|
+
if _, exists := session.graphStore.sourceKeys[originalKeyFile]; exists {
|
|
75
|
+
t.Fatal("committed store retained the pre-rename source identity")
|
|
76
|
+
}
|
|
77
|
+
renamedSource := session.compiler.Program().SourceFile(renamed)
|
|
78
|
+
if renamedSource == nil {
|
|
79
|
+
t.Fatal("renamed source did not enter the resident program")
|
|
80
|
+
}
|
|
81
|
+
renamedKey := session.graphStore.sourceKeys[renamedSource.FileName()]
|
|
82
|
+
if renamedKey == "" {
|
|
83
|
+
t.Fatal("renamed source did not acquire a committed shard identity")
|
|
84
|
+
}
|
|
85
|
+
if renamedKey == originalKey {
|
|
86
|
+
t.Fatalf("renamed source reused the pre-rename shard key %q", renamedKey)
|
|
87
|
+
}
|
|
88
|
+
if !containsUpsertedShardKey(next, renamedKey) {
|
|
89
|
+
t.Fatalf("generation did not publish the renamed source shard %q", renamedKey)
|
|
90
|
+
}
|
|
91
|
+
nextConsumerKey := session.graphStore.sourceKeys[consumerKeyFile]
|
|
92
|
+
if nextConsumerKey == initialConsumerKey {
|
|
93
|
+
t.Fatalf("dependent shard identity %q survived a repointed import", nextConsumerKey)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestServeShardsRetryPreservesCommittedGeneration verifies projection failure
|
|
11
|
+
// cannot advance the native shard store. The same captured compiler change is
|
|
12
|
+
// retried against the prior base and publishes exactly the next sequence only
|
|
13
|
+
// after every replacement validates.
|
|
14
|
+
func TestServeShardsRetryPreservesCommittedGeneration(t *testing.T) {
|
|
15
|
+
root := graphSessionFixture(t)
|
|
16
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
17
|
+
if err != nil {
|
|
18
|
+
t.Fatal(err)
|
|
19
|
+
}
|
|
20
|
+
defer session.Close()
|
|
21
|
+
initial, _, _, err := session.SnapshotShards()
|
|
22
|
+
if err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
committed := session.graphStore
|
|
26
|
+
file := filepath.Join(root, "src", "index.ts")
|
|
27
|
+
if err := os.WriteFile(file, []byte("export class AfterRetry {}\n"), 0o644); err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
session.cwd = "relative-project"
|
|
32
|
+
snapshot, _, changed, err := session.SnapshotShards()
|
|
33
|
+
if err == nil || !strings.Contains(err.Error(), "project root") {
|
|
34
|
+
t.Fatalf("projection error = %v, want absolute-root rejection", err)
|
|
35
|
+
}
|
|
36
|
+
if snapshot != nil || changed || session.graphStore != committed || session.pending == nil {
|
|
37
|
+
t.Fatalf("failed transaction mutated publication state: snapshot:%v changed:%v store:%v pending:%v", snapshot != nil, changed, session.graphStore != committed, session.pending != nil)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
session.cwd = root
|
|
41
|
+
recovered, mode, changed, err := session.SnapshotShards()
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if recovered == nil || mode != serveModeIncremental || !changed || recovered.Sequence != initial.Sequence+1 || recovered.BaseGeneration != initial.Generation {
|
|
46
|
+
t.Fatalf("recovered transaction = snapshot:%#v mode:%q changed:%v", recovered, mode, changed)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"runtime"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestServeShardsReuseUnaffectedSources verifies a private body edit advances
|
|
11
|
+
// only the changed content-addressed source shard. TypeScript's forced
|
|
12
|
+
// declaration output proves the public shape stayed fixed, so neither the
|
|
13
|
+
// dependent nor unrelated source is re-extracted or retransmitted.
|
|
14
|
+
//
|
|
15
|
+
// 1. Commit a project where one source has a dependent and an unrelated peer.
|
|
16
|
+
// 2. Change only the source's private function body and request another shard snapshot.
|
|
17
|
+
// 3. Require one extracted source and no dependent or unrelated shard replacement.
|
|
18
|
+
func TestServeShardsReuseUnaffectedSources(t *testing.T) {
|
|
19
|
+
root := t.TempDir()
|
|
20
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
21
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}`)
|
|
24
|
+
writeGraphFile(t, filepath.Join(root, "src", "value.ts"), "export function value(): number { return 1; }\n")
|
|
25
|
+
writeGraphFile(t, filepath.Join(root, "src", "consumer.ts"), "import { value } from './value';\nexport function consume(): number { return value(); }\n")
|
|
26
|
+
writeGraphFile(t, filepath.Join(root, "src", "unrelated.ts"), "export const unrelated = true;\n")
|
|
27
|
+
|
|
28
|
+
session, err := newGraphSession(root, "tsconfig.json")
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
defer session.Close()
|
|
33
|
+
initial, mode, changed, err := session.SnapshotShards()
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
if initial == nil || mode != serveModeInitial || !changed {
|
|
38
|
+
t.Fatalf("initial snapshot = snapshot:%v mode:%q changed:%v", initial != nil, mode, changed)
|
|
39
|
+
}
|
|
40
|
+
valueFile := filepath.Join(root, "src", "value.ts")
|
|
41
|
+
consumerFile := filepath.Join(root, "src", "consumer.ts")
|
|
42
|
+
unrelatedFile := filepath.Join(root, "src", "unrelated.ts")
|
|
43
|
+
valueSource := session.compiler.Program().SourceFile(valueFile)
|
|
44
|
+
consumerSource := session.compiler.Program().SourceFile(consumerFile)
|
|
45
|
+
unrelatedSource := session.compiler.Program().SourceFile(unrelatedFile)
|
|
46
|
+
if valueSource == nil || consumerSource == nil || unrelatedSource == nil {
|
|
47
|
+
t.Fatal("fixture source was absent from resident program")
|
|
48
|
+
}
|
|
49
|
+
valueKeyFile := valueSource.FileName()
|
|
50
|
+
consumerKeyFile := consumerSource.FileName()
|
|
51
|
+
unrelatedKeyFile := unrelatedSource.FileName()
|
|
52
|
+
initialValueKey := session.graphStore.sourceKeys[valueKeyFile]
|
|
53
|
+
initialConsumerKey := session.graphStore.sourceKeys[consumerKeyFile]
|
|
54
|
+
initialUnrelatedKey := session.graphStore.sourceKeys[unrelatedKeyFile]
|
|
55
|
+
unrelatedWireFile := session.graphStore.wireSources[unrelatedKeyFile]
|
|
56
|
+
for index := range session.graphStore.provenance.Sources {
|
|
57
|
+
if session.graphStore.provenance.Sources[index].File != unrelatedKeyFile {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if runtime.GOOS == "windows" {
|
|
61
|
+
volume := "C:"
|
|
62
|
+
if filepath.VolumeName(root) == volume {
|
|
63
|
+
volume = "Z:"
|
|
64
|
+
}
|
|
65
|
+
session.graphStore.provenance.Sources[index].File = volume + "/unrelated.ts"
|
|
66
|
+
} else {
|
|
67
|
+
session.graphStore.provenance.Sources[index].File = "//foreign/share/unrelated.ts"
|
|
68
|
+
}
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
if unrelatedWireFile == "" {
|
|
72
|
+
t.Fatal("unrelated source was absent from physical/wire provenance")
|
|
73
|
+
}
|
|
74
|
+
if err := os.WriteFile(valueFile, []byte("export function value(): number { return 2; }\n"), 0o644); err != nil {
|
|
75
|
+
t.Fatal(err)
|
|
76
|
+
}
|
|
77
|
+
delta, mode, changed, err := session.SnapshotShards()
|
|
78
|
+
if err != nil {
|
|
79
|
+
t.Fatal(err)
|
|
80
|
+
}
|
|
81
|
+
if delta == nil || mode != serveModeIncremental || !changed || delta.BaseSequence != initial.Sequence || delta.BaseGeneration != initial.Generation {
|
|
82
|
+
t.Fatalf("incremental coordinates: snapshot=%#v mode=%q changed=%v", delta, mode, changed)
|
|
83
|
+
}
|
|
84
|
+
nextValueKey := session.graphStore.sourceKeys[valueKeyFile]
|
|
85
|
+
if nextValueKey == initialValueKey {
|
|
86
|
+
t.Fatal("changed source retained its content-addressed shard key")
|
|
87
|
+
}
|
|
88
|
+
if !containsString(delta.Deletes, initialValueKey) {
|
|
89
|
+
t.Fatalf("delta did not delete superseded source shard %q: %v", initialValueKey, delta.Deletes)
|
|
90
|
+
}
|
|
91
|
+
if session.graphStore.sourceKeys[consumerKeyFile] != initialConsumerKey || session.graphStore.sourceKeys[unrelatedKeyFile] != initialUnrelatedKey {
|
|
92
|
+
t.Fatal("body edit moved an unchanged source shard identity")
|
|
93
|
+
}
|
|
94
|
+
if len(session.graphStore.extractedFiles) != 1 || session.graphStore.extractedFiles[0] != valueKeyFile {
|
|
95
|
+
t.Fatalf("private body edit extracted %v, want only %s", session.graphStore.extractedFiles, valueKeyFile)
|
|
96
|
+
}
|
|
97
|
+
wireFilePreserved := false
|
|
98
|
+
for _, source := range session.graphStore.wireProvenance.Sources {
|
|
99
|
+
if source.File == unrelatedWireFile {
|
|
100
|
+
wireFilePreserved = true
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if !wireFilePreserved {
|
|
105
|
+
t.Fatalf("unchanged wire provenance lost %q", unrelatedWireFile)
|
|
106
|
+
}
|
|
107
|
+
for _, upsert := range delta.Upserts {
|
|
108
|
+
if upsert.Shard.Key == initialConsumerKey || upsert.Shard.Key == initialUnrelatedKey {
|
|
109
|
+
t.Fatalf("delta retransmitted byte-identical shard %q", upsert.Shard.Key)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func containsString(values []string, expected string) bool {
|
|
115
|
+
for _, value := range values {
|
|
116
|
+
if value == expected {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"fmt"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
11
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
12
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// DeclarationShapeDigest hashes the declaration output TypeScript itself
|
|
16
|
+
// derives for one source file. Resident consumers compare the old and new
|
|
17
|
+
// values to distinguish a private body edit from a public semantic movement
|
|
18
|
+
// before expanding invalidation through reverse dependencies.
|
|
19
|
+
func (p *Program) DeclarationShapeDigest(file *ast.SourceFile) (string, error) {
|
|
20
|
+
if p == nil || p.TSProgram == nil || file == nil {
|
|
21
|
+
return "", fmt.Errorf("driver: declaration shape requires a loaded source file")
|
|
22
|
+
}
|
|
23
|
+
if err := p.ApplyLinkedPlugins(); err != nil {
|
|
24
|
+
return "", err
|
|
25
|
+
}
|
|
26
|
+
var signature strings.Builder
|
|
27
|
+
emitted := false
|
|
28
|
+
p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
|
|
29
|
+
TargetSourceFile: file,
|
|
30
|
+
EmitOnly: shimcompiler.EmitOnlyForcedDts,
|
|
31
|
+
WriteFile: func(_ string, text string, data *shimcompiler.WriteFileData) error {
|
|
32
|
+
emitted = true
|
|
33
|
+
if data != nil && data.SourceMapUrlPos >= 0 && data.SourceMapUrlPos <= len(text) {
|
|
34
|
+
text = text[:data.SourceMapUrlPos]
|
|
35
|
+
}
|
|
36
|
+
signature.WriteString(text)
|
|
37
|
+
if data != nil {
|
|
38
|
+
for _, diagnostic := range data.Diagnostics {
|
|
39
|
+
appendDeclarationShapeDiagnostic(&signature, file, diagnostic)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return nil
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
if !emitted {
|
|
46
|
+
// Mirrors tsgo's incremental fallback to the file version when forced
|
|
47
|
+
// declaration emit has no output. This is conservative: any body movement
|
|
48
|
+
// expands dependents instead of risking a stale semantic closure.
|
|
49
|
+
signature.WriteString(file.Text())
|
|
50
|
+
}
|
|
51
|
+
digest := sha256.Sum256([]byte(signature.String()))
|
|
52
|
+
return hex.EncodeToString(digest[:]), nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func appendDeclarationShapeDiagnostic(builder *strings.Builder, source *ast.SourceFile, diagnostic *ast.Diagnostic) {
|
|
56
|
+
if diagnostic == nil {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
builder.WriteString("\n")
|
|
60
|
+
diagnosticFile := diagnostic.File()
|
|
61
|
+
if diagnosticFile != nil && diagnosticFile != source {
|
|
62
|
+
builder.WriteString(shimtspath.EnsurePathIsNonModuleName(shimtspath.GetRelativePathFromDirectory(
|
|
63
|
+
shimtspath.GetDirectoryPath(string(source.Path())),
|
|
64
|
+
string(diagnosticFile.Path()),
|
|
65
|
+
shimtspath.ComparePathsOptions{},
|
|
66
|
+
)))
|
|
67
|
+
}
|
|
68
|
+
if diagnosticFile != nil {
|
|
69
|
+
builder.WriteString(fmt.Sprintf("(%d,%d): ", diagnostic.Pos(), diagnostic.Len()))
|
|
70
|
+
}
|
|
71
|
+
builder.WriteString(diagnostic.Category().Name())
|
|
72
|
+
builder.WriteString(fmt.Sprintf("%d: ", diagnostic.Code()))
|
|
73
|
+
builder.WriteString(string(diagnostic.MessageKey()))
|
|
74
|
+
builder.WriteString("\n")
|
|
75
|
+
for _, argument := range diagnostic.MessageArgs() {
|
|
76
|
+
builder.WriteString(argument)
|
|
77
|
+
builder.WriteString("\n")
|
|
78
|
+
}
|
|
79
|
+
for _, chain := range diagnostic.MessageChain() {
|
|
80
|
+
appendDeclarationShapeDiagnostic(builder, source, chain)
|
|
81
|
+
}
|
|
82
|
+
for _, related := range diagnostic.RelatedInformation() {
|
|
83
|
+
appendDeclarationShapeDiagnostic(builder, source, related)
|
|
84
|
+
}
|
|
85
|
+
}
|
package/driver/program.go
CHANGED
|
@@ -513,8 +513,10 @@ func ApplySourcePreamble(text string, preamble string) string {
|
|
|
513
513
|
return bom + preamble + rest
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
// SourceFiles exposes the program's
|
|
517
|
-
// files
|
|
516
|
+
// SourceFiles exposes the program's resident non-declaration source files.
|
|
517
|
+
// Imported implementation files from source-distributed dependencies can be
|
|
518
|
+
// present; consumers that need project-owned files must apply their own root
|
|
519
|
+
// predicate.
|
|
518
520
|
func (p *Program) SourceFiles() []*ast.SourceFile {
|
|
519
521
|
// Discarded on purpose; see SourceFile. `Diagnostics` carries the failure.
|
|
520
522
|
_ = p.ApplyLinkedPlugins()
|
|
@@ -564,6 +566,19 @@ func (p *Program) HasLinkedProgramPlugins() bool {
|
|
|
564
566
|
// Diagnostics returns project diagnostics that must block compilation or
|
|
565
567
|
// runtime execution before any JavaScript is emitted or evaluated.
|
|
566
568
|
func (p *Program) Diagnostics() []Diagnostic {
|
|
569
|
+
return p.diagnostics(nil)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// DiagnosticsForFiles returns diagnostics whose semantic work is restricted to
|
|
573
|
+
// selected source files, plus the program/global diagnostics that qualify the
|
|
574
|
+
// same immutable Program generation. The resident graph shard producer uses it
|
|
575
|
+
// for the compiler-invalidated closure; callers that need the complete project
|
|
576
|
+
// continue to use Diagnostics.
|
|
577
|
+
func (p *Program) DiagnosticsForFiles(files []*ast.SourceFile) []Diagnostic {
|
|
578
|
+
return p.diagnostics(files)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
func (p *Program) diagnostics(files []*ast.SourceFile) []Diagnostic {
|
|
567
582
|
if p == nil || p.TSProgram == nil {
|
|
568
583
|
return []Diagnostic{{Message: "driver: nil program"}}
|
|
569
584
|
}
|
|
@@ -600,14 +615,28 @@ func (p *Program) Diagnostics() []Diagnostic {
|
|
|
600
615
|
})
|
|
601
616
|
}
|
|
602
617
|
ctx := context.Background()
|
|
603
|
-
raw
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
618
|
+
var raw []*ast.Diagnostic
|
|
619
|
+
if files == nil {
|
|
620
|
+
raw = shimcompiler.GetDiagnosticsOfAnyProgram(
|
|
621
|
+
ctx,
|
|
622
|
+
p.TSProgram,
|
|
623
|
+
nil,
|
|
624
|
+
false,
|
|
625
|
+
p.TSProgram.GetBindDiagnostics,
|
|
626
|
+
p.TSProgram.GetSemanticDiagnostics,
|
|
627
|
+
)
|
|
628
|
+
} else {
|
|
629
|
+
for _, file := range files {
|
|
630
|
+
raw = append(raw, shimcompiler.GetDiagnosticsOfAnyProgram(
|
|
631
|
+
ctx,
|
|
632
|
+
p.TSProgram,
|
|
633
|
+
file,
|
|
634
|
+
false,
|
|
635
|
+
p.TSProgram.GetBindDiagnostics,
|
|
636
|
+
p.TSProgram.GetSemanticDiagnostics,
|
|
637
|
+
)...)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
611
640
|
raw = filterDiagnostics(raw)
|
|
612
641
|
return append(out, convertDiagnostics(shimcompiler.SortAndDeduplicateDiagnostics(raw))...)
|
|
613
642
|
}
|
package/internal/graph/build.go
CHANGED
|
@@ -8,23 +8,53 @@ import (
|
|
|
8
8
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
// Build walks the program's
|
|
12
|
-
// each top-level declaration.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// graph only as the resolved target of an edge (see Resolve).
|
|
11
|
+
// Build walks the program's workspace source files and records a node for
|
|
12
|
+
// each top-level declaration. The checker can load a dependency's raw `.ts`
|
|
13
|
+
// entry, so IsWorkspaceSourceFile owns the declaration boundary instead of
|
|
14
|
+
// assuming every non-declaration source is authored here. External boundary
|
|
15
|
+
// leaves enter the graph only as the resolved target of an edge (see Resolve).
|
|
16
16
|
func Build(prog *driver.Program) *Graph {
|
|
17
|
+
return BuildFiles(prog, nil, nil)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// BuildFiles resolves graph facts owned by selected source files against one
|
|
21
|
+
// immutable Program. A nil selection preserves Build's complete-project
|
|
22
|
+
// behavior. A non-nil selection emits only replacement nodes and outgoing
|
|
23
|
+
// facts for those files while using baseNodes as a read-only endpoint index.
|
|
24
|
+
//
|
|
25
|
+
// This is the semantic primitive a resident shard store needs: an ordinary edit
|
|
26
|
+
// asks the new checker only about its invalidated closure instead of walking
|
|
27
|
+
// every declaration again, while cross-file targets retain the exact stable IDs
|
|
28
|
+
// established by the preceding committed generation.
|
|
29
|
+
func BuildFiles(prog *driver.Program, selected []string, baseNodes map[string]*Node) *Graph {
|
|
30
|
+
selectedFiles := map[string]bool{}
|
|
31
|
+
if selected != nil {
|
|
32
|
+
for _, file := range selected {
|
|
33
|
+
selectedFiles[file] = true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
g := &Graph{
|
|
18
|
-
Nodes:
|
|
19
|
-
bodyNodes:
|
|
20
|
-
seen:
|
|
21
|
-
resolved:
|
|
37
|
+
Nodes: map[string]*Node{},
|
|
38
|
+
bodyNodes: map[string]bool{},
|
|
39
|
+
seen: map[edgeKey]struct{}{},
|
|
40
|
+
resolved: map[*shimast.Node]*Target{},
|
|
41
|
+
edgeEvidenceFiles: map[string]string{},
|
|
42
|
+
baseNodes: baseNodes,
|
|
43
|
+
selectedFiles: selectedFiles,
|
|
44
|
+
ExportedTargets: map[string]bool{},
|
|
45
|
+
ImplementationSources: map[string]map[string]bool{},
|
|
22
46
|
}
|
|
23
47
|
for _, file := range prog.SourceFiles() {
|
|
48
|
+
if !IsWorkspaceSourceFile(file) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if selected != nil && !selectedFiles[file.FileName()] {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
24
54
|
g.putModuleNode(file)
|
|
25
55
|
collectDeclarations(g, file)
|
|
26
56
|
}
|
|
27
|
-
g.addEdges(prog)
|
|
57
|
+
g.addEdges(prog, selectedFiles, selected != nil)
|
|
28
58
|
return g
|
|
29
59
|
}
|
|
30
60
|
|
|
@@ -49,6 +79,28 @@ func SourceTexts(prog *driver.Program) map[string]string {
|
|
|
49
79
|
return out
|
|
50
80
|
}
|
|
51
81
|
|
|
82
|
+
// SourceTextsForFiles returns resident checker text only for the named source
|
|
83
|
+
// files. It is the partial-build counterpart of SourceTexts: selected graph
|
|
84
|
+
// shards still receive exact evidence and signatures, without walking or
|
|
85
|
+
// retaining every unchanged source body again.
|
|
86
|
+
func SourceTextsForFiles(prog *driver.Program, files []string) map[string]string {
|
|
87
|
+
if prog == nil || prog.TSProgram == nil {
|
|
88
|
+
return map[string]string{}
|
|
89
|
+
}
|
|
90
|
+
_ = prog.ApplyLinkedPlugins()
|
|
91
|
+
selected := make(map[string]bool, len(files))
|
|
92
|
+
for _, file := range files {
|
|
93
|
+
selected[file] = true
|
|
94
|
+
}
|
|
95
|
+
out := make(map[string]string, len(files))
|
|
96
|
+
for _, source := range prog.TSProgram.SourceFiles() {
|
|
97
|
+
if source != nil && selected[source.FileName()] {
|
|
98
|
+
out[source.FileName()] = source.Text()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
52
104
|
// collectDeclarations records a node for each declaration statement in file,
|
|
53
105
|
// plus a method node for each callable member of a class or interface, so
|
|
54
106
|
// method-to-method calls have both endpoints. It descends into namespace bodies
|