Compare commits

..

48 Commits

Author SHA1 Message Date
ae7ebb8127
go pendingcall chan -> struct 2025-12-05 22:41:26 +03:00
388e36721e
refactor 2025-12-05 14:31:02 +03:00
c93627c44d
refactor 2025-12-05 14:18:56 +03:00
48dfe992cc
version bump 2025-12-05 14:10:53 +03:00
df68281137
tests for go 2025-11-17 09:39:54 +03:00
07c8367abb
move testdata 2025-11-17 09:33:36 +03:00
d61e3d7aa6
finally support blobs (go->ts as arg and return) 2025-11-16 11:56:16 +03:00
0e6a217c14
panic recover handleCall 2025-11-16 10:21:06 +03:00
e66e0c6cae
refactor 2025-11-16 10:18:50 +03:00
5d4ab2c7ac
refactor 2025-11-16 10:17:00 +03:00
6647365a80
... 2025-11-16 09:32:58 +03:00
510e0a108d
refactor 2025-11-16 09:23:46 +03:00
7f0d7047e9
example 2025-11-16 09:00:57 +03:00
768eaca60a
convert return types in go template 2025-11-16 07:32:49 +03:00
2c6429065b
wait with timeout + correct error processing 2025-11-16 06:59:52 +03:00
5b52490495
conv types 2025-11-16 06:45:44 +03:00
0465225e39
conv typs 2025-11-16 06:42:45 +03:00
64b0c461a2
fix ts lib this binding 2025-11-16 06:34:28 +03:00
803ad02772
convert float64 to int 2025-11-15 17:54:30 +03:00
bd0e0d8fec
rename params to args 2025-11-15 17:40:33 +03:00
0f57e8ffcb
small refactoring 2025-11-14 19:45:07 +03:00
f1f4620de5
gitignore 2025-11-14 19:35:22 +03:00
c40c973b55
launch example 2025-11-14 19:35:07 +03:00
d50fb45547
tsgen blobs 2025-11-14 18:43:09 +03:00
a7f7c4bb29
errors 2025-11-14 18:42:01 +03:00
e7315c38ae
add []byte to goparser 2025-11-14 18:21:10 +03:00
f24a0d706c
support for []byte (code not finished, temp commit) 2025-11-11 09:05:18 +03:00
6b7014a2b7
revert changes 2025-11-11 09:04:01 +03:00
cafc5036ef
some code 2025-11-09 18:45:24 +03:00
1b794147ec
tests; fixes for ts process management 2025-11-08 17:47:46 +03:00
da8401ce16
vitest 2025-11-08 17:40:52 +03:00
f57463bbd7
jest is garbage 2025-11-08 16:44:13 +03:00
77c70f6d15
add jest 2025-11-08 13:51:42 +03:00
d32060fbe9
asyncqueue for ts 2025-11-08 13:35:07 +03:00
ede31be47c
error processing 2025-11-08 11:01:15 +03:00
59b61ec584
a lil bit refactoring 2025-11-08 11:01:06 +03:00
713d455386
gitignore 2025-11-08 11:00:35 +03:00
e12666c9e4
gitignore 2025-11-08 11:00:15 +03:00
aa58241beb
gitignore 2025-11-08 10:59:40 +03:00
8c4d28ed0e
a lil bit refactoring 2025-11-08 10:59:21 +03:00
0fba01b201
a lil bit refactoring 2025-11-08 10:51:28 +03:00
45eae71fe3
a lil bit refactoring 2025-11-08 10:49:08 +03:00
10ac5dfae8
a lil bit refactoring 2025-11-08 10:41:17 +03:00
2759bec46c
a lil bit refactoring 2025-11-08 09:52:54 +03:00
e04edeb2ed
a lil bit refactoring 2025-11-08 09:52:14 +03:00
3373d9a841
deps 2025-11-08 09:45:00 +03:00
427cf77c7f
readme 2025-11-08 09:40:06 +03:00
269682e4ab
squash 2025-11-08 09:37:30 +03:00
44 changed files with 1357 additions and 481 deletions

3
.gitignore vendored
View File

@ -3,4 +3,7 @@
/go.work.sum
node_modules
*.js
*.js.map
/example/ts/dist
/lib/ts/dist
/TODO.md

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# Readme draft
1. write code
2. annotate it (see below)
3. launch `kitcom -src path/to/source.A -dest path/to/generated/file.B`
4. Use generated file for IPC
## Typescript:
Currently only whole classes are supported.
### Annotate:
```typescript
/**
* @kittenipc api
*/
class ClassName {}
```
### Usage:
```typescript
const localApi = new LocalAPI(); // LocalAPI is written by hand
const ipc = new ChildIPC(localApi);
const goApi = new RemoteAPI(ipc); // RemoteAPI is generated by kitcom
await ipc.start();
// work
await ipc.wait();
```
## Golang:
Currently only whole structs are supported
### Annotate
```go
// kittenipc:api
type StructName struct {
}
```
### Usage:
```go
localApi := LocalAPI{} // LocalAPI is written by hand
cmd := exec.Command(fmt.Sprintf("node %s", "path to compiled js"))
ipc, err := kittenipc.NewParent(cmd, &localApi)
remoteApi := RemoteAPI{Ipc: ipc} // RemoteAPI is generated by kitcom
if err != nil {
log.Panic(err)
}
if err := ipc.Start(); err != nil {
log.Panic(err)
}
// work
if err := kit.Wait(); err != nil {
log.Panic(err)
}
```
LocalAPI on one side is RemoteAPI on the other side
## C++, Rust, Python:
To be done
# Library status
Work in progress. No tests, no docs, code is not finished! Not everything is working yet. Code is partly crap.

7
example/Makefile Normal file
View File

@ -0,0 +1,7 @@
default:
ipc:
@kitcom -src golang -dest ts/src/remote.ts
@kitcom -src ts/src -dest golang/remote.go -pkg main
.PHONY: ipc

View File

@ -6,42 +6,63 @@ import (
"os"
"os/exec"
"path"
"time"
kittenipc "efprojects.com/kitten-ipc"
)
// kittenipc:api
type IpcApi struct {
type GoIpcApi struct {
}
func (api IpcApi) Div(a int, b int) (int, error) {
func (api GoIpcApi) Div(a int, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("zero division")
}
return a / b, nil
}
func (api GoIpcApi) XorData(data1 []byte, data2 []byte) ([]byte, error) {
if len(data1) == 0 || len(data2) == 0 {
return nil, fmt.Errorf("empty input data")
}
if len(data1) != len(data2) {
return nil, fmt.Errorf("input data length mismatch")
}
result := make([]byte, len(data1))
for i := 0; i < len(data1); i++ {
result[i] = data1[i] ^ data2[i]
}
return result, nil
}
func main() {
cwd, err := os.Getwd()
if err != nil {
log.Panic(err)
}
api := IpcApi{}
localApi := GoIpcApi{}
cmdStr := fmt.Sprintf("node %s", path.Join(cwd, "..", "ts/index.js"))
cmd := exec.Command(cmdStr)
cmd := exec.Command("node", path.Join(cwd, "ts/dist/index.js"))
kit, err := kittenipc.NewParent(cmd, &api)
ipc, err := kittenipc.NewParent(cmd, &localApi)
if err != nil {
log.Panic(err)
}
if err := kit.Start(); err != nil {
if err := ipc.Start(); err != nil {
log.Panic(err)
}
if err := kit.Wait(); err != nil {
remoteApi := TsIpcApi{Ipc: ipc}
res, err := remoteApi.Div(10, 2)
if err != nil {
log.Panic(err)
}
log.Printf("call result go->ts Div = %v", res)
if err := ipc.Wait(1 * time.Second); err != nil {
log.Panic(err)
}
}

View File

@ -21,5 +21,5 @@ func (t *TsIpcApi) Div(
return 0, fmt.Errorf("call to TsIpcApi.Div failed: %w", err)
}
_ = results
return results[0].(int), nil
return int(results[0].(float64)), nil
}

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC;;GAEG;AACH,MAAM,QAAQ;IACV,GAAG,CAAC,CAAS,EAAE,CAAS;QACpB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;CACJ;AAED,KAAK,UAAU,IAAI;IACf,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC;IAEhC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAElB,OAAO,CAAC,GAAG,CAAC,QAAQ,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAE9C,IAAI,CAAC;QACD,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACT,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC"}

View File

@ -7,7 +7,10 @@
"": {
"name": "kitten-ipc-example",
"version": "1.0.0",
"license": "Apache 2.0"
"license": "Apache 2.0",
"dependencies": {
"@types/node": "^22.10.5"
}
},
"../../../../../../opt/homebrew/lib/node_modules/list": {
"version": "2.0.19",
@ -45,6 +48,21 @@
"@types/node": "^22.10.5",
"ts-events": "^3.4.1"
}
},
"node_modules/@types/node": {
"version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
}
}
}

