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:

Table 1. Kinds of linker output
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 -war

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.

-deploy; the Maven plugin puts them in target/gwt/deploy (a WEB-INF/deploy directory appears under -war too when you run the compiler as in the lab)

Private

Optional reports about the compile, such as the compile report.

-extra

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:

The linker lines of Core.gwt.xml in 2.13.1
<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:

Linker invocation order (compiler log, trimmed)
   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.

sequenceDiagram autonumber participant B as Browser participant H as Host page participant N as bookshelf.nocache.js participant F as Hidden iframe participant S as Server or CDN B->>H: parse the host page H->>S: GET bookshelf.nocache.js (revalidated, never cached long) S-->>N: selection script N->>N: sendStats bootstrap begin, processMetas (gwt:property, gwt:onLoadErrorFn) N->>N: computeScriptBase (baseUrl meta, own script src, base element, page URL) N->>N: run property providers (locale, user.agent) and pick the strong name N->>H: add link elements for the module stylesheets alt installCode is true (default, obfuscated output) N->>S: GET STRONGNAME.cache.js through a script element in head S-->>N: module.onScriptDownloaded with an array of strings N->>F: wait for body, create the iframe, install each string as script text else installCode is false (direct_install, PRETTY, DETAILED) N->>F: wait for body, create the iframe, add a script element with src F->>S: GET STRONGNAME.cache.js end F->>F: evaluate the module, then gwtOnLoad F->>H: run every entry point (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):

Table 2. What the selection script does, in order
Step Function in the script What it does

1

sendStats('bootstrap', 'begin')

Reports the first startup event to window.__gwtStatsEvent, if the page defined one (see Watching the sequence: __gwtStatsEvent).

2

processMetas()

Reads the meta tags of the host page: gwt:property, gwt:onPropertyErrorFn and gwt:onLoadErrorFn. A meta name that carries another module’s prefix (other::gwt:property) is skipped, and this module’s prefix (bookshelf::) is removed first.

3

computeScriptBase()

Finds the module base URL (the directory that holds the nocache.js), in this order: a baseUrl entry from a gwt:property meta tag, the src of the script element whose URL contains <module>.nocache.js, the last base element, and as a last resort the directory of the page URL. Relative results are made absolute.

4

getCompiledCodeFilename()

Runs the property providers, looks the values up in the permutation map that the linker embedded, and returns the file name STRONGNAME.cache.js relative to the module base. A strong name may carry a :N suffix, which the script splits into the file name and a "soft permutation" number.

5

the Super Dev Mode hook

If the page is served over http: or file: and sessionStorage holds a Super Dev Mode URL for this module, the script loads that URL instead and stops (see The Super Dev Mode hook).

6

loadExternalStylesheets()

Adds one link rel="stylesheet" element to the head for every <stylesheet src="..."/> of the module, unless another module on the page already added the same URL.

7

sendStats('bootstrap', 'end') and installScript(filename)

Loads the compiled code, the part that depends on installCode: the next section.

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.

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:

The compiler output of the Bookshelf module
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.

compilation-mappings.txt of the Bookshelf (hashes shortened to a letter)
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:

statshost.html: a host page that records the startup events
<!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>

The events that arrived, in order, for the default (obfuscated) Bookshelf build:

Startup events of the Bookshelf, in the order received
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 with display: none, because the comment in installLocationIframe.js says some browsers do not run code in a display: none frame.

  • The page should be in standards mode (<!doctype html>): the script reads compatMode to 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:

The Bookshelf DOM after startup (trimmed)
<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.

Table 3. Meta tags understood by the selection script
Meta name Content Use

gwt:property

name=value (the text up to the first = is the name; without = the value is empty)

Sets a deferred-binding property for this page load, for example locale=en, or the special entry baseUrl=... that overrides the module base URL.

gwt:onPropertyErrorFn

The name of a global function, evaluated with eval

Called when a property has a value that no permutation supports. The function receives the property name, the allowed values and the bad value.

gwt:onLoadErrorFn

The name of a global function, evaluated with eval

Called with the module name and an Error when the compiled code cannot be loaded.

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:

A host page with a load-error handler
<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:

Table 4. Result of a missing permutation file in 2.13.1
Build What the page observed

direct_install (script element with src in the iframe)

onBookshelfLoadError was called: badge: Failed to load http://127.0.0.1:9953/broken-direct/badge/SAFARI_HASH.cache.js

Default xsiframe, obfuscated (installCode is true)

