26 changed files with 611 additions and 692 deletions
@ -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; |
|||
} |
|||
@ -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; |
|||
}, |
|||
}; |
|||
} |
|||
@ -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 +1 @@ |
|||
let e;window.initBlock=function(e="",n="",r){t=function(){document.querySelectorAll(n).forEach((e=>r(e)))},t()};let t,n={};function r(r={}){r.template&&(e=r.template),r.data&&(n=r.data),e&&function(e,n,r){const a=Handlebars.compile(e);let o;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(){return"/"}));try{o=a(n)}catch(e){o=`<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=o,t&&t()}(e,n||{},document.getElementById("hbs-container"))}!function(){function e(){const e=new URLSearchParams(window.location.search),t={};for(const[n,r]of e)t[n]=r;return t}!function(){const t=new URLSearchParams({name:e().data||"default"});fetch(`/data?${t}`).then((e=>e.json())).then((e=>{n=e.data,r({data:n})}))}(),window.addEventListener("message",(function(e){const t=e.data,a="dataUpdate:";if("string"==typeof t&&t.startsWith(a))try{n=JSON.parse(t.substring(a.length)),r({data:n})}catch(e){console.log("Error parsing incoming data.",e)}}))}(),function(){const e=window.io.connect();e.on("error",(function(e){console.log(e)})),e.on("templateUpdate",(function(e){r({template:e.template})}))}(); |
|||
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
File diff suppressed because one or more lines are too long
@ -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;;;AAGF;EACE;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EACA;EACA;;AAEA;EACE;EAEA;EACA;;;AHiEJ;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"} |
|||
@ -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; |
|||
} |
|||
|
|||
@ -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, |
|||
}); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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."); |
|||
} |
|||
|
|||
} |
|||
@ -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(); |
|||
} |
|||
}); |
|||
} |
|||
@ -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!'}); |
|||
} |
|||
@ -1,604 +1,32 @@ |
|||
#!/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 { |
|||
getBlockConfigs, |
|||
getBlockData, |
|||
getBlockName, |
|||
getConfigs, |
|||
getImagesList, |
|||
isFileEmpty, |
|||
readJSONFile, |
|||
removeCommentsFromCss, |
|||
removeCommentsFromJs, |
|||
replaceNames, |
|||
syncFilesWithCloud, |
|||
uploadFile, |
|||
verifyVersion, |
|||
zipProject |
|||
} from "./helpers.js"; |
|||
import PluginError from 'plugin-error'; |
|||
import {Server} from "socket.io"; |
|||
import {createServer} from 'http'; |
|||
import {authtoken, connect} from "ngrok"; |
|||
import bodyParser from "body-parser"; |
|||
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 = await getDataFiles(projectPath); |
|||
const DEFAULT_VIEW_LAYOUT = 'alignfull'; |
|||
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 = []; |
|||
let ignoreVersionSync = false; |
|||
let bs; |
|||
|
|||
/** |
|||
* Init server |
|||
*/ |
|||
|
|||
const app = express(); |
|||
|
|||
// parse application/x-www-form-urlencoded
|
|||
app.use(bodyParser.urlencoded({extended: false})) |
|||
|
|||
// parse application/json
|
|||
app.use(bodyParser.json()) |
|||
|
|||
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')); |
|||
|
|||
//
|
|||
// Routes
|
|||
//
|
|||
|
|||
app.get('/', async (req, res, next) => { |
|||
const data = getBlockConfigs({modulesPath, dataFiles}); |
|||
if (data.error && data.errorMessage) { |
|||
return res.send(data.errorMessage); |
|||
} |
|||
|
|||
const baseView = config.has('baseView') ? config.get('baseView') : DEFAULT_VIEW_LAYOUT; |
|||
const baseViewUrl = `view/${baseView}`; |
|||
|
|||
data.helpers = { |
|||
include_partial: (filesPath) => handlebarLayoutsPath(modulesPath, filesPath), |
|||
} |
|||
|
|||
try { |
|||
const verifiedVersion = await verifyVersion(projectPath, blocksRegistry); |
|||
if (!verifiedVersion && !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.baseView = baseView; |
|||
data.port = `/${baseViewUrl}`; |
|||
data.previewFrameUrl = `${previewFrameUrl}/${baseViewUrl}`; |
|||
data.shareUrl = shareUrl; |
|||
|
|||
// TODO: Need to review this logic, conflicts with the browsersync work after "/sync" action.
|
|||
// 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 = getDataOfFrame(!!req.query.iframe); |
|||
|
|||
if (data.error && data.errorMessage) { |
|||
return res.send(data.errorMessage); |
|||
} |
|||
|
|||
const baseView = req.params.baseView ?? DEFAULT_VIEW_LAYOUT; |
|||
|
|||
res.render(baseView, data) |
|||
}); |
|||
|
|||
app.get('/publish', async (req, res) => { |
|||
const data = await readJSONFile(path.join(projectPath, `block.json`)); |
|||
|
|||
// Trigger build on the registry server only if the type of the unit is `foundation` or `component`.
|
|||
const uploadStaticFiles = ['foundation', 'component'].includes(data.type); |
|||
|
|||
// Prepare list of static files for the registry server.
|
|||
if (uploadStaticFiles) { |
|||
data.static_files = { |
|||
css: getBlockName(data.name).name + '.min.css', |
|||
js: getBlockName(data.name).name + '.min.js', |
|||
images: await getImagesList(path.join(projectPath, 'src', 'images')), |
|||
} |
|||
|
|||
if (await isFileEmpty(path.join(projectPath, `src/scripts`, data.static_files.js), true)) { |
|||
delete data.static_files.js; |
|||
} |
|||
|
|||
if (!data.static_files.images.length) { |
|||
delete data.static_files.images; |
|||
} |
|||
} |
|||
|
|||
let responseData = { |
|||
uploadBundleUrl: undefined, |
|||
staticFilesUrls: {} |
|||
}; |
|||
|
|||
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; |
|||
} |
|||
|
|||
// Start files uploading process.
|
|||
try { |
|||
if (responseData.uploadBundleUrl) { |
|||
await zipProject(path.join(projectPath, 'src'), path.join(projectPath, 'dist.zip')); |
|||
await uploadFile(path.join(projectPath, 'dist.zip'), responseData.uploadBundleUrl); // Bundle
|
|||
} |
|||
|
|||
if (uploadStaticFiles) { |
|||
if (responseData.staticFilesUrls.css) { |
|||
await uploadFile( |
|||
path.join(projectPath, 'src/styles', data.static_files.css), |
|||
responseData.staticFilesUrls.css.uploadUrl, |
|||
(content) => { |
|||
if (responseData.staticFilesUrls.images) { |
|||
content = replaceNames(content, data.static_files.images, responseData.staticFilesUrls.images); |
|||
} |
|||
|
|||
removeCommentsFromCss(content) |
|||
return content; |
|||
}); // CSS
|
|||
} |
|||
|
|||
if (responseData.staticFilesUrls.js) { |
|||
await uploadFile( |
|||
path.join(projectPath, 'src/scripts', data.static_files.js), |
|||
responseData.staticFilesUrls.js.uploadUrl, |
|||
(data) => removeCommentsFromJs(data) |
|||
); // JS
|
|||
} |
|||
|
|||
if (responseData.staticFilesUrls.images) { |
|||
for (let i = 0; i < data.static_files.images.length; i++) { |
|||
await uploadFile( |
|||
path.join(projectPath, 'src/images', data.static_files.images[i]), |
|||
responseData.staticFilesUrls.images[i].uploadUrl, |
|||
); // Images
|
|||
} |
|||
} |
|||
} |
|||
} catch (err) { |
|||
// TODO: Need to update the registry server.
|
|||
await fs.unlink(path.join(projectPath, 'dist.zip')); // Remove local bundle
|
|||
|
|||
res.json({success: false, message: err.message}); |
|||
return; |
|||
} |
|||
|
|||
await fs.unlink(path.join(projectPath, 'dist.zip')); // Remove local bundle
|
|||
|
|||
// Trigger project's global files build on the registry server.
|
|||
if (uploadStaticFiles) { |
|||
try { |
|||
await triggerGlobalProjectFilesBuild(getBlockName(data.name).project); |
|||
} catch (err) { |
|||
res.json({success: false, message: 'Something wrong with Project Builder.'}); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
try { |
|||
await syncFilesWithCloud(path.join(projectPath, 'block.json'), bs); |
|||
} catch (err) { |
|||
res.json({success: false, message: err}); |
|||
return; |
|||
} |
|||
|
|||
res.json({success: true}); |
|||
}); |
|||
|
|||
app.get('/data', async (req, res) => { |
|||
let jsonDataFileName = req.query.name ? req.query.name : 'default'; |
|||
|
|||
const dataFiles = await getDataFiles(projectPath); |
|||
const data = await getBlockData(jsonDataFileName, {projectPath}); |
|||
|
|||
let designPreviewFiles = []; |
|||
try { |
|||
designPreviewFiles = getListOfDesignPreviewFiles(jsonDataFileName, await fs.readdir(path.join(projectPath, 'design', 'preview'))); |
|||
} catch (err) { |
|||
console.log('Preview Design doesn\'t exist'); |
|||
} |
|||
|
|||
return res.json({ |
|||
dataOptions: dataFiles, |
|||
designPreview: designPreviewFiles, |
|||
data, |
|||
}); |
|||
}); |
|||
|
|||
app.post('/sync', async (req, res) => { |
|||
if (req.body['ignore']) { |
|||
ignoreVersionSync = true; |
|||
res.json({status: 200, message: 'Version upgrade is ignored.'}); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
await syncFilesWithCloud(path.join(projectPath, `block.json`), bs, true); |
|||
} catch (err) { |
|||
res.status(500).json({status: 500, message: err}); |
|||
} |
|||
|
|||
res.json({status: 200, message: 'Successfully synced!'}); |
|||
}); |
|||
|
|||
// 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 getDataOfFrame(isIframe = false) { |
|||
const data = {config: getBlockConfigs({modulesPath, dataFiles})}; |
|||
if (data.error && data.errorMessage) { |
|||
return data; |
|||
} |
|||
|
|||
data.helpers = { |
|||
include_partial: (filesPath) => handlebarLayoutsPath(modulesPath, filesPath), |
|||
} |
|||
|
|||
if (isIframe) { |
|||
data.iframeMode = true; |
|||
} |
|||
|
|||
return data; |
|||
} |
|||
|
|||
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}`); |
|||
|
|||
// BS is global variable.
|
|||
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; |
|||
} |
|||
|
|||
async function triggerGlobalProjectFilesBuild(project) { |
|||
const response = await fetch(`${blocksRegistry}/project-files`, { |
|||
method: 'POST', |
|||
body: JSON.stringify({project}), |
|||
headers: {'Content-Type': 'application/json'} |
|||
}); |
|||
|
|||
return response.json(); |
|||
} |
|||
|
|||
async function getDataFiles(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.'); |
|||
} |
|||
|
|||
return dataFiles; |
|||
} |
|||
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); |
|||
|
|||
Loading…
Reference in new issue