Skip to content

[v3] Update and fix runtime JS API#3295

Merged
leaanthony merged 11 commits intowailsapp:v3-alphafrom
fbbdev:update_js_api
Mar 20, 2024
Merged

[v3] Update and fix runtime JS API#3295
leaanthony merged 11 commits intowailsapp:v3-alphafrom
fbbdev:update_js_api

Conversation

@fbbdev
Copy link
Copy Markdown
Collaborator

@fbbdev fbbdev commented Mar 5, 2024

Description

The main purpose of this PR is to bring the JS Window API up to date wrt the current WebviewWindow interface, as briefly discussed some time ago in the wails3 discord channel. In addition, it brings some bug fixes, code cleanups and quality-of-life improvements to the JS runtime. I decided to pack everything into a single PR because many changes overlap and I wanted to avoid a cascade of merge conflicts.

As a result this PR is somewhat large. In order to facilitate the review process, I divided it into many logically independent commits. I am gonna explain the changes in detail below through separate comments on a per-commit basis.

I am opening the PR against the v3-alpha branch. Should this PR be merged, I am willing to take care of any potential merge conflicts with the v3-alpha-linux branch.

Here follows a summary of the changes, some of which are breaking:

  1. The entry point of the bundled runtime (v3/internal/runtime/desktop/compiled/main.js) has been refactored to avoid duplicating the export list.
  2. Breaking change. To improve encapsulation, the @wailsio/runtime package does not export the API to the window.wails object anymore, and does not start the WML system. The bundled runtime.js still does both.
  3. The JS type of the Screen struct has been updated to match its Go counterpart.
  4. The WebviewWindow.IsFocused method has been exposed on the Window interface.
  5. Breaking change. The JS Window API has been updated to match the Window interface. This change is breaking because some methods have changed name and/or return type (although the most commonly used ones remain unchanged).
  6. Breaking change. The Window API module (@wailsio/runtime/src/window) now exports the containing window object as a default export. It is not possible anymore to use ESM named import or namespace import syntax.
  7. The method used to clean up previous listeners when reloading WML pages was wrong and has been fixed.
  8. The WML system now supports multiple space-separated trigger events.
  9. Breaking change. The bundled runtime is now an ES Module! It exports the same API as the @wailsio/runtime package. The benefits being that it can be easily imported into other modules and the ESM import system ensures it is loaded at most once. The change is breaking because the type="module" attribute must now be added to all script tags loading it.

As regards OS compatibility,

  • on Windows, it shouldn't be a problem as the WebView2 library ships with very recent versions of the Chromium runtime;
  • on macOS, I targeted Safari 10.1 shipping with macOS 10.12 Sierra;
  • on Linux, I am not sure about the webkit version shipping there, but existing runtime code already required at least Safari 10 (because of arrow functions and the fetch API), so nothing should have changed.

Because of change no. 5, this PR supersedes #3280

I am gonna push the updated changelog ASAP.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

How Has This Been Tested?

Before testing the PR code, I recommend running go clean ./... in the v3 folder to ensure the build system picks up all changes.

