Skip to content

Add Wicket Islands, an experimental module for JavaScript framework components - #1834

Open
papegaaij wants to merge 15 commits into
masterfrom
islands
Open

papegaaij wants to merge 15 commits into
masterfrom
islands

Conversation

@papegaaij

Copy link
Copy Markdown
Contributor

Draft for review: Wicket Islands, an experimental module (wicket-experimental/wicket-islands) for embedding JavaScript framework components in Wicket pages, React first. Wicket keeps owning pages, navigation and layout; an island is a component with a client-side peer.

What is in it

  • Core hook (d1f3b29): a partial page update can carry named data for client-side code, a new part of <ajax-response> handled by both Ajax engines.
  • Server side: Island<P> (props rendered as escaped JSON, mounted by the runtime), page-bound endpoints whose calls answer with the result plus Wicket's partial updates, failed calls answered as invalid / expired / forbidden, islands preserved across an ancestor's re-render, FormIsland<T,P> as a real FormComponent (conversion, validation, feedback).
  • Page-less endpoints: stateless, parallel, session-bound calls outside the page lock, with authorisation, and handles that give such a call page context without the page itself.
  • Loading: Wicket reads a Vite manifest from the classpath; in development mode islands load from the Vite dev server with hot reload. CSP strict mode works, and island modules get the nonce for styles they inject.
  • Client side (npm workspace in the module): the runtime (mount contract, page client store, TanStack Query with invalidation), the React adapter and a Vite plugin. No node build runs in the default Maven build.
  • Examples, built with -Pjs-test:
    • Treasure Hunt (/treasurehunt): page-bound calls, preserve vs. remount, a form island, page-less high scores.
    • Task Board (/taskboard): optimistic drag-and-drop that rolls back on a stale board or a WIP limit, a Wicket ModalDialog with rich-text and date-picker form islands, sanitized HTML (OWASP Java HTML Sanitizer), a client-only filter, a chart refreshed after a move.

Running the examples

mvn install -DskipTests -Pfast
cd wicket-examples
mvn -Pjs-test jetty:run

Then http://localhost:8080/treasurehunt and http://localhost:8080/taskboard.

Tests

  • Unit tests for the module (WicketTester) and the client runtime (Vitest), QUnit for the Ajax data part.
  • Selenium tests for both examples (both Ajax engines), run with -Pjs-test -Dwicket.selenium=true. Like VeilPageSeleniumTest they do not run in CI.
  • mvn clean verify -Pjs-test is green.

Open questions for the committers

  • Name. "Islands" is a working name.
  • npm under the ASF. Publishing the runtime and adapters needs a check with the PMC and infra.
  • Endpoint declaration style. Registration only, or @IslandCallable annotations as well; this shapes the planned TypeScript contract generation.
  • Handle store default. Session map (needs sticky sessions) or an encrypted token.
  • The released examples war. The release build does not activate -Pjs-test, so the war carries no JavaScript. Bundling it would need npm registry access in the release build and the third-party licence list in the war's LICENSE.

Not built yet

Typed contracts (TypeScript generated from the Java declarations), an IslandTester, store seeding, the JAX-RS integration, Vue and Angular adapters, websocket push.

🤖 Generated with Claude Code

papegaaij and others added 10 commits November 5, 2026 18:57
An Ajax response could carry components, header contributions and
scripts, but no plain data. Client-side code that needed a value from the
server had to receive it through an evaluated script, for example by
appending JavaScript that stores JSON somewhere for later pickup. That
mixes data with code, depends on where the script happens to run, and
offers no ordering guarantee relative to the rest of the response.

IPartialPageRequestHandler (and with it AjaxRequestTarget and the
websocket request handler) now has addData(name, value). Each entry is
written as a <data name="..."> element in the <ajax-response>. Both Ajax
libraries publish it on the new topic '/ajax/call/data' with the call's
attributes, the name and the value, after the components have been
replaced and the scripts evaluated, and before the success handlers run.
Wicket delivers the value as text and never interprets it.

