@ywal123456/jskim 0.1.1 → 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 +113 -145
- package/docs/configuration.md +217 -175
- package/docs/create-jskim.md +22 -7
- package/package.json +1 -1
- package/scripts/commands/dev-command.js +10 -109
- package/scripts/commands/watch-command.js +15 -25
- package/scripts/lib/build-project.js +18 -12
- package/scripts/lib/create-nunjucks-env.js +39 -1
- package/scripts/lib/create-watch-runtime.js +516 -0
- 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
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const chokidar = require('chokidar');
|
|
6
|
+
const { loadConfig, CONFIG_FILENAME } = require('./load-config');
|
|
7
|
+
const { resolveProject } = require('./resolve-project');
|
|
8
|
+
const { createProjectWatcher } = require('./create-project-watcher');
|
|
9
|
+
const { createStaticServer } = require('./create-static-server');
|
|
10
|
+
const { createLiveReload } = require('./create-live-reload');
|
|
11
|
+
const { formatListenError } = require('../commands/serve-errors');
|
|
12
|
+
|
|
13
|
+
const DEV_RESTART_KEYS = [
|
|
14
|
+
'outputDir',
|
|
15
|
+
'serve.host',
|
|
16
|
+
'serve.port',
|
|
17
|
+
'dev.liveReload',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* watch / dev 共通の実行ランタイムです。
|
|
22
|
+
* jskim.config.js の hot reload と project watcher 再構成を担当します。
|
|
23
|
+
*
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @param {'watch'|'dev'} options.mode
|
|
26
|
+
* @param {string} options.workspaceRoot
|
|
27
|
+
* @param {string|undefined} options.projectName
|
|
28
|
+
* @param {string} [options.commandName]
|
|
29
|
+
* @param {string} [options.usageLine]
|
|
30
|
+
* @returns {{ start: Function, close: Function }}
|
|
31
|
+
*/
|
|
32
|
+
function createWatchRuntime(options) {
|
|
33
|
+
const mode = options.mode;
|
|
34
|
+
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
35
|
+
const projectName = options.projectName;
|
|
36
|
+
const commandName = options.commandName || mode;
|
|
37
|
+
const usageLine =
|
|
38
|
+
options.usageLine || `npm run ${commandName} -- <project-name>`;
|
|
39
|
+
|
|
40
|
+
let currentProject = null;
|
|
41
|
+
let configPath = null;
|
|
42
|
+
let projectWatcher = null;
|
|
43
|
+
let configWatcher = null;
|
|
44
|
+
let liveReload = null;
|
|
45
|
+
let staticServer = null;
|
|
46
|
+
let liveReloadEnabled = false;
|
|
47
|
+
|
|
48
|
+
let stopping = false;
|
|
49
|
+
let started = false;
|
|
50
|
+
let configDebounceTimer = null;
|
|
51
|
+
let configReloadPromise = null;
|
|
52
|
+
let pendingConfigReload = false;
|
|
53
|
+
let sourceBuilding = false;
|
|
54
|
+
|
|
55
|
+
function resolveCurrentProject() {
|
|
56
|
+
const loaded = loadConfig(workspaceRoot);
|
|
57
|
+
const project = resolveProject({
|
|
58
|
+
config: loaded.config,
|
|
59
|
+
workspaceRoot,
|
|
60
|
+
projectName,
|
|
61
|
+
commandName,
|
|
62
|
+
usageLine,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
project,
|
|
66
|
+
configPath: loaded.configPath,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function start() {
|
|
71
|
+
if (started) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
started = true;
|
|
75
|
+
|
|
76
|
+
const resolved = resolveCurrentProject();
|
|
77
|
+
currentProject = resolved.project;
|
|
78
|
+
configPath = resolved.configPath;
|
|
79
|
+
|
|
80
|
+
if (mode === 'dev') {
|
|
81
|
+
await startDevSession(currentProject, { initial: true });
|
|
82
|
+
} else {
|
|
83
|
+
await startWatchSession(currentProject, { initial: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
beginConfigWatching();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function startWatchSession(project, { initial }) {
|
|
90
|
+
projectWatcher = createProjectWatcher(project, {
|
|
91
|
+
runInitialBuild: true,
|
|
92
|
+
logChanges: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
wireSourceBuildTracking(projectWatcher);
|
|
96
|
+
|
|
97
|
+
if (initial) {
|
|
98
|
+
projectWatcher.on('ready', ({ displayPaths, debounceMs }) => {
|
|
99
|
+
console.log(`[JSKim] プロジェクトを監視しています: ${project.name}`);
|
|
100
|
+
console.log('パス:');
|
|
101
|
+
for (const display of displayPaths) {
|
|
102
|
+
console.log(`- ${display}`);
|
|
103
|
+
}
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(`Debounce: ${debounceMs}ms`);
|
|
106
|
+
console.log('終了するには Ctrl+C を押してください。');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await projectWatcher.start();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function startDevSession(project, { initial }) {
|
|
114
|
+
liveReloadEnabled = project.dev.liveReload;
|
|
115
|
+
const host = project.serve.host.trim();
|
|
116
|
+
const port = project.serve.port;
|
|
117
|
+
const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
|
|
118
|
+
|
|
119
|
+
liveReload = createLiveReload({
|
|
120
|
+
projectName: project.name,
|
|
121
|
+
enabled: liveReloadEnabled,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
staticServer = createStaticServer({
|
|
125
|
+
rootDir: project.outputDir,
|
|
126
|
+
host,
|
|
127
|
+
port,
|
|
128
|
+
projectName: project.name,
|
|
129
|
+
handleInternalRequest: (req, res, meta) =>
|
|
130
|
+
liveReload.handleRequest(req, res, meta),
|
|
131
|
+
transformHtml: liveReloadEnabled
|
|
132
|
+
? (html) => liveReload.injectHtml(html)
|
|
133
|
+
: undefined,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
projectWatcher = createProjectWatcher(project, {
|
|
137
|
+
runInitialBuild: true,
|
|
138
|
+
logChanges: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
wireSourceBuildTracking(projectWatcher);
|
|
142
|
+
wireDevSourceReload(projectWatcher);
|
|
143
|
+
|
|
144
|
+
await projectWatcher.start({ watchFiles: false });
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await staticServer.start();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const formatted = formatListenError(err, {
|
|
150
|
+
projectName: project.name,
|
|
151
|
+
host,
|
|
152
|
+
port,
|
|
153
|
+
kind: '開発',
|
|
154
|
+
});
|
|
155
|
+
await cleanupDevComponents();
|
|
156
|
+
throw formatted;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await projectWatcher.startWatching();
|
|
160
|
+
|
|
161
|
+
if (initial) {
|
|
162
|
+
console.log('[JSKim] 開発サーバーを起動しました。');
|
|
163
|
+
console.log(`プロジェクト: ${project.name}`);
|
|
164
|
+
console.log(`ルート: ${outputDisplay}`);
|
|
165
|
+
console.log(`URL: ${staticServer.url}`);
|
|
166
|
+
console.log(
|
|
167
|
+
`ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
|
|
168
|
+
);
|
|
169
|
+
console.log('終了するには Ctrl+C を押してください。');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function wireSourceBuildTracking(watcher) {
|
|
174
|
+
watcher.on('build:start', () => {
|
|
175
|
+
sourceBuilding = true;
|
|
176
|
+
});
|
|
177
|
+
watcher.on('build:success', () => {
|
|
178
|
+
sourceBuilding = false;
|
|
179
|
+
maybeRunPendingConfigReload();
|
|
180
|
+
});
|
|
181
|
+
watcher.on('build:failure', () => {
|
|
182
|
+
sourceBuilding = false;
|
|
183
|
+
maybeRunPendingConfigReload();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function wireDevSourceReload(watcher) {
|
|
188
|
+
watcher.on('build:success', ({ initial }) => {
|
|
189
|
+
if (!initial && liveReloadEnabled && liveReload) {
|
|
190
|
+
liveReload.broadcastReload();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function beginConfigWatching() {
|
|
196
|
+
if (configWatcher || stopping) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
configWatcher = chokidar.watch(configPath, {
|
|
201
|
+
ignoreInitial: true,
|
|
202
|
+
persistent: true,
|
|
203
|
+
awaitWriteFinish: {
|
|
204
|
+
stabilityThreshold: 50,
|
|
205
|
+
pollInterval: 25,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
configWatcher.on('all', (eventName) => {
|
|
210
|
+
if (
|
|
211
|
+
eventName === 'add' ||
|
|
212
|
+
eventName === 'change' ||
|
|
213
|
+
eventName === 'unlink'
|
|
214
|
+
) {
|
|
215
|
+
queueConfigReload(eventName);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
configWatcher.on('error', (err) => {
|
|
220
|
+
const message = err && err.message ? err.message : String(err);
|
|
221
|
+
console.error(`[JSKim] 設定ファイルの監視エラー: ${message}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function queueConfigReload(eventName) {
|
|
226
|
+
if (stopping) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (eventName === 'unlink') {
|
|
231
|
+
console.log(
|
|
232
|
+
`[JSKim] 設定ファイルが一時的に削除されました。以前の正常な設定を継続します。`
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
console.log('[JSKim] 設定ファイルの変更を検出しました。');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (configDebounceTimer) {
|
|
239
|
+
clearTimeout(configDebounceTimer);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const debounceMs =
|
|
243
|
+
currentProject && currentProject.watch
|
|
244
|
+
? currentProject.watch.debounce
|
|
245
|
+
: 150;
|
|
246
|
+
|
|
247
|
+
configDebounceTimer = setTimeout(() => {
|
|
248
|
+
configDebounceTimer = null;
|
|
249
|
+
requestConfigReload();
|
|
250
|
+
}, debounceMs);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function requestConfigReload() {
|
|
254
|
+
if (stopping) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (configReloadPromise || sourceBuilding) {
|
|
259
|
+
pendingConfigReload = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
configReloadPromise = (async () => {
|
|
264
|
+
try {
|
|
265
|
+
await performConfigReload();
|
|
266
|
+
} finally {
|
|
267
|
+
configReloadPromise = null;
|
|
268
|
+
if (!stopping && pendingConfigReload) {
|
|
269
|
+
pendingConfigReload = false;
|
|
270
|
+
requestConfigReload();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function maybeRunPendingConfigReload() {
|
|
277
|
+
if (!stopping && pendingConfigReload && !configReloadPromise) {
|
|
278
|
+
pendingConfigReload = false;
|
|
279
|
+
requestConfigReload();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function performConfigReload() {
|
|
284
|
+
if (stopping) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!fs.existsSync(configPath)) {
|
|
289
|
+
// unlink 後まだ復元されていない
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let candidate;
|
|
294
|
+
try {
|
|
295
|
+
const loaded = loadConfig(workspaceRoot);
|
|
296
|
+
candidate = resolveProject({
|
|
297
|
+
config: loaded.config,
|
|
298
|
+
workspaceRoot,
|
|
299
|
+
projectName,
|
|
300
|
+
commandName,
|
|
301
|
+
usageLine,
|
|
302
|
+
});
|
|
303
|
+
} catch (err) {
|
|
304
|
+
const message = err && err.message ? err.message : String(err);
|
|
305
|
+
console.error('[JSKim] 設定ファイルの再読み込みに失敗しました。');
|
|
306
|
+
console.error(message);
|
|
307
|
+
console.error('[JSKim] 以前の正常な設定を継続します。');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (mode === 'dev') {
|
|
312
|
+
const restartKeys = getRestartRequiredChanges(currentProject, candidate);
|
|
313
|
+
if (restartKeys.length > 0) {
|
|
314
|
+
console.warn(
|
|
315
|
+
'[JSKim] 次の設定変更を反映するにはdev processの再起動が必要です:'
|
|
316
|
+
);
|
|
317
|
+
for (const key of restartKeys) {
|
|
318
|
+
console.warn(`- ${key}`);
|
|
319
|
+
}
|
|
320
|
+
console.warn('[JSKim] 以前の正常な設定を継続します。');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log('[JSKim] 設定を再読み込みしました。');
|
|
326
|
+
|
|
327
|
+
let buildSucceeded = false;
|
|
328
|
+
try {
|
|
329
|
+
await replaceProjectWatcher(candidate, {
|
|
330
|
+
onInitialBuildSuccess: () => {
|
|
331
|
+
buildSucceeded = true;
|
|
332
|
+
},
|
|
333
|
+
onInitialBuildFailure: () => {
|
|
334
|
+
buildSucceeded = false;
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
currentProject = candidate;
|
|
338
|
+
console.log('[JSKim] 監視対象を更新しました。');
|
|
339
|
+
if (mode === 'watch') {
|
|
340
|
+
console.log('パス:');
|
|
341
|
+
for (const display of projectWatcher.displayPaths) {
|
|
342
|
+
console.log(`- ${display}`);
|
|
343
|
+
}
|
|
344
|
+
console.log(`Debounce: ${projectWatcher.debounceMs}ms`);
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const message = err && err.message ? err.message : String(err);
|
|
348
|
+
console.error('[JSKim] 設定の再読み込み後に監視の更新に失敗しました。');
|
|
349
|
+
console.error(message);
|
|
350
|
+
console.error('[JSKim] 以前の正常な設定を継続します。');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!buildSucceeded) {
|
|
355
|
+
console.error(
|
|
356
|
+
'[JSKim] 設定の再読み込み後にbuildが失敗しました。監視は新しい設定で継続します。'
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (mode === 'dev' && liveReloadEnabled && liveReload) {
|
|
362
|
+
liveReload.broadcastReload();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function replaceProjectWatcher(project, hooks = {}) {
|
|
367
|
+
if (projectWatcher) {
|
|
368
|
+
try {
|
|
369
|
+
await projectWatcher.close();
|
|
370
|
+
} catch {
|
|
371
|
+
// reload 時の close エラーは無視
|
|
372
|
+
}
|
|
373
|
+
projectWatcher = null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
projectWatcher = createProjectWatcher(project, {
|
|
377
|
+
runInitialBuild: true,
|
|
378
|
+
logChanges: true,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
wireSourceBuildTracking(projectWatcher);
|
|
382
|
+
if (mode === 'dev') {
|
|
383
|
+
wireDevSourceReload(projectWatcher);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const onSuccess = ({ initial }) => {
|
|
387
|
+
if (initial && hooks.onInitialBuildSuccess) {
|
|
388
|
+
hooks.onInitialBuildSuccess();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const onFailure = ({ initial }) => {
|
|
392
|
+
if (initial && hooks.onInitialBuildFailure) {
|
|
393
|
+
hooks.onInitialBuildFailure();
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
projectWatcher.on('build:success', onSuccess);
|
|
397
|
+
projectWatcher.on('build:failure', onFailure);
|
|
398
|
+
|
|
399
|
+
await projectWatcher.start();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function cleanupDevComponents() {
|
|
403
|
+
if (projectWatcher) {
|
|
404
|
+
try {
|
|
405
|
+
await projectWatcher.close();
|
|
406
|
+
} catch {
|
|
407
|
+
// ignore
|
|
408
|
+
}
|
|
409
|
+
projectWatcher = null;
|
|
410
|
+
}
|
|
411
|
+
if (liveReload) {
|
|
412
|
+
try {
|
|
413
|
+
liveReload.close();
|
|
414
|
+
} catch {
|
|
415
|
+
// ignore
|
|
416
|
+
}
|
|
417
|
+
liveReload = null;
|
|
418
|
+
}
|
|
419
|
+
if (staticServer) {
|
|
420
|
+
try {
|
|
421
|
+
await staticServer.stop();
|
|
422
|
+
} catch {
|
|
423
|
+
// ignore
|
|
424
|
+
}
|
|
425
|
+
staticServer = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function close() {
|
|
430
|
+
if (stopping) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
stopping = true;
|
|
434
|
+
|
|
435
|
+
if (configDebounceTimer) {
|
|
436
|
+
clearTimeout(configDebounceTimer);
|
|
437
|
+
configDebounceTimer = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (configWatcher) {
|
|
441
|
+
try {
|
|
442
|
+
await configWatcher.close();
|
|
443
|
+
} catch {
|
|
444
|
+
// ignore
|
|
445
|
+
}
|
|
446
|
+
configWatcher = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (configReloadPromise) {
|
|
450
|
+
try {
|
|
451
|
+
await configReloadPromise;
|
|
452
|
+
} catch {
|
|
453
|
+
// ignore
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (mode === 'dev') {
|
|
458
|
+
await cleanupDevComponents();
|
|
459
|
+
} else if (projectWatcher) {
|
|
460
|
+
try {
|
|
461
|
+
await projectWatcher.close();
|
|
462
|
+
} catch {
|
|
463
|
+
// ignore
|
|
464
|
+
}
|
|
465
|
+
projectWatcher = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
start,
|
|
471
|
+
close,
|
|
472
|
+
get project() {
|
|
473
|
+
return currentProject;
|
|
474
|
+
},
|
|
475
|
+
get configFileName() {
|
|
476
|
+
return CONFIG_FILENAME;
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @param {object} previous
|
|
483
|
+
* @param {object} next
|
|
484
|
+
* @returns {string[]}
|
|
485
|
+
*/
|
|
486
|
+
function getRestartRequiredChanges(previous, next) {
|
|
487
|
+
const changes = [];
|
|
488
|
+
if (previous.outputDir !== next.outputDir) {
|
|
489
|
+
changes.push('outputDir');
|
|
490
|
+
}
|
|
491
|
+
if (previous.serve.host !== next.serve.host) {
|
|
492
|
+
changes.push('serve.host');
|
|
493
|
+
}
|
|
494
|
+
if (previous.serve.port !== next.serve.port) {
|
|
495
|
+
changes.push('serve.port');
|
|
496
|
+
}
|
|
497
|
+
if (previous.dev.liveReload !== next.dev.liveReload) {
|
|
498
|
+
changes.push('dev.liveReload');
|
|
499
|
+
}
|
|
500
|
+
return changes;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function toDisplayPath(abs, workspaceRoot) {
|
|
504
|
+
const resolved = path.resolve(abs);
|
|
505
|
+
const rel = path.relative(workspaceRoot, resolved);
|
|
506
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
507
|
+
return resolved.split(path.sep).join('/');
|
|
508
|
+
}
|
|
509
|
+
return rel.split(path.sep).join('/');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
module.exports = {
|
|
513
|
+
createWatchRuntime,
|
|
514
|
+
getRestartRequiredChanges,
|
|
515
|
+
DEV_RESTART_KEYS,
|
|
516
|
+
};
|
|
@@ -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 : {};
|