@ywal123456/jskim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/bin/jskim.js +105 -0
- package/docs/configuration.md +322 -0
- package/docs/create-jskim.md +80 -0
- package/docs/publishing.md +134 -0
- package/package.json +57 -0
- package/scripts/build.js +13 -0
- package/scripts/commands/build-command.js +29 -0
- package/scripts/commands/dev-command.js +164 -0
- package/scripts/commands/help-text.js +46 -0
- package/scripts/commands/path-display.js +21 -0
- package/scripts/commands/register-shutdown.js +32 -0
- package/scripts/commands/serve-command.js +88 -0
- package/scripts/commands/serve-errors.js +82 -0
- package/scripts/commands/watch-command.js +71 -0
- package/scripts/dev.js +13 -0
- package/scripts/lib/build-project.js +106 -0
- package/scripts/lib/clean-output.js +119 -0
- package/scripts/lib/copy-files.js +116 -0
- package/scripts/lib/create-live-reload.js +201 -0
- package/scripts/lib/create-nunjucks-env.js +61 -0
- package/scripts/lib/create-project-watcher.js +272 -0
- package/scripts/lib/create-static-server.js +351 -0
- package/scripts/lib/load-config.js +64 -0
- package/scripts/lib/merge-config.js +130 -0
- package/scripts/lib/render-pages.js +121 -0
- package/scripts/lib/resolve-project.js +198 -0
- package/scripts/lib/resolve-watch-paths.js +134 -0
- package/scripts/serve.js +13 -0
- package/scripts/watch.js +13 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const fse = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ビルド前に outputDir だけを安全に削除します。
|
|
9
|
+
*
|
|
10
|
+
* 次のパスは削除しません:
|
|
11
|
+
* - ファイルシステムルート
|
|
12
|
+
* - ワークスペースルート
|
|
13
|
+
* - sourceDir
|
|
14
|
+
* - sourceDir の祖先
|
|
15
|
+
* - 空 / 不正なパス
|
|
16
|
+
*
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {string} options.outputDir
|
|
19
|
+
* @param {string} options.sourceDir
|
|
20
|
+
* @param {string} options.workspaceRoot
|
|
21
|
+
* @param {string} options.projectName
|
|
22
|
+
*/
|
|
23
|
+
async function cleanOutput({
|
|
24
|
+
outputDir,
|
|
25
|
+
sourceDir,
|
|
26
|
+
workspaceRoot,
|
|
27
|
+
projectName,
|
|
28
|
+
}) {
|
|
29
|
+
const resolvedOutput = path.resolve(outputDir);
|
|
30
|
+
const resolvedSource = path.resolve(sourceDir);
|
|
31
|
+
const resolvedWorkspace = path.resolve(workspaceRoot);
|
|
32
|
+
|
|
33
|
+
assertSafeToClean({
|
|
34
|
+
outputDir: resolvedOutput,
|
|
35
|
+
sourceDir: resolvedSource,
|
|
36
|
+
workspaceRoot: resolvedWorkspace,
|
|
37
|
+
projectName,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(resolvedOutput)) {
|
|
41
|
+
await fse.remove(resolvedOutput);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertSafeToClean({
|
|
46
|
+
outputDir,
|
|
47
|
+
sourceDir,
|
|
48
|
+
workspaceRoot,
|
|
49
|
+
projectName,
|
|
50
|
+
}) {
|
|
51
|
+
if (!outputDir || String(outputDir).trim() === '') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[JSKim] クリーンを拒否しました: outputDir が空です。\n` +
|
|
54
|
+
`プロジェクト: ${projectName}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parsed = path.parse(outputDir);
|
|
59
|
+
// ファイルシステムルート(例: C:\ や /)
|
|
60
|
+
if (outputDir === parsed.root || outputDir === path.sep) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[JSKim] クリーンを拒否しました: outputDir がファイルシステムルートです。\n` +
|
|
63
|
+
`プロジェクト: ${projectName}\n` +
|
|
64
|
+
`パス: ${outputDir}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (samePath(outputDir, workspaceRoot)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[JSKim] クリーンを拒否しました: outputDir がワークスペースルートです。\n` +
|
|
71
|
+
`プロジェクト: ${projectName}\n` +
|
|
72
|
+
`パス: ${outputDir}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (samePath(outputDir, sourceDir)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`[JSKim] クリーンを拒否しました: outputDir が sourceDir と同じです。\n` +
|
|
79
|
+
`プロジェクト: ${projectName}\n` +
|
|
80
|
+
`パス: ${outputDir}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isAncestorPath(outputDir, sourceDir)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[JSKim] クリーンを拒否しました: outputDir が sourceDir の祖先です。\n` +
|
|
87
|
+
`プロジェクト: ${projectName}\n` +
|
|
88
|
+
`outputDir: ${outputDir}\n` +
|
|
89
|
+
`sourceDir: ${sourceDir}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function samePath(a, b) {
|
|
95
|
+
const na = path.resolve(a);
|
|
96
|
+
const nb = path.resolve(b);
|
|
97
|
+
if (process.platform === 'win32') {
|
|
98
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
return na === nb;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ancestor が descendant の厳密な祖先ディレクトリなら true。
|
|
105
|
+
*/
|
|
106
|
+
function isAncestorPath(ancestor, descendant) {
|
|
107
|
+
const a = path.resolve(ancestor);
|
|
108
|
+
const d = path.resolve(descendant);
|
|
109
|
+
if (samePath(a, d)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const rel = path.relative(a, d);
|
|
113
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
cleanOutput,
|
|
118
|
+
assertSafeToClean,
|
|
119
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fse = require('fs-extra');
|
|
5
|
+
const fg = require('fast-glob');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* copy ルールに従って静的ファイルをコピーします。
|
|
9
|
+
* copy.from が無い場合は警告して続行します。
|
|
10
|
+
*
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* @param {object} options.project
|
|
13
|
+
* @returns {Promise<{ copiedCount: number, files: string[] }>}
|
|
14
|
+
*/
|
|
15
|
+
async function copyFiles({ project }) {
|
|
16
|
+
const { name, sourceDir, outputDir, copy } = project;
|
|
17
|
+
const rules = Array.isArray(copy) ? copy : [];
|
|
18
|
+
let copiedCount = 0;
|
|
19
|
+
const files = [];
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < rules.length; i += 1) {
|
|
22
|
+
const rule = rules[i];
|
|
23
|
+
|
|
24
|
+
if (!rule || typeof rule !== 'object') {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[JSKim] プロジェクト "${name}" の copy[${i}] が不正です。\n` +
|
|
27
|
+
`原因: 各 copy ルールは from/to を持つオブジェクトである必要があります。`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[JSKim] プロジェクト "${name}" の copy[${i}].from が不正です。\n` +
|
|
34
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fromDir = path.resolve(sourceDir, rule.from);
|
|
39
|
+
const toDir = path.resolve(outputDir, rule.to || '');
|
|
40
|
+
|
|
41
|
+
if (!(await fse.pathExists(fromDir))) {
|
|
42
|
+
console.warn(
|
|
43
|
+
`[JSKim] 警告: copy[${i}].from が存在しないためスキップします。\n` +
|
|
44
|
+
` プロジェクト: ${name}\n` +
|
|
45
|
+
` パス: ${fromDir}\n` +
|
|
46
|
+
` 設定: copy[${i}].from = ${rule.from}`
|
|
47
|
+
);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stat = await fse.stat(fromDir);
|
|
52
|
+
|
|
53
|
+
if (stat.isFile()) {
|
|
54
|
+
try {
|
|
55
|
+
await fse.ensureDir(path.dirname(toDir));
|
|
56
|
+
await fse.copy(fromDir, toDir, { overwrite: true });
|
|
57
|
+
copiedCount += 1;
|
|
58
|
+
files.push(toDir);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const message = err && err.message ? err.message : String(err);
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[JSKim] プロジェクト "${name}" のファイルコピーに失敗しました。\n` +
|
|
63
|
+
`From: ${fromDir}\n` +
|
|
64
|
+
`To: ${toDir}\n` +
|
|
65
|
+
`原因: ${message}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!stat.isDirectory()) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[JSKim] 警告: copy[${i}].from はファイルでもディレクトリでもないためスキップします。\n` +
|
|
74
|
+
` プロジェクト: ${name}\n` +
|
|
75
|
+
` パス: ${fromDir}`
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const matches = await fg('**/*', {
|
|
81
|
+
cwd: fromDir,
|
|
82
|
+
onlyFiles: true,
|
|
83
|
+
dot: false,
|
|
84
|
+
absolute: false,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
matches.sort();
|
|
88
|
+
|
|
89
|
+
for (const relativeMatch of matches) {
|
|
90
|
+
const srcFile = path.join(fromDir, relativeMatch);
|
|
91
|
+
const destFile = path.join(toDir, relativeMatch);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await fse.ensureDir(path.dirname(destFile));
|
|
95
|
+
await fse.copy(srcFile, destFile, { overwrite: true });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const message = err && err.message ? err.message : String(err);
|
|
98
|
+
throw new Error(
|
|
99
|
+
`[JSKim] プロジェクト "${name}" のアセットコピーに失敗しました。\n` +
|
|
100
|
+
`From: ${srcFile}\n` +
|
|
101
|
+
`To: ${destFile}\n` +
|
|
102
|
+
`原因: ${message}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
copiedCount += 1;
|
|
107
|
+
files.push(destFile);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { copiedCount, files };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
copyFiles,
|
|
116
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LIVE_RELOAD_PATH = '/_jskim/live-reload';
|
|
4
|
+
const HEARTBEAT_MS = 20000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SSE ベースのライブリロード管理です。
|
|
8
|
+
* dist には書き込まず、dev の HTML レスポンス注入と SSE 配信だけを担当します。
|
|
9
|
+
*
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* @param {string} options.projectName
|
|
12
|
+
* @param {boolean} [options.enabled=true]
|
|
13
|
+
*/
|
|
14
|
+
function createLiveReload({ projectName, enabled = true }) {
|
|
15
|
+
const clients = new Set();
|
|
16
|
+
let heartbeatTimer = null;
|
|
17
|
+
let closed = false;
|
|
18
|
+
|
|
19
|
+
function ensureHeartbeat() {
|
|
20
|
+
if (!enabled || heartbeatTimer || closed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
heartbeatTimer = setInterval(() => {
|
|
24
|
+
for (const res of [...clients]) {
|
|
25
|
+
try {
|
|
26
|
+
res.write(': heartbeat\n\n');
|
|
27
|
+
} catch {
|
|
28
|
+
removeClient(res);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}, HEARTBEAT_MS);
|
|
32
|
+
if (typeof heartbeatTimer.unref === 'function') {
|
|
33
|
+
heartbeatTimer.unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeClient(res) {
|
|
38
|
+
if (!clients.has(res)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
clients.delete(res);
|
|
42
|
+
try {
|
|
43
|
+
res.end();
|
|
44
|
+
} catch {
|
|
45
|
+
// 切断済み
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 内部 SSE リクエストを処理します。
|
|
51
|
+
* @returns {boolean} 処理したら true
|
|
52
|
+
*/
|
|
53
|
+
function handleRequest(req, res, context = {}) {
|
|
54
|
+
if (!enabled || closed) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pathname = context.pathname || '';
|
|
59
|
+
if (pathname !== LIVE_RELOAD_PATH) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
64
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
65
|
+
res.statusCode = 405;
|
|
66
|
+
res.setHeader('Allow', 'GET, HEAD');
|
|
67
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
68
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
69
|
+
const body = Buffer.from('このHTTPメソッドは使用できません。\n', 'utf8');
|
|
70
|
+
res.setHeader('Content-Length', String(body.length));
|
|
71
|
+
if (method === 'HEAD') {
|
|
72
|
+
res.end();
|
|
73
|
+
} else {
|
|
74
|
+
res.end(body);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.statusCode = 200;
|
|
80
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
81
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
82
|
+
res.setHeader('Connection', 'keep-alive');
|
|
83
|
+
// nginx 等のバッファリング回避(ローカルでも無害)
|
|
84
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
85
|
+
|
|
86
|
+
if (method === 'HEAD') {
|
|
87
|
+
res.end();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
res.write(': connected\n\n');
|
|
92
|
+
clients.add(res);
|
|
93
|
+
ensureHeartbeat();
|
|
94
|
+
|
|
95
|
+
const onClose = () => {
|
|
96
|
+
removeClient(res);
|
|
97
|
+
};
|
|
98
|
+
req.on('close', onClose);
|
|
99
|
+
res.on('close', onClose);
|
|
100
|
+
res.on('error', onClose);
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getClientScript() {
|
|
106
|
+
return [
|
|
107
|
+
'<script>',
|
|
108
|
+
'(function () {',
|
|
109
|
+
' try {',
|
|
110
|
+
` var source = new EventSource(${JSON.stringify(LIVE_RELOAD_PATH)});`,
|
|
111
|
+
" source.addEventListener('reload', function () {",
|
|
112
|
+
' window.location.reload();',
|
|
113
|
+
' });',
|
|
114
|
+
' source.onerror = function () {',
|
|
115
|
+
" console.info('[JSKim] ライブリロード接続を再試行しています…');",
|
|
116
|
+
' };',
|
|
117
|
+
' } catch (err) {',
|
|
118
|
+
" console.warn('[JSKim] ライブリロードを開始できませんでした。');",
|
|
119
|
+
' }',
|
|
120
|
+
'})();',
|
|
121
|
+
'</script>',
|
|
122
|
+
].join('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* HTML 文字列に client script を注入します(メモリ上のみ)。
|
|
127
|
+
* @param {string} html
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function injectHtml(html) {
|
|
131
|
+
if (!enabled || typeof html !== 'string') {
|
|
132
|
+
return html;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const script = getClientScript();
|
|
136
|
+
const bodyClose = html.search(/<\/body>/i);
|
|
137
|
+
if (bodyClose !== -1) {
|
|
138
|
+
return html.slice(0, bodyClose) + script + html.slice(bodyClose);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const htmlClose = html.search(/<\/html>/i);
|
|
142
|
+
if (htmlClose !== -1) {
|
|
143
|
+
return html.slice(0, htmlClose) + script + html.slice(htmlClose);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return html + script;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function broadcastReload() {
|
|
150
|
+
if (!enabled || closed) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const payload = `event: reload\ndata: ${JSON.stringify({
|
|
155
|
+
project: projectName,
|
|
156
|
+
})}\n\n`;
|
|
157
|
+
|
|
158
|
+
for (const res of [...clients]) {
|
|
159
|
+
try {
|
|
160
|
+
res.write(payload);
|
|
161
|
+
} catch {
|
|
162
|
+
removeClient(res);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function close() {
|
|
168
|
+
if (closed) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
closed = true;
|
|
172
|
+
|
|
173
|
+
if (heartbeatTimer) {
|
|
174
|
+
clearInterval(heartbeatTimer);
|
|
175
|
+
heartbeatTimer = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const res of [...clients]) {
|
|
179
|
+
removeClient(res);
|
|
180
|
+
}
|
|
181
|
+
clients.clear();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
enabled: Boolean(enabled),
|
|
186
|
+
path: LIVE_RELOAD_PATH,
|
|
187
|
+
handleRequest,
|
|
188
|
+
injectHtml,
|
|
189
|
+
broadcastReload,
|
|
190
|
+
getClientScript,
|
|
191
|
+
close,
|
|
192
|
+
get clientCount() {
|
|
193
|
+
return clients.size;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
createLiveReload,
|
|
200
|
+
LIVE_RELOAD_PATH,
|
|
201
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const nunjucks = require('nunjucks');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* プロジェクト用の Nunjucks 環境を作成します。
|
|
9
|
+
* loader ルート: sourceDir + templates[](重複除去)。
|
|
10
|
+
*
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* @param {string} options.sourceDir
|
|
13
|
+
* @param {string[]} options.templates
|
|
14
|
+
* @returns {nunjucks.Environment}
|
|
15
|
+
*/
|
|
16
|
+
function createNunjucksEnv({ sourceDir, templates }) {
|
|
17
|
+
const roots = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
|
|
20
|
+
function addRoot(absPath) {
|
|
21
|
+
const normalized = path.resolve(absPath);
|
|
22
|
+
const key = normalized.toLowerCase();
|
|
23
|
+
if (seen.has(key)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
seen.add(key);
|
|
30
|
+
roots.push(normalized);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addRoot(sourceDir);
|
|
34
|
+
|
|
35
|
+
const templateDirs = Array.isArray(templates) ? templates : [];
|
|
36
|
+
for (const rel of templateDirs) {
|
|
37
|
+
addRoot(path.resolve(sourceDir, rel));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (roots.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[JSKim] 有効な Nunjucks loader パスがありません。\n` +
|
|
43
|
+
`sourceDir: ${sourceDir}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const loader = new nunjucks.FileSystemLoader(roots, {
|
|
48
|
+
noCache: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const env = new nunjucks.Environment(loader, {
|
|
52
|
+
autoescape: true,
|
|
53
|
+
noCache: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return env;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
createNunjucksEnv,
|
|
61
|
+
};
|