ttsc 0.35.0 → 0.36.1
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/driver/compilation_snapshot.go +267 -0
- package/driver/compilation_snapshot_keeps_inputs_across_stages_test.go +130 -0
- package/driver/compilation_snapshot_preserves_checked_emission_test.go +100 -0
- package/driver/graph.go +9 -3
- package/driver/program.go +45 -14
- package/lib/compiler/internal/CompilationSnapshot.d.ts +33 -0
- package/lib/compiler/internal/CompilationSnapshot.js +90 -0
- package/lib/compiler/internal/CompilationSnapshot.js.map +1 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +7 -4
- package/lib/compiler/internal/build/BuildExecution.js +68 -5
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/appendBuildOutput.js +4 -0
- package/lib/compiler/internal/build/appendBuildOutput.js.map +1 -1
- package/lib/compiler/internal/build/runExternalEmitProvenance.d.ts +17 -14
- package/lib/compiler/internal/build/runExternalEmitProvenance.js +71 -42
- package/lib/compiler/internal/build/runExternalEmitProvenance.js.map +1 -1
- package/lib/compiler/internal/parseNativeTransformOutput.js +4 -0
- package/lib/compiler/internal/parseNativeTransformOutput.js.map +1 -1
- package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.d.ts +2 -1
- package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js +4 -1
- package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -3
- package/lib/compiler/internal/transformProjectInMemory.js +29 -5
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/CompilationSnapshot.ts +97 -0
- package/src/compiler/internal/build/BuildExecution.ts +96 -4
- package/src/compiler/internal/build/appendBuildOutput.ts +4 -0
- package/src/compiler/internal/build/runExternalEmitProvenance.ts +95 -64
- package/src/compiler/internal/parseNativeTransformOutput.ts +5 -0
- package/src/compiler/internal/sharedHost/inheritedSidecarEnv.ts +8 -1
- package/src/compiler/internal/transformProjectInMemory.ts +46 -5
- package/src/structures/ITtscCompilerTransformation.ts +6 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/base64"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"sync"
|
|
10
|
+
"time"
|
|
11
|
+
"unicode/utf8"
|
|
12
|
+
|
|
13
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
14
|
+
"github.com/microsoft/typescript-go/shim/vfs"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// compilationSnapshotFS retains the first compiler text, identity and directory
|
|
18
|
+
// answers of one invocation. Sequential native check and transform stages share
|
|
19
|
+
// its private document; a new invocation receives a new document. This is a
|
|
20
|
+
// read-once compiler view, not an atomic snapshot of the whole physical volume.
|
|
21
|
+
// Writes remain with the underlying filesystem and cannot replace captured reads.
|
|
22
|
+
// The invocation owns the uncapped captured text and removes its document after
|
|
23
|
+
// all stages finish. The mutex serializes native compiler readers within a stage;
|
|
24
|
+
// the owning JS coordinator runs stages sequentially, never concurrent writers.
|
|
25
|
+
type compilationSnapshotFS struct {
|
|
26
|
+
vfs.FS
|
|
27
|
+
mu sync.Mutex
|
|
28
|
+
path string
|
|
29
|
+
data compilationSnapshotData
|
|
30
|
+
dirty bool
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type compilationSnapshotData struct {
|
|
34
|
+
Version int `json:"version"`
|
|
35
|
+
ID string `json:"id"`
|
|
36
|
+
Reads map[string]compilationSnapshotRead `json:"reads"`
|
|
37
|
+
Files map[string]bool `json:"files"`
|
|
38
|
+
Directories map[string]bool `json:"directories"`
|
|
39
|
+
Entries map[string]vfs.Entries `json:"entries"`
|
|
40
|
+
Realpaths map[string]string `json:"realpaths"`
|
|
41
|
+
Stats map[string]*compilationSnapshotInfo `json:"stats"`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type compilationSnapshotInfo struct {
|
|
45
|
+
Filename string `json:"name"`
|
|
46
|
+
Length int64 `json:"size"`
|
|
47
|
+
Bits os.FileMode `json:"mode"`
|
|
48
|
+
Modified time.Time `json:"modified"`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func (i *compilationSnapshotInfo) Name() string { return i.Filename }
|
|
52
|
+
func (i *compilationSnapshotInfo) Size() int64 { return i.Length }
|
|
53
|
+
func (i *compilationSnapshotInfo) Mode() os.FileMode { return i.Bits }
|
|
54
|
+
func (i *compilationSnapshotInfo) ModTime() time.Time { return i.Modified }
|
|
55
|
+
func (i *compilationSnapshotInfo) IsDir() bool { return i.Bits.IsDir() }
|
|
56
|
+
func (i *compilationSnapshotInfo) Sys() any { return nil }
|
|
57
|
+
|
|
58
|
+
type compilationSnapshotRead struct {
|
|
59
|
+
Text string `json:"text"`
|
|
60
|
+
OK bool `json:"ok"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// compilationSnapshotReadWire keeps normal UTF-8 readable and encodes only
|
|
64
|
+
// text JSON cannot represent losslessly. Decoding happens once when opening a
|
|
65
|
+
// stage, not on each cached compiler read.
|
|
66
|
+
type compilationSnapshotReadWire struct {
|
|
67
|
+
Text string `json:"text"`
|
|
68
|
+
OK bool `json:"ok"`
|
|
69
|
+
Encoding string `json:"encoding,omitempty"`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (r compilationSnapshotRead) MarshalJSON() ([]byte, error) {
|
|
73
|
+
value := compilationSnapshotReadWire{Text: r.Text, OK: r.OK}
|
|
74
|
+
if !utf8.ValidString(value.Text) {
|
|
75
|
+
value.Text = base64.StdEncoding.EncodeToString([]byte(value.Text))
|
|
76
|
+
value.Encoding = "base64"
|
|
77
|
+
}
|
|
78
|
+
return json.Marshal(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func (r *compilationSnapshotRead) UnmarshalJSON(bytes []byte) error {
|
|
82
|
+
var value compilationSnapshotReadWire
|
|
83
|
+
if err := json.Unmarshal(bytes, &value); err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
switch value.Encoding {
|
|
87
|
+
case "":
|
|
88
|
+
case "base64":
|
|
89
|
+
decoded, err := base64.StdEncoding.DecodeString(value.Text)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return fmt.Errorf("invalid compilation snapshot text: %w", err)
|
|
92
|
+
}
|
|
93
|
+
value.Text = string(decoded)
|
|
94
|
+
default:
|
|
95
|
+
return fmt.Errorf("unknown compilation snapshot text encoding %q", value.Encoding)
|
|
96
|
+
}
|
|
97
|
+
r.Text, r.OK = value.Text, value.OK
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// openCompilationSnapshot accepts only an invocation-owned versioned document.
|
|
102
|
+
// Invalid or unreadable transport is an error, never permission to read live
|
|
103
|
+
// inputs under another stage's snapshot identity.
|
|
104
|
+
func openCompilationSnapshot(inner vfs.FS, path string) (*compilationSnapshotFS, error) {
|
|
105
|
+
bytes, err := os.ReadFile(path)
|
|
106
|
+
if err != nil {
|
|
107
|
+
return nil, err
|
|
108
|
+
}
|
|
109
|
+
var data compilationSnapshotData
|
|
110
|
+
if err := json.Unmarshal(bytes, &data); err != nil {
|
|
111
|
+
return nil, err
|
|
112
|
+
}
|
|
113
|
+
if data.Version != 1 || data.ID == "" || data.Reads == nil || data.Files == nil || data.Directories == nil || data.Entries == nil || data.Realpaths == nil || data.Stats == nil {
|
|
114
|
+
return nil, fmt.Errorf("invalid compilation snapshot %q", path)
|
|
115
|
+
}
|
|
116
|
+
return &compilationSnapshotFS{FS: inner, path: path, data: data}, nil
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func (s *compilationSnapshotFS) key(path string) string {
|
|
120
|
+
return shimtspath.Canonicalize(vfs.CaseSensitivityOf(s.FS), shimtspath.NormalizePath(path))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (s *compilationSnapshotFS) realpath(path shimtspath.RootedPath) shimtspath.RootedPath {
|
|
124
|
+
key := s.key(string(path))
|
|
125
|
+
if value, ok := s.data.Realpaths[key]; ok {
|
|
126
|
+
return shimtspath.RootedPath(value)
|
|
127
|
+
}
|
|
128
|
+
value := s.FS.Realpath(path)
|
|
129
|
+
s.data.Realpaths[key] = string(value)
|
|
130
|
+
s.dirty = true
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (s *compilationSnapshotFS) Realpath(path shimtspath.RootedPath) shimtspath.RootedPath {
|
|
135
|
+
s.mu.Lock()
|
|
136
|
+
defer s.mu.Unlock()
|
|
137
|
+
return s.realpath(path)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func (s *compilationSnapshotFS) ReadFile(path shimtspath.RootedFilePath) (string, bool) {
|
|
141
|
+
s.mu.Lock()
|
|
142
|
+
defer s.mu.Unlock()
|
|
143
|
+
key := s.key(string(path))
|
|
144
|
+
if value, ok := s.data.Reads[key]; ok {
|
|
145
|
+
return value.Text, value.OK
|
|
146
|
+
}
|
|
147
|
+
physical := s.realpath(shimtspath.RootedPath(path))
|
|
148
|
+
physicalKey := s.key(string(physical))
|
|
149
|
+
if value, ok := s.data.Reads[physicalKey]; ok {
|
|
150
|
+
s.data.Reads[key] = value
|
|
151
|
+
s.dirty = true
|
|
152
|
+
return value.Text, value.OK
|
|
153
|
+
}
|
|
154
|
+
text, ok := s.FS.ReadFile(path)
|
|
155
|
+
value := compilationSnapshotRead{Text: text, OK: ok}
|
|
156
|
+
s.data.Reads[key] = value
|
|
157
|
+
s.dirty = true
|
|
158
|
+
if ok && filepath.IsAbs(string(physical)) {
|
|
159
|
+
s.data.Reads[physicalKey] = value
|
|
160
|
+
s.dirty = true
|
|
161
|
+
}
|
|
162
|
+
return text, ok
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func (s *compilationSnapshotFS) FileExists(path shimtspath.RootedFilePath) bool {
|
|
166
|
+
s.mu.Lock()
|
|
167
|
+
defer s.mu.Unlock()
|
|
168
|
+
key := s.key(string(path))
|
|
169
|
+
if value, ok := s.data.Files[key]; ok {
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
value := s.FS.FileExists(path)
|
|
173
|
+
s.data.Files[key] = value
|
|
174
|
+
s.dirty = true
|
|
175
|
+
if value {
|
|
176
|
+
s.realpath(shimtspath.RootedPath(path))
|
|
177
|
+
}
|
|
178
|
+
return value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (s *compilationSnapshotFS) DirectoryExists(path shimtspath.RootedDirectoryPath) bool {
|
|
182
|
+
s.mu.Lock()
|
|
183
|
+
defer s.mu.Unlock()
|
|
184
|
+
key := s.key(string(path))
|
|
185
|
+
if value, ok := s.data.Directories[key]; ok {
|
|
186
|
+
return value
|
|
187
|
+
}
|
|
188
|
+
value := s.FS.DirectoryExists(path)
|
|
189
|
+
s.data.Directories[key] = value
|
|
190
|
+
s.dirty = true
|
|
191
|
+
if value {
|
|
192
|
+
s.realpath(shimtspath.RootedPath(path))
|
|
193
|
+
}
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (s *compilationSnapshotFS) Stat(path shimtspath.RootedPath) vfs.FileInfo {
|
|
198
|
+
s.mu.Lock()
|
|
199
|
+
defer s.mu.Unlock()
|
|
200
|
+
key := s.key(string(path))
|
|
201
|
+
value, found := s.data.Stats[key]
|
|
202
|
+
if !found {
|
|
203
|
+
info := s.FS.Stat(path)
|
|
204
|
+
if info != nil {
|
|
205
|
+
value = &compilationSnapshotInfo{Filename: info.Name(), Length: info.Size(), Bits: info.Mode(), Modified: info.ModTime()}
|
|
206
|
+
}
|
|
207
|
+
s.data.Stats[key] = value
|
|
208
|
+
s.dirty = true
|
|
209
|
+
if info != nil {
|
|
210
|
+
s.realpath(path)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if value == nil {
|
|
214
|
+
return nil
|
|
215
|
+
}
|
|
216
|
+
return value
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func (s *compilationSnapshotFS) GetAccessibleEntries(path shimtspath.RootedDirectoryPath) vfs.Entries {
|
|
220
|
+
s.mu.Lock()
|
|
221
|
+
defer s.mu.Unlock()
|
|
222
|
+
key := s.key(string(path))
|
|
223
|
+
value, ok := s.data.Entries[key]
|
|
224
|
+
if !ok {
|
|
225
|
+
value = s.FS.GetAccessibleEntries(path)
|
|
226
|
+
value.Files = append([]string{}, value.Files...)
|
|
227
|
+
value.Directories = append([]string{}, value.Directories...)
|
|
228
|
+
s.data.Entries[key] = value
|
|
229
|
+
s.dirty = true
|
|
230
|
+
}
|
|
231
|
+
// The compiler owns its returned slices and may sort them in place.
|
|
232
|
+
value.Files = append([]string{}, value.Files...)
|
|
233
|
+
value.Directories = append([]string{}, value.Directories...)
|
|
234
|
+
if value.Symlinks != nil {
|
|
235
|
+
copied := make(map[string]struct{}, len(value.Symlinks))
|
|
236
|
+
for key := range value.Symlinks {
|
|
237
|
+
copied[key] = struct{}{}
|
|
238
|
+
}
|
|
239
|
+
value.Symlinks = copied
|
|
240
|
+
}
|
|
241
|
+
return value
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// save transfers actual read answers to the next sequential native stage. A
|
|
245
|
+
// complete temporary file is renamed over the private document; no reader is
|
|
246
|
+
// allowed to start until this stage returns. Failed publication leaves no new
|
|
247
|
+
// snapshot authority and the stage reports the native failure.
|
|
248
|
+
func (s *compilationSnapshotFS) save() error {
|
|
249
|
+
s.mu.Lock()
|
|
250
|
+
defer s.mu.Unlock()
|
|
251
|
+
if !s.dirty {
|
|
252
|
+
return nil
|
|
253
|
+
}
|
|
254
|
+
bytes, err := json.Marshal(s.data)
|
|
255
|
+
if err != nil {
|
|
256
|
+
return err
|
|
257
|
+
}
|
|
258
|
+
staging := s.path + ".next"
|
|
259
|
+
if err := os.WriteFile(staging, bytes, 0600); err != nil {
|
|
260
|
+
return err
|
|
261
|
+
}
|
|
262
|
+
if err := os.Rename(staging, s.path); err != nil {
|
|
263
|
+
return err
|
|
264
|
+
}
|
|
265
|
+
s.dirty = false
|
|
266
|
+
return nil
|
|
267
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"reflect"
|
|
7
|
+
"sync"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// TestCompilationSnapshotKeepsInputsAcrossStages verifies captured text,
|
|
14
|
+
// absence, identity and directory answers survive edits and stage handoff.
|
|
15
|
+
// A disk comparison cannot establish which bytes an earlier compiler used.
|
|
16
|
+
//
|
|
17
|
+
// 1. Read authored existing, empty and missing files and directory membership.
|
|
18
|
+
// 2. Edit and create those paths, then read concurrently and from a new stage.
|
|
19
|
+
// 3. Open a new invocation and require it to observe the edited inputs.
|
|
20
|
+
//
|
|
21
|
+
// @evidence contracts/testing.md#behavioral-verification The actual snapshot VFS serves old text and absence after disk mutation, persists those answers to another stage and admits new bytes only in a new invocation.
|
|
22
|
+
// @evidence contracts/testing.md#independent-expectations Literal BEFORE, AFTER, empty and missing states, authored Unicode/invalid byte text and the original directory listing define the oracle independently of snapshot serialization.
|
|
23
|
+
// @evidence contracts/testing.md#distinguishing-cases Existing, empty, Unicode plus invalid UTF-8 text, failed reads, missing existence, returned slice mutation, concurrent readers, sequential stage handoff and a new invocation exercise distinct read-once boundaries; malformed transport must fail.
|
|
24
|
+
// @evidence contracts/testing.md#execution-ownership This in-process driver Go unit calls its maintained VFS directly over a private temporary tree; it starts no compiler, installation or product host.
|
|
25
|
+
func TestCompilationSnapshotKeepsInputsAcrossStages(t *testing.T) {
|
|
26
|
+
root := t.TempDir()
|
|
27
|
+
source := filepath.Join(root, "source.ts")
|
|
28
|
+
empty := filepath.Join(root, "empty.ts")
|
|
29
|
+
encoded := filepath.Join(root, "encoded.ts")
|
|
30
|
+
encodedText := "// \xff\x80\nexport const value = \"한글😀\";\n"
|
|
31
|
+
missing := filepath.Join(root, "missing.ts")
|
|
32
|
+
write := func(file, text string) {
|
|
33
|
+
t.Helper()
|
|
34
|
+
if err := os.WriteFile(file, []byte(text), 0600); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
write(source, "BEFORE")
|
|
39
|
+
write(empty, "")
|
|
40
|
+
write(encoded, encodedText)
|
|
41
|
+
newDocument := func(id string) string {
|
|
42
|
+
t.Helper()
|
|
43
|
+
file := filepath.Join(t.TempDir(), "snapshot.json")
|
|
44
|
+
write(file, `{"version":1,"id":"`+id+`","reads":{},"files":{},"directories":{},"entries":{},"realpaths":{},"stats":{}}`)
|
|
45
|
+
return file
|
|
46
|
+
}
|
|
47
|
+
document := newDocument("first")
|
|
48
|
+
first, err := openCompilationSnapshot(DefaultFS(), document)
|
|
49
|
+
if err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
file := shimtspath.RootedFilePathFromAbsolute(source)
|
|
53
|
+
absent := shimtspath.RootedFilePathFromAbsolute(missing)
|
|
54
|
+
directory := shimtspath.RootedDirectoryPathFromAbsolute(root)
|
|
55
|
+
if text, ok := first.ReadFile(file); !ok || text != "BEFORE" {
|
|
56
|
+
t.Fatalf("first read: %q %v", text, ok)
|
|
57
|
+
}
|
|
58
|
+
if text, ok := first.ReadFile(shimtspath.RootedFilePathFromAbsolute(empty)); !ok || text != "" {
|
|
59
|
+
t.Fatalf("empty read: %q %v", text, ok)
|
|
60
|
+
}
|
|
61
|
+
if _, ok := first.ReadFile(absent); ok || first.FileExists(absent) {
|
|
62
|
+
t.Fatal("missing input was present")
|
|
63
|
+
}
|
|
64
|
+
if !first.DirectoryExists(directory) {
|
|
65
|
+
t.Fatal("source directory missing")
|
|
66
|
+
}
|
|
67
|
+
if text, ok := first.ReadFile(shimtspath.RootedFilePathFromAbsolute(encoded)); !ok || text != encodedText {
|
|
68
|
+
t.Fatalf("encoded first read: %q %v", text, ok)
|
|
69
|
+
}
|
|
70
|
+
physical := first.Realpath(shimtspath.RootedPath(shimtspath.RootedFilePathFromAbsolute(source)))
|
|
71
|
+
original := first.GetAccessibleEntries(directory)
|
|
72
|
+
expected := append([]string{}, original.Files...)
|
|
73
|
+
original.Files[0] = "caller mutation"
|
|
74
|
+
write(source, "AFTER")
|
|
75
|
+
write(empty, "FILLED")
|
|
76
|
+
write(encoded, "AFTER")
|
|
77
|
+
write(missing, "CREATED")
|
|
78
|
+
var readers sync.WaitGroup
|
|
79
|
+
for range 8 {
|
|
80
|
+
readers.Go(func() {
|
|
81
|
+
if text, ok := first.ReadFile(file); !ok || text != "BEFORE" {
|
|
82
|
+
t.Errorf("concurrent read: %q %v", text, ok)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
readers.Wait()
|
|
87
|
+
if err := first.save(); err != nil {
|
|
88
|
+
t.Fatal(err)
|
|
89
|
+
}
|
|
90
|
+
second, err := openCompilationSnapshot(DefaultFS(), document)
|
|
91
|
+
if err != nil {
|
|
92
|
+
t.Fatal(err)
|
|
93
|
+
}
|
|
94
|
+
if text, ok := second.ReadFile(file); !ok || text != "BEFORE" {
|
|
95
|
+
t.Fatalf("next stage: %q %v", text, ok)
|
|
96
|
+
}
|
|
97
|
+
if text, ok := second.ReadFile(shimtspath.RootedFilePathFromAbsolute(empty)); !ok || text != "" {
|
|
98
|
+
t.Fatalf("empty next stage: %q %v", text, ok)
|
|
99
|
+
}
|
|
100
|
+
if text, ok := second.ReadFile(shimtspath.RootedFilePathFromAbsolute(encoded)); !ok || text != encodedText {
|
|
101
|
+
t.Fatalf("encoded next stage: %q %v", text, ok)
|
|
102
|
+
}
|
|
103
|
+
if _, ok := second.ReadFile(absent); ok || second.FileExists(absent) {
|
|
104
|
+
t.Fatal("next stage admitted a created input")
|
|
105
|
+
}
|
|
106
|
+
if !reflect.DeepEqual(second.GetAccessibleEntries(directory).Files, expected) {
|
|
107
|
+
t.Fatal("directory answer changed")
|
|
108
|
+
}
|
|
109
|
+
if second.Realpath(shimtspath.RootedPath(shimtspath.RootedFilePathFromAbsolute(source))) != physical {
|
|
110
|
+
t.Fatal("physical source coordinate changed")
|
|
111
|
+
}
|
|
112
|
+
fresh, err := openCompilationSnapshot(DefaultFS(), newDocument("next"))
|
|
113
|
+
if err != nil {
|
|
114
|
+
t.Fatal(err)
|
|
115
|
+
}
|
|
116
|
+
if text, ok := fresh.ReadFile(file); !ok || text != "AFTER" {
|
|
117
|
+
t.Fatalf("new invocation: %q %v", text, ok)
|
|
118
|
+
}
|
|
119
|
+
if !fresh.FileExists(absent) {
|
|
120
|
+
t.Fatal("new invocation missed created source")
|
|
121
|
+
}
|
|
122
|
+
write(document, `{"version":2}`)
|
|
123
|
+
if _, err := openCompilationSnapshot(DefaultFS(), document); err == nil {
|
|
124
|
+
t.Fatal("invalid snapshot accepted")
|
|
125
|
+
}
|
|
126
|
+
write(document, "{")
|
|
127
|
+
if _, err := openCompilationSnapshot(DefaultFS(), document); err == nil {
|
|
128
|
+
t.Fatal("malformed snapshot accepted")
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
10
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// TestCompilationSnapshotPreservesCheckedEmission verifies consecutive compiler
|
|
14
|
+
// stages check and emit captured text after the physical source is edited.
|
|
15
|
+
// The next invocation must still reject the new ill-typed source.
|
|
16
|
+
//
|
|
17
|
+
// 1. Load and check an authored string assignment under a private snapshot.
|
|
18
|
+
// 2. Introduce an incompatible number, then load and emit the captured source.
|
|
19
|
+
// 3. Start a fresh snapshot and require the actual type error to be reported.
|
|
20
|
+
//
|
|
21
|
+
// @evidence contracts/testing.md#behavioral-verification Real driver loading, diagnostics, graph capture and emission share captured BEFORE text across two native Programs while a new invocation reports TS2322 for the changed disk source.
|
|
22
|
+
// @evidence contracts/testing.md#independent-expectations Authored string and number assignments define TypeScript assignability; literal BEFORE emission and diagnostic code 2322 do not come from implementation snapshots.
|
|
23
|
+
// @evidence contracts/testing.md#distinguishing-cases A same-invocation stage must preserve old successful emission while a new invocation must observe the adjacent type error; source graph identity and output provenance must remain populated.
|
|
24
|
+
// @evidence contracts/testing.md#execution-ownership This Go driver unit exercises linked compiler APIs in-process over private files, without a consumer installation, native build or spawned product host.
|
|
25
|
+
func TestCompilationSnapshotPreservesCheckedEmission(t *testing.T) {
|
|
26
|
+
root := t.TempDir()
|
|
27
|
+
source := filepath.Join(root, "source.ts")
|
|
28
|
+
config := filepath.Join(root, "tsconfig.json")
|
|
29
|
+
document := filepath.Join(t.TempDir(), "inputs.json")
|
|
30
|
+
write := func(file, text string) {
|
|
31
|
+
t.Helper()
|
|
32
|
+
if err := os.WriteFile(file, []byte(text), 0600); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
fresh := func(id string) {
|
|
37
|
+
write(document, `{"version":1,"id":"`+id+`","reads":{},"files":{},"directories":{},"entries":{},"realpaths":{},"stats":{}}`)
|
|
38
|
+
}
|
|
39
|
+
write(config, `{"compilerOptions":{"target":"es2020","module":"commonjs","types":[],"strict":true},"files":["source.ts"]}`)
|
|
40
|
+
write(source, `export const value: string = "BEFORE";`)
|
|
41
|
+
fresh("first")
|
|
42
|
+
t.Setenv("TTSC_COMPILATION_SNAPSHOT", document)
|
|
43
|
+
load := func() *Program {
|
|
44
|
+
t.Helper()
|
|
45
|
+
program, diagnostics, err := LoadProgram(root, config, LoadProgramOptions{})
|
|
46
|
+
if err != nil || program == nil || len(diagnostics) != 0 {
|
|
47
|
+
t.Fatalf("load: %v %v", err, diagnostics)
|
|
48
|
+
}
|
|
49
|
+
return program
|
|
50
|
+
}
|
|
51
|
+
first := load()
|
|
52
|
+
if diagnostics := first.Diagnostics(); len(diagnostics) != 0 {
|
|
53
|
+
t.Fatalf("first check: %v", diagnostics)
|
|
54
|
+
}
|
|
55
|
+
if err := first.Close(); err != nil {
|
|
56
|
+
t.Fatal(err)
|
|
57
|
+
}
|
|
58
|
+
write(source, `export const value: string = 123;`)
|
|
59
|
+
second := load()
|
|
60
|
+
defer second.Close()
|
|
61
|
+
if diagnostics := second.Diagnostics(); len(diagnostics) != 0 {
|
|
62
|
+
t.Fatalf("captured check: %v", diagnostics)
|
|
63
|
+
}
|
|
64
|
+
graph := NewTransformGraph(second, root)
|
|
65
|
+
if graph.CompilationSnapshot != "first" || len(graph.InputProofFailures) != 0 {
|
|
66
|
+
t.Fatalf("graph lost captured authority: %+v", graph)
|
|
67
|
+
}
|
|
68
|
+
var emitted strings.Builder
|
|
69
|
+
writer, provenance, err := second.NewEmitProvenanceRecorder(func(_ shimtspath.RootedFilePath, text string, _ *shimcompiler.WriteFileData) error {
|
|
70
|
+
emitted.WriteString(text)
|
|
71
|
+
return nil
|
|
72
|
+
})
|
|
73
|
+
if err != nil {
|
|
74
|
+
t.Fatal(err)
|
|
75
|
+
}
|
|
76
|
+
if _, diagnostics, err := second.EmitAll(NewRewriteSet(), writer); err != nil || len(diagnostics) != 0 {
|
|
77
|
+
t.Fatalf("emit: %v %v", err, diagnostics)
|
|
78
|
+
}
|
|
79
|
+
if !strings.Contains(emitted.String(), "BEFORE") || strings.Contains(emitted.String(), "123") {
|
|
80
|
+
t.Fatalf("emitted another input: %s", emitted.String())
|
|
81
|
+
}
|
|
82
|
+
if len(provenance()) == 0 {
|
|
83
|
+
t.Fatal("captured emission lost source ownership")
|
|
84
|
+
}
|
|
85
|
+
if err := second.Close(); err != nil {
|
|
86
|
+
t.Fatal(err)
|
|
87
|
+
}
|
|
88
|
+
fresh("next")
|
|
89
|
+
next := load()
|
|
90
|
+
defer next.Close()
|
|
91
|
+
found := false
|
|
92
|
+
for _, diagnostic := range next.Diagnostics() {
|
|
93
|
+
if diagnostic.Code == 2322 {
|
|
94
|
+
found = true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if !found {
|
|
98
|
+
t.Fatal("new invocation did not report the edited source's TS2322")
|
|
99
|
+
}
|
|
100
|
+
}
|
package/driver/graph.go
CHANGED
|
@@ -70,6 +70,9 @@ const bundledScheme = "bundled:///"
|
|
|
70
70
|
// @evidenceExclude contracts/performance.md#reuse-equivalent-work This schema carries proof without coordinating artifact reuse.
|
|
71
71
|
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The caller-owned value owns no resident cache or resource.
|
|
72
72
|
type TransformGraph struct {
|
|
73
|
+
// CompilationSnapshot identifies the shared read-once view of every native stage.
|
|
74
|
+
// It authorizes fresh delivery, not reuse after the disk changes.
|
|
75
|
+
CompilationSnapshot string `json:"compilationSnapshot,omitempty"`
|
|
73
76
|
// Membership is the effective native root-admission and output-exclusion policy.
|
|
74
77
|
Membership *TransformMembership `json:"membership,omitempty"`
|
|
75
78
|
// ContentMapperInputs are declared project dependencies, not observed reads.
|
|
@@ -93,14 +96,14 @@ type TransformGraph struct {
|
|
|
93
96
|
// `cmd/ttsc api-transform` and the linked-plugin utility host both do.
|
|
94
97
|
// Returns nil only for a nil or unloaded program.
|
|
95
98
|
//
|
|
96
|
-
// @evidence contracts/common.md#principled-implementation Compiler references, global scope, config ancestry
|
|
99
|
+
// @evidence contracts/common.md#principled-implementation Compiler references, global scope, config ancestry and replayed predicates form one generation's dependency proof; a shared snapshot identity is attached only after its captured answers are published.
|
|
97
100
|
// @evidence contracts/common.md#clear-and-simple-design Reference collection, resolution replay, and proof attachment share one output-key owner.
|
|
98
101
|
// @evidence contracts/common.md#prohibited-implementation-shortcuts Unobserved or contradictory inputs remain failures rather than later filesystem snapshots substituted for compiler reads.
|
|
99
102
|
// @evidence contracts/common.md#meaningful-documentation Native prose specifies envelope integration and unloaded-program behavior following the documentation skill.
|
|
100
103
|
// @evidence contracts/portability.md#os-neutral-implementation Keys use native filepath conversion and compiler case policy; identity metadata comes from reported adapter realpaths, whose native VFS error fallback can retain lexical spelling rather than certifying physical resolution.
|
|
101
|
-
// @evidence contracts/performance.md#efficient-algorithms Direct adjacency avoids computing every transitive closure. Construction scans resident sources/references, converts path text and sorts per-source targets, globals and resolver inputs; a distinct-input set selects proof attachment, whose rich and legacy projections can perform two observer lookups. Resolver task sorting, native replay reads/metadata/enumeration and observation merging remain part of this call's delegated cost;
|
|
104
|
+
// @evidence contracts/performance.md#efficient-algorithms Direct adjacency avoids computing every transitive closure. Construction scans resident sources/references, converts path text and sorts per-source targets, globals and resolver inputs; a distinct-input set selects proof attachment, whose rich and legacy projections can perform two observer lookups. Resolver task sorting, native replay reads/metadata/enumeration and observation merging remain part of this call's delegated cost; dirty snapshot publication additionally encodes and writes the complete captured text, paths and predicate population. No work or byte ceiling is imposed here.
|
|
102
105
|
// @evidence contracts/performance.md#reuse-equivalent-work Final proof projection reuses the current Program observer, including its exact physical-target read for a lexical alias; it does not replace missing compiler evidence with a new read. Resolver replay separately performs native queries and merges contradictions into that generation. No prior graph or later filesystem generation is reused by this constructor.
|
|
103
|
-
// @evidence contracts/performance.md#bound-retention-and-release-resources Temporary adjacency/input/proof maps grow with resident references, distinct paths and reported predicate bytes. Graph arrays/maps transfer to the caller, while replay observations merge into the caller-owned current Program observer. This call owns no resident historical graph cache or native handle, and supplies no population/byte ceiling or caller-lifetime disposal policy.
|
|
106
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Temporary adjacency/input/proof maps grow with resident references, distinct paths and reported predicate bytes. Graph arrays/maps transfer to the caller, while replay observations merge into the caller-owned current Program observer. Snapshot encoding adds a temporary complete document buffer, and a failed rename can leave invocation-owned staging for coordinator cleanup. This call owns no resident historical graph cache or persistent native handle, and supplies no population/byte ceiling or caller-lifetime disposal policy.
|
|
104
107
|
func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
|
|
105
108
|
if prog == nil || prog.TSProgram == nil {
|
|
106
109
|
return nil
|
|
@@ -154,6 +157,9 @@ func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
|
|
|
154
157
|
graph.InputProofFailures = resolution.Failures
|
|
155
158
|
}
|
|
156
159
|
graph.attachInputProof(prog, cwd)
|
|
160
|
+
if snapshot := prog.compilationSnapshot; snapshot != nil && snapshot.save() == nil {
|
|
161
|
+
graph.CompilationSnapshot = snapshot.data.ID
|
|
162
|
+
}
|
|
157
163
|
return graph
|
|
158
164
|
}
|
|
159
165
|
|
package/driver/program.go
CHANGED
|
@@ -2,6 +2,7 @@ package driver
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"errors"
|
|
5
6
|
"fmt"
|
|
6
7
|
"io"
|
|
7
8
|
"os"
|
|
@@ -329,8 +330,9 @@ type Program struct {
|
|
|
329
330
|
compilerArgumentInputs []string
|
|
330
331
|
|
|
331
332
|
// FS is the input-observing filesystem, including any source preamble layer.
|
|
332
|
-
FS
|
|
333
|
-
|
|
333
|
+
FS vfs.FS
|
|
334
|
+
compilationSnapshot *compilationSnapshotFS
|
|
335
|
+
inputObserver *inputObservationFS
|
|
334
336
|
|
|
335
337
|
// SourcePreamble is the injected text used when correcting authored positions.
|
|
336
338
|
SourcePreamble string
|
|
@@ -407,16 +409,16 @@ type LoadProgramOptions struct {
|
|
|
407
409
|
|
|
408
410
|
// Close releases the checker pool lease acquired by LoadProgram and, on the
|
|
409
411
|
// TypeScript 7.1 target, stops the project's content-mapper processes. Its
|
|
410
|
-
// error
|
|
412
|
+
// error joins content-mapper shutdown and compilation-snapshot publication errors.
|
|
411
413
|
//
|
|
412
|
-
// @evidence contracts/common.md#principled-implementation The
|
|
414
|
+
// @evidence contracts/common.md#principled-implementation The checker lease and project hosts are detached once; dirty snapshot answers are published through their detached invocation owner, and mapper and publication errors are joined without skipping mapper shutdown.
|
|
413
415
|
// @evidence contracts/common.md#clear-and-simple-design Checker and content-mapper ownership share one cleanup operation instead of independent releases by each consumer.
|
|
414
|
-
// @evidence contracts/common.md#prohibited-implementation-shortcuts Cleanup invokes the owned callbacks rather than patching the checker pool, and returns
|
|
416
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts Cleanup invokes the owned callbacks rather than patching the checker pool, and returns joined mapper/publication errors instead of fabricating success.
|
|
415
417
|
// @evidence contracts/common.md#meaningful-documentation Native prose names the lease and the mapper processes released and the error returned, following the documentation skill.
|
|
416
|
-
// @evidence contracts/portability.md#os-neutral-implementation Checker release is in-process;
|
|
417
|
-
// @
|
|
418
|
-
// @
|
|
419
|
-
// @evidence contracts/performance.md#bound-retention-and-release-resources Program owns
|
|
418
|
+
// @evidence contracts/portability.md#os-neutral-implementation Checker release is in-process; mapper shutdown uses the shim's os/exec boundary, while dirty snapshot publication uses native file writes and same-directory rename.
|
|
419
|
+
// @evidence contracts/performance.md#efficient-algorithms Release bookkeeping is fixed work; a dirty snapshot additionally serializes and writes its complete captured text, paths and predicates, with temporary encoded bytes proportional to that population.
|
|
420
|
+
// @evidence contracts/performance.md#reuse-equivalent-work Detaching publication ownership prevents repeated Close from overwriting a later stage; a clean snapshot avoids repeated serialization. This does not establish output-cache freshness.
|
|
421
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Program owns the checker lease, project hosts and one snapshot publication owner; Close detaches them once. The FS stack and caller aliases can still retain captured text and predicate maps until released; invocation storage cleanup belongs to the coordinator, and publication failure can leave staging for that cleanup.
|
|
420
422
|
func (p *Program) Close() error {
|
|
421
423
|
if p.checkerRelease != nil {
|
|
422
424
|
p.checkerRelease()
|
|
@@ -424,7 +426,12 @@ func (p *Program) Close() error {
|
|
|
424
426
|
}
|
|
425
427
|
hosts := p.hosts
|
|
426
428
|
p.hosts = nil
|
|
427
|
-
|
|
429
|
+
var snapshotError error
|
|
430
|
+
if snapshot := p.compilationSnapshot; snapshot != nil {
|
|
431
|
+
p.compilationSnapshot = nil
|
|
432
|
+
snapshotError = snapshot.save()
|
|
433
|
+
}
|
|
434
|
+
return errors.Join(hosts.Close(), snapshotError)
|
|
428
435
|
}
|
|
429
436
|
|
|
430
437
|
// ParseTSConfig parses a tsconfig.json file via tsgo's native JSONC parser.
|
|
@@ -590,15 +597,20 @@ func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
|
|
|
590
597
|
// Relative cwd is resolved against the process cwd when filepath.Abs succeeds;
|
|
591
598
|
// callers should provide an absolute anchor. tsconfigPath may be relative to it.
|
|
592
599
|
//
|
|
600
|
+
// An enabled private compilation-snapshot document shares first-read text and
|
|
601
|
+
// lookup answers across sequential native stages. This is not an atomic volume
|
|
602
|
+
// snapshot. Program owns dirty publication through Close; the invocation
|
|
603
|
+
// coordinator owns removal of the private transport storage.
|
|
604
|
+
//
|
|
593
605
|
// @evidence contracts/common.md#principled-implementation Plugin preambles enter before parsing, native CLI/config merging resolves options before Program creation, and the facade associates the observing VFS and borrowed checker with that generation; this is not a certificate of every filesystem access by plugins or their host.
|
|
594
606
|
// @evidence contracts/common.md#clear-and-simple-design One loader orders plugin config, filesystem layers, config overrides and checker acquisition; an optional failure sink transfers only the rejected load's observed recovery inputs.
|
|
595
607
|
// @evidence contracts/common.md#prohibited-implementation-shortcuts Invalid configuration or CLI input returns before construction; an explicitly empty argument list does not fall through to an ancestor's flags.
|
|
596
608
|
// @evidence contracts/common.md#meaningful-documentation Native prose and options document one-shot load, path anchoring, force overrides and checker ownership under documentation-skill guidance.
|
|
597
609
|
// @evidence contracts/portability.md#os-neutral-implementation Native cwd resolution uses filepath/tspath and the chosen VFS provides actual case and path capabilities; semantic wrapper ownership uses an explicit config path.
|
|
598
|
-
// @evidence contracts/performance.md#efficient-algorithms One load composes plugin/preamble work, config/CLI parsing, native Program processing and checker acquisition with VFS observation. A requested failed-load graph additionally snapshots, projects and sorts the observed predicate population and invokes the caller's sink. Read-text hashing runs on each observed read, including repeated reads; config/source bytes, native resolution and directory work, plugin callbacks and retained observation maps govern cost rather than only facade construction.
|
|
599
|
-
// @
|
|
600
|
-
// @evidence contracts/performance.md#bound-retention-and-release-resources Errors before checker acquisition return no lease; an optional synchronous sink receives call-owned recovery metadata once after observation starts. Successful loading transfers checker and mapper ownership plus input/plugin ledgers to Program; Close releases owned native resources
|
|
601
|
-
func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program, []Diagnostic, error) {
|
|
610
|
+
// @evidence contracts/performance.md#efficient-algorithms One load composes plugin/preamble work, config/CLI parsing, native Program processing and checker acquisition with VFS observation. A requested failed-load graph additionally snapshots, projects and sorts the observed predicate population and invokes the caller's sink. An enabled snapshot also parses the preceding full document and publishes its dirty complete captured population. Read-text hashing runs on each observed read, including repeated reads; config/source bytes, native resolution and directory work, plugin callbacks and retained observation maps govern cost rather than only facade construction.
|
|
611
|
+
// @evidence contracts/performance.md#reuse-equivalent-work An enabled private document shares first-read answers only across sequential stages of its invocation; fresh invocations start empty. Session and resident hosts retain ownership of cross-request Program reuse.
|
|
612
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Errors before checker acquisition return no lease; an optional synchronous sink receives call-owned recovery metadata once after observation starts. Successful loading transfers checker and mapper ownership plus input/plugin ledgers to Program; Close releases owned native resources and detaches snapshot publication; FS/caller reachability controls captured-memory lifetime, and the invocation coordinator removes transport storage.
|
|
613
|
+
func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (program *Program, diagnostics []Diagnostic, loadError error) {
|
|
602
614
|
if !filepath.IsAbs(cwd) {
|
|
603
615
|
if abs, err := filepath.Abs(cwd); err == nil {
|
|
604
616
|
cwd = abs
|
|
@@ -624,6 +636,24 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
|
|
|
624
636
|
if fs == nil {
|
|
625
637
|
fs = DefaultFS()
|
|
626
638
|
}
|
|
639
|
+
var snapshot *compilationSnapshotFS
|
|
640
|
+
if filename := os.Getenv("TTSC_COMPILATION_SNAPSHOT"); filename != "" {
|
|
641
|
+
var err error
|
|
642
|
+
snapshot, err = openCompilationSnapshot(fs, filename)
|
|
643
|
+
if err != nil {
|
|
644
|
+
return nil, nil, fmt.Errorf("compilation snapshot: %w", err)
|
|
645
|
+
}
|
|
646
|
+
fs = snapshot
|
|
647
|
+
defer func() {
|
|
648
|
+
if err := snapshot.save(); err != nil {
|
|
649
|
+
if program != nil {
|
|
650
|
+
program.Close()
|
|
651
|
+
program = nil
|
|
652
|
+
}
|
|
653
|
+
loadError = errors.Join(loadError, fmt.Errorf("compilation snapshot publication: %w", err))
|
|
654
|
+
}
|
|
655
|
+
}()
|
|
656
|
+
}
|
|
627
657
|
inputObserver := newInputObservationFS(fs)
|
|
628
658
|
loaded := false
|
|
629
659
|
if options.OnFailureInputs != nil {
|
|
@@ -716,6 +746,7 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
|
|
|
716
746
|
mapperDependencyError: mapperDependencyError,
|
|
717
747
|
compilerArgumentInputs: compilerArgumentInputs,
|
|
718
748
|
FS: fs,
|
|
749
|
+
compilationSnapshot: snapshot,
|
|
719
750
|
inputObserver: inputObserver,
|
|
720
751
|
SourcePreamble: options.SourcePreamble,
|
|
721
752
|
}
|