@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.
@@ -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 merged = mergeConfig(config.defaults || {}, projectConfig);
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
- if (!Array.isArray(merged.render) || merged.render.length === 0) {
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] プロジェクト "${name}" render 設定が無い、または空です。\n` +
85
- `jskim.config.js の defaults.render または projects.${name}.render を定義してください。`
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
- for (let i = 0; i < merged.render.length; i += 1) {
90
- const rule = merged.render[i];
91
- if (!rule || typeof rule !== 'object') {
92
- throw new Error(
93
- `[JSKim] プロジェクト "${name}" render[${i}] が不正です。\n` +
94
- `原因: render ルールはオブジェクトである必要があります。`
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[${i}].extension が不正です。\n` +
112
- `原因: extension は必須です(例: ".html")。`
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
- `[JSKim] 設定値が不正です: watch.debounce\n` +
148
- `プロジェクト: ${projectName}\n` +
149
- `0以上の有限な数値を指定してください。\n` +
150
- `受け取った値: ${String(debounce)}`
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
- `[JSKim] 設定値が不正です: serve.host\n` +
162
- `プロジェクト: ${projectName}\n` +
163
- `空でない文字列を指定してください。\n` +
164
- `受け取った値: ${String(host)}`
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
- `[JSKim] 設定値が不正です: serve.port\n` +
176
- `プロジェクト: ${projectName}\n` +
177
- `1から65535までの整数を指定してください。\n` +
178
- `受け取った値: ${String(port)}`
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
- `[JSKim] 設定値が不正です: dev.liveReload\n` +
189
- `プロジェクト: ${projectName}\n` +
190
- `boolean を指定してください。\n` +
191
- `受け取った値: ${String(liveReload)}`
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
- * sourceDir 配下の render[].from / templates[] / copy[].from を監視します。
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 { name, sourceDir, outputDir, workspaceRoot, render, templates, copy } =
16
- project;
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
- for (const rule of render) {
43
- if (rule && rule.from) {
44
- addCandidate(path.join(sourceDir, rule.from));
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
+ };