@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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/build/build-wasm.cjs +240 -0
- package/build/pack-prepare.cjs +68 -0
- package/cmd/ttsc-wasm/main.go +130 -0
- package/cmd/ttsc-wasm/main_wasm.go +17 -0
- package/dist/go.mod +54 -0
- package/dist/ttsc.wasm +0 -0
- package/dist/wasm_exec.js +575 -0
- package/go.mod +54 -0
- package/go.sum +26 -0
- package/host/api.go +205 -0
- package/host/doc.go +31 -0
- package/host/host.go +317 -0
- package/host/host_native.go +12 -0
- package/host/plugin.go +59 -0
- package/lib/src/MemFS.d.ts +68 -0
- package/lib/src/MemFS.js +384 -0
- package/lib/src/MemFS.js.map +1 -0
- package/lib/src/api.d.ts +87 -0
- package/lib/src/api.js +20 -0
- package/lib/src/api.js.map +1 -0
- package/lib/src/index.d.ts +6 -0
- package/lib/src/index.js +10 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/instantiate.d.ts +27 -0
- package/lib/src/instantiate.js +71 -0
- package/lib/src/instantiate.js.map +1 -0
- package/package.json +49 -0
- package/src/MemFS.ts +566 -0
- package/src/api.ts +112 -0
- package/src/index.ts +28 -0
- package/src/instantiate.ts +114 -0
- package/vendor/shim/ast/extra-shim.json +4 -0
- package/vendor/shim/ast/go.mod +5 -0
- package/vendor/shim/ast/lint.go +328 -0
- package/vendor/shim/ast/shim.go +220 -0
- package/vendor/shim/ast/surface.go +685 -0
- package/vendor/shim/bundled/extra-shim.json +1 -0
- package/vendor/shim/bundled/go.mod +5 -0
- package/vendor/shim/bundled/shim.go +23 -0
- package/vendor/shim/checker/extra-shim.json +31 -0
- package/vendor/shim/checker/go.mod +5 -0
- package/vendor/shim/checker/shim.go +174 -0
- package/vendor/shim/compiler/extra-shim.json +4 -0
- package/vendor/shim/compiler/go.mod +5 -0
- package/vendor/shim/compiler/shim.go +65 -0
- package/vendor/shim/core/extra-shim.json +4 -0
- package/vendor/shim/core/go.mod +5 -0
- package/vendor/shim/core/shim.go +29 -0
- package/vendor/shim/diagnosticwriter/go.mod +5 -0
- package/vendor/shim/diagnosticwriter/lint.go +145 -0
- package/vendor/shim/diagnosticwriter/shim.go +29 -0
- package/vendor/shim/lsp/extra-shim.json +4 -0
- package/vendor/shim/lsp/go.mod +16 -0
- package/vendor/shim/lsp/go.sum +26 -0
- package/vendor/shim/lsp/shim.go +42 -0
- package/vendor/shim/parser/extra-shim.json +4 -0
- package/vendor/shim/parser/go.mod +5 -0
- package/vendor/shim/parser/shim.go +61 -0
- package/vendor/shim/printer/go.mod +5 -0
- package/vendor/shim/printer/shim.go +23 -0
- package/vendor/shim/scanner/extra-shim.json +4 -0
- package/vendor/shim/scanner/go.mod +5 -0
- package/vendor/shim/scanner/shim.go +134 -0
- package/vendor/shim/tsoptions/extra-shim.json +4 -0
- package/vendor/shim/tsoptions/go.mod +5 -0
- package/vendor/shim/tsoptions/shim.go +173 -0
- package/vendor/shim/tspath/extra-shim.json +4 -0
- package/vendor/shim/tspath/go.mod +5 -0
- package/vendor/shim/tspath/shim.go +236 -0
- package/vendor/shim/vfs/cachedvfs/extra-shim.json +4 -0
- package/vendor/shim/vfs/cachedvfs/go.mod +5 -0
- package/vendor/shim/vfs/cachedvfs/shim.go +12 -0
- package/vendor/shim/vfs/extra-shim.json +4 -0
- package/vendor/shim/vfs/go.mod +5 -0
- package/vendor/shim/vfs/osvfs/extra-shim.json +4 -0
- package/vendor/shim/vfs/osvfs/go.mod +5 -0
- package/vendor/shim/vfs/osvfs/shim.go +13 -0
- 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;
|