The handler was not called. The window reported Uncaught ReferenceError: code is not defined.

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:

BadgeEntryPoint.java: the lab module that every linker below was run with
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());
  }
}
Badge.gwt.xml and a variant that switches the linker
<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>

It has two permutations (the two user agents), which is why the sso linker will complain below. The compile command was:

Compiling a lab module with the compiler class
java -cp "$LAB_CLASSPATH" com.google.gwt.dev.Compiler -sourceLevel 17 \
  -war out/direct -extra out/direct-extra com.example.bookshelf.linkers.BadgeDirect

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":

Table 5. How xsiframe installs the compiled code
Compile installCode default What the permutation file and the bootstrap do

-style OBFUSCATED (the default)

true

The .cache.js file is a call module.onScriptDownloaded([ "...", "..." ]) with the program as an array of JavaScript strings. The bootstrap downloads it with a script element in the host page’s head and then installs each string as the text of a script element inside the iframe.

-style PRETTY or DETAILED

false

The .cache.js file is plain JavaScript. The bootstrap adds a script element with src to the iframe.

any style with <set-configuration-property name="installCode" value="false"/>

false

Same as the line above; the lab output was byte-identical to the direct_install build.

The first characters of a Bookshelf-sized permutation file show the difference:

The start of a permutation file, obfuscated and direct
# 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:

Switching to direct_install in the module XML
<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:

Two sso errors, reproduced
# 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:

BadgeSsoOne.gwt.xml: one permutation, so sso links
<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:

