@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
404 lines (359 loc) • 10.9 kB
JavaScript
/**
* Module for watching user file changes
*
* @module initWatcher
*/
const anymatch = require("anymatch");
const fs = require("fs");
const path = require("path");
const watch = require("node-watch");
const socketIo = require("socket.io");
const getConfig = require("../config");
const yargs = require("./args.js");
const setState = require("../state");
const { readFile } = require("../state/file-contents.js");
const helpers = require("../helpers.js");
const log = require("../logger.js");
const { messages } = require("../config.json");
const setEngines = require("./engines.js");
const setPartials = require("./partials.js");
const setStatic = require("./static.js");
const setViewHelpers = require("./view-helpers.js");
const setViews = require("./views.js");
let triggeredEvents = [];
let foldersToWatch;
let timeout;
let appInstance;
let ioInstance;
/**
* @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 && appInstance.get("config").ui.reload) {
ioInstance.emit("fileChanged", reloadParent);
}
triggeredEvents = [];
log("success", `${messages.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} app - the express instance
* @param {object[]} events - array of event objects
* @returns {Promise<object>} the updated state.fileContents object
*/
async function updateFileContents(app, events) {
const data = helpers.cloneDeep(app.get("state").fileContents);
const promises = [];
for (const { event, changedPath } of events) {
if (
fs.existsSync(changedPath) &&
fs.lstatSync(changedPath).isFile() &&
(helpers.fileIsTemplateFile(app, changedPath) ||
helpers.fileIsDataFile(app, changedPath) ||
helpers.fileIsDocumentationFile(app, changedPath) ||
helpers.fileIsInfoFile(app, changedPath) ||
helpers.fileIsSchemaFile(app, changedPath))
) {
const fullPath = path.join(process.cwd(), changedPath);
if (event === "update") {
promises.push(
new Promise((resolve) => {
readFile(app, changedPath).then((result) => {
data[fullPath] = result;
resolve();
});
})
);
} else if (event === "remove") {
promises.push(
new Promise((resolve) => {
delete data[fullPath];
resolve();
})
);
}
}
}
return Promise.all(promises).then(() => {
return data;
});
}
/**
*
*/
async function handleFileChange() {
for (const extension of appInstance.get("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(appInstance, {
sourceTree: true,
fileContents: true,
menu: true,
partials: true,
});
changeFileCallback(true, true);
}
// removed a directory or file
else if (triggeredEventsIncludes(triggeredEvents, ["remove"])) {
await setState(appInstance, {
sourceTree: true,
fileContents: await updateFileContents(appInstance, triggeredEvents),
menu: true,
partials: true,
});
changeFileCallback(true, true);
// 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 appInstance
.get("config")
.assets.customProperties.files.includes(changedPath);
})
) {
await setState(appInstance, {
css: true,
});
} else {
await setState(appInstance, {
menu: true,
});
}
changeFileCallback(true, false);
// updates file is a js file
} else if (
triggeredEvents.find(({ changedPath }) => {
return changedPath.endsWith(".js");
})
) {
await setState(appInstance, {
menu: true,
});
changeFileCallback(true, false);
// updated file is a template file
} else if (
triggeredEvents.filter((event) =>
helpers.fileIsTemplateFile(appInstance, event.changedPath)
).length > 0
) {
if (
Object.keys(appInstance.get("state").partials).includes(
triggeredEvents[0].changedPath.replace(
path.join(appInstance.get("config").components.folder, "/"),
""
)
)
) {
// updated
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
});
changeFileCallback(true, false);
} else {
// added
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
sourceTree: true,
menu: true,
partials: true,
});
await setPartials.registerPartial(
appInstance,
triggeredEvents.find((event) => event.event === "update").changedPath
);
changeFileCallback(true, true);
}
// updated file is a mock file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsDataFile(appInstance, changedPath)
)
) {
const hasBeenAdded = !Object.keys(
appInstance.get("state").fileContents
).includes(path.join(process.cwd(), triggeredEvents[0].changedPath));
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
sourceTree: hasBeenAdded,
menu: true,
});
changeFileCallback(true, true);
// updated file is a doc file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsDocumentationFile(appInstance, changedPath)
)
) {
const hasBeenAdded = !Object.keys(
appInstance.get("state").fileContents
).includes(path.join(process.cwd(), triggeredEvents[0].changedPath));
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
sourceTree: hasBeenAdded,
menu: hasBeenAdded,
});
changeFileCallback(true, hasBeenAdded);
// updated file is an info file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsInfoFile(appInstance, changedPath)
)
) {
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
menu: true,
});
changeFileCallback(true, true);
// updated file is a schema file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsSchemaFile(appInstance, changedPath)
)
) {
await setState(appInstance, {
fileContents: await updateFileContents(appInstance, triggeredEvents),
});
changeFileCallback(true, false);
// updated file is an asset file
} else if (
triggeredEvents.some(({ changedPath }) =>
helpers.fileIsAssetFile(appInstance, changedPath)
)
) {
if (appInstance.get("config").ui.reloadAfterChanges.componentAssets) {
changeFileCallback(true, false);
}
} else {
await setState(appInstance, {
sourceTree: true,
fileContents: true,
menu: true,
partials: true,
});
changeFileCallback(true, true);
}
}
module.exports = function Watcher(server, app) {
appInstance = app;
ioInstance = socketIo(server);
const { components, assets, extensions } = appInstance.get("config");
foldersToWatch = [
components.folder,
...assets.folder.map((f) => path.join(app.get("config").assets.root, f)),
...assets.css
.filter(
(f) =>
!f.startsWith("http://") &&
!f.startsWith("https://") &&
!f.startsWith("://")
)
.map((f) => path.join(app.get("config").assets.root, f)),
...assets.js
.map((file) => file.src)
.filter(
(f) =>
!f.startsWith("http://") &&
!f.startsWith("https://") &&
!f.startsWith("://")
)
.map((f) => path.join(app.get("config").assets.root, f)),
];
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 (app.get("config").userFileName) {
fs.watch(app.get("config").userFileName, async (eventType) => {
if (eventType === "change") {
configurationFileUpdated(app);
}
});
}
let watcher;
try {
watcher = watch(foldersToWatch, {
recursive: true,
filter(f, skip) {
if (anymatch(components.ignores, f)) return skip;
return true;
},
});
} catch (e) {
log("error", e.message);
}
if (watcher) {
watcher.on("change", (event, changedPath) => {
triggeredEvents.push({ event, changedPath });
if (!timeout) {
console.clear();
log("info", messages.updatingStarted);
timeout = setTimeout(() => {
timeout = null;
handleFileChange();
}, 10);
}
});
} else {
log("error", messages.watchingFilesFailed);
}
};
async function configurationFileUpdated(app) {
log("info", messages.updatingConfiguration);
delete require.cache[require.resolve(path.resolve(process.cwd(), ".miyagi"))];
const config = await helpers.updateConfigForRendererIfNecessary(
getConfig(yargs.argv)
);
if (config) {
app.set("config", config);
await setEngines(app);
await setState(app, {
sourceTree: true,
menu: true,
partials: true,
fileContents: true,
css: true,
});
setStatic(app);
setViews(app);
setViewHelpers(app);
await setPartials.registerAll(app);
log("success", `${messages.updatingConfigurationDone}\n`);
ioInstance.emit("fileChanged", true);
}
}