- Update assets_json directly from node - Show error overlay or success message - Open file in editor from error overlayversion-14
@@ -20,7 +20,9 @@ let { | |||
log, | |||
log_warn, | |||
log_error, | |||
bench_path | |||
} = require("./utils"); | |||
let { get_redis_subscriber } = require("../node_utils"); | |||
let argv = yargs | |||
.usage("Usage: node esbuild [options]") | |||
@@ -173,11 +175,19 @@ function build_files({ files, outdir }) { | |||
watch: WATCH_MODE | |||
? { | |||
onRebuild(error, result) { | |||
if (error) console.error("watch build failed:", error); | |||
else { | |||
if (error) { | |||
log_error( | |||
"There was an error during rebuilding changes." | |||
); | |||
log(); | |||
log(chalk.dim(error.stack)); | |||
notify_redis({ error }); | |||
} else { | |||
console.log( | |||
`${new Date().toLocaleTimeString()}: Compiled changes...` | |||
); | |||
write_meta_file(result.metafile); | |||
notify_redis({ success: true }); | |||
} | |||
} | |||
} | |||
@@ -268,8 +278,64 @@ function write_meta_file(metafile) { | |||
} | |||
} | |||
return fs.promises.writeFile( | |||
path.resolve(assets_path, "frappe", "dist", "assets.json"), | |||
JSON.stringify(out, null, 4) | |||
let assets_json = JSON.stringify(out, null, 4); | |||
return fs.promises | |||
.writeFile( | |||
path.resolve(assets_path, "frappe", "dist", "assets.json"), | |||
assets_json | |||
) | |||
.then(() => { | |||
let client = get_redis_subscriber("redis_cache"); | |||
// update assets_json cache in redis, so that it can be read directly by python | |||
return client.set("assets_json", assets_json); | |||
}); | |||
} | |||
async function notify_redis({ error, success }) { | |||
let subscriber = get_redis_subscriber("redis_socketio"); | |||
// notify redis which in turns tells socketio to publish this to browser | |||
let payload = null; | |||
if (error) { | |||
let formatted = await esbuild.formatMessages(error.errors, { | |||
kind: "error", | |||
terminalWidth: 100 | |||
}); | |||
let stack = error.stack.replace(new RegExp(bench_path, "g"), ""); | |||
payload = { | |||
error, | |||
formatted, | |||
stack | |||
}; | |||
} | |||
if (success) { | |||
payload = { | |||
success: true | |||
}; | |||
} | |||
subscriber.publish( | |||
"events", | |||
JSON.stringify({ | |||
event: "build_event", | |||
message: payload | |||
}) | |||
); | |||
} | |||
function open_in_editor() { | |||
let subscriber = get_redis_subscriber("redis_socketio"); | |||
subscriber.on("message", (event, file) => { | |||
if (event === "open_in_editor") { | |||
file = JSON.parse(file); | |||
let file_path = path.resolve(file.file); | |||
console.log("Opening file in editor:", file_path); | |||
let launch = require("launch-editor"); | |||
launch(`${file_path}:${file.line}:${file.column}`); | |||
} | |||
}); | |||
subscriber.subscribe("open_in_editor"); | |||
} | |||
open_in_editor(); |
@@ -0,0 +1,111 @@ | |||
<template> | |||
<div class="build-error-overlay" @click.self="data = null" v-show="data"> | |||
<div class="window" v-if="data"> | |||
<div v-for="(error, i) in data.formatted" :key="i"> | |||
<!-- prettier-ignore --> | |||
<pre class="frame"><component :is="error_component(error, i)" /></pre> | |||
</div> | |||
<pre class="stack">{{ data.stack }}</pre> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
export default { | |||
name: "BuildError", | |||
data() { | |||
return { | |||
data: null | |||
}; | |||
}, | |||
methods: { | |||
show(data) { | |||
this.data = data; | |||
}, | |||
hide() { | |||
this.data = null; | |||
}, | |||
open_in_editor(location) { | |||
frappe.socketio.socket.emit("open_in_editor", location); | |||
}, | |||
error_component(error, i) { | |||
let location = this.data.error.errors[i].location; | |||
let location_string = `${location.file}:${location.line}:${ | |||
location.column | |||
}`; | |||
let template = error.replace( | |||
" > " + location_string, | |||
` > <a class="file-link" @click="open">${location_string}</a>` | |||
); | |||
return { | |||
template: `<div>${template}</div>`, | |||
methods: { | |||
open() { | |||
frappe.socketio.socket.emit("open_in_editor", location); | |||
} | |||
} | |||
}; | |||
} | |||
} | |||
}; | |||
</script> | |||
<style> | |||
.build-error-overlay { | |||
position: fixed; | |||
top: 0; | |||
left: 0; | |||
width: 100%; | |||
height: 100%; | |||
z-index: 9999; | |||
margin: 0; | |||
background: rgba(0, 0, 0, 0.66); | |||
--monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, | |||
monospace; | |||
--dim: var(--gray-400); | |||
} | |||
.window { | |||
font-family: var(--monospace); | |||
line-height: 1.5; | |||
width: 800px; | |||
color: #d8d8d8; | |||
margin: 30px auto; | |||
padding: 25px 40px; | |||
position: relative; | |||
background: #181818; | |||
border-radius: 6px 6px 8px 8px; | |||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); | |||
overflow: hidden; | |||
border-top: 8px solid var(--red); | |||
} | |||
pre { | |||
font-family: var(--monospace); | |||
font-size: 13px; | |||
margin-top: 0; | |||
margin-bottom: 1em; | |||
overflow-x: auto; | |||
scrollbar-width: none; | |||
} | |||
code { | |||
font-size: 13px; | |||
font-family: var(--monospace); | |||
color: var(--yellow); | |||
} | |||
.message { | |||
line-height: 1.3; | |||
font-weight: 600; | |||
white-space: pre-wrap; | |||
} | |||
.frame { | |||
color: var(--yellow); | |||
} | |||
.stack { | |||
font-size: 13px; | |||
color: var(--dim); | |||
} | |||
.file-link { | |||
text-decoration: underline !important; | |||
cursor: pointer; | |||
} | |||
</style> |
@@ -0,0 +1,52 @@ | |||
<template> | |||
<div | |||
v-if="is_shown" | |||
class="flex justify-between build-success-message align-center" | |||
> | |||
<div class="mr-4">Compiled successfully</div> | |||
<a class="text-white underline" href="/" @click.prevent="reload"> | |||
Refresh | |||
</a> | |||
</div> | |||
</template> | |||
<script> | |||
export default { | |||
name: "BuildSuccess", | |||
data() { | |||
return { | |||
is_shown: false | |||
}; | |||
}, | |||
methods: { | |||
show() { | |||
this.is_shown = true; | |||
if (this.timeout) { | |||
clearTimeout(this.timeout); | |||
} | |||
this.timeout = setTimeout(() => { | |||
this.hide(); | |||
}, 10000); | |||
}, | |||
hide() { | |||
this.is_shown = false; | |||
}, | |||
reload() { | |||
window.location.reload(); | |||
} | |||
} | |||
}; | |||
</script> | |||
<style> | |||
.build-success-message { | |||
position: fixed; | |||
z-index: 9999; | |||
bottom: 0; | |||
right: 0; | |||
background: rgba(0, 0, 0, 0.6); | |||
border-radius: var(--border-radius); | |||
padding: 0.5rem 1rem; | |||
color: white; | |||
font-weight: 500; | |||
margin: 1rem; | |||
} | |||
</style> |
@@ -0,0 +1,48 @@ | |||
import BuildError from "./BuildError.vue"; | |||
import BuildSuccess from "./BuildSuccess.vue"; | |||
let $container = $("#build-events-overlay"); | |||
let success = null; | |||
let error = null; | |||
frappe.realtime.on("build_event", data => { | |||
if (data.success) { | |||
show_build_success(data); | |||
} else if (data.error) { | |||
show_build_error(data); | |||
} | |||
}); | |||
function show_build_success() { | |||
if (error) { | |||
error.hide(); | |||
} | |||
if (!success) { | |||
let target = $('<div class="build-success-container">') | |||
.appendTo($container) | |||
.get(0); | |||
let vm = new Vue({ | |||
el: target, | |||
render: h => h(BuildSuccess) | |||
}); | |||
success = vm.$children[0]; | |||
} | |||
success.show(); | |||
} | |||
function show_build_error(data) { | |||
if (success) { | |||
success.hide(); | |||
} | |||
if (!error) { | |||
let target = $('<div class="build-error-container">') | |||
.appendTo($container) | |||
.get(0); | |||
let vm = new Vue({ | |||
el: target, | |||
render: h => h(BuildError) | |||
}); | |||
error = vm.$children[0]; | |||
} | |||
error.show(data); | |||
} |
@@ -117,7 +117,7 @@ frappe.Application = class Application { | |||
this.setup_user_group_listeners(); | |||
// listen to build errors | |||
this.setup_build_error_listener(); | |||
this.setup_build_events(); | |||
if (frappe.sys_defaults.email_user_password) { | |||
var email_list = frappe.sys_defaults.email_user_password.split(','); | |||
@@ -585,11 +585,9 @@ frappe.Application = class Application { | |||
} | |||
} | |||
setup_build_error_listener() { | |||
setup_build_events() { | |||
if (frappe.boot.developer_mode) { | |||
frappe.realtime.on('build_error', (data) => { | |||
console.log(data); | |||
}); | |||
frappe.require("build_events.bundle.js"); | |||
} | |||
} | |||
@@ -2,7 +2,7 @@ import "./jquery-bootstrap"; | |||
import Vue from "vue/dist/vue.esm.js"; | |||
import moment from "moment/min/moment-with-locales.js"; | |||
import momentTimezone from "moment-timezone/builds/moment-timezone-with-data.js"; | |||
import "socket.io-client/dist/socket.io.slim.js"; | |||
import io from "socket.io-client/dist/socket.io.slim.js"; | |||
import Sortable from "./lib/Sortable.min.js"; | |||
// TODO: esbuild | |||
// Don't think jquery.hotkeys is being used anywhere. Will remove this after being sure. | |||
@@ -12,3 +12,4 @@ import Sortable from "./lib/Sortable.min.js"; | |||
window.moment = momentTimezone; | |||
window.Vue = Vue; | |||
window.Sortable = Sortable; | |||
window.io = io |
@@ -762,18 +762,15 @@ def get_build_version(): | |||
def get_assets_json(): | |||
if not hasattr(frappe.local, "assets_json"): | |||
assets_json = frappe.cache().get_value("assets_json", shared=True) | |||
cache = frappe.cache() | |||
# using .get instead of .get_value to avoid pickle.loads | |||
assets_json = cache.get("assets_json") | |||
if not assets_json: | |||
import json | |||
assets_json = json.loads( | |||
frappe.read_file("assets/frappe/dist/assets.json") | |||
) | |||
frappe.cache().set_value("assets_json", assets_json, shared=True) | |||
frappe.local.assets_json = assets_json | |||
assets_json = frappe.read_file("assets/frappe/dist/assets.json") | |||
cache.set_value("assets_json", assets_json, shared=True) | |||
frappe.local.assets_json = frappe.safe_decode(assets_json) | |||
return frappe.local.assets_json | |||
return frappe.parse_json(frappe.local.assets_json) | |||
def get_bench_relative_path(file_path): | |||
@@ -35,6 +35,7 @@ | |||
<div id="body"></div> | |||
<footer></footer> | |||
</div> | |||
<div id="build-events-overlay"></div> | |||
<script type="text/javascript"> | |||
window._version_number = "{{ build_version }}"; | |||
@@ -38,9 +38,9 @@ function get_conf() { | |||
return conf; | |||
} | |||
function get_redis_subscriber() { | |||
function get_redis_subscriber(kind="redis_socketio") { | |||
const conf = get_conf(); | |||
const host = conf.redis_socketio || conf.redis_async_broker_port; | |||
const host = conf[kind] || conf.redis_async_broker_port; | |||
return redis.createClient(host); | |||
} | |||
@@ -1,9 +1,9 @@ | |||
{ | |||
"name": "frappe-framework", | |||
"scripts": { | |||
"build": "node rollup/build.js", | |||
"production": "FRAPPE_ENV=production node rollup/build.js", | |||
"watch": "node rollup/watch.js", | |||
"build": "node esbuild", | |||
"production": "node esbuild --production", | |||
"watch": "node esbuild --watch", | |||
"snyk-protect": "snyk protect" | |||
}, | |||
"repository": { | |||
@@ -64,6 +64,7 @@ | |||
"fast-glob": "^3.2.5", | |||
"graphlib": "^2.1.8", | |||
"http-proxy": "^1.18.1", | |||
"launch-editor": "^2.2.1", | |||
"less": "^3.11.1", | |||
"md5": "^2.3.0", | |||
"rollup": "^1.2.2", | |||
@@ -199,6 +199,11 @@ io.on('connection', function (socket) { | |||
'type' | |||
); | |||
}); | |||
socket.on('open_in_editor', (data) => { | |||
let s = get_redis_subscriber('redis_socketio'); | |||
s.publish('open_in_editor', JSON.stringify(data)); | |||
}); | |||
}); | |||
subscriber.on("message", function (_channel, message) { | |||
@@ -4520,6 +4520,14 @@ latest-version@^5.0.0: | |||
dependencies: | |||
package-json "^6.3.0" | |||
launch-editor@^2.2.1: | |||
version "2.2.1" | |||
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.2.1.tgz#871b5a3ee39d6680fcc26d37930b6eeda89db0ca" | |||
integrity sha512-On+V7K2uZK6wK7x691ycSUbLD/FyKKelArkbaAMSSJU8JmqmhwN2+mnJDNINuJWSrh2L0kDk+ZQtbC/gOWUwLw== | |||
dependencies: | |||
chalk "^2.3.0" | |||
shell-quote "^1.6.1" | |||
lazy-cache@^1.0.3: | |||
version "1.0.4" | |||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" | |||
@@ -7711,6 +7719,11 @@ shebang-regex@^1.0.0: | |||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" | |||
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= | |||
shell-quote@^1.6.1: | |||
version "1.7.2" | |||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" | |||
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== | |||
should-equal@^2.0.0: | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" | |||