You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
16 KiB
602 lines
16 KiB
#!/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";
|
|
|
|
/**
|
|
* Constants
|
|
*/
|
|
|
|
const {isDev, modulesPath, projectPath, developmentBlockName} = getConfigs();
|
|
const blocksRegistry = isDev ? 'http://localhost:3020' : PRODUCTION_REGISTRY_URL;
|
|
const DevToolToken = 'D9lgz0TvzXCnp0xnwVBL109DaAR6Puk6F7YewDhgmP8='; // Temporary token for development purposes.
|
|
|
|
const dataFiles = await getDataFiles(projectPath);
|
|
const DEFAULT_VIEW_LAYOUT = 'alignfull';
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
|