ttsc 0.27.0 → 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/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 +17 -6
- 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 +253 -25
- 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 +57 -8
- package/driver/plugins.go +125 -14
- 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/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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
- package/lib/launcher/internal/runtimeHooks.js +93 -2
- package/lib/launcher/internal/runtimeHooks.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/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
- package/package.json +8 -8
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/shim.go +3 -0
- package/shim/compiler/emithook.go +10 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/index.ts +1 -0
- package/src/launcher/internal/runtimeHooks.ts +111 -2
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/structures/ITtscCompilerTransformation.ts +7 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +15 -6
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"slices"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// TestRunDumpDeclaresTheCapabilityItsOutputCarries verifies that the shipped
|
|
15
|
+
// `dump` command's own output declares the documentation-tag capability, and
|
|
16
|
+
// that a project carrying no tag emits the field nowhere.
|
|
17
|
+
//
|
|
18
|
+
// The claim is the contract, and it can only be tested where it is made. A test
|
|
19
|
+
// that hands a capability list to the marshaller and reads it back proves the
|
|
20
|
+
// slice was copied; it leaves the production list free to lose the member with
|
|
21
|
+
// nothing failing. That matters because the two states a consumer must
|
|
22
|
+
// distinguish — "this declaration cites nothing" and "this producer never
|
|
23
|
+
// looked" — are the same absent field, so a dump that carries tags and forgets
|
|
24
|
+
// to say so is read as a repository where nothing cites anything.
|
|
25
|
+
//
|
|
26
|
+
// 1. Run the `dump` subcommand over a project with a tag, capturing stdout.
|
|
27
|
+
// 2. Assert the capability is declared and the tag is present in the output.
|
|
28
|
+
// 3. Run it over a project with no tag and assert the field appears nowhere,
|
|
29
|
+
// while the claim still does.
|
|
30
|
+
func TestRunDumpDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
|
|
31
|
+
tagged := runDumpForCapabilityProbe(t, `/** @evidence docs/a.md#x Cited. */
|
|
32
|
+
export function subject(): void {}
|
|
33
|
+
`)
|
|
34
|
+
untagged := runDumpForCapabilityProbe(t, `/** Ordinary documentation. */
|
|
35
|
+
export function subject(): void {}
|
|
36
|
+
`)
|
|
37
|
+
|
|
38
|
+
for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
|
|
39
|
+
var parsed struct {
|
|
40
|
+
Provenance struct {
|
|
41
|
+
Capabilities []string `json:"capabilities"`
|
|
42
|
+
} `json:"provenance"`
|
|
43
|
+
}
|
|
44
|
+
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
45
|
+
t.Fatalf("%s dump is not JSON: %v", name, err)
|
|
46
|
+
}
|
|
47
|
+
if !slices.Contains(parsed.Provenance.Capabilities, graph.CapabilityDocTags) {
|
|
48
|
+
t.Fatalf("%s dump declares %v, missing %q; a consumer would then read a "+
|
|
49
|
+
"tagged declaration as citing nothing",
|
|
50
|
+
name, parsed.Provenance.Capabilities, graph.CapabilityDocTags)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
|
|
55
|
+
t.Fatalf("the tagged dump carried no tag text")
|
|
56
|
+
}
|
|
57
|
+
// Read the raw document rather than a decoded slice: an absent field and an
|
|
58
|
+
// empty one decode identically, and absent is what the wire contract says.
|
|
59
|
+
// The key is matched with its colon, because the capability shares the name
|
|
60
|
+
// and rides the same document as a bare string.
|
|
61
|
+
if strings.Contains(untagged, `"docTags":`) {
|
|
62
|
+
t.Fatalf("the untagged dump carried a docTags field; it must be absent")
|
|
63
|
+
}
|
|
64
|
+
if !strings.Contains(tagged, `"docTags":`) {
|
|
65
|
+
t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// runDumpForCapabilityProbe runs the dump subcommand over a one-file project and
|
|
70
|
+
// returns its stdout.
|
|
71
|
+
func runDumpForCapabilityProbe(t *testing.T, source string) string {
|
|
72
|
+
t.Helper()
|
|
73
|
+
root := t.TempDir()
|
|
74
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
75
|
+
"compilerOptions": {
|
|
76
|
+
"target": "ES2022",
|
|
77
|
+
"module": "commonjs",
|
|
78
|
+
"strict": true,
|
|
79
|
+
"rootDir": "src",
|
|
80
|
+
"outDir": "dist"
|
|
81
|
+
},
|
|
82
|
+
"files": ["src/main.ts"]
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
writeGraphFile(t, filepath.Join(root, "src", "main.ts"), source)
|
|
86
|
+
|
|
87
|
+
var out, errOut bytes.Buffer
|
|
88
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
89
|
+
stdout, stderr = &out, &errOut
|
|
90
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
91
|
+
|
|
92
|
+
if code := run([]string{"dump", "--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
|
|
93
|
+
t.Fatalf("dump exited %d: %s", code, errOut.String())
|
|
94
|
+
}
|
|
95
|
+
return out.String()
|
|
96
|
+
}
|
package/cmd/ttscgraph/serve.go
CHANGED
|
@@ -57,27 +57,82 @@ const (
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
// fullSnapshotCapabilities is what a snapshot from a resident compiler session
|
|
60
|
-
// proves. Both commands that own a real Program declare
|
|
61
|
-
// exists so the envelope's claim and the dump's claim cannot drift
|
|
60
|
+
// proves. Both commands that own a real Program declare every one of them; the
|
|
61
|
+
// constant exists so the envelope's claim and the dump's claim cannot drift
|
|
62
|
+
// apart.
|
|
62
63
|
var fullSnapshotCapabilities = []string{
|
|
63
64
|
graph.CapabilityUniverse,
|
|
64
65
|
graph.CapabilitySourceDigests,
|
|
65
66
|
graph.CapabilityDiskDigests,
|
|
66
67
|
graph.CapabilityDiagnostics,
|
|
68
|
+
graph.CapabilityDocTags,
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
// serveCapabilities is what this server can prove
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
71
|
+
// serveCapabilities is what this server can prove before a session exists —
|
|
72
|
+
// the frame a client reads when the project could not even be loaded. Once a
|
|
73
|
+
// session exists its own capabilities answer, because those depend on what a
|
|
74
|
+
// plugin published and this list cannot know it.
|
|
73
75
|
var serveCapabilities = fullSnapshotCapabilities
|
|
74
76
|
|
|
77
|
+
// capabilities is what THIS session proves, which is the shared list plus the
|
|
78
|
+
// artifact claim when a plugin published one.
|
|
79
|
+
//
|
|
80
|
+
// The claim has to be conditional for the same reason it is on the dump command:
|
|
81
|
+
// a project that publishes no artifacts and a session that was never handed any
|
|
82
|
+
// are the same absent nodes, and only the claim separates them.
|
|
83
|
+
func (s *graphSession) capabilities() []string {
|
|
84
|
+
if len(s.artifacts) == 0 {
|
|
85
|
+
return fullSnapshotCapabilities
|
|
86
|
+
}
|
|
87
|
+
return append(
|
|
88
|
+
append([]string{}, fullSnapshotCapabilities...),
|
|
89
|
+
graph.CapabilityArtifactNodes,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// artifactProducer names the second producer behind this session's artifact
|
|
94
|
+
// nodes, and is nil when it has none.
|
|
95
|
+
// provenance is this session's published origin for one generation.
|
|
96
|
+
func (s *graphSession) provenance(texts map[string]string) graph.Provenance {
|
|
97
|
+
built := graph.NewProvenance(
|
|
98
|
+
serveProducer(),
|
|
99
|
+
s.capabilities(),
|
|
100
|
+
s.configDigests,
|
|
101
|
+
s.roots,
|
|
102
|
+
texts,
|
|
103
|
+
s.diskDigests,
|
|
104
|
+
)
|
|
105
|
+
built.ArtifactProducer = s.artifactProducer()
|
|
106
|
+
return built
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (s *graphSession) artifactProducer() *graph.Producer {
|
|
110
|
+
if len(s.artifacts) == 0 {
|
|
111
|
+
return nil
|
|
112
|
+
}
|
|
113
|
+
return &graph.Producer{Tool: "@ttsc/lint graph-nodes"}
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
type serveRequest struct {
|
|
76
117
|
ID int `json:"id"`
|
|
77
118
|
// GraphSnapshotVersion opts into the incremental shard protocol. Omitted
|
|
78
|
-
// requests retain the
|
|
79
|
-
// @ttsc/graph clients.
|
|
119
|
+
// requests retain the full-dump response for existing @ttsc/graph clients.
|
|
80
120
|
GraphSnapshotVersion int `json:"graphSnapshotVersion,omitempty"`
|
|
121
|
+
// Artifacts is the path to the set a plugin published for this request, which
|
|
122
|
+
// the client re-derives when the documents or lint configuration behind it
|
|
123
|
+
// moved. It rides every request rather than only the changed ones: the client
|
|
124
|
+
// is the only side that can see those inputs, and the server is the only side
|
|
125
|
+
// that knows what it last applied, so the honest exchange is the client
|
|
126
|
+
// stating what it currently has and the server comparing it with its own.
|
|
127
|
+
//
|
|
128
|
+
// Three states, which is why it is a pointer. Absent is a client with no
|
|
129
|
+
// opinion — one predating this field, or one driving a session through the
|
|
130
|
+
// startup flag — and the set already applied stands. Empty is a client
|
|
131
|
+
// stating it now has none, which withdraws that set: a project whose
|
|
132
|
+
// publisher was removed must stop being answered with its artifacts, and
|
|
133
|
+
// without a way to say so it would be answered with them until the editor
|
|
134
|
+
// restarted. A path is the set to apply.
|
|
135
|
+
Artifacts *string `json:"artifacts,omitempty"`
|
|
81
136
|
}
|
|
82
137
|
|
|
83
138
|
type serveResponse struct {
|
|
@@ -119,8 +174,30 @@ func errorResponse(id int, message string) serveResponse {
|
|
|
119
174
|
}
|
|
120
175
|
|
|
121
176
|
type graphSession struct {
|
|
122
|
-
cwd
|
|
123
|
-
tsconfig
|
|
177
|
+
cwd string
|
|
178
|
+
tsconfig string
|
|
179
|
+
// artifacts is the set a plugin published: a second producer's facts about
|
|
180
|
+
// documents this Program never read. Refreshing it is therefore not a refresh
|
|
181
|
+
// of the graph — editing a Markdown heading moves the artifact and not one
|
|
182
|
+
// compiler fact — so the two invalidations stay separate questions, and this
|
|
183
|
+
// one is answered by adoptArtifacts rather than by the build universe.
|
|
184
|
+
artifacts []graph.Artifact
|
|
185
|
+
// artifactsDigest is the content of the set currently applied. Content, and
|
|
186
|
+
// not the path or its modification time: the client overwrites one file per
|
|
187
|
+
// process and project, so the path never moves for a given session, and a
|
|
188
|
+
// republish triggered by a document whose headings did not actually change
|
|
189
|
+
// writes the same bytes and must therefore cost nothing.
|
|
190
|
+
artifactsDigest [sha256.Size]byte
|
|
191
|
+
// artifactsFile and artifactsStat are what the digest was taken from, kept so
|
|
192
|
+
// an unchanged file need not be read at all. The set is one entry per
|
|
193
|
+
// document section, model field, and operation, so it is bounded by the
|
|
194
|
+
// project's documentation rather than by anything small — and this comparison
|
|
195
|
+
// runs before every graph request, where reading and hashing that whole file
|
|
196
|
+
// to learn it did not move is the kind of cost a resident session pays
|
|
197
|
+
// forever.
|
|
198
|
+
artifactsFile string
|
|
199
|
+
artifactsStat artifactsFileState
|
|
200
|
+
|
|
124
201
|
compiler *driver.Session
|
|
125
202
|
configHashes map[string][sha256.Size]byte
|
|
126
203
|
auxStates map[string]diskState
|
|
@@ -140,21 +217,134 @@ type graphSession struct {
|
|
|
140
217
|
graphStore *serveGraphStore
|
|
141
218
|
requestProtocol int
|
|
142
219
|
requestProtocolSelected bool
|
|
143
|
-
// pending remembers
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
220
|
+
// pending remembers work the next request owes regardless of what the build
|
|
221
|
+
// universe says: a generation whose state was captured but whose selected
|
|
222
|
+
// projection failed, or an artifact set adopted after the graph carrying the
|
|
223
|
+
// previous one was built. Until it is discharged, an otherwise unchanged
|
|
224
|
+
// request retries the recorded invalidation instead of falsely confirming the
|
|
225
|
+
// older client graph.
|
|
147
226
|
pending *graphChange
|
|
148
227
|
}
|
|
149
228
|
|
|
150
229
|
func newGraphSession(cwd, tsconfig string) (*graphSession, error) {
|
|
151
|
-
|
|
230
|
+
return newGraphSessionWithArtifacts(cwd, tsconfig, nil)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// newGraphSessionWithArtifacts is the constructor the serve command uses, and
|
|
234
|
+
// the plain one above is it with nothing published. The artifacts are read once
|
|
235
|
+
// and held for the session: they are a second producer's facts about documents
|
|
236
|
+
// this Program never read, so refreshing them is a different question from
|
|
237
|
+
// refreshing the graph, and only the first answer is wired today.
|
|
238
|
+
func newGraphSessionWithArtifacts(
|
|
239
|
+
cwd, tsconfig string,
|
|
240
|
+
artifacts []graph.Artifact,
|
|
241
|
+
) (*graphSession, error) {
|
|
242
|
+
session := &graphSession{cwd: cwd, tsconfig: tsconfig, artifacts: artifacts}
|
|
152
243
|
if err := session.reload(); err != nil {
|
|
153
244
|
return nil, err
|
|
154
245
|
}
|
|
155
246
|
return session, nil
|
|
156
247
|
}
|
|
157
248
|
|
|
249
|
+
// adoptArtifacts makes what the request states the set this session applies,
|
|
250
|
+
// and reports whether that replaced a set some already-built graph carries.
|
|
251
|
+
//
|
|
252
|
+
// A nil statement is a client with no opinion, and the set already applied
|
|
253
|
+
// stands: dropping it on the say-so of a client that never mentioned artifacts
|
|
254
|
+
// would delete them from the graph of every session driven through the startup
|
|
255
|
+
// flag alone.
|
|
256
|
+
//
|
|
257
|
+
// A read failure is returned rather than swallowed. The client wrote this file
|
|
258
|
+
// moments ago and named it in the same request, so a file that cannot be read
|
|
259
|
+
// is a broken exchange, not a project without artifacts — and answering with a
|
|
260
|
+
// silently artifact-free graph would look exactly like a correct answer.
|
|
261
|
+
func (s *graphSession) adoptArtifacts(named *string) (bool, error) {
|
|
262
|
+
if named == nil {
|
|
263
|
+
return false, nil
|
|
264
|
+
}
|
|
265
|
+
path := strings.TrimSpace(*named)
|
|
266
|
+
if path == "" {
|
|
267
|
+
return s.applyArtifacts(nil, withdrawnArtifactsDigest), nil
|
|
268
|
+
}
|
|
269
|
+
stat, err := os.Stat(path)
|
|
270
|
+
if err != nil {
|
|
271
|
+
return false, err
|
|
272
|
+
}
|
|
273
|
+
state := artifactsFileState{size: stat.Size(), modTime: stat.ModTime().UnixNano()}
|
|
274
|
+
if path == s.artifactsFile && state == s.artifactsStat {
|
|
275
|
+
return false, nil
|
|
276
|
+
}
|
|
277
|
+
// One read, hashed and decoded from the same bytes. Reading twice would let
|
|
278
|
+
// an overwrite land between them and leave this session recording an identity
|
|
279
|
+
// for a set it is not holding — after which a later request naming the file
|
|
280
|
+
// it recorded would be answered as unchanged.
|
|
281
|
+
contents, err := os.ReadFile(path)
|
|
282
|
+
if err != nil {
|
|
283
|
+
return false, err
|
|
284
|
+
}
|
|
285
|
+
next, err := graph.ParseArtifacts(contents)
|
|
286
|
+
if err != nil {
|
|
287
|
+
return false, err
|
|
288
|
+
}
|
|
289
|
+
s.artifactsFile = path
|
|
290
|
+
s.artifactsStat = state
|
|
291
|
+
return s.applyArtifacts(next, sha256.Sum256(contents)), nil
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// artifactsFileState is what an unchanged published file looks like from the
|
|
295
|
+
// outside: size and modification time, never contents. It exists only to decide
|
|
296
|
+
// whether the file is worth reading, and the digest taken from those contents is
|
|
297
|
+
// still what decides whether the set is worth applying.
|
|
298
|
+
type artifactsFileState struct {
|
|
299
|
+
size int64
|
|
300
|
+
// modTime is nanoseconds since the epoch rather than a time.Time, so the
|
|
301
|
+
// struct compares with == and means it. A time.Time carries a location
|
|
302
|
+
// pointer and an optional monotonic reading, and comparing two of those with
|
|
303
|
+
// == is a well-known way to get a false inequality that reads as a changed
|
|
304
|
+
// file forever.
|
|
305
|
+
modTime int64
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// applyArtifacts installs a set and reports whether it differs from the applied
|
|
309
|
+
// one. The first application of a session reports a change like any other and
|
|
310
|
+
// costs nothing: no graph has been projected yet, so the invalidation it
|
|
311
|
+
// records is discharged by the initial projection that was going to happen
|
|
312
|
+
// regardless.
|
|
313
|
+
func (s *graphSession) applyArtifacts(next []graph.Artifact, digest [sha256.Size]byte) bool {
|
|
314
|
+
if digest == s.artifactsDigest {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
s.artifacts = next
|
|
318
|
+
s.artifactsDigest = digest
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// withdrawnArtifactsDigest stands for "the client states it has none".
|
|
323
|
+
//
|
|
324
|
+
// A constant rather than the hash of an empty file, so that the withdrawn state
|
|
325
|
+
// and a file whose contents happen to hash to the same value cannot be confused
|
|
326
|
+
// — and domain-separated so it is not the digest of anything a producer writes.
|
|
327
|
+
var withdrawnArtifactsDigest = sha256.Sum256([]byte("ttscgraph:artifacts:none"))
|
|
328
|
+
|
|
329
|
+
// invalidateArtifacts records that the next projection must be a full one.
|
|
330
|
+
//
|
|
331
|
+
// Full, because replacing the published set is not a per-file delta: it moves
|
|
332
|
+
// the artifact nodes and every citation edge into them at once, and the
|
|
333
|
+
// incremental path is built to answer a different question — which sources
|
|
334
|
+
// changed. It happens to reach the same answer here, through the closure
|
|
335
|
+
// expansion that pulls a citing source back in, but that is a property of
|
|
336
|
+
// machinery aimed elsewhere and not a guarantee this depends on.
|
|
337
|
+
//
|
|
338
|
+
// Full, but not a reload: the Program is reused untouched, because no compiler
|
|
339
|
+
// input moved. That is the whole point of keeping documents out of the build
|
|
340
|
+
// universe — a Markdown edit costs one projection, never one typecheck.
|
|
341
|
+
func (s *graphSession) invalidateArtifacts() {
|
|
342
|
+
if s.pending != nil && s.pending.full {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
s.pending = &graphChange{mode: serveModeRebuild, full: true}
|
|
346
|
+
}
|
|
347
|
+
|
|
158
348
|
func (s *graphSession) Close() error {
|
|
159
349
|
if s.compiler == nil {
|
|
160
350
|
return nil
|
|
@@ -441,6 +631,7 @@ func missingRootInputs(configs []*shimtsoptions.ParsedCommandLine, sourceHashes
|
|
|
441
631
|
func (s *graphSession) buildDump() (graph.Dump, error) {
|
|
442
632
|
program := s.compiler.Program()
|
|
443
633
|
built := graph.Build(program)
|
|
634
|
+
graph.ApplyArtifacts(built, s.artifacts)
|
|
444
635
|
// One texts map feeds both the spans and the manifest digests, so the bytes a
|
|
445
636
|
// span points into are provably the bytes the manifest attests to.
|
|
446
637
|
texts := graph.SourceTexts(program)
|
|
@@ -451,14 +642,7 @@ func (s *graphSession) buildDump() (graph.Dump, error) {
|
|
|
451
642
|
graph.GitIgnoredFiles(s.cwd, built),
|
|
452
643
|
texts,
|
|
453
644
|
graph.DumpOrigin{
|
|
454
|
-
Provenance:
|
|
455
|
-
serveProducer(),
|
|
456
|
-
fullSnapshotCapabilities,
|
|
457
|
-
s.configDigests,
|
|
458
|
-
s.roots,
|
|
459
|
-
texts,
|
|
460
|
-
s.diskDigests,
|
|
461
|
-
),
|
|
645
|
+
Provenance: s.provenance(texts),
|
|
462
646
|
Diagnostics: graph.NewDiagnostics(program),
|
|
463
647
|
},
|
|
464
648
|
)
|
|
@@ -861,9 +1045,23 @@ func runServe(args []string) int {
|
|
|
861
1045
|
fs.SetOutput(stderr)
|
|
862
1046
|
cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
|
|
863
1047
|
tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
|
|
1048
|
+
artifactsFlag := fs.String(
|
|
1049
|
+
"artifacts",
|
|
1050
|
+
"",
|
|
1051
|
+
"path to the artifacts a plugin published (JSON); absent or missing means none",
|
|
1052
|
+
)
|
|
864
1053
|
if err := fs.Parse(args); err != nil {
|
|
865
1054
|
return 2
|
|
866
1055
|
}
|
|
1056
|
+
artifacts, artifactsErr := graph.LoadArtifacts(strings.TrimSpace(*artifactsFlag))
|
|
1057
|
+
if artifactsErr != nil {
|
|
1058
|
+
fmt.Fprintf(
|
|
1059
|
+
stderr,
|
|
1060
|
+
"ttscgraph: could not read the published artifacts: %v\n",
|
|
1061
|
+
artifactsErr,
|
|
1062
|
+
)
|
|
1063
|
+
return 1
|
|
1064
|
+
}
|
|
867
1065
|
|
|
868
1066
|
cwd := strings.TrimSpace(*cwdFlag)
|
|
869
1067
|
if cwd == "" {
|
|
@@ -880,10 +1078,22 @@ func runServe(args []string) int {
|
|
|
880
1078
|
cwd = shimtspath.ResolvePath(cwd)
|
|
881
1079
|
tsconfig := strings.TrimSpace(*tsconfigFlag)
|
|
882
1080
|
|
|
883
|
-
return
|
|
1081
|
+
return serveSnapshotsWithArtifacts(os.Stdin, stdout, cwd, tsconfig, artifacts)
|
|
884
1082
|
}
|
|
885
1083
|
|
|
886
1084
|
func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int {
|
|
1085
|
+
return serveSnapshotsWithArtifacts(input, output, cwd, tsconfig, nil)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// serveSnapshotsWithArtifacts is the loop the serve command runs, and
|
|
1089
|
+
// serveSnapshots is it with nothing published — the shape every existing case
|
|
1090
|
+
// drives, unchanged.
|
|
1091
|
+
func serveSnapshotsWithArtifacts(
|
|
1092
|
+
input io.Reader,
|
|
1093
|
+
output io.Writer,
|
|
1094
|
+
cwd, tsconfig string,
|
|
1095
|
+
artifacts []graph.Artifact,
|
|
1096
|
+
) int {
|
|
887
1097
|
scanner := bufio.NewScanner(input)
|
|
888
1098
|
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
889
1099
|
encoder := json.NewEncoder(output)
|
|
@@ -932,7 +1142,7 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
|
|
|
932
1142
|
var loadDuration time.Duration
|
|
933
1143
|
if session == nil {
|
|
934
1144
|
loadStarted := time.Now()
|
|
935
|
-
created, err :=
|
|
1145
|
+
created, err := newGraphSessionWithArtifacts(cwd, tsconfig, artifacts)
|
|
936
1146
|
loadDuration = time.Since(loadStarted)
|
|
937
1147
|
if err != nil {
|
|
938
1148
|
if err := encodeServeResponseWithTrace(
|
|
@@ -962,6 +1172,18 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
|
|
|
962
1172
|
}
|
|
963
1173
|
session.requestProtocol = request.GraphSnapshotVersion
|
|
964
1174
|
session.requestProtocolSelected = true
|
|
1175
|
+
if replaced, err := session.adoptArtifacts(request.Artifacts); err != nil {
|
|
1176
|
+
if err := encodeServeResponseWithTrace(encoder, errorResponse(
|
|
1177
|
+
request.ID,
|
|
1178
|
+
fmt.Sprintf("ttscgraph: could not read the published artifacts: %v", err),
|
|
1179
|
+
), requestStarted, loadDuration, 0, 0); err != nil {
|
|
1180
|
+
fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
|
|
1181
|
+
return 1
|
|
1182
|
+
}
|
|
1183
|
+
continue
|
|
1184
|
+
} else if replaced {
|
|
1185
|
+
session.invalidateArtifacts()
|
|
1186
|
+
}
|
|
965
1187
|
var dump *graph.Dump
|
|
966
1188
|
var snapshot *serveGraphSnapshot
|
|
967
1189
|
var mode string
|
|
@@ -975,6 +1197,12 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
|
|
|
975
1197
|
dump, mode, changed, err = session.Snapshot()
|
|
976
1198
|
}
|
|
977
1199
|
response := newServeResponse(request.ID)
|
|
1200
|
+
// The envelope answers for THIS session, not for the command in general. An
|
|
1201
|
+
// `unchanged` response carries no dump, so the envelope is the only place a
|
|
1202
|
+
// client learns what this server proves — and a session holding artifacts
|
|
1203
|
+
// publishes a dump that claims them. Leaving the shared list here made the
|
|
1204
|
+
// envelope and the dump disagree on the one frame that has no dump to check.
|
|
1205
|
+
response.Capabilities = session.capabilities()
|
|
978
1206
|
response.Dump = dump
|
|
979
1207
|
response.Snapshot = snapshot
|
|
980
1208
|
response.Mode = mode
|