@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 +143 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +122 -0
- package/package.json +47 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|