The deprecation warnings
   [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:

Table 6. The module directory of each primary linker (lab module with two permutations)
Linker Files Notes

xsiframe (obfuscated)

badge.nocache.js 6,623 B, two .cache.js of about 10.7 KB, badge.devmode.js, compilation-mappings.txt

Default.

xsiframe with -style PRETTY

badge.nocache.js 12,890 B, two .cache.js of about 24.6 KB

Readable output; installCode is false.

direct_install

badge.nocache.js 6,397 B, two .cache.js of about 10.5 KB, badge.devmode.js, compilation-mappings.txt

Plain code in the permutation files.

sso

badge.nocache.js 12,259 B only

One permutation, no split points.

std

badge.nocache.js 5,193 B, two .cache.html of about 9.9 KB, hosted.html

The code is in HTML files loaded into an iframe; the fragment strategy is XHR.

xs

badge.nocache.js 5,022 B, two .cache.js of about 9.9 KB

Cross-site compatible; sets compiler.predeclare.cross.fragment.references.

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:

std fails when an element has the module’s id
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

Table 7. Which primary linker for which job
Situation Linker Why

A normal application with locales, split points and a server

xsiframe (do nothing)

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 unsafe-inline

direct_install (or installCode=false), plus strict-dynamic if you use split points

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

xsiframe (obfuscated) for no CORS configuration, direct_install only if the CDN sends Access-Control-Allow-Origin

The measured behaviour is in Cross-site loading.

One script to hand to third parties

sso, with a collapsed permutation and no split points

One file, no iframe.

A project that still uses std or xs

Migrate to xsiframe

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: *:

A host page that loads the module from another origin
<script src="http://127.0.0.1:8953/obf/badge/badge.nocache.js"></script>
Table 8. Cross-site loading, measured
Build Remote server without CORS Remote server with CORS Observation

xsiframe, obfuscated

starts

starts

GWT.getModuleBaseURL() was http://127.0.0.1:8953/obf/badge/. The code is fetched by a script element without crossorigin, so no header is needed.

direct_install

fails

starts

Access to script at 'http://127.0.0.1:8953/direct/badge/GECKO_HASH.cache.js' from origin 'http://127.0.0.1:9953' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

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:

A host page for two modules, with per-module meta tags
<!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:

ShelfEntryPoint.java: the second module writes into its own 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:

Two modules, two locales, one page (DOM dump of headless Chrome)
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_activeModules object with moduleName, moduleBase, bindings and canRedirect; Super Dev Mode reads it, and so can your monitoring.

  • Names must differ. The module function (window.shelf, window.loans) and the iframe id are derived from the module name. Give every module on a page a different rename-to short name, and never reuse the name as an element id.

  • Stylesheets are deduplicated. loadExternalStylesheets() records each URL in window.__gwt_stylesLoaded and adds a link element 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 Counter permutation (it uses the Button widget) is about 35.8 KB, a Badge permutation 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 locale that one module supports may be rejected by another; the gwt:onPropertyErrorFn meta 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.

A servlet host page that keeps the bootstrap tag and passes data as inert JSON
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:

  1. Extend com.google.gwt.core.ext.Linker or, usually, com.google.gwt.core.ext.linker.AbstractLinker, which adds the emitString, emitBytes and emitInputStream helpers.

  2. Annotate the class with @LinkerOrder(Order.PRE | PRIMARY | POST).

  3. Annotate it with @Shardable. Non-shardable linkers are deprecated according to the Javadoc, and a shardable linker implements link(logger, context, artifacts, onePermutation), which is called once per compiled permutation (with onePermutation true) and once more after all of them (with onePermutation false, and only the artifacts that are @Transferable).

  4. 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 second define-linker with 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:

PrecacheManifestLinker.java: a post-linker that lists the public files
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:

PrecacheManifest.gwt.xml: define and add the linker
<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:

badge.precache.json as written by the linker (hashes replaced)
{
  "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 under deferredjs/ 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.js must 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:

Table 9. Pre- and post-linkers of 2.13.1
Name Added by What it does

rpcPolicyManifest (pre)

RemoteService.gwt.xml, so any module that can use GWT-RPC

Writes manifest.txt, a map from RPC proxy class to its serialization policy file.

rpcLog (post)

RemoteService.gwt.xml

Emits the RPC log artifacts.

soycReport (post)

Core.gwt.xml

Turns the compile report files into private artifacts. The reports themselves are off unless the compiler runs with -compileReport; the linker does nothing otherwise.

symbolMaps (post)

Core.gwt.xml

Writes the symbol maps as deploy artifacts (in the lab: WEB-INF/deploy/badge/symbolMaps/<hash>.symbolMap). The JUnit module redefines this name with its own class.

precompress (post)

<inherits name="com.google.gwt.precompress.Precompress"/>

Writes a .gz copy of the files matching precompress.path.regexes (default .*\.html, .*\.js, .*\.css); with precompress.leave.originals set to false it removes the originals.

The precompress linker was run on the lab module. The module directory afterwards:

The lab module with Precompress inherited
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:

Table 10. Configuration properties of the xsiframe family
Property Default Effect

xsiframe.failIfScriptTag

TRUE

With TRUE, a <script> element in any inherited *.gwt.xml fails the link; with FALSE the elements are ignored and logged (Ignoring the following script tags in the gwt.xml file).

installCode

true for obfuscated output, false otherwise (direct_install changes the default to false; an explicit value wins)

See the table in the xsiframe section.

installScriptJs

chosen from installCode

Name of the classpath resource spliced in as the install script. If you change it, keep installCode consistent or fragment loading breaks, as the Javadoc warns.

computeScriptBaseJs

computeScriptBase.js

Replaces the base-URL algorithm, for example to read the base from a global variable that a portal sets.

devModeRedirectEnabled, devModeUrlWhitelistRegexp

true, localhost only

The Super Dev Mode hook (see above).

includeSourceMapUrl

false

true appends the standard source map URL to the primary fragment; any other text is used as a template with __MODULE__, __FRAGMENT__ and __HASH__.

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).

Table 11. Which linker mode starts under which script policy (Chrome 155)
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, -style PRETTY

starts

starts

fails

starts

direct_install

starts

starts

fails

starts

xsiframe with installCode=false

starts

not run

fails

starts

sso (one permutation)

starts

not run

starts

starts

xs

fails

not run

fails

fails

std

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:

The two kinds of CSP failure
# 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 installScript sets script.text, which is an inline script from the browser’s point of view. The only ways to allow it are unsafe-inline, a hash of every string (impossible, the code changes with every build), or a nonce combined with strict-dynamic. Switching to direct_install removes 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 own nocache.js tag has the nonce. strict-dynamic says: 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 except sso, 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:

SplitEntryPoint.java: one split point
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"));
      }
    });
  }
}
Table 12. Code splitting under CSP: did the fragment load?
Build script-src 'self' script-src 'nonce-...' 'strict-dynamic'

xsiframe, obfuscated

no

yes

installCode=false (so also direct_install)

no: the console shows Executing inline script violates ... from the nocache.js

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:

