diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3814526..a531c77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [24.x, latest] + node-version: [24.x] runs-on: ${{ matrix.os }} @@ -30,13 +30,23 @@ jobs: - run: npm run build --if-present - run: npm test - - name: test-cov - if: matrix['node-version'] == '24.x' && matrix['os'] == 'ubuntu-latest' - run: npm run test-cov + build-go: - - name: coveralls - if: matrix['node-version'] == '24.x' && matrix['os'] == 'ubuntu-latest' - uses: coverallsapp/github-action@main + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go-version: [1.24] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Use Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./coverage/lcov.info + go-version: ${{ matrix.go-version }} + - run: go build ./... + working-directory: go + - run: go test -v ./... + working-directory: go diff --git a/.gitignore b/.gitignore index 6778c70..6305d54 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,11 @@ typings/ *~ +dist +dist-test + +.idea + package-lock.json yarn.lock diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..e825e8d --- /dev/null +++ b/go/go.mod @@ -0,0 +1,5 @@ +module github.com/jsonicjs/multisource/go + +go 1.24.7 + +require github.com/jsonicjs/jsonic/go v0.1.4 diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..dc99d17 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,2 @@ +github.com/jsonicjs/jsonic/go v0.1.4 h1:V1KEzmg/jIwk25+JYj8ig1+B7190rHmH8WqZbT7XlgA= +github.com/jsonicjs/jsonic/go v0.1.4/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg= diff --git a/go/multisource.go b/go/multisource.go new file mode 100644 index 0000000..c67c219 --- /dev/null +++ b/go/multisource.go @@ -0,0 +1,245 @@ +/* Copyright (c) 2025 Richard Rodger, MIT License */ + +package multisource + +import ( + "encoding/json" + "path" + "strings" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// MultiSourceOptions configures the multisource parser. +type MultiSourceOptions struct { + Resolver Resolver + Path string + MarkChar string + Processor map[string]Processor + ImplicitExt []string +} + +// PathSpec represents a normalized path to a source. +type PathSpec struct { + Kind string // Source kind, usually normalized file extension. + Path string // Original path (possibly relative). + Full string // Normalized full path. + Base string // Current base path. + Abs bool // Path was absolute. +} + +// Resolution is the result of resolving a path spec. +type Resolution struct { + PathSpec + Src string // Source content. + Val any // Processed value. + Found bool // True if source was found. + Search []string // List of searched paths. +} + +// Resolver finds source content for a given path spec. +type Resolver func(spec PathSpec, opts *MultiSourceOptions) Resolution + +// Processor converts resolved source content into a value. +type Processor func(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) + +// NONE represents an unknown or missing extension. +const NONE = "" + +// DefaultProcessor returns the raw source string as the value. +func DefaultProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) { + res.Val = res.Src +} + +// JSONProcessor parses JSON source content. +func JSONProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) { + if res.Src == "" { + res.Val = nil + return + } + var val any + if err := json.Unmarshal([]byte(res.Src), &val); err != nil { + res.Val = res.Src + return + } + res.Val = val +} + +// JsonicProcessor parses source content using jsonic. +func JsonicProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) { + if res.Src == "" { + res.Val = nil + return + } + val, err := j.Parse(res.Src) + if err != nil { + res.Val = res.Src + return + } + res.Val = val +} + +// MakeMemResolver creates a resolver that looks up paths in a map. +func MakeMemResolver(files map[string]string) Resolver { + return func(spec PathSpec, opts *MultiSourceOptions) Resolution { + res := Resolution{ + PathSpec: spec, + Found: false, + } + + potentials := buildPotentials(spec.Full, opts.ImplicitExt) + res.Search = potentials + + for _, p := range potentials { + if src, ok := files[p]; ok { + res.Full = p + res.Kind = extKind(p) + res.Src = src + res.Found = true + return res + } + } + + return res + } +} + +// ResolvePathSpec normalizes a path specification. +func ResolvePathSpec(specPath string, base string) PathSpec { + abs := strings.HasPrefix(specPath, "/") || strings.HasPrefix(specPath, "\\") + + var full string + if abs { + full = specPath + } else if specPath != "" { + if base != "" { + full = base + "/" + specPath + } else { + full = specPath + } + } + + kind := extKind(full) + + return PathSpec{ + Kind: kind, + Path: specPath, + Full: full, + Base: base, + Abs: abs, + } +} + +// Parse parses a jsonic string with multisource support. +func Parse(src string, opts ...MultiSourceOptions) (any, error) { + var o MultiSourceOptions + if len(opts) > 0 { + o = opts[0] + } + j := MakeJsonic(o) + return j.Parse(src) +} + +// MakeJsonic creates a jsonic instance configured with multisource support. +func MakeJsonic(opts ...MultiSourceOptions) *jsonic.Jsonic { + var o MultiSourceOptions + if len(opts) > 0 { + o = opts[0] + } + + dopts := defaultOpts() + if o.MarkChar == "" { + o.MarkChar = dopts.MarkChar + } + if o.Processor == nil { + o.Processor = dopts.Processor + } + if o.ImplicitExt == nil { + o.ImplicitExt = dopts.ImplicitExt + } + if o.Resolver == nil { + o.Resolver = dopts.Resolver + } + + for i, ext := range o.ImplicitExt { + if !strings.HasPrefix(ext, ".") { + o.ImplicitExt[i] = "." + ext + } + } + + bTrue := true + + jopts := jsonic.Options{ + Value: &jsonic.ValueOptions{ + Lex: &bTrue, + }, + } + + j := jsonic.Make(jopts) + + pluginMap := map[string]any{ + "_opts": &o, + } + j.Use(MultiSource, pluginMap) + + return j +} + +func defaultOpts() *MultiSourceOptions { + return &MultiSourceOptions{ + MarkChar: "@", + Processor: map[string]Processor{ + NONE: DefaultProcessor, + "json": JSONProcessor, + "jsonic": JsonicProcessor, + "jsc": JsonicProcessor, + }, + ImplicitExt: []string{".jsonic", ".jsc", ".json"}, + Resolver: MakeMemResolver(map[string]string{}), + } +} + +func getOpts(m map[string]any) *MultiSourceOptions { + if m == nil { + return defaultOpts() + } + if o, ok := m["_opts"].(*MultiSourceOptions); ok { + return o + } + return defaultOpts() +} + +func getProcessor(kind string, procmap map[string]Processor) Processor { + if proc, ok := procmap[kind]; ok { + return proc + } + if proc, ok := procmap[NONE]; ok { + return proc + } + return DefaultProcessor +} + +func buildPotentials(fullpath string, implicitExt []string) []string { + if fullpath == "" { + return nil + } + potentials := []string{fullpath} + ext := path.Ext(fullpath) + if ext == "" { + for _, ie := range implicitExt { + potentials = append(potentials, fullpath+ie) + } + for _, ie := range implicitExt { + potentials = append(potentials, fullpath+"/index"+ie) + } + } + return potentials +} + +func extKind(fullpath string) string { + ext := path.Ext(fullpath) + if ext == "" { + return NONE + } + return strings.TrimPrefix(ext, ".") +} diff --git a/go/multisource_test.go b/go/multisource_test.go new file mode 100644 index 0000000..84d80c6 --- /dev/null +++ b/go/multisource_test.go @@ -0,0 +1,309 @@ +/* Copyright (c) 2025 Richard Rodger, MIT License */ + +package multisource + +import ( + "reflect" + "strings" + "testing" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// assert is a test helper that checks deep equality. +func assert(t *testing.T, name string, got, want any) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("%s:\n got: %#v\n want: %#v", name, got, want) + } +} + +func TestHappy(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + "b.jsc": `{b:2}`, + "c.txt": `CCC`, + "d.json": `{"d":3}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{a: @a.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "jsonic-ref", m["a"], map[string]any{"a": float64(1)}) + + r, err = j.Parse(`{c: @c.txt}`) + if err != nil { + t.Fatal(err) + } + m, _ = r.(map[string]any) + assert(t, "txt-ref", m["c"], "CCC") + + r, err = j.Parse(`{d: @d.json}`) + if err != nil { + t.Fatal(err) + } + m, _ = r.(map[string]any) + assert(t, "json-ref", m["d"], map[string]any{"d": float64(3)}) +} + +func TestImplicitExt(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + "b.jsc": `{b:2}`, + "c.json": `{"c":3}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{x: @a}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "implicit-jsonic", m["x"], map[string]any{"a": float64(1)}) + + r, err = j.Parse(`{x: @b}`) + if err != nil { + t.Fatal(err) + } + m, _ = r.(map[string]any) + assert(t, "implicit-jsc", m["x"], map[string]any{"b": float64(2)}) + + r, err = j.Parse(`{x: @c}`) + if err != nil { + t.Fatal(err) + } + m, _ = r.(map[string]any) + assert(t, "implicit-json", m["x"], map[string]any{"c": float64(3)}) +} + +func TestMultipleSources(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + "b.jsonic": `{b:2}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{x: @a.jsonic, y: @b.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "multi-a", m["x"], map[string]any{"a": float64(1)}) + assert(t, "multi-b", m["y"], map[string]any{"b": float64(2)}) +} + +func TestNotFound(t *testing.T) { + files := map[string]string{} + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{x: @missing}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "not-found", m["x"], nil) +} + +func TestBasePath(t *testing.T) { + files := map[string]string{ + "data/a.jsonic": `{a:1}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + Path: "data", + }) + + r, err := j.Parse(`{x: @a.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "base-path", m["x"], map[string]any{"a": float64(1)}) +} + +func TestJSONSource(t *testing.T) { + files := map[string]string{ + "config.json": `{"host":"localhost","port":8080}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{config: @config.json}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + cfg, _ := m["config"].(map[string]any) + assert(t, "json-host", cfg["host"], "localhost") + assert(t, "json-port", cfg["port"], float64(8080)) +} + +func TestIndexFile(t *testing.T) { + files := map[string]string{ + "mymod/index.jsonic": `{x:1}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{mod: @mymod}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "index-file", m["mod"], map[string]any{"x": float64(1)}) +} + +func TestMixedValues(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{x: @a.jsonic, y: 2, z: "hello"}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "ref-val", m["x"], map[string]any{"a": float64(1)}) + assert(t, "num-val", m["y"], float64(2)) + assert(t, "str-val", m["z"], "hello") +} + +func TestEmptyInput(t *testing.T) { + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(map[string]string{}), + }) + + r, err := j.Parse(`{}`) + if err != nil { + t.Fatal(err) + } + assert(t, "empty", r, map[string]any{}) +} + +func TestResolvePathSpec(t *testing.T) { + ps := ResolvePathSpec("a.jsonic", "base") + assert(t, "full", ps.Full, "base/a.jsonic") + assert(t, "kind", ps.Kind, "jsonic") + assert(t, "abs", ps.Abs, false) + + ps = ResolvePathSpec("/abs/a.json", "base") + assert(t, "abs-full", ps.Full, "/abs/a.json") + assert(t, "abs-kind", ps.Kind, "json") + assert(t, "abs-abs", ps.Abs, true) + + ps = ResolvePathSpec("noext", "") + assert(t, "noext-full", ps.Full, "noext") + assert(t, "noext-kind", ps.Kind, "") +} + +func TestBuildPotentials(t *testing.T) { + exts := []string{".jsonic", ".jsc", ".json"} + + p := buildPotentials("foo", exts) + assert(t, "pot-0", p[0], "foo") + assert(t, "pot-1", p[1], "foo.jsonic") + assert(t, "pot-2", p[2], "foo.jsc") + assert(t, "pot-3", p[3], "foo.json") + assert(t, "pot-idx-1", p[4], "foo/index.jsonic") + + p = buildPotentials("bar.json", exts) + assert(t, "has-ext", len(p), 1) + assert(t, "has-ext-0", p[0], "bar.json") +} + +func TestCustomProcessor(t *testing.T) { + files := map[string]string{ + "data.csv": "a,b,c", + } + + csvProc := func(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) { + parts := make([]any, 0) + for _, s := range splitCSV(res.Src) { + parts = append(parts, s) + } + res.Val = parts + } + + procs := map[string]Processor{ + NONE: DefaultProcessor, + "csv": csvProc, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + Processor: procs, + }) + + r, err := j.Parse(`{data: @data.csv}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "csv", m["data"], []any{"a", "b", "c"}) +} + +// splitCSV is a simple CSV field splitter for testing. +func splitCSV(s string) []string { + var result []string + for _, field := range strings.Split(s, ",") { + result = append(result, strings.TrimSpace(field)) + } + return result +} + +func TestParse(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + } + + r, err := Parse(`{x: @a.jsonic}`, MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "parse", m["x"], map[string]any{"a": float64(1)}) +} + +func TestAbsolutePath(t *testing.T) { + files := map[string]string{ + "/etc/config.jsonic": `{env:"prod"}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + Path: "ignored", + }) + + r, err := j.Parse(`{cfg: @/etc/config.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "abs-path", m["cfg"], map[string]any{"env": "prod"}) +} diff --git a/go/plugin.go b/go/plugin.go new file mode 100644 index 0000000..4708099 --- /dev/null +++ b/go/plugin.go @@ -0,0 +1,221 @@ +/* Copyright (c) 2025 Richard Rodger, MIT License */ + +package multisource + +import ( + "strings" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// lexMode tracks which kind of token the custom matchers should produce. +type lexMode int + +const ( + modeNormal lexMode = iota // Normal jsonic parsing. + modePath // Scanning a multisource path after @. +) + +// MultiSource is a jsonic plugin that adds multisource reference support. +// When '@path' is encountered in the input, the path is resolved using +// the configured resolver and processed into a value. +func MultiSource(j *jsonic.Jsonic, pluginOpts map[string]any) { + opts := getOpts(pluginOpts) + markChar := opts.MarkChar + if markChar == "" { + markChar = "@" + } + markByte := markChar[0] + + mode := modeNormal + + cfg := j.Config() + + // Register the mark character as a fixed token. + AT := j.Token("#AT", markChar) + cfg.SortFixedTokens() + + // Register custom token type for multisource paths. + MP := j.Token("#MP") + + // Standard tokens. + ZZ := j.Token("#ZZ") + OB := j.Token("#OB") // { + CB := j.Token("#CB") // } + CL := j.Token("#CL") // : + CA := j.Token("#CA") // , + _ = ZZ + _ = OB + _ = CB + _ = CL + _ = CA + + // Add the mark character to ender chars so built-in matchers stop there. + if cfg.EnderChars == nil { + cfg.EnderChars = make(map[rune]bool) + } + cfg.EnderChars[rune(markByte)] = true + + // Path matcher: reads the path after @. + // Runs at priority < 2e6 so it executes before built-in matchers. + j.AddMatcher("msrcpath", 100000, func(lex *jsonic.Lex) *jsonic.Token { + if mode != modePath { + return nil + } + mode = modeNormal + + pnt := lex.Cursor() + src := lex.Src + sI := pnt.SI + cI := pnt.CI + + if sI >= pnt.Len { + return nil + } + + // Skip leading whitespace. + for sI < pnt.Len && (src[sI] == ' ' || src[sI] == '\t') { + sI++ + cI++ + } + + ch := src[sI] + // Don't match at delimiters or quotes. + if ch == ',' || ch == '}' || ch == ']' || ch == '{' || ch == '[' || + ch == '\n' || ch == '\r' || ch == markByte { + return nil + } + + // Handle quoted paths: "path" or 'path'. + if ch == '"' || ch == '\'' { + quote := ch + sI++ // skip opening quote + cI++ + startI := sI + for sI < pnt.Len && src[sI] != quote { + if src[sI] == '\\' && sI+1 < pnt.Len { + sI += 2 + cI += 2 + continue + } + sI++ + cI++ + } + pathStr := src[startI:sI] + raw := src[pnt.SI:sI] + if sI < pnt.Len { + sI++ // skip closing quote + cI++ + raw = src[pnt.SI:sI] + } + tkn := lex.Token("#MP", MP, pathStr, raw) + pnt.SI = sI + pnt.CI = cI + return tkn + } + + startI := sI + + // Read path until a delimiter. + for sI < pnt.Len { + c := src[sI] + if c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == ',' || c == '}' || c == ']' || c == ':' || + c == '{' || c == '[' || c == markByte { + break + } + sI++ + cI++ + } + + if sI == startI { + return nil + } + + raw := src[startI:sI] + pathStr := strings.TrimSpace(raw) + + tkn := lex.Token("#MP", MP, pathStr, raw) + pnt.SI = sI + pnt.CI = cI + return tkn + }) + + // resolveSource resolves a multisource path and sets the node value. + resolveSource := func(pathStr string) any { + spec := ResolvePathSpec(pathStr, opts.Path) + res := opts.Resolver(spec, opts) + + if !res.Found { + return nil + } + + proc := getProcessor(res.Kind, opts.Processor) + proc(&res, opts, j) + + return res.Val + } + + // Extend the val rule to handle @path in value position. + j.Rule("val", func(rs *jsonic.RuleSpec) { + newAlts := []*jsonic.AltSpec{ + // @path in value position: resolve and use as value. + { + S: [][]jsonic.Tin{{AT}}, + P: "msrc", + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + mode = modePath + }, + }, + } + rs.Open = append(newAlts, rs.Open...) + }) + + // Extend the pair rule to handle @path in pair position (merge into map). + j.Rule("pair", func(rs *jsonic.RuleSpec) { + newAlts := []*jsonic.AltSpec{ + { + S: [][]jsonic.Tin{{AT}}, + P: "msrc", + U: map[string]any{"msrc_merge": true}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + mode = modePath + }, + }, + } + rs.Open = append(newAlts, rs.Open...) + }) + + // The msrc rule handles resolving the multisource path. + j.Rule("msrc", func(rs *jsonic.RuleSpec) { + rs.Clear() + + rs.Open = []*jsonic.AltSpec{ + { + S: [][]jsonic.Tin{{MP}}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + pathStr, _ := r.O0.Val.(string) + if pathStr == "" { + pathStr = r.O0.Src + } + + val := resolveSource(pathStr) + r.Node = val + + // If parent requested merge, merge the resolved map. + if r.Parent != nil && r.Parent != jsonic.NoRule { + if _, doMerge := r.Parent.U["msrc_merge"]; doMerge { + if m, ok := val.(map[string]any); ok { + if parent, ok := r.Parent.Node.(map[string]any); ok { + for k, v := range m { + parent[k] = v + } + } + } + } + } + }, + }, + } + }) +} diff --git a/package.json b/package.json index 510b5f9..ffc8998 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "scripts": { "test": "node --enable-source-maps --test \"dist-test/*.test.js\"", "test-cov": "rm -rf ./coverage && mkdir -p ./coverage && node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --enable-source-maps --test \"dist-test/*.test.js\"", - "test-some": "node --enable-source-maps --test-name-pattern=\"$TEST_PATTERN\" --test \"dist-test/*.test.js\"", + "test-some": "node --enable-source-maps --test-name-pattern=\"$npm_config_pattern\" --test \"dist-test/*.test.js\"", "watch": "tsc --build src test -w", "build": "tsc --build src test", "clean": "rm -rf dist dist-test node_modules yarn.lock package-lock.json", @@ -48,7 +48,7 @@ "devDependencies": { "@hapi/code": "^9.0.3", "@jsonic/doc": "^0.0.9", - "@types/node": "^25.3.3", + "@types/node": "^25.4.0", "jsonic-multisource-pkg-test": "^0.0.1", "memfs": "^4.56.11", "typescript": "^5.9.3" diff --git a/src/tsconfig.json b/src/tsconfig.json index 9e11f2d..da88ae7 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -3,14 +3,12 @@ "esModuleInterop": true, "module": "nodenext", "noEmitOnError": true, - "outDir":"../dist", - "rootDir":".", + "outDir": "../dist", + "rootDir": ".", + "declaration": true, "resolveJsonModule": true, "sourceMap": true, "strict": true, - "target": "es2021", - "declaration": true, - "declarationDir": "../dist" + "target": "ES2021" } } -