150 lines
6.8 KiB
Markdown
150 lines
6.8 KiB
Markdown
# Common pitfalls and how to avoid them
|
|
|
|
These are real issues discovered during plugin development. Read before writing
|
|
plugin logic — each one will save you debugging time.
|
|
|
|
## Pitfall 1: Importing `@teable/*` packages instead of using host globals
|
|
|
|
```typescript
|
|
// WRONG — webpack externals map these away at build time, so at runtime
|
|
// the import produces `undefined` or throws:
|
|
import { useFields } from '@teable/sdk'; // → undefined at runtime
|
|
import { Button } from '@teable/ui-lib'; // → undefined at runtime
|
|
import { definePlugin } from '@teable/plugin-sdk'; // fine (it re-exports from host)
|
|
|
|
// CORRECT — always read from the host global:
|
|
const host = window.__TEABLE_HOST__;
|
|
const { useFields } = host.SDK;
|
|
const { Button } = host.UILib;
|
|
const { definePlugin } = host.PluginSDK;
|
|
const React = host.React;
|
|
```
|
|
|
|
The webpack `externals` config in the scaffold template maps `@teable/sdk` →
|
|
`window.__TEABLE_HOST__.SDK` at build time. This means the bundled code replaces
|
|
the import with `window.__TEABLE_HOST__.SDK`, but destructuring from the import
|
|
statement doesn't survive the webpack transformation — use direct `host.*`
|
|
access instead. The only exception is `@teable/plugin-sdk` which re-exports
|
|
from `window.__TEABLE_HOST__.PluginSDK` at the level webpack expects.
|
|
|
|
**Symptoms**: `useFields is not a function`, `Cannot read properties of undefined`,
|
|
or components silently rendering nothing.
|
|
|
|
## Pitfall 2: `window.__TEABLE_HOST__` is undefined
|
|
|
|
The host object is injected by `HostInitializer` which mounts inside
|
|
`<AppProvider>`. This happens after the initial React tree render. If you try
|
|
to access `window.__TEABLE_HOST__` in a script that executes before the host
|
|
tree mounts, it will be `undefined`.
|
|
|
|
**Fix**: The `PluginLoader` component ensures the host is ready before loading
|
|
plugin scripts, so plugins loaded through the normal flow don't hit this. But
|
|
if you're testing a plugin directly (e.g. loading main.js in a browser console),
|
|
you'll see `__TEABLE_HOST__` is null until the app renders.
|
|
|
|
## Pitfall 3: Upload returns 403 — not a developer
|
|
|
|
Only users in the `plugin_developer` DB table can upload bundles. The
|
|
`DeveloperGuard` checks this. If you get 403 on `upload-bundle`, your user is
|
|
not whitelisted.
|
|
|
|
**Fix**: Add the user to `plugin_developer`:
|
|
|
|
```sql
|
|
INSERT INTO plugin_developer (id, user_id, email, created_by)
|
|
VALUES (gen_random_uuid(), '<userId>', '<email>', '<adminUserId>');
|
|
```
|
|
|
|
## Pitfall 4: Enable (plugin-config) must be called before install
|
|
|
|
Installing a plugin to a position (contextMenu / panel / view / dashboard)
|
|
requires the plugin to be enabled first. If you skip enable and call install
|
|
directly, you'll get 403.
|
|
|
|
**The correct sequence is always**:
|
|
1. Upload bundle → get pluginId
|
|
2. Enable via `PUT /base/:baseId/plugin-config/:pluginId`
|
|
3. Install to position
|
|
|
|
## Pitfall 5: `getSelectionRecords` returns empty array, not throw
|
|
|
|
The old iframe/ZIP `bridge.getSelectionRecords()` threw when nothing was
|
|
selected. The dynamic system's `PluginSDK.getSelectionRecords()` returns an
|
|
empty array instead. You still need to handle the empty case in your UI,
|
|
but you don't need a try/catch wrapper.
|
|
|
|
```typescript
|
|
const { getSelectionRecords } = host.PluginSDK;
|
|
const res = await getSelectionRecords(tableId, viewId, selection);
|
|
if (res.records.length === 0) {
|
|
// Show "no records selected" message — no try/catch needed
|
|
}
|
|
```
|
|
|
|
## Pitfall 6: Plugin component renders nothing / context menu empty
|
|
|
|
If your plugin uploads, enables, and installs successfully but the component
|
|
never appears:
|
|
|
|
1. **Check `plugin.json` `positions`** — it must include the position you're
|
|
installing to (e.g. `"positions": ["contextMenu"]`)
|
|
2. **Check `onLoad` registration** — `ctx.addContextMenuAction('main', Component)`
|
|
must be called inside `onLoad`
|
|
3. **Check Authority Matrix** — the user's role must have the plugin enabled
|
|
in AM. For the test account, the owner bypasses AM, but for other users,
|
|
an admin must enable the plugin per role in the RoleEditor UI.
|
|
4. **Check browser console** — errors in `onLoad` are silently caught; add a
|
|
`console.log` at the top of `onLoad` to verify it runs.
|
|
|
|
## Pitfall 7: Component uses JSX but webpack doesn't transpile it
|
|
|
|
The scaffold template uses `ts-loader` with `jsx: "react-jsx"` in tsconfig.
|
|
If you're setting up the webpack config manually, make sure:
|
|
- `tsconfig.json` has `"jsx": "react-jsx"` under `compilerOptions`
|
|
- The plugin does NOT need `import React from 'react'` at the top when using
|
|
`react-jsx` transform — JSX is auto-transpiled. But you still need
|
|
`const React = host.React` for any explicit `React.createElement` calls.
|
|
|
|
## Pitfall 8: Chunk 404 at runtime — multipart strips directory names (2026-10)
|
|
|
|
`webpack output.chunkFilename: 'static/js/[name].chunk.js'` builds fine, the
|
|
upload succeeds, but the browser 404s the chunk: multer/busboy flattens
|
|
multipart filenames, so the server stored `469.chunk.js` while the bundle
|
|
requests `static/js/469.chunk.js`.
|
|
|
|
**Fix**: keep chunk names FLAT — `chunkFilename: '[name]-[contenthash].chunk.js'`
|
|
(contenthash prevents collisions). Public path still works:
|
|
`output.publicPath: '/api/plugin/<id>/runtime/'`.
|
|
|
|
## Pitfall 9: `Bundle must be IIFE format` after a successful build
|
|
|
|
Three producer-side causes, all seen in the wild:
|
|
1. **Terser license banner** — bundling any npm package with license comments
|
|
makes webpack emit `/*! For license information... */` BEFORE `!function`.
|
|
Fix: `optimization.minimizer: [new TerserPlugin({ extractComments: false })]`.
|
|
2. **Development-mode webpack** output starts with `/******/` banner comments.
|
|
The dev watch loop therefore builds with `--mode production`.
|
|
3. **Vite/esbuild IIFE** output starts `var <name>=(function...` — the
|
|
validator accepts it (since 2026-10); no post-processing needed.
|
|
|
|
## Pitfall 10: vite scaffold `pnpm install` fails with ERR_PNPM_IGNORED_BUILDS
|
|
|
|
pnpm ≥ 10/11 blocks esbuild's postinstall and (on patched hosts) treats it as
|
|
fatal. Fix (scaffold generates it): `pnpm-workspace.yaml` with
|
|
`allowBuilds:\n esbuild: true`. esbuild also works without its script (binary
|
|
comes from optionalDependencies) — only the pnpm gate needs the allow entry.
|
|
|
|
## Pitfall 11: PAT gets 403 fetching `/api/plugin/:id/runtime/*`
|
|
|
|
Access tokens only pass routes with `@TokenAccess()` or `@Permissions` — the
|
|
runtime asset serving has neither. This is by design (the browser loads with a
|
|
session). For curl verification use a signed `auth_session` cookie; for uploads
|
|
use the PAT (`upload-bundle` and `POST /api/plugin` are token-accessible).
|
|
|
|
## Pitfall 12: "react/jsx-runtime would duplicate React" worry
|
|
|
|
It does not. The jsx automatic transform imports `react/jsx-runtime`, which is
|
|
bundled into the plugin on purpose: it only creates plain element objects.
|
|
Hooks/context/state go through the externalized `react` → host singleton. This
|
|
is exactly how the deployed ticket-console/batch-edit-pro bundles behave.
|