This adds an abstract method to IPartialPageRequestHandler, implemented
in AbstractPartialPageRequestHandler, and an abstract writeData to
PartialPageUpdate, implemented in XmlPartialPageUpdate. Applications that
use these classes see no change. A custom implementation that does not
extend those base classes has to implement the new method. The data part
is the foundation for embedding JavaScript framework components, which
need a result channel for server calls that is separate from scripts.

Migration guide: "IPartialPageRequestHandler#addData(String,
CharSequence) and PartialPageUpdate#writeData(Response, String,
CharSequence) are new abstract methods. Implementations that do not
extend AbstractPartialPageRequestHandler or XmlPartialPageUpdate must
implement them."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omponents

Wicket has no supported way to embed components written for a client-side
framework such as React, Vue or Angular. Applications glue them in by hand:
state passed through data attributes or inline scripts, server calls
through custom behaviors, results returned through evaluated JavaScript.

The new experimental module wicket-islands makes such a component the
client-side peer of a Wicket component. An Island renders a host element
with its type, a key that identifies it across re-renders, and its state:
the props from its model as JSON, escaped for the script element they are
written into, and the Ajax attributes for calling back. Calls registered
with onCall run in the context of the component as Wicket Ajax requests,
so a handler can update other components through the request target; the
result, or a structured failure (invalid, forbidden, error), is returned
as a data entry of the Ajax response. Payloads are deserialized by a
pluggable serializer, Jackson by default, which rejects properties the
request type does not declare.

The client-side modules come from a Vite build: in production from its
manifest on the classpath, with the built files served by a resource that
serves only files listed in the manifest; in development optionally from
a Vite dev server.

The module is new and changes no existing API. The client-side runtime,
the React adapter and an example follow separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Islands

The wicket-islands module rendered island hosts and answered their calls,
but nothing in the browser mounted a framework component into a host or
made the calls.

An npm workspace in wicket-islands/npm now holds three packages. The
runtime finds island hosts, mounts the module registered for their type,
keeps a mounted island alive when Wicket re-renders it or an ancestor
(moving its root element into the new host and passing the new props),
unmounts islands whose host is gone, and sends calls as Wicket Ajax
requests whose promise settles after the response's other updates have
been applied. The React adapter turns a component into an island module
that receives the props as React props and keeps its state across
updates. The Vite plugin builds one entry per island type, named after
it, so the Java side finds it in the manifest, and serves the entries to
a Wicket application in development.

The packages are type-checked and tested with Vitest under the js-test
profile; the runtime tests run against the real wicket-ajax.js. The
build of the module is unchanged without the profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h and in dev mode

Running Wicket Islands in a real application, the Treasure Hunt example,
showed several defects.

An island call that could not reach its island was answered the way
Wicket answers any listener invocation in that situation: a redirect to
an error page or to a re-rendered page. The island's promise never
settled. The client now sends the island's key with each call, and the
endpoint answers 'expired' when it reaches a different island instance,
as on a page recreated after expiry; Island allows its listener to be
called after expiry so the check can run. A request cycle listener,
installed with the settings, answers island calls that fail earlier:
an expired, stale or removed page as 'expired', a call that is not
allowed or not authorized as 'forbidden'.

The URLs of island modules were rendered relative to the servlet
context, which misses the Wicket filter path; they are now generated
from the mounted resource reference. In development, the dev-server
loader renders Vite's csp-nonce meta tag, so the styles Vite injects
pass a nonce-based Content Security Policy.

The Vite plugin no longer imports vite at runtime, which failed when
the plugin was installed as a link. It gains an adapter option: island
modules export a plain component and the generated entry wraps it,
which keeps them eligible for React Fast Refresh. wicket-islands-react
provides the options as the reactIslands preset in
wicket-islands-react/vite (replacing wicket-islands-react/preamble),
and takes wicket-islands as a peer dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game board built with React, mounted as a Wicket island in an
otherwise plain Wicket page: probing a cell is an island call whose
response also updates Wicket labels, "new game" re-renders the panel
around the board while the board keeps running, and changing the board
size replaces the island. The board's props show the treasures only
once they are found or the game is over, as props are written into the
page.

