mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: display error overlay (#607)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
@@ -8,7 +8,9 @@ use crate::app::App;
|
|||||||
use crate::build;
|
use crate::build;
|
||||||
use crate::mode::Mode;
|
use crate::mode::Mode;
|
||||||
use crate::scaffold_project;
|
use crate::scaffold_project;
|
||||||
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
use crate::source_builder::{
|
||||||
|
bundle_axum_source, check_tuono_folder, create_client_entry_files, generate_fallback_html,
|
||||||
|
};
|
||||||
use crate::watch;
|
use crate::watch;
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -82,6 +84,7 @@ pub fn app() -> std::io::Result<()> {
|
|||||||
app.build_tuono_config()
|
app.build_tuono_config()
|
||||||
.expect("Failed to build tuono.config.ts");
|
.expect("Failed to build tuono.config.ts");
|
||||||
|
|
||||||
|
generate_fallback_html(&app)?;
|
||||||
app.check_server_availability(Mode::Dev);
|
app.check_server_availability(Mode::Dev);
|
||||||
|
|
||||||
watch::watch().unwrap();
|
watch::watch().unwrap();
|
||||||
|
|||||||
@@ -5,13 +5,32 @@ use std::io::prelude::*;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use clap::crate_version;
|
use clap::crate_version;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::mode::Mode;
|
use crate::mode::Mode;
|
||||||
use crate::route::AxumInfo;
|
use crate::route::AxumInfo;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
|
|
||||||
pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
|
const FALLBACK_HTML: &str = r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window['__TUONO_SERVER_PAYLOAD__'] = [SERVER_PAYLOAD]
|
||||||
|
</script>
|
||||||
|
<script type="module" async>
|
||||||
|
import RefreshRuntime from '[BASE_URL]/vite-server/@react-refresh'
|
||||||
|
RefreshRuntime.injectIntoGlobalHook(window)
|
||||||
|
window.$RefreshReg$ = () => { }
|
||||||
|
window.$RefreshSig$ = () => (type) => type
|
||||||
|
window.__vite_plugin_react_preamble_installed__ = true
|
||||||
|
</script>
|
||||||
|
<script type="module" async src="[BASE_URL]/vite-server/@vite/client"></script>
|
||||||
|
<script type="module" async src="[BASE_URL]/vite-server/client-main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
|
||||||
// Do not manually update this file
|
// Do not manually update this file
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from './routeTree.gen'
|
||||||
import { serverSideRendering } from 'tuono/ssr'
|
import { serverSideRendering } from 'tuono/ssr'
|
||||||
@@ -19,7 +38,7 @@ import { serverSideRendering } from 'tuono/ssr'
|
|||||||
export const renderFn = serverSideRendering(routeTree)
|
export const renderFn = serverSideRendering(routeTree)
|
||||||
";
|
";
|
||||||
|
|
||||||
pub const CLIENT_ENTRY_DATA: &str = "// File automatically generated by tuono
|
const CLIENT_ENTRY_DATA: &str = "// File automatically generated by tuono
|
||||||
// Do not manually update this file
|
// Do not manually update this file
|
||||||
import 'vite/modulepreload-polyfill'
|
import 'vite/modulepreload-polyfill'
|
||||||
import { hydrate } from 'tuono/hydration'
|
import { hydrate } from 'tuono/hydration'
|
||||||
@@ -31,7 +50,7 @@ import { routeTree } from './routeTree.gen'
|
|||||||
hydrate(routeTree)
|
hydrate(routeTree)
|
||||||
";
|
";
|
||||||
|
|
||||||
pub const AXUM_ENTRY_POINT: &str = r##"
|
const AXUM_ENTRY_POINT: &str = r##"
|
||||||
// File automatically generated
|
// File automatically generated
|
||||||
// Do not manually change it
|
// Do not manually change it
|
||||||
|
|
||||||
@@ -184,6 +203,33 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
|||||||
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_html_fallback(app: &App) -> String {
|
||||||
|
if let Some(config) = app.config.as_ref() {
|
||||||
|
if let Some(origin) = &config.server.origin {
|
||||||
|
FALLBACK_HTML.replace("[BASE_URL]", origin)
|
||||||
|
} else {
|
||||||
|
let url = format!("http://{}:{}", config.server.host, config.server.port);
|
||||||
|
FALLBACK_HTML.replace("[BASE_URL]", url.as_str())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_fallback_html(app: &App) -> io::Result<()> {
|
||||||
|
let base_path = std::env::current_dir().unwrap_or_else(|_| {
|
||||||
|
error!("Failed to get current directory");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let mut data_file = fs::File::create(base_path.join(".tuono/index.html"))?;
|
||||||
|
|
||||||
|
let fallback_html = create_html_fallback(app);
|
||||||
|
|
||||||
|
data_file.write_all(fallback_html.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_tuono_folder() -> io::Result<()> {
|
pub fn check_tuono_folder() -> io::Result<()> {
|
||||||
let dev_folder = Path::new(DEV_FOLDER);
|
let dev_folder = Path::new(DEV_FOLDER);
|
||||||
if !&dev_folder.is_dir() {
|
if !&dev_folder.is_dir() {
|
||||||
@@ -244,4 +290,16 @@ mod tests {
|
|||||||
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
||||||
assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_create_fallback_html_with_default_config() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.config = Some(Default::default());
|
||||||
|
|
||||||
|
let fallback_html = create_html_fallback(&app);
|
||||||
|
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/@react-refresh"));
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/@vite/client"));
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/client-main.tsx"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ pub struct ServerConfig {
|
|||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
ServerConfig {
|
||||||
|
host: "localhost".to_string(),
|
||||||
|
origin: None,
|
||||||
|
port: 3000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
}
|
}
|
||||||
@@ -23,3 +33,17 @@ impl Config {
|
|||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_default() {
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
assert_eq!(config.server.host, "localhost".to_string());
|
||||||
|
assert_eq!(config.server.origin, None);
|
||||||
|
assert_eq!(config.server.port, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-10
@@ -36,10 +36,10 @@ struct ProdJs;
|
|||||||
impl ProdJs {
|
impl ProdJs {
|
||||||
thread_local! {
|
thread_local! {
|
||||||
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
||||||
Ssr::from(
|
Ssr::from(
|
||||||
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
|
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
|
||||||
).unwrap()
|
).unwrap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,11 +47,22 @@ struct DevJs;
|
|||||||
|
|
||||||
impl DevJs {
|
impl DevJs {
|
||||||
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
|
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
|
||||||
Ssr::from(
|
let bundle_path = read_to_string(PathBuf::from(DEV_BUNDLE_PATH));
|
||||||
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
|
|
||||||
"",
|
if let Ok(source) = bundle_path {
|
||||||
)
|
let ssr = Ssr::from(source, "");
|
||||||
.unwrap()
|
if let Ok(mut ssr) = ssr {
|
||||||
.render_to_string(params)
|
ssr.render_to_string(params)
|
||||||
|
} else {
|
||||||
|
let fallback_html = read_to_string(PathBuf::from("./.tuono/index.html"))
|
||||||
|
.unwrap_or("Fallback HTML not loaded".to_string());
|
||||||
|
Ok(fallback_html.replace("[SERVER_PAYLOAD]", params.unwrap_or("")))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let fallback_html = read_to_string(PathBuf::from("./.tuono/index.html"))
|
||||||
|
.unwrap_or("Fallback HTML not loaded".to_string());
|
||||||
|
|
||||||
|
Ok(fallback_html.replace("[SERVER_PAYLOAD]", params.unwrap_or("")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* Most of this file is derived from the Vite project.
|
||||||
|
* We need to re-export it to implement the required changes for Tuono.
|
||||||
|
*
|
||||||
|
* Source: https://github.com/vitejs/vite/blob/2c51565ec044904a080ef5649034c37f02212c7b/packages/vite/src/client/overlay.ts#L209
|
||||||
|
* License: https://github.com/vitejs/vite/blob/main/LICENSE
|
||||||
|
*
|
||||||
|
* @see https://github.com/tuono-labs/tuono/pull/607
|
||||||
|
* @see https://github.com/vitejs/vite/issues/19552
|
||||||
|
*/
|
||||||
|
import type { ErrorPayload, Plugin } from 'vite'
|
||||||
|
|
||||||
|
// Set the `:host` styles to ensure that Playwright can detect the element as visible
|
||||||
|
const templateStyle = /*css*/ `
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 99999;
|
||||||
|
--monospace: 'SFMono-Regular', Consolas,
|
||||||
|
'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
--red: #ff5555;
|
||||||
|
--yellow: #e2aa53;
|
||||||
|
--purple: #cfa4ff;
|
||||||
|
--cyan: #2dd9da;
|
||||||
|
--dim: #c9c9c9;
|
||||||
|
|
||||||
|
--window-background: #181818;
|
||||||
|
--window-color: #d8d8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window {
|
||||||
|
font-family: var(--monospace);
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 80vw;
|
||||||
|
color: var(--window-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 2.5vh 4vw;
|
||||||
|
position: relative;
|
||||||
|
background: var(--window-background);
|
||||||
|
border-radius: 6px 6px 8px 8px;
|
||||||
|
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 8px solid var(--red);
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: var(--monospace);
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 0;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.frame::-webkit-scrollbar {
|
||||||
|
display: block;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.frame::-webkit-scrollbar-thumb {
|
||||||
|
background: #999;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.frame {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin {
|
||||||
|
color: var(--purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
color: var(--cyan);
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
border-top: 1px dotted #999;
|
||||||
|
padding-top: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--monospace);
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: rgb(38, 40, 44);
|
||||||
|
color: rgb(166, 167, 171);
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border-width: 0.0625rem 0.0625rem 0.1875rem;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgb(54, 57, 64);
|
||||||
|
border-image: initial;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fileRE = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g
|
||||||
|
const codeframeRE = /^(?:>?\s*\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm
|
||||||
|
|
||||||
|
const overlayTemplate = `
|
||||||
|
<div class="backdrop" part="backdrop">
|
||||||
|
<div class="window" part="window">
|
||||||
|
<pre class="message" part="message">
|
||||||
|
<span class="plugin" part="plugin"></span>
|
||||||
|
<span class="message-body" part="message-body"></span>
|
||||||
|
</pre>
|
||||||
|
<pre class="file" part="file">
|
||||||
|
</pre>
|
||||||
|
<pre class="frame" part="frame"></pre>
|
||||||
|
<pre class="stack" part="stack"></pre>
|
||||||
|
<div class="tip" part="tip">Click outside, press <kbd>Esc</kbd> key, or fix the code to dismiss.</div>
|
||||||
|
</div>
|
||||||
|
<style>${templateStyle}</style>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const HTMLElement: typeof globalThis.HTMLElement =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-extraneous-class
|
||||||
|
globalThis.HTMLElement ?? class {}
|
||||||
|
|
||||||
|
export class ErrorOverlay extends HTMLElement {
|
||||||
|
root: ShadowRoot
|
||||||
|
closeOnEsc: (event: KeyboardEvent) => void
|
||||||
|
|
||||||
|
constructor(err: ErrorPayload['err'], links = true) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.root = this.attachShadow({ mode: 'open' })
|
||||||
|
const root = this.getRoot()
|
||||||
|
|
||||||
|
root.innerHTML = overlayTemplate
|
||||||
|
|
||||||
|
codeframeRE.lastIndex = 0
|
||||||
|
const hasFrame = err.frame && codeframeRE.test(err.frame)
|
||||||
|
const message = hasFrame
|
||||||
|
? err.message.replace(codeframeRE, '')
|
||||||
|
: err.message
|
||||||
|
|
||||||
|
if (err.plugin) {
|
||||||
|
this.text('.plugin', `[plugin:${err.plugin}] `)
|
||||||
|
}
|
||||||
|
this.text('.message-body', message.trim())
|
||||||
|
|
||||||
|
const [file] = (err.loc?.file || err.id || 'unknown file').split(`?`)
|
||||||
|
if (err.loc && file) {
|
||||||
|
this.text('.file', `${file}:${err.loc.line}:${err.loc.column}`, links)
|
||||||
|
} else if (err.id && file) {
|
||||||
|
this.text('.file', file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFrame && err.frame) {
|
||||||
|
this.text('.frame', err.frame.trim())
|
||||||
|
}
|
||||||
|
this.text('.stack', err.stack, links)
|
||||||
|
|
||||||
|
const rootWindowElement = root.querySelector('.window') as HTMLElement
|
||||||
|
rootWindowElement.addEventListener('click', (event: Event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.closeOnEsc = (event): void => {
|
||||||
|
if (event.key === 'Escape' || event.code === 'Escape') {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeOnEsc = this.getCloseOnEsc()
|
||||||
|
|
||||||
|
document.addEventListener('keydown', closeOnEsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoot(): ShadowRoot {
|
||||||
|
return this.root
|
||||||
|
}
|
||||||
|
|
||||||
|
getCloseOnEsc(): this['closeOnEsc'] {
|
||||||
|
return this.closeOnEsc
|
||||||
|
}
|
||||||
|
|
||||||
|
text(selector: string, text: string, linkFiles = false): void {
|
||||||
|
const root = this.getRoot()
|
||||||
|
|
||||||
|
const el = root.querySelector(selector) as HTMLElement
|
||||||
|
if (!linkFiles) {
|
||||||
|
el.textContent = text
|
||||||
|
} else {
|
||||||
|
let curIndex = 0
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
fileRE.lastIndex = 0
|
||||||
|
while ((match = fileRE.exec(text))) {
|
||||||
|
const { 0: file, index } = match
|
||||||
|
const frag = text.slice(curIndex, index)
|
||||||
|
el.appendChild(document.createTextNode(frag))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.textContent = file
|
||||||
|
link.className = 'file-link'
|
||||||
|
el.appendChild(link)
|
||||||
|
curIndex += frag.length + file.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(): void {
|
||||||
|
const closeOnEsc = this.getCloseOnEsc()
|
||||||
|
this.parentNode?.removeChild(this)
|
||||||
|
document.removeEventListener('keydown', closeOnEsc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOverlayCode(): string {
|
||||||
|
return `
|
||||||
|
const overlayTemplate = \`${overlayTemplate}\`;
|
||||||
|
${ErrorOverlay.toString()}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchOverlay(code: string): string {
|
||||||
|
return code.replace(
|
||||||
|
'class ErrorOverlay',
|
||||||
|
getOverlayCode() + '\nclass ViteErrorOverlay',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorOverlayVitePlugin: Plugin = {
|
||||||
|
name: 'tuono-error-overlay-plugin',
|
||||||
|
transform(code, id, opts = {}) {
|
||||||
|
if (opts.ssr) return
|
||||||
|
if (!id.includes('vite/dist/client/client.mjs')) return
|
||||||
|
|
||||||
|
return patchOverlay(code)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import { TuonoFsRouterPlugin } from 'tuono-fs-router-vite-plugin'
|
|||||||
|
|
||||||
import type { TuonoConfig } from '../config'
|
import type { TuonoConfig } from '../config'
|
||||||
|
|
||||||
|
import { ErrorOverlayVitePlugin } from './error-overlay'
|
||||||
|
|
||||||
import { blockingAsync } from './utils'
|
import { blockingAsync } from './utils'
|
||||||
import { createJsonConfig, loadConfig } from './config'
|
import { createJsonConfig, loadConfig } from './config'
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ const developmentCSRWatch = (): void => {
|
|||||||
{
|
{
|
||||||
// Entry point for the development vite proxy
|
// Entry point for the development vite proxy
|
||||||
base: '/vite-server/',
|
base: '/vite-server/',
|
||||||
|
plugins: [ErrorOverlayVitePlugin],
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
host: config.server.host,
|
host: config.server.host,
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import react from '@vitejs/plugin-react-swc'
|
|||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
defineConfig({
|
defineConfig({
|
||||||
|
/**
|
||||||
|
* add explicit build target to avoid transpilation on class properties
|
||||||
|
* @see https://github.com/tuono-labs/tuono/pull/607#discussion_r1983979427
|
||||||
|
*/
|
||||||
|
build: { target: 'es2022' },
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
}),
|
}),
|
||||||
defineViteConfig({
|
defineViteConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user