@kb-labs/studio-plugin-tools 0.2.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,143 @@
1
+ # @kb-labs/studio-plugin-tools
2
+
3
+ Build tooling for Studio plugin pages (Module Federation remotes).
4
+ Uses Rspack with `@module-federation/enhanced` for native MF support.
5
+
6
+ ## Usage
7
+
8
+ ```typescript
9
+ // rspack.studio.config.mjs
10
+ import { createStudioRemoteConfig } from '@kb-labs/studio-plugin-tools';
11
+
12
+ export default await createStudioRemoteConfig({
13
+ name: 'myPlugin',
14
+ exposes: {
15
+ './MyPage': './src/studio/pages/MyPage.tsx',
16
+ },
17
+ });
18
+ ```
19
+
20
+ ## Plugin page conventions
21
+
22
+ - `export default function MyPage() { ... }` — **default export only**, no named export of the same component
23
+ - Import all UI from `@kb-labs/sdk/studio` — never from `@kb-labs/studio-ui-kit` directly
24
+ - No top-level side effects in the page module
25
+
26
+ ## Available hooks
27
+
28
+ All hooks are available via `import { ... } from '@kb-labs/sdk/studio'`:
29
+
30
+ | Hook | Purpose |
31
+ |------|---------|
32
+ | `useData<T>(endpoint, opts?)` | REST GET with caching and polling |
33
+ | `useMutateData<I,O>(endpoint, method?)` | REST POST/PUT/PATCH/DELETE mutations |
34
+ | `useSSE<T>(endpoint, opts?)` | Server-sent events with reconnect |
35
+ | `useInfiniteData<T>(endpoint, opts)` | Cursor/offset pagination |
36
+ | `useWebSocket<S,R>(url, opts?)` | Bidirectional WebSocket |
37
+ | `useEventBus()` | Cross-plugin pub/sub communication |
38
+ | `usePermissions()` | Permission checks |
39
+ | `useNavigation()` | Programmatic routing |
40
+ | `useNotification()` | Toast/alert notifications |
41
+ | `useTheme()` | Design tokens and theme mode |
42
+ | `usePage()` | Page context (pageId, pluginId, permissions) |
43
+
44
+ ---
45
+
46
+ ## Troubleshooting
47
+
48
+ ### `Objects are not valid as a React child` / `mountIndeterminateComponent`
49
+
50
+ **Symptom:** Plugin page crashes immediately on first load with one of:
51
+ - `Objects are not valid as a React child (found: object with keys {$$typeof, type, key, ref, props})`
52
+ - `TypeError: Cannot read properties of null (reading 'useRef')`
53
+ - Error points to `mountIndeterminateComponent` in the host's `main.js`
54
+
55
+ These errors all mean one thing: **React version mismatch** between the plugin bundle and the Studio host.
56
+
57
+ **Root cause — React 19 bundled into the plugin:**
58
+
59
+ pnpm resolves `"react": ">=18.0.0"` (from peer deps) to the latest available React, which may be **React 19**. Module Federation requires all remotes to share the **same** React version as the host (which uses React 18). When React 19 code ends up in the plugin bundle, `Symbol.for("react.transitional.element")` is used instead of `Symbol.for("react.element")`, and elements from one React can't be reconciled by the other.
60
+
61
+ **`createStudioRemoteConfig` catches this at build time:**
62
+
63
+ Since v0.1.x, `createStudioRemoteConfig` validates the resolved React version before building. If React 19 is detected, the build fails immediately with a clear error:
64
+
65
+ ```
66
+ ❌ Studio plugin build failed: React 19.x.x detected.
67
+
68
+ Studio host runs React 18. When a plugin bundles React 19, elements use
69
+ Symbol.for('react.transitional.element') which React 18 cannot reconcile,
70
+ causing "Objects are not valid as a React child" crash at runtime.
71
+
72
+ Fix: pin React 18 in your plugin's devDependencies: ...
73
+ ```
74
+
75
+ **Fix:** Pin React 18 in `devDependencies` of the plugin's CLI package:
76
+
77
+ ```json
78
+ // packages/my-plugin-cli/package.json
79
+ {
80
+ "devDependencies": {
81
+ "react": "^18.3.1",
82
+ "react-dom": "^18.3.1"
83
+ }
84
+ }
85
+ ```
86
+
87
+ Then reinstall and rebuild the widget:
88
+
89
+ ```bash
90
+ pnpm --filter @kb-labs/my-plugin-cli install
91
+ pnpm --filter @kb-labs/my-plugin-cli run build:studio
92
+ ```
93
+
94
+ **Reference plugins:** `@kb-labs/commit-cli`, `@kb-labs/agent-cli` — both pin `react@^18.3.1` in devDependencies.
95
+
96
+ ---
97
+
98
+ ### `Objects are not valid as a React child` — stale ui-kit dist
99
+
100
+ **Symptom:** Plugin crashes after ui-kit source was changed, but `studio-ui-kit` dist was not rebuilt.
101
+
102
+ The Studio dev server serves `@kb-labs/studio-ui-kit` from its **compiled dist** (`dist/index.js`), not from TypeScript source. Plugins also bundle a fallback copy of ui-kit from the same dist. If the source is fixed but the dist is stale, both the host's shared scope and the plugin's fallback bundle serve the old broken code.
103
+
104
+ A SubComponent alias like `export const UIDescriptionsItem = AntDescriptions.Item` compiles to an antd internal object — not a React function component. When React tries to render `<UIDescriptionsItem />`, it receives this object as the component type and throws.
105
+
106
+ **Fix:** Rebuild `studio-ui-kit` dist, then restart Studio:
107
+
108
+ ```bash
109
+ pnpm --filter @kb-labs/studio-ui-kit build
110
+ kb-dev restart studio
111
+ ```
112
+
113
+ **Rule:** Any time you change `studio-ui-kit` source, you must rebuild its dist before changes are visible.
114
+
115
+ ---
116
+
117
+ ### Plugin navigation — error from previous plugin "leaks" to next
118
+
119
+ **Symptom:** Navigating from a broken plugin page to a working one — the working page shows the same crash error. Clicking Retry fixes it.
120
+
121
+ **Root cause:** `PageErrorBoundary` is a class component that holds error state. Without a `key` prop on `PageContainer`, React reuses the same boundary instance between routes and the error state persists.
122
+
123
+ **Fix:** Already applied in `plugin-page-v2.tsx` — `PageContainer` renders with `key={`${plugin.remoteName}::${page.entry}`}` so React fully unmounts/remounts on navigation.
124
+
125
+ ---
126
+
127
+ ### Double export causes MF bundling issues
128
+
129
+ **Symptom:** Plugin page crashes with component type errors.
130
+
131
+ **Wrong:**
132
+ ```tsx
133
+ export function MyPage() { ... } // named export
134
+ export default MyPage; // + default export of same component
135
+ ```
136
+
137
+ **Right:**
138
+ ```tsx
139
+ function MyPage() { ... } // no named export
140
+ export default MyPage;
141
+ ```
142
+
143
+ MF expose bundles the module graph starting from the exposed entry. A named export of the same component can confuse the bundler into including two instances of the component in different chunks.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @kb-labs/studio-plugin-tools
3
+ *
4
+ * Build tooling for plugin Studio pages (Module Federation remotes).
5
+ * Uses Rspack with @module-federation/enhanced for native MF support.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // rspack.studio.config.ts
10
+ * const { createStudioRemoteConfig } = require('@kb-labs/studio-plugin-tools');
11
+ *
12
+ * module.exports = createStudioRemoteConfig({
13
+ * name: 'commitPlugin',
14
+ * exposes: {
15
+ * './CommitOverview': './src/studio/pages/CommitOverview.tsx',
16
+ * },
17
+ * });
18
+ * ```
19
+ */
20
+ /**
21
+ * Shared dependencies that match the Studio host.
22
+ * Loaded once by the host — remotes reuse them via MF shared scope.
23
+ */
24
+ declare const STUDIO_SHARED_DEPS: Record<string, {
25
+ singleton: boolean;
26
+ requiredVersion: string;
27
+ }>;
28
+ interface KbStudioRemoteOptions {
29
+ /** Module Federation remote name (must match manifest remoteName) */
30
+ name: string;
31
+ /** Exposed modules: { './PageName': './src/studio/pages/PageName.tsx' } */
32
+ exposes: Record<string, string>;
33
+ /** Override or extend shared deps */
34
+ shared?: Record<string, {
35
+ singleton?: boolean;
36
+ requiredVersion?: string;
37
+ }>;
38
+ /** Remote entry filename (default: 'remoteEntry.js') */
39
+ filename?: string;
40
+ /** Output directory (default: 'dist/widgets') */
41
+ outputDir?: string;
42
+ }
43
+ declare function createStudioRemoteConfig(options: KbStudioRemoteOptions): Promise<Record<string, unknown>>;
44
+
45
+ export { type KbStudioRemoteOptions, STUDIO_SHARED_DEPS, createStudioRemoteConfig };
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ // src/index.ts
2
+ var STUDIO_SHARED_DEPS = {
3
+ react: { singleton: true, requiredVersion: "^18.3.0" },
4
+ "react-dom": { singleton: true, requiredVersion: "^18.3.0" },
5
+ "react-router-dom": { singleton: true, requiredVersion: "^7.0.0" },
6
+ antd: { singleton: true, requiredVersion: "^5.21.0" },
7
+ "@ant-design/icons": { singleton: true, requiredVersion: "^5.4.0" },
8
+ "@tanstack/react-query": { singleton: true, requiredVersion: "^5.0.0" },
9
+ zustand: { singleton: true, requiredVersion: "^5.0.0" },
10
+ "@kb-labs/studio-hooks": { singleton: true, requiredVersion: "^0.1.0" },
11
+ "@kb-labs/studio-event-bus": { singleton: true, requiredVersion: "^0.1.0" },
12
+ "@kb-labs/studio-ui-kit": { singleton: true, requiredVersion: "^0.1.0" },
13
+ "@kb-labs/studio-ui-core": { singleton: true, requiredVersion: "^0.1.0" },
14
+ "@kb-labs/sdk": { singleton: true, requiredVersion: "^0.1.0" }
15
+ };
16
+ async function validateReactVersion(resolve) {
17
+ const fs = await import("fs");
18
+ const reactVersion = await (async () => {
19
+ try {
20
+ const reactPkgPath = resolve("react/package.json");
21
+ const reactPkg = JSON.parse(fs.readFileSync(reactPkgPath, "utf8"));
22
+ return reactPkg.version ?? null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ })();
27
+ if (!reactVersion) {
28
+ return;
29
+ }
30
+ const majorStr = reactVersion.split(".").at(0) ?? "0";
31
+ const major = parseInt(majorStr, 10);
32
+ if (major >= 19) {
33
+ throw new Error(
34
+ `
35
+
36
+ \u274C Studio plugin build failed: React ${reactVersion} detected.
37
+
38
+ Studio host runs React 18. When a plugin bundles React 19, elements use
39
+ Symbol.for('react.transitional.element') which React 18 cannot reconcile,
40
+ causing "Objects are not valid as a React child" crash at runtime.
41
+
42
+ Fix: pin React 18 in your plugin's devDependencies:
43
+
44
+ "devDependencies": {
45
+ "react": "^18.3.1",
46
+ "react-dom": "^18.3.1"
47
+ }
48
+
49
+ Then reinstall and rebuild:
50
+
51
+ pnpm install
52
+ pnpm build:studio
53
+ `
54
+ );
55
+ }
56
+ }
57
+ async function createStudioRemoteConfig(options) {
58
+ const { ModuleFederationPlugin } = await import("@module-federation/enhanced/rspack");
59
+ const path = await import("path");
60
+ const { createRequire } = await import("module");
61
+ const require2 = createRequire(import.meta.url);
62
+ await validateReactVersion(require2.resolve);
63
+ const outputDir = options.outputDir ?? "dist/widgets";
64
+ return {
65
+ entry: {},
66
+ output: {
67
+ path: path.resolve(process.cwd(), outputDir),
68
+ publicPath: "auto",
69
+ clean: true
70
+ },
71
+ resolve: {
72
+ extensions: [".tsx", ".ts", ".jsx", ".js"]
73
+ },
74
+ module: {
75
+ rules: [
76
+ {
77
+ test: /\.tsx?$/,
78
+ exclude: /node_modules/,
79
+ use: {
80
+ loader: "builtin:swc-loader",
81
+ options: {
82
+ jsc: {
83
+ parser: { syntax: "typescript", tsx: true },
84
+ transform: { react: { runtime: "automatic" } }
85
+ }
86
+ }
87
+ }
88
+ },
89
+ {
90
+ test: /\.module\.css$/,
91
+ use: [
92
+ require2.resolve("style-loader"),
93
+ { loader: require2.resolve("css-loader"), options: { modules: true } }
94
+ ]
95
+ },
96
+ {
97
+ test: /\.css$/,
98
+ exclude: /\.module\.css$/,
99
+ use: [require2.resolve("style-loader"), require2.resolve("css-loader")]
100
+ }
101
+ ]
102
+ },
103
+ plugins: [
104
+ new ModuleFederationPlugin({
105
+ name: options.name,
106
+ filename: options.filename ?? "remoteEntry.js",
107
+ exposes: options.exposes,
108
+ shared: {
109
+ ...STUDIO_SHARED_DEPS,
110
+ ...options.shared
111
+ }
112
+ })
113
+ ],
114
+ optimization: {
115
+ minimize: true
116
+ }
117
+ };
118
+ }
119
+ export {
120
+ STUDIO_SHARED_DEPS,
121
+ createStudioRemoteConfig
122
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@kb-labs/studio-plugin-tools",
3
+ "version": "0.2.0",
4
+ "description": "Build tooling for plugin Studio pages — Rspack config for Module Federation remotes",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "clean": "rimraf dist",
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "type-check": "tsc --noEmit",
23
+ "test": "vitest run --passWithNoTests",
24
+ "lint": "eslint . --max-warnings=0"
25
+ },
26
+ "dependencies": {
27
+ "@module-federation/enhanced": "^0.8.0",
28
+ "css-loader": "^7.0.0",
29
+ "style-loader": "^4.0.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@rspack/core": ">=1.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
36
+ "@rspack/core": "^1.7.0",
37
+ "@types/node": "^24.3.3",
38
+ "rimraf": "^6.0.1",
39
+ "tsup": "^8.5.0",
40
+ "typescript": "^5.6.3",
41
+ "vitest": "^3.2.4"
42
+ },
43
+ "engines": {
44
+ "node": ">=20.0.0",
45
+ "pnpm": ">=9.0.0"
46
+ }
47
+ }