mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
feat(toolbar): Toolbar API improvements (#10665)
* feat(toolbar): Add a `astro:toolbar` module * fix: use entrypoint * feat: add new shape for defining toolbar apps * fix: types * feat(toolbar): Add helpers features (#10667) * feat(toolbar): Add helpers features * fix: consistent payloads and naming * chore: changeset * nit: rename eventTarget to app * feat: add server-side helpers * test: update test to use new APIs * fix: types * nit: erikaaaaa * feat: add new event * Update .changeset/khaki-pianos-burn.md * test: use data to create text * Apply suggestions from code review Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * nit: use diff * nit: documentation effort * test: fix --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * nit: small changes to helpers * nit: update changeset * fix: move to astro/toolbar for building purposes * feat(toolbar): Add a toolbar example (#10793) * feat: add a toolbar starter * test: skip examples that are not Astro projects * nit: small changes * feat: setup for build step * fix: add to devdep * docs: add commands to README * fix: reorder classes to make more sense * fix: add improvements from recipe * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
6fc4c0e420
commit
7b4f284020
22 changed files with 453 additions and 47 deletions
53
.changeset/khaki-pianos-burn.md
Normal file
53
.changeset/khaki-pianos-burn.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
"astro": minor
|
||||
---
|
||||
|
||||
Adds new utilities to ease the creation of toolbar apps including `defineToolbarApp` to make it easier to define your toolbar app and `app` and `server` helpers for easier communication between the toolbar and the server. These new utilities abstract away some of the boilerplate code that is common in toolbar apps, and lower the barrier of entry for app authors.
|
||||
|
||||
For example, instead of creating an event listener for the `app-toggled` event and manually typing the value in the callback, you can now use the `onAppToggled` method. Additionally, communicating with the server does not require knowing any of the Vite APIs anymore, as a new `server` object is passed to the `init` function that contains easy to use methods for communicating with the server.
|
||||
|
||||
```diff
|
||||
import { defineToolbarApp } from "astro/toolbar";
|
||||
|
||||
export default defineToolbarApp({
|
||||
init(canvas, app, server) {
|
||||
|
||||
- app.addEventListener("app-toggled", (e) => {
|
||||
- console.log(`App is now ${state ? "enabled" : "disabled"}`);.
|
||||
- });
|
||||
|
||||
+ app.onToggled(({ state }) => {
|
||||
+ console.log(`App is now ${state ? "enabled" : "disabled"}`);
|
||||
+ });
|
||||
|
||||
- if (import.meta.hot) {
|
||||
- import.meta.hot.send("my-app:my-client-event", { message: "world" });
|
||||
- }
|
||||
|
||||
+ server.send("my-app:my-client-event", { message: "world" })
|
||||
|
||||
- if (import.meta.hot) {
|
||||
- import.meta.hot.on("my-server-event", (data: {message: string}) => {
|
||||
- console.log(data.message);
|
||||
- });
|
||||
- }
|
||||
|
||||
+ server.on<{ message: string }>("my-server-event", (data) => {
|
||||
+ console.log(data.message); // data is typed using the type parameter
|
||||
+ });
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Server helpers are also available on the server side, for use in your integrations, through the new `toolbar` object:
|
||||
|
||||
```ts
|
||||
"astro:server:setup": ({ toolbar }) => {
|
||||
toolbar.on<{ message: string }>("my-app:my-client-event", (data) => {
|
||||
console.log(data.message);
|
||||
toolbar.send("my-server-event", { message: "hello" });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This is a backwards compatible change and your your existing dev toolbar apps will continue to function. However, we encourage you to build your apps with the new helpers, following the [updated Dev Toolbar API documentation](https://docs.astro.build/en/reference/dev-toolbar-app-reference/).
|
1
examples/toolbar-app/.codesandbox/Dockerfile
Normal file
1
examples/toolbar-app/.codesandbox/Dockerfile
Normal file
|
@ -0,0 +1 @@
|
|||
FROM node:18-bullseye
|
21
examples/toolbar-app/.gitignore
vendored
Normal file
21
examples/toolbar-app/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# production build
|
||||
dist
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
40
examples/toolbar-app/README.md
Normal file
40
examples/toolbar-app/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Astro Starter Kit: Toolbar App
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template toolbar-app
|
||||
```
|
||||
|
||||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/toolbar-app)
|
||||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/toolbar-app)
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/toolbar-app/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── app.ts
|
||||
├── integration.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
The logic of your app is in the appropriately named `app.ts` file. This is where the vast majority of your toolbar app logic will live.
|
||||
|
||||
The `integration.ts` file is a simple Astro integration file that will be used to add your app into the toolbar.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :------------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Watch for changes and build your app automatically |
|
||||
| `npm run build` | Build your app to `./dist/` |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
20
examples/toolbar-app/package.json
Normal file
20
examples/toolbar-app/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@example/toolbar-app",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"astro": "^4.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/integration.js",
|
||||
"./app": "./dist/app.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^4.6.1"
|
||||
}
|
||||
}
|
16
examples/toolbar-app/src/app.ts
Normal file
16
examples/toolbar-app/src/app.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { defineToolbarApp } from 'astro/toolbar';
|
||||
|
||||
// Guide: https://docs.astro.build/en/recipes/making-toolbar-apps/
|
||||
// API Reference: https://docs.astro.build/en/reference/dev-toolbar-app-reference/
|
||||
export default defineToolbarApp({
|
||||
init(canvas) {
|
||||
const astroWindow = document.createElement('astro-dev-toolbar-window');
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = 'Hello, Astro!';
|
||||
|
||||
astroWindow.append(text);
|
||||
|
||||
canvas.append(astroWindow);
|
||||
},
|
||||
});
|
17
examples/toolbar-app/src/integration.ts
Normal file
17
examples/toolbar-app/src/integration.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import type { AstroIntegration } from 'astro';
|
||||
|
||||
// API Reference: https://docs.astro.build/en/reference/integrations-reference/
|
||||
export default {
|
||||
name: 'my-astro-integration',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ addDevToolbarApp }) => {
|
||||
addDevToolbarApp({
|
||||
id: "my-toolbar-app",
|
||||
name: "My Toolbar App",
|
||||
icon: "🚀",
|
||||
entrypoint: fileURLToPath(new URL('./app.js', import.meta.url))
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies AstroIntegration;
|
7
examples/toolbar-app/tsconfig.json
Normal file
7
examples/toolbar-app/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
}
|
||||
}
|
|
@ -304,6 +304,8 @@ test.describe('Dev Toolbar', () => {
|
|||
await expect(myAppWindow).toHaveCount(1);
|
||||
await expect(myAppWindow).toBeVisible();
|
||||
|
||||
await expect(myAppWindow).toContainText('Hello from the server!');
|
||||
|
||||
// Toggle app off
|
||||
await appButton.click();
|
||||
await expect(myAppWindow).not.toBeVisible();
|
||||
|
|
|
@ -10,8 +10,18 @@ export function myIntegration() {
|
|||
const importPath = dirname(fileURLToPath(import.meta.url));
|
||||
const pluginPath = join(importPath, 'custom-plugin.js');
|
||||
|
||||
addDevToolbarApp(pluginPath);
|
||||
addDevToolbarApp({
|
||||
id: 'my-plugin',
|
||||
name: 'My Plugin',
|
||||
icon: 'astro:logo',
|
||||
entrypoint: pluginPath
|
||||
});
|
||||
},
|
||||
'astro:server:setup': ({ toolbar }) => {
|
||||
toolbar.onAppInitialized("my-plugin", () => {
|
||||
toolbar.send("super-server-event", { message: "Hello from the server!" })
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
export default {
|
||||
id: 'my-plugin',
|
||||
name: 'My Plugin',
|
||||
icon: 'astro:logo',
|
||||
init(canvas, eventTarget) {
|
||||
import { defineToolbarApp } from "astro/toolbar";
|
||||
|
||||
export default defineToolbarApp({
|
||||
init(canvas, app, server) {
|
||||
const astroWindow = document.createElement('astro-dev-toolbar-window');
|
||||
const myButton = document.createElement('astro-dev-toolbar-button');
|
||||
myButton.size = 'medium';
|
||||
|
@ -13,16 +12,17 @@ export default {
|
|||
console.log('Clicked!');
|
||||
});
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent("toggle-notification", {
|
||||
detail: {
|
||||
level: "warning",
|
||||
},
|
||||
})
|
||||
);
|
||||
app.toggleNotification({
|
||||
state: true,
|
||||
level: 'warning'
|
||||
})
|
||||
|
||||
server.on("super-server-event", (data) => {
|
||||
astroWindow.appendChild(document.createTextNode(data.message));
|
||||
});
|
||||
|
||||
astroWindow.appendChild(myButton);
|
||||
|
||||
canvas.appendChild(astroWindow);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"./client/*": "./dist/runtime/client/*",
|
||||
"./components": "./components/index.ts",
|
||||
"./components/*": "./components/*",
|
||||
"./toolbar": "./dist/toolbar/index.js",
|
||||
"./assets": "./dist/assets/index.js",
|
||||
"./assets/utils": "./dist/assets/utils/index.js",
|
||||
"./assets/endpoint/*": "./dist/assets/endpoint/*.js",
|
||||
|
|
|
@ -38,10 +38,15 @@ import type {
|
|||
TransitionBeforePreparationEvent,
|
||||
TransitionBeforeSwapEvent,
|
||||
} from '../transitions/events.js';
|
||||
import type { DeepPartial, OmitIndexSignature, Simplify } from '../type-utils.js';
|
||||
import type { DeepPartial, OmitIndexSignature, Simplify, WithRequired } from '../type-utils.js';
|
||||
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||
import type {
|
||||
ToolbarAppEventTarget,
|
||||
ToolbarServerHelpers,
|
||||
} from '../runtime/client/dev-toolbar/helpers.js';
|
||||
import type { getToolbarServerCommunicationHelpers } from '../integrations/index.js';
|
||||
|
||||
export { type AstroIntegrationLogger };
|
||||
export type { AstroIntegrationLogger, ToolbarServerHelpers };
|
||||
|
||||
export type {
|
||||
MarkdownHeading,
|
||||
|
@ -2100,7 +2105,7 @@ export interface AstroSettings {
|
|||
* Map of directive name (e.g. `load`) to the directive script code
|
||||
*/
|
||||
clientDirectives: Map<string, string>;
|
||||
devToolbarApps: string[];
|
||||
devToolbarApps: (DevToolbarAppEntry | string)[];
|
||||
middlewares: { pre: string[]; post: string[] };
|
||||
tsConfig: TSConfig | undefined;
|
||||
tsConfigPath: string | undefined;
|
||||
|
@ -2735,7 +2740,8 @@ export interface AstroIntegration {
|
|||
* TODO: Fully remove in Astro 5.0
|
||||
*/
|
||||
addDevOverlayPlugin: (entrypoint: string) => void;
|
||||
addDevToolbarApp: (entrypoint: string) => void;
|
||||
// TODO: Deprecate the `string` overload once a few apps have been migrated to the new API.
|
||||
addDevToolbarApp: (entrypoint: DevToolbarAppEntry | string) => void;
|
||||
addMiddleware: (mid: AstroIntegrationMiddleware) => void;
|
||||
logger: AstroIntegrationLogger;
|
||||
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
|
||||
|
@ -2751,6 +2757,7 @@ export interface AstroIntegration {
|
|||
'astro:server:setup'?: (options: {
|
||||
server: vite.ViteDevServer;
|
||||
logger: AstroIntegrationLogger;
|
||||
toolbar: ReturnType<typeof getToolbarServerCommunicationHelpers>;
|
||||
}) => void | Promise<void>;
|
||||
'astro:server:start'?: (options: {
|
||||
address: AddressInfo;
|
||||
|
@ -3006,13 +3013,53 @@ export interface ClientDirectiveConfig {
|
|||
entrypoint: string;
|
||||
}
|
||||
|
||||
export interface DevToolbarApp {
|
||||
type DevToolbarAppMeta = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: Icon;
|
||||
init?(canvas: ShadowRoot, eventTarget: EventTarget): void | Promise<void>;
|
||||
};
|
||||
|
||||
// The param passed to `addDevToolbarApp` in the integration
|
||||
export type DevToolbarAppEntry = DevToolbarAppMeta & {
|
||||
entrypoint: string;
|
||||
};
|
||||
|
||||
// Public API for the dev toolbar
|
||||
export type DevToolbarApp = {
|
||||
/**
|
||||
* @deprecated The `id`, `name`, and `icon` properties should now be defined when using `addDevToolbarApp`.
|
||||
*
|
||||
* Ex: `addDevToolbarApp({ id: 'my-app', name: 'My App', icon: '🚀', entrypoint: '/path/to/app' })`
|
||||
*
|
||||
* In the future, putting these properties directly on the app object will be removed.
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @deprecated The `id`, `name`, and `icon` properties should now be defined when using `addDevToolbarApp`.
|
||||
*
|
||||
* Ex: `addDevToolbarApp({ id: 'my-app', name: 'My App', icon: '🚀', entrypoint: '/path/to/app' })`
|
||||
*
|
||||
* In the future, putting these properties directly on the app object will be removed.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @deprecated The `id`, `name`, and `icon` properties should now be defined when using `addDevToolbarApp`.
|
||||
*
|
||||
* Ex: `addDevToolbarApp({ id: 'my-app', name: 'My App', icon: '🚀', entrypoint: '/path/to/app' })`
|
||||
*
|
||||
* In the future, putting these properties directly on the app object will be removed.
|
||||
*/
|
||||
icon?: Icon;
|
||||
init?(
|
||||
canvas: ShadowRoot,
|
||||
app: ToolbarAppEventTarget,
|
||||
server: ToolbarServerHelpers
|
||||
): void | Promise<void>;
|
||||
beforeTogglingOff?(canvas: ShadowRoot): boolean | Promise<boolean>;
|
||||
}
|
||||
};
|
||||
|
||||
// An app that has been loaded and as such contain all of its properties
|
||||
export type ResolvedDevToolbarApp = DevToolbarAppMeta & Omit<DevToolbarApp, 'id' | 'name' | 'icon'>;
|
||||
|
||||
// TODO: Remove in Astro 5.0
|
||||
export type DevOverlayPlugin = DevToolbarApp;
|
||||
|
|
|
@ -18,7 +18,7 @@ import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.j
|
|||
import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js';
|
||||
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
||||
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
|
||||
import astroDevToolbar from '../vite-plugin-dev-toolbar/vite-plugin-dev-toolbar.js';
|
||||
import astroDevToolbar from '../toolbar/vite-plugin-dev-toolbar.js';
|
||||
import envVitePlugin from '../vite-plugin-env/index.js';
|
||||
import vitePluginFileURL from '../vite-plugin-fileurl/index.js';
|
||||
import astroHeadPlugin from '../vite-plugin-head/index.js';
|
||||
|
|
|
@ -61,6 +61,45 @@ function getLogger(integration: AstroIntegration, logger: Logger) {
|
|||
return integrationLogger;
|
||||
}
|
||||
|
||||
const serverEventPrefix = 'astro-dev-toolbar';
|
||||
|
||||
export function getToolbarServerCommunicationHelpers(server: ViteDevServer) {
|
||||
return {
|
||||
/**
|
||||
* Send a message to the dev toolbar that an app can listen for. The payload can be any serializable data.
|
||||
* @param event - The event name
|
||||
* @param payload - The payload to send
|
||||
*/
|
||||
send: <T>(event: string, payload: T) => {
|
||||
server.hot.send(event, payload);
|
||||
},
|
||||
/**
|
||||
* Receive a message from a dev toolbar app.
|
||||
* @param event
|
||||
* @param callback
|
||||
*/
|
||||
on: <T>(event: string, callback: (data: T) => void) => {
|
||||
server.hot.on(event, callback);
|
||||
},
|
||||
/**
|
||||
* Fired when an app is initialized.
|
||||
* @param appId - The id of the app that was initialized
|
||||
* @param callback - The callback to run when the app is initialized
|
||||
*/
|
||||
onAppInitialized: (appId: string, callback: (data: Record<string, never>) => void) => {
|
||||
server.hot.on(`${serverEventPrefix}:${appId}:initialized`, callback);
|
||||
},
|
||||
/**
|
||||
* Fired when an app is toggled on or off.
|
||||
* @param appId - The id of the app that was toggled
|
||||
* @param callback - The callback to run when the app is toggled
|
||||
*/
|
||||
onAppToggled: (appId: string, callback: (data: { state: boolean }) => void) => {
|
||||
server.hot.on(`${serverEventPrefix}:${appId}:toggled`, callback);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runHookConfigSetup({
|
||||
settings,
|
||||
command,
|
||||
|
@ -305,6 +344,7 @@ export async function runHookServerSetup({
|
|||
hookResult: integration.hooks['astro:server:setup']({
|
||||
server,
|
||||
logger: getLogger(integration, logger),
|
||||
toolbar: getToolbarServerCommunicationHelpers(server),
|
||||
}),
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @ts-expect-error
|
||||
import { loadDevToolbarApps } from 'astro:dev-toolbar';
|
||||
import type { DevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js';
|
||||
import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js';
|
||||
import { ToolbarAppEventTarget } from './helpers.js';
|
||||
import { settings } from './settings.js';
|
||||
import type { AstroDevToolbar, DevToolbarApp } from './toolbar.js';
|
||||
// @ts-expect-error - This module is private and untyped
|
||||
import { loadDevToolbarApps } from 'astro:toolbar:internal';
|
||||
|
||||
let overlay: AstroDevToolbar;
|
||||
|
||||
|
@ -74,7 +75,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
} as const;
|
||||
|
||||
const prepareApp = (appDefinition: DevToolbarAppDefinition, builtIn: boolean): DevToolbarApp => {
|
||||
const eventTarget = new EventTarget();
|
||||
const eventTarget = new ToolbarAppEventTarget();
|
||||
const app: DevToolbarApp = {
|
||||
...appDefinition,
|
||||
builtIn: builtIn,
|
||||
|
|
103
packages/astro/src/runtime/client/dev-toolbar/helpers.ts
Normal file
103
packages/astro/src/runtime/client/dev-toolbar/helpers.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
type NotificationPayload = {
|
||||
state: true;
|
||||
level?: 'error' | 'warn' | 'info';
|
||||
} | {
|
||||
state: false
|
||||
};
|
||||
|
||||
type AppStatePayload = {
|
||||
state: boolean;
|
||||
};
|
||||
|
||||
type AppToggledEvent = (opts: { state: boolean }) => void;
|
||||
|
||||
type ToolbarPlacementUpdatedEvent = (opts: { placement: 'bottom-left' | 'bottom-center' | 'bottom-right' }) => void;
|
||||
|
||||
export class ToolbarAppEventTarget extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the notification state of the toolbar
|
||||
* @param options - The notification options
|
||||
* @param options.state - The state of the notification
|
||||
* @param options.level - The level of the notification, optional when state is false
|
||||
*/
|
||||
toggleNotification(options: NotificationPayload) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('toggle-notification', {
|
||||
detail: {
|
||||
state: options.state,
|
||||
level: options.state === true ? options.level : undefined,
|
||||
} satisfies NotificationPayload,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the app state on or off
|
||||
* @param options - The app state options
|
||||
* @param options.state - The new state of the app
|
||||
*/
|
||||
toggleState(options: AppStatePayload) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('app-toggled', {
|
||||
detail: {
|
||||
state: options.state,
|
||||
} satisfies AppStatePayload,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the app is toggled on or off
|
||||
* @param callback - The callback to run when the event is fired, takes an object with the new state
|
||||
*/
|
||||
onToggled(callback: AppToggledEvent) {
|
||||
this.addEventListener('app-toggled', (evt) => {
|
||||
if (!(evt instanceof CustomEvent)) return;
|
||||
callback(evt.detail);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the toolbar placement is updated by the user
|
||||
* @param callback - The callback to run when the event is fired, takes an object with the new placement
|
||||
*/
|
||||
onToolbarPlacementUpdated(callback: ToolbarPlacementUpdatedEvent) {
|
||||
this.addEventListener('placement-updated', (evt) => {
|
||||
if (!(evt instanceof CustomEvent)) return;
|
||||
callback(evt.detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const serverHelpers = {
|
||||
/**
|
||||
* Send a message to the server, the payload can be any serializable data.
|
||||
*
|
||||
* The server can listen for this message in the `astro:server:config` hook of an Astro integration, using the `toolbar.on` method.
|
||||
*
|
||||
* @param event - The event name
|
||||
* @param payload - The payload to send
|
||||
*/
|
||||
send: <T>(event: string, payload: T) => {
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(event, payload);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Receive a message from the server.
|
||||
* @param event - The event name
|
||||
* @param callback - The callback to run when the event is received.
|
||||
* The payload's content will be passed to the callback as an argument
|
||||
*/
|
||||
on: <T>(event: string, callback: (data: T) => void) => {
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on(event, callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export type ToolbarServerHelpers = typeof serverHelpers;
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable no-console */
|
||||
import type { DevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js';
|
||||
import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js';
|
||||
import { serverHelpers, type ToolbarAppEventTarget } from './helpers.js';
|
||||
import { settings } from './settings.js';
|
||||
import { type Icon, getIconElement, isDefinedIcon } from './ui-library/icons.js';
|
||||
import { type Placement } from './ui-library/window.js';
|
||||
|
@ -12,7 +13,7 @@ export type DevToolbarApp = DevToolbarAppDefinition & {
|
|||
state: boolean;
|
||||
level?: 'error' | 'warning' | 'info';
|
||||
};
|
||||
eventTarget: EventTarget;
|
||||
eventTarget: ToolbarAppEventTarget;
|
||||
};
|
||||
const WS_EVENT_NAME = 'astro-dev-toolbar';
|
||||
// TODO: Remove in Astro 5.0
|
||||
|
@ -385,7 +386,7 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
try {
|
||||
settings.logger.verboseLog(`Initializing app ${app.id}`);
|
||||
|
||||
await app.init?.(shadowRoot, app.eventTarget);
|
||||
await app.init?.(shadowRoot, app.eventTarget, serverHelpers);
|
||||
app.status = 'ready';
|
||||
|
||||
if (import.meta.hot) {
|
||||
|
|
5
packages/astro/src/toolbar/index.ts
Normal file
5
packages/astro/src/toolbar/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { DevToolbarApp } from '../@types/astro.js';
|
||||
|
||||
export function defineToolbarApp(app: DevToolbarApp) {
|
||||
return app;
|
||||
}
|
|
@ -3,8 +3,8 @@ import type { AstroPluginOptions } from '../@types/astro.js';
|
|||
import { telemetry } from '../events/index.js';
|
||||
import { eventAppToggled } from '../events/toolbar.js';
|
||||
|
||||
const VIRTUAL_MODULE_ID = 'astro:dev-toolbar';
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
const PRIVATE_VIRTUAL_MODULE_ID = 'astro:toolbar:internal';
|
||||
const resolvedPrivateVirtualModuleId = '\0' + PRIVATE_VIRTUAL_MODULE_ID;
|
||||
|
||||
export default function astroDevToolbar({ settings, logger }: AstroPluginOptions): vite.Plugin {
|
||||
let telemetryTimeout: ReturnType<typeof setTimeout>;
|
||||
|
@ -20,8 +20,8 @@ export default function astroDevToolbar({ settings, logger }: AstroPluginOptions
|
|||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === VIRTUAL_MODULE_ID) {
|
||||
return resolvedVirtualModuleId;
|
||||
if (id === PRIVATE_VIRTUAL_MODULE_ID) {
|
||||
return resolvedPrivateVirtualModuleId;
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
|
@ -57,29 +57,42 @@ export default function astroDevToolbar({ settings, logger }: AstroPluginOptions
|
|||
});
|
||||
},
|
||||
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" })`
|
||||
if (id === resolvedPrivateVirtualModuleId) {
|
||||
return `
|
||||
export const loadDevToolbarApps = async () => {
|
||||
return (await Promise.all([${settings.devToolbarApps
|
||||
.map(
|
||||
(plugin) =>
|
||||
`safeLoadPlugin(async () => (await import(${JSON.stringify(
|
||||
plugin
|
||||
)})).default, ${JSON.stringify(plugin)})`
|
||||
`safeLoadPlugin(${JSON.stringify(plugin)}, async () => (await import(${JSON.stringify(
|
||||
typeof plugin === 'string' ? plugin : plugin.entrypoint
|
||||
)})).default, ${JSON.stringify(typeof plugin === 'string' ? plugin : plugin.entrypoint)})`
|
||||
)
|
||||
.join(',')}])).filter(app => app);
|
||||
.join(',')}]));
|
||||
};
|
||||
|
||||
async function safeLoadPlugin(importEntrypoint, entrypoint) {
|
||||
async function safeLoadPlugin(appDefinition, importEntrypoint, entrypoint) {
|
||||
try {
|
||||
const app = await importEntrypoint();
|
||||
let app;
|
||||
if (typeof appDefinition === 'string') {
|
||||
app = await importEntrypoint();
|
||||
|
||||
if (typeof app !== 'object' || !app.id || !app.name) {
|
||||
throw new Error("Apps must default export an object with an id, and a name.");
|
||||
if (typeof app !== 'object' || !app.id || !app.name) {
|
||||
throw new Error("Apps must default export an object with an id, and a name.");
|
||||
}
|
||||
} else {
|
||||
app = appDefinition;
|
||||
|
||||
if (typeof app !== 'object' || !app.id || !app.name || !app.entrypoint) {
|
||||
throw new Error("Apps must be an object with an id, a name and an entrypoint.");
|
||||
}
|
||||
|
||||
const loadedApp = await importEntrypoint();
|
||||
|
||||
if (typeof loadedApp !== 'object') {
|
||||
throw new Error("App entrypoint must default export an object.");
|
||||
}
|
||||
|
||||
app = { ...app, ...loadedApp };
|
||||
}
|
||||
|
||||
return app;
|
|
@ -381,6 +381,12 @@ importers:
|
|||
specifier: ^0.33.3
|
||||
version: 0.33.3
|
||||
|
||||
examples/toolbar-app:
|
||||
devDependencies:
|
||||
astro:
|
||||
specifier: ^4.6.1
|
||||
version: link:../../packages/astro
|
||||
|
||||
examples/view-transitions:
|
||||
devDependencies:
|
||||
'@astrojs/node':
|
||||
|
|
|
@ -6,9 +6,11 @@ import * as path from 'node:path';
|
|||
import pLimit from 'p-limit';
|
||||
import { tsconfigResolverSync } from 'tsconfig-resolver';
|
||||
|
||||
const skippedExamples = ['toolbar-app', 'component']
|
||||
|
||||
function checkExamples() {
|
||||
let examples = readdirSync('./examples', { withFileTypes: true });
|
||||
examples = examples.filter((dirent) => dirent.isDirectory());
|
||||
examples = examples.filter((dirent) => dirent.isDirectory()).filter((dirent) => !skippedExamples.includes(dirent.name));
|
||||
|
||||
console.log(`Running astro check on ${examples.length} examples...`);
|
||||
|
||||
|
|
Loading…
Reference in a new issue