Skip to main content
On this pageOverview

Oxlint Plugin

Overview

Foldkit projects use oxlint for linting and @foldkit/oxlint-plugin for rules that understand Foldkit naming and Message conventions.

Scaffolded Projects

Create Foldkit app includes .oxlintrc.json, a lint script, oxlint, and @foldkit/oxlint-plugin. A generated project includes this .oxlintrc.json:

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["typescript"],
  "jsPlugins": [
    {
      "name": "foldkit",
      "specifier": "@foldkit/oxlint-plugin"
    }
  ],
  "categories": {
    "correctness": "off"
  },
  "rules": {
    "no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_",
        "destructuredArrayIgnorePattern": "^_"
      }
    ],
    "typescript/no-explicit-any": "error",
    "typescript/consistent-type-assertions": [
      "error",
      {
        "assertionStyle": "never"
      }
    ],
    "foldkit/no-noop-message": "error",
    "foldkit/got-submodel-message-name": "error",
    "foldkit/message-binding-matches-tag": "error",
    "foldkit/got-prefix-requires-submodel-payload": "error",
    "foldkit/no-empty-object-tagged-call": "error",
    "foldkit/prefer-callable-message-constructor": "error",
    "foldkit/command-binding-matches-name": "error"
  },
  "ignorePatterns": [
    "dist/",
    "node_modules/",
    "repos/",
    "**/*.d.ts",
    "vite.config.ts",
    "vitest.config.ts",
    "**/*.config.js",
    "**/*.config.mjs"
  ]
}

Foldkit Rules

The rules below cover Foldkit-specific cases that oxlint does not know about on its own: callable Message constructors, Submodel wrapper Messages with a message payload, matching m() tags, no-field Message constructors, and Command names.

foldkit/no-noop-message

Rejects catch-all Messages that make update branches and traces less meaningful. Name the event that happened instead.

import { m } from 'foldkit/message'

// ❌ Bad
const NoOp = m('NoOp')

// ✅ Good
const ClickedSave = m('ClickedSave')

foldkit/got-submodel-message-name

Requires wrapper Messages around Submodel Messages to use the Got*Message convention.

import { m } from 'foldkit/message'

import * as Child from './child'

// ❌ Bad
const ChildChanged = m('ChildChanged', {
  message: Child.Message,
})

// ✅ Good
const GotChildMessage = m('GotChildMessage', {
  message: Child.Message,
})

foldkit/message-binding-matches-tag

Keeps a Message binding and its m() tag identical, so renames do not leave misleading traces behind.

import { m } from 'foldkit/message'

// ❌ Bad
const ClickedSave = m('ClickedSubmit')

// ✅ Good
const ClickedSubmit = m('ClickedSubmit')

foldkit/got-prefix-requires-submodel-payload

Reserves the Got* prefix for Submodel wrappers. Any Got-prefixed Message must include a child Message payload named message.

import { Schema as S } from 'effect'
import { m } from 'foldkit/message'

import * as Child from './child'

{
  // ❌ Bad: Got is reserved for Submodel wrappers.
  const GotWeather = m('GotWeather', {
    temperature: S.Number,
  })
}

{
  // ✅ Good: use a name that does not start with Got for Command results.
  const ReceivedWeather = m('ReceivedWeather', {
    temperature: S.Number,
  })
}

{
  // ❌ Bad: Got-prefixed wrappers must carry child Messages.
  const GotChildMessage = m('GotChildMessage', {
    id: S.String,
  })
}

{
  // ✅ Good: Got wraps a child Message.
  const GotChildMessage = m('GotChildMessage', {
    id: S.String,
    message: Child.Message,
  })
}

foldkit/no-empty-object-tagged-call

Catches empty-object calls to no-field Message constructors. A no-field Message should be called with no arguments.

import { m } from 'foldkit/message'

const ClickedSave = m('ClickedSave')

// ❌ Bad
const badMessage = ClickedSave({})

// ✅ Good
const goodMessage = ClickedSave()

foldkit/prefer-callable-message-constructor

Prevents constructing Messages by typing or casting object literals. Use the callable Schema constructor instead.

import { Schema as S } from 'effect'
import { m } from 'foldkit/message'

const ClickedSave = m('ClickedSave')
const Message = S.Union([ClickedSave])
type Message = typeof Message.Type

// ❌ Bad
const badMessage: Message = {
  _tag: 'ClickedSave',
}

// ✅ Good
const goodMessage = ClickedSave()

foldkit/command-binding-matches-name

Keeps a Command binding name in sync with the name passed to Command.define.

import { Effect } from 'effect'
import { Command } from 'foldkit'
import { m } from 'foldkit/message'

const CompletedFetchUser = m('CompletedFetchUser')

// ❌ Bad
const SaveUser = Command.define(
  'FetchUser',
  CompletedFetchUser,
)(Effect.succeed(CompletedFetchUser()))

// ✅ Good
const FetchUser = Command.define(
  'FetchUser',
  CompletedFetchUser,
)(Effect.succeed(CompletedFetchUser()))

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson