|
| 1 | +/** @module compile/module */ |
| 2 | +/// <reference path="../types/types.js" /> |
| 3 | +import fsPromises from 'fs/promises' |
| 4 | +import { getFileList, isFileDir } from '../utils/fs.js' |
| 5 | +import * as ui from '../utils/ui.js' |
| 6 | +import fs from 'fs' |
| 7 | +import path from 'path' |
| 8 | +import { pipeline } from 'stream/promises' |
| 9 | +import readline from 'readline' |
| 10 | +import { copy } from 'fs-extra/esm' |
| 11 | +import { deleteAsync } from 'del' |
| 12 | + |
| 13 | +/** |
| 14 | + * #### Read from a file and return a readable stream |
| 15 | + * @private |
| 16 | + * @param {string} filePath - env variables |
| 17 | + * @returns {fs.ReadStream} readable stream |
| 18 | + */ |
| 19 | +function createReadStream (filePath) { |
| 20 | + return fs.createReadStream(filePath, 'utf8') |
| 21 | +} |
| 22 | + |
| 23 | +/** |
| 24 | +
|
| 25 | + * #### insert content into a file using streams |
| 26 | + * @async |
| 27 | + * @private |
| 28 | + * @param {string} destinationFile - destination file |
| 29 | + * @param {string} insertContent - content to insert |
| 30 | + * @param {string} initialContent - initial content |
| 31 | + * @param {number} index - index |
| 32 | + * @returns undefined |
| 33 | + */ |
| 34 | +async function insertContentIntoFile (destinationFile, insertContent, initialContent, index) { |
| 35 | + // Create a temp file to write the content to |
| 36 | + const tempFilePath = `standalone-modules/temp${index}` |
| 37 | + // Write the content to the temp file |
| 38 | + const writeStream = fs.createWriteStream(tempFilePath, 'utf8') |
| 39 | + |
| 40 | + writeStream.write(insertContent) |
| 41 | + await pipeline(initialContent, writeStream) |
| 42 | + writeStream.end() |
| 43 | + |
| 44 | + // Replace the original file with the temp file |
| 45 | + await fsPromises.rename(tempFilePath, destinationFile) |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * #### concatenate content from multiple streams |
| 50 | + * @async |
| 51 | + * @private |
| 52 | + * @param {fs.ReadStream[]} streams - list of streams |
| 53 | + * @returns {Promise<string>} concatenated content |
| 54 | + */ |
| 55 | +async function concatenateStreams (streams) { |
| 56 | + let concatenatedContent = '' |
| 57 | + // add patterns to remove blocks |
| 58 | + const rmblockPattern = /{#.*\[rmstart\] #}[\s\S]*?{#.*\[rmend\] #}/g |
| 59 | + const rmTmplBlockPattern = /<!--[\s\S]*?-->/g |
| 60 | + const importblockPattern = /{%\s*import.*%}/g |
| 61 | + |
| 62 | + for (const stream of streams) { |
| 63 | + const rl = readline.createInterface({ |
| 64 | + input: stream, |
| 65 | + crlfDelay: Infinity |
| 66 | + }) |
| 67 | + |
| 68 | + for await (const line of rl) { |
| 69 | + concatenatedContent += line + '\n' |
| 70 | + } |
| 71 | + concatenatedContent = concatenatedContent.replace(rmblockPattern, '') |
| 72 | + concatenatedContent = concatenatedContent.replace(importblockPattern, '') |
| 73 | + concatenatedContent = concatenatedContent.replace(rmTmplBlockPattern, '') |
| 74 | + } |
| 75 | + return concatenatedContent |
| 76 | +} |
| 77 | + |
| 78 | +/** |
| 79 | + * #### files to include during standalone module compilation |
| 80 | + * @typedef {Object} FileIncludes |
| 81 | + * @property {Array<string>} files - list of files to include |
| 82 | + * @property {Array<string>} dirs - list of directories to include |
| 83 | + */ |
| 84 | + |
| 85 | +/** |
| 86 | + * #### module initial configuration to be used during standalone module compilation |
| 87 | + * @typedef {Object} InitModuleConfig |
| 88 | + * @property {JSON} meta - module meta data |
| 89 | + * @property {FileIncludes} template - module template data |
| 90 | + * @property {FileIncludes} js - module js data |
| 91 | + * @property {FileIncludes} css - module css data |
| 92 | + */ |
| 93 | + |
| 94 | +/** |
| 95 | + * #### Standalone Modules Config |
| 96 | + * @typedef {Object} StandaloneModulesConfig |
| 97 | + * @property {string} moduleSrcConfigFile - path to the config file defining the module source |
| 98 | + * @property {string} moduleSrcName - module source name |
| 99 | + * @property {string} moduleSrcPath - path to the module source |
| 100 | + * @property {string} newModuleName - new module name |
| 101 | + * @property {string} newModulePath - path to the new module |
| 102 | + * @property {JSON} newModuleMeta - module meta data |
| 103 | + * @property {(FILE_LIST | { path: string; name: string; })[]} newModuleTemplateIncludes - path to the module template includes |
| 104 | + * @property {(FILE_LIST | { path: string; name: string; })[]} newModuleJsIncludes - path to the module js includes |
| 105 | + * @property {(FILE_LIST | { path: string; name: string; })[]} newModuleCssIncludes - path to the module css includes |
| 106 | + */ |
| 107 | + |
| 108 | +/** |
| 109 | + * #### Collect File Includes |
| 110 | + * @async |
| 111 | + * @private |
| 112 | + * @param {FileIncludes} fileIncludes - files to include |
| 113 | + * @param {string} moduleSrcName - module source name |
| 114 | + * @returns {Promise<(FILE_LIST|{ path: string; name: string;})[]>} list of files to include |
| 115 | + */ |
| 116 | +async function collectFileIncludes (fileIncludes, moduleSrcName) { |
| 117 | + const files = [] |
| 118 | + if (fileIncludes !== undefined) { |
| 119 | + for await (const dir of fileIncludes.dirs) { |
| 120 | + const dirFiles = await getFileList(`${dir}`, { objectMode: true }) |
| 121 | + files.push(...dirFiles) |
| 122 | + } |
| 123 | + for await (let file of fileIncludes.files) { |
| 124 | + if (file.includes('module.html')) { |
| 125 | + file = `theme/modules/${moduleSrcName}/${file}` |
| 126 | + } else if (file.includes('module.css')) { |
| 127 | + file = `theme/modules/${moduleSrcName}/${file}` |
| 128 | + } else if (file.includes('module.js')) { |
| 129 | + file = `theme/modules/${moduleSrcName}/${file}` |
| 130 | + } |
| 131 | + |
| 132 | + if (await isFileDir(`${file}`)) { |
| 133 | + files.push({ path: `${file}`, name: path.basename(file) }) |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + return files |
| 138 | +} |
| 139 | + |
| 140 | +/** |
| 141 | + * #### Parse Standalone Modules Config |
| 142 | + * @async |
| 143 | + * @private |
| 144 | + * @param {FILE_LIST[]} configFiles - env variables |
| 145 | + * @returns {Promise<StandaloneModulesConfig[]>} standalone modules config |
| 146 | + */ |
| 147 | +async function parseStandaloneModulesConfig (configFiles) { |
| 148 | + /** |
| 149 | + * @type {Array<StandaloneModulesConfig>} |
| 150 | + */ |
| 151 | + const config = [] |
| 152 | + if (configFiles !== undefined) { |
| 153 | + for await (const file of configFiles) { |
| 154 | + if (await isFileDir(file.path)) { |
| 155 | + const data = await import(file.path) |
| 156 | + const moduleSrcName = file.name.replace('.js', '') |
| 157 | + const moduleSrcPath = `theme/modules/${moduleSrcName}` |
| 158 | + let newModuleName = moduleSrcName |
| 159 | + if (data.default && data.default.name) { |
| 160 | + const newModuleNameVal = data.default.name |
| 161 | + newModuleName = `${newModuleNameVal.toLowerCase() |
| 162 | + .replace(/\s+/g, '-') // Replace spaces with dashes |
| 163 | + .replace(/[^a-z0-9-]/g, '')}.module` // Remove non-alphanumeric characters except dashes |
| 164 | + } |
| 165 | + const newModulePath = `standalone-modules/${newModuleName}` |
| 166 | + if (await isFileDir(moduleSrcPath)) { |
| 167 | + /** |
| 168 | + * @type {StandaloneModulesConfig} |
| 169 | + */ |
| 170 | + const moduleConfig = { |
| 171 | + moduleSrcConfigFile: file.path, |
| 172 | + moduleSrcName, |
| 173 | + moduleSrcPath, |
| 174 | + newModuleName, |
| 175 | + newModulePath, |
| 176 | + newModuleMeta: data.default.meta, |
| 177 | + newModuleTemplateIncludes: await collectFileIncludes(data.default.template, moduleSrcName), |
| 178 | + newModuleJsIncludes: await collectFileIncludes(data.default.js, moduleSrcName), |
| 179 | + newModuleCssIncludes: await collectFileIncludes(data.default.css, moduleSrcName) |
| 180 | + } |
| 181 | + config.push(moduleConfig) |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + return config |
| 187 | +} |
| 188 | + |
| 189 | +/** |
| 190 | + * #### update module meta data |
| 191 | + * @async |
| 192 | + * @private |
| 193 | + * @param {StandaloneModulesConfig} config - module config |
| 194 | + * @returns undefined |
| 195 | + */ |
| 196 | +async function updateModuleMeta (config) { |
| 197 | + const existingMeta = await fsPromises.readFile(`${config.newModulePath}/meta.json`, 'utf8') |
| 198 | + if (existingMeta !== undefined) { |
| 199 | + const existingMetaObj = JSON.parse(existingMeta || '{}') |
| 200 | + const updatedMeta = JSON.stringify({ ...existingMetaObj, ...config.newModuleMeta }, null, 2) |
| 201 | + await fsPromises.writeFile(`${config.newModulePath}/meta.json`, updatedMeta, 'utf8') |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +/** |
| 206 | + * #### Compile Module |
| 207 | + * @async |
| 208 | + * @param {boolean} [hideStatus] - hide status messages |
| 209 | + * @returns undefined |
| 210 | + */ |
| 211 | +async function compileModule (hideStatus) { |
| 212 | + try { |
| 213 | + const timeStart = ui.startTask('compileModule') |
| 214 | + |
| 215 | + const moduleSrcConfigFiles = await getFileList(`${process.cwd()}/standalone-modules/*module.js`, { objectMode: true }) |
| 216 | + const config = await parseStandaloneModulesConfig(moduleSrcConfigFiles) |
| 217 | + const fileList = [] |
| 218 | + for await (const [index, module] of config.entries()) { |
| 219 | + // create destination directory |
| 220 | + await fsPromises.mkdir(module.newModulePath, { recursive: true }) |
| 221 | + // delete all files in the destination directory |
| 222 | + await deleteAsync([`${module.newModulePath}/*`], { force: true }) |
| 223 | + // copy source files to destination directory |
| 224 | + await copy(`${module.moduleSrcPath}`, `${module.newModulePath}`) |
| 225 | + // update module meta data |
| 226 | + await updateModuleMeta(module) |
| 227 | + |
| 228 | + // create readable streams for all source files |
| 229 | + const templateSrcStream = module.newModuleTemplateIncludes.map(filePath => createReadStream(filePath.path)) |
| 230 | + const templateDistStream = createReadStream(`${module.newModulePath}/module.html`) |
| 231 | + const jsDistStream = createReadStream(`${module.moduleSrcPath}/module.js`) |
| 232 | + const jsSrcStream = module.newModuleJsIncludes.map(filePath => createReadStream(filePath.path)) |
| 233 | + const cssDistStream = createReadStream(`${module.moduleSrcPath}/module.css`) |
| 234 | + const cssSrcStream = module.newModuleCssIncludes.map(filePath => createReadStream(filePath.path)) |
| 235 | + |
| 236 | + // concatenate content from all source streams |
| 237 | + const concatTemplateSrc = await concatenateStreams(templateSrcStream) |
| 238 | + const concatTemplateDist = await concatenateStreams([templateDistStream]) |
| 239 | + const concatJsDist = await concatenateStreams([jsDistStream]) |
| 240 | + const concatJsSrc = await concatenateStreams(jsSrcStream) |
| 241 | + const concatCssDist = await concatenateStreams([cssDistStream]) |
| 242 | + const concatCssSrc = await concatenateStreams(cssSrcStream) |
| 243 | + |
| 244 | + // insert the concatenated content into the destination file |
| 245 | + await insertContentIntoFile(`${module.newModulePath}/module.html`, concatTemplateSrc, concatTemplateDist, index) |
| 246 | + await insertContentIntoFile(`${module.newModulePath}/module.js`, concatJsSrc, concatJsDist, index) |
| 247 | + await insertContentIntoFile(`${module.newModulePath}/module.css`, concatCssSrc, concatCssDist, index) |
| 248 | + // remove unnecessary files |
| 249 | + await deleteAsync([`${module.newModulePath}/fields.js`], { force: true }) |
| 250 | + |
| 251 | + const file = { |
| 252 | + name: `${module.newModulePath}.js`, |
| 253 | + size: '', |
| 254 | + dist: `${module.moduleSrcPath} --> ${module.newModulePath}` |
| 255 | + } |
| 256 | + fileList.push(file) |
| 257 | + } |
| 258 | + |
| 259 | + hideStatus || ui.endTask({ files: fileList, taskName: 'compileModule', timeStart }) |
| 260 | + } catch (error) { |
| 261 | + console.error(error) |
| 262 | + } |
| 263 | +} |
| 264 | + |
| 265 | +export { compileModule } |
0 commit comments