Projects: weft.json

From docs/projects.md at 1ab1443 View on GitHub

A real app has more than one screen, and its screens share things: design tokens, components of its own, the actions the app handles and the shape of its data. A project file, weft.json, declares them once. Every tool finds it by walking up from the screen, the way TypeScript finds tsconfig.json, so a screen never names what it uses. The same file holds the tools' settings, so every screen of the project is checked, rendered and exported the same way.

The specification is SPEC §10; this page shows it in use.

An example

examples/project/ is a small shop with two screens:

examples/project/
├── weft.json
├── catalog.json          a component of its own, <rating>, and a ghost button
├── catalogs/
│   └── acme-ui.catalog.json a library of <acme-…> components, imported from a manifest
├── data.schema.json      the shape of the app's data
├── sample.data.json      data to preview the screens with
├── tokens/
│   ├── theme.resolver.json the layers below, then a light or dark theme
│   ├── base.tokens.json    colours, spacing and the body text
│   ├── brand.tokens.json   the brand colour, layered over the base
│   └── dark.tokens.json    the brand colour of the dark theme
└── screens/
    ├── cart.weft
    └── review.weft
{
  "$schema": "../../schemas/weft.schema.json",
  "tokens": "tokens/theme.resolver.json",
  "catalog": ["catalogs/acme-ui.catalog.json", "catalog.json"],
  "actions": ["cart.checkout", "cart.remove", "nav.back"],
  "data": "data.schema.json",
  "validate": { "mode": "strict" },
  "render": { "data": "sample.data.json" }
}

The first five members are the shared resources:

MemberWhat it is
tokensDTCG token files, in layer order: a later file overrides an earlier one, and aliases are resolved after the merge. Or one DTCG resolver file, for tokens with modes such as light and dark (below).
catalogThe catalogs merged over the core catalog, in order (below). Libraries such as acme-ui come first; the project's own catalog, last, adds new components and new values or props on core and library ones. It may widen them, never narrow them.
actionsThe action names the app handles. An on-* value outside the list is an error.
dataA JSON Schema (a 2020-12 subset) of the app's data. Bindings are checked against it, names and types.
$schemaIgnored by the tools. Editors use it to complete and check the file.

The rest are tool settings, described below.

File names are relative to weft.json and must stay inside its folder: ../shared.json or an absolute path is refused.

What the project changes

With the project, the cart screen is valid. Without it, <rating> and the ghost button do not exist:

$ weft validate examples/project/screens/cart.weft; echo "exit $?"
exit 0
$ weft validate examples/project/screens/cart.weft --no-project --catalog packages/catalog/catalog.json --strict; echo "exit $?"
examples/project/screens/cart.weft:8:11 W401 <rating> is not in catalog weft-core 0.2.0. — use a catalog component, or an extension named x-<vendor>-rating
examples/project/screens/cart.weft:10:31 W203 "ghost" is not an allowed value.
exit 1

The project also catches what the core catalog cannot know: an action the app does not handle and a binding to data that does not exist.

$ sed 's/\$.cart.total/$.cart.totl/; s/nav.back/nav.home/' examples/project/screens/cart.weft > examples/project/screens/typo.weft
$ weft validate examples/project/screens/typo.weft; echo "exit $?"
examples/project/screens/typo.weft:20:23 W308 Action "nav.home" is not provided by the host.
examples/project/screens/typo.weft:19:22 W315 The data schema does not declare "totl" in $.cart. — did you mean "total"?
exit 1
$ rm examples/project/screens/typo.weft

Light and dark: a resolver

Tokens that differ by mode (light and dark, compact and roomy, two brands) live in a DTCG resolver file, which tokens names instead of a list. The example project's tokens/theme.resolver.json layers the base and brand files as before, then adds the dark theme's file when the theme is dark:

{
  "version": "2025.10",
  "sets": {
    "foundation": {
      "sources": [{ "$ref": "base.tokens.json" }, { "$ref": "brand.tokens.json" }]
    }
  },
  "modifiers": {
    "theme": {
      "contexts": { "light": [], "dark": [{ "$ref": "dark.tokens.json" }] },
      "default": "light"
    }
  },
  "resolutionOrder": [{ "$ref": "#/sets/foundation" }, { "$ref": "#/modifiers/theme" }]
}
  • Validation uses the default of each modifier (its first context when it names none).
  • A modifier with light and dark contexts is the appearance: generated SwiftUI colours, pages and stylesheets follow the system's light or dark mode.
  • File references are relative to the resolver and stay inside the project. Every problem is W705 (or W703 and W704 for files) with a pointer into the resolver, and the rest still loads.
  • A plain list of token files works as before.
  • In Figma the contexts of a modifier become variable modes, and in Penpot token themes, when the plugin is given that modifier; exporting from the file gives the modes back as a resolver document (@weft/figma, @weft/penpot).
  • React and SolidJS components read their tokens from CSS custom properties: weft css-tokens writes them as weft-tokens.css, light values first and the dark ones under prefers-color-scheme: dark. Link it once in the app, with weft css-base's weft-base.css after it (the base stylesheet).

Several catalogs

