ttsc 0.26.2 → 0.28.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/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +28 -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/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +357 -60
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +77 -11
- 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 +506 -15
- package/driver/program.go +54 -1
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +45 -2
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -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/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.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 +47 -3
- package/lib/launcher/internal/runtimeHooks.js +746 -64
- 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/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- 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/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- 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 +38 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +78 -0
- package/shim/compiler/emithook.go +10 -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/project/createNativeProjectContextArgs.ts +19 -4
- 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/index.ts +1 -0
- 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 +934 -68
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +46 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -2
- package/utility/host.go +18 -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/emit_plugin.go
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package driver
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
4
|
"errors"
|
|
6
5
|
"strings"
|
|
7
6
|
"sync"
|
|
@@ -97,13 +96,30 @@ func (p *Program) EmitLinkedTransforms(writeFile shimcompiler.WriteFile) ([]Diag
|
|
|
97
96
|
// because the emit context's update hook only records the original, it does not
|
|
98
97
|
// copy `DeclarationBase.Symbol`.
|
|
99
98
|
//
|
|
100
|
-
//
|
|
101
|
-
// MarkLinkedReferencesRecursively
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
99
|
+
// #201 added this against a real panic in the walk that
|
|
100
|
+
// MarkLinkedReferencesRecursively used to run over the transformed tree. That
|
|
101
|
+
// walk no longer happens here — the builtin chain is built from the parse tree
|
|
102
|
+
// below — so the original trigger is gone, and no probe reproduces a panic with
|
|
103
|
+
// this call stubbed out.
|
|
104
|
+
//
|
|
105
|
+
// Keep it anyway, because the remaining exposure is unproven rather than
|
|
106
|
+
// absent. tsgo's own defense against a plugin-built node is ast.IsParseTreeNode,
|
|
107
|
+
// which most EmitResolver reference methods test to bail out early — and a
|
|
108
|
+
// REBUILT container does not trip it. The emit context stamps
|
|
109
|
+
// NodeFlagsSynthesized when its factory creates a node, but ast.updateNode then
|
|
110
|
+
// ASSIGNS `updated.Flags = original.Flags` and clears the marker again, so a
|
|
111
|
+
// container carrying no symbol still answers "parse tree node" and is let
|
|
112
|
+
// through to checker name resolution. There resolveName reaches
|
|
113
|
+
// getSymbolOfDeclaration(container), an unguarded dereference for a class,
|
|
114
|
+
// class expression, or interface (enum and module are nil-checked upstream).
|
|
115
|
+
// What the probes could not settle is whether the builtin transformers ever
|
|
116
|
+
// actually hand a rebuilt container to one of those methods.
|
|
117
|
+
//
|
|
118
|
+
// Restoring the symbol from the original (the symbol object is shared and
|
|
119
|
+
// node-independent for lookup) makes that path resolve the way it would on the
|
|
120
|
+
// parse tree, at the cost of one walk per file. EmitContext.ParseNode is
|
|
121
|
+
// unaffected either way: it walks MostOriginal before testing the predicate, so
|
|
122
|
+
// it always lands on the genuine parse node.
|
|
107
123
|
func restoreOriginalDeclarationSymbols(ec *shimprinter.EmitContext, node *shimast.Node) {
|
|
108
124
|
if node == nil {
|
|
109
125
|
return
|
|
@@ -181,7 +197,39 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
|
|
|
181
197
|
}
|
|
182
198
|
shimast.SetParentInChildrenUnset(out.AsNode())
|
|
183
199
|
restoreOriginalDeclarationSymbols(ec, out.AsNode())
|
|
184
|
-
|
|
200
|
+
// Build the chain from the PARSE tree and transform the plugin's tree.
|
|
201
|
+
// Upstream passes one file to both roles only because it has no plugin
|
|
202
|
+
// pass between them: emitJSFile hands getScriptTransformers the file it
|
|
203
|
+
// parsed, then reassigns it to the transform result. Here they differ.
|
|
204
|
+
//
|
|
205
|
+
// getScriptTransformers reads its file for three things: in-JS-file, JSX
|
|
206
|
+
// language variant, and emitResolver.MarkLinkedReferencesRecursively. The
|
|
207
|
+
// first two survive UpdateSourceFile, so marking is the whole difference,
|
|
208
|
+
// and marking is a per-node checker resolution walk under the single
|
|
209
|
+
// checker mutex.
|
|
210
|
+
//
|
|
211
|
+
// Marking the plugin's tree does not merely spend that walk on nodes the
|
|
212
|
+
// binder never saw, it emits broken JavaScript. Elision reads the marks
|
|
213
|
+
// back for the file's parse-tree imports, so a reference the plugin
|
|
214
|
+
// REBUILT (a fresh identifier linked with SetOriginal, which is what any
|
|
215
|
+
// partial rewrite produces) leaves its import unmarked. The module
|
|
216
|
+
// transform still aliases that reference and elision still drops the
|
|
217
|
+
// binding the alias names: `dep_1.foo` with no `const dep_1 =
|
|
218
|
+
// require(...)`, a ReferenceError at load. The emit_plugin_rebuilt_*
|
|
219
|
+
// tests pin each import binding shape, plus the ES module lane where the
|
|
220
|
+
// whole import declaration disappears instead of just its binding.
|
|
221
|
+
//
|
|
222
|
+
// Nothing is lost by skipping the plugin's nodes. UpdateSourceFile
|
|
223
|
+
// rebuilds a SourceFile through copyFrom, which carries over neither
|
|
224
|
+
// Locals nor Symbol, and an ordinary import binds into Locals, so no
|
|
225
|
+
// resolveName branch reaches an import from a synthetic identifier. An
|
|
226
|
+
// import the plugin synthesized needs no mark at all: it has no parse
|
|
227
|
+
// original, and elision preserves it unconditionally.
|
|
228
|
+
//
|
|
229
|
+
// Marks accumulate on the checker and are never cleared, so marking one
|
|
230
|
+
// fixed tree per file also stops a second emit on the same Program from
|
|
231
|
+
// inheriting the first pass's plugin-tree marks.
|
|
232
|
+
for _, tr := range shimcompiler.GetScriptTransformers(ec, host, sf) {
|
|
185
233
|
out = tr.TransformSourceFile(out)
|
|
186
234
|
}
|
|
187
235
|
// Print through the source-map-aware helper so a `sourceMap` /
|
|
@@ -222,12 +270,30 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
|
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
272
|
}
|
|
225
|
-
|
|
273
|
+
// The declaration pass below doubles as this lane's build-information pass,
|
|
274
|
+
// so it also runs for a JavaScript-only `incremental` / `composite` project
|
|
275
|
+
// that has no declarations to write at all.
|
|
276
|
+
if !options.GetEmitDeclarations() && !p.emitsBuildInfo() {
|
|
226
277
|
return nil, nil
|
|
227
278
|
}
|
|
228
279
|
|
|
280
|
+
// What the build information this pass writes does and does not claim.
|
|
281
|
+
//
|
|
282
|
+
// The JavaScript above was hand-assembled and written outside tsgo's
|
|
283
|
+
// emitter, so tsgo's snapshot never saw it happen and records the JS emit of
|
|
284
|
+
// every file as still pending. That error is one-directional and safe: a
|
|
285
|
+
// consumer reading it can only decide to emit again, never to skip a file
|
|
286
|
+
// ttsc actually transformed. Making the record exact would mean running
|
|
287
|
+
// tsgo's own JavaScript emit a second time and discarding its output, paying
|
|
288
|
+
// a full emit to describe work already done. Everything else in the file —
|
|
289
|
+
// the compiler version, the resolved options, per-file versions and
|
|
290
|
+
// signatures, and the declaration state this pass does produce — is accurate.
|
|
291
|
+
//
|
|
292
|
+
// ttsc itself never reads build information back (see
|
|
293
|
+
// `shimcompiler.EmitFreshWithBuildInfo`), so this asymmetry costs a ttsc
|
|
294
|
+
// rebuild nothing.
|
|
229
295
|
var wfMu sync.Mutex
|
|
230
|
-
result := p.
|
|
296
|
+
result := p.emitProgram(shimcompiler.EmitOptions{
|
|
231
297
|
EmitOnly: shimcompiler.EmitOnlyDts,
|
|
232
298
|
WriteFile: func(fileName string, text string, data *shimcompiler.WriteFileData) error {
|
|
233
299
|
wfMu.Lock()
|
package/driver/graph.go
CHANGED
|
@@ -23,9 +23,10 @@ const bundledScheme = "bundled:///"
|
|
|
23
23
|
//
|
|
24
24
|
// - Edges maps each file to its direct resolved references — imports,
|
|
25
25
|
// re-exports, `/// <reference>` targets, type reference directives, and
|
|
26
|
-
// ambient-module declaration files, type-only edges included.
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// ambient-module declaration files, type-only edges included. A leaf file
|
|
27
|
+
// has an empty list so the node and its compiler-time input proof remain
|
|
28
|
+
// explicit. Direct edges are the minimal sufficient statistic; consumers
|
|
29
|
+
// that need a flat per-file list compute the reachability closure themselves.
|
|
29
30
|
// - Globals lists the files that contribute to the global scope (ambient
|
|
30
31
|
// declaration files, script files, global augmentations, `typeRoots`
|
|
31
32
|
// entries). A change to any of them can affect every file.
|
|
@@ -39,10 +40,12 @@ const bundledScheme = "bundled:///"
|
|
|
39
40
|
// map: project-relative slash paths, falling back to slash-normalized
|
|
40
41
|
// absolute paths outside the project root (see TransformOutputKey).
|
|
41
42
|
type TransformGraph struct {
|
|
42
|
-
Edges
|
|
43
|
-
Globals
|
|
44
|
-
Configs
|
|
45
|
-
Candidates
|
|
43
|
+
Edges map[string][]string `json:"edges"`
|
|
44
|
+
Globals []string `json:"globals"`
|
|
45
|
+
Configs []string `json:"configs"`
|
|
46
|
+
Candidates map[string][]string `json:"candidates,omitempty"`
|
|
47
|
+
InputHashes map[string]*string `json:"inputHashes,omitempty"`
|
|
48
|
+
InputRealpaths map[string]*string `json:"inputRealpaths,omitempty"`
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
// NewTransformGraph computes the reference graph of a loaded program, keyed
|
|
@@ -69,16 +72,66 @@ func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
|
|
|
69
72
|
if shimcompiler.FileAffectsGlobalScope(file) {
|
|
70
73
|
graph.Globals = append(graph.Globals, key)
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// Keep leaf modules as empty adjacency-list entries. Besides making the
|
|
76
|
+
// graph's node universe explicit, this lets attachInputProof bind every
|
|
77
|
+
// source file to the bytes TypeScript-Go actually read. Omitting a leaf
|
|
78
|
+
// would let an A-B-A edit during compilation pair B's output with identical
|
|
79
|
+
// pre/post project snapshots for A.
|
|
80
|
+
graph.Edges[key] = referenceTargets(prog, cwd, file)
|
|
76
81
|
}
|
|
77
82
|
sort.Strings(graph.Globals)
|
|
78
83
|
graph.Configs = configChain(prog, cwd)
|
|
84
|
+
graph.attachInputProof(prog, cwd)
|
|
79
85
|
return graph
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
// attachInputProof pairs every graph path with the state the compiler
|
|
89
|
+
// filesystem actually returned while constructing the resident Program. A
|
|
90
|
+
// missing entry means proof was incomplete or contradictory; persistent hosts
|
|
91
|
+
// then keep the fresh result but decline cross-build reuse.
|
|
92
|
+
func (graph *TransformGraph) attachInputProof(prog *Program, cwd string) {
|
|
93
|
+
if prog == nil || prog.inputObserver == nil {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
inputs := map[string]struct{}{}
|
|
97
|
+
for source, targets := range graph.Edges {
|
|
98
|
+
inputs[source] = struct{}{}
|
|
99
|
+
for _, target := range targets {
|
|
100
|
+
inputs[target] = struct{}{}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for _, input := range graph.Globals {
|
|
104
|
+
inputs[input] = struct{}{}
|
|
105
|
+
}
|
|
106
|
+
for _, input := range graph.Configs {
|
|
107
|
+
inputs[input] = struct{}{}
|
|
108
|
+
}
|
|
109
|
+
for source, candidates := range graph.Candidates {
|
|
110
|
+
inputs[source] = struct{}{}
|
|
111
|
+
for _, candidate := range candidates {
|
|
112
|
+
inputs[candidate] = struct{}{}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
hashes := map[string]*string{}
|
|
116
|
+
realpaths := map[string]*string{}
|
|
117
|
+
for input := range inputs {
|
|
118
|
+
file := filepath.FromSlash(input)
|
|
119
|
+
if !filepath.IsAbs(file) {
|
|
120
|
+
file = filepath.Join(cwd, file)
|
|
121
|
+
}
|
|
122
|
+
hash, realpath, ok := prog.inputObserver.proof(file)
|
|
123
|
+
if !ok {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
hashes[input] = hash
|
|
127
|
+
realpaths[input] = realpath
|
|
128
|
+
}
|
|
129
|
+
if len(hashes) != 0 {
|
|
130
|
+
graph.InputHashes = hashes
|
|
131
|
+
graph.InputRealpaths = realpaths
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
82
135
|
// referenceTargets resolves one file's direct reference set to sorted envelope
|
|
83
136
|
// keys, dropping bundled library files and the file itself.
|
|
84
137
|
func referenceTargets(prog *Program, cwd string, file *ast.SourceFile) []string {
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"sync"
|
|
8
|
+
|
|
9
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
10
|
+
"github.com/microsoft/typescript-go/shim/vfs"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var observedDirectoryDigest = func() string {
|
|
14
|
+
digest := sha256.Sum256([]byte("ttsc:host-input:directory\x00"))
|
|
15
|
+
return hex.EncodeToString(digest[:])
|
|
16
|
+
}()
|
|
17
|
+
|
|
18
|
+
type observedInputKind uint8
|
|
19
|
+
|
|
20
|
+
const (
|
|
21
|
+
observedInputMissing observedInputKind = iota
|
|
22
|
+
observedInputFile
|
|
23
|
+
observedInputDirectory
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
type observedInput struct {
|
|
27
|
+
contentHash *string
|
|
28
|
+
kind observedInputKind
|
|
29
|
+
realpath *string
|
|
30
|
+
unstable bool
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// inputObservationFS records the exact disk state returned through the
|
|
34
|
+
// compiler filesystem. A later transform envelope can therefore prove which
|
|
35
|
+
// bytes and resolution-candidate states produced its resident Program instead
|
|
36
|
+
// of attaching post-compile disk hashes to an earlier result.
|
|
37
|
+
type inputObservationFS struct {
|
|
38
|
+
vfs.FS
|
|
39
|
+
caseSensitive bool
|
|
40
|
+
mu sync.Mutex
|
|
41
|
+
observations map[string]observedInput
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func newInputObservationFS(inner vfs.FS) *inputObservationFS {
|
|
45
|
+
return &inputObservationFS{
|
|
46
|
+
FS: inner,
|
|
47
|
+
caseSensitive: inner.UseCaseSensitiveFileNames(),
|
|
48
|
+
observations: map[string]observedInput{},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (fs *inputObservationFS) FileExists(path string) bool {
|
|
53
|
+
exists := fs.FS.FileExists(path)
|
|
54
|
+
if exists {
|
|
55
|
+
// Existence participates in resolution, but only ReadFile returns bytes
|
|
56
|
+
// that can influence the resident Program. Do not duplicate every
|
|
57
|
+
// resolver probe with an eager file read.
|
|
58
|
+
fs.observe(path, observedInput{
|
|
59
|
+
kind: observedInputFile,
|
|
60
|
+
realpath: fs.currentRealpath(path),
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
fs.observe(path, observedInput{kind: observedInputMissing})
|
|
64
|
+
}
|
|
65
|
+
return exists
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func (fs *inputObservationFS) ReadFile(path string) (string, bool) {
|
|
69
|
+
contents, ok := fs.FS.ReadFile(path)
|
|
70
|
+
if ok {
|
|
71
|
+
digest := sha256.Sum256([]byte(contents))
|
|
72
|
+
hash := hex.EncodeToString(digest[:])
|
|
73
|
+
fs.observe(path, observedInput{
|
|
74
|
+
contentHash: &hash,
|
|
75
|
+
kind: observedInputFile,
|
|
76
|
+
realpath: fs.currentRealpath(path),
|
|
77
|
+
})
|
|
78
|
+
} else {
|
|
79
|
+
fs.observe(path, observedInput{kind: observedInputMissing})
|
|
80
|
+
}
|
|
81
|
+
return contents, ok
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func (fs *inputObservationFS) DirectoryExists(path string) bool {
|
|
85
|
+
exists := fs.FS.DirectoryExists(path)
|
|
86
|
+
if exists {
|
|
87
|
+
fs.observe(path, observedInput{
|
|
88
|
+
kind: observedInputDirectory,
|
|
89
|
+
realpath: fs.currentRealpath(path),
|
|
90
|
+
})
|
|
91
|
+
} else {
|
|
92
|
+
fs.observe(path, observedInput{kind: observedInputMissing})
|
|
93
|
+
}
|
|
94
|
+
return exists
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func (fs *inputObservationFS) GetAccessibleEntries(path string) vfs.Entries {
|
|
98
|
+
// DirectoryExists records directory identity on the ordinary compiler path.
|
|
99
|
+
// An empty listing alone cannot distinguish a missing directory from an
|
|
100
|
+
// existing empty one without issuing an extra stat for every enumeration.
|
|
101
|
+
return fs.FS.GetAccessibleEntries(path)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
|
|
105
|
+
info := fs.FS.Stat(path)
|
|
106
|
+
if info == nil {
|
|
107
|
+
fs.observe(path, observedInput{kind: observedInputMissing})
|
|
108
|
+
} else if info.IsDir() {
|
|
109
|
+
fs.observe(path, observedInput{
|
|
110
|
+
kind: observedInputDirectory,
|
|
111
|
+
realpath: fs.currentRealpath(path),
|
|
112
|
+
})
|
|
113
|
+
} else {
|
|
114
|
+
fs.observe(path, observedInput{
|
|
115
|
+
kind: observedInputFile,
|
|
116
|
+
realpath: fs.currentRealpath(path),
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
return info
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (fs *inputObservationFS) Realpath(path string) string {
|
|
123
|
+
realpath := fs.FS.Realpath(path)
|
|
124
|
+
if realpath != "" {
|
|
125
|
+
resolved := filepath.Clean(realpath)
|
|
126
|
+
fs.observeRealpath(path, &resolved)
|
|
127
|
+
}
|
|
128
|
+
return realpath
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func (fs *inputObservationFS) currentRealpath(path string) *string {
|
|
132
|
+
realpath := fs.FS.Realpath(path)
|
|
133
|
+
if realpath == "" {
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
resolved := filepath.Clean(realpath)
|
|
137
|
+
return &resolved
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func (fs *inputObservationFS) observationKey(path string) string {
|
|
141
|
+
if !filepath.IsAbs(path) {
|
|
142
|
+
return ""
|
|
143
|
+
}
|
|
144
|
+
return shimtspath.GetCanonicalFileName(
|
|
145
|
+
shimtspath.NormalizePath(path),
|
|
146
|
+
fs.caseSensitive,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func (fs *inputObservationFS) observe(path string, next observedInput) {
|
|
151
|
+
key := fs.observationKey(path)
|
|
152
|
+
if key == "" {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
fs.mu.Lock()
|
|
156
|
+
defer fs.mu.Unlock()
|
|
157
|
+
keys := []string{key}
|
|
158
|
+
// A compiler read can arrive through an 8.3, case-variant, or already-real
|
|
159
|
+
// spelling while resolution recorded the selected lexical alias. Index the
|
|
160
|
+
// returned bytes by the final physical path too, so proof can join the two
|
|
161
|
+
// observations without another disk read.
|
|
162
|
+
if next.contentHash != nil && next.realpath != nil {
|
|
163
|
+
physicalKey := fs.observationKey(*next.realpath)
|
|
164
|
+
if physicalKey != "" && physicalKey != key {
|
|
165
|
+
keys = append(keys, physicalKey)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for _, observationKey := range keys {
|
|
169
|
+
fs.mergeObservation(observationKey, next)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// mergeObservation merges one observation key while the caller holds fs.mu.
|
|
174
|
+
func (fs *inputObservationFS) mergeObservation(key string, next observedInput) {
|
|
175
|
+
previous, found := fs.observations[key]
|
|
176
|
+
if !found {
|
|
177
|
+
fs.observations[key] = next
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if previous.unstable || next.unstable || previous.kind != next.kind || !sameOptionalString(previous.realpath, next.realpath) {
|
|
181
|
+
previous.unstable = true
|
|
182
|
+
previous.contentHash = nil
|
|
183
|
+
previous.realpath = nil
|
|
184
|
+
fs.observations[key] = previous
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if previous.contentHash != nil && next.contentHash != nil && *previous.contentHash != *next.contentHash {
|
|
188
|
+
previous.unstable = true
|
|
189
|
+
previous.contentHash = nil
|
|
190
|
+
previous.realpath = nil
|
|
191
|
+
fs.observations[key] = previous
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if previous.contentHash == nil && next.contentHash != nil {
|
|
195
|
+
previous.contentHash = next.contentHash
|
|
196
|
+
}
|
|
197
|
+
fs.observations[key] = previous
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func (fs *inputObservationFS) observeRealpath(path string, realpath *string) {
|
|
201
|
+
key := fs.observationKey(path)
|
|
202
|
+
if key == "" {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
fs.mu.Lock()
|
|
206
|
+
defer fs.mu.Unlock()
|
|
207
|
+
previous, found := fs.observations[key]
|
|
208
|
+
if !found {
|
|
209
|
+
// A realpath by itself cannot prove whether the path was a file or
|
|
210
|
+
// directory. The ordinary resolver probes record that state separately.
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if previous.unstable || !sameOptionalString(previous.realpath, realpath) {
|
|
214
|
+
previous.unstable = true
|
|
215
|
+
previous.contentHash = nil
|
|
216
|
+
previous.realpath = nil
|
|
217
|
+
fs.observations[key] = previous
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func sameOptionalString(left, right *string) bool {
|
|
222
|
+
if left == nil || right == nil {
|
|
223
|
+
return left == nil && right == nil
|
|
224
|
+
}
|
|
225
|
+
return *left == *right
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// proof returns a stable compiler-time state. A nil hash/realpath with ok=true
|
|
229
|
+
// is an explicit JSON null for a path observed missing; ok=false means the
|
|
230
|
+
// compiler never produced a complete, internally consistent observation.
|
|
231
|
+
func (fs *inputObservationFS) proof(path string) (hash, realpath *string, ok bool) {
|
|
232
|
+
key := fs.observationKey(path)
|
|
233
|
+
if key == "" {
|
|
234
|
+
return nil, nil, false
|
|
235
|
+
}
|
|
236
|
+
fs.mu.Lock()
|
|
237
|
+
observation, found := fs.observations[key]
|
|
238
|
+
// TypeScript-Go can probe a lexical symlink candidate and then read the
|
|
239
|
+
// selected source by its physical filename. Reuse that exact observed read
|
|
240
|
+
// for the alias instead of issuing an eager duplicate read from FileExists.
|
|
241
|
+
if found && !observation.unstable && observation.kind == observedInputFile && observation.contentHash == nil && observation.realpath != nil {
|
|
242
|
+
targetKey := fs.observationKey(*observation.realpath)
|
|
243
|
+
target, targetFound := fs.observations[targetKey]
|
|
244
|
+
if targetFound && !target.unstable && target.kind == observedInputFile && target.contentHash != nil && sameOptionalString(target.realpath, observation.realpath) {
|
|
245
|
+
targetHash := *target.contentHash
|
|
246
|
+
observation.contentHash = &targetHash
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
fs.mu.Unlock()
|
|
250
|
+
if !found || observation.unstable {
|
|
251
|
+
return nil, nil, false
|
|
252
|
+
}
|
|
253
|
+
switch observation.kind {
|
|
254
|
+
case observedInputMissing:
|
|
255
|
+
return nil, nil, true
|
|
256
|
+
case observedInputDirectory:
|
|
257
|
+
hash := observedDirectoryDigest
|
|
258
|
+
if observation.realpath == nil {
|
|
259
|
+
return nil, nil, false
|
|
260
|
+
}
|
|
261
|
+
realpath := *observation.realpath
|
|
262
|
+
return &hash, &realpath, true
|
|
263
|
+
case observedInputFile:
|
|
264
|
+
if observation.contentHash == nil || observation.realpath == nil {
|
|
265
|
+
return nil, nil, false
|
|
266
|
+
}
|
|
267
|
+
hash := *observation.contentHash
|
|
268
|
+
realpath := *observation.realpath
|
|
269
|
+
return &hash, &realpath, true
|
|
270
|
+
default:
|
|
271
|
+
return nil, nil, false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"runtime"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestInputObservationFSRejectsRestoredContent(t *testing.T) {
|
|
14
|
+
root := t.TempDir()
|
|
15
|
+
file := filepath.Join(root, "external.d.ts")
|
|
16
|
+
if err := os.WriteFile(file, []byte("A"), 0o644); err != nil {
|
|
17
|
+
t.Fatal(err)
|
|
18
|
+
}
|
|
19
|
+
observed := newInputObservationFS(DefaultFS())
|
|
20
|
+
if contents, ok := observed.ReadFile(file); !ok || contents != "A" {
|
|
21
|
+
t.Fatalf("first read = %q, %v", contents, ok)
|
|
22
|
+
}
|
|
23
|
+
if err := os.WriteFile(file, []byte("B"), 0o644); err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
if contents, ok := observed.ReadFile(file); !ok || contents != "B" {
|
|
27
|
+
t.Fatalf("second read = %q, %v", contents, ok)
|
|
28
|
+
}
|
|
29
|
+
if err := os.WriteFile(file, []byte("A"), 0o644); err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
if _, _, ok := observed.proof(file); ok {
|
|
33
|
+
t.Fatal("A-B-A observation unexpectedly produced reusable proof")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func TestInputObservationFSProvesReadBytesAndMissingCandidates(t *testing.T) {
|
|
38
|
+
root := t.TempDir()
|
|
39
|
+
file := filepath.Join(root, "selected.ts")
|
|
40
|
+
missing := filepath.Join(root, "superseding.ts")
|
|
41
|
+
contents := "export const selected = true;\n"
|
|
42
|
+
if err := os.WriteFile(file, []byte(contents), 0o644); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
observed := newInputObservationFS(DefaultFS())
|
|
46
|
+
if !observed.FileExists(file) {
|
|
47
|
+
t.Fatal("selected file was not found")
|
|
48
|
+
}
|
|
49
|
+
if _, ok := observed.ReadFile(file); !ok {
|
|
50
|
+
t.Fatal("selected file was not read")
|
|
51
|
+
}
|
|
52
|
+
if observed.FileExists(missing) {
|
|
53
|
+
t.Fatal("missing candidate unexpectedly exists")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
digest := sha256.Sum256([]byte(contents))
|
|
57
|
+
wantHash := hex.EncodeToString(digest[:])
|
|
58
|
+
hash, realpath, ok := observed.proof(file)
|
|
59
|
+
if !ok || hash == nil || *hash != wantHash {
|
|
60
|
+
t.Fatalf("selected proof hash = %v, %v", hash, ok)
|
|
61
|
+
}
|
|
62
|
+
wantRealpath, err := filepath.EvalSymlinks(file)
|
|
63
|
+
if err != nil {
|
|
64
|
+
t.Fatal(err)
|
|
65
|
+
}
|
|
66
|
+
if realpath == nil || filepath.Clean(*realpath) != filepath.Clean(wantRealpath) {
|
|
67
|
+
t.Fatalf("selected proof realpath = %v, want %q", realpath, wantRealpath)
|
|
68
|
+
}
|
|
69
|
+
hash, realpath, ok = observed.proof(missing)
|
|
70
|
+
if !ok || hash != nil || realpath != nil {
|
|
71
|
+
t.Fatalf("missing proof = %v, %v, %v", hash, realpath, ok)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func TestInputObservationFSHashesCompilerDecodedText(t *testing.T) {
|
|
76
|
+
root := t.TempDir()
|
|
77
|
+
file := filepath.Join(root, "bom.ts")
|
|
78
|
+
text := "export const value = true;\n"
|
|
79
|
+
raw := append([]byte{0xef, 0xbb, 0xbf}, []byte(text)...)
|
|
80
|
+
if err := os.WriteFile(file, raw, 0o644); err != nil {
|
|
81
|
+
t.Fatal(err)
|
|
82
|
+
}
|
|
83
|
+
observed := newInputObservationFS(DefaultFS())
|
|
84
|
+
contents, ok := observed.ReadFile(file)
|
|
85
|
+
if !ok || contents != text {
|
|
86
|
+
t.Fatalf("compiler read = %q, %v; want decoded text", contents, ok)
|
|
87
|
+
}
|
|
88
|
+
digest := sha256.Sum256([]byte(text))
|
|
89
|
+
hash, _, ok := observed.proof(file)
|
|
90
|
+
if !ok || hash == nil || *hash != hex.EncodeToString(digest[:]) {
|
|
91
|
+
t.Fatalf("decoded proof hash = %v, %v", hash, ok)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestInputObservationFSJoinsSelectedAliasProbeToPhysicalRead(t *testing.T) {
|
|
96
|
+
root := t.TempDir()
|
|
97
|
+
target := filepath.Join(root, "target")
|
|
98
|
+
if err := os.Mkdir(target, 0o755); err != nil {
|
|
99
|
+
t.Fatal(err)
|
|
100
|
+
}
|
|
101
|
+
physical := filepath.Join(target, "value.js")
|
|
102
|
+
contents := "export const value = true;\n"
|
|
103
|
+
if err := os.WriteFile(physical, []byte(contents), 0o644); err != nil {
|
|
104
|
+
t.Fatal(err)
|
|
105
|
+
}
|
|
106
|
+
alias := filepath.Join(root, "alias")
|
|
107
|
+
if runtime.GOOS == "windows" {
|
|
108
|
+
command := exec.Command(
|
|
109
|
+
"node",
|
|
110
|
+
"-e",
|
|
111
|
+
`require("node:fs").symlinkSync(process.argv[1], process.argv[2], "junction")`,
|
|
112
|
+
target,
|
|
113
|
+
alias,
|
|
114
|
+
)
|
|
115
|
+
if output, err := command.CombinedOutput(); err != nil {
|
|
116
|
+
t.Skipf("directory junction unavailable on this host: %v: %s", err, output)
|
|
117
|
+
}
|
|
118
|
+
} else if err := os.Symlink(target, alias); err != nil {
|
|
119
|
+
t.Skipf("directory symlink unavailable on this host: %v", err)
|
|
120
|
+
}
|
|
121
|
+
lexical := filepath.Join(alias, "value.js")
|
|
122
|
+
observed := newInputObservationFS(DefaultFS())
|
|
123
|
+
if !observed.FileExists(lexical) {
|
|
124
|
+
t.Fatal("selected lexical alias was not found")
|
|
125
|
+
}
|
|
126
|
+
if _, ok := observed.ReadFile(physical); !ok {
|
|
127
|
+
t.Fatal("selected physical source was not read")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
digest := sha256.Sum256([]byte(contents))
|
|
131
|
+
hash, realpath, ok := observed.proof(lexical)
|
|
132
|
+
if !ok || hash == nil || *hash != hex.EncodeToString(digest[:]) {
|
|
133
|
+
t.Fatalf("alias proof hash = %v, %v", hash, ok)
|
|
134
|
+
}
|
|
135
|
+
if realpath == nil {
|
|
136
|
+
t.Fatalf("alias proof realpath = nil, want %q", physical)
|
|
137
|
+
}
|
|
138
|
+
actualInfo, err := os.Stat(*realpath)
|
|
139
|
+
if err != nil {
|
|
140
|
+
t.Fatal(err)
|
|
141
|
+
}
|
|
142
|
+
expectedInfo, err := os.Stat(physical)
|
|
143
|
+
if err != nil {
|
|
144
|
+
t.Fatal(err)
|
|
145
|
+
}
|
|
146
|
+
if !os.SameFile(actualInfo, expectedInfo) {
|
|
147
|
+
t.Fatalf("alias proof realpath = %q, want %q", *realpath, physical)
|
|
148
|
+
}
|
|
149
|
+
}
|