/*eslint-disable no-console */ const path = require('path'); const fs = require('fs'); const babel = require('babel-core'); const less = require('less'); const chokidar = require('chokidar'); const path_join = path.resolve; // for file watcher const app = require('express')(); const http = require('http').Server(app); const io = require('socket.io')(http); const touch = require("touch"); // basic setup const sites_path = path_join(__dirname, '..', '..', '..', 'sites'); const apps_path = path_join(__dirname, '..', '..', '..', 'apps'); // the apps folder const apps_contents = fs.readFileSync(path_join(sites_path, 'apps.txt'), 'utf8'); const apps = apps_contents.split('\n'); const app_paths = apps.map(app => path_join(apps_path, app, app)) // base_path of each app const assets_path = path_join(sites_path, 'assets'); let build_map = make_build_map(); let compiled_js_cache = {}; // cache each js file after it is compiled const file_watcher_port = get_conf().file_watcher_port; // command line args const action = process.argv[2] || '--build'; if (['--build', '--watch'].indexOf(action) === -1) { console.log('Invalid argument: ', action); process.exit(); } if (action === '--build') { const minify = process.argv[3] === '--minify' ? true : false; build(minify); } if (action === '--watch') { watch(); } function build(minify) { for (const output_path in build_map) { pack(output_path, build_map[output_path], minify); } touch(path_join(sites_path, '.build'), {force:true}); } let socket_connection = false; function watch() { http.listen(file_watcher_port, function () { console.log('file watching on *:', file_watcher_port); }); if (process.env.CI) { // don't watch inside CI return; } compile_less().then(() => { build(); watch_less(function (filename) { if(socket_connection) { io.emit('reload_css', filename); } }); watch_js(//function (filename) { // if(socket_connection) { // io.emit('reload_js', filename); // } //} ); watch_build_json(); }); io.on('connection', function (socket) { socket_connection = true; socket.on('disconnect', function() { socket_connection = false; }) }); } function pack(output_path, inputs, minify, file_changed) { let output_txt = ''; for (const file of inputs) { if (!fs.existsSync(file)) { console.log('File not found: ', file); continue; } let force_compile = false; if (file_changed) { // if file_changed is passed and is equal to file, force_compile it force_compile = file_changed === file; } let file_content = get_compiled_file(file, output_path, minify, force_compile); if(!minify) { output_txt += `\n/*\n *\t${file}\n */\n` } output_txt += file_content; output_txt = output_txt.replace(/['"]use strict['"];/, ''); } const target = path_join(assets_path, output_path); try { fs.writeFileSync(target, output_txt); console.log(`Wrote ${output_path} - ${get_file_size(target)}`); return target; } catch (e) { console.log('Error writing to file', output_path); console.log(e); } } function get_compiled_file(file, output_path, minify, force_compile) { const output_type = output_path.split('.').pop(); let file_content; if (force_compile === false) { // force compile is false // attempt to get from cache file_content = compiled_js_cache[file]; if (file_content) { return file_content; } } file_content = fs.readFileSync(file, 'utf-8'); if (file.endsWith('.html') && output_type === 'js') { file_content = html_to_js_template(file, file_content); } if(file.endsWith('class.js')) { file_content = minify_js(file_content, file); } if (minify && file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) { file_content = babelify(file_content, file, minify); } compiled_js_cache[file] = file_content; return file_content; } function babelify(content, path, minify) { let presets = ['env']; const plugins = ['transform-object-rest-spread'] // Minification doesn't work when loading Frappe Desk // Avoid for now, trace the error and come back. try { return babel.transform(content, { presets: presets, plugins: plugins, comments: false }).code; } catch (e) { console.log('Cannot babelify', path); console.log(e); return content; } } function minify_js(content, path) { try { return babel.transform(content, { comments: false }).code; } catch (e) { console.log('Cannot minify', path); console.log(e); return content; } } function make_build_map() { const build_map = {}; for (const app_path of app_paths) { const build_json_path = path_join(app_path, 'public', 'build.json'); if (!fs.existsSync(build_json_path)) continue; let build_json = fs.readFileSync(build_json_path); try { build_json = JSON.parse(build_json); } catch (e) { console.log(e); continue; } for (const target in build_json) { const sources = build_json[target]; const new_sources = []; for (const source of sources) { const s = path_join(app_path, source); new_sources.push(s); } if (new_sources.length) build_json[target] = new_sources; else delete build_json[target]; } Object.assign(build_map, build_json); } return build_map; } function compile_less() { return new Promise(function (resolve) { const promises = []; for (const app_path of app_paths) { const public_path = path_join(app_path, 'public'); const less_path = path_join(public_path, 'less'); if (!fs.existsSync(less_path)) continue; const files = fs.readdirSync(less_path); for (const file of files) { if(file.includes('variables.less')) continue; promises.push(compile_less_file(file, less_path, public_path)) } } Promise.all(promises).then(() => { console.log('Less files compiled'); resolve(); }); }); } function compile_less_file(file, less_path, public_path) { const file_content = fs.readFileSync(path_join(less_path, file), 'utf8'); const output_file = file.split('.')[0] + '.css'; console.log('compiling', file); return less.render(file_content, { paths: [less_path], filename: file, sourceMap: false }).then(output => { const out_css = path_join(public_path, 'css', output_file); fs.writeFileSync(out_css, output.css); return out_css; }).catch(e => { console.log('Error compiling ', file); console.log(e); }); } function watch_less(ondirty) { const less_paths = app_paths.map(path => path_join(path, 'public', 'less')); const to_watch = filter_valid_paths(less_paths); chokidar.watch(to_watch).on('change', (filename) => { console.log(filename, 'dirty'); var last_index = filename.lastIndexOf('/'); const less_path = filename.slice(0, last_index); const public_path = path_join(less_path, '..'); filename = filename.split('/').pop(); compile_less_file(filename, less_path, public_path) .then(css_file_path => { // build the target css file for which this css file is input for (const target in build_map) { const sources = build_map[target]; if (sources.includes(css_file_path)) { pack(target, sources); ondirty && ondirty(target); break; } } }); touch(path_join(sites_path, '.build'), {force:true}); }); } function watch_js(ondirty) { chokidar.watch([ path_join(apps_path, '**', '*.js'), path_join(apps_path, '**', '*.html') ]).on('change', (filename) => { // build the target js file for which this js/html file is input for (const target in build_map) { const sources = build_map[target]; if (sources.includes(filename)) { console.log(filename, 'dirty'); pack(target, sources, null, filename); ondirty && ondirty(target); // break; } } touch(path_join(sites_path, '.build'), {force:true}); }); } function watch_build_json() { const build_json_paths = app_paths.map(path => path_join(path, 'public', 'build.json')); const to_watch = filter_valid_paths(build_json_paths); chokidar.watch(to_watch).on('change', (filename) => { console.log(filename, 'updated'); build_map = make_build_map(); }); } function filter_valid_paths(paths) { return paths.filter(path => fs.existsSync(path)); } function html_to_js_template(path, content) { let key = path.split('/'); key = key[key.length - 1]; key = key.split('.')[0]; content = scrub_html_template(content); return `frappe.templates['${key}'] = '${content}';\n`; } function scrub_html_template(content) { content = content.replace(/\s/g, ' '); content = content.replace(/()/g, ''); return content.replace("'", "\'"); } function get_file_size(filepath) { const stats = fs.statSync(filepath); const size = stats.size; // convert it to humanly readable format. const i = Math.floor(Math.log(size) / Math.log(1024)); return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i]; } function get_conf() { // defaults var conf = { file_watcher_port: 6787 }; var read_config = function(path) { if (!fs.existsSync(path)) return; var bench_config = JSON.parse(fs.readFileSync(path)); for (var key in bench_config) { if (bench_config[key]) { conf[key] = bench_config[key]; } } } read_config(path_join(sites_path, 'common_site_config.json')); return conf; }