Custom classes
An extension's classes are ordinary Jx
.class.json
descriptors: JSON Schema 2020-12 documents that describe a class — its constructor parameters, fields, and methods — with an optional
$implementation
key pointing at the JavaScript module that implements it. Hosts read the descriptor to learn everything about the class; they import the implementation only to invoke it.
Each class named in the
manifest
's
classes
map becomes a
$prototype
-visible name in any project that enables the extension. A page can then use it as state with no
$src
:
{
"state": {
"about": { "$prototype": "Markdown", "src": "./content/about.md" }
}
}
A descriptor at a glance
The parser's
Markdown.class.json
, heavily abbreviated:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Markdown",
"$prototype": "Class",
"extends": "Object",
"$implementation": "./markdown.js",
"format": { "extensions": [".md"], "mediaType": "text/markdown", "…": "…" },
"$defs": {
"parameters": {
"src": { "identifier": "src", "type": { "type": "string" } },
"…": {}
},
"constructor": {
"role": "constructor",
"parameters": [{ "$ref": "#/$defs/parameters/src" }, "…"],
"body": ["this.config = config;"]
},
"methods": {
"resolve": { "role": "method", "scope": "instance", "identifier": "resolve" },
"parse": { "role": "parse", "scope": "static", "timing": ["compiler", "server", "client"] },
"serialize": { "role": "serialize", "scope": "static", "…": "…" },
"…": {}
}
}
}
The
$defs
object is organized into well-known categories:
| Category | Purpose |
|---|---|
parameters |
Constructor parameter properties (config object fields) |
fields |
Instance fields (private if
#
-prefixed) |
constructor |
Constructor body and super args |
methods |
Instance methods and accessors, plus static capability methods by
role |
returnTypes |
Named return-type schemas for tooling |
The external class contract
Every class — extension-shipped or project-local — satisfies the same runtime contract:
Constructor receives a single
configobject containing allstateproperties except the reserved keywords ($prototype,$src,$export,timing,default,description). For the state entry above,configis{ src: "./content/about.md" }.Value resolution is checked in order:
instance.resolve()(async, awaited) →instance.value(getter or property) → the instance itself.Reactivity is optional : implement
subscribe(callback)/unsubscribe()and the runtime re-renders when you notify.
Methods declare their output shape with a JSON Schema in
returnType
(often a
$ref
into
$defs/returnTypes
). Tooling uses this to reason about a class without executing it — a
resolve
whose
returnType
is an array marks instances as valid sources for mapped iteration, so Studio offers the class in repeater pickers.
$implementation
$implementation
is resolved relative to the
.class.json
file and names the module exporting the class; the export is named by the descriptor's
title
. The parser ships TypeScript in
src/
and points
$implementation
at the built neighbor (
./markdown.js
).
When
$implementation
is
absent
, the class is self-contained: the compiler generates an ES class from the schema itself — constructor (with
super()
support), private
#fields
, getters/setters, async methods, and the
extends
clause all come from the descriptor. This is how small classes ship with no JavaScript at all; structured
body
statements are covered in
Functions
.
Admission blocks
A plain class with nothing but the contract above is a state prototype — construct,
resolve()
, done. A class additionally participates in
host dispatch
through one or more top-level admission blocks:
| Block | Grants | Covered in |
|---|---|---|
format |
File-extension dispatch: parsing, serialization, content discovery/loading, Studio editing. | Custom formats |
project |
Ownership of a project.json section: data loading,
$paths
resolution, Studio settings UI. |
Project sections |
server |
A mounted route subtree in the deployed-site worker and the dev server. | Server mounts |
connector |
A database connection provider: dialect, schema deploy, bindings, connection testing. | Connectors |
Markdown
carries a
format
block; the connector's
Data
class carries
project
and
server
(it owns the
data
section and mounts
/_jx/data
). Blocks compose freely on one class.
How hosts introspect
Hosts follow a strict JSON-first contract — the registry (
buildExtensionRegistry()
in
@jxsuite/schema/extension-registry
) implements it once for node and browser hosts:
Resolve each
extensionsentry to a package root; read the manifest named by its"jx"field; read each listed.class.json.Detect participation via the admission blocks.
Find capabilities by scanning
$defs.methodsfor well-knownrolevalues; the method'sidentifier(fallback: its key) names the static method.To invoke: import
$implementation, take the export named by the classtitle, callExport[identifier](...args).Respect
timing: if the host's environment is not listed, delegate to the dev server.
Step 4 is the only point where extension code runs in a host. Everything Studio shows — settings forms, format icons, capability option UIs — comes from steps 1–3.
Capability methods
Capabilities are the static methods step 3 discovers:
parse
,
load
,
projectData
,
mount
,
lower
, and the rest of the well-known roles. Their contract — timing, options-as-parameters, and the compile-away
lower
hook — is the subject of
Capability methods
.
Related
Capability methods — the static method contract in depth
Custom formats — the
formatadmission blockData prototypes — the built-in classes yours sit beside
Tutorial: a TOML format extension — a full descriptor built from scratch