29 Commits

Author SHA1 Message Date
roman 4192d16b7a Added doctype to all HTML templates 2024-01-21 23:43:07 +02:00
roman 12abd35cfa Middleware is not necessary 2024-01-21 23:39:50 +02:00
roman 073ed52932 Added CORS header. 2024-01-21 23:31:07 +02:00
roman 8f2605be9d Updated Dockerfile 2024-01-14 01:14:19 +02:00
roman 9e7860542b Added Dockerfile 2024-01-14 00:58:38 +02:00
roman f48b2a3274 - Rebuilt the entire project - now we can pass blockName in URL.
- Added "ViewMode"
2024-01-14 00:36:14 +02:00
roman b76f83d4fa Merge pull request 'dev' (#9) from dev into master
Reviewed-on: AXE-WEB/block-dev-tool#9
2024-01-06 16:16:40 +00:00
roman 977ec0003c Merge branch 'master' into dev
# Conflicts:
#	package-lock.json
#	package.json
2024-01-06 18:15:57 +02:00
roman 0e91c42e89 Update $base_url parameter since the block location changed. 2023-12-15 12:33:26 +02:00
roman 0a4fd3608f Updated path of assets files. 2023-11-13 01:01:35 +02:00
roman 11152dda87 1.0.28 2023-11-06 15:51:40 +02:00
roman 246c9ddd35 Fix capitalization function 2023-11-06 15:51:26 +02:00
roman 164ef7433a 1.0.30 2023-08-27 17:29:05 +03:00
roman a6decb9cfb Remove Block Name from Sync action. 2023-08-27 17:28:54 +03:00
roman 9c85a89a8f Fix issue of blocks folder 2023-08-23 16:55:53 +03:00
roman 58ed3de9dc Make sure we sync versions after each Publish action.
Update versions in JSON files.
2023-08-19 22:38:19 +03:00
roman 3de4a4abb6 1.0.29 2023-08-19 18:09:39 +03:00
roman 256450226b Ask if user wants to download latest version of block on lunch of the devTool. 2023-08-19 18:08:39 +03:00
roman 51db3da072 Fix scrolling issues & swiperjs conflicts. 2023-08-18 23:28:13 +03:00
roman 05c5697bc2 Fit the devTool to nodejs v16. 2023-08-14 16:11:21 +03:00
roman 01e4160040 1.0.28 2023-08-14 15:50:21 +03:00
roman 81f5166043 Print error message to have better understanding of dataObjects errors. 2023-08-14 15:50:12 +03:00
roman a1929fe33c Support missing images folder. 2023-08-13 23:50:52 +03:00
roman 840c002834 Added default layout view. 2023-08-13 23:26:38 +03:00
roman f4474fb7e4 Support work without DevConfigs. 2023-08-13 22:52:45 +03:00
roman 8ca6cb0365 Added "Content-Type" to files that are sent to S3. 2023-08-13 14:27:25 +03:00
roman e2553358be Added support of Project Global Assets. 2023-08-12 21:01:45 +03:00
roman 34dbff5701 Hide "Version" label 2023-06-26 20:05:11 +03:00
roman c408f2f5b3 Added option to generate test variations - updated error messages. 2023-06-25 22:18:44 +03:00
49 changed files with 1123 additions and 18837 deletions
+19
View File
@@ -0,0 +1,19 @@
FROM node:19.4.0-slim as node
WORKDIR /app/
COPY ./package.json .
COPY ./package-lock.json .
COPY ./inc ./inc
COPY ./layouts ./layouts
COPY ./platforms ./platforms
COPY ./routes ./routes
COPY ./env.js .
COPY ./helpers.js .
COPY ./rollup.config.js .
COPY ./server.js .
RUN npm install
EXPOSE 3010
CMD npm run view-mode
+240 -10
View File
@@ -9,6 +9,12 @@ 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";
import {REGISTRY_URL} from "@axe-web/create-block/env.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -26,6 +32,7 @@ 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 {
@@ -44,33 +51,46 @@ function getErrorHtml(message = '', errorMessage = '') {
}
export async function getBlockData(jsonFileName = 'default', {projectPath} = {jsonFileName: 'default'}) {
let data = await readJSONFile(path.join(projectPath, 'data', `${jsonFileName}.json`));
const filePath = path.join(projectPath, 'data', `${jsonFileName}.json`);
const data = await readJSONFile(filePath);
if (data.error) {
return data;
console.log(filePath, data.errorMessage.replace(/<[^>]*>?/gm, ''));
return {};
}
return data;
}
export function getBlockConfigs(args = {modulesPath: '', dataFiles: []}) {
return Object.assign(JSON.parse(JSON.stringify(config)), // The entire config object.
export async function getBlockConfigs(args) {
const config = await readJSONFile(path.join('blocks', '@' + args.project, args.name, 'config', 'default.json'));
const updatedConfig = Object.assign({}, config, // The entire config object.
{
projectDir: args.modulesPath, dataFiles: args.dataFiles.map((name) => {
projectDir: '', dataFiles: args.dataFiles.map((name) => {
return {
name,
};
}), remToPx: config.has('remToPx') ? config.get('remToPx') : 16,
}), 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);
}
// Remove all @ from the beginning of the string.
name = name.replace(/^@/, '');
const arr = name.split('/');
return {
blockName: `@${arr[0]}/${arr[1]}`,
project: arr[0],
name: arr[1],
};
@@ -98,7 +118,7 @@ export function capitalize(str) {
return str
.toLowerCase()
.split(/[ -_]/g)
.split(/[-_ ]/g)
.filter((word) => !!word)
.map((word) => {
return word.charAt(0).toUpperCase() + word.slice(1);
@@ -160,3 +180,213 @@ export async function buildExportFiles(blockName, platform) {
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. [${blockName}]`
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(blockName, projectPath, blocksRegistry) {
const block = await getBlockFromCloud(blockName, blocksRegistry);
if (!block) {
throw new Error(`Block not found. [${blockName}]`);
}
const blockJson = await readJSONFile(path.join(projectPath, `block.json`));
if (blockJson.error) {
return false; // Block doesn't exist locally.
}
/*
* This block is managed on cloud.
* Let's detect the latest version.
*/
return block.version === blockJson.version;
}
export async function syncFilesWithCloud(blockName, bs, sourceFiles = false) {
const block = await getBlockFromCloud(blockName, REGISTRY_URL);
let fileExists = false;
try {
await readJSONFile(path.join('blocks', '@' + block.project, block.name, 'block.json'));
fileExists = true;
} catch (e) {
// Dir doesn't exist.
}
// TODO: Pause watcher of the block?
blockName = `@${block.project}/${block.name}`;
await new Promise((resolve) => {
const createBlockModulePath = `./node_modules/@axe-web/create-block`;
const action = sourceFiles ? fileExists ? 'sync --source' : 'pull' : 'sync';
exec(`BLOCK_NAME=${blockName} node ${createBlockModulePath}/create-block.js ${action}`, (err, stdout, stderr) => {
if (err || stderr) {
const message = err || stderr;
console.error('Error:', message);
throw new Error(message);
}
console.log(stdout);
resolve();
});
});
// TODO: Release watcher of the block?
}
export async function getBlockVariations(projectPath) {
const dataFiles = [];
try {
await fs.access(path.join(projectPath, 'data'));
const files = prepareListOfDataFiles(await fs.readdir(path.join(projectPath, 'data')));
dataFiles.push(...files);
} catch (e) {
console.log('Warning: data folder not found.', projectPath);
}
return dataFiles;
}
export function prepareListOfDataFiles(dataFiles) {
return dataFiles
.filter((fileName) => fileName.split('.').pop() === 'json')
.map((fileName) => {
const splitName = fileName.split('.');
splitName.pop();
return splitName.join('');
})
.sort();
}
export function handlebarLayoutsPath() {
return path.join(...arguments)
.replace(/\\/g, '/'); // Windows path issue. Fix all "\" for Handlebars.
}
export function getListOfDesignPreviewFiles(jsonDataFileName, previewFiles, block) {
return previewFiles
.filter(fileName => {
return fileName.startsWith(jsonDataFileName + '.');
})
.map(fileName => {
const fileData = fileName.split('.');
const fileFormat = fileData.pop();
const previewSize = fileData.pop();
return {
dataSource: jsonDataFileName,
widthDimension: Number.parseInt(previewSize, 10),
url: `/design/${block.project}/${block.name}/preview/${fileName}`,
};
});
}
+109
View File
@@ -0,0 +1,109 @@
import gulp from "gulp";
import path from "path";
import sourcemaps from "gulp-sourcemaps";
import babel from "gulp-babel";
import PluginError from "plugin-error";
import rename from "gulp-rename";
import uglify from "gulp-uglify";
import dartSass from 'sass';
import gulpSass from "gulp-sass";
import {getBlockName} from "../helpers.js";
const sass = gulpSass(dartSass);
export function setupWatcher(viewSync) {
const watchSCSS = gulp.watch(['blocks/**/*.scss'], {delay: 400});
watchSCSS.on('change', async function (filepath, stats) {
const pathArray = filepath.split('/', 3);
pathArray.shift();
const block = getBlockName(pathArray.join('/'));
buildStyleFiles(path.join('blocks', '@' + block.project, block.name), () => {
viewSync.syncTemplate(block.blockName, 'styleUpdate');
})
});
const watchJS = gulp.watch(['blocks/**/*.js', '!blocks/**/*.min.js'], {delay: 400});
watchJS.on('change', async function (filepath) {
const pathArray = filepath.split('/', 3);
pathArray.shift();
const block = getBlockName(pathArray.join('/'));
buildScriptFiles(path.join('blocks', '@' + block.project, block.name), () => {
viewSync.syncTemplate(block.blockName, 'scriptUpdate');
})
});
const watchHBS = gulp.watch(['blocks/**/*.hbs'], {delay: 400});
watchHBS.on('change', async function (filepath) {
const pathArray = filepath.split('/', 3);
pathArray.shift();
const block = getBlockName(pathArray.join('/'));
await viewSync.syncTemplate(block.blockName);
});
}
export function buildAssetFiles(projectPath) {
// Register tasks.
gulp.task('build-script-files', (done) => buildScriptFiles(projectPath, done));
gulp.task('build-styling-files', (done) => buildStyleFiles(projectPath, done));
// Run first build.
return new Promise((resolve) => {
gulp.series('build-script-files', 'build-styling-files', function () {
resolve();
})();
});
}
function showError(errorMessage) {
console.log(errorMessage);
// TODO: Send this message to browser.
// So the developer can understand there is an error.
}
function buildScriptFiles(projectPath = '', done) {
const files = getJSBundleFiles(projectPath);
const stream = gulp.src(files, {base: path.posix.join(projectPath, 'src')})
.pipe(sourcemaps.init({}))
.pipe(babel()).on('error', function (error) {
showError(new PluginError('JavaScript', error).toString());
done();
})
.pipe(gulp.src(path.join(projectPath, 'vendor/*.js')))
// .pipe(gulp.dest('src/'))
.pipe(rename({extname: '.min.js'}))
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(path.posix.join(projectPath, 'src')));
stream.on('end', done);
return stream;
}
function getJSBundleFiles(projectPath = '') {
return [path.posix.join(projectPath, "src/**/*.js"), path.posix.join(projectPath, "src/**/*.mjs"), "!" + path.posix.join(projectPath, "src/**/*.min.js")];
}
function buildStyleFiles(projectPath = '', done) {
const stream = gulp.src(path.join(projectPath, 'src/**/*.scss'), {base: path.posix.join(projectPath, 'src')})
.pipe(sourcemaps.init({}))
.pipe(sass.sync({outputStyle: 'compressed'}).on('error', function (error) {
showError(new PluginError('SCSS', error.messageFormatted).toString());
// sass.logError(error);
done();
}))
// .pipe(gulp.dest('src/'))
.pipe(rename({extname: '.min.css'}))
.pipe(sourcemaps.write('.', {}))
.pipe(gulp.dest(path.posix.join(projectPath, 'src')));
stream.on('end', done);
return stream;
}
+4
View File
@@ -0,0 +1,4 @@
export function setHeaders(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}
+53
View File
@@ -0,0 +1,53 @@
import express from "express";
import bodyParser from "body-parser";
import {createServer} from "http";
import {create} from "express-handlebars";
import {escape} from "lodash-es";
import {sanitizeUrl} from "@braintree/sanitize-url";
import path from "path";
export function init() {
const app = express();
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}));
// parse application/json
app.use(bodyParser.json());
const hbs = getHandlebarsViewEngine();
app.engine('.hbs', hbs.engine);
app.set('view engine', '.hbs');
const modulesPath = '.';
app.set('views', path.join(modulesPath, 'layouts'));
// Static Files of blockDevTool.
app.use(express.static(path.join(modulesPath, 'layouts')));
return {
expressApp: app,
httpServer: createServer(app),
};
}
function getHandlebarsViewEngine() {
return create({
extname: '.hbs', defaultLayout: false, partialsDir: ['.'], helpers: handlebarsHelpers()
});
}
function handlebarsHelpers() {
return {
esc_attr(attr) {
return escape(attr);
},
esc_url(url) {
return sanitizeUrl(url);
},
esc_html(html) {
return html;
},
};
}
+50
View File
@@ -0,0 +1,50 @@
import {Server} from "socket.io";
import fs from "fs/promises";
import {getBlockName, handlebarLayoutsPath} from "../helpers.js";
import path from "path";
export class ViewSync {
constructor(httpServer) {
this.sessions = {};
this.init(httpServer);
}
addSession(session, blockName) {
if (!this.sessions[blockName]) {
this.sessions[blockName] = [];
}
this.sessions[blockName].push(session);
}
init(httpServer) {
const io = new Server(httpServer);
io.on('connection', async (socket) => {
const blockName = socket.handshake.query.block;
if (!blockName) {
return;
}
this.addSession(socket, blockName);
await this.syncTemplate(blockName);
});
// return httpServer;
}
async syncTemplate(blockName, updateType = 'templateUpdate') {
const block = getBlockName(blockName);
const projectPath = path.join('blocks', '@' + block.project, block.name);
const hbsTemplate = await fs.readFile(handlebarLayoutsPath(projectPath, 'src', `${block.name}.template.hbs`), 'utf8');
if (!this.sessions[blockName]) {
return;
}
this.sessions[blockName].forEach(session => {
session.emit(updateType, {block: blockName, template: hbsTemplate});
});
}
}
+1
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
{{> (include_partial "layouts/partials/head") }}
+1
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
{{> (include_partial "layouts/partials/head") }}
+1
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
{{> (include_partial "layouts/partials/head") }}
+4 -2
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
<head>
@@ -7,12 +8,13 @@
<title>Block Development Tool</title>
</head>
<body>
<body class="{{#if viewMode }}view-mode{{/if}}">
{{> (include_partial "layouts/partials/toolbar") }}
<script>
window.devTool = {
blockName: '{{ blockName }}',
previewFrameUrl: '{{ previewFrameUrl }}',
{{#if publicUrl}}
publicUrl: true,
@@ -21,7 +23,7 @@
</script>
<div class="preview">
<iframe id="preview_frame" src="{{ previewFrameUrl }}?iframe=true" class="breakpoint"></iframe>
<iframe id="preview_frame" src="{{ previewFrameUrl }}?iframe=true&block={{blockName}}" class="breakpoint"></iframe>
</div>
<script src="/scripts/dist/index.min.js"></script>
+3 -1
View File
@@ -3,10 +3,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/swiper@8.4.5/swiper-bundle.min.css"/>
{{#if config.project.css }} <link rel="stylesheet" href="{{ config.project.css }}">
{{/if}}
{{#if config.cssUrl }}
{{#each config.cssUrl }}
<link rel="stylesheet" href="{{ this }}">
{{/each}}
{{/if}}<link rel="stylesheet" href="/styles/page--view.css">{{# if config.blockName}}
<link rel="stylesheet" href="/styles/{{ config.blockName }}.min.css">{{/if}}
<link rel="stylesheet" id="block-stylesheet" href="{{staticFilesPath}}/styles/{{ config.blockName }}.min.css?v=1">{{/if}}
</head>
+5 -2
View File
@@ -2,10 +2,13 @@
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
<script src="/scripts/dist/frame-index.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://unpkg.com/swiper@8.4.5/swiper-bundle.min.js"></script>{{#if config.jsUrl }}
<script src="https://unpkg.com/swiper@8.4.5/swiper-bundle.min.js"></script>
{{#if config.project.js }}<script src="{{ config.project.js }}"></script>
{{/if}}
{{#if config.jsUrl }}
{{#each config.jsUrl }}<script src="{{ this }}"></script>
{{/each}}
{{/if}}
{{#if config.blockName }}
<script src="/scripts/{{ config.blockName }}.min.js"></script>
<script id="block-script" src="{{staticFilesPath}}/scripts/{{ config.blockName }}.min.js?v=1"></script>
{{/if}}
+3 -3
View File
@@ -3,8 +3,8 @@
<div class="page_toolbar__left"></div>
<div class="page_toolbar__middle">
<div>
Version: <b>1rem = {{ config.version }}px</b>
<div style="display: none">
Version: {{ config.version }}
</div>
</div>
@@ -15,6 +15,6 @@
{{#if styleGuideUrl}}
<a href="{{ styleGuideUrl }}" target="_blank" class="palette" title="Open Style Guide"></a>
{{/if}}
<a href="{{ previewFrameUrl }}" target="_blank" class="open_in_new_tab" title="Open in New Window"></a>
<a href="{{ previewFrameUrl }}?block={{blockName}}" target="_blank" class="open_in_new_tab" title="Open in New Window"></a>
</div>
</header>
+1 -167
View File
@@ -1,167 +1 @@
window.initBlock = initBlock;
let template;
let data = {};
let reload;
// Blocks Initialization.
function initBlock(blockName = '', selector = '', cb) {
reload = function () {
document.querySelectorAll(selector).forEach((el) => cb(el));
};
reload();
}
// Scrollbars / Frame resizes notifications.
(function () {
let height;
handleHeightChange(); // Initial frame's height setup.
setupResizeListener(); // Listen to frame's height changes.
///
function setupResizeListener() {
const resizeObserver = new ResizeObserver(handleHeightChange);
resizeObserver.observe(document.body);
}
function handleHeightChange() {
const updatedHeight = getCurrentHeight();
if (height === updatedHeight) {
return;
}
const RESIZE_CODE = 'resize:';
height = updatedHeight;
window.parent.postMessage(RESIZE_CODE + JSON.stringify({height}), '*');
}
function getCurrentHeight() {
return document.querySelector('#hbs-container').scrollHeight;
}
})();
// Data Updates Listeners.
(function () {
loadDataOptions();
listenToDataOptionsUpdates();
function listenToDataOptionsUpdates() {
window.addEventListener('message', function (event) {
const message = event.data;
const prefix = 'dataUpdate:';
if (typeof message !== "string" || !message.startsWith(prefix)) {
return;
}
try {
data = JSON.parse(message.substring(prefix.length));
updateBlock({data});
} catch (e) {
console.log('Error parsing incoming data.', e);
}
});
}
function getQueryParams() {
const urlParams = new URLSearchParams(window.location.search);
const params = {};
for (const [key, value] of urlParams) {
params[key] = value;
}
return params;
}
function loadDataOptions() {
const queryParameters = new URLSearchParams({name: getQueryParams().data || 'default'});
fetch(`/data?${queryParameters}`)
.then((response) => response.json())
.then((response) => {
data = response.data; // Update state.
updateBlock({data});
});
}
})();
// Listen to Template updates.
(function () {
initSocket();
function initSocket() {
const socket = window.io.connect();
socket.on('error', function (err) {
console.log(err);
});
// socket.on('connect', function () {
// console.log('user connected', socket.id)
// });
socket.on('templateUpdate', function (args) {
updateBlock({template: args.template});
});
}
})();
function updateBlock(args = {}) {
if (args.template) {
template = args.template; // Update state.
}
if (args.data) {
data = args.data; // Update state.
}
if (!template) {
return;
}
renderBlock(template, data || {}, document.getElementById("hbs-container"));
}
function renderBlock(templateHbs, jsonData, target) {
const template = Handlebars.compile(templateHbs);
/**
* Handlebars Helpers
*/
Handlebars.registerHelper('esc_attr', function (attr) {
return attr;
});
Handlebars.registerHelper('esc_url', function (attr) {
return attr;
});
Handlebars.registerHelper('esc_html', function (attr) {
return attr;
});
Handlebars.registerHelper('base_url', function () {
return '/';
});
let html;
try {
html = template(jsonData);
} catch (e) {
html = `<div style="max-width: 1280px; margin: 1rem auto;">
<h1 style="all: unset; font-size: 1.5rem; font-weight: bold; display: block;">Syntax Error:</h1>
<pre style="all: unset; padding: 10px 15px; background-color: #ffe6e6; border: 1px solid red; border-radius: 0.25rem; overflow: auto; display: block; white-space: pre;">${e.toString()}</pre>
</div>`;
}
target.innerHTML = html;
if (reload) {
reload();
}
}
//# sourceMappingURL=frame-index.min.js.map
let e;window.initBlock=function(e="",n="",o){t=function(){document.querySelectorAll(n).forEach((e=>o(e)))},t()};let t,n={};!function(){const o=r().block;function r(){const e=new URLSearchParams(window.location.search),t={};for(const[n,o]of e)t[n]=o;return t}function a(e){const t=document.getElementById(e),n=t.parentNode;let o,r;return"SCRIPT"===t.tagName?(o=document.createElement("script"),o.type="text/javascript",r="src"):"LINK"===t.tagName&&(o=document.createElement("link"),o.rel="stylesheet",r="href"),o.id=e,o[r]=t[r].replace(/(\?v=)[^&]+/,`$1${Date.now()}`),t.remove(),n.append(o),o}function c(r={}){r.template&&(e=r.template),r.data&&(n=r.data),e&&function(e,n,r){const a=Handlebars.compile(e);let c;Handlebars.registerHelper("esc_attr",(function(e){return e})),Handlebars.registerHelper("esc_url",(function(e){return e})),Handlebars.registerHelper("esc_html",(function(e){return e})),Handlebars.registerHelper("base_url",(function(){const e=["block"];return o&&e.push(o.substr(1)),["",...e,""].join("/")}));try{c=a(n)}catch(e){c=`<div style="max-width: 1280px; margin: 1rem auto;">\n <h1 style="all: unset; font-size: 1.5rem; font-weight: bold; display: block;">Syntax Error:</h1>\n <pre style="all: unset; padding: 10px 15px; background-color: #ffe6e6; border: 1px solid red; border-radius: 0.25rem; overflow: auto; display: block; white-space: pre;">${e.toString()}</pre>\n </div>`}r.innerHTML=c,t&&t()}(e,n||{},document.getElementById("hbs-container"))}!function(){const e=new URLSearchParams({block:o,name:r().data||"default"});fetch(`/data?${e}`).then((e=>e.json())).then((e=>{n=e.data,c({data:n})}))}(),window.addEventListener("message",(function(e){const t=e.data,o="dataUpdate:";if("string"==typeof t&&t.startsWith(o))try{n=JSON.parse(t.substring(o.length)),c({data:n})}catch(e){console.log("Error parsing incoming data.",e)}})),function(){const e=window.io.connect("",{query:`block=${o}`});e.on("error",(function(e){console.log(e)})),e.on("templateUpdate",(function(e){c({template:e.template})})),e.on("scriptUpdate",(function(e){a("block-script").onload=()=>{c({template:e.template})}})),e.on("styleUpdate",(function(e){a("block-stylesheet")}))}()}();
File diff suppressed because one or more lines are too long
+15 -9917
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-21
View File
@@ -1,21 +0,0 @@
export function setupFrameResizeListener() {
const previewFrame = getPreviewFrame();
window.addEventListener('message', function (e) {
const RESIZE_CODE = 'resize:';
if (typeof e.data !== 'string' || !e.data.startsWith(RESIZE_CODE)) {
return;
}
const data = JSON.parse(e.data.substring(RESIZE_CODE.length))
let height = Number.parseInt(data.height)
if (height > 20000) {
height = 20000; // Limit max height.
}
previewFrame.style.height = height + 'px'
});
}
export function getPreviewFrame() {
return document.getElementById('preview_frame');
}
+59 -52
View File
@@ -15,48 +15,10 @@ function initBlock(blockName = '', selector = '', cb) {
reload();
}
// Scrollbars / Frame resizes notifications.
(function () {
let height;
const debug = false;
handleHeightChange(); // Initial frame's height setup.
setupResizeListener(); // Listen to frame's height changes.
///
function setupResizeListener() {
const resizeObserver = new ResizeObserver(handleHeightChange);
resizeObserver.observe(document.body);
}
function handleHeightChange() {
const updatedHeight = getCurrentHeight();
if (debug) {
console.log('Height Updates', 'Old vs New: ' + height, updatedHeight);
}
if (height === updatedHeight) {
return;
}
const RESIZE_CODE = 'resize:';
height = updatedHeight;
window.parent.postMessage(RESIZE_CODE + JSON.stringify({height}), '*');
if (debug) {
console.log('Resize message sent: ', height)
}
}
function getCurrentHeight() {
return document.querySelector('#hbs-container').scrollHeight;
}
})();
// Data Updates Listeners.
(function () {
const block = getQueryParams().block;
loadDataOptions();
listenToDataOptionsUpdates();
@@ -90,7 +52,10 @@ function initBlock(blockName = '', selector = '', cb) {
}
function loadDataOptions() {
const queryParameters = new URLSearchParams({name: getQueryParams().data || 'default'});
const queryParameters = new URLSearchParams({
block,
name: getQueryParams().data || 'default'
});
fetch(`/data?${queryParameters}`)
.then((response) => response.json())
.then((response) => {
@@ -98,14 +63,12 @@ function initBlock(blockName = '', selector = '', cb) {
updateBlock({data});
})
}
})();
// Listen to Template updates.
(function () {
// Listen to Template updates.
initSocket();
function initSocket() {
const socket = window.io.connect();
const socket = window.io.connect('', {query: `block=${block}`});
socket.on('error', function (err) {
console.log(err);
@@ -118,10 +81,47 @@ function initBlock(blockName = '', selector = '', cb) {
socket.on('templateUpdate', function (args) {
updateBlock({template: args.template});
});
}
})();
function updateBlock(args = {}) {
socket.on('scriptUpdate', function (args) {
const tag = updateTag('block-script');
tag.onload = () => {
updateBlock({template: args.template});
}
});
socket.on('styleUpdate', function (args) {
updateTag('block-stylesheet');
});
}
function updateTag(id) {
const tag = document.getElementById(id);
const wrapper = tag.parentNode;
let clone;
let attr;
if (tag.tagName === 'SCRIPT') {
clone = document.createElement('script');
clone.type = 'text/javascript';
attr = 'src';
} else if (tag.tagName === 'LINK') {
clone = document.createElement('link');
clone.rel = 'stylesheet';
attr = 'href';
}
clone.id = id;
// Add version to the stylesheet URL, make sure we override the cache and already existing version there.
clone[attr] = tag[attr].replace(/(\?v=)[^&]+/, `$1${Date.now()}`);
tag.remove();
wrapper.append(clone);
return clone;
}
function updateBlock(args = {}) {
if (args.template) {
template = args.template; // Update state.
}
@@ -135,9 +135,9 @@ function updateBlock(args = {}) {
}
renderBlock(template, data || {}, document.getElementById("hbs-container"));
}
}
function renderBlock(templateHbs, jsonData, target) {
function renderBlock(templateHbs, jsonData, target) {
const template = Handlebars.compile(templateHbs);
/**
@@ -156,7 +156,13 @@ function renderBlock(templateHbs, jsonData, target) {
});
Handlebars.registerHelper('base_url', function () {
return '/';
const list = ['block'];
if (block) {
list.push(block.substr(1));
}
return ['', ...list, ''].join('/');
});
let html;
@@ -174,4 +180,5 @@ function renderBlock(templateHbs, jsonData, target) {
if (reload) {
reload();
}
}
}
})();
+5 -2
View File
@@ -3,13 +3,16 @@
import {setupResponsiveness} from './toolbar/responsive.jsx';
import {setupPublish} from "./toolbar/publish.jsx";
import {setupDataOptions} from "./toolbar/data-options/DataOptions.jsx";
import {getPreviewFrame, setupFrameResizeListener} from "./frame/editor.js";
const rootAttributes = {
previewFrame: getPreviewFrame(),
};
setupFrameResizeListener();
// setupFrameResizeListener();
setupResponsiveness(rootAttributes);
setupDataOptions(rootAttributes);
setupPublish(rootAttributes);
function getPreviewFrame() {
return document.getElementById('preview_frame');
}
+101
View File
@@ -0,0 +1,101 @@
'use strict';
import React, {useState} from "react";
import * as ReactDOM from "react-dom/client";
function init() {
const wrapper = document.querySelector('#screen');
const root = ReactDOM.createRoot(wrapper);
const html = (<SyncScreen/>);
root.render(html);
}
function SyncScreen() {
const [loading, setLoading] = useState(null);
const [error, setError] = useState(null);
return <>
<section className="container py-5">
<h1 style={{marginBottom: '2rem'}}>Oops... Block not in sync.</h1>
{error && <p className="alert alert-danger">{error}</p>}
{loading ?
<>
{loading === 'syncing' &&
<p>Version upgrade in progress...</p>
}
<p>Please wait...</p>
</>
:
<>
<p>Your version of the block is not in sync with the cloud (not latest version).<br/>
Would you like to update it?</p>
<div className="options">
<button className="btn btn-primary" style={{marginRight: '0.5rem'}} onClick={runSyncLogic}>Yes, Update to
Latest
</button>
<button className="btn btn-secondary" onClick={ignoreVersionSync}>Ignore</button>
</div>
</>
}
</section>
</>
async function ignoreVersionSync() {
// Add "ignore" query parameter to current URl and redirect.
const url = new URL(window.location.href);
url.searchParams.set('ignoreVersionSync', 'true');
window.location = url.href;
}
async function runSyncLogic() {
setLoading('syncing');
let data = {};
try {
const response = await fetch('/sync', {
method: 'POST',
body: JSON.stringify({block: getQueryParams().block}),
headers: {
'Content-Type': 'application/json'
}
});
data = await response.json();
} catch (err) {
setError('Error: ' + err.message);
setLoading(false);
return;
}
if (data.status !== 200) {
setError(data.message);
setLoading(false);
return;
}
setTimeout(() => window.location.reload(), 1000);
}
// TODO: This function is used in multiple places (repeated code).
// Move to a common place.
function getQueryParams() {
const urlParams = new URLSearchParams(window.location.search);
const params = {};
for (const [key, value] of urlParams) {
params[key] = value;
}
return params;
}
}
init();
@@ -125,12 +125,13 @@ function DataOptions(props = {}) {
return fetch(url, requestOptions)
.then(response => response.json())
.then(result => {
console.log(result)
if (result.statusCode !== 200) {
throw new Error(result.message);
}
const data = result.variation;
updateState({dataText: JSON.stringify(data, null, 2), data});
updateState({dataText: JSON.stringify(data, null, 2), data, errorMessage: null, loading: false});
updateIframe(data);
})
.catch(error => {
@@ -168,7 +169,7 @@ function DataOptions(props = {}) {
}
async function fetchDataOptions(name = 'default') {
const queryParameters = new URLSearchParams({name});
const queryParameters = new URLSearchParams({name, block: window.devTool.blockName});
const response = await fetch(`/data?${queryParameters}`);
return await response.json();
}
+1
View File
@@ -43,6 +43,7 @@ function Publish(props = {}) {
export function setupPublish(rootAttributes) {
// INIT
const wrapper = document.createElement('div');
wrapper.setAttribute('id', 'publish-btn');
document.querySelector('.page_toolbar__right').append(wrapper)
const root = ReactDOM.createRoot(wrapper);
+2 -1
View File
@@ -65,7 +65,8 @@ function Responsive(props = {}) {
function updateController() {
let frameBreakpoint = breakpoint;
if (typeof frameBreakpoint !== 'string') {
frameBreakpoint = frameBreakpoint + 'px';
const scrollbarWidth = 15;
frameBreakpoint = (scrollbarWidth + frameBreakpoint) + 'px';
}
previewFrame.style.setProperty('--breakpoint', frameBreakpoint);
+1
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
{{> (include_partial "layouts/partials/head") }}
+1 -5
View File
@@ -1,5 +1,4 @@
.preview {
overflow-y: scroll;
height: calc(100% - var(--top_panel_height));
position: relative;
}
@@ -9,17 +8,14 @@
margin-right: auto;
margin-left: auto;
min-height: 100%;
//height: 100%;
--top_spacing: 0px;
--breakpoint_top_spacing: 30px;
margin-top: var(--top_spacing);
height: 100%;
background-color: white;
border: 0;
outline: 1px solid #E2E8F0;
transition: max-width .3s ease-in-out, width .3s ease-in-out, margin-top .3s ease-in-out;
&.has-breakpoint {
-9
View File
@@ -1,9 +0,0 @@
.swiper {
&-slide {
width: initial;
}
&-wrapper {
height: initial;
}
}
+3 -3
View File
@@ -8,6 +8,9 @@ body {
font-size: 1rem;
background-color: #F7FAFC;
}
body.view-mode #publish-btn {
display: none;
}
.btn {
--btn-color: #333;
@@ -114,7 +117,6 @@ body {
}
.preview {
overflow-y: scroll;
height: calc(100% - var(--top_panel_height));
position: relative;
}
@@ -127,10 +129,8 @@ body {
--top_spacing: 0px;
--breakpoint_top_spacing: 30px;
margin-top: var(--top_spacing);
height: 100%;
background-color: white;
border: 0;
outline: 1px solid #E2E8F0;
transition: max-width 0.3s ease-in-out, width 0.3s ease-in-out, margin-top 0.3s ease-in-out;
}
#preview_frame.has-breakpoint {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["page--main.scss","_buttons.scss","_overlay.scss","_page--preview.scss"],"names":[],"mappings":"AAAA;EACE;EACA;;;AAGF;EACE;EACA;EAEA;;;ACTF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;;AC5BJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AFIF;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKF;EADF;IAEI;;;AAKN;EAEE;EACA;EACA;EACA;;AAGF;EACE;;;AGlFJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EAEA;EACA;EAEA;;AAEA;EACE;EAEA;EACA;;;AH6DJ;EACE;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;;;AAGF;EAEE;EACA","file":"page--main.css"}
{"version":3,"sourceRoot":"","sources":["page--main.scss","_buttons.scss","_overlay.scss","_page--preview.scss"],"names":[],"mappings":"AAAA;EACE;EACA;;;AAGF;EACE;EACA;EAEA;;AAGE;EACE;;;ACbN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;;AC5BJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AFUF;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKF;EADF;IAEI;;;AAKN;EAEE;EACA;EACA;EACA;;AAGF;EACE;;;AGxFJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EACA;EACA;;AAEA;EACE;EAEA;EACA;;;AHuEJ;EACE;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;;;AAGF;EAEE;EACA","file":"page--main.css"}
+6
View File
@@ -8,6 +8,12 @@ body {
font-size: 1rem;
//overflow: none;
background-color: #F7FAFC;
&.view-mode {
#publish-btn {
display: none;
}
}
}
@import "buttons";
+1 -17
View File
@@ -2,24 +2,8 @@ body {
margin: 0;
}
main {
margin-left: auto;
margin-right: auto;
min-height: 100%;
overflow-x: hidden;
}
main .swiper-slide {
width: initial;
}
main .swiper-wrapper {
height: initial;
}
.body--iframe {
overflow-y: hidden;
}
.body--iframe main {
overflow-y: auto;
overflow-y: scroll;
}
.fullscreen_layout {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["page--view.scss","_page--view-swiper.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;;ACPA;EACE;;AAGF;EACE;;;ADSJ;EACE;;AAEA;EAGE;;;AAIJ;EACE;EACA;EACA;EACA","file":"page--view.css"}
{"version":3,"sourceRoot":"","sources":["page--view.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;EACA;EACA","file":"page--view.css"}
+1 -18
View File
@@ -2,25 +2,9 @@ body {
margin: 0;
}
main {
margin-left: auto;
margin-right: auto;
min-height: 100%;
overflow-x: hidden;
// Fixes scrolling issues of swiperJS. Should be included in all projects.
@import "page--view-swiper";
}
// iFrame mode
.body--iframe {
overflow-y: hidden;
main {
// If you change to "overflow: initial", the margin-top/bottom of first/last element will be not included.
// Test on fresh block setup where heading has margin-top.
overflow-y: auto;
}
overflow-y: scroll;
}
.fullscreen_layout {
@@ -29,4 +13,3 @@ main {
background-image: url('https://i.ibb.co/pjwL8D1/shapelined-JBKdviwe-XI-unsplash.jpg');
background-size: cover;
}
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Block Development Tool</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
</head>
<body>
<div id="screen">Loading...</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/scripts/dist/sync.min.js"></script>
</body>
</html>
+116 -8123
View File
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -1,16 +1,17 @@
{
"name": "@axe-web/block-dev-tool",
"version": "1.0.27",
"version": "1.0.31",
"author": {
"name": "AXE-WEB",
"email": "office@axe-web.com",
"url": "https://axe-web.com/"
},
"scripts": {
"info": "NODE_ENV=development BLOCK_NAME=header MODULE_PATH= node debug.js",
"dev": "NODE_ENV=development BLOCK_NAME=header MODULE_PATH= node server.js",
"build-platform": "NODE_ENV=development BLOCK_NAME=header MODULE_PATH= node ./build.js",
"dev-dev-tool": "NODE_ENV=development rollup --config rollup.config.js --watch",
"info": "node debug.js",
"dev": "node server.js",
"view-mode": "VIEW_MODE=true node server.js",
"build-platform": "BLOCK_NAME=swiper-test MODULE_PATH= node ./build.js",
"dev-dev-tool": "rollup --config rollup.config.js --watch",
"build-dev-tool": "rollup --config rollup.config.js"
},
"engines": {
@@ -19,8 +20,10 @@
"license": "ISC",
"type": "module",
"dependencies": {
"@axe-web/create-block": "^1.1.30",
"@braintree/sanitize-url": "^6.0.0",
"archiver": "^5.3.1",
"body-parser": "^1.20.2",
"browser-sync": "^2.27.9",
"config": "^3.3.7",
"escape-html": "^1.0.3",
@@ -37,6 +40,7 @@
"lodash-es": "^4.17.21",
"mem-fs": "^2.2.1",
"mem-fs-editor": "^9.5.0",
"mime-types": "^2.1.35",
"ngrok": "^5.0.0-beta.2",
"node-fetch": "^3.2.10",
"open": "^8.4.0",
@@ -26,6 +26,10 @@ export async function createDistFolder(blockName, projectPath = '') {
}
export function getBlockFields(block = {}, type = 'content') {
if (!block['field_groups']) {
return [];
}
const fields_group = block['field_groups'].find((group) => group.name === type);
const fields = [];
+2 -2
View File
@@ -71,8 +71,8 @@ class Component_Builder {
$this->register_default_handlebar_helpers();
$this->add_handlebar( 'base_url', function ( $context ) {
$path = join( '/', [ 'blocks', $this->block_project, $this->block_name, 'templates' ] );
return plugins_url( $path . '/', 'axeweb-blocks-library/axeweb-blocks-library.php' );
$path = join( '/', [ $this->block_project, $this->block_name, 'templates' ] );
return join( '/', [ get_site_url(),'wp-content', 'axe-web-blocks', $path, '' ] );
} );
}
@@ -22,8 +22,8 @@ class <%= blockClassModel %>_Component extends \Core\Component {
// $version = \Core\Global_Functions::get_current_version_number(); // In Theme
// $base_path = get_template_directory_uri() . '/components/partials/<%= blockFilename %>/';
wp_enqueue_style( 'block-<%= blockFilename %>', plugins_url( 'templates/styles/<%= blockFilename %>.min.css', __FILE__ ), ['assets-style'], self::VERSION );<% if (include_script) { %>
wp_enqueue_script( 'block-<%= blockFilename %>', plugins_url( 'templates/scripts/<%= blockFilename %>.min.js', __FILE__ ), ['assets-script'], self::VERSION, true );<% } %>
wp_enqueue_style( 'block-<%= blockFilename %>', $this->get_assets_path_url( 'templates/styles/<%= blockFilename %>.min.css' ), ['assets-style'], self::VERSION );<% if (include_script) { %>
wp_enqueue_script( 'block-<%= blockFilename %>', $this->get_assets_path_url( 'templates/scripts/<%= blockFilename %>.min.js' ), ['assets-script'], self::VERSION, true );<% } %>
wp_enqueue_script( 'script-block-<%= blockFilename %>' );
}<% } %>
@@ -32,13 +32,13 @@ class <%= blockClassModel %>_Component extends \Core\Component {
'style_assets' => [
[
'name' => '<%= blockFilename %>',
'url' => plugins_url( 'templates/styles/<%= blockFilename %>.min.css', __FILE__ ),
'url' => $this->get_assets_path_url( 'templates/styles/<%= blockFilename %>.min.css' ),
]
],
'script_assets' => [
[
'name' => '<%= blockFilename %>',
'url' => plugins_url( 'templates/scripts/<%= blockFilename %>.min.js', __FILE__ ),
'url' => $this->get_assets_path_url( 'templates/scripts/<%= blockFilename %>.min.js' ),
]
]
] );
@@ -50,7 +50,7 @@ class <%= blockClassModel %>_Component extends \Core\Component {
$asset_file_front = include( plugin_dir_path( __FILE__ ) . '/templates/gutenberg-block/build/front.asset.php' );
wp_enqueue_script(
'gutenberg-<%= blockFilename %>-scripts-front',
plugins_url( 'templates/gutenberg-block/build/front.js', __FILE__ ),
$this->get_assets_path_url( 'templates/gutenberg-block/build/front.js' ),
$asset_file_front['dependencies'],
$asset_file_front['version'],
true
+9 -2
View File
@@ -1,5 +1,5 @@
import path from "path";
import {mkdir, copyFile} from "fs/promises";
import fs, {mkdir, copyFile} from "fs/promises";
import {capitalize, createFiles, getBlockName, getConfigs, readJSONFile} from "../../helpers.js";
import {fileURLToPath} from 'url';
import {copy} from "fs-extra";
@@ -68,10 +68,17 @@ export async function buildWordPress(blockName, args = {}) {
path.join(distPath, 'templates', 'scripts', `${data.blockFilename}.min.js`),
);
try {
const imagesPath = path.join(projectPath, 'src', 'images');
await fs.access(imagesPath);
await copy(
path.join(projectPath, 'src', 'images'),
path.join(imagesPath),
path.join(distPath, 'templates', 'images'),
);
} catch (err) {
// Folder doesn't exist.
}
const phpDataObject = await execPHPFile(path.join(phpGeneratorPath, 'build.php'), 'jsonToPhp', {
json: await readJSONFile(path.join(projectPath, 'data', 'default.json'), "utf8"),
+22
View File
@@ -47,4 +47,26 @@ export default [{
commonjs(),
!devMode && terser()
]
}, {
input: 'layouts/scripts/sync.jsx',
output: {
file: 'layouts/scripts/dist/sync.min.js',
sourcemap: devMode
},
plugins: [
nodeResolve({
extensions: [".js"],
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: true,
}),
babel({
compact: false,
babelHelpers: 'bundled',
presets: ["@babel/preset-react"],
}),
commonjs(),
!devMode && terser()
]
}];
+43
View File
@@ -0,0 +1,43 @@
import {getBlockConfigs, getBlockName, getBlockVariations, handlebarLayoutsPath} from "../helpers.js";
import path from "path";
import {buildAssetFiles} from "../inc/changes-watcher.js";
export default async function (req, res) {
const blockName = req.query.block;
const block = getBlockName(blockName);
const blockDir = path.join('blocks', '@' + block.project, block.name);
const dataFiles = await getBlockVariations(blockDir);
const data = await getDataOfFrame(!!req.query.iframe, Object.assign({}, block, {dataFiles}));
if (data.error && data.errorMessage) {
return res.send(data.errorMessage);
}
const baseView = req.params.baseView ?? 'alignfull';
await buildAssetFiles(blockDir);
res.render(baseView, data)
}
async function getDataOfFrame(isIframe = false, block) {
const data = {config: await getBlockConfigs(block)};
if (data.error && data.errorMessage) {
return data;
}
data.helpers = {
include_partial: (filesPath) => handlebarLayoutsPath(filesPath),
}
if (isIframe) {
data.iframeMode = true;
}
data.staticFilesPath = `/block/${block.project}/${block.name}`;
return data;
}
+30
View File
@@ -0,0 +1,30 @@
import {
getBlockData,
getBlockName,
getBlockVariations,
getListOfDesignPreviewFiles,
} from "../helpers.js";
import path from "path";
import fs from "fs/promises";
export default async function (req, res, next) {
let variationName = req.query.name ? req.query.name : 'default';
const block = getBlockName(req.query.block);
const blockDir = path.join('blocks', '@' + block.project, block.name);
const dataFiles = await getBlockVariations(blockDir);
const data = await getBlockData(variationName, {projectPath: blockDir});
let designPreviewFiles = [];
try {
designPreviewFiles = getListOfDesignPreviewFiles(variationName, await fs.readdir(path.join(blockDir, 'design', 'preview')), block);
} catch (err) {
console.log('Preview Design doesn\'t exist');
}
return res.json({
dataOptions: dataFiles,
designPreview: designPreviewFiles,
data,
});
}
+47
View File
@@ -0,0 +1,47 @@
import {getBlockConfigs, getBlockName, getBlockVariations, handlebarLayoutsPath, verifyVersion} from "../helpers.js";
import path from "path";
import {REGISTRY_URL} from "@axe-web/create-block/env.js";
export async function index(req, res, next) {
const blockName = req.query.block;
const block = getBlockName(blockName);
const blockDir = path.join('blocks', '@' + block.project, block.name);
const dataFiles = await getBlockVariations(blockDir);
const data = await getBlockConfigs(Object.assign({}, block, {dataFiles}));
if (data.error && data.errorMessage) {
// TODO: Throw Error.
}
data.helpers = {
include_partial: (filesPath) => handlebarLayoutsPath('.', filesPath),
}
// TODO: Make sure we sync block before we start working on it.
try {
const verifiedVersion = await verifyVersion(blockName, blockDir, REGISTRY_URL);
if (!verifiedVersion && !req.query['ignoreVersionSync']) {
return res.render('sync', data);
}
} catch (err) {
const errorMessage = "Can't verify block version.";
console.log(errorMessage, err);
return next(new Error(errorMessage));
}
data.blockName = blockName;
data.baseView = 'alignfull';
data.port = `/view/${data.baseView}`;
let previewFrameUrl = ``; // This variable is used in `*.hbs` and it will be updated once BrowserSync is ready.
data.previewFrameUrl = `${previewFrameUrl}${data.port}`;
data.previewFrameUrlNewWindow = `${previewFrameUrl}${data.port}`;
data.shareUrl = '';
data.viewMode = process.env.VIEW_MODE ?? false;
res.render('index', data);
}
+7
View File
@@ -0,0 +1,7 @@
export default async function (req, res, next) {
if (process.env.VIEW_MODE) {
throw new Error("Can't publish in view mode.");
}
}
+23
View File
@@ -0,0 +1,23 @@
export function staticFiles(req, res) {
const fileName = req.params[0];
const path = `blocks/@${req.params.project}/${req.params.blockName}/src/${fileName}`;
return deliverStaticFile(path, res);
}
export function previewFiles(req, res) {
const fileName = req.params[0];
const path = `blocks/@${req.params.project}/${req.params.blockName}/design/${fileName}`;
return deliverStaticFile(path, res);
}
function deliverStaticFile(path, res) {
// If file doesn't exist, return 404.
res.sendFile(path, {root: './'}, function (err) {
if (err) {
res.send('File not found.');
res.status(err.status).end();
}
});
}
+11
View File
@@ -0,0 +1,11 @@
import {syncFilesWithCloud} from "../helpers.js";
export default async function (req, res, next) {
try {
await syncFilesWithCloud(req.body.block, null, true);
} catch (err) {
return res.status(500).json({status: 500, message: err.message});
}
res.json({status: 200, message: 'Successfully synced!'});
}
+24 -430
View File
@@ -1,441 +1,35 @@
#!/usr/bin/env node
import {PRODUCTION_REGISTRY_URL} from "./env.js";
import path from 'path';
import fetch from "node-fetch";
import express from 'express';
import {create} from 'express-handlebars';
import browserSync from 'browser-sync';
import config from 'config';
import gulp from 'gulp';
import babel from "gulp-babel";
import uglify from "gulp-uglify";
import rename from "gulp-rename";
import dartSass from 'sass';
import gulpSass from 'gulp-sass';
import sourcemaps from "gulp-sourcemaps";
import fs from "fs/promises";
import open from "open";
import {sanitizeUrl} from "@braintree/sanitize-url";
import sanitizeHtml from 'sanitize-html';
import {escape} from "lodash-es";
import {getBlockData, getBlockConfigs, getConfigs, readJSONFile, zipProject} from "./helpers.js";
import PluginError from 'plugin-error';
import {Server} from "socket.io";
import {createServer} from 'http';
import {authtoken, connect} from "ngrok";
import {init} from "./inc/server.js";
import {index} from "./routes/index.js";
import dataRoute from "./routes/data.js";
import baseView from "./routes/base-view.js";
import syncRoute from "./routes/sync.js";
import publishRoute from "./routes/publish.js";
import {ViewSync} from "./inc/view-sync.js";
import {previewFiles, staticFiles} from "./routes/static-files.js";
import {setupWatcher} from "./inc/changes-watcher.js";
/**
* Constants
*/
const {expressApp, httpServer} = init();
const {isDev, modulesPath, projectPath, developmentBlockName} = getConfigs();
const blocksRegistry = isDev ? 'http://localhost:3020' : PRODUCTION_REGISTRY_URL;
const DevToolToken = 'D9lgz0TvzXCnp0xnwVBL109DaAR6Puk6F7YewDhgmP8='; // Temporary token for development purposes.
const viewSync = new ViewSync(httpServer);
setupWatcher(viewSync);
const dataFiles = prepareListOfDataFiles(await fs.readdir(path.join(projectPath, 'data')));
const PORT = process.env.PORT || 3010;
/**
* State
*/
let port = 3000; // This variable is used in `*.hbs` and it will be updated once BrowserSync is ready.
let previewFrameUrl = `/`; // This variable is used in `*.hbs` and it will be updated once BrowserSync is ready.
let shareUrl = '';
const sessions = [];
/**
* Init server
*/
const app = express();
const httpServer = createServer(app);
initSessionsServer(httpServer);
const sass = gulpSass(dartSass);
const hbs = create({
extname: '.hbs', defaultLayout: false, partialsDir: ['.'], helpers: {
esc_attr(attr) {
return escape(attr);
}, esc_url(url) {
return sanitizeUrl(url);
}, esc_html(html) {
// TODO: Check if we can remove this helper.
return html;
}, safe_html(html) {
return sanitizeHtml(html);
}
}
httpServer.listen(PORT, '0.0.0.0', () => {
console.log(`Server app listening on port ${PORT}`)
});
app.engine('.hbs', hbs.engine);
app.set('view engine', '.hbs');
app.set('views', path.join(modulesPath, 'layouts'));
// Middleware
// expressApp.use(setHeaders);
//
// Routes
//
expressApp.get('/', index);
expressApp.get('/data', dataRoute);
expressApp.get('/view/:baseView', baseView);
expressApp.get('/design/:project/:blockName/*', previewFiles);
expressApp.get('/block/:project/:blockName/*', staticFiles);
expressApp.post('/sync', syncRoute);
expressApp.get('/publish', publishRoute);
app.get('/', (req, res) => {
const data = getBlockConfigs({modulesPath, dataFiles});
if (data.error && data.errorMessage) {
return res.send(data.errorMessage);
}
const baseView = config.has('baseView') ? config.get('baseView') : 'container';
const baseViewUrl = `view/${baseView}`;
data.helpers = {
include_partial: (filesPath) => handlebarLayoutsPath(modulesPath, filesPath),
}
data.baseView = baseView;
data.port = `/${baseViewUrl}`;
data.previewFrameUrl = `${previewFrameUrl}/${baseViewUrl}`;
// data.previewFrameUrl = `/${baseViewUrl}`;
data.shareUrl = shareUrl;
if (req.headers.referer) {
// NGROK, public URL
data.shareUrl = undefined; // Link already shared.
data.previewFrameUrl = `/${baseViewUrl}`;
data.publicUrl = true;
}
res.render('index', data);
});
app.get('/view/:baseView', (req, res) => {
const data = {config: getBlockConfigs({modulesPath, dataFiles})};
if (data.error && data.errorMessage) {
return res.send(data.errorMessage);
}
data.helpers = {
include_partial: (filesPath) => handlebarLayoutsPath(modulesPath, filesPath),
}
if (!!req.query.iframe) {
data.iframeMode = true;
}
const baseView = req.params.baseView ?? 'container';
res.render(baseView, data)
});
app.get('/publish', async (req, res) => {
const data = await readJSONFile(path.join(projectPath, `block.json`));
let responseData = {
uploadUrl: undefined
};
try {
const response = await fetch(`${blocksRegistry}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {'Content-Type': 'application/json'}
});
responseData = await response.json();
} catch (e) {
res.json({success: false, message: 'Blocks Registry server is not available.'});
return;
}
if (responseData.statusCode !== 200) {
res.json({success: false, message: 'Error on registry level.'});
return;
}
if (responseData.uploadUrl) {
await zipProject(path.join(projectPath, 'src'), path.join(projectPath, 'dist.zip'));
const body = await fs.readFile(path.join(projectPath, 'dist.zip'));
const response = await fetch(`${responseData.uploadUrl}`, {
method: 'PUT',
body,
headers: {'Content-Type': 'application/zip'}
});
if (response.status !== 200) {
res.json({success: false, message: "Can't upload the archive, permissions error."});
// TODO: Need to update the registry server.
await fs.unlink(path.join(projectPath, 'dist.zip'));
return;
}
}
res.json({success: true});
await fs.unlink(path.join(projectPath, 'dist.zip'));
});
app.get('/data', async (req, res) => {
let jsonDataFileName = req.query.name ? req.query.name : 'default';
const dataFiles = prepareListOfDataFiles(await fs.readdir(path.join(projectPath, 'data')));
const data = await getBlockData(jsonDataFileName, {projectPath});
const designPreviewFiles = getListOfDesignPreviewFiles(jsonDataFileName, await fs.readdir(path.join(projectPath, 'design', 'preview')));
return res.json({
dataOptions: dataFiles,
designPreview: designPreviewFiles,
data,
});
});
// Errors handler
app.use(handleSyntaxErrors);
// Static Files
app.use(express.static(path.join(projectPath, 'src')));
app.use(express.static(path.join(projectPath, 'design')));
app.use(express.static(path.join(modulesPath, 'layouts')));
// Custom Middleware
app.use(setHeaders);
// Setup Gulp
await buildAssetFiles();
// BrowserSync
shareUrl = await getShareableUrl();
const bsOptions = await startBrowserSync();
port = bsOptions.port;
previewFrameUrl = bsOptions.previewFrameUrl;
await open(bsOptions.devToolUrl);
//
// Functions
//
function getListOfDesignPreviewFiles(jsonDataFileName, previewFiles) {
return previewFiles
.filter(fileName => {
return fileName.startsWith(jsonDataFileName + '.');
})
.map(fileName => {
const fileData = fileName.split('.');
const fileFormat = fileData.pop();
const previewSize = fileData.pop();
return {
dataSource: jsonDataFileName,
widthDimension: Number.parseInt(previewSize, 10),
url: `/preview/${fileName}`,
};
});
}
function startBrowserSync() {
return new Promise((resolve, reject) => {
const listener = httpServer.listen(0, async () => {
const PORT = listener.address().port;
console.log(`The web server has started on port ${PORT}`);
const bs = browserSync.create();
const files = getJSBundleFiles();
gulp.watch(files, {delay: 400}, gulp.series(['build-script-files', function (cb) {
browserSyncReload(bs, 'js', 'Script Files Change');
return cb();
}]));
gulp.watch(path.posix.join(projectPath, "src/**/*.scss"), {delay: 400}, gulp.series(['build-styling-files', function (cb) {
browserSyncReload(bs, 'css', 'Style Files Change');
return cb();
}]));
bs.watch(path.join(projectPath, "src/**/*.hbs"), function () {
return syncTemplate(sessions);
});
const args = {
proxy: `http://localhost:${PORT}`,
open: false
};
if (shareUrl) {
args.socket = {
domain: shareUrl
};
}
bs.init(args, (err, bs) => {
if (err) {
return reject(err);
}
const options = bs.getOptions().toJS();
const urls = {
devTool: options.urls.local.replace(options.port, options.proxy.url.port),
previewFrame: options.urls.local,
};
// If local network is available.
if (options.urls.external) {
urls.devTool = options.urls.external.replace(options.port, options.proxy.url.port);
urls.previewFrame = options.urls.external;
}
resolve({
devToolUrl: urls.devTool,
previewFrameUrl: urls.previewFrame,
port: options.port
});
});
});
});
}
function browserSyncReload(bs, extension = '', message = '') {
if (isDev) {
// console.log(event, file);
console.log(message);
}
if (extension) {
extension = "*." + extension;
}
bs.reload(extension);
}
function getJSBundleFiles() {
return [path.posix.join(projectPath, "src/**/*.js"), path.posix.join(projectPath, "src/**/*.mjs"), "!" + path.posix.join(projectPath, "src/**/*.min.js")];
}
function buildScriptFiles(done) {
const files = getJSBundleFiles();
return gulp.src(files, {base: path.posix.join(projectPath, 'src')})
.pipe(sourcemaps.init({}))
.pipe(babel()).on('error', function (error) {
showError(new PluginError('JavaScript', error).toString());
done();
})
.pipe(gulp.src(path.join(projectPath, 'vendor/*.js')))
// .pipe(gulp.dest('src/'))
.pipe(rename({extname: '.min.js'}))
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(path.posix.join(projectPath, 'src')));
}
function buildStyleFiles(done) {
return gulp.src(path.join(projectPath, 'src/**/*.scss'), {base: path.posix.join(projectPath, 'src')})
.pipe(sourcemaps.init({}))
.pipe(sass.sync({outputStyle: 'compressed'}).on('error', function (error) {
showError(new PluginError('SCSS', error.messageFormatted).toString());
// sass.logError(error);
done();
}))
// .pipe(gulp.dest('src/'))
.pipe(rename({extname: '.min.css'}))
.pipe(sourcemaps.write('.', {}))
.pipe(gulp.dest(path.posix.join(projectPath, 'src')))
}
function buildAssetFiles() {
// Register tasks.
gulp.task('build-script-files', buildScriptFiles);
gulp.task('build-styling-files', buildStyleFiles);
// Run first build.
return new Promise((resolve) => {
gulp.series('build-script-files', 'build-styling-files', function () {
resolve();
})();
});
}
function showError(errorMessage) {
console.log(errorMessage);
// TODO: Send this message to browser.
// So the developer can understand there is an error.
}
function prepareListOfDataFiles(dataFiles) {
return dataFiles
.filter((fileName) => fileName.split('.').pop() === 'json')
.map((fileName) => {
const splitName = fileName.split('.');
splitName.pop();
return splitName.join('');
})
.sort();
}
function handleSyntaxErrors(err, req, res, next) {
if (err) {
return res.render('error', {
helpers: {
include_partial: (filesPath) => path.join(modulesPath, filesPath),
},
err
});
}
next();
}
function handlebarLayoutsPath() {
return path.join(...arguments)
.replace(/\\/g, '/'); // Windows path issue. Fix all "\" for Handlebars.
}
function initSessionsServer(httpServer) {
const io = new Server(httpServer);
io.on('connection', async (socket) => {
sessions.push(socket);
await syncTemplate(sessions);
});
return httpServer;
}
function setHeaders(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}
async function syncTemplate(sessions = []) {
const blockName = config.has('blockName') ? config.get('blockName') : developmentBlockName;
const hbsTemplate = await fs.readFile(handlebarLayoutsPath(projectPath, 'src', `${blockName}.template.hbs`), 'utf8');
sessions.forEach(socket => {
socket.emit('templateUpdate', {template: hbsTemplate});
});
}
async function getShareableUrl() {
let domain = PRODUCTION_REGISTRY_URL;
let data = {}
try {
const response = await fetch(`${domain}/dev-tool/?devToolToken=${DevToolToken}`);
data = await response.json();
} catch (e) {
console.log('Error:', `Can't load data from DevTool endpoint: ${domain}`);
}
if (data.statusCode !== 200) {
console.log('Reply from DevTool endpoint:', data.message);
return null;
}
let url;
try {
await authtoken(data.ngrokApiToken);
url = await connect(port);
} catch (e) {
console.log('Error:', `Can't connect to ngrok, probably wrong token.`);
}
return url;
}