Foxel
Menu
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:

typescript
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:

bash
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.

python
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.py needs the plugins/<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:

bash
# 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

  1. Upload the .foxpkg on Foxel's plugin page using an account with plugin management permission.
  2. Inspect the result's success, not just HTTP 200. Read message / errors on failure.
  3. Inspect status and last_error. States are loading, active, disabled, and failed; frontends load only active plugins.
  4. Open an associated file, or use a standalone entry with both openApp: true and mountApp.
  5. Select the plugin iframe in browser developer tools for Console and Network checks; inspect Foxel process logs for backend errors.
  6. 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:

FieldUpload requirement
foxpkgExplicit, exactly "1.0"
key, name, description, author, websiteNonempty strings; key should also satisfy Foxel naming rules
versionExactly three numeric segments, such as 1.0.0, without v or prerelease suffixes
i18n.en.name, i18n.en.descriptionNonempty 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

SymptomCheck
Missing manifestmanifest.json must be at the ZIP root
Missing route moduleUse archive path backend/routes/api.py and include the file
Missing processor type / factoryDeclare manifest type; export PROCESSOR_FACTORY in Python
Waiting after script loadInspect iframe errors and confirm FoxelRegister is called
createRoot / hooks errorsCheck external mappings and duplicated React
Missing stylesCheck frontend.styles, emitted filenames, resource requests
API 404 with /api/apiRemove /api from host.callApi paths
Resource 404 / 409Check public paths and revision; reopen after reinstalling
ModuleNotFoundError: domainOfficial packer is outside the Foxel layout or using the wrong Python environment
Local install works, upload failsComplete English metadata, author, website, and a three-part numeric version

Next: complete example.