8.0 KiB
Converting a project into an Exchange resource
A conversion is a large mechanical rename plus one genuine refactor. It is very easy to produce a project that is perfectly named and completely broken — so the verification gates below are not optional garnish, they are the point.
Check the target shape first with
ignition-exchange-conformance;
this skill is what to do about the findings.
Work on a copy
Never convert in place. Copy the project to its new name, leave the original untouched, and diff against it at the end. The original is your parity baseline — without it you cannot prove you did not drop a feature.
Ignition project folder name is the project name, so the copy's directory must already be the new lowercase-dashed name.
Order matters
Do it in this order. Step 2 invalidates bindings that step 3 would otherwise have to rewrite twice.
- Delete what must not ship. Vision
client-tags/, scratch views, dev-only resources. - Session properties → view properties, while paths are still the familiar old ones. Verify the app still works before touching namespaces.
- Re-namespace views, style classes, scripts, page config — one mechanical sweep.
- Python pass — tabs, camelCase, docstrings, loggers.
- Content sweep — branding, internal names, placeholder text.
- Verify (gates below), then package.
Step 2 is the real refactor
Everything else is search-and-replace. This one changes how the app works.
The guide forbids session custom properties in an Exchange resource because importing merges them into the host project's session props. Replace with:
- State lives on the root view as
customproperties. - Flows down to child views as
params(the guide is explicit:params= public/configuration,custom= internal). - Flows up as page-scoped messages handled on the root view, named
exchange.resourceName.handlerName.
Bindings must stay reactive. Do not park binding-read state in
system.util.getGlobals() — globals do not trigger Perspective binding updates.
Globals are only for non-reactive cross-session data, and most resources need
none.
Before rewriting a binding, check what it actually depended on. A bidirectional
binding that writes a session prop does not necessarily re-trigger the data fetch;
if the fetch keyed off a refreshToken, then an explicit Apply button was already
the commit point and your message-based version preserves behaviour exactly. Read
the dependency graph rather than assuming.
Jython 2.7 constraints
Gateway-scoped code is Jython 2.7. Violations are silent until they reach the gateway.
- No f-strings.
%formatting only. - No
typing,statistics,zoneinfo. - Java exceptions bypass
except Exception:— use bareexcept:withsys.exc_info()in gateway-facing defensive code. - Scripts embedded in
view.jsonare JSON strings that must be tab-indented: the first character of every line in the block is a tab.
Python renames
The guide wants camelCase functions and variables (deliberately not PEP-8) and tab indentation. Two traps:
- Rename through the tokenizer, not regex. A blind replace will corrupt
string literals and attribute names that happen to match (
lt.tm_wday). - Do not rename data-dictionary keys. Keys inside dicts returned to view bindings are a wire format between the scripts and every binding that reads them. They are not covered by any rule and renaming them is a large, risky diff for no conformance gain.
Tab conversion applies to indentation only — bracket continuation lines starting with spaces are fine and normal. Verify with the tokenizer's INDENT tokens, not by grepping for leading spaces.
Verification gates
Run all four. Each catches something the others cannot.
1 · Conformance
python3 skills/ignition-exchange-conformance/exchange_lint.py <new-project>
Target: 0 failures. Warnings are a judgement call.
2 · Referential integrity
python3 skills/ignition-exchange-convert/verify_refs.py <new-project> --old <OldNamespace>
Resolves every embedded view path, style-class reference, view.params.X /
view.custom.X, and every call into the project library against the functions
that actually exist. Also flags leftover old-namespace strings, surviving
session.custom reads, untabbed script transforms, f-strings, and resource.json
drift. Target: 0 breakages.
With ~40 function renames, a missed call site is the single most likely defect, and nothing else finds it.
3 · Feature parity
python3 skills/ignition-exchange-convert/parity.py <original-project> <new-project>
Fingerprints every view — component types and names, bindings by target property and type, script transforms, event and message handlers, embedded views, declared params — with names normalised for case, punctuation and snake/camel. A pure rename cancels out; a dropped feature does not.
Target: zero LOST or MISSING lines. ADDED lines are expected — the
session-props refactor legitimately adds params and handlers. Read the diff, do
not just count it.
4 · Behaviour
If the project has a test suite over its pure modules, run it against the
converted library. Point the package name at the new location and alias
camelCase functions and their keyword arguments back to the original names —
a renamed kwarg (higher_is_worse → higherIsWorse) breaks callers that pass it
by name, which no static check catches.
Compare against the original project's result, not against zero failures. A pre-existing failure staying pre-existing is a pass.
Then: load it on a gateway
Static analysis cannot prove a screen renders. Load the project and open it. A clean project load proves resources parse; only a browser proves components render.
De-branding
Conformance checkers read names, never label text. Sweep user-visible strings
explicitly — props.text, title, placeholder, tooltip — for company names,
project codenames, contest or demo references, internal hostnames, ticket numbers.
Strip the text rather than deleting the component, so layout and parity are
unchanged.
Testing a minimum-version claim
To claim a floor lower than the gateway you built on, load the project on that
version — against a copy of the files. An older gateway rewrites every
resource.json with lastModification metadata and leaves root-owned
.resources/ artifacts behind that your user account cannot delete (clean up
with a throwaway container rather than sudo).
A clean load proves resources parse. It does not prove screens render — a component property introduced in a later version fails at render time, not load time. Open the client.
Reporting
Report what the gates actually said, with numbers. A conversion reported accurately with two known gaps is worth far more than one claimed clean. If a gate is a false positive, say which and why — the tooling is not infallible and both bundled scripts have known blind spots documented in their docstrings.
Provenance
Procedure and both scripts derived from a full conversion of a 20-view / 32-style / 3-module Perspective project (2026-09-15), verified on Ignition 8.1.20 and 8.3.7. The gate thresholds are the ones that actually caught defects during it.