diff --git a/.changeset/plugin-typescript-build.md b/.changeset/plugin-typescript-build.md new file mode 100644 index 0000000..dfa2aae --- /dev/null +++ b/.changeset/plugin-typescript-build.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Build bundled Pi Web plugins from TypeScript during development and release packaging while shipping browser-loadable JavaScript modules. diff --git a/README.md b/README.md index 297e0fa..8fe12f2 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Pi Web keeps its own state intentionally small: Pi Web production installs can load trusted local UI plugins without rebuilding Pi Web. Plugins are browser-side ES modules that can add action-palette actions, workspace panels, and workspace-label metadata. They do not run in the session daemon and are not sandboxed. -The supported package shape is intentionally singular: `piWeb.plugins` entries with explicit `id` and `module`, plus a browser module that exports `{ apiVersion: 1, name, activate }`. The bundled `pi-web-plugins/info` plugin is the canonical minimal real example, and `pi-web-plugins/pi-web` demonstrates a dynamic status panel. +The supported package shape is intentionally singular: `piWeb.plugins` entries with explicit `id` and `module`, plus a browser module that exports `{ apiVersion: 1, name, activate }`. The bundled `pi-web-plugins/info` TypeScript source is the canonical minimal real example, and `pi-web-plugins/pi-web` demonstrates a dynamic status panel. A useful prompt for AI agents: @@ -211,7 +211,7 @@ npm run dev:web npm run dev:client ``` -You can restart `dev:web` or `dev:client` without stopping active Pi sessions. +`dev:web` also watches bundled plugin TypeScript and rebuilds the browser-loaded plugin JavaScript under `dist/pi-web-plugins/`. You can restart `dev:web` or `dev:client` without stopping active Pi sessions. ## Production-style run from a checkout @@ -229,7 +229,7 @@ npm run pack:dry npm publish --access public ``` -`prepack` builds `dist/` before npm creates the tarball, and `prepublishOnly` runs verification before publishing. Releases can also be published by the GitHub Actions npm workflow when a GitHub release is published. +`prepack` builds `dist/` and bundled plugin JavaScript before npm creates the tarball, and `prepublishOnly` runs verification before publishing. Releases can also be published by the GitHub Actions npm workflow when a GitHub release is published. Pi Web uses a single-line CalVer-inspired npm version: `MAJOR.YYYYMM.SEQUENCE`, for example `1.202605.1`. The major number signals breaking-change eras; the middle number is the release month; the final number increments for additional releases in that month. Older major eras may be deprecated rather than maintained in parallel. @@ -260,7 +260,7 @@ Environment variables: A practical local or server setup is two user services: - `pi-web-sessiond.service` runs `npm run start:sessiond` without autoreload. -- `pi-web-ui-dev.service` runs `npm run dev:web` and `npm run dev:client` for API reloads and Vite HMR. +- `pi-web-ui-dev.service` runs `npm run dev:web` and `npm run dev:client` for API reloads, bundled plugin rebuilds, and Vite HMR. Example units: diff --git a/docs/plugins.html b/docs/plugins.html index dfe3f66..9c54d13 100644 --- a/docs/plugins.html +++ b/docs/plugins.html @@ -143,9 +143,16 @@ After editing, check the manifest endpoint and browser-console failure cases.Info plugin. It is intentionally small while still using all core contribution types: one action, one workspace label, and one workspace panel.
+
+ Bundled Pi Web plugins are developed as TypeScript in the repository, while their package metadata
+ points at the built JavaScript ES modules that the browser loads. npm run dev:web watches and
+ rebuilds bundled plugin TS into dist/pi-web-plugins/ during development, and npm run build
+ emits JS before release packaging.
+
pi-web-plugins/info/package.json shows the required metadata shape.pi-web-plugins/info/pi-web-plugin.js shows the browser module shape.pi-web-plugins/info/pi-web-plugin.ts shows the TypeScript source shape.dist/pi-web-plugins/info/pi-web-plugin.js is the built browser module in a checkout.Read it on GitHub: diff --git a/docs/plugins.md b/docs/plugins.md index aeca357..609fbbc 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -65,11 +65,19 @@ After editing, check the manifest endpoint and browser-console failure cases. Pi Web ships a real bundled `info` plugin. Use it as the reference example because it is intentionally small while still exercising all core contribution types: an action, a workspace label, and a workspace panel. -Files: +Bundled Pi Web plugins are developed as TypeScript in the repository, but their `package.json` metadata still points at built JavaScript because plugins are loaded by the browser as JS ES modules. `npm run dev:web` watches and rebuilds bundled plugin TS into `dist/pi-web-plugins/` during development, and `npm run build` emits the JS before packaging a release. + +Source files: ```text pi-web-plugins/info/package.json -pi-web-plugins/info/pi-web-plugin.js +pi-web-plugins/info/pi-web-plugin.ts +``` + +Built module: + +```text +dist/pi-web-plugins/info/pi-web-plugin.js ``` Package metadata: @@ -538,7 +546,7 @@ Pi Web does not provide a plugin cache/invalidation framework. Keep host callbac If you are an AI agent building or editing a Pi Web plugin, follow this checklist: -1. Create or update a plugin folder with `package.json` and `pi-web-plugin.js`. +1. Create or update a plugin folder with `package.json` and a JavaScript module such as `pi-web-plugin.js`. 2. Use the single supported package metadata shape: `piWeb.plugins` array with `{ id, module }` entries. 3. Default-export `{ apiVersion: 1, name, activate }` from the module. 4. Return `{ contributions: { actions, workspacePanels, workspaceLabels } }` from `activate()`. diff --git a/eslint.config.js b/eslint.config.js index 75e5487..ae08e23 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,7 +8,7 @@ export default defineConfig([ ignores: ["dist/**", "node_modules/**"], }, { - files: ["src/**/*.ts", "extensions/**/*.ts", "vite.config.ts", "vitest.config.ts"], + files: ["src/**/*.ts", "extensions/**/*.ts", "pi-web-plugins/**/*.ts", "vite.config.ts", "vitest.config.ts"], extends: [ js.configs.recommended, tseslint.configs.strictTypeChecked, diff --git a/package.json b/package.json index 04e37bb..318eec6 100644 --- a/package.json +++ b/package.json @@ -16,19 +16,20 @@ "README.md", "LICENSE", "extensions", - "pi-web-plugins", "docs/plugins.md", "docs/assets" ], "scripts": { "dev": "bash -c 'trap \"kill 0\" EXIT; npm run dev:sessiond & npm run dev:web & npm run dev:client & wait'", "dev:sessiond": "tsx watch src/server/sessiond.ts", - "dev:web": "tsx watch src/server/index.ts", + "dev:web": "bash -c 'set -e; npm run build:plugins; trap \"kill 0\" EXIT; npm run dev:plugins & tsx watch src/server/index.ts & wait'", "dev:server": "npm run dev:web", "dev:client": "vite --host 0.0.0.0", - "build": "tsc -p tsconfig.build.json && vite build", + "dev:plugins": "node scripts/build-plugins.mjs --watch", + "build": "tsc -p tsconfig.build.json && npm run build:plugins && vite build", + "build:plugins": "tsc -p tsconfig.plugins.json && node scripts/build-plugins.mjs", "typecheck": "tsc --noEmit", - "lint": "eslint \"src/**/*.ts\" \"extensions/**/*.ts\" vite.config.ts vitest.config.ts", + "lint": "eslint \"src/**/*.ts\" \"extensions/**/*.ts\" \"pi-web-plugins/**/*.ts\" vite.config.ts vitest.config.ts", "test": "vitest run --config vitest.config.ts", "verify": "npm run typecheck && npm run lint && npm test", "start": "tsx src/server/index.ts", diff --git a/pi-web-plugins/info/pi-web-plugin.js b/pi-web-plugins/info/pi-web-plugin.ts similarity index 90% rename from pi-web-plugins/info/pi-web-plugin.js rename to pi-web-plugins/info/pi-web-plugin.ts index ec24cc2..52dc709 100644 --- a/pi-web-plugins/info/pi-web-plugin.js +++ b/pi-web-plugins/info/pi-web-plugin.ts @@ -1,4 +1,6 @@ -export default { +import type { PiWebPlugin } from "../../src/client/src/plugins/types"; + +const plugin: PiWebPlugin = { apiVersion: 1, name: "Info Plugin", activate: ({ html }) => ({ @@ -40,3 +42,5 @@ export default { }, }), }; + +export default plugin; diff --git a/pi-web-plugins/pi-web/pi-web-plugin.js b/pi-web-plugins/pi-web/pi-web-plugin.ts similarity index 79% rename from pi-web-plugins/pi-web/pi-web-plugin.js rename to pi-web-plugins/pi-web/pi-web-plugin.ts index ac6d31a..aa48e8e 100644 --- a/pi-web-plugins/pi-web/pi-web-plugin.js +++ b/pi-web-plugins/pi-web/pi-web-plugin.ts @@ -1,20 +1,25 @@ -function messagesFor(state) { - return state?.piWebStatus?.messages ?? []; +import type { TemplateResult } from "lit"; +import type { AppState } from "../../src/client/src/appState"; +import type { HtmlTemplateTag, PiWebPlugin } from "../../src/client/src/plugins/types"; +import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebStatusMessage, PiWebStatusResponse } from "../../src/shared/apiTypes"; + +function messagesFor(state: AppState): PiWebStatusMessage[] { + return state.piWebStatus?.messages ?? []; } -function statusFor(state) { - return state?.piWebStatus; +function statusFor(state: AppState): PiWebStatusResponse | undefined { + return state.piWebStatus; } -function messageCount(state) { +function messageCount(state: AppState): number { return messagesFor(state).length; } -function isLocalOrUnknownInstallation(installation) { +function isLocalOrUnknownInstallation(installation: PiWebInstallationInfo | undefined): boolean { return installation === undefined || installation.kind === "local" || installation.kind === "unknown"; } -function shouldShowStatusPanel(state) { +function shouldShowStatusPanel(state: AppState): boolean { const status = statusFor(state); if (messageCount(state) > 0) return true; if (status === undefined) return false; @@ -22,15 +27,15 @@ function shouldShowStatusPanel(state) { || isLocalOrUnknownInstallation(status.components.sessiond.installation); } -function formatVersion(version) { +function formatVersion(version: string | undefined): string { return version === undefined || version === "" ? "unknown" : version; } -function installationLabel(installation) { +function installationLabel(installation: PiWebInstallationInfo | undefined): string { if (installation === undefined) return "installation unknown"; if (installation.kind === "pi-package") { const scope = installation.scope === undefined ? "" : ` ยท ${installation.scope}`; - const source = installation.source === undefined ? "Pi package" : installation.source; + const source = installation.source ?? "Pi package"; return `${source}${scope}`; } if (installation.kind === "npm-global") return "global npm package"; @@ -38,8 +43,8 @@ function installationLabel(installation) { return "installation unknown"; } -function renderComponent(html, component) { - const status = component.available === false +function renderComponent(html: HtmlTemplateTag, component: PiWebComponentStatus): TemplateResult { + const status = !component.available ? "unavailable" : component.stale ? "restart needed" @@ -54,17 +59,17 @@ function renderComponent(html, component) { `; } -function renderCommand(html, label, command) { +function renderCommand(html: HtmlTemplateTag, label: string, command: string): TemplateResult { return html`
${command}
-
+