@ywal123456/jskim 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ywal123456/jskim",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Nunjucks-based static HTML build environment with watch, serve, and live reload",
5
5
  "keywords": [
6
6
  "nunjucks",
@@ -1,20 +1,6 @@
1
1
  'use strict';
2
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
- const rel = path.relative(workspaceRoot, abs);
13
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
14
- return abs.split(path.sep).join('/');
15
- }
16
- return rel.split(path.sep).join('/');
17
- }
3
+ const { toDisplayPath } = require('../lib/to-display-path');
18
4
 
19
5
  module.exports = {
20
6
  toDisplayPath,
@@ -87,7 +87,8 @@ function wrapSyncCallable(fn, label) {
87
87
  if (result && typeof result.then === 'function') {
88
88
  throw new Error(
89
89
  `[JSKim] 非同期${label.startsWith('filter') ? 'filter' : 'global'}は現在サポートされていません。\n` +
90
- `対象: ${label}`
90
+ `対象: ${label}\n` +
91
+ `同期の function のみ登録できます。`
91
92
  );
92
93
  }
93
94
  return result;
@@ -184,9 +184,9 @@ function createProjectWatcher(project, options = {}) {
184
184
  `[JSKim] プロジェクト "${project.name}" の初回ビルドに失敗しました。ウォッチャーは開始します。`
185
185
  );
186
186
  } else {
187
- const representative = events[events.length - 1] || null;
188
187
  console.error(`[JSKim] 再ビルドに失敗しました`);
189
188
  console.error(`プロジェクト: ${project.name}`);
189
+ const representative = events[events.length - 1] || null;
190
190
  if (representative) {
191
191
  console.error(`イベント: ${representative.event}`);
192
192
  console.error(`ファイル: ${representative.file}`);
@@ -194,7 +194,12 @@ function createProjectWatcher(project, options = {}) {
194
194
  if (events.length > 1) {
195
195
  console.error(`変更数: ${events.length} ファイル`);
196
196
  }
197
- console.error(`原因: ${message}`);
197
+ // 診断付きメッセージは重複ヘッダーを避けるためそのまま出力する
198
+ if (String(message).startsWith('[JSKim]')) {
199
+ console.error(message);
200
+ } else {
201
+ console.error(`原因: ${message}`);
202
+ }
198
203
  console.error(
199
204
  `[JSKim] ウォッチャーは継続中です。修正して保存すると再試行します。`
200
205
  );
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const { toDisplayPath } = require('./to-display-path');
4
+
5
+ /**
6
+ * Error から短い原因メッセージを取り出します(stack は含めません)。
7
+ * @param {unknown} err
8
+ * @returns {string}
9
+ */
10
+ function getCauseMessage(err) {
11
+ if (!err) {
12
+ return String(err);
13
+ }
14
+ if (typeof err === 'string') {
15
+ return err;
16
+ }
17
+ if (err && typeof err.message === 'string' && err.message.trim() !== '') {
18
+ return err.message;
19
+ }
20
+ return String(err);
21
+ }
22
+
23
+ /**
24
+ * Nunjucks が lineno / colno を提供する場合だけ位置を返します。
25
+ * message 本文の正規表現解析は行いません。
26
+ *
27
+ * @param {unknown} err
28
+ * @returns {{ line?: number, column?: number }}
29
+ */
30
+ function readNunjucksLocation(err) {
31
+ if (!err || typeof err !== 'object') {
32
+ return {};
33
+ }
34
+
35
+ const lineno = err.lineno;
36
+ const colno = err.colno;
37
+ const result = {};
38
+
39
+ if (Number.isInteger(lineno) && lineno > 0) {
40
+ result.line = lineno;
41
+ }
42
+ if (Number.isInteger(colno) && colno > 0) {
43
+ result.column = colno;
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * Nunjucks レンダリング失敗の診断メッセージを組み立てます。
51
+ *
52
+ * @param {object} options
53
+ * @param {string} options.projectName
54
+ * @param {string} options.sourceFile
55
+ * @param {string} options.templatePath
56
+ * @param {string} [options.workspaceRoot]
57
+ * @param {unknown} options.err
58
+ * @returns {string}
59
+ */
60
+ function formatRenderError(options) {
61
+ const {
62
+ projectName,
63
+ sourceFile,
64
+ templatePath,
65
+ workspaceRoot,
66
+ err,
67
+ } = options;
68
+
69
+ const sourceDisplay = toDisplayPath(sourceFile, workspaceRoot);
70
+ const location = readNunjucksLocation(err);
71
+ const cause = getCauseMessage(err);
72
+
73
+ const lines = [
74
+ `[JSKim] プロジェクト "${projectName}" の Nunjucks レンダリングに失敗しました。`,
75
+ `ソース: ${sourceDisplay}`,
76
+ `テンプレート: ${templatePath}`,
77
+ ];
78
+
79
+ if (location.line != null) {
80
+ lines.push(`行: ${location.line}`);
81
+ }
82
+ if (location.column != null) {
83
+ lines.push(`列: ${location.column}`);
84
+ }
85
+
86
+ lines.push(`原因: ${cause}`);
87
+ lines.push('テンプレート構文または参照先(extends / include)を確認してください。');
88
+
89
+ return lines.join('\n');
90
+ }
91
+
92
+ /**
93
+ * 出力パス衝突の診断メッセージを組み立てます。
94
+ *
95
+ * @param {object} options
96
+ * @param {string} options.projectName
97
+ * @param {string} options.outputFile
98
+ * @param {string} options.sourceFileA
99
+ * @param {string} options.sourceFileB
100
+ * @param {number} [options.ruleIndexA]
101
+ * @param {number} [options.ruleIndexB]
102
+ * @param {string} [options.workspaceRoot]
103
+ * @returns {string}
104
+ */
105
+ function formatCollisionError(options) {
106
+ const {
107
+ projectName,
108
+ outputFile,
109
+ sourceFileA,
110
+ sourceFileB,
111
+ ruleIndexA,
112
+ ruleIndexB,
113
+ workspaceRoot,
114
+ } = options;
115
+
116
+ const lines = [
117
+ `[JSKim] 出力パスが衝突しています。`,
118
+ `プロジェクト: ${projectName}`,
119
+ `出力: ${toDisplayPath(outputFile, workspaceRoot)}`,
120
+ formatCollisionSource('ソース1', sourceFileA, ruleIndexA, workspaceRoot),
121
+ formatCollisionSource('ソース2', sourceFileB, ruleIndexB, workspaceRoot),
122
+ '同じ出力になるソースを分けるか、どちらかを除外してください。',
123
+ ];
124
+
125
+ return lines.join('\n');
126
+ }
127
+
128
+ function formatCollisionSource(label, sourceFile, ruleIndex, workspaceRoot) {
129
+ const display = toDisplayPath(sourceFile, workspaceRoot);
130
+ if (Number.isInteger(ruleIndex) && ruleIndex >= 0) {
131
+ return `${label}: ${display} (files[${ruleIndex}])`;
132
+ }
133
+ return `${label}: ${display}`;
134
+ }
135
+
136
+ /**
137
+ * sourceDir / outputDir 外へのパス逸脱メッセージを組み立てます。
138
+ *
139
+ * @param {object} options
140
+ * @param {string} options.projectName
141
+ * @param {string} options.label
142
+ * @param {string} options.root
143
+ * @param {string} options.candidate
144
+ * @param {'sourceDir'|'outputDir'|string} [options.rootKind]
145
+ * @param {string} [options.workspaceRoot]
146
+ * @returns {string}
147
+ */
148
+ function formatPathOutsideError(options) {
149
+ const {
150
+ projectName,
151
+ label,
152
+ root,
153
+ candidate,
154
+ rootKind,
155
+ workspaceRoot,
156
+ } = options;
157
+
158
+ const kindLabel =
159
+ rootKind === 'sourceDir'
160
+ ? 'sourceDir'
161
+ : rootKind === 'outputDir'
162
+ ? 'outputDir'
163
+ : '許可ルート';
164
+
165
+ return [
166
+ `[JSKim] プロジェクト "${projectName}" の ${label} が許可範囲外です。`,
167
+ `種別: ${kindLabel} の外への参照`,
168
+ `ルート: ${toDisplayPath(root, workspaceRoot)}`,
169
+ `対象: ${toDisplayPath(candidate, workspaceRoot)}`,
170
+ '相対パスが sourceDir / outputDir 内に収まるように設定してください。',
171
+ ].join('\n');
172
+ }
173
+
174
+ /**
175
+ * 設定バリデーション向けの共通ヘッダー行を組み立てます。
176
+ *
177
+ * @param {object} options
178
+ * @param {string} options.projectName
179
+ * @param {string} options.configKey
180
+ * @param {string} [options.detail]
181
+ * @param {string} [options.received]
182
+ * @param {string} [options.hint]
183
+ * @returns {string}
184
+ */
185
+ function formatConfigValidationError(options) {
186
+ const { projectName, configKey, detail, received, hint } = options;
187
+ const lines = [
188
+ `[JSKim] 設定値が不正です: ${configKey}`,
189
+ `プロジェクト: ${projectName}`,
190
+ `設定キー: projects.${projectName} または defaults の ${configKey}`,
191
+ ];
192
+
193
+ if (detail) {
194
+ lines.push(detail);
195
+ }
196
+ if (received !== undefined) {
197
+ lines.push(`受け取った値: ${received}`);
198
+ }
199
+ if (hint) {
200
+ lines.push(hint);
201
+ }
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ /**
207
+ * 書き込み失敗などの I/O エラーメッセージを組み立てます。
208
+ *
209
+ * @param {object} options
210
+ * @param {string} options.projectName
211
+ * @param {string} options.actionLabel
212
+ * @param {string} options.targetFile
213
+ * @param {string} [options.workspaceRoot]
214
+ * @param {unknown} options.err
215
+ * @returns {string}
216
+ */
217
+ function formatIoError(options) {
218
+ const { projectName, actionLabel, targetFile, workspaceRoot, err } = options;
219
+ return [
220
+ `[JSKim] プロジェクト "${projectName}" の${actionLabel}に失敗しました。`,
221
+ `対象: ${toDisplayPath(targetFile, workspaceRoot)}`,
222
+ `原因: ${getCauseMessage(err)}`,
223
+ ].join('\n');
224
+ }
225
+
226
+ module.exports = {
227
+ getCauseMessage,
228
+ readNunjucksLocation,
229
+ formatRenderError,
230
+ formatCollisionError,
231
+ formatPathOutsideError,
232
+ formatConfigValidationError,
233
+ formatIoError,
234
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
+ const { toDisplayPath } = require('./to-display-path');
4
5
 
5
6
  const CONFIG_FILENAME = 'jskim.config.js';
6
7
 
@@ -11,6 +12,7 @@ const CONFIG_FILENAME = 'jskim.config.js';
11
12
  */
12
13
  function loadConfig(workspaceRoot) {
13
14
  const configPath = path.join(workspaceRoot, CONFIG_FILENAME);
15
+ const configDisplay = toDisplayPath(configPath, workspaceRoot);
14
16
 
15
17
  let config;
16
18
  try {
@@ -25,21 +27,21 @@ function loadConfig(workspaceRoot) {
25
27
 
26
28
  if (missingConfig) {
27
29
  throw new Error(
28
- `[JSKim] 設定ファイルが見つかりません: ${configPath}\n` +
30
+ `[JSKim] 設定ファイルが見つかりません: ${configDisplay}\n` +
29
31
  `ワークスペースルートに ${CONFIG_FILENAME} を作成してください。`
30
32
  );
31
33
  }
32
34
  }
33
35
 
34
36
  throw new Error(
35
- `[JSKim] 設定ファイルの読み込みに失敗しました: ${configPath}\n` +
37
+ `[JSKim] 設定ファイルの読み込みに失敗しました: ${configDisplay}\n` +
36
38
  `原因: ${err && err.message ? err.message : String(err)}`
37
39
  );
38
40
  }
39
41
 
40
42
  if (!config || typeof config !== 'object') {
41
43
  throw new Error(
42
- `[JSKim] 設定が不正です: ${configPath}\n` +
44
+ `[JSKim] 設定が不正です: ${configDisplay}\n` +
43
45
  `原因: module.exports はオブジェクトである必要があります。`
44
46
  );
45
47
  }
@@ -47,7 +49,7 @@ function loadConfig(workspaceRoot) {
47
49
  if (!config.projects || typeof config.projects !== 'object') {
48
50
  throw new Error(
49
51
  `[JSKim] 設定が不正です: projects が無い、またはオブジェクトではありません。\n` +
50
- `設定: ${configPath}`
52
+ `設定: ${configDisplay}`
51
53
  );
52
54
  }
53
55
 
@@ -5,6 +5,13 @@ const fs = require('node:fs');
5
5
  const fse = require('fs-extra');
6
6
  const fg = require('fast-glob');
7
7
  const { computeRootPath } = require('./render-pages');
8
+ const { toDisplayPath } = require('./to-display-path');
9
+ const {
10
+ formatRenderError,
11
+ formatCollisionError,
12
+ formatPathOutsideError,
13
+ formatIoError,
14
+ } = require('./format-diagnostic');
8
15
 
9
16
  const RESERVED_CONTEXT_KEYS = new Set(['rootPath']);
10
17
 
@@ -33,14 +40,15 @@ async function processFiles({ env, project }) {
33
40
  const fromDir = path.resolve(sourceDir, rule.from);
34
41
  const toDir = path.resolve(outputDir, rule.to);
35
42
 
36
- assertInside(sourceDir, fromDir, name, `files[${i}].from`);
37
- assertInside(outputDir, toDir, name, `files[${i}].to`);
43
+ assertInside(sourceDir, fromDir, name, `files[${i}].from`, 'sourceDir', project.workspaceRoot);
44
+ assertInside(outputDir, toDir, name, `files[${i}].to`, 'outputDir', project.workspaceRoot);
38
45
 
39
46
  if (!(await fse.pathExists(fromDir))) {
40
47
  throw new Error(
41
48
  `[JSKim] プロジェクト "${name}" の files[${i}].from が存在しません。\n` +
42
- `パス: ${fromDir}\n` +
43
- `設定: files[${i}].from = ${rule.from}`
49
+ `設定キー: files[${i}].from\n` +
50
+ `設定値: ${rule.from}\n` +
51
+ `パス: ${toDisplayPath(fromDir, project.workspaceRoot)}`
44
52
  );
45
53
  }
46
54
 
@@ -67,7 +75,14 @@ async function processFiles({ env, project }) {
67
75
  : normalizedRel;
68
76
  const outputFile = path.resolve(toDir, outRel.split('/').join(path.sep));
69
77
 
70
- assertInside(outputDir, outputFile, name, `files[${i}] output`);
78
+ assertInside(
79
+ outputDir,
80
+ outputFile,
81
+ name,
82
+ `files[${i}] output`,
83
+ 'outputDir',
84
+ project.workspaceRoot
85
+ );
71
86
 
72
87
  const collisionKey =
73
88
  process.platform === 'win32'
@@ -77,11 +92,15 @@ async function processFiles({ env, project }) {
77
92
  if (planned.has(collisionKey)) {
78
93
  const previous = planned.get(collisionKey);
79
94
  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)}`
95
+ formatCollisionError({
96
+ projectName: name,
97
+ outputFile,
98
+ sourceFileA: previous.sourceFile,
99
+ sourceFileB: sourceFile,
100
+ ruleIndexA: previous.ruleIndex,
101
+ ruleIndexB: i,
102
+ workspaceRoot: project.workspaceRoot,
103
+ })
85
104
  );
86
105
  }
87
106
 
@@ -103,7 +122,20 @@ async function processFiles({ env, project }) {
103
122
  await fse.ensureDir(path.dirname(item.outputFile));
104
123
 
105
124
  if (item.kind === 'copy') {
106
- await fse.copy(item.sourceFile, item.outputFile, { overwrite: true });
125
+ try {
126
+ await fse.copy(item.sourceFile, item.outputFile, { overwrite: true });
127
+ } catch (err) {
128
+ throw new Error(
129
+ formatIoError({
130
+ projectName: name,
131
+ actionLabel: 'ファイルコピー',
132
+ targetFile: item.sourceFile,
133
+ workspaceRoot: project.workspaceRoot,
134
+ err,
135
+ }) +
136
+ `\n出力: ${toDisplayPath(item.outputFile, project.workspaceRoot)}`
137
+ );
138
+ }
107
139
  copiedCount += 1;
108
140
  outputFiles.push(item.outputFile);
109
141
  continue;
@@ -120,16 +152,30 @@ async function processFiles({ env, project }) {
120
152
  try {
121
153
  text = env.render(templatePath, context);
122
154
  } catch (err) {
123
- const message = err && err.message ? err.message : String(err);
124
155
  throw new Error(
125
- `[JSKim] プロジェクト "${name}" の Nunjucks レンダリングに失敗しました。\n` +
126
- `ソース: ${item.sourceFile}\n` +
127
- `テンプレート: ${templatePath}\n` +
128
- `原因: ${message}`
156
+ formatRenderError({
157
+ projectName: name,
158
+ sourceFile: item.sourceFile,
159
+ templatePath,
160
+ workspaceRoot: project.workspaceRoot,
161
+ err,
162
+ })
129
163
  );
130
164
  }
131
165
 
132
- await fse.writeFile(item.outputFile, text, 'utf8');
166
+ try {
167
+ await fse.writeFile(item.outputFile, text, 'utf8');
168
+ } catch (err) {
169
+ throw new Error(
170
+ formatIoError({
171
+ projectName: name,
172
+ actionLabel: 'レンダリング結果の書き込み',
173
+ targetFile: item.outputFile,
174
+ workspaceRoot: project.workspaceRoot,
175
+ err,
176
+ })
177
+ );
178
+ }
133
179
  renderedCount += 1;
134
180
  outputFiles.push(item.outputFile);
135
181
  }
@@ -218,12 +264,17 @@ function samePath(a, b) {
218
264
  return na === nb;
219
265
  }
220
266
 
221
- function assertInside(root, candidate, projectName, label) {
267
+ function assertInside(root, candidate, projectName, label, rootKind, workspaceRoot) {
222
268
  if (!isInsideOrSame(root, candidate)) {
223
269
  throw new Error(
224
- `[JSKim] プロジェクト "${projectName}" の ${label} が許可範囲外です。\n` +
225
- `ルート: ${root}\n` +
226
- `対象: ${candidate}`
270
+ formatPathOutsideError({
271
+ projectName,
272
+ label,
273
+ root,
274
+ candidate,
275
+ rootKind,
276
+ workspaceRoot,
277
+ })
227
278
  );
228
279
  }
229
280
  }
@@ -235,6 +286,7 @@ function assertNoReservedDataCollision(data, projectName) {
235
286
  throw new Error(
236
287
  `[JSKim] data のキーが予約語と衝突しています: ${key}\n` +
237
288
  `プロジェクト: ${projectName}\n` +
289
+ `設定キー: data.${key}\n` +
238
290
  `予約キー: ${[...RESERVED_CONTEXT_KEYS].join(', ')}`
239
291
  );
240
292
  }
@@ -248,14 +300,6 @@ function buildRenderContext(data, rootPath) {
248
300
  };
249
301
  }
250
302
 
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
303
  module.exports = {
260
304
  processFiles,
261
305
  stripTrailingNjk,
@@ -3,6 +3,8 @@
3
3
  const path = require('node:path');
4
4
  const fse = require('fs-extra');
5
5
  const fg = require('fast-glob');
6
+ const { formatRenderError, formatIoError } = require('./format-diagnostic');
7
+ const { toDisplayPath } = require('./to-display-path');
6
8
 
7
9
  /**
8
10
  * 出力ファイルのディレクトリから outputDir までの相対パスとして rootPath を計算します。
@@ -39,7 +41,7 @@ function computeRootPath(outputFilePath, outputDir) {
39
41
  * @returns {Promise<{ renderedCount: number, files: string[] }>}
40
42
  */
41
43
  async function renderPages({ env, project }) {
42
- const { name, sourceDir, outputDir, render } = project;
44
+ const { name, sourceDir, outputDir, render, workspaceRoot } = project;
43
45
  let renderedCount = 0;
44
46
  const files = [];
45
47
 
@@ -54,8 +56,9 @@ async function renderPages({ env, project }) {
54
56
  if (!(await fse.pathExists(fromDir))) {
55
57
  throw new Error(
56
58
  `[JSKim] プロジェクト "${name}" の render[${i}].from が存在しません。\n` +
57
- `パス: ${fromDir}\n` +
58
- `設定: render[${i}].from = ${rule.from}`
59
+ `設定キー: render[${i}].from\n` +
60
+ `設定値: ${rule.from}\n` +
61
+ `パス: ${toDisplayPath(fromDir, workspaceRoot)}`
59
62
  );
60
63
  }
61
64
 
@@ -94,7 +97,9 @@ async function renderPages({ env, project }) {
94
97
  ) {
95
98
  throw new Error(
96
99
  `[JSKim] data のキーが予約語と衝突しています: rootPath\n` +
97
- `プロジェクト: ${name}`
100
+ `プロジェクト: ${name}\n` +
101
+ `設定キー: data.rootPath\n` +
102
+ `予約キー: rootPath`
98
103
  );
99
104
  }
100
105
 
@@ -102,12 +107,14 @@ async function renderPages({ env, project }) {
102
107
  try {
103
108
  html = env.render(templatePath, context);
104
109
  } catch (err) {
105
- const message = err && err.message ? err.message : String(err);
106
110
  throw new Error(
107
- `[JSKim] プロジェクト "${name}" の Nunjucks レンダリングに失敗しました。\n` +
108
- `ソース: ${sourceFile}\n` +
109
- `テンプレート: ${templatePath}\n` +
110
- `原因: ${message}`
111
+ formatRenderError({
112
+ projectName: name,
113
+ sourceFile,
114
+ templatePath,
115
+ workspaceRoot,
116
+ err,
117
+ })
111
118
  );
112
119
  }
113
120
 
@@ -115,11 +122,14 @@ async function renderPages({ env, project }) {
115
122
  await fse.ensureDir(path.dirname(outputFile));
116
123
  await fse.writeFile(outputFile, html, 'utf8');
117
124
  } catch (err) {
118
- const message = err && err.message ? err.message : String(err);
119
125
  throw new Error(
120
- `[JSKim] プロジェクト "${name}" のレンダリング結果の書き込みに失敗しました。\n` +
121
- `出力: ${outputFile}\n` +
122
- `原因: ${message}`
126
+ formatIoError({
127
+ projectName: name,
128
+ actionLabel: 'レンダリング結果の書き込み',
129
+ targetFile: outputFile,
130
+ workspaceRoot,
131
+ err,
132
+ })
123
133
  );
124
134
  }
125
135
 
@@ -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
  * 読み込んだ設定から名前付きプロジェクトを解決・検証します。
@@ -98,6 +99,7 @@ function resolveProject({
98
99
  throw new Error(
99
100
  `[JSKim] files と ${conflict.join(' / ')} を同時に設定できません。\n` +
100
101
  `プロジェクト: ${name}\n` +
102
+ `衝突: files と ${conflict.join(' / ')}\n` +
101
103
  `files mode を使う場合は render / copy を空にしてください。\n` +
102
104
  `legacy mode を使う場合は files を設定しないでください。`
103
105
  );
@@ -211,10 +213,13 @@ function validateData(data, projectName) {
211
213
  }
212
214
  if (typeof data !== 'object' || Array.isArray(data)) {
213
215
  throw new Error(
214
- `[JSKim] 設定値が不正です: data\n` +
215
- `プロジェクト: ${projectName}\n` +
216
- `plain object を指定してください(null / array / primitive は不可)。\n` +
217
- `受け取った値: ${Array.isArray(data) ? 'array' : typeof data}`
216
+ formatConfigValidationError({
217
+ projectName,
218
+ configKey: 'data',
219
+ detail:
220
+ 'plain object を指定してください(null / array / primitive は不可)。',
221
+ received: Array.isArray(data) ? 'array' : typeof data,
222
+ })
218
223
  );
219
224
  }
220
225
  }
@@ -225,26 +230,35 @@ function validateNunjucks(nunjucks, projectName) {
225
230
  }
226
231
  if (typeof nunjucks !== 'object' || Array.isArray(nunjucks)) {
227
232
  throw new Error(
228
- `[JSKim] 設定値が不正です: nunjucks\n` +
229
- `プロジェクト: ${projectName}\n` +
230
- `plain object を指定してください。`
233
+ formatConfigValidationError({
234
+ projectName,
235
+ configKey: 'nunjucks',
236
+ detail: 'plain object を指定してください。',
237
+ received: Array.isArray(nunjucks) ? 'array' : typeof nunjucks,
238
+ })
231
239
  );
232
240
  }
233
241
 
234
242
  const filters = nunjucks.filters || {};
235
243
  if (typeof filters !== 'object' || Array.isArray(filters)) {
236
244
  throw new Error(
237
- `[JSKim] 設定値が不正です: nunjucks.filters\n` +
238
- `プロジェクト: ${projectName}\n` +
239
- `plain object を指定してください。`
245
+ formatConfigValidationError({
246
+ projectName,
247
+ configKey: 'nunjucks.filters',
248
+ detail: 'plain object を指定してください。',
249
+ received: Array.isArray(filters) ? 'array' : typeof filters,
250
+ })
240
251
  );
241
252
  }
242
253
  for (const [key, value] of Object.entries(filters)) {
243
254
  if (typeof value !== 'function') {
244
255
  throw new Error(
245
- `[JSKim] 設定値が不正です: nunjucks.filters.${key}\n` +
246
- `プロジェクト: ${projectName}\n` +
247
- `filter は function である必要があります。`
256
+ formatConfigValidationError({
257
+ projectName,
258
+ configKey: `nunjucks.filters.${key}`,
259
+ detail: 'filter は function である必要があります。',
260
+ received: typeof value,
261
+ })
248
262
  );
249
263
  }
250
264
  }
@@ -252,9 +266,12 @@ function validateNunjucks(nunjucks, projectName) {
252
266
  const globals = nunjucks.globals || {};
253
267
  if (typeof globals !== 'object' || Array.isArray(globals)) {
254
268
  throw new Error(
255
- `[JSKim] 設定値が不正です: nunjucks.globals\n` +
256
- `プロジェクト: ${projectName}\n` +
257
- `plain object を指定してください。`
269
+ formatConfigValidationError({
270
+ projectName,
271
+ configKey: 'nunjucks.globals',
272
+ detail: 'plain object を指定してください。',
273
+ received: Array.isArray(globals) ? 'array' : typeof globals,
274
+ })
258
275
  );
259
276
  }
260
277
  }
@@ -268,10 +285,12 @@ function validateWatchConfig(watch, projectName) {
268
285
  debounce < 0
269
286
  ) {
270
287
  throw new Error(
271
- `[JSKim] 設定値が不正です: watch.debounce\n` +
272
- `プロジェクト: ${projectName}\n` +
273
- `0以上の有限な数値を指定してください。\n` +
274
- `受け取った値: ${String(debounce)}`
288
+ formatConfigValidationError({
289
+ projectName,
290
+ configKey: 'watch.debounce',
291
+ detail: '0以上の有限な数値を指定してください。',
292
+ received: String(debounce),
293
+ })
275
294
  );
276
295
  }
277
296
  }
@@ -282,10 +301,12 @@ function validateServeConfig(serve, projectName) {
282
301
 
283
302
  if (typeof host !== 'string' || host.trim() === '') {
284
303
  throw new Error(
285
- `[JSKim] 設定値が不正です: serve.host\n` +
286
- `プロジェクト: ${projectName}\n` +
287
- `空でない文字列を指定してください。\n` +
288
- `受け取った値: ${String(host)}`
304
+ formatConfigValidationError({
305
+ projectName,
306
+ configKey: 'serve.host',
307
+ detail: '空でない文字列を指定してください。',
308
+ received: String(host),
309
+ })
289
310
  );
290
311
  }
291
312
 
@@ -296,10 +317,12 @@ function validateServeConfig(serve, projectName) {
296
317
  port > 65535
297
318
  ) {
298
319
  throw new Error(
299
- `[JSKim] 設定値が不正です: serve.port\n` +
300
- `プロジェクト: ${projectName}\n` +
301
- `1から65535までの整数を指定してください。\n` +
302
- `受け取った値: ${String(port)}`
320
+ formatConfigValidationError({
321
+ projectName,
322
+ configKey: 'serve.port',
323
+ detail: '1から65535までの整数を指定してください。',
324
+ received: String(port),
325
+ })
303
326
  );
304
327
  }
305
328
  }
@@ -309,10 +332,12 @@ function validateDevConfig(dev, projectName) {
309
332
 
310
333
  if (typeof liveReload !== 'boolean') {
311
334
  throw new Error(
312
- `[JSKim] 設定値が不正です: dev.liveReload\n` +
313
- `プロジェクト: ${projectName}\n` +
314
- `boolean を指定してください。\n` +
315
- `受け取った値: ${String(liveReload)}`
335
+ formatConfigValidationError({
336
+ projectName,
337
+ configKey: 'dev.liveReload',
338
+ detail: 'boolean を指定してください。',
339
+ received: String(liveReload),
340
+ })
316
341
  );
317
342
  }
318
343
  }
@@ -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
+ };