A shared catalog is a library. It names a prefix, and every component it adds starts with it: acme-ui has the prefix acme and adds <acme-button>, <acme-rating>, <acme-card> and <acme-badge>. It may say which core catalog it was written for, with requires: { "weft-core": "0.2.0" }. The project's own catalog has no prefix and comes last, so it can add its own components and widen core and library ones, as catalog.json adds the ghost variant to <acme-button>.

Two libraries cannot claim one name, prefix or component (W711), a library adds only components under its prefix (W713), and a missing or incompatible required catalog is a warning (W714). The tools list where every component comes from: weft_capabilities names each catalog with its prefix and file, and weft_catalog names each component's catalog and the catalogs that extend it.

weft import-cem --prefix acme turns a Custom Elements Manifest into such a library; without --prefix it prints a hint when every tag shares a first segment.

A library can also come from an installed npm package: the entry { "package": "@acme/ui" } finds node_modules/@acme/ui in the project folder or a folder above it, and reads the catalog its package.json names under weft.catalog. Publishing a catalog has the details.

Choosing the project

Every tool that reads a screen looks for the first weft.json in the screen's folder or above it.

  • --project <file> uses another project file.
  • --no-project uses none.
  • --catalog <file> (the weft command) replaces the project's catalogs and keeps the rest. Repeat it to give several, in order: --catalog catalogs/acme-ui.catalog.json --catalog catalog.json. A file whose catalog is named weft-core replaces the core catalog itself.

The MCP server has no screen to look from, so it never looks: its host names the file with --project (see The MCP server), and a tool call can pass a whole project as its project argument.

Settings

Anything a tool lets you choose can also be set in weft.json, in one section per tool. Every section and every key is optional. An argument you give a tool always wins over the file, and the file wins over the tool's default.

KeyDefaultWhat it sets
validate.mode"lenient""strict" makes unknown elements and attributes errors in weft validate (--lenient overrides it), and is the default of strict in the MCP server started with the project.
format.writefalseweft fmt rewrites the file instead of printing it (--print overrides it).
explain.contextfalseweft explain also lists the context entries (as --context does).
render.datanoneSample data for rendered pages (--data overrides it).
render.tokensthe project's tokensToken files for rendered pages, layered the same way, or one resolver file (--tokens overrides it).
render.outDirnext to the screenWhere rendered pages go (an output path overrides it).
render.appearancethe default context"light" or "dark": which theme of a resolver's light and dark modifier rendered pages use (--appearance overrides it).
export.html.outDirstandard outputWhere weft html writes <screen>.html (--out-dir overrides it).
export.html.sourcefalseThe page keeps the screen in a leading comment, so weft import-html gives it back exactly (--no-source overrides it). Off by default: a deployed page would publish it.
export.html.datanoneSample data weft html shows in the page instead of keeping bindings as a template (--data overrides it).
export.react.context, export.solid.context, export.html.context, export.slint.context, export.swiftui.context"strip" for html, "keep" for the othersWhether the source comment carries the screen's context (--context keep|strip overrides it).
import.react.context, import.solid.context, import.html.context, import.slint.context, import.swiftui.context"keep""drop" leaves out the context an import reads back from a source comment (--context keep|drop overrides it).
export.react.outDirnext to the screenWhere exported React components go; weft react prints when it is absent (--out-dir overrides it).
export.react.typescript, export.solid.typescriptfalseWrite TSX with typed props (--javascript overrides it).
export.react.source, export.solid.sourcefalseThe component keeps the screen in a leading comment, so weft import-react and weft import-solid give it back exactly (--no-source overrides it).
export.solid.outDirstandard outputWhere weft solid writes <screen>.jsx or .tsx (--out-dir overrides it).
export.lit.outDirstandard outputWhere weft lit writes <screen>.js (--out-dir overrides it).
import.html.outDirnext to the pageWhere screens imported from HTML go; weft import-html prints when it is absent (--out-dir overrides it).
import.react.outDir, import.solid.outDirstandard outputWhere weft import-react and weft import-solid write <file>.weft (--out-dir overrides it).
export.swiftui.outDirstandard outputWhere weft swiftui writes <screen>.swift and weft swiftui-tokens writes WeftTokens.swift, and the Xcode command plugin's export (next to the screen when absent; --out-dir overrides it). The build tool plugin ignores it and writes into the build folder.
export.css.outDirstandard outputWhere weft css-tokens writes weft-tokens.css, the stylesheet with the --weft-… properties that React, SolidJS and Lit components read, and weft css-base writes weft-base.css, their base rules (--out-dir overrides it).
export.swiftui.sharedTokenstrueScreens read the tokens from one shared WeftTokens.swift instead of each carrying a theme with the tokens it uses (--shared-tokens and --no-shared-tokens override it). The Xcode build tool plugin writes WeftTokens.swift once per target.
export.swiftui.datanoneSample data weft swiftui builds the model's sample from, for #Preview (--data overrides it).
import.swiftui.outDirstandard outputWhere weft import-swiftui writes <file>.weft, and the Xcode command plugin's import (next to the view when absent; --out-dir overrides it).
export.a2ui.outDir, import.a2ui.outDirstandard outputWhere weft a2ui writes <screen>.a2ui.json and weft import-a2ui writes <file>.weft (--out-dir overrides them).
export.slint.outDir, import.slint.outDirstandard outputWhere weft slint writes <screen>.slint and weft import-slint writes <file>.weft (--out-dir overrides them).
export.schema.outDirstandard outputWhere weft schema writes document.schema.json, the JSON Schema of the documents the project's catalog admits (--out-dir overrides it).
import.cem.outDirstandard outputWhere weft import-cem writes <file>.catalog.json, the catalog imported from a Custom Elements Manifest (--out-dir overrides it).
import.cem.name, import.cem.versionthe manifest's file stem; 0.0.0The name and version of that catalog (--name and --version override them).
import.cem.prefixnoneThe prefix of that catalog, which makes it a library that loads beside the project's own catalog (--prefix overrides it).
import.figma.outDirthe working directoryWhere the plugins' figma-pull script writes <screen id>.weft, a Figma frame read through the REST API (an output path argument overrides it).
mcp.limits.*see packages/mcp/README.mdThe MCP server's bounds on one call: markupChars, dataChars, patches, patchesChars, projectChars, diagnostics, inputElements.
mcp.context"read-write""read-only" makes weft_patch refuse every context patch (W512); weft_context still reads the context.
plugins.<name>noneSettings of a plugin or tool Weft does not know. Weft only checks that each is an object.
plugins.open-design.tokensDirnext to the design systemWhere the Open Design plugin's design-md script writes the tokens it maps from a DESIGN.md or tokens.css. A file name like any other (W703 when it is absolute or leaves the project), and an unknown key in plugins.open-design is W702.

