ejsのincludeにおける不可解なエラー

公開:2020-03-06 07:21
更新:2020-03-06 07:53
カテゴリ:Webサイトリニューアル,ejs

ビルド環境を構築中に、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.ejsindex.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()関数をあたりを見てみればわかるだろうと思ってデバッガでステップ実行してみた。

20200306-01

pathにはheader.ejsが入っている。そしてincludeFile()を見てみる。

20200306-02

本丸らしき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は設定していないのでここはスキップされる。

20200306-03

そして以下に進むのだが、includePathundefinedなので最初の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オプションを指定しないとインクルードファイルのパス解決が行われないのはちょっと変な仕様だなあと思った。。