@ywal123456/jskim 0.2.0 → 0.3.1
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 +112 -162
- package/docs/configuration.md +204 -207
- package/docs/create-jskim.md +22 -7
- package/package.json +1 -1
- package/scripts/commands/path-display.js +1 -15
- package/scripts/lib/build-project.js +18 -12
- package/scripts/lib/create-nunjucks-env.js +40 -1
- package/scripts/lib/create-project-watcher.js +7 -2
- package/scripts/lib/format-diagnostic.js +234 -0
- package/scripts/lib/load-config.js +6 -4
- package/scripts/lib/merge-config.js +87 -2
- package/scripts/lib/process-files.js +307 -0
- package/scripts/lib/render-pages.js +39 -13
- package/scripts/lib/resolve-project.js +193 -44
- package/scripts/lib/resolve-watch-paths.js +31 -13
- package/scripts/lib/to-display-path.js +28 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { mergeConfig } = require('./merge-config');
|
|
6
|
+
const { formatConfigValidationError } = require('./format-diagnostic');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 読み込んだ設定から名前付きプロジェクトを解決・検証します。
|
|
@@ -12,7 +13,7 @@ const { mergeConfig } = require('./merge-config');
|
|
|
12
13
|
* @param {string} options.workspaceRoot
|
|
13
14
|
* @param {string|undefined} options.projectName
|
|
14
15
|
* @param {string} [options.commandName='build']
|
|
15
|
-
* @param {string} [options.usageLine]
|
|
16
|
+
* @param {string} [options.usageLine]
|
|
16
17
|
* @returns {object} 解決済みプロジェクト
|
|
17
18
|
*/
|
|
18
19
|
function resolveProject({
|
|
@@ -45,7 +46,8 @@ function resolveProject({
|
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
+
const defaults = config.defaults || {};
|
|
50
|
+
const merged = mergeConfig(defaults, projectConfig);
|
|
49
51
|
|
|
50
52
|
if (!merged.sourceDir || String(merged.sourceDir).trim() === '') {
|
|
51
53
|
throw new Error(
|
|
@@ -79,39 +81,43 @@ function resolveProject({
|
|
|
79
81
|
);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
validateData(merged.data, name);
|
|
85
|
+
validateNunjucks(merged.nunjucks, name);
|
|
86
|
+
|
|
87
|
+
const hasFiles = Array.isArray(merged.files) && merged.files.length > 0;
|
|
88
|
+
const hasRender = Array.isArray(merged.render) && merged.render.length > 0;
|
|
89
|
+
const hasCopy = Array.isArray(merged.copy) && merged.copy.length > 0;
|
|
90
|
+
|
|
91
|
+
if (hasFiles && (hasRender || hasCopy)) {
|
|
92
|
+
const conflict = [];
|
|
93
|
+
if (hasRender) {
|
|
94
|
+
conflict.push('render');
|
|
95
|
+
}
|
|
96
|
+
if (hasCopy) {
|
|
97
|
+
conflict.push('copy');
|
|
98
|
+
}
|
|
83
99
|
throw new Error(
|
|
84
|
-
`[JSKim]
|
|
85
|
-
|
|
100
|
+
`[JSKim] files と ${conflict.join(' / ')} を同時に設定できません。\n` +
|
|
101
|
+
`プロジェクト: ${name}\n` +
|
|
102
|
+
`衝突: files と ${conflict.join(' / ')}\n` +
|
|
103
|
+
`files mode を使う場合は render / copy を空にしてください。\n` +
|
|
104
|
+
`legacy mode を使う場合は files を設定しないでください。`
|
|
86
105
|
);
|
|
87
106
|
}
|
|
88
107
|
|
|
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() === '') {
|
|
108
|
+
const pipelineMode = hasFiles ? 'files' : 'legacy';
|
|
109
|
+
|
|
110
|
+
if (pipelineMode === 'files') {
|
|
111
|
+
validateFilesRules(merged.files, name);
|
|
112
|
+
} else {
|
|
113
|
+
if (!hasRender) {
|
|
110
114
|
throw new Error(
|
|
111
|
-
`[JSKim] プロジェクト "${name}" の render
|
|
112
|
-
|
|
115
|
+
`[JSKim] プロジェクト "${name}" の render 設定が無い、または空です。\n` +
|
|
116
|
+
`jskim.config.js の defaults.render または projects.${name}.render を定義してください。\n` +
|
|
117
|
+
`新しい files pipeline を使う場合は defaults.files を設定してください。`
|
|
113
118
|
);
|
|
114
119
|
}
|
|
120
|
+
validateRenderRules(merged.render, name);
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
validateWatchConfig(merged.watch, name);
|
|
@@ -124,9 +130,13 @@ function resolveProject({
|
|
|
124
130
|
outputDir,
|
|
125
131
|
sourceDirConfig: merged.sourceDir,
|
|
126
132
|
outputDirConfig: merged.outputDir,
|
|
133
|
+
pipelineMode,
|
|
127
134
|
render: merged.render,
|
|
128
135
|
templates: merged.templates,
|
|
129
136
|
copy: merged.copy,
|
|
137
|
+
files: merged.files || [],
|
|
138
|
+
data: merged.data,
|
|
139
|
+
nunjucks: merged.nunjucks,
|
|
130
140
|
build: merged.build,
|
|
131
141
|
watch: merged.watch,
|
|
132
142
|
serve: merged.serve,
|
|
@@ -135,6 +145,137 @@ function resolveProject({
|
|
|
135
145
|
};
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
function validateFilesRules(files, projectName) {
|
|
149
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
150
|
+
const rule = files[i];
|
|
151
|
+
if (!rule || typeof rule !== 'object') {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}] が不正です。\n` +
|
|
154
|
+
`原因: 各 files ルールはオブジェクトである必要があります。`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].from が不正です。\n` +
|
|
160
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (rule.include !== undefined) {
|
|
164
|
+
if (!Array.isArray(rule.include) || rule.include.length === 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].include が不正です。\n` +
|
|
167
|
+
`原因: include は空でない glob 配列である必要があります。`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (rule.exclude !== undefined && !Array.isArray(rule.exclude)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[JSKim] プロジェクト "${projectName}" の files[${i}].exclude が不正です。\n` +
|
|
174
|
+
`原因: exclude は配列である必要があります。`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateRenderRules(render, projectName) {
|
|
181
|
+
for (let i = 0; i < render.length; i += 1) {
|
|
182
|
+
const rule = render[i];
|
|
183
|
+
if (!rule || typeof rule !== 'object') {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}] が不正です。\n` +
|
|
186
|
+
`原因: 各 render ルールはオブジェクトである必要があります。`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (!rule.from || String(rule.from).trim() === '') {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].from が不正です。\n` +
|
|
192
|
+
`原因: from は必須です(sourceDir 基準)。`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(rule.include) || rule.include.length === 0) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].include が不正です。\n` +
|
|
198
|
+
`原因: include は空でない glob パターン配列である必要があります。`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!rule.extension || String(rule.extension).trim() === '') {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`[JSKim] プロジェクト "${projectName}" の render[${i}].extension が不正です。\n` +
|
|
204
|
+
`原因: extension は必須です(例: ".html")。`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateData(data, projectName) {
|
|
211
|
+
if (data == null) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
formatConfigValidationError({
|
|
217
|
+
projectName,
|
|
218
|
+
configKey: 'data',
|
|
219
|
+
detail:
|
|
220
|
+
'plain object を指定してください(null / array / primitive は不可)。',
|
|
221
|
+
received: Array.isArray(data) ? 'array' : typeof data,
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateNunjucks(nunjucks, projectName) {
|
|
228
|
+
if (nunjucks == null) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (typeof nunjucks !== 'object' || Array.isArray(nunjucks)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
formatConfigValidationError({
|
|
234
|
+
projectName,
|
|
235
|
+
configKey: 'nunjucks',
|
|
236
|
+
detail: 'plain object を指定してください。',
|
|
237
|
+
received: Array.isArray(nunjucks) ? 'array' : typeof nunjucks,
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const filters = nunjucks.filters || {};
|
|
243
|
+
if (typeof filters !== 'object' || Array.isArray(filters)) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
formatConfigValidationError({
|
|
246
|
+
projectName,
|
|
247
|
+
configKey: 'nunjucks.filters',
|
|
248
|
+
detail: 'plain object を指定してください。',
|
|
249
|
+
received: Array.isArray(filters) ? 'array' : typeof filters,
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
254
|
+
if (typeof value !== 'function') {
|
|
255
|
+
throw new Error(
|
|
256
|
+
formatConfigValidationError({
|
|
257
|
+
projectName,
|
|
258
|
+
configKey: `nunjucks.filters.${key}`,
|
|
259
|
+
detail: 'filter は function である必要があります。',
|
|
260
|
+
received: typeof value,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const globals = nunjucks.globals || {};
|
|
267
|
+
if (typeof globals !== 'object' || Array.isArray(globals)) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
formatConfigValidationError({
|
|
270
|
+
projectName,
|
|
271
|
+
configKey: 'nunjucks.globals',
|
|
272
|
+
detail: 'plain object を指定してください。',
|
|
273
|
+
received: Array.isArray(globals) ? 'array' : typeof globals,
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
138
279
|
function validateWatchConfig(watch, projectName) {
|
|
139
280
|
const debounce = watch && watch.debounce;
|
|
140
281
|
|
|
@@ -144,10 +285,12 @@ function validateWatchConfig(watch, projectName) {
|
|
|
144
285
|
debounce < 0
|
|
145
286
|
) {
|
|
146
287
|
throw new Error(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
288
|
+
formatConfigValidationError({
|
|
289
|
+
projectName,
|
|
290
|
+
configKey: 'watch.debounce',
|
|
291
|
+
detail: '0以上の有限な数値を指定してください。',
|
|
292
|
+
received: String(debounce),
|
|
293
|
+
})
|
|
151
294
|
);
|
|
152
295
|
}
|
|
153
296
|
}
|
|
@@ -158,10 +301,12 @@ function validateServeConfig(serve, projectName) {
|
|
|
158
301
|
|
|
159
302
|
if (typeof host !== 'string' || host.trim() === '') {
|
|
160
303
|
throw new Error(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
304
|
+
formatConfigValidationError({
|
|
305
|
+
projectName,
|
|
306
|
+
configKey: 'serve.host',
|
|
307
|
+
detail: '空でない文字列を指定してください。',
|
|
308
|
+
received: String(host),
|
|
309
|
+
})
|
|
165
310
|
);
|
|
166
311
|
}
|
|
167
312
|
|
|
@@ -172,10 +317,12 @@ function validateServeConfig(serve, projectName) {
|
|
|
172
317
|
port > 65535
|
|
173
318
|
) {
|
|
174
319
|
throw new Error(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
320
|
+
formatConfigValidationError({
|
|
321
|
+
projectName,
|
|
322
|
+
configKey: 'serve.port',
|
|
323
|
+
detail: '1から65535までの整数を指定してください。',
|
|
324
|
+
received: String(port),
|
|
325
|
+
})
|
|
179
326
|
);
|
|
180
327
|
}
|
|
181
328
|
}
|
|
@@ -185,10 +332,12 @@ function validateDevConfig(dev, projectName) {
|
|
|
185
332
|
|
|
186
333
|
if (typeof liveReload !== 'boolean') {
|
|
187
334
|
throw new Error(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
335
|
+
formatConfigValidationError({
|
|
336
|
+
projectName,
|
|
337
|
+
configKey: 'dev.liveReload',
|
|
338
|
+
detail: 'boolean を指定してください。',
|
|
339
|
+
received: String(liveReload),
|
|
340
|
+
})
|
|
192
341
|
);
|
|
193
342
|
}
|
|
194
343
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ワークスペース相対の表示用パスへ変換します。
|
|
7
|
+
* @param {string} abs
|
|
8
|
+
* @param {string} workspaceRoot
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function toDisplayPath(abs, workspaceRoot) {
|
|
12
|
+
if (!abs) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
const resolved = path.resolve(abs);
|
|
16
|
+
if (!workspaceRoot) {
|
|
17
|
+
return resolved.split(path.sep).join('/');
|
|
18
|
+
}
|
|
19
|
+
const rel = path.relative(workspaceRoot, resolved);
|
|
20
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
21
|
+
return resolved.split(path.sep).join('/');
|
|
22
|
+
}
|
|
23
|
+
return rel.split(path.sep).join('/');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
toDisplayPath,
|
|
28
|
+
};
|