View File

@ -8,5 +8,8 @@
"type": "module",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@types/node": "^22.10.5"
}
}

View File

@ -1,15 +0,0 @@
// Code generated by kitcom. DO NOT EDIT.
import { ParentIPC, ChildIPC } from "kitten-ipc";
export default class IpcApi {
protected ipc: ParentIPC | ChildIPC;
constructor(ipc: ParentIPC | ChildIPC) {
this.ipc = ipc;
}
async Div(a: number, b: number): Promise<number> {
const results = await this.ipc.call("IpcApi.Div", a, b);
return results[0] as number;
}
}

View File

@ -1,5 +1,5 @@
import {ChildIPC} from 'kitten-ipc';
import GoIpcApi from './goapi.gen.js';
import GoIpcApi from './remote.js';
/**
* @kittenipc api
@ -7,7 +7,7 @@ import GoIpcApi from './goapi.gen.js';
class TsIpcApi {
Div(a: number, b: number): number {
if (b === 0) {
throw new Error('division by zero');
throw new Error('zero division');
}
return a / b;
}
@ -16,17 +16,15 @@ class TsIpcApi {
async function main() {
const localApi = new TsIpcApi();
const ipc = new ChildIPC(localApi);
const goApi = new GoIpcApi(ipc);
const remoteApi = new GoIpcApi(ipc);
await ipc.start();
console.log(`12/3=${await goApi.Div(12, 3)}`);
console.log(`call result ts->go Div = ${await remoteApi.Div(10, 2)}`);
try {
await goApi.Div(10, 0);
} catch (e) {
console.trace(e);
}
const data1 = Buffer.alloc(10, 0b10101010);
const data2 = Buffer.alloc(10, 0b11110000);
console.log(`call result ts->go XorData = ${(await remoteApi.XorData(data1, data2)).toString('hex')}`);
await ipc.wait();
}

22
example/ts/src/remote.ts Normal file
View File

@ -0,0 +1,22 @@
// Code generated by kitcom. DO NOT EDIT.
import { ParentIPC, ChildIPC } from "kitten-ipc";
export default class GoIpcApi {
protected ipc: ParentIPC | ChildIPC;
constructor(ipc: ParentIPC | ChildIPC) {
this.ipc = ipc;
}
async Div(a: number, b: number): Promise<number> {
const results = await this.ipc.call("GoIpcApi.Div", a, b);
return results[0] as number;
}
async XorData(data1: Buffer, data2: Buffer): Promise<Buffer> {
const results = await this.ipc.call("GoIpcApi.XorData", data1, data2);
results[0] = Buffer.from(results[0], "base64");
return results[0] as Buffer;
}
}

View File

@ -3,31 +3,30 @@
"rootDir": "./src",
"outDir": "./dist",
"module": "nodenext",
"target": "esnext",
// "lib": ["esnext"],
// "types": ["node"],
"module": "Node16",
"target": "ES2022",
"lib": ["ES2022"],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"verbatimModuleSyntax": true,
// "isolatedModules": true,
// "skipLibCheck": true
}
"moduleDetection": "force",
// "isolatedModules": true,
// "skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,11 +1,13 @@
SHELL := /bin/bash
tsgo_dir = ./kitcom/internal/tsgo
tsgo_dir = ./internal/tsgo
my_package = efprojects.com/kitten-ipc/kitcom/internal/tsgo
default:
@echo "Please read Makefile for available targets"
vendor_tsgo:
@mkdir -p $(tsgo_dir)
@git clone --depth 1 https://github.com/microsoft/typescript-go
@ -13,10 +15,13 @@ vendor_tsgo:
@cp -r ./typescript-go/internal/* $(tsgo_dir)
@rm -rf @rm -rf typescript-go
remove_tsgo_tests:
@find $(tsgo_dir) -name "*_test.go" -exec rm {} \;
# just for "fun"
# tree shaking. written in make just for "fun"
# caution: may cause eye hazard
remove_tsgo_unused:
@set -e ; \
dirs=`find $(tsgo_dir) -type d -mindepth 1 -maxdepth 1` ; \

View File

@ -3,7 +3,7 @@ module efprojects.com/kitten-ipc/kitcom
go 1.25.1
require (
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
golang.org/x/sync v0.17.0
golang.org/x/text v0.29.0
golang.org/x/text v0.30.0
)

View File

@ -1,6 +1,6 @@
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

View File

@ -1,34 +0,0 @@
{{- /*gotype: efprojects.com/kitten-ipc/kitcom.goGenData*/ -}}
// Code generated by kitcom. DO NOT EDIT.
package {{.PkgName}}
import (
"fmt"
kittenipc "efprojects.com/kitten-ipc"
)
{{range $e := .Api.Endpoints}}
type {{.Name}} struct {
Ipc kittenipc.Callable
}
{{range $mtd := $e.Methods}}
func ({{$e.Name | receiver}} *{{$e.Name}}) {{$mtd.Name}}(
{{range $mtd.Params}}{{.Name}} {{.Type | typedef}}, {{end}}
) (
{{range $mtd.Ret}}{{.Type | typedef}}, {{end}}error,
) {
results, err := {{$e.Name | receiver}}.Ipc.Call("{{$e.Name}}.{{$mtd.Name}}"{{range $mtd.Params}}, {{.Name}}{{end}})
if err != nil {
return {{range $mtd.Ret}}{{.Type | zerovalue}}, {{end}} fmt.Errorf("call to {{$e.Name}}.{{$mtd.Name}} failed: %w", err)
}
_ = results
return {{range $idx, $ret := $mtd.Ret}}results[{{$idx}}].({{$ret.Type | typedef}}), {{end}}nil
}
{{end}}
{{end}}

View File

