In my case, I was trying to get this to work in the context of AppsScript running from a Google Sheet extension, producing a custom dialog. Unfortunately, while your hack to getting the //# sourceURL=filename.js pragma comment into the HtmlOutput worked, there was still something else downstream stripping all the comments.
So I had to work around it by returning output that was a simple script to add a script element with the generated code, plus the pragma comment, into the DOM at runtime. Even more hacky, but without this, it's impossible to debug...
Edited/Added:
Here's the essence of what I had to do in order to go from a JavaScript module in my repo (.js containing utility functions, for example) to the same file in DevTools loaded as a result of opening a custom dialog given an HTML template in GAS.
I start with a module util.js:
// a comment to preserve
const sayHello = () => console.log('hello');
window.myApp = { sayHello };
- Run this through a Rollup IIFE build. The output is
util.js.html (.html because that's what clasp will sync and what you can load as a template).
Using Rollup is key because it doesn't mangle its output like Webpack does into a single-line eval() statement that absolutely requires a source map to read. So the code it produces is code largely as you wrote it, but perhaps transpiled to hit an ES target you set, or even convert JSX into JS (or TypeScript into JS).
The result is a self-executing script:
(function () {
'use strict';
// a comment to preserve
const sayHello = () => console.log('hello');
window.myApp = { sayHello };
})();
Create a GAS function called includeScript.
In your dialog's HTML template file, include your generated script using the function:
<?!= includeScript('path/to/generated/util.js.html') />
The includeScript() function is where all the magic happens.
- Load the script template file's content while preserving comments:
// we basically use this just to be able to load the raw file from the cloud-based file system
// NOTE: crucially, this retains comments
// NOTE: we have to wrap in a <script> block to make sure that the HtmlOutput's content, which
// we get later on, will NOT XML-encode the string, which would mess-up things like less-than
// operators ('<' would become '<'); by putting the raw content in a <script> block, its
// treated as CDATA instead of HTML code that needs to be escaped
const rawContent = `<script>${HtmlService.createTemplateFromFile(
'path/to/generated/util.js.html'
).getRawContent()}</script>`;
const template = HtmlService.createTemplate(rawContent);
Note that this is the point where you could apply a scope to the template prior to evaluating it, if you wanted/needed to.
- Get the actual code out the script (still with comments so far):
const output = template.evaluate();
// unwrap the now-evaluated raw content from the temporary <script> block because we're now
// going to put the code _into_ an actual <script> element in the DOM (when the generated
// output runs in the browser)
let code = output
.getContent()
.replace(/^<script>/gm, '')
.replace(/<\/script>$/gm, '');
Note that the technique suggested in this post (above) must only work for Google Web Services, not GAS, because comments are still stripped. So now we have to do something really hacky in order to get the raw code we now have in code while preserving comments.
First, add the pragma comment to the source that identifies its original module structure/path/directory so that you can find this file in DevTools once it loads:
code = `${code}\n//# sourceURL=path/to/original/util.js`;
Finally, return the actual string that the HtmlService will use in the HTML file it's loading. Remember, the includeScript() function is executing in the template you're giving to SpreadsheetApp.getUi().showModalDialog(...):
<?!= includeScript('path/to/generated/util.js.html') />
What we need to do is URI-encode (to preserve extended characters in your strings), and Base64-encode to preserve quotes and such that would otherwise get converted into HTML characters like ", the code so that we end-up with a string that the HtmlService essentially won't touch (i.e. strip comments, including that sourceURL pragma critical to debugging in DevTools).
And because the browser won't load this on its own, we need to wrap that blob into a little loader function that will undo these encodings in the browser:
return `<script>
(function () {
const code = decodeURIComponent(atob('${Utilities.base64Encode(
encodeURIComponent(code)
)}'));
const scriptEl = document.createElement('script');
scriptEl.textContent = code;
document.body.appendChild(scriptEl);
})();
</script>`;
That's for the "dev" version of your code. The "prod" version doesn't need to do all this because presumably, you don't need comments in your prod version, and you probably also minify it anyway.
So for "prod", this is all you need instead of the self-executing function that unwraps the encoding (because you don't need to encode the code in the first place since you don't need to preserve anything):
return `<script>${code}</script>`;
And I do have Dev and Prod builds of each of my JS and JSX modules, through Rollup. My Dev builds preserve comments and don't minify, while my Prod builds strip comments and minify.
Hopefully this helps... It's too much code in too many files to just post here. I should really OSS my GAS build solution, but that's for another time.