feat(bloblang): Improve Bloblang playground with Go/WASM migration#570
feat(bloblang): Improve Bloblang playground with Go/WASM migration#570iamramtin wants to merge 14 commits intowarpstreamlabs:mainfrom
Conversation
- Replaced JavaScript integration with Go and WASM instead - Improved WASM API exposure and updated JS integration - Added full syntax support to formatter (operators, keywords, pipes) Signed-off-by: Ramtin Mesgari <[email protected]>
Signed-off-by: Ramtin Mesgari <[email protected]>
…lidation API - Moved documentation HTML generation from JS to Go - Pre-generate docHTML for functions/methods - Added validation function - Added JSON utilities (format/minify/validate) with WASM and HTTP endpoints - Updated JS to prioritise WASM with native fallbacks - Centralized WASM function registration Signed-off-by: Ramtin Mesgari <[email protected]>
- Tests core functions: execute, validate, syntax, format, autocomplete - Tests JSON utilities: formatJSON, minifyJSON, validateJSON Signed-off-by: Ramtin Mesgari <[email protected]>
- Replace hardcoded values with CSS variables - Remove duplicate CSS rules - Fix mobile responsive layout - Add Bento-themed scrollbars to all editors Signed-off-by: Ramtin Mesgari <[email protected]>
Also simplified WASM initialization and auto-register server/WASM handlers. Signed-off-by: Ramtin Mesgari <[email protected]>
- Remove redundant JSON utilities from Go/WASM (use native JS instead) - Extract BloblangAPI abstraction layer for WASM/server routing - Remove unused code and dead methods - Improve error handling and WASM initialization flow - Improve UX Signed-off-by: Ramtin Mesgari <[email protected]>
0b9da37 to
0d0b520
Compare
- Fix type mismatch in getCompletions() for functionSpecWithHTML/methodSpecWithHTML - Improve autocompletion suggestions for Bloblang keywords - Add tests to verify completion types and structure Signed-off-by: Ramtin Mesgari <[email protected]>
0d0b520 to
fe4aa15
Compare
a6dd66b to
4e96b2c
Compare
Signed-off-by: Ramtin Mesgari <[email protected]>
4e96b2c to
b98ab91
Compare
|
Hey @gregfurman @jem-davies just a friendly ping to see if we can get this merged in? |
|
@iamramtin hey! Thanks for the ping and sorry for delay, will give this a priority review 👍 |
gregfurman
left a comment
There was a problem hiding this comment.
I'll do another pass but have left some comments on structure and the pattern(s) used. Lmk thoughts!
internal/cli/blobl/core.go
Outdated
| return AutocompletionResponse{ | ||
| Completions: []CompletionItem{}, | ||
| Success: false, | ||
| Error: fmt.Sprintf("Failed to get syntax data: %v", err), |
There was a problem hiding this comment.
| Error: fmt.Sprintf("Failed to get syntax data: %v", err), | |
| Error: fmt.Sprintf("Failed to get syntax data: %s", err), |
There was a problem hiding this comment.
Why should we use %s here for error types?
There was a problem hiding this comment.
Ahh sorry. Meant to say err.Error(), since that would extract the string form of the error:
| Error: fmt.Sprintf("Failed to get syntax data: %v", err), | |
| Error: fmt.Sprintf("Failed to get syntax data: %s", err.Error()), |
There was a problem hiding this comment.
Are these not effectively the same thing when the error is not nil?
Makefile
Outdated
|
|
||
| playground-test: | ||
| @go test ./internal/cli/blobl -timeout 3m |
There was a problem hiding this comment.
Wont this just run as a part of the normal test suite? Why do we need to add an extra make command?
internal/cli/blobl/wasm.go
Outdated
| // | ||
| // Arguments: | ||
| // - args[0]: JSON string containing AutocompletionRequest with line, column, prefix, beforeCursor | ||
| // | ||
| // Returns a JS object with: | ||
| // - "completions": array of completion items with caption, value, meta, type, score, docHTML | ||
| // - "success": true if autocompletion succeeded, false otherwise | ||
| // - "error": error message if autocompletion failed, else nil |
There was a problem hiding this comment.
I don't think this level of verbosity is necessary
| // | |
| // Arguments: | |
| // - args[0]: JSON string containing AutocompletionRequest with line, column, prefix, beforeCursor | |
| // | |
| // Returns a JS object with: | |
| // - "completions": array of completion items with caption, value, meta, type, score, docHTML | |
| // - "success": true if autocompletion succeeded, false otherwise | |
| // - "error": error message if autocompletion failed, else nil |
internal/cli/blobl/wasm.go
Outdated
| // Excludes functions that depend on system resources (env vars, filesystem). | ||
| var getWasmEnvironment = sync.OnceValue(func() *bloblang.Environment { | ||
| return bloblang.GlobalEnvironment().WithoutFunctions("env", "file") | ||
| }) |
There was a problem hiding this comment.
Already mentioned, but why not we replace this with an init and run the InitializeWASM logic as well as all JS function registration when the package gets initialised i.e
func init() {
env := bloblang.GlobalEnvironment().WithoutFunctions("env", "file")
InitializeWASM()
// .. pass 'env' around
}Then we can import
internal/cli/blobl/types.go
Outdated
| type FormatMappingResponse struct { | ||
| Success bool `json:"success"` | ||
| Formatted string `json:"formatted"` | ||
| Error string `json:"error,omitempty"` |
There was a problem hiding this comment.
I think we can use the globally available Error function from the JS environment i.e
var errorResp FormatMappingResponse
err := js.Global().Get("Error").New(errorResp.Error)
panic(err) // or maybe ignore if errorThen we can treat the error case as a JS-level error and handle with a try-catch instead of this error payload approach.
Also, that way we don't need to pass around an error attribute and can focus on payload responses.
wdyt?
internal/cli/blobl/types.go
Outdated
|
|
||
| // FunctionSpecWrapper wraps query.FunctionSpec to implement Spec interface | ||
| type FunctionSpecWrapper struct { | ||
| query.FunctionSpec |
There was a problem hiding this comment.
I don't think the entire struct needs to be embedded here since you're just using some attributes. Consider
| query.FunctionSpec | |
| spec query.FunctionSpec |
and then
func (f FunctionSpecWrapper) GetDescription() string { return f.spec.Description }There was a problem hiding this comment.
Good call, I think this explicitness is much better 👍
internal/cli/blobl/types.go
Outdated
| } | ||
|
|
||
| // Spec represents a unified interface for function and method specs | ||
| type Spec interface { |
There was a problem hiding this comment.
Why not use a dedicated Spec struct here instead with the attributes we need? Feels like 2 more structs + an interface is a bit of overkill.
There was a problem hiding this comment.
How would you feel about me adding a BaseSpec to internal/bloblang/query/docs.go and having FunctionSpec and MethodSpec embed those shared attributes instead of duplicating all this logic in this file?
There was a problem hiding this comment.
See commit 407ffa62b and LMK what you think, this is non-breaking and fully compatible and should let me not have to redeclare these shared attributes
There was a problem hiding this comment.
And follow up 1da4e310a to see how it affects the current code
There was a problem hiding this comment.
Wdyt of placing all of these WASM playground related functions in their own package i.e cli/blobl/wasm or /playground?
…nd MethodSpec Signed-off-by: Ramtin Mesgari <[email protected]>
Signed-off-by: Ramtin Mesgari <[email protected]>
Signed-off-by: Ramtin Mesgari <[email protected]>
Signed-off-by: Ramtin Mesgari <[email protected]>
Signed-off-by: Ramtin Mesgari <[email protected]>
|
@gregfurman addressed comments, LMK WYT 🏁 |
| } | ||
| } | ||
|
|
||
| // Test validate (Bloblang validation) |
There was a problem hiding this comment.
| // Test validate (Bloblang validation) |
| { | ||
| name: "invalid syntax", | ||
| mapping: `root.bad =`, | ||
| valid: false, |
There was a problem hiding this comment.
valid is implied to be false if empty, so it can just be omitted
| name: "unknown function", | ||
| mapping: `root = fake_function()`, | ||
| valid: false, | ||
| }, |
There was a problem hiding this comment.
Perhaps add some more negative test cases i.e
| }, | |
| }, | |
| { | |
| name: "invalid formatting", | |
| mapping: `root=fake_function()`, | |
| valid: false, // should this be false? | |
| }, | |
| { | |
| name: "invalid root name", | |
| mapping: `ROOT = this`, | |
| valid: false, | |
| }, |
There was a problem hiding this comment.
ROOT = this would be valid because it would be assigning this to a variable called ROOT rather than Bloblang special root key, i.e.
{
"ROOT": {
"name": "bento",
"stars": 1500
}
}
instead of
{
"name": "bento",
"stars": 1500
}
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| valid, err := ValidateBloblangMapping(env, tt.mapping) |
There was a problem hiding this comment.
Since valid will always be false on error and true when no error is present, I can't really see a need for checking (and returning) both values.
| // ValidateBloblangMapping validates a Bloblang mapping without executing it. | ||
| // Note: Not currently used by the playground UI ('execute' already validates). | ||
| func ValidateBloblangMapping(env *bloblang.Environment, mapping string) (bool, error) { | ||
| if mapping == "" { | ||
| return false, errors.New("mapping cannot be empty") | ||
| } | ||
|
|
||
| _, err := env.NewMapping(mapping) | ||
| if err != nil { | ||
| return false, fmt.Errorf("could not parse mapping: %v", err) | ||
| } | ||
|
|
||
| return true, nil | ||
| } |
There was a problem hiding this comment.
Is this extra utility function necessary? Can we not just use the result of the underlying NewMapping?
_, err := env.NewMapping(mapping)
if err != nil {
// ...
}| func getParamTypeString(param query.ParamDefinition) string { | ||
| switch param.ValueType { | ||
| case value.TString: | ||
| return "string" | ||
| case value.TInt: | ||
| return "integer" | ||
| case value.TFloat: | ||
| return "number" | ||
| case value.TBool: | ||
| return "boolean" | ||
| case value.TArray: | ||
| return "array" | ||
| case value.TObject: | ||
| return "object" | ||
| case value.TTimestamp: | ||
| return "timestamp" | ||
| default: | ||
| return "any" | ||
| } | ||
| } |
There was a problem hiding this comment.
Is this necessary?
The underlying value.Type is just an alias to a string so running string(param.ValueType) would convert the type into it's string equivalent
| func getParamTypeString(param query.ParamDefinition) string { | |
| switch param.ValueType { | |
| case value.TString: | |
| return "string" | |
| case value.TInt: | |
| return "integer" | |
| case value.TFloat: | |
| return "number" | |
| case value.TBool: | |
| return "boolean" | |
| case value.TArray: | |
| return "array" | |
| case value.TObject: | |
| return "object" | |
| case value.TTimestamp: | |
| return "timestamp" | |
| default: | |
| return "any" | |
| } | |
| } | |
| func getParamTypeString(param query.ParamDefinition) string { | |
| return string(param.ValueType) | |
| } |
| } | ||
|
|
||
| // formatBloblang formats Bloblang code with indentation and consistent spacing | ||
| func formatBloblang(originalMapping string) (string, error) { |
There was a problem hiding this comment.
ok wow I can't pretend to say I understand much of what is happening here re: all of this parsing. How did we do it previously in the playground?
| if !c.Bool("no-open") { | ||
| u, err := url.Parse("http://localhost:" + port) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to parse URL: %w", err) | ||
| } | ||
| openBrowserAt(u.String()) |
There was a problem hiding this comment.
Why the refactor here?
There was a problem hiding this comment.
Would it be possible to revert this refactor? I'm finding it a bit tough to see what was added versus what was refactored
There was a problem hiding this comment.
I understand the difficulty in reviewing this as a mixed change, but I'd prefer not to revert as most of the diff is structural rather than behavioural
The refactor separates server lifecycle, routing, and handlers into explicit types and methods, removes large inline closures, and centralises the state into a server type. This was necessary to make this file more idiomatic when adding the new endpoints cleanly without further growing what was previously essentially just a single function
Functionally, behaviuor is the same, main change is turning implicit structure into explicit structure so future additions don't compound the layout
I’m happy to walk through the structure or split follow-ups if that helps
| // ------------------------------------------------------------------------------ | ||
|
|
||
| // BaseSpec contains fields shared by function and method specifications. | ||
| type BaseSpec struct { |
There was a problem hiding this comment.
Thoughts on just having a custom struct inside the blobl package that maps FunctionSpec or MethodSpec to a common single type? That way, the internal/bloblang functionality does not need to be touched at all and we can keep this unified approach logic where its needed
Summary
Improves the Bloblang playground by migrating core logic from JavaScript to Go/WASM.
The playground previously duplicated some of Bloblang logic in JavaScript, creating maintenance overhead and potential inconsistencies. By moving to Go/WASM, we:
Changes
Go/WASM Migration:
JavaScript Simplification:
Other Improvements:
Testing
1. Run Unit Tests
2. Test Server Mode (Go backend via HTTP)
Visit playground and verify:
3. Test WASM Mode (Client-side execution)
Visit the playground in the docs and verify:
Breaking Changes
None