Targ Apps Docs
Chroma

chroma.json and schemas

Every chroma.json field, the manifest the templates generate, and how to validate it in your editor and with the CLI.

chroma.json is the manifest the CLI reads for dev, build, and validate, and the file the browser plugin loader reads at runtime. Its shape is described by JSON Schema draft 2020-12 files published on this site.

Schema files

All schemas live under /schemas/chroma/ and are the canonical source. The aliases under /schemas/targ-apps/ exist for apps in the Targ-Apps suite; each alias references the same rules, so either URL validates a chroma.json.

FileDescribes
chroma.schema.jsonchroma.json, the app manifest
chroma.plugin.schema.jsonchroma.plugin.json, a plugin manifest
chroma-fs.schema.jsonchroma-fs.json, storage configuration
chroma-pkg.schema.jsonThe engine's pkg/package.json
jsconfig.schema.jsonAn editor jsconfig.json
legacy-lib-registry.schema.jsonThe legacy-library registry

Examples for each are in /schemas/chroma/examples/. Plugin-specific formats are under /schemas/chroma-plugins/.

Typical dynamic manifest

This is exactly what chroma new <name> --template dynamic generates (the standard template is the same apart from name):

{
  "$schema": "https://docs.targapps.xyz/schemas/chroma/chroma.schema.json",
  "name": "chroma-dynamic-app",
  "mode": "dynamic",
  "plugins": ["chroma-fs", "chroma-router", "chroma-style"],
  "entry": "index.html",
  "outDir": "dist",
  "engine": { "vendorDir": "pkg", "rebuildWasm": true },
  "build": { "jsEntry": "src/main.chr", "outfile": "bundle.js", "format": "esm", "minify": true, "sourcemap": true },
  "package": { "zip": true },
  "dev": { "port": 5173, "desktop": true }
}

Two defaults to know about:

  • dev.desktop: true makes chroma dev open Chroma Desktop. Pass --no-desktop or set it to false to stay in the browser. This affects development host only; it does not add a second production build target (Runtime modes).
  • engine.rebuildWasm: true makes chroma build recompile the engine, which needs a source-checkout install. Set it to false to build against the pkg/ already in your project.

The plugins array is copied into every chroma build output. Removing a plugin changes what ships in dist/, not which runtime mode you choose at deploy time. Host-specific plugin behavior (for example Chroma-Fs IndexedDB vs the Desktop filesystem bridge) is documented under Runtime modes and Plugins.

Only static and dynamic are valid mode values. standard is a template name, not a mode.

Field reference

Three programs read chroma.json, and they do not agree on the full field list. In the table, Schema is whether chroma.schema.json accepts the field, CLI is whether chroma dev, chroma build, chroma new, or chroma add read it, and Linter is whether @chroma/linter (and the VS Code extension) reads it beyond schema validation. "Schema only" means the field is accepted and documented but nothing acts on it.

FieldTypeDefaultSchemaCLILinterEffect
$schemastring-acceptednoselects the schema fileEditor hints; the linter resolves the schema from it.
namestringrequiredrequiredyesnodev window title, archive name when package.zip is true, required by validate. chroma new rewrites it.
mode"static" | "dynamic"requiredrequiredyesyes (absent counts as dynamic)Selects the build path. Any other value stops build with Unknown mode.
entrystringindex.htmlacceptedyesyesHTML entry file.
outDirstringdistacceptedyesnoBuild output folder, recreated on every build.
pluginsstring[]-acceptedyesyesPlugin ids under plugins/<id>/. chroma new vendors them; validate and the linter check the folders; the browser plugins/loader.js loads them.
engine.vendorDirstringpkgacceptedyesnoWhere the compiled engine lives in the project.
engine.rebuildWasmbooleanfalseacceptedyes (dynamic)noRecompile the engine before bundling. Needs a source-checkout install.
build.jsEntrystring-required in dynamic modeyesyesEntry module for the bundle, for example src/main.chr.
build.outfilestringbundle.jsacceptedyes (dynamic)noBundle file name.
build.scripts, build.stylesstring[]-acceptedyes (static)yes (static)Files minified individually. The linter reports a missing file.
build.format"esm"-acceptednonoSchema only. The CLI always passes --format=esm.
build.minify, build.sourcemapboolean-acceptednonoSchema only. The CLI always minifies and never emits source maps, whatever these say.
package.zipboolean-acceptedyes (dynamic)noArchive the build output.
dev.portinteger4173acceptedyesnoPort for chroma dev; --port overrides it.
dev.desktopbooleanfalseacceptedyesnoOpen Chroma Desktop in develop mode (dynamic only).
dev.hotReloadbooleantrueacceptedyesnoBrowser live reload; see Live reload.
legacyLibrariesobject[]-acceptedwritten by chroma add onlynoDeclares vendored legacy UMD libraries (id, source, version, url, disabled). dev and build do not read it. See Legacy libraries.

Fields the schema rejects

The schema sets additionalProperties: false on the root and on engine, build, package, dev, and every legacyLibraries item, so any key not in the table above fails schema validation. Four keys are read by the linter anyway:

FieldRead byReal behavior
routes (object, hash to page file)LinterEach key should start with #/ (warning otherwise) and each value must be an existing file (error). In .chr mode each value must also end in .chr. The CLI ignores it.
source.formatLinter"chr" turns on the .chr rules. The CLI ignores it.
lint.chrSource, lint.chrOnlySeverityLinterchrSource forces .chr mode on or off; chrOnlySeverity: "warning" downgrades the .chr extension errors to warnings. The CLI ignores them.
build.chrEntryLinterAlternative entry checked when build.jsEntry is absent. The CLI never reads it (a dynamic build needs build.jsEntry).

So a manifest that uses any of these gets chroma/schema errors from the linter next to the linter's own diagnostics, and chroma validate fails at its schema step (it exits before the structural checks). To keep chroma validate clean, leave them out; the linter infers .chr mode on its own when build.jsEntry ends in .chr or src/ contains a .chr file.

How the CLI finds a key

On macOS and Linux the CLI looks up each key by name at any depth in the file (with jq, or a text search when jq is missing), so a key name that appears twice, such as name or scripts, can resolve to the wrong one. On Windows (chroma.ps1) the CLI reads the exact paths shown above (engine.vendorDir, build.jsEntry, dev.port, and so on). Keep key names unique and use the paths in the table.

Validate

Add $schema as the first key so VS Code and other editors show completions and errors:

{
  "$schema": "https://docs.targapps.xyz/schemas/chroma/chroma.schema.json",
  "name": "my-app",
  "mode": "dynamic"
}

Then check the project from the terminal:

chroma validate              # schema + structure
chroma validate --no-schema  # structure only, works offline

chroma validate, the linter, and the schema check different things; the Linter page compares them. Flags and where the CLI looks for the schema catalog are in chroma validate. On Windows, chroma validate runs the structural checks only.

Common validation errors

MessageFix
"mode" must be "static" or "dynamic"Use dynamic for bundled apps; standard is only a chroma new template.
dynamic mode requires build.jsEntryAdd "build": { "jsEntry": "src/main.chr" }.
entry file not foundCreate the HTML file or fix entry.
build.jsEntry not foundFix the path or create the file.
engine vendor dir missing WASM pkgCopy pkg/ from a template or re-run chroma new.
plugin "<id>" missing at plugins/<id>/Vendor the plugin or remove it from plugins.
  • Runtime modes — Portable (web) vs Executable (Desktop); manifest fields that affect dev host only
  • CLI — validate, dev, build, and the compile pipeline
  • Templates — default dev.desktop and plugins per template

On this page