0
Fork 0
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:
Erika 2024-01-22 15:37:03 -05:00 committed by GitHub
parent 2b5f1d6be8
commit eed0e8757c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 96 additions and 13 deletions

View file

@ -0,0 +1,5 @@
---
"astro": patch
---
Fix apps being able to crash the dev toolbar in certain cases

View file

@ -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>;
}

View file

@ -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';

View file

@ -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}`);
}
},
},

View file

@ -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');

View file

@ -44,6 +44,13 @@ function getSettings() {
return _settings;
},
updateSetting,
log,
logger: {
log,
verboseLog: (message: string) => {
if (_settings.verbose) {
log(message);
}
},
},
};
}

View file

@ -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}"]`
);

View file

@ -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;
}
`;
}
},