Second round of changes to implement WAR design.
http://code.google.com/p/google-web-toolkit/wiki/WAR_Design_1_6

Review by: bobv


git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4302 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/LinkerContext.java b/dev/core/src/com/google/gwt/core/ext/LinkerContext.java
index 120de31..7f1d37f 100644
--- a/dev/core/src/com/google/gwt/core/ext/LinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/LinkerContext.java
@@ -40,6 +40,13 @@
   String getModuleFunctionName();
 
   /**
+   * Returns the time at which the module being compiled was last modified. Can
+   * be used to set an appropriate timestamp on artifacts which depend solely on
+   * the module definition.
+   */
+  long getModuleLastModified();
+
+  /**
    * Returns the name of the module being compiled.
    */
   String getModuleName();
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/AbstractLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/AbstractLinker.java
index f2b3cd3..9f009e4 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/AbstractLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/AbstractLinker.java
@@ -36,12 +36,29 @@
    * @return an artifact that contains the given data
    * @throws UnableToCompleteException
    */
+  @SuppressWarnings("unused")
   protected final SyntheticArtifact emitBytes(TreeLogger logger, byte[] what,
       String partialPath) throws UnableToCompleteException {
     return new SyntheticArtifact(getClass(), partialPath, what);
   }
 
   /**
+   * A helper method to create an artifact from an array of bytes.
+   * 
+   * @param logger a TreeLogger
+   * @param what the data to emit
+   * @param partialPath the partial path of the resource
+   * @return an artifact that contains the given data
+   * @param lastModified the last modified time of the new artifact
+   * @throws UnableToCompleteException
+   */
+  @SuppressWarnings("unused")
+  protected final SyntheticArtifact emitBytes(TreeLogger logger, byte[] what,
+      String partialPath, long lastModified) throws UnableToCompleteException {
+    return new SyntheticArtifact(getClass(), partialPath, what, lastModified);
+  }
+
+  /**
    * A helper method to create an artifact to emit the contents of an
    * InputStream.
    * 
@@ -54,7 +71,25 @@
       InputStream what, String partialPath) throws UnableToCompleteException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     Util.copy(logger, what, out);
-    return new SyntheticArtifact(getClass(), partialPath, out.toByteArray());
+    return emitBytes(logger, out.toByteArray(), partialPath);
+  }
+
+  /**
+   * A helper method to create an artifact to emit the contents of an
+   * InputStream.
+   * 
+   * @param logger a TreeLogger
+   * @param what the source InputStream
+   * @param partialPath the partial path of the emitted resource
+   * @param lastModified the last modified time of the new artifact
+   * @return an artifact that contains the contents of the InputStream
+   */
+  protected final SyntheticArtifact emitInputStream(TreeLogger logger,
+      InputStream what, String partialPath, long lastModified)
+      throws UnableToCompleteException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    Util.copy(logger, what, out);
+    return emitBytes(logger, out.toByteArray(), partialPath, lastModified);
   }
 
   /**
@@ -71,6 +106,20 @@
   }
 
   /**
+   * A helper method to create an artifact to emit a String.
+   * 
+   * @param logger a TreeLogger
+   * @param what the contents of the Artifact to emit
+   * @param partialPath the partial path of the emitted resource
+   * @param lastModified the last modified time of the new artifact
+   * @return an artifact that contains the contents of the given String
+   */
+  protected final SyntheticArtifact emitString(TreeLogger logger, String what,
+      String partialPath, long lastModified) throws UnableToCompleteException {
+    return emitBytes(logger, Util.getBytes(what), partialPath, lastModified);
+  }
+
+  /**
    * A helper method to create an artifact from an array of bytes with a strong
    * name.
    * 
@@ -88,4 +137,24 @@
     String strongName = prefix + Util.computeStrongName(what) + suffix;
     return emitBytes(logger, what, strongName);
   }
+
+  /**
+   * A helper method to create an artifact from an array of bytes with a strong
+   * name.
+   * 
+   * @param logger a TreeLogger
+   * @param what the data to emit
+   * @param prefix a non-null string to prepend to the hash to determine the
+   *          Artifact's partial path
+   * @param suffix a non-null string to append to the hash to determine the
+   *          Artifact's partial path
+   * @param lastModified the last modified time of the new artifact
+   * @return an artifact that contains the given data
+   */
+  protected final SyntheticArtifact emitWithStrongName(TreeLogger logger,
+      byte[] what, String prefix, String suffix, long lastModified)
+      throws UnableToCompleteException {
+    String strongName = prefix + Util.computeStrongName(what) + suffix;
+    return emitBytes(logger, what, strongName, lastModified);
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java b/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
index dd9bafd..d9283d0 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
@@ -27,13 +27,19 @@
  */
 public class SyntheticArtifact extends EmittedArtifact {
   private final byte[] data;
-  private final long lastModified = System.currentTimeMillis();
+  private final long lastModified;
 
   SyntheticArtifact(Class<? extends Linker> linkerType, String partialPath,
       byte[] data) {
+    this(linkerType, partialPath, data, System.currentTimeMillis());
+  }
+
+  SyntheticArtifact(Class<? extends Linker> linkerType, String partialPath,
+      byte[] data, long lastModified) {
     super(linkerType, partialPath);
     assert data != null;
     this.data = data;
+    this.lastModified = lastModified;
   }
 
   @Override
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
index 71d941b..e475f84 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
@@ -117,10 +117,23 @@
       LinkerContext context, ArtifactSet artifacts)
       throws UnableToCompleteException {
     String selectionScript = generateSelectionScript(logger, context, artifacts);
-    byte[] selectionScriptBytes = Util.getBytes(context.optimizeJavaScript(
-        logger, selectionScript));
-    return emitBytes(logger, selectionScriptBytes, context.getModuleName()
-        + ".nocache.js");
+    selectionScript = context.optimizeJavaScript(logger, selectionScript);
+
+    /*
+     * Last modified is important to keep hosted mode refreses from clobbering
+     * web mode compiles. We set the timestamp on the hosted mode selection
+     * script to the same mod time as the module (to allow updates). For web
+     * mode, we just set it to now.
+     */
+    long lastModified;
+    if (artifacts.find(CompilationResult.class).size() == 0) {
+      lastModified = context.getModuleLastModified();
+    } else {
+      lastModified = System.currentTimeMillis();
+    }
+
+    return emitString(logger, selectionScript, context.getModuleName()
+        + ".nocache.js", lastModified);
   }
 
   protected String generatePropertyProvider(SelectionProperty prop) {
@@ -223,9 +236,10 @@
     if (startPos != -1) {
       StringBuffer text = new StringBuffer();
       if (compilations.size() == 0) {
-        // We'll see this when running in Hosted Mode. The way the selection
-        // templates are structured is such that this line won't be executed
-        text.append("strongName = null;");
+        // Hosted mode link.
+        text.append("alert('This module needs to be (re)compiled, "
+            + "please run a compile or use the Compile/Browse button in hosted mode');");
+        text.append("return;");
 
       } else if (compilations.size() == 1) {
         // Just one distinct compilation; no need to evaluate properties
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
index 04719cb..df24b42 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
@@ -109,6 +109,7 @@
   private final Map<Class<? extends Linker>, String> linkerShortNames = new HashMap<Class<? extends Linker>, String>();
 
   private final String moduleFunctionName;
+  private final long moduleLastModified;
   private final String moduleName;
 
   private final Map<String, StandardSelectionProperty> propertiesByName = new HashMap<String, StandardSelectionProperty>();
@@ -125,6 +126,7 @@
     this.jjsOptions = jjsOptions;
     this.moduleFunctionName = module.getFunctionName();
     this.moduleName = module.getName();
+    this.moduleLastModified = module.lastModified();
 
     // Sort the linkers into the order they should actually run.
     List<Class<? extends Linker>> sortedLinkers = new ArrayList<Class<? extends Linker>>();
@@ -295,6 +297,10 @@
     return moduleFunctionName;
   }
 
+  public long getModuleLastModified() {
+    return moduleLastModified;
+  }
+
   public String getModuleName() {
     return moduleName;
   }
@@ -442,11 +448,11 @@
         outFile = new File(outputPath, artifact.getPartialPath());
       }
 
-      // TODO(scottb): figure out how to do a clean.
-      // assert !outFile.exists() : "Attempted to overwrite " +
-      // outFile.getPath();
-      Util.copy(artifactLogger, artifact.getContents(artifactLogger), outFile);
-      outFile.setLastModified(artifact.getLastModified());
+      if (!outFile.exists()
+          || (outFile.lastModified() <= artifact.getLastModified())) {
+        Util.copy(artifactLogger, artifact.getContents(artifactLogger), outFile);
+        outFile.setLastModified(artifact.getLastModified());
+      }
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
index d02be07..aeec95e 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
@@ -17,7 +17,7 @@
 function gwtOnLoad(errFn, modName, modBase){
   $moduleName = modName;
   $moduleBase = modBase;
-  if (!external.gwtOnLoad(window, modName, "1.5")) {
+  if (!external.gwtOnLoad(window, modName, "1.6")) {
     if (errFn) {
       errFn(modName);
     }
@@ -29,7 +29,7 @@
 };
 
 window.onunload = function() {
-  external.gwtOnLoad(window, null, "1.5");
+  external.gwtOnLoad(window, null, "1.6");
 };
 
 window.__gwt_module_id = 0;
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index 89517d8..838093e 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -19,15 +19,18 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.LinkerOrder;
 import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.core.ext.linker.impl.HostedModeLinker;
 import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
 import com.google.gwt.dev.About;
 import com.google.gwt.dev.util.DefaultTextOutput;
-import com.google.gwt.dev.util.Util;
-import com.google.gwt.util.tools.Utility;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
 
 /**
  * Implements the canonical GWT bootstrap sequence that loads the GWT module in
@@ -48,8 +51,34 @@
     try {
       // Add hosted mode iframe contents
       // TODO move this into own impl package if HostedModeLinker goes away
-      String hostedHtml = Utility.getFileFromClassPath("com/google/gwt/core/ext/linker/impl/hosted.html");
-      toReturn.add(emitBytes(logger, Util.getBytes(hostedHtml), "hosted.html"));
+      URL resource = HostedModeLinker.class.getResource("hosted.html");
+      if (resource == null) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to find support resource 'hosted.html'");
+        throw new UnableToCompleteException();
+      }
+
+      final URLConnection connection = resource.openConnection();
+      // TODO: extract URLArtifact class?
+      EmittedArtifact hostedHtml = new EmittedArtifact(IFrameLinker.class,
+          "hosted.html") {
+        @Override
+        public InputStream getContents(TreeLogger logger)
+            throws UnableToCompleteException {
+          try {
+            return connection.getInputStream();
+          } catch (IOException e) {
+            logger.log(TreeLogger.ERROR, "Unable to copy support resource", e);
+            throw new UnableToCompleteException();
+          }
+        }
+
+        @Override
+        public long getLastModified() {
+          return connection.getLastModified();
+        }
+      };
+      toReturn.add(hostedHtml);
     } catch (IOException e) {
       logger.log(TreeLogger.ERROR, "Unable to copy support resource", e);
       throw new UnableToCompleteException();
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js b/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
index 1f35a26..d98c012 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
@@ -65,14 +65,16 @@
   // --------------- INTERNAL FUNCTIONS ---------------
 
   function isHostedMode() {
+    var result = false;
     try {
-      return ($wnd.external && $wnd.external.gwtOnLoad &&
+      result = ($wnd.external && $wnd.external.gwtOnLoad &&
           ($wnd.location.search.indexOf('gwt.hybrid') == -1));
     } catch (e) {
       // Defensive: some versions of IE7 reportedly can throw an exception
       // evaluating "external.gwtOnLoad".
-      return false;
     }
+    isHostedMode = function() { return result; };
+    return result;
   }
 
   // Called by onScriptLoad(), onInjectionDone(), and onload(). It causes
@@ -338,6 +340,17 @@
 
   // do it early for compile/browse rebasing
   computeScriptBase();
+
+  var strongName;
+  if (isHostedMode()) {
+    if ($wnd.external.initModule && $wnd.external.initModule('__MODULE_NAME__')) {
+      // Refresh the page to update this selection script!
+      $wnd.location.reload();
+      return;
+    }
+    strongName = "hosted.html?__MODULE_FUNC__";
+  }
+
   processMetas();
 
   // --------------- WINDOW ONLOAD HOOK ---------------
@@ -350,10 +363,7 @@
     type: 'selectingPermutation'
   });
 
-  var strongName;
-  if (isHostedMode()) {
-    strongName = "hosted.html?__MODULE_FUNC__";
-  } else {
+  if (!strongName) {
     try {
 // __PERMUTATIONS_BEGIN__
       // Permutation logic
diff --git a/dev/core/src/com/google/gwt/dev/GWTCompiler.java b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
index 93d16ab..b6e2646 100644
--- a/dev/core/src/com/google/gwt/dev/GWTCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
@@ -33,7 +33,10 @@
 
 /**
  * The main executable entry point for the GWT Java to JavaScript compiler.
+ * 
+ * @deprecated use {@link Compiler} instead
  */
+@Deprecated
 public class GWTCompiler {
 
   static final class ArgProcessor extends Precompile.ArgProcessor {
@@ -91,6 +94,9 @@
   }
 
   public static void main(String[] args) {
+    System.err.println("WARNING: '" + GWTCompiler.class.getName()
+        + "' is deprecated and will be removed in a future release.");
+    System.err.println("Use '" + Compiler.class.getName() + "' instead.");
     /*
      * NOTE: main always exits with a call to System.exit to terminate any
      * non-daemon threads that were started in Generators. Typically, this is to
@@ -119,7 +125,23 @@
     this.options = new GWTCompilerOptionsImpl(options);
   }
 
+  /**
+   * Compiles the set of modules specified in the options.
+   */
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
+    ModuleDef[] modules = new ModuleDef[options.getModuleNames().size()];
+    int i = 0;
+    for (String moduleName : options.getModuleNames()) {
+      modules[i++] = ModuleDefLoader.loadFromClassPath(logger, moduleName);
+    }
+    return run(logger, modules);
+  }
+
+  /**
+   * Compiles a specific set of modules.
+   */
+  public boolean run(TreeLogger logger, ModuleDef... modules)
+      throws UnableToCompleteException {
     PerfLogger.start("compile");
     boolean tempWorkDir = false;
     try {
@@ -128,8 +150,8 @@
         tempWorkDir = true;
       }
 
-      for (String moduleName : options.getModuleNames()) {
-        ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
+      for (ModuleDef module : modules) {
+        String moduleName = module.getName();
         File compilerWorkDir = options.getCompilerWorkDir(moduleName);
 
         if (options.isValidateOnly()) {
diff --git a/dev/core/src/com/google/gwt/dev/GWTShell.java b/dev/core/src/com/google/gwt/dev/GWTShell.java
index a83f85d..63c70b2 100644
--- a/dev/core/src/com/google/gwt/dev/GWTShell.java
+++ b/dev/core/src/com/google/gwt/dev/GWTShell.java
@@ -32,7 +32,10 @@
 
 /**
  * The main executable class for the hosted mode shell.
+ * 
+ * @deprecated use {@link HostedMode} instead
  */
+@Deprecated
 public class GWTShell extends HostedModeBase {
 
   /**
@@ -103,6 +106,9 @@
   }
 
   public static void main(String[] args) {
+    System.err.println("WARNING: '" + GWTShell.class.getName()
+        + "' is deprecated and will be removed in a future release.");
+    System.err.println("Use '" + HostedMode.class.getName() + "' instead.");
     /*
      * NOTE: main always exits with a call to System.exit to terminate any
      * non-daemon threads that were started in Generators. Typically, this is to
@@ -134,16 +140,21 @@
     this.options.copyFrom(options);
   }
 
+  @Override
+  protected void compile(TreeLogger logger) throws UnableToCompleteException {
+    throw new UnsupportedOperationException();
+  }
+
   /**
    * Compiles a logical module def. The caller can modify the specified module
    * def programmatically in some cases (this is needed for JUnit support, for
    * example).
    */
+  @Override
   protected void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException {
     LegacyCompilerOptions newOptions = new GWTCompilerOptionsImpl(options);
-    newOptions.addModuleName(moduleDef.getName());
-    new GWTCompiler(newOptions).run(logger);
+    new GWTCompiler(newOptions).run(logger, moduleDef);
   }
 
   @Override
@@ -172,6 +183,17 @@
   }
 
   @Override
+  protected boolean initModule(String moduleName) {
+    /*
+     * Not used in legacy mode due to GWTShellServlet playing this role.
+     * 
+     * TODO: something smarter here and actually make GWTShellServlet less
+     * magic?
+     */
+    return false;
+  }
+
+  @Override
   protected void shutDownServer() {
     // Stop the HTTP server.
     //
diff --git a/dev/core/src/com/google/gwt/dev/HostedMode.java b/dev/core/src/com/google/gwt/dev/HostedMode.java
index f19bf27..351e10b 100644
--- a/dev/core/src/com/google/gwt/dev/HostedMode.java
+++ b/dev/core/src/com/google/gwt/dev/HostedMode.java
@@ -18,11 +18,9 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.ArtifactSet;
-import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
 import com.google.gwt.dev.Compiler.CompilerOptionsImpl;
 import com.google.gwt.dev.cfg.ModuleDef;
-import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.shell.ArtifactAcceptor;
 import com.google.gwt.dev.shell.ServletContainer;
 import com.google.gwt.dev.shell.ServletContainerLauncher;
@@ -40,7 +38,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.BindException;
-import java.util.IdentityHashMap;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -109,18 +107,13 @@
       registerHandler(new ArgHandlerStartupURLs());
       registerHandler(new ArgHandlerWarDir(options));
       registerHandler(new ArgHandlerExtraDir(options));
-      registerHandler(new ArgHandlerWorkDirOptional(options) {
-        @Override
-        public String[] getDefaultArgs() {
-          return new String[] {"-workDir", "work"};
-        }
-      });
+      registerHandler(new ArgHandlerWorkDirOptional(options));
       registerHandler(new ArgHandlerModuleName(options));
     }
 
     @Override
     protected String getName() {
-      return GWTShell.class.getName();
+      return HostedMode.class.getName();
     }
   }
 
@@ -141,8 +134,9 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    HostedMode shellMain = new HostedMode();
-    if (shellMain.new ArgProcessor().processArgs(args)) {
+    HostedMode hostedMode = new HostedMode();
+    if (hostedMode.new ArgProcessor().processArgs(args)) {
+      hostedMode.run();
       // Exit w/ success code.
       System.exit(0);
     }
@@ -165,7 +159,16 @@
    */
   private ServletContainerLauncher launcher = new JettyLauncher();
 
-  private final Map<ModuleDef, StandardLinkerContext> linkerMap = new IdentityHashMap<ModuleDef, StandardLinkerContext>();
+  /**
+   * Maps each active linker stack by module.
+   */
+  private final Map<String, StandardLinkerContext> linkerStacks = new HashMap<String, StandardLinkerContext>();
+
+  /**
+   * The set of specified modules by name; the keys represent the renamed name
+   * of each module rather than the canonical name.
+   */
+  private Map<String, ModuleDef> modulesByName = new HashMap<String, ModuleDef>();
 
   /**
    * The server that was started.
@@ -203,11 +206,15 @@
     return false;
   }
 
+  @Override
+  protected void compile(TreeLogger logger) throws UnableToCompleteException {
+    CompilerOptions newOptions = new CompilerOptionsImpl(options);
+    new Compiler(newOptions).run(logger);
+  }
+
   protected void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException {
-    CompilerOptions newOptions = new CompilerOptionsImpl(options);
-    newOptions.addModuleName(moduleDef.getName());
-    new Compiler(newOptions).run(logger);
+    throw new UnsupportedOperationException();
   }
 
   @Override
@@ -217,21 +224,51 @@
 
   @Override
   protected ArtifactAcceptor doCreateArtifactAcceptor(final ModuleDef module) {
-    final File moduleOutDir = new File(options.getWarDir(), module.getName());
     return new ArtifactAcceptor() {
       public void accept(TreeLogger logger, ArtifactSet newlyGeneratedArtifacts)
           throws UnableToCompleteException {
-        StandardLinkerContext linkerStack = linkerMap.get(module);
-        ArtifactSet artifacts = linkerStack.invokeRelink(logger,
-            newlyGeneratedArtifacts);
-        // TODO: extras
-        linkerStack.produceOutputDirectory(logger, artifacts, moduleOutDir,
-            null);
+        relink(logger, module, newlyGeneratedArtifacts);
       }
     };
   }
 
   @Override
+  protected boolean initModule(String moduleName) {
+    ModuleDef module = modulesByName.get(moduleName);
+    if (module == null) {
+      getTopLogger().log(
+          TreeLogger.WARN,
+          "Unknown module requested '"
+              + moduleName
+              + "'; all active GWT modules must be specified in the command line arguments");
+      return false;
+    }
+    try {
+      boolean shouldRefreshPage = false;
+      if (module.isGwtXmlFileStale()) {
+        shouldRefreshPage = true;
+        module = loadModule(getTopLogger(), module.getCanonicalName(), false);
+      }
+      link(getTopLogger(), module, true);
+      return shouldRefreshPage;
+    } catch (UnableToCompleteException e) {
+      // Already logged.
+      return false;
+    }
+  }
+
+  /*
+   * Overridden to keep our map up to date.
+   */
+  @Override
+  protected ModuleDef loadModule(TreeLogger logger, String moduleName,
+      boolean refresh) throws UnableToCompleteException {
+    ModuleDef module = super.loadModule(logger, moduleName, refresh);
+    modulesByName.put(module.getName(), module);
+    return module;
+  }
+
+  @Override
   protected void shutDownServer() {
     if (server != null) {
       try {
@@ -261,40 +298,20 @@
     }
 
     for (String moduleName : options.getModuleNames()) {
-      TreeLogger linkLogger = getTopLogger().branch(TreeLogger.DEBUG,
-          "Prelinking module " + moduleName);
+      TreeLogger loadLogger = getTopLogger().branch(TreeLogger.DEBUG,
+          "Loading module " + moduleName);
       try {
-        ModuleDef module = ModuleDefLoader.loadFromClassPath(linkLogger,
-            moduleName);
+        ModuleDef module = loadModule(loadLogger, moduleName, false);
 
         // TODO: Validate servlet tags.
         String[] servletPaths = module.getServletPaths();
         if (servletPaths.length > 0) {
-          linkLogger.log(TreeLogger.WARN,
+          loadLogger.log(TreeLogger.WARN,
               "Ignoring legacy <servlet> tag(s) in module '" + moduleName
                   + "'; add servlet tags to your web.xml instead");
         }
 
-        File moduleOutDir = new File(options.getWarDir(), moduleName);
-        StandardLinkerContext linkerStack = new StandardLinkerContext(
-            linkLogger, module, options);
-        linkerMap.put(module, linkerStack);
-
-        // TODO: remove all public files initially, only conditionally emit.
-        ArtifactSet artifacts = linkerStack.invokeLink(linkLogger);
-        for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
-          TreeLogger artifactLogger = linkLogger.branch(TreeLogger.DEBUG,
-              "Emitting resource " + artifact.getPartialPath(), null);
-
-          if (!artifact.isPrivate()) {
-            File outFile = new File(moduleOutDir, artifact.getPartialPath());
-            // if (!outFile.exists()) {
-            Util.copy(artifactLogger, artifact.getContents(artifactLogger),
-                outFile);
-            outFile.setLastModified(artifact.getLastModified());
-            // }
-          }
-        }
+        link(loadLogger, module, false);
       } catch (UnableToCompleteException e) {
         // Already logged.
         return -1;
@@ -317,4 +334,61 @@
     }
     return -1;
   }
+
+  /**
+   * Perform an initial hosted mode link, without overwriting newer or
+   * unmodified files in the output folder.
+   * 
+   * @param logger the logger to use
+   * @param module the module to link
+   * @param includePublicFiles if <code>true</code>, include public files in
+   *          the link, otherwise do not include them
+   * @throws UnableToCompleteException
+   */
+  private void link(TreeLogger logger, ModuleDef module,
+      boolean includePublicFiles) throws UnableToCompleteException {
+    // TODO: move the module-specific computations to a helper function.
+    File moduleOutDir = new File(options.getWarDir(), module.getName());
+    File moduleExtraDir = (options.getExtraDir() == null) ? null : new File(
+        options.getExtraDir(), module.getName());
+
+    // Create a new active linker stack for the fresh link.
+    StandardLinkerContext linkerStack = new StandardLinkerContext(logger,
+        module, options);
+    linkerStacks.put(module.getName(), linkerStack);
+
+    if (!includePublicFiles) {
+      linkerStack.getArtifacts().clear();
+    }
+
+    ArtifactSet artifacts = linkerStack.invokeLink(logger);
+    linkerStack.produceOutputDirectory(logger, artifacts, moduleOutDir,
+        moduleExtraDir);
+  }
+
+  /**
+   * Perform hosted mode relink when new artifacts are generated, without
+   * overwriting newer or unmodified files in the output folder.
+   * 
+   * @param logger the logger to use
+   * @param module the module to link
+   * @param newlyGeneratedArtifacts the set of new artifacts
+   * @throws UnableToCompleteException
+   */
+  private void relink(TreeLogger logger, ModuleDef module,
+      ArtifactSet newlyGeneratedArtifacts) throws UnableToCompleteException {
+    // TODO: move the module-specific computations to a helper function.
+    File moduleOutDir = new File(options.getWarDir(), module.getName());
+    File moduleExtraDir = (options.getExtraDir() == null) ? null : new File(
+        options.getExtraDir(), module.getName());
+
+    // Find the existing linker stack.
+    StandardLinkerContext linkerStack = linkerStacks.get(module.getName());
+    assert linkerStack != null;
+
+    ArtifactSet artifacts = linkerStack.invokeRelink(logger,
+        newlyGeneratedArtifacts);
+    linkerStack.produceOutputDirectory(logger, artifacts, moduleOutDir,
+        moduleExtraDir);
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/HostedModeBase.java b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
index f351ff8..1c5f4d4 100644
--- a/dev/core/src/com/google/gwt/dev/HostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
@@ -226,18 +226,22 @@
   }
 
   private class BrowserWidgetHostImpl implements BrowserWidgetHost {
-    public BrowserWidgetHostImpl() {
-    }
 
-    public void compile(ModuleDef moduleDef) throws UnableToCompleteException {
-      HostedModeBase.this.compile(getLogger(), moduleDef);
+    public void compile() throws UnableToCompleteException {
+      if (isLegacyMode()) {
+        throw new UnsupportedOperationException();
+      }
+      HostedModeBase.this.compile(getLogger());
     }
 
     public void compile(String[] moduleNames) throws UnableToCompleteException {
+      if (!isLegacyMode()) {
+        throw new UnsupportedOperationException();
+      }
       for (int i = 0; i < moduleNames.length; i++) {
         String moduleName = moduleNames[i];
-        ModuleDef moduleDef = loadModule(moduleName, getLogger());
-        compile(moduleDef);
+        ModuleDef moduleDef = loadModule(getLogger(), moduleName, true);
+        HostedModeBase.this.compile(getLogger(), moduleDef);
       }
     }
 
@@ -254,7 +258,7 @@
 
         // Try to find an existing loaded version of the module def.
         //
-        ModuleDef moduleDef = loadModule(moduleName, logger);
+        ModuleDef moduleDef = loadModule(logger, moduleName, true);
         assert (moduleDef != null);
 
         TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
@@ -271,6 +275,14 @@
       return getTopLogger();
     }
 
+    public boolean initModule(String moduleName) {
+      return HostedModeBase.this.initModule(moduleName);
+    }
+
+    public boolean isLegacyMode() {
+      return HostedModeBase.this instanceof GWTShell;
+    }
+
     public String normalizeURL(String whatTheUserTyped) {
       return HostedModeBase.this.normalizeURL(whatTheUserTyped);
     }
@@ -279,24 +291,6 @@
         throws UnableToCompleteException {
       return HostedModeBase.this.openNewBrowserWindow();
     }
-
-    /**
-     * Load a module.
-     * 
-     * @param moduleName name of the module to load
-     * @param logger TreeLogger to use
-     * @return the loaded module
-     * @throws UnableToCompleteException
-     */
-    private ModuleDef loadModule(String moduleName, TreeLogger logger)
-        throws UnableToCompleteException {
-      boolean assumeFresh = !alreadySeenModules.contains(moduleName);
-      ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger,
-          moduleName, !assumeFresh);
-      alreadySeenModules.add(moduleName);
-      assert (moduleDef != null) : "Required module state is absent";
-      return moduleDef;
-    }
   }
 
   static {
@@ -485,11 +479,18 @@
   }
 
   /**
-   * Compiles a module.
+   * Compiles a module (legacy only).
    */
+  @Deprecated
   protected abstract void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException;
 
+  /**
+   * Compiles all modules.
+   */
+  protected abstract void compile(TreeLogger logger)
+      throws UnableToCompleteException;
+
   protected abstract HostedModeBaseOptions createOptions();
 
   protected abstract ArtifactAcceptor doCreateArtifactAcceptor(ModuleDef module);
@@ -546,6 +547,18 @@
   }
 
   /**
+   * Called from a selection script as it begins to load in hosted mode. This
+   * triggers a hosted mode link, which might actually update the running
+   * selection script.
+   * 
+   * @param moduleName the module to link
+   * @return <code>true</code> if the selection script was overwritten; this
+   *         will trigger a full-page refresh by the calling (out of date)
+   *         selection script
+   */
+  protected abstract boolean initModule(String moduleName);
+
+  /**
    * By default we will open the application window.
    * 
    * @return true if we are running in headless mode
@@ -554,6 +567,25 @@
     return headlessMode;
   }
 
+  /**
+   * Load a module.
+   * 
+   * @param moduleName name of the module to load
+   * @param logger TreeLogger to use
+   * @param refresh if <code>true</code>, refresh the module from disk
+   * @return the loaded module
+   * @throws UnableToCompleteException
+   */
+  protected ModuleDef loadModule(TreeLogger logger, String moduleName,
+      boolean refresh) throws UnableToCompleteException {
+    refresh &= alreadySeenModules.contains(moduleName);
+    ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger, moduleName,
+        refresh);
+    alreadySeenModules.add(moduleName);
+    assert (moduleDef != null) : "Required module state is absent";
+    return moduleDef;
+  }
+
   protected boolean notDone() {
     if (!mainWnd.isDisposed()) {
       return true;
diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java
index a255423..1436f7f 100644
--- a/dev/core/src/com/google/gwt/dev/Link.java
+++ b/dev/core/src/com/google/gwt/dev/Link.java
@@ -189,6 +189,7 @@
   private static void doProduceOutput(TreeLogger logger, ArtifactSet artifacts,
       StandardLinkerContext linkerContext, ModuleDef module, File outDir,
       File extraDir) throws UnableToCompleteException {
+    // TODO: move the module-specific computations to a helper function.
     File moduleOutDir = new File(outDir, module.getName());
     File moduleExtraDir = (extraDir == null) ? null : new File(extraDir,
         module.getName());
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
index 8f66eaf..7c9fb94 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -351,14 +351,18 @@
   }
 
   public boolean isGwtXmlFileStale() {
+    return lastModified() > moduleDefCreationTime;
+  }
+
+  public long lastModified() {
+    long lastModified = moduleDefCreationTime;
     for (Iterator<File> iter = gwtXmlFiles.iterator(); iter.hasNext();) {
       File xmlFile = iter.next();
-      if ((!xmlFile.exists())
-          || (xmlFile.lastModified() > moduleDefCreationTime)) {
-        return true;
+      if (xmlFile.exists()) {
+        lastModified = Math.min(lastModified, xmlFile.lastModified());
       }
     }
-    return false;
+    return lastModified;
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
index 7d4b788..8ac02f1 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
@@ -93,7 +93,18 @@
       openWebModeButton = newItem("new-web-mode-window.gif", "&Compile/Browse",
           "Compiles and opens the current URL in the system browser");
       openWebModeButton.addSelectionListener(this);
-      openWebModeButton.setEnabled(false);
+      updateWebMode();
+    }
+
+    @Deprecated
+    public void updateWebMode() {
+      if (!openWebModeButton.isDisposed()) {
+        if (getHost().isLegacyMode()) {
+          openWebModeButton.setEnabled(!loadedModules.isEmpty());
+        } else {
+          openWebModeButton.setEnabled(true);
+        }
+      }
     }
 
     public void widgetDefaultSelected(SelectionEvent e) {
@@ -114,22 +125,26 @@
         browser.stop();
       } else if (evt.widget == openWebModeButton) {
         // first, compile
-        Set<String> keySet = new HashSet<String>();
-        for (Map.Entry<?, ModuleSpace> entry : loadedModules.entrySet()) {
-          ModuleSpace module = entry.getValue();
-          keySet.add(module.getModuleName());
-        }
-        String[] moduleNames = Util.toStringArray(keySet);
-        if (moduleNames.length == 0) {
-          // A latent problem with a module.
-          //
-          openWebModeButton.setEnabled(false);
-          return;
-        }
         try {
           Cursor waitCursor = getDisplay().getSystemCursor(SWT.CURSOR_WAIT);
           getShell().setCursor(waitCursor);
-          getHost().compile(moduleNames);
+          if (getHost().isLegacyMode()) {
+            Set<String> keySet = new HashSet<String>();
+            for (Map.Entry<?, ModuleSpace> entry : loadedModules.entrySet()) {
+              ModuleSpace module = entry.getValue();
+              keySet.add(module.getModuleName());
+            }
+            String[] moduleNames = Util.toStringArray(keySet);
+            if (moduleNames.length == 0) {
+              // A latent problem with a module.
+              //
+              openWebModeButton.setEnabled(false);
+              return;
+            }
+            getHost().compile(moduleNames);
+          } else {
+            getHost().compile();
+          }
         } catch (UnableToCompleteException e) {
           // Already logged by callee.
           //
@@ -154,9 +169,10 @@
   }
 
   /**
-   * The version number that should be passed into gwtOnLoad.
+   * The version number that should be passed into gwtOnLoad. Must match the
+   * version in hosted.html.
    */
-  private static final String EXPECTED_GWT_ONLOAD_VERSION = "1.5";
+  private static final String EXPECTED_GWT_ONLOAD_VERSION = "1.6";
 
   public static void launchExternalBrowser(TreeLogger logger, String location) {
     // check GWT_EXTERNAL_BROWSER first, it overrides everything else
@@ -330,7 +346,7 @@
 
     // Enable the compile button since we successfully loaded.
     //
-    toolbar.openWebModeButton.setEnabled(true);
+    toolbar.updateWebMode();
   }
 
   /**
@@ -356,27 +372,19 @@
         loadedModules.remove(key);
       }
     }
-    if (loadedModules.isEmpty()) {
-      if (!toolbar.openWebModeButton.isDisposed()) {
-        // Disable the compile button.
-        //
-        toolbar.openWebModeButton.setEnabled(false);
-      }
-    }
+    toolbar.updateWebMode();
   }
 
   /**
-   * Report that gwtOnLoad was called with the wrong number of
+   * Report that an external method was called with the wrong number of
    * arguments.
-   * 
-   * @param numArgs number of arguments supplied
    */
-  protected void reportIncorrectGwtOnLoadInvocation(int numArgs) {
+  protected void reportIncorrectInvocation(String name, int expectedArgs,
+      int actualArgs) {
     getHost().getLogger().log(
         TreeLogger.ERROR,
-        "Not enough arguments ("
-        + numArgs
-            + ") passed to external.gwtOnLoad(), expected (3); "
+        "Not enough arguments (" + actualArgs + ") passed to external." + name
+            + "(), expected (" + expectedArgs + "); "
             + "your hosted mode bootstrap file may be out of date; "
             + "if you are using -noserver try recompiling and redeploying "
             + "your app");
@@ -398,8 +406,8 @@
   /**
    * Validate that the supplied hosted.html version matches.
    * 
-   * This is to detect cases where users upgrade to a new version
-   * but forget to update the generated hosted.html file.
+   * This is to detect cases where users upgrade to a new version but forget to
+   * update the generated hosted.html file.
    * 
    * @param version version supplied by hosted.html file
    * @return true if the version is valid, false otherwise
@@ -408,8 +416,7 @@
     if (!EXPECTED_GWT_ONLOAD_VERSION.equals(version)) {
       getHost().getLogger().log(
           TreeLogger.ERROR,
-          "Invalid version number \""
-              + version
+          "Invalid version number \"" + version
               + "\" passed to external.gwtOnLoad(), expected \""
               + EXPECTED_GWT_ONLOAD_VERSION
               + "\"; your hosted mode bootstrap file may be out of date; "
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
index 8c557e0..74c2175 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
@@ -17,16 +17,29 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.cfg.ModuleDef;
 
 /**
  * Interface that unifies access to the <code>BrowserWidget</code>,
  * <code>ModuleSpaceHost</code>, and the compiler.
  */
 public interface BrowserWidgetHost {
-  void compile(String[] modules) throws UnableToCompleteException;
+  /**
+   * Perform a web-mode compile on the user-specified set of modules. Used in
+   * non-legacy mode.
+   * 
+   * @throws UnableToCompleteException
+   */
+  void compile() throws UnableToCompleteException;
 
-  void compile(ModuleDef module) throws UnableToCompleteException;
+  /**
+   * Compile the specified set of modules, used in legacy mode.
+   * 
+   * @param modules the names of the modules to compile
+   * @throws UnableToCompleteException
+   * @deprecated will be removed when legacy shell mode is removed
+   */
+  @Deprecated
+  void compile(String[] modules) throws UnableToCompleteException;
 
   // Factor this out if BrowserWidget becomes decoupled from hosted mode
   ModuleSpaceHost createModuleSpaceHost(BrowserWidget widget, String moduleName)
@@ -34,6 +47,26 @@
 
   TreeLogger getLogger();
 
+  /**
+   * Called from a selection script as it begins to load in hosted mode. This
+   * triggers a hosted mode link, which might actually update the running
+   * selection script.
+   * 
+   * @param moduleName the module to link
+   * @return <code>true</code> if the selection script was overwritten; this
+   *         will trigger a full-page refresh by the calling (out of date)
+   *         selection script
+   */
+  boolean initModule(String moduleName);
+
+  /**
+   * Returns <code>true</code> if running in legacy mode.
+   * 
+   * @deprecated will be removed when legacy shell mode is removed
+   */
+  @Deprecated
+  boolean isLegacyMode();
+
   String normalizeURL(String whatTheUserTyped);
 
   BrowserWidget openNewBrowserWindow() throws UnableToCompleteException;
diff --git a/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java b/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
index f0cbe8c..e1f8aef 100644
--- a/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
+++ b/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
@@ -70,6 +70,17 @@
     }
 
     /**
+     * Causes a link to occur for the specified module.
+     * 
+     * @param moduleName the module name to link
+     * @return <code>true</code> if this module is stale and should be
+     *         reloaded
+     */
+    public boolean initModule(String moduleName) {
+      return getHost().initModule(moduleName);
+    }
+
+    /**
      * Unload one or more modules.
      * 
      * @param scriptObject window to unload, 0 if all
diff --git a/dev/linux/src/com/google/gwt/dev/shell/moz/LowLevelMoz.java b/dev/linux/src/com/google/gwt/dev/shell/moz/LowLevelMoz.java
index 1606f78..2b89c2e 100644
--- a/dev/linux/src/com/google/gwt/dev/shell/moz/LowLevelMoz.java
+++ b/dev/linux/src/com/google/gwt/dev/shell/moz/LowLevelMoz.java
@@ -75,6 +75,8 @@
    * Safari.
    */
   interface ExternalObject {
+    boolean initModule(String moduleName);
+
     boolean gwtOnLoad(int scriptGlobalObject, String moduleName, String version);
   }
 
diff --git a/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java b/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
index 439cae7..3d0fe1a 100644
--- a/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
+++ b/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
@@ -39,6 +39,9 @@
       if ("gwtonload".equalsIgnoreCase(name)) {
         return LowLevelSaf.wrapDispatchMethod(jsContext, "gwtOnload",
             new GwtOnLoad());
+      } else if ("initmodule".equalsIgnoreCase(name)) {
+        return LowLevelSaf.wrapDispatchMethod(jsContext, "initModule",
+            new InitModule());
       }
       // Native code eats the same ref it gave us.
       return LowLevelSaf.getJsUndefined(jsContext);
@@ -88,6 +91,17 @@
       }
     }
 
+    /**
+     * Causes a link to occur for the specified module.
+     * 
+     * @param moduleName the module name to link
+     * @return <code>true</code> if this module is stale and should be
+     *         reloaded
+     */
+    public boolean initModule(String moduleName) {
+      return getHost().initModule(moduleName);
+    }
+
     public void setField(int jsContext, String name, int value) {
       try {
         // TODO (knorton): This should produce an error. The SetProperty
@@ -127,8 +141,8 @@
           return jsFalse;
         }
 
-        if (jsargs.length < 2) {
-          reportIncorrectGwtOnLoadInvocation(jsargs.length);
+        if (jsargs.length < 3) {
+          reportIncorrectInvocation("gwtOnLoad", 3, jsargs.length);
           return jsFalse;
         }
 
@@ -141,16 +155,10 @@
         }
         String moduleName = LowLevelSaf.toString(jsContext, jsargs[1]);
 
-        /*
-         * gwtOnLoad may or may not be called with a third argument indicating
-         * which version of GWT the JavaScript bootstrap sequence assumes that
-         * its talking to.
-         */
-        String version = null;
-        if (jsargs.length == 3 && !LowLevelSaf.isJsNull(jsContext, jsargs[2])
-            && LowLevelSaf.isJsString(jsContext, jsargs[2])) {
-          version = LowLevelSaf.toString(jsContext, jsargs[2]);
+        if (!LowLevelSaf.isJsString(jsContext, jsargs[2])) {
+          return jsFalse;
         }
+        String version = LowLevelSaf.toString(jsContext, jsargs[2]);
 
         boolean result = ((ExternalObject) thisObj).gwtOnLoad(jsargs[0],
             moduleName, version);
@@ -168,6 +176,46 @@
     }
   }
 
+  private final class InitModule implements DispatchMethod {
+
+    public int invoke(int jsContext, int jsthis, int[] jsargs, int[] exception) {
+      int jsFalse = LowLevelSaf.toJsBoolean(jsContext, false);
+      LowLevelSaf.pushJsContext(jsContext);
+      try {
+        if (!LowLevelSaf.isDispatchObject(jsContext, jsthis)) {
+          return jsFalse;
+        }
+
+        Object thisObj = LowLevelSaf.unwrapDispatchObject(jsContext, jsthis);
+        if (!(thisObj instanceof ExternalObject)) {
+          return jsFalse;
+        }
+
+        if (jsargs.length < 1) {
+          reportIncorrectInvocation("initModule", 1, jsargs.length);
+          return jsFalse;
+        }
+
+        if (!LowLevelSaf.isJsString(jsContext, jsargs[0])) {
+          return jsFalse;
+        }
+        String moduleName = LowLevelSaf.toString(jsContext, jsargs[0]);
+
+        boolean result = ((ExternalObject) thisObj).initModule(moduleName);
+        // Native code eats the same ref it gave us.
+        return LowLevelSaf.toJsBoolean(jsContext, result);
+      } catch (Throwable e) {
+        return jsFalse;
+      } finally {
+        for (int jsarg : jsargs) {
+          LowLevelSaf.gcUnprotect(jsContext, jsarg);
+        }
+        LowLevelSaf.gcUnprotect(jsContext, jsthis);
+        LowLevelSaf.popJsContext(jsContext);
+      }
+    }
+  }
+
   private static final int REDRAW_PERIOD = 250;
 
   static {
diff --git a/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java b/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
index 2303df1..6e3aa62 100644
--- a/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
+++ b/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
@@ -86,6 +86,17 @@
       }
     }
 
+    /**
+     * Causes a link to occur for the specified module.
+     * 
+     * @param moduleName the module name to link
+     * @return <code>true</code> if this module is stale and should be
+     *         reloaded
+     */
+    public boolean initModule(String moduleName) {
+      return getHost().initModule(moduleName);
+    }
+
     @Override
     protected void getIDsOfNames(String[] names, int[] ids)
         throws HResultException {
@@ -98,6 +109,9 @@
       if (name.equals("gwtonload")) {
         ids[0] = 1;
         return;
+      } else if (name.equals("initmodule")) {
+        ids[0] = 2;
+        return;
       }
 
       throw new HResultException(DISP_E_UNKNOWNNAME);
@@ -126,15 +140,15 @@
       } else if (dispId == 1) {
         if ((flags & COM.DISPATCH_METHOD) != 0) {
           try {
-            if (params.length < 2) {
-              reportIncorrectGwtOnLoadInvocation(params.length);
+            if (params.length < 3) {
+              reportIncorrectInvocation("gwtOnLoad", 3, params.length);
               throw new HResultException(COM.E_INVALIDARG);
             }
             IDispatch frameWnd = (params[0].getType() == COM.VT_DISPATCH)
                 ? params[0].getDispatch() : null;
             String moduleName = (params[1].getType() == COM.VT_BSTR)
                 ? params[1].getString() : null;
-            String version = (params.length > 2 && params[2].getType() == COM.VT_BSTR)
+            String version = (params[2].getType() == COM.VT_BSTR)
                 ? params[2].getString() : null;
             boolean success = gwtOnLoad(frameWnd, moduleName, version);
 
@@ -159,6 +173,38 @@
           }
         }
         throw new HResultException(COM.E_NOTSUPPORTED);
+      } else if (dispId == 2) {
+        if ((flags & COM.DISPATCH_METHOD) != 0) {
+          try {
+            if (params.length < 1) {
+              reportIncorrectInvocation("initModule", 1, params.length);
+              throw new HResultException(COM.E_INVALIDARG);
+            }
+            String moduleName = (params[0].getType() == COM.VT_BSTR)
+                ? params[0].getString() : null;
+            boolean reload = initModule(moduleName);
+
+            // boolean return type
+            return new Variant(reload);
+          } catch (SWTException e) {
+            throw new HResultException(COM.E_INVALIDARG);
+          }
+        } else if ((flags & COM.DISPATCH_PROPERTYGET) != 0) {
+          // property get on the method itself
+          try {
+            Method gwtOnLoadMethod = getClass().getMethod("initModule",
+                new Class[] {String.class});
+            MethodAdaptor methodAdaptor = new MethodAdaptor(gwtOnLoadMethod);
+            IDispatchImpl funcObj = new MethodDispatch(null, methodAdaptor);
+            IDispatch disp = new IDispatch(funcObj.getAddress());
+            disp.AddRef();
+            return new Variant(disp);
+          } catch (Exception e) {
+            // just return VT_EMPTY
+            return new Variant();
+          }
+        }
+        throw new HResultException(COM.E_NOTSUPPORTED);
       }
 
       // The specified member id is out of range.
diff --git a/samples/dynatable2/src.war/WEB-INF/web.xml b/samples/dynatable2/src.war/WEB-INF/web.xml
index 01c3949..1feeb14 100644
--- a/samples/dynatable2/src.war/WEB-INF/web.xml
+++ b/samples/dynatable2/src.war/WEB-INF/web.xml
@@ -9,7 +9,7 @@
 	</servlet>
 	<servlet-mapping>
 		<servlet-name>calendar</servlet-name>
-		<url-pattern>/com.google.gwt.sample.dynatable2.DynaTable2/calendar</url-pattern>
+		<url-pattern>/dynatable2/calendar</url-pattern>
 	</servlet-mapping>
 
 </web-app>
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index a5141a3..5d68977 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -28,7 +28,6 @@
 import com.google.gwt.dev.cfg.Properties;
 import com.google.gwt.dev.cfg.Property;
 import com.google.gwt.dev.javac.CompilationUnit;
-import com.google.gwt.dev.shell.BrowserWidgetHost;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.junit.client.TimeoutException;
 import com.google.gwt.junit.client.impl.GWTRunner;
@@ -590,9 +589,7 @@
         ((BindingProperty) userAgent).setAllowedValues(userAgentString);
       }
     }
-    BrowserWidgetHost browserHost = getBrowserHost();
-    assert (browserHost != null);
-    browserHost.compile(module);
+    super.compile(getTopLogger(), module);
   }
 
   /**