ttsc 0.26.2 → 0.27.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/README.md +9 -0
- package/cmd/ttsc/api_transform.go +17 -3
- package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
- package/cmd/ttscgraph/disk_realpath_other.go +16 -0
- package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
- package/cmd/ttscgraph/serve.go +104 -35
- package/driver/emit_plugin.go +20 -3
- package/driver/graph.go +64 -11
- package/driver/input_observation.go +273 -0
- package/driver/input_observation_test.go +149 -0
- package/driver/plugins.go +384 -4
- package/driver/program.go +54 -1
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +45 -2
- package/lib/TtscCompiler.d.ts +12 -11
- package/lib/TtscCompiler.js +44 -29
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.js +2 -2
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +4 -3
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +1 -1
- package/lib/compiler/internal/runBuild.js +80 -34
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +2 -2
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
- package/lib/compiler/internal/sharedHostHelpers.js +39 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +12 -4
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
- package/lib/compiler/internal/transformProjectInMemory.js +239 -35
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/schema.js +12 -5
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
- package/lib/internal/createCanonicalTempDirectory.js +42 -0
- package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
- package/lib/internal/projectInputPathIdentity.d.ts +3 -0
- package/lib/internal/projectInputPathIdentity.js +11 -5
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/resolveNodeBinary.d.ts +14 -0
- package/lib/internal/resolveNodeBinary.js +150 -0
- package/lib/internal/resolveNodeBinary.js.map +1 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.d.ts +3 -0
- package/lib/launcher/internal/prepareExecution.js +42 -14
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +25 -13
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +18 -3
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +2 -0
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
- package/lib/launcher/internal/runtimeHooks.js +653 -62
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
- package/lib/plugin/internal/buildSourcePlugin.js +754 -72
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
- package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/register.d.ts +1 -0
- package/lib/register.js +46 -0
- package/lib/register.js.map +1 -0
- package/lib/structures/ITtscCompilerContext.d.ts +6 -3
- package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +75 -0
- package/shim/compiler/incremental.go +45 -8
- package/src/TtscCompiler.ts +55 -33
- package/src/compiler/internal/captureProcessOutput.ts +3 -2
- package/src/compiler/internal/compileProjectInMemory.ts +4 -3
- package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
- package/src/compiler/internal/runBuild.ts +102 -39
- package/src/compiler/internal/runSingleFileEmit.ts +2 -2
- package/src/compiler/internal/sharedHostHelpers.ts +44 -0
- package/src/compiler/internal/startResidentTransform.ts +12 -3
- package/src/compiler/internal/transformProjectInMemory.ts +410 -39
- package/src/flags/schema.ts +12 -5
- package/src/internal/createCanonicalTempDirectory.ts +53 -0
- package/src/internal/projectInputPathIdentity.ts +17 -4
- package/src/internal/resolveNodeBinary.ts +173 -0
- package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
- package/src/launcher/internal/prepareExecution.ts +50 -14
- package/src/launcher/internal/runTtsc.ts +24 -13
- package/src/launcher/internal/runTtscserver.ts +18 -3
- package/src/launcher/internal/runTtsx.ts +2 -0
- package/src/launcher/internal/runtimeHooks.ts +823 -66
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +39 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +9 -3
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
- package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
- package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
package/driver/plugins.go
CHANGED
|
@@ -5,7 +5,9 @@ import (
|
|
|
5
5
|
"fmt"
|
|
6
6
|
"os"
|
|
7
7
|
"path/filepath"
|
|
8
|
+
"sort"
|
|
8
9
|
"strings"
|
|
10
|
+
"sync"
|
|
9
11
|
)
|
|
10
12
|
|
|
11
13
|
// LinkedPluginsEnv is the environment variable ttsc sets to pass the JSON
|
|
@@ -27,6 +29,44 @@ const LinkedPluginsEnv = "TTSC_LINKED_PLUGINS_JSON"
|
|
|
27
29
|
// still receive it.
|
|
28
30
|
const PluginConfigDirEnv = "TTSC_PLUGIN_CONFIG_DIR"
|
|
29
31
|
|
|
32
|
+
// TsgoArgsEnv is the environment variable through which the ttsc launcher
|
|
33
|
+
// passes the tsgo CLI flags it did not consume itself (`--strict`,
|
|
34
|
+
// `--declaration`, the single-file lane's output containment, …) to a native
|
|
35
|
+
// sidecar. The value is a JSON array of argv tokens; an empty or absent value
|
|
36
|
+
// means nothing was forwarded.
|
|
37
|
+
//
|
|
38
|
+
// It rides the environment for the same reason PluginConfigDirEnv does, only
|
|
39
|
+
// with sharper consequences. The payload used to travel as a `--tsgo-args`
|
|
40
|
+
// flag, which #113 appended to a plugin protocol third-party hosts had already
|
|
41
|
+
// frozen. A Go `flag.FlagSet` created with `flag.ContinueOnError` treats an
|
|
42
|
+
// undeclared flag as fatal, so every sidecar built before #113 answered
|
|
43
|
+
// `flag provided but not defined: -tsgo-args` and exited 2 — including on
|
|
44
|
+
// `ttsc <file.ts>`, where the launcher forwards its own output-containment
|
|
45
|
+
// flags and the user passed nothing at all (issue #1188). An unknown
|
|
46
|
+
// environment variable is inert to every host; an unknown flag is fatal to all
|
|
47
|
+
// of them. ttsc's own hosts still accept `--tsgo-args` so an older launcher
|
|
48
|
+
// paired with a newer host keeps working.
|
|
49
|
+
const TsgoArgsEnv = "TTSC_TSGO_ARGS"
|
|
50
|
+
|
|
51
|
+
// TsgoArgsFromEnv decodes the forwarded tsgo argv the launcher published in
|
|
52
|
+
// TsgoArgsEnv. An absent or whitespace-only value yields a nil slice and no
|
|
53
|
+
// error, so a host can call this unconditionally.
|
|
54
|
+
//
|
|
55
|
+
// Hosts that also declare a `--tsgo-args` flag should prefer the explicit flag
|
|
56
|
+
// value and fall back to this; LoadProgram already does that for every
|
|
57
|
+
// driver-based host.
|
|
58
|
+
func TsgoArgsFromEnv() ([]string, error) {
|
|
59
|
+
raw := strings.TrimSpace(os.Getenv(TsgoArgsEnv))
|
|
60
|
+
if raw == "" {
|
|
61
|
+
return nil, nil
|
|
62
|
+
}
|
|
63
|
+
var args []string
|
|
64
|
+
if err := json.Unmarshal([]byte(raw), &args); err != nil {
|
|
65
|
+
return nil, fmt.Errorf("ttsc driver: invalid %s: %w", TsgoArgsEnv, err)
|
|
66
|
+
}
|
|
67
|
+
return args, nil
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
// PluginConfigBaseDir returns the directory where a plugin anchors its
|
|
31
71
|
// config-file discovery walk and resolves relative "configFile" paths.
|
|
32
72
|
// The explicit PluginConfigDirEnv channel wins when set; otherwise the
|
|
@@ -60,6 +100,90 @@ type PluginContext struct {
|
|
|
60
100
|
Cwd string
|
|
61
101
|
Entry PluginEntry
|
|
62
102
|
Tsconfig string
|
|
103
|
+
|
|
104
|
+
reportHostInput func(string)
|
|
105
|
+
reportHostInputHash func(string, *string)
|
|
106
|
+
reportHostInputHashUnknown func(string)
|
|
107
|
+
reportHostInputRealpath func(string, *string)
|
|
108
|
+
reportHostInputRealpathUnknown func(string)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ReportHostInputHash declares the exact file state consumed by a native
|
|
112
|
+
// plugin. hash is a lowercase SHA-256 digest for an observed file and nil for
|
|
113
|
+
// a missing candidate. Conflicting observations are retained as host inputs
|
|
114
|
+
// but omitted from PluginHostInputHashes, forcing persistent adapters to
|
|
115
|
+
// decline narrow reuse without failing the transform.
|
|
116
|
+
func (ctx PluginContext) ReportHostInputHash(file string, hash *string) {
|
|
117
|
+
if ctx.reportHostInputHash == nil || strings.TrimSpace(file) == "" {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if !filepath.IsAbs(file) {
|
|
121
|
+
file = filepath.Join(ctx.Cwd, file)
|
|
122
|
+
}
|
|
123
|
+
file = filepath.Clean(file)
|
|
124
|
+
if hash != nil && !isLowerSHA256(*hash) {
|
|
125
|
+
if ctx.reportHostInputHashUnknown != nil {
|
|
126
|
+
ctx.reportHostInputHashUnknown(file)
|
|
127
|
+
} else if ctx.reportHostInput != nil {
|
|
128
|
+
ctx.reportHostInput(file)
|
|
129
|
+
}
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
ctx.reportHostInputHash(file, hash)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ReportHostInputRealpath declares the physical path resolved while a native
|
|
136
|
+
// plugin consumed file. realpath is absolute for an observed path and nil for
|
|
137
|
+
// a missing candidate. Conflicting observations remain host inputs but are
|
|
138
|
+
// omitted from PluginHostInputRealpaths so adapters cannot attach an earlier
|
|
139
|
+
// result to a retargeted symlink or junction.
|
|
140
|
+
func (ctx PluginContext) ReportHostInputRealpath(file string, realpath *string) {
|
|
141
|
+
if ctx.reportHostInputRealpath == nil || strings.TrimSpace(file) == "" {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if !filepath.IsAbs(file) {
|
|
145
|
+
file = filepath.Join(ctx.Cwd, file)
|
|
146
|
+
}
|
|
147
|
+
file = filepath.Clean(file)
|
|
148
|
+
if realpath != nil {
|
|
149
|
+
if strings.TrimSpace(*realpath) == "" || !filepath.IsAbs(*realpath) {
|
|
150
|
+
if ctx.reportHostInputRealpathUnknown != nil {
|
|
151
|
+
ctx.reportHostInputRealpathUnknown(file)
|
|
152
|
+
} else if ctx.reportHostInput != nil {
|
|
153
|
+
ctx.reportHostInput(file)
|
|
154
|
+
}
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
resolved := filepath.Clean(*realpath)
|
|
158
|
+
realpath = &resolved
|
|
159
|
+
}
|
|
160
|
+
ctx.reportHostInputRealpath(file, realpath)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func isLowerSHA256(value string) bool {
|
|
164
|
+
if len(value) != 64 {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
for _, char := range value {
|
|
168
|
+
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ReportHostInput declares an absolute file whose content or presence was
|
|
176
|
+
// consumed while the native plugin evaluated configuration. Native transform
|
|
177
|
+
// envelopes expose the generation-wide union so persistent hosts can invalidate
|
|
178
|
+
// without re-evaluating plugin config on the JavaScript side.
|
|
179
|
+
func (ctx PluginContext) ReportHostInput(file string) {
|
|
180
|
+
if ctx.reportHostInput == nil || strings.TrimSpace(file) == "" {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
if !filepath.IsAbs(file) {
|
|
184
|
+
file = filepath.Join(ctx.Cwd, file)
|
|
185
|
+
}
|
|
186
|
+
ctx.reportHostInput(filepath.Clean(file))
|
|
63
187
|
}
|
|
64
188
|
|
|
65
189
|
// SourcePreamblePlugin can inject source text before TypeScript-Go parses the
|
|
@@ -92,9 +216,244 @@ type EmitTransformPlugin interface {
|
|
|
92
216
|
type linkedPluginState struct {
|
|
93
217
|
cwd string
|
|
94
218
|
entries []PluginEntry
|
|
219
|
+
inputs *pluginHostInputScopes
|
|
95
220
|
tsconfig string
|
|
96
221
|
}
|
|
97
222
|
|
|
223
|
+
// pluginHostInputScopes keeps one observation set per plugin hook. A hash from
|
|
224
|
+
// one hook cannot prove another hook's same-file dependency when that hook only
|
|
225
|
+
// reported the path, while ReportHostInput followed by ReportHostInputHash in
|
|
226
|
+
// one hook remains one complete observation.
|
|
227
|
+
type pluginHostInputScopes struct {
|
|
228
|
+
mu sync.Mutex
|
|
229
|
+
scopes []*pluginHostInputs
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
type pluginHostInputs struct {
|
|
233
|
+
files map[string]struct{}
|
|
234
|
+
hashes map[string]pluginHostInputHash
|
|
235
|
+
realpaths map[string]pluginHostInputHash
|
|
236
|
+
mu sync.Mutex
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type pluginHostInputHash struct {
|
|
240
|
+
hash *string
|
|
241
|
+
known bool
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
func newPluginHostInputScopes() *pluginHostInputScopes {
|
|
245
|
+
return &pluginHostInputScopes{}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func newPluginHostInputs() *pluginHostInputs {
|
|
249
|
+
return &pluginHostInputs{
|
|
250
|
+
files: map[string]struct{}{},
|
|
251
|
+
hashes: map[string]pluginHostInputHash{},
|
|
252
|
+
realpaths: map[string]pluginHostInputHash{},
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func (inputs *pluginHostInputScopes) newScope() *pluginHostInputs {
|
|
257
|
+
scope := newPluginHostInputs()
|
|
258
|
+
if inputs == nil {
|
|
259
|
+
return scope
|
|
260
|
+
}
|
|
261
|
+
inputs.mu.Lock()
|
|
262
|
+
inputs.scopes = append(inputs.scopes, scope)
|
|
263
|
+
inputs.mu.Unlock()
|
|
264
|
+
return scope
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func (inputs *pluginHostInputScopes) snapshot() []*pluginHostInputs {
|
|
268
|
+
if inputs == nil {
|
|
269
|
+
return nil
|
|
270
|
+
}
|
|
271
|
+
inputs.mu.Lock()
|
|
272
|
+
defer inputs.mu.Unlock()
|
|
273
|
+
return append([]*pluginHostInputs(nil), inputs.scopes...)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func (inputs *pluginHostInputs) addHash(file string, hash *string) {
|
|
277
|
+
if inputs == nil {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
inputs.mu.Lock()
|
|
281
|
+
defer inputs.mu.Unlock()
|
|
282
|
+
inputs.files[file] = struct{}{}
|
|
283
|
+
previous, exists := inputs.hashes[file]
|
|
284
|
+
if !exists {
|
|
285
|
+
inputs.hashes[file] = pluginHostInputHash{hash: cloneStringPointer(hash), known: true}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
if !previous.known || !sameStringPointer(previous.hash, hash) {
|
|
289
|
+
inputs.hashes[file] = pluginHostInputHash{known: false}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func (inputs *pluginHostInputs) invalidateHash(file string) {
|
|
294
|
+
if inputs == nil {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
inputs.mu.Lock()
|
|
298
|
+
defer inputs.mu.Unlock()
|
|
299
|
+
inputs.files[file] = struct{}{}
|
|
300
|
+
inputs.hashes[file] = pluginHostInputHash{known: false}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func (inputs *pluginHostInputs) addRealpath(file string, realpath *string) {
|
|
304
|
+
if inputs == nil {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
inputs.mu.Lock()
|
|
308
|
+
defer inputs.mu.Unlock()
|
|
309
|
+
inputs.files[file] = struct{}{}
|
|
310
|
+
previous, exists := inputs.realpaths[file]
|
|
311
|
+
if !exists {
|
|
312
|
+
inputs.realpaths[file] = pluginHostInputHash{hash: cloneStringPointer(realpath), known: true}
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if !previous.known || !sameStringPointer(previous.hash, realpath) {
|
|
316
|
+
inputs.realpaths[file] = pluginHostInputHash{known: false}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func (inputs *pluginHostInputs) invalidateRealpath(file string) {
|
|
321
|
+
if inputs == nil {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
inputs.mu.Lock()
|
|
325
|
+
defer inputs.mu.Unlock()
|
|
326
|
+
inputs.files[file] = struct{}{}
|
|
327
|
+
inputs.realpaths[file] = pluginHostInputHash{known: false}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func cloneStringPointer(value *string) *string {
|
|
331
|
+
if value == nil {
|
|
332
|
+
return nil
|
|
333
|
+
}
|
|
334
|
+
cloned := *value
|
|
335
|
+
return &cloned
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func sameStringPointer(left, right *string) bool {
|
|
339
|
+
if left == nil || right == nil {
|
|
340
|
+
return left == nil && right == nil
|
|
341
|
+
}
|
|
342
|
+
return *left == *right
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func (inputs *pluginHostInputs) add(file string) {
|
|
346
|
+
if inputs == nil {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
inputs.mu.Lock()
|
|
350
|
+
inputs.files[file] = struct{}{}
|
|
351
|
+
inputs.mu.Unlock()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func (inputs *pluginHostInputs) snapshot() (map[string]struct{}, map[string]pluginHostInputHash, map[string]pluginHostInputHash) {
|
|
355
|
+
if inputs == nil {
|
|
356
|
+
return nil, nil, nil
|
|
357
|
+
}
|
|
358
|
+
inputs.mu.Lock()
|
|
359
|
+
defer inputs.mu.Unlock()
|
|
360
|
+
files := make(map[string]struct{}, len(inputs.files))
|
|
361
|
+
for file := range inputs.files {
|
|
362
|
+
files[file] = struct{}{}
|
|
363
|
+
}
|
|
364
|
+
hashes := make(map[string]pluginHostInputHash, len(inputs.hashes))
|
|
365
|
+
for file, observation := range inputs.hashes {
|
|
366
|
+
hashes[file] = pluginHostInputHash{
|
|
367
|
+
hash: cloneStringPointer(observation.hash),
|
|
368
|
+
known: observation.known,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
realpaths := make(map[string]pluginHostInputHash, len(inputs.realpaths))
|
|
372
|
+
for file, observation := range inputs.realpaths {
|
|
373
|
+
realpaths[file] = pluginHostInputHash{
|
|
374
|
+
hash: cloneStringPointer(observation.hash),
|
|
375
|
+
known: observation.known,
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return files, hashes, realpaths
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
func (inputs *pluginHostInputScopes) list() []string {
|
|
382
|
+
union := map[string]struct{}{}
|
|
383
|
+
for _, scope := range inputs.snapshot() {
|
|
384
|
+
files, _, _ := scope.snapshot()
|
|
385
|
+
for file := range files {
|
|
386
|
+
union[file] = struct{}{}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
files := make([]string, 0, len(union))
|
|
390
|
+
for file := range union {
|
|
391
|
+
files = append(files, file)
|
|
392
|
+
}
|
|
393
|
+
sort.Strings(files)
|
|
394
|
+
return files
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
func (inputs *pluginHostInputScopes) hashList() map[string]*string {
|
|
398
|
+
combined := map[string]pluginHostInputHash{}
|
|
399
|
+
for _, scope := range inputs.snapshot() {
|
|
400
|
+
files, hashes, _ := scope.snapshot()
|
|
401
|
+
for file := range files {
|
|
402
|
+
observation, exists := hashes[file]
|
|
403
|
+
if !exists || !observation.known {
|
|
404
|
+
combined[file] = pluginHostInputHash{known: false}
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
previous, exists := combined[file]
|
|
408
|
+
if !exists {
|
|
409
|
+
combined[file] = observation
|
|
410
|
+
} else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
|
|
411
|
+
combined[file] = pluginHostInputHash{known: false}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
hashes := map[string]*string{}
|
|
416
|
+
for file, observation := range combined {
|
|
417
|
+
if observation.known {
|
|
418
|
+
hashes[file] = cloneStringPointer(observation.hash)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if len(hashes) == 0 {
|
|
422
|
+
return nil
|
|
423
|
+
}
|
|
424
|
+
return hashes
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func (inputs *pluginHostInputScopes) realpathList() map[string]*string {
|
|
428
|
+
combined := map[string]pluginHostInputHash{}
|
|
429
|
+
for _, scope := range inputs.snapshot() {
|
|
430
|
+
files, _, realpaths := scope.snapshot()
|
|
431
|
+
for file := range files {
|
|
432
|
+
observation, exists := realpaths[file]
|
|
433
|
+
if !exists || !observation.known {
|
|
434
|
+
combined[file] = pluginHostInputHash{known: false}
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
previous, exists := combined[file]
|
|
438
|
+
if !exists {
|
|
439
|
+
combined[file] = observation
|
|
440
|
+
} else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
|
|
441
|
+
combined[file] = pluginHostInputHash{known: false}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
realpaths := map[string]*string{}
|
|
446
|
+
for file, observation := range combined {
|
|
447
|
+
if observation.known {
|
|
448
|
+
realpaths[file] = cloneStringPointer(observation.hash)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if len(realpaths) == 0 {
|
|
452
|
+
return nil
|
|
453
|
+
}
|
|
454
|
+
return realpaths
|
|
455
|
+
}
|
|
456
|
+
|
|
98
457
|
var pluginRegistry []any
|
|
99
458
|
|
|
100
459
|
// RegisterPlugin registers a driver-level plugin implementation. Linked Go
|
|
@@ -113,7 +472,7 @@ func RegisterPlugin(plugin any) {
|
|
|
113
472
|
func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error) {
|
|
114
473
|
input := strings.TrimSpace(os.Getenv(LinkedPluginsEnv))
|
|
115
474
|
if input == "" {
|
|
116
|
-
return linkedPluginState{cwd: cwd, tsconfig: tsconfigPath}, nil
|
|
475
|
+
return linkedPluginState{cwd: cwd, inputs: newPluginHostInputScopes(), tsconfig: tsconfigPath}, nil
|
|
117
476
|
}
|
|
118
477
|
var entries []PluginEntry
|
|
119
478
|
if err := json.Unmarshal([]byte(input), &entries); err != nil {
|
|
@@ -122,6 +481,7 @@ func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error)
|
|
|
122
481
|
return linkedPluginState{
|
|
123
482
|
cwd: cwd,
|
|
124
483
|
entries: entries,
|
|
484
|
+
inputs: newPluginHostInputScopes(),
|
|
125
485
|
tsconfig: tsconfigPath,
|
|
126
486
|
}, nil
|
|
127
487
|
}
|
|
@@ -218,9 +578,29 @@ func registeredPlugin(index int) (any, bool) {
|
|
|
218
578
|
|
|
219
579
|
// context builds the PluginContext the driver passes to each plugin hook.
|
|
220
580
|
func (state linkedPluginState) context(entry PluginEntry) PluginContext {
|
|
581
|
+
inputs := state.inputs.newScope()
|
|
221
582
|
return PluginContext{
|
|
222
|
-
Cwd:
|
|
223
|
-
Entry:
|
|
224
|
-
Tsconfig:
|
|
583
|
+
Cwd: state.cwd,
|
|
584
|
+
Entry: entry,
|
|
585
|
+
Tsconfig: state.tsconfig,
|
|
586
|
+
reportHostInput: inputs.add,
|
|
587
|
+
reportHostInputHash: inputs.addHash,
|
|
588
|
+
reportHostInputHashUnknown: inputs.invalidateHash,
|
|
589
|
+
reportHostInputRealpath: inputs.addRealpath,
|
|
590
|
+
reportHostInputRealpathUnknown: inputs.invalidateRealpath,
|
|
225
591
|
}
|
|
226
592
|
}
|
|
593
|
+
|
|
594
|
+
// hostInputs returns the exact native configuration inputs reported in this
|
|
595
|
+
// generation.
|
|
596
|
+
func (state linkedPluginState) hostInputs() []string {
|
|
597
|
+
return state.inputs.list()
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
func (state linkedPluginState) hostInputHashes() map[string]*string {
|
|
601
|
+
return state.inputs.hashList()
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
func (state linkedPluginState) hostInputRealpaths() map[string]*string {
|
|
605
|
+
return state.inputs.realpathList()
|
|
606
|
+
}
|
package/driver/program.go
CHANGED
|
@@ -197,6 +197,7 @@ type Program struct {
|
|
|
197
197
|
checkerRelease func()
|
|
198
198
|
Host shimcompiler.CompilerHost
|
|
199
199
|
FS vfs.FS
|
|
200
|
+
inputObserver *inputObservationFS
|
|
200
201
|
SourcePreamble string
|
|
201
202
|
plugins linkedPluginState
|
|
202
203
|
pluginsApplied bool
|
|
@@ -263,6 +264,23 @@ func ParseTSConfig(fs vfs.FS, cwd, tsconfigPath string, host shimcompiler.Compil
|
|
|
263
264
|
return parsed, nil, nil
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
// resolveTsgoArgs picks the forwarded tsgo argv for this load: the caller's
|
|
268
|
+
// explicit LoadProgramOptions.TsgoArgs when it has one, otherwise whatever the
|
|
269
|
+
// launcher published in TsgoArgsEnv.
|
|
270
|
+
//
|
|
271
|
+
// The explicit value wins so a host that declares its own `--tsgo-args` flag
|
|
272
|
+
// (cmd/ttsc, the utility host, @ttsc/lint) keeps deciding for itself, and an
|
|
273
|
+
// embedder that deliberately passes an empty argv is not overridden by an
|
|
274
|
+
// environment variable an ancestor ttsc process happened to set. The fallback
|
|
275
|
+
// is what carries the payload into a third-party sidecar whose flag set does
|
|
276
|
+
// not declare `--tsgo-args` at all — see TsgoArgsEnv and issue #1188.
|
|
277
|
+
func resolveTsgoArgs(explicit []string) ([]string, error) {
|
|
278
|
+
if len(explicit) > 0 {
|
|
279
|
+
return explicit, nil
|
|
280
|
+
}
|
|
281
|
+
return TsgoArgsFromEnv()
|
|
282
|
+
}
|
|
283
|
+
|
|
266
284
|
// parseTsgoArgs runs forwarded tsgo CLI flags through TypeScript-Go's own
|
|
267
285
|
// command-line parser, yielding a CompilerOptions overlay ParseTSConfig merges
|
|
268
286
|
// over the tsconfig. This is how a plugin build — which constructs its Program
|
|
@@ -357,6 +375,8 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
|
|
|
357
375
|
if fs == nil {
|
|
358
376
|
fs = DefaultFS()
|
|
359
377
|
}
|
|
378
|
+
inputObserver := newInputObservationFS(fs)
|
|
379
|
+
fs = inputObserver
|
|
360
380
|
if options.SourcePreamble != "" {
|
|
361
381
|
fs = sourcePreambleFS{
|
|
362
382
|
FS: fs,
|
|
@@ -365,7 +385,11 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
|
|
|
365
385
|
}
|
|
366
386
|
host := DefaultHost(cwd, fs)
|
|
367
387
|
|
|
368
|
-
|
|
388
|
+
tsgoArgs, err := resolveTsgoArgs(options.TsgoArgs)
|
|
389
|
+
if err != nil {
|
|
390
|
+
return nil, nil, err
|
|
391
|
+
}
|
|
392
|
+
cliOptions, cliDiags, err := parseTsgoArgs(tsgoArgs, host)
|
|
369
393
|
if err != nil {
|
|
370
394
|
return nil, nil, err
|
|
371
395
|
}
|
|
@@ -401,6 +425,7 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
|
|
|
401
425
|
checkerRelease: done,
|
|
402
426
|
Host: host,
|
|
403
427
|
FS: fs,
|
|
428
|
+
inputObserver: inputObserver,
|
|
404
429
|
SourcePreamble: options.SourcePreamble,
|
|
405
430
|
}
|
|
406
431
|
prog.plugins = pluginState
|
|
@@ -563,6 +588,34 @@ func (p *Program) HasLinkedProgramPlugins() bool {
|
|
|
563
588
|
return p != nil && p.plugins.hasProgramPlugins()
|
|
564
589
|
}
|
|
565
590
|
|
|
591
|
+
// PluginHostInputs returns the generation-wide native configuration files
|
|
592
|
+
// reported by linked plugins while this Program was loaded or transformed.
|
|
593
|
+
func (p *Program) PluginHostInputs() []string {
|
|
594
|
+
if p == nil {
|
|
595
|
+
return nil
|
|
596
|
+
}
|
|
597
|
+
return p.plugins.hostInputs()
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// PluginHostInputHashes returns evaluation-time fingerprints for the subset of
|
|
601
|
+
// native host inputs whose exact state plugins reported without conflict.
|
|
602
|
+
func (p *Program) PluginHostInputHashes() map[string]*string {
|
|
603
|
+
if p == nil {
|
|
604
|
+
return nil
|
|
605
|
+
}
|
|
606
|
+
return p.plugins.hostInputHashes()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// PluginHostInputRealpaths returns evaluation-time physical identities for the
|
|
610
|
+
// subset of native host inputs whose symlink or junction target was observed
|
|
611
|
+
// without conflict.
|
|
612
|
+
func (p *Program) PluginHostInputRealpaths() map[string]*string {
|
|
613
|
+
if p == nil {
|
|
614
|
+
return nil
|
|
615
|
+
}
|
|
616
|
+
return p.plugins.hostInputRealpaths()
|
|
617
|
+
}
|
|
618
|
+
|
|
566
619
|
// Diagnostics returns project diagnostics that must block compilation or
|
|
567
620
|
// runtime execution before any JavaScript is emitted or evaluated.
|
|
568
621
|
func (p *Program) Diagnostics() []Diagnostic {
|
|
@@ -23,9 +23,10 @@ type ModuleResolutionContext struct {
|
|
|
23
23
|
|
|
24
24
|
// SupersedingModuleCandidates returns, per source-file envelope key, the
|
|
25
25
|
// resolution candidates strictly ahead of each module target the loaded
|
|
26
|
-
// program selected.
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// program selected. When the selected target is reported by physical path, it
|
|
27
|
+
// also retains that candidate's lexical spelling. A missing member or a link
|
|
28
|
+
// retarget can therefore become a freshness input without making the whole
|
|
29
|
+
// resolution search (including lower-priority paths) an invalidation input.
|
|
29
30
|
//
|
|
30
31
|
// Candidate enumeration is host-owned. It mirrors the resident graph session's
|
|
31
32
|
// speculation for relative modules, paths, rootDirs, package imports/exports,
|
|
@@ -75,7 +76,10 @@ func SupersedingModuleCandidates(prog *Program, cwd string) map[string][]string
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// ModuleResolutionPredecessors returns the paths in the host-owned candidate
|
|
78
|
-
// search that precede resolvedFileName.
|
|
79
|
+
// search that precede resolvedFileName. When the compiler reports the winner by
|
|
80
|
+
// physical realpath, the result also retains that selected candidate's lexical
|
|
81
|
+
// spelling: retargeting its symlink/junction can change resolution without
|
|
82
|
+
// changing the old or new target bytes. An unresolved specifier keeps the full
|
|
79
83
|
// candidate list through ModuleResolutionCandidates; a resolved specifier must
|
|
80
84
|
// not track a lower-priority path because creating it cannot alter resolution.
|
|
81
85
|
func ModuleResolutionPredecessors(
|
|
@@ -101,7 +105,11 @@ func ModuleResolutionPredecessors(
|
|
|
101
105
|
// spellings already agree pays nothing for it.
|
|
102
106
|
for index, candidate := range candidates {
|
|
103
107
|
if sameExistingPath(candidate, resolvedFileName) {
|
|
104
|
-
|
|
108
|
+
end := index
|
|
109
|
+
if !sameCandidatePath(candidate, resolvedFileName, caseSensitive) {
|
|
110
|
+
end++
|
|
111
|
+
}
|
|
112
|
+
return compactStringsInOrder(candidates[:end])
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
// Never widen freshness to candidates whose precedence relative to the
|
package/driver/rewrite.go
CHANGED
|
@@ -129,7 +129,7 @@ func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.Em
|
|
|
129
129
|
}
|
|
130
130
|
return DefaultWriteFile(fileName, text)
|
|
131
131
|
}
|
|
132
|
-
result := p.
|
|
132
|
+
result := p.emitProgram(shimcompiler.EmitOptions{
|
|
133
133
|
WriteFile: wf,
|
|
134
134
|
})
|
|
135
135
|
return result, p.convertProgramDiagnostics(result.Diagnostics), nil
|
|
@@ -197,13 +197,56 @@ func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcom
|
|
|
197
197
|
return DefaultWriteFile(fileName, patched)
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
result := p.
|
|
200
|
+
result := p.emitProgram(shimcompiler.EmitOptions{
|
|
201
201
|
TargetSourceFile: target,
|
|
202
202
|
WriteFile: wf,
|
|
203
203
|
})
|
|
204
204
|
return result, p.convertProgramDiagnostics(result.Diagnostics), nil
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// emitProgram runs one whole-program or single-file emit, taking tsgo's
|
|
208
|
+
// incremental lane when the resolved compiler options ask for build
|
|
209
|
+
// information.
|
|
210
|
+
//
|
|
211
|
+
// tsgo's own CLI branches the same way — `performIncrementalCompilation` vs
|
|
212
|
+
// `performCompilation`, on `CompilerOptions.IsIncremental()` — but it branches
|
|
213
|
+
// in `internal/execute`, which a host constructing its Program in-process never
|
|
214
|
+
// enters. ttsc always took the plain branch, so `incremental`, `composite`, and
|
|
215
|
+
// `tsBuildInfoFile` parsed, resolved, and then vanished: a plugin-carrying
|
|
216
|
+
// project emitted its JavaScript and no `.tsbuildinfo` at all (issue #1188).
|
|
217
|
+
// `driver/emit_containment.go` had already exempted `.tsbuildinfo` from the
|
|
218
|
+
// outDir guard for a write that could not happen.
|
|
219
|
+
//
|
|
220
|
+
// A single-file emit stays on the plain lane. Build information describes a
|
|
221
|
+
// whole program, and tsgo's incremental program returns early on a
|
|
222
|
+
// `TargetSourceFile` request without writing any, so routing it there would
|
|
223
|
+
// only add a snapshot computation nothing reads.
|
|
224
|
+
func (p *Program) emitProgram(options shimcompiler.EmitOptions) *shimcompiler.EmitResult {
|
|
225
|
+
ctx := context.Background()
|
|
226
|
+
if options.TargetSourceFile == nil && p.emitsBuildInfo() {
|
|
227
|
+
return shimcompiler.EmitFreshWithBuildInfo(ctx, p.TSProgram, options)
|
|
228
|
+
}
|
|
229
|
+
return p.TSProgram.Emit(ctx, options)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// emitsBuildInfo reports whether this program's resolved options ask tsgo to
|
|
233
|
+
// write build information.
|
|
234
|
+
//
|
|
235
|
+
// The question is delegated to `CompilerOptions.IsIncremental`, the same
|
|
236
|
+
// predicate tsgo's own CLI branches on, rather than restated as
|
|
237
|
+
// `Incremental || Composite` here: a second copy of the rule is a second thing
|
|
238
|
+
// to keep in step with upstream.
|
|
239
|
+
func (p *Program) emitsBuildInfo() bool {
|
|
240
|
+
if p == nil || p.TSProgram == nil {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
options := p.TSProgram.Options()
|
|
244
|
+
if options == nil {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
return options.IsIncremental()
|
|
248
|
+
}
|
|
249
|
+
|
|
207
250
|
// DefaultWriteFile is the default disk writer used when EmitAll's caller does not
|
|
208
251
|
// supply a custom WriteFile callback.
|
|
209
252
|
func DefaultWriteFile(fileName, text string) error {
|
package/lib/TtscCompiler.d.ts
CHANGED
|
@@ -48,15 +48,16 @@ export declare class TtscCompiler {
|
|
|
48
48
|
/**
|
|
49
49
|
* Remove compiled cache artifacts for this compiler instance.
|
|
50
50
|
*
|
|
51
|
-
* Removes
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* project-local caches. A user-provided `GOCACHE` is never removed.
|
|
51
|
+
* Removes an explicit `cacheDir` wholesale. Otherwise removes the plugin
|
|
52
|
+
* binary subdirectory, a safely identified ttsc-owned Go build cache
|
|
53
|
+
* (including an external `TTSC_GO_CACHE_DIR`), and the two legacy
|
|
54
|
+
* project-local caches. A user-provided `GOCACHE` is never removed. Every
|
|
55
|
+
* resolved deletion target is validated before the first removal. The cache
|
|
55
56
|
* location comes from this instance's `cacheDir` and environment
|
|
56
57
|
* (`TTSC_CACHE_DIR` / `TTSC_GO_CACHE_DIR`), defaulting to
|
|
57
|
-
* `<workspaceRoot>/node_modules/.cache/ttsc`.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* `<workspaceRoot>/node_modules/.cache/ttsc`. Any resolved cache target that
|
|
59
|
+
* equals or contains the project, or names a filesystem root, is rejected
|
|
60
|
+
* before any directory is removed.
|
|
60
61
|
*
|
|
61
62
|
* @returns Cache directories that were removed.
|
|
62
63
|
*/
|
|
@@ -107,10 +108,10 @@ export declare class TtscCompiler {
|
|
|
107
108
|
* The effective environment for this instance's source-plugin builds and
|
|
108
109
|
* clean targets: `context.env` merged over `process.env`, matching the
|
|
109
110
|
* documented {@link ITtscCompilerContext.env} contract that child compiler,
|
|
110
|
-
* native-plugin,
|
|
111
|
-
* fresh object so callers never mutate the shared
|
|
112
|
-
* `context.env` was supplied this is a plain copy of
|
|
113
|
-
* default behavior is unchanged.
|
|
111
|
+
* native-plugin, native-host, and isolated descriptor processes already
|
|
112
|
+
* receive. Returned as a fresh object so callers never mutate the shared
|
|
113
|
+
* `process.env`; when no `context.env` was supplied this is a plain copy of
|
|
114
|
+
* `process.env`, so CLI / default behavior is unchanged.
|
|
114
115
|
*/
|
|
115
116
|
private resolveEffectiveEnv;
|
|
116
117
|
}
|