Add a format end to end
This page takes a new file format from "Poka shows nothing" to a preview in both the desktop app and the web app. It covers the whole route across the four repositories and says which delivery path to choose. The detailed craft of writing the parser is in Write a parser; this page is the map around it.
You need the four repositories checked out as siblings under one parent
directory (poka-interfaces, poka-parsers, poka-viewers, poka-samples),
and a working Rust toolchain with the wasm32-wasip1 target
(rustup target add wasm32-wasip1). The scripts in this repository find the
siblings through scripts/_poka_paths.py; python3 scripts/_poka_paths.py
shows what they resolve on your machine.
1. Check whether the format is already handled
- Look the extension up in
FILE_FORMATS.csvin poka-parsers, and in the route tableweb/src/api/formats.json. - If the extension is routed, open a real file in the app first. If it previews correctly, there is nothing to add.
- If a module already reads the format under another name, you want an alias, not a new crate: see Route an extension to an existing parser.
2. Choose a delivery path
| Path | Use it when | Where the work lives |
|---|---|---|
| A. Host plain-text list | The format is plain text or text-based markup and a module would only read the file and show it | desktop/src-tauri/src/code_exts.rs in this repository |
| B. Native host handler | The webview can draw the bytes itself (PNG, JPEG, WebP, AVIF, SVG, PDF), or a required Rust crate will not compile to wasm32-wasip1 |
desktop/src-tauri/src/commands.rs in this repository |
| C. Dedicated module | One format that needs its own decoder and a response only it can draw | a new crate in poka-parsers, emitting a PreviewResponse |
| D. Shared module | Several formats decode through one library | one crate in poka-parsers, several manifests (or aliases) pointing at the same .wasm |
| E. Parser plus shared viewer | The format is table, tree, text, hex, metadata card, 3D scene or similar | a new crate in poka-parsers that emits an *Ast; the viewer already exists in poka-viewers |
| F. Parser plus viewer, several tabs | One file has several natural views (a model beside its tree, one drawing per page) | as E, but the parser answers with a MultiView |
Most new formats are C, D, E or F. Prefer E: the parser then ships no HTML or CSS and inherits the host theme. Path B is a last resort, because every host-side handler needs a full desktop rebuild and has to be kept in step in both engines.
Path A needs care. The host checks its plain-text list before the module
registry, so an extension in CODE_EXTS shadows any module that is added for it
later. Use it only for formats that will stay plain text, and never for a format
that has its own geometry or structure to draw.
The order in which the host decides is: native handlers, then the plain-text list, then the module registry (How a preview is produced).
3. Write the module (paths C, D, E, F)
Work in poka-parsers and follow
Write a parser. In short:
- Look at real files from
poka-samplesbefore writing any code. - Create
parsers/poka-<ext>/withCargo.toml,plugin.json,src/lib.rsand a thinsrc/main.rs. The crate depends on the sharedpoka-preview-protocrate; it carries nobuild.rsof its own and no copy of the proto. - Pick the AST that fits (Choosing an AST). Bound everything: bytes read, rows or nodes emitted, recursion depth.
- For path E, set
"viewer"inplugin.jsonto the id of an existing shared viewer. For path C, leaveviewerout. - For path D, list the extra extensions in
aliases, or add a line toparsers/aliases.tomlwhen the format needs its own id or name. - For path F, answer with a
PreviewResponsewhose type isTYPE_MULTIVIEW. Each tab has atitleand one ofscene3d,svg,html,dwg(shown as they are) orviewer_input(the bytes of an*Astof the type the manifest'sviewerdecodes; both hosts run that viewer once for the tab). A tab with no viewer to run through is dropped, and the host adds a Source tab when the file is text. A change to this mechanism lands in both hosts (extension_host.rsandweb/src/engine-core.js); the shared fixture isscripts/fixtures/viewer-tabs.json. - If the format needs a field the schema lacks, change
core/proto/preview.protohere first; see Changing the proto below.
Set memory_mb and max_time_ms from the largest real sample, not a guess. They
are limits the host enforces.
4. Add a sample to the corpus
Add a real, redistributable file to poka-samples (flat, in sample files/)
with a row in its PROVENANCE.csv. Never add personal or client data, or a file
you may not redistribute. Follow that repository's CONTRIBUTING.md.
5. Build and install
# build the module (from the poka-parsers repository root, so its .cargo/config.toml applies)
cd ../poka-parsers
cargo build -p poka-<ext> --target wasm32-wasip1 --release
# desktop: link the built wasm into the extensions directory
cd ../poka-interfaces
python3 scripts/install-extensions.py
# web: copy built modules into web/extensions/ for the local web app
python3 scripts/install-web-extensions.py
install-extensions.py installs by symlink, so rebuilding a module is picked up
without installing again. Do not set CARGO_TARGET_DIR: each sibling repository
pins one shared target directory in its own .cargo/config.toml, and the
variable overrides that pin. See
Build and run from source for the rest of the setup.
6. Test
From poka-parsers:
cargo test -p poka-<ext> # unit tests
python3 scripts/corpus-tests.py poka-<ext> # the corpus suite
python3 scripts/test-manifests.py # every plugin.json, including the new one
From this repository:
python3 scripts/test-parsers.py poka-<ext> # run the installed parser over the corpus
python3 scripts/test-parsers-robustness.py poka-<ext> # empty, zero, 0xFF, random and truncated input: no panic, no hang
Then open a sample of the format in the desktop app (poka <file>) and in the
local web app (cd web && cargo run, then drop the file on
http://127.0.0.1:21220). The two must agree. For the engine-level gates
(node scripts/test-client-engine.mjs --strict, node scripts/test-parity.mjs)
see "Tests" in AGENTS.md.
7. Update the registry and the route table
- In poka-parsers, run
python3 scripts/sync-file-formats.py --apply. It adds or updates the format's row inFILE_FORMATS.csv, and computes the derived columns. Never edit those by hand. - If the format was a line in
parsers/unsupported.toml, delete the line in the same change. - The web route table
web/src/api/formats.jsonis regenerated from the pinned parser and viewer releases, not from your working tree: a new module reaches poka.sh when poka-parsers cuts a release and the pin inweb/extension-releases.jsonis bumped. Runpython3 scripts/fetch-extensions.pyandpython3 scripts/gen_formats_json.pyafter a bump, and commitformats.jsonwith it.
Routing by content
If the new format shares an extension with others, do not claim the extension a
second time: one handler per extension. A shared extension is routed by content.
Add a signature to web/src/content-signatures.json (an .xml is routed by its
root element with xml_root, and by namespace with xml_ns where the root name
is shared), and a case to scripts/fixtures/routing-cases.json. Both hosts read
the same signatures. Poka-parsers' scripts/xml-roots.py lists the XML roots in
the corpus.
Changing the proto
core/proto/preview.proto in this repository is the canonical copy.
poka-parsers and poka-viewers each carry a vendored copy that is kept in step by
hand. After you change the canonical file, copy it into both siblings'
core/proto/ and rebuild the crates that use it. Never check in a generated
poka.preview.rs. scripts/check-interface-parity.py guards the hosts here;
scripts/check-proto-drift.py in poka-parsers guards against a second proto copy
appearing inside a repository.
Things that catch people out
- A viewer id must exist. The
viewervalue must be the id of a crate underpoka-viewers/viewers/;test-manifests.pychecks it. - Licences. No GPL or LGPL code in a wasm module. A native LGPL library may
be loaded at run time as a separate library, never linked or bundled. The
workspaces run
cargo deny. - Disabled default features. Many crates pull in C-bound codecs that do not
link for
wasm32-wasip1. Start withdefault-features = falseand add the minimum. - Generated output. A module that generates a mesh (tessellated from
STEP, say) returns it as a
Scene3DAst, never as file bytes for the host to decode:ModelPreviewis retired (poka-parsers#453). A generated raster goes inImagePreview.data.