mirror of
https://github.com/withastro/astro.git
synced 2025-01-27 22:19:04 -05:00
fix: handle error at load for apps (#9768)
* fix: handle error at load for apps
* Revert "fix: handle error at load for apps"
This reverts commit df9e98a8c4
.
* fix: handle errors in load and init
* chore: changeset
* fix: build
This commit is contained in:
parent
2b5f1d6be8
commit
eed0e8757c
8 changed files with 96 additions and 13 deletions
5
.changeset/honest-impalas-walk.md
Normal file
5
.changeset/honest-impalas-walk.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"astro": patch
|
||||
---
|
||||
|
||||
Fix apps being able to crash the dev toolbar in certain cases
|
|
@ -2702,7 +2702,7 @@ export interface ClientDirectiveConfig {
|
|||
export interface DevToolbarApp {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: Icon;
|
||||
icon?: Icon;
|
||||
init?(canvas: ShadowRoot, eventTarget: EventTarget): void | Promise<void>;
|
||||
beforeTogglingOff?(canvas: ShadowRoot): boolean | Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export type LoggerLabel =
|
|||
| 'middleware'
|
||||
| 'preferences'
|
||||
| 'redirects'
|
||||
| 'toolbar'
|
||||
// SKIP_FORMAT: A special label that tells the logger not to apply any formatting.
|
||||
// Useful for messages that are already formatted, like the server start message.
|
||||
| 'SKIP_FORMAT';
|
||||
|
|
|
@ -26,7 +26,7 @@ const settingsRows = [
|
|||
|
||||
settings.updateSetting('disableAppNotification', evt.currentTarget.checked);
|
||||
const action = evt.currentTarget.checked ? 'disabled' : 'enabled';
|
||||
settings.log(`App notification badges ${action}`);
|
||||
settings.logger.verboseLog(`App notification badges ${action}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ const settingsRows = [
|
|||
if (evt.currentTarget instanceof HTMLInputElement) {
|
||||
settings.updateSetting('verbose', evt.currentTarget.checked);
|
||||
const action = evt.currentTarget.checked ? 'enabled' : 'disabled';
|
||||
settings.log(`Verbose logging ${action}`);
|
||||
settings.logger.verboseLog(`Verbose logging ${action}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -201,7 +201,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
const iconContainer = document.createElement('div');
|
||||
const iconElement = document.createElement('template');
|
||||
iconElement.innerHTML = getAppIcon(app.icon);
|
||||
iconElement.innerHTML = app.icon ? getAppIcon(app.icon) : '?';
|
||||
iconContainer.append(iconElement.content.cloneNode(true));
|
||||
|
||||
const notification = document.createElement('div');
|
||||
|
|
|
@ -44,6 +44,13 @@ function getSettings() {
|
|||
return _settings;
|
||||
},
|
||||
updateSetting,
|
||||
log,
|
||||
logger: {
|
||||
log,
|
||||
verboseLog: (message: string) => {
|
||||
if (_settings.verbose) {
|
||||
log(message);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -126,6 +126,11 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
#dev-bar #bar-container .item[data-app-error]:hover, #dev-bar #bar-container .item[data-app-error]:focus-visible {
|
||||
cursor: not-allowed;
|
||||
background: #ff252520;
|
||||
}
|
||||
|
||||
#dev-bar .item:first-of-type {
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
|
@ -166,6 +171,10 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
border-top: 5px solid #343841;
|
||||
}
|
||||
|
||||
#dev-bar .item[data-app-error] .icon {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#dev-bar .item:hover .item-tooltip, #dev-bar .item:not(.active):focus-visible .item-tooltip {
|
||||
transition: opacity 0.2s ease-in-out 200ms;
|
||||
opacity: 1;
|
||||
|
@ -266,7 +275,7 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
|
||||
// Create app canvases
|
||||
this.apps.forEach(async (app) => {
|
||||
if (settings.config.verbose) console.log(`Creating app canvas for ${app.id}`);
|
||||
settings.logger.verboseLog(`Creating app canvas for ${app.id}`);
|
||||
const appCanvas = document.createElement('astro-dev-toolbar-app-canvas');
|
||||
appCanvas.dataset.appId = app.id;
|
||||
this.shadowRoot?.append(appCanvas);
|
||||
|
@ -353,24 +362,40 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
const shadowRoot = this.getAppCanvasById(app.id)!.shadowRoot!;
|
||||
app.status = 'loading';
|
||||
try {
|
||||
if (settings.config.verbose) console.info(`Initializing app ${app.id}`);
|
||||
settings.logger.verboseLog(`Initializing app ${app.id}`);
|
||||
|
||||
await app.init?.(shadowRoot, app.eventTarget);
|
||||
app.status = 'ready';
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(`${WS_EVENT_NAME}:${app.id}:initialized`);
|
||||
// TODO: Remove in Astro 5.0
|
||||
import.meta.hot.send(`${WS_EVENT_NAME_DEPRECATED}:${app.id}:initialized`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to init app ${app.id}, error: ${e}`);
|
||||
app.status = 'error';
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send('astro:devtoolbar:error:init', {
|
||||
app: app,
|
||||
error: e instanceof Error ? e.stack : e,
|
||||
});
|
||||
}
|
||||
|
||||
const appButton = this.getAppButtonById(app.id);
|
||||
const appTooltip = appButton?.querySelector<HTMLElement>('.item-tooltip');
|
||||
|
||||
if (appButton && appTooltip) {
|
||||
appButton.toggleAttribute('data-app-error', true);
|
||||
appTooltip.innerText = `Error initializing ${app.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAppTemplate(app: DevToolbarApp) {
|
||||
return `<button class="item" data-app-id="${app.id}">
|
||||
<div class="icon">${getAppIcon(app.icon)}<div class="notification"></div></div>
|
||||
<div class="icon">${app.icon ? getAppIcon(app.icon) : '?'}<div class="notification"></div></div>
|
||||
<span class="item-tooltip">${app.name}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
@ -385,6 +410,10 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
);
|
||||
}
|
||||
|
||||
getAppButtonById(id: string) {
|
||||
return this.shadowRoot.querySelector<HTMLElement>(`[data-app-id="${id}"]`);
|
||||
}
|
||||
|
||||
async toggleAppStatus(app: DevToolbarApp) {
|
||||
const activeApp = this.getActiveApp();
|
||||
if (activeApp) {
|
||||
|
@ -418,7 +447,7 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
}
|
||||
|
||||
app.active = newStatus ?? !app.active;
|
||||
const mainBarButton = this.shadowRoot.querySelector(`[data-app-id="${app.id}"]`);
|
||||
const mainBarButton = this.getAppButtonById(app.id);
|
||||
const moreBarButton = this.getAppCanvasById('astro:more')?.shadowRoot?.querySelector(
|
||||
`[data-app-id="${app.id}"]`
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { AstroPluginOptions } from '../@types/astro.js';
|
|||
const VIRTUAL_MODULE_ID = 'astro:dev-toolbar';
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
||||
export default function astroDevToolbar({ settings }: AstroPluginOptions): vite.Plugin {
|
||||
export default function astroDevToolbar({ settings, logger }: AstroPluginOptions): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:dev-toolbar',
|
||||
config() {
|
||||
|
@ -20,14 +20,55 @@ export default function astroDevToolbar({ settings }: AstroPluginOptions): vite.
|
|||
return resolvedVirtualModuleId;
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
server.ws.on('astro:devtoolbar:error:load', (args) => {
|
||||
logger.error(
|
||||
'toolbar',
|
||||
`Failed to load dev toolbar app from ${args.entrypoint}: ${args.error}`
|
||||
);
|
||||
});
|
||||
|
||||
server.ws.on('astro:devtoolbar:error:init', (args) => {
|
||||
logger.error(
|
||||
'toolbar',
|
||||
`Failed to initialize dev toolbar app ${args.app.name} (${args.app.id}):\n${args.error}`
|
||||
);
|
||||
});
|
||||
},
|
||||
async load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
// TODO: In Astro 5.0, we should change the addDevToolbarApp function to separate the logic from the app's metadata.
|
||||
// That way, we can pass the app's data to the dev toolbar without having to load the app's entrypoint, which will allow
|
||||
// for a better UI in the browser where we could still show the app's name and icon even if the app's entrypoint fails to load.
|
||||
// ex: `addDevToolbarApp({ id: 'astro:dev-toolbar:app', name: 'App', icon: '🚀', entrypoint: "./src/something.ts" })`
|
||||
return `
|
||||
export const loadDevToolbarApps = async () => {
|
||||
return [${settings.devToolbarApps
|
||||
.map((plugin) => `(await import(${JSON.stringify(plugin)})).default`)
|
||||
.join(',')}];
|
||||
return (await Promise.all([${settings.devToolbarApps
|
||||
.map((plugin) => `safeLoadPlugin(${JSON.stringify(plugin)})`)
|
||||
.join(',')}])).filter(app => app);
|
||||
};
|
||||
|
||||
async function safeLoadPlugin(entrypoint) {
|
||||
try {
|
||||
const app = (await import(/* @vite-ignore */ entrypoint)).default;
|
||||
|
||||
if (typeof app !== 'object' || !app.id || !app.name) {
|
||||
throw new Error("Apps must default export an object with an id, and a name.");
|
||||
}
|
||||
|
||||
return app;
|
||||
} catch (err) {
|
||||
console.error(\`Failed to load dev toolbar app from \${entrypoint}: \${err.message}\`);
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send('astro:devtoolbar:error:load', { entrypoint: entrypoint, error: err.message })
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue