@ttsc/wasm 0.11.0-dev.20260517

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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/build/build-wasm.cjs +240 -0
  4. package/build/pack-prepare.cjs +68 -0
  5. package/cmd/ttsc-wasm/main.go +130 -0
  6. package/cmd/ttsc-wasm/main_wasm.go +17 -0
  7. package/dist/go.mod +54 -0
  8. package/dist/ttsc.wasm +0 -0
  9. package/dist/wasm_exec.js +575 -0
  10. package/go.mod +54 -0
  11. package/go.sum +26 -0
  12. package/host/api.go +205 -0
  13. package/host/doc.go +31 -0
  14. package/host/host.go +317 -0
  15. package/host/host_native.go +12 -0
  16. package/host/plugin.go +59 -0
  17. package/lib/src/MemFS.d.ts +68 -0
  18. package/lib/src/MemFS.js +384 -0
  19. package/lib/src/MemFS.js.map +1 -0
  20. package/lib/src/api.d.ts +87 -0
  21. package/lib/src/api.js +20 -0
  22. package/lib/src/api.js.map +1 -0
  23. package/lib/src/index.d.ts +6 -0
  24. package/lib/src/index.js +10 -0
  25. package/lib/src/index.js.map +1 -0
  26. package/lib/src/instantiate.d.ts +27 -0
  27. package/lib/src/instantiate.js +71 -0
  28. package/lib/src/instantiate.js.map +1 -0
  29. package/package.json +49 -0
  30. package/src/MemFS.ts +566 -0
  31. package/src/api.ts +112 -0
  32. package/src/index.ts +28 -0
  33. package/src/instantiate.ts +114 -0
  34. package/vendor/shim/ast/extra-shim.json +4 -0
  35. package/vendor/shim/ast/go.mod +5 -0
  36. package/vendor/shim/ast/lint.go +328 -0
  37. package/vendor/shim/ast/shim.go +220 -0
  38. package/vendor/shim/ast/surface.go +685 -0
  39. package/vendor/shim/bundled/extra-shim.json +1 -0
  40. package/vendor/shim/bundled/go.mod +5 -0
  41. package/vendor/shim/bundled/shim.go +23 -0
  42. package/vendor/shim/checker/extra-shim.json +31 -0
  43. package/vendor/shim/checker/go.mod +5 -0
  44. package/vendor/shim/checker/shim.go +174 -0
  45. package/vendor/shim/compiler/extra-shim.json +4 -0
  46. package/vendor/shim/compiler/go.mod +5 -0
  47. package/vendor/shim/compiler/shim.go +65 -0
  48. package/vendor/shim/core/extra-shim.json +4 -0
  49. package/vendor/shim/core/go.mod +5 -0
  50. package/vendor/shim/core/shim.go +29 -0
  51. package/vendor/shim/diagnosticwriter/go.mod +5 -0
  52. package/vendor/shim/diagnosticwriter/lint.go +145 -0
  53. package/vendor/shim/diagnosticwriter/shim.go +29 -0
  54. package/vendor/shim/lsp/extra-shim.json +4 -0
  55. package/vendor/shim/lsp/go.mod +16 -0
  56. package/vendor/shim/lsp/go.sum +26 -0
  57. package/vendor/shim/lsp/shim.go +42 -0
  58. package/vendor/shim/parser/extra-shim.json +4 -0
  59. package/vendor/shim/parser/go.mod +5 -0
  60. package/vendor/shim/parser/shim.go +61 -0
  61. package/vendor/shim/printer/go.mod +5 -0
  62. package/vendor/shim/printer/shim.go +23 -0
  63. package/vendor/shim/scanner/extra-shim.json +4 -0
  64. package/vendor/shim/scanner/go.mod +5 -0
  65. package/vendor/shim/scanner/shim.go +134 -0
  66. package/vendor/shim/tsoptions/extra-shim.json +4 -0
  67. package/vendor/shim/tsoptions/go.mod +5 -0
  68. package/vendor/shim/tsoptions/shim.go +173 -0
  69. package/vendor/shim/tspath/extra-shim.json +4 -0
  70. package/vendor/shim/tspath/go.mod +5 -0
  71. package/vendor/shim/tspath/shim.go +236 -0
  72. package/vendor/shim/vfs/cachedvfs/extra-shim.json +4 -0
  73. package/vendor/shim/vfs/cachedvfs/go.mod +5 -0
  74. package/vendor/shim/vfs/cachedvfs/shim.go +12 -0
  75. package/vendor/shim/vfs/extra-shim.json +4 -0
  76. package/vendor/shim/vfs/go.mod +5 -0
  77. package/vendor/shim/vfs/osvfs/extra-shim.json +4 -0
  78. package/vendor/shim/vfs/osvfs/go.mod +5 -0
  79. package/vendor/shim/vfs/osvfs/shim.go +13 -0
  80. package/vendor/shim/vfs/shim.go +22 -0
