4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

<?!= HtmlService.createHtmlOutputFromFile('JavaScript').getContent(); ?>

参考例: apps-script-samples/templates/web-app at master · gsuitedevs/apps-script-samples

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

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

Webpack Plugin 追加

導入

$ 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

対応

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

Make plugin compatible with the HTML Webpack Plugin version 4 by DominikPalo · Pull Request #55 · DustinJackson/html-webpack-inline-source-plugin

/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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?