Most examples have been brought up to date and thoroughly tested. The only exceptions are the build example (not relevant to the changes), the dev example (it is incomplete and probably not relevant), the drag-n-drop example (it has been updated but not tested as it doesn't work on macOS anyways).

The menu and plugin examples are not working with my test configuration, but the problems do not appear to originate with the new changes.

  • Windows
  • macOS
  • Linux

Test Configuration

Go Version     | go1.22.0                                                                      
Revision       | 12d12a33dec1d31f487b70f87bc31e80252f327c                                      
Modified       | true                                                                          
-buildmode     | exe                                                                           
-compiler      | gc                                                                            
CGO_CFLAGS     |                                                                               
CGO_CPPFLAGS   |                                                                               
CGO_CXXFLAGS   |                                                                               
CGO_ENABLED    | 1                                                                             
CGO_LDFLAGS    |                                                                               
DefaultGODEBUG | httplaxcontentlength=1,httpmuxgo121=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
GOAMD64        | v1                                                                            
GOARCH         | amd64                                                                         
GOOS           | darwin                                                                        
vcs            | git                                                                           
vcs.modified   | true                                                                          
vcs.revision   | 12d12a33dec1d31f487b70f87bc31e80252f327c                                      
vcs.time       | 2024-02-21T12:40:32Z                                                          

# System
Name            | MacOS                                   
Version         | 12.6.6                                  
ID              | 21G646                                  
Branding        | Monterey                                
Platform        | darwin                                  
Architecture    | amd64                                   
Apple Silicon   | unknown                                 
CPU             | Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Xcode cli tools | 2395                                    
CPU             | Unknown                                 
GPU             | Unknown                                 
Memory          | Unknown                                 

# Diagnosis
 SUCCESS  Your system is ready for Wails development!

Checklist:

  • I have updated the changelog with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (N/A)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 5, 2024

Important

Auto Review Skipped

Auto reviews are disabled on base/target branches other than the default branch. Please add the base/target branch pattern to the list of additional branches to be reviewed in the settings.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository.

To trigger a single review, invoke the @coderabbitai review command.

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share

Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit 0f3bed1 – Cleanup bundled runtime entry point

This commit wraps changes no. 1, 2 from the summary.

All code from the entry point of the @wailsio/runtime package (see index.js) was duplicated in the entry point of the runtime bundle (see main.js), and the two had fallen slightly out of sync.

Keeping them aligned is burdensome for contributors who may not be aware of the existence of duplicated code, so instead of bringing everything up to date I used the inject option of the esbuild packager to eliminate the need for duplication altogether.

I also reworked things so that the '@wailsio/runtime' package does not populate the window.wails object and does not start the WML system. This improves encapsulation and reduces unexpected side effects for people importing the package into frontend frameworks. The bundled runtime script still does both things automatically.

I moved the debugLog and whenReady utility functions to a new file utils.js and created a new WML.Enable API method that people can call to start WML when using the npm package.

Going forward, you might consider moving all private code with side effects to an internal module to be either imported through an internal URL or automatically injected into internally served pages: it should include the code populating the global _wails object and all code related to clipboard, context menus, drag-n-drop, titlebar and RPC. Such a change would make all those functionalities available by default, and it would remove the hazard of conflicts between multiple instances of the runtime. It would also make exported bindings independent from the npm package.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit 96f7c9a – Fix JS representation of Screen struct

This commit wraps change no. 3 from the summary.

The Screen.Rotation field was changed to a float32 in go code but was still typed as an enum in JS code.

I verified the code that marshals the go struct to JSON, then fixed the JS type.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit 2829b33 – Expose IsFocused method in Window interface

This commit wraps change no. 4 from the summary.

Self-explanatory 🤷🏻‍♂️

@leaanthony
Copy link
Copy Markdown
Member

leaanthony commented Mar 5, 2024

Really appreciate you taking this on 🙏 On the whole it looks fine. The reason we removed the runtime from being embedded by default was to give developers the flexibility to choose whether they wanted the entire runtime loaded or pick and choose what they wanted from the npm module. I did consider embedding the compiled runtime if it wasn't production but then that'll create inconsistent behaviour between prod and non-prod. In this PR, I believe both debug and non debug runtimes will be included in every build? My approach was going to create an asset handler that embeds the compiled runtimes that could be used for examples or anyone who wants the full bundle. If the compiled runtime is bundled each time then developers lose the benefit of the npm module. Thoughts?

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit 125b893 – Update JS window API

This commit wraps changes no. 5 and 6 from the summary.

I copied (almost) all methods from the Window interface, assigned them serial IDs, rewrote the über-switch in messageprocessor_window.go, copied the IDs to the JS file and described all methods with JSDoc.

The only omitted methods are those related to private JS APIs like context menus, event dispatch and so on.

There are a few surprises here though.

1. Some methods changed name

Specifically, Screen becomes GetScreen; GetZoomLevel/SetZoomLevel become GetZoom/SetZoom (I shouldn't have missed anything). This was of course not my choice as I just followed the current Window API.

2. Some methods changed return type

Specifically, the getter methods GetZoomLevel, Width and Height (I might be missing something but I hope not) used to return JS objects with one field (zoom, width and height respectively). Since the encoding/json package can directly marshal numeric values, I changed the return type of all those methods to number, so that they better match the Go API.

It's not clear whether the previous approach was intentional, as their return types were not specified in JSDoc; see for reference:

3. The methods now live on an unexported class Window

This replaces the previous anonymous object. I took care to bind all instance method so that they can be used easily in event handlers just like before.

4. The module exports the thisWindow instance as a default export

See window.js:643.

This was done to avoid duplication, as it is way too easy to have window objects fall out of sync with the module-exported API. Typing also improves. All methods of the default instance are available at wails.Window.[Method] just like before.

This is of course a breaking change. It is also the change I am least happy with, but I can see no way around it, because

  1. the massive amount of duplication here is a real problem, but
  2. ESM export syntax is too inflexible, blocking most different approaches.

Alternatively, we could just export the Window class, the Get method and the thisWindow instance (maybe naming it Self?). This would break existing code much more (e.g. wails.Window.Maximise() becomes wails.Window.Self.Maximise()), but it would preserve the uniformity of the import syntax. Any thoughts?

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

My approach was going to create an asset handler that embeds the compiled runtimes that could be used for examples or anyone who wants the full bundle. If the compiled runtime is bundled each time then developers lose the benefit of the npm module. Thoughts?

I think I get your point (but please tell me if not). Having a separate asset handler should mean that the optimizer can drop the embedded runtime when it's not used. I'd be happy to work on this, but I can't see clearly what kind of API you have in mind.

To be specific, how should it be plugged into the server pipeline? As middleware? As a different kind of asset server with its own constructor function? As a wrapper for the default asset server? Surely it cannot be passed as an option to the default asset server constructor, as that would most probably prevent dead code elimination from kicking in. Should it reside in its own separate package?

If we can work out the details here (or on discord, as you prefer) I will get to work on it.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

BTW, what do you think of the npm package not populating the window.wails object by default?

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

A further comment about the embedded runtime. In my discussion of the related commit I was proposing as a future change to embed and maybe even inject automatically the private runtime API (which is quite minimal), and leave the public API to the npm package, plus maybe an optional asset handler for examples and vanilla apps.

I think this would be the best approach overall, but it requires some more effort and I don't think it fits in this PR.

The asset handler you propose, on the other hand, would be a nice temporary solution. I was thinking that this could actually be best implemented as a middleware residing in a separate package; thus one might easily inject it into their own middleware chain, and the whole package would never be included in the binary if unused.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit ab0f409 – Fix cleanup of WML listeners

This commit wraps changes no. 7 and 8 from the summary.

Change 7 (support multiple space-separated triggers) is very simple and is achieved by scanning the trigger attribute with a regexp (see wml.js:210–212). The ugly while loop is necessary because the String.matchAll method was only added in the latest release of Safari and WebKit.

Change 6 is needed to fix a bug where calling WML.Reload would not clean up existing listeners. You can reproduce this as follows: run the WML example (before the PR) in debug mode, open the devtools, run wails.WML.Reload() in the console, then click the button labeled 'Emit an event' – the event will be emitted twice.

There are two problems with the existing code:

  1. the major one is that removeEventListener is invoked with a closure that has just been declared (see e.g. here). However, this new closure will always be a newly created object, distinct from the one used in the previous call to addEventListener;
  2. the minor one is that removeEventListener is called with the current trigger attribute, which might have changed, and is never called on elements whose wml-* attributes changed or disappeared.

The solution is twofold as well:

  1. use a global, fixed event handler for everything, and decide on the fly which actions to perform (this incidentally reduces the need for calls to WML.Reload);
  2. store the active trigger events for each element.

As regards the latter part of the solution, HTMX does it in the dumbest possible way to maximise browser compatibility, i.e. it defines a custom named property (not an attribute!) on each targeted node. In wails' case, however, the constraints are weaker as it supports way less browsers and it always runs global updates (whereas HTMX runs partial updates too).

For very recent browsers, I use the AbortController/AbortSignal approach which allows removing ALL associated event listeners at once (see wml.js:97–131). Note that this approach does not support partial updates.

For older browsers, I use a WeakMap to associate a trigger list to each element (see wml.js:138–195). This is better than a Map or array as it doesn't stop garbage collection of mapped elements. When performing a reload, the whole document tree is visited and listeners are removed if present (this is similar to what HTMX does, unfortunately WeakMaps cannot be iterated over). As an optimisation, the loop terminates as soon as the count of active elements reaches zero. This kicks in especially when running Reload for the first time.

The two approaches are wrapped in a uniform API and selected with a feature test (see wml.js:197 and utils.js:31–44).

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

Commit c4bf8a2 – Bundle runtime as ES module

This commit wraps changes no. 9 from the summary.

I think it's nice to have, brings minimal disruption to existing code (just add type="module" attributes) and prevents initialisation code from running more than once.

@fbbdev
Copy link
Copy Markdown
Collaborator Author

fbbdev commented Mar 5, 2024

This is pretty much it, all other commits are just maintenance operations. It's a lot of stuff, don't worry and take your time to answer, and as always thank you for your work!

Summarising, apart from any comments you might have, I'd like to get your feedback about the following three points:

  • Window module exports (see here, last paragraphs)
  • Runtime asset handler API (see here and here, last paragraph of the edited comment)
  • Whether the npm package should in your opinion export the API globally and/or automatically start the WML subsystem

@stffabi
Copy link
Copy Markdown
Collaborator

stffabi commented Mar 5, 2024

  • Runtime asset handler API (see here and here, last paragraph of the edited comment)

Just as a reference if you probably start working on that one, all code is already there for injection in v2:

} else if d.isRuntimeInjectionMatch(path) {
recorder := &bodyRecorder{
ResponseWriter: rw,
doRecord: func(code int, h http.Header) bool {
if code == http.StatusNotFound {
return true
}
if code != http.StatusOK {
return false
}
return strings.Contains(h.Get(HeaderContentType), "text/html")
},
}
handler.ServeHTTP(recorder, req)
body := recorder.Body()
if body == nil {
// The body has been streamed and not recorded, we are finished
return
}
code := recorder.Code()
switch code {
case http.StatusOK:
content, err := d.processIndexHTML(body.Bytes())
if err != nil {
d.serveError(rw, err, "Unable to processIndexHTML")
return
}
d.writeBlob(rw, indexHTML, content)
case http.StatusNotFound:
d.writeBlob(rw, indexHTML, defaultHTML)
default:
rw.WriteHeader(code)
}

So you don't have to start all over :)

// Add the method to the map
result.set(method, targetWindow[method]);
}
export function Boot() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you think about calling this Enable?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sounds better to me too, will rename it.

@leaanthony
Copy link
Copy Markdown
Member

Thanks for taking the time to go through all this! On the whole I think this is a great step forward. I'll be moving the compiled runtime into its own FS handler so it can be better used rather than having copies of runtime.js everywhere - but that's for a different PR!

@leaanthony
Copy link
Copy Markdown
Member

@fbbdev - apologies! As part of fixing the merge conflicts I've appeared to created a bad merge. I did push up a reset but that does not appear to have worked as intended. Could you please force push your branch again? Thanks 😬

Copy link
Copy Markdown
Member

@leaanthony leaanthony left a comment

Choose a reason for hiding this comment

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

This is incredible! Thank you 🙏

@leaanthony leaanthony merged commit 2b9891d into wailsapp:v3-alpha Mar 20, 2024
Comment thread mkdocs-website/docs/en/changelog.md Outdated
### Changed

<<<<<<< HEAD
=======
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Whooops 😆

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All good 😂

@fbbdev fbbdev deleted the update_js_api branch March 20, 2024 18:08
atterpac pushed a commit to atterpac/wails that referenced this pull request Apr 7, 2024
* Cleanup bundled runtime entry point

* Fix JS representation of Screen struct

* Expose IsFocused method in Window interface

* Update JS window API

* Fix cleanup of WML listeners

* Bundle runtime as ES module

* Update runtime dependencies

* Update runtime types and events

* Update bundled runtime

* Update changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
leaanthony added a commit that referenced this pull request Oct 2, 2025
Fixes #4489

## Problem
The npm package @wailsio/runtime doesn't work for drag-and-drop because
Go backend calls window.wails.Window.HandlePlatformFileDrop(), but the
npm package doesn't populate window.wails (by design for encapsulation).

## Root Cause
PR #3295 (March 2024) intentionally removed window.wails assignment from
the npm package to improve encapsulation. However, this broke platform
handlers that Go backend relies on.

## Solution
Move HandlePlatformFileDrop from public API (window.wails) to internal
API (window._wails), following the existing pattern:
- window._wails.invoke
- window._wails.environment
- window._wails.flags

## Changes
- Register handlePlatformFileDrop in window._wails namespace
- Update Go backend to call window._wails.handlePlatformFileDrop()
- Use camelCase naming for consistency with other internal API methods
- Rebuild bundled runtime with changes

## Benefits
✅ npm package works without window.wails pollution
✅ Maintains encapsulation of public API
✅ Platform handlers clearly separated as internal
✅ Follows existing internal API conventions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
leaanthony added a commit that referenced this pull request Oct 2, 2025
* fix: Move HandlePlatformFileDrop to window._wails internal API

Fixes #4489

## Problem
The npm package @wailsio/runtime doesn't work for drag-and-drop because
Go backend calls window.wails.Window.HandlePlatformFileDrop(), but the
npm package doesn't populate window.wails (by design for encapsulation).

## Root Cause
PR #3295 (March 2024) intentionally removed window.wails assignment from
the npm package to improve encapsulation. However, this broke platform
handlers that Go backend relies on.

## Solution
Move HandlePlatformFileDrop from public API (window.wails) to internal
API (window._wails), following the existing pattern:
- window._wails.invoke
- window._wails.environment
- window._wails.flags

## Changes
- Register handlePlatformFileDrop in window._wails namespace
- Update Go backend to call window._wails.handlePlatformFileDrop()
- Use camelCase naming for consistency with other internal API methods
- Rebuild bundled runtime with changes

## Benefits
✅ npm package works without window.wails pollution
✅ Maintains encapsulation of public API
✅ Platform handlers clearly separated as internal
✅ Follows existing internal API conventions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Add changelog entry for drag-and-drop fix

Updates UNRELEASED_CHANGELOG.md with the fix for #4489.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

3 participants