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