elowen-plugin-ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # elowen-plugin-ui-kit
2
+
3
+ Contract types and build toolchain for [Elowen](https://github.com/dragocz95/elowen) plugin browser
4
+ UIs. A plugin ships ONE built same-origin ESM bundle; the Elowen web app loads it and hands it the
5
+ host runtime on `window.ElowenUiRuntime` (React, curated UI components, an authenticated `api` fetch,
6
+ SPA `navigate`). The bundle registers its pages and settings panels with
7
+ `window.__elowenRegisterPluginUi(pluginName, registration)`.
8
+
9
+ ## Types
10
+
11
+ `index.d.ts` is the single source of truth for the contract: `ElowenUiRuntime`,
12
+ `PluginUiRegistration`, `PluginPageProps` and the `PLUGIN_UI_API_VERSION` constant. It also augments
13
+ `Window`, so plugin sources typecheck against the real surface.
14
+
15
+ ## Building a bundle
16
+
17
+ ```js
18
+ import { buildPluginUiBundle } from 'elowen-plugin-ui-kit/build';
19
+ await buildPluginUiBundle({ entry: 'web-src/index.tsx', outfile: 'web/index.js' });
20
+ ```
21
+
22
+ or from a script: `elowen-plugin-ui-build web-src/index.tsx web/index.js`.
23
+
24
+ The build bundles everything into one ESM file and aliases `react`, `react-dom` and
25
+ `react/jsx-runtime` imports to shims reading the HOST's instances from `window.ElowenUiRuntime` — a
26
+ bundle can never ship a second React. Content-hashing of the output URL is the daemon's job; the
27
+ build just emits the file the plugin manifest's `web.entry` points at.
package/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { buildPluginUiBundle } from './build.js';
3
+
4
+ const [entry, outfile] = process.argv.slice(2);
5
+ if (!entry || !outfile) {
6
+ console.error('usage: elowen-plugin-ui-build <entry> <outfile>');
7
+ process.exit(2);
8
+ }
9
+ await buildPluginUiBundle({ entry, outfile });
package/build.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface BuildPluginUiBundleOptions {
2
+ /** The bundle entry point (index.tsx/ts/jsx/js of the plugin's web sources). */
3
+ entry: string;
4
+ /** Where to write the single built ESM file (the plugin manifest's `web.entry`). */
5
+ outfile: string;
6
+ /** Minify the output (default false — the daemon serves it immutably either way). */
7
+ minify?: boolean;
8
+ /** Extra module resolution roots (NODE_PATH-style) for dependencies that live outside the plugin's
9
+ * own tree — e.g. the host web app's node_modules for icon libraries. */
10
+ nodePaths?: string[];
11
+ }
12
+
13
+ /** Bundle `entry` into the ESM file at `outfile`. Throws on any build error. */
14
+ export declare function buildPluginUiBundle(options: BuildPluginUiBundleOptions): Promise<void>;
package/build.js ADDED
@@ -0,0 +1,34 @@
1
+ /** esbuild toolchain for plugin browser-UI bundles: one TS/TSX/JS entry → one self-contained
2
+ * same-origin ESM file the daemon serves on a content-hash URL (the daemon hashes it — the build
3
+ * emits a plain `index.js`). React imports are aliased to shims that read the HOST's instance from
4
+ * `window.ElowenUiRuntime`, so a bundle can never ship a second React (two copies break hooks). */
5
+ import { build } from 'esbuild';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const shim = (name) => fileURLToPath(new URL(`./shims/${name}.cjs`, import.meta.url));
9
+
10
+ /** Bundle `entry` into the ESM file at `outfile`. Throws on any build error. */
11
+ export async function buildPluginUiBundle({ entry, outfile, minify = false, nodePaths }) {
12
+ await build({
13
+ entryPoints: [entry],
14
+ outfile,
15
+ // Extra module resolution roots — e.g. the host web app's node_modules, so a bundle may use the
16
+ // SAME icon/library versions the app ships without duplicating them in the repo root.
17
+ ...(nodePaths && nodePaths.length > 0 ? { nodePaths } : {}),
18
+ bundle: true,
19
+ format: 'esm',
20
+ platform: 'browser',
21
+ // Modern evergreen browsers — the web app itself targets the same class of engines.
22
+ target: ['es2022'],
23
+ jsx: 'automatic',
24
+ minify,
25
+ legalComments: 'none',
26
+ alias: {
27
+ // esbuild picks the LONGEST matching alias, so the jsx-runtime entries win over plain `react`.
28
+ react: shim('react'),
29
+ 'react-dom': shim('react-dom'),
30
+ 'react/jsx-runtime': shim('jsx-runtime'),
31
+ 'react/jsx-dev-runtime': shim('jsx-runtime'),
32
+ },
33
+ });
34
+ }
package/index.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /** Contract types for Elowen plugin browser UIs — the ONE source of truth shared by the web app
2
+ * (which installs `window.ElowenUiRuntime` and consumes registrations) and plugin bundles (which
3
+ * read the runtime and call `window.__elowenRegisterPluginUi`). The web app's `web/lib/pluginUi.tsx`
4
+ * imports these types; keep the two sides in lockstep by editing THIS file only. */
5
+ import type * as React from 'react';
6
+ import type * as ReactDom from 'react-dom';
7
+ import type * as JsxRuntime from 'react/jsx-runtime';
8
+ import type { ComponentType } from 'react';
9
+
10
+ /** See index.js — bump on incompatible changes to `ElowenUiRuntime`. Deliberately a LITERAL type:
11
+ * the web app re-declares the value and annotates it with `typeof PLUGIN_UI_API_VERSION`, so a kit
12
+ * bump that forgets the host fails the web typecheck instead of drifting silently. */
13
+ export declare const PLUGIN_UI_API_VERSION: 1;
14
+
15
+ /** Props every plugin page/settings component receives. */
16
+ export interface PluginPageProps {
17
+ plugin: string;
18
+ /** Path params captured by `:name` segments of the matched route pattern. */
19
+ params: Record<string, string>;
20
+ /** The raw path segments under /p/<plugin>/. */
21
+ rest: string[];
22
+ /** Where this component is mounted. A settings component renders in BOTH places: inside the Settings
23
+ * deck, where the surrounding panel already names the section, and as a standalone page at
24
+ * /p/<plugin>, where nothing else does — so on a page it owes the reader a page header of its own
25
+ * (`components.PluginPageHeader`) and must not repeat the section title inside its card. */
26
+ surface: 'page' | 'deck';
27
+ /** Report an autosave state to the surrounding surface, which owns the shared indicator (status plus
28
+ * the Retry a failed save needs): the page masthead for a section reached at /p/<plugin>, the deck
29
+ * header for a host that mounts sections in a settings deck. A section that renders its own indicator
30
+ * inside a group header can ignore it; a section declaring `layout: 'orbital'` cannot — the orbital
31
+ * group is a field of pods with no header to hold one, so this channel is the only place its user
32
+ * ever learns that a save failed. */
33
+ onSaveState?: (status: 'idle' | 'saving' | 'saved' | 'error', retry?: () => void) => void;
34
+ }
35
+
36
+ /** What a bundle hands to window.__elowenRegisterPluginUi. Routes are `/`-joined segment patterns
37
+ * (`''` = the root page, `detail/:id` captures params). `settings` components are keyed by the
38
+ * manifest's `web.settings[].id` and render inside the Settings page's control deck. */
39
+ export interface PluginUiRegistration {
40
+ requiresApiVersion: number;
41
+ pages?: Record<string, ComponentType<PluginPageProps>>;
42
+ settings?: Record<string, ComponentType<PluginPageProps>>;
43
+ }
44
+
45
+ /** The host API surface a bundle finds on `window.ElowenUiRuntime`: the HOST's React instance (a
46
+ * bundle must never ship its own — the build aliases `react` imports here), a curated set of the
47
+ * app's UI components, an authenticated same-origin `api` fetch, and SPA navigation. */
48
+ export interface ElowenUiRuntime {
49
+ apiVersion: number;
50
+ react: typeof React;
51
+ reactDom: typeof ReactDom;
52
+ jsxRuntime: typeof JsxRuntime;
53
+ components: Record<string, ComponentType<never>>;
54
+ /** Curated React hooks (i18n, toasts, the app's react-query data hooks). Safe across the boundary:
55
+ * the bundle runs on the HOST's React instance, so the rules of hooks hold. A bundle narrows each
56
+ * entry to the signature it expects; an absent name means the host predates the bundle. */
57
+ hooks: Record<string, unknown>;
58
+ /** Curated pure helpers (formatting, session/task mapping, error shaping) shared with bundles. */
59
+ utils: Record<string, unknown>;
60
+ api: (path: string, init?: RequestInit) => Promise<unknown>;
61
+ navigate: (href: string) => void;
62
+ }
63
+
64
+ declare global {
65
+ interface Window {
66
+ ElowenUiRuntime?: ElowenUiRuntime;
67
+ __elowenRegisterPluginUi?: (plugin: string, registration: PluginUiRegistration) => void;
68
+ }
69
+ }
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /** The single source of truth for the plugin browser-UI contract version. The web app installs
2
+ * `window.ElowenUiRuntime` stamped with this number; a bundle whose `requiresApiVersion` is NEWER
3
+ * renders a placeholder instead of executing against a contract it was not built for. Bump on
4
+ * incompatible changes to the `ElowenUiRuntime` surface (see index.d.ts). */
5
+ export const PLUGIN_UI_API_VERSION = 1;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "elowen-plugin-ui-kit",
3
+ "version": "0.1.0",
4
+ "description": "Contract types and esbuild toolchain for building Elowen plugin browser-UI bundles.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./build": {
14
+ "types": "./build.d.ts",
15
+ "default": "./build.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "elowen-plugin-ui-build": "./bin.js"
20
+ },
21
+ "files": [
22
+ "index.js",
23
+ "index.d.ts",
24
+ "build.js",
25
+ "build.d.ts",
26
+ "bin.js",
27
+ "shims/",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "@types/react": "^19.2.0",
32
+ "@types/react-dom": "^19.2.0",
33
+ "esbuild": "^0.28.2"
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ // See shims/react.cjs — same trick for `react/jsx-runtime` (and jsx-dev-runtime), which esbuild's
2
+ // `jsx: 'automatic'` mode imports from every file containing JSX.
3
+ const runtime = typeof window !== 'undefined' ? window.ElowenUiRuntime : undefined;
4
+ if (!runtime) throw new Error('elowen-plugin-ui-kit: window.ElowenUiRuntime is missing — plugin bundles only run inside the Elowen web app');
5
+ module.exports = runtime.jsxRuntime;
@@ -0,0 +1,6 @@
1
+ // See shims/react.cjs — same trick for `react-dom`. Note there is deliberately NO shim for
2
+ // `react-dom/client`: a plugin never owns a root (the host renders its components), so an import of
3
+ // createRoot should fail the build loudly instead of resolving to undefined at runtime.
4
+ const runtime = typeof window !== 'undefined' ? window.ElowenUiRuntime : undefined;
5
+ if (!runtime) throw new Error('elowen-plugin-ui-kit: window.ElowenUiRuntime is missing — plugin bundles only run inside the Elowen web app');
6
+ module.exports = runtime.reactDom;
@@ -0,0 +1,7 @@
1
+ // Compiled INTO every plugin bundle in place of `react` (see build.js): the plugin must render with
2
+ // the HOST's React instance — a second copy breaks hooks and context. CJS on purpose: esbuild turns
3
+ // named ESM imports from a CJS module into property accesses, so `import { useState } from 'react'`
4
+ // works without enumerating React's exports here.
5
+ const runtime = typeof window !== 'undefined' ? window.ElowenUiRuntime : undefined;
6
+ if (!runtime) throw new Error('elowen-plugin-ui-kit: window.ElowenUiRuntime is missing — plugin bundles only run inside the Elowen web app');
7
+ module.exports = runtime.react;