larrylint
🦙 larrylint
Opinionated rules for Laioutr apps: an ESLint preset for your editor and a CLI for CI.
Laioutr apps are Nuxt modules with a lot of moving parts: sections and blocks, orchestr handlers, middleware, API clients, server routes. larrylint keeps them where they belong and stops the imports that make a codebase drift, like app code pulling in server code or handlers importing each other. It also catches the mistakes that only show up in Studio or in production, like schema fields that never reach the component, fallbacks that never apply, or cookies written after orchestr has sent the headers.
Usage
Check the Laioutr app in the current folder. No install and no config needed:
npx larrylint
Set it up for good. This installs larrylint, adds its rules to your eslint.config, and records existing violations in a baseline so only new code has to follow the rules:
npx larrylint init
After that, eslint . and your editor report larrylint's rules next to your own.
token-namespaces reads the app's installed @laioutr-core/canonical-types, so install the app's dependencies first. larrylint stops with an error when it's missing.
Rules
Structure
| Rule | Description |
|---|---|
larrylint/layers |
App, server and shared code don't import each other, runtime code doesn't import build-time code, and node: modules stay on the server. As Laioutr's coding standards ask, handlers get API clients from the orchestr context, nothing imports handler files, and components don't import sections or blocks. |
larrylint/heavy-imports |
Sections, blocks and plugins load the packages in heavyPackages with import(): frontend-core imports every section and block up front, and plugins run on every page, so a static import loads the package on every page. |
larrylint/config-keys |
Runtime config keys like '@laioutr-app/shopware' are the app itself or one of its dependencies, otherwise the config is missing at runtime. |
larrylint/public-config |
The module doesn't copy its whole options object into runtimeConfig.public, which would send every secret in it to the browser. |
Type imports are always fine, since they don't end up in the bundle.
Orchestr
| Rule | Description |
|---|---|
larrylint/orchestr-files |
Laioutr loads every file in orchestr/ as a Nitro plugin, so only handler files belong there. |
larrylint/orchestr-cookies |
Cookies and headers are only written in extendRequest() and action handlers, since query, link and resolver handlers and use() middleware can run after the headers are sent. Cookies go through frontend-core's setManagedCookie() and deleteManagedCookie(), which the Studio preview needs. |
larrylint/handler-exports |
Handler files export their handler as default. Without one, the build fails. |
larrylint/initware-throws |
extendRequest() doesn't throw, directly or through a function it calls: it runs before every query, so a throw breaks every page. |
larrylint/token-namespaces |
Your own tokens stay out of the namespaces of Laioutr's canonical types, like ecommerce/, where a later canonical-types release or another app can take the same id. |
Sections and blocks
| Rule | Description |
|---|---|
larrylint/definition-folder |
defineSection() and defineBlock() live where module.ts registers sections and blocks, and every .vue registered there has one. Files that module.ts wires up by hand, like replacements for upstream sections, can live anywhere. |
larrylint/component-name |
The component of a definition matches its file name. |
larrylint/definition-description |
Definitions have a studio.description, which Studio shows in its section picker and AI agents read through Laioutr's MCP server. |
larrylint/single-root |
Sections and blocks render one root element, also through a component they wrap. Otherwise Vue drops the data-lfc-* attributes Studio needs to select them in the preview. |
larrylint/slot-children |
Nothing counts slot children with .length: frontend-core passes all blocks of a slot as one Fragment. |
larrylint/reserved-field-names |
No top-level schema fields named key, ref, ref_for, ref_key, class or style, which Vue handles itself, and no slots on sections, which frontend-core overwrites. |
larrylint/field-name-case |
Top-level schema field names have no - and don't start with $, which Vue renames or rejects as props. |
larrylint/required-fields |
Schema fields have no required, which Studio ignores. Give them a default instead. |
larrylint/unused-fields |
The component reads every schema field it defines, directly, through the section it's a block of, or in another field's if, so editors don't get fields that do nothing. |
larrylint/dead-fallbacks |
No ?? fallbacks that are never used: frontend-core fills unset pickers with their first option, content alignments with the center, checkboxes with false and text fields with ''. |
Frontend
| Rule | Description |
|---|---|
larrylint/button-type |
No type on the ui-kit buttons: they always render their button-type prop, so type="submit" silently renders a dead button. Autofixable. |
larrylint/mutation-errors |
An awaited or dropped mutateAsync() handles its error. Otherwise a failed mutation replaces the whole section or block with an error, or nothing handles it. |
larrylint/resolve-result |
Nothing checks the result of linkResolver.resolve(): a link it can't resolve comes back as a #… fallback, not as an empty value. |
larrylint/hand-built-links |
Links to pages come from linkResolver, not from paths like `/hotels/${slug}`: page paths are set per page and language in Studio, and each market adds its own prefix, like /en. |
larrylint/internal-anchors |
Internal and resolved links use <NuxtLink>: a plain <a> reloads the page and breaks Studio's navigation sync. |
Layout
larrylint follows the runtime layout from Laioutr's coding standards:
src/
├── module.ts # registers sections, blocks, orchestr handlers and plugins
└── runtime/
├── app/ # client code
│ ├── components/ # UI components
│ ├── sections/ # defineSection() components
│ └── blocks/ # defineBlock() components
├── server/ # server code
│ ├── client/ # API client factories
│ ├── const/
│ ├── mappers/
│ ├── middleware/ # defineOrchestr() with extendRequest()
│ ├── orchestr/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
│ ├── orchestr-helper/ # helper logic for handlers
│ └── utils/
└── shared/ # code for both sides, like orchestr tokens
Where your sections, blocks and handlers live, larrylint reads from your module.ts: the folders and globs you pass to registerLaioutrApp(), and the plugins you pass to addPlugin(). So other folders work too, like section/ and block/ in Laioutr's own ui-app. Without a module.ts it can read, larrylint assumes the folders of Laioutr's app starter: app/sections/, app/blocks/ and server/orchestr/.
Besides that, larrylint only relies on the names app/components/ and server/client/, and on the split into app/, server/ and shared/.
Baseline
On an existing codebase, larrylint init records all current violations in larrylint-baseline.json, counted per file and rule. A file stays quiet as long as it has no more violations of a rule than recorded. Once it gets more, all of them show again, like ESLint's bulk suppressions. The baseline applies in your editor, in eslint . and in the CLI.
After fixing old violations, shrink the baseline:
npx larrylint --baseline
Configuration
Most apps need none. heavyPackages lists the packages that sections, blocks and plugins must load with import(), for heavy-imports. Set it in your package.json:
{
"larrylint": {
"heavyPackages": ["leaflet"]
}
}
Or in a larrylint.config.ts:
import { defineLarrylintConfig } from 'larrylint'
export default defineLarrylintConfig({
heavyPackages: ['leaflet'],
})
ESLint
larrylint init adds the rules to your eslint.config for you. To do it by hand with @nuxt/eslint-config or @antfu/eslint-config:
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
import larrylint from 'larrylint'
export default createConfigForNuxt().append(larrylint())
With a plain array, like @laioutr/eslint-config:
import config from '@laioutr/eslint-config/nuxt-module'
import larrylint from 'larrylint'
export default [...config, ...(await larrylint())]
The preset only brings rules, no parsers or style rules, so it runs on top of whatever your config already uses.
CLI
larrylint [--cwd <folder>] [--fix] [--baseline]
larrylint init [--cwd <folder>]
--fixfixes what can be fixed automatically.--baselinerecords the current violations inlarrylint-baseline.json.
The CLI only runs larrylint's rules, independent of your ESLint setup, and exits with code 1 on new violations.
License
Published under the MIT License.
Related Packages
ESLint flat-config plugin for Nuxt 4 anti-patterns and AI-slop detection. Shares...
ESLint rules to enforce Nuxt 4 layer architecture with configurable imports
ESLint flat-config plugin for Vue 3 anti-patterns and AI-slop detection. Shares...
Shared ESLint plugin and flat config for DiPhyx Nuxt/Vue + Harlemify projects
An ESLint plugin to enforce consistent usage of 'data-testid' attributes in Vue....
Version History
| Version | Released | License |
|---|---|---|
| 0.2.2 | MIT | |
| 0.2.1 | MIT | |
| 0.2.0 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT | |
| 0.0.1 |