ttsc 0.13.1 → 0.14.0-dev.20260528.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -3
- package/cmd/ttscserver/main.go +56 -14
- package/cmd/ttscserver/run_lsp_forwards_execute_command_advertisement_flags_test.go +55 -0
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +45 -0
- package/driver/lsp.go +21 -1
- package/internal/lspserver/lsp_frame.go +30 -10
- package/internal/lspserver/lsp_native_plugin_source.go +380 -0
- package/internal/lspserver/lsp_plugin.go +16 -11
- package/internal/lspserver/lsp_proxy.go +1153 -73
- package/internal/lspserver/lsp_server.go +17 -5
- package/lib/TtscCompiler.d.ts +5 -4
- package/lib/TtscCompiler.js +56 -4
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +9 -7
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/getCompilerVersionText.js +6 -14
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.d.ts +1 -0
- package/lib/launcher/internal/prepareExecution.js +31 -16
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +3 -1
- package/lib/launcher/internal/runTtscserver.js +88 -11
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +39 -26
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +1 -1
- package/lib/structures/ITtscCompilerResult.d.ts +11 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +14 -3
- package/lib/structures/ITtscPlugin.d.ts +24 -15
- package/lib/structures/ITtscPluginCapabilities.d.ts +17 -6
- package/lib/structures/ITtscPluginContributor.d.ts +4 -2
- package/lib/structures/ITtscPluginFactoryContext.d.ts +3 -3
- package/lib/structures/ITtscProjectPluginConfig.d.ts +3 -3
- package/lib/structures/TtscPluginStage.d.ts +4 -3
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +7 -4
- package/package.json +10 -10
- package/shim/ast/lint.go +1 -1
- package/shim/checker/shim.go +3 -0
- package/shim/core/shim.go +11 -0
- package/src/TtscCompiler.ts +67 -4
- package/src/compiler/internal/transformProjectInMemory.ts +10 -5
- package/src/launcher/internal/getCompilerVersionText.ts +4 -13
- package/src/launcher/internal/prepareExecution.ts +31 -16
- package/src/launcher/internal/runTtscserver.ts +105 -11
- package/src/launcher/internal/runTtsx.ts +41 -29
- package/src/structures/ITtscCompilerContext.ts +1 -1
- package/src/structures/ITtscCompilerResult.ts +12 -0
- package/src/structures/ITtscCompilerTransformation.ts +15 -3
- package/src/structures/ITtscPlugin.ts +24 -15
- package/src/structures/ITtscPluginCapabilities.ts +18 -6
- package/src/structures/ITtscPluginContributor.ts +4 -2
- package/src/structures/ITtscPluginFactoryContext.ts +3 -3
- package/src/structures/ITtscProjectPluginConfig.ts +3 -3
- package/src/structures/TtscPluginStage.ts +4 -3
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +7 -4
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ npx ttsc format
|
|
|
57
57
|
|
|
58
58
|
### Editor
|
|
59
59
|
|
|
60
|
-
Install the VS Code extension
|
|
60
|
+
Install the VS Code extension for live TypeScript-Go editor features plus
|
|
61
|
+
saved-state ttsc plugin diagnostics and actions:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
npm install -D @ttsc/vscode
|
|
@@ -143,11 +144,12 @@ interface IMember {
|
|
|
143
144
|
The transform replaces `typia.is<IMember>()` with dedicated JavaScript checks at build time:
|
|
144
145
|
|
|
145
146
|
```js
|
|
146
|
-
import
|
|
147
|
+
import typia from "typia";
|
|
147
148
|
import * as __typia_transform__isFormatEmail from "typia/lib/internal/_isFormatEmail";
|
|
149
|
+
import * as __typia_transform__isFormatUuid from "typia/lib/internal/_isFormatUuid";
|
|
148
150
|
import * as __typia_transform__isTypeUint32 from "typia/lib/internal/_isTypeUint32";
|
|
149
|
-
import typia from "typia";
|
|
150
151
|
import { v4 } from "uuid";
|
|
152
|
+
|
|
151
153
|
const matched = (() => {
|
|
152
154
|
const _io0 = (input) =>
|
|
153
155
|
"string" === typeof input.id &&
|
|
@@ -167,6 +169,34 @@ const matched = (() => {
|
|
|
167
169
|
console.log(matched); // true
|
|
168
170
|
```
|
|
169
171
|
|
|
172
|
+
## Programmatic API
|
|
173
|
+
|
|
174
|
+
Embed `ttsc` from another Node tool with the `TtscCompiler` class:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { TtscCompiler } from "ttsc";
|
|
178
|
+
|
|
179
|
+
const compiler = new TtscCompiler({ cwd: "./project" });
|
|
180
|
+
const result = compiler.compile();
|
|
181
|
+
|
|
182
|
+
if (result.type === "success") {
|
|
183
|
+
for (const [path, text] of Object.entries(result.output)) {
|
|
184
|
+
// path is project-relative ("dist/index.js", "dist/index.d.ts", ...)
|
|
185
|
+
console.log(path, text.length);
|
|
186
|
+
}
|
|
187
|
+
} else if (result.type === "failure") {
|
|
188
|
+
for (const d of result.diagnostics) {
|
|
189
|
+
console.error(`${d.file}:${d.line}:${d.character} ${d.messageText}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
See the [Programmatic API guide](https://ttsc.dev/docs/ttsc/api) for the full
|
|
195
|
+
lifecycle, plugin overrides, and patterns. For browser embedding, see
|
|
196
|
+
[`@ttsc/wasm`](https://github.com/samchon/ttsc/tree/master/packages/wasm) and
|
|
197
|
+
the higher-level [`@ttsc/playground`](https://github.com/samchon/ttsc/tree/master/packages/playground)
|
|
198
|
+
package.
|
|
199
|
+
|
|
170
200
|
### List of Plugins
|
|
171
201
|
|
|
172
202
|
`ttsc` ships a few small utility plugins in this repository.
|
package/cmd/ttscserver/main.go
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Command ttscserver is the Go LSP host shipped by ttsc. It wraps the
|
|
2
2
|
// project-selected TypeScript-Go LSP server process and proxies traffic
|
|
3
|
-
// between the editor and that server
|
|
4
|
-
//
|
|
3
|
+
// between the editor and that server. The JavaScript launcher resolves project
|
|
4
|
+
// plugins first and passes an LSP manifest so this command can merge plugin
|
|
5
|
+
// diagnostics, code actions, and ttsc-owned executeCommand handling.
|
|
5
6
|
//
|
|
6
7
|
// The JavaScript launcher (`packages/ttsc/src/launcher/ttscserver.ts`)
|
|
7
8
|
// resolves the native binary and forwards stdio so editors can spawn
|
|
@@ -86,8 +87,13 @@ func runLSP(args []string) int {
|
|
|
86
87
|
fs.SetOutput(stderr)
|
|
87
88
|
stdioFlag := fs.Bool("stdio", false, "communicate with the editor over stdin/stdout")
|
|
88
89
|
cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
|
|
90
|
+
tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
|
|
89
91
|
tsgoFlag := fs.String("tsgo", "", "absolute tsgo binary path (defaults to TTSC_TSGO_BINARY)")
|
|
90
|
-
progressDelayFlag := fs.Duration("progress-delay", 250*time.Millisecond, "
|
|
92
|
+
progressDelayFlag := fs.Duration("progress-delay", 250*time.Millisecond, "accepted for compatibility; ignored by the external tsgo LSP process")
|
|
93
|
+
suppressExecuteCommandProviderFlag := fs.Bool("suppress-execute-command-provider", false, "do not advertise ttsc executeCommand ids during initialize")
|
|
94
|
+
suppressExecuteCommandIDsFlag := fs.String("suppress-execute-command-ids", "", "comma-separated ttsc executeCommand ids to omit during initialize")
|
|
95
|
+
executeCommandIDPrefixFlag := fs.String("execute-command-id-prefix", "", "prefix to apply to advertised executeCommand ids")
|
|
96
|
+
_ = fs.String("clientProcessId", "", "ignored VSCode language-client compatibility flag")
|
|
91
97
|
if err := fs.Parse(args); err != nil {
|
|
92
98
|
return 2
|
|
93
99
|
}
|
|
@@ -114,14 +120,28 @@ func runLSP(args []string) int {
|
|
|
114
120
|
tsgoBinary = strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY"))
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
err :=
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
source, err := lspserver.NewNativePluginSource(lspserver.NativePluginSourceOptions{
|
|
124
|
+
Cwd: cwd,
|
|
125
|
+
Err: stderr,
|
|
126
|
+
ManifestJSON: os.Getenv("TTSC_LSP_PLUGINS_JSON"),
|
|
127
|
+
Tsconfig: strings.TrimSpace(*tsconfigFlag),
|
|
128
|
+
})
|
|
129
|
+
if err != nil {
|
|
130
|
+
fmt.Fprintf(stderr, "ttscserver: %v\n", err)
|
|
131
|
+
return 2
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
err = runLSPServer(ctx, lspserver.LSPServerOptions{
|
|
135
|
+
In: stdin,
|
|
136
|
+
Out: stdout,
|
|
137
|
+
Err: stderr,
|
|
138
|
+
Cwd: cwd,
|
|
139
|
+
TsgoBinary: tsgoBinary,
|
|
140
|
+
Source: source,
|
|
141
|
+
SuppressExecuteCommandProvider: *suppressExecuteCommandProviderFlag,
|
|
142
|
+
SuppressedExecuteCommandIDs: splitCSV(*suppressExecuteCommandIDsFlag),
|
|
143
|
+
ExecuteCommandIDPrefix: strings.TrimSpace(*executeCommandIDPrefixFlag),
|
|
144
|
+
ProgressDelay: *progressDelayFlag,
|
|
125
145
|
})
|
|
126
146
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
127
147
|
fmt.Fprintf(stderr, "ttscserver: %v\n", err)
|
|
@@ -155,14 +175,36 @@ Usage:
|
|
|
155
175
|
Options:
|
|
156
176
|
--stdio Communicate with the editor over stdin/stdout.
|
|
157
177
|
--cwd <dir> Project root used as the tsgo server working directory.
|
|
178
|
+
--tsconfig <path> Project config path used by ttsc plugin sidecars.
|
|
158
179
|
--tsgo <path> Absolute tsgo binary path (defaults to TTSC_TSGO_BINARY).
|
|
159
|
-
--
|
|
180
|
+
--suppress-execute-command-provider
|
|
181
|
+
Do not advertise ttsc executeCommand ids during initialize.
|
|
182
|
+
--suppress-execute-command-ids <ids>
|
|
183
|
+
Comma-separated executeCommand ids to omit during initialize.
|
|
184
|
+
--execute-command-id-prefix <prefix>
|
|
185
|
+
Prefix advertised executeCommand ids for multi-client hosts.
|
|
186
|
+
--progress-delay D Accepted for compatibility; currently ignored by the external tsgo LSP process.
|
|
160
187
|
|
|
161
188
|
Typical embedding:
|
|
162
189
|
Editors spawn ttscserver via the JavaScript launcher (resolves the
|
|
163
190
|
per-platform native binary and passes the project tsgo path) and exchange
|
|
164
191
|
LSP messages over stdio. The upstream tsgo server provides hover,
|
|
165
|
-
completion, definitions, and diagnostics
|
|
166
|
-
|
|
192
|
+
completion, definitions, and diagnostics. LSP-capable ttsc sidecars are
|
|
193
|
+
discovered by the JavaScript launcher and merged into the same stream.
|
|
167
194
|
`))
|
|
168
195
|
}
|
|
196
|
+
|
|
197
|
+
func splitCSV(value string) []string {
|
|
198
|
+
if value == "" {
|
|
199
|
+
return nil
|
|
200
|
+
}
|
|
201
|
+
fields := strings.Split(value, ",")
|
|
202
|
+
out := make([]string, 0, len(fields))
|
|
203
|
+
for _, field := range fields {
|
|
204
|
+
trimmed := strings.TrimSpace(field)
|
|
205
|
+
if trimmed != "" {
|
|
206
|
+
out = append(out, trimmed)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out
|
|
210
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"reflect"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// TestRunLSPForwardsExecuteCommandAdvertisementFlags verifies the native
|
|
13
|
+
// command passes executeCommand registration controls into the LSP host.
|
|
14
|
+
//
|
|
15
|
+
// The VS Code extension relies on these flags to suppress wrapper-owned command
|
|
16
|
+
// ids and namespace custom plugin commands per project root. Parsing the flags
|
|
17
|
+
// in the command is not enough; this pins the handoff into LSPServerOptions.
|
|
18
|
+
//
|
|
19
|
+
// 1. Substitute the runLSPServer seam and capture its options.
|
|
20
|
+
// 2. Run runLSP with suppression and prefix flags.
|
|
21
|
+
// 3. Assert the captured LSPServerOptions carry the parsed values.
|
|
22
|
+
func TestRunLSPForwardsExecuteCommandAdvertisementFlags(t *testing.T) {
|
|
23
|
+
prev := runLSPServer
|
|
24
|
+
var captured lspserver.LSPServerOptions
|
|
25
|
+
runLSPServer = func(_ context.Context, opts lspserver.LSPServerOptions) error {
|
|
26
|
+
captured = opts
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
defer func() { runLSPServer = prev }()
|
|
30
|
+
|
|
31
|
+
outBuf := &bytes.Buffer{}
|
|
32
|
+
errBuf := &bytes.Buffer{}
|
|
33
|
+
withIO(t, outBuf, errBuf, nil, func() {
|
|
34
|
+
if code := runLSP([]string{
|
|
35
|
+
"--stdio",
|
|
36
|
+
"--cwd", t.TempDir(),
|
|
37
|
+
"--suppress-execute-command-provider",
|
|
38
|
+
"--suppress-execute-command-ids", " ttsc.lint.fixAll, ttsc.format.document ,, ",
|
|
39
|
+
"--execute-command-id-prefix", " ttsc.vscode.root. ",
|
|
40
|
+
}); code != 0 {
|
|
41
|
+
t.Fatalf("expected exit 0, got %d (stderr=%q)", code, errBuf.String())
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if !captured.SuppressExecuteCommandProvider {
|
|
46
|
+
t.Fatal("expected SuppressExecuteCommandProvider to be true")
|
|
47
|
+
}
|
|
48
|
+
expectedIDs := []string{"ttsc.lint.fixAll", "ttsc.format.document"}
|
|
49
|
+
if !reflect.DeepEqual(captured.SuppressedExecuteCommandIDs, expectedIDs) {
|
|
50
|
+
t.Fatalf("expected suppressed ids %#v, got %#v", expectedIDs, captured.SuppressedExecuteCommandIDs)
|
|
51
|
+
}
|
|
52
|
+
if captured.ExecuteCommandIDPrefix != "ttsc.vscode.root." {
|
|
53
|
+
t.Fatalf("expected trimmed execute command prefix, got %q", captured.ExecuteCommandIDPrefix)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestRunLSPUsesNativePluginManifestSource verifies the command wires the
|
|
12
|
+
// launcher-provided plugin manifest into the proxy source.
|
|
13
|
+
//
|
|
14
|
+
// A previous implementation always passed NullPluginSource, so editor sessions
|
|
15
|
+
// could never receive ttsc plugin diagnostics or commands. This test pins the
|
|
16
|
+
// command-level seam without starting tsgo: the JavaScript launcher owns plugin
|
|
17
|
+
// discovery, and the Go command must turn its manifest into a NativePluginSource.
|
|
18
|
+
//
|
|
19
|
+
// 1. Set TTSC_LSP_PLUGINS_JSON to an empty but valid manifest.
|
|
20
|
+
// 2. Substitute runLSPServer and capture its LSPServerOptions.
|
|
21
|
+
// 3. Run `ttscserver --stdio` against a temp cwd.
|
|
22
|
+
// 4. Assert Source is the native sidecar-backed implementation.
|
|
23
|
+
func TestRunLSPUsesNativePluginManifestSource(t *testing.T) {
|
|
24
|
+
t.Setenv("TTSC_LSP_PLUGINS_JSON", `{"plugins":[],"lspPlugins":[]}`)
|
|
25
|
+
|
|
26
|
+
prev := runLSPServer
|
|
27
|
+
var captured lspserver.LSPServerOptions
|
|
28
|
+
runLSPServer = func(_ context.Context, opts lspserver.LSPServerOptions) error {
|
|
29
|
+
captured = opts
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
defer func() { runLSPServer = prev }()
|
|
33
|
+
|
|
34
|
+
outBuf := &bytes.Buffer{}
|
|
35
|
+
errBuf := &bytes.Buffer{}
|
|
36
|
+
withIO(t, outBuf, errBuf, nil, func() {
|
|
37
|
+
if code := runLSP([]string{"--stdio", "--cwd", t.TempDir(), "--tsconfig", "tsconfig.app.json"}); code != 0 {
|
|
38
|
+
t.Fatalf("expected exit 0, got %d (stderr=%q)", code, errBuf.String())
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if _, ok := captured.Source.(*lspserver.NativePluginSource); !ok {
|
|
43
|
+
t.Fatalf("expected NativePluginSource, got %T", captured.Source)
|
|
44
|
+
}
|
|
45
|
+
}
|
package/driver/lsp.go
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lsp.go re-exports the public LSP surface from internal/lspserver so that
|
|
2
|
-
// downstream consumers (plugins, the
|
|
2
|
+
// downstream consumers (plugins, the VS Code extension, tests) import only the
|
|
3
3
|
// driver package rather than reaching into the internal package directly. All
|
|
4
4
|
// symbols are type aliases or var assignments so they are interchangeable with
|
|
5
5
|
// the originals at the call site.
|
|
@@ -57,6 +57,21 @@ type PluginSource = lspserver.PluginSource
|
|
|
57
57
|
// NullPluginSource is the driver-level alias for lspserver.NullPluginSource.
|
|
58
58
|
type NullPluginSource = lspserver.NullPluginSource
|
|
59
59
|
|
|
60
|
+
// NativePluginManifest is the driver-level alias for lspserver.NativePluginManifest.
|
|
61
|
+
type NativePluginManifest = lspserver.NativePluginManifest
|
|
62
|
+
|
|
63
|
+
// NativePluginConfigEntry is the driver-level alias for lspserver.NativePluginConfigEntry.
|
|
64
|
+
type NativePluginConfigEntry = lspserver.NativePluginConfigEntry
|
|
65
|
+
|
|
66
|
+
// NativeLSPPluginEntry is the driver-level alias for lspserver.NativeLSPPluginEntry.
|
|
67
|
+
type NativeLSPPluginEntry = lspserver.NativeLSPPluginEntry
|
|
68
|
+
|
|
69
|
+
// NativePluginSourceOptions is the driver-level alias for lspserver.NativePluginSourceOptions.
|
|
70
|
+
type NativePluginSourceOptions = lspserver.NativePluginSourceOptions
|
|
71
|
+
|
|
72
|
+
// NativePluginSource is the driver-level alias for lspserver.NativePluginSource.
|
|
73
|
+
type NativePluginSource = lspserver.NativePluginSource
|
|
74
|
+
|
|
60
75
|
// ProxyOptions is the driver-level alias for lspserver.ProxyOptions.
|
|
61
76
|
type ProxyOptions = lspserver.ProxyOptions
|
|
62
77
|
|
|
@@ -79,6 +94,10 @@ type LSPUpstreamRunner = lspserver.LSPUpstreamRunner
|
|
|
79
94
|
// the proxy will read without returning ErrFrameTooLarge.
|
|
80
95
|
const MaxFrameBytes = lspserver.MaxFrameBytes
|
|
81
96
|
|
|
97
|
+
// MaxHeaderBytes is the maximum byte length of a JSON-RPC frame header block
|
|
98
|
+
// the proxy will read without returning ErrFrameTooLarge.
|
|
99
|
+
const MaxHeaderBytes = lspserver.MaxHeaderBytes
|
|
100
|
+
|
|
82
101
|
// Sentinel errors forwarded from lspserver.
|
|
83
102
|
var ErrCommandNotHandled = lspserver.ErrCommandNotHandled
|
|
84
103
|
var ErrFrameClosed = lspserver.ErrFrameClosed
|
|
@@ -90,6 +109,7 @@ var ErrLSPTsgoBinaryRequired = lspserver.ErrLSPTsgoBinaryRequired
|
|
|
90
109
|
|
|
91
110
|
// Constructor and utility functions forwarded from lspserver.
|
|
92
111
|
var NewProxy = lspserver.NewProxy
|
|
112
|
+
var NewNativePluginSource = lspserver.NewNativePluginSource
|
|
93
113
|
var NewFrameReader = lspserver.NewFrameReader
|
|
94
114
|
var WriteFrame = lspserver.WriteFrame
|
|
95
115
|
var ParseEnvelope = lspserver.ParseEnvelope
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// JSON-RPC Content-Length framing as used by LSP over stdio. ttscserver
|
|
2
2
|
// wraps tsgo's LSP server but sits between it and the editor so it can
|
|
3
3
|
// merge plugin diagnostics into outgoing publishDiagnostics and inject
|
|
4
|
-
// ttsc-owned code actions. Both legs of the
|
|
4
|
+
// ttsc-owned code actions and executeCommand results. Both legs of the
|
|
5
|
+
// proxy work on raw bytes.
|
|
5
6
|
//
|
|
6
7
|
// Headers are preserved when read so callers that inspect Content-Type
|
|
7
8
|
// or vendor-specific headers can react to them; outgoing frames written
|
|
@@ -23,11 +24,11 @@ import (
|
|
|
23
24
|
// shut down a pump loop without surfacing an error to the editor.
|
|
24
25
|
var ErrFrameClosed = errors.New("lsp: frame stream closed")
|
|
25
26
|
|
|
26
|
-
// ErrFrameTooLarge reports that a peer
|
|
27
|
-
// the safety cap. ttscserver bounds
|
|
28
|
-
// editor or compromised pipe cannot drive the proxy OOM with a single
|
|
29
|
-
// gigabyte-scale
|
|
30
|
-
var ErrFrameTooLarge = errors.New("lsp: frame
|
|
27
|
+
// ErrFrameTooLarge reports that a peer sent a header block or announced a
|
|
28
|
+
// Content-Length above the safety cap. ttscserver bounds both defensively so a
|
|
29
|
+
// confused editor or compromised pipe cannot drive the proxy OOM with a single
|
|
30
|
+
// gigabyte-scale frame.
|
|
31
|
+
var ErrFrameTooLarge = errors.New("lsp: frame exceeds maximum size")
|
|
31
32
|
|
|
32
33
|
// MaxFrameBytes caps Content-Length to 64 MiB. The largest payload the
|
|
33
34
|
// LSP base protocol routinely produces is workspace symbol responses
|
|
@@ -35,6 +36,11 @@ var ErrFrameTooLarge = errors.New("lsp: frame body exceeds maximum size")
|
|
|
35
36
|
// editor traffic while still capping attacker-supplied lengths.
|
|
36
37
|
const MaxFrameBytes = 64 << 20
|
|
37
38
|
|
|
39
|
+
// MaxHeaderBytes caps the LSP header block to 64 KiB. Real LSP traffic carries
|
|
40
|
+
// only Content-Length plus occasional Content-Type, so anything larger is a
|
|
41
|
+
// malformed peer rather than useful protocol data.
|
|
42
|
+
const MaxHeaderBytes = 64 << 10
|
|
43
|
+
|
|
38
44
|
// FrameReader decodes Content-Length-framed JSON-RPC messages from r. It
|
|
39
45
|
// is intentionally permissive about extra headers (we forward whatever the
|
|
40
46
|
// peer sent) and strict about the Content-Length value because tsgo's
|
|
@@ -56,13 +62,27 @@ func (fr *FrameReader) Read() (headers string, body []byte, err error) {
|
|
|
56
62
|
var headerBuf strings.Builder
|
|
57
63
|
contentLength := -1
|
|
58
64
|
for {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
var lineBuf strings.Builder
|
|
66
|
+
for {
|
|
67
|
+
chunk, lineErr := fr.br.ReadSlice('\n')
|
|
68
|
+
if len(chunk) != 0 {
|
|
69
|
+
if headerBuf.Len()+lineBuf.Len()+len(chunk) > MaxHeaderBytes {
|
|
70
|
+
return "", nil, fmt.Errorf("%w (header got more than %d bytes)", ErrFrameTooLarge, MaxHeaderBytes)
|
|
71
|
+
}
|
|
72
|
+
lineBuf.Write(chunk)
|
|
73
|
+
}
|
|
74
|
+
if lineErr == nil {
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
if errors.Is(lineErr, bufio.ErrBufferFull) {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if lineErr == io.EOF && headerBuf.Len() == 0 && lineBuf.Len() == 0 {
|
|
62
81
|
return "", nil, ErrFrameClosed
|
|
63
82
|
}
|
|
64
83
|
return "", nil, fmt.Errorf("lsp: header read: %w", lineErr)
|
|
65
84
|
}
|
|
85
|
+
line := lineBuf.String()
|
|
66
86
|
trimmed := strings.TrimRight(line, "\r\n")
|
|
67
87
|
if trimmed == "" {
|
|
68
88
|
break
|
|
@@ -107,7 +127,7 @@ func parseContentLength(line string) (int, bool) {
|
|
|
107
127
|
|
|
108
128
|
// WriteFrame serializes body with a Content-Length header to w. The header
|
|
109
129
|
// uses CRLF line endings to match the LSP base protocol exactly so editors
|
|
110
|
-
// that strict-parse the header block (notably
|
|
130
|
+
// that strict-parse the header block (notably VS Code's client) accept the
|
|
111
131
|
// message without warnings.
|
|
112
132
|
func WriteFrame(w io.Writer, body []byte) error {
|
|
113
133
|
header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body))
|