feat: add import support to wasm playground editor#16440
Conversation
Enable `imports:` resolution in the browser-based wasm compiler by introducing a virtual filesystem. The editor now supports multiple file tabs — the main workflow file plus any shared imports — and passes them to the Go wasm runtime as a files map. Changes: - Add virtual_fs.go / virtual_fs_wasm.go for in-memory file storage - Replace os.ReadFile with readFileFunc in import/include/yaml parsers - Update remote_fetch_wasm.go to check virtual FS instead of os.Stat - Accept optional files object in compileWorkflow(md, files) - Thread files through compiler-worker.js and compiler-loader.js - Add file tab UI to the playground editor (add/remove/switch tabs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds import/include support to the WASM playground compiler by introducing a virtual filesystem for in-browser builds and wiring the editor/worker protocol to pass additional files for import resolution.
Changes:
- Introduces a parser-level overridable
readFileFunc, with a WASM virtual filesystem implementation (SetVirtualFiles/ClearVirtualFiles). - Updates include/import/YAML-import code paths to use
readFileFuncinstead ofos.ReadFile. - Extends the WASM compiler bridge and playground editor to pass/maintain a
filesmap (via file tabs) soimports:can resolve in the browser.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/parser/yaml_import.go | Switch YAML import reads to readFileFunc. |
| pkg/parser/virtual_fs.go | Adds readFileFunc indirection (defaults to os.ReadFile). |
| pkg/parser/virtual_fs_wasm.go | Implements in-memory virtual FS + overrides readFileFunc for WASM. |
| pkg/parser/remote_fetch_wasm.go | Uses virtual FS existence checks for include resolution in WASM. |
| pkg/parser/include_processor.go | Reads included files via readFileFunc. |
| pkg/parser/include_expander.go | Reads included files via readFileFunc. |
| pkg/parser/import_processor.go | Reads imported files via readFileFunc (including topo-sort). |
| docs/public/wasm/compiler-worker.js | Extends worker protocol to forward optional files. |
| docs/public/wasm/compiler-loader.js | Extends compile(markdown, files?) API and message payload. |
| docs/public/editor/index.html | Adds tabbed file UI and passes non-main files as imports to the compiler. |
| cmd/gh-aw-wasm/main.go | Accepts optional filesObject, sets virtual FS, and compiles with import support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // jsObjectToFileMap converts a JS object {path: content, ...} to map[string][]byte. | ||
| func jsObjectToFileMap(obj js.Value) map[string][]byte { | ||
| files := make(map[string][]byte) | ||
|
|
||
| // Use Object.keys() to iterate over the JS object | ||
| keys := js.Global().Get("Object").Call("keys", obj) | ||
| length := keys.Length() | ||
| for i := 0; i < length; i++ { | ||
| key := keys.Index(i).String() | ||
| value := obj.Get(key).String() | ||
| files[key] = []byte(value) | ||
| } | ||
|
|
||
| return files |
There was a problem hiding this comment.
jsObjectToFileMap assumes the optional second argument is a plain object whose values are strings. If a caller passes a non-object (e.g. a string/array) or non-string values, .String() will coerce silently (e.g. "[object Object]") and lead to confusing compile errors. Consider validating args[1] is an object and that each value is a string (or can be converted to one) and rejecting the Promise with a clear message when the shape is invalid.
| // Set up virtual filesystem for import resolution | ||
| if files != nil { | ||
| parser.SetVirtualFiles(files) | ||
| defer parser.ClearVirtualFiles() | ||
| } |
There was a problem hiding this comment.
SetVirtualFiles/ClearVirtualFiles mutate package-level globals, but compileWorkflow launches each request in a goroutine. If multiple compileWorkflow() calls are in-flight concurrently (possible for createWorkerCompiler().compile() consumers), the virtual FS can be overwritten/cleared mid-compilation, producing incorrect results. Consider serializing compilation in the WASM entrypoint (e.g., a mutex) or making the virtual FS state per-compilation instead of global.
docs/public/editor/index.html
Outdated
| const tab = document.createElement('div'); | ||
| tab.className = 'tab' + (file.name === activeTab ? ' active' : ''); | ||
| tab.dataset.name = file.name; | ||
|
|
||
| const label = document.createElement('span'); | ||
| label.textContent = file.name; | ||
| tab.appendChild(label); | ||
|
|
||
| // Close button (not for the main workflow file) | ||
| if (file.name !== MAIN_FILE) { | ||
| const close = document.createElement('button'); | ||
| close.className = 'tab-close'; | ||
| close.title = 'Remove file'; | ||
| close.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.749.749 0 011.275.326.749.749 0 01-.215.734L9.06 8l3.22 3.22a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215L8 9.06l-3.22 3.22a.751.751 0 01-1.042-.018.751.751 0 01-.018-1.042L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>'; | ||
| close.addEventListener('click', (e) => { | ||
| e.stopPropagation(); | ||
| removeTab(file.name); | ||
| }); | ||
| tab.appendChild(close); | ||
| } | ||
|
|
||
| tab.addEventListener('click', () => switchTab(file.name)); | ||
| tabBar.insertBefore(tab, tabAdd); | ||
| } |
There was a problem hiding this comment.
Tabs are rendered as clickable <div> elements with click handlers, which are not keyboard-focusable by default and lack appropriate ARIA roles. This makes the new tab system hard to use with keyboard-only navigation and screen readers. Consider rendering tabs as <button> elements (or add role="tab", tabindex="0", and keyboard handlers for Enter/Space + focus management).
# Conflicts: # docs/public/editor/index.html
The schema versioning change (e995df1) requires lock files to have a gh-aw-metadata comment. Update the test lock content to include it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # pkg/workflow/action_sha_validation_test.go
# Conflicts: # docs/public/editor/index.html
Summary
virtual_fs.go/virtual_fs_wasm.go) so the wasm compiler can resolveimports:without real filesystem accessos.ReadFilecalls in import/include processors with an overridablereadFileFunc— native builds default toos.ReadFile, wasm builds use the virtual FScompileWorkflow(md, files?)to accept an optional files map for import resolutionshared/tools.md), switch between tabs, and the compiler resolves them automaticallyTest plan
GOOS=js GOARCH=wasm go build ./cmd/gh-aw-wasmsucceedsgo build ./cmd/gh-awstill works (native build)go test -short ./pkg/parser/passesgo test -short ./pkg/workflow/passesimports:, verify compilation works🤖 Generated with Claude Code