The frontend lives in src/main/frontend and is built with Vite under
the js-test profile into a generated resource directory. Without the
profile the example shows that it was built without its JavaScript.
In development mode it can load the board from a Vite dev server
(-Dwicket.islands.devServer) for hot reload.

WellFormedXmlTestCase now skips node and node_modules directories,
which the frontend build creates under the examples' source tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Islands

Islands could render props and receive page-bound calls, but could not
share state with each other, take part in a Wicket form, or fetch data
without the page lock.

The client runtime keeps named client stores per page, shared by its
islands without involving the server. FormIsland is a form component
whose client-side peer edits its value: the value travels as JSON in a
hidden input, is converted with the island serializer (JSON that does not
fit the value type is a conversion error) and passes a sanitize hook, and
the peer receives the form state, including the component's feedback
messages; after an Ajax validation the island adds itself to the target.
Ajax behaviors on a form island serialize the hidden input. Page-less
endpoints are registered with the application together with a required
authorization, made callable by an island with usePageless, and served
outside the page lock to POST requests carrying the islands header from
the same origin. The runtime keeps a TanStack Query client per page and
applies the query keys the server invalidates, also from classic Ajax
requests through IslandTargets; the React adapter adds useStore,
useFormValue, useFormState, useIslandQuery and useIslandMutation.

Island and FormIsland share their island support; IslandCall#getIsland()
is replaced by getComponent(), as a call can come from either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scores

The probe history is a second island that shares the highlighted cell
with the board through a client store. When a game is over, a plain
Wicket form with a form island for the player's name and colour saves the
score; the high-score list is an island fetching the scores through a
page-less endpoint, refreshed when a score is saved.

Under the js-test profile, jetty:run also reads the generated Vite
output, so the example works even when target/classes is rebuilt without
it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Treasure Hunt example was only checked by hand in a browser, so
nothing in the build would notice the example breaking while the
islands module changes.

TreasureHuntSeleniumTest plays the game in a headless Chrome with either
Ajax engine: a probe updating the board, the history and the Wicket
labels; the hover shared between two islands; a new game keeping the
islands mounted and a new board size mounting a new board; a probe from
a stale tab answered as expired; and a score validated, saved with
markup in the name and listed through the page-less endpoint. Each test
also fails on browser errors, CSP violations included, and on an island
that outlived its host. Like the other Selenium tests it only runs with
-Dwicket.selenium=true, and it is skipped when the examples were built
without -Pjs-test. The status line and the probe count get a class to
find them by, because markup ids are generated in deployment mode.

The page explains which parts are islands and what each demonstrates,
and how to work on the React components with hot reload. The source
viewer lists the island modules and vite.config.js next to the Java
source, so both halves of the example can be read in one place.

The Vite build writes THIRD-PARTY-LICENSES.md next to the manifest,
listing the npm packages bundled into the war with their licences. It
is not served. The release build does not use -Pjs-test, so the
released war bundles none of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules the CSP nonce

A page-less endpoint could not know which record the page shows unless
the client told it, and anything the client sends can be changed: the
members of one board could be asked for another by editing an id.

An island now passes a context with usePageless(name, endpointId,
model). When the island renders, the context goes into the
application's IIslandHandleStore and the island state carries only an
opaque handle, which the runtime sends with each call in the
X-Wicket-Islands-Handle header. The endpoint reads the context with
PagelessCall#getHandle. The default SessionHandleStore keeps up to 1000
handles per session in the session's metadata, each 24 random bytes
from the application's secure random supplier and bound to the endpoint
it was issued for; issuing one binds a temporary session. A handle that
does not resolve (unknown, from another session, for another endpoint,
or pushed out by newer ones) is answered as expired before the endpoint
runs, and getHandle on a call without a handle rejects it as invalid.
An island keeps its handle while the context stays equal. A handle is
not an authorization: the endpoint still checks access to the context,
as the Javadoc says where the methods are.

