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/README.md
CHANGED
|
@@ -30,6 +30,15 @@ npx ttsc --watch # rebuild on save
|
|
|
30
30
|
|
|
31
31
|
`ttsx` runs a file the way `tsx` or `ts-node` does, but it type-checks the whole project first, so a type error stops the run before anything executes.
|
|
32
32
|
|
|
33
|
+
Preload the same checked runtime when Node or a test runner owns the process:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node --require ttsc/register src/index.ts
|
|
37
|
+
mocha --require ttsc/register --extension ts,tsx "test/**/*.ts"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`ttsc/register` discovers the nearest `tsconfig.json` for each TypeScript root, applies its plugins, and refuses to execute when the project does not type-check.
|
|
41
|
+
|
|
33
42
|
That covers the CLI. The integrations each have a short guide:
|
|
34
43
|
|
|
35
44
|
- [`@ttsc/unplugin`](https://ttsc.dev/docs/setup/unplugin): Vite, Rollup, Rolldown, esbuild, webpack, Rspack, Next.js, Turbopack, Farm, and Bun.
|
package/cmd/graphbench/main.go
CHANGED
|
@@ -29,10 +29,16 @@ type metrics struct {
|
|
|
29
29
|
Nodes int `json:"nodes"`
|
|
30
30
|
ExternalNodes int `json:"externalNodes"`
|
|
31
31
|
Edges map[string]int `json:"edges"`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
// EdgeVocabulary names which of the three edge-kind vocabularies Edges is
|
|
33
|
+
// keyed by, so a reader comparing these numbers against a dump does not
|
|
34
|
+
// compare the wrong things. Build produces *internal* kinds; a dump carries
|
|
35
|
+
// finer *wire* kinds (value-call splits into calls/instantiates/renders);
|
|
36
|
+
// the viewer folds wire kinds onto *display* families.
|
|
37
|
+
EdgeVocabulary string `json:"edgeVocabulary"`
|
|
38
|
+
TotalEdges int `json:"totalEdges"`
|
|
39
|
+
SymbolFiles int `json:"symbolFiles"`
|
|
40
|
+
CoveredFiles int `json:"coveredFiles"`
|
|
41
|
+
Coverage float64 `json:"coverage"`
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
func main() {
|
|
@@ -82,10 +88,13 @@ func run() int {
|
|
|
82
88
|
// counts, edges per kind, and fair coverage (workspace files holding at least one
|
|
83
89
|
// node that an edge connects to a node in another file).
|
|
84
90
|
func summarize(g *graph.Graph, sourceFiles int) metrics {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
// Seeded with every kind Build can produce, so a family that happened not to
|
|
92
|
+
// occur reports 0 rather than vanishing from the object. Three of the seven
|
|
93
|
+
// used to be seeded and the map grew the rest on first sight, which meant the
|
|
94
|
+
// reported shape depended on the project measured.
|
|
95
|
+
edges := map[string]int{}
|
|
96
|
+
for _, kind := range graph.EdgeKinds() {
|
|
97
|
+
edges[string(kind)] = 0
|
|
89
98
|
}
|
|
90
99
|
for _, edge := range g.Edges {
|
|
91
100
|
edges[string(edge.Kind)]++
|
|
@@ -123,13 +132,14 @@ func summarize(g *graph.Graph, sourceFiles int) metrics {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
return metrics{
|
|
126
|
-
SourceFiles:
|
|
127
|
-
Nodes:
|
|
128
|
-
ExternalNodes:
|
|
129
|
-
Edges:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
SourceFiles: sourceFiles,
|
|
136
|
+
Nodes: len(g.Nodes),
|
|
137
|
+
ExternalNodes: external,
|
|
138
|
+
Edges: edges,
|
|
139
|
+
EdgeVocabulary: "internal",
|
|
140
|
+
TotalEdges: len(g.Edges),
|
|
141
|
+
SymbolFiles: len(symbolFiles),
|
|
142
|
+
CoveredFiles: len(coveredFiles),
|
|
143
|
+
Coverage: coverage,
|
|
134
144
|
}
|
|
135
145
|
}
|
package/cmd/graphdump/main.go
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
package main
|
|
7
7
|
|
|
8
8
|
import (
|
|
9
|
+
"errors"
|
|
9
10
|
"flag"
|
|
10
11
|
"fmt"
|
|
12
|
+
"io"
|
|
11
13
|
"os"
|
|
12
14
|
"path/filepath"
|
|
13
15
|
|
|
@@ -17,18 +19,37 @@ import (
|
|
|
17
19
|
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
18
20
|
)
|
|
19
21
|
|
|
22
|
+
// Package-level streams so command tests can capture I/O without patching the
|
|
23
|
+
// os globals, and argv arrives as a parameter for the same reason. Both mirror
|
|
24
|
+
// the shipped sibling in cmd/ttscgraph, whose capability claim is tested the
|
|
25
|
+
// same way; this command's claim went undefended because it had neither seam.
|
|
26
|
+
var (
|
|
27
|
+
stdout io.Writer = os.Stdout
|
|
28
|
+
stderr io.Writer = os.Stderr
|
|
29
|
+
)
|
|
30
|
+
|
|
20
31
|
func main() {
|
|
21
|
-
os.Exit(run())
|
|
32
|
+
os.Exit(run(os.Args[1:]))
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
func run() int {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
func run(args []string) int {
|
|
36
|
+
fs := flag.NewFlagSet("graphdump", flag.ContinueOnError)
|
|
37
|
+
fs.SetOutput(stderr)
|
|
38
|
+
cwd := fs.String("cwd", ".", "project root")
|
|
39
|
+
tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
|
|
40
|
+
pretty := fs.Bool("pretty", false, "indent the JSON output")
|
|
41
|
+
if err := fs.Parse(args); err != nil {
|
|
42
|
+
// `-h` asks for the usage this just printed, so it is a request that
|
|
43
|
+
// succeeded rather than an argument that failed. Under the global flag set
|
|
44
|
+
// this command used to read, `ExitOnError` already exited 0 for it.
|
|
45
|
+
if errors.Is(err, flag.ErrHelp) {
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
return 2
|
|
49
|
+
}
|
|
29
50
|
|
|
30
51
|
// Resolve the project root the same way LoadProgram does (absolute, then
|
|
31
|
-
// tsgo-normalized) so the
|
|
52
|
+
// tsgo-normalized) so the dump path mapper receives the same canonical root
|
|
32
53
|
// grammar and drive-letter case as the compiler's source paths.
|
|
33
54
|
root := *cwd
|
|
34
55
|
if abs, err := filepath.Abs(root); err == nil {
|
|
@@ -38,11 +59,11 @@ func run() int {
|
|
|
38
59
|
|
|
39
60
|
prog, _, err := driver.LoadProgram(root, *tsconfig, driver.LoadProgramOptions{})
|
|
40
61
|
if err != nil {
|
|
41
|
-
fmt.Fprintf(
|
|
62
|
+
fmt.Fprintf(stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
|
|
42
63
|
return 1
|
|
43
64
|
}
|
|
44
65
|
if prog == nil {
|
|
45
|
-
fmt.Fprintf(
|
|
66
|
+
fmt.Fprintf(stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
|
|
46
67
|
return 1
|
|
47
68
|
}
|
|
48
69
|
defer func() { _ = prog.Close() }()
|
|
@@ -62,7 +83,11 @@ func run() int {
|
|
|
62
83
|
// No version: this tool is built from the tree on demand and never
|
|
63
84
|
// stamped, and an invented one would be worse than an absent one.
|
|
64
85
|
graph.Producer{Tool: "graphdump", Typescript: graph.TypescriptVersion()},
|
|
65
|
-
|
|
86
|
+
// The declaration walk is the shipped one, so this dump carries every
|
|
87
|
+
// documentation tag and must say so: under the capability contract an
|
|
88
|
+
// absent claim means the producer never looked, which would make a
|
|
89
|
+
// consumer read a tagged declaration as citing nothing.
|
|
90
|
+
[]string{graph.CapabilitySourceDigests, graph.CapabilityDocTags},
|
|
66
91
|
nil,
|
|
67
92
|
nil,
|
|
68
93
|
texts,
|
|
@@ -70,9 +95,9 @@ func run() int {
|
|
|
70
95
|
),
|
|
71
96
|
}, *pretty)
|
|
72
97
|
if err != nil {
|
|
73
|
-
fmt.Fprintf(
|
|
98
|
+
fmt.Fprintf(stderr, "graphdump: %v\n", err)
|
|
74
99
|
return 1
|
|
75
100
|
}
|
|
76
|
-
fmt.
|
|
101
|
+
fmt.Fprintln(stdout, string(data))
|
|
77
102
|
return 0
|
|
78
103
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"slices"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// TestRunDeclaresTheCapabilityItsOutputCarries verifies that every capability
|
|
16
|
+
// this command declares is paired with the output that backs it, and that a
|
|
17
|
+
// project carrying no documentation tag emits that field nowhere.
|
|
18
|
+
//
|
|
19
|
+
// The claim is a separate literal from the shipped `ttscgraph dump`'s, so the
|
|
20
|
+
// test that guards that one guards nothing here: deleting graph.CapabilityDocTags
|
|
21
|
+
// from this command's list used to break nothing at all, in a package that had
|
|
22
|
+
// never had a test file. The claim matters because the two states a consumer must
|
|
23
|
+
// distinguish — "this declaration cites nothing" and "this producer never
|
|
24
|
+
// looked" — are the same absent field, so a dump that carries tags and forgets to
|
|
25
|
+
// say so is read as a repository where nothing cites anything.
|
|
26
|
+
//
|
|
27
|
+
// The declared set is compared whole rather than member by member, so a
|
|
28
|
+
// capability added to the list arrives with the evidence for it or fails here.
|
|
29
|
+
//
|
|
30
|
+
// 1. Run the command over a project with a tag, capturing stdout.
|
|
31
|
+
// 2. Assert the declared set is exactly what this command claims, and that each
|
|
32
|
+
// claim is backed: a tag in the output, and a digested source.
|
|
33
|
+
// 3. Run it over a project with no tag and assert the field appears nowhere,
|
|
34
|
+
// while both claims still do.
|
|
35
|
+
func TestRunDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
|
|
36
|
+
tagged := runGraphdump(t, `/** @evidence docs/a.md#x Cited. */
|
|
37
|
+
export function subject(): void {}
|
|
38
|
+
`)
|
|
39
|
+
untagged := runGraphdump(t, `/** Ordinary documentation. */
|
|
40
|
+
export function subject(): void {}
|
|
41
|
+
`)
|
|
42
|
+
|
|
43
|
+
declared := []string{graph.CapabilityDocTags, graph.CapabilitySourceDigests}
|
|
44
|
+
slices.Sort(declared)
|
|
45
|
+
for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
|
|
46
|
+
var parsed struct {
|
|
47
|
+
Provenance struct {
|
|
48
|
+
Capabilities []string `json:"capabilities"`
|
|
49
|
+
Sources []struct {
|
|
50
|
+
File string `json:"file"`
|
|
51
|
+
CheckerDigest string `json:"checkerDigest"`
|
|
52
|
+
} `json:"sources"`
|
|
53
|
+
} `json:"provenance"`
|
|
54
|
+
}
|
|
55
|
+
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
56
|
+
t.Fatalf("%s dump is not JSON: %v", name, err)
|
|
57
|
+
}
|
|
58
|
+
got := slices.Clone(parsed.Provenance.Capabilities)
|
|
59
|
+
slices.Sort(got)
|
|
60
|
+
if !slices.Equal(got, declared) {
|
|
61
|
+
t.Fatalf("%s dump declares %v, want %v; an absent claim means the producer "+
|
|
62
|
+
"never looked, so a tagged declaration reads as citing nothing and a "+
|
|
63
|
+
"digested source reads as unread",
|
|
64
|
+
name, got, declared)
|
|
65
|
+
}
|
|
66
|
+
// The sourceDigests claim, backed the same way the docTags claim is: a
|
|
67
|
+
// declared capability whose output carries nothing is the tautology this
|
|
68
|
+
// test exists to avoid.
|
|
69
|
+
digested := 0
|
|
70
|
+
for _, source := range parsed.Provenance.Sources {
|
|
71
|
+
if source.CheckerDigest != "" {
|
|
72
|
+
digested++
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if digested == 0 {
|
|
76
|
+
t.Fatalf("%s dump declares %q over %d sources, none of which carries a checker digest",
|
|
77
|
+
name, graph.CapabilitySourceDigests, len(parsed.Provenance.Sources))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
|
|
82
|
+
t.Fatalf("the tagged dump carried no tag text")
|
|
83
|
+
}
|
|
84
|
+
// Read the raw document rather than a decoded slice: an absent field and an
|
|
85
|
+
// empty one decode identically, and absent is what the wire contract says. The
|
|
86
|
+
// key is matched with its colon, because the capability shares the name and
|
|
87
|
+
// rides the same document as a bare string.
|
|
88
|
+
if strings.Contains(untagged, `"docTags":`) {
|
|
89
|
+
t.Fatalf("the untagged dump carried a docTags field; it must be absent")
|
|
90
|
+
}
|
|
91
|
+
if !strings.Contains(tagged, `"docTags":`) {
|
|
92
|
+
t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// runGraphdump runs the command over a one-file project and returns its stdout.
|
|
97
|
+
//
|
|
98
|
+
// It goes through run(args) and the package streams rather than through os.Args,
|
|
99
|
+
// flag.CommandLine, and an os.Stdout pipe. Those seams exist on the shipped
|
|
100
|
+
// sibling for this reason, and this command grew them so its own claim could be
|
|
101
|
+
// asserted against its own output without patching process globals.
|
|
102
|
+
func runGraphdump(t *testing.T, source string) string {
|
|
103
|
+
t.Helper()
|
|
104
|
+
root := t.TempDir()
|
|
105
|
+
writeGraphdumpFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
106
|
+
"compilerOptions": {
|
|
107
|
+
"target": "ES2022",
|
|
108
|
+
"module": "commonjs",
|
|
109
|
+
"strict": true,
|
|
110
|
+
"rootDir": "src",
|
|
111
|
+
"outDir": "dist"
|
|
112
|
+
},
|
|
113
|
+
"files": ["src/main.ts"]
|
|
114
|
+
}
|
|
115
|
+
`)
|
|
116
|
+
writeGraphdumpFile(t, filepath.Join(root, "src", "main.ts"), source)
|
|
117
|
+
|
|
118
|
+
var out, errOut bytes.Buffer
|
|
119
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
120
|
+
stdout, stderr = &out, &errOut
|
|
121
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
122
|
+
|
|
123
|
+
if code := run([]string{"--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
|
|
124
|
+
t.Fatalf("graphdump exited %d: %s", code, errOut.String())
|
|
125
|
+
}
|
|
126
|
+
return out.String()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func writeGraphdumpFile(t *testing.T, path, content string) {
|
|
130
|
+
t.Helper()
|
|
131
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
135
|
+
t.Fatal(err)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestRunReportsAnUnknownFlagWithoutExitingTheProcess verifies that a bad
|
|
10
|
+
// argument returns an exit code and writes usage to the command's own stream,
|
|
11
|
+
// instead of terminating the process.
|
|
12
|
+
//
|
|
13
|
+
// This is the negative twin of the capability case and the reason the seams
|
|
14
|
+
// exist. The command used to parse flag.CommandLine, whose default error
|
|
15
|
+
// handling is flag.ExitOnError: a malformed argument called os.Exit(2) from
|
|
16
|
+
// inside run, which no test can observe and which would take a test binary down
|
|
17
|
+
// with it. Nothing else about the command is reachable while that is true.
|
|
18
|
+
//
|
|
19
|
+
// 1. Run the command with an unknown flag, capturing both streams.
|
|
20
|
+
// 2. Assert it returns 2 rather than exiting, and that nothing reached stdout.
|
|
21
|
+
// 3. Assert the usage text went to the command's stderr stream.
|
|
22
|
+
// 4. Assert `-h` still succeeds, because asking for usage is not an error.
|
|
23
|
+
func TestRunReportsAnUnknownFlagWithoutExitingTheProcess(t *testing.T) {
|
|
24
|
+
var out, errOut bytes.Buffer
|
|
25
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
26
|
+
stdout, stderr = &out, &errOut
|
|
27
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
28
|
+
|
|
29
|
+
code := run([]string{"--not-a-flag"})
|
|
30
|
+
|
|
31
|
+
if code != 2 {
|
|
32
|
+
t.Fatalf("graphdump exited %d for an unknown flag, want 2", code)
|
|
33
|
+
}
|
|
34
|
+
if out.Len() != 0 {
|
|
35
|
+
t.Fatalf("a rejected invocation wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
|
|
36
|
+
}
|
|
37
|
+
if !strings.Contains(errOut.String(), "not-a-flag") {
|
|
38
|
+
t.Fatalf("stderr does not name the rejected flag: %q", errOut.String())
|
|
39
|
+
}
|
|
40
|
+
if !strings.Contains(errOut.String(), "-tsconfig") {
|
|
41
|
+
t.Fatalf("stderr carried no usage text: %q", errOut.String())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The negative twin of the rejection: `flag.ContinueOnError` reports `-h` as
|
|
45
|
+
// an error too, and mapping every parse failure to 2 would turn the help flag
|
|
46
|
+
// into a failed invocation. The global flag set this command used to read
|
|
47
|
+
// exited 0 for it.
|
|
48
|
+
out.Reset()
|
|
49
|
+
errOut.Reset()
|
|
50
|
+
if code := run([]string{"-h"}); code != 0 {
|
|
51
|
+
t.Fatalf("graphdump exited %d for -h, want 0", code)
|
|
52
|
+
}
|
|
53
|
+
if !strings.Contains(errOut.String(), "-tsconfig") {
|
|
54
|
+
t.Fatalf("-h printed no usage text: %q", errOut.String())
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestRunReportsAnUnloadableProjectWithoutPrintingADump verifies that a project
|
|
10
|
+
// the command cannot load exits non-zero, prints nothing to stdout, and names
|
|
11
|
+
// what it could not load.
|
|
12
|
+
//
|
|
13
|
+
// The benchmark viewer pipeline runs this command through `execFileSync`, which
|
|
14
|
+
// raises only on a non-zero exit and otherwise parses stdout as JSON. Every
|
|
15
|
+
// failure path here therefore has to be a non-zero exit: turning one into 0
|
|
16
|
+
// hands the pipeline an empty string, and the caller dies inside its JSON parse
|
|
17
|
+
// with an opaque error instead of reading the diagnostic this command already
|
|
18
|
+
// wrote. All three failure paths used to be unpinned, so replacing every
|
|
19
|
+
// `return 1` with `return 0` broke no test.
|
|
20
|
+
//
|
|
21
|
+
// 1. Run the command against a tsconfig that does not exist.
|
|
22
|
+
// 2. Assert it returns 1 and wrote nothing to stdout.
|
|
23
|
+
// 3. Assert stderr names the command and the path it could not load.
|
|
24
|
+
func TestRunReportsAnUnloadableProjectWithoutPrintingADump(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
|
|
27
|
+
var out, errOut bytes.Buffer
|
|
28
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
29
|
+
stdout, stderr = &out, &errOut
|
|
30
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
31
|
+
|
|
32
|
+
code := run([]string{"--cwd", root, "--tsconfig", "absent.json"})
|
|
33
|
+
|
|
34
|
+
if code != 1 {
|
|
35
|
+
t.Fatalf("graphdump exited %d for an unloadable project, want 1", code)
|
|
36
|
+
}
|
|
37
|
+
if out.Len() != 0 {
|
|
38
|
+
t.Fatalf("a failed run wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
|
|
39
|
+
}
|
|
40
|
+
if !strings.Contains(errOut.String(), "graphdump:") {
|
|
41
|
+
t.Fatalf("stderr does not identify the command: %q", errOut.String())
|
|
42
|
+
}
|
|
43
|
+
if !strings.Contains(errOut.String(), "absent.json") {
|
|
44
|
+
t.Fatalf("stderr does not name the tsconfig it could not load: %q", errOut.String())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -26,6 +26,18 @@ type apiTransformResult struct {
|
|
|
26
26
|
// content can influence a transformed module, so bundler caches invalidate
|
|
27
27
|
// soundly without per-plugin reporting.
|
|
28
28
|
Graph *driver.TransformGraph `json:"graph,omitempty"`
|
|
29
|
+
// Dependencies and DependenciesComplete are the envelope's dependency side
|
|
30
|
+
// channel. This lane runs no plugin transform at all — each output is the
|
|
31
|
+
// file's own parsed text — so every file is complete with an empty list
|
|
32
|
+
// unless a linked plugin is active and declares otherwise; see
|
|
33
|
+
// driver.Program.TransformDependenciesFor.
|
|
34
|
+
Dependencies map[string][]string `json:"dependencies,omitempty"`
|
|
35
|
+
DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
|
|
36
|
+
// HostInputs are absolute native plugin config files evaluated while this
|
|
37
|
+
// generation loaded. JavaScript hosts merge them with descriptor inputs.
|
|
38
|
+
HostInputs []string `json:"hostInputs,omitempty"`
|
|
39
|
+
HostInputHashes map[string]*string `json:"hostInputHashes,omitempty"`
|
|
40
|
+
HostInputRealpaths map[string]*string `json:"hostInputRealpaths,omitempty"`
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
// runAPITransform implements the `api-transform` sub-command. It loads the
|
|
@@ -69,7 +81,11 @@ func runAPITransform(args []string) int {
|
|
|
69
81
|
return 2
|
|
70
82
|
}
|
|
71
83
|
typescript := map[string]string{}
|
|
84
|
+
var dependencies driver.TransformDependencies
|
|
72
85
|
var graph *driver.TransformGraph
|
|
86
|
+
var hostInputs []string
|
|
87
|
+
var hostInputHashes map[string]*string
|
|
88
|
+
var hostInputRealpaths map[string]*string
|
|
73
89
|
if prog != nil {
|
|
74
90
|
defer prog.Close()
|
|
75
91
|
// Compute the reference graph before SourceFiles() runs linked plugin
|
|
@@ -81,12 +97,21 @@ func runAPITransform(args []string) int {
|
|
|
81
97
|
typescript[apiOutputKey(cwd, file.FileName())] = file.Text()
|
|
82
98
|
}
|
|
83
99
|
diags = append(diags, prog.Diagnostics()...)
|
|
100
|
+
dependencies = prog.TransformDependenciesFor(cwd)
|
|
101
|
+
hostInputs = prog.PluginHostInputs()
|
|
102
|
+
hostInputHashes = prog.PluginHostInputHashes()
|
|
103
|
+
hostInputRealpaths = prog.PluginHostInputRealpaths()
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
result := apiTransformResult{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
Dependencies: dependencies.Dependencies,
|
|
108
|
+
DependenciesComplete: dependencies.Complete,
|
|
109
|
+
Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
|
|
110
|
+
Graph: graph,
|
|
111
|
+
HostInputs: hostInputs,
|
|
112
|
+
HostInputHashes: hostInputHashes,
|
|
113
|
+
HostInputRealpaths: hostInputRealpaths,
|
|
114
|
+
TypeScript: typescript,
|
|
90
115
|
}
|
|
91
116
|
for _, diag := range diags {
|
|
92
117
|
result.Diagnostics = append(result.Diagnostics, toAPICompileDiagnostic(diag))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"os/exec"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"runtime"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestAuxiliaryIdentityStateTracksRetargetsNotSourceContents proves the two
|
|
12
|
+
// owners of a selected lexical module path stay separate:
|
|
13
|
+
//
|
|
14
|
+
// 1. ordinary source bytes are owned by graphSession.sourceHashes, so an
|
|
15
|
+
// identity-only auxiliary state must not turn their edit into a reload;
|
|
16
|
+
// 2. retargeting the same lexical symlink or junction must invalidate even
|
|
17
|
+
// when the old and new target bytes agree.
|
|
18
|
+
func TestAuxiliaryIdentityStateTracksRetargetsNotSourceContents(t *testing.T) {
|
|
19
|
+
root := t.TempDir()
|
|
20
|
+
first := filepath.Join(root, "first")
|
|
21
|
+
second := filepath.Join(root, "second")
|
|
22
|
+
if err := os.MkdirAll(first, 0o755); err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
if err := os.MkdirAll(second, 0o755); err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
for _, directory := range []string{first, second} {
|
|
29
|
+
if err := os.WriteFile(filepath.Join(directory, "selection.ts"), []byte("export const value = 1;\n"), 0o644); err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
link := filepath.Join(root, "linked")
|
|
35
|
+
createAuxiliaryDirectoryLink(t, first, link)
|
|
36
|
+
selected := filepath.Join(link, "selection.ts")
|
|
37
|
+
previous := captureDiskStates([]auxiliaryInput{{path: selected, identityOnly: true}})
|
|
38
|
+
initial := previous[selected]
|
|
39
|
+
if !initial.Exists || initial.Realpath == "" || !initial.IdentityOnly {
|
|
40
|
+
t.Fatalf("initial identity state = %+v", initial)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if err := os.WriteFile(filepath.Join(first, "selection.ts"), []byte("export const value = 2;\n"), 0o644); err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
if diskStatesChanged(previous) {
|
|
47
|
+
t.Fatal("selected source contents leaked into identity-only auxiliary invalidation")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if err := os.Remove(link); err != nil {
|
|
51
|
+
t.Fatal(err)
|
|
52
|
+
}
|
|
53
|
+
createAuxiliaryDirectoryLink(t, second, link)
|
|
54
|
+
if !diskStatesChanged(previous) {
|
|
55
|
+
t.Fatal("same-byte lexical link retarget did not invalidate its physical identity")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
merged := compactAuxiliaryInputs([]auxiliaryInput{
|
|
59
|
+
{path: selected, identityOnly: true},
|
|
60
|
+
{path: selected},
|
|
61
|
+
})
|
|
62
|
+
if len(merged) != 1 || merged[0].identityOnly {
|
|
63
|
+
t.Fatalf("content-sensitive duplicate did not win: %+v", merged)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func createAuxiliaryDirectoryLink(t *testing.T, target, link string) {
|
|
68
|
+
t.Helper()
|
|
69
|
+
if runtime.GOOS == "windows" {
|
|
70
|
+
command := exec.Command(
|
|
71
|
+
"node",
|
|
72
|
+
"-e",
|
|
73
|
+
`require("node:fs").symlinkSync(process.argv[1], process.argv[2], "junction")`,
|
|
74
|
+
target,
|
|
75
|
+
link,
|
|
76
|
+
)
|
|
77
|
+
if output, err := command.CombinedOutput(); err != nil {
|
|
78
|
+
t.Skipf("directory junction unavailable on this host: %v: %s", err, output)
|
|
79
|
+
}
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if err := os.Symlink(target, link); err != nil {
|
|
83
|
+
t.Skipf("directory symlink unavailable on this host: %v", err)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import "path/filepath"
|
|
6
|
+
|
|
7
|
+
func diskRealpath(path string) string {
|
|
8
|
+
resolved, err := filepath.EvalSymlinks(path)
|
|
9
|
+
if err != nil {
|
|
10
|
+
return ""
|
|
11
|
+
}
|
|
12
|
+
if absolute, err := filepath.Abs(resolved); err == nil {
|
|
13
|
+
resolved = absolute
|
|
14
|
+
}
|
|
15
|
+
return filepath.Clean(resolved)
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"golang.org/x/sys/windows"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// diskRealpath asks the opened object for its final path. filepath.EvalSymlinks
|
|
13
|
+
// can leave a readable file below a directory junction unresolved on Windows,
|
|
14
|
+
// which would erase the physical identity this state exists to retain.
|
|
15
|
+
func diskRealpath(path string) string {
|
|
16
|
+
pointer, err := windows.UTF16PtrFromString(path)
|
|
17
|
+
if err != nil {
|
|
18
|
+
return ""
|
|
19
|
+
}
|
|
20
|
+
handle, err := windows.CreateFile(
|
|
21
|
+
pointer,
|
|
22
|
+
windows.FILE_READ_ATTRIBUTES,
|
|
23
|
+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
|
|
24
|
+
nil,
|
|
25
|
+
windows.OPEN_EXISTING,
|
|
26
|
+
windows.FILE_FLAG_BACKUP_SEMANTICS,
|
|
27
|
+
0,
|
|
28
|
+
)
|
|
29
|
+
if err != nil {
|
|
30
|
+
return ""
|
|
31
|
+
}
|
|
32
|
+
defer windows.CloseHandle(handle)
|
|
33
|
+
|
|
34
|
+
buffer := make([]uint16, windows.MAX_PATH)
|
|
35
|
+
for {
|
|
36
|
+
length, err := windows.GetFinalPathNameByHandle(
|
|
37
|
+
handle,
|
|
38
|
+
&buffer[0],
|
|
39
|
+
uint32(len(buffer)),
|
|
40
|
+
0,
|
|
41
|
+
)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return ""
|
|
44
|
+
}
|
|
45
|
+
if length < uint32(len(buffer)) {
|
|
46
|
+
resolved := windows.UTF16ToString(buffer[:length])
|
|
47
|
+
if strings.HasPrefix(resolved, `\\?\UNC\`) {
|
|
48
|
+
resolved = `\\` + strings.TrimPrefix(resolved, `\\?\UNC\`)
|
|
49
|
+
} else {
|
|
50
|
+
resolved = strings.TrimPrefix(resolved, `\\?\`)
|
|
51
|
+
}
|
|
52
|
+
return filepath.Clean(resolved)
|
|
53
|
+
}
|
|
54
|
+
buffer = make([]uint16, length+1)
|
|
55
|
+
}
|
|
56
|
+
}
|