|
| 1 | +/* |
| 2 | + * Copyright 2020-2022 G-Labs. All Rights Reserved. |
| 3 | + * https://zuixjs.github.io/zuix |
| 4 | + * |
| 5 | + * Licensed under the MIT license. See LICENSE file. |
| 6 | + * |
| 7 | + */ |
| 8 | + |
| 9 | +/* |
| 10 | + * |
| 11 | + * This file is part of |
| 12 | + * zUIx, Javascript library for component-based development. |
| 13 | + * https://zuixjs.github.io/zuix |
| 14 | + * |
| 15 | + * @author Generoso Martello - https://github.com/genemars |
| 16 | + * @version 1.0 |
| 17 | + * |
| 18 | + */ |
| 19 | + |
| 20 | +/** |
| 21 | + * @param eleventyConfig |
| 22 | + */ |
| 23 | +const path = require('path'); |
| 24 | +const config = require('config'); |
| 25 | +const fs = require('fs'); |
| 26 | +const chokidar = require('chokidar'); |
| 27 | +const moment = require('moment'); |
| 28 | +const nunjucks = require('nunjucks'); |
| 29 | + |
| 30 | +const { |
| 31 | + compilePage, |
| 32 | + copyFolder, |
| 33 | + generateServiceWorker, |
| 34 | + generateAppConfig, |
| 35 | + wrapDom, |
| 36 | + wrapCss |
| 37 | +} = require('zuix'); |
| 38 | + |
| 39 | +// Read configuration either from './config/{default}.json' |
| 40 | +// or './config/production.json' based on current `NODE_ENV' |
| 41 | +// environment variable value |
| 42 | +let zuixConfig = config.get('zuix'); |
| 43 | +const sourceFolder = zuixConfig.get('build.input'); |
| 44 | +const buildFolder = zuixConfig.get('build.output'); |
| 45 | +const copyFiles = zuixConfig.get('build.copy'); |
| 46 | +const ignoreFiles = zuixConfig.get('build.ignore'); |
| 47 | +const componentsFolders = zuixConfig.get('build.componentsFolders'); |
| 48 | +const dataFolder = zuixConfig.get('build.dataFolder'); |
| 49 | +const includesFolder = zuixConfig.get('build.includesFolder'); |
| 50 | +// this file is a temporary file create to trigger 11ty build |
| 51 | +const triggerFile = path.join(sourceFolder, '.zuix.build.md'); |
| 52 | +const triggerFileOut = path.join(buildFolder, '.zuix.build.tmp'); |
| 53 | +// replace {{variables}} eventually employed in the config |
| 54 | +zuixConfig = JSON.parse(nunjucks.renderString(JSON.stringify(zuixConfig), zuixConfig)); |
| 55 | + |
| 56 | +const normalizeMarkup = (s) => s.trim().split('\n').filter((l) => { |
| 57 | + if (l.trim().length > 0) { |
| 58 | + return l; |
| 59 | + } |
| 60 | +}).join('\n'); |
| 61 | + |
| 62 | +function getZuixConfig() { |
| 63 | + return { |
| 64 | + sourceFolder, |
| 65 | + buildFolder, |
| 66 | + dataFolder, |
| 67 | + includesFolder, |
| 68 | + copyFiles, |
| 69 | + ignoreFiles, |
| 70 | + componentsFolders, |
| 71 | + baseUrl: zuixConfig.app.baseUrl, |
| 72 | + all: zuixConfig |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +function startWatcher(eleventyConfig, browserSync) { |
| 77 | + // Watch zuix.js folders and files (`./source/lib`, `./source/app`, zuixConfig.copy), ignored by 11ty |
| 78 | + const watchEvents = {add: true, change: true, unlink: true}; |
| 79 | + const watchFiles = []; |
| 80 | + copyFiles.forEach((f) => { |
| 81 | + f = path.resolve(path.join(sourceFolder, f)); |
| 82 | + watchFiles.push(f); |
| 83 | + }); |
| 84 | + componentsFolders.map(f => { |
| 85 | + f = path.resolve(path.join(sourceFolder, f)); |
| 86 | + watchFiles.push(f); |
| 87 | + }); |
| 88 | + const copyFilesWatcher = chokidar.watch(watchFiles).on('all', (event, file, stats) => { |
| 89 | + if (watchEvents[event] && fs.existsSync(file)) { |
| 90 | + const outputFile = path.resolve(path.join(buildFolder, file.substring(path.resolve(sourceFolder).length))); |
| 91 | + const outputFolder = path.dirname(outputFile); |
| 92 | + if (!fs.existsSync(outputFolder)) { |
| 93 | + fs.mkdirSync(outputFolder, { recursive: true }) |
| 94 | + } |
| 95 | + fs.copyFileSync(file, outputFile); |
| 96 | + } else { |
| 97 | + // TODO: maybe remove file from output folder as well? |
| 98 | + } |
| 99 | + if (browserSync) { |
| 100 | + browserSync.reload(); |
| 101 | + } |
| 102 | + }); |
| 103 | + const includes = path.join(sourceFolder, includesFolder); |
| 104 | + const allFilesWatcher = chokidar.watch([sourceFolder]).on('all', (event, file, stats) => { |
| 105 | + if (event.startsWith('unlink') && file !== triggerFile) { |
| 106 | + forceRebuild(); |
| 107 | + } else if (!file.startsWith(includes) && file.indexOf(path.join('/', '_inc', '/')) !== -1) { |
| 108 | + forceRebuild(); |
| 109 | + } |
| 110 | + }); |
| 111 | +} |
| 112 | + |
| 113 | +function forceRebuild() { |
| 114 | + fs.writeFileSync(triggerFile, `--- |
| 115 | +permalink: .zuix.build.tmp |
| 116 | +--- |
| 117 | +Temporary file to trigger 11ty build`); |
| 118 | +} |
| 119 | + |
| 120 | +function initEleventyZuix(eleventyConfig) { |
| 121 | + const postProcessFiles = []; |
| 122 | + const changedFiles = []; |
| 123 | + let rebuildAll = true; |
| 124 | + copyDependencies(); |
| 125 | + // zUIx.js specific code and life-cycle hooks |
| 126 | + eleventyConfig.addGlobalData("app", zuixConfig.app); |
| 127 | + eleventyConfig.addWatchTarget('./templates/tags/'); |
| 128 | + // Add zUIx transform |
| 129 | + eleventyConfig.addTransform('zuix-js', function(content) { |
| 130 | + const inputPath = this.inputPath; |
| 131 | + const outputPath = this.outputPath; |
| 132 | + const hasChanged = changedFiles.find(f => path.resolve(f) === path.resolve(inputPath)); |
| 133 | + if (!rebuildAll && !hasChanged) return content; |
| 134 | + // populates a list of `.html` files |
| 135 | + // to be post processed after build |
| 136 | + if (outputPath && outputPath.endsWith('.html')) { |
| 137 | + let file = path.resolve(outputPath); |
| 138 | + const baseFolder = path.resolve(zuixConfig.build.output); |
| 139 | + if (file.startsWith(baseFolder)) { |
| 140 | + file = file.substring(baseFolder.length + 1); |
| 141 | + } |
| 142 | + postProcessFiles.push({file, baseFolder: zuixConfig.build.output}); |
| 143 | + } |
| 144 | + return content; |
| 145 | + }); |
| 146 | + eleventyConfig.on('beforeWatch', (cf) => { |
| 147 | + // changedFiles is an array of files that changed |
| 148 | + // to trigger the watch/serve build |
| 149 | + changedFiles.length = 0; |
| 150 | + const baseFolder = path.resolve(zuixConfig.build.input); |
| 151 | + const dataFolder = path.join(baseFolder, zuixConfig.build.dataFolder); |
| 152 | + const includesFolder = path.join(baseFolder, zuixConfig.build.includesFolder); |
| 153 | + const templateChanged = cf.find(f => path.resolve(f).startsWith(includesFolder)); |
| 154 | + const dataChanged = cf.find(f => path.resolve(f).startsWith(dataFolder)); |
| 155 | + if (templateChanged || dataChanged) { |
| 156 | + rebuildAll = true; |
| 157 | + return; |
| 158 | + } |
| 159 | + changedFiles.push(...cf); |
| 160 | + }); |
| 161 | + eleventyConfig.on('afterBuild', async function(args) { |
| 162 | + console.log(); |
| 163 | + postProcessFiles.forEach((pf) => { |
| 164 | + const result = compilePage(pf.file, pf.file, { |
| 165 | + baseFolder: pf.baseFolder, |
| 166 | + ...zuixConfig |
| 167 | + }); |
| 168 | + // TODO: check result code and report |
| 169 | + }); |
| 170 | + postProcessFiles.length = 0; |
| 171 | + if (zuixConfig.build.serviceWorker) { |
| 172 | + console.log('Updating Service Worker... '); |
| 173 | + await generateServiceWorker().then(function () { |
| 174 | + console.log('... done.'); |
| 175 | + }); |
| 176 | + } else { |
| 177 | + console.log(); |
| 178 | + } |
| 179 | + if (rebuildAll) { |
| 180 | + // reverts to incremental build mode |
| 181 | + rebuildAll = false; |
| 182 | + } |
| 183 | + // delete temporary build-trigger file if found |
| 184 | + if (fs.existsSync(triggerFile)) { |
| 185 | + fs.unlinkSync(triggerFile); |
| 186 | + fs.unlinkSync(triggerFileOut); |
| 187 | + } |
| 188 | + }); |
| 189 | + if (process.argv.indexOf('--serve') === -1) { |
| 190 | + // copy files in production mode |
| 191 | + copyFiles.forEach((f) => { |
| 192 | + f = path.join(sourceFolder, f); |
| 193 | + eleventyConfig.addPassthroughCopy(f); |
| 194 | + }); |
| 195 | + componentsFolders.map(f => { |
| 196 | + f = path.join(sourceFolder, f); |
| 197 | + eleventyConfig.addPassthroughCopy(f); |
| 198 | + }); |
| 199 | + } |
| 200 | + eleventyConfig.setDataDeepMerge(true); |
| 201 | +} |
| 202 | + |
| 203 | +function copyDependencies() { |
| 204 | + // Copy last zUIx release |
| 205 | + copyFolder(`${process.cwd()}/node_modules/zuix-dist/js`, `${buildFolder}/js/zuix`, (err) => { |
| 206 | + if (err) console.log(err); |
| 207 | + }); |
| 208 | + // Auto-generated config.js |
| 209 | + generateAppConfig(zuixConfig); |
| 210 | + // Copy other dependencies |
| 211 | + // - elasticlurn search engine |
| 212 | + copyFolder(`${process.cwd()}/node_modules/elasticlunr/release`, `${buildFolder}/js/elasticlunr`, (err) => { |
| 213 | + if (err) console.log(err); |
| 214 | + }); |
| 215 | + // - Flex Layout Attribute |
| 216 | + copyFolder(`${process.cwd()}/node_modules/flex-layout-attribute/css`, `${buildFolder}/css/fla`, (err) => { |
| 217 | + if (err) console.log(err); |
| 218 | + }); |
| 219 | + // - Animate.CSS |
| 220 | + fs.copyFileSync(`${process.cwd()}/node_modules/animate.css/animate.min.css`, `${buildFolder}/css/animate.min.css`); |
| 221 | +} |
| 222 | + |
| 223 | +function rawFileInclude(page, fileName) { |
| 224 | + const inputPath = path.dirname(page.inputPath); |
| 225 | + let rawFile = path.join(inputPath, fileName); |
| 226 | + if (!fs.existsSync(rawFile)) { |
| 227 | + rawFile = path.join(inputPath, page.fileSlug, fileName); |
| 228 | + } |
| 229 | + if (!fs.existsSync(rawFile)) { |
| 230 | + rawFile = path.join(zuixConfig.build.input, fileName); |
| 231 | + } |
| 232 | + if (!fs.existsSync(rawFile)) { |
| 233 | + rawFile = path.join(zuixConfig.build.input, zuixConfig.build.includesFolder, fileName); |
| 234 | + } |
| 235 | + if (fs.existsSync(rawFile)) { |
| 236 | + return normalizeMarkup(fs.readFileSync(rawFile).toString('utf8')); |
| 237 | + } else { |
| 238 | + // TODO: report error |
| 239 | + throw new Error('File not found'); |
| 240 | + } |
| 241 | +} |
| 242 | + |
| 243 | +function configure(eleventyConfig) { |
| 244 | + initEleventyZuix(eleventyConfig); |
| 245 | + |
| 246 | + /* |
| 247 | + || Eleventy plugins |
| 248 | + */ |
| 249 | + const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); |
| 250 | + eleventyConfig.addPlugin(syntaxHighlight); |
| 251 | + |
| 252 | + /* |
| 253 | + || Add data collections |
| 254 | + */ |
| 255 | + |
| 256 | + // this is used by the searchFilter |
| 257 | + eleventyConfig.addCollection('posts_searchIndex', (collection) => { |
| 258 | + return [...collection.getFilteredByGlob(path.join(zuixConfig.build.input, 'pages/**/*.md'))]; |
| 259 | + }); |
| 260 | + |
| 261 | + /* |
| 262 | + || Add custom data filters |
| 263 | + */ |
| 264 | + |
| 265 | + // TODO: maybe scan folder and add automatically |
| 266 | + const filtersPath = path.resolve(sourceFolder, '_filters'); |
| 267 | + eleventyConfig.addFilter( |
| 268 | + 'search', |
| 269 | + require(path.join(filtersPath, 'searchFilter')) |
| 270 | + ); |
| 271 | + eleventyConfig.addFilter( |
| 272 | + 'date', |
| 273 | + (date, format) => moment(date).format(format || 'YYYY-MM-DD') |
| 274 | + ); |
| 275 | + |
| 276 | + /* |
| 277 | + || Add short codes |
| 278 | + */ |
| 279 | + |
| 280 | + eleventyConfig.addShortcode('rawFile', function(fileName) { |
| 281 | + return rawFileInclude(this.page, fileName); |
| 282 | + }); |
| 283 | + |
| 284 | + eleventyConfig.addPairedShortcode('unpre', function(content) { |
| 285 | + content = content.substring(content.indexOf('```') + 3); |
| 286 | + content = content.substring(content.indexOf('\n') + 1); |
| 287 | + content = content.substring(0, content.lastIndexOf('```')); |
| 288 | + return normalizeMarkup(content); |
| 289 | + }); |
| 290 | + |
| 291 | + eleventyConfig.addPairedShortcode('layout', function(content, ...args) { |
| 292 | + return `<div layout="${args[0]}" ${args[1]}>${normalizeMarkup(content)}</div>`; |
| 293 | + }); |
| 294 | + |
| 295 | + eleventyConfig.addPairedShortcode('zx', function(content, template, ...args) { |
| 296 | + const p = `./templates/tags/${template}.js`; |
| 297 | + if (fs.existsSync(p)) { |
| 298 | + delete require.cache[require.resolve(p)]; |
| 299 | + return normalizeMarkup(require(p)(nunjucks.renderString, content, ...args)); |
| 300 | + } |
| 301 | + return ''; // 'Not implemented! (' + content + ') [' + args + ']'; |
| 302 | + }); |
| 303 | + |
| 304 | + eleventyConfig.addPairedShortcode('wrapDom', function(content, cssId) { |
| 305 | + return wrapDom(content, cssId); |
| 306 | + }); |
| 307 | + eleventyConfig.addPairedShortcode('wrapCss', function(content, cssId, encapsulate) { |
| 308 | + return wrapCss(`[${cssId}]`, content, encapsulate); |
| 309 | + }); |
| 310 | +} |
| 311 | + |
| 312 | +module.exports = { |
| 313 | + startWatcher, configure, getZuixConfig |
| 314 | +} |
0 commit comments