Skip to content

Nested relative loads + injectable io/fs.FS for the Go port (parity with TS)#12

Merged
rjrodger merged 3 commits into
mainfrom
claude/funny-brahmagupta-1kiyao
Jun 9, 2026
Merged

Nested relative loads + injectable io/fs.FS for the Go port (parity with TS)#12
rjrodger merged 3 commits into
mainfrom
claude/funny-brahmagupta-1kiyao

Conversation

@rjrodger

@rjrodger rjrodger commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Brings the Go port to parity with the canonical TypeScript @jsonic/multisource on two fronts: nested relative includes, and an injectable filesystem for the file/pkg resolvers. Two focused commits.

1. Resolve nested relative loads against each source's own directory

Bug: a relative reference inside a loaded source resolved against the top-level opts.Path, not against the loaded source's own directory. So a → b → c across directories failed (the deepest reference was looked up in the wrong place).

Fix (the full, immutable version — not the opts.Path save/restore hack): each loaded source's full path is threaded through ctx.Meta["multisource"]["path"] (with the enclosing chain in "parents"), exactly mirroring TS's per-file ctx.meta.multisource.path. JsonicProcessor passes this meta into the nested parse via ParseMeta, and resolveSource derives the base directory from it. The parent parse context is copied, not mutated, so arbitrary nesting depth works and sibling loads stay independent.

  • Processor now receives *jsonic.Context (matching the TS processor signature).
  • sourceDir derives the base lexically: a path with separators → its directory; a flat in-memory key (e.g. a.jsc) → "", so bare nested refs still resolve (this is what the filepath.Dir-based quick patch would have broken for the mem resolver — now covered by a regression test).

2. Injectable io/fs.FS for the file and pkg resolvers

Closes the "No virtual filesystem in Go" gap. The resolvers can now read from an injected filesystem instead of the OS:

  • MultiSourceOptions.FS fs.FS — instance-level.
  • ctx.Meta["fs"] — per-parse override, the direct analog of TS's j('...', { fs }); propagates to nested loads via the copied meta.
  • OS filesystem remains the default (all existing behavior unchanged).

Reads route through a small internal vfs view (osVFS for the OS, ioVFS for an io/fs.FS) so the resolution logic stays single-sourced.

Updated known Go ↔ TS difference

The virtual-filesystem note in CLAUDE.md now reads, in essence:

Both ports let the file/pkg resolvers read from an injected filesystem instead of the OS (the default in both). TS uses ctx.meta.fs — a node:fs subset (e.g. memfs) keyed by absolute paths. Go uses MultiSourceOptions.FS or ctx.Meta["fs"] — an io/fs.FS (e.g. testing/fstest.MapFS) keyed by relative, slash-separated paths (fs.ValidPath). Because an io/fs.FS is rooted and relative, a Go reference under an injected FS resolves relative to the FS root, not as an absolute path.

Tests

  • TS (34/34): added explicit nested-relative-dirs and nested-relative-sibling-dirs (memfs), mirroring the Go tests and confirming TS already had this behavior.
  • Go (38/38): nested/file tests converted to hermetic testing/fstest.MapFS; retained one OS-backed nested test (TestNestedRelativeLoadOSFiles) for absolute-path coverage; added fs-injection tests (opts.FS, ctx.Meta["fs"], pkg sub-path/index/main, walk-up); plus the mem flat-key regression test.
  • gofmt / go vet clean. Docs updated in both doc/ files.

Notes

  • The Resolver signature gained *jsonic.Context (matches TS, enables per-parse fs). This is the only public-API change beyond the new FS field.
  • The TS pkg resolver still has a TODO for require-over-virtual-fs, so Go's pkg-over-fs support is slightly ahead of TS there — harmless and tested, but easy to gate if you'd prefer to wait for TS.
  • No version bump or release tag (per repo policy).

https://claude.ai/code/session_01Y8AZKH4bFM9WcDh5eXNp5U


Generated by Claude Code

claude added 2 commits June 9, 2026 10:59
… parity with TS)

A relative reference inside a loaded source now resolves against that source's own directory rather than the top-level opts.Path, matching the canonical TypeScript @jsonic/multisource. Each loaded source's full path is threaded through ctx.Meta["multisource"]["path"] (with the enclosing chain in "parents"); JsonicProcessor passes this meta into the nested parse via ParseMeta, and resolveSource derives the base directory from it. The parent parse context is copied, not mutated, so arbitrary nesting depth works and sibling loads stay independent.

