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) => {
if (err || stderr) {
const message = err || stderr;
console.error('Error:', message);
throw new Error(message);
}
console.log(stdout);
resolve();
});
});
bs.resume();
}