@ywal123456/jskim 0.2.0 → 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 +112 -162
- package/docs/configuration.md +204 -207
- package/docs/create-jskim.md +22 -7
- package/package.json +1 -1
- package/scripts/lib/build-project.js +18 -12
- package/scripts/lib/create-nunjucks-env.js +39 -1
- 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
|
@@ -7,13 +7,15 @@ const nunjucks = require('nunjucks');
|
|
|
7
7
|
/**
|
|
8
8
|
* プロジェクト用の Nunjucks 環境を作成します。
|
|
9
9
|
* loader ルート: sourceDir + templates[](重複除去)。
|
|
10
|
+
* filters / globals を登録します。
|
|
10
11
|
*
|
|
11
12
|
* @param {object} options
|
|
12
13
|
* @param {string} options.sourceDir
|
|
13
14
|
* @param {string[]} options.templates
|
|
15
|
+
* @param {object} [options.nunjucks]
|
|
14
16
|
* @returns {nunjucks.Environment}
|
|
15
17
|
*/
|
|
16
|
-
function createNunjucksEnv({ sourceDir, templates }) {
|
|
18
|
+
function createNunjucksEnv({ sourceDir, templates, nunjucks: nunjucksConfig }) {
|
|
17
19
|
const roots = [];
|
|
18
20
|
const seen = new Set();
|
|
19
21
|
|
|
@@ -48,14 +50,50 @@ function createNunjucksEnv({ sourceDir, templates }) {
|
|
|
48
50
|
noCache: true,
|
|
49
51
|
});
|
|
50
52
|
|
|
53
|
+
// HTML 互換のため autoescape は維持する。
|
|
54
|
+
// JSON/JS 出力が必要な filter は SafeString を返すこと。
|
|
51
55
|
const env = new nunjucks.Environment(loader, {
|
|
52
56
|
autoescape: true,
|
|
53
57
|
noCache: true,
|
|
54
58
|
});
|
|
55
59
|
|
|
60
|
+
const config =
|
|
61
|
+
nunjucksConfig && typeof nunjucksConfig === 'object' ? nunjucksConfig : {};
|
|
62
|
+
registerFilters(env, config.filters || {});
|
|
63
|
+
registerGlobals(env, config.globals || {});
|
|
64
|
+
|
|
56
65
|
return env;
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
function registerFilters(env, filters) {
|
|
69
|
+
for (const [name, fn] of Object.entries(filters)) {
|
|
70
|
+
env.addFilter(name, wrapSyncCallable(fn, `filter:${name}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function registerGlobals(env, globals) {
|
|
75
|
+
for (const [name, value] of Object.entries(globals)) {
|
|
76
|
+
if (typeof value === 'function') {
|
|
77
|
+
env.addGlobal(name, wrapSyncCallable(value, `global:${name}`));
|
|
78
|
+
} else {
|
|
79
|
+
env.addGlobal(name, value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function wrapSyncCallable(fn, label) {
|
|
85
|
+
return function wrapped(...args) {
|
|
86
|
+
const result = fn.apply(this, args);
|
|
87
|
+
if (result && typeof result.then === 'function') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[JSKim] 非同期${label.startsWith('filter') ? 'filter' : 'global'}は現在サポートされていません。\n` +
|
|
90
|
+
`対象: ${label}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
module.exports = {
|
|
60
98
|
createNunjucksEnv,
|
|
61
99
|
};
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* defaults とプロジェクト設定をマージします。
|
|
5
5
|
*
|
|
6
6
|
* - スカラー: プロジェクト側があれば優先
|
|
7
|
-
* - オブジェクト (build / watch / serve / dev): 1段階の shallow merge
|
|
8
|
-
* - 配列 (render / templates / copy): プロジェクト側配列が defaults を丸ごと置き換え
|
|
7
|
+
* - オブジェクト (build / watch / serve / dev / data / nunjucks): 1段階の shallow merge
|
|
8
|
+
* - 配列 (render / templates / copy / files): プロジェクト側配列が defaults を丸ごと置き換え
|
|
9
9
|
*
|
|
10
10
|
* 元の defaults / プロジェクトオブジェクトは変更しません。
|
|
11
11
|
*
|
|
@@ -23,6 +23,9 @@ function mergeConfig(defaults, project) {
|
|
|
23
23
|
render: pickArray(override.render, base.render, []),
|
|
24
24
|
templates: pickArray(override.templates, base.templates, []),
|
|
25
25
|
copy: pickArray(override.copy, base.copy, []),
|
|
26
|
+
files: pickOptionalArray(override.files, base.files),
|
|
27
|
+
data: mergePlainObject(base.data, override.data),
|
|
28
|
+
nunjucks: mergeNunjucks(base.nunjucks, override.nunjucks),
|
|
26
29
|
build: mergeBuild(base.build, override.build),
|
|
27
30
|
watch: mergeWatch(base.watch, override.watch),
|
|
28
31
|
serve: mergeServe(base.serve, override.serve),
|
|
@@ -35,6 +38,19 @@ function mergeConfig(defaults, project) {
|
|
|
35
38
|
render: merged.render.map((rule) => ({ ...rule })),
|
|
36
39
|
templates: [...merged.templates],
|
|
37
40
|
copy: merged.copy.map((rule) => ({ ...rule })),
|
|
41
|
+
files:
|
|
42
|
+
merged.files == null
|
|
43
|
+
? null
|
|
44
|
+
: merged.files.map((rule) => ({
|
|
45
|
+
...rule,
|
|
46
|
+
include: Array.isArray(rule.include) ? [...rule.include] : undefined,
|
|
47
|
+
exclude: Array.isArray(rule.exclude) ? [...rule.exclude] : undefined,
|
|
48
|
+
})),
|
|
49
|
+
data: isPlainObject(merged.data) ? { ...merged.data } : merged.data,
|
|
50
|
+
nunjucks: {
|
|
51
|
+
filters: { ...merged.nunjucks.filters },
|
|
52
|
+
globals: { ...merged.nunjucks.globals },
|
|
53
|
+
},
|
|
38
54
|
build: { ...merged.build },
|
|
39
55
|
watch: { ...merged.watch },
|
|
40
56
|
serve: { ...merged.serve },
|
|
@@ -56,6 +72,75 @@ function pickArray(projectValue, defaultValue, fallback) {
|
|
|
56
72
|
return fallback.slice();
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
/**
|
|
76
|
+
* files 未設定を null で表し、legacy と区別します。
|
|
77
|
+
* @returns {object[]|null}
|
|
78
|
+
*/
|
|
79
|
+
function pickOptionalArray(projectValue, defaultValue) {
|
|
80
|
+
if (Array.isArray(projectValue)) {
|
|
81
|
+
return projectValue.slice();
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(defaultValue)) {
|
|
84
|
+
return defaultValue.slice();
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mergePlainObject(defaultValue, projectValue) {
|
|
90
|
+
if (projectValue !== undefined && !isPlainObject(projectValue)) {
|
|
91
|
+
return projectValue;
|
|
92
|
+
}
|
|
93
|
+
if (projectValue === undefined && defaultValue !== undefined && !isPlainObject(defaultValue)) {
|
|
94
|
+
return defaultValue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const base = isPlainObject(defaultValue) ? defaultValue : {};
|
|
98
|
+
const override = isPlainObject(projectValue) ? projectValue : {};
|
|
99
|
+
return { ...base, ...override };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isPlainObject(value) {
|
|
103
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mergeNunjucks(defaultValue, projectValue) {
|
|
107
|
+
const base =
|
|
108
|
+
defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue)
|
|
109
|
+
? defaultValue
|
|
110
|
+
: {};
|
|
111
|
+
const override =
|
|
112
|
+
projectValue && typeof projectValue === 'object' && !Array.isArray(projectValue)
|
|
113
|
+
? projectValue
|
|
114
|
+
: {};
|
|
115
|
+
|
|
116
|
+
const baseFilters =
|
|
117
|
+
base.filters && typeof base.filters === 'object' && !Array.isArray(base.filters)
|
|
118
|
+
? base.filters
|
|
119
|
+
: {};
|
|
120
|
+
const overrideFilters =
|
|
121
|
+
override.filters &&
|
|
122
|
+
typeof override.filters === 'object' &&
|
|
123
|
+
!Array.isArray(override.filters)
|
|
124
|
+
? override.filters
|
|
125
|
+
: {};
|
|
126
|
+
|
|
127
|
+
const baseGlobals =
|
|
128
|
+
base.globals && typeof base.globals === 'object' && !Array.isArray(base.globals)
|
|
129
|
+
? base.globals
|
|
130
|
+
: {};
|
|
131
|
+
const overrideGlobals =
|
|
132
|
+
override.globals &&
|
|
133
|
+
typeof override.globals === 'object' &&
|
|
134
|
+
!Array.isArray(override.globals)
|
|
135
|
+
? override.globals
|
|
136
|
+
: {};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
filters: { ...baseFilters, ...overrideFilters },
|
|
140
|
+
globals: { ...baseGlobals, ...overrideGlobals },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
59
144
|
function mergeBuild(defaultBuild, projectBuild) {
|
|
60
145
|
const base =
|
|
61
146
|
defaultBuild && typeof defaultBuild === 'object' ? defaultBuild : {};
|
|
@@ -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
|
|