@ -13,6 +13,23 @@ const (
TArray ValType = 5
)
func (v ValType) String() string {
switch v {
case TInt:
return "int"
case TString:
return "string"
case TBool:
return "bool"
case TBlob:
return "blob"
case TArray:
return "array"
default:
panic("unreachable code")
}
}
type Val struct {
Name string
Type ValType

View File

@ -0,0 +1,33 @@
package common
import (
"fmt"
"efprojects.com/kitten-ipc/kitcom/internal/api"
)
type Parser struct {
Files []string
}
func (p *Parser) AddFile(path string) {
p.Files = append(p.Files, path)
}
func (p *Parser) MapFiles(parseFile func(path string) ([]api.Endpoint, error)) (*api.Api, error) {
var apis api.Api
for _, f := range p.Files {
endpoints, err := parseFile(f)
if err != nil {
return nil, fmt.Errorf("parse file: %w", err)
}
apis.Endpoints = append(apis.Endpoints, endpoints...)
}
if len(apis.Endpoints) == 0 {
return nil, fmt.Errorf("no endpoints found")
}
return &apis, nil
}

View File

@ -8,11 +8,15 @@ import (
"strings"
"text/template"
"efprojects.com/kitten-ipc/kitcom/api"
_ "embed"
"efprojects.com/kitten-ipc/kitcom/internal/api"
)
//go:embed go_gen.tmpl
// todo: check int overflow
// todo: check float is whole
//go:embed gogen.tmpl
var templateString string
type goGenData struct {
@ -46,6 +50,17 @@ func (g *GoApiGenerator) Generate(apis *api.Api, destFile string) error {
}
return td, nil
},
"convtype": func(valDef string, t api.ValType) (string, error) {
td, ok := map[api.ValType]string{
api.TInt: fmt.Sprintf("int(%s.(float64))", valDef),
api.TString: fmt.Sprintf("%s.(string)", valDef),
api.TBool: fmt.Sprintf("%s.(bool)", valDef),
}[t]
if !ok {
return "", fmt.Errorf("cannot convert type %v for val %s", t, valDef)
}
return td, nil
},
"zerovalue": func(t api.ValType) (string, error) {
v, ok := map[api.ValType]string{
api.TInt: "0",

View File

@ -0,0 +1,33 @@
{{- /*gotype: efprojects.com/kitten-ipc/kitcom/internal/golang.goGenData*/ -}}
// Code generated by kitcom. DO NOT EDIT.
package {{ .PkgName }}
import (
"fmt"
kittenipc "efprojects.com/kitten-ipc"
)
{{ range $e := .Api.Endpoints }}
type {{ .Name }} struct {
Ipc kittenipc.Callable
}
{{ range $mtd := $e.Methods }}
func ({{ $e.Name | receiver }} *{{ $e.Name }}) {{ $mtd.Name }}(
{{ range $mtd.Params }}{{ .Name }} {{ .Type | typedef }}, {{ end }}
) (
{{ range $mtd.Ret }}{{ .Type | typedef }}, {{ end }}error,
) {
results, err := {{ $e.Name | receiver }}.Ipc.Call("{{ $e.Name }}.{{ $mtd.Name }}"{{ range $mtd.Params }}, {{ .Name }}{{ end }})
if err != nil {
return {{ range $mtd.Ret }}{{ .Type | zerovalue }}, {{ end }} fmt.Errorf("call to {{ $e.Name }}.{{ $mtd.Name }} failed: %w", err)
}
_ = results
return {{ range $idx, $ret := $mtd.Ret }}{{ convtype ( printf "results[%d]" $idx ) $ret.Type }}, {{ end }}nil
}
{{ end }}
{{ end }}

View File

@ -7,39 +7,21 @@ import (
"go/token"
"regexp"
"efprojects.com/kitten-ipc/kitcom/api"
"efprojects.com/kitten-ipc/kitcom/internal/api"
"efprojects.com/kitten-ipc/kitcom/internal/common"
)
var decorComment = regexp.MustCompile(`^//\s?kittenipc:api$`)
type GoApiParser struct {
files []string
*common.Parser
}
func (g *GoApiParser) AddFile(path string) {
g.files = append(g.files, path)
func (p *GoApiParser) Parse() (*api.Api, error) {
return p.MapFiles(p.parseFile)
}
func (g *GoApiParser) Parse() (*api.Api, error) {
var apis api.Api
for _, f := range g.files {
endpoints, err := g.parseFile(f)
if err != nil {
return nil, fmt.Errorf("parse file: %w", err)
}
apis.Endpoints = append(apis.Endpoints, endpoints...)
}
if len(apis.Endpoints) == 0 {
return nil, fmt.Errorf("no endpoints found")
}
return &apis, nil
}
func (g *GoApiParser) parseFile(sourceFile string) ([]api.Endpoint, error) {
func (p *GoApiParser) parseFile(sourceFile string) ([]api.Endpoint, error) {
var endpoints []api.Endpoint
fileSet := token.NewFileSet()
@ -115,45 +97,36 @@ func (g *GoApiParser) parseFile(sourceFile string) ([]api.Endpoint, error) {
if recvIdent.Name == endpoint.Name {
var apiMethod api.Method
apiMethod.Name = funcDecl.Name.Name
for _, param := range funcDecl.Type.Params.List {
var apiPar api.Val
ident := param.Type.(*ast.Ident)
switch ident.Name {
case "int":
apiPar.Type = api.TInt
case "string":
apiPar.Type = api.TString
case "bool":
apiPar.Type = api.TBool
default:
return nil, fmt.Errorf("parameter type %s is not supported yet", ident.Name)
for i, param := range funcDecl.Type.Params.List {
apiPar, err := fieldToVal(param, false)
if err != nil {
return nil, fmt.Errorf("parse parameter %d for method %s: %w", i, apiMethod.Name, err)
}
if apiPar == nil {
continue
}
if len(param.Names) != 1 {
return nil, fmt.Errorf("all parameters in method %s should be named", apiMethod.Name)
}
apiPar.Name = param.Names[0].Name
apiMethod.Params = append(apiMethod.Params, apiPar)
apiMethod.Params = append(apiMethod.Params, *apiPar)
}
for _, ret := range funcDecl.Type.Results.List {
var apiRet api.Val
ident := ret.Type.(*ast.Ident)
switch ident.Name {
case "int":
apiRet.Type = api.TInt
case "string":
apiRet.Type = api.TString
case "bool":
apiRet.Type = api.TBool
case "error":
// errors are processed other way
continue
default:
return nil, fmt.Errorf("return type %s is not supported yet", ident.Name)
for i, ret := range funcDecl.Type.Results.List {
apiRet, err := fieldToVal(ret, true)
if err != nil {
return nil, fmt.Errorf("parse return value %d for method %s: %w", i, apiMethod.Name, err)
}
if apiRet == nil {
continue
}
if len(ret.Names) > 0 {
apiRet.Name = ret.Names[0].Name
}
apiMethod.Ret = append(apiMethod.Ret, apiRet)
apiMethod.Ret = append(apiMethod.Ret, *apiRet)
}
endpoints[i].Methods = append(endpoints[i].Methods, apiMethod)
}
@ -161,3 +134,41 @@ func (g *GoApiParser) parseFile(sourceFile string) ([]api.Endpoint, error) {
}
return endpoints, nil
}
func fieldToVal(param *ast.Field, returning bool) (*api.Val, error) {
var val api.Val
switch paramType := param.Type.(type) {
case *ast.Ident:
switch paramType.Name {
case "int":
val.Type = api.TInt
case "string":
val.Type = api.TString
case "bool":
val.Type = api.TBool
case "error":
if returning {
return nil, nil
} else {
return nil, fmt.Errorf("errors are supported only as return types")
}
default:
return nil, fmt.Errorf("parameter type %s is not supported yet", paramType.Name)
}
case *ast.ArrayType:
switch elementType := paramType.Elt.(type) {
case *ast.Ident:
switch elementType.Name {
case "byte":
val.Type = api.TBlob
default:
return nil, fmt.Errorf("parameter type %s is not supported yet", elementType.Name)
}
default:
return nil, fmt.Errorf("parameter type %T is not supported yet", elementType)
}
default:
return nil, fmt.Errorf("parameter type %T is not supported yet", paramType)
}
return &val, nil
}

View File

@ -8,11 +8,12 @@ import (
"os/exec"
"text/template"
"efprojects.com/kitten-ipc/kitcom/api"
_ "embed"
"efprojects.com/kitten-ipc/kitcom/internal/api"
)
//go:embed ts_gen.tmpl
//go:embed tsgen.tmpl
var templateString string
type tsGenData struct {
@ -34,6 +35,7 @@ func (g *TypescriptApiGenerator) Generate(apis *api.Api, destFile string) error
api.TInt: "number",
api.TString: "string",
api.TBool: "boolean",
api.TBlob: "Buffer",
}[t]
if !ok {
return "", fmt.Errorf("cannot generate type %v", t)

View File

@ -0,0 +1,31 @@
{{- /*gotype: efprojects.com/kitten-ipc/kitcom/internal/ts.tsGenData*/ -}}
// Code generated by kitcom. DO NOT EDIT.
import {ParentIPC, ChildIPC} from 'kitten-ipc';
{{ range $e := .Api.Endpoints }}
export default class {{ $e.Name }} {
protected ipc: ParentIPC | ChildIPC;
constructor(ipc: ParentIPC | ChildIPC) {
this.ipc = ipc;
}
{{ range $mtd := $e.Methods }}
async {{ $mtd.Name }}(
{{ range $par := $mtd.Params }}{{ $par.Name }}: {{ $par.Type | typedef }}, {{ end }}
): Promise<{{ if len $mtd.Ret }}{{ (index $mtd.Ret 0).Type | typedef }}{{ else }}void{{ end }}> {
const results = await this.ipc.call('{{ $e.Name }}.{{ $mtd.Name }}',
{{ range $par := $mtd.Params }}{{ $par.Name }}, {{ end }}
);
{{- if eq (index $mtd.Ret 0).Type.String "blob" -}}
results[0] = Buffer.from(results[0], 'base64');
{{- end -}}
return {{ range $i, $ret := $mtd.Ret }}{{ if $i }}, {{ end }}results[{{ $i }}] as {{ $ret.Type | typedef }}{{ end }}
}
{{ end }}
}
{{ end }}

View File

@ -6,44 +6,23 @@ import (
"os"
"strings"
"efprojects.com/kitten-ipc/kitcom/api"
"efprojects.com/kitten-ipc/kitcom/internal/api"
"efprojects.com/kitten-ipc/kitcom/internal/common"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
const TagName = "kittenipc"
const TagComment = "api"
type TypescriptApiParser struct {
files []string
*common.Parser
}
func (t *TypescriptApiParser) AddFile(path string) {
t.files = append(t.files, path)
func (p *TypescriptApiParser) Parse() (*api.Api, error) {
return p.MapFiles(p.parseFile)
}
func (t *TypescriptApiParser) Parse() (*api.Api, error) {
var apis api.Api
for _, f := range t.files {
endpoints, err := t.parseFile(f)
if err != nil {
return nil, fmt.Errorf("parse file: %w", err)
}
apis.Endpoints = append(apis.Endpoints, endpoints...)
}
if len(apis.Endpoints) == 0 {
return nil, fmt.Errorf("no endpoints found")
}
return &apis, nil
}
func (t *TypescriptApiParser) parseFile(sourceFilePath string) ([]api.Endpoint, error) {
func (p *TypescriptApiParser) parseFile(sourceFilePath string) ([]api.Endpoint, error) {
var endpoints []api.Endpoint
f, err := os.Open(sourceFilePath)
@ -72,37 +51,11 @@ func (t *TypescriptApiParser) parseFile(sourceFilePath string) ([]api.Endpoint,
}
cls := node.AsClassDeclaration()
jsDocNodes := cls.JSDoc(nil)
if len(jsDocNodes) == 0 {
return false
}
var isApi bool
outer:
for _, jsDocNode := range jsDocNodes {
jsDoc := jsDocNode.AsJSDoc()
if jsDoc.Tags == nil {
continue
}
for _, tag := range jsDoc.Tags.Nodes {
if tag.TagName().Text() == TagName {
for _, com := range tag.Comments() {
if strings.TrimSpace(com.Text()) == TagComment {
isApi = true
break outer
}
}
}
}
}
if !isApi {
if !p.isApiNode(cls) {
return false
}
var endpoint api.Endpoint
endpoint.Name = cls.Name().Text()
for _, member := range cls.MemberList().Nodes {
@ -121,32 +74,23 @@ func (t *TypescriptApiParser) parseFile(sourceFilePath string) ([]api.Endpoint,
par := parNode.AsParameterDeclaration()
var apiPar api.Val
apiPar.Name = par.Name().Text()
switch par.Type.Kind {
case ast.KindNumberKeyword:
apiPar.Type = api.TInt
case ast.KindStringKeyword:
apiPar.Type = api.TString
case ast.KindBooleanKeyword:
apiPar.Type = api.TBool
default:
err = fmt.Errorf("parameter type %s is not supported yet", par.Type.Kind)
t, typeErr := p.fieldToVal(par.Type)
if typeErr != nil {
err = fmt.Errorf("failed to parse parameter %s: %w", apiPar.Name, typeErr)
return false
}
apiPar.Type = t
apiMethod.Params = append(apiMethod.Params, apiPar)
}
if method.Type != nil {
var apiRet api.Val
switch method.Type.Kind {
case ast.KindNumberKeyword:
apiRet.Type = api.TInt
case ast.KindStringKeyword:
apiRet.Type = api.TString
case ast.KindBooleanKeyword:
apiRet.Type = api.TBool
default:
err = fmt.Errorf("return type %s is not supported yet", method.Type.Kind)
t, typeErr := p.fieldToVal(method.Type)
if typeErr != nil {
err = fmt.Errorf("failed to parse return type: %w", typeErr)
return false
}
apiRet.Type = t
apiMethod.Ret = []api.Val{apiRet}
}
endpoint.Methods = append(endpoint.Methods, apiMethod)
@ -163,3 +107,51 @@ func (t *TypescriptApiParser) parseFile(sourceFilePath string) ([]api.Endpoint,
return endpoints, nil
}
func (p *TypescriptApiParser) fieldToVal(typ *ast.TypeNode) (api.ValType, error) {
switch typ.Kind {
case ast.KindNumberKeyword:
return api.TInt, nil
case ast.KindStringKeyword:
return api.TString, nil
case ast.KindBooleanKeyword:
return api.TBool, nil
case ast.KindTypeReference:
refNode := typ.AsTypeReferenceNode()
ident := refNode.TypeName.AsIdentifier()
switch ident.Text {
case "Buffer":
return api.TBlob, nil
default:
return 0, fmt.Errorf("reference type %s is not supported yet", ident.Text)
}
default:
return 0, fmt.Errorf("type %s is not supported yet", typ.Kind)
}
}
const TagName = "kittenipc"
const TagComment = "api"
func (p *TypescriptApiParser) isApiNode(cls *ast.ClassDeclaration) bool {
jsDocNodes := cls.JSDoc(nil)
if len(jsDocNodes) == 0 {
return false
}
for _, jsDocNode := range jsDocNodes {
jsDoc := jsDocNode.AsJSDoc()
if jsDoc.Tags == nil {
continue
}
for _, tag := range jsDoc.Tags.Nodes {
if tag.TagName().Text() == TagName {
for _, com := range tag.Comments() {
if strings.TrimSpace(com.Text()) == TagComment {
return true
}
}
}
}
}
return false
}

View File

@ -1,5 +0,0 @@
go test fuzz v1
string(".ts")
string("/**@0\n * */0")
int(99)
int(0)

View File

@ -1,5 +0,0 @@
go test fuzz v1
string(".ts")
string("/")
int(99)
int(1)

View File

@ -10,9 +10,10 @@ import (
"path"
"path/filepath"
"efprojects.com/kitten-ipc/kitcom/api"
"efprojects.com/kitten-ipc/kitcom/golang"
"efprojects.com/kitten-ipc/kitcom/ts"
"efprojects.com/kitten-ipc/kitcom/internal/api"
"efprojects.com/kitten-ipc/kitcom/internal/common"
"efprojects.com/kitten-ipc/kitcom/internal/golang"
"efprojects.com/kitten-ipc/kitcom/internal/ts"
)
type ApiParser interface {
@ -27,46 +28,45 @@ type ApiGenerator interface {
func main() {
src := flag.String("src", "", "Source file/dir")
dest := flag.String("dest", "", "Dest file")
pkgName := flag.String("pkgname", "", "Package name (for go)")
pkgName := flag.String("pkg", "", "Package name (for go)")
flag.Parse()
if *src == "" || *dest == "" {
log.Panic("source and destination must be set")
log.Fatalln("source and destination must be set")
}
srcAbs, err := filepath.Abs(*src)
if err != nil {
log.Panic(err)
log.Fatalln(err)
}
destAbs, err := filepath.Abs(*dest)
if err != nil {
log.Panic(err)
log.Fatalln(err)
}
apiParser, err := apiParserByPath(srcAbs)
if err != nil {
log.Panic(err)
}
apis, err := apiParser.Parse()
if err != nil {
log.Panic(err)
log.Fatalln(err)
}
apiGenerator, err := apiGeneratorByPath(destAbs, *pkgName)
if err != nil {
log.Panic(err)
log.Fatalln(err)
}
apis, err := apiParser.Parse()
if err != nil {
log.Fatalln(err)
}
if err := apiGenerator.Generate(apis, destAbs); err != nil {
log.Panic(err)
log.Fatalln(err)
}
}
func apiParserByPath(src string) (ApiParser, error) {
s, err := os.Stat(src)
pathFI, err := os.Stat(src)
if err != nil {
return nil, fmt.Errorf("stat src: %w", err)
}
@ -74,21 +74,21 @@ func apiParserByPath(src string) (ApiParser, error) {
var parser ApiParser
var ext string
if s.IsDir() {
if err := filepath.Walk(src, func(curPath string, i fs.FileInfo, err error) error {
if pathFI.IsDir() {
if err := filepath.Walk(src, func(curPath string, fileinfo fs.FileInfo, err error) error {
if err != nil {
return err
}
if i.IsDir() {
if fileinfo.IsDir() {
return nil
}
p, err := apiParserByFilePath(i.Name())
p, err := apiParserByFilePath(fileinfo.Name())
if err == nil {
if parser == nil {
parser = p
ext = path.Ext(i.Name())
} else if path.Ext(i.Name()) != ext {
ext = path.Ext(fileinfo.Name())
} else if path.Ext(fileinfo.Name()) != ext {
return fmt.Errorf("path contain multiple supported filetypes")
}
parser.AddFile(curPath)
@ -115,9 +115,13 @@ func apiParserByPath(src string) (ApiParser, error) {
func apiParserByFilePath(src string) (ApiParser, error) {
switch path.Ext(src) {
case ".go":
return &golang.GoApiParser{}, nil
return &golang.GoApiParser{
Parser: &common.Parser{},
}, nil
case ".ts":
return &ts.TypescriptApiParser{}, nil
return &ts.TypescriptApiParser{
Parser: &common.Parser{},
}, nil
case ".js":
return nil, fmt.Errorf("vanilla javascript is not supported and never will be")
case "":

View File

@ -1,25 +0,0 @@
// Code generated by kitcom. DO NOT EDIT.
import {ParentIPC, ChildIPC} from 'kitten-ipc';
{{- /*gotype: efprojects.com/kitten-ipc/kitcom/ts.tsGenData*/ -}}
{{range $e := .Api.Endpoints}}
export default class {{$e.Name}} {
protected ipc: ParentIPC | ChildIPC;
constructor(ipc: ParentIPC | ChildIPC) {
this.ipc = ipc;
}
{{range $mtd := $e.Methods}}
async {{ $mtd.Name }}(
{{ range $par := $mtd.Params }}{{$par.Name}}: {{$par.Type | typedef }}, {{end}}
): Promise<{{if len $mtd.Ret}}{{(index $mtd.Ret 0).Type | typedef }}{{else}}void{{end}}> {
const results = await this.ipc.call('{{$e.Name}}.{{$mtd.Name}}',
{{range $par := $mtd.Params}}{{$par.Name}}, {{end}}
);
return {{range $i, $ret := $mtd.Ret}}{{if $i}}, {{end}}results[{{$i}}] as {{$ret.Type | typedef}}{{end}}
}
{{end}}
}
{{end}}

View File

@ -2,6 +2,7 @@ package golang
import (
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"flag"
@ -39,7 +40,7 @@ type Message struct {
Type MsgType `json:"type"`
Id int64 `json:"id"`
Method string `json:"method"`
Params Vals `json:"params"`
Args Vals `json:"args"`
Result Vals `json:"result"`
Error string `json:"error"`
}
@ -48,13 +49,18 @@ type Callable interface {
Call(method string, params ...any) (Vals, error)
}
type pendingCall struct {
resultChan chan mo.Result[Vals]
resultType reflect.Type
}
type ipcCommon struct {
localApis map[string]any
socketPath string
conn net.Conn
errCh chan error
nextId int64
pendingCalls map[int64]chan mo.Result[Vals]
pendingCalls map[int64]*pendingCall
processingCalls atomic.Int64
stopRequested atomic.Bool
mu sync.Mutex
@ -65,7 +71,9 @@ func (ipc *ipcCommon) readConn() {
scn.Buffer(nil, maxMessageLength)
for scn.Scan() {
var msg Message
if err := json.Unmarshal(scn.Bytes(), &msg); err != nil {
msgBytes := scn.Bytes()
//log.Println(string(msgBytes))
if err := json.Unmarshal(msgBytes, &msg); err != nil {
ipc.raiseErr(fmt.Errorf("unmarshal message: %w", err))
break
}
@ -91,7 +99,6 @@ func (ipc *ipcCommon) sendMsg(msg Message) error {
if err != nil {
return fmt.Errorf("marshal message: %w", err)
}
data = append(data, '\n')
if _, err := ipc.conn.Write(data); err != nil {
@ -109,51 +116,93 @@ func (ipc *ipcCommon) handleCall(msg Message) {
ipc.processingCalls.Add(1)
defer ipc.processingCalls.Add(-1)
parts := strings.Split(msg.Method, ".")
if len(parts) != 2 {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("invalid method: %s", msg.Method))
return
}
endpointName, methodName := parts[0], parts[1]
defer func() {
if err := recover(); err != nil {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("handle call panicked: %s", err))
}
}()
localApi, ok := ipc.localApis[endpointName]
if !ok {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("endpoint not found: %s", endpointName))
return
}
method := reflect.ValueOf(localApi).MethodByName(methodName)
if !method.IsValid() {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("method not found: %s", msg.Method))
method, err := ipc.findMethod(msg.Method)
if err != nil {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("find method: %w", err))
return
}
argsCount := method.Type().NumIn()
if len(msg.Params) != argsCount {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("argument count mismatch: expected %d, got %d", argsCount, len(msg.Params)))
if len(msg.Args) != argsCount {
ipc.sendResponse(msg.Id, nil, fmt.Errorf("args count mismatch: expected %d, got %d", argsCount, len(msg.Args)))
return
}
var args []reflect.Value
for _, param := range msg.Params {
args = append(args, reflect.ValueOf(param))
for i, arg := range msg.Args {
paramType := method.Type().In(i)
argType := reflect.TypeOf(arg)
arg = ipc.convType(paramType, argType, arg)
args = append(args, reflect.ValueOf(arg))
}
results := method.Call(args)
resVals := results[0 : len(results)-1]
resErrVal := results[len(results)-1]
allResultVals := method.Call(args)
retResultVals := allResultVals[0 : len(allResultVals)-1]
errResultVals := allResultVals[len(allResultVals)-1]
var res []any
for _, resVal := range resVals {
res = append(res, resVal.Interface())
var results []any
for _, resVal := range retResultVals {
results = append(results, resVal.Interface())
}
var resErr error
if !resErrVal.IsNil() {
resErr = resErrVal.Interface().(error)
if !errResultVals.IsNil() {
resErr = errResultVals.Interface().(error)
}
ipc.sendResponse(msg.Id, res, resErr)
ipc.sendResponse(msg.Id, results, resErr)
}
func (ipc *ipcCommon) convType(needType reflect.Type, gotType reflect.Type, arg any) any {
switch needType.Kind() {
case reflect.Int:
// JSON decodes any number to float64. If we need int, we should check and convert
if gotType.Kind() == reflect.Float64 {
floatArg := arg.(float64)
if float64(int64(floatArg)) == floatArg && !needType.OverflowInt(int64(floatArg)) {
arg = int(floatArg)
}
}
case reflect.Slice:
switch needType.Elem().Kind() {
case reflect.Uint8:
if gotType.Kind() == reflect.String {
var err error
arg, err = base64.StdEncoding.DecodeString(arg.(string))
if err != nil {
panic(fmt.Sprintf("decode base64: %s", err))
}
}
}
}
return arg
}
func (ipc *ipcCommon) findMethod(methodName string) (reflect.Value, error) {
parts := strings.Split(methodName, ".")
if len(parts) != 2 {
return reflect.Value{}, fmt.Errorf("invalid method: %s", methodName)
}
endpointName, methodName := parts[0], parts[1]
localApi, ok := ipc.localApis[endpointName]
if !ok {
return reflect.Value{}, fmt.Errorf("endpoint not found: %s", endpointName)
}
method := reflect.ValueOf(localApi).MethodByName(methodName)
if !method.IsValid() {
return reflect.Value{}, fmt.Errorf("method not found: %s", methodName)
}
return method, nil
}
func (ipc *ipcCommon) sendResponse(id int64, result []any, err error) {
@ -174,7 +223,7 @@ func (ipc *ipcCommon) sendResponse(id int64, result []any, err error) {
func (ipc *ipcCommon) handleResponse(msg Message) {
ipc.mu.Lock()
ch, ok := ipc.pendingCalls[msg.Id]
call, ok := ipc.pendingCalls[msg.Id]
if ok {
delete(ipc.pendingCalls, msg.Id)
}
@ -191,8 +240,8 @@ func (ipc *ipcCommon) handleResponse(msg Message) {
} else {
res = mo.Err[Vals](fmt.Errorf("remote error: %s", msg.Error))
}
ch <- res
close(ch)
call.resultChan <- res
close(call.resultChan)
}
func (ipc *ipcCommon) Call(method string, params ...any) (Vals, error) {
@ -203,15 +252,17 @@ func (ipc *ipcCommon) Call(method string, params ...any) (Vals, error) {
ipc.mu.Lock()
id := ipc.nextId
ipc.nextId++
resChan := make(chan mo.Result[Vals], 1)
ipc.pendingCalls[id] = resChan
call := &pendingCall{
resultChan: make(chan mo.Result[Vals], 1),
}
ipc.pendingCalls[id] = call
ipc.mu.Unlock()
msg := Message{
Type: MsgCall,
Id: id,
Method: method,
Params: params,
Args: params,
}
if err := ipc.sendMsg(msg); err != nil {
@ -221,7 +272,7 @@ func (ipc *ipcCommon) Call(method string, params ...any) (Vals, error) {
return nil, fmt.Errorf("send call: %w", err)
}
result := <-resChan
result := <-call.resultChan
return result.Get()
}
@ -237,7 +288,7 @@ func (ipc *ipcCommon) closeConn() {
defer ipc.mu.Unlock()
_ = ipc.conn.Close()
for _, call := range ipc.pendingCalls {
call <- mo.Err[Vals](fmt.Errorf("call cancelled due to ipc termination"))
call.resultChan <- mo.Err[Vals](fmt.Errorf("call cancelled due to ipc termination"))
}
}
@ -251,7 +302,7 @@ func NewParent(cmd *exec.Cmd, localApis ...any) (*ParentIPC, error) {
p := ParentIPC{
ipcCommon: &ipcCommon{
localApis: mapTypeNames(localApis),
pendingCalls: make(map[int64]chan mo.Result[Vals]),
pendingCalls: make(map[int64]*pendingCall),
errCh: make(chan error, 1),
socketPath: filepath.Join(os.TempDir(), fmt.Sprintf("kitten-ipc-%d.sock", os.Getpid())),
},
@ -326,34 +377,46 @@ func (p *ParentIPC) Stop() error {
}
p.stopRequested.Store(true)
if err := p.cmd.Process.Signal(syscall.SIGINT); err != nil {
return fmt.Errorf("send SIGTERM: %w", err)
return fmt.Errorf("send SIGINT: %w", err)
}
return p.Wait()
}
func (p *ParentIPC) Wait() error {
func (p *ParentIPC) Wait(timeout ...time.Duration) (retErr error) {
waitErrCh := make(chan error, 1)
const defaultTimeout = time.Duration(1<<63 - 1) // max duration in go
_timeout := variadicToOption(timeout).OrElse(defaultTimeout)
go func() {
waitErrCh <- p.cmd.Wait()
}()
var retErr error
select {
case err := <-p.errCh:
retErr = fmt.Errorf("ipc internal error: %w", err)
case err := <-waitErrCh:
if err != nil {
var exitErr *exec.ExitError
if ok := errors.As(err, &exitErr); ok {
if !exitErr.Success() {
ws, ok := exitErr.Sys().(syscall.WaitStatus)
if !(ok && ws.Signaled() && ws.Signal() == syscall.SIGINT && p.stopRequested.Load()) {
retErr = fmt.Errorf("cmd wait: %w", err)
loop:
for {
select {
case err := <-p.errCh:
retErr = mergeErr(retErr, fmt.Errorf("ipc internal error: %w", err))
break loop
case err := <-waitErrCh:
if err != nil {
var exitErr *exec.ExitError
if ok := errors.As(err, &exitErr); ok {
if !exitErr.Success() {
ws, ok := exitErr.Sys().(syscall.WaitStatus)
if !(ok && ws.Signaled() && ws.Signal() == syscall.SIGINT && p.stopRequested.Load()) {
retErr = mergeErr(retErr, fmt.Errorf("cmd wait: %w", err))
}
}
} else {
retErr = mergeErr(retErr, fmt.Errorf("cmd wait: %w", err))
}
} else {
retErr = fmt.Errorf("cmd wait: %w", err)
}
break loop
case <-time.After(_timeout):
p.stopRequested.Store(true)
if err := p.cmd.Process.Signal(syscall.SIGINT); err != nil {
retErr = mergeErr(retErr, fmt.Errorf("send SIGINT: %w", err))
}
}
}
@ -371,7 +434,7 @@ func NewChild(localApis ...any) (*ChildIPC, error) {
c := ChildIPC{
ipcCommon: &ipcCommon{
localApis: mapTypeNames(localApis),
pendingCalls: make(map[int64]chan mo.Result[Vals]),
pendingCalls: make(map[int64]*pendingCall),
errCh: make(chan error, 1),
},
}
@ -426,3 +489,13 @@ func mapTypeNames(types []any) map[string]any {
}
return result
}
func variadicToOption[T any](variadic []T) mo.Option[T] {
if len(variadic) >= 2 {
panic("variadic param count must be 0 or 1")
}
if len(variadic) == 0 {
return mo.None[T]()
}
return mo.Some(variadic[0])
}

40
lib/golang/lib_test.go Normal file
View File

@ -0,0 +1,40 @@
package golang
import (
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewParent(t *testing.T) {
t.Run("socket argument in command", func(t *testing.T) {
cmd := exec.Command("/bin/sh", ipcSocketArg, "/tmp/kek")
_, err := NewParent(cmd)
assert.Error(t, err)
})
t.Run("nonexistent binary", func(t *testing.T) {
cmd := exec.Command("/nonexistent/binary")
p, err := NewParent(cmd)
assert.NoError(t, err)
assert.Error(t, p.Start())
})
t.Run("connection timeout", func(t *testing.T) {
cmd := exec.Command("../testdata/sleep15.sh")
p, err := NewParent(cmd)
assert.NoError(t, err)
assert.Error(t, p.Start())
})
t.Run("child finished before accepting connection", func(t *testing.T) {
cmd := exec.Command("../testdata/sleep3.sh")
p, err := NewParent(cmd)
assert.NoError(t, err)
start := time.Now()
assert.Error(t, p.Start())
assert.WithinDuration(t, time.Now(), start, time.Second*4)
})
}

3
lib/testdata/sleep15.sh vendored Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
sleep 15s

3
lib/testdata/sleep3.sh vendored Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
sleep 3s

View File

@ -1,2 +0,0 @@
export { ParentIPC, ChildIPC } from './lib.js';
//# sourceMappingURL=index.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAC"}

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAC"}

60
lib/ts/dist/lib.d.ts vendored
View File

@ -1,60 +0,0 @@
import * as net from 'node:net';
declare enum MsgType {
Call = 1,
Response = 2
}
type Vals = any[];
interface CallMessage {
type: MsgType.Call;
id: number;
method: string;
params: Vals;
}
interface ResponseMessage {
type: MsgType.Response;
id: number;
result?: Vals;
error?: string;
}
type Message = CallMessage | ResponseMessage;
interface CallResult {
result: Vals;
error: Error | null;
}
declare abstract class IPCCommon {
protected localApis: Record<string, any>;
protected socketPath: string;
protected conn: net.Socket | null;
protected nextId: number;
protected pendingCalls: Record<number, (result: CallResult) => void>;
protected stopRequested: boolean;
protected processingCalls: number;
protected onError?: (err: Error) => void;
protected onClose?: () => void;
protected constructor(localApis: object[], socketPath: string);
protected readConn(): void;
protected processMsg(msg: Message): void;
protected sendMsg(msg: Message): void;
protected handleCall(msg: CallMessage): Promise<void>;
protected handleResponse(msg: ResponseMessage): void;
stop(): void;
call(method: string, ...params: Vals): Promise<Vals>;
protected raiseErr(err: Error): void;
}
export declare class ParentIPC extends IPCCommon {
private readonly cmdPath;
private readonly cmdArgs;
private cmd;
private readonly listener;
constructor(cmdPath: string, cmdArgs: string[], ...localApis: object[]);
start(): Promise<void>;
private acceptConn;
wait(): Promise<void>;
}
export declare class ChildIPC extends IPCCommon {
constructor(...localApis: object[]);
start(): Promise<void>;
wait(): Promise<void>;
}
export {};
//# sourceMappingURL=lib.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAUhC,aAAK,OAAO;IACR,IAAI,IAAI;IACR,QAAQ,IAAI;CACf;AAED,KAAK,IAAI,GAAG,GAAG,EAAE,CAAC;AAElB,UAAU,WAAW;IACjB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,CAAC;CAChB;AAED,UAAU,eAAe;IACrB,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,OAAO,GAAG,WAAW,GAAG,eAAe,CAAC;AAE7C,UAAU,UAAU;IAChB,MAAM,EAAE,IAAI,CAAC;IACb,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACvB;AAED,uBAAe,SAAS;IACpB,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzC,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAQ;IACzC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAK;IAC7B,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC,CAAM;IAC1E,SAAS,CAAC,aAAa,EAAE,OAAO,CAAS;IACzC,SAAS,CAAC,eAAe,EAAE,MAAM,CAAK;IACtC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B,SAAS,aAAa,SAAS,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM;IAS7D,SAAS,CAAC,QAAQ,IAAI,IAAI;IA4B1B,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAWxC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;cAWrB,UAAU,CAAC,GAAG,EAAE,WAAW;IA6C3C,SAAS,CAAC,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI;IAapD,IAAI;IAWJ,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBpD,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI;CAGvC;AAGD,qBAAa,SAAU,SAAQ,SAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,GAAG,CAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;gBAE1B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,EAAE,MAAM,EAAE;IAahE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAwBd,UAAU;IAmBlB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB9B;AAGD,qBAAa,QAAS,SAAQ,SAAS;gBACvB,GAAG,SAAS,EAAE,MAAM,EAAE;IAI5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAa9B"}

File diff suppressed because one or more lines are too long

View File

@ -1,38 +0,0 @@
{
"name": "kitten-ipc",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kitten-ipc",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@types/node": "^22.10.5",
"ts-events": "^3.4.1"
}
},
"node_modules/@types/node": {
"version": "22.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/ts-events": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/ts-events/-/ts-events-3.4.1.tgz",
"integrity": "sha512-px05Slmyh6Bnfi7ma0YIU6cYXnisi+iL/2lhClu+s0ZkTdfPosiGp0H8aoQW7ASSXgcXYXAqujD0CcKYr5YlAw==",
"license": "ISC"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
}
}
}

View File

@ -1,27 +1,28 @@
{
"name": "kitten-ipc",
"description": "ipc lib",
"version": "1.0.0",
"version": "1.0.1",
"author": "Egor3f <ef@efprojects.com>",
"license": "Apache 2.0",
"license": "Apache-2.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/types.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/types.d.ts"
}
},
"scripts": {
"build": "tsc"
"build": "tsc",
"test": "vitest run --teardown-timeout=20000 --test-timeout=20000 --sequence.concurrent"
},
"dependencies": {
"@types/node": "^22.10.5",
"ts-events": "^3.4.1"
"@types/node": "^22.10.5"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"devDependencies": {
"typescript": "^5.9.3",
"vitest": "^4.0.8"
}
}

24
lib/ts/src/asyncqueue.ts Normal file
View File

@ -0,0 +1,24 @@
export class AsyncQueue<T> {
private store: T[] = [];
private collectors: ((val: T[]) => void)[] = [];
put(val: T) {
this.store.push(val);
for(const collector of this.collectors) {
collector(this.store);
}
this.collectors = [];
}
async collect(): Promise<T[]> {
if(this.store.length > 0) {
const store = this.store;
this.store = [];
return new Promise(resolve => resolve(store));
} else {
return new Promise(resolve => {
this.collectors.push(resolve);
})
}
}
}

14
lib/ts/src/lib.test.ts Normal file
View File

@ -0,0 +1,14 @@
import {test} from 'vitest';
import {ParentIPC} from './lib.js';
test('test connection timeout', async ({expect}) => {
const parentIpc = new ParentIPC('../testdata/sleep15.sh', []);
await parentIpc.start();
await expect(parentIpc.wait()).rejects.toThrowError('timed out');
});
test('test process stop before connection accept', async ({expect}) => {
const parentIpc = new ParentIPC('../testdata/sleep3.sh', []);
await parentIpc.start();
await expect(parentIpc.wait()).rejects.toThrowError('command exited before connection established');
});

View File

@ -5,9 +5,12 @@ import * as os from 'node:os';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as util from 'node:util';
import {AsyncQueue} from './asyncqueue.js';
const IPC_SOCKET_ARG = 'ipc-socket';
type JSONSerializable = string | number | boolean;
enum MsgType {
Call = 1,
Response = 2,
@ -19,7 +22,7 @@ interface CallMessage {
type: MsgType.Call,
id: number,
method: string;
params: Vals;
args: Vals;
}
interface ResponseMessage {
@ -44,7 +47,9 @@ abstract class IPCCommon {
protected pendingCalls: Record<number, (result: CallResult) => void> = {};
protected stopRequested: boolean = false;
protected processingCalls: number = 0;
protected onError?: (err: Error) => void;
protected ready = false;
protected errorQueue = new AsyncQueue<Error>();
protected onClose?: () => void;
protected constructor(localApis: object[], socketPath: string) {
@ -82,12 +87,14 @@ abstract class IPCCommon {
this.raiseErr(new Error(`${ e }`));
}
});
this.ready = true;
}
protected processMsg(msg: Message): void {
switch (msg.type) {
case MsgType.Call:
this.handleCall(msg);
this.handleCall(msg).catch((e) => this.errorQueue.put(e));
break;
case MsgType.Response:
this.handleResponse(msg);
@ -124,18 +131,18 @@ abstract class IPCCommon {
}
const argsCount = method.length;
if (msg.params.length !== argsCount) {
if (msg.args.length !== argsCount) {
this.sendMsg({
type: MsgType.Response,
id: msg.id,
error: `argument count mismatch: expected ${ argsCount }, got ${ msg.params.length }`
error: `argument count mismatch: expected ${ argsCount }, got ${ msg.args.length }`
});
return;
}
try {
this.processingCalls++;
let result = method.apply(endpoint, msg.params);
let result = method.apply(endpoint, msg.args);
if (result instanceof Promise) {
result = await result;
}
@ -164,18 +171,7 @@ abstract class IPCCommon {
callback({result: msg.result || [], error: err});
}
stop() {
if (this.stopRequested) {
throw new Error('close already requested');
}
if (!this.conn || this.conn.readyState === 'closed') {
throw new Error('connection already closed');
}
this.stopRequested = true;
if (this.onClose) this.onClose();
}
call(method: string, ...params: Vals): Promise<Vals> {
call(method: string, ...args: Vals): Promise<Vals> {
return new Promise((resolve, reject) => {
const id = this.nextId++;
@ -187,7 +183,7 @@ abstract class IPCCommon {
}
};
try {
this.sendMsg({type: MsgType.Call, id, method, params});
this.sendMsg({type: MsgType.Call, id, method, args: args.map(this.convType)});
} catch (e) {
delete this.pendingCalls[id];
reject(new Error(`send call: ${ e }`));
@ -195,8 +191,36 @@ abstract class IPCCommon {
});
}
private convType(arg: any): JSONSerializable {
// noinspection FallThroughInSwitchStatementJS
switch (typeof arg) {
case 'string':
case 'boolean':
case 'number':
return arg;
// @ts-expect-error TS7029
case 'object':
if(arg instanceof Buffer) {
return arg.toString('base64');
}
default:
throw new Error(`arg type ${typeof arg} is not supported`);
}
}
stop() {
if (this.stopRequested) {
throw new Error('close already requested');
}
if (!this.conn || this.conn.readyState === 'closed') {
throw new Error('connection already closed');
}
this.stopRequested = true;
if (this.onClose) this.onClose();
}
protected raiseErr(err: Error): void {
if (this.onError) this.onError(err);
this.errorQueue.put(err);
}
}
@ -226,7 +250,6 @@ export class ParentIPC extends IPCCommon {
} catch {
}
await new Promise<void>((resolve, reject) => {
this.listener.listen(this.socketPath, () => {
resolve();
@ -264,19 +287,27 @@ export class ParentIPC extends IPCCommon {
}
async wait(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.cmd) throw new Error('Command is not started yet');
return new Promise(async (resolve, reject) => {
if (!this.cmd) {
reject('Command is not started yet');
return;
}
this.cmd.addListener('close', (code, signal) => {
if (signal || code) {
if (signal) reject(new Error(`Process exited with signal ${ signal }`));
else reject(new Error(`Process exited with code ${ code }`));
} else if(!this.ready) {
reject('command exited before connection established');
} else {
resolve();
}
});
this.onError = (err) => {
reject(err);
};
const errors = await this.errorQueue.collect();
if(errors.length === 1) {
reject(errors[0]);
} else if(errors.length > 1) {
reject(new Error(errors.map(Error.toString).join(', ')));
}
});
}
}
@ -298,10 +329,13 @@ export class ChildIPC extends IPCCommon {
}
async wait(): Promise<void> {
return new Promise((resolve, reject) => {
this.onError = (err) => {
reject(err);
};
return new Promise(async (resolve, reject) => {
const errors = await this.errorQueue.collect();
if(errors.length === 1) {
reject(errors[0]);
} else if(errors.length > 1) {
reject(new Error(errors.map(Error.toString).join(', ')));
}
this.onClose = () => {
if (this.processingCalls === 0) {
this.conn?.destroy();
@ -338,6 +372,10 @@ function sleep(ms: number): Promise<void> {
// throws on timeout
function timeout<T>(prom: Promise<T>, ms: number): Promise<T> {
return Promise.race(
[prom, new Promise((res, reject) => setTimeout(reject, ms))]
[
prom,
new Promise((res, reject) => {
setTimeout(() => {reject(new Error('timed out'))}, ms)
})]
) as Promise<T>;
}

585
lib/ts/yarn.lock Normal file
View File

@ -0,0 +1,585 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/aix-ppc64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c"
integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==
"@esbuild/android-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752"
integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==
"@esbuild/android-arm@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a"
integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==
"@esbuild/android-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16"
integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==
"@esbuild/darwin-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd"
integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==
"@esbuild/darwin-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e"
integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==
"@esbuild/freebsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe"
integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==
"@esbuild/freebsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3"
integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==
"@esbuild/linux-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977"
integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==
"@esbuild/linux-arm@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9"
integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==
"@esbuild/linux-ia32@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0"
integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==
"@esbuild/linux-loong64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0"
integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==
"@esbuild/linux-mips64el@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd"
integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==
"@esbuild/linux-ppc64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869"
integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==
"@esbuild/linux-riscv64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6"
integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==
"@esbuild/linux-s390x@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663"
integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==
"@esbuild/linux-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306"
integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==
"@esbuild/netbsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4"
integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==
"@esbuild/netbsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076"
integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==
"@esbuild/openbsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd"
integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==
"@esbuild/openbsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679"
integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==
"@esbuild/openharmony-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d"
integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==
"@esbuild/sunos-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6"
integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==
"@esbuild/win32-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323"
integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==
"@esbuild/win32-ia32@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267"
integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==
"@esbuild/win32-x64@0.25.12":
version "0.25.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
"@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@rollup/rollup-android-arm-eabi@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz#63f6bdc496180079976e655473d5bea99b21f3ff"
integrity sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==
"@rollup/rollup-android-arm64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz#177f5e504d2f332edd0ddd3682f91ab72528fb60"
integrity sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==
"@rollup/rollup-darwin-arm64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz#ffdbe0cc43c88a35be2821f99cdff4c7a5ee2116"
integrity sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==
"@rollup/rollup-darwin-x64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz#27a4852923010abbcd1f028c7e8bd6bf0ccbe755"
integrity sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==
"@rollup/rollup-freebsd-arm64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz#a02b83018e487674ab445198786bef9b41cad9f0"
integrity sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==
"@rollup/rollup-freebsd-x64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz#fe898a4f0ff7c30f8377c3976ae76b89720c41da"
integrity sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==
"@rollup/rollup-linux-arm-gnueabihf@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz#be5a731a9f7bd7bc707457a768940b6107a9215e"
integrity sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==
"@rollup/rollup-linux-arm-musleabihf@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz#30ce6548a9e3591303507c37280300edb0cd1d14"
integrity sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==
"@rollup/rollup-linux-arm64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz#ec76f4223335e86cd61b0d596f34e97223f4f711"
integrity sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==
"@rollup/rollup-linux-arm64-musl@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz#9d4d87c2988ec8e4bb3cf4516dda7ef6d09dcd3d"
integrity sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==
"@rollup/rollup-linux-loong64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz#584bc6f3c33b30c3dbfdad36ac9c7792e4df5199"
integrity sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==
"@rollup/rollup-linux-ppc64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz#3e9a3b095a7d7da6043cb9caa54439d3b598aaf5"
integrity sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==
"@rollup/rollup-linux-riscv64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz#f3c3d6523d246eef4aa1eed265f1ba31b9eef7c8"
integrity sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==
"@rollup/rollup-linux-riscv64-musl@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz#0a8944b4f29a1ba923fb9c2ddb829e621f004988"
integrity sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==
"@rollup/rollup-linux-s390x-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz#bcb48f2d509ef6b33ba89f7d76a2f3805be8d4c8"
integrity sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==
"@rollup/rollup-linux-x64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz#ca9045e3b8e8dc0797e55d0229d5c664211bf366"
integrity sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==
"@rollup/rollup-linux-x64-musl@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz#740876db76078e37bd43cc8584ff1c7f6b382df8"
integrity sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==
"@rollup/rollup-openharmony-arm64@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz#3ff19213afe46b806fb6ec105f2664e4027e4cbc"
integrity sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==
"@rollup/rollup-win32-arm64-msvc@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz#cbba39610831747f8050a306811776534df1030d"
integrity sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==
"@rollup/rollup-win32-ia32-msvc@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz#5453c7ebba95d2bbfcc94c744c05586d587fb640"
integrity sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==
"@rollup/rollup-win32-x64-gnu@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz#01e1acb0dacb220d13c8992340f7bc868a564832"
integrity sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==
"@rollup/rollup-win32-x64-msvc@4.53.1":
version "4.53.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz#56eeb602545ec03ce84633b331c2e3ece07b99c3"
integrity sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==
"@standard-schema/spec@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
"@types/chai@^5.2.2":
version "5.2.3"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==
dependencies:
"@types/deep-eql" "*"
assertion-error "^2.0.1"
"@types/deep-eql@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
"@types/estree@1.0.8", "@types/estree@^1.0.0":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/node@^22.10.5":
version "22.19.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.0.tgz#849606ef3920850583a4e7ee0930987c35ad80be"
integrity sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==
dependencies:
undici-types "~6.21.0"
"@vitest/expect@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.8.tgz#02df33fb1f99091df660a80b7113e6d2f176ee10"
integrity sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==
dependencies:
"@standard-schema/spec" "^1.0.0"
"@types/chai" "^5.2.2"
"@vitest/spy" "4.0.8"
"@vitest/utils" "4.0.8"
chai "^6.2.0"
tinyrainbow "^3.0.3"
"@vitest/mocker@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.0.8.tgz#8fe875716e742635beb132a5e93ef8c151b0a4ec"
integrity sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==
dependencies:
"@vitest/spy" "4.0.8"
estree-walker "^3.0.3"
magic-string "^0.30.21"
"@vitest/pretty-format@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.8.tgz#752866f7dc62aa448af34404b2f9f1a4e1e6f656"
integrity sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==
dependencies:
tinyrainbow "^3.0.3"
"@vitest/runner@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.0.8.tgz#bfa9605eb5dc498dda8abe66d900caef31269ff6"
integrity sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==
dependencies:
"@vitest/utils" "4.0.8"
pathe "^2.0.3"
"@vitest/snapshot@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.0.8.tgz#3ec18bdfa96f8e383d12f156d1c73c7dcfe1fd3d"
integrity sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==
dependencies:
"@vitest/pretty-format" "4.0.8"
magic-string "^0.30.21"
pathe "^2.0.3"
"@vitest/spy@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.8.tgz#d8f071143901bd2ee31a805b468c054e481ec615"
integrity sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==
"@vitest/utils@4.0.8":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.8.tgz#00dcf405df47a64157c0edcc3832f678ab577cef"
integrity sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==
dependencies:
"@vitest/pretty-format" "4.0.8"
tinyrainbow "^3.0.3"
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
chai@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.0.tgz#181bca6a219cddb99c3eeefb82483800ffa550ce"
integrity sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==
debug@^4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
es-module-lexer@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
esbuild@^0.25.0:
version "0.25.12"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.12"
"@esbuild/android-arm" "0.25.12"
"@esbuild/android-arm64" "0.25.12"
"@esbuild/android-x64" "0.25.12"
"@esbuild/darwin-arm64" "0.25.12"
"@esbuild/darwin-x64" "0.25.12"
"@esbuild/freebsd-arm64" "0.25.12"
"@esbuild/freebsd-x64" "0.25.12"
"@esbuild/linux-arm" "0.25.12"
"@esbuild/linux-arm64" "0.25.12"
"@esbuild/linux-ia32" "0.25.12"
"@esbuild/linux-loong64" "0.25.12"
"@esbuild/linux-mips64el" "0.25.12"
"@esbuild/linux-ppc64" "0.25.12"
"@esbuild/linux-riscv64" "0.25.12"
"@esbuild/linux-s390x" "0.25.12"
"@esbuild/linux-x64" "0.25.12"
"@esbuild/netbsd-arm64" "0.25.12"
"@esbuild/netbsd-x64" "0.25.12"
"@esbuild/openbsd-arm64" "0.25.12"
"@esbuild/openbsd-x64" "0.25.12"
"@esbuild/openharmony-arm64" "0.25.12"
"@esbuild/sunos-x64" "0.25.12"
"@esbuild/win32-arm64" "0.25.12"
"@esbuild/win32-ia32" "0.25.12"
"@esbuild/win32-x64" "0.25.12"
estree-walker@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
expect-type@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3"
integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==
fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
rollup@^4.43.0:
version "4.53.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.1.tgz#84d1d378584a15dedfcdcff7767a8b9d92d8d3d9"
integrity sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.53.1"
"@rollup/rollup-android-arm64" "4.53.1"
"@rollup/rollup-darwin-arm64" "4.53.1"
"@rollup/rollup-darwin-x64" "4.53.1"
"@rollup/rollup-freebsd-arm64" "4.53.1"
"@rollup/rollup-freebsd-x64" "4.53.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.53.1"
"@rollup/rollup-linux-arm-musleabihf" "4.53.1"
"@rollup/rollup-linux-arm64-gnu" "4.53.1"
"@rollup/rollup-linux-arm64-musl" "4.53.1"
"@rollup/rollup-linux-loong64-gnu" "4.53.1"
"@rollup/rollup-linux-ppc64-gnu" "4.53.1"
"@rollup/rollup-linux-riscv64-gnu" "4.53.1"
"@rollup/rollup-linux-riscv64-musl" "4.53.1"
"@rollup/rollup-linux-s390x-gnu" "4.53.1"
"@rollup/rollup-linux-x64-gnu" "4.53.1"
"@rollup/rollup-linux-x64-musl" "4.53.1"
"@rollup/rollup-openharmony-arm64" "4.53.1"
"@rollup/rollup-win32-arm64-msvc" "4.53.1"
"@rollup/rollup-win32-ia32-msvc" "4.53.1"
"@rollup/rollup-win32-x64-gnu" "4.53.1"
"@rollup/rollup-win32-x64-msvc" "4.53.1"
fsevents "~2.3.2"
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
stackback@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
tinybench@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinyexec@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tinyrainbow@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz#984a5b1c1b25854a9b6bccbe77964d0593d1ea42"
integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
"vite@^6.0.0 || ^7.0.0":
version "7.2.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.2.tgz#17dd62eac2d0ca0fa90131c5f56e4fefb8845362"
integrity sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==
dependencies:
esbuild "^0.25.0"
fdir "^6.5.0"
picomatch "^4.0.3"
postcss "^8.5.6"
rollup "^4.43.0"
tinyglobby "^0.2.15"
optionalDependencies:
fsevents "~2.3.3"
vitest@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.0.8.tgz#0c61a81261cf51450c70bc3c9a05a31d8526b14d"
integrity sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==
dependencies:
"@vitest/expect" "4.0.8"
"@vitest/mocker" "4.0.8"
"@vitest/pretty-format" "4.0.8"
"@vitest/runner" "4.0.8"
"@vitest/snapshot" "4.0.8"
"@vitest/spy" "4.0.8"
"@vitest/utils" "4.0.8"
debug "^4.4.3"
es-module-lexer "^1.7.0"
expect-type "^1.2.2"
magic-string "^0.30.21"
pathe "^2.0.3"
picomatch "^4.0.3"
std-env "^3.10.0"
tinybench "^2.9.0"
tinyexec "^0.3.2"
tinyglobby "^0.2.15"
tinyrainbow "^3.0.3"
vite "^6.0.0 || ^7.0.0"
why-is-node-running "^2.3.0"
why-is-node-running@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
dependencies:
siginfo "^2.0.0"
stackback "0.0.2"