-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WEP] Service tooling proposal #3496
Conversation
Deploying wails with Cloudflare Pages
|
First of all thank you for your work on this and sorry for the very late feedback. It is true, as you wrote in the proposal, that people can manage services by hand without too much effort. Nonetheless, I think this contribution fits very well wails' mentality of providing ergonomic, moderately opinionated development tools on top of the go distribution, and it is definitely going to play a part, however small, in making the developer experience smoother and simpler. I would like to object to the currently proposed code layout, and suggest an alternative. The main problem IMO is that it does not fit very well the layout of generated JS code, and it is going to introduce friction for the developers on the frontend side. A secondary problem is that I don't think having a per-service go.mod file makes sense for application-level services, which are most probably going to import internal subpackages of the application module, and are probably never going to be reused. Regarding the main problem, the binding generator currently outputs:
Service files can be imported with the usual syntax import * as ServiceName from "<package>/<servicename>.js" whereas index files are designed to facilitate imports like this import {ServiceName1, ServiceName2} from "<package>"; Having one folder per service is gonna make indexes redundant and practically useless, as well as complicate JS imports, which need to be typed by hand. The former pattern is going to become: import * as ServiceName from "<package>/<servicename>/<servicename>.js" The latter will look like this: import {ServiceName1} "<package>/<servicename1>";
import {ServiceName2} "<package>/<servicename2>"; Not nice... I understand that having per-service folders was meant to facilitate method handling, and I am gonna propose a couple alternative solutions below. Regarding the secondary problem, I explained my reasons above. I'd just like to add this: I remember reading around the internet that having nested go.mod files should always be avoided, except for versioning purposes. I always try to abide by this rule, but maybe it was just someone's personal opinion? Aren't there any drawbacks to this? On to the alternatives. I'd like to propose the following layout for applications:
For reusable, non application-specific services, I propose adding a new plugin template to the init command that would result in the following layout:
The service tool would use the former layout if the current package is main, the latter if it is non-main. This logic has the added benefit of making the service tool work seamlessly in the services folder, as well as in any other subpackage that might want to export its own services. The A service file type ServiceName struct { /* ... */ }
func (s *ServiceName) InitService() { /* ... */ }
func (s *ServiceName) ShutdownService() { /* ... */ } as well as any user-defined methods. The func Services() application.Service {
return application.CombineServices(
application.NewService(&ServiceName1{}),
application.NewService(&ServiceName2{}),
)
} The name Then application templates would be amended to include the following default configuration: import "<module path>/services"
// ...
func main() {
// ...
application.New(application.Options{
// ...
Bind: []application.Service{
services.Services(),
},
// ...
})
// ...
}
Let me explain the rationale behind this approach:
Two problems still have to be handled:
Problem 1 can be easily solved by parsing the package, then editing the Go AST and printing it back instead of manipulating source code directly. The only constraint being that the Problem 2 can be solved in the same way, but I was actually thinking that maybe method handling could be omitted entirely. Service methods are not the same as routes/actions in a web framework: they are just plain simple Go methods, with zero boilerplate. I don't see this feature being used much, and I feel like it would be a bit overkill. The only case where we'd still need to edit methods would be service renaming and removal, and it can be easily done with the type-checker + AST editing. I am of course available for help or discussion in implementing AST/type manipulation. In this regard, I was having a look at things to understand how hard it would be and I found this interesting lib: https://pkg.go.dev/github.com/dave/dst |
Thanks @fbbdev for the insightful feedback 🙏 I'm making services for myselfThis is almost certainly the default use case and the one we should consider the most. In this scenario, there is no need for I'm making a sharable serviceThis is where someone has written a service and would like to share it. Sharing should absolutely be done via Go modules. So then the question is, how do we get from a local service to a shareable one? Perhaps it's just adding a The only issue with composing multiple services into the same folder is that each 3rd party library you pull in may have different package names (they might not all be We should also be clear on terms 😅 For me a service is discrete & shareable code that can be given to the I would be very wary of manipulating source code or copying code from 3rd party packages to local directories as, honestly, I think it's going to be very hard with lots of opportunities to go wrong. The ideal scenario is where someone uses Again, thanks for taking the time to provide feedback. What a great community we have! 🚀 |
I'm not sure if I understand the concern on this, it would essentially just be acting as if created a package for something and generate as such. I was under the impression this move was mostly going to be handled with the golangs build system for instance I can There would be a concern of imports here being fairly lengthy on the frontend but could that be solved with the plugin.yml? maybe parser checks if there's a "package" field that standard should be similar to neovim package managers for example |
I've been working an an implementation of this idea with the thoughts listed above, I've reached a semi-functioning point and wanted to garner some feedback before fully committing. The PR below implements plugins as a service without much effort by the end user and no pain to anyone to conform to the plugin interface for every bound service. There is a type Plugin interface {
Shutdown() error
Init() error
Name() error
} Any service that is provided to the
I believe this targets most of the concerns in this conversation mainly
Below ill link the PR to my changes, this is a draft PR as there is still some things to sort out if this is the desired route, ie; CLI tooling mostly, would love feedback on what kind of tooling we would want? if any? There is some desire to have CLI commands to read the Notes on the PR: Notes on the SqlitePlugin
// main.go
sqliteConf := sqlite.Sqlite{
DbName: "wailsPlugin",
}
app := application.New(application.Options{
Name: "v3-tutorial",
Description: "A demo of using raw HTML & CSS",
Services: []application.Service{
application.NewService(&sqliteConf),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
}) See PR: #3570 Thank you all for your input, looking forward to your thoughts/concerns |
Update: Pushed new commit to #3570, this supports detecting if a service implements an Previous discussion in the discord threads had pointed towards using FQN for the path for handlers. For example if I created an sqlite plugin at In discussion with other maintainers we opted for another solution, an optional parameter inside NewBindings, this will allow for users to define their own prefix only if they want to use that service handler and they choose the prefix. This would look like. // if sqlite implements http.Handler it will be registered under services/sqlite
application.NewService(&sqlite{}, "sqlite")
// sqlite can still implement the handler but will not be registered if the prefix isn't provided
application.NewService(&sqlite{})
// another service handler for fileserver /services/files/<filepath> to request a file
application.NewService(&fileserver{}, "files") Necessary adjustments have been made to the bindings generator to accommodate this change. type FileServer struct {}
func (p *FileServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
app := application.Get()
var err error
app.Logger.Warn("Attempting Serving file", "file", req.URL.Path)
fileData, err := os.ReadFile(req.URL.Path)
if err != nil {
app.Logger.Warn("Could not load file", "file", req.URL.Path)
res.WriteHeader(http.StatusBadRequest)
res.Write([]byte(fmt.Sprintf("Could not load file %s", req.URL.Path)))
return
}
app.Logger.Info("Serving dynamic asset", "file", req.URL.Path)
res.Write(fileData)
} application.NewService(&FileServer{}, "files") Inside the JavaScript console let mod = await fetch("/services/files/go.mod") |
I'm going to close this as it was an old proposal and we have since implemented some of it. |
Description
This proposal outlines new CLI functionality for the new "Services" concept in v3, which can be user to create new services in a standard way. It offers a foundation for future service management, such as installing 3rd party services.
I'm willing to implement this myself.