refactor(flow-client): Migrate from GWT to TypeScript#24703
Draft
Artur- wants to merge 63 commits into
Draft
Conversation
…gration Turn on the GWT compiler's generateJsInteropExports so classes annotated with @jstype are published to window.Vaadin.Flow.internal.* and callable from TypeScript. This is the foundation for migrating flow-client from GWT to TypeScript top-down: new TypeScript calls into the still-Java GWT bundle through these exports (TypeScript -> GWT only), so no class is ever implemented twice. Adds a minimal JsInteropProbe export plus a GwtTest and a web-test-runner test that prove, in CI, that the export is emitted and callable end to end, and an internal/gwtExports.ts accessor establishing the typed-access convention for exported classes. No production code is migrated yet. See MIGRATION_STRATEGY.md for the full plan.
676bb39 to
7d6345f
Compare
…e TS migration Annotate ApplicationConnection with @jstype and expose the operations that back the published window.Vaadin.Flow.clients[appId] object as exported methods (isActive, getByNodeId, getNodeId, addDomBindingListener, poll, resolveUri, sendEventMessage, getUIId, connectWebComponent, debug). The two JSNI publication blocks now delegate to these methods instead of inlining the registry lookups, so each piece of logic lives in exactly one place. The constructor is marked @JSignore: exporting a member whose body builds the engine pulls MessageSender's GWT.create(PushConnectionFactory) rebind into the JsInterop export pass, which the GWT compiler cannot resolve ("Rebind result ... could not be found"). The instance is created on the GWT side; only the methods are exported. Behavior is unchanged.
…ration Annotate ApplicationConfiguration with @jstype so the TypeScript code that will take over client publication can read the translated application configuration (production mode, request timing, exported web components, servlet version, UI id, ...). Purely additive: the getters are already public, behavior is unchanged.
…urface Add typed interfaces in internal/gwtExports.ts mirroring the exported ApplicationConnection client API and the ApplicationConfiguration getters. The ApplicationConnection constructor is intentionally not exported (see @JSignore), so TypeScript consumes only the methods; the instance is created on the GWT side. No runtime behavior changes.
7d6345f to
218ad2c
Compare
… migration Export the dev/profiling helpers that back the published clients[] object: getJavaClass, isHiddenByServer and getElementStyleProperties (used by the dev getNodeInfo), and getProfilingData (moved from the inline JSNI publication into an exported native method). Add them to the gwtExports TypeScript contract. Behavior-preserving: the publication now delegates getProfilingData to the new method; the other helpers were already called by the dev publication block. This completes the exported client-API surface so the publication of clients[] can be built in TypeScript.
…bject Add internal/publishClient.ts, the TypeScript replacement for the JSNI publishJavascriptMethods / publishDevelopmentModeJavascriptMethods blocks. It builds window.Vaadin.Flow.clients[appId] by delegating to the exported methods of the GWT-constructed ApplicationConnection and reads flags from the exported ApplicationConfiguration. Not yet wired into the bootstrap: the handoff that calls this with the constructed instance, and the removal of the JSNI publication, is a separate step. No runtime behavior changes.
Cover publishClient with fakes for the exported ApplicationConnection and ApplicationConfiguration: the TestBench-id key (window-name suffix stripped), core API delegation, the requestTiming getProfilingData branch, and the dev-mode getVersionInfo / debug / getNodeInfo branch.
Switch the ApplicationConnection constructor to hand itself and its configuration to the TypeScript publishClient (window.Vaadin.Flow.internal. publishClient) instead of building window.Vaadin.Flow.clients[appId] in JSNI. Both publishJavascriptMethods and publishDevelopmentModeJavascriptMethods JSNI blocks are removed; the published object now delegates to the exported ApplicationConnection methods and is assembled in publishClient.ts. Flow.ts registers publishClient before init() (the sole caller of the engine's init, so every bootstrap path is covered). The GwtApplicationConnectionTest mock provides a matching stub. The GWT engine still constructs the ApplicationConnection — its constructor cannot be exported — and then hands the instance to TypeScript.
The ApplicationConnection constructor calls window.Vaadin.Flow.internal.publishClient, but Flow.ts registered it only on the client-side router path. Web component embedding (and any path that starts the engine without going through Flow.ts) hit "publishClient is not a function", failing app bootstrap. Register publishClient at the start of the engine's init() instead, so every caller of init() gets it right before the engine constructs the ApplicationConnection. It runs per init(), so it survives any window.Vaadin reset that happens during bootstrapping.
JsInteropProbe (with its GwtTest and web-test-runner test) proved the GWT->TS JsInterop export mechanism before any real class was migrated. ApplicationConnection and ApplicationConfiguration are now exported and exercised end to end (FlowTests, the it-tests, GwtApplicationConnectionTest), so the probe is redundant. Drop it and trim internal/gwtExports.ts to the typed contracts the TypeScript code uses.
Move ElementUtil's logic (hasTag, getElementById, getElementByName) to internal/ElementUtil.ts; the Java class now delegates to it via JSNI. Add a central registerInternals.ts that publishes the TypeScript implementations the GWT engine calls into (replacing the inline publishClient registration in the FlowClient.js wrapper), so future migrations register there. Unit-test the TypeScript in ElementUtilTests.ts. First step of the import-direction migration: the GWT engine calls into the TypeScript implementation, and the old Java logic is removed.
GwtTests do not run the engine's init(), so the TypeScript implementations the GWT engine delegates to (window.Vaadin.Flow.internal.*, such as the migrated ElementUtil) were absent. GwtBasicElementBinderTest's ElementUtil calls therefore failed with "hasTag/getElementById of undefined". Bundle those implementations with esbuild and transpile them to ES5 (the HtmlUnit used by GwtTests cannot parse newer syntax or the unicode regex flag), embed the bundle via a GWT ClientBundle, and eval it in ClientEngineTestBase.gwtSetUp() so every GwtTest registers the same implementations production does. GwtApplicationConnectionTest now preserves and reuses the real publishClient instead of a hand-written stub.
Move the URL helpers redirect, getAbsoluteUrl and isAbsoluteUrl from WidgetUtil.java to WidgetUtil.ts, registered on window.Vaadin.Flow.internal.WidgetUtil; the Java methods now delegate to it. This is the first slice of the WidgetUtil migration; the remaining helpers follow in later commits. getAbsoluteUrl stays covered by GwtWidgetUtilTest, and WidgetUtilTests covers isAbsoluteUrl and getAbsoluteUrl in mocha.
…Script Move the JavaScript object/property helpers (getJsProperty, setJsProperty, hasOwnJsProperty, hasJsProperty, isUndefined, deleteJsProperty, isTrueish, getKeys, createJsonObject, createJsonObjectWithoutPrototype, equalsInJS) from WidgetUtil.java to WidgetUtil.ts; the Java methods now delegate to it. These helpers are exercised pervasively by the binder GwtTests and are covered directly by WidgetUtilTests.
Move stringify and the toPrettyJson serializer from WidgetUtil.java to WidgetUtil.ts; the Java methods delegate to it. toPrettyJson keeps its GWT.isScript() guard so the legacy hosted-mode fallback is preserved - only its JSNI implementation moves. Covered by WidgetUtilTests.
Move isLitElement and whenRendered from LitUtils.java to LitUtils.ts; the Java methods delegate to it. whenRendered still wraps the Runnable in $entry on the Java side so GWT exception handling is preserved, passing the guarded callback to the TypeScript implementation. Covered by LitUtilsTests.
Move addReadyCallback from ReactUtils.java to ReactUtils.ts; the Java method delegates to it, still wrapping the Runnable in $entry on the Java side so GWT exception handling is preserved. isInitialized stays in Java as it is pure Java logic (a Supplier null check) with no JavaScript interop. Covered by ReactUtilsTests.
Move setState, getState, setProperty, loadingStarted, loadingFinished and loadingFailed from ConnectionIndicator.java to ConnectionIndicator.ts; the Java methods delegate to it. The connection-state string constants stay in Java as they are used by Java callers. Covered by the connection-state handler GwtTests (GwtDefaultConnectionStateHandlerTest, GwtLoadingIndicatorStateHandlerTest).
…Script Move the three JSNI probes - getBrowserString (user agent), checkForTouchDevice and isIos - from BrowserInfo.java to BrowserInfo.ts; the Java methods delegate to it. The browser/OS detection itself stays in the shared BrowserDetails Java class (used by both server and client), so the rest of BrowserInfo is unchanged. Covered by BrowserInfoTests.
Move resendRequest from XhrConnection.java to XhrConnection.ts; the Java method delegates to it. Covered by XhrConnectionTests. Also raise the typescript-eslint default-project file cap (default 8, now exceeded by the growing src/test/frontend suite) so linting keeps working as more *Tests.ts files are added.
…tyDefined to TS Move the isPropertyDefined Polymer-property check from ExecuteJavaScriptElementUtils.java to ExecuteJavaScriptElementUtils.ts; the Java method delegates to it. The StateNode/binding logic stays in Java. Covered by GwtExecuteJavaScriptElementUtilsTest and ExecuteJavaScriptElementUtilsTests.
…Script Move recreateNodes, showPopover and getShadowRootElement from SystemErrorHandler.java to SystemErrorHandler.ts; the Java methods delegate to it. recreateNodes now snapshots the live element collection before mutating it. Covered by SystemErrorHandlerTests.
Move createReturnChannelCallback and applyCaptures from ClientJsonCodec.java to ClientJsonCodec.ts; the Java methods delegate to it. createReturnChannelCallback still wires the $entry-guarded ServerConnector callback on the Java side; applyCaptures preserves the caller's this. Covered by GwtClientJsonCodecTest and ClientJsonCodecTests.
Move supportsHtmlWhenReady, addHtmlImportsReadyHandler, addOnloadHandler, getStyleSheetLength and runPromiseExpression from ResourceLoader.java to ResourceLoader.ts; the Java methods delegate, keeping the Runnable/listener callbacks $entry-guarded on the Java side. Covered by ResourceLoaderTests.
The migrated runPromiseExpression delegation wrapped the promise supplier in $entry, so an exception thrown while evaluating a dynamic-import expression was reported both to GWT's uncaught-exception handler and by runPromiseExpression's own catch (console.error), producing an extra SEVERE log entry (DynamicDependencyIT.dependecyLoaderThrows_errorLogged expected 2, got 3). The supplier's exception is expected and handled by the try/catch, so it must not be $entry-wrapped; onSuccess and onError stay wrapped.
Move the four Web Storage wrappers (local/session get and set) to a StorageUtil TS module; the Java methods delegate via JSNI.
Move the navigator.sendBeacon wrapper to a MessageSender TS module; the Java method delegates via JSNI.
…Script Move bindPolymerModelProperties and its hookUpPolymerElement helper to a SimpleElementBindingStrategy TS module. The Java method now builds the $entry-wrapped, node/tree-capturing callbacks (handlePropertiesChanged, fireReadyEvent, handleListItemPropertyChange) and delegates the Polymer DOM wiring (the _propertiesChanged/ready and dom-repeat prototype patching) to TS. The now-unused native hookUpPolymerElement is removed.
The migrated TypeScript implementations are eval'd in $wnd, so bundle code resolves Promise from $wnd rather than the GWT module window that gwtSetUp polyfills. Mirror the same synchronous Promise onto $wnd so deferred bundle callbacks (the Polymer whenDefined handling in SimpleElementBindingStrategy) run immediately in tests, as the engine's JSNI did when it ran in the module window. Fixes the GwtBasicElementBinderTest and GwtPolymerModelTest deferred-Polymer regressions.
…to TypeScript Move the default Atmosphere configuration builder to the TS module; the Java method delegates and passes PushConstants.MESSAGE_DELIMITER down so the constant stays the single source of truth.
…undary Mark PolymerUtils model-data, doConnect, createConfig, ExecuteJavaScriptProcessor context builder, StorageUtil, MessageSender.sendBeacon and the Polymer model-property binding as done and CI-green. Document the real-logic leaf phase as complete, the $wnd-vs-module-window Promise/polyfill rule, the local bundle-rebuild trap, and that the remaining structural tier needs bottom-up caller migration rather than leaf delegation.
…ypeScript Move the element.$server RPC-object helpers (initPromiseHandler, removeMethod, getMethods, rejectPromises and the node-reading getPolymerPropertyObject) to a ServerEventObject TS module operating on the $server object passed from Java. defineMethod stays in Java: its handler is assigned to the $server object wrapped in $entry, which routes the handler's exceptions to GWT's uncaught-exception handler and cannot be reproduced from TypeScript. First step of the structural tier.
…lding to TypeScript Move the system-error notification construction to the SystemErrorHandler TS module, reusing the already-migrated showPopover and getShadowRootElement helpers. The Java method delegates and passes a logError callback so the Console production-mode gating is preserved. Removes the now-unused findShadowRoot helper and the Java getShadowRootElement native.
…ypeScript Move the vaadin.browserLog localStorage flag check to a Console TS module; the Java method delegates. The browser-console logging, its GWT.isScript() gating and the uncaught-exception-handler machinery stay in Java.
…eScript Move the bootstrap configuration readers (getConfigString, getConfigValueMap, getConfigStringArray, getConfigBoolean, getConfigError, getVaadinVersion, getAtmosphereVersion) to a JsoConfiguration TS module; the Java methods delegate, passing the config object. getConfigInteger stays in Java (boxed java.lang.Integer return) and getAtmosphereJSVersion stays in Java because its JSNI reference to AtmospherePushConnection.isAtmosphereLoaded keeps that class reachable for the GWT deferred-binding of AtmospherePushConnection$Factory in MessageSender. First step of the bootstrap/config subsystem rewrite.
…getters CI prettier requires parentheses around the nullish-coalescing in `info ? info.vaadinVersion ?? null : null`, but the local format hook strips them. Rewrite as `info?.vaadinVersion ?? null` which both agree on.
Delete the ErrorMessage JavaScriptObject overlay. The session-expired error flows as the raw native config object: JsoConfiguration.getConfigError and ApplicationConfiguration's getter/setter now use JavaScriptObject, and SystemErrorHandler reads its caption/message/url fields via the TS WidgetUtil.getJsProperty. Removes one GWT JSO overlay class from the bootstrap/config cluster.
Move startApplicationImmediately, deferStartApplication and registerCallback to a Bootstrapper TS module; the Java methods delegate, passing the $entry-wrapped doStartApplication/startApplication callbacks down. vaadinBootstrapLoaded stays in Java because its $wnd.Vaadin.Flow check is circular with registerInternals.
…peScript Move the $wnd.Vaadin.Flow.getApp lookup to the Bootstrapper TS module; the Java method delegates. This is the last clean leaf of the bootstrap/config cluster; vaadinBootstrapLoaded stays Java (circular with registerInternals).
…ypeScript Move the last config reader to TS. GWT represents java.lang.Integer as a JS number at runtime, so the TS function returns the number (or null) and the Java native hands it back as the boxed Integer the callers expect. Validated by the it-tests, which read UI_ID and heartbeatInterval on every app boot.
…ger to TypeScript" This reverts commit 55f22eb.
Move the self-contained profiler leaves (getPerformanceTiming, getGwtStatsEvents, clearEventsList, hasHighPrecisionTime, round) to a Profiler TS module; the Java methods delegate. The GwtStatsEvent JSO accessors, the __gwtStatsEvent logger setup and the RelativeTimeSupplier getRelativeTime implementations stay in Java for now.
Move ensureLogger and ensureNoLogger (the __gwtStatsEvent collecting-logger install/teardown) to the Profiler TS module; the Java methods delegate. The logGwtEvent reporter (GWT/EVT_GROUP refs), the GwtStatsEvent JSO accessors and the RelativeTimeSupplier getRelativeTime impls stay in Java for now.
Move the __gwtStatsEvent reporter to the Profiler TS module; the Java method passes the Java-computed values (EVT_GROUP, GWT.getModuleName(), getRelativeTimeMillis()) down and TS builds the event object. The GwtStatsEvent JSO accessors and the RelativeTimeSupplier getRelativeTime impls remain in Java.
…e the Java class
LocationParser had no Java callers, so port its getParameter query-string
parser to a standalone TS module (matching Java's split("=", 2) first-=
semantics via indexOf) with a mocha test, and delete LocationParser.java and
its JUnit test. First Java class fully removed from flow-client (106 -> 105
main Java files).
Move pushArray, spliceArray and clear (the per-mutation bulk array ops, using spread) to a JsArray TS module; the JsniHelper methods delegate. The per-element getValueNative/setValueNative stay in Java (hottest path).
…ewrite plan Record why the import-direction pattern (TS impl + Java native delegation) is exhausted: it only fits JSNI classes whose bodies are JS logic, all now migrated. The remaining ~78 pure-Java files form OO connected components (abstract Computation/ReactiveEventRouter subclassed by node features, interfaces implemented in Java, Reactive<->Computation mutual recursion) that cannot migrate incrementally -- the reactive cluster alone is a ~20-30 file all-or-nothing component. Add a bottom-up staged plan (collections -> reactive -> node features -> state tree -> binding) for the wholesale core rewrite.
…t -> callback) Make Computation a concrete class that runs an injected recompute Command, instead of an abstract doRecompute() that callers override. The one production subclass (Reactive.runWhenDependenciesChange) now uses the callback constructor. A protected no-arg constructor still supports overriding doRecompute (the CountingComputation test helper). First decoupling step toward migrating the reactive component to TypeScript: removes the abstract-subclass blocker from production so Computation can later move behind a thin shell. Validated by ComputationTest and ReactiveTest (JVM).
…(composition) Make ReactiveEventRouter a concrete class taking its wrap/dispatch behavior as constructor callbacks (nested ListenerWrapper/EventDispatcher functional interfaces) instead of abstract methods. MapProperty, NodeMap and NodeList now instantiate it with lambdas instead of anonymous subclasses; the TestReactiveEventRouter helper likewise. Second decoupling step toward a TS reactive layer: removes the abstract-generic-subclass blocker so the router can later move to TS with node features as plain consumers. Validated by MapPropertyTest/NodeListTest/NodeMapTest/ComputationTest/ReactiveTest (JVM).
…-cutover) Port the reactive core (Reactive, Computation, ReactiveEventRouter and the event/listener/value types) to a TypeScript module, mirroring the Java semantics, with mocha tests porting ReactiveTest + ComputationTest (17 cases). This intentionally coexists with the Java reactive package for now: per the agreed plan for the core rewrite, the TS implementation is built and validated in isolation first, then the consumers are wired to it and the Java package deleted in a cutover. No Java is touched, so the build stays green.
…utover) Port MapProperty and its change event/listener to TypeScript on top of the TS reactive core, with mocha tests porting the MapPropertyTest behaviour (value get/set, change events, reactive read tracking, removeValue, getValueOrDefault overloads, syncToServer active/inactive). The state-tree classes it touches (NodeMap/StateNode/StateTree) are not ported yet, so the slice it needs is declared as TS contracts the future node features will satisfy at cutover. Continues the build-alongside core rewrite; no Java touched, build stays green.
…-alongside) Port NodeFeature (base), NodeMap and NodeList plus their property-add and splice events/listeners to TypeScript on top of the TS reactive core and MapProperty, with mocha tests porting NodeMapTest (9) and NodeListTest (5). The StateNode slice the features need is a TS contract the future TS StateNode will satisfy at cutover. Continues the build-alongside state-tree chunk; no Java touched, build green.
…over) Port StateNode and its node-unregister event/listener to TypeScript on top of the TS node features, with mocha tests porting StateNodeTest (4). The Java Class<?>-keyed nodeData map becomes a map keyed by JS constructor function (object.constructor / getNodeData(clazz)). StateTree is Registry-coupled and not ported yet, so the slice StateNode and the node features need is a TS contract the future TS StateTree will satisfy at cutover. Continues the build-alongside state-tree chunk; no Java touched, build green.
Document the layers landed so far (reactive core, MapProperty, node features, StateNode) and the contract/cutover approach in MIGRATION_STRATEGY.md.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


No description provided.