- 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, | ||||
log_warn, | log_warn, | ||||
log_error, | log_error, | ||||
bench_path | |||||
} = require("./utils"); | } = require("./utils"); | ||||
let { get_redis_subscriber } = require("../node_utils"); | |||||
let argv = yargs | let argv = yargs | ||||
.usage("Usage: node esbuild [options]") | .usage("Usage: node esbuild [options]") | ||||
@@ -173,11 +175,19 @@ function build_files({ files, outdir }) { | |||||
watch: WATCH_MODE | watch: WATCH_MODE | ||||
? { | ? { | ||||
onRebuild(error, result) { | 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( | console.log( | ||||
`${new Date().toLocaleTimeString()}: Compiled changes...` | `${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(); | this.setup_user_group_listeners(); | ||||
// listen to build errors | // listen to build errors | ||||
this.setup_build_error_listener(); | |||||
this.setup_build_events(); | |||||
if (frappe.sys_defaults.email_user_password) { | if (frappe.sys_defaults.email_user_password) { | ||||
var email_list = frappe.sys_defaults.email_user_password.split(','); | 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) { | 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 Vue from "vue/dist/vue.esm.js"; | ||||
import moment from "moment/min/moment-with-locales.js"; | import moment from "moment/min/moment-with-locales.js"; | ||||
import momentTimezone from "moment-timezone/builds/moment-timezone-with-data.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"; | import Sortable from "./lib/Sortable.min.js"; | ||||
// TODO: esbuild | // TODO: esbuild | ||||
// Don't think jquery.hotkeys is being used anywhere. Will remove this after being sure. | // 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.moment = momentTimezone; | ||||
window.Vue = Vue; | window.Vue = Vue; | ||||
window.Sortable = Sortable; | window.Sortable = Sortable; | ||||
window.io = io |
@@ -762,18 +762,15 @@ def get_build_version(): | |||||
def get_assets_json(): | def get_assets_json(): | ||||
if not hasattr(frappe.local, "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: | 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): | def get_bench_relative_path(file_path): | ||||
@@ -35,6 +35,7 @@ | |||||
<div id="body"></div> | <div id="body"></div> | ||||
<footer></footer> | <footer></footer> | ||||
</div> | </div> | ||||
<div id="build-events-overlay"></div> | |||||
<script type="text/javascript"> | <script type="text/javascript"> | ||||
window._version_number = "{{ build_version }}"; | window._version_number = "{{ build_version }}"; | ||||
@@ -38,9 +38,9 @@ function get_conf() { | |||||
return conf; | return conf; | ||||
} | } | ||||
function get_redis_subscriber() { | |||||
function get_redis_subscriber(kind="redis_socketio") { | |||||
const conf = get_conf(); | 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); | return redis.createClient(host); | ||||
} | } | ||||
@@ -1,9 +1,9 @@ | |||||
{ | { | ||||
"name": "frappe-framework", | "name": "frappe-framework", | ||||
"scripts": { | "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" | "snyk-protect": "snyk protect" | ||||
}, | }, | ||||
"repository": { | "repository": { | ||||
@@ -64,6 +64,7 @@ | |||||
"fast-glob": "^3.2.5", | "fast-glob": "^3.2.5", | ||||
"graphlib": "^2.1.8", | "graphlib": "^2.1.8", | ||||
"http-proxy": "^1.18.1", | "http-proxy": "^1.18.1", | ||||
"launch-editor": "^2.2.1", | |||||
"less": "^3.11.1", | "less": "^3.11.1", | ||||
"md5": "^2.3.0", | "md5": "^2.3.0", | ||||
"rollup": "^1.2.2", | "rollup": "^1.2.2", | ||||
@@ -199,6 +199,11 @@ io.on('connection', function (socket) { | |||||
'type' | '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) { | subscriber.on("message", function (_channel, message) { | ||||
@@ -4520,6 +4520,14 @@ latest-version@^5.0.0: | |||||
dependencies: | dependencies: | ||||
package-json "^6.3.0" | 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: | lazy-cache@^1.0.3: | ||||
version "1.0.4" | version "1.0.4" | ||||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" | 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" | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" | ||||
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= | 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: | should-equal@^2.0.0: | ||||
version "2.0.0" | version "2.0.0" | ||||
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" | resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" | ||||