Directories are file names too: relative to weft.json, no trailing /. A missing directory is created.

Here a copy of the example sends pages and components to out/:

$ cp -r examples/project weft-tour/shop
$ cd weft-tour/shop
$ cat > weft.json <<'EOF'
{
  "tokens": ["tokens/base.tokens.json", "tokens/brand.tokens.json"],
  "catalog": "catalog.json",
  "actions": ["cart.checkout", "cart.remove", "nav.back"],
  "data": "data.schema.json",
  "validate": { "mode": "strict" },
  "render": { "data": "sample.data.json", "outDir": "out" },
  "export": { "react": { "outDir": "out" } }
}
EOF
$ node ../../plugins/shared/scripts/render.ts screens/cart.weft
Wrote /path/to/weft/weft-tour/shop/out/cart.html
file:///path/to/weft/weft-tour/shop/out/cart.html
$ node ../../plugins/shared/scripts/export.ts screens/cart.weft
Wrote /path/to/weft/weft-tour/shop/out/cart.jsx
$ cd ../..

The page shows the sample cart from render.data, with the brand layer's tokens. In Claude Code and Cursor, /weft:render, /weft:export and /weft:import run these same scripts.

When the file is wrong

A project file never crashes a tool. Each problem is a diagnostic that points into the file with a JSON Pointer:

$ echo '{"tokens": ["tokens/base.tokens.json", "../outside.json"], "validate": {"mode": "loose"}}' > weft-tour/broken.json
$ weft validate weft-tour/shop/screens/cart.weft --project weft-tour/broken.json; echo "exit $?"
weft-tour/broken.json:#/tokens/0 W704 The file "tokens/base.tokens.json" cannot be read.
weft-tour/broken.json:#/tokens/1 W703 The file name "../outside.json" is absolute, leaves the project directory or is malformed.
weft-tour/broken.json:#/validate/mode W701 "validate.mode" must be "strict" or "lenient".
weft-tour/shop/screens/cart.weft:6:46 W306 Token "space.sm" does not exist.
weft-tour/shop/screens/cart.weft:8:11 W401 <rating> is not in catalog weft-core 0.2.0. — use a catalog component, or an extension named x-<vendor>-rating
weft-tour/shop/screens/cart.weft:8:80 W306 Token "color.star" does not exist.
weft-tour/shop/screens/cart.weft:10:31 W203 "ghost" is not an allowed value.
weft-tour/shop/screens/cart.weft:18:38 W306 Token "space.md" does not exist.
exit 1
  • A file that cannot be read (W704) or a name that leaves the folder (W703) is left out; the rest of the project still applies, which is why the screen's own problems follow.
  • A setting of the wrong type (W701) is an error. The tool would otherwise run with a default you did not choose.
  • An unknown key (W702) is only a warning, so a file written for a newer Weft still works. A misspelled key shows up the same way: "colour" in render prints W702 Unknown key "colour" in "render".
  • The render, export and import scripts and the MCP server refuse to work with a project that has errors, and write nothing.

schemas/weft.schema.json is the JSON Schema of the file. Point $schema at it and your editor completes every key and flags these mistakes as you type.

Limits

  • One project per screen. A project cannot include another one, and there are no shared fragments (a header or a card kept in its own file) yet; their design waits for approval.
  • render.data and render.tokens apply to the render script and write-page; the MCP server's weft_render takes data as an argument.
  • The Node front end of the weft command (packages/core/src/cli.ts) does not read projects. Use the Rust program.