flow
flow copied to clipboard
Development mode without node or npm
Describe your motivation
When you are starting an application in development mode, a couple of things that are not fast have to happen:
- If there is no node installed or the installed node is not of a suitable version, node needs to be installed
- The modules defined in package.json needs to be installed using
npm installwhich downloads and copies the files intonode_modules. This is slow even if the files have been previously downloaded and only need to be copied intonode_modules - Vite is started and processes the files in
frontendand used files fromnode_modulesinto a suitable format so the browser can load them
Describe the solution you'd like
There should be a serverless development mode that mostly skips all 3 mentioned tasks.
The serverless development mode would load frontend files from two possible locations:
- A "default app bundle" which contains all Vaadin components and possibly some other components. It is produced as part of the Flow/Vaadin build for each version and the JavaScript/resources are included in a jar. A Flow application without frontend customizations, templates or addons (which do frontend customizations) can use this bundle directly for all development and never need any tooling for the frontend when developing.
- A bundle that is maintained as part of the project, inside
src/main/resources
How this would work is:
- When starting the server, check if the default app bundle can be used (no extra dependencies or newer dependency versions, no extra frontend files contributed from addons or the frontend folder). If ok, load the default app bundle in the browser
- If there are extra frontend files or dependency changes, check if there is an app bundle in
src/main/resources. If there is, check if that has been compiled with the same frontend files and dependencies that are currently in use. If there is a match, load this bundle in the browser. - If there are modifications and the app bundle is not in sync, remove the app bundle, install frontend tooling, compile a new bundle into
src/main/resources. Then use the new bundle.
Use cases where this is good:
- You start a new project - no need for any frontend tooling except checking the frontend files and dependencies in the project and comparing to the default app bundle. Startup time reduced dramatically.
- You add an add-on to a project - the person adding the addon restarts the server which recompiles the bundle. It takes some time. The addon dependency + new bundle is commited to the repository and the other developers in the project do not need to compile the frontend at all.
- You customize a file in the frontend folder. Same as adding an addon. You compile once, other devs do not.
Use cases which are bad unless we make them good:
- The theme is processed as part of the frontend bundle. We would need to change that so that the theme can be separately processed and modified/loaded without a frontend build.
- You modify files inside the frontend folder many times per day, e.g. you are using templates. Before you can see the change, you recompile the frontend. This is much slower than a Vite hot reload would be as the Vite server only recompiles one file. If you are using templates, you should use the dev server dev mode and not the serverless mode. This can probably be improved also but would be a separate feature on top of this.
Additional context
This is what starting a new app potentially could look like
https://user-images.githubusercontent.com/260340/190645365-89bf01ec-92f6-45ff-86bf-10b36c587c99.mov
I like the idea a lot, but added some inline comments below to hopefully make it even better.
A "default app bundle" which contains all Vaadin components and possibly some other components.
"vaadin-development-bundle" sounds like a good name for the artefact - allowing it to be excluded on production builds 😉 you know I love my clean prod build.
A bundle that is maintained as part of the project, inside src/main/resources
I feel like this is the wrong directory, because it allows accidentally publishing this to the production artefacts increasing it's size and it's "indirect" dependencies. I would suggest to use a folder next to frontend or within?
If there are modifications and the app bundle is not in sync, remove the app bundle, install frontend tooling, compile a new bundle
That would be the default - but what would you think about a "Vaadin-Development-Container" allowing people to deploy one or many of those in their on-premise location to which their code is send to compile (in development mode)? This would drastically reduce the local complexity from developer machines and allows Vaadin to create a tested environment where you can say "it works!" where code is compiled freeing the developer computer which is really similar to other "modern" ideas where centralized builds or even development environments are provided .. *[1] - this container has to be free, but you can still monetize it by providing a dedicated online service for it if people don't like on premise
Additional comments:
Like you described, it is a must have that the processing of theming files work flawlessly like now without additional complexity for the end user. (custom java http server * [2] is sadly only available in Java 18)
Additionally I think there has to be a way to somehow get the .js / .ts files in the main project to work without the creation of a new bundle to use development of those files.. even tho I understand that you don't want to create your own typescript compiler. Otherwise debugging / developing of those could be a real hassle and reducing DX quite a lot. Or should there be a way to switch between the "development modes" with a single feature flag?
[1] Of course it has to bit easily customizable like configuring npm proxy and should work fully offline with a local mirror / artifactory
[2] https://openjdk.org/jeps/408
-
I support the statement of @knoobie that
src/main/resourcesis the wrong location. Everything in there will be copied into the JAR/WAR. -
Please don't call it "serverless development mode" because serverless is FaaS like AWS Lambda.
A bundle that is maintained as part of the project, inside src/main/resources
I feel like this is the wrong directory, because it allows accidentally publishing this to the production artefacts increasing it's size and it's "indirect" dependencies. I would suggest to use a folder next to frontend or within?
The idea was that you could deploy the development build also to a server for testing but it is probably more sensible indeed to keep it out of the jar/war and have the same restrictions as for the current dev mode: you need to have access to the project folder
That would be the default - but what would you think about a "Vaadin-Development-Container" allowing people to deploy one or many of those in their on-premise location to which their code is send to compile (in development mode)? This would drastically reduce the local complexity from developer machines and allows Vaadin to create a tested environment where you can say "it works!" where code is compiled freeing the developer computer which is really similar to other "modern" ideas where centralized builds or even development environments are provided .. *[1] - this container has to be free, but you can still monetize it by providing a dedicated online service for it if people don't like on premise
This sounds like the CDN service for compiling the widget set for Vaadin 7. Something worth to consider outside this ticket
Additionally I think there has to be a way to somehow get the .js / .ts files in the main project to work without the creation of a new bundle to use development of those files.. even tho I understand that you don't want to create your own typescript compiler. Otherwise debugging / developing of those could be a real hassle and reducing DX quite a lot. Or should there be a way to switch between the "development modes" with a single feature flag?
For a first version I think it would be fine if you are able to switch between the current, Vite server based dev mode, and a file based dev mode with e.g. a configuration option for the plugin in pom.xml. If you have custom js/ts files in frontend and you often change those, you would stick to a Vite server based dev mode and otherwise stay with the default.
Doing compilation of ONLY files in the frontend folder and not the whole bundle could be nice but is sounds like it is a bit out of scope for this idea, which aims to not install node/npm at all in those many cases when they are not needed and not make DX worse in those cases it is needed.
The idea was that you could deploy the development build also to a server for testing
I don't know if there is a use case for that. One should always test what maybe will make its way to production. So in a CI/CD pipeline there is the build first and then the deployment.
I did some tests and think that it highly depends on the computer and the internet connection speed.
In starting a new application downloaded from start.vaadin.com takes around one minute. Incl. npm install.
After the time from starting the app to the first interaction is 20 seconds.
For me, this is acceptable.
I would think that your happy path isn't the demographic that this should fix / improve 😉
Coming from V6 to V8 I have to say that the additional tooling with node/npm isn't really welcomed with the "older" developers which just used Vaadin and build their widgetset once and it worked on every computer without additional tooling.
Additionally a lot of problems with people custom/old node/npm, weird network situations, configuration or even just random errors trying to install node/npm is really hurting Vaadin's reputation in my opinion in the last years, making it harder to convince people to use and stay with Vaadin because of the additional tooling which most people don't even need for simple or even complex apps.
So reducing the moving parts to improve developer experience is at this point a must have if Vaadin wants to attract more of their older audience that really wants to stay away from JS - which feels kinda impossible if node/npm is locally needed just for development of a standard app.
That's an interesting insight. As someone never used Vaadin in production before Flow I can't compare the developer experience. But I can tell from my work with clients that they rarely have issues with node/npm.
Improving the developer experience is always welcome as long as it no complicate the overall framework.
But I can tell from my work with clients that they rarely have issues with node/npm.
Personally I never had problems as well that couldn't be solved by a little bit of googling but we've seen so many problems here, on stack overflow or discord where people can't handle it or it randomly broke - often I haven't seen these people coming back to ask other questions, which got me thinking if they just stopped using Vaadin because of this bad start.
welcome as long as it no complicate the overall framework.
I think the introduction of automatically downloading and installing / updating node/npm was already a huge complexity for the framework.
But "forcing" people to install both by hand is really hard as well to gather new people. So this new mode could be a way to get rid of this complexity (I don't think that Artur planned this, but it would be possible) by providing a (hopefully) way more simpler way for people to build their app locally.
But still allowing people who want to do "advanced" / frontend stuff with the option to do so with a configuration change and automatically or manuel installation of node/npm if needed.
Made a quick prototype here https://github.com/Artur-/no-node-app
it has a lot of limitations (only a fixed bundle, no editing of theme etc) but should demonstrate the idea
Interesting!
There is one crucial change that I discussed with @mstahv several times:
With your prototype, the prepare-frontend MUST run otherwise, the application does not start! Currently prepare-frontend is not needed and the Vaadin plugin must only be used in production mode.
For me it starts fine by running the Application class from VS Code. What error do you see?
If I run it directly in IntelliJ I get the following exception.
The reason is that the frontend plugin was not run with prepare-frontend.
java.io.IOException: Unable to find index.html. It should be available in the frontend folder when running in development mode
at com.vaadin.flow.server.communication.IndexHtmlRequestHandler.getIndexHtmlDocument(IndexHtmlRequestHandler.java:330) ~[flow-server-23.3.nonodemode-20220920.140944-2.jar:23.3.nonodemode-SNAPSHOT]
at com.vaadin.flow.server.communication.IndexHtmlRequestHandler.synchronizedHandleRequest(IndexHtmlRequestHandler.java:84) ~[flow-server-23.3.nonodemode-20220920.140944-2.jar:23.3.nonodemode-SNAPSHOT]
at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:40) ~[flow-server-23.3.nonodemode-20220920.140944-2.jar:23.3.nonodemode-SNAPSHOT]
at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1564) ~[flow-server-23.3.nonodemode-20220920.140944-2.jar:23.3.nonodemode-SNAPSHOT]
at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:364) ~[flow-server-23.3.nonodemode-20220920.140944-2.jar:23.3.nonodemode-SNAPSHOT]
at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106) ~[vaadin-spring-23.3.nonodemode-20220920.141154-2.jar:na]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.65.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:711) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:459) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:353) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:313) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.springframework.web.servlet.mvc.ServletForwardingController.handleRequestInternal(ServletForwardingController.java:141) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:177) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:51) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.65.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.22.jar:5.3.22]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.65.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.22.jar:5.3.22]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.22.jar:5.3.22]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.22.jar:5.3.22]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
This is now tracked in https://github.com/vaadin/platform/issues/3554