- go/plugin.go: thread ctx through resolveSource; derive base from the enclosing source's directory; add metaSourcePath/sourceDir/childMeta helpers.
- go/multisource.go: Processor now receives *jsonic.Context; JsonicProcessor uses ParseMeta to thread meta into nested parses.
- go/nested_test.go: cross-directory (a->b->c), per-directory sibling, flat in-memory, and meta-threading tests.
- ts: explicit nested-relative-dirs and nested-relative-sibling-dirs tests.
- docs: drop the closed base-path gap from CLAUDE.md; document nested relative resolution in both doc/ files.

https://claude.ai/code/session_01Y8AZKH4bFM9WcDh5eXNp5U
…h TS ctx.meta.fs)

The file and pkg resolvers can now read from an injected filesystem instead of the OS, mirroring the TypeScript ctx.meta.fs injection point. The filesystem is supplied via MultiSourceOptions.FS (instance-level) or ctx.Meta["fs"] (per-parse, like TS's j('...', { fs })); the OS remains the default. Reads route through a small vfs view (osVFS for the OS, ioVFS for an io/fs.FS), so the resolver logic stays uniform. Note: an io/fs.FS uses relative, slash-separated paths (fs.ValidPath), so references under an injected FS resolve relative to the FS root rather than as absolute paths.

The Resolver signature now also receives *jsonic.Context, matching the TypeScript resolver and letting resolvers read the per-parse fs.

- go/resolver.go: vfs abstraction (osVFS/ioVFS), resolveVFS precedence, fsClean; file + pkg resolvers read via the vfs; signatures gain ctx.
- go/multisource.go: add MultiSourceOptions.FS; Resolver/MakeMemResolver gain ctx.
- go/plugin.go: pass ctx to the resolver.
- go/fs_test.go: fs-injection tests (opts.FS, ctx.Meta fs, pkg sub-path/index/main, walk-up).
- go/nested_test.go: nested/file tests converted to testing/fstest.MapFS (hermetic), one OS-backed nested test retained.
- docs: update the virtual-filesystem note in CLAUDE.md; document FS + the in-memory how-to in doc/multisource-go.md.

https://claude.ai/code/session_01Y8AZKH4bFM9WcDh5eXNp5U

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb2e9a6345

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/resolver.go
func resolveInPkgDir(nodeModules, ref string, exts []string, search *[]string) (full, src string, found bool) {
target := filepath.Join(nodeModules, filepath.FromSlash(ref))
func resolveInPkgDir(v vfs, nodeModules, ref string, exts []string, search *[]string) (full, src string, found bool) {
target := v.join(nodeModules, ref)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the resolved full path for package-relative loads

When MakePkgResolver processes a file from a package that contains a nested relative reference such as ./child.jsonic, the new nested-meta path is reflected in spec.Full (for example node_modules/pkg/child.jsonic), but this code rebuilds the lookup from spec.Path only and searches under each node_modules root as node_modules/./child.jsonic. That means package-internal relative references still fail even though the commit documents nested relative loads as resolving against the containing source directory; the resolver needs to use the already-resolved full path for relative refs instead of treating them as package names.

Useful? React with 👍 / 👎.

…+ Go)

A relative reference (./x, ../x) found inside a source loaded from a package
was treated as a node_modules package name: the pkg resolver searched
node_modules/./x instead of resolving against the loaded source's own
directory. Now both resolvers detect an explicit relative reference and
resolve it against the containing source's directory via the base-joined full
path, exactly as the file resolver does (works over an injected fs too).

- TS: makePkgResolver special-cases relative refs (resolvePathSpec base +
  buildPotentials over ps.full); add pkg-relative-ref test + rel/ fixtures.
- Go: MakePkgResolver mirrors this through the vfs view; add
  TestPkgResolverRelativeInPkg (hermetic fstest.MapFS).
- Document the behaviour in both doc/ files.

Bare-package and absolute references are unchanged.

https://claude.ai/code/session_01Y8AZKH4bFM9WcDh5eXNp5U
@rjrodger rjrodger merged commit 66b5a21 into main Jun 9, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants