import path from 'path'; import {BLOCK_NAME, getModulePath, getProjectPath, IS_DEV} from "./env.js"; import config from 'config'; import {fileURLToPath} from 'url'; import memFs from 'mem-fs'; import editor from 'mem-fs-editor'; import fsExtra from "fs-extra"; import archiver from "archiver"; import {buildWordPress} from "./platforms/wordpress/wordpress-adapter.js"; import {buildHubspotEmail} from "./platforms/hubspot/hubspot-email-adapter.js"; import {buildHubspotPage} from "./platforms/hubspot/hubspot-page-adapter.js"; import fs from "fs/promises"; import {constants} from "fs"; import fetch from "node-fetch"; import mime from "mime-types"; import {exec} from "child_process"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export function getConfigs() { return { isDev: IS_DEV, developmentBlockName: BLOCK_NAME, modulesPath: getModulePath(), projectPath: getProjectPath(), }; } export async function readJSONFile(jsonFile) { let data = {}; try { await fs.access(jsonFile, constants.F_OK | constants.R_OK); data = await fsExtra.readJson(jsonFile); } catch (e) { return { error: true, errorMessage: getErrorHtml("JSON Syntax error. Please make sure the dataFile is valid.", e), }; } return data; } function getErrorHtml(message = '', errorMessage = '') { return `

${message}

${errorMessage}
`; } export async function getBlockData(jsonFileName = 'default', {projectPath} = {jsonFileName: 'default'}) { const filePath = path.join(projectPath, 'data', `${jsonFileName}.json`); const data = await readJSONFile(filePath); if (data.error) { console.log(filePath, data.errorMessage.replace(/<[^>]*>?/gm, '')); return {}; } return data; } export function getBlockConfigs(args = {modulesPath: '', dataFiles: []}) { const updatedConfig = Object.assign(JSON.parse(JSON.stringify(config)), // The entire config object. { projectDir: args.modulesPath, dataFiles: args.dataFiles.map((name) => { return { name, }; }), remToPx: config.has('remToPx') ? config.get('remToPx') : 16, }); // Avoid cache conflict on Global Project css/js files. if (updatedConfig.project) { updatedConfig.project.css = updatedConfig.project.css ? updatedConfig.project.css + '?cache=' + Date.now() : undefined; updatedConfig.project.js = updatedConfig.project.js ? updatedConfig.project.js + '?cache=' + Date.now() : undefined; } return updatedConfig; } export function getBlockName(name = '') { if (name.startsWith('@')) { name = name.substring(1); } const arr = name.split('/'); return { project: arr[0], name: arr[1], }; } export async function createFiles(data, files = [], {pathDist, generatorsPath}) { generatorsPath = generatorsPath ?? path.join(__dirname, 'generators/block/templates'); const store = memFs.create(); const filesystem = editor.create(store); for (let file of files) { const from = typeof file !== 'string' ? `${generatorsPath}/${file.from}` : `${generatorsPath}/${file}`; const to = typeof file !== 'string' ? `${pathDist}/${file.to}` : `${pathDist}/${file}`; await filesystem.copyTplAsync(from, to, data); } return filesystem.commit(); // Promise } export function capitalize(str) { if (typeof str !== 'string') { return ''; } return str .toLowerCase() .split(/[ -_]/g) .filter((word) => !!word) .map((word) => { return word.charAt(0).toUpperCase() + word.slice(1); }) .join(' '); } export async function zipProject(srcDir, outputFileName = 'dist.zip') { // create a file to stream archive data to. const output = await fsExtra.createWriteStream(outputFileName); const archive = archiver('zip', {}); // listen for all archive data to be written // 'close' event is fired only when a file descriptor is involved output.on('close', function () { console.log(archive.pointer() + ' total bytes'); console.log('archiver has been finalized and the output file descriptor has closed.'); }); // This event is fired when the data source is drained no matter what was the data source. // It is not part of this library but rather from the NodeJS Stream API. // @see: https://nodejs.org/api/stream.html#stream_event_end output.on('end', function () { console.log('Data has been drained'); }); // good practice to catch warnings (ie stat failures and other non-blocking errors) archive.on('warning', function (err) { if (err.code === 'ENOENT') { // log warning } else { // throw error throw err; } }); // good practice to catch this error explicitly archive.on('error', function (err) { throw err; }); // pipe archive data to the file archive.pipe(output); // append files from a subdirectory, putting its contents at the root of archive archive.directory(srcDir, false); // finalize the archive (ie we are done appending files but streams have to finish yet) // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand await archive.finalize(); } export async function buildExportFiles(blockName, platform) { if (platform.name.startsWith('wordpress')) { await buildWordPress(blockName, {platform: platform.name}); } else if (platform.name === 'hubspot-email') { await buildHubspotEmail(blockName) } else if (platform.name === 'hubspot') { await buildHubspotPage(blockName) } } export function removeCommentsFromCss(content) { return content.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, ''); } export function removeCommentsFromJs(content) { return content.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, ''); } export async function uploadFile(filePath, uploadUrl, validator) { const options = {}; const contentType = mime.lookup(filePath); if (['text/css', 'application/javascript'].includes(contentType)) { options.encoding = 'utf8'; } let body = await fs.readFile(filePath, options); if (validator) { body = validator(body); } try { const response = await fetch(uploadUrl, { method: 'PUT', body: body, headers: {'Content-Type': contentType} }); return response.status !== 200; } catch (err) { console.log(err) const fileName = filePath.split('/').pop(); throw new Error(`Can't upload "${fileName}" file. Server permission error.`); } } export async function getImagesList(rootFolder, subFolder = '') { const imagesPath = path.join(rootFolder, subFolder); try { await fs.access(imagesPath); } catch (err) { // Folder doesn't exist. return []; } const images = await fs.readdir(imagesPath); const imagesList = []; for (const image of images) { const stats = await fs.stat(path.join(imagesPath, image)); if (stats.isDirectory()) { const subImages = await getImagesList(rootFolder, image); imagesList.push(...subImages); } else { imagesList.push(path.join(subFolder, image)); } } return imagesList; } export async function isFileEmpty(filePath, ignoreComments = false) { const fileContent = await fs.readFile(filePath, 'utf8'); const lines = fileContent.split('\n'); for (const line of lines) { if (ignoreComments && line.trim().startsWith('//')) { continue; } if (line.trim().length > 0) { return false; } } return true; } export function replaceNames(content, images, uploadedImages) { images.forEach((image, index) => { content = content.replace(image, uploadedImages[index].fileName); }); return content; } export async function getBlockFromCloud(blockName, blocksRegistry) { const queryString = new URLSearchParams(); queryString.append('blockName', blockName); queryString.append('includeDevConfig', 'true'); const response = await fetch(`${blocksRegistry}?${queryString.toString()}`); const responseData = await response.json(); if (!responseData || !responseData.name) { const message = "⚠️ Block not found, please contact administrator." throw new Error(message); } if (responseData.statusCode && responseData.statusCode !== 200) { const message = ["⚠️ [ERROR]", responseData.message || "Server side error."].join(' '); throw new Error(message); } return responseData; } export async function verifyVersion(projectPath, blocksRegistry) { const blockJson = await readJSONFile(path.join(projectPath, `block.json`)); const blockName = getBlockName(blockJson.name); if (typeof blockJson.version === 'undefined' || !blockName.name) { return true; } /* * This block is managed on cloud. * Let's detect the latest version. */ const block = await getBlockFromCloud('@' + blockJson.name, blocksRegistry); return block.version === blockJson.version; } export async function syncFilesWithCloud(jsonBlockPath, bs, sourceFiles = false) { const blockJson = await readJSONFile(jsonBlockPath); const blockName = blockJson.name.startsWith('@') ? blockJson.name : `@${blockJson.name}`; bs.pause(); // Looks like it takes time to pause the browser-sync server, so delay(setTimeout) is necessary. await new Promise((resolve) => setTimeout(() => resolve(), 1000)); await new Promise((resolve) => { const args = sourceFiles ? '--source' : ''; const createBlockModulePath = `./node_modules/@axe-web/create-block`; // exec(`BLOCK_NAME=${blockName} node ${createBlockModulePath}/create-block.js sync ${args}`, (err, stdout, stderr) => { exec(`node ${createBlockModulePath}/create-block.js sync ${args}`, (err, stdout, stderr) => { if (err || stderr) { const message = err || stderr; console.error('Error:', message); throw new Error(message); } console.log(stdout); resolve(); }); }); bs.resume(); }