ビルド環境を構築中に、ejsのコンパイルのテストしていたところ、include
で不可解なエラーに遭遇した。
<!-- index.ejs -->
<% data = {title:'login'};-%>
<!DOCTYPE html>
<html lang="ja">
<head>
<%-include('header.ejs',{data:data}) %>
</head>
<body>
<body>
<div class="container">
<sf-textbox help="入れてください。何か。" class="small" label="ボックス1" placeholder="何か入れてください" name="name1" id="idd"></sf-textbox>
<sf-textbox label="ボックス2" placeholder="プレースホルダ" name="name2" id="idd1"></sf-textbox>
</div>
<script type="module" async >
import register from '/js/bundle.mjs';
register();
</script>
</body>
</body>
</html>
これを以下のようなスクリプトを作成してコンパイルしようとした。
// buildEjs.mjs
import ejs from 'ejs';
import fs from 'fs';
import path from 'path';
(async ()=> {
const src = process.argv[2];
const dest = process.argv[3];
let template = await fs.promises.readFile(src,'utf8')
let cwd = process.cwd();
console.info(`ejs.render:${src} => ${dest}`);
let html = ejs.render(template);
await fs.promises.writeFile(dest,html,'utf8');
})();
これに引数をつけて実行してみたところ、以下のエラーが出る。
node --experimental-modules ./tools/buildEjs.mjs ./src/ejs/index.ejs ./public/html/index.html
(node:1747) UnhandledPromiseRejectionWarning: Error: ejs:5
3| <html lang="ja">
4| <head>
>> 5| <%-include('header.ejs',{data:data}) %>
6| </head>
7| <body>
8| <body>
Could not find the include file "header.ejs"
at getIncludePath (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:165:13)
at includeFile (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:291:19)
at include (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:680:16)
at eval (eval at compile (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:652:12), <anonymous>:11:16)
at anonymous (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:682:17)
at Object.exports.render (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:419:37)
at file:///home/sfpg/pj/www/node/scms/tools/buildEjs.mjs:13:18
ちなみにheader.ejs
はindex.ejs
と同じディレクトリに置いている。ejsのドキュメントには以下のように書いてある
For example if you have "./views/users.ejs" and "./views/user/show.ejs" you would use <%- include('user/show'); %>.
しかし意図したようには動作しない。どうもコマンドを実行しているカレントディレクトリと、ソースファイルがあるディレクトリは異なるので、そのせいかもしれないなと思った。なのでコンパイルするコマンドを以下のように書き換えた。
// buildEjs.mjs
import ejs from 'ejs';
import fs from 'fs';
import path from 'path';
(async ()=> {
const src = process.argv[2];
const dest = process.argv[3];
let template = await fs.promises.readFile(src,'utf8');
// カレント・ディレクトリのバックアップ
let cwd = process.cwd();
// カレント・ディレクトリをsrcのディレクトリに切り替え
process.chdir(path.dirname(src));
console.info(`ejs.render:${src} => ${dest}`);
let html = ejs.render(template);
// カレント・ディレクトリをもとに戻す
process.chdir(cwd);
await fs.promises.writeFile(dest,html,'utf8');
})();
実行してみると同じエラーが発生する。
(node:1871) UnhandledPromiseRejectionWarning: Error: ejs:5
3| <html lang="ja">
4| <head>
>> 5| <%-include('header.ejs',{data:data}) %>
6| </head>
7| <body>
8| <body>
Could not find the include file "header.ejs"
at getIncludePath (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:165:13)
at includeFile (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:291:19)
at include (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:680:16)
at eval (eval at compile (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:652:12), <anonymous>:11:16)
at anonymous (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:682:17)
at Object.exports.render (/home/sfpg/pj/www/node/scms/node_modules/ejs/lib/ejs.js:419:37)
at file:///home/sfpg/pj/www/node/scms/tools/buildEjs.mjs:13:18
(node:1871) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This e
これはおかしい。ということでスタックトレースを参考にejsのインクルードの動きを追ってみることにした。おそらくはinclude()
関数をあたりを見てみればわかるだろうと思ってデバッガでステップ実行してみた。
path
にはheader.ejs
が入っている。そしてincludeFile()
を見てみる。
本丸らしきgetIncludePath(path, opts);
が見えてきたのでトレースを進める。以下はgetIncludePath(path,opts)
のソースである。
/**
* Get the path to the included file by Options
*
* @param {String} path specified path
* @param {Options} options compilation options
* @return {String}
*/
function getIncludePath(path, options) {
var includePath;
var filePath;
var views = options.views;
var match = /^[A-Za-z]+:\\|^\//.exec(path);
// Abs path
if (match && match.length) {
includePath = exports.resolveInclude(path.replace(/^\/*/,''), options.root || '/', true);
}
// Relative paths
else {
// Look relative to a passed filename first
if (options.filename) {
filePath = exports.resolveInclude(path, options.filename);
if (fs.existsSync(filePath)) {
includePath = filePath;
}
}
// Then look in any views directories
if (!includePath) {
if (Array.isArray(views) && views.some(function (v) {
filePath = exports.resolveInclude(path, v, true);
return fs.existsSync(filePath);
})) {
includePath = filePath;
}
}
if (!includePath) {
throw new Error('Could not find the include file "' +
options.escapeFunction(path) + '"');
}
}
return includePath;
}
path
は相対パスなので意図したように進んでいく。がここで??な部分が出現。options.filename
があればresolveInclude()
を実行してfilePath
を取得するようになっている。現状options.filename
は設定していないのでここはスキップされる。
そして以下に進むのだが、includePath
はundefined
なので最初のif(!includePath)
以下に進む。がoptions.views
を設定していないのでここでもresolveInclude()
は呼ばれず、最終的にincludePath
には何も値が設定されずエラーとなってしまっている。
if (!includePath) {
if (Array.isArray(views) && views.some(function (v) {
filePath = exports.resolveInclude(path, v, true);
return fs.existsSync(filePath);
})) {
includePath = filePath;
}
}
if (!includePath) {
throw new Error('Could not find the include file "' +
options.escapeFunction(path) + '"');
}
どうもoptions.filename
もしくはoptions.views
に何か値を設定しないとエラーになってしまうようである。ejsのドキュメントで両パラメータを確認してみる。filename
については以下の記載があった。
filename Used by cache to key caches, and for includes
キャッシュのキーおよびインクルード用に使用されるとある。options.views
についてはドキュメントに記載がなかった。views
に関してはソースを見るに、ejsファイルパスの配列をセットするようである。
ソースをみるとfilename
に親ソースのファイル名を追加すれば、そこを起点にファイルパス解決を行ってくれそうなので、buildEjs.mjs
にオプションを追加してみた。
// buildEjs.mjs
import ejs from 'ejs';
import fs from 'fs';
import path from 'path';
(async ()=> {
const src = process.argv[2];
const dest = process.argv[3];
let template = await fs.promises.readFile(src,'utf8')
let cwd = process.cwd();
process.chdir(path.dirname(src));
console.info(`ejs.render:${src} => ${dest}`);
// オプションを追加
let html = ejs.render(template,{filename:path.basename(src)});
process.chdir(cwd);
await fs.promises.writeFile(dest,html,'utf8');
})();
これでようやく意図した動きとなった。がfilename
オプションを指定しないとインクルードファイルのパス解決が行われないのはちょっと変な仕様だなあと思った。。