@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
520 lines (476 loc) • 16.2 kB
JavaScript
const v8 = require("v8");
const path = require("path");
const deepMerge = require("deepmerge");
const log = require("./logger.js");
const { messages, engines } = require("./config.json");
const { getFiles } = require("./state/helpers.js");
/**
* Module for globally used helper functions
*
* @module helpers
*/
module.exports = {
/**
* Removes all keys starting with $ from an object
*
* @param {object} [obj] the object whose keys with $ should be removed
* @returns {object} the modified object
*/
removeInternalKeys: function (obj = {}) {
const o = {};
for (const [key, value] of Object.entries(obj)) {
if (!key.startsWith("$") || key === "$ref" || key === "$opts") {
o[key] = value;
}
}
return o;
},
/**
* Returns everything after the last "." of a file extension (e.g. `html.twig` -> `twig`)
*
* @param {string} [extension] - File extension like `twig` or `html.twig`
* @returns {string} the last part of a the file extension
*/
getSingleFileExtension: function (extension = "") {
return extension.slice(extension.lastIndexOf(".") + 1);
},
/**
* Normalizes a string be replacing whitespace, underscore, / etc with - and lowercases it
*
* @param {string} [str] string that should be normalized
* @returns {string} the normalized string
*/
normalizeString: function (str = "") {
if (typeof str === "string") {
return str
.replace(/[^\w\s]/gi, "-")
.replace(/_/g, "-")
.replace(/ /g, "-")
.toLowerCase();
}
return str;
},
/**
* If '<component>' is set as the file name in the config, it returns the given file name, otherwise it returns the value from the config
*
* @param {string} nameInConfig - The defined name for a file in the config
* @param {string} fileName - The actual file name
* @returns {string} the filename based on the configuration file
*/
getResolvedFileName: function (nameInConfig, fileName) {
if (nameInConfig === "<component>") {
return fileName;
}
return nameInConfig;
},
/**
* Creates a deep clone of a object using internal v8 methods
*
* @param {object} obj - the object to clone
* @returns {object} clone of rhe given object
*/
cloneDeep: function (obj) {
return v8.deserialize(v8.serialize(obj));
},
/**
* Accepts a path relative from the config.components.folder and returns the complete path based on the file system
*
* @param {object} app - the express instance
* @param {string} shortPath - a relative file path based from the components folder
* @returns {string} absolute file path
*/
getFullPathFromShortPath: function (app, shortPath) {
return path.join(
process.cwd(),
`${app.get("config").components.folder}/${shortPath}`
);
},
/**
* Accepts an absolute (file system based) path and returns the short path relative from config.components.folder
*
* @param {object} app - the express instance
* @param {string} fullPath - absolute file path
* @returns {string} relative file path based from the components folder
*/
getShortPathFromFullPath: function (app, fullPath) {
return fullPath.replace(
`${path.join(process.cwd(), app.get("config").components.folder)}/`,
""
);
},
/**
* Accepts a template file path and returns the path to the corresponding mock file
*
* @param {object} app - the express instance
* @param {string} filePath - file path to a template file
* @returns {string} file path to the corresponding mock file
*/
getDataPathFromTemplatePath: function (app, filePath) {
return filePath.replace(
path.basename(filePath),
`${app.get("config").files.mocks.name}.${
app.get("config").files.mocks.extension
}`
);
},
/**
* Accepts a template file path and returns the path to the corresponding documentation file
*
* @param {object} app - the express instance
* @param {string} filePath - file path to a template file
* @returns {string} file path to the corresponding doc file
*/
getDocumentationPathFromTemplatePath: function (app, filePath) {
return filePath.replace(
path.basename(filePath),
`${app.get("config").files.docs.name}.${
app.get("config").files.docs.extension
}`
);
},
/**
* Accepts a template file path and returns the path to the corresponding info file
*
* @param {object} app - the express instance
* @param {string} filePath - file path to a template file
* @returns {string} file path to the corresponding info file
*/
getInfoPathFromTemplatePath: function (app, filePath) {
return filePath.replace(
path.basename(filePath),
`${app.get("config").files.info.name}.${
app.get("config").files.info.extension
}`
);
},
/**
* Accepts a template file path and returns the path to the corresponding schema file
*
* @param {object} app - the express instance
* @param {string} filePath - file path to a template file
* @returns {string} file path to the corresponding schema file
*/
getSchemaPathFromTemplatePath: function (app, filePath) {
return filePath.replace(
path.basename(filePath),
`${app.get("config").files.schema.name}.${
app.get("config").files.schema.extension
}`
);
},
/**
* Accepts a file path and checks if it is a mock file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a mock file
*/
fileIsDataFile: function (app, filePath) {
return (
path.basename(filePath) ===
`${app.get("config").files.mocks.name}.${
app.get("config").files.mocks.extension
}` ||
module.exports.getShortPathFromFullPath(app, filePath) ===
`data.${app.get("config").files.mocks.extension}`
);
},
/**
* Accepts a file path and checks if it is a documentation file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a doc file
*/
fileIsDocumentationFile: function (app, filePath) {
return (
filePath.replace(`${process.cwd()}/`, "") ===
path.join(
app.get("config").components.folder,
`README.${app.get("config").files.docs.extension}`
) ||
path.basename(filePath) ===
`${app.get("config").files.docs.name}.${
app.get("config").files.docs.extension
}`
);
},
/**
* Accepts a file path and checks if it is an info file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a info file
*/
fileIsInfoFile: function (app, filePath) {
return (
path.basename(filePath) ===
`${app.get("config").files.info.name}.${
app.get("config").files.info.extension
}`
);
},
/**
* Accepts a file path and checks if it is a schema file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a schema file
*/
fileIsSchemaFile: function (app, filePath) {
return (
path.basename(filePath) ===
`${app.get("config").files.schema.name}.${
app.get("config").files.schema.extension
}`
);
},
/**
* Accepts a file path and checks if it is component js or css file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a css or js file
*/
fileIsAssetFile: function (app, filePath) {
return (
path.basename(filePath) ===
`${module.exports.getResolvedFileName(
app.get("config").files.css.name,
path.basename(filePath, `.${app.get("config").files.css.extension}`)
)}.${app.get("config").files.css.extension}` ||
path.basename(filePath) ===
`${module.exports.getResolvedFileName(
app.get("config").files.js.name,
path.basename(filePath, `.${app.get("config").files.js.extension}`)
)}.${app.get("config").files.js.extension}`
);
},
/**
* Accepts a file path and returns checks if it is a template file
*
* @param {object} app - the express instance
* @param {string} filePath - path to any type of file
* @returns {boolean} is true if the given file is a template file
*/
fileIsTemplateFile: function (app, filePath) {
return (
path.basename(filePath) ===
`${module.exports.getResolvedFileName(
app.get("config").files.templates.name,
path.basename(
filePath,
`.${app.get("config").files.templates.extension}`
)
)}.${app.get("config").files.templates.extension}`
);
},
/**
* @param {object} app - the express instance
* @param {string} directoryPath - a component file path
* @returns {string} the template file path
*/
getTemplateFilePathFromDirectoryPath: function (app, directoryPath) {
return path.join(
directoryPath,
`${module.exports.getResolvedFileName(
app.get("config").files.templates.name,
path.basename(directoryPath)
)}.${app.get("config").files.templates.extension}`
);
},
/**
* @param {object} app
* @param {string} templateFilePath
* @returns {string}
*/
getDirectoryPathFromFullTemplateFilePath: function (app, templateFilePath) {
return path.dirname(
module.exports.getShortPathFromFullPath(app, templateFilePath)
);
},
/**
* Updates the config with smartly guessed template extension and/or template engine
* if missing
*
* @param {object} config - the user configuration object
* @returns {Promise<object>} the updated config
*/
async updateConfigForRendererIfNecessary(config) {
if (
config.engine &&
config.engine.name &&
!config.files.templates.extension
) {
config =
module.exports.updateConfigWithGuessedExtensionBasedOnEngine(config);
} else if (
(!config.engine || (config.engine && !config.engine.name)) &&
config.files.templates.extension
) {
config =
module.exports.updateConfigWithGuessedEngineBasedOnExtension(config);
} else if (
(!config.engine || (config.engine && !config.engine.name)) &&
!config.files.templates.extension
) {
config =
await module.exports.updateConfigWithGuessedEngineAndExtensionBasedOnFiles(
config
);
} else {
log("info", messages.scanningFiles);
}
return config;
},
/**
* Tries to guess the template files extension based on defined engine name.
*
* @param {object} config - the user configuration object
* @returns {object|boolean} is either the updated config or false if guessing failed
*/
updateConfigWithGuessedExtensionBasedOnEngine(config) {
log("info", messages.tryingToGuessExtensionBasedOnEngine);
const engineFromConfig = guessExtensionFromEngine(config.engine.name);
if (engineFromConfig) {
config.files.templates.extension = engineFromConfig.extension;
log(
"warn",
messages.templateExtensionGuessedBasedOnTemplateEngine.replace(
"{{extension}}",
config.files.templates.extension
)
);
return config;
} else {
log("error", messages.guessingExtensionFailed);
log("error", messages.missingExtension);
return false;
}
},
/**
* Tries to guess the engine name based on defined template files extension.
*
* @param {object} config - the user configuration object
* @returns {object|boolean} is either the updated config or false if guessing failed
*/
updateConfigWithGuessedEngineBasedOnExtension(config) {
log("info", messages.tryingToGuessEngineBasedOnExtension);
const guessedEngine = guessEngineFromExtension(
config.files.templates.extension
);
if (guessedEngine) {
config.engine.name = guessedEngine.engine;
log(
"warn",
messages.engineGuessedBasedOnExtension.replace(
"{{engine}}",
config.engine.name
)
);
return config;
} else {
log("error", messages.guessingEngineFailed);
log("error", messages.missingEngine);
return false;
}
},
/**
* Tries to guess the template files extension and engine name by scanning
* the component folder and looking for possible template files.
*
* @param {object} config - the user configuration object
* @returns {Promise<object|boolean>} is either the updated config or false if guessing failed
*/
async updateConfigWithGuessedEngineAndExtensionBasedOnFiles(config) {
log("info", messages.tryingToGuessEngineAndExtension);
log("info", messages.scanningFiles);
const guessedConf = await guessEngineAndExtensionFromFiles(config);
if (guessedConf) {
log(
"warn",
messages.engineAndExtensionGuessedBasedOnFiles
.replace("{{engine}}", guessedConf.engine.name)
.replace("{{extension}}", guessedConf.files.templates.extension)
);
return deepMerge(config, guessedConf);
} else {
log("error", messages.guessingEngineAndExtensionFailed);
log("error", messages.missingEngine);
return false;
}
},
};
/**
* Returns the engine name that belongs to a given extension
*
* @param {string} extension - the file extension from the user configuration
* @returns {{engine: string, extension: string}} the related engine name
*/
function guessEngineFromExtension(extension) {
return engines.find((engine) => engine.extension === extension);
}
/**
* Returns the template files extension that belongs to a given engine
*
* @param {string} engineName - the engine name from the user configuration
* @returns {{engine: string, extension: string}} the related template files extension
*/
function guessExtensionFromEngine(engineName) {
return engines.find(({ engine }) => engine === engineName);
}
/**
* Scans the files, tries to find template files and based on the result
* returns an object with engine.name and files.templates.extension
*
* @param {object} config - the user configuration object
* @returns {Promise<{files: object, engine: object}|null>} is either an object with `files` and `engine` or `null` if guessing failed
*/
async function guessEngineAndExtensionFromFiles(config) {
const extensions = await getAllAvailableTemplateExtensions(
Object.values(engines).map((engine) => engine.extension),
config.components.folder,
config.components.ignores
);
if (extensions.length === 1) {
return {
files: {
templates: {
extension: extensions[0],
},
},
engine: {
name: engines.find((engine) => engine.extension === extensions[0])
.engine,
},
};
}
return null;
}
/**
* Returns all extensions that belong to template files found in the components folder
*
* @param {Array} possibleExtensions - an array of possible template files extensions
* @param {string} folder - the component folder from the user configuration
* @param {Array} ignores - the folders to ignore from the user configuration
* @returns {Promise<Array>} an array of template files extension found in the component folder
*/
async function getAllAvailableTemplateExtensions(
possibleExtensions,
folder,
ignores
) {
const extensions = await getFiles(folder, ignores, function (res) {
const extname = path.extname(res);
const extension = extname.startsWith(".") ? extname.slice(1) : extname;
if (possibleExtensions.includes(extension)) {
return extension;
}
return null;
});
return extensions
? extensions.filter(function (elem, index, self) {
return index === self.indexOf(elem);
})
: [];
}