Skip to content

Replace the GWT client engine with a TypeScript implementation - #25210

Closed
totally-not-ai[bot] wants to merge 83 commits into
flow-server-remove-client-enginefrom
flow-client-ts-console-gating
Closed

Replace the GWT client engine with a TypeScript implementation#25210
totally-not-ai[bot] wants to merge 83 commits into
flow-server-remove-client-enginefrom
flow-client-ts-console-gating

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Removes the GWT-based Flow client engine and replaces it with a TypeScript port that ships as part of the frontend bundle.

  • New engine — 79 modules under flow-client/src/main/frontend/internal/, a module-for-module port of the Java client: bootstrap (Bootstrapper, ApplicationConfiguration, ApplicationConnection), the reactive core and node features (StateNode, StateTree, NodeMap, NodeList, MapProperty, TreeChangeProcessor), the binding layer (Binder, SimpleElementBindingStrategy, TextBindingStrategy, Debouncer, ServerEventObject), networking (MessageSender, MessageHandler, XhrConnection, AtmospherePushConnection, Heartbeat, Poller, reconnect/loading-indicator state handling), and the support services (DependencyLoader, ResourceLoader, Profiler, PolymerUtils, LitUtils, ReactUtils, WidgetUtil, URIResolver, …).
  • Removed — all 105 Java classes of com.vaadin.client.**, the GWT linker com.vaadin.flow.linker.ClientEngineLinker, the *.gwt.xml modules and the single-script template, the src/test-gwt HtmlUnit suite, and the corresponding Java unit tests. flow-client/pom.xml drops the gwt-maven-plugin, the gwt-user/gwt-dev/gwt-elemental dependencies, the sdm profile, and the GWT-specific surefire/spotless/build-helper configuration. BootstrapHandler no longer resolves and injects a client.nocache.js / compiled client-engine <script> tag.
  • Buildscripts/client.js no longer inlines the compiled GWT bundle into FlowClient.js; it emits a small entry point that calls onModuleLoad() from the TypeScript bootstrap.
  • Tests — 85 web-test-runner suites under src/test/frontend/ covering the ported modules, replacing the deleted Java/GWT tests.

Why

The GWT toolchain was the last piece of the client that could not be built, linted, debugged, or tested with the same tooling as the rest of the frontend. Porting it removes a separate compiler and dependency stack, drops the extra client-engine script request from bootstrap, and makes the client engine ordinary TypeScript that ships through the existing bundler.