package/host/api.go ADDED
@@ -0,0 +1,205 @@
1
+ // Shared API helpers behind the wasm and non-wasm entrypoints.
2
+ //
3
+ // `host.Expose` (under //go:build js) and the cmd's native `main.go` both
4
+ // call into these helpers so build/check/transform behavior stays
5
+ // bit-identical across the two surfaces.
6
+ package host
7
+
8
+ import (
9
+ "encoding/json"
10
+ "fmt"
11
+ "path/filepath"
12
+ "strings"
13
+
14
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
15
+
16
+ "github.com/samchon/ttsc/packages/ttsc/driver"
17
+ )
18
+
19
+ // APIResult is the JSON shape every plugin-dispatch endpoint returns to the
20
+ // JS host. `code` follows the native CLI exit-code contract (0 success, 2
21
+ // usage error, 3 runtime error). `stdout` / `stderr` are raw strings the
22
+ // caller can render in a console panel.
23
+ type APIResult struct {
24
+ Code int `json:"code"`
25
+ Stdout string `json:"stdout"`
26
+ Stderr string `json:"stderr"`
27
+ }
28
+
29
+ // CompileResult mirrors `ttsc api-compile`. Field names are TypeScript-style
30
+ // so JS callers can use it directly without remapping.
31
+ type CompileResult struct {
32
+ Diagnostics []CompileDiagnostic `json:"diagnostics,omitempty"`
33
+ // Output maps cwd-relative paths to emitted JS / d.ts text. Empty when
34
+ // the program produced no files (e.g. `noEmit` projects).
35
+ Output map[string]string `json:"output"`
36
+ }
37
+
38
+ // TransformResult is the no-emit companion. The `typescript` map keys files
39
+ // the same way `output` does in compile mode.
40
+ type TransformResult struct {
41
+ Diagnostics []CompileDiagnostic `json:"diagnostics,omitempty"`
42
+ TypeScript map[string]string `json:"typescript"`
43
+ }
44
+
45
+ // CompileDiagnostic is the public TypeScript-side diagnostic DTO. Mirrors the
46
+ // shape `ttsc api-compile` writes so the JS host can render it without
47
+ // remapping fields.
48
+ type CompileDiagnostic struct {
49
+ File *string `json:"file"`
50
+ Category string `json:"category"`
51
+ Code int32 `json:"code"`
52
+ Start *int `json:"start,omitempty"`
53
+ Length *int `json:"length,omitempty"`
54
+ Line int `json:"line,omitempty"`
55
+ Character int `json:"character,omitempty"`
56
+ MessageText string `json:"messageText"`
57
+ }
58
+
59
+ // Build runs `ttsc build`-shaped emit: load the project, emit JS, return the
60
+ // emitted text map + diagnostics as JSON. `cwd` must be an absolute path
61
+ // inside the host filesystem; `tsconfigPath` may be relative to cwd.
62
+ func Build(cwd, tsconfigPath string) ([]byte, int, error) {
63
+ prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
64
+ ForceEmit: true,
65
+ })
66
+ if err != nil {
67
+ return nil, 2, err
68
+ }
69
+ if prog != nil {
70
+ defer prog.Close()
71
+ diags = append(diags, prog.Diagnostics()...)
72
+ }
73
+ output := map[string]string{}
74
+ if prog != nil {
75
+ rewrites := driver.NewRewriteSet()
76
+ writeFile := shimcompiler.WriteFile(
77
+ func(fileName, text string, _ *shimcompiler.WriteFileData) error {
78
+ output[apiOutputKey(cwd, fileName)] = text
79
+ return nil
80
+ },
81
+ )
82
+ _, emitDiags, eerr := prog.EmitAll(rewrites, writeFile)
83
+ if eerr != nil {
84
+ return nil, 3, fmt.Errorf("emit failed: %w", eerr)
85
+ }
86
+ diags = append(diags, emitDiags...)
87
+ }
88
+ result := CompileResult{
89
+ Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
90
+ Output: output,
91
+ }
92
+ for _, d := range diags {
93
+ result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
94
+ }
95
+ data, err := json.Marshal(result)
96
+ if err != nil {
97
+ return nil, 3, fmt.Errorf("result marshal failed: %w", err)
98
+ }
99
+ code := 0
100
+ if driver.CountErrors(diags) > 0 {
101
+ code = 2
102
+ }
103
+ return data, code, nil
104
+ }
105
+
106
+ // Check runs the typecheck pipeline without emitting JS. The returned JSON
107
+ // has the same shape as Build but with an empty `output` map.
108
+ func Check(cwd, tsconfigPath string) ([]byte, int, error) {
109
+ prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
110
+ ForceNoEmit: true,
111
+ })
112
+ if err != nil {
113
+ return nil, 2, err
114
+ }
115
+ if prog != nil {
116
+ defer prog.Close()
117
+ diags = append(diags, prog.Diagnostics()...)
118
+ }
119
+ result := CompileResult{
120
+ Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
121
+ Output: map[string]string{},
122
+ }
123
+ for _, d := range diags {
124
+ result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
125
+ }
126
+ data, err := json.Marshal(result)
127
+ if err != nil {
128
+ return nil, 3, fmt.Errorf("result marshal failed: %w", err)
129
+ }
130
+ code := 0
131
+ if driver.CountErrors(diags) > 0 {
132
+ code = 2
133
+ }
134
+ return data, code, nil
135
+ }
136
+
137
+ // Transform returns every source file the program saw, keyed by
138
+ // project-relative path. The playground uses this to render the TS view
139
+ // after source rewrites (e.g. paths rewriting) have been applied.
140
+ func Transform(cwd, tsconfigPath string) ([]byte, int, error) {
141
+ prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
142
+ ForceNoEmit: true,
143
+ })
144
+ if err != nil {
145
+ return nil, 2, err
146
+ }
147
+ typescript := map[string]string{}
148
+ if prog != nil {
149
+ defer prog.Close()
150
+ for _, file := range prog.SourceFiles() {
151
+ typescript[apiOutputKey(cwd, file.FileName())] = file.Text()
152
+ }
153
+ diags = append(diags, prog.Diagnostics()...)
154
+ }
155
+ result := TransformResult{
156
+ Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
157
+ TypeScript: typescript,
158
+ }
159
+ for _, d := range diags {
160
+ result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
161
+ }
162
+ data, err := json.Marshal(result)
163
+ if err != nil {
164
+ return nil, 3, fmt.Errorf("result marshal failed: %w", err)
165
+ }
166
+ code := 0
167
+ if driver.CountErrors(diags) > 0 {
168
+ code = 2
169
+ }
170
+ return data, code, nil
171
+ }
172
+
173
+ func toAPIDiagnostic(diag driver.Diagnostic) CompileDiagnostic {
174
+ var file *string
175
+ if diag.File != "" {
176
+ value := filepath.ToSlash(diag.File)
177
+ file = &value
178
+ }
179
+ category := "error"
180
+ if diag.Severity == driver.SeverityWarning {
181
+ category = "warning"
182
+ }
183
+ return CompileDiagnostic{
184
+ File: file,
185
+ Category: category,
186
+ Code: diag.Code,
187
+ Start: diag.Start,
188
+ Length: diag.Length,
189
+ Line: diag.Line,
190
+ Character: diag.Column,
191
+ MessageText: diag.Message,
192
+ }
193
+ }
194
+
195
+ func apiOutputKey(cwd, fileName string) string {
196
+ rel, err := filepath.Rel(cwd, fileName)
197
+ if err != nil || isOutsideRelativePath(rel) {
198
+ return filepath.ToSlash(fileName)
199
+ }
200
+ return filepath.ToSlash(rel)
201
+ }
202
+
203
+ func isOutsideRelativePath(rel string) bool {
204
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
205
+ }
package/host/doc.go ADDED
@@ -0,0 +1,31 @@
1
+ // Package host is the reusable Go scaffolding plugin authors import to build
2
+ // their own ttsc playground wasm.
3
+ //
4
+ // A consumer wasm looks like this:
5
+ //
6
+ // //go:build js
7
+ // package main
8
+ //
9
+ // import (
10
+ // "github.com/samchon/ttsc/packages/wasm/host"
11
+ // yourplugin "example.com/your/plugin"
12
+ // )
13
+ //
14
+ // func main() {
15
+ // host.Expose("yourApi", host.Config{
16
+ // Plugins: []host.Plugin{yourplugin.New()},
17
+ // })
18
+ // select {}
19
+ // }
20
+ //
21
+ // Expose binds `globalThis[name]` to an object that exposes ttsc's base
22
+ // project commands (build, check, transform, version) plus a `plugin(name,
23
+ // command, opts)` dispatcher that routes into a Plugin's CLI-shaped Run
24
+ // callback. Plugins keep ttsc's existing argv-based contract — the same
25
+ // shape the native sidecars implement — so the wasm and the native CLI can
26
+ // share their Run* entry points byte-for-byte.
27
+ //
28
+ // The package is browser-agnostic: every JS-facing helper sits behind
29
+ // //go:build js, so a consumer can `go build ./...` natively without GOOS=js
30
+ // to keep CI happy.
31
+ package host
package/host/host.go ADDED
@@ -0,0 +1,317 @@
1
+ //go:build js && wasm
2
+
3
+ // JS-side bindings for the host package.
4
+ //
5
+ // `Expose` installs `globalThis[apiName]` with the base ttsc endpoints
6
+ // (version, build, check, transform) plus a `plugin(name, command, opts)`
7
+ // dispatcher that routes into the consumer's registered plugins. The handler
8
+ // shape mirrors typia's `ttsc-typia` wasm so JS callers can build a single
9
+ // boot helper that works against any host-built binary.
10
+ package host
11
+
12
+ import (
13
+ "bytes"
14
+ "fmt"
15
+ "io"
16
+ "os"
17
+ "runtime"
18
+ "syscall/js"
19
+ "time"
20
+ )
21
+
22
+ // Build metadata. Override at link time via
23
+ // `-ldflags "-X github.com/samchon/ttsc/packages/wasm/host.version=..."`.
24
+ var (
25
+ version = "0.0.0-dev"
26
+ commit = "dev"
27
+ date = "unknown"
28
+ )
29
+
30
+ // API stability: experimental until v1.0; signatures may change between
31
+ // minor releases. Pin exact versions in production playgrounds.
32
+ //
33
+ // Expose installs `globalThis[apiName]` with the base API endpoints and the
34
+ // plugin dispatcher, then keeps the Go runtime alive forever.
35
+ //
36
+ // The contract:
37
+ //
38
+ // - globalThis[apiName].version() → version banner
39
+ // - globalThis[apiName].build({ cwd, tsconfig }) → Promise<CompileResult>
40
+ // - globalThis[apiName].check({ cwd, tsconfig }) → Promise<CompileResult>
41
+ // - globalThis[apiName].transform({ cwd, tsconfig }) → Promise<TransformResult>
42
+ // - globalThis[apiName].plugin({ name, command, ...opts}) → Promise<APIResult>
43
+ // - globalThis[apiName].plugins() → string[] of registered names
44
+ //
45
+ // A matching readiness resolver is invoked: `globalThis[`${apiName}Ready`]`.
46
+ // JS callers register this BEFORE go.run begins so they can await wasm boot.
47
+ func Expose(apiName string, cfg Config) {
48
+ plugins := map[string]Plugin{}
49
+ pluginNames := make([]string, 0, len(cfg.Plugins))
50
+ for _, p := range cfg.Plugins {
51
+ if p == nil {
52
+ continue
53
+ }
54
+ name := p.Name()
55
+ if name == "" {
56
+ continue
57
+ }
58
+ if _, dup := plugins[name]; dup {
59
+ panic(fmt.Sprintf("host.Expose: duplicate plugin name %q", name))
60
+ }
61
+ plugins[name] = p
62
+ pluginNames = append(pluginNames, name)
63
+ }
64
+
65
+ api := map[string]any{
66
+ "version": js.FuncOf(jsVersion),
67
+ "build": js.FuncOf(jsBuild),
68
+ "check": js.FuncOf(jsCheck),
69
+ "transform": js.FuncOf(jsTransform),
70
+ "plugin": js.FuncOf(jsPluginDispatch(plugins)),
71
+ "plugins": js.FuncOf(jsPluginsList(pluginNames)),
72
+ }
73
+ js.Global().Set(apiName, js.ValueOf(api))
74
+
75
+ if ready := js.Global().Get(apiName + "Ready"); ready.Type() == js.TypeFunction {
76
+ ready.Invoke()
77
+ }
78
+
79
+ // A perpetual idle goroutine keeps the wasm runtime alive without
80
+ // tripping Go's deadlock detector while syscall.fsCall callbacks are in
81
+ // flight. `select {}` alone is wrongly classified as "all goroutines
82
+ // asleep" the moment the FS path hands a request to JS.
83
+ go func() {
84
+ for {
85
+ time.Sleep(time.Hour)
86
+ }
87
+ }()
88
+ select {}
89
+ }
90
+
91
+ // jsVersion returns the build metadata for this wasm binary.
92
+ func jsVersion(this js.Value, args []js.Value) any {
93
+ return js.ValueOf(map[string]any{
94
+ "version": version,
95
+ "commit": commit,
96
+ "date": date,
97
+ "go": runtime.Version(),
98
+ "goos": runtime.GOOS,
99
+ "goarch": runtime.GOARCH,
100
+ })
101
+ }
102
+
103
+ // jsBuild → Promise<{ code, stdout, stderr, result }>. `result` carries the
104
+ // compile JSON; the surrounding shape mirrors plugin dispatch so JS callers
105
+ // can write one error-handling branch.
106
+ func jsBuild(this js.Value, args []js.Value) any {
107
+ opts := optionsArg(args)
108
+ return makePromise(func() any {
109
+ return runProjectCommand(opts, Build)
110
+ })
111
+ }
112
+
113
+ // jsCheck → Promise<{ code, stdout, stderr, result }>.
114
+ func jsCheck(this js.Value, args []js.Value) any {
115
+ opts := optionsArg(args)
116
+ return makePromise(func() any {
117
+ return runProjectCommand(opts, Check)
118
+ })
119
+ }
120
+
121
+ // jsTransform → Promise<{ code, stdout, stderr, result }>.
122
+ func jsTransform(this js.Value, args []js.Value) any {
123
+ opts := optionsArg(args)
124
+ return makePromise(func() any {
125
+ return runProjectCommand(opts, Transform)
126
+ })
127
+ }
128
+
129
+ // jsPluginDispatch routes `api.plugin({ name, command, ...opts })` to the
130
+ // matching registered Plugin's Run. The returned shape is identical to the
131
+ // base endpoints so a JS caller has one result type to handle.
132
+ func jsPluginDispatch(plugins map[string]Plugin) func(this js.Value, args []js.Value) any {
133
+ return func(this js.Value, args []js.Value) any {
134
+ opts := optionsArg(args)
135
+ name := stringProp(opts, "name")
136
+ command := stringProp(opts, "command")
137
+ argv := buildPluginArgv(opts)
138
+ plugin, ok := plugins[name]
139
+ if !ok {
140
+ return errorPromise(2, fmt.Sprintf("host: unknown plugin %q", name))
141
+ }
142
+ return makePromise(func() any {
143
+ res := runWithCapturedIO(func() int {
144
+ return plugin.Run(command, argv)
145
+ })
146
+ return js.ValueOf(map[string]any{
147
+ "code": res.Code,
148
+ "stdout": res.Stdout,
149
+ "stderr": res.Stderr,
150
+ "result": "",
151
+ })
152
+ })
153
+ }
154
+ }
155
+
156
+ // jsPluginsList returns the registered plugin names so JS callers can probe
157
+ // the wasm's contents at boot time.
158
+ func jsPluginsList(names []string) func(this js.Value, args []js.Value) any {
159
+ return func(this js.Value, args []js.Value) any {
160
+ out := make([]any, len(names))
161
+ for i, n := range names {
162
+ out[i] = n
163
+ }
164
+ return js.ValueOf(out)
165
+ }
166
+ }
167
+
168
+ // runProjectCommand wraps the base build/check/transform endpoints into the
169
+ // uniform `{ code, stdout, stderr, result }` shape. The result string is the
170
+ // JSON the endpoint produced; JSON.parse it on the JS side.
171
+ func runProjectCommand(opts js.Value, fn func(cwd, tsconfig string) ([]byte, int, error)) any {
172
+ cwd := stringProp(opts, "cwd")
173
+ tsconfig := stringProp(opts, "tsconfig")
174
+ if tsconfig == "" {
175
+ tsconfig = "tsconfig.json"
176
+ }
177
+ if cwd == "" {
178
+ return errorResponse(2, "host: \"cwd\" is required")
179
+ }
180
+ data, code, err := fn(cwd, tsconfig)
181
+ if err != nil {
182
+ return js.ValueOf(map[string]any{
183
+ "code": code,
184
+ "stdout": "",
185
+ "stderr": err.Error() + "\n",
186
+ "result": "",
187
+ })
188
+ }
189
+ return js.ValueOf(map[string]any{
190
+ "code": code,
191
+ "stdout": "",
192
+ "stderr": "",
193
+ "result": string(data),
194
+ })
195
+ }
196
+
197
+ // buildPluginArgv translates the JS options object into a CLI-shaped argv. We
198
+ // pass through every string / boolean property the caller set, except the
199
+ // `name` / `command` slots which the host consumes itself. This is the same
200
+ // translation typia's wasm does for `--cwd / --tsconfig / --output / ...`.
201
+ func buildPluginArgv(opts js.Value) []string {
202
+ out := []string{}
203
+ if opts.Type() != js.TypeObject {
204
+ return out
205
+ }
206
+ keys := js.Global().Get("Object").Call("keys", opts)
207
+ n := keys.Length()
208
+ for i := 0; i < n; i++ {
209
+ key := keys.Index(i).String()
210
+ switch key {
211
+ case "name", "command":
212
+ continue
213
+ }
214
+ value := opts.Get(key)
215
+ switch value.Type() {
216
+ case js.TypeString:
217
+ out = append(out, "--"+key+"="+value.String())
218
+ case js.TypeBoolean:
219
+ if value.Bool() {
220
+ out = append(out, "--"+key)
221
+ }
222
+ case js.TypeNumber:
223
+ out = append(out, fmt.Sprintf("--%s=%v", key, value.Float()))
224
+ }
225
+ }
226
+ return out
227
+ }
228
+
229
+ // makePromise wraps a Go computation as a JS Promise. The executor
230
+ // synchronously captures the resolve/reject callbacks; the work itself runs
231
+ // in a goroutine so the JS event loop can drive `fs.stat` and friends to
232
+ // completion before we ask Go to receive the callback.
233
+ func makePromise(work func() any) js.Value {
234
+ var executor js.Func
235
+ executor = js.FuncOf(func(this js.Value, args []js.Value) any {
236
+ resolve := args[0]
237
+ reject := args[1]
238
+ go func() {
239
+ defer func() {
240
+ if r := recover(); r != nil {
241
+ err := js.Global().Get("Error").New(fmt.Sprintf("%v", r))
242
+ reject.Invoke(err)
243
+ }
244
+ executor.Release()
245
+ }()
246
+ resolve.Invoke(work())
247
+ }()
248
+ return nil
249
+ })
250
+ return js.Global().Get("Promise").New(executor)
251
+ }
252
+
253
+ func errorPromise(code int, message string) js.Value {
254
+ return makePromise(func() any { return errorResponse(code, message) })
255
+ }
256
+
257
+ func errorResponse(code int, message string) any {
258
+ return js.ValueOf(map[string]any{
259
+ "code": code,
260
+ "stdout": "",
261
+ "stderr": message + "\n",
262
+ "result": "",
263
+ })
264
+ }
265
+
266
+ func optionsArg(args []js.Value) js.Value {
267
+ if len(args) == 0 || args[0].Type() != js.TypeObject {
268
+ return js.ValueOf(map[string]any{})
269
+ }
270
+ return args[0]
271
+ }
272
+
273
+ func stringProp(obj js.Value, key string) string {
274
+ v := obj.Get(key)
275
+ if v.Type() != js.TypeString {
276
+ return ""
277
+ }
278
+ return v.String()
279
+ }
280
+
281
+ // runWithCapturedIO redirects os.Stdout / os.Stderr through in-memory pipes
282
+ // for the duration of `task`. Plugin Run methods write to os.Stdout / os.Stderr
283
+ // the same way the native sidecar binaries do; capturing the output lets the
284
+ // JS host render it in a console panel without spawning a subprocess.
285
+ func runWithCapturedIO(task func() int) APIResult {
286
+ prevOut, prevErr := os.Stdout, os.Stderr
287
+ rOut, wOut, _ := os.Pipe()
288
+ rErr, wErr, _ := os.Pipe()
289
+ os.Stdout = wOut
290
+ os.Stderr = wErr
291
+
292
+ var outBuf, errBuf bytes.Buffer
293
+ done := make(chan struct{}, 2)
294
+ go func() {
295
+ _, _ = io.Copy(&outBuf, rOut)
296
+ done <- struct{}{}
297
+ }()
298
+ go func() {
299
+ _, _ = io.Copy(&errBuf, rErr)
300
+ done <- struct{}{}
301
+ }()
302
+
303
+ code := task()
304
+
305
+ _ = wOut.Close()
306
+ _ = wErr.Close()
307
+ <-done
308
+ <-done
309
+ os.Stdout = prevOut
310
+ os.Stderr = prevErr
311
+
312
+ return APIResult{
313
+ Code: code,
314
+ Stdout: outBuf.String(),
315
+ Stderr: errBuf.String(),
316
+ }
317
+ }
@@ -0,0 +1,12 @@
1
+ //go:build !js
2
+
3
+ package host
4
+
5
+ // Expose is a no-op on native targets. Consumers can `go build ./...` to
6
+ // type-check their wasm entry point without GOOS=js; the function symbol
7
+ // stays in scope but the body short-circuits so it's safe to call from a
8
+ // non-wasm sanity-test entrypoint.
9
+ func Expose(apiName string, cfg Config) {
10
+ _ = apiName
11
+ _ = cfg
12
+ }
package/host/plugin.go ADDED
@@ -0,0 +1,59 @@
1
+ package host
2
+
3
+ // API stability: experimental until v1.0; signatures may change between
4
+ // minor releases. Pin exact versions in production playgrounds.
5
+ //
6
+ // Plugin is the in-process equivalent of ttsc's native CLI sidecar.
7
+ //
8
+ // The native CLI invokes plugins by spawning their binary with argv (e.g.
9
+ // `@ttsc/lint check --tsconfig=tsconfig.json --plugins-json=...`). Inside the
10
+ // wasm there is no subprocess support, so the consumer wasm bundles plugin
11
+ // code directly and exposes the same dispatch through Plugin.Run.
12
+ //
13
+ // A typical Plugin implementation in the consumer wasm is a thin adapter that
14
+ // forwards to the same Run* function the native sidecar's `main.go` calls:
15
+ //
16
+ // type bannerPlugin struct{}
17
+ //
18
+ // func (bannerPlugin) Name() string { return "@ttsc/banner" }
19
+ // func (bannerPlugin) Run(command string, args []string) int {
20
+ // switch command {
21
+ // case "build": return utility.RunBuild(args)
22
+ // case "check": return utility.RunCheck(args)
23
+ // case "transform": return utility.RunTransform(args)
24
+ // default: return 2
25
+ // }
26
+ // }
27
+ //
28
+ // The host installs the package-level writers in `runWithCapturedIO` before
29
+ // calling Run, so anything the plugin writes to ttsc's `stdout` / `stderr`
30
+ // streams is captured and returned to the JS caller.
31
+ type Plugin interface {
32
+ // Name is the npm-style plugin id (e.g. `@ttsc/banner`). The JS side
33
+ // passes this exact string when dispatching: `api.plugin("@ttsc/banner",
34
+ // "build", opts)`. Names must be unique within a Config.
35
+ Name() string
36
+
37
+ // Run dispatches a subcommand. `command` is the verb the JS caller asked
38
+ // for (typically build / check / transform / fix / format / version);
39
+ // `args` is the rest of the argv, already prefixed with `--flag=value`
40
+ // pairs the host built from the JS options object.
41
+ //
42
+ // Return the exit code (0 for success, 2 for usage errors, 3 for runtime
43
+ // errors — mirrors the native CLI exit-code contract). Anything written
44
+ // to `os.Stdout` / `os.Stderr` is captured by the host.
45
+ //
46
+ // API stability: experimental until v1.0; the signature is expected to
47
+ // change to `Run(ctx *PluginContext) int` in a follow-up release.
48
+ Run(command string, args []string) int
49
+ }
50
+
51
+ // Config carries the optional registrations the host applies before binding
52
+ // `globalThis[name]`. Pass `Config{}` for a vanilla ttsc + tsgo wasm.
53
+ type Config struct {
54
+ // Plugins are dispatched through `api.plugin(name, command, opts)` from
55
+ // JS. Their Run methods share the same `os.Stdout` / `os.Stderr` streams
56
+ // the base build/check/transform endpoints use, so diagnostics render
57
+ // the same way no matter which lane produced them.
58
+ Plugins []Plugin
59
+ }
@@ -0,0 +1,68 @@
1
+ export interface IWasmExecFS {
2
+ constants: Record<string, number>;
3
+ writeSync(fd: number, buf: Uint8Array): number;
4
+ write(fd: number, buf: Uint8Array, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException | null, n: number) => void): void;
5
+ open(path: string, flags: number, mode: number, callback: (err: NodeJS.ErrnoException | null, fd: number) => void): void;
6
+ close(fd: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
7
+ read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException | null, n: number) => void): void;
8
+ readdir(path: string, callback: (err: NodeJS.ErrnoException | null, entries: string[]) => void): void;
9
+ mkdir(path: string, perm: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
10
+ stat(path: string, callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void): void;
11
+ lstat(path: string, callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void): void;
12
+ fstat(fd: number, callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void): void;
13
+ fsync(fd: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
14
+ unlink(path: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
15
+ rename(from: string, to: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
16
+ rmdir(path: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
17
+ chmod(path: string, mode: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
18
+ fchmod(fd: number, mode: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
19
+ chown(path: string, uid: number, gid: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
20
+ fchown(fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
21
+ lchown(path: string, uid: number, gid: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
22
+ utimes(path: string, atime: number, mtime: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
23
+ link(path: string, link: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
24
+ symlink(path: string, link: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
25
+ readlink(path: string, callback: (err: NodeJS.ErrnoException | null, link: string) => void): void;
26
+ truncate(path: string, length: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
27
+ ftruncate(fd: number, length: number, callback: (err: NodeJS.ErrnoException | null) => void): void;
28
+ }
29
+ export interface IFileStats {
30
+ isDirectory(): boolean;
31
+ isFile(): boolean;
32
+ size: number;
33
+ mode: number;
34
+ mtimeMs: number;
35
+ atimeMs: number;
36
+ ctimeMs: number;
37
+ dev: number;
38
+ ino: number;
39
+ nlink: number;
40
+ uid: number;
41
+ gid: number;
42
+ rdev: number;
43
+ blksize: number;
44
+ blocks: number;
45
+ }
46
+ export declare class MemFSError extends Error {
47
+ code: string;
48
+ errno: number;
49
+ path?: string;
50
+ syscall?: string;
51
+ constructor(code: string, syscall: string, path?: string);
52
+ }
53
+ export interface IMemFSHost {
54
+ fs: IWasmExecFS;
55
+ writeFile(path: string, data: string | Uint8Array): void;
56
+ readFile(path: string): Uint8Array | null;
57
+ readFileText(path: string): string | null;
58
+ exists(path: string): boolean;
59
+ mkdirp(path: string): void;
60
+ stdout: {
61
+ buffer: string;
62
+ };
63
+ stderr: {
64
+ buffer: string;
65
+ };
66
+ resetStdio(): void;
67
+ }
68
+ export declare function createMemFS(): IMemFSHost;