Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# CLAUDE.md

Guidance for working in this repository.

## What this is

`@jsonic/multisource` is a [Jsonic](https://jsonic.senecajs.org) plugin that
loads partial values from external sources (files, packages, in-memory maps)
into a single parse result. A directive character (`@` by default) marks a
reference; the plugin resolves it, processes the resolved text, and splices
the result into the parse output.

The repository ships **two implementations**:

- **TypeScript** — `src/`, tests in `test/`, built to `dist/` + `dist-test/`.
- **Go** — `go/`, a port published as the `github.com/jsonicjs/multisource/go`
module.

## TypeScript is canonical

**The TypeScript implementation is the source of truth.** The Go package is a
port that follows TS semantics. When the two diverge, **TS wins** — change Go
to match TS, not the other way around (unless you are deliberately fixing a
bug in TS first, then porting the fix).

When adding or changing behaviour:

1. Implement and test it in TypeScript first.
2. Port the change to Go, mirroring the public API names and behaviour as
closely as the language allows.
3. Keep both test suites and both `doc/` files in sync.

## Build & test

Use the `Makefile` targets (they wrap npm and go):

```sh
make build # build-ts + build-go
make test # test-ts + test-go
make build-ts # npm run build (tsc --build src test)
make test-ts # npm test (node --test dist-test/*.test.js)
make build-go # cd go && go build ./...
make test-go # cd go && go test ./...
```

Notes:

- TS tests run against the **built** output in `dist-test/`, so run
`make build-ts` (or `npm run build`) before `make test-ts`.
- `npm run reset` does a clean reinstall + build + test.
- Go has no external runtime deps beyond the other `jsonicjs/*/go` modules.

## Resolver & processor parity

Resolvers turn a reference into source text; processors turn source text into
a value. The public factory names are kept aligned across the two ports:

| Resolver (TS) | Resolver (Go) | Resolves from |
| -------------------- | -------------------- | -------------------- |
| `makeMemResolver` | `MakeMemResolver` | in-memory `map` |
| `makeFileResolver` | `MakeFileResolver` | filesystem |
| `makePkgResolver` | `MakePkgResolver` | `node_modules` |

| Processor (TS) | Processor (Go) | Handles |
| ------------------------- | ------------------- | ------------------ |
| default (string) | `DefaultProcessor` | unknown / raw |
| `makeJsonicProcessor` | `JsonicProcessor` | `.jsonic`, `.jsc` |
| (json via jsonic) | `JSONProcessor` | `.json` |
| `makeJavaScriptProcessor` | — (Node-only) | `.js` |

## Known Go ↔ TS differences

The ports are not line-for-line identical. Current intentional/known gaps:

- **`MakePkgResolver` is a portable subset.** Go has no `require.resolve`, so
the Go resolver walks `node_modules` directories, honours `package.json`
`"main"` for bare references, and tries implicit extensions / index files.
It does not implement Node's full module-resolution algorithm (e.g.
conditional `"exports"`).
- **No virtual filesystem in Go.** The TS resolvers accept a `ctx.meta.fs`
(used by tests via `memfs`); the Go resolvers use the OS filesystem
directly.
- **No `.js` processor in Go.** Executing JavaScript modules is Node-specific.
- **Base-path resolution.** The Go plugin resolves a reference against
`opts.Path` once, before calling the resolver; it does not yet track each
parent file's directory for relative nested includes the way the TS
`resolvePathSpec` does via `ctx.meta.multisource.path`.

If you close any of these gaps, update this list and both `doc/` files.

## Releasing the Go module