Behavior fixes made during the port

  • Native DOM throughout — the DomApi abstraction and the Polymer/HTML-import DOM path are gone; the binding layer, ApplicationConnection, and dependency loading use native DOM directly, and the HTML-imports-ready bootstrap gate is removed (chore: remove Polymer 1 DOM API and HTML import loading #25145, refactor: use the elemental DOM API directly in the client engine #25146).
  • Container refills no longer flash empty — when the server clears and refills a container in one change set, old children are kept until the replacements are inserted, preserving scroll position (mirrors GWT fix fix: keep replaced children until the new ones are in place #25147).
  • client.getNodeInfo(nodeId).javaClass works again — the engine read node property "class" instead of the "jc" the server writes. JAVA_CLASS now lives in the NodeProperties table with the other wire names mirrored from com.vaadin.flow.internal.nodefeature.NodeProperties; the test fixture that hardcoded the wrong key now uses real wire names.
  • Production-mode log gating restored — the port had dropped Console.setProductionMode, so all 64 log statements across 21 engine modules logged unconditionally (including the raw session-resynchronization response body). Logging is routed through the ported Console again, and an eslint no-console rule over src/main/frontend/internal keeps it that way; two deliberately ungated sites are marked.
  • Synchronous startup — the bootstrap imports the engine statically instead of via a dynamic import(), matching the old Java bootstrap and removing an always-fetched 105 kB chunk from the startup critical path.

API Changes

API Changes: flow-server-remove-client-engine vs origin/main

105 public classes/interfaces removed (all of com.vaadin.client.** plus com.vaadin.flow.linker.ClientEngineLinker); no additions and no signature changes. All public/protected members of these types are removed with them. The only other Java edits (BootstrapHandler, ClassesSerializableTest) touch private members and test configuration only.

com.vaadin.client

// Removed
public class ApplicationConfiguration
public class ApplicationConnection
public class BrowserInfo
public interface Command
public class ConnectionIndicator
public final class Console
public class DefaultRegistry extends Registry
public class DependencyLoader
public class ElementUtil
public final class ExecuteJavaScriptElementUtils
public class ExistingElementMap
public class InitialPropertiesHandler
public final class LitUtils
public final class PolymerUtils
public class Profiler
public final class ReactUtils
public class Registry
public class ResourceLoader
public class StorageUtil
public class SystemErrorHandler
public class TrackingScheduler extends SchedulerImpl
public class UILifecycle
public class URIResolver extends VaadinUriResolver
public final class ValueMap extends JavaScriptObject
public class WidgetUtil

com.vaadin.client.bootstrap

// Removed
public class Bootstrapper implements EntryPoint
public final class ErrorMessage extends JavaScriptObject
public final class JsoConfiguration extends JavaScriptObject
public class LocationParser

com.vaadin.client.communication

// Removed
public class AtmospherePushConnection implements PushConnection
public interface ConnectionStateHandler
public class DefaultConnectionStateHandler implements ConnectionStateHandler
public class Heartbeat
public class LoadingIndicatorConfigurator
public class LoadingIndicatorStateHandler
public class MessageHandler
public class MessageSender
public class PollConfigurator
public class Poller
public class PushConfiguration
public interface PushConnection
public interface PushConnectionFactory
public class ReconnectConfiguration
public class ReconnectionAttemptEvent
public class RequestResponseTracker
public class RequestStartingEvent extends Event<RequestStartingEvent.Handler>
public class ResponseHandlingEndedEvent
public class ResponseHandlingStartedEvent
public class ServerConnector
public class ServerRpcQueue
public class XhrConnection
public class XhrConnectionError

com.vaadin.client.flow

// Removed
public class ConstantPool
public class ExecuteJavaScriptProcessor
public class NodeUnregisterEvent
public interface NodeUnregisterListener
public class StateNode
public class StateTree
public class TreeChangeProcessor

com.vaadin.client.flow.binding

// Removed
public final class Binder
public interface BinderContext
public interface BindingStrategy<T extends Node>
public class Debouncer
public class ServerEventHandlerBinder
public final class ServerEventObject extends JavaScriptObject
public class SimpleElementBindingStrategy implements BindingStrategy<Element>
public class TextBindingStrategy implements BindingStrategy<Text>

com.vaadin.client.flow.collection

// Removed
public class JsArray<T>
public class JsCollections
public class JsMap<K, V>
public class JsSet<V>
public interface JsWeakMap<K, V>
public class JreJsArray<T> extends JsArray<T>       // com.vaadin.client.flow.collection.jre
public class JreJsMap<K, V> extends JsMap<K, V>     // com.vaadin.client.flow.collection.jre
public class JreJsSet<V> extends JsSet<V>           // com.vaadin.client.flow.collection.jre
public class JreJsWeakMap<K, V> implements JsWeakMap<K, V> // com.vaadin.client.flow.collection.jre

com.vaadin.client.flow.dom

// Removed
public class DomApi
public interface DomElement extends DomNode
public interface DomNode

com.vaadin.client.flow.model

// Removed
public class UpdatableModelProperties

com.vaadin.client.flow.nodefeature

// Removed
public class ListSpliceEvent extends ReactiveValueChangeEvent
public interface ListSpliceListener
public class MapProperty implements ReactiveValue
public class MapPropertyAddEvent extends ReactiveValueChangeEvent
public interface MapPropertyAddListener
public class MapPropertyChangeEvent extends ReactiveValueChangeEvent
public interface MapPropertyChangeListener
public abstract class NodeFeature
public class NodeList extends NodeFeature implements ReactiveValue
public class NodeMap extends NodeFeature implements ReactiveValue

com.vaadin.client.flow.reactive

// Removed
public abstract class Computation implements ReactiveValueChangeListener
public interface FlushListener
public class InvalidateEvent
public interface InvalidateListener
public class Reactive
public abstract class ReactiveEventRouter<L, E extends ReactiveValueChangeEvent>
public interface ReactiveValue
public abstract class ReactiveValueChangeEvent
public interface ReactiveValueChangeListener

com.vaadin.client.flow.util

// Removed
public class ClientJsonCodec
public class JsObject
public class NativeFunction

com.vaadin.client.gwt

// Removed
public class SimpleEventBus extends EventBus  // com.vaadin.client.gwt.com.google.web.bindery.event.shared
public class Xhr                              // com.vaadin.client.gwt.elemental.js.util

com.vaadin.flow.linker.ClientEngineLinker

// Removed
public class ClientEngineLinker extends SingleScriptLinker

Artur- and others added 30 commits July 8, 2026 14:52
First step of the GWT->TypeScript client migration (see MIGRATION_PR_PLAN.md).
Ports the dependency-free leaf utilities and their unit tests: assert, Console,
WidgetUtil, BrowserInfo, JsArray, StorageUtil, SharedUtil, DomApi, Profiler.

Also raises the typescript-eslint default-project file cap (default 8) in
eslint.config.js: the src/test/frontend test files run under the default project,
and this PR pushes the count past 8, so the cap must grow with the suite.

Additive only -- nothing is wired into the running client (GWT remains live); the
modules are exercised by mocha (45 tests).
Ports the reactive dependency-tracking core and the node-feature layer:
reactive, NodeFeature, NodeFeatures, NodeMap, NodeList, MapProperty.

Depends only on the leaf utilities (PR1); the nodefeature layer is decoupled
from the state tree via the MapPropertyOwner interface. Additive only -- not
wired into the running client. Exercised by mocha (38 tests).
Ports the state-tree layer (StateNode, StateTree, TreeChangeProcessor,
ConstantPool, ExistingElementMap) and the JSON codec primitives it decodes
values through (ClientJsonCodec, JsonConstants).

Depends on PR1 leaves + PR2 reactive/nodefeature. Additive only -- not wired
into the running client. Exercised by mocha (42 tests).
Ports the element-binding layer that syncs StateNode data to the DOM:
SimpleElementBindingStrategy (+ the Polymer model-property binding split-out),
Binder, BindingStrategy, TextBindingStrategy, ServerEventHandlerBinder,
ServerEventObject, Debouncer, ElementUtil, LitUtils, ReactUtils,
PolymerUtils, PolymerModelTree, and UpdatableModelProperties.

Depends on PR1-3 (leaves, reactive/nodefeature, state tree); event dispatch to
the server is via injected callbacks, so no communication-layer dependency yet.
Additive only -- not wired into the running client. Exercised by mocha
(28 test files).
…cle to TypeScript

Ports the support-services layer: resource/dependency loading (ResourceRegistry,
ResourceLoader, DependencyLoader, EagerDependencyTracker), server-driven JS
execution (ExecuteJavaScriptProcessor, ExecuteJavaScriptElementUtils), the
application configuration + lifecycle (ApplicationConfiguration, JsoConfiguration,
UILifecycle, InitialPropertiesHandler, LocationParser), the deferred-send
scheduler (TrackingScheduler), the Registry base, and SystemErrorHandler.

Depends on PR1-4; closes over already-ported code (no network-layer dependency).
Additive only -- not wired into the running client. Exercised by mocha (86 tests).
Ports the communication + push + connection-state cluster (mutually dependent, so
landed together): message handling and RPC (MessageHandler, MessageSender +
sendBeacon, ServerConnector, ServerRpcQueue, MessageOrdering,
RequestResponseTracker, XhrConnection, ResynchronizationState, URIResolver), push
(PushConnection, AtmospherePushConnection, PushConfiguration), and connection
state (ConnectionStateHandler, DefaultConnectionStateHandler, ReconnectStateMachine,
ReconnectConfiguration, ConnectionMessageType, ConnectionIndicator,
LoadingIndicator configurator/handler, Heartbeat, Poller, PollConfigurator).

Depends on PR1-5; services are injected via Registry rather than imported, so it
closes over ported code. Additive only -- not wired into the running client.
Exercised by mocha (102 tests).
Ports the top layer that wires the client together: DefaultRegistry (the DI hub
that instantiates every service), ApplicationConnection (the engine entry via
ApplicationConnection.create), Bootstrapper, and the published client API
(publishClient, clientApi).

Completes the TypeScript port of the client engine -- all 80 internal modules are
now present and unit-tested. Still additive: nothing is wired into the running
client (GWT remains live); the cutover is a separate step. Depends on PR1-6.
Exercised by mocha (23 tests).
With the TypeScript engine now the live client (previous commit), the GWT client
is dead. Remove it: the com.vaadin.client.* sources and their JUnit + GWTTestCase
tests, the ClientEngine*.gwt.xml modules, ClientEngineLinker and its single-script
template, and the GWT dependencies / gwt-maven-plugin / GWT test wiring from
pom.xml and package.json.

flow-client is now a TypeScript + resources module with no Java sources. Nothing
outside flow-client referenced com.vaadin.client.
…cript

FlowClient.init() -- imported and called by Flow.ts -- is generated by
scripts/client.js. Change the generator to emit the TypeScript bootstrap
(onModuleLoad from Bootstrapper) instead of wrapping the compiled GWT engine blob,
making the TypeScript engine the live client. The GWT compile output is no longer
read.

The full mocha suite, including FlowTests (which exercises the bootstrap), passes
against the TypeScript client. The GWT sources and build wiring are removed in the
follow-up PR.
BootstrapHandler emitted a client.nocache.js <script> and computed the GWT client
engine URL, but the live client loads via Flow.ts -> FlowClient, so this path is
dead once the GWT engine is gone. Remove getClientEngineUrl/getClientEngine, the
CLIENT_ENGINE_NOCACHE_FILE constant and its emission in setupFrameworkLibraries,
and the matching compile.properties mock in MockServletServiceSessionSetup.

Nothing referenced the removed API.
Artur- and others added 9 commits August 11, 2026 16:48
Mirrors the GWT client fix #25147 in the TypeScript port: when the
server clears and refills a container in one change set, the old
children are now kept until the replacements are inserted, so the
container is never momentarily empty and the surrounding scroll
position is preserved. Uses native DOM (DomApi is removed from this
port).
Console.java suppressed all browser logging in production mode unless the
localStorage flag vaadin.browserLog was set to "true", and
ApplicationConfiguration.setProductionMode fed the flag in via
Console.setProductionMode. The TypeScript port only carried over
isLocalStorageFlagEnabled, dropped the setProductionMode call and had every
ported module call console.debug/log/warn/error directly, so all 64 log
statements across 21 engine modules logged unconditionally -- including a
console.log of the raw session-resynchronization response body.

Port the rest of Console: setProductionMode plus gated debug/log/warn/error,
route the engine's logging through it, and reinstate the setProductionMode call
in ApplicationConfiguration. Two sites keep logging ungated because their Java
counterparts were JSNI calling console.error directly (ResourceLoader's dynamic
dependency promise) or reported the failure outside the gate via
Console.reportStacktrace (ExecuteJavaScriptProcessor.invoke); both are marked.
An eslint no-console rule over src/main/frontend/internal keeps the gating from
regressing.
@Artur-
Artur- changed the base branch from main to flow-server-remove-client-engine August 13, 2026 08:08
@totally-not-ai totally-not-ai Bot changed the title flow client ts console gating I'll start the API changes check while I review the diff.I'll analyze the API changes now.Analysis complete. Merge base: 9b627a7d10. The Java diff is 105 deleted files plus 2 modified files whose changes are entirely private (BootstrapHandler's CLIENT_ENGINE_NOCACHE_FILE constant and three private methods; ClassesSerializableTest's exclusion list inside a method body) — no API impact from those two. Aug 13, 2026
@github-actions github-actions Bot added +0.0.1 and removed +1.0.0 labels Aug 13, 2026
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 424 files   -  26   1 509 suites  +58   1h 27m 59s ⏱️ +42s
10 578 tests +169  10 511 ✅ +169  67 💤 ±0  0 ❌ ±0 
11 014 runs  +169  10 946 ✅ +169  68 💤 ±0  0 ❌ ±0 

Results for commit 3c839b9. ± Comparison against base commit d306eb5.

doStartApplication loaded ApplicationConnection through a dynamic import, which
made the application start asynchronously where the Java bootstrap was
synchronous. The stated reason -- keeping the engine out of the bundle the
HtmlUnit-based GwtTests load -- no longer applies now that the GWT client and
its tests are gone.

The dynamic import also split the engine into its own bundler chunk (esbuild
with code splitting: 4.8kB entry + 105kB engine + 1.2kB shared, versus a single
111kB chunk when imported statically). That chunk is always fetched, since
Flow.ts imports FlowClient only when it is about to start a UI, so the split
only added a round-trip to the startup critical path.

Import the engine statically. No engine module has top-level side effects and
the module graph is free of cycles, so evaluating it when FlowClient is imported
is safe. Covered by a test asserting the client API is published by the time
doStartApplication returns, which fails with the dynamic import.
@totally-not-ai totally-not-ai Bot changed the title I'll start the API changes check while I review the diff.I'll analyze the API changes now.Analysis complete. Merge base: 9b627a7d10. The Java diff is 105 deleted files plus 2 modified files whose changes are entirely private (BootstrapHandler's CLIENT_ENGINE_NOCACHE_FILE constant and three private methods; ClassesSerializableTest's exclusion list inside a method body) — no API impact from those two. Replace the GWT client engine with a TypeScript implementation Aug 13, 2026
@vaadin-bot vaadin-bot added +1.0.0 and removed +0.0.1 labels Aug 13, 2026
@totally-not-ai totally-not-ai Bot changed the title Replace the GWT client engine with a TypeScript implementation Replace the GWT client engine with a TypeScript port Aug 13, 2026
@Artur- Artur- changed the title Replace the GWT client engine with a TypeScript port fix(flow-client): restore production-mode log gating and import the engine statically Aug 13, 2026
The engine assembly port declared its own JAVA_CLASS constant with the value
"class", but NodeProperties.JAVA_CLASS is "jc". getJavaClass therefore read a key
the server never writes and always returned null, so the javaClass field of the
dev-mode client.getNodeInfo(nodeId) API -- used by the dev tools to report the
component type behind an element -- was always empty.

Drop the local constant and add JAVA_CLASS to the TS NodeProperties table, so the
value sits with the other wire names mirrored from
com.vaadin.flow.internal.nodefeature.NodeProperties and cannot drift on its own
again.

The ApplicationConnection test fixture hardcoded the same wrong key, which is why
this passed unnoticed; it now uses the wire names the server actually writes and
fails if the constant is wrong.
@totally-not-ai totally-not-ai Bot changed the title fix(flow-client): restore production-mode log gating and import the engine statically Replace the GWT client engine with a TypeScript implementation Aug 13, 2026
Artur- pushed a commit that referenced this pull request Aug 14, 2026
Follow-up to #25210. The port comments described work in progress -- "built
alongside the Java version", "not ported yet", "contracts satisfied at cutover",
constraints imposed by the HtmlUnit-based GwtTests, and $entry wrappers on the
Java side -- none of which is true now that the GWT client is gone. publishClient
still claimed it was "not yet wired into the bootstrap", pointing at a
MIGRATION_STRATEGY.md that does not exist in the repository, and
SimpleElementBindingStrategy said its functions would be assembled into a class
that it already defines. Reword them to describe the code as it is, keeping the
references to the GWT original where they explain why something looks the way it
does. The "Slice N:" section headers, which numbered the migration steps, lose
the numbering and keep their titles.

Also remove what the GWT deletion left behind: the com/google/gwt/dev/js/globals
resource, the gwt.version property and the gwt-elemental dependencyManagement
entry in the root pom (no module depends on GWT any more), and
ApplicationConstants.CLIENT_ENGINE_PATH, which named a folder that is no longer
served or referenced. SessionCloseLogoutIT no longer allows console errors from
the removed client engine path.
@Artur-

Artur- commented Aug 14, 2026

Copy link
Copy Markdown
Member

Commits distributed into the PRs that introduced the code they fix.

@Artur- Artur- closed this Aug 14, 2026
@Artur-
Artur- deleted the flow-client-ts-console-gating branch August 14, 2026 08:01
@Artur-
Artur- restored the flow-client-ts-console-gating branch August 14, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants