Nested relative loads + injectable io/fs.FS for the Go port (parity with TS)#12
Conversation
… 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
There was a problem hiding this comment.
💡 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".
| 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) |
There was a problem hiding this comment.
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
Brings the Go port to parity with the canonical TypeScript
@jsonic/multisourceon 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. Soa → b → cacross directories failed (the deepest reference was looked up in the wrong place).Fix (the full, immutable version — not the
opts.Pathsave/restore hack): each loaded source's full path is threaded throughctx.Meta["multisource"]["path"](with the enclosing chain in"parents"), exactly mirroring TS's per-filectx.meta.multisource.path.JsonicProcessorpasses this meta into the nested parse viaParseMeta, andresolveSourcederives the base directory from it. The parent parse context is copied, not mutated, so arbitrary nesting depth works and sibling loads stay independent.Processornow receives*jsonic.Context(matching the TS processor signature).sourceDirderives 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 thefilepath.Dir-based quick patch would have broken for the mem resolver — now covered by a regression test).2. Injectable
io/fs.FSfor the file and pkg resolversCloses 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'sj('...', { fs }); propagates to nested loads via the copied meta.Reads route through a small internal
vfsview (osVFSfor the OS,ioVFSfor anio/fs.FS) so the resolution logic stays single-sourced.Updated known Go ↔ TS difference
The virtual-filesystem note in
CLAUDE.mdnow reads, in essence:Tests
nested-relative-dirsandnested-relative-sibling-dirs(memfs), mirroring the Go tests and confirming TS already had this behavior.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 vetclean. Docs updated in bothdoc/files.Notes
Resolversignature gained*jsonic.Context(matches TS, enables per-parse fs). This is the only public-API change beyond the newFSfield.TODOforrequire-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.https://claude.ai/code/session_01Y8AZKH4bFM9WcDh5eXNp5U
Generated by Claude Code