webpackでjsをhtmlにインライン出力

## なぜインラインにしたいか

Google Apps Script でウェブアプリを作る場合、分割したJavaScriptやCSSを読み込むためには下記のようにしてインクルードする事がある。

```html
<?!= HtmlService.createHtmlOutputFromFile('JavaScript').getContent(); ?>
```
参考例: [apps-script-samples/templates/web-app at master · gsuitedevs/apps-script-samples](https://github.com/gsuitedevs/apps-script-samples/tree/master/templates/web-app)

Webpack/Babel でバンドルした React の小さなアプリを GAS で配信しようとしたところ、別ファイルにした JS が期待通りにインクルードされないことがあった。

HTML にベタ書きすると問題なく動くので読み込む途中に止められた？ 原因は未調査ながらHTMLにインラインで JS を書き込むことに。

## Webpack Plugin 追加

* [html-webpack-plugin - npm](https://www.npmjs.com/package/html-webpack-plugin)
* [html-webpack-inline-source-plugin - npm](https://www.npmjs.com/package/html-webpack-inline-source-plugin)

### 導入

```shell
$ npm install --save-dev html-webpack-plugin html-webpack-inline-source-plugin
```

### ファイル構成

```
┬ dist
├ src
│ ├ index.js
│ └ index.html
└ webpack.config.js
```

### 設定ファイル更新

```webpack.config.js
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: '.(js|css)$',
      template: "./src/index.html"
      minify: false,
    }),
    new HtmlWebpackInlineSourcePlugin()
  ]
```

### コンパイル失敗

下記エラー出て止まる

```
node_modules/html-webpack-inline-source-plugin/index.js:21
      ? compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync.bind(compilation.hooks.htmlWebpackPluginAlterAssetTags, 'html-webpack-inline-source-plugin')

TypeError: Cannot read property 'tapAsync' of undefined
```

関連 Issue: [Cannot read property 'tapAsync' of undefined · Issue #63 · DustinJackson/html-webpack-inline-source-plugin](https://github.com/DustinJackson/html-webpack-inline-source-plugin/issues/63)

### 対応

下記内容参考に手元で編集。

[Make plugin compatible with the HTML Webpack Plugin version 4 by DominikPalo · Pull Request #55 · DustinJackson/html-webpack-inline-source-plugin](https://github.com/DustinJackson/html-webpack-inline-source-plugin/pull/55/files)

```/node_modules/html-webpack-inline-source-plugin/index.js
'use strict';
var escapeRegex = require('escape-string-regexp');
var path = require('path');
var slash = require('slash');
var sourceMapUrl = require('source-map-url');

function HtmlWebpackInlineSourcePlugin (htmlWebpackPlugin) {
  this.htmlWebpackPlugin = htmlWebpackPlugin;
}

HtmlWebpackInlineSourcePlugin.prototype.apply = function (compiler) {
  var self = this;

  // Hook into the html-webpack-plugin processing
  compiler.hooks.compilation.tap('html-webpack-inline-source-plugin', compilation => {
    self.htmlWebpackPlugin
      .getHooks(compilation)
      .alterAssetTagGroups.tapAsync('html-webpack-inline-source-plugin', (htmlPluginData, callback) => {
        if (!htmlPluginData.plugin.options.inlineSource) {
          return callback(null, htmlPluginData);
        }

        var regexStr = htmlPluginData.plugin.options.inlineSource;

        var result = self.processTags(compilation, regexStr, htmlPluginData);

        callback(null, result);
      });
  });
};

HtmlWebpackInlineSourcePlugin.prototype.processTags = function (compilation, regexStr, pluginData) {
  var self = this;

  var bodyTags = [];
  var headTags = [];

  var regex = new RegExp(regexStr);
  var filename = pluginData.plugin.options.filename;

  pluginData.headTags.forEach(function (tag) {
    headTags.push(self.processTag(compilation, regex, tag, filename));
  });

  pluginData.bodyTags.forEach(function (tag) {
    bodyTags.push(self.processTag(compilation, regex, tag, filename));
  });

  return { headTags: headTags, bodyTags: bodyTags, plugin: pluginData.plugin, outputName: pluginData.outputName };
};

HtmlWebpackInlineSourcePlugin.prototype.resolveSourceMaps = function (compilation, assetName, asset) {
  var source = asset.source();
  var out = compilation.outputOptions;
  // Get asset file absolute path
  var assetPath = path.join(out.path, assetName);
  // Extract original sourcemap URL from source string
  if (typeof source !== 'string') {
    source = source.toString();
  }
  var mapUrlOriginal = sourceMapUrl.getFrom(source);
  // Return unmodified source if map is unspecified, URL-encoded, or already relative to site root
  if (!mapUrlOriginal || mapUrlOriginal.indexOf('data:') === 0 || mapUrlOriginal.indexOf('/') === 0) {
    return source;
  }
  // Figure out sourcemap file path *relative to the asset file path*
  var assetDir = path.dirname(assetPath);
  var mapPath = path.join(assetDir, mapUrlOriginal);
  var mapPathRelative = path.relative(out.path, mapPath);
  // Starting with Node 6, `path` module throws on `undefined`
  var publicPath = out.publicPath || '';
  // Prepend Webpack public URL path to source map relative path
  // Calling `slash` converts Windows backslashes to forward slashes
  var mapUrlCorrected = slash(path.join(publicPath, mapPathRelative));
  // Regex: exact original sourcemap URL, possibly '*/' (for CSS), then EOF, ignoring whitespace
  var regex = new RegExp(escapeRegex(mapUrlOriginal) + '(\\s*(?:\\*/)?\\s*$)');
  // Replace sourcemap URL and (if necessary) preserve closing '*/' and whitespace
  return source.replace(regex, function (match, group) {
    return mapUrlCorrected + group;
  });
};

HtmlWebpackInlineSourcePlugin.prototype.processTag = function (compilation, regex, tag, filename) {
  var assetUrl;

  // inline js
  if (tag.tagName === 'script' && tag.attributes && regex.test(tag.attributes.src)) {
    assetUrl = tag.attributes.src;
    tag = {
      tagName: 'script',
      closeTag: true,
      attributes: {
        type: 'text/javascript'
      }
    };

  // inline css
  } else if (tag.tagName === 'link' && regex.test(tag.attributes.href)) {
    assetUrl = tag.attributes.href;
    tag = {
      tagName: 'style',
      closeTag: true,
      attributes: {
        type: 'text/css'
      }
    };
  }

  if (assetUrl) {
    // Strip public URL prefix from asset URL to get Webpack asset name
    var publicUrlPrefix = compilation.outputOptions.publicPath || '';
    // if filename is in subfolder, assetUrl should be prepended folder path
    if (path.basename(filename) !== filename) {
      assetUrl = path.dirname(filename) + '/' + assetUrl;
    }
    var assetName = path.posix.relative(publicUrlPrefix, assetUrl);
    var asset = getAssetByName(compilation.assets, assetName);
    var updatedSource = this.resolveSourceMaps(compilation, assetName, asset);
    tag.innerHTML = (tag.tagName === 'script') ? updatedSource.replace(/(<)(\/script>)/g, '\\x3C$2') : updatedSource;
  }

  return tag;
};

function getAssetByName (assests, assetName) {
  for (var key in assests) {
    if (assests.hasOwnProperty(key)) {
      var processedKey = path.posix.relative('', key);
      if (processedKey === assetName) {
        return assests[key];
      }
    }
  }
}

module.exports = HtmlWebpackInlineSourcePlugin;
```

```webpack.config.js
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: '.(js|css)$',
      template: "./src/index.html",
      minify: false,
    }),
    new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin)
  ]
```

## 結果

```
Hash: c9c28c8b762c181cdaea
Version: webpack 4.43.0
Time: 3768ms
Built at: 2020-05-20 16:44:57
     Asset      Size  Chunks             Chunk Names
index.html  15.2 KiB          [emitted]  
  index.js  14.9 KiB       0  [emitted]  index
Entrypoint index = index.js
[9] ./src/index.js 13.7 KiB {0} [built]
    + 17 hidden modules
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 693 bytes {0} [built]
```

`dist/HTML` の `</body>` 直前に`<script>// バンドルされたコード</script>` が挿入された。

手でモジュール編集という微妙さが残る。

### 環境

Node v12.16.2
npm 6.14.4
iMac 10.13.6