UNPKG

genysite

Version:

Simple Static Site Generator

644 lines (511 loc) 19.1 kB
#!/usr/bin/env node const fs = require('fs') const clc = require("cli-color"); const nunjucks = require('nunjucks') const figlet = require('figlet'); const sass = require('sass') const {spawn} = require('child_process') const path = require('path') const marked = require('marked') const { Command, Argument } = require('commander') const merge = require('merge-deep'); const { v4: uuidv4 } = require('uuid'); const createTemplateEngine = require('./libs/templateEngine') const reactTemplateEngine = require('./libs/reactEngine/index') const remoteTemplates = require('./remote_templates') const TEMPLATES_DIR = path.resolve(__dirname, 'templates') const TEMPLATES = fs.readdirSync(TEMPLATES_DIR) const ROOT = path.resolve(".") const CONFIG_NAME = 'genysite.config.js' const CONFIG_PATH = path.join(ROOT, CONFIG_NAME) const packageJson = require('./package.json') const ACTIONS = { COMPILE: 'compile', INSTALL: 'install', TEMPLATES: 'templates' } let programConfig = { verbose: false, debug: false } let config = {} /** * Get Main Project configuration file (genysite.config.js) */ const getConfig = async () => { if (fs.existsSync(CONFIG_PATH) === false) { return {} } const configData = require(CONFIG_PATH) if (typeof configData === 'function') { config = await configData() } else if (typeof configData === 'object') { config = configData } else { console.error(`Configuration file cannot be processed`) process.exit(1) } return config } /** ========================================================================================== * COMPILATION ENTRY POINT * ========================================================================================*/ const compile = async () => { /** * Get Main Config */ config = await getConfig() /** * Get Constants */ const HOMEPAGE = config.homepage || ''; const DIST_FOLDER = path.resolve(ROOT, config.dist || 'docs') const SRC_FOLDER = path.resolve(ROOT, 'src') const TMP_FOLDER = path.resolve(__dirname, 'tmp') const PAGES_FOLDER = path.resolve(SRC_FOLDER, 'pages') const ASSETS_FOLDER = path.resolve(SRC_FOLDER, 'assets') const LIBS_FOLDER = path.resolve(SRC_FOLDER, 'libs') const TEMPLATE_FOLDER = path.resolve(SRC_FOLDER, 'template') const TEMPLATE_ASSETS_FOLDER = path.resolve(TEMPLATE_FOLDER, 'assets') const TEMPLATE_CONFIG_FILE = path.resolve(TEMPLATE_FOLDER, 'config.js') /** * Check if Temp folder exists */ if (fs.existsSync(TMP_FOLDER) === false) { fs.mkdirSync(TMP_FOLDER) } /** * Check if Template has its own config and merge it with the main project */ if (fs.existsSync(TEMPLATE_CONFIG_FILE) === true) { const templateConfig = require(TEMPLATE_CONFIG_FILE) config = merge(templateConfig, config) } /** * Set nunjucks environment */ const env = createTemplateEngine(SRC_FOLDER, HOMEPAGE, config) /** * Run postScript */ if (typeof config.postScript === 'function') { const renderString = filePath => { const content = fs.readFileSync(filePath, 'utf-8') return new nunjucks.runtime.SafeString(env.renderString(content, config.data)) } config = await config.postScript(renderString) } /** * Check FileSystem */ let hasAssets = true let hasTemplateFolder = true let hasTemplateAssets = true /** * Check SRC path */ if (fs.existsSync(SRC_FOLDER) === false){ console.error("Source folder 'src' does not exists") process.exit(1) } else if (programConfig.verbose) { console.info('Source folder: ', SRC_FOLDER) } /** * Check SRC/PAGES path */ if (fs.existsSync(PAGES_FOLDER) === false){ console.error("Pages folder 'src/pages' does not exists") process.exit(1) } else if (programConfig.verbose) { console.info('Page folder: ', SRC_FOLDER) } /** * Check SRC/ASSETS path */ if (fs.existsSync(ASSETS_FOLDER) === false){ console.info("No assets found") hasAssets = false } else if (programConfig.verbose) { console.info('Assets found : ', ASSETS_FOLDER) } /** * Check SRC/TEMPLATE path */ if (fs.existsSync(TEMPLATE_FOLDER) === false){ console.info("No template was found") hasTemplateFolder=false hasTemplateAssets=false } else if (programConfig.verbose) { console.info('Template found: ', TEMPLATE_FOLDER) } /** * Check SRC/TEMPLATE/ASSETS path */ if (hasTemplateFolder === true && fs.existsSync(TEMPLATE_ASSETS_FOLDER) === false){ console.info("No template's assets found") hasTemplateAssets=false } else if (programConfig.verbose) { console.info('Template assets found: ', TEMPLATE_ASSETS_FOLDER) } let dirPool = [] let pagesPool = [] /** ================================= GET TREE ==================================== * Get Pages Structure Informations * Return the tree * ==============================================================================*/ const performPages = (filePath, rootPath = '') => { const priority = Array.isArray(config.priority) === true ? config.priority : null const pages = fs.readdirSync(filePath, {withFileTypes: true}) return pages // .filter(pageInfo => path.extname(pageInfo.name) === FILE_EXTENSION || pageInfo.isDirectory() === true) .map(pageInfo => { const newFilePath = path.join(filePath, pageInfo.name) const newRootPath = path.join(rootPath, pageInfo.name) const withoutExtension = path.join(rootPath, pageInfo.name.replace(path.extname(pageInfo.name), '')) const newFrontPath = path.join(rootPath, pageInfo.name.replace(path.extname(pageInfo.name), config.react === true ? '' : '.html')) if (pageInfo.isDirectory() === true) { const name = config.names?.[newRootPath] || pageInfo.name dirPool.push(newRootPath) return {name, brutName: pageInfo.name, children: performPages(newFilePath, newRootPath)} } const name = config.names?.[newRootPath.replace(path.extname(pageInfo.name), '')] || pageInfo.name pagesPool.push({filePath: newFilePath, rootPath: newRootPath}) return {name: name.replace(path.extname(pageInfo.name), ''), brutName: pageInfo.name.replace(path.extname(pageInfo.name), ''), path: config.index === withoutExtension ? '' : newFrontPath.toLowerCase()} }).sort((a, b) => { /** * Order the tree, * First By priority, * Secondy By name */ if (priority === null) { return 0; } const reversePriority = [...priority].reverse() const aIndex = reversePriority.indexOf(a.brutName) const bIndex = reversePriority.indexOf(b.brutName) if (aIndex === bIndex) { return a.brutName > b.brutName ? 1 : -1 } if (aIndex > bIndex) { return -1 } if (bIndex > aIndex) { return 1 } return 0; }) } const tree = performPages(PAGES_FOLDER) /** ================================= CREATE OUTPUT FILE SYSTEM ================== */ /** * Delete the output folder if exists */ if (fs.existsSync(DIST_FOLDER) === true) { fs.rmSync(DIST_FOLDER, {recursive: true}) if (programConfig.verbose) { console.log('Delete dist folder: ', DIST_FOLDER) } } /** * Create the output folder */ fs.mkdirSync(DIST_FOLDER) /** * Create dist folders */ if (programConfig.verbose && dirPool.length > 0) { console.log('Create Dist Folders', dirPool) } /** * Create the sub dist folder */ dirPool.forEach(entry => { const folderPath = path.join(DIST_FOLDER, entry) fs.mkdirSync(folderPath) }) /** ============================ COPY ASSETS AND PERFORM SASS FILES ================== */ /** * Copy assets */ if (hasAssets === true) { if (programConfig.verbose) { console.log('Create Dist Assets Folder') } fs.cpSync(ASSETS_FOLDER, path.join(DIST_FOLDER, 'assets'), {recursive: true}) } else if (hasTemplateAssets === true) { if (programConfig.verbose) { console.log('Create Dist Assets Folder') } fs.mkdirSync(path.join(DIST_FOLDER, 'assets')); } if (hasTemplateAssets === true) { if (programConfig.verbose) { console.log('Create Dist Template Assets Folder') } fs.cpSync(TEMPLATE_ASSETS_FOLDER, path.join(DIST_FOLDER, 'assets', 'template'), {recursive: true}) } /** * Compile Sass file */ const performSass = (rootDir) => { if (fs.existsSync(rootDir) === false) { return; } const files = fs.readdirSync(rootDir, {withFileTypes: true}) files.forEach(fileInfo => { const fullPath = path.join(rootDir, fileInfo.name) if (fileInfo.isDirectory() === true) { return performSass(fullPath) } if (['.scss', '.sass'].indexOf(path.extname(fileInfo.name)) !== -1) { const compiledSass = sass.compile(fullPath); const cssFileName = `${path.basename(fileInfo.name, path.extname(fileInfo.name))}.css` if (programConfig.verbose) { console.log('Perform Sass File: ', fullPath) } fs.writeFileSync(path.join(rootDir, cssFileName), compiledSass.css) } }) } performSass(path.join(DIST_FOLDER, 'assets')) /** ============================ GET THE TEMPLATE DATA ================== */ /** * Get template extra data */ const templateData = { ...(config.data || {}), tree } if (programConfig.verbose) { console.log('Template Variables: ', JSON.stringify(templateData, null, 4)) console.log('Engine: ', config.react !== true ? 'Default' : 'React') } /** ============================================================================== * PAGES GENERATION * =============================================================================*/ /** ================================ TO HTML =================================== */ if (config.react !== true) { const promises = pagesPool.map(pageData => new Promise((resolve, reject) => { const templatePath = path.join('pages/', pageData.rootPath) env.render(templatePath, templateData, (error, result) => { if (error) { return reject(error) } if (path.extname(templatePath) === '.md') { result = marked.parse(result) } const target = path.join(DIST_FOLDER, pageData.rootPath) fs.writeFileSync(target.replace(path.extname(target), '.html'), result, 'utf8') resolve(true) }) })) Promise.all(promises) .then(results => { console.log('Well done') process.exit(0) }) .catch(error => { console.error(error) process.exit(1) }) } else { /** ================================ TO REACT =================================== */ /** * Create a uniq temp folder and its components folder */ const tmpFolder = path.resolve(TMP_FOLDER, `./${uuidv4()}`) const componentsFolder = path.resolve(tmpFolder, `./components`) const libsFolder = path.resolve(tmpFolder, `./libs`) fs.mkdirSync(tmpFolder); fs.mkdirSync(componentsFolder); if (programConfig.verbose) { console.log('Temp folder created: ', tmpFolder) } /** * Perform simple pages (not components [.js]) */ const promises = pagesPool.filter(pageData => path.extname(pageData.rootPath) !== '.js').map(pageData => new Promise((resolve, reject) => { const templatePath = path.join('pages/', pageData.rootPath) env.render(templatePath, templateData, (error, result) => { if (error) { return reject(error) } if (path.extname(templatePath) === '.md') { result = marked.parse(result) } resolve({...pageData, content: result, path: pageData.filePath.replace(PAGES_FOLDER, '').replace(path.extname(pageData.filePath), '')}) }) })) /** * Perform components pages (.js) */ const components = pagesPool.filter(pageData => path.extname(pageData.rootPath) === '.js').map(pageData => { const tmpFilePath = path.resolve(componentsFolder, `./${pageData.rootPath}`) const tmpDirName = path.dirname(tmpFilePath) fs.mkdirSync(tmpDirName, {recursive: true}) fs.copyFileSync(pageData.filePath, tmpFilePath) return {path: `./components/${pageData.rootPath}`, route: pageData.filePath.replace(PAGES_FOLDER, '').toLowerCase()} }) /** * Copy libs folder */ if (fs.existsSync(LIBS_FOLDER)) { fs.cpSync(LIBS_FOLDER, libsFolder, {recursive:true}) } Promise.all(promises) .then(results => { /** * Give the data to the react engine */ const workDone = () => { reactTemplateEngine(results, components, templateData, config, {dist: DIST_FOLDER, tmp: tmpFolder, verbose: programConfig.verbose}) .then(() => { console.log('Well done') process.exit(0) }) .catch(error => { console.error(error) process.exit(1) }) } /** * Copy the default index.html if not given */ if (!config.index_html) { fs.copyFileSync(path.resolve(__dirname, './libs/reactEngine/base_index.html'), path.resolve(tmpFolder, './index.html')) return workDone() } /** * Copy the given index.html after parsing it with nunjuck */ if (programConfig.verbose) { console.log('Generate custom index.html') } env.render(config.index_html, templateData, (error, result) => { if (error) { console.error(error) process.exit(1) } fs.writeFileSync(path.resolve(tmpFolder, './index.html'), result) return workDone() }) }) .catch(error => { console.error(error) process.exit(1) }) } } /** ========================================================================================== * INSTALL TEMPLATE ENTRY POINT * ========================================================================================*/ const installTemplate = async (templateUrl) => { const checkPackageJson = () => { const tplPackageJsonPath = path.resolve(TEMPLATE_FOLDER, './tpl_package.json') const packageJsonPath = path.resolve(TEMPLATE_FOLDER, './package.json') const makeEnd = () => { console.log('Template installed') process.exit(0) } if (fs.existsSync(tplPackageJsonPath) === false) { return makeEnd() } fs.copyFileSync(tplPackageJsonPath, packageJsonPath) const npmInstall = spawn('npm', ['install', '--path', TEMPLATE_FOLDER]) npmInstall.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); npmInstall.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); npmInstall.on('close', errorCode => { if (errorCode !== 0) { console.error(`Error code ${errorCode}`) return process.exit(1) } process.exit(0) }) } config = await getConfig() const SRC_FOLDER = path.resolve(ROOT, 'src') const TEMPLATE_FOLDER = path.resolve(SRC_FOLDER, 'template') if (fs.existsSync(TEMPLATE_FOLDER) === true) { fs.rmSync(TEMPLATE_FOLDER, {recursive: true}) } if (TEMPLATES.indexOf(templateUrl) !== -1) { fs.cpSync(path.join(TEMPLATES_DIR, templateUrl), TEMPLATE_FOLDER, {recursive: true}) return checkPackageJson() } if (remoteTemplates[templateUrl] !== undefined) { templateUrl = remoteTemplates[templateUrl] } const clone = spawn('git', ['clone', templateUrl, TEMPLATE_FOLDER]) clone.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); clone.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); clone.on('close', errorCode => { if (errorCode !== 0) { console.error(`Error code ${errorCode}`) return process.exit(1) } return checkPackageJson() }) } /** ========================================================================================== * LIST TEMPLATES ENTRY POINT * ========================================================================================*/ const listTemplates = () => { console.log([...TEMPLATES, ...Object.keys(remoteTemplates)]) process.exit(0) } /** ========================================================================================== * PARSE COMMAND * ========================================================================================*/ const parseCommand = async () => { const program = new Command() program .name('genysite') .description('Simply Static Website Generator') .version(packageJson.version) .option('-d, --debug', 'output extra debugging') .option('-v, --verbose', 'output verbose') ; program.addArgument(new Argument('[command]', 'command').choices(Object.values(ACTIONS)).default(ACTIONS.COMPILE)) program.addArgument(new Argument('[param]', 'param')) program.parse(process.argv); const [action, param] = program.processedArgs const options = program.opts(); if (options.debug) { console.info('Options :', options) } if (options.verbose) { console.info('Run with verbose level') } programConfig = options switch(action) { case ACTIONS.COMPILE: return compile() case ACTIONS.TEMPLATES: return listTemplates() case ACTIONS.INSTALL: if (typeof param !== 'string') { console.log('Git repo or template namemust be provided') return process.exit(1) } return installTemplate(param) default: console.log('Unknow action') process.exit(1) } } /** ========================================================================================== * ENTRY POINT * ========================================================================================*/ const run = async () => { console.log(clc.magenta(figlet.textSync('Rose Marie La Catin !!!'))) parseCommand() } run()