cro-development-tool
Version:
A tool to quickly developer CRO tests with your favorite editor, using live reload, with SCSS and ES6 support
403 lines (402 loc) • 15.6 kB
JavaScript
/**
* This file will start a watcher on the /klanten/ folder
* when a change has been found in a file, it will try to compile the assats with babel
* and create a bundled file
*
* use the chrome plugin for hot reloading current page with css and js inject
*
* @author Jonas van Ineveld
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import chokidar from 'chokidar';
import { rootDir, getFile, getInfoFromPath, ensureExists, stripWorkDir, getProjectFolder, currentConfig } from './helpers.js';
import { rollup } from 'rollup';
import { string } from 'rollup-plugin-string';
import modify from 'rollup-plugin-modify';
import { babel } from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import sass from 'node-sass';
import nodeResolve from 'rollup-plugin-node-resolve';
import fs from 'fs';
import path from 'path';
import moment from 'moment';
import autoprefixer from 'autoprefixer';
import postcss from 'postcss';
// const cliSpinners = require('cli-spinners');
import ora from 'ora';
import chalk from 'chalk';
import { liveReload } from './live-reload-server.js';
import { terser } from 'rollup-plugin-terser';
import prettier from 'rollup-plugin-prettier';
import json from 'rollup-plugin-json';
// const {rollupExtractConfig} = require('./rollup-extract-config');
const defaultBrowserTarget = currentConfig.browserTarget;
const babelOptions = (env = 'dev', babelOptions) => {
let config = {
// 'runtimeHelpers': true,
babelHelpers: 'runtime',
'presets': [
['@babel/preset-env', {
'targets': {
browsers: babelOptions.browserTarget ? babelOptions.browserTarget : defaultBrowserTarget,
},
'modules': false,
}]
],
exclude: 'node_modules/**',
plugins: [
['transform-class-properties', { spec: false }],
['@babel/plugin-transform-runtime', {
'regenerator': true
}]
]
};
if (env === 'prod') {
config.plugins.push(['transform-remove-console', { 'exclude': ['error', 'warn'] }]);
}
return config;
};
const bundleOptions = (env = 'dev', bundleOptions) => {
const bundlerModules = [
babel(babelOptions(env, bundleOptions.babelOptions)),
nodeResolve(),
commonjs(),
string({
include: '**/*.html',
}),
json(), // support for including .json files as import
// rollupExtractConfig()
];
if (env === 'dev') {
bundlerModules.push(prettier({
parser: 'babel',
useTabs: true,
singleQuote: true,
}));
}
if (env === 'prod') {
bundlerModules.push(modify({
find: '\\t',
replace: ''
}));
if (!bundleOptions.preventMinify) {
bundlerModules.push(terser({
mangle: false
})); // minify and uglify the code so it's smaller
}
}
return bundlerModules;
};
// to keep track of last generated resources to push to websocket,
// we store it here. So this is for when compiling JS, also sending the latest css with it
const lastCompiledResources = {
css: '',
js: ''
};
const getResource = function (path) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (fs.existsSync(path)) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', function (err, contents) {
if (err) {
reject(err);
return;
}
resolve(contents);
});
}).catch(err => {
console.error(err);
});
}
}
catch (err) {
console.error(err);
}
return false;
});
};
const requestTestFiles = function (test) {
return __awaiter(this, void 0, void 0, function* () {
let testDir = path.join(rootDir, currentConfig.rootDir, test.customer, test.test, (test.variation ? test.variation : '')), cssDevPath = path.join(testDir, 'generated', 'dev', 'output.css'), jsDevPath = path.join(testDir, 'generated', 'dev', 'output.js');
// console.log({test, testDir, cssDevPath, jsDevPath})
let css = yield getResource(cssDevPath), js = yield getResource(jsDevPath);
return { css, js };
});
};
const reloadSocket = new liveReload(requestTestFiles);
const buildBundle = function (targetPath, type = 'dev', extraSettings) {
return __awaiter(this, void 0, void 0, function* () {
let bundleSetting = {
babelOptions: {}
};
if (extraSettings) {
if (extraSettings.browserList) {
// shoudl do shings
bundleSetting.babelOptions.browserTarget = extraSettings.browserList;
}
if (extraSettings.parser) {
// shoudl do shings
bundleSetting.babelOptions.longParse = true;
}
if (extraSettings.nominify) {
bundleSetting.preventMinify = true;
}
}
let bundlePlugins = bundleOptions(type, bundleSetting);
let bundle = yield rollup({
input: targetPath,
plugins: bundlePlugins,
output: {
intro: '/* Compiled: ' + moment().format('DD-MM-YYYY HH:mm:ss') + ' */'
}
});
let projectFolder = targetPath.substring(0, targetPath.lastIndexOf('/')), bundleWriteOutputOptions = {
file: path.join(projectFolder, 'generated', type, 'output.js'),
format: 'iife',
intro: `
/**
* CRO Development
* Compiled: ` + moment().format('DD-MM-YYYY HH:mm:ss') + `
**/
`
}, bundleWriteOptions = {
sourceMap: 'inline',
strict: false,
'runtimeHelpers': false,
plugins: bundlePlugins,
output: bundleWriteOutputOptions
};
switch (type) {
case 'dev':
bundleWriteOptions.sourceMap = 'inline';
break;
case 'prod':
break;
}
let output = yield bundle.write(bundleWriteOutputOptions);
if (output.output[0].code) {
let codeWithComment = output.output[0].code.replace(/\n$/g, '') + '\n/* Compiled: ' + moment().format('DD-MM-YYYY HH:mm:ss') + ' */';
fs.writeFileSync(bundleWriteOptions.output.file, codeWithComment);
}
return output;
});
};
const compilationSuccess = function (file, type) {
let time = moment().format('HH:mm:ss');
console.log(chalk.white(chalk.bgGreenBright.black(' Nice! ') + ' ' + chalk.bgWhite.black(' ' + time + ' ') + ' ' + chalk.bgWhite.black(' ' + type + ' ') + ' ' + stripWorkDir(file)));
};
const buildHTMLfile = (changedFilePath) => {
let projectFolder = getProjectFolder(changedFilePath);
const devJs = getFile(path.join(projectFolder, 'generated', 'dev', 'output.js')), devStyle = getFile(path.join(projectFolder, 'generated', 'dev', 'output.css')), prodJs = getFile(path.join(projectFolder, 'generated', 'prod', 'output.js')), prodStyle = getFile(path.join(projectFolder, 'generated', 'prod', 'output.css')), devOutFile = path.join(projectFolder, 'generated', 'dev', 'output.html'), prodOutFile = path.join(projectFolder, 'generated', 'prod', 'output.html');
let prodCode = '', devCode = '';
if (devStyle) {
devCode += `<style>
${devStyle}
</style>`;
}
if (devJs) {
devCode += `
<script>
${devJs}
</script>`;
}
if (devStyle) {
prodCode += `<style>
${prodStyle}
</style>`;
}
if (devJs) {
prodCode += `
<script>
${prodJs}
</script>
`;
}
fs.writeFile(devOutFile, devCode, function (error) {
if (error) {
console.log('error in html file generation:', error);
}
});
fs.writeFile(prodOutFile, prodCode, function (error) {
if (error) {
console.log('error in html file generation:', error);
}
});
};
const buildSass = (targetPath, type = 'dev') => __awaiter(void 0, void 0, void 0, function* () {
let projectFolder = getProjectFolder(targetPath), buildOptions = {
file: path.join(projectFolder, 'index.scss'),
outFile: null,
outputStyle: 'compact',
sourceMap: false
};
if (!targetPath.includes('.scss', '.sass')) {
return;
}
const spinner = ora({ text: 'CSS ' + type, interval: 10 }).start();
switch (type) {
case 'dev':
buildOptions.outFile = path.join(projectFolder, 'generated', 'dev', 'output.css');
buildOptions.outputStyle = 'compact';
buildOptions.sourceMap = false;
break;
case 'prod':
buildOptions.outFile = path.join(projectFolder, 'generated', 'prod', 'output.css');
buildOptions.outputStyle = 'compact';
break;
}
let buildFolder = buildOptions.outFile.substring(0, buildOptions.outFile.lastIndexOf('/'));
yield new Promise((res, rej) => {
ensureExists(buildFolder, false, err => err ? rej(err) : res(true));
});
let cssResult = null;
let result = yield new Promise((res, rej) => {
try {
// console.log('shloud be', buildOptions)
sass.render(buildOptions, (error, result) => {
// console.log(error, result);
if (!error) {
try {
cssResult = result.css.toString().replace(/^\n/gm, '');
if (type === 'prod') {
postcss([autoprefixer({ overrideBrowserslist: 'last 3 versions' })]).process(cssResult, { map: false, from: undefined }).then(result => {
result.warnings().forEach(warn => {
console.warn(warn.toString());
});
// No errors during the compilation, write this result on the disk
fs.writeFile(buildOptions.outFile, result.css, function (err) {
if (err) {
rej(err);
}
res(true);
});
});
}
else {
// No errors during the compilation, write this result on the disk+
fs.writeFile(buildOptions.outFile, result.css.toString().replace(/^\n/gm, ''), function (err) {
if (err) {
rej(err);
}
if (!result.map) {
res(true);
}
});
if (result.map) {
fs.writeFile(buildOptions.outFile + '.map', result.map, function (err) {
if (err) {
rej(err);
}
res(true);
});
}
}
}
catch (error) {
console.error(error);
// expected output: ReferenceError: nonExistentFunction is not defined
// Note - error messages will vary depending on browser
}
}
else {
console.log('\nFile: ' + error.file);
console.log('Line: ' + error.line + ':' + error.column + ' :: ' + error.message);
}
});
}
catch (error) {
console.error(error);
rej(error);
}
spinner.stop();
});
if (result) {
spinner.stop();
compilationSuccess(buildOptions.outFile, 'css');
lastCompiledResources.css = cssResult.toString();
let testinfo = yield getInfoFromPath(targetPath);
// console.log('sending path info', testinfo)
reloadSocket.sendCSSUpdate(lastCompiledResources.css, testinfo);
}
spinner.stop();
});
const buildJavascript = (targetPath) => __awaiter(void 0, void 0, void 0, function* () {
if (!targetPath.includes('.js')) {
return;
}
const spinner = ora({ text: 'Bundeling & transpiling JS prod', interval: 10 }).start();
let testinfo = yield getInfoFromPath(targetPath);
try {
yield buildBundle(targetPath, 'prod', testinfo.js.headers).catch(err => {
console.error(err);
throw new Error('Error transpiling');
});
// console.log('result', bundleresult)
}
catch (error) {
spinner.stop();
console.log('\n\n' + error);
return;
}
spinner.text = 'Bundeling & transpiling JS dev';
let devJSBundle = {
output: []
};
try {
devJSBundle = yield buildBundle(targetPath, 'dev', testinfo.js.headers).catch(err => {
console.error(err);
throw new Error('Error transpiling ');
});
}
catch (error) {
spinner.stop();
console.log('\n\n' + error);
return;
}
lastCompiledResources.js = devJSBundle.output[0].code;
spinner.stop();
compilationSuccess(targetPath, 'js');
// console.log('testinfo',testinfo)
reloadSocket.sendJSUpdate(lastCompiledResources, testinfo);
});
const fileChanged = (path) => __awaiter(void 0, void 0, void 0, function* () {
if (path.includes('index.js')) {
yield buildJavascript(path);
buildHTMLfile(path);
}
if (path.includes('.scss')) {
yield buildSass(path, 'prod');
yield buildSass(path, 'dev');
buildHTMLfile(path);
}
});
const listenToFileChanges = function () {
const chokidarSettings = {
ignored: [/.*\/generated\/.*/],
persistent: true,
ignoreInitial: false,
alwaysStat: false,
};
const changeWatcher = chokidar.watch(path.join(rootDir, currentConfig.rootDir), chokidarSettings);
changeWatcher.on('change', fileChanged);
};
const initTool = function () {
console.log('/** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
console.log(' * CRO Development 4 life ♡');
listenToFileChanges(); // Use Chokadir to check for filechanges
console.log(' * All projects are being watched. Have fun!');
console.log(' * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */');
console.log('\n');
};
initTool();
/** Stop process from beeing terminated */
process.stdin.resume();