Linkers & Bootstrap
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 and the Jakarta Servlet 6 stack of the modular-webapp archetype, checked on 2026-10-09.
The compiler ran on JDK 21 with -sourceLevel 17.
It is build and tooling knowledge with a runtime half: the linkers run inside the compiler on the developer machine or in CI, and the script they write, <module>.nocache.js, runs in the browser before any of your client code.
Nothing on this page is compiled from your Java source except the tiny entry points of the lab modules.
Every command, file listing, console message and DOM dump below comes from a private copy of the Bookshelf scenario of this section, extended with the lab modules printed on the page.
They were compiled with com.google.gwt.dev.Compiler of 2.13.1 and loaded in headless Chrome 155 from two static Python servers (one acting as the host page’s origin, one as a CDN).
Only Chrome was run: Firefox and Safari were not, and the Content Security Policy results in particular are Chrome results.
The 32-character content hashes that name the permutation files are shown as GECKO_HASH and SAFARI_HASH, and the nonces are placeholders (NONCE_PLACEHOLDER).
The compiler turns Java into JavaScript, but a browser cannot simply load "the" JavaScript of a GWT application, because there are several versions of it (one per permutation) and the right one depends on the browser and the locale. The linker is the stage of the compiler that decides how the compiled code is packaged and how it finds its way into the page. It writes the small bootstrap script that you put in your host page, it names and places the permutation files, and it writes the side files (policy files, symbol maps, reports). Most applications never think about it, because the default linker does the right thing. You need this page when you load a module from a CDN, put two modules on one page, tighten the Content Security Policy, produce a single file, or add a build step that must see every output file.
Where linkers sit in the build
The linker stage comes last in the compile: the compiler parses the Java, resolves deferred binding, compiles one JavaScript program per permutation (see Compiler and compilation), and then hands everything to the linkers.
The Developer Guide describes the subsystem like this: each output file is an artifact, and the linkers decide each file’s name, location and content.
The files are classified by EmittedArtifact.Visibility:
| Visibility | Meaning | Where it lands with -war, -deploy, -extra |
|---|---|---|
Public |
Files meant to be downloaded by browsers: the bootstrap, the permutation files, images and CSS of the public path. |
The module directory under |
Source |
Files served only during development: Java source and source maps for the browser debugger (the Developer Guide’s description). |
Not part of a production module directory |
Deploy |
Files the application server needs but browsers must not get, such as the GWT-RPC serialization policy manifest and the symbol maps. |
|
Private |
Optional reports about the compile, such as the compile report. |
|
Linkers belong to three categories, and the module XML names them:
-
A primary linker is the main one and writes the bootstrap. Only one primary linker runs per compile: adding a second primary linker with
<add-linker>replaces the first. -
Pre-linkers run before the primary linker, post-linkers run after it. Both categories are additive.
-
The category is declared on the linker class with
@LinkerOrder(Order.PRE),@LinkerOrder(Order.PRIMARY)or@LinkerOrder(Order.POST).
A linker has a short name that is defined once with <define-linker name="..." class="..."/> and switched on with <add-linker name="..."/>.
The Core module of 2.13.1 (inherited by User, so by every application) defines the standard ones and activates the default set:
<define-linker name="sso" class="com.google.gwt.core.linker.SingleScriptLinker" />
<define-linker name="std" class="com.google.gwt.core.linker.IFrameLinker" />
<inherits name="com.google.gwt.core.XSLinker" /> <!-- defines "xs" -->
<inherits name="com.google.gwt.core.CrossSiteIframeLinker" /> <!-- defines "xsiframe" and "direct_install" -->
<!-- std and xs linkers are deprecated and will emit a warning, which can be turned off -->
<define-configuration-property name="iframe.linker.suppressDeprecationWarning" is-multi-valued="false" />
<set-configuration-property name="iframe.linker.suppressDeprecationWarning" value="false" />
<define-linker name="soycReport" class="com.google.gwt.core.linker.SoycReportLinker" />
<define-linker name="symbolMaps" class="com.google.gwt.core.linker.SymbolMapsLinker" />
<add-linker name="xsiframe" />
<add-linker name="soycReport" />
<add-linker name="symbolMaps" />
That is the whole reason a plain module has the xsiframe linker: no line in your module asks for it.
The names xsiframe, direct_install, sso, std and xs are the available primary linkers, and the Developer Guide lists them in exactly that way.
The order in which linkers run
Running the lab module with -logLevel DEBUG shows the order in which 2.13.1 invokes the linkers for one permutation.
The extra Precache manifest line is the custom linker of Custom linkers:
Invoking Linker RPC policy file manifest
Invoking Linker Cross-Site-Iframe
Invoking Linker Precache manifest
Invoking Linker RPC log linker
Invoking Linker Export CompilationResult symbol maps
Invoking Linker Emit compile report artifacts
The first line is a pre-linker, the second is the primary linker, and the rest are post-linkers.
The same six lines are repeated for every permutation and once more for the final link after all permutations are compiled.
The Developer Guide says post-linkers run "in reverse lexical order of their name".
The 2.13.1 source and this log say otherwise: StandardLinkerContext collects the post-linkers in the order in which their add-linker elements were processed and then reverses that list ("Get all the post-linkers IN REVERSE ORDER"), and the Javadoc of Linker says the exact order is determined by the order of the add-linker tags.
A linker added last (here precacheManifest, added by the module that inherits it) therefore runs first among the post-linkers.
That matters when one post-linker reads the output of another: write the dependency into the order of your inherits and add-linker lines, and look at the DEBUG log to confirm it.
The bootstrap sequence
The file you reference from the host page, bookshelf/bookshelf.nocache.js, is the selection script.
It is written by the primary linker from a template (CrossSiteIframeTemplate.js for the default linker) into which the linker splices a set of helper scripts: the permutation lookup, the property providers, the base-URL computation and the install code.
Its job is small and specific: work out which compiled script this browser needs, load it into an isolated place in the page, and call onModuleLoad.
The numbers in the diagram correspond to what the template does, in this order (the helper names are the function names visible in a -style PRETTY build of bookshelf.nocache.js):
| Step | Function in the script | What it does |
|---|---|---|
1 |
|
Reports the first startup event to |
2 |
|
Reads the |
3 |
|
Finds the module base URL (the directory that holds the |
4 |
|
Runs the property providers, looks the values up in the permutation map that the linker embedded, and returns the file name |
5 |
the Super Dev Mode hook |
If the page is served over |
6 |
|
Adds one |
7 |
|
Loads the compiled code, the part that depends on |
The helper scripts are not private to the default linker.
CrossSiteIframeLinker has protected methods such as getJsInstallScript, getJsComputeScriptBase, getJsPermutations and getJsProcessMetas that name the resource to splice in, which is what makes Custom linkers practical.
Source: GWT 2.13.1 source — CrossSiteIframeTemplate.js, GWT — Organizing Projects: The Bootstrap Sequence
The files of the Bookshelf
Compiled with the default settings, the Bookshelf (two locales and two user agents, so six permutations, and one split point for the admin area) writes this module directory:
bookshelf/
bookshelf.nocache.js 7,847 bytes the selection script: revalidate on every request
<6 permutation hashes>.cache.js about 263 KB each, named by content hash
bookshelf.devmode.js 14,281 bytes used only by classic Dev Mode (gwt.codesvr)
compilation-mappings.txt 460 bytes which property values each cache.js serves
deferredjs/<hash>/1.cache.js ... code-split fragments (one directory per permutation)
<2 hashes>.gwt.rpc GWT-RPC serialization policy files
clear.cache.gif 43 bytes used to make a URL absolute (see computeScriptBase)
gwt/clean/... the public path of the Clean theme
The mapping file is the key to the sequence.
It lists, for each permutation file, the property values that select it, and the same table is compiled into the nocache.js as calls such as k([Zb,Ub],$b) in the obfuscated script.
A.cache.js
locale en
user.agent safari
B.cache.js
locale default
user.agent gecko1_8
C.cache.js
locale en
user.agent gecko1_8
(three more entries for locale default/en/es with safari/gecko1_8)
The sibling page on the compiler shows the permutation matrix and how -style changes it.
This page starts after that: it is about what the browser does with the files.
Watching the sequence: __gwtStatsEvent
The selection script calls window.__gwtStatsEvent(event) at every step, if the page defines that function before the script runs.
An event has moduleName, sessionId, subSystem, evtGroup, millis and type; __gwtStatsSessionId identifies the page load.
This is the supported hook for startup timing, and it is how the lab recorded the real sequence of the Bookshelf:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bookshelf</title>
<meta name="gwt:property" content="locale=en">
<script>
window.__gwtStatsSessionId = 'lab';
window.__gwtStats = [];
window.__gwtStatsEvent = function (e) {
window.__gwtStats.push(e.evtGroup + ':' + e.type);
return true;
};
</script>
</head>
<body>
<script src="bookshelf/bookshelf.nocache.js"></script>
</body>
</html>
Source: GWT — Lightweight Metrics
The events that arrived, in order, for the default (obfuscated) Bookshelf build:
bootstrap:begin
bootstrap:selectingPermutation
loadExternalRefs:begin
loadExternalRefs:end
bootstrap:end
moduleStartup:moduleRequested
moduleStartup:moduleEvalStart
moduleStartup:moduleEvalEnd
0:begin <- the RPC subsystem: the XSRF token request
0:requestSerialized
0:requestSent
moduleStartup:end <- gwtOnLoad has returned, onModuleLoad ran
0:responseReceived
0:responseDeserialized
0:end
moduleStartup:moduleRequested is the moment installScript starts the download and moduleEvalStart/moduleEvalEnd surround the evaluation of the compiled code in the iframe.
A build that installs the script with a src attribute (see the next section) adds one more event, moduleStartup:scriptTagAdded, when the script element has been added to the iframe.
The numbers you would report in a startup dashboard are the differences between these events; Compile reports and optimization is the place for the code-size side of the same story.
The host page the sequence assumes
A host page is any HTML that has a script element pointing at the nocache.js.
The selection script waits for document.body before it creates the iframe, so the tag may sit in the head.
Three details of the template are worth knowing:
-
The iframe gets the module name as its
id(scriptFrame.id = 'bookshelf'). Do not give another element that id. -
The iframe is positioned off screen (
left: -1000px) and not hidden withdisplay: none, because the comment ininstallLocationIframe.jssays some browsers do not run code in adisplay: noneframe. -
The page should be in standards mode (
<!doctype html>): the script readscompatModeto decide whether the frame it creates gets a doctype.
Here is the DOM of the Bookshelf (the direct_install build) after the module started, from a headless Chrome dump (trimmed), showing the extra elements the bootstrap and the module added:
<head>
<meta charset="utf-8">
<title>Bookshelf</title>
<meta name="gwt:property" content="locale=en">
<link rel="stylesheet" href="http://127.0.0.1:9953/app-direct/bookshelf/gwt/clean/clean.css">
<style>.CFEWP3D-a-a{background-color:#1b4f72;...}</style> <!-- written by CssResource.ensureInjected() -->
</head>
<body>
<script src="bookshelf/bookshelf.nocache.js"></script>
<iframe id="bookshelf" tabindex="-1" style="position: absolute; width: 0px; height: 0px; border-width: medium; border-style: none; border-color: currentcolor; border-image: none; left: -1000px; top: -1000px;"></iframe>
<div style="position: absolute; inset: 0px;"> <!-- the RootLayoutPanel with the shell -->
...
The link element is the stylesheet from <stylesheet src> of the Clean theme, added by step 6; the style element is the injected CSS of the application’s CssResource.
Both matter for the Content Security Policy section.
The meta tags and the module base URL
The host page can configure the bootstrap with three meta names.
They are all read by processMetas(), they all accept a module prefix, and the values are used exactly as the source shows.
| Meta name | Content | Use |
|---|---|---|
|
|
Sets a deferred-binding property for this page load, for example |
|
The name of a global function, evaluated with |
Called when a property has a value that no permutation supports. The function receives the property name, the allowed values and the bad value. |
|
The name of a global function, evaluated with |
Called with the module name and an |
A module:: prefix limits a tag to one module: <meta name="loans::gwt:property" content="locale=es"> is read by the loans module and ignored by every other one.
The lab put two modules, shelf and loans, on one page and gave them different locales this way (see Several modules on one page).
Note that the content of the two function-valued metas is passed to eval: only a page author may write them, never put user data in a meta tag.
A broken load, and a bug in the default install path
Do the error hooks work?
The lab removed the permutation files of the badge module and loaded a host page that registers gwt:onLoadErrorFn:
<meta name="gwt:onLoadErrorFn" content="onBookshelfLoadError">
<script>
window.onBookshelfLoadError = function (moduleName, e) {
document.documentElement.setAttribute('data-load-error', moduleName + ': ' + e.message);
};
</script>
<script src="badge/badge.nocache.js"></script>
The result depends on the install path:
| Build | What the page observed |
|---|---|
|
|
Default xsiframe, obfuscated ( |
The handler was not called. The window reported |
The cause is in installScriptEarlyDownload.js of 2.13.1: the onerror handler of the download script builds its message with a variable code that exists only in another function, so the handler throws instead of calling __errFn.
The installScriptDirect.js variant defines the handler inside the function that has code, and it works.
Until that is fixed, do not rely on gwt:onLoadErrorFn to report a failed download in a default build; watch for a missing moduleStartup:moduleEvalEnd event in __gwtStatsEvent (or a timeout) instead.
The cause was found in the source and confirmed by the run on 2.13.1; it was not checked against a newer release.
The Super Dev Mode hook
The default linker also emits the hook that makes Super Dev Mode work with the same host page.
When the page protocol is http: or file: and sessionStorage has an entry named __gwtDevModeHook:<module>, the script validates the stored URL against a whitelist and loads it instead of the compiled code.
The default whitelist, from CrossSiteIframeLinker, is http://(localhost|127\.0\.0\.1)(:\d+)?/.*; anything else is ignored and logged as Ignoring non-whitelisted Dev Mode URL: ....
Three configuration properties control the hook: devModeRedirectEnabled (default true), devModeUrlWhitelistRegexp and the protocol rule, which a custom linker can change by overriding getJsDevModeRedirectHookPermitted.
The Javadoc of the protocol hook recommends refusing the redirect on https and on host names that end users visit; see Development mode and debugging for the Super Dev Mode workflow and its security notes.
The query parameter ?gwt.codesvr= belongs to the classic Dev Mode and makes the script load <module>.devmode.js instead of a permutation; the classic plugin-based mode is legacy.
The primary linkers
The Developer Guide lists three current primary linkers and two deprecated ones.
The lab module Badge (one entry point that writes its module name, base URL and permutation into the page) was compiled with each of them.
Its source is this:
package com.example.bookshelf.linkers.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
/** Writes what the bootstrap handed over into the page. */
public class BadgeEntryPoint implements EntryPoint {
@Override
public void onModuleLoad() {
Element slot = Document.get().getElementById("badge-slot");
if (slot == null) {
slot = Document.get().getBody();
}
slot.setInnerText("module=" + GWT.getModuleName()
+ " base=" + GWT.getModuleBaseURL()
+ " permutation=" + GWT.getPermutationStrongName());
Document.get().setTitle("started:" + GWT.getModuleName());
}
}
<module rename-to="badge">
<inherits name="com.google.gwt.user.User" />
<source path="client" />
<entry-point class="com.example.bookshelf.linkers.client.BadgeEntryPoint" />
</module>
<!-- BadgeDirect.gwt.xml -->
<module rename-to="badge">
<inherits name="com.example.bookshelf.linkers.Badge" />
<add-linker name="direct_install" />
</module>
Source: GWT — Linkers: Available linkers
It has two permutations (the two user agents), which is why the sso linker will complain below.
The compile command was:
java -cp "$LAB_CLASSPATH" com.google.gwt.dev.Compiler -sourceLevel 17 \
-war out/direct -extra out/direct-extra com.example.bookshelf.linkers.BadgeDirect
Source: GWT — Command line tools
xsiframe: the default
xsiframe (class CrossSiteIframeLinker, description "Cross-Site-Iframe") "uses an iframe to hold the code and a script tag to download the code".
Because the code is fetched with a script element and never with XMLHttpRequest, the module directory can be on another origin (see Cross-site loading), and because the compiled code lives in its own iframe the module’s globals do not collide with the page’s or with another module’s, and code-split fragments can be installed into the same frame later.
How the code reaches the iframe is a choice of the property installCode, whose default is "is the output obfuscated":
| Compile | installCode default |
What the permutation file and the bootstrap do |
|---|---|---|
|
true |
The |
|
false |
The |
any style with |
false |
Same as the line above; the lab output was byte-identical to the |
The first characters of a Bookshelf-sized permutation file show the difference:
# default, installCode=true
badge.onScriptDownloaded(["var $wnd = $wnd || window.parent;var __gwtModuleFunction = $wnd.badge;var $sendStats = ...
# direct_install (or installCode=false)
var $wnd = $wnd || window.parent;var __gwtModuleFunction = $wnd.badge;var $sendStats = ...
The comment in installScriptEarlyDownload.js describes the mechanism: the download starts immediately, and the contents are installed into the iframe once the document body is ready.
The consequence that matters today is the second half of the table: inline script text is not allowed by a strict Content Security Policy.
That is why direct_install exists.
Two further properties of xsiframe are worth knowing.
First, it does not support <script src="..."/> elements in a *.gwt.xml file, because it has no place to put them in the host page: the compile fails unless you set xsiframe.failIfScriptTag to FALSE (see Pitfalls).
Second, it uses the ScriptTagLoadingStrategy for code splitting, because CrossSiteIframeLinker.gwt.xml switches the AsyncFragmentLoader.LoadingStrategy with <when-linker-added name="xsiframe"/>; the default strategy of every other linker, XhrLoadingStrategy, downloads fragments with XMLHttpRequest.
The <when-linker-added> condition was added in 2.9.0, according to the release notes.
direct_install
direct_install (class DirectInstallLinker) "adds a script tag directly to the iframe rather than downloading the JavaScript code as a list of strings"; its Javadoc says it is "exactly like CrossSiteIframeLinker except that it defaults to `installCode=false`".
You turn it on with one line in the module:
<add-linker name="direct_install" />
Because the primary linker is replaced, the other linkers (the policy manifest, symbol maps and so on) still run. The 2.5.1 release notes introduced the linker with the remark that applications built with it "should work in a page where inline scripts are forbidden", which was written for Chrome extensions and is exactly the Content Security Policy case. The lab confirms it for 2.13.1, with one exception (code splitting) that is the subject of Content Security Policy.
The price of the src install is visible in the cross-site section: the script element carries crossOrigin='' when the page has a host name, and so a module directory on another origin needs an Access-Control-Allow-Origin header.
sso: a single script
sso (class SingleScriptLinker, description "Single Script") writes one JavaScript file that contains both the bootstrap and the compiled code: no iframe and no separate permutation file.
That makes it the right choice for exactly one situation: a module that has one permutation and no split points, for example a small widget script that you want to ship as a single file to other sites.
Both conditions are checked at link time, and the lab hit both errors:
# BadgeSso: two user agents give two permutations
Invoking Linker Single Script
[ERROR] The module must have exactly one distinct permutation when using the Single Script Linker; found 2
# SplitSso: one permutation, one GWT.runAsync split point
[ERROR] The module must not have multiple fragments when using the Single Script Linker.
Invoking Linker Single Script
[ERROR] Failed to link
Both compiles exit with status 1. Fixing the first error means collapsing every property that has more than one value, in the module XML:
<module rename-to="badge">
<inherits name="com.example.bookshelf.linkers.Badge" />
<add-linker name="sso" />
<set-property name="user.agent" value="safari" />
</module>
The result is one file, badge.nocache.js, of 12,259 bytes (the whole program is in it), and nothing else in the module directory.
Fixing user.agent to one value is a trade: the code is compiled for that one value, so test the result in every browser you support.
Use sso for a module whose audience you control; for an application with locales, use xsiframe or direct_install.
std and xs: deprecated
std (IFrameLinker, "IFrame") and xs (XSLinker, "Cross-Site") are what the book’s loader chapter describes.
The release notes of 2.7.0 (RC1) deprecated both: "IFrameLinker and XSLinker are deprecated because they don’t work in Super Dev Mode".
Both still exist in 2.13.1, and both print a deprecation warning, three times for one compile:
[WARN] IFrame linker is deprecated; consider switching to the xsiframe linker
[WARN] Cross-Site linker is deprecated; consider switching to the xsiframe linker
The warning is controlled by the configuration property iframe.linker.suppressDeprecationWarning (default false), but suppressing it is not a fix.
What they write is also different from the default:
| Linker | Files | Notes |
|---|---|---|
|
|
Default. |
|
|
Readable output; |
|
|
Plain code in the permutation files. |
|
|
One permutation, no split points. |
|
|
The code is in HTML files loaded into an iframe; the fragment strategy is XHR. |
|
|
Cross-site compatible; sets |
Neither linker works with a strict Content Security Policy (see Content Security Policy), and std has an extra problem that the lab reproduced: the std bootstrap looks the iframe up with getElementById(<module name>), so a host page that has an element with the module name as its id fails before the application starts:
Uncaught TypeError: Cannot set properties of undefined (setting '__gwt_isKnownPropertyValue')
source: http://127.0.0.1:9953/std/badge/badge.nocache.js (2)
The same host page, with the same <div id="badge">, starts normally under xsiframe and xs, which only create the frame and never look it up.
The book’s gwt.js loader, module.nocache.html, .cache.html files and nocache-xs.js belong to this family or to an even older one; none of them is written by a 2.13.1 build with the default linker (see Where older material is outdated).
Choosing a linker
| Situation | Linker | Why |
|---|---|---|
A normal application with locales, split points and a server |
|
The default; it supports every feature of this section and works with Super Dev Mode, and the deprecation notes of the other linkers point to it. |
A host page with a strict CSP that cannot allow |
|
The bootstrap adds no inline script. See Content Security Policy for the exact policies that were measured. |
A module directory on a CDN you do not control |
|
The measured behaviour is in Cross-site loading. |
One script to hand to third parties |
|
One file, no iframe. |
A project that still uses |
Migrate to |
Deprecated since 2.7.0 and unsupported with Super Dev Mode. |
Cross-site loading
The book’s chapter on the bootstrap describes a separate "cross-site" loader, nocache-xs.js, for modules hosted on a different site from the host page.
With the default linker there is no second file: the ordinary nocache.js is cross-site capable, and the host page just uses an absolute URL.
The module base is computed from the script element’s src, so every other file (permutations, fragments, policy files, stylesheets) is requested from the same remote directory.
The lab served the host page from port 9953 and the module directory from a second server on port 8953, once without CORS headers and once with Access-Control-Allow-Origin: *:
<script src="http://127.0.0.1:8953/obf/badge/badge.nocache.js"></script>
| Build | Remote server without CORS | Remote server with CORS | Observation |
|---|---|---|---|
xsiframe, obfuscated |
starts |
starts |
|
|
fails |
starts |
|
The failing case is explained by installScriptDirect.js: if (location.host) { script.crossOrigin=''; }.
Setting crossorigin to the empty string requests the file in "anonymous" CORS mode, and the response must then carry an Access-Control-Allow-Origin header that allows the page’s origin.
The default build does not set the attribute, which is why the older cross-site advice ("the xs linker is for other sites") no longer applies to it.
Two more things cross sites with the module and are not solved by the linker.
RPC calls go to GWT.getModuleBaseURL() by default, so a remote module sends them to the remote origin; the server integration page has the measured CORS failure and the two ways out.
And a script loaded from another origin runs with the full authority of the host page that includes it, so load a module only from an origin you trust as much as your own server.
Several modules on one page
Two GWT modules can share a page, each with its own nocache.js script tag and each in its own iframe.
That is the scenario of a portal, a CMS or a legacy page into which two small GWT widgets are embedded.
The lab used two modules, shelf and loans, each with the locale property, and this host page:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>host</title>
<meta name="gwt:property" content="locale=en">
<meta name="loans::gwt:property" content="locale=es">
</head>
<body>
<div id="shelf-slot"></div>
<div id="loans-slot"></div>
<script src="shelf/shelf.nocache.js"></script>
<script src="loans/loans.nocache.js"></script>
</body>
</html>
Each entry point writes its own report into its slot:
public class ShelfEntryPoint implements EntryPoint {
@Override
public void onModuleLoad() {
Document.get().getElementById("shelf-slot").setInnerText(GWT.getModuleName()
+ " locale=" + LocaleInfo.getCurrentLocale().getLocaleName()
+ " base=" + GWT.getModuleBaseURL());
}
}
The page after loading:
shelf-slot -> shelf locale=en base=http://127.0.0.1:9953/multi/shelf/
loans-slot -> loans locale=es base=http://127.0.0.1:9953/multi/loans/
iframes: ['shelf', 'loans']
window.__gwt_activeModules keys: shelf,loans
The unprefixed gwt:property sets English for every module that does not have its own tag, and loans::gwt:property overrides it for the loans module only.
Removing the prefixed tag and setting the first one to Spanish gave locale=es in both.
Source: GWT 2.13.1 source — processMetas.js
The things that make this work and the things to watch:
-
The registry. Every module adds an entry to the page-wide
window.__gwt_activeModulesobject withmoduleName,moduleBase,bindingsandcanRedirect; Super Dev Mode reads it, and so can your monitoring. -
Names must differ. The module function (
window.shelf,window.loans) and the iframeidare derived from the module name. Give every module on a page a differentrename-toshort name, and never reuse the name as an element id. -
Stylesheets are deduplicated.
loadExternalStylesheets()records each URL inwindow.__gwt_stylesLoadedand adds alinkelement only once, so two modules that inherit the same theme share one CSS file. -
Code is not shared. Each module is compiled as a separate program with its own copy of every library class it uses. Measured with the lab modules, a
Counterpermutation (it uses theButtonwidget) is about 35.8 KB, aBadgepermutation about 10.7 KB, and one module that inherits both is about 36.0 KB, instead of about 46.4 KB for the two scripts. With a real widget library the duplication is in the hundreds of kilobytes. -
State is not shared. Static fields live in each module’s own iframe. If the modules must talk to each other, use the page: a JsInterop export (see JsInterop) or DOM events.
-
Properties are per module. A
localethat one module supports may be rejected by another; thegwt:onPropertyErrorFnmeta tag, which also accepts the module prefix, is the hook.
The alternative to several modules is one module with several entry points (<entry-point> elements, or an entry point that decides by looking at the page) and, for the code that is rarely needed, code splitting with GWT.runAsync, which Code splitting covers.
Choose several modules when the pieces are owned by different teams or released at different times; choose one when they share code.
Dynamic host pages
The host page does not have to be a static file.
The Developer Guide article on dynamic host pages shows a servlet that writes the page, and the reason is practical: the server can decide per request whether to include the nocache.js tag at all (only for a logged-in user, for instance), and it can put initial data into the page so that the module does not need a first RPC.
The article’s steps for a static host page also describe the bootstrap of its time, which included an MD5-named .cache.html file; with the default linker the file is .cache.js.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=UTF-8");
String nonce = Nonces.newNonce(); // SecureRandom, 16 bytes, Base64, per response
resp.setHeader("Content-Security-Policy",
"script-src 'nonce-" + nonce + "' 'strict-dynamic'; object-src 'none'; base-uri 'none'");
PrintWriter out = resp.getWriter();
out.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>Bookshelf</title>")
.append("<meta name=\"gwt:property\" content=\"locale=en\">")
.append("<script nonce=\"").append(nonce)
.append("\" src=\"bookshelf/bookshelf.nocache.js\"></script>")
.append("</head><body></body></html>");
}
This snippet was not compiled or run (the nonce mechanics were measured with static host pages, see Content Security Policy), and it leaves out the data block on purpose.
The article puts the user’s e-mail address into a second script element by string concatenation, which is a script-injection hole for any value that contains </script>; the server integration page has the corrected version (an inert application/json data block with escaping) and the client side that reads it with setText.
Nonces.newNonce() is the application’s own helper and not a GWT class; the nonce must come from a secure random generator and change on every response.
Custom linkers
When the standard output is not enough, you write your own linker.
The reasons are mostly build-side: listing the output files for a service worker or an offline cache, adding a file such as a version stamp or a hash manifest, stripping files that must not be deployed, pre-compressing, or changing how the bootstrap works.
The Developer Guide gives the recipe in four sentences, and the Javadoc of Linker fills in the rest:
-
Extend
com.google.gwt.core.ext.Linkeror, usually,com.google.gwt.core.ext.linker.AbstractLinker, which adds theemitString,emitBytesandemitInputStreamhelpers. -
Annotate the class with
@LinkerOrder(Order.PRE | PRIMARY | POST). -
Annotate it with
@Shardable. Non-shardable linkers are deprecated according to the Javadoc, and a shardable linker implementslink(logger, context, artifacts, onePermutation), which is called once per compiled permutation (withonePermutationtrue) and once more after all of them (withonePermutationfalse, and only the artifacts that are@Transferable). -
Register it with
<define-linker name="short_name" class="fully.qualified.Class"/>and switch it on with<add-linker name="short_name"/>. The name must be a valid Java identifier. A seconddefine-linkerwith the same name redefines the linker.
A precache manifest linker
The Bookshelf wants an offline shell, and a service worker needs the list of files to precache.
The module directory contains hashed permutation files, so the list changes with every build and must be generated.
This linker is a post-linker that waits for the final link, collects every public file, and writes <module>.precache.json:
package com.example.bookshelf.linker;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.Shardable;
import java.io.IOException;
import java.io.InputStream;
import java.util.TreeMap;
/** Post-linker that lists the public files of a module so a service worker can precache them. */
@LinkerOrder(Order.POST)
@Shardable
public class PrecacheManifestLinker extends AbstractLinker {
@Override
public String getDescription() {
return "Precache manifest";
}
@Override
public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts,
boolean onePermutation) throws UnableToCompleteException {
ArtifactSet result = new ArtifactSet(artifacts);
if (onePermutation) {
// Per-permutation call: the nocache.js bootstrap does not exist yet.
return result;
}
TreeMap<String, Long> files = new TreeMap<>();
for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
if (artifact.getVisibility() == Visibility.Public) {
files.put(artifact.getPartialPath(), sizeOf(logger, artifact));
}
}
StringBuilder json = new StringBuilder("{\n \"module\": \"")
.append(context.getModuleName()).append("\",\n \"files\": [\n");
String separator = "";
for (var entry : files.entrySet()) {
json.append(separator).append(" {\"path\": \"").append(entry.getKey())
.append("\", \"bytes\": ").append(entry.getValue()).append('}');
separator = ",\n";
}
json.append("\n ]\n}\n");
result.add(emitString(logger, json.toString(), context.getModuleName() + ".precache.json"));
return result;
}
private static long sizeOf(TreeLogger logger, EmittedArtifact artifact)
throws UnableToCompleteException {
try (InputStream in = artifact.getContents(logger)) {
return in.readAllBytes().length;
} catch (IOException e) {
logger.log(TreeLogger.ERROR, "Cannot read " + artifact.getPartialPath(), e);
throw new UnableToCompleteException();
}
}
}
The linker class is not client code.
It must not be inside a <source path> of any module (the compiler would try to translate it), it needs gwt-dev on its compile classpath, and it must be on the classpath of the compiler when the module is built.
In a Maven project the simplest layout is a separate *-linker module (or a library jar) that the gwt-app module depends on; the lab compiled the class with javac --release 17 against gwt-dev and put the output directory on the compiler classpath.
The registration lives in a small module that applications inherit:
<module>
<define-linker name="precacheManifest" class="com.example.bookshelf.linker.PrecacheManifestLinker" />
<add-linker name="precacheManifest" />
</module>
The application module then only needs <inherits name="com.example.bookshelf.linkers.PrecacheManifest"/>.
Compiled with the Badge module, the module directory gained badge.precache.json:
{
"module": "badge",
"files": [
{"path": "GECKO_HASH.cache.js", "bytes": 10667},
{"path": "SAFARI_HASH.cache.js", "bytes": 10665},
{"path": "badge.devmode.js", "bytes": 14261},
{"path": "badge.nocache.js", "bytes": 6623},
{"path": "clear.cache.gif", "bytes": 43},
{"path": "compilation-mappings.txt", "bytes": 142}
]
}
What the run shows, and what a real implementation must handle:
-
The file is a public artifact, so it is written next to the
nocache.js; paths are relative to the module directory. -
The list contains files a browser does not need (
badge.devmode.js,compilation-mappings.txt) and not the files of other kinds (code-split fragments appear underdeferredjs/in a module that has split points). A production linker should filter by name, and should fail the build on a surprise, not skip it silently. -
The service worker itself is not generated here. It must be served from a path that covers the pages it controls (usually the application root, not the module directory), and the
nocache.jsmust never be precached, because the point of its name is that it is always revalidated: precache it only with a "network first" strategy, or exclude it and cache the hashed files only. GWT 2.13.1 has no application-cache linker; that mechanism was removed from browsers, and a service worker with a generated manifest is the replacement. -
The linker runs inside the compile, so it can read the artifacts' real bytes (as here) to compute sizes or integrity hashes. Do not put secrets or build-machine paths into a public artifact.
Linkers that ship with GWT
Besides the primary linkers, GWT ships a few more that applications switch on by inheriting a module. The Developer Guide separates them into "internal" linkers that applications do not name, and the pre-compression linker that they do:
| Name | Added by | What it does |
|---|---|---|
|
|
Writes |
|
|
Emits the RPC log artifacts. |
|
|
Turns the compile report files into private artifacts. The reports themselves are off unless the compiler runs with |
|
|
Writes the symbol maps as deploy artifacts (in the lab: |
|
|
Writes a |
The precompress linker was run on the lab module.
The module directory afterwards:
10667 GECKO_HASH.cache.js
4312 GECKO_HASH.cache.js.gz
10665 SAFARI_HASH.cache.js
4311 SAFARI_HASH.cache.js.gz
14261 badge.devmode.js
4419 badge.devmode.js.gz
6623 badge.nocache.js
2909 badge.nocache.js.gz
43 clear.cache.gif
142 compilation-mappings.txt
A web server that supports precompressed files (Server integration and deployment has the caching and compression rules) can serve the .gz files with Content-Encoding: gzip and skip compressing at request time.
Customising the default linker without writing a new one
CrossSiteIframeLinker reads configuration properties that a module can set, which is lighter than a subclass.
Each one was checked in the 2.13.1 source:
| Property | Default | Effect |
|---|---|---|
|
|
With |
|
true for obfuscated output, false otherwise ( |
See the table in the |
|
chosen from |
Name of the classpath resource spliced in as the install script. If you change it, keep |
|
|
Replaces the base-URL algorithm, for example to read the base from a global variable that a portal sets. |
|
true, localhost only |
The Super Dev Mode hook (see above). |
|
|
|
A subclass can override more: getJsInstallLocation (where the iframe goes), getJsModuleFunctionErrorCatch (wrap the bootstrap in a try/catch and run your code on failure), shouldUseSelfForWindowAndDocument (the Javadoc says it is for worker threads), and shouldIncludeBootstrapInPrimaryFragment.
Content Security Policy
A Content Security Policy (CSP) tells the browser which scripts it may run.
The GWT bootstrap is a good test of a policy, because it creates scripts at run time in two ways (a script element with src, and a script element with inline text) and it does so in an iframe that inherits the page’s policy.
The SafeHtml and XSS page measured policies against a lab module and found that script-src 'self' is enough to start it; the GWT-RPC page recorded a run in which the same policy stopped a module with an inline-script message.
Both are right, and this page explains why: they were compiled with different output styles.
The SafeHtml lab was compiled with -style PRETTY, for which installCode is false.
The RPC lab used the default obfuscated output, for which installCode is true and the bootstrap installs the code as inline script text.
The lab ran the same Badge module in headless Chrome 155 under five header policies and several builds.
A pass means that the module started (the title became started:badge and the entry point wrote into the page).
| Build | script-src 'self' |
script-src 'self' 'unsafe-inline' |
script-src 'nonce-...' (nonce only) |
script-src 'nonce-...' 'strict-dynamic' |
|---|---|---|---|---|
xsiframe, obfuscated (default) |
fails |
starts |
fails |
starts |
xsiframe, |
starts |
starts |
fails |
starts |
|
starts |
starts |
fails |
starts |
xsiframe with |
starts |
not run |
fails |
starts |
|
starts |
not run |
starts |
starts |
|
fails |
not run |
fails |
fails |
|
fails |
not run |
fails |
fails |
In every policy the nonce, when present, was on both the probe script and the nocache.js tag, and the nonce value was a placeholder.
The messages behind the failures, with the hash of the inline script replaced:
# xsiframe obfuscated, script-src 'self'
Executing inline script violates the following Content Security Policy directive 'script-src 'self''.
Either the 'unsafe-inline' keyword, a hash ('sha256-INLINE_SCRIPT_HASH'), or a nonce ('nonce-...') is
required to enable inline execution. The action has been blocked. source: .../badge.nocache.js (9)
# any build, script-src 'nonce-NONCE_PLACEHOLDER' only
Loading the script 'http://127.0.0.1:9953/obf/badge/GECKO_HASH.cache.js' violates the following
Content Security Policy directive: "script-src 'nonce-NONCE_PLACEHOLDER'". Note that 'script-src-elem'
was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked.
The two failures have different causes, and both follow from the sequence earlier on this page:
-
Inline text. The default
installScriptsetsscript.text, which is an inline script from the browser’s point of view. The only ways to allow it areunsafe-inline, a hash of every string (impossible, the code changes with every build), or a nonce combined withstrict-dynamic. Switching todirect_installremoves the problem at the source. -
The script element GWT creates. The permutation file is requested by a script element that the bootstrap creates (in the host page’s
head, or inside the iframe), and that element has no nonce. A nonce-only policy therefore blocks it, even though the host page’s ownnocache.jstag has the nonce.strict-dynamicsays: scripts created by a trusted (nonced) script are trusted too, which is the shape of a GWT bootstrap. This is also why the "nonce alone" row fails for every build exceptsso, which has no second script to load.
The sso row is the most CSP-friendly output, and xs and std are the worst: both of them rely on inline script in the frame, so even the strict-dynamic policy did not start them.
Code splitting needs more than script-src 'self'
A module with GWT.runAsync split points has a third script source, and it is an inline one for every xsiframe-family build, including direct_install.
A split fragment is wrapped by the linker as $wnd.<module>.runAsyncCallbackN("...the code as a string...") (CrossSiteIframeLinker.wrapDeferredFragment), and __installRunAsyncCode in runAsync.js installs the string as the text of a script element in the iframe.
The lab used a module with one split point, SplitEntryPoint:
public class SplitEntryPoint implements EntryPoint {
@Override
public void onModuleLoad() {
GWT.runAsync(new RunAsyncCallback() {
@Override
public void onFailure(Throwable reason) {
RootPanel.get().add(new Label("failed: " + reason.getMessage()));
}
@Override
public void onSuccess() {
RootPanel.get().add(new Label("admin area loaded"));
}
});
}
}
Source: MDN — Content Security Policy
| Build | script-src 'self' |
script-src 'nonce-...' 'strict-dynamic' |
|---|---|---|
xsiframe, obfuscated |
no |
yes |
|
no: the console shows |
yes |
So for an application that uses code splitting (the Bookshelf’s admin area does), script-src 'self' alone is not a complete policy even with direct_install: use a nonce with strict-dynamic, or accept unsafe-inline for scripts.
Application code that injects scripts has the same constraint.
ScriptInjector.fromString is inline text too, and the SafeHtml page measured that it copies the nonce of the first script element that has one; Code splitting covers the fragments themselves.
A policy for the Bookshelf
The measurements give a policy the Bookshelf can use with its split admin area, its XSRF-protected RPC and the default linker, where the server writes the nonce into the host page as in the dynamic host page example:
Content-Security-Policy:
script-src 'nonce-NONCE_PLACEHOLDER' 'strict-dynamic';
object-src 'none';
base-uri 'none';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
frame-ancestors 'self'
-
The nonce goes on the
nocache.jstag and on any inline script of the host page, is generated per response from a secure random generator, and is never a constant in a file. -
style-src 'unsafe-inline'is the SafeHtml page’s measured need:StyleInjectorandCssResource.ensureInjected()write inlinestyleelements and the module XML’s<stylesheet>adds alinkelement from the module base. -
RPC payloads in protocol version 7 are evaluated with
evaland need'unsafe-eval'; protocol version 8 (-Dgwt.rpc.version=8on the server) usesJSON.parseand does not. The GWT-RPC page has the measured case. -
default-srcwas left out. Set it only after you have listed what the application loads, as the SafeHtml page shows.
Run on the real Bookshelf module (the XSRF call failed because no server was running), the direct_install build put its shell into the page under script-src 'self' and under the nonce policy, and the obfuscated default build did so under the nonce policy and not under script-src 'self'.
The admin area’s fragment was not opened in that run; the split-point result above is from the single-split-point lab module.
The sibling GWT-specific CSP facts, already measured on the SafeHtml page, are not repeated here: the gwt.cspCompatModeEnabled binding property (default false) that makes widgets avoid javascript: targets, StyleInjector having no nonce support, and the 2.12.0 release note "Fix CSP issues in linkers and dev mode".
Worked example: the Bookshelf’s bootstrap
Putting the page together for the Bookshelf:
-
Linker. Keep the default
xsiframewhile the policy allowsunsafe-inlineor a nonce withstrict-dynamic. If the security team requires a policy withoutunsafe-inlineand withoutstrict-dynamic, switch the application module todirect_install, and note that the split admin area then cannot load (see the code splitting table above). -
Host page. Serve it from a servlet (the dynamic host page above) so it can carry a per-response nonce and the locale meta tag from the user’s profile, and serve it with a short cache time.
-
Files. Cache
bookshelf.nocache.jswith revalidation and the hashed files for a long time; the server integration page has the header table. Precompress with thePrecompressmodule if the server uses static.gzfiles. -
Offline. Add the precache manifest linker and a service worker that precaches the hashed files only.
-
Observability. Define
__gwtStatsEventin the host page to report the startup events, and a timeout that reports a module that never reachedmoduleStartup:end, becausegwt:onLoadErrorFndoes not fire for a failed download of the default build. -
Second module. If the Bookshelf’s reading list is embedded in a portal as a separate module, give it its own
rename-to, a prefixedgwt:propertytag, and its own nonce-carrying script tag.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
|
Collapse the properties with |
|
|
Remove the split points or use another linker |
|
A |
Put the script in the host page, or add |
|
|
Remove the |
|
An element has the module name as its |
Rename the element, or leave |
Console |
Default obfuscated |
|
Console |
A nonce-only policy: the script element GWT creates has no nonce |
Add |
|
A split fragment is installed as inline script text, in every xsiframe-family build |
Nonce with |
|
The install script sets |
Configure the header, or use the default |
|
A bug in |
Detect failures with |
Two modules on a page show the wrong locale |
One unprefixed |
Use |
A stale application after a deploy |
The |
Revalidate it on every request and cache only the hashed files for long (see Server integration and deployment) |
Recent changes
-
2.5.1 (2013):
DirectInstallLinkerappears in the release notes with the remark that applications built with it work on pages where inline scripts are forbidden. -
2.7.0 (RC1):
IFrameLinkerandXSLinkerare deprecated because they do not work in Super Dev Mode; Super Dev Mode becomes the default (the release notes say "Super Dev Mode is now the default"). -
2.9.0: the
<when-linker-added>element is added to the module XML. -
2.11.0: "Avoid document.write in Chrome with iframe linker" and "Avoid document write in single script linker loading" are listed.
-
2.12.0 (2024-10-29): "Fix CSP issues in linkers and dev mode, provide CSP workarounds for GWT-RPC payloads".
-
2.13.0 and 2.13.1: the release notes list no linker change; the behaviour on this page is that of 2.13.1.
Where older material is outdated
The loader chapter of "GWT in Action" describes a bootstrap in which a gwt.js script and a gwt:module meta tag load a module.nocache.html file, which loads one of several browser-specific *.cache.html files into a hidden iframe, with a separate module.nocache-xs.js for cross-site use.
The principle (a small bootstrap that picks the permutation, hashed files that can be cached forever) is the same.
The details have all changed:
| In the older material | In 2.13.1 |
|---|---|
|
Gone. Use a script element for |
|
|
One |
One |
|
Not written. The ordinary |
Browser permutations for |
|
Linkers |
Deprecated since 2.7.0. |
The dynamic host page article is also from the older period: it calls the module file MD5.cache.html, uses App Engine’s Users API, and writes data into a script block by concatenation; the example of reading it with JSNI is legacy as well (see the page on JSNI for the current alternatives).