The "pageless" entries of the island state change from a URL to an
object with the URL and an optional handle; the runtime reads the new
form. Nothing has been released, so no application is affected.

Component libraries that inject style elements, Mantine and TipTap among
them, need the page's CSP nonce, as Wicket's strict policy allows inline
styles only with it. Every island now renders the csp-nonce meta tag
that only the dev-server loader rendered before, and the runtime
exports cspNonce() to read it; the React adapter re-exports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second worked use case: a team board whose state lives in an
application-wide repository rather than in the page, with React islands
next to a plain Wicket summary, dialog and form.

- Cards are dragged between columns (dnd-kit). The board shows a move at
  once (React's useOptimistic) and calls the server, which checks the
  column's limit and the board's version. A refused move rolls back; a
  refusal because the board changed also carries the current board.
  "Let a teammate move a card" changes the repository behind the page's
  back to show that.
- A double click calls the island, which opens a plain Wicket
  ModalDialog. Its form mixes classic fields with two form islands, a
  Mantine rich-text editor and date picker, validated and submitted
  together. The description's HTML comes from the browser, so it goes
  through an allowlist sanitizer (OWASP Java HTML Sanitizer, added to the
  root pom's dependency management) before it reaches the card; a
  preview renders it unescaped only because of that.
- The filter writes a client store that the board reads. The assignee
  suggestions and the cards-per-week chart come from page-less endpoints
  that get the board as a handle and check that the session may see it.
  A move invalidates the chart's data.

Every session gets a board of its own, so a public examples site cannot
be defaced; two tabs of one browser share it. Mantine is used without
its CSS reset, which would restyle the Wicket page around the islands,
and gets the page's CSP nonce for the styles it injects. The npm
packages are pinned to versions published at least two weeks ago; all
that are bundled are MIT, ISC or 0BSD and listed in the Vite build's
THIRD-PARTY-LICENSES.md.

TaskBoardPageTest covers the calls and the form with WicketTester;
TaskBoardSeleniumTest uses the board in Chrome with either Ajax engine.
The browser plumbing it shares with TreasureHuntSeleniumTest moved into
IslandSeleniumTestCase.

The js-test profile no longer configures an extra classpath for
jetty:run. MavenWebAppContext has setExtraClasspath(String) and
setExtraClasspath(List<Resource>), and the configurator picked either,
so jetty:run failed now and then with "Cannot create instance of class
Resource". The Vite output reaches target/classes as a resource anyway.

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

@papegaaij papegaaij left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review of the branch as of 5be988d: the core <data> part of the Ajax response, the islands module (Java, client runtime, React adapter, Vite plugin) and both examples. Line-level findings are inline; this comment has the overview and what doesn't belong to a single line.

Worth fixing before anyone builds on it, in rough order:

  1. Islands that use the same query name share one cached result (react/src/index.tsx): the second island shows the first island's data.
  2. canCallListenerAfterExpiry() returns true for the whole component, so every Ajax behavior on an island or form island runs on a page Wicket recreated after expiry (Island.java, FormIsland.java).
  3. Any global Ajax call listener makes the island state invalid JSON, and then no island on the page mounts (IslandSupport.java).
  4. The Vite plugin leaves base at /, so imported assets, CSS url() and the CSS of lazily imported chunks point outside Wicket's mount (vite-plugin/src/index.js).
  5. A FormIsland re-renders itself after every Ajax validation; with an Ajax behavior on it, keystrokes typed during the round trip are lost and focus drops (FormIsland.java).
  6. One broken island host stops the islands after it from mounting (runtime.ts).
  7. A result that fails to serialize is answered as invalid and logged at DEBUG (IslandEndpointBehavior.java, PagelessEndpointResource.java).
  8. sanitize() receives null, contrary to its Javadoc (FormIsland.java).

Items 1, 2, 4 and 8 were reproduced with throwaway tests, which are not committed. Item 5 follows from the code and was not reproduced in a browser. A JSON null does not get past required: FormComponent#validate re-checks it after conversion.

Security. No exploitable vulnerability found. What holds:

  • The props JSON is escaped for its <script> element, and the runtime never evaluates or inserts what it receives.
  • Page-less endpoints accept only a POST with the custom header, and refuse a Sec-Fetch-Site other than same-origin.
  • Every page-less endpoint must declare its authorization.
  • Handles are 24 bytes from the secure random supplier, bound to the session and the endpoint.
  • Unknown JSON properties are rejected and no type information from the client is honoured.
  • Error detail is shown only in development mode.
  • Only files listed in the manifest are served.

Two points that don't sit on one line:

  • npm becomes part of the trusted base. Island bundles run with the page's privileges: a compromised dependency can invoke any listener on the page as the user and read the handles. The design notes treat island modules as trusted like markup, and that is the right model, but the documentation should spell out that this now includes the application's npm dependency tree. See also the inline comment on npm ci.
  • Jackson serializes every getter. Handing an entity to an island as props publishes all its fields and can trigger lazy loading of its relations. The Island Javadoc warns about it; a strict default (e.g. records only, or a mapper without auto-detected getters) would make the safe path the easy one.

What else will bite applications using islands, beyond the inline comments:

  • Inputs rendered inside an island that sits in a Wicket <form> take part in it: a named input is submitted with the form and can collide with a Wicket input name, Enter in an island input submits the form, and a <button> without a type is a submit button.
  • Client state is not part of page versions, and calls from a back-navigated tab reach that older page version, as for any Ajax behavior.
  • Handles live in the session, so clusters need sticky sessions or session replication.

Conventions. The Java follows Wicket's format, public API has Javadoc with @since, and the commit messages follow AGENTS.md. The React code is idiomatic (useSyncExternalStore, useOptimistic, default-exported components so Fast Refresh works). A few nits inline.

🤖 Generated with Claude Code

papegaaij and others added 4 commits November 10, 2026 10:06
The review of pull request #1834 found these, now fixed:

- Islands using the same query name shared one cache entry, so the
  second island showed the first island's data and never made its own
  call. Queries are now keyed [name, scope, payload]: the island for a
  page-bound call, the handle or the endpoint for a page-less one, so
  page-less results are still shared where they cannot differ. The
  server's invalidate(name) still reaches all of them, as a prefix.
- canCallListenerAfterExpiry() returned true for the whole island, so
  every Ajax behavior on an island or form island ran on a page Wicket
  recreated after expiry. It now returns true only while the island's
  endpoint behavior is being invoked; the key check still answers those
  calls as expired.
- The island state embedded the behavior's full Ajax attributes. Global
  call listeners, preconditions and dynamic parameters - the usual way to
  add a busy indicator - are rendered as functions, which made the state
  invalid JSON and stopped every island on the page from mounting. The
  state now carries the URL, method and channel only. Listeners added to
  every behavior therefore no longer apply to island calls; listeners
  subscribed through Wicket.Event still fire. An island without
  page-bound calls renders no Ajax attributes, and its endpoint behavior
  no longer makes the page stateful.
- The Vite plugin left base at "/", so imported assets, CSS url() and
  the CSS of lazily imported chunks pointed outside Wicket's mount. It
  now sets base "./" for builds unless the application sets one.
- A form island re-rendered itself after every Ajax validation. With an
  Ajax behavior on it, the re-render replaced the value with the
  request's, dropped the focus and lost what was typed during the round
  trip. It now sends its form state as a data entry, which the runtime
  applies without touching the value; a re-render the application asks
  for still replaces both.
- One broken island host stopped the hosts after it from mounting, and
  a module that failed to mount stayed registered. Each host is now
  mounted on its own, and a failure is logged with the host's id.
- A result that could not be serialized - a server bug - was answered
  as invalid and logged at DEBUG. It is now answered as error and logged
  at ERROR, for page-bound and page-less calls.
- sanitize() received null for the JSON text null, contrary to its
  Javadoc. It is skipped for null; required still applies.

And the smaller points:

- The island key now comes from the application's secure random
  supplier instead of ThreadLocalRandom: where no resource isolation
  listener is installed, it is what stops a cross-site POST from
  reaching a handler.
- A page-less call is aborted when its island unmounts, as page-bound
  calls are.
- An HTTP error from Wicket Ajax is reported as error (or forbidden for
  403), not as a network failure, and an answer that cannot be parsed as
  error.
- The page's QueryClient no longer refetches page-bound queries when the
  window gets the focus, and retries only network failures.
- IslandSettings.install() returns the installed settings when called
  again, instead of mounting and registering everything twice.
- IslandCallRejectedException drops null messages, so rejecting with the
  message of an exception that has none no longer throws.
- usePageless() fails at once for an endpoint id that is not registered.
- In development mode the Vite manifest is read again when it changes.
- The default JacksonIslandSerializer writes only records, collections,
  maps, arrays, enums and single values. It used to write every getter,
  so an entity handed to an island published all its fields and could
  load its relations; that now fails at render time.
  defaultMapperBuilder() keeps the rule for applications adding modules,
  and a mapper of their own opts out. Props that were plain classes need
  to become records.
- The Javadoc of Island now says that island modules, and with them the
  application's npm dependencies, are trusted like markup; what moving
  an island into a re-rendered host costs; that islands in repeaters
  recreating their items are remounted; how islands relate to page
  versions and to an enclosing form. SessionHandleStore says that
  handles need sticky sessions or session replication in a cluster.
- The conversion error of FormIsland has a default bundle entry,
  "The value of '${label}' is not valid."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ripts

The frontend-maven-plugin version and the Node and npm versions it
installs were repeated in the poms of wicket-js-tests, wicket-islands
and wicket-examples, where they would drift apart. The root pom now
manages the plugin, with the node.version and npm.version properties,
and the modules only say what to run.

The islands module and the examples ran npm ci with the install scripts
of every dependency, which on a CI or release machine is a supply-chain
exposure for little gain. Both now run npm ci --ignore-scripts; the
builds and tests need none of those scripts (esbuild and rollup ship
their binaries as optional dependencies). wicket-js-tests is unchanged.

Raised in the review of pull request #1834.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of pull request #1834 noted that examples get copied, and
found these in the task board:

- EditCardPanel captured a Board snapshot in its choice renderer and
  submit handler, so it was serialized with the page and its member list
  stayed as it was when the dialog opened. It now reads the board through
  the page's model and keeps only the card's id.
- The rich-text editor offered more than the sanitizer keeps (any
  heading level, horizontal rules, links with a target), which was
  dropped silently on save, and never showed a value the server changed.
  StarterKit now matches the allowlist, and the editor follows the value
  the server sends back.
- Opening a card took a double click; dnd-kit takes Enter and Space to
  pick a card up, so there was no keyboard path. Each card has an Edit
  button, whose keys do not reach the card's keyboard sensor.
- The assignee search sent a request per keystroke; it is debounced.

The description is also validated while the user types (at most 1000
characters of HTML), through an Ajax behavior on the form island. That
shows the form state reaching the editor without a re-render, and a new
Selenium test types during the round trips and checks that nothing is
lost and the editor keeps the focus.

The taller cards put the bottom of a column below the fold, where the
Selenium drags dropped, and dnd-kit's default collision detection picked
the target with the largest overlap rather than the one under the
pointer. The board now drops under the pointer, falling back to the
overlap for the keyboard, and the tests drop on the column title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@papegaaij
papegaaij marked this pull request as ready for review November 10, 2026 18:03
@papegaaij
papegaaij requested a review from dashorst November 10, 2026 18:05
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.72%. Comparing base (8902c1a) to head (6da04fc).

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #1834   +/-   ##
=========================================
  Coverage     62.71%   62.72%           
- Complexity    11512    11520    +8     
=========================================
  Files          1254     1254           
  Lines         48665    48690   +25     
  Branches       6845     6851    +6     
=========================================
+ Hits          30521    30541   +20     
- Misses        15468    15469    +1     
- Partials       2676     2680    +4     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

2 participants