Better magic comments at the end of JavaScript fragments in CrossSiteIframeLinker

- Switch to '#' instead of '@' since this changed in the sourcemap spec
- Set sourcemap URL for every fragment, not just the primary one
- Add template variable support to the includeSourceMapUrl config property

Template variables:
  __MODULE__ substitutes the module name
  __HASH__ substitutes the strong name of the fragment
  __FRAGMENT__ substitutes the fragment number

This makes it easier to build a URL that a servlet can resolve to the
right sourcemap file.

Public API changes:
 - New versions of two methods in SelectionScriptLinker
 - Added CrossSiteIframeLinker.getSourceMapUrl()

Change-Id: I2207d2b258df8c1088129017e5039fbfd0edb093
Review-Link: https://gwt-review.googlesource.com/#/c/4891/
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 233c5a7..46023f3 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
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.AbstractLinker;
 import com.google.gwt.core.ext.linker.Artifact;
@@ -356,7 +357,16 @@
     String prefix = getDeferredFragmentPrefix(logger, context, fragment);
     b.append(prefix);
     b.append(js);
-    b.append(getDeferredFragmentSuffix(logger, context, fragment));
+    String suffix = getDeferredFragmentSuffix(logger, context, fragment);
+    if (suffix == null) {
+      suffix = getDeferredFragmentSuffix2(logger, context, fragment, strongName);
+      if (suffix == null) {
+        logger.log(Type.ERROR, "Neither getDeferredFragmentSuffix nor getDeferredFragmentSuffix2 "
+            + "were overridden in linker: " + getClass().getName());
+        throw new UnableToCompleteException();
+      }
+    }
+    b.append(suffix);
     SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact
         = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, fragment);
     editsArtifact.prefixLines(prefix);
@@ -394,7 +404,16 @@
     artifacts.add(editsArtifact);
     b.append(modulePrefix);
     b.append(js);
-    b.append(getModuleSuffix(logger, context));
+    String suffix = getModuleSuffix(logger, context);
+    if (suffix == null) {
+      suffix = getModuleSuffix2(logger, context, strongName);
+      if (suffix == null) {
+        logger.log(Type.ERROR, "Neither getModuleSuffix nor getModuleSuffix2 were overridden in "
+            + "linker: " + getClass().getName());
+        throw new UnableToCompleteException();
+      }
+    }
+    b.append(suffix);
     return wrapPrimaryFragment(logger, context, b.toString(), artifacts, result);
   }
 
@@ -424,8 +443,26 @@
     return "";
   }
 
+  /**
+   * Returns the suffix at the end of a JavaScript fragment other than the initial fragment
+   * (deprecated version). The default version returns null, which will cause
+   * {@link #getDeferredFragmentSuffix2} to be called instead. Subclasses should switch to
+   * extending getDeferredFragmentSuffix2.
+   */
+  @Deprecated
   protected String getDeferredFragmentSuffix(TreeLogger logger, LinkerContext context,
       int fragment) {
+    return null;
+  }
+
+  /**
+   * Returns the suffix at the end of a JavaScript fragment other than the initial fragment
+   * (new version). This method won't be called if {@link #getDeferredFragmentSuffix} is overridden
+   * to return non-null. Subclasses should stop implementing getDeferredFramgnentSuffix and
+   * implement getDeferredFragmentSuffix2 instead.
+   */
+  protected String getDeferredFragmentSuffix2(TreeLogger logger, LinkerContext context,
+      int fragment, String strongName) {
     return "";
   }
 
@@ -476,8 +513,26 @@
     return getModulePrefix(logger, context, strongName);
   }
 