The Go module is versioned independently (see `const Version` in
`go/multisource.go`) and tagged `go/vX.Y.Z`. Use `make publish-go V=x.y.z`.
Do not bump or tag a release unless explicitly asked.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ single [Jsonic](https://jsonic.senecajs.org) parse result.

## Documentation

Documentation for both language implementations follows the
[Diátaxis](https://diataxis.fr) framework (Tutorials, How-to guides,
Explanation, Reference).
Documentation is available for both language implementations:

- TypeScript: [`doc/multisource-ts.md`](doc/multisource-ts.md)
- Go: [`doc/multisource-go.md`](doc/multisource-go.md)
Expand Down
70 changes: 68 additions & 2 deletions doc/multisource-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,48 @@ out, _ := j.Parse(`{x: @a}`)

## How-to guides

### Load sources from the filesystem

`MakeFileResolver` reads sources from disk, resolving the reference to an
absolute path and trying implicit extensions and index files when the path
has none:

```go
j := multisource.MakeJsonic(multisource.MultiSourceOptions{
Resolver: multisource.MakeFileResolver(),
Path: "config", // base directory for relative references
})
out, _ := j.Parse(`{db: @"database.jsonic"}`)
```

Pass `FileResolverOptions` to supply a path transformer or preloaded
contents (checked before touching disk):

```go
multisource.MakeFileResolver(multisource.FileResolverOptions{
PathFinder: func(spec string) string { return "conf/" + spec },
Preload: map[string]string{"/abs/path/a.jsonic": "{a:1}"},
})
```

### Resolve references from node_modules

`MakePkgResolver` resolves references inside `node_modules` folders. With no
options it walks up from the current working directory; pass `Paths` to set
explicit search roots:

```go
j := multisource.MakeJsonic(multisource.MultiSourceOptions{
Resolver: multisource.MakePkgResolver(multisource.PkgResolverOptions{
Paths: []string{"/path/to/project"},
}),
})
out, _ := j.Parse(`{cfg: @"some-pkg/config.jsonic"}`)
```

A bare package reference (`@"some-pkg"`) resolves via the package's
`package.json` `"main"`, falling back to index files.

### Supply a custom resolver

Implement the `Resolver` function type. It must populate `Resolution.Found`
Expand Down Expand Up @@ -160,7 +202,7 @@ All added alternates share the `multisource` group tag, supplied via the
### `Version`

```go
const Version = "0.1.0"
const Version = "0.1.4"
```

Go module release version.
Expand Down Expand Up @@ -196,11 +238,35 @@ Convenience wrapper around `MakeJsonic().Parse(src)`.

| Name | Kind |
| ---------------------- | --------------------- |
| `MakeMemResolver` | Resolver factory |
| `MakeMemResolver` | Resolver factory (in-memory map) |
| `MakeFileResolver` | Resolver factory (filesystem) |
| `MakePkgResolver` | Resolver factory (node_modules) |
| `DefaultProcessor` | Raw passthrough |
| `JSONProcessor` | `.json` via stdlib |
| `JsonicProcessor` | `.jsonic`, `.jsc` |

These mirror the canonical TypeScript resolvers (`makeMemResolver`,
`makeFileResolver`, `makePkgResolver`). `MakePkgResolver` implements the
portable subset of the TypeScript package resolver: it walks `node_modules`
directories, honours a package's `package.json` `"main"` for bare references,
and tries implicit extensions and index files. It does not reproduce Node's
full `require.resolve` algorithm (for example, conditional `"exports"`).

```go
// MakeFileResolver — load sources from disk.
type FileResolverOptions struct {
PathFinder func(spec string) string // Transform the raw reference path.
Preload map[string]string // Full path -> content, checked first.
}
func MakeFileResolver(opts ...FileResolverOptions) Resolver

// MakePkgResolver — resolve references inside node_modules.
type PkgResolverOptions struct {
Paths []string // node_modules search roots; empty walks up from cwd.
}
func MakePkgResolver(opts ...PkgResolverOptions) Resolver
```

### Types

```go
Expand Down
20 changes: 18 additions & 2 deletions go/multisource.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,30 @@ func buildPotentials(fullpath string, implicitExt []string) []string {
return nil
}
potentials := []string{fullpath}
ext := path.Ext(fullpath)
if ext == "" {

// Determine the final path segment in a separator-agnostic way: the
// in-memory resolver keys on forward slashes, while the file/pkg resolvers
// pass OS-native paths (e.g. Windows backslashes from filepath.Abs).
base := fullpath
if i := strings.LastIndexAny(fullpath, `/\`); i >= 0 {
base = fullpath[i+1:]
}

if path.Ext(base) == "" {
// Implicit extensions.
for _, ie := range implicitExt {
potentials = append(potentials, fullpath+ie)
}
// Folder index file.
for _, ie := range implicitExt {
potentials = append(potentials, fullpath+"/index"+ie)
}
// Folder index file including the folder name, e.g. foo/index.foo.jsonic.
if base != "" && base != "." {
for _, ie := range implicitExt {
potentials = append(potentials, fullpath+"/index."+base+ie)
}
}
}
return potentials
}
Expand Down
9 changes: 9 additions & 0 deletions go/multisource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,19 @@ func TestBuildPotentials(t *testing.T) {
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")
assert(t, "pot-idx-folder", p[7], "foo/index.foo.jsonic")

p = buildPotentials("bar.json", exts)
assert(t, "has-ext", len(p), 1)
assert(t, "has-ext-0", p[0], "bar.json")

// Windows-style (backslash) paths must extract the final segment, not the
// whole path, for the folder-name index variant. Regression guard: the
// `path` package only splits on '/', so filepath.Abs output on Windows
// (backslashes) would otherwise corrupt the folder name.
w := buildPotentials(`C:\proj\h`, exts)
assert(t, "win-no-ext", len(w), 10)
assert(t, "win-folder-index", w[7], `C:\proj\h/index.h.jsonic`)
}

func TestCustomProcessor(t *testing.T) {
Expand Down
Loading
Loading