@ywal123456/jskim 0.1.1 → 0.3.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/README.md +113 -145
- package/docs/configuration.md +217 -175
- package/docs/create-jskim.md +22 -7
- package/package.json +1 -1
- package/scripts/commands/dev-command.js +10 -109
- package/scripts/commands/watch-command.js +15 -25
- package/scripts/lib/build-project.js +18 -12
- package/scripts/lib/create-nunjucks-env.js +39 -1
- package/scripts/lib/create-watch-runtime.js +516 -0
- package/scripts/lib/merge-config.js +87 -2
- package/scripts/lib/process-files.js +263 -0
- package/scripts/lib/render-pages.js +17 -1
- package/scripts/lib/resolve-project.js +152 -28
- package/scripts/lib/resolve-watch-paths.js +31 -13
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const fse = require('fs-extra');
|
|
6
|
+
const fg = require('fast-glob');
|
|
7
|
+
const { computeRootPath } = require('./render-pages');
|
|
8
|
+
|
|
9
|
+
const RESERVED_CONTEXT_KEYS = new Set(['rootPath']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* files pipeline を実行します。
|
|
13
|
+
* *.njk → render(末尾 .njk のみ削除)
|
|
14
|
+
* それ以外 → byte copy
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {import('nunjucks').Environment} options.env
|
|
18
|
+
* @param {object} options.project
|
|
19
|
+
* @returns {Promise<{ renderedCount: number, copiedCount: number, files: string[] }>}
|
|
20
|
+
*/
|
|
21
|
+
async function processFiles({ env, project }) {
|
|
22
|
+
const { name, sourceDir, outputDir, files, templates, data } = project;
|
|
23
|
+
const rules = Array.isArray(files) ? files : [];
|
|
24
|
+
const templateRoots = resolveTemplateRoots(sourceDir, templates);
|
|
25
|
+
|
|
26
|
+
assertNoReservedDataCollision(data, name);
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, { sourceFile: string, outputFile: string, kind: string, relative: string, ruleIndex: number }>} */
|
|
29
|
+
const planned = new Map();
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < rules.length; i += 1) {
|
|
32
|
+
const rule = normalizeFilesRule(rules[i], name, i);
|
|
33
|
+
const fromDir = path.resolve(sourceDir, rule.from);
|
|
34
|
+
const toDir = path.resolve(outputDir, rule.to);
|
|
35
|
+
|
|
36
|
+
assertInside(sourceDir, fromDir, name, `files[${i}].from`);
|
|
37
|
+
assertInside(outputDir, toDir, name, `files[${i}].to`);
|
|
38
|
+
|
|
39
|
+
if (!(await fse.pathExists(fromDir))) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[JSKim] プロジェクト "${name}" の files[${i}].from が存在しません。\n` +
|
|
42
|
+
`パス: ${fromDir}\n` +
|
|
43
|
+
`設定: files[${i}].from = ${rule.from}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const matches = await fg(rule.include, {
|
|
48
|
+
cwd: fromDir,
|
|
49
|
+
onlyFiles: true,
|
|
50
|
+
dot: false,
|
|
51
|
+
absolute: false,
|
|
52
|
+
ignore: rule.exclude,
|
|
53
|
+
});
|
|
54
|
+
matches.sort();
|
|
55
|
+
|
|
56
|
+
for (const relativeMatch of matches) {
|
|
57
|
+
const normalizedRel = relativeMatch.split(path.sep).join('/');
|
|
58
|
+
const sourceFile = path.join(fromDir, relativeMatch);
|
|
59
|
+
|
|
60
|
+
if (isInsideAny(templateRoots, sourceFile)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isNjk = normalizedRel.endsWith('.njk');
|
|
65
|
+
const outRel = isNjk
|
|
66
|
+
? stripTrailingNjk(normalizedRel)
|
|
67
|
+
: normalizedRel;
|
|
68
|
+
const outputFile = path.resolve(toDir, outRel.split('/').join(path.sep));
|
|
69
|
+
|
|
70
|
+
assertInside(outputDir, outputFile, name, `files[${i}] output`);
|
|
71
|
+
|
|
72
|
+
const collisionKey =
|
|
73
|
+
process.platform === 'win32'
|
|
74
|
+
? outputFile.toLowerCase()
|
|
75
|
+
: outputFile;
|
|
76
|
+
|
|
77
|
+
if (planned.has(collisionKey)) {
|
|
78
|
+
const previous = planned.get(collisionKey);
|
|
79
|
+
throw new Error(
|
|
80
|
+
`[JSKim] 出力パスが衝突しています。\n` +
|
|
81
|
+
`プロジェクト: ${name}\n` +
|
|
82
|
+
`出力: ${toDisplay(outputFile, project.workspaceRoot)}\n` +
|
|
83
|
+
`ソース1: ${toDisplay(previous.sourceFile, project.workspaceRoot)}\n` +
|
|
84
|
+
`ソース2: ${toDisplay(sourceFile, project.workspaceRoot)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
planned.set(collisionKey, {
|
|
89
|
+
sourceFile,
|
|
90
|
+
outputFile,
|
|
91
|
+
kind: isNjk ? 'render' : 'copy',
|
|
92
|
+
relative: normalizedRel,
|
|
93
|
+
ruleIndex: i,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let renderedCount = 0;
|
|
99
|
+
let copiedCount = 0;
|
|
100
|
+
const outputFiles = [];
|
|
101
|
+
|
|
102
|
+
for (const item of planned.values()) {
|
|
103
|
+
await fse.ensureDir(path.dirname(item.outputFile));
|
|
104
|
+
|
|
105
|
+
if (item.kind === 'copy') {
|
|
106
|
+
await fse.copy(item.sourceFile, item.outputFile, { overwrite: true });
|
|
107
|
+
copiedCount += 1;
|
|
108
|
+
outputFiles.push(item.outputFile);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const templatePath = path
|
|
113
|
+
.relative(sourceDir, item.sourceFile)
|
|
114
|
+
.split(path.sep)
|
|
115
|
+
.join('/');
|
|
116
|
+
const rootPath = computeRootPath(item.outputFile, outputDir);
|
|
117
|
+
const context = buildRenderContext(data, rootPath);
|
|
118
|
+
|
|
119
|
+
let text;
|
|
120
|
+
try {
|
|
121
|
+
text = env.render(templatePath, context);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = err && err.message ? err.message : String(err);
|
|
124
|
+
throw new Error(
|
|
125
|
+
`[JSKim] プロジェクト "${name}" の Nunjucks レンダリングに失敗しました。\n` +
|
|
126
|
+
`ソース: ${item.sourceFile}\n` +
|
|
127
|
+
`テンプレート: ${templatePath}\n` +
|
|
128
|
+
`原因: ${message}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await fse.writeFile(item.outputFile, text, 'utf8');
|
|
133
|
+
renderedCount += 1;
|
|
134
|
+
outputFiles.push(item.outputFile);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { renderedCount, copiedCount, files: outputFiles };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeFilesRule(rule, projectName, index) {
|
|
141
|
+
if (!rule || typeof rule !== 'object') {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`[JSKim] プロジェクト "${projectName}" の files[${index}] が不正です。\n` +
|
|
144
|
+
`原因: 各 files ルールはオブジェクトである必要があります。`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`[JSKim] プロジェクト "${projectName}" の files[${index}].from が不正です。\n` +
|
|
150
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const include = Array.isArray(rule.include) ? rule.include : ['**/*'];
|
|
155
|
+
if (include.length === 0) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[JSKim] プロジェクト "${projectName}" の files[${index}].include が不正です。\n` +
|
|
158
|
+
`原因: include は空でない glob 配列である必要があります。`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const exclude = Array.isArray(rule.exclude) ? rule.exclude : [];
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
from: String(rule.from).trim(),
|
|
166
|
+
to: rule.to == null ? '' : String(rule.to),
|
|
167
|
+
include: [...include],
|
|
168
|
+
exclude: [...exclude],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function stripTrailingNjk(relativePath) {
|
|
173
|
+
if (relativePath.endsWith('.njk')) {
|
|
174
|
+
return relativePath.slice(0, -4);
|
|
175
|
+
}
|
|
176
|
+
return relativePath;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveTemplateRoots(sourceDir, templates) {
|
|
180
|
+
const roots = [];
|
|
181
|
+
for (const rel of Array.isArray(templates) ? templates : []) {
|
|
182
|
+
if (!rel) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const abs = path.resolve(sourceDir, rel);
|
|
186
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
187
|
+
roots.push(abs);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return roots;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isInsideAny(roots, candidate) {
|
|
194
|
+
for (const root of roots) {
|
|
195
|
+
if (isInsideOrSame(root, candidate)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isInsideOrSame(parent, child) {
|
|
203
|
+
const p = path.resolve(parent);
|
|
204
|
+
const c = path.resolve(child);
|
|
205
|
+
if (samePath(p, c)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const rel = path.relative(p, c);
|
|
209
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function samePath(a, b) {
|
|
213
|
+
const na = path.resolve(a);
|
|
214
|
+
const nb = path.resolve(b);
|
|
215
|
+
if (process.platform === 'win32') {
|
|
216
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
217
|
+
}
|
|
218
|
+
return na === nb;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assertInside(root, candidate, projectName, label) {
|
|
222
|
+
if (!isInsideOrSame(root, candidate)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`[JSKim] プロジェクト "${projectName}" の ${label} が許可範囲外です。\n` +
|
|
225
|
+
`ルート: ${root}\n` +
|
|
226
|
+
`対象: ${candidate}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function assertNoReservedDataCollision(data, projectName) {
|
|
232
|
+
const keys = Object.keys(data || {});
|
|
233
|
+
for (const key of keys) {
|
|
234
|
+
if (RESERVED_CONTEXT_KEYS.has(key)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`[JSKim] data のキーが予約語と衝突しています: ${key}\n` +
|
|
237
|
+
`プロジェクト: ${projectName}\n` +
|
|
238
|
+
`予約キー: ${[...RESERVED_CONTEXT_KEYS].join(', ')}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildRenderContext(data, rootPath) {
|
|
245
|
+
return {
|
|
246
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
247
|
+
rootPath,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toDisplay(abs, workspaceRoot) {
|
|
252
|
+
const rel = path.relative(workspaceRoot, abs);
|
|
253
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
254
|
+
return abs.split(path.sep).join('/');
|
|
255
|
+
}
|
|
256
|
+
return rel.split(path.sep).join('/');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
processFiles,
|
|
261
|
+
stripTrailingNjk,
|
|
262
|
+
RESERVED_CONTEXT_KEYS,
|
|
263
|
+
};
|
|
@@ -81,10 +81,26 @@ async function renderPages({ env, project }) {
|
|
|
81
81
|
.join('/');
|
|
82
82
|
|
|
83
83
|
const rootPath = computeRootPath(outputFile, outputDir);
|
|
84
|
+
const context = {
|
|
85
|
+
...(project.data && typeof project.data === 'object'
|
|
86
|
+
? project.data
|
|
87
|
+
: {}),
|
|
88
|
+
rootPath,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
project.data &&
|
|
93
|
+
Object.prototype.hasOwnProperty.call(project.data, 'rootPath')
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`[JSKim] data のキーが予約語と衝突しています: rootPath\n` +
|
|
97
|
+
`プロジェクト: ${name}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
let html;
|
|
86
102
|
try {
|
|
87
|
-
html = env.render(templatePath,
|
|
103
|
+
html = env.render(templatePath, context);
|
|
88
104
|
} catch (err) {
|
|
89
105
|
const message = err && err.message ? err.message : String(err);
|
|
90
106
|
throw new Error(
|
|
@@ -12,7 +12,7 @@ const { mergeConfig } = require('./merge-config');
|
|
|
12
12
|
* @param {string} options.workspaceRoot
|
|
13
13
|
* @param {string|undefined} options.projectName
|
|
14
14
|
* @param {string} [options.commandName='build']
|
|
15
|
-
* @param {string} [options.usageLine]
|
|
15
|
+
* @param {string} [options.usageLine]
|
|
16
16
|
* @returns {object} 解決済みプロジェクト
|
|
17
17
|
*/
|
|
18
18
|
function resolveProject({
|
|
@@ -45,7 +45,8 @@ function resolveProject({
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const
|
|
48
|
+
const defaults = config.defaults || {};
|
|
49
|
+
const merged = mergeConfig(defaults, projectConfig);
|
|
49
50
|
|
|
50
51
|
if (!merged.sourceDir || String(merged.sourceDir).trim() === '') {
|
|
51
52
|
throw new Error(
|
|
@@ -79,39 +80,42 @@ function resolveProject({
|
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
validateData(merged.data, name);
|
|
84
|
+
validateNunjucks(merged.nunjucks, name);
|
|
85
|
+
|
|
86
|
+
const hasFiles = Array.isArray(merged.files) && merged.files.length > 0;
|
|
87
|
+
const hasRender = Array.isArray(merged.render) && merged.render.length > 0;
|
|
88
|
+
const hasCopy = Array.isArray(merged.copy) && merged.copy.length > 0;
|
|
89
|
+
|
|
90
|
+
if (hasFiles && (hasRender || hasCopy)) {
|
|
91
|
+
const conflict = [];
|
|
92
|
+
if (hasRender) {
|
|
93
|
+
conflict.push('render');
|
|
94
|
+
}
|
|
95
|
+
if (hasCopy) {
|
|
96
|
+
conflict.push('copy');
|
|
97
|
+
}
|
|
83
98
|
throw new Error(
|
|
84
|
-
`[JSKim]
|
|
85
|
-
|
|
99
|
+
`[JSKim] files と ${conflict.join(' / ')} を同時に設定できません。\n` +
|
|
100
|
+
`プロジェクト: ${name}\n` +
|
|
101
|
+
`files mode を使う場合は render / copy を空にしてください。\n` +
|
|
102
|
+
`legacy mode を使う場合は files を設定しないでください。`
|
|
86
103
|
);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
if (!rule.from || String(rule.from).trim() === '') {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`[JSKim] プロジェクト "${name}" の render[${i}].from が不正です。\n` +
|
|
100
|
-
`原因: from は必須です(sourceDir 基準)。`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(rule.include) || rule.include.length === 0) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`[JSKim] プロジェクト "${name}" の render[${i}].include が不正です。\n` +
|
|
106
|
-
`原因: include は空でない glob パターン配列である必要があります。`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (!rule.extension || String(rule.extension).trim() === '') {
|
|
106
|
+
const pipelineMode = hasFiles ? 'files' : 'legacy';
|
|
107
|
+
|
|
108
|
+
if (pipelineMode === 'files') {
|
|
109
|
+
validateFilesRules(merged.files, name);
|
|
110
|
+
} else {
|
|
111
|
+
if (!hasRender) {
|
|
110
112
|
throw new Error(
|
|
111
|
-
`[JSKim] プロジェクト "${name}" の render
|
|
112
|
-
|
|
113
|
+
`[JSKim] プロジェクト "${name}" の render 設定が無い、または空です。\n` +
|
|
114
|
+
`jskim.config.js の defaults.render または projects.${name}.render を定義してください。\n` +
|
|
115
|
+
`新しい files pipeline を使う場合は defaults.files を設定してください。`
|
|
113
116
|
);
|
|
114
117
|
}
|
|
118
|
+
validateRenderRules(merged.render, name);
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
validateWatchConfig(merged.watch, name);
|
|
@@ -124,9 +128,13 @@ function resolveProject({
|
|
|
124
128
|
outputDir,
|
|
125
129
|
sourceDirConfig: merged.sourceDir,
|
|
126
130
|
outputDirConfig: merged.outputDir,
|
|
131
|
+
pipelineMode,
|
|
127
132
|
render: merged.render,
|
|
128
133
|
templates: merged.templates,
|
|
129
134
|
copy: merged.copy,
|
|
135
|
+
files: merged.files || [],
|
|
136
|
+
data: merged.data,
|
|
137
|
+
nunjucks: merged.nunjucks,
|
|
130
138
|
build: merged.build,
|
|
131
139
|
watch: merged.watch,
|
|
132
140
|
serve: merged.serve,
|
|
@@ -135,6 +143,122 @@ function resolveProject({
|
|
|
135
143
|
};
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
function validateFilesRules(files, projectName) {
|
|
147
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
148
|
+
const rule = files[i];
|
|
149
|
+
if (!rule || typeof rule !== 'object') {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}] が不正です。\n` +
|
|
152
|
+
`原因: 各 files ルールはオブジェクトである必要があります。`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].from が不正です。\n` +
|
|
158
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (rule.include !== undefined) {
|
|
162
|
+
if (!Array.isArray(rule.include) || rule.include.length === 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].include が不正です。\n` +
|
|
165
|
+
`原因: include は空でない glob 配列である必要があります。`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (rule.exclude !== undefined && !Array.isArray(rule.exclude)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].exclude が不正です。\n` +
|
|
172
|
+
`原因: exclude は配列である必要があります。`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateRenderRules(render, projectName) {
|
|
179
|
+
for (let i = 0; i < render.length; i += 1) {
|
|
180
|
+
const rule = render[i];
|
|
181
|
+
if (!rule || typeof rule !== 'object') {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}] が不正です。\n` +
|
|
184
|
+
`原因: 各 render ルールはオブジェクトである必要があります。`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].from が不正です。\n` +
|
|
190
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(rule.include) || rule.include.length === 0) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].include が不正です。\n` +
|
|
196
|
+
`原因: include は空でない glob パターン配列である必要があります。`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (!rule.extension || String(rule.extension).trim() === '') {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].extension が不正です。\n` +
|
|
202
|
+
`原因: extension は必須です(例: ".html")。`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateData(data, projectName) {
|
|
209
|
+
if (data == null) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`[JSKim] 設定値が不正です: data\n` +
|
|
215
|
+
`プロジェクト: ${projectName}\n` +
|
|
216
|
+
`plain object を指定してください(null / array / primitive は不可)。\n` +
|
|
217
|
+
`受け取った値: ${Array.isArray(data) ? 'array' : typeof data}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateNunjucks(nunjucks, projectName) {
|
|
223
|
+
if (nunjucks == null) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (typeof nunjucks !== 'object' || Array.isArray(nunjucks)) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`[JSKim] 設定値が不正です: nunjucks\n` +
|
|
229
|
+
`プロジェクト: ${projectName}\n` +
|
|
230
|
+
`plain object を指定してください。`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const filters = nunjucks.filters || {};
|
|
235
|
+
if (typeof filters !== 'object' || Array.isArray(filters)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`[JSKim] 設定値が不正です: nunjucks.filters\n` +
|
|
238
|
+
`プロジェクト: ${projectName}\n` +
|
|
239
|
+
`plain object を指定してください。`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
243
|
+
if (typeof value !== 'function') {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`[JSKim] 設定値が不正です: nunjucks.filters.${key}\n` +
|
|
246
|
+
`プロジェクト: ${projectName}\n` +
|
|
247
|
+
`filter は function である必要があります。`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const globals = nunjucks.globals || {};
|
|
253
|
+
if (typeof globals !== 'object' || Array.isArray(globals)) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`[JSKim] 設定値が不正です: nunjucks.globals\n` +
|
|
256
|
+
`プロジェクト: ${projectName}\n` +
|
|
257
|
+
`plain object を指定してください。`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
138
262
|
function validateWatchConfig(watch, projectName) {
|
|
139
263
|
const debounce = watch && watch.debounce;
|
|
140
264
|
|
|
@@ -5,15 +5,25 @@ const path = require('node:path');
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 解決済みプロジェクト設定から監視ディレクトリを計算します。
|
|
8
|
-
*
|
|
8
|
+
* files mode: files[].from / templates[]
|
|
9
|
+
* legacy mode: render[].from / templates[] / copy[].from
|
|
9
10
|
* outputDir / dist / node_modules は監視しません。
|
|
10
11
|
*
|
|
11
12
|
* @param {object} project
|
|
12
13
|
* @returns {{ absolutePaths: string[], displayPaths: string[] }}
|
|
13
14
|
*/
|
|
14
15
|
function resolveWatchPaths(project) {
|
|
15
|
-
const {
|
|
16
|
-
|
|
16
|
+
const {
|
|
17
|
+
name,
|
|
18
|
+
sourceDir,
|
|
19
|
+
outputDir,
|
|
20
|
+
workspaceRoot,
|
|
21
|
+
render,
|
|
22
|
+
templates,
|
|
23
|
+
copy,
|
|
24
|
+
files,
|
|
25
|
+
pipelineMode,
|
|
26
|
+
} = project;
|
|
17
27
|
|
|
18
28
|
const absolutePaths = [];
|
|
19
29
|
const seen = new Set();
|
|
@@ -39,9 +49,23 @@ function resolveWatchPaths(project) {
|
|
|
39
49
|
absolutePaths.push(resolved);
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
if (pipelineMode === 'files') {
|
|
53
|
+
for (const rule of Array.isArray(files) ? files : []) {
|
|
54
|
+
if (rule && rule.from) {
|
|
55
|
+
addCandidate(path.join(sourceDir, rule.from));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
for (const rule of Array.isArray(render) ? render : []) {
|
|
60
|
+
if (rule && rule.from) {
|
|
61
|
+
addCandidate(path.join(sourceDir, rule.from));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const rule of Array.isArray(copy) ? copy : []) {
|
|
66
|
+
if (rule && rule.from) {
|
|
67
|
+
addCandidate(path.join(sourceDir, rule.from));
|
|
68
|
+
}
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -51,16 +75,10 @@ function resolveWatchPaths(project) {
|
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
|
|
54
|
-
for (const rule of Array.isArray(copy) ? copy : []) {
|
|
55
|
-
if (rule && rule.from) {
|
|
56
|
-
addCandidate(path.join(sourceDir, rule.from));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
78
|
if (absolutePaths.length === 0) {
|
|
61
79
|
throw new Error(
|
|
62
80
|
`[JSKim] プロジェクト "${name}" の監視パスがありません。\n` +
|
|
63
|
-
`原因: render[].from / templates[] / copy[].from のいずれも既存パスに解決できませんでした。\n` +
|
|
81
|
+
`原因: files[].from / render[].from / templates[] / copy[].from のいずれも既存パスに解決できませんでした。\n` +
|
|
64
82
|
`sourceDir: ${sourceDir}`
|
|
65
83
|
);
|
|
66
84
|
}
|