@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
403 lines (356 loc) • 9.34 kB
JavaScript
/**
* Module for watching user file changes
* @module initWatcher
*/
import anymatch from "anymatch";
import fs from "fs";
import path from "path";
import watch from "node-watch";
import { WebSocketServer } from "ws";
import getConfig from "../config.js";
import yargs from "./args.js";
import setState from "../state/index.js";
import { readFile } from "../state/file-contents.js";
import * as helpers from "../helpers.js";
import log from "../logger.js";
import { t } from "../i18n/index.js";
import setEngines from "./engines.js";
import setStatic from "./static.js";
import setViews from "./views.js";
let triggeredEvents = [];
let timeout;
/**
* @param {boolean} [reload] - is true if the page should be reloaded
* @param {boolean} [reloadParent] - is true if the parent window should be reloaded
*/
function changeFileCallback(reload, reloadParent) {
if (reload && global.config.ui.reload) {
// ioInstance.emit("fileChanged", reloadParent);
sockets.forEach((ws) => {
ws.send(reloadParent ? "reloadParent" : "");
});
}
triggeredEvents = [];
log("success", `${t("updatingDone")}\n`);
}
/**
* @param {Array} triggered - the triggered events
* @param {Array} events - array of events to check against
* @returns {boolean} is true if the triggered events include the events to check against
*/
function triggeredEventsIncludes(triggered, events) {
const flattened = triggered.map((event) => event.event);
for (let i = 0; i < flattened.length; i += 1) {
if (events.includes(flattened[i])) {
return true;
}
}
return false;
}
/**
* @param {object[]} events - array of event objects
* @returns {Promise<object>} the updated state.fileContents object
*/
async function updateFileContents(events) {
const data = helpers.cloneDeep(global.state.fileContents);
try {
await Promise.all(
events.map(async ({ changedPath }) => {
const fullPath = path.join(process.cwd(), changedPath);
if (
fs.existsSync(changedPath) &&
fs.lstatSync(changedPath).isFile() &&
(helpers.fileIsTemplateFile(changedPath) ||
helpers.fileIsDataFile(changedPath) ||
helpers.fileIsDocumentationFile(changedPath) ||
helpers.fileIsSchemaFile(changedPath))
) {
try {
const result = await readFile(changedPath);
data[fullPath] = result;
return Promise.resolve();
} catch (err) {
return Promise.reject(err.message);
}
} else {
delete data[fullPath];
return Promise.resolve();
}
}),
);
return data;
} catch (err) {
log("error", err);
}
}
/**
*
*/
async function handleFileChange() {
for (const extension of global.config.extensions) {
const ext = Array.isArray(extension) ? extension[0] : extension;
const opts =
Array.isArray(extension) && extension[1] ? extension[1] : { locales: {} };
if (ext.callbacks?.fileChanged) {
await ext.callbacks.fileChanged(opts);
}
}
// a directory has been changed
if (
triggeredEvents.some(
({ changedPath }) =>
fs.existsSync(changedPath) && fs.lstatSync(changedPath).isDirectory(),
)
) {
await setState({
sourceTree: true,
fileContents: true,
menu: true,
partials: true,
});
changeFileCallback(true, true);
}
// removed a directory or file
else if (triggeredEventsIncludes(triggeredEvents, ["remove"])) {
await setState({
sourceTree: true,
fileContents: await updateFileContents(triggeredEvents),
menu: true,
partials: true,
});
changeFileCallback(true, true);
// updated file is a template file
} else if (
triggeredEvents.filter((event) =>
helpers.fileIsTemplateFile(event.changedPath),
).length > 0
) {
if (
Object.keys(global.state.partials).includes(
triggeredEvents[0].changedPath.replace(
path.join(global.config.components.folder, "/"),
"",
),
)
) {
// updated
await setState({
fileContents: await updateFileContents(triggeredEvents),
});
changeFileCallback(true, false);
} else {
// added
await setState({
fileContents: await updateFileContents(triggeredEvents),
sourceTree: true,
menu: true,
partials: true,
});
changeFileCallback(true, true);
}
// updated file is a mock file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsDataFile(changedPath),
)
) {
const hasBeenAdded = !Object.keys(global.state.fileContents).includes(
path.join(process.cwd(), triggeredEvents[0].changedPath),
);
await setState({
fileContents: await updateFileContents(triggeredEvents),
sourceTree: hasBeenAdded,
menu: true,
});
changeFileCallback(true, true);
// updated file is a doc file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsDocumentationFile(changedPath),
)
) {
const hasBeenAdded = !Object.keys(global.state.fileContents).includes(
path.join(process.cwd(), triggeredEvents[0].changedPath),
);
await setState({
fileContents: await updateFileContents(triggeredEvents),
sourceTree: hasBeenAdded,
menu: hasBeenAdded,
});
changeFileCallback(true, hasBeenAdded);
// updated file is a schema file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsSchemaFile(changedPath),
)
) {
await setState({
fileContents: await updateFileContents(triggeredEvents),
});
changeFileCallback(true, false);
// updated file is an asset file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsAssetFile(changedPath),
)
) {
if (global.config.ui.reloadAfterChanges.componentAssets) {
changeFileCallback(true, false);
}
// updated file is a css file
} else if (
triggeredEvents.find(({ changedPath }) => {
return changedPath.endsWith(".css");
})
) {
// updated file contains custom properties for the styleguide
if (
triggeredEvents.find(({ changedPath }) => {
return global.config.assets.customProperties.files.includes(
changedPath,
);
})
) {
await setState({
css: true,
});
} else {
await setState({
menu: true,
});
}
if (global.config.ui.reloadAfterChanges.componentAssets) {
changeFileCallback(true, false);
}
// updated file is a js file
} else if (
triggeredEvents.find(({ changedPath }) => {
return changedPath.endsWith(".js");
})
) {
await setState({
menu: true,
});
if (global.config.ui.reloadAfterChanges.componentAssets) {
changeFileCallback(true, false);
}
} else {
await setState({
sourceTree: true,
fileContents: true,
menu: true,
partials: true,
});
changeFileCallback(true, true);
}
}
const sockets = [];
/**
* @param {object} server
*/
export default function Watcher(server) {
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", function open(ws) {
sockets.push(ws);
});
server.on("upgrade", (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
});
const { components, docs, assets, extensions } = global.config;
const foldersToWatch = [
...assets.folder.map((f) => path.join(global.config.assets.root, f)),
...assets.css
.filter(
(f) =>
!f.startsWith("http://") &&
!f.startsWith("https://") &&
!f.startsWith("://"),
)
.map((f) => path.join(global.config.assets.root, f)),
...assets.js
.map((file) => file.src)
.filter(
(f) =>
!f.startsWith("http://") &&
!f.startsWith("https://") &&
!f.startsWith("://"),
)
.map((f) => path.join(global.config.assets.root, f)),
];
if (components.folder) {
foldersToWatch.push(components.folder);
}
if (docs?.folder && fs.existsSync(docs.folder)) {
foldersToWatch.push(docs.folder);
}
for (const extension of extensions) {
const ext = Array.isArray(extension) ? extension[0] : extension;
const opts =
Array.isArray(extension) && extension[1] ? extension[1] : { locales: {} };
if (ext.extendWatcher) {
const watch = ext.extendWatcher(opts);
foldersToWatch.push(path.join(watch.folder, watch.lang));
}
}
if (global.config.userFileName) {
fs.watch(global.config.userFileName, async (eventType) => {
if (eventType === "change") {
configurationFileUpdated();
}
});
}
let watcher;
try {
watcher = watch(
foldersToWatch.filter((folder) => fs.existsSync(folder)),
{
recursive: true,
filter(f, skip) {
if (anymatch(components.ignores, f)) return skip;
return true;
},
},
);
} catch (e) {
log("error", e);
}
if (watcher) {
watcher.on("change", (event, changedPath) => {
triggeredEvents.push({ event, changedPath });
if (!timeout) {
console.clear();
log("info", t("updatingStarted"));
timeout = setTimeout(() => {
timeout = null;
handleFileChange();
}, 10);
}
});
} else {
log("error", t("watchingFilesFailed"));
}
}
/**
* @returns {Promise<void>}
*/
async function configurationFileUpdated() {
log("info", t("updatingConfiguration"));
const config = await getConfig(yargs.argv);
if (config) {
global.config = config;
await setEngines();
await setState({
sourceTree: true,
menu: true,
partials: true,
fileContents: true,
css: true,
});
setStatic();
setViews();
log("success", `${t("updatingConfigurationDone")}\n`);
changeFileCallback(true, true);
}
}