なぜインラインにしたいか
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
対応
下記内容参考に手元で編集。
/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