@schalkneethling/miyagi-core 4.2.0 → 4.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/dist/js/iframe.js +1 -1
- package/frontend/assets/js/_socket.js +79 -6
- package/lib/config.js +24 -0
- package/lib/default-config.js +58 -0
- package/lib/init/args.js +13 -0
- package/lib/init/config.js +216 -0
- package/lib/init/watcher.js +605 -234
- package/package.json +3 -3
package/lib/init/watcher.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import anymatch from "anymatch";
|
|
7
|
+
import chokidar from "chokidar";
|
|
7
8
|
import fs from "fs";
|
|
8
9
|
import path from "path";
|
|
9
|
-
import watch from "node-watch";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
11
|
import getConfig from "../config.js";
|
|
12
12
|
import yargs from "./args.js";
|
|
@@ -19,64 +19,284 @@ import setEngines from "./engines.js";
|
|
|
19
19
|
import setStatic from "./static.js";
|
|
20
20
|
import setViews from "./views.js";
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const SOCKET_PATH = "/__miyagi_ws";
|
|
23
|
+
const sockets = new Set();
|
|
24
|
+
const DEFAULT_RELOAD_MESSAGE = {
|
|
25
|
+
type: "reload",
|
|
26
|
+
scope: "iframe",
|
|
27
|
+
reason: "change",
|
|
28
|
+
paths: [],
|
|
29
|
+
};
|
|
30
|
+
const RELOAD_SCOPES = new Set(["none", "iframe", "parent"]);
|
|
31
|
+
let restartFileWatcher = null;
|
|
24
32
|
|
|
25
33
|
/**
|
|
26
|
-
* @param {
|
|
27
|
-
* @
|
|
34
|
+
* @param {string} eventType
|
|
35
|
+
* @returns {number}
|
|
28
36
|
*/
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
function getEventPriority(eventType) {
|
|
38
|
+
// Higher value = stronger event for the same path during burst coalescing.
|
|
39
|
+
// Example: if a file emits "change" then "unlink", we keep "unlink".
|
|
40
|
+
switch (eventType) {
|
|
41
|
+
case "unlinkDir":
|
|
42
|
+
return 5;
|
|
43
|
+
case "unlink":
|
|
44
|
+
return 4;
|
|
45
|
+
case "addDir":
|
|
46
|
+
return 3;
|
|
47
|
+
case "add":
|
|
48
|
+
return 2;
|
|
49
|
+
case "change":
|
|
50
|
+
return 1;
|
|
51
|
+
default:
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} inputPath
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function normalizeRelativePath(inputPath) {
|
|
61
|
+
const absolutePath = path.resolve(inputPath);
|
|
62
|
+
return path.relative(process.cwd(), absolutePath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {object} source
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function resolveSourcePath(source) {
|
|
70
|
+
if (path.isAbsolute(source.path)) {
|
|
71
|
+
return source.path;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return path.resolve(process.cwd(), source.path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} scope
|
|
79
|
+
* @param {object} [payload]
|
|
80
|
+
*/
|
|
81
|
+
function sendReload(scope, payload = {}) {
|
|
82
|
+
const normalizedScope = RELOAD_SCOPES.has(scope) ? scope : "parent";
|
|
83
|
+
const reloadEnabled = global.config.watch?.reload?.enabled ?? global.config.ui.reload;
|
|
84
|
+
|
|
85
|
+
if (!reloadEnabled || normalizedScope === "none") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const message = JSON.stringify({
|
|
90
|
+
...DEFAULT_RELOAD_MESSAGE,
|
|
91
|
+
...payload,
|
|
92
|
+
scope: normalizedScope,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for (const ws of sockets) {
|
|
96
|
+
if (ws.readyState !== 1) {
|
|
97
|
+
sockets.delete(ws);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ws.send(message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} color
|
|
107
|
+
* @param {string} value
|
|
108
|
+
* @param {boolean} useColors
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
function colorize(color, value, useColors) {
|
|
112
|
+
if (!useColors) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const colors = {
|
|
117
|
+
cyan: "\x1b[36m",
|
|
118
|
+
green: "\x1b[32m",
|
|
119
|
+
yellow: "\x1b[33m",
|
|
120
|
+
grey: "\x1b[90m",
|
|
121
|
+
reset: "\x1b[0m",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return `${colors[color] || ""}${value}${colors.reset}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {object} report
|
|
129
|
+
* @param {object} watchConfig
|
|
130
|
+
*/
|
|
131
|
+
function printWatchReport(report, watchConfig) {
|
|
132
|
+
const reportConfig = watchConfig?.report || {};
|
|
133
|
+
if (!reportConfig.enabled || !reportConfig.onStart) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (reportConfig.format === "json") {
|
|
138
|
+
console.info(JSON.stringify(report));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const useColors = reportConfig.useColors && process.stdout.isTTY;
|
|
143
|
+
|
|
144
|
+
if (reportConfig.format === "summary") {
|
|
145
|
+
console.info(
|
|
146
|
+
`${colorize("cyan", "Watch report:", useColors)} backend=${report.backend
|
|
147
|
+
} sources=${report.meta.sourceCount} ignores=${report.meta.ignoreCount}`,
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.info(colorize("cyan", "\nWatch report", useColors));
|
|
153
|
+
console.info(
|
|
154
|
+
` ${colorize("grey", "Watcher backend:", useColors)} ${colorize(
|
|
155
|
+
"green",
|
|
156
|
+
report.backend,
|
|
157
|
+
useColors,
|
|
158
|
+
)}`,
|
|
159
|
+
);
|
|
160
|
+
console.info(
|
|
161
|
+
` ${colorize("grey", "Diagnostics:", useColors)} sources=${report.meta.sourceCount
|
|
162
|
+
}, ignores=${report.meta.ignoreCount}`,
|
|
163
|
+
);
|
|
164
|
+
console.info(` ${colorize("grey", "Resolved sources:", useColors)}`);
|
|
165
|
+
|
|
166
|
+
for (const source of report.sources) {
|
|
167
|
+
const stateLabel = source.exists ? "exists" : "missing";
|
|
168
|
+
const stateColor = source.exists ? "green" : "yellow";
|
|
169
|
+
console.info(
|
|
170
|
+
` - ${source.id} (${source.type}) ${source.resolvedPath} [${colorize(
|
|
171
|
+
stateColor,
|
|
172
|
+
stateLabel,
|
|
173
|
+
useColors,
|
|
174
|
+
)}]`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.info(` ${colorize("grey", "Ignored patterns:", useColors)}`);
|
|
179
|
+
for (const pattern of report.ignore.patterns) {
|
|
180
|
+
console.info(` - ${pattern}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.info(` ${colorize("grey", "Reload rules:", useColors)}`);
|
|
184
|
+
for (const [key, scope] of Object.entries(report.reload.rules)) {
|
|
185
|
+
console.info(` - ${key}: ${scope}`);
|
|
186
|
+
}
|
|
187
|
+
console.info("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {object} watchConfig
|
|
192
|
+
* @returns {object[]}
|
|
193
|
+
*/
|
|
194
|
+
function getExtensionSources(watchConfig) {
|
|
195
|
+
const extensionSources = [];
|
|
196
|
+
|
|
197
|
+
for (const extension of global.config.extensions) {
|
|
198
|
+
const ext = Array.isArray(extension) ? extension[0] : extension;
|
|
199
|
+
const opts =
|
|
200
|
+
Array.isArray(extension) && extension[1] ? extension[1] : { locales: {} };
|
|
201
|
+
|
|
202
|
+
if (!ext.extendWatcher) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const extensionWatch = ext.extendWatcher(opts);
|
|
207
|
+
if (!extensionWatch?.folder || !extensionWatch?.lang) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
extensionSources.push({
|
|
212
|
+
id: `extension-${extensionWatch.lang}`,
|
|
213
|
+
type: "dir",
|
|
214
|
+
path: path.join(extensionWatch.folder, extensionWatch.lang),
|
|
215
|
+
recursive: true,
|
|
216
|
+
optional: true,
|
|
34
217
|
});
|
|
35
218
|
}
|
|
36
219
|
|
|
37
|
-
|
|
38
|
-
log("success", `${t("updatingDone")}\n`);
|
|
220
|
+
return [...(watchConfig.sources || []), ...extensionSources];
|
|
39
221
|
}
|
|
40
222
|
|
|
41
223
|
/**
|
|
42
|
-
* @param {
|
|
43
|
-
* @
|
|
44
|
-
* @returns {boolean} is true if the triggered events include the events to check against
|
|
224
|
+
* @param {object} watchConfig
|
|
225
|
+
* @returns {object}
|
|
45
226
|
*/
|
|
46
|
-
function
|
|
47
|
-
const
|
|
227
|
+
function resolveWatchTargets(watchConfig) {
|
|
228
|
+
const sources = getExtensionSources(watchConfig);
|
|
229
|
+
const ignorePatterns = [
|
|
230
|
+
...(watchConfig.ignore?.defaults ? ["node_modules/**", ".git/**"] : []),
|
|
231
|
+
...(watchConfig.ignore?.patterns || []),
|
|
232
|
+
];
|
|
233
|
+
const reportSources = [];
|
|
234
|
+
const targets = [];
|
|
235
|
+
|
|
236
|
+
for (const source of sources) {
|
|
237
|
+
if (!source || typeof source.path !== "string") continue;
|
|
238
|
+
const resolvedPath = resolveSourcePath(source);
|
|
239
|
+
const exists = fs.existsSync(resolvedPath);
|
|
240
|
+
|
|
241
|
+
reportSources.push({
|
|
242
|
+
id: source.id,
|
|
243
|
+
type: source.type,
|
|
244
|
+
inputPath: source.path,
|
|
245
|
+
resolvedPath,
|
|
246
|
+
exists,
|
|
247
|
+
recursive: source.recursive !== false,
|
|
248
|
+
optional: source.optional === true,
|
|
249
|
+
ignored: anymatch(ignorePatterns, normalizeRelativePath(resolvedPath)),
|
|
250
|
+
});
|
|
48
251
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return true;
|
|
252
|
+
if (exists) {
|
|
253
|
+
targets.push(resolvedPath);
|
|
52
254
|
}
|
|
53
255
|
}
|
|
54
256
|
|
|
55
|
-
return
|
|
257
|
+
return {
|
|
258
|
+
targets: [...new Set(targets)].sort(),
|
|
259
|
+
ignorePatterns,
|
|
260
|
+
report: {
|
|
261
|
+
backend: "chokidar",
|
|
262
|
+
sources: reportSources.sort((a, b) => a.id.localeCompare(b.id)),
|
|
263
|
+
ignore: {
|
|
264
|
+
defaultsEnabled: Boolean(watchConfig.ignore?.defaults),
|
|
265
|
+
patterns: [...new Set(ignorePatterns)].sort(),
|
|
266
|
+
},
|
|
267
|
+
reload: {
|
|
268
|
+
rules: watchConfig.reload.rules,
|
|
269
|
+
},
|
|
270
|
+
meta: {
|
|
271
|
+
sourceCount: reportSources.length,
|
|
272
|
+
ignoreCount: [...new Set(ignorePatterns)].length,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
56
276
|
}
|
|
57
277
|
|
|
58
278
|
/**
|
|
59
|
-
* @param {
|
|
60
|
-
* @returns {Promise<object>}
|
|
279
|
+
* @param {Array<{ eventType: string, changedPath: string, relativePath: string }>} events
|
|
280
|
+
* @returns {Promise<object>}
|
|
61
281
|
*/
|
|
62
282
|
async function updateFileContents(events) {
|
|
63
283
|
const data = helpers.cloneDeep(global.state.fileContents);
|
|
64
284
|
|
|
65
285
|
try {
|
|
66
286
|
await Promise.all(
|
|
67
|
-
events.map(async ({ changedPath }) => {
|
|
68
|
-
const fullPath = path.
|
|
287
|
+
events.map(async ({ changedPath, relativePath }) => {
|
|
288
|
+
const fullPath = path.resolve(changedPath);
|
|
69
289
|
|
|
70
290
|
if (
|
|
71
|
-
fs.existsSync(
|
|
72
|
-
fs.lstatSync(
|
|
73
|
-
(helpers.fileIsTemplateFile(
|
|
74
|
-
helpers.fileIsDataFile(
|
|
75
|
-
helpers.fileIsDocumentationFile(
|
|
76
|
-
helpers.fileIsSchemaFile(
|
|
291
|
+
fs.existsSync(fullPath) &&
|
|
292
|
+
fs.lstatSync(fullPath).isFile() &&
|
|
293
|
+
(helpers.fileIsTemplateFile(relativePath) ||
|
|
294
|
+
helpers.fileIsDataFile(relativePath) ||
|
|
295
|
+
helpers.fileIsDocumentationFile(relativePath) ||
|
|
296
|
+
helpers.fileIsSchemaFile(relativePath))
|
|
77
297
|
) {
|
|
78
298
|
try {
|
|
79
|
-
const result = await readFile(
|
|
299
|
+
const result = await readFile(fullPath);
|
|
80
300
|
data[fullPath] = result;
|
|
81
301
|
return Promise.resolve();
|
|
82
302
|
} catch (err) {
|
|
@@ -96,9 +316,18 @@ async function updateFileContents(events) {
|
|
|
96
316
|
}
|
|
97
317
|
|
|
98
318
|
/**
|
|
99
|
-
*
|
|
319
|
+
* @param {string} changedPath
|
|
320
|
+
* @returns {boolean}
|
|
321
|
+
*/
|
|
322
|
+
function pathExistsAsDirectory(changedPath) {
|
|
323
|
+
return fs.existsSync(changedPath) && fs.lstatSync(changedPath).isDirectory();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {Array<{ eventType: string, changedPath: string, relativePath: string }>} events
|
|
328
|
+
* @returns {Promise<void>}
|
|
100
329
|
*/
|
|
101
|
-
async function handleFileChange() {
|
|
330
|
+
async function handleFileChange(events) {
|
|
102
331
|
for (const extension of global.config.extensions) {
|
|
103
332
|
const ext = Array.isArray(extension) ? extension[0] : extension;
|
|
104
333
|
const opts =
|
|
@@ -109,12 +338,58 @@ async function handleFileChange() {
|
|
|
109
338
|
}
|
|
110
339
|
}
|
|
111
340
|
|
|
112
|
-
|
|
341
|
+
const watchRules = global.config.watch.reload.rules;
|
|
342
|
+
const templateEvents = events.filter(({ relativePath }) =>
|
|
343
|
+
helpers.fileIsTemplateFile(relativePath),
|
|
344
|
+
);
|
|
345
|
+
const dataEvents = events.filter(({ relativePath }) =>
|
|
346
|
+
helpers.fileIsDataFile(relativePath),
|
|
347
|
+
);
|
|
348
|
+
const docEvents = events.filter(({ relativePath }) =>
|
|
349
|
+
helpers.fileIsDocumentationFile(relativePath),
|
|
350
|
+
);
|
|
351
|
+
const schemaEvents = events.filter(({ relativePath }) =>
|
|
352
|
+
helpers.fileIsSchemaFile(relativePath),
|
|
353
|
+
);
|
|
354
|
+
const componentAssetEvents = events.filter(({ relativePath }) =>
|
|
355
|
+
helpers.fileIsAssetFile(relativePath),
|
|
356
|
+
);
|
|
357
|
+
const cssEvents = events.filter(({ relativePath }) => relativePath.endsWith(".css"));
|
|
358
|
+
const jsEvents = events.filter(({ relativePath }) => relativePath.endsWith(".js"));
|
|
359
|
+
const hasRemoveEvents = events.some(({ eventType }) =>
|
|
360
|
+
["unlink", "unlinkDir"].includes(eventType),
|
|
361
|
+
);
|
|
362
|
+
const hasDirectoryEvents = events.some(
|
|
363
|
+
({ changedPath, eventType }) =>
|
|
364
|
+
["addDir", "unlinkDir"].includes(eventType) || pathExistsAsDirectory(changedPath),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const configFilePath = global.config.userFileName
|
|
368
|
+
? path.resolve(process.cwd(), global.config.userFileName)
|
|
369
|
+
: null;
|
|
370
|
+
const hasConfigFileEvent =
|
|
371
|
+
configFilePath &&
|
|
372
|
+
events.some(({ changedPath }) => path.resolve(changedPath) === configFilePath);
|
|
373
|
+
|
|
374
|
+
if (hasConfigFileEvent && global.config.watch.configFile.enabled) {
|
|
375
|
+
await configurationFileUpdated();
|
|
376
|
+
sendReload("parent", {
|
|
377
|
+
reason: "config",
|
|
378
|
+
paths: [global.config.userFileName],
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
113
383
|
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
384
|
+
hasDirectoryEvents &&
|
|
385
|
+
!hasRemoveEvents &&
|
|
386
|
+
templateEvents.length === 0 &&
|
|
387
|
+
dataEvents.length === 0 &&
|
|
388
|
+
docEvents.length === 0 &&
|
|
389
|
+
schemaEvents.length === 0 &&
|
|
390
|
+
componentAssetEvents.length === 0 &&
|
|
391
|
+
cssEvents.length === 0 &&
|
|
392
|
+
jsEvents.length === 0
|
|
118
393
|
) {
|
|
119
394
|
await setState({
|
|
120
395
|
sourceTree: true,
|
|
@@ -123,117 +398,135 @@ async function handleFileChange() {
|
|
|
123
398
|
partials: true,
|
|
124
399
|
});
|
|
125
400
|
|
|
126
|
-
|
|
401
|
+
sendReload(watchRules.unknown, {
|
|
402
|
+
reason: "directory",
|
|
403
|
+
paths: events.map(({ relativePath }) => relativePath),
|
|
404
|
+
});
|
|
405
|
+
log("success", `${t("updatingDone")}\n`);
|
|
406
|
+
return;
|
|
127
407
|
}
|
|
128
408
|
|
|
129
|
-
|
|
130
|
-
else if (triggeredEventsIncludes(triggeredEvents, ["remove"])) {
|
|
409
|
+
if (hasRemoveEvents) {
|
|
131
410
|
await setState({
|
|
132
411
|
sourceTree: true,
|
|
133
|
-
fileContents: await updateFileContents(
|
|
412
|
+
fileContents: await updateFileContents(events),
|
|
134
413
|
menu: true,
|
|
135
414
|
partials: true,
|
|
136
415
|
});
|
|
137
416
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
417
|
+
sendReload("parent", {
|
|
418
|
+
reason: "remove",
|
|
419
|
+
paths: events.map(({ relativePath }) => relativePath),
|
|
420
|
+
});
|
|
421
|
+
log("success", `${t("updatingDone")}\n`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (templateEvents.length > 0) {
|
|
426
|
+
const allTemplatePathsExistAsPartials = templateEvents.every(({ relativePath }) => {
|
|
427
|
+
const shortPath = relativePath.replace(
|
|
428
|
+
path.join(global.config.components.folder, "/"),
|
|
429
|
+
"",
|
|
430
|
+
);
|
|
431
|
+
return Object.keys(global.state.partials).includes(shortPath);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (allTemplatePathsExistAsPartials) {
|
|
154
435
|
await setState({
|
|
155
|
-
fileContents: await updateFileContents(
|
|
436
|
+
fileContents: await updateFileContents(templateEvents),
|
|
156
437
|
});
|
|
157
438
|
|
|
158
|
-
|
|
439
|
+
sendReload(watchRules.template, {
|
|
440
|
+
reason: "template",
|
|
441
|
+
paths: templateEvents.map(({ relativePath }) => relativePath),
|
|
442
|
+
});
|
|
159
443
|
} else {
|
|
160
|
-
// added
|
|
161
444
|
await setState({
|
|
162
|
-
fileContents: await updateFileContents(
|
|
445
|
+
fileContents: await updateFileContents(templateEvents),
|
|
163
446
|
sourceTree: true,
|
|
164
447
|
menu: true,
|
|
165
448
|
partials: true,
|
|
166
449
|
});
|
|
167
450
|
|
|
168
|
-
|
|
451
|
+
sendReload("parent", {
|
|
452
|
+
reason: "template-added",
|
|
453
|
+
paths: templateEvents.map(({ relativePath }) => relativePath),
|
|
454
|
+
});
|
|
169
455
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
) {
|
|
176
|
-
const hasBeenAdded =
|
|
177
|
-
|
|
456
|
+
|
|
457
|
+
log("success", `${t("updatingDone")}\n`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (dataEvents.length > 0) {
|
|
462
|
+
const hasBeenAdded = dataEvents.some(
|
|
463
|
+
({ eventType, changedPath }) =>
|
|
464
|
+
eventType === "add" ||
|
|
465
|
+
!Object.keys(global.state.fileContents).includes(path.resolve(changedPath)),
|
|
178
466
|
);
|
|
179
467
|
|
|
180
468
|
await setState({
|
|
181
|
-
fileContents: await updateFileContents(
|
|
469
|
+
fileContents: await updateFileContents(dataEvents),
|
|
182
470
|
sourceTree: hasBeenAdded,
|
|
183
471
|
menu: true,
|
|
184
472
|
});
|
|
185
473
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
474
|
+
sendReload(watchRules.data, {
|
|
475
|
+
reason: hasBeenAdded ? "data-added" : "data",
|
|
476
|
+
paths: dataEvents.map(({ relativePath }) => relativePath),
|
|
477
|
+
});
|
|
478
|
+
log("success", `${t("updatingDone")}\n`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (docEvents.length > 0) {
|
|
483
|
+
const hasBeenAdded = docEvents.some(
|
|
484
|
+
({ eventType, changedPath }) =>
|
|
485
|
+
eventType === "add" ||
|
|
486
|
+
!Object.keys(global.state.fileContents).includes(path.resolve(changedPath)),
|
|
195
487
|
);
|
|
196
488
|
|
|
197
489
|
await setState({
|
|
198
|
-
fileContents: await updateFileContents(
|
|
490
|
+
fileContents: await updateFileContents(docEvents),
|
|
199
491
|
sourceTree: hasBeenAdded,
|
|
200
492
|
menu: hasBeenAdded,
|
|
201
493
|
});
|
|
202
494
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
495
|
+
sendReload(watchRules.docs, {
|
|
496
|
+
reason: hasBeenAdded ? "docs-added" : "docs",
|
|
497
|
+
paths: docEvents.map(({ relativePath }) => relativePath),
|
|
498
|
+
});
|
|
499
|
+
log("success", `${t("updatingDone")}\n`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (schemaEvents.length > 0) {
|
|
210
504
|
await setState({
|
|
211
|
-
fileContents: await updateFileContents(
|
|
505
|
+
fileContents: await updateFileContents(schemaEvents),
|
|
212
506
|
});
|
|
213
507
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
508
|
+
sendReload(watchRules.schema, {
|
|
509
|
+
reason: "schema",
|
|
510
|
+
paths: schemaEvents.map(({ relativePath }) => relativePath),
|
|
511
|
+
});
|
|
512
|
+
log("success", `${t("updatingDone")}\n`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (componentAssetEvents.length > 0) {
|
|
517
|
+
sendReload(watchRules.componentAsset, {
|
|
518
|
+
reason: "component-asset",
|
|
519
|
+
paths: componentAssetEvents.map(({ relativePath }) => relativePath),
|
|
520
|
+
});
|
|
521
|
+
log("success", `${t("updatingDone")}\n`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (cssEvents.length > 0) {
|
|
231
526
|
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
})
|
|
527
|
+
cssEvents.some(({ relativePath }) =>
|
|
528
|
+
global.config.assets.customProperties.files.includes(relativePath),
|
|
529
|
+
)
|
|
237
530
|
) {
|
|
238
531
|
await setState({
|
|
239
532
|
css: true,
|
|
@@ -244,156 +537,231 @@ async function handleFileChange() {
|
|
|
244
537
|
});
|
|
245
538
|
}
|
|
246
539
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// updated file is a js file
|
|
251
|
-
} else if (
|
|
252
|
-
triggeredEvents.find(({ changedPath }) => {
|
|
253
|
-
return changedPath.endsWith(".js");
|
|
254
|
-
})
|
|
255
|
-
) {
|
|
256
|
-
await setState({
|
|
257
|
-
menu: true,
|
|
540
|
+
sendReload(watchRules.globalCss, {
|
|
541
|
+
reason: "css",
|
|
542
|
+
paths: cssEvents.map(({ relativePath }) => relativePath),
|
|
258
543
|
});
|
|
544
|
+
log("success", `${t("updatingDone")}\n`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
259
547
|
|
|
260
|
-
|
|
261
|
-
changeFileCallback(true, false);
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
548
|
+
if (jsEvents.length > 0) {
|
|
264
549
|
await setState({
|
|
265
|
-
sourceTree: true,
|
|
266
|
-
fileContents: true,
|
|
267
550
|
menu: true,
|
|
268
|
-
partials: true,
|
|
269
551
|
});
|
|
270
552
|
|
|
271
|
-
|
|
553
|
+
sendReload(watchRules.globalJs, {
|
|
554
|
+
reason: "js",
|
|
555
|
+
paths: jsEvents.map(({ relativePath }) => relativePath),
|
|
556
|
+
});
|
|
557
|
+
log("success", `${t("updatingDone")}\n`);
|
|
558
|
+
return;
|
|
272
559
|
}
|
|
560
|
+
|
|
561
|
+
await setState({
|
|
562
|
+
sourceTree: true,
|
|
563
|
+
fileContents: true,
|
|
564
|
+
menu: true,
|
|
565
|
+
partials: true,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
sendReload(watchRules.unknown, {
|
|
569
|
+
reason: "unknown",
|
|
570
|
+
paths: events.map(({ relativePath }) => relativePath),
|
|
571
|
+
});
|
|
572
|
+
log("success", `${t("updatingDone")}\n`);
|
|
273
573
|
}
|
|
274
574
|
|
|
275
|
-
|
|
575
|
+
/**
|
|
576
|
+
* @param {object} pendingByPath
|
|
577
|
+
* @returns {Array<{ eventType: string, changedPath: string, relativePath: string }>}
|
|
578
|
+
*/
|
|
579
|
+
function snapshotPendingEvents(pendingByPath) {
|
|
580
|
+
return [...pendingByPath.values()]
|
|
581
|
+
// Process highest-priority events first to keep structural changes deterministic.
|
|
582
|
+
.sort((a, b) => getEventPriority(b.eventType) - getEventPriority(a.eventType))
|
|
583
|
+
.map((entry) => ({
|
|
584
|
+
eventType: entry.eventType,
|
|
585
|
+
changedPath: entry.changedPath,
|
|
586
|
+
relativePath: normalizeRelativePath(entry.changedPath),
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
276
589
|
|
|
277
590
|
/**
|
|
278
591
|
* @param {object} server
|
|
279
592
|
*/
|
|
280
593
|
export default function Watcher(server) {
|
|
281
594
|
const wss = new WebSocketServer({ noServer: true });
|
|
595
|
+
let watcher;
|
|
596
|
+
let debounceTimer = null;
|
|
597
|
+
let isProcessing = false;
|
|
598
|
+
const pendingByPath = new Map();
|
|
599
|
+
|
|
600
|
+
const startHeartbeat = () => {
|
|
601
|
+
const heartbeatConfig = global.config.watch?.socket?.heartbeat;
|
|
602
|
+
if (!heartbeatConfig?.enabled) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
282
605
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
606
|
+
return setInterval(() => {
|
|
607
|
+
for (const ws of sockets) {
|
|
608
|
+
if (ws.readyState !== 1) {
|
|
609
|
+
sockets.delete(ws);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
286
612
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
});
|
|
613
|
+
ws.ping();
|
|
614
|
+
}
|
|
615
|
+
}, heartbeatConfig.intervalMs);
|
|
616
|
+
};
|
|
292
617
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const sharedCssToWatch = (assets.shared?.css || [])
|
|
296
|
-
.filter(
|
|
297
|
-
(f) =>
|
|
298
|
-
!f.startsWith("http://") &&
|
|
299
|
-
!f.startsWith("https://") &&
|
|
300
|
-
!f.startsWith("://"),
|
|
301
|
-
)
|
|
302
|
-
.map((f) => path.join(global.config.assets.root, f));
|
|
303
|
-
|
|
304
|
-
const sharedJsToWatch = (assets.shared?.js || [])
|
|
305
|
-
.map((file) => file.src || file)
|
|
306
|
-
.filter(
|
|
307
|
-
(f) =>
|
|
308
|
-
!f.startsWith("http://") &&
|
|
309
|
-
!f.startsWith("https://") &&
|
|
310
|
-
!f.startsWith("://"),
|
|
311
|
-
)
|
|
312
|
-
.map((f) => path.join(global.config.assets.root, f));
|
|
313
|
-
|
|
314
|
-
const foldersToWatch = [
|
|
315
|
-
...assets.folder.map((f) => path.join(global.config.assets.root, f)),
|
|
316
|
-
...assets.css
|
|
317
|
-
.filter(
|
|
318
|
-
(f) =>
|
|
319
|
-
!f.startsWith("http://") &&
|
|
320
|
-
!f.startsWith("https://") &&
|
|
321
|
-
!f.startsWith("://"),
|
|
322
|
-
)
|
|
323
|
-
.map((f) => path.join(global.config.assets.root, f)),
|
|
324
|
-
...assets.js
|
|
325
|
-
.map((file) => file.src)
|
|
326
|
-
.filter(
|
|
327
|
-
(f) =>
|
|
328
|
-
!f.startsWith("http://") &&
|
|
329
|
-
!f.startsWith("https://") &&
|
|
330
|
-
!f.startsWith("://"),
|
|
331
|
-
)
|
|
332
|
-
.map((f) => path.join(global.config.assets.root, f)),
|
|
333
|
-
...sharedCssToWatch,
|
|
334
|
-
...sharedJsToWatch,
|
|
335
|
-
];
|
|
618
|
+
const heartbeatInterval = startHeartbeat();
|
|
336
619
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
620
|
+
const processPendingEvents = async () => {
|
|
621
|
+
if (isProcessing) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
340
624
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
625
|
+
if (pendingByPath.size === 0) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
344
628
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
629
|
+
isProcessing = true;
|
|
630
|
+
try {
|
|
631
|
+
const events = snapshotPendingEvents(pendingByPath);
|
|
632
|
+
pendingByPath.clear();
|
|
633
|
+
await handleFileChange(events);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
log("error", "Error while processing file changes.", error);
|
|
636
|
+
} finally {
|
|
637
|
+
isProcessing = false;
|
|
638
|
+
if (pendingByPath.size > 0) {
|
|
639
|
+
void processPendingEvents();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const enqueueEvent = (eventType, changedPath) => {
|
|
645
|
+
const absolutePath = path.resolve(changedPath);
|
|
646
|
+
const existing = pendingByPath.get(absolutePath);
|
|
647
|
+
const shouldReplace =
|
|
648
|
+
!existing ||
|
|
649
|
+
getEventPriority(eventType) >= getEventPriority(existing.eventType);
|
|
650
|
+
|
|
651
|
+
if (shouldReplace) {
|
|
652
|
+
// Keep only the strongest event per path inside the coalescing window.
|
|
653
|
+
pendingByPath.set(absolutePath, {
|
|
654
|
+
eventType,
|
|
655
|
+
changedPath: absolutePath,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const watchBehavior = global.config.watch?.behavior || {};
|
|
660
|
+
const delay = Math.max(
|
|
661
|
+
watchBehavior.debounceMs || 0,
|
|
662
|
+
watchBehavior.coalesceWindowMs || 0,
|
|
663
|
+
);
|
|
349
664
|
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
foldersToWatch.push(path.join(watch.folder, watch.lang));
|
|
665
|
+
if (debounceTimer) {
|
|
666
|
+
clearTimeout(debounceTimer);
|
|
353
667
|
}
|
|
354
|
-
}
|
|
355
668
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
669
|
+
debounceTimer = setTimeout(() => {
|
|
670
|
+
debounceTimer = null;
|
|
671
|
+
void processPendingEvents();
|
|
672
|
+
}, delay);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const setupFileWatcher = () => {
|
|
676
|
+
const watchConfig = global.config.watch || {};
|
|
677
|
+
if (watchConfig.enabled === false) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (watcher) {
|
|
682
|
+
void watcher.close();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const { targets, ignorePatterns, report } = resolveWatchTargets(watchConfig);
|
|
686
|
+
const awaitWriteFinish = watchConfig.behavior?.awaitWriteFinish || {};
|
|
687
|
+
|
|
688
|
+
printWatchReport(report, watchConfig);
|
|
689
|
+
|
|
690
|
+
if (targets.length === 0) {
|
|
691
|
+
log("error", t("watchingFilesFailed"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
watcher = chokidar.watch(targets, {
|
|
696
|
+
ignoreInitial: true,
|
|
697
|
+
persistent: true,
|
|
698
|
+
ignored(changedPath) {
|
|
699
|
+
const relativePath = normalizeRelativePath(changedPath);
|
|
700
|
+
return anymatch(ignorePatterns, relativePath);
|
|
701
|
+
},
|
|
702
|
+
awaitWriteFinish: awaitWriteFinish.enabled
|
|
703
|
+
? {
|
|
704
|
+
stabilityThreshold: awaitWriteFinish.stabilityThresholdMs,
|
|
705
|
+
pollInterval: awaitWriteFinish.pollIntervalMs,
|
|
706
|
+
}
|
|
707
|
+
: false,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
watcher.on("all", (eventType, changedPath) => {
|
|
711
|
+
const watchDebug = global.config.watch?.debug || {};
|
|
712
|
+
if (watchDebug.logEvents) {
|
|
713
|
+
log("info", `watch:event=${eventType} path=${changedPath}`);
|
|
360
714
|
}
|
|
715
|
+
|
|
716
|
+
log("info", t("updatingStarted"));
|
|
717
|
+
enqueueEvent(eventType, changedPath);
|
|
361
718
|
});
|
|
362
|
-
}
|
|
363
719
|
|
|
364
|
-
|
|
720
|
+
watcher.on("error", (error) => {
|
|
721
|
+
log("error", t("watchingFilesFailed"), error);
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
restartFileWatcher = setupFileWatcher;
|
|
365
725
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
726
|
+
wss.on("connection", function open(ws) {
|
|
727
|
+
sockets.add(ws);
|
|
728
|
+
|
|
729
|
+
ws.on("close", () => {
|
|
730
|
+
sockets.delete(ws);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
ws.on("error", () => {
|
|
734
|
+
sockets.delete(ws);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
server.on("upgrade", (request, socket, head) => {
|
|
739
|
+
const requestUrl = new URL(
|
|
740
|
+
request.url || "/",
|
|
741
|
+
`http://${request.headers.host || "localhost"}`,
|
|
376
742
|
);
|
|
377
|
-
} catch (e) {
|
|
378
|
-
log("error", e);
|
|
379
|
-
}
|
|
380
743
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
timeout = setTimeout(() => {
|
|
389
|
-
timeout = null;
|
|
390
|
-
handleFileChange();
|
|
391
|
-
}, 10);
|
|
392
|
-
}
|
|
744
|
+
if (requestUrl.pathname !== SOCKET_PATH) {
|
|
745
|
+
socket.destroy();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
750
|
+
wss.emit("connection", ws, request);
|
|
393
751
|
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
setupFileWatcher();
|
|
755
|
+
|
|
756
|
+
server.on("close", () => {
|
|
757
|
+
if (heartbeatInterval) {
|
|
758
|
+
clearInterval(heartbeatInterval);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (watcher) {
|
|
762
|
+
void watcher.close();
|
|
763
|
+
}
|
|
764
|
+
});
|
|
397
765
|
}
|
|
398
766
|
|
|
399
767
|
/**
|
|
@@ -406,18 +774,21 @@ async function configurationFileUpdated() {
|
|
|
406
774
|
|
|
407
775
|
if (config) {
|
|
408
776
|
global.config = config;
|
|
777
|
+
if (restartFileWatcher) {
|
|
778
|
+
restartFileWatcher();
|
|
779
|
+
}
|
|
409
780
|
await setEngines();
|
|
410
781
|
await setState({
|
|
411
782
|
sourceTree: true,
|
|
783
|
+
fileContents: true,
|
|
412
784
|
menu: true,
|
|
413
785
|
partials: true,
|
|
414
|
-
fileContents: true,
|
|
415
786
|
css: true,
|
|
416
787
|
});
|
|
788
|
+
|
|
417
789
|
setStatic();
|
|
418
790
|
setViews();
|
|
419
791
|
|
|
420
792
|
log("success", `${t("updatingConfigurationDone")}\n`);
|
|
421
|
-
changeFileCallback(true, true);
|
|
422
793
|
}
|
|
423
794
|
}
|