@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 +1 -1
- package/scripts/commands/path-display.js +1 -15
- package/scripts/lib/create-nunjucks-env.js +2 -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/process-files.js +73 -29
- package/scripts/lib/render-pages.js +23 -13
- package/scripts/lib/resolve-project.js +57 -32
- package/scripts/lib/to-display-path.js +28 -0
package/package.json
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
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
|
-
|
|
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] 設定ファイルが見つかりません: ${
|
|
30
|
+
`[JSKim] 設定ファイルが見つかりません: ${configDisplay}\n` +
|
|
29
31
|
`ワークスペースルートに ${CONFIG_FILENAME} を作成してください。`
|
|
30
32
|
);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
throw new Error(
|
|
35
|
-
`[JSKim] 設定ファイルの読み込みに失敗しました: ${
|
|
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] 設定が不正です: ${
|
|
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
|
-
`設定: ${
|
|
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
|
-
|
|
43
|
-
|
|
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(
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
`
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
};
|