A header for a Bookshelf host page with a nonce
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.js tag 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: StyleInjector and CssResource.ensureInjected() write inline style elements and the module XML’s <stylesheet> adds a link element from the module base.

  • RPC payloads in protocol version 7 are evaluated with eval and need 'unsafe-eval'; protocol version 8 (-Dgwt.rpc.version=8 on the server) uses JSON.parse and does not. The GWT-RPC page has the measured case.

  • default-src was 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:

  1. Linker. Keep the default xsiframe while the policy allows unsafe-inline or a nonce with strict-dynamic. If the security team requires a policy without unsafe-inline and without strict-dynamic, switch the application module to direct_install, and note that the split admin area then cannot load (see the code splitting table above).

  2. 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.

  3. Files. Cache bookshelf.nocache.js with revalidation and the hashed files for a long time; the server integration page has the header table. Precompress with the Precompress module if the server uses static .gz files.

  4. Offline. Add the precache manifest linker and a service worker that precaches the hashed files only.

  5. Observability. Define __gwtStatsEvent in the host page to report the startup events, and a timeout that reports a module that never reached moduleStartup:end, because gwt:onLoadErrorFn does not fire for a failed download of the default build.

  6. Second module. If the Bookshelf’s reading list is embedded in a portal as a separate module, give it its own rename-to, a prefixed gwt:property tag, and its own nonce-carrying script tag.

Pitfalls

Table 13. Linker and bootstrap problems with the messages that were reproduced
Symptom Cause Fix

[ERROR] The module must have exactly one distinct permutation when using the Single Script Linker; found 2

sso with a module that has more than one permutation (two user agents, or locales)

Collapse the properties with <set-property>, or use xsiframe

[ERROR] The module must not have multiple fragments when using the Single Script Linker.

sso with a GWT.runAsync split point

Remove the split points or use another linker

The Cross-Site-Iframe linker does not support <script> tags in the gwt.xml files ... and the compile fails

A <script src="..."/> element in the module or a module it inherits

Put the script in the host page, or add <set-configuration-property name='xsiframe.failIfScriptTag' value='FALSE'/>

[WARN] IFrame linker is deprecated; consider switching to the xsiframe linker (or Cross-Site linker)

std or xs is active

Remove the <add-linker> line

Uncaught TypeError: Cannot set properties of undefined (setting '__gwt_isKnownPropertyValue') with std

An element has the module name as its id, so the frame lookup returns the wrong element

Rename the element, or leave std

Console Executing inline script violates the following Content Security Policy directive 'script-src 'self'' from the nocache.js

Default obfuscated xsiframe installs the code as script text

direct_install, or installCode=false, or a nonce with strict-dynamic

Console Loading the script '...cache.js' violates the following Content Security Policy directive: "script-src 'nonce-...'"

A nonce-only policy: the script element GWT creates has no nonce

Add 'strict-dynamic', or switch to sso if the module qualifies

script-src 'self' passes at startup but the admin area never opens

A split fragment is installed as inline script text, in every xsiframe-family build

Nonce with strict-dynamic, or no split points

Access to script at '...cache.js' from origin '...' has been blocked by CORS policy with direct_install from a CDN

The install script sets crossOrigin='', so the CDN must send Access-Control-Allow-Origin

Configure the header, or use the default installCode build

Uncaught ReferenceError: code is not defined and gwt:onLoadErrorFn never called

A bug in installScriptEarlyDownload.js (the installCode=true path) in 2.13.1

Detect failures with __gwtStatsEvent and a timeout, or use a build that installs by src

Two modules on a page show the wrong locale

One unprefixed gwt:property tag applies to all modules

Use module::gwt:property tags for the module that differs

A stale application after a deploy

The nocache.js was cached, so the browser kept an old permutation map

Revalidate it on every request and cache only the hashed files for long (see Server integration and deployment)

Recent changes

  • 2.5.1 (2013): DirectInstallLinker appears in the release notes with the remark that applications built with it work on pages where inline scripts are forbidden.

  • 2.7.0 (RC1): IFrameLinker and XSLinker are 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:

Table 14. The book’s loader against GWT 2.13.1
In the older material In 2.13.1

gwt.js and <meta name="gwt:module" content="...">

Gone. Use a script element for <module>.nocache.js. The only gwt: meta names left are gwt:property, gwt:onPropertyErrorFn and gwt:onLoadErrorFn.

module.nocache.html

module.nocache.js, written by the default linker.

One *.cache.html file per browser, named by MD5

One *.cache.js file per permutation, named by a 32-character content hash. .cache.html exists only with the deprecated std linker.

module.nocache-xs.js for cross-site loading

Not written. The ordinary nocache.js loads cross-site (see the section on cross-site loading).

Browser permutations for ie6, opera, gecko

user.agent has gecko1_8 and safari only, as the permutation maps in this page show.

Linkers std and xs as the choice

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).