-  protected abstract String getModuleSuffix(TreeLogger logger,
-      LinkerContext context) throws UnableToCompleteException;
+  /**
+   * Returns the suffix for the initial JavaScript fragment (deprecated version).
+   * The default returns null, which will cause {@link #getModuleSuffix2} to be called instead.
+   * Subclasses should switch to extending getModuleSuffix2.
+   */
+  @Deprecated
+  protected String getModuleSuffix(TreeLogger logger,
+      LinkerContext context) throws UnableToCompleteException {
+    return null;
+  }
+
+  /**
+   * Returns the suffix for the initial JavaScript fragment (new version). This version
+   * will not be called if {@link #getModuleSuffix} is overridden so that it doesn't return null.
+   * Subclasses should stop implementing getModuleSuffix and implmenet getModuleSuffix2 instead.
+   */
+  protected String getModuleSuffix2(TreeLogger logger,
+      LinkerContext context, String strongName) throws UnableToCompleteException {
+    return null;
+  }
 
   /**
    * Some subclasses support "chunking" of the primary fragment. If chunking will be supported, this
diff --git a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
index 5507ecd..446a93f 100644
--- a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
@@ -192,9 +192,13 @@
     return ".cache.js";
   }
 
-   protected String getDeferredFragmentSuffix(TreeLogger logger, LinkerContext context,
-      int fragment) {
-    return "\n//@ sourceURL=" + context.getModuleName() + "-" + fragment + ".js\n";
+  @Override
+  protected String getDeferredFragmentSuffix2(TreeLogger logger, LinkerContext context,
+      int fragment, String strongName) {
+
+    DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
+    writeMagicComments(out, context, fragment, strongName);
+    return out.toString();
   }
 
   @Override
@@ -442,7 +446,11 @@
   }
 
   @Override
-  protected String getModuleSuffix(TreeLogger logger, LinkerContext context) {
+  protected String getModuleSuffix2(TreeLogger logger, LinkerContext context,
+      String strongName) {
+
+    // Note: this method won't be called if getModuleSuffix() is overridden and returns non-null.
+
     DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
 
     out.print("$sendStats('moduleStartup', 'moduleEvalEnd');");
@@ -452,20 +460,8 @@
         + "__gwtModuleFunction.__computePropValue);");
     out.newlineOpt();
     out.print("$sendStats('moduleStartup', 'end');");
-    String includeSourceMapUrl = getStringConfigurationProperty(context, "includeSourceMapUrl", "false");
-    if (!"false".equalsIgnoreCase(includeSourceMapUrl)) {
-      String sourceMapUrl = SymbolMapsLinker.SourceMapArtifact.sourceMapFilenameForFragment(0);
-      if (!"true".equalsIgnoreCase(includeSourceMapUrl)) {
-        sourceMapUrl = includeSourceMapUrl;
-      }
-      // The sourceURL magic comment can cause browsers to ignore the X-SourceMap header
-      // This magic comment ensures that they can still locate them in that case
-      out.print("\n//@ sourceMappingURL=" + sourceMapUrl + " ");
-    }
-    // Magic comment serves several purposes:
-    // 1. renames strongName to a stable name in browser debugger
-    // 2. provides name to scripts installed via eval()
-    out.print("\n//@ sourceURL=" + context.getModuleName() + "-0.js\n");
+
+    writeMagicComments(out, context, 0, strongName);
     return out.toString();
   }
 
@@ -479,6 +475,27 @@
     return "com/google/gwt/core/linker/CrossSiteIframeTemplate.js";
   }
 
+  /**
+   * Returns the sourcemap URL that will be put in the comment at the end of a JavaScript
+   * fragment, or null if the comment should be omitted. The default implementation uses
+   * the includeSourceMapUrl config property.
+   */
+  protected String getSourceMapUrl(LinkerContext context, String strongName, int fragmentId) {
+    String val = getStringConfigurationProperty(context, "includeSourceMapUrl",  "false");
+
+    if ("false".equalsIgnoreCase(val)) {
+      return null;
+    }
+
+    if ("true".equalsIgnoreCase(val)) {
+      return SymbolMapsLinker.SourceMapArtifact.sourceMapFilenameForFragment(fragmentId);
+    }
+
+    return val.replaceAll("__HASH__", strongName)
+        .replaceAll("__FRAGMENT__", String.valueOf(fragmentId))
+        .replaceAll("__MODULE__", context.getModuleName());
+  }
+
   protected String getStringConfigurationProperty(LinkerContext context,
       String name, String def) {
     for (ConfigurationProperty property : context.getConfigurationProperties()) {
@@ -646,4 +663,24 @@
     }
     return out.toString();
   }
+
+  private void writeMagicComments(DefaultTextOutput out, LinkerContext context, int fragmentId,
+      String strongName) {
+    String sourceMapUrl = getSourceMapUrl(context, strongName, fragmentId);
+    if (sourceMapUrl != null) {
+      // This magic comment determines where a browser debugger looks for a sourcemap,
+      // except that it may be overridden by a "SourceMap" header in the HTTP response when
+      // loading the JavaScript.
+      // (Note: even if you're using the HTTP header, you still have to set this to an arbitrary
+      // value, or Chrome won't enable sourcemaps.)
+      out.print("\n//# sourceMappingURL=" + sourceMapUrl + " ");
+    }
+
+    // This magic comment determines the name of the JavaScript fragment in a browser debugger.
+    // (In Chrome it typically shows up under "(no domain)".)
+    // We need to set it explicitly because the JavaScript code may be installed via an "eval"
+    // statement and even if we're not using an eval, the filename contains the strongname which
+    // isn't stable across recompiles.
+    out.print("\n//# sourceURL=" + context.getModuleName() + "-" + fragmentId + ".js\n");
+  }
 }
diff --git a/user/src/com/google/gwt/core/CrossSiteIframeLinker.gwt.xml b/user/src/com/google/gwt/core/CrossSiteIframeLinker.gwt.xml
index b359425..08a4f14 100644
--- a/user/src/com/google/gwt/core/CrossSiteIframeLinker.gwt.xml
+++ b/user/src/com/google/gwt/core/CrossSiteIframeLinker.gwt.xml
@@ -30,9 +30,16 @@
      - debugging in browsers.
      - Specification: http://goo.gl/ZQ3V3
      - Takes on the following values:
-     - if false, no sourcemap URL declarations are included at the end of the primary fragment script
-     - if true, a relative URL to the standard sourcemap for the primary fragment is included
-     - else the value is assumed to be a custom URL -->
+     - if "false":
+     -   no sourcemap URL declarations are included at the end of the primary fragment script
+     - if "true":
+     -    a relative URL to the standard sourcemap for the primary fragment is included.
+     - Otherwise:
+     -   The value is assumed to be a custom URL. The following tokens will be substituted:
+     -   __MODULE__ is replaced with the module name
+     -   __FRAGMENT__ is replaced with the fragment id, which will be 0 for the initial fragment.
+     -   __HASH__ is replaced with the strong name of the fragment.
+   -->
   <define-configuration-property name="includeSourceMapUrl" is-multi-valued="false"/>
   <set-configuration-property name="includeSourceMapUrl" value="false"/>
   <set-configuration-property name="xsiframe.failIfScriptTag" value="TRUE"/>