On this page
Build and Package
Foxel loads built IIFE scripts; it does not execute TypeScript, JSX, or a Vite development server directly. A .foxpkg is a ZIP archive with manifest.json at the root, without an enclosing project folder.
Vite configuration
This follows the official plugins' Vite 8 setup and works with the complete example:
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
publicDir: false,
define: { "process.env.NODE_ENV": JSON.stringify("production") },
build: {
outDir: "frontend",
emptyOutDir: false,
sourcemap: false,
cssCodeSplit: false,
lib: {
entry: "src/index.tsx",
formats: ["iife"],
name: "FoxelJsonViewer",
fileName: () => "index.js",
cssFileName: "player",
},
rolldownOptions: {
external: ["react", "react-dom", "react-dom/client", "antd", "@ant-design/icons"],
output: {
globals: {
react: "window.__FOXEL_EXTERNALS__.React",
"react-dom": "window.__FOXEL_EXTERNALS__.ReactDOM",
"react-dom/client": "window.__FOXEL_EXTERNALS__.ReactDOM",
antd: "window.__FOXEL_EXTERNALS__.antd",
"@ant-design/icons": "window.__FOXEL_EXTERNALS__.AntdIcons",
},
},
},
},
});
Vite's publicDir: false disables automatic public-directory copying. The manifest's frontend.publicDir: "frontend" defines Foxel's public asset directory. These are separate settings.
emptyOutDir: false preserves icons placed manually in frontend/; remove stale files when renaming or deleting assets. CSS becomes frontend/player.css and must be listed in manifest styles.
Current official plugins use Vite 8 rolldownOptions. Older Vite projects should use the configuration supported by their version, including rollupOptions where appropriate. Externalize React, ReactDOM, Ant Design, and icons when used; keep react/jsx-runtime bundled because the host has no global for it.
Build checks
The complete example defines these scripts:
bun install
bun run typecheck
bun run build
bun run foxpkg
foxpkg rebuilds before packaging, so routine use can be bun run typecheck followed by bun run foxpkg. Commands such as dev, clean, and build.sh are project-specific, not provided by the host.
Check IIFE output, registration, external mappings, stylesheets, and other assets. Foxel has no requirement that bundles stay below 50 KB. Inspect accidental duplication of shared dependencies rather than treating a fixed size as proof of correctness.
Standalone packer
Save this as pack.py in the plugin root. It uses only the Python standard library, packages manifest.json plus existing frontend/, backend/, and assets/ files, and fails on missing declared files.
import json
import re
import zipfile
from pathlib import Path, PurePosixPath
ROOT = Path(__file__).resolve().parent
manifest = json.loads((ROOT / "manifest.json").read_text(encoding="utf-8"))
key = manifest["key"]
if not re.fullmatch(r"[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+", key):
raise SystemExit("Invalid plugin key")
if manifest.get("foxpkg") != "1.0":
raise SystemExit("Unsupported foxpkg format")
files = {"manifest.json"}
for folder in ("frontend", "backend", "assets"):
for path in (ROOT / folder).rglob("*"):
if path.is_symlink():
raise SystemExit(f"Symlink is not allowed: {path}")
if path.is_file() and "__pycache__" not in path.parts:
files.add(path.relative_to(ROOT).as_posix())
frontend = manifest.get("frontend") or {}
required = [frontend.get("entry"), *(frontend.get("styles") or []),
*(frontend.get("assets") or [])]
icon = frontend.get("icon")
if icon and not icon.startswith(("/", "http://", "https://")):
required.append(icon)
backend = manifest.get("backend") or {}
for group in ("routes", "processors"):
required.extend(item["module"] for item in backend.get(group) or [])
for name in files | {value for value in required if value}:
path = PurePosixPath(name)
if path.is_absolute() or ".." in path.parts or "\\" in name:
raise SystemExit(f"Invalid archive path: {name}")
for name in required:
if name and name not in files:
raise SystemExit(f"Missing package file: {name}")
out = ROOT / "dist" / f"{key}.foxpkg"
out.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name in sorted(files):
archive.write(ROOT / name, name)
with zipfile.ZipFile(out) as archive:
bad = archive.testzip()
if bad:
raise SystemExit(f"Corrupt archive member: {bad}")
print(out)
Adapt the included directories if you choose a different resource layout. These basic checks do not replace full Foxel manifest and compatibility validation. Even public assets outside the default directory must be included in the archive.
Source files, README files, and build configuration may be included; the seven official plugins include them. The host does not prohibit src/. Exclude node_modules/, caches, old dist/, and local environment configuration.
Official repository packer requirements
The seven official Foxel-Studio plugins provide bun run foxpkg, but their current pack.py finds the Foxel root through ROOT.parents[1] and imports domain.plugins for validation:
- Frontend installation, type checks, and builds can run in a standalone clone.
- Their original
pack.pyneeds theplugins/<key>/layout under the Foxel repository and a prepared Foxel Python environment. - Independently maintained plugins can use the standard-library packer above, then validate separately in Foxel.
From the Foxel repository root:
# From the Foxel repository root; replace the package path.
uv run python - /absolute/path/com.example.jsonviewer/dist/com.example.jsonviewer.foxpkg <<'PYTHON'
import sys
from pathlib import Path
from domain.plugins.compatibility import check_compatibility
from domain.plugins.loader import PluginLoader
manifest, staged = PluginLoader.unpack_foxpkg(Path(sys.argv[1]).read_bytes())
try:
check_compatibility(manifest)
print("Package and compatibility checks passed:", manifest.key)
finally:
import shutil
shutil.rmtree(staged)
PYTHON
This validates and extracts to a temporary staging directory, then removes it. It neither installs the plugin nor executes backend modules. Check backend exports, processor conflicts, and behavior through an actual installation.
Local installation and debugging
- Upload the
.foxpkgon Foxel's plugin page using an account with plugin management permission. - Inspect the result's
success, not just HTTP 200. Readmessage/errorson failure. - Inspect
statusandlast_error. States areloading,active,disabled, andfailed; frontends load only active plugins. - Open an associated file, or use a standalone entry with both
openApp: trueandmountApp. - Select the plugin iframe in browser developer tools for Console and Network checks; inspect Foxel process logs for backend errors.
- After edits, rebuild, repackage, reinstall, close old windows, and reopen.
Updates switch versions under the same key. Previously disabled plugins remain disabled. Failed activation attempts preserve the previous working version where possible; inspect installation results or last_error. Do not use direct edits to installed version directories as an update workflow.
Publish to App Center
Local Foxel installation and this site's App Center have different validation requirements. This site uses lib/plugins/foxpkg.ts and requires:
| Field | Upload requirement |
|---|---|
foxpkg | Explicit, exactly "1.0" |
key, name, description, author, website | Nonempty strings; key should also satisfy Foxel naming rules |
version | Exactly three numeric segments, such as 1.0.0, without v or prerelease suffixes |
i18n.en.name, i18n.en.description | Nonempty strings |
Prepare the archive, description, release notes, and screenshots, then submit through the Developer Center review workflow. Passing upload validation does not prove runtime compatibility; install locally first. Update manifest.version for each release, preferably alongside package.json.
The upload form also requires Chinese and English tags. Updating an existing plugin requires ownership of its key, a unique version higher than the latest uploaded version, and release notes.
Troubleshooting
| Symptom | Check |
|---|---|
| Missing manifest | manifest.json must be at the ZIP root |
| Missing route module | Use archive path backend/routes/api.py and include the file |
| Missing processor type / factory | Declare manifest type; export PROCESSOR_FACTORY in Python |
| Waiting after script load | Inspect iframe errors and confirm FoxelRegister is called |
createRoot / hooks errors | Check external mappings and duplicated React |
| Missing styles | Check frontend.styles, emitted filenames, resource requests |
API 404 with /api/api | Remove /api from host.callApi paths |
| Resource 404 / 409 | Check public paths and revision; reopen after reinstalling |
ModuleNotFoundError: domain | Official packer is outside the Foxel layout or using the wrong Python environment |
| Local install works, upload fails | Complete English metadata, author, website, and a three-part numeric version |
Next: complete example.